Repository: aquasecurity/starboard
Pull request #333 (2021-01-12 16:52:29)

refactor: Define interface for resource configuration checkers
This is a preliminary step to onboard Popeye or any other configuration auditing tool and reuse code to integrate Polaris with Starboard Operator.

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/333?src=pr&el=h1) Report
> Merging [#333](https://codecov.io/gh/aquasecurity/starboard/pull/333?src=pr&el=desc) (0577f2c) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/5af029b2ea20fad4f8d284458e884255c123a3ba?el=desc) (5af029b) will **decrease** coverage by `37.14%`.
> The diff coverage is `72.00%`.

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

```diff
@@             Coverage Diff             @@
##             main     #333       +/-   ##
===========================================
- Coverage   65.70%   28.56%   -37.15%     
===========================================
  Files          57       53        -4     
  Lines        2799     2591      -208     
===========================================
- Hits         1839      740     -1099     
- Misses        720     1796     +1076     
+ Partials      240       55      -185     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/333?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/cmd/scan\_configaudit.go](https://codecov.io/gh/aquasecurity/starboard/pull/333/diff?src=pr&el=tree#diff-cGtnL2NtZC9zY2FuX2NvbmZpZ2F1ZGl0Lmdv) | `0.00% <0.00%> (-50.00%)` | :arrow_down: |
| [pkg/vulnerabilityreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/333/diff?src=pr&el=tree#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvc2Nhbm5lci5nbw==) | `0.00% <0.00%> (-73.47%)` | :arrow_down: |
| [pkg/polaris/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/333/diff?src=pr&el=tree#diff-cGtnL3BvbGFyaXMvc2Nhbm5lci5nbw==) | `45.18% <77.14%> (-42.02%)` | :arrow_down: |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/333/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/333/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/333/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/333/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/333/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/333/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/333/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/333/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/333?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/333?src=pr&el=footer). Last update [5af029b...fe99938](https://codecov.io/gh/aquasecurity/starboard/pull/333?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 41db49b..377d963 100644
--- a/pkg/cmd/scan_configaudit.go
+++ b/pkg/cmd/scan_configaudit.go
@@ -60,7 +60,8 @@ 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(config)
+		report, err := polaris.NewScanner(starboard.NewScheme(), kubeClientset, opts, plugin).Scan(ctx, workload, gvk)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/configauditreport/plugin.go b/pkg/configauditreport/plugin.go
new file mode 100644
index 0000000..bcee232
--- /dev/null
+++ b/pkg/configauditreport/plugin.go
@@ -0,0 +1,20 @@
+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)
+
+	ParseConfigAuditResult(logsReader io.ReadCloser) (v1alpha1.ConfigAuditResult, error)
+}
diff --git a/pkg/polaris/scanner.go b/pkg/polaris/scanner.go
index 7563340..647118b 100644
--- a/pkg/polaris/scanner.go
+++ b/pkg/polaris/scanner.go
@@ -3,15 +3,16 @@ 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"
-	"github.com/google/uuid"
 	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
@@ -28,32 +29,29 @@ const (
 	configVolume         = "config"
 )
 
-type Config interface {
-	GetPolarisImageRef() (string, error)
-}
-
+// 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
-	config    Config
 	opts      kube.ScannerOpts
 	pods      *pod.Manager
-	converter Converter
+	plugin    configauditreport.Plugin
+	ext.IDGenerator
 }
 
 func NewScanner(
 	scheme *runtime.Scheme,
 	clientset kubernetes.Interface,
-	config Config,
 	opts kube.ScannerOpts,
+	plugin configauditreport.Plugin,
 ) *Scanner {
 	return &Scanner{
-		scheme:    scheme,
-		config:    config,
-		clientset: clientset,
-		opts:      opts,
-		pods:      pod.NewPodManager(clientset),
-		converter: NewConverter(config),
+		scheme:      scheme,
+		clientset:   clientset,
+		opts:        opts,
+		pods:        pod.NewPodManager(clientset),
+		plugin:      plugin,
+		IDGenerator: ext.NewGoogleUUIDGenerator(),
 	}
 }
 
@@ -96,7 +94,7 @@ func (s *Scanner) Scan(ctx context.Context, workload kube.Object, gvk schema.Gro
 		return v1alpha1.ConfigAuditReport{}, fmt.Errorf("getting logs: %w", err)
 	}
 
-	result, err := s.converter.Convert(logsReader)
+	result, err := s.plugin.ParseConfigAuditResult(logsReader)
 	defer func() {
 		_ = logsReader.Close()
 	}()
@@ -107,29 +105,14 @@ func (s *Scanner) Scan(ctx context.Context, workload kube.Object, gvk schema.Gro
 		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()
+	jobSpec, err := s.plugin.GetScanJobSpec(workload, gvk)
 	if err != nil {
 		return nil, err
 	}
-	sourceName := s.sourceNameFrom(workload, gvk)
 	return &batchv1.Job{
 		ObjectMeta: metav1.ObjectMeta{
-			Name:      uuid.New().String(),
+			Name:      s.GenerateID(),
 			Namespace: starboard.NamespaceName,
 			Labels: map[string]string{
 				kube.LabelResourceKind:      string(workload.Kind),
@@ -149,54 +132,104 @@ func (s *Scanner) preparePolarisJob(workload kube.Object, gvk schema.GroupVersio
 						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,
-									},
-								},
-							},
+				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},
-						},
+				},
+			},
+		},
+		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)
+}
diff --git a/pkg/vulnerabilityreport/scanner.go b/pkg/vulnerabilityreport/scanner.go
index 93811d0..04184f7 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),
@@ -118,7 +118,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 +177,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/polaris/scanner_test.go b/pkg/polaris/scanner_test.go
new file mode 100644
index 0000000..f4bffbd
--- /dev/null
+++ b/pkg/polaris/scanner_test.go
@@ -0,0 +1,103 @@
+package polaris_test
+
+import (
+	"testing"
+
+	"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"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+	"k8s.io/utils/pointer"
+)
+
+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(tc.config)
+			jobSpec, err := plugin.GetScanJobSpec(tc.workload, tc.gvk)
+			require.NoError(t, err, tc.name)
+			assert.Equal(t, tc.expectedJobSpec, jobSpec, tc.name)
+		})
+	}
+
+}