diff --git a/go.mod b/go.mod index e49d742..b0b1f59 100644 --- a/go.mod +++ b/go.mod @@ -41,6 +41,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de // indirect github.com/mailru/easyjson v0.7.7 // indirect + github.com/mitchellh/mapstructure v1.5.0 github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect github.com/monochromegane/go-gitignore v0.0.0-20200626010858-205db1a8cc00 // indirect diff --git a/go.sum b/go.sum index 2ebdb30..8b45c89 100644 --- a/go.sum +++ b/go.sum @@ -91,6 +91,8 @@ github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de h1:9TO3cAIGXtEhn github.com/liggitt/tabwriter v0.0.0-20181228230101-89fcab3d43de/go.mod h1:zAbeS9B/r2mtpb6U+EI2rYA5OAXxsYw6wTamcNW+zcE= github.com/mailru/easyjson v0.7.7 h1:UGYAvKxe3sBsEDzO8ZeWOSlIQfWFlxbzLZe7hwFURr0= github.com/mailru/easyjson v0.7.7/go.mod h1:xzfreul335JAWq5oZzymOObrkdz5UnU4kGfJJLY9Nlc= +github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= +github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= diff --git a/pkg/artifacts/artifacts.go b/pkg/artifacts/artifacts.go index 94276fc..969d438 100644 --- a/pkg/artifacts/artifacts.go +++ b/pkg/artifacts/artifacts.go @@ -1,9 +1,9 @@ package artifacts import ( - "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" - "github.com/aquasecurity/trivy-kubernetes/pkg/k8s" + "github.com/aquasecurity/trivy-kubernetes/pkg/k8s/docker" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" ) // Artifact holds information for kubernetes scannable resources @@ -13,22 +13,30 @@ type Artifact struct { Labels map[string]string Name string Images []string + Credentials []docker.Auth RawResource map[string]interface{} } // FromResource is a factory method to create an Artifact from an unstructured.Unstructured -func FromResource(resource unstructured.Unstructured) (*Artifact, error) { +func FromResource(resource unstructured.Unstructured, serverAuths map[string]docker.Auth) (*Artifact, error) { nestedKeys := getContainerNestedKeys(resource.GetKind()) - images := make([]string, 0) - + credentials := make([]docker.Auth, 0) cTypes := []string{"containers", "ephemeralContainers", "initContainers"} + for _, t := range cTypes { cTypeImages, err := extractImages(resource, append(nestedKeys, t)) if err != nil { return nil, err } images = append(images, cTypeImages...) + for _, im := range cTypeImages { + as, err := k8s.MapContainerNamesToDockerAuths(im, serverAuths) + if err != nil { + return nil, err + } + credentials = append(credentials, as) + } } // we don't check found here, if the name is not found it will be an empty string @@ -47,6 +55,7 @@ func FromResource(resource unstructured.Unstructured) (*Artifact, error) { Labels: labels, Name: name, Images: images, + Credentials: credentials, RawResource: resource.Object, }, nil } diff --git a/pkg/artifacts/artifacts_test.go b/pkg/artifacts/artifacts_test.go index ceba7bc..b5bfad6 100644 --- a/pkg/artifacts/artifacts_test.go +++ b/pkg/artifacts/artifacts_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "testing" + "github.com/aquasecurity/trivy-kubernetes/pkg/k8s/docker" "github.com/stretchr/testify/assert" "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime" @@ -32,7 +33,7 @@ func TestFromResource(t *testing.T) { for _, test := range tests { t.Run(test.Name, func(t *testing.T) { - result, err := FromResource(test.Resource) + result, err := FromResource(test.Resource, map[string]docker.Auth{}) if err != nil { t.Fatal(err) } diff --git a/pkg/k8s/docker/config.go b/pkg/k8s/docker/config.go new file mode 100644 index 0000000..bc0fd65 --- /dev/null +++ b/pkg/k8s/docker/config.go @@ -0,0 +1,142 @@ +package docker + +import ( + "encoding/base64" + "encoding/json" + "fmt" + "net/url" + "strings" + + containerimage "github.com/google/go-containerregistry/pkg/name" +) + +type BasicAuth string + +func NewBasicAuth(username, password string) BasicAuth { + var v = new(BasicAuth) + v.Encode(username, password) + return *v +} + +func (v *BasicAuth) Encode(username, password string) { + *v = BasicAuth(base64.StdEncoding.EncodeToString( + []byte(fmt.Sprintf("%s:%s", username, password)))) +} + +func (v *BasicAuth) Decode() (string, string, error) { + bytes, err := base64.StdEncoding.DecodeString(string(*v)) + if err != nil { + return "", "", err + } + split := strings.SplitN(string(bytes), ":", 2) + if len(split) != 2 { + return "", "", fmt.Errorf("expected username and password concatenated with a colon (:)") + } + return split[0], split[1], nil +} + +func (v BasicAuth) String() string { + return "[REDACTED]" +} + +// Auth represent credentials used to login to a Docker registry. +type Auth struct { + Auth BasicAuth `json:"auth,omitempty"` + Username string `json:"username,omitempty"` + Password string `json:"password,omitempty"` +} + +func (v Auth) String() string { + return "[REDACTED]" +} + +// Config represents Docker configuration which is typically saved as `~/.docker/config.json`. +type Config struct { + Auths map[string]Auth `json:"auths"` +} + +func (c *Config) Read(contents []byte, isLegacy bool) error { + if isLegacy { + // Because ~/.dockercfg contents is "auths" field in ~/.docker/config.json + // we can simply pass it to "Auths" field of Config + auths := make(map[string]Auth) + if err := json.Unmarshal(contents, &auths); err != nil { + return err + } + *c = Config{ + Auths: auths, + } + } else { + if err := json.Unmarshal(contents, c); err != nil { + return err + } + } + var err error + c.Auths, err = decodeAuths(c.Auths) + return err +} + +func decodeAuths(auths map[string]Auth) (map[string]Auth, error) { + decodedAuths := make(map[string]Auth) + for server, entry := range auths { + if entry == (Auth{}) { + continue + } + + if strings.TrimSpace(string(entry.Auth)) == "" { + decodedAuths[server] = Auth{ + Username: entry.Username, + Password: entry.Password, + } + continue + } + + username, password, err := entry.Auth.Decode() + if err != nil { + return nil, err + } + + decodedAuths[server] = Auth{ + Auth: entry.Auth, + Username: username, + Password: password, + } + + } + return decodedAuths, nil +} + +func (c Config) Write() ([]byte, error) { + bytes, err := json.Marshal(&c) + if err != nil { + return nil, err + } + return bytes, nil +} + +// GetServerFromImageRef returns registry server from the specified imageRef. +func GetServerFromImageRef(imageRef string) (string, error) { + ref, err := containerimage.ParseReference(imageRef) + if err != nil { + return "", err + } + return ref.Context().RegistryStr(), nil +} + +// GetServerFromDockerAuthKey returns the registry server for the specified Docker auth key. +// +// In ~/.docker/config.json auth keys can be specified as URLs or host names. +// For the sake of comparison we need to normalize the registry identifier. +func GetServerFromDockerAuthKey(key string) (string, error) { + + if !(strings.HasPrefix(key, "http://") || strings.HasPrefix(key, "https://")) { + key = fmt.Sprintf("https://%s", key) + } + + parsed, err := url.Parse(key) + if err != nil { + return "", err + } + + return parsed.Host, nil +} diff --git a/pkg/k8s/docker/config_test.go b/pkg/k8s/docker/config_test.go new file mode 100644 index 0000000..68a6157 --- /dev/null +++ b/pkg/k8s/docker/config_test.go @@ -0,0 +1,276 @@ +package docker + +import ( + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestNewBasicAuth(t *testing.T) { + assert.Equal(t, BasicAuth("Zm9vOmJhcg=="), NewBasicAuth("foo", "bar")) +} + +func TestConfig_Read(t *testing.T) { + testCases := []struct { + name string + + givenJSON string + + expectedAuth map[string]Auth + expectedError error + + isLegacy bool + }{ + { + name: "Should return empty credentials when content is empty JSON object", + givenJSON: "{}", + expectedAuth: map[string]Auth{}, + }, + { + name: "Should return empty credentials when content is null JSON", + givenJSON: "null", + expectedAuth: map[string]Auth{}, + }, + { + name: "Should return error when content is blank", + givenJSON: "", + expectedError: errors.New("unexpected end of JSON input"), + }, + { + name: "Should return server credentials stored as username and password encoded in the auth property", + givenJSON: `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "ZG9ja2VyOmh1Yg==" + }, + "harbor.domain": { + "auth": "YWRtaW46SGFyYm9yMTIzNDU=" + } + } + }`, + expectedAuth: map[string]Auth{ + "https://index.docker.io/v1/": { + Auth: "ZG9ja2VyOmh1Yg==", + Username: "docker", + Password: "hub", + }, + "harbor.domain": { + Auth: "YWRtaW46SGFyYm9yMTIzNDU=", + Username: "admin", + Password: "Harbor12345", + }, + }, + }, + { + name: "Should return server credentials stored in username and password properties", + givenJSON: `{ + "auths": { + "https://index.docker.io/v1/": { + "auth": "ZG9ja2VyOmh1Yg==" + }, + "harbor.domain": { + "username": "admin", + "password": "Harbor12345" + } + } + }`, + expectedAuth: map[string]Auth{ + "https://index.docker.io/v1/": { + Auth: "ZG9ja2VyOmh1Yg==", + Username: "docker", + Password: "hub", + }, + "harbor.domain": { + Username: "admin", + Password: "Harbor12345", + }, + }, + }, + { + name: "Should skip empty server entries", + givenJSON: `{ + "auths": { + "https://index.docker.io/v1/": { + + }, + "harbor.domain": { + "auth": "YWRtaW46SGFyYm9yMTIzNDU=" + } + } + }`, + expectedAuth: map[string]Auth{ + "harbor.domain": { + Auth: "YWRtaW46SGFyYm9yMTIzNDU=", + Username: "admin", + Password: "Harbor12345", + }, + }, + }, + { + name: "Should return error when auth is not username and password concatenated with a colon", + givenJSON: `{ + "auths": { + "my-registry.domain.io": { + "auth": "b25seXVzZXJuYW1l" + } + } + }`, + expectedError: errors.New("expected username and password concatenated with a colon (:)"), + }, + { + name: "Should process legacy .dockercfg json", + isLegacy: true, + givenJSON: `{ + "https://index.docker.io/v1/": { + "auth": "ZG9ja2VyOmh1Yg==" + }, + "harbor.domain": { + "auth": "YWRtaW46SGFyYm9yMTIzNDU=" + } + }`, + expectedAuth: map[string]Auth{ + "https://index.docker.io/v1/": { + Auth: "ZG9ja2VyOmh1Yg==", + Username: "docker", + Password: "hub", + }, + "harbor.domain": { + Auth: "YWRtaW46SGFyYm9yMTIzNDU=", + Username: "admin", + Password: "Harbor12345", + }, + }, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + dockerConfig := &Config{} + err := dockerConfig.Read([]byte(tc.givenJSON), tc.isLegacy) + switch { + case tc.expectedError != nil: + assert.EqualError(t, err, tc.expectedError.Error()) + default: + assert.NoError(t, err) + assert.Equal(t, tc.expectedAuth, dockerConfig.Auths) + } + }) + + } +} + +func TestGetServerFromDockerAuthKey(t *testing.T) { + testCases := []struct { + authKey string + expectedServer string + }{ + { + authKey: "34.86.43.13:80", + expectedServer: "34.86.43.13:80", + }, + { + authKey: "core.harbor.domain:8080", + expectedServer: "core.harbor.domain:8080", + }, + { + authKey: "rg.pl-waw.scw.cloud/trivyoperator", + expectedServer: "rg.pl-waw.scw.cloud", + }, + { + authKey: "rg.pl-waw.scw.cloud:7777/private", + expectedServer: "rg.pl-waw.scw.cloud:7777", + }, + { + authKey: "registry.aquasec.com", + expectedServer: "registry.aquasec.com", + }, + { + authKey: "https://index.docker.io/v1/", + expectedServer: "index.docker.io", + }, + { + authKey: "https://registry:3780/", + expectedServer: "registry:3780", + }, + } + + for _, tc := range testCases { + t.Run(tc.authKey, func(t *testing.T) { + server, err := GetServerFromDockerAuthKey(tc.authKey) + require.NoError(t, err) + assert.Equal(t, tc.expectedServer, server) + }) + } +} + +func TestGetServerFromImageRef(t *testing.T) { + testCases := []struct { + imageRef string + expectedServer string + }{ + { + imageRef: "nginx:1.16", + expectedServer: "index.docker.io", + }, + { + imageRef: "aquasec/trivy:0.10.0", + expectedServer: "index.docker.io", + }, + { + imageRef: "docker.io/aquasec/harbor-scanner-trivy:0.10.0", + expectedServer: "index.docker.io", + }, + { + imageRef: "index.docker.io/aquasec/harbor-scanner-trivy:0.10.0", + expectedServer: "index.docker.io", + }, + { + imageRef: "gcr.io/google-samples/hello-app:1.0", + expectedServer: "gcr.io", + }, + } + for _, tc := range testCases { + t.Run(tc.imageRef, func(t *testing.T) { + server, err := GetServerFromImageRef(tc.imageRef) + require.NoError(t, err) + assert.Equal(t, tc.expectedServer, server) + }) + } +} + +func TestBasicAuth_Decode(t *testing.T) { + testCases := []struct { + name string + v BasicAuth + want string + want1 string + }{ + { + name: "Decode GCR", + v: BasicAuth("X2pzb25fa2V5OnsKICAidHlwZSI6ICJzZXJ2aWNlX2FjY291bnQiLAogICJwcm9qZWN0X2lkIjogInRlc3QiLAogICJwcml2YXRlX2tleV9pZCI6ICIzYWRhczM0YXNkYXMzNHdhZGFkIiwKICAicHJpdmF0ZV9rZXkiOiAiLS0tLS1CRUdJTiBQUklWQVRFIEtFWS0tLS0tXG4tLS0tLUVORCBQUklWQVRFIEtFWS0tLS0tXG4iLAogICJjbGllbnRfZW1haWwiOiAidGVzdEB0ZXN0LmlhbS5nc2VydmljZWFjY291bnQuY29tIiwKICAiY2xpZW50X2lkIjogIjM0MzI0MjM0MzI0MzI0IiwKICAiYXV0aF91cmkiOiAiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tL28vb2F1dGgyL2F1dGgiLAogICJ0b2tlbl91cmkiOiAiaHR0cHM6Ly9vYXV0aDIuZ29vZ2xlYXBpcy5jb20vdG9rZW4iLAogICJhdXRoX3Byb3ZpZGVyX3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vb2F1dGgyL3YxL2NlcnRzIiwKICAiY2xpZW50X3g1MDlfY2VydF91cmwiOiAiaHR0cHM6Ly93d3cuZ29vZ2xlYXBpcy5jb20vcm9ib3QvdjEvbWV0YWRhdGEveDUwOS90ZXN0LWdjci1mNWRoM2g1ZyU0MHRlc3QuaWFtLmdzZXJ2aWNlYWNjb3VudC5jb20iCn0="), + want: "_json_key", + want1: `{ + "type": "service_account", + "project_id": "test", + "private_key_id": "3adas34asdas34wadad", + "private_key": "-----BEGIN PRIVATE KEY-----\n-----END PRIVATE KEY-----\n", + "client_email": "test@test.iam.gserviceaccount.com", + "client_id": "34324234324324", + "auth_uri": "https://accounts.google.com/o/oauth2/auth", + "token_uri": "https://oauth2.googleapis.com/token", + "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs", + "client_x509_cert_url": "https://www.googleapis.com/robot/v1/metadata/x509/test-gcr-f5dh3h5g%40test.iam.gserviceaccount.com" +}`, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + got, got1, err := tc.v.Decode() + require.NoError(t, err) + assert.Equalf(t, tc.want, got, "Decode()") + assert.Equalf(t, tc.want1, got1, "Decode()") + }) + } +} diff --git a/pkg/k8s/k8s.go b/pkg/k8s/k8s.go index 3a9dc85..d8a30f6 100644 --- a/pkg/k8s/k8s.go +++ b/pkg/k8s/k8s.go @@ -6,11 +6,14 @@ import ( "strings" "github.com/aquasecurity/trivy-kubernetes/pkg/bom" + "github.com/aquasecurity/trivy-kubernetes/pkg/k8s/docker" containerimage "github.com/google/go-containerregistry/pkg/name" + ms "github.com/mitchellh/mapstructure" corev1 "k8s.io/api/core/v1" k8sapierror "k8s.io/apimachinery/pkg/api/errors" "k8s.io/apimachinery/pkg/api/meta" metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/apis/meta/v1/unstructured" "k8s.io/apimachinery/pkg/runtime/schema" "k8s.io/cli-runtime/pkg/genericclioptions" "k8s.io/client-go/dynamic" @@ -51,6 +54,8 @@ const ( ClusterRoleBindings = "clusterrolebindings" Nodes = "nodes" k8sComponentNamespace = "kube-system" + + serviceAccountDefault = "default" ) // Cluster interface represents the operations needed to scan a cluster @@ -74,6 +79,8 @@ type Cluster interface { CreateClusterBom(ctx context.Context) (*bom.Result, error) // GetClusterVersion return cluster git version GetClusterVersion() string + // AuthByResource return image pull secrets by resource pod spec + AuthByResource(resource unstructured.Unstructured) (map[string]docker.Auth, error) } type cluster struct { @@ -455,3 +462,197 @@ func (c *cluster) ClusterNameVersion() (string, string, error) { } return clusterName, version.GitVersion, nil } + +// ListImagePullSecretsByPodSpec return image pull secrets by pod spec +func (r *cluster) ListImagePullSecretsByPodSpec(ctx context.Context, spec *corev1.PodSpec, ns string) (map[string]docker.Auth, error) { + if spec == nil { + return map[string]docker.Auth{}, nil + } + imagePullSecrets := spec.ImagePullSecrets + + sa, err := r.getServiceAccountByPodSpec(ctx, spec, ns) + if err != nil && !k8sapierror.IsNotFound(err) { + return nil, err + } + imagePullSecrets = append(sa.ImagePullSecrets, imagePullSecrets...) + + secrets, err := r.ListByLocalObjectReferences(ctx, imagePullSecrets, ns) + if err != nil { + return nil, err + } + + return mapDockerRegistryServersToAuths(secrets, true) +} + +func (r *cluster) getServiceAccountByPodSpec(ctx context.Context, spec *corev1.PodSpec, ns string) (*corev1.ServiceAccount, error) { + serviceAccountName := spec.ServiceAccountName + if serviceAccountName == "" { + serviceAccountName = serviceAccountDefault + } + sa, err := r.clientset.CoreV1().ServiceAccounts(ns).Get(ctx, serviceAccountName, metav1.GetOptions{}) + if err != nil { + return sa, fmt.Errorf("getting service account by name: %s/%s: %w", ns, serviceAccountName, err) + } + return sa, nil +} + +func (r *cluster) ListByLocalObjectReferences(ctx context.Context, refs []corev1.LocalObjectReference, ns string) ([]*corev1.Secret, error) { + secrets := make([]*corev1.Secret, 0) + + for _, secretRef := range refs { + secret, err := r.clientset.CoreV1().Secrets(ns).Get(ctx, secretRef.Name, metav1.GetOptions{}) + if err != nil { + if k8sapierror.IsNotFound(err) { + continue + } + return nil, fmt.Errorf("getting secret by name: %s/%s: %w", ns, secretRef.Name, err) + } + secrets = append(secrets, secret) + } + return secrets, nil +} + +// MapDockerRegistryServersToAuths creates the mapping from a Docker registry server +// to the Docker authentication credentials for the specified slice of image pull Secrets. +func mapDockerRegistryServersToAuths(imagePullSecrets []*corev1.Secret, multiSecretSupport bool) (map[string]docker.Auth, error) { + auths := make(map[string]docker.Auth) + for _, secret := range imagePullSecrets { + var data []byte + var hasRequiredData, isLegacy bool + + switch secret.Type { + case corev1.SecretTypeDockerConfigJson: + data, hasRequiredData = secret.Data[corev1.DockerConfigJsonKey] + case corev1.SecretTypeDockercfg: + data, hasRequiredData = secret.Data[corev1.DockerConfigKey] + isLegacy = true + default: + continue + } + + // Skip a secrets of type "kubernetes.io/dockerconfigjson" or "kubernetes.io/dockercfg" which does not contain + // the required ".dockerconfigjson" or ".dockercfg" key. + if !hasRequiredData { + continue + } + dockerConfig := &docker.Config{} + err := dockerConfig.Read(data, isLegacy) + if err != nil { + return nil, fmt.Errorf("reading %s or %s field of %q secret: %w", corev1.DockerConfigJsonKey, corev1.DockerConfigKey, secret.Namespace+"/"+secret.Name, err) + } + for authKey, auth := range dockerConfig.Auths { + server, err := docker.GetServerFromDockerAuthKey(authKey) + if err != nil { + return nil, err + } + if a, ok := auths[server]; multiSecretSupport && ok { + user := fmt.Sprintf("%s,%s", a.Username, auth.Username) + pass := fmt.Sprintf("%s,%s", a.Password, auth.Password) + auths[server] = docker.Auth{Username: user, Password: pass} + } else { + auths[server] = auth + } + } + } + return auths, nil +} + +type ContainerImages map[string]string + +func MapContainerNamesToDockerAuths(imageRef string, auths map[string]docker.Auth) (docker.Auth, error) { + wildcardServers := GetWildcardServers(auths) + + var authsCred docker.Auth + server, err := docker.GetServerFromImageRef(imageRef) + if err != nil { + return authsCred, err + } + if auth, ok := auths[server]; ok { + return auth, nil + } + if len(wildcardServers) > 0 { + if wildcardDomain := matchSubDomain(wildcardServers, server); len(wildcardDomain) > 0 { + if auth, ok := auths[wildcardDomain]; ok { + return auth, nil + } + } + } + + return authsCred, nil +} + +func GetWildcardServers(auths map[string]docker.Auth) []string { + wildcardServers := make([]string, 0) + for server := range auths { + if strings.HasPrefix(server, "*.") { + wildcardServers = append(wildcardServers, server) + } + } + return wildcardServers +} + +func matchSubDomain(wildcardServers []string, subDomain string) string { + for _, domain := range wildcardServers { + domainWithoutWildcard := strings.Replace(domain, "*", "", 1) + if strings.HasSuffix(subDomain, domainWithoutWildcard) { + return domain + } + } + return "" +} + +func getWorkloadPodSpec(un unstructured.Unstructured) (*corev1.PodSpec, error) { + switch un.GetKind() { + case KindPod: + objectMap, ok, err := unstructured.NestedMap(un.Object, []string{"spec"}...) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("unstructured resource do not match Pod spec") + } + return mapToPodSpec(objectMap) + case KindCronJob: + objectMap, ok, err := unstructured.NestedMap(un.Object, []string{"spec", "jobTemplate", "spec", "template", "spec"}...) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("unstructured resource do not match Pod spec") + } + return mapToPodSpec(objectMap) + case KindDeployment: + objectMap, ok, err := unstructured.NestedMap(un.Object, []string{"spec", "template", "spec"}...) + if err != nil { + return nil, err + } + if !ok { + return nil, fmt.Errorf("unstructured resource do not match Pod spec") + } + return mapToPodSpec(objectMap) + default: + return nil, nil + } +} + +func mapToPodSpec(objectMap map[string]interface{}) (*corev1.PodSpec, error) { + ps := &corev1.PodSpec{} + err := ms.Decode(objectMap, ps) + if err != nil && len(ps.Containers) == 0 { + return nil, err + } + return ps, nil +} + +func (r *cluster) AuthByResource(resource unstructured.Unstructured) (map[string]docker.Auth, error) { + podSpec, err := getWorkloadPodSpec(resource) + if err != nil { + return nil, err + } + var serverAuths map[string]docker.Auth + serverAuths, err = r.ListImagePullSecretsByPodSpec(context.Background(), podSpec, resource.GetNamespace()) + if err != nil { + return nil, err + } + return serverAuths, nil +} diff --git a/pkg/trivyk8s/trivyk8s.go b/pkg/trivyk8s/trivyk8s.go index a475324..c39e447 100644 --- a/pkg/trivyk8s/trivyk8s.go +++ b/pkg/trivyk8s/trivyk8s.go @@ -124,7 +124,11 @@ func (c *client) ListArtifacts(ctx context.Context) ([]*artifacts.Artifact, erro if c.ignoreResource(resource) { continue } - artifact, err := artifacts.FromResource(lastAppliedResource) + auths, err := c.cluster.AuthByResource(lastAppliedResource) + if err != nil { + return nil, fmt.Errorf("failed getting auth for gvr: %v - %w", gvr, err) + } + artifact, err := artifacts.FromResource(lastAppliedResource, auths) if err != nil { return nil, err } @@ -238,8 +242,11 @@ func (c *client) GetArtifact(ctx context.Context, kind, name string) (*artifacts if err != nil { return nil, fmt.Errorf("failed getting resource for gvr: %v - %w", gvr, err) } - - artifact, err := artifacts.FromResource(*resource) + auths, err := c.cluster.AuthByResource(*resource) + if err != nil { + return nil, fmt.Errorf("failed getting auth for gvr: %v - %w", gvr, err) + } + artifact, err := artifacts.FromResource(*resource, auths) if err != nil { return nil, err }