Repository: aquasecurity/starboard
Pull request #943 (2022-02-01 09:19:31)

feat: Add option to run Vuln scan job in worload namespace
new option in configmap starboard has added ie. ``vulnerabilityReports.scanJobsInSameNamespace`` to enable running
scan job in the workload namespace.
Once this option is enabled then vulnerability scan jobs will start running in same namespace of workload.
Also it will run with same serviceAccount and imagepullSecret of workload.
This option helps us to schedule scan job with Trivy fs command on any node.

this implements the approach mentioned here: https://github.com/aquasecurity/starboard/pull/917

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/943?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) Report
> Merging [#943](https://codecov.io/gh/aquasecurity/starboard/pull/943?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (8407e6d) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/5672fd4a4d608d9b094802098f3e950ec396ff51?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (5672fd4) will **decrease** coverage by `18.45%`.
> The diff coverage is `42.55%`.

[![Impacted file tree graph](https://codecov.io/gh/aquasecurity/starboard/pull/943/graphs/tree.svg?width=650&height=150&src=pr&token=qqTGKAJ0Wb&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)](https://codecov.io/gh/aquasecurity/starboard/pull/943?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)

```diff
@@             Coverage Diff             @@
##             main     #943       +/-   ##
===========================================
- Coverage   64.70%   46.25%   -18.46%     
===========================================
  Files          59       42       -17     
  Lines        6982     5824     -1158     
===========================================
- Hits         4518     2694     -1824     
- Misses       2004     2964      +960     
+ Partials      460      166      -294     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/943?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) | Coverage Δ | |
|---|---|---|
| [pkg/operator/controller/vulnerabilityreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/943/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvdnVsbmVyYWJpbGl0eXJlcG9ydC5nbw==) | `0.00% <0.00%> (-60.64%)` | :arrow_down: |
| [pkg/operator/etc/config.go](https://codecov.io/gh/aquasecurity/starboard/pull/943/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2V0Yy9jb25maWcuZ28=) | `75.75% <ø> (ø)` | |
| [pkg/plugin/factory.go](https://codecov.io/gh/aquasecurity/starboard/pull/943/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3BsdWdpbi9mYWN0b3J5Lmdv) | `0.00% <0.00%> (-81.14%)` | :arrow_down: |
| [pkg/starboard/plugin.go](https://codecov.io/gh/aquasecurity/starboard/pull/943/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3N0YXJib2FyZC9wbHVnaW4uZ28=) | `50.00% <0.00%> (-3.34%)` | :arrow_down: |
| [pkg/vulnerabilityreport/builder.go](https://codecov.io/gh/aquasecurity/starboard/pull/943/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvYnVpbGRlci5nbw==) | `68.75% <37.50%> (-10.20%)` | :arrow_down: |
| [pkg/plugin/trivy/plugin.go](https://codecov.io/gh/aquasecurity/starboard/pull/943/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3BsdWdpbi90cml2eS9wbHVnaW4uZ28=) | `80.01% <58.82%> (-0.77%)` | :arrow_down: |
| [pkg/operator/controller/limit\_checker.go](https://codecov.io/gh/aquasecurity/starboard/pull/943/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvbGltaXRfY2hlY2tlci5nbw==) | `76.00% <100.00%> (+6.00%)` | :arrow_up: |
| [pkg/kubehunter/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/943/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2t1YmVodW50ZXIvc2Nhbm5lci5nbw==) | `0.00% <0.00%> (-74.49%)` | :arrow_down: |
| [pkg/operator/controller/configauditreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/943/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY29uZmlnYXVkaXRyZXBvcnQuZ28=) | `0.00% <0.00%> (-65.51%)` | :arrow_down: |
| ... and [37 more](https://codecov.io/gh/aquasecurity/starboard/pull/943/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/943?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/943?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity). Last update [5672fd4...8407e6d](https://codecov.io/gh/aquasecurity/starboard/pull/943?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity).

chen-keinan: @deven0t this flow of vulnerability scan in workload namespace is enabled via config parameter, which goes across the flow of creating a vulnerability scan job, is there a way to add a test which validates this flow, so we could protect it ?
deven0t: > @deven0t this flow of vulnerability scan in workload namespace is enabled via config parameter, which goes across the flow of creating a vulnerability scan job, is there a way to add a test which validates this flow, so we could protect it ?

Thanks @chen-keinan for feedback, after discussion with Danial we have decided to change that configuration from env to starboard configmap. I have added some unit test cases to validate this flow. Please review it again.
chen-keinan: @deven0t great job!! LGTM
deven0t: Hi @danielpacak @chen-keinan, I think we missed one point in this proposal and in implementation as well. How plugin config will be available for scan job in the namespace of workload. eg trivy.severity, trivy.githubToken. Currently, the scan job doesn't fail with errors coz these are optional ENV vars. But if we don't provide them in the namespace of workload then the scan job will run without these configs.
danielpacak: > Hi @danielpacak @chen-keinan, I think we missed one point in this proposal and in implementation as well. How plugin config will be available for scan job in the namespace of workload. eg trivy.severity, trivy.githubToken. Currently, the scan job doesn't fail with errors coz these are optional ENV vars. But if we don't provide them in the namespace of workload then the scan job will run without these configs.

That's a very good catch @deven0t We resolved this problem in config audits by copying a snapshot of plugin config to a temporary Secret that's owned by a scan job. And I think we should do the same for Trivy plugin. In general, someone might update starboard-trivy-config by the time the scan pod is actually run and we'll bump into this problem even if we run scan jobs in the starboard-system namespace.
danielpacak: Hey @deven0t As we discussed I think we should mark this PR as draft and implement similar mechanism to store Trivy config snapshot in a temporary Secret instead of using the `starboard-trivy-config` ConfigMap directly.
deven0t: > Hey @deven0t As we discussed I think we should mark this PR as draft and implement similar mechanism to store Trivy config snapshot in a temporary Secret instead of using the `starboard-trivy-config` ConfigMap directly.

Hi @danielpacak , as we discussed this change will not be enabled by default, so it will not impact any existing flow. We can proceed with this PR and later with the new PR we will add a mechanism to create temp secret and use it for scan job purpose. 

Patch:
diff --git a/docs/design/design_vuln_scan_job_in_same_namespace_of_workload.md b/docs/design/design_vuln_scan_job_in_same_namespace_of_workload.md
index b744517..31e01bb 100644
--- a/docs/design/design_vuln_scan_job_in_same_namespace_of_workload.md
+++ b/docs/design/design_vuln_scan_job_in_same_namespace_of_workload.md
@@ -23,9 +23,9 @@ we cannot reuse the same ImagePullSecret available on the workload.
 
 ## Solution
 
-Add an environment variable to operator eg `OPERATOR_VULNERABILITY_SCANNER_SAME_NAMESPACE=true`. This way operator can schedule
-and monitor scan jobs in same namespace where workload is running. And plugins will act accordingly to utilize the service 
-account and ImagePullSecret available on the workload.
+Consider there is an option given to enable running vulnerability scan jobs in the same namespace of workload. Operator
+detects it, so it can schedule and monitor scan jobs in same namespace where workload is running. And plugins will act 
+accordingly to utilize the service account and ImagePullSecret available on the workload.
 
 
 ### Example
diff --git a/docs/settings.md b/docs/settings.md
index 7a96e66..d93b19f 100644
--- a/docs/settings.md
+++ b/docs/settings.md
@@ -52,16 +52,17 @@ EOF
 The following table lists available settings with their default values. Check plugins' documentation to see
 configuration settings for common use cases. For example, switch Trivy from [Standalone] to [ClientServer] mode.
 
-| CONFIGMAP KEY                  | DEFAULT                               | DESCRIPTION |
-| ------------------------------ | ------------------------------------- | ----------- |
-| `vulnerabilityReports.scanner` | `Trivy`                               | The name of the plugin that generates vulnerability reports. Either `Trivy` or `Aqua`. |
-| `configAuditReports.scanner`   | `Polaris`                             | The name of the plugin that generates config audit reports. Either `Polaris` or `Conftest`. |
-| `scanJob.tolerations`          | N/A                                   | JSON representation of the [tolerations] to be applied to the scanner pods so that they can run on nodes with matching taints. Example: `'[{"key":"key1", "operator":"Equal", "value":"value1", "effect":"NoSchedule"}]'` |
-| `scanJob.annotations`          | N/A                                   | One-line comma-separated representation of the annotations which the user wants the scanner pods to be annotated with. Example: `foo=bar,env=stage` will annotate the scanner pods with the annotations `foo: bar` and `env: stage` |
-| `scanJob.templateLabel`        | N/A                                   | One-line comma-separated representation of the template labels which the user wants the scanner pods to be labeled with. Example: `foo=bar,env=stage` will labeled the scanner pods with the labels `foo: bar` and `env: stage` |
-| `kube-bench.imageRef`          | `docker.io/aquasec/kube-bench:v0.6.5`  | kube-bench image reference |
-| `kube-hunter.imageRef`         | `docker.io/aquasec/kube-hunter:0.6.3` | kube-hunter image reference |
-| `kube-hunter.quick`            | `"false"`                             | Whether to use kube-hunter's "quick" scanning mode (subnet 24). Set to `"true"` to enable. |
+| CONFIGMAP KEY                                 | DEFAULT                               | DESCRIPTION |
+| --------------------------------------------- | ------------------------------------- | ----------- |
+| `vulnerabilityReports.scanner`                | `Trivy`                               | The name of the plugin that generates vulnerability reports. Either `Trivy` or `Aqua`. |
+| `vulnerabilityReports.scanJobsInSameNamespace`| `"false"`                             | Whether to run vulnerability scan jobs in same namespace of workload. Set `"true"` to enable. |
+| `configAuditReports.scanner`                  | `Polaris`                             | The name of the plugin that generates config audit reports. Either `Polaris` or `Conftest`. |
+| `scanJob.tolerations`                         | N/A                                   | JSON representation of the [tolerations] to be applied to the scanner pods so that they can run on nodes with matching taints. Example: `'[{"key":"key1", "operator":"Equal", "value":"value1", "effect":"NoSchedule"}]'` |
+| `scanJob.annotations`                         | N/A                                   | One-line comma-separated representation of the annotations which the user wants the scanner pods to be annotated with. Example: `foo=bar,env=stage` will annotate the scanner pods with the annotations `foo: bar` and `env: stage` |
+| `scanJob.templateLabel`                       | N/A                                   | One-line comma-separated representation of the template labels which the user wants the scanner pods to be labeled with. Example: `foo=bar,env=stage` will labeled the scanner pods with the labels `foo: bar` and `env: stage` |
+| `kube-bench.imageRef`                         | `docker.io/aquasec/kube-bench:v0.6.5`  | kube-bench image reference |
+| `kube-hunter.imageRef`                        | `docker.io/aquasec/kube-hunter:0.6.3` | kube-hunter image reference |
+| `kube-hunter.quick`                           | `"false"`                             | Whether to use kube-hunter's "quick" scanning mode (subnet 24). Set to `"true"` to enable. |
 
 !!! tip
     You can find it handy to delete a configuration key, which was not created by default by the `starboard init`
diff --git a/pkg/operator/controller/limit_checker.go b/pkg/operator/controller/limit_checker.go
index 98f7aa8..89707d3 100644
--- a/pkg/operator/controller/limit_checker.go
+++ b/pkg/operator/controller/limit_checker.go
@@ -13,16 +13,18 @@ type LimitChecker interface {
 	Check(ctx context.Context) (bool, int, error)
 }
 
-func NewLimitChecker(config etc.Config, client client.Client) LimitChecker {
+func NewLimitChecker(config etc.Config, client client.Client, starboardConfig starboard.ConfigData) LimitChecker {
 	return &checker{
-		config: config,
-		client: client,
+		config:          config,
+		client:          client,
+		starboardConfig: starboardConfig,
 	}
 }
 
 type checker struct {
-	config etc.Config
-	client client.Client
+	config          etc.Config
+	client          client.Client
+	starboardConfig starboard.ConfigData
 }
 
 func (c *checker) Check(ctx context.Context) (bool, int, error) {
@@ -36,9 +38,14 @@ func (c *checker) Check(ctx context.Context) (bool, int, error) {
 
 func (c *checker) countScanJobs(ctx context.Context) (int, error) {
 	var scanJobs batchv1.JobList
-	err := c.client.List(ctx, &scanJobs, client.MatchingLabels{
+	listOptions := []client.ListOption{client.MatchingLabels{
 		starboard.LabelK8SAppManagedBy: starboard.AppStarboard,
-	}, client.InNamespace(c.config.Namespace))
+	}}
+	if !c.starboardConfig.VulnerabilityScanJobsInSameNamespace() {
+		// scan jobs are running in only starboard operator namespace
+		listOptions = append(listOptions, client.InNamespace(c.config.Namespace))
+	}
+	err := c.client.List(ctx, &scanJobs, listOptions...)
 	if err != nil {
 		return 0, err
 	}
diff --git a/pkg/operator/controller/vulnerabilityreport.go b/pkg/operator/controller/vulnerabilityreport.go
index 2ff14a7..a4e0014 100644
--- a/pkg/operator/controller/vulnerabilityreport.go
+++ b/pkg/operator/controller/vulnerabilityreport.go
@@ -23,6 +23,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/builder"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+	"sigs.k8s.io/controller-runtime/pkg/predicate"
 	"sigs.k8s.io/controller-runtime/pkg/reconcile"
 )
 
@@ -73,13 +74,13 @@ func (r *VulnerabilityReportReconciler) SetupWithManager(mgr ctrl.Manager) error
 			return err
 		}
 	}
+	var predicates []predicate.Predicate
+	if !r.ConfigData.VulnerabilityScanJobsInSameNamespace() {
+		predicates = append(predicates, InNamespace(r.Config.Namespace))
+	}
+	predicates = append(predicates, ManagedByStarboardOperator, IsVulnerabilityReportScan, JobHasAnyCondition)
 	return ctrl.NewControllerManagedBy(mgr).
-		For(&batchv1.Job{}, builder.WithPredicates(
-			InNamespace(r.Config.Namespace),
-			ManagedByStarboardOperator,
-			IsVulnerabilityReportScan,
-			JobHasAnyCondition,
-		)).
+		For(&batchv1.Job{}, builder.WithPredicates(predicates...)).
 		Complete(r.reconcileJobs())
 }
 
@@ -260,7 +261,6 @@ func (r *VulnerabilityReportReconciler) submitScanJob(ctx context.Context, owner
 	}
 
 	for _, secret := range secrets {
-		secret.Namespace = r.PluginContext.GetNamespace()
 		err = r.Client.Create(ctx, secret)
 		if err != nil {
 			if k8sapierror.IsAlreadyExists(err) {
diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go
index 6d78215..2864793 100644
--- a/pkg/operator/operator.go
+++ b/pkg/operator/operator.go
@@ -124,7 +124,7 @@ func Start(ctx context.Context, buildInfo starboard.BuildInfo, operatorConfig et
 	}
 
 	objectResolver := kube.ObjectResolver{Client: mgr.GetClient()}
-	limitChecker := controller.NewLimitChecker(operatorConfig, mgr.GetClient())
+	limitChecker := controller.NewLimitChecker(operatorConfig, mgr.GetClient(), starboardConfig)
 	logsReader := kube.NewLogsReader(kubeClientset)
 	secretsReader := kube.NewSecretsReader(mgr.GetClient())
 
diff --git a/pkg/plugin/factory.go b/pkg/plugin/factory.go
index 2087dbb..ca8c8e9 100644
--- a/pkg/plugin/factory.go
+++ b/pkg/plugin/factory.go
@@ -75,6 +75,7 @@ func (r *Resolver) GetVulnerabilityPlugin() (vulnerabilityreport.Plugin, starboa
 		WithNamespace(r.namespace).
 		WithServiceAccountName(r.serviceAccountName).
 		WithClient(r.client).
+		WithStarboardConfig(r.config).
 		Get()
 
 	switch scanner {
diff --git a/pkg/plugin/trivy/plugin.go b/pkg/plugin/trivy/plugin.go
index dcf66dc..b2d55de 100644
--- a/pkg/plugin/trivy/plugin.go
+++ b/pkg/plugin/trivy/plugin.go
@@ -956,10 +956,17 @@ func (p *plugin) getPodSpecForStandaloneFSMode(ctx starboard.PluginContext, conf
 	if err != nil {
 		return corev1.PodSpec{}, nil, err
 	}
-	// get nodeName from running pods.
-	spec.NodeName, err = p.objectResolver.GetNodeName(context.Background(), workload)
-	if err != nil {
-		return corev1.PodSpec{}, nil, err
+	pullPolicy := corev1.PullIfNotPresent
+	// nodeName to schedule scan job explicitly on specific node.
+	var nodeName string
+	if !ctx.GetStarboardConfig().VulnerabilityScanJobsInSameNamespace() {
+		// get nodeName from running pods.
+		nodeName, err = p.objectResolver.GetNodeName(context.Background(), workload)
+		if err != nil {
+			return corev1.PodSpec{}, nil, fmt.Errorf("failed resolving node name for workload %q: %w",
+				workload.GetNamespace()+"/"+workload.GetName(), err)
+		}
+		pullPolicy = corev1.PullNever
 	}
 
 	trivyImageRef, err := config.GetImageRef()
@@ -1103,7 +1110,7 @@ func (p *plugin) getPodSpecForStandaloneFSMode(ctx starboard.PluginContext, conf
 		containers = append(containers, corev1.Container{
 			Name:                     c.Name,
 			Image:                    c.Image,
-			ImagePullPolicy:          corev1.PullNever,
+			ImagePullPolicy:          pullPolicy,
 			TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
 			Env:                      env,
 			Command: []string{
@@ -1135,7 +1142,7 @@ func (p *plugin) getPodSpecForStandaloneFSMode(ctx starboard.PluginContext, conf
 		})
 	}
 
-	return corev1.PodSpec{
+	podSpec := corev1.PodSpec{
 		Affinity:                     starboard.LinuxNodeAffinity(),
 		RestartPolicy:                corev1.RestartPolicyNever,
 		ServiceAccountName:           ctx.GetServiceAccountName(),
@@ -1144,8 +1151,14 @@ func (p *plugin) getPodSpecForStandaloneFSMode(ctx starboard.PluginContext, conf
 		InitContainers:               []corev1.Container{initContainerCopyBinary, initContainerDB},
 		Containers:                   containers,
 		SecurityContext:              &corev1.PodSecurityContext{},
-		NodeName:                     spec.NodeName,
-	}, secrets, nil
+	}
+
+	if !ctx.GetStarboardConfig().VulnerabilityScanJobsInSameNamespace() {
+		// schedule scan job explicitly on specific node.
+		podSpec.NodeName = nodeName
+	}
+
+	return podSpec, secrets, nil
 }
 
 func (p *plugin) appendTrivyInsecureEnv(config Config, image string, env []corev1.EnvVar) ([]corev1.EnvVar, error) {
diff --git a/pkg/starboard/config.go b/pkg/starboard/config.go
index 3d858f8..f0148d1 100644
--- a/pkg/starboard/config.go
+++ b/pkg/starboard/config.go
@@ -46,14 +46,15 @@ type BuildInfo struct {
 type Scanner string
 
 const (
-	keyVulnerabilityReportsScanner = "vulnerabilityReports.scanner"
-	keyConfigAuditReportsScanner   = "configAuditReports.scanner"
-	keyKubeBenchImageRef           = "kube-bench.imageRef"
-	keyKubeHunterImageRef          = "kube-hunter.imageRef"
-	keyKubeHunterQuick             = "kube-hunter.quick"
-	keyScanJobTolerations          = "scanJob.tolerations"
-	keyScanJobAnnotations          = "scanJob.annotations"
-	keyScanJobPodTemplateLabels    = "scanJob.podTemplateLabels"
+	keyVulnerabilityReportsScanner       = "vulnerabilityReports.scanner"
+	KeyVulnerabilityScansInSameNamespace = "vulnerabilityReports.scanJobsInSameNamespace"
+	keyConfigAuditReportsScanner         = "configAuditReports.scanner"
+	keyKubeBenchImageRef                 = "kube-bench.imageRef"
+	keyKubeHunterImageRef                = "kube-hunter.imageRef"
+	keyKubeHunterQuick                   = "kube-hunter.quick"
+	keyScanJobTolerations                = "scanJob.tolerations"
+	keyScanJobAnnotations                = "scanJob.annotations"
+	keyScanJobPodTemplateLabels          = "scanJob.podTemplateLabels"
 )
 
 // ConfigData holds Starboard configuration settings as a set of key-value
@@ -88,6 +89,15 @@ func (c ConfigData) GetVulnerabilityReportsScanner() (Scanner, error) {
 	return Scanner(value), nil
 }
 
+func (c ConfigData) VulnerabilityScanJobsInSameNamespace() bool {
+	var ok bool
+	var value string
+	if value, ok = c[KeyVulnerabilityScansInSameNamespace]; !ok {
+		return false
+	}
+	return value == "true"
+}
+
 func (c ConfigData) GetConfigAuditReportsScanner() (Scanner, error) {
 	var ok bool
 	var value string
diff --git a/pkg/starboard/plugin.go b/pkg/starboard/plugin.go
index 7cd6843..4729de9 100644
--- a/pkg/starboard/plugin.go
+++ b/pkg/starboard/plugin.go
@@ -44,6 +44,8 @@ type PluginContext interface {
 	// GetServiceAccountName return the name of the K8s Service Account used to run workloads
 	// created by Starboard.
 	GetServiceAccountName() string
+	// GetStarboardConfig returns starboard configuration.
+	GetStarboardConfig() ConfigData
 }
 
 // GetPluginConfigMapName returns the name of a ConfigMap used to configure a plugin
@@ -58,6 +60,7 @@ type pluginContext struct {
 	client             client.Client
 	namespace          string
 	serviceAccountName string
+	starboardConfig    ConfigData
 }
 
 func (p *pluginContext) GetName() string {
@@ -121,6 +124,10 @@ func (p *pluginContext) GetServiceAccountName() string {
 	return p.serviceAccountName
 }
 
+func (p *pluginContext) GetStarboardConfig() ConfigData {
+	return p.starboardConfig
+}
+
 type PluginContextBuilder struct {
 	ctx *pluginContext
 }
@@ -151,6 +158,11 @@ func (b *PluginContextBuilder) WithServiceAccountName(name string) *PluginContex
 	return b
 }
 
+func (b *PluginContextBuilder) WithStarboardConfig(config ConfigData) *PluginContextBuilder {
+	b.ctx.starboardConfig = config
+	return b
+}
+
 func (b *PluginContextBuilder) Get() PluginContext {
 	return b.ctx
 }
diff --git a/pkg/vulnerabilityreport/builder.go b/pkg/vulnerabilityreport/builder.go
index 3f9f526..e294913 100644
--- a/pkg/vulnerabilityreport/builder.go
+++ b/pkg/vulnerabilityreport/builder.go
@@ -129,6 +129,11 @@ func (s *ScanJobBuilder) Get() (*batchv1.Job, []*corev1.Secret, error) {
 			},
 		},
 	}
+	// secrets will be created with scan jobs in same namespace where scan job will run
+	for i, _ := range secrets {
+		secrets[i].Namespace = s.pluginContext.GetNamespace()
+	}
+	s.updateScanJobForWorkloadNamespace(job, spec, secrets)
 
 	err = kube.ObjectToObjectMeta(s.object, &job.ObjectMeta)
 	if err != nil {
@@ -143,6 +148,23 @@ func (s *ScanJobBuilder) Get() (*batchv1.Job, []*corev1.Secret, error) {
 	return job, secrets, nil
 }
 
+// When run scan job in workload namespace is enabled then this method will update scanjob spec with these changes
+// - namespace same as workload
+// - service account same as workload service account
+// - ImagePullSecret same as workload imagePullSecret
+func (s *ScanJobBuilder) updateScanJobForWorkloadNamespace(job *batchv1.Job, podspec corev1.PodSpec, secrets []*corev1.Secret) {
+	operatorConfig := s.pluginContext.GetStarboardConfig()
+	if !operatorConfig.VulnerabilityScanJobsInSameNamespace() {
+		return
+	}
+	job.Namespace = s.object.GetNamespace()
+	job.Spec.Template.Spec.ServiceAccountName = podspec.ServiceAccountName
+	job.Spec.Template.Spec.ImagePullSecrets = podspec.ImagePullSecrets
+	for i, _ := range secrets {
+		secrets[i].Namespace = s.object.GetNamespace()
+	}
+}
+
 func GetScanJobName(obj client.Object) string {
 	return fmt.Sprintf("scan-vulnerabilityreport-%s", kube.ComputeHash(kube.ObjectRef{
 		Kind:      kube.Kind(obj.GetObjectKind().GroupVersionKind().Kind),


Test patch:
diff --git a/pkg/operator/controller/limit_checker_test.go b/pkg/operator/controller/limit_checker_test.go
index 3a72bbc..2b3dba0 100644
--- a/pkg/operator/controller/limit_checker_test.go
+++ b/pkg/operator/controller/limit_checker_test.go
@@ -20,6 +20,7 @@ var _ = Describe("LimitChecker", func() {
 		Namespace:               "starboard-operator",
 		ConcurrentScanJobsLimit: 2,
 	}
+	defaultStarboardConfig := starboard.GetDefaultConfig()
 
 	Context("When there are more jobs than limit", func() {
 
@@ -53,7 +54,7 @@ var _ = Describe("LimitChecker", func() {
 				}},
 			).Build()
 
-			instance := controller.NewLimitChecker(config, client)
+			instance := controller.NewLimitChecker(config, client, defaultStarboardConfig)
 			limitExceeded, jobsCount, err := instance.Check(context.TODO())
 			Expect(err).ToNot(HaveOccurred())
 			Expect(limitExceeded).To(BeTrue())
@@ -79,7 +80,7 @@ var _ = Describe("LimitChecker", func() {
 				}},
 			).Build()
 
-			instance := controller.NewLimitChecker(config, client)
+			instance := controller.NewLimitChecker(config, client, defaultStarboardConfig)
 			limitExceeded, jobsCount, err := instance.Check(context.TODO())
 			Expect(err).ToNot(HaveOccurred())
 			Expect(limitExceeded).To(BeFalse())
@@ -87,4 +88,46 @@ var _ = Describe("LimitChecker", func() {
 		})
 
 	})
+
+	Context("When there are more jobs than limit running in different namespace", func() {
+
+		It("Should return true", func() {
+			client := fake.NewClientBuilder().WithScheme(starboard.NewScheme()).WithObjects(
+				&batchv1.Job{ObjectMeta: metav1.ObjectMeta{
+					Name:      "logs-exporter",
+					Namespace: "starboard-operator",
+				}},
+				&batchv1.Job{ObjectMeta: metav1.ObjectMeta{
+					Name:      "scan-vulnerabilityreport-hash1",
+					Namespace: "default",
+					Labels: map[string]string{
+						starboard.LabelK8SAppManagedBy: starboard.AppStarboard,
+					},
+				}},
+				&batchv1.Job{ObjectMeta: metav1.ObjectMeta{
+					Name:      "scan-vulnerabilityreport-hash2",
+					Namespace: "prod",
+					Labels: map[string]string{
+						starboard.LabelK8SAppManagedBy: starboard.AppStarboard,
+					},
+				}},
+				&batchv1.Job{ObjectMeta: metav1.ObjectMeta{
+					Name:      "scan-configauditreport-hash3",
+					Namespace: "stage",
+					Labels: map[string]string{
+						starboard.LabelK8SAppManagedBy: starboard.AppStarboard,
+					},
+				}},
+			).Build()
+			starboardConfig := defaultStarboardConfig
+			starboardConfig[starboard.KeyVulnerabilityScansInSameNamespace] = "true"
+			instance := controller.NewLimitChecker(config, client, starboardConfig)
+			limitExceeded, jobsCount, err := instance.Check(context.TODO())
+			Expect(err).ToNot(HaveOccurred())
+			Expect(limitExceeded).To(BeTrue())
+			Expect(jobsCount).To(Equal(3))
+		})
+
+	})
+
 })
diff --git a/pkg/plugin/trivy/plugin_test.go b/pkg/plugin/trivy/plugin_test.go
index bf32d5c..143442f 100644
--- a/pkg/plugin/trivy/plugin_test.go
+++ b/pkg/plugin/trivy/plugin_test.go
@@ -2961,6 +2961,320 @@ CVE-2019-1543`,
 		})
 	}
 
+	testCases = []struct {
+		name            string
+		config          map[string]string
+		workloadSpec    client.Object
+		expectedSecrets []corev1.Secret
+		expectedJobSpec corev1.PodSpec
+	}{{
+		name: "Trivy fs scan command in Standalone mode",
+		config: map[string]string{
+			"trivy.imageRef":                  "docker.io/aquasec/trivy:0.22.0",
+			"trivy.mode":                      string(trivy.Standalone),
+			"trivy.command":                   string(trivy.Filesystem),
+			"trivy.resources.requests.cpu":    "100m",
+			"trivy.resources.requests.memory": "100M",
+			"trivy.resources.limits.cpu":      "500m",
+			"trivy.resources.limits.memory":   "500M",
+		},
+		workloadSpec: &corev1.Pod{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "Pod",
+				APIVersion: "v1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "nginx",
+				Namespace: "prod-ns",
+			},
+			Spec: corev1.PodSpec{
+				Containers: []corev1.Container{
+					{
+						Name:  "nginx",
+						Image: "nginx:1.9.1",
+					},
+				},
+				NodeName:           "kind-control-pane",
+				ServiceAccountName: "nginx-sa",
+			}},
+		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.22.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.22.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.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_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),
+						RunAsUser:              pointer.Int64(0),
+					},
+				},
+			},
+			SecurityContext: &corev1.PodSecurityContext{},
+		},
+	}}
+	// Test cases when starboard is enabled with option to run job in the namespace of workload
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			fakeclient := fake.NewClientBuilder().WithObjects(
+				&corev1.ConfigMap{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:      "starboard-trivy-config",
+						Namespace: "starboard-ns",
+					},
+					Data: tc.config,
+				},
+			).Build()
+			pluginContext := starboard.NewPluginContext().
+				WithName(trivy.Plugin).
+				WithNamespace("starboard-ns").
+				WithServiceAccountName("starboard-sa").
+				WithClient(fakeclient).
+				WithStarboardConfig(map[string]string{starboard.KeyVulnerabilityScansInSameNamespace: "true"}).
+				Get()
+			instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator(), fakeclient)
+			jobSpec, secrets, err := instance.GetScanJobSpec(pluginContext, tc.workloadSpec, nil)
+			require.NoError(t, err)
+			assert.Empty(t, secrets)
+			assert.Equal(t, tc.expectedJobSpec, jobSpec)
+		})
+	}
 }
 
 var (
diff --git a/pkg/vulnerabilityreport/builder_test.go b/pkg/vulnerabilityreport/builder_test.go
index 2979579..47cd000 100644
--- a/pkg/vulnerabilityreport/builder_test.go
+++ b/pkg/vulnerabilityreport/builder_test.go
@@ -64,76 +64,154 @@ func TestReportBuilder(t *testing.T) {
 }
 
 func TestScanJobBuilder(t *testing.T) {
-	g := gomega.NewGomegaWithT(t)
-	job, _, err := vulnerabilityreport.NewScanJobBuilder().
-		WithPlugin(&testPlugin{}).
-		WithPluginContext(starboard.NewPluginContext().
-			WithName("test-plugin").
-			WithNamespace("starboard-ns").
-			WithServiceAccountName("starboard-sa").
-			Get()).
-		WithTimeout(3 * time.Second).
-		WithObject(&appsv1.ReplicaSet{
-			TypeMeta: metav1.TypeMeta{
-				Kind:       "ReplicaSet",
-				APIVersion: "apps/v1",
-			},
+	t.Run("Should get scan job with labels", func(t *testing.T) {
+		g := gomega.NewGomegaWithT(t)
+		job, _, err := vulnerabilityreport.NewScanJobBuilder().
+			WithPlugin(&testPlugin{}).
+			WithPluginContext(starboard.NewPluginContext().
+				WithName("test-plugin").
+				WithNamespace("starboard-ns").
+				WithServiceAccountName("starboard-sa").
+				Get()).
+			WithTimeout(3 * time.Second).
+			WithObject(&appsv1.ReplicaSet{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "ReplicaSet",
+					APIVersion: "apps/v1",
+				},
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "nginx-6799fc88d8",
+					Namespace: "prod-ns",
+				},
+				Spec: appsv1.ReplicaSetSpec{
+					Template: corev1.PodTemplateSpec{
+						Spec: corev1.PodSpec{
+							Containers: []corev1.Container{
+								{
+									Name:  "nginx",
+									Image: "nginx:1.16",
+								},
+							},
+						},
+					},
+					Selector: &metav1.LabelSelector{},
+				},
+			}).
+			Get()
+		g.Expect(err).ToNot(gomega.HaveOccurred())
+		g.Expect(job).ToNot(gomega.BeNil())
+		g.Expect(job).To(gomega.Equal(&batchv1.Job{
 			ObjectMeta: metav1.ObjectMeta{
-				Name:      "nginx-6799fc88d8",
-				Namespace: "prod-ns",
+				Name:      "scan-vulnerabilityreport-64d65c457",
+				Namespace: "starboard-ns",
+				Labels: map[string]string{
+					starboard.LabelK8SAppManagedBy:            "starboard",
+					starboard.LabelVulnerabilityReportScanner: "test-plugin",
+					starboard.LabelResourceKind:               "ReplicaSet",
+					starboard.LabelResourceName:               "nginx-6799fc88d8",
+					starboard.LabelResourceNamespace:          "prod-ns",
+					starboard.LabelResourceSpecHash:           "7d6c657bf7",
+				},
+				Annotations: map[string]string{
+					starboard.AnnotationContainerImages: `{"nginx":"nginx:1.16"}`,
+				},
 			},
-			Spec: appsv1.ReplicaSetSpec{
+			Spec: batchv1.JobSpec{
+				BackoffLimit:          pointer.Int32Ptr(0),
+				Completions:           pointer.Int32Ptr(1),
+				ActiveDeadlineSeconds: pointer.Int64Ptr(3),
 				Template: corev1.PodTemplateSpec{
-					Spec: corev1.PodSpec{
-						Containers: []corev1.Container{
-							{
-								Name:  "nginx",
-								Image: "nginx:1.16",
-							},
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{
+							starboard.LabelK8SAppManagedBy:            "starboard",
+							starboard.LabelVulnerabilityReportScanner: "test-plugin",
+							starboard.LabelResourceKind:               "ReplicaSet",
+							starboard.LabelResourceName:               "nginx-6799fc88d8",
+							starboard.LabelResourceNamespace:          "prod-ns",
+							starboard.LabelResourceSpecHash:           "7d6c657bf7",
 						},
 					},
+					Spec: corev1.PodSpec{},
 				},
-				Selector: &metav1.LabelSelector{},
-			},
-		}).
-		Get()
-	g.Expect(err).ToNot(gomega.HaveOccurred())
-	g.Expect(job).ToNot(gomega.BeNil())
-	g.Expect(job).To(gomega.Equal(&batchv1.Job{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      "scan-vulnerabilityreport-64d65c457",
-			Namespace: "starboard-ns",
-			Labels: map[string]string{
-				starboard.LabelK8SAppManagedBy:            "starboard",
-				starboard.LabelVulnerabilityReportScanner: "test-plugin",
-				starboard.LabelResourceKind:               "ReplicaSet",
-				starboard.LabelResourceName:               "nginx-6799fc88d8",
-				starboard.LabelResourceNamespace:          "prod-ns",
-				starboard.LabelResourceSpecHash:           "7d6c657bf7",
-			},
-			Annotations: map[string]string{
-				starboard.AnnotationContainerImages: `{"nginx":"nginx:1.16"}`,
 			},
-		},
-		Spec: batchv1.JobSpec{
-			BackoffLimit:          pointer.Int32Ptr(0),
-			Completions:           pointer.Int32Ptr(1),
-			ActiveDeadlineSeconds: pointer.Int64Ptr(3),
-			Template: corev1.PodTemplateSpec{
+		}))
+	})
+
+	t.Run("Should get scan job running in workload namespace", func(t *testing.T) {
+		g := gomega.NewGomegaWithT(t)
+		job, _, err := vulnerabilityreport.NewScanJobBuilder().
+			WithPlugin(&testPlugin{}).
+			WithPluginContext(starboard.NewPluginContext().
+				WithName("test-plugin").
+				WithNamespace("starboard-ns").
+				WithServiceAccountName("starboard-sa").
+				WithStarboardConfig(starboard.ConfigData{
+					starboard.KeyVulnerabilityScansInSameNamespace: "true"},
+				).
+				Get()).
+			WithTimeout(3 * time.Second).
+			WithObject(&appsv1.ReplicaSet{
+				TypeMeta: metav1.TypeMeta{
+					Kind:       "ReplicaSet",
+					APIVersion: "apps/v1",
+				},
 				ObjectMeta: metav1.ObjectMeta{
-					Labels: map[string]string{
-						starboard.LabelK8SAppManagedBy:            "starboard",
-						starboard.LabelVulnerabilityReportScanner: "test-plugin",
-						starboard.LabelResourceKind:               "ReplicaSet",
-						starboard.LabelResourceName:               "nginx-6799fc88d8",
-						starboard.LabelResourceNamespace:          "prod-ns",
-						starboard.LabelResourceSpecHash:           "7d6c657bf7",
+					Name:      "nginx-6799fc88d8",
+					Namespace: "prod-ns",
+				},
+				Spec: appsv1.ReplicaSetSpec{
+					Template: corev1.PodTemplateSpec{
+						Spec: corev1.PodSpec{
+							Containers: []corev1.Container{
+								{
+									Name:  "nginx",
+									Image: "nginx:1.16",
+								},
+							},
+						},
+					},
+					Selector: &metav1.LabelSelector{},
+				},
+			}).
+			Get()
+		g.Expect(err).ToNot(gomega.HaveOccurred())
+		g.Expect(job).ToNot(gomega.BeNil())
+		g.Expect(job).To(gomega.Equal(&batchv1.Job{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "scan-vulnerabilityreport-64d65c457",
+				Namespace: "prod-ns",
+				Labels: map[string]string{
+					starboard.LabelK8SAppManagedBy:            "starboard",
+					starboard.LabelVulnerabilityReportScanner: "test-plugin",
+					starboard.LabelResourceKind:               "ReplicaSet",
+					starboard.LabelResourceName:               "nginx-6799fc88d8",
+					starboard.LabelResourceNamespace:          "prod-ns",
+					starboard.LabelResourceSpecHash:           "7d6c657bf7",
+				},
+				Annotations: map[string]string{
+					starboard.AnnotationContainerImages: `{"nginx":"nginx:1.16"}`,
+				},
+			},
+			Spec: batchv1.JobSpec{
+				BackoffLimit:          pointer.Int32Ptr(0),
+				Completions:           pointer.Int32Ptr(1),
+				ActiveDeadlineSeconds: pointer.Int64Ptr(3),
+				Template: corev1.PodTemplateSpec{
+					ObjectMeta: metav1.ObjectMeta{
+						Labels: map[string]string{
+							starboard.LabelK8SAppManagedBy:            "starboard",
+							starboard.LabelVulnerabilityReportScanner: "test-plugin",
+							starboard.LabelResourceKind:               "ReplicaSet",
+							starboard.LabelResourceName:               "nginx-6799fc88d8",
+							starboard.LabelResourceNamespace:          "prod-ns",
+							starboard.LabelResourceSpecHash:           "7d6c657bf7",
+						},
 					},
+					Spec: corev1.PodSpec{},
 				},
-				Spec: corev1.PodSpec{},
 			},
-		},
-	}))
+		}))
+	})
 }
 
 type testPlugin struct {