Repository: aquasecurity/starboard
Pull request #405 (2021-02-18 11:34:51)

refactor: Separate kube-bench -specific code
Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/405?src=pr&el=h1) Report
> Merging [#405](https://codecov.io/gh/aquasecurity/starboard/pull/405?src=pr&el=desc) (8861bc0) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/1bb07be9d6fba79fb3e2f1e3e4159b08c83d3038?el=desc) (1bb07be) will **decrease** coverage by `35.95%`.
> The diff coverage is `82.69%`.

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

```diff
@@             Coverage Diff             @@
##             main     #405       +/-   ##
===========================================
- Coverage   67.75%   31.79%   -35.96%     
===========================================
  Files          59       57        -2     
  Lines        3098     3016       -82     
===========================================
- Hits         2099      959     -1140     
- Misses        738     1997     +1259     
+ Partials      261       60      -201     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/405?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/cmd/scan\_ciskubebench.go](https://codecov.io/gh/aquasecurity/starboard/pull/405/diff?src=pr&el=tree#diff-cGtnL2NtZC9zY2FuX2Npc2t1YmViZW5jaC5nbw==) | `0.00% <0.00%> (-56.25%)` | :arrow_down: |
| [pkg/kubebench/io.go](https://codecov.io/gh/aquasecurity/starboard/pull/405/diff?src=pr&el=tree#diff-cGtnL2t1YmViZW5jaC9pby5nbw==) | `78.26% <0.00%> (-7.46%)` | :arrow_down: |
| [pkg/kubebench/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/405/diff?src=pr&el=tree#diff-cGtnL2t1YmViZW5jaC9zY2FubmVyLmdv) | `62.38% <84.86%> (-29.19%)` | :arrow_down: |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/405/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/405/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/405/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/405/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/405/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/405/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/405/diff?src=pr&el=tree#diff-cGtnL2NtZC9rdWJlX2JlbmNoLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [35 more](https://codecov.io/gh/aquasecurity/starboard/pull/405/diff?src=pr&el=tree-more) | |

------

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


Patch:
diff --git a/pkg/cmd/scan_ciskubebench.go b/pkg/cmd/scan_ciskubebench.go
index afd3e84..d72a292 100644
--- a/pkg/cmd/scan_ciskubebench.go
+++ b/pkg/cmd/scan_ciskubebench.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"sync"
 
+	"github.com/aquasecurity/starboard/pkg/ext"
 	"github.com/aquasecurity/starboard/pkg/kubebench"
 	"github.com/aquasecurity/starboard/pkg/starboard"
 	"github.com/spf13/cobra"
@@ -57,7 +58,8 @@ func ScanKubeBenchReports(cf *genericclioptions.ConfigFlags) func(cmd *cobra.Com
 		}
 
 		scheme := starboard.NewScheme()
-		scanner := kubebench.NewScanner(scheme, kubeClientset, config, opts)
+		plugin := kubebench.NewKubeBenchPlugin(ext.NewSystemClock(), config)
+		scanner := kubebench.NewScanner(scheme, kubeClientset, opts, plugin)
 
 		kubeClient, err := client.New(kubeConfig, client.Options{
 			Scheme: scheme,
diff --git a/pkg/kubebench/converter.go b/pkg/kubebench/converter.go
deleted file mode 100644
index 972bc7f..0000000
--- a/pkg/kubebench/converter.go
+++ /dev/null
@@ -1,69 +0,0 @@
-package kubebench
-
-import (
-	"encoding/json"
-	"io"
-
-	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
-	"github.com/aquasecurity/starboard/pkg/ext"
-	"github.com/aquasecurity/starboard/pkg/starboard"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-type Converter struct {
-	ext.Clock
-	Config
-}
-
-func (c *Converter) Convert(reader io.Reader) (v1alpha1.CISKubeBenchOutput, error) {
-	output := &struct {
-		Controls []v1alpha1.CISKubeBenchSection `json:"Controls"`
-	}{}
-
-	decoder := json.NewDecoder(reader)
-	err := decoder.Decode(output)
-	if err != nil {
-		return v1alpha1.CISKubeBenchOutput{}, err
-	}
-
-	imageRef, err := c.Config.GetKubeBenchImageRef()
-	if err != nil {
-		return v1alpha1.CISKubeBenchOutput{}, err
-	}
-	version, err := starboard.GetVersionFromImageRef(imageRef)
-	if err != nil {
-		return v1alpha1.CISKubeBenchOutput{}, err
-	}
-
-	return v1alpha1.CISKubeBenchOutput{
-		Scanner: v1alpha1.Scanner{
-			Name:    "kube-bench",
-			Vendor:  "Aqua Security",
-			Version: version,
-		},
-		Summary:         c.summary(output.Controls),
-		UpdateTimestamp: metav1.NewTime(c.Clock.Now()),
-		Sections:        output.Controls,
-	}, nil
-}
-
-func (c *Converter) summary(sections []v1alpha1.CISKubeBenchSection) v1alpha1.CISKubeBenchSummary {
-	totalPass := 0
-	totalInfo := 0
-	totalWarn := 0
-	totalFail := 0
-
-	for _, section := range sections {
-		totalPass += section.TotalPass
-		totalInfo += section.TotalInfo
-		totalWarn += section.TotalWarn
-		totalFail += section.TotalFail
-	}
-
-	return v1alpha1.CISKubeBenchSummary{
-		PassCount: totalPass,
-		InfoCount: totalInfo,
-		WarnCount: totalWarn,
-		FailCount: totalFail,
-	}
-}
diff --git a/pkg/kubebench/io.go b/pkg/kubebench/io.go
index e32dd86..c8abdcc 100644
--- a/pkg/kubebench/io.go
+++ b/pkg/kubebench/io.go
@@ -61,6 +61,9 @@ func (w *rw) FindByOwner(ctx context.Context, node kube.Object) (*v1alpha1.CISKu
 		Name: node.Name,
 	}, report)
 	if err != nil {
+		if errors.IsNotFound(err) {
+			return nil, nil
+		}
 		return nil, err
 	}
 	return report, nil
diff --git a/pkg/kubebench/scanner.go b/pkg/kubebench/scanner.go
index 9de182b..799840f 100644
--- a/pkg/kubebench/scanner.go
+++ b/pkg/kubebench/scanner.go
@@ -2,7 +2,9 @@ package kubebench
 
 import (
 	"context"
+	"encoding/json"
 	"fmt"
+	"io"
 
 	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
 	"github.com/aquasecurity/starboard/pkg/ext"
@@ -23,41 +25,44 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
 )
 
-const (
-	kubeBenchContainerName = "kube-bench"
-)
+// Plugin defines the interface between Starboard and Kubernetes configuration
+// checker with CIS Kubernetes Benchmarks.
+type Plugin interface {
 
-type Config interface {
-	GetKubeBenchImageRef() (string, error)
+	// GetScanJobSpec describes the pod that will be created by Starboard when
+	// it schedules a Kubernetes job to audit the configuration of the specified
+	// node.
+	GetScanJobSpec(node corev1.Node) (corev1.PodSpec, error)
+
+	// ParseCISKubeBenchOutput is a callback to parse and convert logs of
+	// the pod controlled by the scan job to v1alpha1.CISKubeBenchOutput.
+	ParseCISKubeBenchOutput(logsStream io.ReadCloser) (v1alpha1.CISKubeBenchOutput, error)
+
+	GetContainerName() string
 }
 
 type Scanner struct {
 	scheme     *runtime.Scheme
-	config     Config
 	opts       kube.ScannerOpts
 	clientset  kubernetes.Interface
 	pods       *pod.Manager
 	logsReader kube.LogsReader
-	converter  *Converter
+	plugin     Plugin
 }
 
 func NewScanner(
 	scheme *runtime.Scheme,
 	clientset kubernetes.Interface,
-	config Config,
 	opts kube.ScannerOpts,
+	plugin Plugin,
 ) *Scanner {
 	return &Scanner{
 		scheme:     scheme,
-		config:     config,
 		opts:       opts,
 		clientset:  clientset,
 		pods:       pod.NewPodManager(clientset),
 		logsReader: kube.NewLogsReader(clientset),
-		converter: &Converter{
-			Clock:  ext.NewSystemClock(),
-			Config: config,
-		},
+		plugin:     plugin,
 	}
 }
 
@@ -80,17 +85,18 @@ func (s *Scanner) Scan(ctx context.Context, node corev1.Node) (v1alpha1.CISKubeB
 			return
 		}
 		// 5. Delete the kube-bench Job
-		klog.V(3).Infof("Deleting job: %s/%s", job.Namespace, job.Name)
+		klog.V(3).Infof("Deleting job %q", 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()
 	// 3. Get kube-bench JSON output from the kube-bench Pod
-	klog.V(3).Infof("Getting logs for %s container in job: %s/%s", kubeBenchContainerName,
-		job.Namespace, job.Name)
-	logsStream, err := s.logsReader.GetLogsByJobAndContainerName(ctx, job, kubeBenchContainerName)
+	klog.V(3).Infof("Getting logs for %q container in job %q", containerName,
+		job.Namespace+"/"+job.Name)
+	logsStream, err := s.logsReader.GetLogsByJobAndContainerName(ctx, job, containerName)
 	if err != nil {
 		return v1alpha1.CISKubeBenchReport{}, fmt.Errorf("getting logs: %w", err)
 	}
@@ -99,7 +105,7 @@ func (s *Scanner) Scan(ctx context.Context, node corev1.Node) (v1alpha1.CISKubeB
 	}()
 
 	// 4. Parse the CISBenchmarkReport from the logs Reader
-	output, err := s.converter.Convert(logsStream)
+	output, err := s.plugin.ParseCISKubeBenchOutput(logsStream)
 	if err != nil {
 		return v1alpha1.CISKubeBenchReport{}, err
 	}
@@ -124,18 +130,18 @@ func (s *Scanner) Scan(ctx context.Context, node corev1.Node) (v1alpha1.CISKubeB
 }
 
 func (s *Scanner) prepareKubeBenchJob(node corev1.Node) (*batchv1.Job, error) {
-	imageRef, err := s.config.GetKubeBenchImageRef()
+	templateSpec, err := s.plugin.GetScanJobSpec(node)
 	if err != nil {
 		return nil, err
 	}
+
 	return &batchv1.Job{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      uuid.New().String(),
 			Namespace: starboard.NamespaceName,
 			Labels: labels.Set{
-				"app.kubernetes.io/name": "starboard-cli",
-				kube.LabelResourceKind:   string(kube.KindNode),
-				kube.LabelResourceName:   node.Name,
+				kube.LabelResourceKind: string(kube.KindNode),
+				kube.LabelResourceName: node.Name,
 			},
 		},
 		Spec: batchv1.JobSpec{
@@ -145,123 +151,209 @@ func (s *Scanner) prepareKubeBenchJob(node corev1.Node) (*batchv1.Job, error) {
 			Template: corev1.PodTemplateSpec{
 				ObjectMeta: metav1.ObjectMeta{
 					Labels: labels.Set{
-						"app.kubernetes.io/name": "starboard-cli",
-						kube.LabelResourceKind:   string(kube.KindNode),
-						kube.LabelResourceName:   node.Name,
+						kube.LabelResourceKind: string(kube.KindNode),
+						kube.LabelResourceName: node.Name,
 					},
 				},
-				Spec: corev1.PodSpec{
-					ServiceAccountName:           starboard.ServiceAccountName,
-					AutomountServiceAccountToken: pointer.BoolPtr(true),
-					RestartPolicy:                corev1.RestartPolicyNever,
-					HostPID:                      true,
-					NodeName:                     node.Name,
-					SecurityContext: &corev1.PodSecurityContext{
-						RunAsUser:  pointer.Int64Ptr(0),
-						RunAsGroup: pointer.Int64Ptr(0),
-						SeccompProfile: &corev1.SeccompProfile{
-							Type: corev1.SeccompProfileTypeRuntimeDefault,
-						},
+				Spec: templateSpec,
+			},
+		},
+	}, nil
+}
+
+const (
+	kubeBenchContainerName = "kube-bench"
+)
+
+type Config interface {
+	GetKubeBenchImageRef() (string, error)
+}
+
+type kubeBenchPlugin struct {
+	clock  ext.Clock
+	config Config
+}
+
+// NewKubeBenchPlugin constructs a new Plugin, which is using an official
+// Kube-Bench container image, with the specified Config.
+func NewKubeBenchPlugin(clock ext.Clock, config Config) Plugin {
+	return &kubeBenchPlugin{
+		clock:  clock,
+		config: config,
+	}
+}
+
+func (k *kubeBenchPlugin) GetScanJobSpec(node corev1.Node) (corev1.PodSpec, error) {
+	imageRef, err := k.config.GetKubeBenchImageRef()
+	if err != nil {
+		return corev1.PodSpec{}, err
+	}
+	return corev1.PodSpec{
+		ServiceAccountName:           starboard.ServiceAccountName,
+		AutomountServiceAccountToken: pointer.BoolPtr(true),
+		RestartPolicy:                corev1.RestartPolicyNever,
+		HostPID:                      true,
+		NodeName:                     node.Name,
+		SecurityContext: &corev1.PodSecurityContext{
+			RunAsUser:  pointer.Int64Ptr(0),
+			RunAsGroup: pointer.Int64Ptr(0),
+			SeccompProfile: &corev1.SeccompProfile{
+				Type: corev1.SeccompProfileTypeRuntimeDefault,
+			},
+		},
+		Volumes: []corev1.Volume{
+			{
+				Name: "var-lib-etcd",
+				VolumeSource: corev1.VolumeSource{
+					HostPath: &corev1.HostPathVolumeSource{
+						Path: "/var/lib/etcd",
 					},
-					Volumes: []corev1.Volume{
-						{
-							Name: "var-lib-etcd",
-							VolumeSource: corev1.VolumeSource{
-								HostPath: &corev1.HostPathVolumeSource{
-									Path: "/var/lib/etcd",
-								},
-							},
-						},
-						{
-							Name: "var-lib-kubelet",
-							VolumeSource: corev1.VolumeSource{
-								HostPath: &corev1.HostPathVolumeSource{
-									Path: "/var/lib/kubelet",
-								},
-							},
-						},
-						{
-							Name: "etc-systemd",
-							VolumeSource: corev1.VolumeSource{
-								HostPath: &corev1.HostPathVolumeSource{
-									Path: "/etc/systemd",
-								},
-							},
-						},
-						{
-							Name: "etc-kubernetes",
-							VolumeSource: corev1.VolumeSource{
-								HostPath: &corev1.HostPathVolumeSource{
-									Path: "/etc/kubernetes",
-								},
-							},
-						},
-						{
-							Name: "usr-bin",
-							VolumeSource: corev1.VolumeSource{
-								HostPath: &corev1.HostPathVolumeSource{
-									Path: "/usr/bin",
-								},
-							},
-						},
+				},
+			},
+			{
+				Name: "var-lib-kubelet",
+				VolumeSource: corev1.VolumeSource{
+					HostPath: &corev1.HostPathVolumeSource{
+						Path: "/var/lib/kubelet",
 					},
-					Containers: []corev1.Container{
-						{
-							Name:                     kubeBenchContainerName,
-							Image:                    imageRef,
-							ImagePullPolicy:          corev1.PullIfNotPresent,
-							TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
-							Command:                  []string{"sh"},
-							Args:                     []string{"-c", "kube-bench --json 2> /dev/null"},
-							SecurityContext: &corev1.SecurityContext{
-								Privileged:               pointer.BoolPtr(false),
-								AllowPrivilegeEscalation: pointer.BoolPtr(false),
-								Capabilities: &corev1.Capabilities{
-									Drop: []corev1.Capability{"all"},
-								},
-								ReadOnlyRootFilesystem: pointer.BoolPtr(true),
-							},
-							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:      "var-lib-etcd",
-									MountPath: "/var/lib/etcd",
-									ReadOnly:  true,
-								},
-								{
-									Name:      "var-lib-kubelet",
-									MountPath: "/var/lib/kubelet",
-									ReadOnly:  true,
-								},
-								{
-									Name:      "etc-systemd",
-									MountPath: "/etc/systemd",
-									ReadOnly:  true,
-								},
-								{
-									Name:      "etc-kubernetes",
-									MountPath: "/etc/kubernetes",
-									ReadOnly:  true,
-								},
-								{
-									Name:      "usr-bin",
-									MountPath: "/usr/local/mount-from-host/bin",
-									ReadOnly:  true,
-								},
-							},
-						},
+				},
+			},
+			{
+				Name: "etc-systemd",
+				VolumeSource: corev1.VolumeSource{
+					HostPath: &corev1.HostPathVolumeSource{
+						Path: "/etc/systemd",
 					},
 				},
 			},
+			{
+				Name: "etc-kubernetes",
+				VolumeSource: corev1.VolumeSource{
+					HostPath: &corev1.HostPathVolumeSource{
+						Path: "/etc/kubernetes",
+					},
+				},
+			},
+			{
+				Name: "usr-bin",
+				VolumeSource: corev1.VolumeSource{
+					HostPath: &corev1.HostPathVolumeSource{
+						Path: "/usr/bin",
+					},
+				},
+			},
+		},
+		Containers: []corev1.Container{
+			{
+				Name:                     kubeBenchContainerName,
+				Image:                    imageRef,
+				ImagePullPolicy:          corev1.PullIfNotPresent,
+				TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
+				Command:                  []string{"sh"},
+				Args:                     []string{"-c", "kube-bench --json 2> /dev/null"},
+				SecurityContext: &corev1.SecurityContext{
+					Privileged:               pointer.BoolPtr(false),
+					AllowPrivilegeEscalation: pointer.BoolPtr(false),
+					Capabilities: &corev1.Capabilities{
+						Drop: []corev1.Capability{"all"},
+					},
+					ReadOnlyRootFilesystem: pointer.BoolPtr(true),
+				},
+				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:      "var-lib-etcd",
+						MountPath: "/var/lib/etcd",
+						ReadOnly:  true,
+					},
+					{
+						Name:      "var-lib-kubelet",
+						MountPath: "/var/lib/kubelet",
+						ReadOnly:  true,
+					},
+					{
+						Name:      "etc-systemd",
+						MountPath: "/etc/systemd",
+						ReadOnly:  true,
+					},
+					{
+						Name:      "etc-kubernetes",
+						MountPath: "/etc/kubernetes",
+						ReadOnly:  true,
+					},
+					{
+						Name:      "usr-bin",
+						MountPath: "/usr/local/mount-from-host/bin",
+						ReadOnly:  true,
+					},
+				},
+			},
+		},
+	}, nil
+}
+
+func (k *kubeBenchPlugin) ParseCISKubeBenchOutput(logsStream io.ReadCloser) (v1alpha1.CISKubeBenchOutput, error) {
+	output := &struct {
+		Controls []v1alpha1.CISKubeBenchSection `json:"Controls"`
+	}{}
+
+	decoder := json.NewDecoder(logsStream)
+	err := decoder.Decode(output)
+	if err != nil {
+		return v1alpha1.CISKubeBenchOutput{}, err
+	}
+
+	imageRef, err := k.config.GetKubeBenchImageRef()
+	if err != nil {
+		return v1alpha1.CISKubeBenchOutput{}, err
+	}
+	version, err := starboard.GetVersionFromImageRef(imageRef)
+	if err != nil {
+		return v1alpha1.CISKubeBenchOutput{}, err
+	}
+
+	return v1alpha1.CISKubeBenchOutput{
+		Scanner: v1alpha1.Scanner{
+			Name:    "kube-bench",
+			Vendor:  "Aqua Security",
+			Version: version,
 		},
+		Summary:         k.summary(output.Controls),
+		UpdateTimestamp: metav1.NewTime(k.clock.Now()),
+		Sections:        output.Controls,
 	}, nil
 }
+
+func (k *kubeBenchPlugin) summary(sections []v1alpha1.CISKubeBenchSection) v1alpha1.CISKubeBenchSummary {
+	totalPass := 0
+	totalInfo := 0
+	totalWarn := 0
+	totalFail := 0
+
+	for _, section := range sections {
+		totalPass += section.TotalPass
+		totalInfo += section.TotalInfo
+		totalWarn += section.TotalWarn
+		totalFail += section.TotalFail
+	}
+
+	return v1alpha1.CISKubeBenchSummary{
+		PassCount: totalPass,
+		InfoCount: totalInfo,
+		WarnCount: totalWarn,
+		FailCount: totalFail,
+	}
+}
+
+func (k *kubeBenchPlugin) GetContainerName() string {
+	return kubeBenchContainerName
+}


Test patch:
diff --git a/pkg/kubebench/converter_test.go b/pkg/kubebench/converter_test.go
deleted file mode 100644
index 751b19c..0000000
--- a/pkg/kubebench/converter_test.go
+++ /dev/null
@@ -1,91 +0,0 @@
-package kubebench_test
-
-import (
-	"encoding/json"
-	"errors"
-	"os"
-	"testing"
-	"time"
-
-	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
-	"github.com/aquasecurity/starboard/pkg/ext"
-	"github.com/aquasecurity/starboard/pkg/kubebench"
-	"github.com/aquasecurity/starboard/pkg/starboard"
-	"github.com/stretchr/testify/assert"
-	"github.com/stretchr/testify/require"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-)
-
-var (
-	fixedTime  = time.Now()
-	fixedClock = ext.NewFixedClock(fixedTime)
-)
-
-func TestConverter_Convert(t *testing.T) {
-	config := starboard.ConfigData{
-		"kube-bench.imageRef": "aquasec/kube-bench:0.3.1",
-	}
-	var testCases = []struct {
-		name string
-		in   string // input File
-		op   string // golden file
-		err  error  // expected error
-	}{
-		{
-			name: "Valid single json object in array",
-			in:   "testdata/valid.json",
-			op:   "testdata/goldenSingle.json",
-			err:  nil,
-		},
-		{
-			name: "invalid json object",
-			in:   "testdata/invalid.json",
-			err:  errors.New("invalid character 'I' looking for beginning of value"),
-		},
-		{
-			name: "Valid multiple json object in array",
-			in:   "testdata/multiObjects.json",
-			op:   "testdata/goldenMultiple.json",
-			err:  nil,
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			inFile, err := os.Open(tc.in)
-			require.NoError(t, err)
-			defer func() {
-				_ = inFile.Close()
-			}()
-
-			converter := &kubebench.Converter{Clock: fixedClock, Config: config}
-			output, err := converter.Convert(inFile)
-
-			switch {
-			case tc.err == nil:
-				require.NoError(t, err)
-				expectedOutput := expectedOutputFrom(t, tc.op)
-				assert.Equal(t, expectedOutput, output, "Converted report does not match expected report")
-			default:
-				assert.EqualError(t, err, tc.err.Error())
-			}
-		})
-	}
-}
-
-func expectedOutputFrom(t *testing.T, fileName string) v1alpha1.CISKubeBenchOutput {
-	t.Helper()
-
-	file, err := os.Open(fileName)
-	require.NoError(t, err)
-	defer file.Close()
-
-	var expectedOutput v1alpha1.CISKubeBenchOutput
-	err = json.NewDecoder(file).Decode(&expectedOutput)
-	require.NoError(t, err)
-
-	// Override time read from file
-	expectedOutput.UpdateTimestamp = metav1.NewTime(fixedTime)
-
-	return expectedOutput
-}
diff --git a/pkg/kubebench/scanner_test.go b/pkg/kubebench/scanner_test.go
new file mode 100644
index 0000000..0062194
--- /dev/null
+++ b/pkg/kubebench/scanner_test.go
@@ -0,0 +1,221 @@
+package kubebench_test
+
+import (
+	"encoding/json"
+	"errors"
+	"os"
+	"testing"
+	"time"
+
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/ext"
+	"github.com/aquasecurity/starboard/pkg/kubebench"
+	"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/utils/pointer"
+)
+
+var (
+	fixedTime  = time.Now()
+	fixedClock = ext.NewFixedClock(fixedTime)
+)
+
+func TestKubeBenchPlugin_GetScanJobSpec(t *testing.T) {
+	config := starboard.ConfigData{
+		"kube-bench.imageRef": "docker.io/aquasec/kube-bench:0.5.0",
+	}
+	node := corev1.Node{
+		ObjectMeta: metav1.ObjectMeta{
+			Name: "control-plane",
+		},
+	}
+	instance := kubebench.NewKubeBenchPlugin(fixedClock, config)
+
+	podSpec, err := instance.GetScanJobSpec(node)
+
+	require.NoError(t, err)
+	assert.Equal(t, corev1.PodSpec{
+		ServiceAccountName:           starboard.ServiceAccountName,
+		AutomountServiceAccountToken: pointer.BoolPtr(true),
+		RestartPolicy:                corev1.RestartPolicyNever,
+		HostPID:                      true,
+		NodeName:                     node.Name,
+		SecurityContext: &corev1.PodSecurityContext{
+			RunAsUser:  pointer.Int64Ptr(0),
+			RunAsGroup: pointer.Int64Ptr(0),
+			SeccompProfile: &corev1.SeccompProfile{
+				Type: corev1.SeccompProfileTypeRuntimeDefault,
+			},
+		},
+		Volumes: []corev1.Volume{
+			{
+				Name: "var-lib-etcd",
+				VolumeSource: corev1.VolumeSource{
+					HostPath: &corev1.HostPathVolumeSource{
+						Path: "/var/lib/etcd",
+					},
+				},
+			},
+			{
+				Name: "var-lib-kubelet",
+				VolumeSource: corev1.VolumeSource{
+					HostPath: &corev1.HostPathVolumeSource{
+						Path: "/var/lib/kubelet",
+					},
+				},
+			},
+			{
+				Name: "etc-systemd",
+				VolumeSource: corev1.VolumeSource{
+					HostPath: &corev1.HostPathVolumeSource{
+						Path: "/etc/systemd",
+					},
+				},
+			},
+			{
+				Name: "etc-kubernetes",
+				VolumeSource: corev1.VolumeSource{
+					HostPath: &corev1.HostPathVolumeSource{
+						Path: "/etc/kubernetes",
+					},
+				},
+			},
+			{
+				Name: "usr-bin",
+				VolumeSource: corev1.VolumeSource{
+					HostPath: &corev1.HostPathVolumeSource{
+						Path: "/usr/bin",
+					},
+				},
+			},
+		},
+		Containers: []corev1.Container{
+			{
+				Name:                     "kube-bench",
+				Image:                    "docker.io/aquasec/kube-bench:0.5.0",
+				ImagePullPolicy:          corev1.PullIfNotPresent,
+				TerminationMessagePolicy: corev1.TerminationMessageFallbackToLogsOnError,
+				Command:                  []string{"sh"},
+				Args:                     []string{"-c", "kube-bench --json 2> /dev/null"},
+				SecurityContext: &corev1.SecurityContext{
+					Privileged:               pointer.BoolPtr(false),
+					AllowPrivilegeEscalation: pointer.BoolPtr(false),
+					Capabilities: &corev1.Capabilities{
+						Drop: []corev1.Capability{"all"},
+					},
+					ReadOnlyRootFilesystem: pointer.BoolPtr(true),
+				},
+				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:      "var-lib-etcd",
+						MountPath: "/var/lib/etcd",
+						ReadOnly:  true,
+					},
+					{
+						Name:      "var-lib-kubelet",
+						MountPath: "/var/lib/kubelet",
+						ReadOnly:  true,
+					},
+					{
+						Name:      "etc-systemd",
+						MountPath: "/etc/systemd",
+						ReadOnly:  true,
+					},
+					{
+						Name:      "etc-kubernetes",
+						MountPath: "/etc/kubernetes",
+						ReadOnly:  true,
+					},
+					{
+						Name:      "usr-bin",
+						MountPath: "/usr/local/mount-from-host/bin",
+						ReadOnly:  true,
+					},
+				},
+			},
+		},
+	}, podSpec)
+}
+
+func TestKubeBenchPlugin_ParseCISKubeBenchOutput(t *testing.T) {
+	config := starboard.ConfigData{
+		"kube-bench.imageRef": "aquasec/kube-bench:0.3.1",
+	}
+	var testCases = []struct {
+		name string
+		in   string // input File
+		op   string // golden file
+		err  error  // expected error
+	}{
+		{
+			name: "Valid single json object in array",
+			in:   "testdata/valid.json",
+			op:   "testdata/goldenSingle.json",
+			err:  nil,
+		},
+		{
+			name: "invalid json object",
+			in:   "testdata/invalid.json",
+			err:  errors.New("invalid character 'I' looking for beginning of value"),
+		},
+		{
+			name: "Valid multiple json object in array",
+			in:   "testdata/multiObjects.json",
+			op:   "testdata/goldenMultiple.json",
+			err:  nil,
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			inFile, err := os.Open(tc.in)
+			require.NoError(t, err)
+			defer func() {
+				_ = inFile.Close()
+			}()
+
+			instance := kubebench.NewKubeBenchPlugin(fixedClock, config)
+			output, err := instance.ParseCISKubeBenchOutput(inFile)
+
+			switch {
+			case tc.err == nil:
+				require.NoError(t, err)
+				expectedOutput := expectedOutputFrom(t, tc.op)
+				assert.Equal(t, expectedOutput, output, "Converted report does not match expected report")
+			default:
+				assert.EqualError(t, err, tc.err.Error())
+			}
+		})
+	}
+}
+
+func expectedOutputFrom(t *testing.T, fileName string) v1alpha1.CISKubeBenchOutput {
+	t.Helper()
+
+	file, err := os.Open(fileName)
+	require.NoError(t, err)
+	defer file.Close()
+
+	var expectedOutput v1alpha1.CISKubeBenchOutput
+	err = json.NewDecoder(file).Decode(&expectedOutput)
+	require.NoError(t, err)
+
+	// Override time read from file
+	expectedOutput.UpdateTimestamp = metav1.NewTime(fixedTime)
+
+	return expectedOutput
+}