diff --git a/docs/integrations/vulnerability-scanners/trivy.md b/docs/integrations/vulnerability-scanners/trivy.md index fe22cdb4e..3c6d9786b 100644 --- a/docs/integrations/vulnerability-scanners/trivy.md +++ b/docs/integrations/vulnerability-scanners/trivy.md @@ -92,6 +92,8 @@ EOF | `trivy.serverURL` | N/A | The endpoint URL of the Trivy server. Required in `ClientServer` mode. | | `trivy.serverTokenHeader` | `Trivy-Token` | The name of the HTTP header to send the authentication token to Trivy server. Only application in `ClientServer` mode when `trivy.serverToken` is specified. | | `trivy.insecureRegistry.` | N/A | The registry to which insecure connections are allowed. There can be multiple registries with different registry ``. | +| `trivy.mirrors.registry.` | N/A | A registry for which a mirror should be used to get an image. For the mirror url `trivy.mirrors.mirror.` with the matching ``. For each entry there must be a corresponding `trivy.mirrors.mirror.` entry. There can be multiple registries with different registry ``. | +| `trivy.mirrors.mirror.` | N/A | The mirror for the registry with ``. | | `trivy.httpProxy` | N/A | The HTTP proxy used by Trivy to download the vulnerabilities database from GitHub. | | `trivy.httpsProxy` | N/A | The HTTPS proxy used by Trivy to download the vulnerabilities database from GitHub. | | `trivy.noProxy` | N/A | A comma separated list of IPs and domain names that are not subject to proxy settings. | diff --git a/pkg/plugin/trivy/plugin.go b/pkg/plugin/trivy/plugin.go index 6952c2f8a..3c340b1e7 100644 --- a/pkg/plugin/trivy/plugin.go +++ b/pkg/plugin/trivy/plugin.go @@ -31,6 +31,8 @@ const ( keyTrivyIgnoreUnfixed = "trivy.ignoreUnfixed" keyTrivyIgnoreFile = "trivy.ignoreFile" keyTrivyInsecureRegistryPrefix = "trivy.insecureRegistry." + keyTrivyMirrorSrcPrefix = "trivy.mirrors.registry." + keyTrivyMirrorDstPrefix = "trivy.mirrors.mirror." keyTrivyHTTPProxy = "trivy.httpProxy" keyTrivyHTTPSProxy = "trivy.httpsProxy" keyTrivyNoProxy = "trivy.noProxy" @@ -105,6 +107,28 @@ func (c Config) GetInsecureRegistries() map[string]bool { return insecureRegistries } +func (c Config) GetMirrors() (map[string]string, error) { + res := make(map[string]string) + for registyKey, registry := range c.Data { + if !strings.HasPrefix(registyKey, keyTrivyMirrorSrcPrefix) { + continue + } + mirrorKey := fmt.Sprintf( + "%v%v", + keyTrivyMirrorDstPrefix, + strings.TrimPrefix(registyKey, keyTrivyMirrorSrcPrefix), + ) + + mirror, ok := c.Data[mirrorKey] + if !ok { + return res, fmt.Errorf("mirror %v missing for %v", mirrorKey, registyKey) + } + + res[registry] = mirror + } + return res, nil +} + // GetResourceRequirements creates ResourceRequirements from the Config. func (c Config) GetResourceRequirements() (corev1.ResourceRequirements, error) { requirements := corev1.ResourceRequirements{ @@ -506,6 +530,11 @@ func (p *plugin) getPodSpecForStandaloneMode(config Config, spec corev1.PodSpec, return corev1.PodSpec{}, nil, err } + mirrors, err := config.GetMirrors() + if err != nil { + return corev1.PodSpec{}, nil, err + } + containers = append(containers, corev1.Container{ Name: c.Name, Image: trivyImageRef, @@ -522,7 +551,7 @@ func (p *plugin) getPodSpecForStandaloneMode(config Config, spec corev1.PodSpec, "--quiet", "--format", "json", - c.Image, + GetMirroredImage(c.Image, mirrors), }, Resources: resourceRequirements, VolumeMounts: volumeMounts, @@ -776,6 +805,11 @@ func (p *plugin) getPodSpecForClientServerMode(config Config, spec corev1.PodSpe return corev1.PodSpec{}, nil, err } + mirrors, err := config.GetMirrors() + if err != nil { + return corev1.PodSpec{}, nil, err + } + containers = append(containers, corev1.Container{ Name: container.Name, Image: trivyImageRef, @@ -792,7 +826,7 @@ func (p *plugin) getPodSpecForClientServerMode(config Config, spec corev1.PodSpe "json", "--remote", trivyServerURL, - container.Image, + GetMirroredImage(container.Image, mirrors), }, VolumeMounts: volumeMounts, Resources: requirements, @@ -946,3 +980,43 @@ func GetScoreFromCVSS(CVSSs map[string]*CVSS) *float64 { return nvdScore } + +func fullImagePath(image string) string { + const defaultRegistry = "index.docker.io" + registryAndImage := strings.Split(image, "/") + // Empty string + if len(registryAndImage) == 0 { + return image + } + + // Add default Registry, if none is present + if len(registryAndImage) == 1 || + !(strings.Contains(registryAndImage[0], ".") || + strings.Contains(registryAndImage[0], ":") || + registryAndImage[0] == "localhost") { + registryAndImage = append([]string{defaultRegistry}, registryAndImage...) + } + + // Add "library" if only image name is given and registry is DockerHub + if len(registryAndImage) == 2 && + (strings.HasSuffix(registryAndImage[0], "docker.io") || + strings.HasSuffix(registryAndImage[0], "docker.com")) { + registryAndImage = append(registryAndImage[:2], registryAndImage[1:]...) + registryAndImage[1] = "library" + } + + return strings.Join(registryAndImage, "/") +} + +func GetMirroredImage(image string, mirrors map[string]string) string { + mirroredImage := fullImagePath(image) + + for k, v := range mirrors { + if strings.HasPrefix(mirroredImage, k) { + mirroredImage = strings.Replace(mirroredImage, k, v, 1) + return mirroredImage + } + } + // If nothing is mirrord, we can simply use the input image without the fullpath. + return image +} diff --git a/pkg/plugin/trivy/plugin_test.go b/pkg/plugin/trivy/plugin_test.go index 80c7fd308..9446e8c9e 100644 --- a/pkg/plugin/trivy/plugin_test.go +++ b/pkg/plugin/trivy/plugin_test.go @@ -261,6 +261,56 @@ func TestConfig_GetInsecureRegistries(t *testing.T) { } } +func TestConfig_GetMirrors(t *testing.T) { + testCases := []struct { + name string + configData trivy.Config + expectedOutput map[string]string + expectError bool + }{ + { + name: "Should return empty map when there is no key with trivy.mirrors.registry. prefix", + configData: trivy.Config{PluginConfig: starboard.PluginConfig{ + Data: map[string]string{ + "foo": "bar", + }, + }}, + expectedOutput: make(map[string]string), + }, + { + name: "Should return mirrors in a map", + configData: trivy.Config{PluginConfig: starboard.PluginConfig{ + Data: map[string]string{ + "trivy.mirrors.registry.docker": "docker.io", + "trivy.mirrors.mirror.docker": "mirror.io", + }, + }}, + expectedOutput: map[string]string{"docker.io": "mirror.io"}, + }, + { + name: "Should return error, if mirror is missing for registry", + configData: trivy.Config{PluginConfig: starboard.PluginConfig{ + Data: map[string]string{ + "trivy.mirrors.registry.docker": "docker.io", + }, + }}, + expectError: true, + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + mirrors, err := tc.configData.GetMirrors() + if tc.expectError { + assert.Error(t, err) + } else { + assert.Equal(t, tc.expectedOutput, mirrors) + } + + }) + } +} + func TestPlugin_Init(t *testing.T) { t.Run("Should create the default config", func(t *testing.T) { @@ -1152,12 +1202,263 @@ CVE-2019-1543`, SecurityContext: &corev1.PodSecurityContext{}, }, }, + { + name: "Standalone mode with mirror", + config: map[string]string{ + "trivy.imageRef": "docker.io/aquasec/trivy:0.14.0", + "trivy.mode": string(trivy.Standalone), + + "trivy.resources.requests.cpu": "100m", + "trivy.resources.requests.memory": "100M", + "trivy.resources.limits.cpu": "500m", + "trivy.resources.limits.memory": "500M", + + "trivy.mirrors.registry.dockerio": "index.docker.io", + "trivy.mirrors.mirror.dockerio": "mirror.io", + }, + workloadSpec: corev1.PodSpec{ + Containers: []corev1.Container{ + { + Name: "nginx", + Image: "nginx:1.16", + }, + }, + }, + expectedJobSpec: corev1.PodSpec{ + Affinity: starboard.LinuxNodeAffinity(), + RestartPolicy: corev1.RestartPolicyNever, + AutomountServiceAccountToken: pointer.BoolPtr(false), + Volumes: []corev1.Volume{ + { + Name: "data", + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumDefault, + }, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: "00000000-0000-0000-0000-000000000001", + Image: "docker.io/aquasec/trivy:0.14.0", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Env: []corev1.EnvVar{ + { + Name: "HTTP_PROXY", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.httpProxy", + Optional: pointer.BoolPtr(true), + }, + }, + }, + { + Name: "HTTPS_PROXY", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.httpsProxy", + Optional: pointer.BoolPtr(true), + }, + }, + }, + { + Name: "NO_PROXY", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.noProxy", + Optional: pointer.BoolPtr(true), + }, + }, + }, + + { + Name: "GITHUB_TOKEN", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.githubToken", + Optional: pointer.BoolPtr(true), + }, + }, + }, + }, + Command: []string{ + "trivy", + }, + Args: []string{ + "--download-db-only", + "--cache-dir", "/var/lib/trivy", + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("100M"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("500M"), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "data", + MountPath: "/var/lib/trivy", + ReadOnly: false, + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "nginx", + Image: "docker.io/aquasec/trivy:0.14.0", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Env: []corev1.EnvVar{ + { + Name: "TRIVY_SEVERITY", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.severity", + Optional: pointer.BoolPtr(true), + }, + }, + }, + { + Name: "TRIVY_IGNORE_UNFIXED", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.ignoreUnfixed", + Optional: pointer.BoolPtr(true), + }, + }, + }, + { + Name: "TRIVY_SKIP_FILES", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.skipFiles", + Optional: pointer.BoolPtr(true), + }, + }, + }, + { + Name: "TRIVY_SKIP_DIRS", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.skipDirs", + Optional: pointer.BoolPtr(true), + }, + }, + }, + { + Name: "HTTP_PROXY", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.httpProxy", + Optional: pointer.BoolPtr(true), + }, + }, + }, + { + Name: "HTTPS_PROXY", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.httpsProxy", + Optional: pointer.BoolPtr(true), + }, + }, + }, + { + Name: "NO_PROXY", + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: "starboard-trivy-config", + }, + Key: "trivy.noProxy", + Optional: pointer.BoolPtr(true), + }, + }, + }, + }, + Command: []string{ + "trivy", + }, + Args: []string{ + "--skip-update", + "--cache-dir", "/var/lib/trivy", + "--quiet", + "--format", "json", + "mirror.io/library/nginx:1.16", + }, + Resources: corev1.ResourceRequirements{ + Requests: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("100m"), + corev1.ResourceMemory: resource.MustParse("100M"), + }, + Limits: corev1.ResourceList{ + corev1.ResourceCPU: resource.MustParse("500m"), + corev1.ResourceMemory: resource.MustParse("500M"), + }, + }, + VolumeMounts: []corev1.VolumeMount{ + { + Name: "data", + ReadOnly: false, + MountPath: "/var/lib/trivy", + }, + }, + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.BoolPtr(false), + AllowPrivilegeEscalation: pointer.BoolPtr(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"all"}, + }, + ReadOnlyRootFilesystem: pointer.BoolPtr(true), + }, + }, + }, + SecurityContext: &corev1.PodSecurityContext{}, + }, + }, { name: "ClientServer mode without insecure registry", config: map[string]string{ - "trivy.imageRef": "docker.io/aquasec/trivy:0.14.0", - "trivy.mode": string(trivy.ClientServer), - "trivy.serverURL": "http://trivy.trivy:4954", + "trivy.imageRef": "docker.io/aquasec/trivy:0.14.0", + "trivy.mode": string(trivy.ClientServer), + "trivy.serverURL": "http://trivy.trivy:4954", "trivy.resources.requests.cpu": "100m", "trivy.resources.requests.memory": "100M", "trivy.resources.limits.cpu": "500m", @@ -1335,10 +1636,10 @@ CVE-2019-1543`, "trivy.mode": string(trivy.ClientServer), "trivy.serverURL": "http://trivy.trivy:4954", "trivy.insecureRegistry.pocRegistry": "poc.myregistry.harbor.com.pl", - "trivy.resources.requests.cpu": "100m", - "trivy.resources.requests.memory": "100M", - "trivy.resources.limits.cpu": "500m", - "trivy.resources.limits.memory": "500M", + "trivy.resources.requests.cpu": "100m", + "trivy.resources.requests.memory": "100M", + "trivy.resources.limits.cpu": "500m", + "trivy.resources.limits.memory": "500M", }, workloadSpec: corev1.PodSpec{ Containers: []corev1.Container{ @@ -1976,3 +2277,56 @@ func TestGetScoreFromCVSS(t *testing.T) { }) } } + +func TestGetMirroredImage(t *testing.T) { + testCases := []struct { + name string + image string + mirrors map[string]string + expected string + }{ + { + name: "no matching mirror, same image", + image: "index.docker.io/library/alpine", + mirrors: map[string]string{"gcr.io": "mirror.io"}, + expected: "index.docker.io/library/alpine", + }, + { + name: "matching mirror, changed image", + image: "index.docker.io/library/alpine", + mirrors: map[string]string{"index.docker.io": "mirror.io"}, + expected: "mirror.io/library/alpine", + }, + { + name: "no matching mirror, default registry", + image: "library/alpine", + mirrors: map[string]string{"gcr.io": "mirror.io"}, + expected: "library/alpine", + }, + { + name: "matching mirror, default registry", + image: "library/alpine", + mirrors: map[string]string{"index.docker.io": "mirror.io"}, + expected: "mirror.io/library/alpine", + }, + { + name: "matching mirror, default registry, expanded image name", + image: "alpine", + mirrors: map[string]string{"index.docker.io": "mirror.io"}, + expected: "mirror.io/library/alpine", + }, + { + name: "matching mirror, no expanded image name", + image: "quay.io/alpine", + mirrors: map[string]string{"quay.io": "mirror.io"}, + expected: "mirror.io/alpine", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + expected := trivy.GetMirroredImage(tc.image, tc.mirrors) + assert.Equal(t, tc.expected, expected) + }) + } +}