Repository: aquasecurity/starboard
Pull request #328 (2021-01-11 13:22:06)

feat(cli): Provide more details when scan job fails
It might happen that a scan job cannot be created. In such cases Starboard CLI hangs forever because it watches the job's status to terminate (JobComplete or JobFailed). However, some failures are reported as Kubernetes events associated with the scan job.

This change adds the events informer to watch events associated with a scan job, and terminate the program when the event of type EventTypeWarning (= error for Kubernetes) is detected.

To reproduce you can delete the starboard service account in the starborad namespace:

```
kubectl delete sa -n starboard starboard
```

## Before

```console
$ kubectl starboard generate vulnerabilityreports deploy/nginx -v 3
I0111 14:33:00.321886   35566 cert_rotation.go:137] Starting client certificate rotation controller
I0111 14:33:00.351359   35566 scanner.go:66] Getting Pod template for workload: {Deployment nginx default}
I0111 14:33:00.357111   35566 scanner.go:72] Scanning with options: {ScanJobTimeout:0s DeleteScanJob:true}
I0111 14:33:00.358993   35566 runner.go:79] Running task and waiting forever
I0111 14:33:00.359382   35566 runnable_job.go:72] Creating job "starboard/92506d31-46ed-4095-a1b1-822e5f076a42"
I0111 14:33:00.372751   35566 reflector.go:175] Starting reflector *v1.Job (30m0s) from pkg/mod/k8s.io/client-go@v0.18.6/tools/cache/reflector.go:125
I0111 14:33:00.372772   35566 reflector.go:211] Listing and watching *v1.Job from pkg/mod/k8s.io/client-go@v0.18.6/tools/cache/reflector.go:125
HANGS FOREVER !!!
```

## After

```console
$ ./bin/starboard generate vulnerabilityreports deploy/nginx -v 3
I0111 14:31:48.312623   35506 cert_rotation.go:137] Starting client certificate rotation controller
I0111 14:31:48.327276   35506 scanner.go:66] Getting Pod template for workload: {Deployment nginx default}
I0111 14:31:48.331739   35506 scanner.go:72] Scanning with options: {ScanJobTimeout:0s DeleteScanJob:true}
I0111 14:31:48.333309   35506 runner.go:79] Running task and waiting forever
I0111 14:31:48.333339   35506 runnable_job.go:73] Creating job "starboard/37dc6d47-e5bf-46c9-9888-400105710fc6"
I0111 14:31:48.339370   35506 reflector.go:175] Starting reflector *v1.Event (30m0s) from pkg/mod/k8s.io/client-go@v0.18.6/tools/cache/reflector.go:125
I0111 14:31:48.339388   35506 reflector.go:211] Listing and watching *v1.Event from pkg/mod/k8s.io/client-go@v0.18.6/tools/cache/reflector.go:125
I0111 14:31:48.339391   35506 reflector.go:175] Starting reflector *v1.Job (30m0s) from pkg/mod/k8s.io/client-go@v0.18.6/tools/cache/reflector.go:125
I0111 14:31:48.339409   35506 reflector.go:211] Listing and watching *v1.Job from pkg/mod/k8s.io/client-go@v0.18.6/tools/cache/reflector.go:125
I0111 14:31:48.439720   35506 runner.go:83] Stopping runner on task completion with error: warning event received: Error creating: pods "37dc6d47-e5bf-46c9-9888-400105710fc6-" is forbidden: error looking up service account starboard/starboard: serviceaccount "starboard" not found (FailedCreate)
error: running scan job: warning event received: Error creating: pods "37dc6d47-e5bf-46c9-9888-400105710fc6-" is forbidden: error looking up service account starboard/starboard: serviceaccount "starboard" not found (FailedCreate)
```

Resolves: #70

Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/328?src=pr&el=h1) Report
> Merging [#328](https://codecov.io/gh/aquasecurity/starboard/pull/328?src=pr&el=desc) (4853f1b) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/b42ffb5e994741cb1fa0c03f8b7e1a05cf1e7981?el=desc) (b42ffb5) will **decrease** coverage by `35.79%`.
> The diff coverage is `0.00%`.

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

```diff
@@             Coverage Diff             @@
##             main     #328       +/-   ##
===========================================
- Coverage   62.20%   26.40%   -35.80%     
===========================================
  Files          56       50        -6     
  Lines        2773     2465      -308     
===========================================
- Hits         1725      651     -1074     
- Misses        822     1762      +940     
+ Partials      226       52      -174     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/328?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/kube/pod/manager.go](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree#diff-cGtnL2t1YmUvcG9kL21hbmFnZXIuZ28=) | `0.00% <0.00%> (-35.14%)` | :arrow_down: |
| [pkg/kube/runnable\_job.go](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree#diff-cGtnL2t1YmUvcnVubmFibGVfam9iLmdv) | `0.00% <0.00%> (-62.00%)` | :arrow_down: |
| [pkg/vulnerabilityreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvc2Nhbm5lci5nbw==) | `0.00% <ø> (-73.47%)` | :arrow_down: |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree#diff-cGtnL2NtZC9nZXQuZ28=) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/find.go](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree#diff-cGtnL2NtZC9maW5kLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/root.go](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree#diff-cGtnL2NtZC9yb290Lmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/scan.go](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree#diff-cGtnL2NtZC9zY2FuLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/polaris.go](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree#diff-cGtnL2NtZC9wb2xhcmlzLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/version.go](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree#diff-cGtnL2NtZC92ZXJzaW9uLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/kube\_bench.go](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree#diff-cGtnL2NtZC9rdWJlX2JlbmNoLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [33 more](https://codecov.io/gh/aquasecurity/starboard/pull/328/diff?src=pr&el=tree-more) | |

------

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


Patch:
diff --git a/go.mod b/go.mod
index e964e5d..04da113 100644
--- a/go.mod
+++ b/go.mod
@@ -9,10 +9,10 @@ require (
 	github.com/google/uuid v1.1.1
 	github.com/onsi/ginkgo v1.14.0
 	github.com/onsi/gomega v1.10.1
-	github.com/spf13/cobra v1.0.0
+	github.com/spf13/cobra v1.1.1
 	github.com/spf13/pflag v1.0.5
-	github.com/stretchr/testify v1.5.1
-	github.com/valyala/quicktemplate v1.6.2
+	github.com/stretchr/testify v1.7.0
+	github.com/valyala/quicktemplate v1.6.3
 	k8s.io/api v0.18.6
 	k8s.io/apiextensions-apiserver v0.18.6
 	k8s.io/apimachinery v0.18.6
diff --git a/go.sum b/go.sum
index 2f2ac0d..7de8410 100644
--- a/go.sum
+++ b/go.sum
@@ -480,7 +480,7 @@ github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+o
 github.com/klauspost/compress v1.4.0/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.4.1/go.mod h1:RyIbtBH6LamlWaDj8nUwkbUhJ87Yi3uG0guNDohfE1A=
 github.com/klauspost/compress v1.10.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
-github.com/klauspost/compress v1.10.10/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
+github.com/klauspost/compress v1.11.0/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs=
 github.com/klauspost/cpuid v0.0.0-20180405133222-e7e905edc00e/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/klauspost/cpuid v1.2.0/go.mod h1:Pj4uuM528wm8OyEC2QMXAi2YiTZ96dNQPGgoMS4s3ek=
 github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ=
@@ -667,8 +667,9 @@ github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkU
 github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
 github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ=
 github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
-github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8=
 github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE=
+github.com/spf13/cobra v1.1.1 h1:KfztREH0tPxJJ+geloSLaAkaPkr4ki2Er5quFV1TDo4=
+github.com/spf13/cobra v1.1.1/go.mod h1:WnodtKOvamDL/PwE2M4iKs8aMDBZ5Q5klgD3qfVJQMI=
 github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
 github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo=
 github.com/spf13/pflag v0.0.0-20170130214245-9ff6c6923cff/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
@@ -687,8 +688,9 @@ github.com/stretchr/testify v0.0.0-20151208002404-e3a8ff8ce365/go.mod h1:a8OnRci
 github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
 github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
 github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
-github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
 github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
+github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY=
+github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
 github.com/subosito/gotenv v1.2.0/go.mod h1:N0PQaV/YGNqwC0u51sEeR/aUtSLEXKX9iv69rRypqCw=
 github.com/tdakkota/asciicheck v0.0.0-20200416190851-d7f85be797a2/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
 github.com/tdakkota/asciicheck v0.0.0-20200416200610-e657995f937b/go.mod h1:yHp0ai0Z9gUljN3o0xMhYJnH/IcvkdTBOX2fmJ93JEM=
@@ -716,10 +718,10 @@ github.com/uudashr/gocognit v1.0.1/go.mod h1:j44Ayx2KW4+oB6SWMv8KsmHzZrOInQav7D3
 github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
 github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
 github.com/valyala/fasthttp v1.2.0/go.mod h1:4vX61m6KN+xDduDNwXrhIAVZaZaZiQ1luJk8LWSxF3s=
-github.com/valyala/fasthttp v1.15.1/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
+github.com/valyala/fasthttp v1.16.0/go.mod h1:YOKImeEosDdBPnxc0gy7INqi3m1zK6A+xl6TwOBhHCA=
 github.com/valyala/quicktemplate v1.2.0/go.mod h1:EH+4AkTd43SvgIbQHYu59/cJyxDoOVRUAfrukLPuGJ4=
-github.com/valyala/quicktemplate v1.6.2 h1:k0vgK7zlmFzqAoIBIOrhrfmZ6JoTGJlLRPLbkPGr2/M=
-github.com/valyala/quicktemplate v1.6.2/go.mod h1:mtEJpQtUiBV0SHhMX6RtiJtqxncgrfmjcUy5T68X8TM=
+github.com/valyala/quicktemplate v1.6.3 h1:O7EuMwuH7Q94U2CXD6sOX8AYHqQqWtmIk690IhmpkKA=
+github.com/valyala/quicktemplate v1.6.3/go.mod h1:fwPzK2fHuYEODzJ9pkw0ipCPNHZ2tD5KW4lOuSdPKzY=
 github.com/valyala/tcplisten v0.0.0-20161114210144-ceec8f93295a/go.mod h1:v3UYOV9WzVtRmSR+PDvWpU/qWl4Wa5LApYYX4ZtKbio=
 github.com/vdemeester/k8s-pkg-credentialprovider v1.17.4/go.mod h1:inCTmtUdr5KJbreVojo06krnTgaeAz/Z7lynpPk/Q2c=
 github.com/vektah/gqlparser v1.1.2/go.mod h1:1ycwN7Ij5njmMkPPAOaRFY4rET2Enx7IkVv3vaXspKw=
@@ -1103,6 +1105,8 @@ gopkg.in/yaml.v2 v2.2.7/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
 gopkg.in/yaml.v2 v2.3.0 h1:clyUAQHOM3G0M3f5vQj7LuJrETvjVot3Z5el9nffUtU=
 gopkg.in/yaml.v2 v2.3.0/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo=
+gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
 gotest.tools v2.2.0+incompatible/go.mod h1:DsYFclhRJ6vuDpmuTbkuFWG+y2sxOXAzmJt81HFBacw=
 honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
 honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
diff --git a/pkg/cmd/scan_configaudit.go b/pkg/cmd/scan_configaudit.go
index 41db49b..86c8fa7 100644
--- a/pkg/cmd/scan_configaudit.go
+++ b/pkg/cmd/scan_configaudit.go
@@ -4,7 +4,8 @@ import (
 	"context"
 
 	"github.com/aquasecurity/starboard/pkg/configauditreport"
-	starboardapi "github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
+	"github.com/aquasecurity/starboard/pkg/ext"
+	"github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
 	"github.com/aquasecurity/starboard/pkg/polaris"
 	"github.com/aquasecurity/starboard/pkg/starboard"
 	"github.com/spf13/cobra"
@@ -60,14 +61,17 @@ func ScanConfigAuditReports(cf *genericclioptions.ConfigFlags) func(cmd *cobra.C
 		if err != nil {
 			return err
 		}
-		report, err := polaris.NewScanner(starboard.NewScheme(), kubeClientset, config, opts).Scan(ctx, workload, gvk)
+		plugin := polaris.NewPlugin(ext.NewSystemClock(), config)
+		scanner := configauditreport.NewScanner(starboard.NewScheme(), kubeClientset, opts, plugin)
+		report, err := scanner.Scan(ctx, workload, gvk)
 		if err != nil {
 			return err
 		}
-		starboardClientset, err := starboardapi.NewForConfig(kubeConfig)
+		starboardClientset, err := versioned.NewForConfig(kubeConfig)
 		if err != nil {
 			return nil
 		}
-		return configauditreport.NewReadWriter(starboardClientset).Write(ctx, report)
+		writer := configauditreport.NewReadWriter(starboardClientset)
+		return writer.Write(ctx, report)
 	}
 }
diff --git a/pkg/configauditreport/doc.go b/pkg/configauditreport/doc.go
new file mode 100644
index 0000000..fd03de4
--- /dev/null
+++ b/pkg/configauditreport/doc.go
@@ -0,0 +1,2 @@
+// This package provides primitives for working with Kubernetes workload configuration checkers.
+package configauditreport
diff --git a/pkg/configauditreport/plugin.go b/pkg/configauditreport/plugin.go
new file mode 100644
index 0000000..1b98500
--- /dev/null
+++ b/pkg/configauditreport/plugin.go
@@ -0,0 +1,22 @@
+package configauditreport
+
+import (
+	"io"
+
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/kube"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+)
+
+// Plugin defines the interface between Starboard and Kubernetes workload
+// configuration checkers / linters / sanitizers. Not a final version, rather
+// first step to separate generic workloads discovery code and Polaris
+// implementation details.
+type Plugin interface {
+	GetScanJobSpec(workload kube.Object, gvk schema.GroupVersionKind) (corev1.PodSpec, error)
+
+	GetContainerName() string
+
+	ParseConfigAuditResult(logsReader io.ReadCloser) (v1alpha1.ConfigAuditResult, error)
+}
diff --git a/pkg/configauditreport/scanner.go b/pkg/configauditreport/scanner.go
new file mode 100644
index 0000000..74c9826
--- /dev/null
+++ b/pkg/configauditreport/scanner.go
@@ -0,0 +1,132 @@
+package configauditreport
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/ext"
+	"github.com/aquasecurity/starboard/pkg/kube"
+	"github.com/aquasecurity/starboard/pkg/kube/pod"
+	"github.com/aquasecurity/starboard/pkg/runner"
+	"github.com/aquasecurity/starboard/pkg/scanners"
+	"github.com/aquasecurity/starboard/pkg/starboard"
+	batchv1 "k8s.io/api/batch/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/client-go/kubernetes"
+	"k8s.io/klog"
+	"k8s.io/utils/pointer"
+)
+
+type Scanner struct {
+	scheme    *runtime.Scheme
+	clientset kubernetes.Interface
+	opts      kube.ScannerOpts
+	pods      *pod.Manager
+	plugin    Plugin
+	ext.IDGenerator
+}
+
+func NewScanner(
+	scheme *runtime.Scheme,
+	clientset kubernetes.Interface,
+	opts kube.ScannerOpts,
+	plugin Plugin,
+) *Scanner {
+	return &Scanner{
+		scheme:      scheme,
+		clientset:   clientset,
+		opts:        opts,
+		plugin:      plugin,
+		pods:        pod.NewPodManager(clientset),
+		IDGenerator: ext.NewGoogleUUIDGenerator(),
+	}
+}
+
+func (s *Scanner) Scan(ctx context.Context, workload kube.Object, gvk schema.GroupVersionKind) (v1alpha1.ConfigAuditReport, error) {
+	klog.V(3).Infof("Getting Pod template for workload: %v", workload)
+
+	_, owner, err := s.pods.GetPodSpecByWorkload(ctx, workload)
+	if err != nil {
+		return v1alpha1.ConfigAuditReport{}, err
+	}
+
+	klog.V(3).Infof("Scanning with options: %+v", s.opts)
+	job, err := s.getScanJob(workload, gvk)
+	if err != nil {
+		return v1alpha1.ConfigAuditReport{}, err
+	}
+
+	err = runner.New().Run(ctx, kube.NewRunnableJob(s.scheme, s.clientset, job))
+	if err != nil {
+		s.pods.LogRunnerErrors(ctx, job)
+		return v1alpha1.ConfigAuditReport{}, fmt.Errorf("running scan job: %w", err)
+	}
+
+	defer func() {
+		if !s.opts.DeleteScanJob {
+			klog.V(3).Infof("Skipping scan job deletion: %s/%s", job.Namespace, job.Name)
+			return
+		}
+		klog.V(3).Infof("Deleting scan job: %s/%s", job.Namespace, job.Name)
+		background := metav1.DeletePropagationBackground
+		_ = s.clientset.BatchV1().Jobs(job.Namespace).Delete(ctx, job.Name, metav1.DeleteOptions{
+			PropagationPolicy: &background,
+		})
+	}()
+
+	containerName := s.plugin.GetContainerName()
+
+	klog.V(3).Infof("Getting logs for %s container in job: %s/%s", containerName,
+		job.Namespace, job.Name)
+	logsReader, err := s.pods.GetContainerLogsByJob(ctx, job, containerName)
+	if err != nil {
+		return v1alpha1.ConfigAuditReport{}, fmt.Errorf("getting logs: %w", err)
+	}
+
+	result, err := s.plugin.ParseConfigAuditResult(logsReader)
+	defer func() {
+		_ = logsReader.Close()
+	}()
+
+	return NewBuilder(s.scheme).
+		Owner(owner).
+		Result(result).
+		Get()
+}
+
+func (s *Scanner) getScanJob(workload kube.Object, gvk schema.GroupVersionKind) (*batchv1.Job, error) {
+	jobSpec, err := s.plugin.GetScanJobSpec(workload, gvk)
+	if err != nil {
+		return nil, err
+	}
+	return &batchv1.Job{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      s.GenerateID(),
+			Namespace: starboard.NamespaceName,
+			Labels: map[string]string{
+				kube.LabelResourceKind:      string(workload.Kind),
+				kube.LabelResourceName:      workload.Name,
+				kube.LabelResourceNamespace: workload.Namespace,
+			},
+		},
+		Spec: batchv1.JobSpec{
+			BackoffLimit:          pointer.Int32Ptr(0),
+			Completions:           pointer.Int32Ptr(1),
+			ActiveDeadlineSeconds: scanners.GetActiveDeadlineSeconds(s.opts.ScanJobTimeout),
+			Template: corev1.PodTemplateSpec{
+				ObjectMeta: metav1.ObjectMeta{
+					Labels: map[string]string{
+						kube.LabelResourceKind:      string(workload.Kind),
+						kube.LabelResourceName:      workload.Name,
+						kube.LabelResourceNamespace: workload.Namespace,
+					},
+				},
+				Spec: jobSpec,
+			},
+		},
+	}, nil
+}
diff --git a/pkg/configauditreport/writer.go b/pkg/configauditreport/writer.go
index 27ad529..45d5381 100644
--- a/pkg/configauditreport/writer.go
+++ b/pkg/configauditreport/writer.go
@@ -3,28 +3,27 @@ package configauditreport
 import (
 	"context"
 
-	clientset "github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
+	"github.com/aquasecurity/starboard/pkg/kube"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/klog"
-
-	starboard "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
-	"github.com/aquasecurity/starboard/pkg/kube"
 )
 
 // Write is the interface that wraps basic methods for persisting ConfigAudit reports.
 //
 // Write persists the given ConfigAuditReport report.
 type Writer interface {
-	Write(ctx context.Context, report starboard.ConfigAuditReport) error
+	Write(ctx context.Context, report v1alpha1.ConfigAuditReport) error
 }
 
 // Reader is the interface that wraps basic methods for reading ConfigAudit reports.
 //
 // Read will return a single ConfigAuditReport that match a specific workload
 type Reader interface {
-	FindByOwner(ctx context.Context, owner kube.Object) (*starboard.ConfigAuditReport, error)
+	FindByOwner(ctx context.Context, owner kube.Object) (*v1alpha1.ConfigAuditReport, error)
 }
 
 type ReadWriter interface {
@@ -33,17 +32,17 @@ type ReadWriter interface {
 }
 
 type readWriter struct {
-	client clientset.Interface
+	clientset versioned.Interface
 }
 
-func NewReadWriter(client clientset.Interface) ReadWriter {
+func NewReadWriter(clientset versioned.Interface) ReadWriter {
 	return &readWriter{
-		client: client,
+		clientset: clientset,
 	}
 }
 
-func (w *readWriter) Write(ctx context.Context, report starboard.ConfigAuditReport) error {
-	existing, err := w.client.AquasecurityV1alpha1().ConfigAuditReports(report.Namespace).
+func (w *readWriter) Write(ctx context.Context, report v1alpha1.ConfigAuditReport) error {
+	existing, err := w.clientset.AquasecurityV1alpha1().ConfigAuditReports(report.Namespace).
 		Get(ctx, report.Name, metav1.GetOptions{})
 
 	if err == nil {
@@ -52,14 +51,14 @@ func (w *readWriter) Write(ctx context.Context, report starboard.ConfigAuditRepo
 		deepCopy.Labels = report.Labels
 		deepCopy.Report = report.Report
 
-		_, err = w.client.AquasecurityV1alpha1().ConfigAuditReports(report.Namespace).
+		_, err = w.clientset.AquasecurityV1alpha1().ConfigAuditReports(report.Namespace).
 			Update(ctx, deepCopy, metav1.UpdateOptions{})
 		return err
 	}
 
 	if errors.IsNotFound(err) {
 		klog.V(3).Infof("Creating ConfigAuditReport %q", report.Namespace+"/"+report.Name)
-		_, err = w.client.AquasecurityV1alpha1().ConfigAuditReports(report.Namespace).
+		_, err = w.clientset.AquasecurityV1alpha1().ConfigAuditReports(report.Namespace).
 			Create(ctx, &report, metav1.CreateOptions{})
 		return err
 	}
@@ -67,8 +66,8 @@ func (w *readWriter) Write(ctx context.Context, report starboard.ConfigAuditRepo
 	return err
 }
 
-func (w *readWriter) FindByOwner(ctx context.Context, workload kube.Object) (*starboard.ConfigAuditReport, error) {
-	list, err := w.client.AquasecurityV1alpha1().ConfigAuditReports(workload.Namespace).List(ctx, metav1.ListOptions{
+func (w *readWriter) FindByOwner(ctx context.Context, workload kube.Object) (*v1alpha1.ConfigAuditReport, error) {
+	list, err := w.clientset.AquasecurityV1alpha1().ConfigAuditReports(workload.Namespace).List(ctx, metav1.ListOptions{
 		LabelSelector: labels.Set{
 			kube.LabelResourceKind:      string(workload.Kind),
 			kube.LabelResourceName:      workload.Name,
diff --git a/pkg/kube/pod/manager.go b/pkg/kube/pod/manager.go
index bc82e2d..6af8308 100644
--- a/pkg/kube/pod/manager.go
+++ b/pkg/kube/pod/manager.go
@@ -5,16 +5,14 @@ import (
 	"fmt"
 	"io"
 
-	"k8s.io/klog"
-
 	"github.com/aquasecurity/starboard/pkg/kube"
-
 	apps "k8s.io/api/apps/v1"
 	batch "k8s.io/api/batch/v1"
 	batchv1beta1 "k8s.io/api/batch/v1beta1"
 	core "k8s.io/api/core/v1"
 	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes"
+	"k8s.io/klog"
 )
 
 type Manager struct {
@@ -123,6 +121,9 @@ func (pw *Manager) GetTerminatedContainersStatusesByJob(ctx context.Context, job
 
 func GetTerminatedContainersStatusesByPod(pod *core.Pod) map[string]*core.ContainerStateTerminated {
 	states := make(map[string]*core.ContainerStateTerminated)
+	if pod == nil {
+		return states
+	}
 	for _, status := range pod.Status.InitContainerStatuses {
 		if status.State.Terminated == nil {
 			continue
@@ -159,7 +160,10 @@ func (pw *Manager) GetPodByJob(ctx context.Context, job *batch.Job) (*core.Pod,
 	if err != nil {
 		return nil, err
 	}
-	return &podList.Items[0], nil
+	if podList != nil && len(podList.Items) > 0 {
+		return &podList.Items[0], nil
+	}
+	return nil, nil
 }
 
 func (pw *Manager) GetPodLogs(ctx context.Context, pod *core.Pod, container string) (io.ReadCloser, error) {
@@ -186,16 +190,3 @@ func (pw *Manager) logTerminatedContainersErrors(statuses map[string]*core.Conta
 		klog.Errorf("Container %s terminated with %s: %s", container, status.Reason, status.Message)
 	}
 }
-
-// GetImages gets a slice of images for the specified PodSpec.
-func GetImages(spec core.PodSpec) (images []string) {
-	for _, c := range spec.InitContainers {
-		images = append(images, c.Image)
-	}
-
-	for _, c := range spec.Containers {
-		images = append(images, c.Image)
-	}
-
-	return
-}
diff --git a/pkg/kube/runnable_job.go b/pkg/kube/runnable_job.go
index d518c4f..8a38f20 100644
--- a/pkg/kube/runnable_job.go
+++ b/pkg/kube/runnable_job.go
@@ -57,7 +57,8 @@ func (r *runnableJob) Run(ctx context.Context) error {
 		defaultResyncDuration,
 		informers.WithNamespace(r.job.Namespace),
 	)
-	jobInformer := informerFactory.Batch().V1().Jobs()
+	jobsInformer := informerFactory.Batch().V1().Jobs()
+	eventsInformer := informerFactory.Core().V1().Events()
 
 	var err error
 
@@ -90,8 +91,8 @@ func (r *runnableJob) Run(ctx context.Context) error {
 
 	complete := make(chan error)
 
-	jobInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
-		UpdateFunc: func(oldObj, newObj interface{}) {
+	jobsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		UpdateFunc: func(_, newObj interface{}) {
 			newJob, ok := newObj.(*batchv1.Job)
 			if !ok {
 				return
@@ -113,6 +114,28 @@ func (r *runnableJob) Run(ctx context.Context) error {
 		},
 	})
 
+	eventsInformer.Informer().AddEventHandler(cache.ResourceEventHandlerFuncs{
+		AddFunc: func(obj interface{}) {
+			event := obj.(*corev1.Event)
+
+			// TODO We might want to look into events associate with the pod controlled by the current scan job.
+			// For example, when a pod cannot be scheduled due to insufficient resource requests,
+			// the event will be attached to the pod, not the job.
+			if event.InvolvedObject.UID != r.job.UID {
+				return
+			}
+
+			if event.Type == corev1.EventTypeNormal {
+				klog.V(3).Infof("Event: %s (%s)", event.Message, event.Reason)
+			}
+
+			if event.Type == corev1.EventTypeWarning {
+				complete <- fmt.Errorf("warning event received: %s (%s)", event.Message, event.Reason)
+				return
+			}
+		},
+	})
+
 	informerFactory.Start(wait.NeverStop)
 	informerFactory.WaitForCacheSync(wait.NeverStop)
 
diff --git a/pkg/polaris/converter.go b/pkg/polaris/converter.go
deleted file mode 100644
index b2c42da..0000000
--- a/pkg/polaris/converter.go
+++ /dev/null
@@ -1,116 +0,0 @@
-package polaris
-
-import (
-	"encoding/json"
-	"io"
-	"time"
-
-	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
-	"github.com/aquasecurity/starboard/pkg/starboard"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-type Converter interface {
-	Convert(reader io.Reader) (v1alpha1.ConfigAuditResult, error)
-}
-
-type converter struct {
-	config Config
-}
-
-func NewConverter(config Config) Converter {
-	return &converter{
-		config: config,
-	}
-}
-
-func (c *converter) Convert(reader io.Reader) (v1alpha1.ConfigAuditResult, error) {
-	var report Report
-	err := json.NewDecoder(reader).Decode(&report)
-	if err != nil {
-		return v1alpha1.ConfigAuditResult{}, err
-	}
-	return c.toConfigAudit(report.Results[0])
-}
-
-// TODO Add success checks to the summary
-func (c *converter) toSummary(podChecks []v1alpha1.Check, containerChecks map[string][]v1alpha1.Check) v1alpha1.ConfigAuditSummary {
-	var summary v1alpha1.ConfigAuditSummary
-	for _, c := range podChecks {
-		if c.Success {
-			continue
-		}
-		switch c.Severity {
-		case v1alpha1.ConfigAuditDangerSeverity:
-			summary.DangerCount++
-		case v1alpha1.ConfigAuditWarningSeverity:
-			summary.WarningCount++
-		}
-	}
-	for _, checks := range containerChecks {
-		for _, c := range checks {
-			if c.Success {
-				continue
-			}
-			switch c.Severity {
-			case v1alpha1.ConfigAuditDangerSeverity:
-				summary.DangerCount++
-			case v1alpha1.ConfigAuditWarningSeverity:
-				summary.WarningCount++
-			}
-		}
-	}
-	return summary
-}
-
-func (c *converter) toConfigAudit(result Result) (v1alpha1.ConfigAuditResult, error) {
-	var podChecks []v1alpha1.Check
-	containerChecks := make(map[string][]v1alpha1.Check)
-
-	for _, pr := range result.PodResult.Results {
-		podChecks = append(podChecks, v1alpha1.Check{
-			ID:       pr.ID,
-			Message:  pr.Message,
-			Success:  pr.Success,
-			Severity: pr.Severity,
-			Category: pr.Category,
-		})
-	}
-
-	for _, cr := range result.PodResult.ContainerResults {
-		var checks []v1alpha1.Check
-		for _, crr := range cr.Results {
-			checks = append(checks, v1alpha1.Check{
-				ID:       crr.ID,
-				Message:  crr.Message,
-				Success:  crr.Success,
-				Severity: crr.Severity,
-				Category: crr.Category,
-			})
-
-		}
-		containerChecks[cr.Name] = checks
-	}
-
-	imageRef, err := c.config.GetPolarisImageRef()
-	if err != nil {
-		return v1alpha1.ConfigAuditResult{}, err
-	}
-
-	version, err := starboard.GetVersionFromImageRef(imageRef)
-	if err != nil {
-		return v1alpha1.ConfigAuditResult{}, err
-	}
-
-	return v1alpha1.ConfigAuditResult{
-		Scanner: v1alpha1.Scanner{
-			Name:    "Polaris",
-			Vendor:  "Fairwinds Ops",
-			Version: version,
-		},
-		Summary:         c.toSummary(podChecks, containerChecks),
-		UpdateTimestamp: metav1.NewTime(time.Now()),
-		PodChecks:       podChecks,
-		ContainerChecks: containerChecks,
-	}, nil
-}
diff --git a/pkg/polaris/doc.go b/pkg/polaris/doc.go
new file mode 100644
index 0000000..19e62a2
--- /dev/null
+++ b/pkg/polaris/doc.go
@@ -0,0 +1,2 @@
+// This package provides primitives for working with Polaris.
+package polaris
diff --git a/pkg/polaris/plugin.go b/pkg/polaris/plugin.go
new file mode 100644
index 0000000..2606494
--- /dev/null
+++ b/pkg/polaris/plugin.go
@@ -0,0 +1,208 @@
+package polaris
+
+import (
+	"encoding/json"
+	"fmt"
+	"io"
+
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/configauditreport"
+	"github.com/aquasecurity/starboard/pkg/ext"
+	"github.com/aquasecurity/starboard/pkg/kube"
+	"github.com/aquasecurity/starboard/pkg/starboard"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/utils/pointer"
+)
+
+const (
+	polarisContainerName = "polaris"
+	configVolume         = "config"
+)
+
+type Config interface {
+	GetPolarisImageRef() (string, error)
+}
+
+type plugin struct {
+	clock  ext.Clock
+	config Config
+}
+
+// NewPlugin constructs a new configauditreport.Plugin, which is using an
+// official Polaris container image to audit Kubernetes workloads.
+func NewPlugin(clock ext.Clock, config Config) configauditreport.Plugin {
+	return &plugin{
+		clock:  clock,
+		config: config,
+	}
+}
+
+func (p *plugin) GetScanJobSpec(workload kube.Object, gvk schema.GroupVersionKind) (corev1.PodSpec, error) {
+	imageRef, err := p.config.GetPolarisImageRef()
+	if err != nil {
+		return corev1.PodSpec{}, err
+	}
+	sourceName := p.sourceNameFrom(workload, gvk)
+
+	return corev1.PodSpec{
+		ServiceAccountName:           starboard.ServiceAccountName,
+		AutomountServiceAccountToken: pointer.BoolPtr(true),
+		RestartPolicy:                corev1.RestartPolicyNever,
+		Affinity:                     starboard.LinuxNodeAffinity(),
+		Volumes: []corev1.Volume{
+			{
+				Name: configVolume,
+				VolumeSource: corev1.VolumeSource{
+					ConfigMap: &corev1.ConfigMapVolumeSource{
+						LocalObjectReference: corev1.LocalObjectReference{
+							Name: starboard.ConfigMapName,
+						},
+					},
+				},
+			},
+		},
+		Containers: []corev1.Container{
+			{
+				Name:                     polarisContainerName,
+				Image:                    imageRef,
+				ImagePullPolicy:          corev1.PullIfNotPresent,
+				TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
+				Resources: corev1.ResourceRequirements{
+					Limits: corev1.ResourceList{
+						corev1.ResourceCPU:    resource.MustParse("300m"),
+						corev1.ResourceMemory: resource.MustParse("300M"),
+					},
+					Requests: corev1.ResourceList{
+						corev1.ResourceCPU:    resource.MustParse("50m"),
+						corev1.ResourceMemory: resource.MustParse("50M"),
+					},
+				},
+				VolumeMounts: []corev1.VolumeMount{
+					{
+						Name:      configVolume,
+						MountPath: "/etc/starboard",
+					},
+				},
+				Command: []string{"polaris"},
+				Args: []string{
+					"audit",
+					"--log-level", "error",
+					"--config", "/etc/starboard/polaris.config.yaml",
+					"--resource", sourceName,
+				},
+			},
+		},
+	}, nil
+}
+
+func (p *plugin) GetContainerName() string {
+	return polarisContainerName
+}
+
+func (p *plugin) ParseConfigAuditResult(logsReader io.ReadCloser) (v1alpha1.ConfigAuditResult, error) {
+	var report Report
+	err := json.NewDecoder(logsReader).Decode(&report)
+	if err != nil {
+		return v1alpha1.ConfigAuditResult{}, err
+	}
+	return p.configAuditResultFrom(report.Results[0])
+}
+
+func (p *plugin) sourceNameFrom(workload kube.Object, gvk schema.GroupVersionKind) string {
+	group := gvk.Group
+	if len(group) > 0 {
+		group = "." + group
+	}
+	return fmt.Sprintf("%s/%s%s/%s/%s",
+		workload.Namespace,
+		gvk.Kind,
+		group,
+		gvk.Version,
+		workload.Name,
+	)
+}
+
+func (p *plugin) configAuditResultFrom(result Result) (v1alpha1.ConfigAuditResult, error) {
+	var podChecks []v1alpha1.Check
+	containerChecks := make(map[string][]v1alpha1.Check)
+
+	for _, pr := range result.PodResult.Results {
+		podChecks = append(podChecks, v1alpha1.Check{
+			ID:       pr.ID,
+			Message:  pr.Message,
+			Success:  pr.Success,
+			Severity: pr.Severity,
+			Category: pr.Category,
+		})
+	}
+
+	for _, cr := range result.PodResult.ContainerResults {
+		var checks []v1alpha1.Check
+		for _, crr := range cr.Results {
+			checks = append(checks, v1alpha1.Check{
+				ID:       crr.ID,
+				Message:  crr.Message,
+				Success:  crr.Success,
+				Severity: crr.Severity,
+				Category: crr.Category,
+			})
+
+		}
+		containerChecks[cr.Name] = checks
+	}
+
+	imageRef, err := p.config.GetPolarisImageRef()
+	if err != nil {
+		return v1alpha1.ConfigAuditResult{}, err
+	}
+
+	version, err := starboard.GetVersionFromImageRef(imageRef)
+	if err != nil {
+		return v1alpha1.ConfigAuditResult{}, err
+	}
+
+	return v1alpha1.ConfigAuditResult{
+		Scanner: v1alpha1.Scanner{
+			Name:    "Polaris",
+			Vendor:  "Fairwinds Ops",
+			Version: version,
+		},
+		Summary:         p.configAuditSummaryFrom(podChecks, containerChecks),
+		UpdateTimestamp: metav1.NewTime(p.clock.Now()),
+		PodChecks:       podChecks,
+		ContainerChecks: containerChecks,
+	}, nil
+}
+
+// TODO Add success checks count to the ConfigAuditSummary
+func (p *plugin) configAuditSummaryFrom(podChecks []v1alpha1.Check, containerChecks map[string][]v1alpha1.Check) v1alpha1.ConfigAuditSummary {
+	var summary v1alpha1.ConfigAuditSummary
+	for _, c := range podChecks {
+		if c.Success {
+			continue
+		}
+		switch c.Severity {
+		case v1alpha1.ConfigAuditDangerSeverity:
+			summary.DangerCount++
+		case v1alpha1.ConfigAuditWarningSeverity:
+			summary.WarningCount++
+		}
+	}
+	for _, checks := range containerChecks {
+		for _, c := range checks {
+			if c.Success {
+				continue
+			}
+			switch c.Severity {
+			case v1alpha1.ConfigAuditDangerSeverity:
+				summary.DangerCount++
+			case v1alpha1.ConfigAuditWarningSeverity:
+				summary.WarningCount++
+			}
+		}
+	}
+	return summary
+}
diff --git a/pkg/polaris/scanner.go b/pkg/polaris/scanner.go
deleted file mode 100644
index 7563340..0000000
--- a/pkg/polaris/scanner.go
+++ /dev/null
@@ -1,202 +0,0 @@
-package polaris
-
-import (
-	"context"
-	"fmt"
-
-	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
-	"github.com/aquasecurity/starboard/pkg/configauditreport"
-	"github.com/aquasecurity/starboard/pkg/kube"
-	"github.com/aquasecurity/starboard/pkg/kube/pod"
-	"github.com/aquasecurity/starboard/pkg/runner"
-	"github.com/aquasecurity/starboard/pkg/scanners"
-	"github.com/aquasecurity/starboard/pkg/starboard"
-	"github.com/google/uuid"
-	batchv1 "k8s.io/api/batch/v1"
-	corev1 "k8s.io/api/core/v1"
-	"k8s.io/apimachinery/pkg/api/resource"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/runtime"
-	"k8s.io/apimachinery/pkg/runtime/schema"
-	"k8s.io/client-go/kubernetes"
-	"k8s.io/klog"
-	"k8s.io/utils/pointer"
-)
-
-const (
-	polarisContainerName = "polaris"
-	configVolume         = "config"
-)
-
-type Config interface {
-	GetPolarisImageRef() (string, error)
-}
-
-type Scanner struct {
-	scheme    *runtime.Scheme
-	clientset kubernetes.Interface
-	config    Config
-	opts      kube.ScannerOpts
-	pods      *pod.Manager
-	converter Converter
-}
-
-func NewScanner(
-	scheme *runtime.Scheme,
-	clientset kubernetes.Interface,
-	config Config,
-	opts kube.ScannerOpts,
-) *Scanner {
-	return &Scanner{
-		scheme:    scheme,
-		config:    config,
-		clientset: clientset,
-		opts:      opts,
-		pods:      pod.NewPodManager(clientset),
-		converter: NewConverter(config),
-	}
-}
-
-func (s *Scanner) Scan(ctx context.Context, workload kube.Object, gvk schema.GroupVersionKind) (v1alpha1.ConfigAuditReport, error) {
-	klog.V(3).Infof("Getting Pod template for workload: %v", workload)
-
-	_, owner, err := s.pods.GetPodSpecByWorkload(ctx, workload)
-	if err != nil {
-		return v1alpha1.ConfigAuditReport{}, err
-	}
-
-	klog.V(3).Infof("Scanning with options: %+v", s.opts)
-	job, err := s.preparePolarisJob(workload, gvk)
-	if err != nil {
-		return v1alpha1.ConfigAuditReport{}, err
-	}
-
-	err = runner.New().Run(ctx, kube.NewRunnableJob(s.scheme, s.clientset, job))
-	if err != nil {
-		s.pods.LogRunnerErrors(ctx, job)
-		return v1alpha1.ConfigAuditReport{}, fmt.Errorf("running polaris job: %w", err)
-	}
-
-	defer func() {
-		if !s.opts.DeleteScanJob {
-			klog.V(3).Infof("Skipping scan job deletion: %s/%s", job.Namespace, job.Name)
-			return
-		}
-		klog.V(3).Infof("Deleting scan job: %s/%s", job.Namespace, job.Name)
-		background := metav1.DeletePropagationBackground
-		_ = s.clientset.BatchV1().Jobs(job.Namespace).Delete(ctx, job.Name, metav1.DeleteOptions{
-			PropagationPolicy: &background,
-		})
-	}()
-
-	klog.V(3).Infof("Getting logs for %s container in job: %s/%s", polarisContainerName,
-		job.Namespace, job.Name)
-	logsReader, err := s.pods.GetContainerLogsByJob(ctx, job, polarisContainerName)
-	if err != nil {
-		return v1alpha1.ConfigAuditReport{}, fmt.Errorf("getting logs: %w", err)
-	}
-
-	result, err := s.converter.Convert(logsReader)
-	defer func() {
-		_ = logsReader.Close()
-	}()
-
-	return configauditreport.NewBuilder(s.scheme).
-		Owner(owner).
-		Result(result).
-		Get()
-}
-
-func (s *Scanner) sourceNameFrom(workload kube.Object, gvk schema.GroupVersionKind) string {
-	group := gvk.Group
-	if len(group) > 0 {
-		group = "." + group
-	}
-	return fmt.Sprintf("%s/%s%s/%s/%s",
-		workload.Namespace,
-		gvk.Kind,
-		group,
-		gvk.Version,
-		workload.Name,
-	)
-}
-
-func (s *Scanner) preparePolarisJob(workload kube.Object, gvk schema.GroupVersionKind) (*batchv1.Job, error) {
-	imageRef, err := s.config.GetPolarisImageRef()
-	if err != nil {
-		return nil, err
-	}
-	sourceName := s.sourceNameFrom(workload, gvk)
-	return &batchv1.Job{
-		ObjectMeta: metav1.ObjectMeta{
-			Name:      uuid.New().String(),
-			Namespace: starboard.NamespaceName,
-			Labels: map[string]string{
-				kube.LabelResourceKind:      string(workload.Kind),
-				kube.LabelResourceName:      workload.Name,
-				kube.LabelResourceNamespace: workload.Namespace,
-			},
-		},
-		Spec: batchv1.JobSpec{
-			BackoffLimit:          pointer.Int32Ptr(0),
-			Completions:           pointer.Int32Ptr(1),
-			ActiveDeadlineSeconds: scanners.GetActiveDeadlineSeconds(s.opts.ScanJobTimeout),
-			Template: corev1.PodTemplateSpec{
-				ObjectMeta: metav1.ObjectMeta{
-					Labels: map[string]string{
-						kube.LabelResourceKind:      string(workload.Kind),
-						kube.LabelResourceName:      workload.Name,
-						kube.LabelResourceNamespace: workload.Namespace,
-					},
-				},
-				Spec: corev1.PodSpec{
-					ServiceAccountName:           starboard.ServiceAccountName,
-					AutomountServiceAccountToken: pointer.BoolPtr(true),
-					RestartPolicy:                corev1.RestartPolicyNever,
-					Affinity:                     starboard.LinuxNodeAffinity(),
-					Volumes: []corev1.Volume{
-						{
-							Name: configVolume,
-							VolumeSource: corev1.VolumeSource{
-								ConfigMap: &corev1.ConfigMapVolumeSource{
-									LocalObjectReference: corev1.LocalObjectReference{
-										Name: starboard.ConfigMapName,
-									},
-								},
-							},
-						},
-					},
-					Containers: []corev1.Container{
-						{
-							Name:                     polarisContainerName,
-							Image:                    imageRef,
-							ImagePullPolicy:          corev1.PullIfNotPresent,
-							TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
-							Resources: corev1.ResourceRequirements{
-								Limits: corev1.ResourceList{
-									corev1.ResourceCPU:    resource.MustParse("300m"),
-									corev1.ResourceMemory: resource.MustParse("300M"),
-								},
-								Requests: corev1.ResourceList{
-									corev1.ResourceCPU:    resource.MustParse("50m"),
-									corev1.ResourceMemory: resource.MustParse("50M"),
-								},
-							},
-							VolumeMounts: []corev1.VolumeMount{
-								{
-									Name:      configVolume,
-									MountPath: "/etc/starboard",
-								},
-							},
-							Command: []string{"polaris"},
-							Args: []string{"audit",
-								"--log-level", "error",
-								"--config", "/etc/starboard/polaris.config.yaml",
-								"--resource", sourceName},
-						},
-					},
-				},
-			},
-		},
-	}, nil
-}
diff --git a/pkg/vulnerabilityreport/scanner.go b/pkg/vulnerabilityreport/scanner.go
index 93811d0..fe1a6d2 100644
--- a/pkg/vulnerabilityreport/scanner.go
+++ b/pkg/vulnerabilityreport/scanner.go
@@ -29,9 +29,9 @@ type Scanner struct {
 	scheme    *runtime.Scheme
 	clientset kubernetes.Interface
 	opts      kube.ScannerOpts
+	plugin    Plugin
 	pods      *pod.Manager
 	ext.IDGenerator
-	Plugin
 	kube.SecretsReader
 }
 
@@ -49,7 +49,7 @@ func NewScanner(
 		scheme:        scheme,
 		clientset:     clientset,
 		opts:          opts,
-		Plugin:        plugin,
+		plugin:        plugin,
 		pods:          pod.NewPodManager(clientset),
 		IDGenerator:   ext.NewGoogleUUIDGenerator(),
 		SecretsReader: kube.NewSecretsReader(clientset),
@@ -101,11 +101,6 @@ func (s *Scanner) Scan(ctx context.Context, workload kube.Object) ([]v1alpha1.Vu
 
 	klog.V(3).Infof("Scan job completed: %s/%s", job.Namespace, job.Name)
 
-	job, err = s.clientset.BatchV1().Jobs(job.Namespace).Get(ctx, job.Name, metav1.GetOptions{})
-	if err != nil {
-		return nil, fmt.Errorf("getting scan job: %w", err)
-	}
-
 	return s.getVulnerabilityReportsByScanJob(ctx, job, owner)
 }
 
@@ -118,7 +113,7 @@ func (s *Scanner) getCredentials(ctx context.Context, spec corev1.PodSpec, ns st
 }
 
 func (s *Scanner) prepareScanJob(workload kube.Object, spec corev1.PodSpec, credentials map[string]docker.Auth) (*batchv1.Job, []*corev1.Secret, error) {
-	templateSpec, secrets, err := s.GetScanJobSpec(spec, credentials)
+	templateSpec, secrets, err := s.plugin.GetScanJobSpec(spec, credentials)
 	if err != nil {
 		return nil, nil, err
 	}
@@ -177,7 +172,7 @@ func (s *Scanner) getVulnerabilityReportsByScanJob(ctx context.Context, job *bat
 		if err != nil {
 			return nil, err
 		}
-		result, err := s.ParseVulnerabilityScanResult(containerImages[c.Name], logReader)
+		result, err := s.plugin.ParseVulnerabilityScanResult(containerImages[c.Name], logReader)
 
 		report, err := NewBuilder(s.scheme).
 			Owner(owner).


Test patch:
diff --git a/pkg/configauditreport/scanner_test.go b/pkg/configauditreport/scanner_test.go
new file mode 100644
index 0000000..0b4fdf8
--- /dev/null
+++ b/pkg/configauditreport/scanner_test.go
@@ -0,0 +1,1 @@
+package configauditreport_test
diff --git a/pkg/configauditreport/writer_test.go b/pkg/configauditreport/writer_test.go
index 87742fb..b05471a 100644
--- a/pkg/configauditreport/writer_test.go
+++ b/pkg/configauditreport/writer_test.go
@@ -4,12 +4,11 @@ import (
 	"context"
 	"testing"
 
-	"github.com/stretchr/testify/assert"
-
 	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
 	"github.com/aquasecurity/starboard/pkg/configauditreport"
 	"github.com/aquasecurity/starboard/pkg/generated/clientset/versioned/fake"
 	"github.com/aquasecurity/starboard/pkg/kube"
+	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
diff --git a/pkg/polaris/converter_test.go b/pkg/polaris/converter_test.go
deleted file mode 100644
index d5fccc0..0000000
--- a/pkg/polaris/converter_test.go
+++ /dev/null
@@ -1,68 +0,0 @@
-package polaris_test
-
-import (
-	"os"
-	"testing"
-
-	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
-	"github.com/aquasecurity/starboard/pkg/polaris"
-	"github.com/aquasecurity/starboard/pkg/starboard"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-)
-
-func TestConverter_Convert(t *testing.T) {
-	// FIXME Deterministic assert!
-	t.Skip("Fix me - the assert is not deterministic")
-	file, err := os.Open("testdata/polaris-report.json")
-	require.NoError(t, err)
-	defer func() {
-		_ = file.Close()
-	}()
-
-	reports, err := polaris.NewConverter(starboard.ConfigData{}).Convert(file)
-	require.NoError(t, err)
-	assert.Equal(t, []v1alpha1.ConfigAuditResult{
-		{
-			Scanner: v1alpha1.Scanner{
-				Name:    "Polaris",
-				Vendor:  "Fairwinds",
-				Version: "latest",
-			},
-			PodChecks: []v1alpha1.Check{
-				{
-					ID:       "hostIPCSet",
-					Message:  "Host IPC is not configured",
-					Success:  true,
-					Severity: "error",
-					Category: "Security",
-				},
-				{
-					ID:       "hostNetworkSet",
-					Message:  "Host network is not configured",
-					Success:  true,
-					Severity: "warning",
-					Category: "Networking",
-				},
-			},
-			ContainerChecks: map[string][]v1alpha1.Check{
-				"db": {
-					{
-						ID:       "cpuLimitsMissing",
-						Message:  "CPU limits are set",
-						Success:  true,
-						Severity: "warning",
-						Category: "Resources",
-					},
-					{
-						ID:       "cpuRequestsMissing",
-						Message:  "CPU requests are set",
-						Success:  true,
-						Severity: "warning",
-						Category: "Resources",
-					},
-				},
-			},
-		},
-	}, reports)
-}
diff --git a/pkg/polaris/plugin_test.go b/pkg/polaris/plugin_test.go
new file mode 100644
index 0000000..77b92a8
--- /dev/null
+++ b/pkg/polaris/plugin_test.go
@@ -0,0 +1,177 @@
+package polaris_test
+
+import (
+	"os"
+	"testing"
+	"time"
+
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/ext"
+	"github.com/aquasecurity/starboard/pkg/kube"
+	"github.com/aquasecurity/starboard/pkg/polaris"
+	"github.com/aquasecurity/starboard/pkg/starboard"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	corev1 "k8s.io/api/core/v1"
+	"k8s.io/apimachinery/pkg/api/resource"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/utils/pointer"
+)
+
+var (
+	fixedTime  = time.Now()
+	fixedClock = ext.NewFixedClock(fixedTime)
+)
+
+func TestPlugin_GetScanJobSpec(t *testing.T) {
+	testCases := []struct {
+		name string
+
+		config   starboard.ConfigData
+		workload kube.Object
+		gvk      schema.GroupVersionKind
+
+		expectedJobSpec corev1.PodSpec
+	}{
+		{
+			name: "Should return job spec for Deployment",
+			config: starboard.ConfigData{
+				"polaris.imageRef": "quay.io/fairwinds/polaris:3.0",
+			},
+			workload: kube.Object{
+				Name:      "nginx",
+				Namespace: corev1.NamespaceDefault,
+				Kind:      kube.KindDeployment,
+			},
+			gvk: schema.GroupVersionKind{
+				Group:   "apps",
+				Version: "v1",
+				Kind:    "Deployment",
+			},
+			expectedJobSpec: corev1.PodSpec{
+				ServiceAccountName:           starboard.ServiceAccountName,
+				AutomountServiceAccountToken: pointer.BoolPtr(true),
+				RestartPolicy:                corev1.RestartPolicyNever,
+				Affinity:                     starboard.LinuxNodeAffinity(),
+				Volumes: []corev1.Volume{
+					{
+						Name: "config",
+						VolumeSource: corev1.VolumeSource{
+							ConfigMap: &corev1.ConfigMapVolumeSource{
+								LocalObjectReference: corev1.LocalObjectReference{
+									Name: starboard.ConfigMapName,
+								},
+							},
+						},
+					},
+				},
+				Containers: []corev1.Container{
+					{
+						Name:                     "polaris",
+						Image:                    "quay.io/fairwinds/polaris:3.0",
+						ImagePullPolicy:          corev1.PullIfNotPresent,
+						TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
+						Resources: corev1.ResourceRequirements{
+							Limits: corev1.ResourceList{
+								corev1.ResourceCPU:    resource.MustParse("300m"),
+								corev1.ResourceMemory: resource.MustParse("300M"),
+							},
+							Requests: corev1.ResourceList{
+								corev1.ResourceCPU:    resource.MustParse("50m"),
+								corev1.ResourceMemory: resource.MustParse("50M"),
+							},
+						},
+						VolumeMounts: []corev1.VolumeMount{
+							{
+								Name:      "config",
+								MountPath: "/etc/starboard",
+							},
+						},
+						Command: []string{"polaris"},
+						Args: []string{
+							"audit",
+							"--log-level", "error",
+							"--config", "/etc/starboard/polaris.config.yaml",
+							"--resource", "default/Deployment.apps/v1/nginx",
+						},
+					},
+				},
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			plugin := polaris.NewPlugin(fixedClock, tc.config)
+			jobSpec, err := plugin.GetScanJobSpec(tc.workload, tc.gvk)
+			require.NoError(t, err, tc.name)
+			assert.Equal(t, tc.expectedJobSpec, jobSpec, tc.name)
+		})
+	}
+
+}
+
+func TestPlugin_GetContainerName(t *testing.T) {
+	plugin := polaris.NewPlugin(fixedClock, starboard.ConfigData{})
+	assert.Equal(t, "polaris", plugin.GetContainerName())
+}
+
+func TestPlugin_ParseConfigAuditResult(t *testing.T) {
+	testReport, err := os.Open("testdata/polaris-report.json")
+	require.NoError(t, err)
+	defer func() {
+		_ = testReport.Close()
+	}()
+
+	config := starboard.ConfigData{
+		"polaris.imageRef": "quay.io/fairwinds/polaris:3.0",
+	}
+	plugin := polaris.NewPlugin(fixedClock, config)
+	result, err := plugin.ParseConfigAuditResult(testReport)
+	require.NoError(t, err)
+	assert.Equal(t, metav1.NewTime(fixedTime), result.UpdateTimestamp)
+	assert.Equal(t, v1alpha1.Scanner{
+		Name:    "Polaris",
+		Vendor:  "Fairwinds Ops",
+		Version: "3.0",
+	}, result.Scanner)
+	assert.Equal(t, v1alpha1.ConfigAuditSummary{
+		DangerCount:  1,
+		WarningCount: 1,
+	}, result.Summary)
+	assert.ElementsMatch(t, []v1alpha1.Check{
+		{
+			ID:       "hostIPCSet",
+			Message:  "Host IPC is not configured",
+			Success:  false,
+			Severity: "danger",
+			Category: "Security",
+		},
+		{
+			ID:       "hostNetworkSet",
+			Message:  "Host network is not configured",
+			Success:  true,
+			Severity: "warning",
+			Category: "Networking",
+		},
+	}, result.PodChecks)
+	assert.Equal(t, map[string][]v1alpha1.Check{
+		"db": {
+			{
+				ID:       "cpuLimitsMissing",
+				Message:  "CPU limits are set",
+				Success:  false,
+				Severity: "warning",
+				Category: "Resources",
+			},
+			{
+				ID:       "cpuRequestsMissing",
+				Message:  "CPU requests are set",
+				Success:  true,
+				Severity: "warning",
+				Category: "Resources",
+			},
+		},
+	}, result.ContainerChecks)
+}
diff --git a/pkg/polaris/testdata/polaris-report.json b/pkg/polaris/testdata/polaris-report.json
index 8d88a1b..63806d8 100644
--- a/pkg/polaris/testdata/polaris-report.json
+++ b/pkg/polaris/testdata/polaris-report.json
@@ -23,8 +23,8 @@
           "hostIPCSet": {
             "ID": "hostIPCSet",
             "Message": "Host IPC is not configured",
-            "Success": true,
-            "Severity": "error",
+            "Success": false,
+            "Severity": "danger",
             "Category": "Security"
           },
           "hostNetworkSet": {
@@ -42,7 +42,7 @@
               "cpuLimitsMissing": {
                 "ID": "cpuLimitsMissing",
                 "Message": "CPU limits are set",
-                "Success": true,
+                "Success": false,
                 "Severity": "warning",
                 "Category": "Resources"
               },