diff --git a/pkg/kube/object.go b/pkg/kube/object.go index 3c8d35194..521943223 100644 --- a/pkg/kube/object.go +++ b/pkg/kube/object.go @@ -445,6 +445,59 @@ func (o *ObjectResolver) GetRelatedReplicasetName(ctx context.Context, object Ob return "", fmt.Errorf("can only get related ReplicaSet for Deployment or Pod, not %q", string(object.Kind)) } +// GetNodeName will return the nodename from one of the running pod of any kubernetes kind +func (o *ObjectResolver) GetNodeName(ctx context.Context, obj client.Object) (nodeName string, err error) { + switch t := obj.(type) { + case *corev1.Pod: + return (obj.(*corev1.Pod)).Spec.NodeName, nil + case *appsv1.Deployment: + replicaSet, err := o.ReplicaSetByDeployment(ctx, obj.(*appsv1.Deployment)) + if err != nil { + return "", err + } + pods, err := o.getPodsByLabelSelector(ctx, obj.GetNamespace(), replicaSet.Spec.Selector.MatchLabels) + if err != nil || (pods != nil && len(pods.Items) == 0) { + return "", err + } + return pods.Items[0].Spec.NodeName, nil + case *appsv1.ReplicaSet: + pods, err := o.getPodsByLabelSelector(ctx, obj.GetNamespace(), obj.(*appsv1.ReplicaSet).Spec.Selector.MatchLabels) + if err != nil || (pods != nil && len(pods.Items) == 0) { + return "", err + } + return pods.Items[0].Spec.NodeName, nil + case *corev1.ReplicationController: + pods, err := o.getPodsByLabelSelector(ctx, obj.GetNamespace(), obj.(*corev1.ReplicationController).Spec.Selector) + if err != nil || (pods != nil && len(pods.Items) == 0) { + return "", err + } + return pods.Items[0].Spec.NodeName, nil + case *appsv1.StatefulSet: + pods, err := o.getPodsByLabelSelector(ctx, obj.GetNamespace(), obj.(*appsv1.StatefulSet).Spec.Selector.MatchLabels) + if err != nil || (pods != nil && len(pods.Items) == 0) { + return "", err + } + return pods.Items[0].Spec.NodeName, nil + case *appsv1.DaemonSet: + pods, err := o.getPodsByLabelSelector(ctx, obj.GetNamespace(), obj.(*appsv1.DaemonSet).Spec.Selector.MatchLabels) + if err != nil || (pods != nil && len(pods.Items) == 0) { + return "", err + } + return pods.Items[0].Spec.NodeName, nil + case *batchv1beta1.CronJob: + //Todo handle cronjob + return "", nil + case *batchv1.Job: + pods, err := o.getPodsByLabelSelector(ctx, obj.GetNamespace(), obj.(*batchv1.Job).Spec.Selector.MatchLabels) + if err != nil || (pods != nil && len(pods.Items) == 0) { + return "", err + } + return pods.Items[0].Spec.NodeName, nil + default: + return "", fmt.Errorf("unsupported workload kind: %T", t) + } +} + func (o *ObjectResolver) getActiveReplicaSetByDeployment(ctx context.Context, object Object) (string, error) { deploy := &appsv1.Deployment{} err := o.Client.Get(ctx, types.NamespacedName{Namespace: object.Namespace, Name: object.Name}, deploy) @@ -488,3 +541,15 @@ func (o *ObjectResolver) getReplicaSetByPod(ctx context.Context, object Object) } return controller.Name, nil } + +func (o *ObjectResolver) getPodsByLabelSelector(ctx context.Context, namespace string, + labelSelector labels.Set) (pods *corev1.PodList, err error) { + pods = &corev1.PodList{} + err = o.Client.List(ctx, pods, client.InNamespace(namespace), + client.MatchingLabelsSelector{Selector: labels.SelectorFromSet(labelSelector)}) + if err != nil { + return pods, fmt.Errorf("listing pods in namespace %s for labelselector %v: %w", namespace, + labelSelector, err) + } + return pods, err +} diff --git a/pkg/operator/controller/vulnerabilityreport.go b/pkg/operator/controller/vulnerabilityreport.go index 95ee0d620..3281f8930 100644 --- a/pkg/operator/controller/vulnerabilityreport.go +++ b/pkg/operator/controller/vulnerabilityreport.go @@ -228,8 +228,13 @@ func (r *VulnerabilityReportReconciler) submitScanJob(ctx context.Context, owner WithTolerations(scanJobTolerations). WithAnnotations(scanJobAnnotations). WithCredentials(credentials). + WithClient(r.Client). Get() + if err != nil { + return fmt.Errorf("getting scan job spec: %w", err) + } + for _, secret := range secrets { secret.Namespace = r.PluginContext.GetNamespace() err = r.Client.Create(ctx, secret) diff --git a/pkg/plugin/trivy/plugin.go b/pkg/plugin/trivy/plugin.go index c16527b25..7ab9bb3e7 100644 --- a/pkg/plugin/trivy/plugin.go +++ b/pkg/plugin/trivy/plugin.go @@ -27,6 +27,7 @@ const ( const ( keyTrivyImageRef = "trivy.imageRef" keyTrivyMode = "trivy.mode" + keyTrivyCommand = "trivy.command" keyTrivySeverity = "trivy.severity" keyTrivyIgnoreUnfixed = "trivy.ignoreUnfixed" keyTrivyIgnoreFile = "trivy.ignoreFile" @@ -58,6 +59,13 @@ const ( ClientServer Mode = "ClientServer" ) +type Command string + +const ( + FileSystemScan Command = "fs" + ImageScan Command = "image" +) + // Config defines configuration params for this plugin. type Config struct { starboard.PluginConfig @@ -86,6 +94,23 @@ func (c Config) GetMode() (Mode, error) { value, keyTrivyMode, Standalone, ClientServer) } +func (c Config) GetCommand() (Command, error) { + var ok bool + var value string + if value, ok = c.Data[keyTrivyCommand]; !ok { + // for backward compatibility, fallback to ImageScan + return ImageScan, nil + } + switch Command(value) { + case ImageScan: + return ImageScan, nil + case FileSystemScan: + return FileSystemScan, nil + } + return "", fmt.Errorf("invalid value (%s) of %s; allowed values (%s, %s)", + value, keyTrivyCommand, ImageScan, FileSystemScan) +} + func (c Config) GetServerURL() (string, error) { return c.GetRequiredData(keyTrivyServerURL) } @@ -95,6 +120,11 @@ func (c Config) IgnoreFileExists() bool { return ok } +func (c Config) IgnoreUnfixed() bool { + _, ok := c.Data[keyTrivyIgnoreUnfixed] + return ok +} + func (c Config) GetInsecureRegistries() map[string]bool { insecureRegistries := make(map[string]bool) for key, val := range c.Data { @@ -205,13 +235,25 @@ func (p *plugin) GetScanJobSpec(ctx starboard.PluginContext, spec corev1.PodSpec if err != nil { return corev1.PodSpec{}, nil, err } - switch mode { - case Standalone: - return p.getPodSpecForStandaloneMode(ctx, config, spec, credentials) - case ClientServer: - return p.getPodSpecForClientServerMode(ctx, config, spec, credentials) - default: - return corev1.PodSpec{}, nil, fmt.Errorf("unrecognized trivy mode: %v", mode) + + command, err := config.GetCommand() + if command == ImageScan { + switch mode { + case Standalone: + return p.getPodSpecForStandaloneMode(ctx, config, spec, credentials) + case ClientServer: + return p.getPodSpecForClientServerMode(ctx, config, spec, credentials) + default: + return corev1.PodSpec{}, nil, fmt.Errorf("unrecognized trivy mode: %v", mode) + } + } else { + switch mode { + case Standalone: + return p.getPodSpecForStandaloneFSMode(ctx, config, spec, credentials) + default: + return corev1.PodSpec{}, nil, fmt.Errorf("unrecognized trivy file scan mode: %v", mode) + + } } } @@ -229,8 +271,10 @@ func (p *plugin) newSecretWithAggregateImagePullCredentials(spec corev1.PodSpec, } const ( - sharedVolumeName = "data" - ignoreFileVolumeName = "ignorefile" + sharedVolumeName = "data" + ignoreFileVolumeName = "ignorefile" + FsSharedVolumeName = "starboard" + SharedVolumeLocationOfTrivy = "/var/starboard/trivy" ) // In the Standalone mode there is the init container responsible for @@ -832,6 +876,205 @@ func (p *plugin) getPodSpecForClientServerMode(ctx starboard.PluginContext, conf }, secrets, nil } +//FileSystem scan option with standalone mode. +//The only difference is that instead of scanning the resource by name, +//We scanning the resource place on a specific file system location using the following command. +// +// trivy --quiet fs --format json --ignore-unfixed file/system/location +// +func (p *plugin) getPodSpecForStandaloneFSMode(ctx starboard.PluginContext, config Config, spec corev1.PodSpec, + credentials map[string]docker.Auth) (corev1.PodSpec, []*corev1.Secret, error) { + var secrets []*corev1.Secret + + if spec.NodeName == "" { + return corev1.PodSpec{}, nil, fmt.Errorf("cannot get nodename from podspec") + } + + trivyImageRef, err := config.GetImageRef() + if err != nil { + return corev1.PodSpec{}, nil, err + } + + trivyConfigName := starboard.GetPluginConfigMapName(Plugin) + + requirements, err := config.GetResourceRequirements() + if err != nil { + return corev1.PodSpec{}, nil, err + } + + volumeMounts := []corev1.VolumeMount{ + { + Name: FsSharedVolumeName, + ReadOnly: false, + MountPath: "/var/starboard", + }, + } + + initContainerCopyBinary := corev1.Container{ + Name: p.idGenerator.GenerateID(), + Image: trivyImageRef, + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Command: []string{ + "cp", + "-v", + "/usr/local/bin/trivy", + SharedVolumeLocationOfTrivy, + }, + Resources: requirements, + VolumeMounts: volumeMounts, + } + + initContainerDB := corev1.Container{ + Name: p.idGenerator.GenerateID(), + Image: trivyImageRef, + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Env: []corev1.EnvVar{ + constructEnvVarSourceFromConfigMap("HTTP_PROXY", trivyConfigName, keyTrivyHTTPProxy), + constructEnvVarSourceFromConfigMap("HTTPS_PROXY", trivyConfigName, keyTrivyHTTPSProxy), + constructEnvVarSourceFromConfigMap("NO_PROXY", trivyConfigName, keyTrivyNoProxy), + { + Name: "GITHUB_TOKEN", + ValueFrom: &corev1.EnvVarSource{ + SecretKeyRef: &corev1.SecretKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: trivyConfigName, + }, + Key: keyTrivyGitHubToken, + Optional: pointer.BoolPtr(true), + }, + }, + }, + }, + Command: []string{ + "trivy", + }, + Args: []string{ + "--download-db-only", + "--cache-dir", + "/var/starboard/trivy-db", + }, + Resources: requirements, + VolumeMounts: volumeMounts, + } + + var containers []corev1.Container + + volumes := []corev1.Volume{ + { + Name: FsSharedVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumDefault, + }, + }, + }, + } + + //TODO Move this to function and refactor the code to use it + if config.IgnoreFileExists() { + volumes = append(volumes, corev1.Volume{ + Name: ignoreFileVolumeName, + VolumeSource: corev1.VolumeSource{ + ConfigMap: &corev1.ConfigMapVolumeSource{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: trivyConfigName, + }, + Items: []corev1.KeyToPath{ + { + Key: keyTrivyIgnoreFile, + Path: ".trivyignore", + }, + }, + }, + }, + }) + + volumeMounts = append(volumeMounts, corev1.VolumeMount{ + Name: ignoreFileVolumeName, + MountPath: "/tmp/trivy/.trivyignore", + SubPath: ".trivyignore", + }) + } + + for _, c := range spec.Containers { + + env := []corev1.EnvVar{ + constructEnvVarSourceFromConfigMap("TRIVY_SEVERITY", trivyConfigName, keyTrivySeverity), + constructEnvVarSourceFromConfigMap("TRIVY_SKIP_FILES", trivyConfigName, keyTrivySkipFiles), + constructEnvVarSourceFromConfigMap("TRIVY_SKIP_DIRS", trivyConfigName, keyTrivySkipDirs), + constructEnvVarSourceFromConfigMap("HTTP_PROXY", trivyConfigName, keyTrivyHTTPProxy), + constructEnvVarSourceFromConfigMap("HTTPS_PROXY", trivyConfigName, keyTrivyHTTPSProxy), + constructEnvVarSourceFromConfigMap("NO_PROXY", trivyConfigName, keyTrivyNoProxy), + } + if config.IgnoreFileExists() { + env = append(env, corev1.EnvVar{ + Name: "TRIVY_IGNOREFILE", + Value: "/tmp/trivy/.trivyignore", + }) + } + if config.IgnoreUnfixed() { + env = append(env, constructEnvVarSourceFromConfigMap("TRIVY_IGNORE_UNFIXED", + trivyConfigName, keyTrivyIgnoreUnfixed)) + } + + env, err = p.appendTrivyInsecureEnv(config, c.Image, env) + if err != nil { + return corev1.PodSpec{}, nil, err + } + + resourceRequirements, err := config.GetResourceRequirements() + if err != nil { + return corev1.PodSpec{}, nil, err + } + + containers = append(containers, corev1.Container{ + Name: c.Name, + Image: c.Image, + ImagePullPolicy: corev1.PullNever, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Env: env, + Command: []string{ + SharedVolumeLocationOfTrivy, + }, + Args: []string{ + "--skip-update", + "--cache-dir", + "/var/starboard/trivy-db", + "--quiet", + "fs", + "--format", + "json", + "/", + }, + Resources: resourceRequirements, + VolumeMounts: volumeMounts, + // Todo review security Context which is better for trivy fs scan + SecurityContext: &corev1.SecurityContext{ + Privileged: pointer.BoolPtr(false), + AllowPrivilegeEscalation: pointer.BoolPtr(false), + Capabilities: &corev1.Capabilities{ + Drop: []corev1.Capability{"all"}, + }, + ReadOnlyRootFilesystem: pointer.BoolPtr(true), + }, + }) + } + + return corev1.PodSpec{ + Affinity: starboard.LinuxNodeAffinity(), + RestartPolicy: corev1.RestartPolicyNever, + ServiceAccountName: ctx.GetServiceAccountName(), + AutomountServiceAccountToken: pointer.BoolPtr(false), + Volumes: volumes, + InitContainers: []corev1.Container{initContainerCopyBinary, initContainerDB}, + Containers: containers, + SecurityContext: &corev1.PodSecurityContext{}, + NodeName: spec.NodeName, + }, secrets, nil +} + func (p *plugin) appendTrivyInsecureEnv(config Config, image string, env []corev1.EnvVar) ([]corev1.EnvVar, error) { ref, err := name.ParseReference(image) if err != nil { @@ -983,6 +1226,22 @@ func GetMirroredImage(image string, mirrors map[string]string) (string, error) { return mirroredImage, nil } } - // If nothing is mirrord, we can simply use the input image. + // If nothing is mirrored, we can simply use the input image. return image, nil } + +func constructEnvVarSourceFromConfigMap(envName, trivyConfigName, trivyConfikey string) (res corev1.EnvVar) { + res = corev1.EnvVar{ + Name: envName, + ValueFrom: &corev1.EnvVarSource{ + ConfigMapKeyRef: &corev1.ConfigMapKeySelector{ + LocalObjectReference: corev1.LocalObjectReference{ + Name: trivyConfigName, + }, + Key: trivyConfikey, + Optional: pointer.BoolPtr(true), + }, + }, + } + return +} diff --git a/pkg/plugin/trivy/plugin_test.go b/pkg/plugin/trivy/plugin_test.go index ded409e36..19f4857cf 100644 --- a/pkg/plugin/trivy/plugin_test.go +++ b/pkg/plugin/trivy/plugin_test.go @@ -116,6 +116,61 @@ func TestConfig_GetMode(t *testing.T) { } } +func TestConfig_GetCommand(t *testing.T) { + testCases := []struct { + name string + configData trivy.Config + expectedError string + expectedCommand trivy.Command + }{ + { + name: "Should return image", + configData: trivy.Config{PluginConfig: starboard.PluginConfig{ + Data: map[string]string{ + "trivy.command": "image", + }, + }}, + expectedCommand: trivy.ImageScan, + }, + { + name: "Should return image when value is not set", + configData: trivy.Config{PluginConfig: starboard.PluginConfig{ + Data: map[string]string{}, + }}, + expectedCommand: trivy.ImageScan, + }, + { + name: "Should return fs", + configData: trivy.Config{PluginConfig: starboard.PluginConfig{ + Data: map[string]string{ + "trivy.command": "fs", + }, + }}, + expectedCommand: trivy.FileSystemScan, + }, + { + name: "Should return error when value is not allowed", + configData: trivy.Config{PluginConfig: starboard.PluginConfig{ + Data: map[string]string{ + "trivy.command": "ls", + }, + }}, + expectedError: "invalid value (ls) of trivy.command; allowed values (image, fs)", + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + command, err := tc.configData.GetCommand() + if tc.expectedError != "" { + require.EqualError(t, err, tc.expectedError) + } else { + require.NoError(t, err) + assert.Equal(t, tc.expectedCommand, command) + } + }) + } +} + func TestConfig_GetResourceRequirements(t *testing.T) { testCases := []struct { name string @@ -222,6 +277,50 @@ CVE-2019-1543`, } } +func TestConfig_IgnoreUnfixed(t *testing.T) { + testCases := []struct { + name string + configData trivy.Config + expectedOutput bool + }{ + { + name: "Should return false", + configData: trivy.Config{PluginConfig: starboard.PluginConfig{ + Data: map[string]string{ + "foo": "bar", + }, + }}, + expectedOutput: false, + }, + { + name: "Should return true", + configData: trivy.Config{PluginConfig: starboard.PluginConfig{ + Data: map[string]string{ + "foo": "bar", + "trivy.ignoreUnfixed": "true", + }, + }}, + expectedOutput: true, + }, + { + name: "Should return false when set it as false", + configData: trivy.Config{PluginConfig: starboard.PluginConfig{ + Data: map[string]string{ + "foo": "bar", + "trivy.ignoreUnfixed": "false", + }, + }}, + expectedOutput: true, + }, + } + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + exists := tc.configData.IgnoreUnfixed() + assert.Equal(t, tc.expectedOutput, exists) + }) + } +} + func TestConfig_GetInsecureRegistries(t *testing.T) { testCases := []struct { name string @@ -2012,6 +2111,278 @@ CVE-2019-1543`, }, }, }, + { + name: "Trivy fs scan command in Standalone mode", + config: map[string]string{ + "trivy.imageRef": "docker.io/aquasec/trivy:0.20.0", + "trivy.mode": string(trivy.Standalone), + "trivy.command": string(trivy.FileSystemScan), + "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{ + { + Name: "nginx", + Image: "nginx:1.9.1", + }, + }, + NodeName: "kind-control-pane", + }, + expectedJobSpec: corev1.PodSpec{ + Affinity: starboard.LinuxNodeAffinity(), + RestartPolicy: corev1.RestartPolicyNever, + ServiceAccountName: "starboard-sa", + AutomountServiceAccountToken: pointer.BoolPtr(false), + Volumes: []corev1.Volume{ + { + Name: trivy.FsSharedVolumeName, + VolumeSource: corev1.VolumeSource{ + EmptyDir: &corev1.EmptyDirVolumeSource{ + Medium: corev1.StorageMediumDefault, + }, + }, + }, + }, + InitContainers: []corev1.Container{ + { + Name: "00000000-0000-0000-0000-000000000001", + Image: "docker.io/aquasec/trivy:0.20.0", + ImagePullPolicy: corev1.PullIfNotPresent, + TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError, + Command: []string{ + "cp", + "-v", + "/usr/local/bin/trivy", + trivy.SharedVolumeLocationOfTrivy, + }, + 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: trivy.FsSharedVolumeName, + ReadOnly: false, + MountPath: "/var/starboard", + }, + }, + }, + { + Name: "00000000-0000-0000-0000-000000000002", + Image: "docker.io/aquasec/trivy:0.20.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/starboard/trivy-db", + }, + 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: trivy.FsSharedVolumeName, + ReadOnly: false, + MountPath: "/var/starboard", + }, + }, + }, + }, + Containers: []corev1.Container{ + { + Name: "nginx", + Image: "nginx:1.9.1", + ImagePullPolicy: corev1.PullNever, + 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_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.SharedVolumeLocationOfTrivy, + }, + Args: []string{ + "--skip-update", + "--cache-dir", + "/var/starboard/trivy-db", + "--quiet", + "fs", + "--format", + "json", + "/", + }, + 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: trivy.FsSharedVolumeName, + ReadOnly: false, + MountPath: "/var/starboard", + }, + }, + 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{}, + NodeName: "kind-control-pane", + }, + }, } for _, tc := range testCases { diff --git a/pkg/vulnerabilityreport/builder.go b/pkg/vulnerabilityreport/builder.go index 786b81773..6f0f51374 100644 --- a/pkg/vulnerabilityreport/builder.go +++ b/pkg/vulnerabilityreport/builder.go @@ -1,6 +1,7 @@ package vulnerabilityreport import ( + "context" "fmt" "strings" "time" @@ -27,6 +28,7 @@ type ScanJobBuilder struct { credentials map[string]docker.Auth tolerations []corev1.Toleration annotations map[string]string + client client.Client } func NewScanJobBuilder() *ScanJobBuilder { @@ -68,12 +70,21 @@ func (s *ScanJobBuilder) WithCredentials(credentials map[string]docker.Auth) *Sc return s } +func (s *ScanJobBuilder) WithClient(client client.Client) *ScanJobBuilder { + s.client = client + return s +} + func (s *ScanJobBuilder) Get() (*batchv1.Job, []*corev1.Secret, error) { spec, err := kube.GetPodSpec(s.object) if err != nil { return nil, nil, err } + // get nodename from running pods. + resolver := kube.ObjectResolver{Client: s.client} + spec.NodeName, err = resolver.GetNodeName(context.Background(), s.object) + templateSpec, secrets, err := s.plugin.GetScanJobSpec(s.pluginContext, spec, s.credentials) if err != nil { return nil, nil, err diff --git a/pkg/vulnerabilityreport/builder_test.go b/pkg/vulnerabilityreport/builder_test.go index f3690b481..c12b583dc 100644 --- a/pkg/vulnerabilityreport/builder_test.go +++ b/pkg/vulnerabilityreport/builder_test.go @@ -16,6 +16,7 @@ import ( metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" "k8s.io/client-go/kubernetes/scheme" "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client/fake" ) func TestReportBuilder(t *testing.T) { @@ -64,6 +65,7 @@ func TestReportBuilder(t *testing.T) { func TestScanJobBuilder(t *testing.T) { g := gomega.NewGomegaWithT(t) + testClient := fake.NewClientBuilder().WithScheme(starboard.NewScheme()).Build() job, _, err := vulnerabilityreport.NewScanJobBuilder(). WithPlugin(&testPlugin{}). WithPluginContext(starboard.NewPluginContext(). @@ -72,6 +74,7 @@ func TestScanJobBuilder(t *testing.T) { WithServiceAccountName("starboard-sa"). Get()). WithTimeout(3 * time.Second). + WithClient(testClient). WithObject(&appsv1.ReplicaSet{ TypeMeta: metav1.TypeMeta{ Kind: "ReplicaSet", @@ -92,6 +95,7 @@ func TestScanJobBuilder(t *testing.T) { }, }, }, + Selector: &metav1.LabelSelector{}, }, }). Get() diff --git a/pkg/vulnerabilityreport/scanner.go b/pkg/vulnerabilityreport/scanner.go index 51b9d91de..37a8bdbe0 100644 --- a/pkg/vulnerabilityreport/scanner.go +++ b/pkg/vulnerabilityreport/scanner.go @@ -99,6 +99,7 @@ func (s *Scanner) Scan(ctx context.Context, workload kube.Object) ([]v1alpha1.Vu WithCredentials(credentials). WithTolerations(scanJobTolerations). WithAnnotations(scanJobAnnotations). + WithClient(s.objectResolver.Client). Get() if err != nil {