Repository: aquasecurity/starboard
Pull request #335 (2021-01-13 11:45:43)

refactor: Cleanup polaris package
Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/335?src=pr&el=h1) Report
> Merging [#335](https://codecov.io/gh/aquasecurity/starboard/pull/335?src=pr&el=desc) (b7bc85f) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/0882c9e5159684f16caeac613fb3474b6b7c3f45?el=desc) (0882c9e) will **decrease** coverage by `35.82%`.
> The diff coverage is `52.47%`.

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

```diff
@@             Coverage Diff             @@
##             main     #335       +/-   ##
===========================================
- Coverage   65.83%   30.01%   -35.83%     
===========================================
  Files          57       53        -4     
  Lines        2810     2592      -218     
===========================================
- Hits         1850      778     -1072     
- Misses        720     1756     +1036     
+ Partials      240       58      -182     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/335?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/cmd/scan\_configaudit.go](https://codecov.io/gh/aquasecurity/starboard/pull/335/diff?src=pr&el=tree#diff-cGtnL2NtZC9zY2FuX2NvbmZpZ2F1ZGl0Lmdv) | `0.00% <0.00%> (-51.36%)` | :arrow_down: |
| [pkg/configauditreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/335/diff?src=pr&el=tree#diff-cGtnL2NvbmZpZ2F1ZGl0cmVwb3J0L3NjYW5uZXIuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/polaris/plugin.go](https://codecov.io/gh/aquasecurity/starboard/pull/335/diff?src=pr&el=tree#diff-cGtnL3BvbGFyaXMvcGx1Z2luLmdv) | `84.87% <84.87%> (ø)` | |
| [pkg/configauditreport/writer.go](https://codecov.io/gh/aquasecurity/starboard/pull/335/diff?src=pr&el=tree#diff-cGtnL2NvbmZpZ2F1ZGl0cmVwb3J0L3dyaXRlci5nbw==) | `86.66% <100.00%> (ø)` | |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/335/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/335/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/335/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/335/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/335/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/335/diff?src=pr&el=tree#diff-cGtnL2NtZC92ZXJzaW9uLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [34 more](https://codecov.io/gh/aquasecurity/starboard/pull/335/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/335?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/335?src=pr&el=footer). Last update [0882c9e...b7bc85f](https://codecov.io/gh/aquasecurity/starboard/pull/335?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/pkg/cmd/scan_configaudit.go b/pkg/cmd/scan_configaudit.go
index 377d963..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,15 +61,17 @@ func ScanConfigAuditReports(cf *genericclioptions.ConfigFlags) func(cmd *cobra.C
 		if err != nil {
 			return err
 		}
-		plugin := polaris.NewPlugin(config)
-		report, err := polaris.NewScanner(starboard.NewScheme(), kubeClientset, opts, plugin).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
index bcee232..1b98500 100644
--- a/pkg/configauditreport/plugin.go
+++ b/pkg/configauditreport/plugin.go
@@ -16,5 +16,7 @@ import (
 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/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 647118b..0000000
--- a/pkg/polaris/scanner.go
+++ /dev/null
@@ -1,235 +0,0 @@
-package polaris
-
-import (
-	"context"
-	"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/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"
-	"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"
-)
-
-// TODO Move the Scanner struct to configauditreport package. It's generic and can be used with similar tools to Polaris.
-type Scanner struct {
-	scheme    *runtime.Scheme
-	clientset kubernetes.Interface
-	opts      kube.ScannerOpts
-	pods      *pod.Manager
-	plugin    configauditreport.Plugin
-	ext.IDGenerator
-}
-
-func NewScanner(
-	scheme *runtime.Scheme,
-	clientset kubernetes.Interface,
-	opts kube.ScannerOpts,
-	plugin configauditreport.Plugin,
-) *Scanner {
-	return &Scanner{
-		scheme:      scheme,
-		clientset:   clientset,
-		opts:        opts,
-		pods:        pod.NewPodManager(clientset),
-		plugin:      plugin,
-		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.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.plugin.ParseConfigAuditResult(logsReader)
-	defer func() {
-		_ = logsReader.Close()
-	}()
-
-	return configauditreport.NewBuilder(s.scheme).
-		Owner(owner).
-		Result(result).
-		Get()
-}
-
-func (s *Scanner) preparePolarisJob(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
-}
-
-///// Polaris specific code that should stay in this package /////
-
-type Config interface {
-	GetPolarisImageRef() (string, error)
-}
-
-type plugin struct {
-	config    Config
-	converter Converter
-}
-
-// NewPlugin constructs a new configauditreport.Plugin, which is using an
-// official Polaris container image to audit Kubernetes workloads.
-func NewPlugin(config Config) configauditreport.Plugin {
-	return &plugin{
-		config:    config,
-		converter: NewConverter(config),
-	}
-}
-
-func (s *plugin) GetScanJobSpec(workload kube.Object, gvk schema.GroupVersionKind) (corev1.PodSpec, error) {
-	imageRef, err := s.config.GetPolarisImageRef()
-	if err != nil {
-		return corev1.PodSpec{}, err
-	}
-	sourceName := s.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 (s *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 (s *plugin) ParseConfigAuditResult(logsReader io.ReadCloser) (v1alpha1.ConfigAuditResult, error) {
-	return s.converter.Convert(logsReader)
-}


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/scanner_test.go b/pkg/polaris/plugin_test.go
similarity index 61%
rename from pkg/polaris/scanner_test.go
rename to pkg/polaris/plugin_test.go
index f4bffbd..c868d8b 100644
--- a/pkg/polaris/scanner_test.go
+++ b/pkg/polaris/plugin_test.go
@@ -1,8 +1,12 @@
 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"
@@ -10,10 +14,16 @@ import (
 	"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
@@ -93,7 +103,7 @@ func TestPlugin_GetScanJobSpec(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			plugin := polaris.NewPlugin(tc.config)
+			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)
@@ -101,3 +111,66 @@ func TestPlugin_GetScanJobSpec(t *testing.T) {
 	}
 
 }
+
+func TestPlugin_GetContainerName(t *testing.T) {
+	plugin := polaris.NewPlugin(fixedClock, starboard.ConfigData{})
+	assert.Equal(t, "polaris", plugin.GetContainerName())
+}
+
+func TestPlugin_ParseConfigAuditResult(t *testing.T) {
+	file, err := os.Open("testdata/polaris-report.json")
+	require.NoError(t, err)
+	defer func() {
+		_ = file.Close()
+	}()
+
+	config := starboard.ConfigData{
+		"polaris.imageRef": "quay.io/fairwinds/polaris:3.0",
+	}
+	plugin := polaris.NewPlugin(fixedClock, config)
+	result, err := plugin.ParseConfigAuditResult(file)
+	require.NoError(t, err)
+	assert.Equal(t, v1alpha1.ConfigAuditResult{
+
+		UpdateTimestamp: metav1.NewTime(fixedTime),
+		Scanner: v1alpha1.Scanner{
+			Name:    "Polaris",
+			Vendor:  "Fairwinds Ops",
+			Version: "3.0",
+		},
+		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",
+				},
+			},
+		},
+	}, result)
+}