Repository: aquasecurity/starboard
Pull request #611 (2021-05-28 13:10:26)

refactor: Allow plugins to provide the default config
Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/611?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) Report
> Merging [#611](https://codecov.io/gh/aquasecurity/starboard/pull/611?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (e70c421) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/ca3211b164642a9b18d32e3118bd61d1fb70687b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (ca3211b) will **decrease** coverage by `30.20%`.
> The diff coverage is `75.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aquasecurity/starboard/pull/611/graphs/tree.svg?width=650&height=150&src=pr&token=qqTGKAJ0Wb&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)](https://codecov.io/gh/aquasecurity/starboard/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)

```diff
@@             Coverage Diff             @@
##             main     #611       +/-   ##
===========================================
- Coverage   69.03%   38.82%   -30.21%     
===========================================
  Files          58       12       -46     
  Lines        4382     1298     -3084     
===========================================
- Hits         3025      504     -2521     
+ Misses       1015      723      -292     
+ Partials      342       71      -271     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/611?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) | Coverage Δ | |
|---|---|---|
| [pkg/operator/operator.go](https://codecov.io/gh/aquasecurity/starboard/pull/611/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL29wZXJhdG9yLmdv) | `39.55% <33.33%> (-24.57%)` | :arrow_down: |
| [pkg/plugin/conftest/plugin.go](https://codecov.io/gh/aquasecurity/starboard/pull/611/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3BsdWdpbi9jb25mdGVzdC9wbHVnaW4uZ28=) | `85.00% <100.00%> (-1.46%)` | :arrow_down: |
| [pkg/configauditreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/611/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2NvbmZpZ2F1ZGl0cmVwb3J0L3NjYW5uZXIuZ28=) | `0.00% <0.00%> (-70.77%)` | :arrow_down: |
| [pkg/operator/controller/vulnerabilityreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/611/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvdnVsbmVyYWJpbGl0eXJlcG9ydC5nbw==) | `0.00% <0.00%> (-65.17%)` | :arrow_down: |
| [pkg/operator/controller/ciskubebenchreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/611/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY2lza3ViZWJlbmNocmVwb3J0Lmdv) | `0.00% <0.00%> (-64.37%)` | :arrow_down: |
| [pkg/operator/predicate/predicate.go](https://codecov.io/gh/aquasecurity/starboard/pull/611/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL3ByZWRpY2F0ZS9wcmVkaWNhdGUuZ28=) | `62.06% <0.00%> (-29.32%)` | :arrow_down: |
| [pkg/plugin/factory.go](https://codecov.io/gh/aquasecurity/starboard/pull/611/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3BsdWdpbi9mYWN0b3J5Lmdv) | `51.21% <0.00%> (-29.27%)` | :arrow_down: |
| [pkg/configauditreport/io.go](https://codecov.io/gh/aquasecurity/starboard/pull/611/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2NvbmZpZ2F1ZGl0cmVwb3J0L2lvLmdv) | `43.90% <0.00%> (-12.20%)` | :arrow_down: |
| [pkg/operator/controller/configauditreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/611/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY29uZmlnYXVkaXRyZXBvcnQuZ28=) | `57.87% <0.00%> (-8.80%)` | :arrow_down: |
| ... and [43 more](https://codecov.io/gh/aquasecurity/starboard/pull/611/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) | |

------

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


Patch:
diff --git a/pkg/cmd/scan_configauditreports.go b/pkg/cmd/scan_configauditreports.go
index d3c3d86..1c719a4 100644
--- a/pkg/cmd/scan_configauditreports.go
+++ b/pkg/cmd/scan_configauditreports.go
@@ -2,6 +2,7 @@ package cmd
 
 import (
 	"context"
+	"fmt"
 
 	"github.com/aquasecurity/starboard/pkg/configauditreport"
 	"github.com/aquasecurity/starboard/pkg/plugin"
@@ -72,6 +73,10 @@ func ScanConfigAuditReports(buildInfo starboard.BuildInfo, cf *genericclioptions
 		if err != nil {
 			return err
 		}
+		err = plugin.Init(pluginContext)
+		if err != nil {
+			return fmt.Errorf("initializing %s plugin: %w", pluginContext.GetName(), err)
+		}
 		scanner := configauditreport.NewScanner(kubeClientset, kubeClient, plugin, pluginContext, config, opts)
 		report, err := scanner.Scan(ctx, workload)
 		if err != nil {
diff --git a/pkg/configauditreport/plugin.go b/pkg/configauditreport/plugin.go
index f9d1303..5a384eb 100644
--- a/pkg/configauditreport/plugin.go
+++ b/pkg/configauditreport/plugin.go
@@ -15,6 +15,10 @@ import (
 // implementation details.
 type Plugin interface {
 
+	// Init is a callback to initialize this plugin, e.g. ensure the default
+	// configuration.
+	Init(ctx starboard.PluginContext) error
+
 	// GetScanJobSpec describes the pod that will be created by Starboard when
 	// it schedules a Kubernetes job to scan the specified workload client.Object.
 	// The plugin might return zero to many v1.Secret objects which will be
diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go
index 590c348..b911ccc 100644
--- a/pkg/operator/operator.go
+++ b/pkg/operator/operator.go
@@ -169,6 +169,11 @@ func Start(ctx context.Context, buildInfo starboard.BuildInfo, operatorConfig et
 			return err
 		}
 
+		err = plugin.Init(pluginContext)
+		if err != nil {
+			return fmt.Errorf("initializing %s plugin: %w", pluginContext.GetName(), err)
+		}
+
 		if err = (&controller.ConfigAuditReportReconciler{
 			Logger:         ctrl.Log.WithName("reconciler").WithName("configauditreport"),
 			Config:         operatorConfig,
diff --git a/pkg/plugin/polaris/plugin.go b/pkg/plugin/polaris/plugin.go
index 3fe7767..2dc24b5 100644
--- a/pkg/plugin/polaris/plugin.go
+++ b/pkg/plugin/polaris/plugin.go
@@ -22,6 +22,193 @@ const (
 	configVolume         = "config"
 )
 
+const (
+	polarisConfigYAML = `checks:
+  # reliability
+  multipleReplicasForDeployment: ignore
+  priorityClassNotSet: ignore
+  # resources
+  cpuRequestsMissing: warning
+  cpuLimitsMissing: warning
+  memoryRequestsMissing: warning
+  memoryLimitsMissing: warning
+  # images
+  tagNotSpecified: danger
+  pullPolicyNotAlways: ignore
+  # healthChecks
+  readinessProbeMissing: warning
+  livenessProbeMissing: warning
+  # networking
+  hostNetworkSet: warning
+  hostPortSet: warning
+  # security
+  hostIPCSet: danger
+  hostPIDSet: danger
+  notReadOnlyRootFilesystem: warning
+  privilegeEscalationAllowed: danger
+  runAsRootAllowed: warning
+  runAsPrivileged: danger
+  dangerousCapabilities: danger
+  insecureCapabilities: warning
+exemptions:
+  - controllerNames:
+    - kube-apiserver
+    - kube-proxy
+    - kube-scheduler
+    - etcd-manager-events
+    - kube-controller-manager
+    - kube-dns
+    - etcd-manager-main
+    rules:
+    - hostPortSet
+    - hostNetworkSet
+    - readinessProbeMissing
+    - livenessProbeMissing
+    - cpuRequestsMissing
+    - cpuLimitsMissing
+    - memoryRequestsMissing
+    - memoryLimitsMissing
+    - runAsRootAllowed
+    - runAsPrivileged
+    - notReadOnlyRootFilesystem
+    - hostPIDSet
+  - controllerNames:
+    - kube-flannel-ds
+    rules:
+    - notReadOnlyRootFilesystem
+    - runAsRootAllowed
+    - notReadOnlyRootFilesystem
+    - readinessProbeMissing
+    - livenessProbeMissing
+    - cpuLimitsMissing
+  - controllerNames:
+    - cert-manager
+    rules:
+    - notReadOnlyRootFilesystem
+    - runAsRootAllowed
+    - readinessProbeMissing
+    - livenessProbeMissing
+  - controllerNames:
+    - cluster-autoscaler
+    rules:
+    - notReadOnlyRootFilesystem
+    - runAsRootAllowed
+    - readinessProbeMissing
+  - controllerNames:
+    - vpa
+    rules:
+    - runAsRootAllowed
+    - readinessProbeMissing
+    - livenessProbeMissing
+    - notReadOnlyRootFilesystem
+  - controllerNames:
+    - datadog
+    rules:
+    - runAsRootAllowed
+    - readinessProbeMissing
+    - livenessProbeMissing
+    - notReadOnlyRootFilesystem
+  - controllerNames:
+    - nginx-ingress-controller
+    rules:
+    - privilegeEscalationAllowed
+    - insecureCapabilities
+    - runAsRootAllowed
+  - controllerNames:
+    - dns-controller
+    - datadog-datadog
+    - kube-flannel-ds
+    - kube2iam
+    - aws-iam-authenticator
+    - datadog
+    - kube2iam
+    rules:
+    - hostNetworkSet
+  - controllerNames:
+    - aws-iam-authenticator
+    - aws-cluster-autoscaler
+    - kube-state-metrics
+    - dns-controller
+    - external-dns
+    - dnsmasq
+    - autoscaler
+    - kubernetes-dashboard
+    - install-cni
+    - kube2iam
+    rules:
+    - readinessProbeMissing
+    - livenessProbeMissing
+  - controllerNames:
+    - aws-iam-authenticator
+    - nginx-ingress-default-backend
+    - aws-cluster-autoscaler
+    - kube-state-metrics
+    - dns-controller
+    - external-dns
+    - kubedns
+    - dnsmasq
+    - autoscaler
+    - tiller
+    - kube2iam
+    rules:
+    - runAsRootAllowed
+  - controllerNames:
+    - aws-iam-authenticator
+    - nginx-ingress-controller
+    - nginx-ingress-default-backend
+    - aws-cluster-autoscaler
+    - kube-state-metrics
+    - dns-controller
+    - external-dns
+    - kubedns
+    - dnsmasq
+    - autoscaler
+    - tiller
+    - kube2iam
+    rules:
+    - notReadOnlyRootFilesystem
+  - controllerNames:
+    - cert-manager
+    - dns-controller
+    - kubedns
+    - dnsmasq
+    - autoscaler
+    - insights-agent-goldilocks-vpa-install
+    - datadog
+    rules:
+    - cpuRequestsMissing
+    - cpuLimitsMissing
+    - memoryRequestsMissing
+    - memoryLimitsMissing
+  - controllerNames:
+    - kube2iam
+    - kube-flannel-ds
+    rules:
+    - runAsPrivileged
+  - controllerNames:
+    - kube-hunter
+    rules:
+    - hostPIDSet
+  - controllerNames:
+    - polaris
+    - kube-hunter
+    - goldilocks
+    - insights-agent-goldilocks-vpa-install
+    rules:
+    - notReadOnlyRootFilesystem
+  - controllerNames:
+    - insights-agent-goldilocks-controller
+    rules:
+    - livenessProbeMissing
+    - readinessProbeMissing
+  - controllerNames:
+    - insights-agent-goldilocks-vpa-install
+    - kube-hunter
+    rules:
+    - runAsRootAllowed
+`
+)
+
 type plugin struct {
 	clock ext.Clock
 }
@@ -34,6 +221,15 @@ func NewPlugin(clock ext.Clock) configauditreport.Plugin {
 	}
 }
 
+func (p *plugin) Init(ctx starboard.PluginContext) error {
+	return ctx.EnsureConfig(starboard.PluginConfig{
+		Data: map[string]string{
+			"polaris.imageRef":    "quay.io/fairwinds/polaris:3.2",
+			"polaris.config.yaml": polarisConfigYAML,
+		},
+	})
+}
+
 func (p *plugin) GetConfigHash(ctx starboard.PluginContext) (string, error) {
 	cm, err := ctx.GetConfig()
 	if err != nil {
diff --git a/pkg/starboard/config.go b/pkg/starboard/config.go
index 9f9a7bd..fdd6f50 100644
--- a/pkg/starboard/config.go
+++ b/pkg/starboard/config.go
@@ -20,193 +20,6 @@ import (
 	"k8s.io/client-go/kubernetes"
 )
 
-const (
-	polarisConfigYAML = `checks:
-  # reliability
-  multipleReplicasForDeployment: ignore
-  priorityClassNotSet: ignore
-  # resources
-  cpuRequestsMissing: warning
-  cpuLimitsMissing: warning
-  memoryRequestsMissing: warning
-  memoryLimitsMissing: warning
-  # images
-  tagNotSpecified: danger
-  pullPolicyNotAlways: ignore
-  # healthChecks
-  readinessProbeMissing: warning
-  livenessProbeMissing: warning
-  # networking
-  hostNetworkSet: warning
-  hostPortSet: warning
-  # security
-  hostIPCSet: danger
-  hostPIDSet: danger
-  notReadOnlyRootFilesystem: warning
-  privilegeEscalationAllowed: danger
-  runAsRootAllowed: warning
-  runAsPrivileged: danger
-  dangerousCapabilities: danger
-  insecureCapabilities: warning
-exemptions:
-  - controllerNames:
-    - kube-apiserver
-    - kube-proxy
-    - kube-scheduler
-    - etcd-manager-events
-    - kube-controller-manager
-    - kube-dns
-    - etcd-manager-main
-    rules:
-    - hostPortSet
-    - hostNetworkSet
-    - readinessProbeMissing
-    - livenessProbeMissing
-    - cpuRequestsMissing
-    - cpuLimitsMissing
-    - memoryRequestsMissing
-    - memoryLimitsMissing
-    - runAsRootAllowed
-    - runAsPrivileged
-    - notReadOnlyRootFilesystem
-    - hostPIDSet
-  - controllerNames:
-    - kube-flannel-ds
-    rules:
-    - notReadOnlyRootFilesystem
-    - runAsRootAllowed
-    - notReadOnlyRootFilesystem
-    - readinessProbeMissing
-    - livenessProbeMissing
-    - cpuLimitsMissing
-  - controllerNames:
-    - cert-manager
-    rules:
-    - notReadOnlyRootFilesystem
-    - runAsRootAllowed
-    - readinessProbeMissing
-    - livenessProbeMissing
-  - controllerNames:
-    - cluster-autoscaler
-    rules:
-    - notReadOnlyRootFilesystem
-    - runAsRootAllowed
-    - readinessProbeMissing
-  - controllerNames:
-    - vpa
-    rules:
-    - runAsRootAllowed
-    - readinessProbeMissing
-    - livenessProbeMissing
-    - notReadOnlyRootFilesystem
-  - controllerNames:
-    - datadog
-    rules:
-    - runAsRootAllowed
-    - readinessProbeMissing
-    - livenessProbeMissing
-    - notReadOnlyRootFilesystem
-  - controllerNames:
-    - nginx-ingress-controller
-    rules:
-    - privilegeEscalationAllowed
-    - insecureCapabilities
-    - runAsRootAllowed
-  - controllerNames:
-    - dns-controller
-    - datadog-datadog
-    - kube-flannel-ds
-    - kube2iam
-    - aws-iam-authenticator
-    - datadog
-    - kube2iam
-    rules:
-    - hostNetworkSet
-  - controllerNames:
-    - aws-iam-authenticator
-    - aws-cluster-autoscaler
-    - kube-state-metrics
-    - dns-controller
-    - external-dns
-    - dnsmasq
-    - autoscaler
-    - kubernetes-dashboard
-    - install-cni
-    - kube2iam
-    rules:
-    - readinessProbeMissing
-    - livenessProbeMissing
-  - controllerNames:
-    - aws-iam-authenticator
-    - nginx-ingress-default-backend
-    - aws-cluster-autoscaler
-    - kube-state-metrics
-    - dns-controller
-    - external-dns
-    - kubedns
-    - dnsmasq
-    - autoscaler
-    - tiller
-    - kube2iam
-    rules:
-    - runAsRootAllowed
-  - controllerNames:
-    - aws-iam-authenticator
-    - nginx-ingress-controller
-    - nginx-ingress-default-backend
-    - aws-cluster-autoscaler
-    - kube-state-metrics
-    - dns-controller
-    - external-dns
-    - kubedns
-    - dnsmasq
-    - autoscaler
-    - tiller
-    - kube2iam
-    rules:
-    - notReadOnlyRootFilesystem
-  - controllerNames:
-    - cert-manager
-    - dns-controller
-    - kubedns
-    - dnsmasq
-    - autoscaler
-    - insights-agent-goldilocks-vpa-install
-    - datadog
-    rules:
-    - cpuRequestsMissing
-    - cpuLimitsMissing
-    - memoryRequestsMissing
-    - memoryLimitsMissing
-  - controllerNames:
-    - kube2iam
-    - kube-flannel-ds
-    rules:
-    - runAsPrivileged
-  - controllerNames:
-    - kube-hunter
-    rules:
-    - hostPIDSet
-  - controllerNames:
-    - polaris
-    - kube-hunter
-    - goldilocks
-    - insights-agent-goldilocks-vpa-install
-    rules:
-    - notReadOnlyRootFilesystem
-  - controllerNames:
-    - insights-agent-goldilocks-controller
-    rules:
-    - livenessProbeMissing
-    - readinessProbeMissing
-  - controllerNames:
-    - insights-agent-goldilocks-vpa-install
-    - kube-hunter
-    rules:
-    - runAsRootAllowed
-`
-)
-
 func NewScheme() *runtime.Scheme {
 	scheme := runtime.NewScheme()
 	_ = corev1.AddToScheme(scheme)
@@ -269,21 +82,6 @@ func GetDefaultConfig() ConfigData {
 	}
 }
 
-// GetDefaultPolarisConfig returns the default Polaris configuration.
-func GetDefaultPolarisConfig() map[string]string {
-	return map[string]string{
-		"polaris.imageRef":    "quay.io/fairwinds/polaris:3.2",
-		"polaris.config.yaml": polarisConfigYAML,
-	}
-}
-
-// GetDefaultConftestConfig return the default Conftest configuration.
-func GetDefaultConftestConfig() map[string]string {
-	return map[string]string{
-		"conftest.imageRef": "openpolicyagent/conftest:v0.25.0",
-	}
-}
-
 func (c ConfigData) GetScanJobTolerations() ([]corev1.Toleration, error) {
 	scanJobTolerations := []corev1.Toleration{}
 	if c["scanJob.tolerations"] == "" {
@@ -409,12 +207,12 @@ type configManager struct {
 }
 
 func (c *configManager) EnsureDefault(ctx context.Context) error {
-	cm, err := c.client.CoreV1().ConfigMaps(c.namespace).Get(ctx, ConfigMapName, metav1.GetOptions{})
+	_, err := c.client.CoreV1().ConfigMaps(c.namespace).Get(ctx, ConfigMapName, metav1.GetOptions{})
 	if err != nil {
 		if !apierrors.IsNotFound(err) {
 			return fmt.Errorf("getting config: %w", err)
 		}
-		cm, err = c.client.CoreV1().ConfigMaps(c.namespace).Create(ctx, &corev1.ConfigMap{
+		_, err = c.client.CoreV1().ConfigMaps(c.namespace).Create(ctx, &corev1.ConfigMap{
 			ObjectMeta: metav1.ObjectMeta{
 				Namespace: c.namespace,
 				Name:      ConfigMapName,
@@ -444,34 +242,6 @@ func (c *configManager) EnsureDefault(ctx context.Context) error {
 		return err
 	}
 
-	scanner, err := ConfigData(cm.Data).GetConfigAuditReportsScanner()
-	if err != nil {
-		return fmt.Errorf("getting scanner: %w", err)
-	}
-
-	var pluginConfigData map[string]string
-	switch scanner {
-	case Polaris:
-		pluginConfigData = GetDefaultPolarisConfig()
-	case Conftest:
-		pluginConfigData = GetDefaultConftestConfig()
-	}
-
-	pluginConfig := &corev1.ConfigMap{
-		ObjectMeta: metav1.ObjectMeta{
-			Namespace: c.namespace,
-			Name:      GetPluginConfigMapName(string(scanner)),
-			Labels: labels.Set{
-				LabelK8SAppManagedBy: "starboard",
-			},
-		},
-		Data: pluginConfigData,
-	}
-	_, err = c.client.CoreV1().ConfigMaps(c.namespace).Create(ctx, pluginConfig, metav1.CreateOptions{})
-	if err != nil && !apierrors.IsAlreadyExists(err) {
-		return err
-	}
-
 	return nil
 }
 
diff --git a/pkg/starboard/plugin.go b/pkg/starboard/plugin.go
index 2d7abf6..7f88b7d 100644
--- a/pkg/starboard/plugin.go
+++ b/pkg/starboard/plugin.go
@@ -3,6 +3,8 @@ package starboard
 import (
 	"context"
 	"fmt"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/labels"
 	"strings"
 
 	corev1 "k8s.io/api/core/v1"
@@ -34,6 +36,8 @@ type PluginContext interface {
 	GetName() string
 	// GetConfig returns the PluginConfig object that holds configuration settings of the plugin.
 	GetConfig() (PluginConfig, error)
+	// EnsureConfig ensures the PluginConfig, typically when a plugin is initialized.
+	EnsureConfig(config PluginConfig) error
 	// GetNamespace return the name of the K8s Namespace where Starboard creates Jobs
 	// and other helper objects.
 	GetNamespace() string
@@ -59,6 +63,23 @@ func (p *pluginContext) GetName() string {
 	return p.name
 }
 
+func (p *pluginContext) EnsureConfig(config PluginConfig) error {
+	err := p.client.Create(context.Background(), &corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Namespace: p.namespace,
+			Name:      GetPluginConfigMapName(p.name),
+			Labels: labels.Set{
+				LabelK8SAppManagedBy: "starboard",
+			},
+		},
+		Data: config.Data,
+	})
+	if err != nil && !errors.IsAlreadyExists(err) {
+		return err
+	}
+	return nil
+}
+
 func (p *pluginContext) GetConfig() (PluginConfig, error) {
 	cm := &corev1.ConfigMap{}
 	secret := &corev1.Secret{}


Test patch:
diff --git a/itest/starboard/starboard_cli_test.go b/itest/starboard/starboard_cli_test.go
index 399c153..fbdc606 100644
--- a/itest/starboard/starboard_cli_test.go
+++ b/itest/starboard/starboard_cli_test.go
@@ -129,15 +129,6 @@ var _ = Describe("Starboard CLI", func() {
 			Expect(err).ToNot(HaveOccurred())
 			Expect(cm.Data).To(BeEquivalentTo(starboard.GetDefaultConfig()))
 
-			var polarisCm corev1.ConfigMap
-			err = kubeClient.Get(context.TODO(), types.NamespacedName{
-				Name:      starboard.GetPluginConfigMapName(string(starboard.Polaris)),
-				Namespace: starboard.NamespaceName,
-			}, &polarisCm)
-
-			Expect(err).ToNot(HaveOccurred())
-			Expect(polarisCm.Data).To(BeEquivalentTo(starboard.GetDefaultPolarisConfig()))
-
 			var secret corev1.Secret
 			err = kubeClient.Get(context.TODO(), types.NamespacedName{
 				Name:      starboard.SecretName,
diff --git a/pkg/configauditreport/builder_test.go b/pkg/configauditreport/builder_test.go
index 3b6b897..3b18020 100644
--- a/pkg/configauditreport/builder_test.go
+++ b/pkg/configauditreport/builder_test.go
@@ -64,11 +64,15 @@ type testPlugin struct {
 	configHash string
 }
 
-func (p *testPlugin) GetScanJobSpec(ctx starboard.PluginContext, obj client.Object) (corev1.PodSpec, []*corev1.Secret, error) {
+func (p *testPlugin) Init(_ starboard.PluginContext) error {
+	return nil
+}
+
+func (p *testPlugin) GetScanJobSpec(_ starboard.PluginContext, obj client.Object) (corev1.PodSpec, []*corev1.Secret, error) {
 	return corev1.PodSpec{}, nil, nil
 }
 
-func (p *testPlugin) ParseConfigAuditReportData(ctx starboard.PluginContext, logsReader io.ReadCloser) (v1alpha1.ConfigAuditResult, error) {
+func (p *testPlugin) ParseConfigAuditReportData(_ starboard.PluginContext, logsReader io.ReadCloser) (v1alpha1.ConfigAuditResult, error) {
 	return v1alpha1.ConfigAuditResult{}, nil
 }
 
@@ -76,7 +80,7 @@ func (p *testPlugin) GetContainerName() string {
 	return ""
 }
 
-func (p *testPlugin) GetConfigHash(ctx starboard.PluginContext) (string, error) {
+func (p *testPlugin) GetConfigHash(_ starboard.PluginContext) (string, error) {
 	return p.configHash, nil
 }
 
diff --git a/pkg/plugin/conftest/plugin.go b/pkg/plugin/conftest/plugin.go
index 25bc47a..b0f236d 100644
--- a/pkg/plugin/conftest/plugin.go
+++ b/pkg/plugin/conftest/plugin.go
@@ -40,6 +40,14 @@ func NewPlugin(idGenerator ext.IDGenerator, clock ext.Clock) configauditreport.P
 	}
 }
 
+func (p *plugin) Init(ctx starboard.PluginContext) error {
+	return ctx.EnsureConfig(starboard.PluginConfig{
+		Data: map[string]string{
+			"conftest.imageRef": "openpolicyagent/conftest:v0.25.0",
+		},
+	})
+}
+
 func (p *plugin) GetConfigHash(ctx starboard.PluginContext) (string, error) {
 	cm, err := ctx.GetConfig()
 	if err != nil {
diff --git a/pkg/plugin/conftest/plugin_test.go b/pkg/plugin/conftest/plugin_test.go
index 4b32cdb..bfb6b6a 100644
--- a/pkg/plugin/conftest/plugin_test.go
+++ b/pkg/plugin/conftest/plugin_test.go
@@ -4,6 +4,7 @@ import (
 	. "github.com/onsi/gomega"
 	. "github.com/onsi/gomega/gstruct"
 
+	"context"
 	"io/ioutil"
 	"strings"
 	"testing"
@@ -16,6 +17,7 @@ import (
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/resource"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/types"
 	"k8s.io/utils/pointer"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
 )
@@ -25,6 +27,47 @@ var (
 	fixedClock = ext.NewFixedClock(fixedTime)
 )
 
+func TestPlugin_Init(t *testing.T) {
+	g := NewGomegaWithT(t)
+
+	client := fake.NewClientBuilder().WithObjects().Build()
+
+	instance := conftest.NewPlugin(ext.NewSimpleIDGenerator(), fixedClock)
+
+	pluginContext := starboard.NewPluginContext().
+		WithName(string(starboard.Conftest)).
+		WithNamespace("starboard-ns").
+		WithServiceAccountName("starboard-sa").
+		WithClient(client).
+		Get()
+	err := instance.Init(pluginContext)
+	g.Expect(err).ToNot(HaveOccurred())
+
+	var cm corev1.ConfigMap
+	err = client.Get(context.Background(), types.NamespacedName{
+		Namespace: "starboard-ns",
+		Name:      "starboard-conftest-config",
+	}, &cm)
+	g.Expect(err).ToNot(HaveOccurred())
+	g.Expect(cm).To(Equal(corev1.ConfigMap{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "v1",
+			Kind:       "ConfigMap",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "starboard-conftest-config",
+			Namespace: "starboard-ns",
+			Labels: map[string]string{
+				"app.kubernetes.io/managed-by": "starboard",
+			},
+			ResourceVersion: "1",
+		},
+		Data: map[string]string{
+			"conftest.imageRef": "openpolicyagent/conftest:v0.25.0",
+		},
+	}))
+}
+
 func TestPlugin_GetScanJobSpec(t *testing.T) {
 	g := NewGomegaWithT(t)
 	sequence := ext.NewSimpleIDGenerator()
diff --git a/pkg/plugin/polaris/plugin_test.go b/pkg/plugin/polaris/plugin_test.go
index 07eb78c..516e027 100644
--- a/pkg/plugin/polaris/plugin_test.go
+++ b/pkg/plugin/polaris/plugin_test.go
@@ -1,6 +1,9 @@
 package polaris_test
 
 import (
+	. "github.com/onsi/gomega"
+
+	"context"
 	"os"
 	"testing"
 	"time"
@@ -9,11 +12,11 @@ import (
 	"github.com/aquasecurity/starboard/pkg/ext"
 	"github.com/aquasecurity/starboard/pkg/plugin/polaris"
 	"github.com/aquasecurity/starboard/pkg/starboard"
-	"github.com/onsi/gomega"
 	appsv1 "k8s.io/api/apps/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/types"
 	"k8s.io/utils/pointer"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/client/fake"
@@ -24,6 +27,55 @@ var (
 	fixedClock = ext.NewFixedClock(fixedTime)
 )
 
+func TestPlugin_Init(t *testing.T) {
+	g := NewGomegaWithT(t)
+
+	client := fake.NewClientBuilder().WithObjects(&corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "starboard-polaris-config",
+			Namespace: "starboard-ns",
+		},
+		Data: map[string]string{
+			"polaris.imageRef": "quay.io/fairwinds/polaris:3.2",
+			"polaris.config.yaml": `checks:
+  cpuRequestsMissing: warning`,
+		},
+	}).Build()
+
+	pluginContext := starboard.NewPluginContext().
+		WithName(string(starboard.Polaris)).
+		WithNamespace("starboard-ns").
+		WithServiceAccountName("starboard-sa").
+		WithClient(client).
+		Get()
+
+	instance := polaris.NewPlugin(fixedClock)
+	err := instance.Init(pluginContext)
+	g.Expect(err).ToNot(HaveOccurred())
+
+	var cm corev1.ConfigMap
+	err = client.Get(context.Background(), types.NamespacedName{
+		Namespace: "starboard-ns",
+		Name:      "starboard-polaris-config",
+	}, &cm)
+	g.Expect(err).ToNot(HaveOccurred())
+	g.Expect(cm).To(Equal(corev1.ConfigMap{
+		TypeMeta: metav1.TypeMeta{
+			APIVersion: "v1",
+			Kind:       "ConfigMap",
+		},
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "starboard-polaris-config",
+			Namespace: "starboard-ns",
+		},
+		Data: map[string]string{
+			"polaris.imageRef": "quay.io/fairwinds/polaris:3.2",
+			"polaris.config.yaml": `checks:
+  cpuRequestsMissing: warning`,
+		},
+	}))
+}
+
 func TestPlugin_GetScanJobSpec(t *testing.T) {
 	testCases := []struct {
 		name string
@@ -109,7 +161,7 @@ func TestPlugin_GetScanJobSpec(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			g := gomega.NewGomegaWithT(t)
+			g := NewGomegaWithT(t)
 
 			pluginContext := starboard.NewPluginContext().
 				WithName(string(starboard.Polaris)).
@@ -126,24 +178,24 @@ func TestPlugin_GetScanJobSpec(t *testing.T) {
 			instance := polaris.NewPlugin(fixedClock)
 			jobSpec, secrets, err := instance.GetScanJobSpec(pluginContext, tc.obj)
 
-			g.Expect(err).ToNot(gomega.HaveOccurred())
-			g.Expect(secrets).To(gomega.BeNil())
-			g.Expect(jobSpec).To(gomega.Equal(tc.expectedJobSpec))
+			g.Expect(err).ToNot(HaveOccurred())
+			g.Expect(secrets).To(BeNil())
+			g.Expect(jobSpec).To(Equal(tc.expectedJobSpec))
 		})
 	}
 
 }
 
 func TestPlugin_GetContainerName(t *testing.T) {
-	g := gomega.NewGomegaWithT(t)
+	g := NewGomegaWithT(t)
 	plugin := polaris.NewPlugin(fixedClock)
-	g.Expect(plugin.GetContainerName()).To(gomega.Equal("polaris"))
+	g.Expect(plugin.GetContainerName()).To(Equal("polaris"))
 }
 
 func TestPlugin_ParseConfigAuditReportData(t *testing.T) {
-	g := gomega.NewGomegaWithT(t)
+	g := NewGomegaWithT(t)
 	testReport, err := os.Open("testdata/polaris-report.json")
-	g.Expect(err).ToNot(gomega.HaveOccurred())
+	g.Expect(err).ToNot(HaveOccurred())
 	defer func() {
 		_ = testReport.Close()
 	}()
@@ -165,19 +217,19 @@ func TestPlugin_ParseConfigAuditReportData(t *testing.T) {
 
 	plugin := polaris.NewPlugin(fixedClock)
 	result, err := plugin.ParseConfigAuditReportData(pluginContext, testReport)
-	g.Expect(err).ToNot(gomega.HaveOccurred())
-	g.Expect(result.UpdateTimestamp).To(gomega.Equal(metav1.NewTime(fixedTime)))
-	g.Expect(result.Scanner).To(gomega.Equal(v1alpha1.Scanner{
+	g.Expect(err).ToNot(HaveOccurred())
+	g.Expect(result.UpdateTimestamp).To(Equal(metav1.NewTime(fixedTime)))
+	g.Expect(result.Scanner).To(Equal(v1alpha1.Scanner{
 		Name:    "Polaris",
 		Vendor:  "Fairwinds Ops",
 		Version: "3.2",
 	}))
-	g.Expect(result.Summary).To(gomega.Equal(v1alpha1.ConfigAuditSummary{
+	g.Expect(result.Summary).To(Equal(v1alpha1.ConfigAuditSummary{
 		PassCount:    2,
 		DangerCount:  1,
 		WarningCount: 1,
 	}))
-	g.Expect(result.PodChecks).To(gomega.ConsistOf(v1alpha1.Check{
+	g.Expect(result.PodChecks).To(ConsistOf(v1alpha1.Check{
 		ID:       "hostIPCSet",
 		Message:  "Host IPC is not configured",
 		Success:  false,
@@ -190,8 +242,8 @@ func TestPlugin_ParseConfigAuditReportData(t *testing.T) {
 		Severity: "warning",
 		Category: "Networking",
 	}))
-	g.Expect(result.ContainerChecks).To(gomega.HaveLen(1))
-	g.Expect(result.ContainerChecks["db"]).To(gomega.ConsistOf(v1alpha1.Check{
+	g.Expect(result.ContainerChecks).To(HaveLen(1))
+	g.Expect(result.ContainerChecks["db"]).To(ConsistOf(v1alpha1.Check{
 		ID:       "cpuLimitsMissing",
 		Message:  "CPU limits are set",
 		Success:  false,
@@ -225,7 +277,7 @@ func TestPlugin_GetConfigHash(t *testing.T) {
 	}
 
 	t.Run("Should return different hash for different config data", func(t *testing.T) {
-		g := gomega.NewGomegaWithT(t)
+		g := NewGomegaWithT(t)
 
 		pluginContext1 := newPluginContextWithConfigData(map[string]string{
 			"foo":   "bar",
@@ -238,15 +290,15 @@ func TestPlugin_GetConfigHash(t *testing.T) {
 
 		plugin := polaris.NewPlugin(fixedClock)
 		hash1, err := plugin.GetConfigHash(pluginContext1)
-		g.Expect(err).ToNot(gomega.HaveOccurred())
+		g.Expect(err).ToNot(HaveOccurred())
 
 		hash2, err := plugin.GetConfigHash(pluginContext2)
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-		g.Expect(hash1).ToNot(gomega.Equal(hash2))
+		g.Expect(err).ToNot(HaveOccurred())
+		g.Expect(hash1).ToNot(Equal(hash2))
 	})
 
 	t.Run("Should return the same hash for the same config data", func(t *testing.T) {
-		g := gomega.NewGomegaWithT(t)
+		g := NewGomegaWithT(t)
 
 		pluginContext1 := newPluginContextWithConfigData(map[string]string{
 			"foo":   "bar",
@@ -259,10 +311,10 @@ func TestPlugin_GetConfigHash(t *testing.T) {
 
 		plugin := polaris.NewPlugin(fixedClock)
 		hash1, err := plugin.GetConfigHash(pluginContext1)
-		g.Expect(err).ToNot(gomega.HaveOccurred())
+		g.Expect(err).ToNot(HaveOccurred())
 
 		hash2, err := plugin.GetConfigHash(pluginContext2)
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-		g.Expect(hash1).To(gomega.Equal(hash2))
+		g.Expect(err).ToNot(HaveOccurred())
+		g.Expect(hash1).To(Equal(hash2))
 	})
 }
diff --git a/pkg/starboard/config_test.go b/pkg/starboard/config_test.go
index 5595d1d..9f3208f 100644
--- a/pkg/starboard/config_test.go
+++ b/pkg/starboard/config_test.go
@@ -262,15 +262,6 @@ func TestConfigManager_EnsureDefault(t *testing.T) {
 			Get(context.TODO(), starboard.SecretName, metav1.GetOptions{})
 		g.Expect(err).ToNot(gomega.HaveOccurred())
 		g.Expect(secret.Data).To(gomega.BeEmpty())
-
-		pluginConfig, err := clientset.CoreV1().ConfigMaps(namespace).
-			Get(context.TODO(), starboard.GetPluginConfigMapName(string(starboard.Polaris)), metav1.GetOptions{})
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-		g.Expect(pluginConfig.Data).To(gomega.Equal(starboard.GetDefaultPolarisConfig()))
-
-		_, err = clientset.CoreV1().ConfigMaps(namespace).
-			Get(context.TODO(), starboard.GetPluginConfigMapName(string(starboard.Conftest)), metav1.GetOptions{})
-		g.Expect(err).To(gomega.MatchError(`configmaps "starboard-conftest-config" not found`))
 	})
 
 	t.Run("Should not modify ConfigMaps nor Secret", func(t *testing.T) {
@@ -337,113 +328,6 @@ func TestConfigManager_EnsureDefault(t *testing.T) {
 		g.Expect(err).To(gomega.MatchError(`configmaps "starboard-polaris-config" not found`))
 	})
 
-	t.Run("Should create ConfigMap for Polaris", func(t *testing.T) {
-		g := gomega.NewGomegaWithT(t)
-		namespace := "starboard-ns"
-		clientset := fake.NewSimpleClientset(
-			&corev1.ConfigMap{
-				ObjectMeta: metav1.ObjectMeta{
-					Namespace: namespace,
-					Name:      starboard.ConfigMapName,
-				},
-				Data: map[string]string{
-					"foo":                        "bar",
-					"configAuditReports.scanner": string(starboard.Polaris),
-				},
-			},
-			&corev1.Secret{
-				ObjectMeta: metav1.ObjectMeta{
-					Namespace: namespace,
-					Name:      starboard.SecretName,
-				},
-				Data: map[string][]byte{
-					"baz": []byte("s3cret"),
-				},
-			},
-		)
-
-		err := starboard.NewConfigManager(clientset, namespace).EnsureDefault(context.TODO())
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-
-		cm, err := clientset.CoreV1().ConfigMaps(namespace).
-			Get(context.TODO(), starboard.ConfigMapName, metav1.GetOptions{})
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-		g.Expect(cm.Data).To(gomega.Equal(map[string]string{
-			"foo":                        "bar",
-			"configAuditReports.scanner": "Polaris",
-		}))
-
-		secret, err := clientset.CoreV1().Secrets(namespace).
-			Get(context.TODO(), starboard.SecretName, metav1.GetOptions{})
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-		g.Expect(secret.Data).To(gomega.Equal(map[string][]byte{
-			"baz": []byte("s3cret"),
-		}))
-
-		pluginConfig, err := clientset.CoreV1().ConfigMaps(namespace).
-			Get(context.TODO(), starboard.GetPluginConfigMapName(string(starboard.Polaris)), metav1.GetOptions{})
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-		g.Expect(pluginConfig.Data).To(gomega.Equal(starboard.GetDefaultPolarisConfig()))
-
-		_, err = clientset.CoreV1().ConfigMaps(namespace).
-			Get(context.TODO(), starboard.GetPluginConfigMapName(string(starboard.Conftest)), metav1.GetOptions{})
-		g.Expect(err).To(gomega.MatchError(`configmaps "starboard-conftest-config" not found`))
-	})
-
-	t.Run("Should create ConfigMap for Conftest", func(t *testing.T) {
-		g := gomega.NewGomegaWithT(t)
-		namespace := "starboard-ns"
-		clientset := fake.NewSimpleClientset(
-			&corev1.ConfigMap{
-				ObjectMeta: metav1.ObjectMeta{
-					Namespace: namespace,
-					Name:      starboard.ConfigMapName,
-				},
-				Data: map[string]string{
-					"foo":                        "bar",
-					"configAuditReports.scanner": string(starboard.Conftest),
-				},
-			},
-			&corev1.Secret{
-				ObjectMeta: metav1.ObjectMeta{
-					Namespace: namespace,
-					Name:      starboard.SecretName,
-				},
-				Data: map[string][]byte{
-					"baz": []byte("s3cret"),
-				},
-			},
-		)
-
-		err := starboard.NewConfigManager(clientset, namespace).EnsureDefault(context.TODO())
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-
-		cm, err := clientset.CoreV1().ConfigMaps(namespace).
-			Get(context.TODO(), starboard.ConfigMapName, metav1.GetOptions{})
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-		g.Expect(cm.Data).To(gomega.Equal(map[string]string{
-			"foo":                        "bar",
-			"configAuditReports.scanner": "Conftest",
-		}))
-
-		secret, err := clientset.CoreV1().Secrets(namespace).
-			Get(context.TODO(), starboard.SecretName, metav1.GetOptions{})
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-		g.Expect(secret.Data).To(gomega.Equal(map[string][]byte{
-			"baz": []byte("s3cret"),
-		}))
-
-		pluginConfig, err := clientset.CoreV1().ConfigMaps(namespace).
-			Get(context.TODO(), starboard.GetPluginConfigMapName(string(starboard.Conftest)), metav1.GetOptions{})
-		g.Expect(err).ToNot(gomega.HaveOccurred())
-		g.Expect(pluginConfig.Data).To(gomega.Equal(map[string]string{
-			"conftest.imageRef": "openpolicyagent/conftest:v0.25.0",
-		}))
-
-		_, err = clientset.CoreV1().ConfigMaps(namespace).
-			Get(context.TODO(), starboard.GetPluginConfigMapName(string(starboard.Polaris)), metav1.GetOptions{})
-		g.Expect(err).To(gomega.MatchError(`configmaps "starboard-polaris-config" not found`))
-	})
 }
 
 func TestConfigManager_Delete(t *testing.T) {