Repository: aquasecurity/starboard
Pull request #282 (2020-12-16 10:41:34)

feat(trivy): Authenticate Trivy client with token and custom headers
Resolves: #248 #249

Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/282?src=pr&el=h1) Report
> Merging [#282](https://codecov.io/gh/aquasecurity/starboard/pull/282?src=pr&el=desc) (7debb37) into [master](https://codecov.io/gh/aquasecurity/starboard/commit/4f58c956a5e1a92c0c8eabd8186b01e1d2354550?el=desc) (4f58c95) will **decrease** coverage by `39.28%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aquasecurity/starboard/pull/282/graphs/tree.svg?width=650&height=150&src=pr&token=qqTGKAJ0Wb)](https://codecov.io/gh/aquasecurity/starboard/pull/282?src=pr&el=tree)

```diff
@@             Coverage Diff             @@
##           master     #282       +/-   ##
===========================================
- Coverage   60.56%   21.27%   -39.29%     
===========================================
  Files          56       50        -6     
  Lines        2660     2331      -329     
===========================================
- Hits         1611      496     -1115     
- Misses        837     1792      +955     
+ Partials      212       43      -169     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/282?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/starboard/config.go](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree#diff-cGtnL3N0YXJib2FyZC9jb25maWcuZ28=) | `64.19% <0.00%> (-2.47%)` | :arrow_down: |
| [pkg/trivy/converter.go](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree#diff-cGtnL3RyaXZ5L2NvbnZlcnRlci5nbw==) | `83.60% <ø> (-9.84%)` | :arrow_down: |
| [pkg/trivy/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree#diff-cGtnL3RyaXZ5L3NjYW5uZXIuZ28=) | `30.66% <ø> (-22.23%)` | :arrow_down: |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree#diff-cGtnL2NtZC9nZXQuZ28=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/find.go](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree#diff-cGtnL2NtZC9maW5kLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/scan.go](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree#diff-cGtnL2NtZC9zY2FuLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/polaris.go](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree#diff-cGtnL2NtZC9wb2xhcmlzLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/version.go](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree#diff-cGtnL2NtZC92ZXJzaW9uLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/kube\_bench.go](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree#diff-cGtnL2NtZC9rdWJlX2JlbmNoLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/kube\_hunter.go](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree#diff-cGtnL2NtZC9rdWJlX2h1bnRlci5nbw==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [33 more](https://codecov.io/gh/aquasecurity/starboard/pull/282/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/282?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/282?src=pr&el=footer). Last update [4f58c95...7debb37](https://codecov.io/gh/aquasecurity/starboard/pull/282?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/docs/configuration.md b/docs/configuration.md
index 1a0b965..e0f6a02 100644
--- a/docs/configuration.md
+++ b/docs/configuration.md
@@ -34,18 +34,21 @@ kubectl patch secret starboard -n starboard \
 The following tables list available configuration parameters with their default
 values.
 
-| CONFIGMAP KEY         | DEFAULT                                                | DESCRIPTION |
-| --------------------- | ------------------------------------------------------ | ----------- |
-| `trivy.httpProxy`     | N/A                                                    | The HTTP proxy used by Trivy to download the vulnerabilities database from GitHub. Only applicable if Trivy runs in the `Standalone` mode. |
-| `trivy.severity`      | `UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL`                     | A comma separated list of severity levels reported by Trivy |
-| `trivy.imageRef`      | `docker.io/aquasec/trivy:0.14.0`                       | Trivy image reference |
-| `trivy.mode`          | `Standalone`                                           | Trivy client mode. Either `Standalone` or `ClientServer`. |
-| `trivy.serverURL`     | `http://trivy-server.trivy-server:4954`                | The endpoint URL of the Trivy server. This parameter is required when Trivy runs in the `ClientServer` mode. |
-| `polaris.config.yaml` | [Check the default value here][default-polaris-config] | Polaris configuration file |
-
-| SECRET KEY          | DESCRIPTION |
-| ------------------- | ----------- |
-| `trivy.githubToken` | The GitHub personal access token used by Trivy to download the vulnerabilities database from GitHub. Only applicable if Trivy runs in the `Standalone` mode. |
+| CONFIGMAP KEY             | DEFAULT                                                | DESCRIPTION |
+| ------------------------- | ------------------------------------------------------ | ----------- |
+| `trivy.httpProxy`         | N/A                                                    | The HTTP proxy used by Trivy to download the vulnerabilities database from GitHub. Only applicable in `Standalone` mode. |
+| `trivy.severity`          | `UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL`                     | A comma separated list of severity levels reported by Trivy |
+| `trivy.imageRef`          | `docker.io/aquasec/trivy:0.14.0`                       | Trivy image reference |
+| `trivy.mode`              | `Standalone`                                           | Trivy client mode. Either `Standalone` or `ClientServer`. Depending on the active mode other settings might be applicable or required. |
+| `trivy.serverURL`         | `http://trivy-server.trivy-server:4954`                | 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. |
+| `polaris.config.yaml`     | [Check the default value here][default-polaris-config] | Polaris configuration file |
+
+| SECRET KEY                  | DESCRIPTION |
+| --------------------------- | ----------- |
+| `trivy.githubToken`         | The GitHub access token used by Trivy to download the vulnerabilities database from GitHub. Only applicable in `Standalone` mode. |
+| `trivy.serverToken`         | The token to authenticate Trivy client with Trivy server. Only applicable in `ClientServer` mode. |
+| `trivy.serverCustomHeaders` | A comma-separated list of custom HTTP headers sent by Trivy client to Trivy server. Only applicable in `ClientServer` mode. |
 
 > **NOTE** You can find it handy to delete a configuration key, which was not created by default by the
 > `starboard init` command. For example, the following `kubectl patch` command deletes the `trivy.httpProxy` key:
diff --git a/pkg/starboard/config.go b/pkg/starboard/config.go
index d252a03..d3ebf0e 100644
--- a/pkg/starboard/config.go
+++ b/pkg/starboard/config.go
@@ -3,19 +3,17 @@ package starboard
 import (
 	"context"
 	"fmt"
-	"k8s.io/apimachinery/pkg/labels"
 
-	starboardv1alpha1 "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/google/go-containerregistry/pkg/name"
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/client-go/kubernetes"
-
-	"github.com/google/go-containerregistry/pkg/name"
 )
 
 const (
@@ -210,7 +208,7 @@ func NewScheme() *runtime.Scheme {
 	_ = corev1.AddToScheme(scheme)
 	_ = batchv1.AddToScheme(scheme)
 	_ = appsv1.AddToScheme(scheme)
-	_ = starboardv1alpha1.AddToScheme(scheme)
+	_ = v1alpha1.AddToScheme(scheme)
 	return scheme
 }
 
@@ -230,12 +228,6 @@ const (
 	ClientServer TrivyMode = "ClientServer"
 )
 
-type TrivyConfig interface {
-	GetTrivyImageRef() string
-	GetTrivyMode() TrivyMode
-	GetTrivyServerURL() string
-}
-
 // ConfigData holds Starboard configuration settings as a set
 // of key-value pairs.
 type ConfigData map[string]string
@@ -247,7 +239,7 @@ type ConfigManager interface {
 	Delete(ctx context.Context) error
 }
 
-// GetDefaultConfig returns the default configuration data.
+// GetDefaultConfig returns the default configuration settings.
 func GetDefaultConfig() ConfigData {
 	return map[string]string{
 		"trivy.severity":      "UNKNOWN,LOW,MEDIUM,HIGH,CRITICAL",
diff --git a/pkg/trivy/converter.go b/pkg/trivy/converter.go
index 4f158df..81c9761 100644
--- a/pkg/trivy/converter.go
+++ b/pkg/trivy/converter.go
@@ -20,10 +20,10 @@ type Converter interface {
 }
 
 type converter struct {
-	config starboard.TrivyConfig
+	config Config
 }
 
-func NewConverter(config starboard.TrivyConfig) Converter {
+func NewConverter(config Config) Converter {
 	return &converter{
 		config: config,
 	}
@@ -98,6 +98,7 @@ func (c *converter) toSummary(vulnerabilities []v1alpha1.Vulnerability) (vs v1al
 	return
 }
 
+// TODO check if it works if both tag and digest are specified
 func (c *converter) parseImageRef(imageRef string) (v1alpha1.Registry, v1alpha1.Artifact, error) {
 	ref, err := name.ParseReference(imageRef)
 	if err != nil {
diff --git a/pkg/trivy/scanner.go b/pkg/trivy/scanner.go
index 315b3d6..69f00b2 100644
--- a/pkg/trivy/scanner.go
+++ b/pkg/trivy/scanner.go
@@ -32,19 +32,26 @@ var (
 
 type scanner struct {
 	idGenerator ext.IDGenerator
-	config      starboard.TrivyConfig
+	config      Config
 	converter   Converter
 }
 
-// NewScanner constructs a new Plugin, which is using an official
-// Trivy container image to scan Kubernetes workloads.
+// Config defines configuration params for the Trivy vulnerabilityreport.Plugin.
+type Config interface {
+	GetTrivyImageRef() string
+	GetTrivyMode() starboard.TrivyMode
+	GetTrivyServerURL() string
+}
+
+// NewScanner constructs a new vulnerabilityreport.Plugin, which is using an
+// official Trivy container image to scan Kubernetes workloads.
 //
 // This Plugin supports both starboard.Standalone and starboard.ClientServer
-// client modes depending on the current starboard.TrivyConfig.
+// client modes depending on the active mode returned by Config.GetTrivyMode.
 //
-// The starboard.ClientServer more is usually more performant, however it
-// requires a Trivy server to be hosted and accessible at the configurable URL.
-func NewScannerPlugin(idGenerator ext.IDGenerator, config starboard.TrivyConfig) vulnerabilityreport.Plugin {
+// The starboard.ClientServer mode is usually more performant, however it
+// requires a Trivy server accessible at the configurable URL.
+func NewScannerPlugin(idGenerator ext.IDGenerator, config Config) vulnerabilityreport.Plugin {
 	return &scanner{
 		idGenerator: idGenerator,
 		config:      config,
@@ -79,18 +86,19 @@ const (
 	sharedVolumeName = "data"
 )
 
-// In the Standalone mode there is the init container responsible for downloading
-// the latest Trivy DB file from GitHub and storing it to the empty volume shared
-// with main containers. In other words, the init container runs the following
-// Trivy command:
+// In the starboard.Standalone mode there is the init container responsible for
+// downloading the latest Trivy DB file from GitHub and storing it to the empty
+// volume shared with main containers. In other words, the init container runs
+// the following Trivy command:
 //
-// trivy --download-db-only --cache-dir /var/lib/trivy
+//     trivy --download-db-only --cache-dir /var/lib/trivy
 //
 // The number of main containers correspond to the number of containers
-// defined for the scanned workload. What's more, each container runs the Trivy
-// scan command and skips the database update:
+// defined for the scanned workload. Each container runs the Trivy image scan
+// command and skips the database download:
 //
-// trivy --skip-update --cache-dir /var/lib/trivy --format json <container image>
+//     trivy --skip-update --cache-dir /var/lib/trivy \
+//       --format json <container image>
 func (s *scanner) getPodSpecForStandaloneMode(spec corev1.PodSpec, credentials map[string]docker.Auth) (corev1.PodSpec, []*corev1.Secret, error) {
 	var secret *corev1.Secret
 	var secrets []*corev1.Secret
@@ -254,11 +262,13 @@ func (s *scanner) getPodSpecForStandaloneMode(spec corev1.PodSpec, credentials m
 	}, secrets, nil
 }
 
-// In the ClientServer mode the number of containers of the pod created by the scan job
-// equals the number of containers defined for the scanned workload.
-// Each container runs Trivy scan command pointing to a remote Trivy server:
+// In the starboard.ClientServer mode the number of containers of the pod
+// created by the scan job equals the number of containers defined for the
+// scanned workload. Each container runs Trivy image scan command and refers
+// to Trivy server URL returned by Config.GetTrivyServerURL:
 //
-// trivy client --remote http://trivy-server.trivy-server:4954 --format json <container image>
+//     trivy client --remote <server URL> \
+//       --format json <container image ref>
 func (s *scanner) getPodSpecForClientServerMode(spec corev1.PodSpec, credentials map[string]docker.Auth) (corev1.PodSpec, []*corev1.Secret, error) {
 	var secret *corev1.Secret
 	var secrets []*corev1.Secret
@@ -285,6 +295,42 @@ func (s *scanner) getPodSpecForClientServerMode(spec corev1.PodSpec, credentials
 					},
 				},
 			},
+			{
+				Name: "TRIVY_TOKEN_HEADER",
+				ValueFrom: &corev1.EnvVarSource{
+					ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: starboard.SecretName,
+						},
+						Key:      "trivy.serverTokenHeader",
+						Optional: pointer.BoolPtr(true),
+					},
+				},
+			},
+			{
+				Name: "TRIVY_TOKEN",
+				ValueFrom: &corev1.EnvVarSource{
+					SecretKeyRef: &corev1.SecretKeySelector{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: starboard.SecretName,
+						},
+						Key:      "trivy.serverToken",
+						Optional: pointer.BoolPtr(true),
+					},
+				},
+			},
+			{
+				Name: "TRIVY_CUSTOM_HEADERS",
+				ValueFrom: &corev1.EnvVarSource{
+					SecretKeyRef: &corev1.SecretKeySelector{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: starboard.SecretName,
+						},
+						Key:      "trivy.serverCustomHeaders",
+						Optional: pointer.BoolPtr(true),
+					},
+				},
+			},
 		}
 
 		if _, ok := credentials[container.Name]; ok && secret != nil {
@@ -335,6 +381,7 @@ func (s *scanner) getPodSpecForClientServerMode(spec corev1.PodSpec, credentials
 			Resources: defaultResourceRequirements,
 		})
 	}
+
 	return corev1.PodSpec{
 		RestartPolicy:                corev1.RestartPolicyNever,
 		AutomountServiceAccountToken: pointer.BoolPtr(false),
diff --git a/pkg/vulnerabilityreport/plugin.go b/pkg/vulnerabilityreport/plugin.go
index f8da71c..79c68e4 100644
--- a/pkg/vulnerabilityreport/plugin.go
+++ b/pkg/vulnerabilityreport/plugin.go
@@ -17,12 +17,12 @@ type Plugin interface {
 	// descriptor.
 	// The second argument maps container names to Docker registry credentials,
 	// which can be passed to the scanner as environment variables with values
-	// set from secrets.
+	// set from returned secrets.
 	GetScanJobSpec(spec corev1.PodSpec, credentials map[string]docker.Auth) (
 		corev1.PodSpec, []*corev1.Secret, error)
 
 	// ParseVulnerabilityScanResult is a callback to parse and convert logs of
-	// the pod controlled by the scan job to the Starboard model.
+	// the pod controlled by the scan job to v1alpha1.VulnerabilityScanResult.
 	ParseVulnerabilityScanResult(imageRef string, logsReader io.ReadCloser) (
 		v1alpha1.VulnerabilityScanResult, error)
 }


Test patch:
diff --git a/pkg/trivy/scanner_test.go b/pkg/trivy/scanner_test.go
index b2f2b94..1fac1f6 100644
--- a/pkg/trivy/scanner_test.go
+++ b/pkg/trivy/scanner_test.go
@@ -1,1 +1,286 @@
 package trivy_test
+
+import (
+	"testing"
+
+	"github.com/aquasecurity/starboard/pkg/ext"
+	"github.com/aquasecurity/starboard/pkg/starboard"
+	"github.com/aquasecurity/starboard/pkg/trivy"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	"k8s.io/utils/pointer"
+)
+
+func TestScanner_GetScanJobSpec(t *testing.T) {
+
+	testCases := []struct {
+		name string
+
+		config       starboard.ConfigData
+		workloadSpec corev1.PodSpec
+
+		expectedSecrets []corev1.Secret
+		expectedJobSpec corev1.PodSpec
+	}{
+		{
+			name: "With Standalone mode",
+			config: starboard.ConfigData{
+				"trivy.imageRef": "docker.io/aquasec/trivy:0.14.0",
+				"trivy.mode":     string(starboard.Standalone),
+			},
+			workloadSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "nginx",
+						Image: "nginx:1.16",
+					},
+				},
+			},
+			expectedJobSpec: corev1.PodSpec{
+				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.ConfigMapName,
+										},
+										Key:      "trivy.httpProxy",
+										Optional: pointer.BoolPtr(true),
+									},
+								},
+							},
+							{
+								Name: "GITHUB_TOKEN",
+								ValueFrom: &corev1.EnvVarSource{
+									SecretKeyRef: &corev1.SecretKeySelector{
+										LocalObjectReference: corev1.LocalObjectReference{
+											Name: starboard.SecretName,
+										},
+										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.ConfigMapName,
+									},
+									Key:      "trivy.severity",
+									Optional: pointer.BoolPtr(true),
+								},
+							},
+						},
+							{
+								Name: "HTTP_PROXY",
+								ValueFrom: &corev1.EnvVarSource{
+									ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+										LocalObjectReference: corev1.LocalObjectReference{
+											Name: starboard.ConfigMapName,
+										},
+										Key:      "trivy.httpProxy",
+										Optional: pointer.BoolPtr(true),
+									},
+								},
+							},
+						},
+						Command: []string{
+							"trivy",
+						},
+						Args: []string{
+							"--skip-update",
+							"--cache-dir", "/var/lib/trivy",
+							"--quiet",
+							"--format", "json",
+							"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",
+							},
+						},
+					},
+				},
+			},
+		},
+		{
+			name: "With ClientServer mode",
+			config: starboard.ConfigData{
+				"trivy.imageRef":  "docker.io/aquasec/trivy:0.14.0",
+				"trivy.mode":      string(starboard.ClientServer),
+				"trivy.serverURL": "http://trivy.trivy:4954",
+			},
+			workloadSpec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "nginx",
+						Image: "nginx:1.16",
+					},
+				},
+			},
+			expectedJobSpec: corev1.PodSpec{
+				RestartPolicy:                corev1.RestartPolicyNever,
+				AutomountServiceAccountToken: pointer.BoolPtr(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.ConfigMapName,
+										},
+										Key:      "trivy.severity",
+										Optional: pointer.BoolPtr(true),
+									},
+								},
+							},
+							{
+								Name: "TRIVY_TOKEN_HEADER",
+								ValueFrom: &corev1.EnvVarSource{
+									ConfigMapKeyRef: &corev1.ConfigMapKeySelector{
+										LocalObjectReference: corev1.LocalObjectReference{
+											Name: starboard.SecretName,
+										},
+										Key:      "trivy.serverTokenHeader",
+										Optional: pointer.BoolPtr(true),
+									},
+								},
+							},
+							{
+								Name: "TRIVY_TOKEN",
+								ValueFrom: &corev1.EnvVarSource{
+									SecretKeyRef: &corev1.SecretKeySelector{
+										LocalObjectReference: corev1.LocalObjectReference{
+											Name: starboard.SecretName,
+										},
+										Key:      "trivy.serverToken",
+										Optional: pointer.BoolPtr(true),
+									},
+								},
+							},
+							{
+								Name: "TRIVY_CUSTOM_HEADERS",
+								ValueFrom: &corev1.EnvVarSource{
+									SecretKeyRef: &corev1.SecretKeySelector{
+										LocalObjectReference: corev1.LocalObjectReference{
+											Name: starboard.SecretName,
+										},
+										Key:      "trivy.serverCustomHeaders",
+										Optional: pointer.BoolPtr(true),
+									},
+								},
+							},
+						},
+						Command: []string{
+							"trivy",
+						},
+						Args: []string{
+							"client",
+							"--quiet",
+							"--format",
+							"json",
+							"--remote",
+							"http://trivy.trivy:4954",
+							"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"),
+							},
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			jobSpec, secrets, err := trivy.NewScannerPlugin(ext.NewSimpleIDGenerator(), tc.config).
+				GetScanJobSpec(tc.workloadSpec, nil)
+			require.NoError(t, err)
+			assert.Empty(t, secrets)
+			assert.Equal(t, tc.expectedJobSpec, jobSpec)
+		})
+	}
+
+}