Repository: aquasecurity/starboard
Pull request #872 (2021-12-23 07:19:23)

feat: Vulnerability scan using Trivy fs scan command
``trivy.command: fs`` will change the trivy scan option to pick up vulnerability scans
using trivy fs scan command

This is the implementation of first approach suggested here #830

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/872?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) Report
> Merging [#872](https://codecov.io/gh/aquasecurity/starboard/pull/872?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (15f5ec7) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/b8d85683bc4e347fb77c648218aba9cdbeb9fbf0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (b8d8568) will **decrease** coverage by `18.81%`.
> The diff coverage is `60.81%`.

[![Impacted file tree graph](https://codecov.io/gh/aquasecurity/starboard/pull/872/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/872?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)

```diff
@@             Coverage Diff             @@
##             main     #872       +/-   ##
===========================================
- Coverage   65.58%   46.77%   -18.82%     
===========================================
  Files          58       41       -17     
  Lines        6460     5567      -893     
===========================================
- Hits         4237     2604     -1633     
- Misses       1796     2808     +1012     
+ Partials      427      155      -272     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/872?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) | Coverage Δ | |
|---|---|---|
| [pkg/kube/object.go](https://codecov.io/gh/aquasecurity/starboard/pull/872/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2t1YmUvb2JqZWN0Lmdv) | `42.05% <0.00%> (-17.59%)` | :arrow_down: |
| [pkg/operator/controller/vulnerabilityreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/872/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%> (-63.64%)` | :arrow_down: |
| [pkg/vulnerabilityreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/872/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvc2Nhbm5lci5nbw==) | `0.00% <0.00%> (-60.40%)` | :arrow_down: |
| [pkg/plugin/trivy/plugin.go](https://codecov.io/gh/aquasecurity/starboard/pull/872/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3BsdWdpbi90cml2eS9wbHVnaW4uZ28=) | `80.89% <76.65%> (-1.90%)` | :arrow_down: |
| [pkg/vulnerabilityreport/builder.go](https://codecov.io/gh/aquasecurity/starboard/pull/872/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvYnVpbGRlci5nbw==) | `77.85% <100.00%> (-6.48%)` | :arrow_down: |
| [pkg/plugin/factory.go](https://codecov.io/gh/aquasecurity/starboard/pull/872/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/kubehunter/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/872/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%> (-75.72%)` | :arrow_down: |
| [pkg/operator/controller/configauditreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/872/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%> (-64.42%)` | :arrow_down: |
| [pkg/kube/runnable\_job.go](https://codecov.io/gh/aquasecurity/starboard/pull/872/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2t1YmUvcnVubmFibGVfam9iLmdv) | `0.00% <0.00%> (-63.73%)` | :arrow_down: |
| ... and [35 more](https://codecov.io/gh/aquasecurity/starboard/pull/872/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/872?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/872?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity). Last update [b8d8568...15f5ec7](https://codecov.io/gh/aquasecurity/starboard/pull/872?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).


Patch:
diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml
index 9e7663d..d76ca94 100644
--- a/.github/workflows/build.yaml
+++ b/.github/workflows/build.yaml
@@ -272,3 +272,24 @@ jobs:
         with:
           version: v1.1.0
           args: release --snapshot --skip-publish --rm-dist
+      - name: Scan Starboard CLI image for vulnerabilities
+        uses: aquasecurity/trivy-action@master
+        with:
+          image-ref: 'docker.io/aquasec/starboard:${{ github.sha }}'
+          exit-code: '1'
+          ignore-unfixed: true
+          severity: 'CRITICAL,HIGH'
+      - name: Scan Starboard Operator image for vulnerabilities
+        uses: aquasecurity/trivy-action@master
+        with:
+          image-ref: 'docker.io/aquasec/starboard-operator:${{ github.sha }}'
+          exit-code: '1'
+          ignore-unfixed: true
+          severity: 'CRITICAL,HIGH'
+      - name: Scan Starboard Scanner Aqua image for vulnerabilities
+        uses: aquasecurity/trivy-action@master
+        with:
+          image-ref: 'docker.io/aquasec/starboard-scanner-aqua:${{ github.sha }}'
+          exit-code: '1'
+          ignore-unfixed: true
+          severity: 'CRITICAL,HIGH'
diff --git a/deploy/crd/ciskubebenchreports.crd.yaml b/deploy/crd/ciskubebenchreports.crd.yaml
index 9ea6b7e..36dfcb5 100644
--- a/deploy/crd/ciskubebenchreports.crd.yaml
+++ b/deploy/crd/ciskubebenchreports.crd.yaml
@@ -4,7 +4,7 @@ metadata:
   name: ciskubebenchreports.aquasecurity.github.io
   labels:
     app.kubernetes.io/managed-by: starboard
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
 spec:
   group: aquasecurity.github.io
   versions:
diff --git a/deploy/crd/clusterconfigauditreports.crd.yaml b/deploy/crd/clusterconfigauditreports.crd.yaml
index 22dad6e..c393658 100644
--- a/deploy/crd/clusterconfigauditreports.crd.yaml
+++ b/deploy/crd/clusterconfigauditreports.crd.yaml
@@ -4,7 +4,7 @@ metadata:
   name: clusterconfigauditreports.aquasecurity.github.io
   labels:
     app.kubernetes.io/managed-by: starboard
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
 spec:
   group: aquasecurity.github.io
   versions:
diff --git a/deploy/crd/clustervulnerabilityreports.crd.yaml b/deploy/crd/clustervulnerabilityreports.crd.yaml
index 6ef0436..e449113 100644
--- a/deploy/crd/clustervulnerabilityreports.crd.yaml
+++ b/deploy/crd/clustervulnerabilityreports.crd.yaml
@@ -4,7 +4,7 @@ metadata:
   name: clustervulnerabilityreports.aquasecurity.github.io
   labels:
     app.kubernetes.io/managed-by: starboard
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
 spec:
   group: aquasecurity.github.io
   versions:
diff --git a/deploy/crd/configauditreports.crd.yaml b/deploy/crd/configauditreports.crd.yaml
index e625e3e..ea28bb2 100644
--- a/deploy/crd/configauditreports.crd.yaml
+++ b/deploy/crd/configauditreports.crd.yaml
@@ -4,7 +4,7 @@ metadata:
   name: configauditreports.aquasecurity.github.io
   labels:
     app.kubernetes.io/managed-by: starboard
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
 spec:
   group: aquasecurity.github.io
   versions:
diff --git a/deploy/crd/kubehunterreports.crd.yaml b/deploy/crd/kubehunterreports.crd.yaml
index 813e6d7..aee3868 100644
--- a/deploy/crd/kubehunterreports.crd.yaml
+++ b/deploy/crd/kubehunterreports.crd.yaml
@@ -4,7 +4,7 @@ metadata:
   name: kubehunterreports.aquasecurity.github.io
   labels:
     app.kubernetes.io/managed-by: starboard
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
 spec:
   group: aquasecurity.github.io
   versions:
diff --git a/deploy/crd/vulnerabilityreports.crd.yaml b/deploy/crd/vulnerabilityreports.crd.yaml
index 7aee166..03dbd3c 100644
--- a/deploy/crd/vulnerabilityreports.crd.yaml
+++ b/deploy/crd/vulnerabilityreports.crd.yaml
@@ -4,7 +4,7 @@ metadata:
   name: vulnerabilityreports.aquasecurity.github.io
   labels:
     app.kubernetes.io/managed-by: starboard
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
 spec:
   group: aquasecurity.github.io
   versions:
diff --git a/deploy/helm/Chart.yaml b/deploy/helm/Chart.yaml
index ab6b481..3bed915 100644
--- a/deploy/helm/Chart.yaml
+++ b/deploy/helm/Chart.yaml
@@ -11,7 +11,7 @@ version: 0.9.0
 # This is the version number of the application being deployed. This version number should be
 # incremented each time you make changes to the application. Versions are not expected to
 # follow Semantic Versioning. They should reflect the version the application is using.
-appVersion: 0.14.0-rc2
+appVersion: 0.14.0
 
 # kubeVersion: A SemVer range of compatible Kubernetes versions (optional)
 
diff --git a/deploy/static/02-starboard-operator.rbac.yaml b/deploy/static/02-starboard-operator.rbac.yaml
index 57ccf9b..4ff615a 100644
--- a/deploy/static/02-starboard-operator.rbac.yaml
+++ b/deploy/static/02-starboard-operator.rbac.yaml
@@ -7,7 +7,7 @@ metadata:
   labels:
     app.kubernetes.io/name: starboard-operator
     app.kubernetes.io/instance: starboard-operator
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
     app.kubernetes.io/managed-by: kubectl
 ---
 apiVersion: rbac.authorization.k8s.io/v1
@@ -17,7 +17,7 @@ metadata:
   labels:
     app.kubernetes.io/name: starboard-operator
     app.kubernetes.io/instance: starboard-operator
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
     app.kubernetes.io/managed-by: kubectl
 rules:
   - apiGroups:
@@ -139,7 +139,7 @@ metadata:
   labels:
     app.kubernetes.io/name: starboard-operator
     app.kubernetes.io/instance: starboard-operator
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
     app.kubernetes.io/managed-by: kubectl
 roleRef:
   apiGroup: rbac.authorization.k8s.io
diff --git a/deploy/static/03-starboard-operator.config.yaml b/deploy/static/03-starboard-operator.config.yaml
index b47c7c6..6a01485 100644
--- a/deploy/static/03-starboard-operator.config.yaml
+++ b/deploy/static/03-starboard-operator.config.yaml
@@ -7,7 +7,7 @@ metadata:
   labels:
     app.kubernetes.io/name: starboard-operator
     app.kubernetes.io/instance: starboard-operator
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
     app.kubernetes.io/managed-by: kubectl
 ---
 apiVersion: v1
@@ -18,7 +18,7 @@ metadata:
   labels:
     app.kubernetes.io/name: starboard-operator
     app.kubernetes.io/instance: starboard-operator
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
     app.kubernetes.io/managed-by: kubectl
 ---
 apiVersion: v1
@@ -29,7 +29,7 @@ metadata:
   labels:
     app.kubernetes.io/name: starboard-operator
     app.kubernetes.io/instance: starboard-operator
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
     app.kubernetes.io/managed-by: kubectl
 data:
   vulnerabilityReports.scanner: "Trivy"
@@ -44,7 +44,7 @@ metadata:
   labels:
     app.kubernetes.io/name: starboard-operator
     app.kubernetes.io/instance: starboard-operator
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
     app.kubernetes.io/managed-by: kubectl
 data:
   trivy.imageRef: "docker.io/aquasec/trivy:0.20.0"
@@ -63,7 +63,7 @@ metadata:
   labels:
     app.kubernetes.io/name: starboard-operator
     app.kubernetes.io/instance: starboard-operator
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
     app.kubernetes.io/managed-by: kubectl
 data:
   polaris.imageRef: "quay.io/fairwinds/polaris:4.2"
diff --git a/deploy/static/04-starboard-operator.deployment.yaml b/deploy/static/04-starboard-operator.deployment.yaml
index 87222d5..d5c67dd 100644
--- a/deploy/static/04-starboard-operator.deployment.yaml
+++ b/deploy/static/04-starboard-operator.deployment.yaml
@@ -7,7 +7,7 @@ metadata:
   labels:
     app.kubernetes.io/name: starboard-operator
     app.kubernetes.io/instance: starboard-operator
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
     app.kubernetes.io/managed-by: kubectl
   annotations:
     prometheus.io/path: /metrics
@@ -30,7 +30,7 @@ metadata:
   labels:
     app.kubernetes.io/name: starboard-operator
     app.kubernetes.io/instance: starboard-operator
-    app.kubernetes.io/version: "0.14.0-rc2"
+    app.kubernetes.io/version: "0.14.0"
     app.kubernetes.io/managed-by: kubectl
 spec:
   replicas: 1
@@ -50,7 +50,7 @@ spec:
       automountServiceAccountToken: true
       containers:
         - name: "starboard-operator"
-          image: "docker.io/aquasec/starboard-operator:0.14.0-rc2"
+          image: "docker.io/aquasec/starboard-operator:0.14.0"
           imagePullPolicy: IfNotPresent
           env:
             - name: OPERATOR_NAMESPACE
diff --git a/pkg/kube/object.go b/pkg/kube/object.go
index 37aa3f9..63d5220 100644
--- a/pkg/kube/object.go
+++ b/pkg/kube/object.go
@@ -300,6 +300,8 @@ func (o *ObjectResolver) ObjectFromObjectRef(ctx context.Context, workload Objec
 }
 
 var ErrReplicaSetNotFound = errors.New("replicaset not found")
+var ErrNoRunningPods = errors.New("no active pods for controller")
+var ErrUnSupportedKind = errors.New("unsupported workload kind")
 
 // ReportOwner resolves the owner of a security report for the specified object.
 func (o *ObjectResolver) ReportOwner(ctx context.Context, obj client.Object) (client.Object, error) {
@@ -445,6 +447,61 @@ 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
+// if there are no running pods then ErrNoRunningPods will be returned.
+// if there are not active replicaset for deployment then ErrReplicaSetNotFound will be returned.
+func (o *ObjectResolver) GetNodeName(ctx context.Context, obj client.Object) (string, error) {
+	switch 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.getActivePodsByLabelSelector(ctx, obj.GetNamespace(), replicaSet.Spec.Selector.MatchLabels)
+		if err != nil {
+			return "", err
+		}
+		return pods[0].Spec.NodeName, nil
+	case *appsv1.ReplicaSet:
+		pods, err := o.getActivePodsByLabelSelector(ctx, obj.GetNamespace(), obj.(*appsv1.ReplicaSet).Spec.Selector.MatchLabels)
+		if err != nil {
+			return "", err
+		}
+		return pods[0].Spec.NodeName, nil
+	case *corev1.ReplicationController:
+		pods, err := o.getActivePodsByLabelSelector(ctx, obj.GetNamespace(), obj.(*corev1.ReplicationController).Spec.Selector)
+		if err != nil {
+			return "", err
+		}
+		return pods[0].Spec.NodeName, nil
+	case *appsv1.StatefulSet:
+		pods, err := o.getActivePodsByLabelSelector(ctx, obj.GetNamespace(), obj.(*appsv1.StatefulSet).Spec.Selector.MatchLabels)
+		if err != nil {
+			return "", err
+		}
+		return pods[0].Spec.NodeName, nil
+	case *appsv1.DaemonSet:
+		pods, err := o.getActivePodsByLabelSelector(ctx, obj.GetNamespace(), obj.(*appsv1.DaemonSet).Spec.Selector.MatchLabels)
+		if err != nil {
+			return "", err
+		}
+		return pods[0].Spec.NodeName, nil
+	case *batchv1beta1.CronJob:
+		//Todo handle cronjob
+		return "", ErrUnSupportedKind
+	case *batchv1.Job:
+		pods, err := o.getActivePodsByLabelSelector(ctx, obj.GetNamespace(), obj.(*batchv1.Job).Spec.Selector.MatchLabels)
+		if err != nil {
+			return "", err
+		}
+		return pods[0].Spec.NodeName, nil
+	default:
+		return "", ErrUnSupportedKind
+	}
+}
+
 func (o *ObjectResolver) getActiveReplicaSetByDeployment(ctx context.Context, object ObjectRef) (string, error) {
 	deploy := &appsv1.Deployment{}
 	err := o.Client.Get(ctx, types.NamespacedName{Namespace: object.Namespace, Name: object.Name}, deploy)
@@ -505,3 +562,27 @@ func (o *ObjectResolver) IsActiveReplicaSet(ctx context.Context, workloadObj cli
 	}
 	return true, nil
 }
+
+func (o *ObjectResolver) GetPodsByLabelSelector(ctx context.Context, namespace string,
+	labelSelector labels.Set) ([]corev1.Pod, error) {
+	podList := &corev1.PodList{}
+	err := o.Client.List(ctx, podList, client.InNamespace(namespace),
+		client.MatchingLabelsSelector{Selector: labels.SelectorFromSet(labelSelector)})
+	if err != nil {
+		return podList.Items, fmt.Errorf("listing pods in namespace %s for labelselector %v: %w", namespace,
+			labelSelector, err)
+	}
+	return podList.Items, err
+}
+
+func (o *ObjectResolver) getActivePodsByLabelSelector(ctx context.Context, namespace string,
+	labelSelector labels.Set) ([]corev1.Pod, error) {
+	pods, err := o.GetPodsByLabelSelector(ctx, namespace, labelSelector)
+	if err != nil {
+		return pods, err
+	}
+	if len(pods) == 0 {
+		return pods, ErrNoRunningPods
+	}
+	return pods, nil
+}
diff --git a/pkg/operator/controller/vulnerabilityreport.go b/pkg/operator/controller/vulnerabilityreport.go
index 0a00a6c..ea1d6e1 100644
--- a/pkg/operator/controller/vulnerabilityreport.go
+++ b/pkg/operator/controller/vulnerabilityreport.go
@@ -4,6 +4,7 @@ import (
 	. "github.com/aquasecurity/starboard/pkg/operator/predicate"
 
 	"context"
+	"errors"
 	"fmt"
 	"reflect"
 
@@ -17,7 +18,7 @@ import (
 	batchv1 "k8s.io/api/batch/v1"
 	batchv1beta1 "k8s.io/api/batch/v1beta1"
 	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/errors"
+	k8sapierror "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/builder"
@@ -92,7 +93,7 @@ func (r *VulnerabilityReportReconciler) reconcileWorkload(workloadKind kube.Kind
 		log.V(1).Info("Getting workload from cache")
 		workloadObj, err := r.ObjectFromObjectRef(ctx, workloadPartial)
 		if err != nil {
-			if errors.IsNotFound(err) {
+			if k8sapierror.IsNotFound(err) {
 				log.V(1).Info("Ignoring cached workload that must have been deleted")
 				return ctrl.Result{}, nil
 			}
@@ -205,7 +206,7 @@ func (r *VulnerabilityReportReconciler) hasActiveScanJob(ctx context.Context, ow
 	job := &batchv1.Job{}
 	err := r.Get(ctx, client.ObjectKey{Namespace: r.Config.Namespace, Name: jobName}, job)
 	if err != nil {
-		if errors.IsNotFound(err) {
+		if k8sapierror.IsNotFound(err) {
 			return false, nil, nil
 		}
 		return false, nil, fmt.Errorf("getting job from cache: %w", err)
@@ -217,6 +218,8 @@ func (r *VulnerabilityReportReconciler) hasActiveScanJob(ctx context.Context, ow
 }
 
 func (r *VulnerabilityReportReconciler) submitScanJob(ctx context.Context, owner client.Object) error {
+	log := r.Logger.WithValues("kind", owner.GetObjectKind().GroupVersionKind().Kind,
+		"name", owner.GetName(), "namespace", owner.GetNamespace())
 	credentials, err := r.CredentialsByWorkload(ctx, owner)
 	if err != nil {
 		return err
@@ -248,11 +251,20 @@ func (r *VulnerabilityReportReconciler) submitScanJob(ctx context.Context, owner
 		WithCredentials(credentials).
 		Get()
 
+	if err != nil {
+		if errors.Is(err, kube.ErrReplicaSetNotFound) || errors.Is(err, kube.ErrNoRunningPods) ||
+			errors.Is(err, kube.ErrUnSupportedKind) {
+			log.V(1).Info("ignoring vulnerability scan", "reason", err)
+			return nil
+		}
+		return fmt.Errorf("constructing scan job: %w", err)
+	}
+
 	for _, secret := range secrets {
 		secret.Namespace = r.PluginContext.GetNamespace()
 		err = r.Client.Create(ctx, secret)
 		if err != nil {
-			if errors.IsAlreadyExists(err) {
+			if k8sapierror.IsAlreadyExists(err) {
 				return nil
 			}
 			return fmt.Errorf("creating secret used by scan job failed: %s: %w", secret.Namespace+"/"+secret.Name, err)
@@ -261,7 +273,7 @@ func (r *VulnerabilityReportReconciler) submitScanJob(ctx context.Context, owner
 
 	err = r.Client.Create(ctx, scanJob)
 	if err != nil {
-		if errors.IsAlreadyExists(err) {
+		if k8sapierror.IsAlreadyExists(err) {
 			// TODO Delete secrets that were created in the previous step. Alternatively we can delete them on schedule.
 			return nil
 		}
@@ -289,7 +301,7 @@ func (r *VulnerabilityReportReconciler) reconcileJobs() reconcile.Func {
 		job := &batchv1.Job{}
 		err := r.Client.Get(ctx, req.NamespacedName, job)
 		if err != nil {
-			if errors.IsNotFound(err) {
+			if k8sapierror.IsNotFound(err) {
 				log.V(1).Info("Ignoring cached job that must have been deleted")
 				return ctrl.Result{}, nil
 			}
@@ -325,7 +337,7 @@ func (r *VulnerabilityReportReconciler) processCompleteScanJob(ctx context.Conte
 
 	owner, err := r.ObjectFromObjectRef(ctx, ownerRef)
 	if err != nil {
-		if errors.IsNotFound(err) {
+		if k8sapierror.IsNotFound(err) {
 			log.V(1).Info("Report owner must have been deleted", "owner", owner)
 			return r.deleteJob(ctx, job)
 		}
@@ -414,7 +426,7 @@ func (r *VulnerabilityReportReconciler) processFailedScanJob(ctx context.Context
 func (r *VulnerabilityReportReconciler) deleteJob(ctx context.Context, job *batchv1.Job) error {
 	err := r.Client.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
 	if err != nil {
-		if errors.IsNotFound(err) {
+		if k8sapierror.IsNotFound(err) {
 			return nil
 		}
 		return fmt.Errorf("deleting job: %w", err)
diff --git a/pkg/plugin/factory.go b/pkg/plugin/factory.go
index f8b14c7..6480096 100644
--- a/pkg/plugin/factory.go
+++ b/pkg/plugin/factory.go
@@ -72,7 +72,7 @@ func (r *Resolver) GetVulnerabilityPlugin() (vulnerabilityreport.Plugin, starboa
 
 	switch scanner {
 	case starboard.Trivy:
-		return trivy.NewPlugin(ext.NewSystemClock(), ext.NewGoogleUUIDGenerator()), pluginContext, nil
+		return trivy.NewPlugin(ext.NewSystemClock(), ext.NewGoogleUUIDGenerator(), r.client), pluginContext, nil
 	case starboard.Aqua:
 		return aqua.NewPlugin(ext.NewGoogleUUIDGenerator(), r.buildInfo), pluginContext, nil
 	}
diff --git a/pkg/plugin/trivy/plugin.go b/pkg/plugin/trivy/plugin.go
index e6c3f69..911c02d 100644
--- a/pkg/plugin/trivy/plugin.go
+++ b/pkg/plugin/trivy/plugin.go
@@ -1,6 +1,7 @@
 package trivy
 
 import (
+	"context"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -28,6 +29,7 @@ const (
 const (
 	keyTrivyImageRef               = "trivy.imageRef"
 	keyTrivyMode                   = "trivy.mode"
+	keyTrivyCommand                = "trivy.command"
 	keyTrivySeverity               = "trivy.severity"
 	keyTrivyIgnoreUnfixed          = "trivy.ignoreUnfixed"
 	keyTrivyIgnoreFile             = "trivy.ignoreFile"
@@ -60,6 +62,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
@@ -88,6 +97,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)
 }
@@ -97,6 +123,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 {
@@ -173,8 +204,9 @@ func (c Config) setResourceLimit(configKey string, k8sResourceList *corev1.Resou
 }
 
 type plugin struct {
-	clock       ext.Clock
-	idGenerator ext.IDGenerator
+	clock          ext.Clock
+	idGenerator    ext.IDGenerator
+	objectResolver *kube.ObjectResolver
 }
 
 // NewPlugin constructs a new vulnerabilityreport.Plugin, which is using an
@@ -185,10 +217,11 @@ type plugin struct {
 //
 // The ClientServer mode is usually more performant, however it
 // requires a Trivy server accessible at the configurable Config.GetServerURL.
-func NewPlugin(clock ext.Clock, idGenerator ext.IDGenerator) vulnerabilityreport.Plugin {
+func NewPlugin(clock ext.Clock, idGenerator ext.IDGenerator, client client.Client) vulnerabilityreport.Plugin {
 	return &plugin{
-		clock:       clock,
-		idGenerator: idGenerator,
+		clock:          clock,
+		idGenerator:    idGenerator,
+		objectResolver: &kube.ObjectResolver{Client: client},
 	}
 }
 
@@ -223,13 +256,25 @@ func (p *plugin) GetScanJobSpec(ctx starboard.PluginContext, workload client.Obj
 	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, workload)
+		default:
+			return corev1.PodSpec{}, nil, fmt.Errorf("unrecognized trivy file scan mode: %v", mode)
+
+		}
 	}
 }
 
@@ -247,8 +292,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
@@ -862,6 +909,211 @@ 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,
+	workload client.Object) (corev1.PodSpec, []*corev1.Secret, error) {
+	var secrets []*corev1.Secret
+	spec, err := kube.GetPodSpec(workload)
+	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
+	}
+
+	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),
+				// Currently Trivy needs to run as root user to scan filesystem, So we will run fs scan job with root user.
+				RunAsUser: pointer.Int64(0),
+			},
+		})
+	}
+
+	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 {
@@ -1030,6 +1282,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
+}


Test patch:
diff --git a/pkg/plugin/trivy/plugin_test.go b/pkg/plugin/trivy/plugin_test.go
index 97ea78d..6de3142 100644
--- a/pkg/plugin/trivy/plugin_test.go
+++ b/pkg/plugin/trivy/plugin_test.go
@@ -118,6 +118,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
@@ -224,6 +279,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
@@ -340,7 +439,7 @@ func TestPlugin_Init(t *testing.T) {
 	t.Run("Should create the default config", func(t *testing.T) {
 		client := fake.NewClientBuilder().WithObjects().Build()
 
-		instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator())
+		instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator(), client)
 
 		pluginContext := starboard.NewPluginContext().
 			WithName(trivy.Plugin).
@@ -402,7 +501,7 @@ func TestPlugin_Init(t *testing.T) {
 				},
 			}).Build()
 
-		instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator())
+		instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator(), client)
 
 		pluginContext := starboard.NewPluginContext().
 			WithName(trivy.Plugin).
@@ -2591,25 +2690,308 @@ 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.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",
+				}},
+			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),
+							RunAsUser:              pointer.Int64(0),
+						},
+					},
+				},
+				SecurityContext: &corev1.PodSecurityContext{},
+				NodeName:        "kind-control-pane",
+			},
+		},
 	}
 
 	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(fake.NewClientBuilder().WithObjects(
-					&corev1.ConfigMap{
-						ObjectMeta: metav1.ObjectMeta{
-							Name:      "starboard-trivy-config",
-							Namespace: "starboard-ns",
-						},
-						Data: tc.config,
-					},
-				).Build()).
+				WithClient(fakeclient).
 				Get()
-			instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator())
+			instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator(), fakeclient)
 			jobSpec, secrets, err := instance.GetScanJobSpec(pluginContext, tc.workloadSpec, nil)
 			require.NoError(t, err)
 			assert.Empty(t, secrets)
@@ -2765,13 +3147,14 @@ func TestPlugin_ParseVulnerabilityReportData(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
+			fakeClient := fake.NewClientBuilder().WithObjects(config).Build()
 			ctx := starboard.NewPluginContext().
 				WithName("Trivy").
 				WithNamespace("starboard-ns").
 				WithServiceAccountName("starboard-sa").
-				WithClient(fake.NewClientBuilder().WithObjects(config).Build()).
+				WithClient(fakeClient).
 				Get()
-			instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator())
+			instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator(), fakeClient)
 			report, err := instance.ParseVulnerabilityReportData(ctx, tc.imageRef, io.NopCloser(strings.NewReader(tc.input)))
 			switch {
 			case tc.expectedError == nil:
diff --git a/pkg/vulnerabilityreport/builder_test.go b/pkg/vulnerabilityreport/builder_test.go
index c96ce70..48e9c1a 100644
--- a/pkg/vulnerabilityreport/builder_test.go
+++ b/pkg/vulnerabilityreport/builder_test.go
@@ -93,6 +93,7 @@ func TestScanJobBuilder(t *testing.T) {
 						},
 					},
 				},
+				Selector: &metav1.LabelSelector{},
 			},
 		}).
 		Get()