Repository: aquasecurity/starboard
Pull request #492 (2021-04-11 06:47:07)

test(cli): Add integration test for `Conftest` scanner
Resolves https://github.com/aquasecurity/starboard/issues/479

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/492?src=pr&el=h1) Report
> Merging [#492](https://codecov.io/gh/aquasecurity/starboard/pull/492?src=pr&el=desc) (bad3a3f) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/8929137cd59fbb21e881b4ee14011ed6d1989c6d?el=desc) (8929137) will **decrease** coverage by `33.35%`.
> The diff coverage is `66.66%`.

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

```diff
@@             Coverage Diff             @@
##             main     #492       +/-   ##
===========================================
- Coverage   69.25%   35.89%   -33.36%     
===========================================
  Files          62       61        -1     
  Lines        3984     3864      -120     
===========================================
- Hits         2759     1387     -1372     
- Misses        928     2396     +1468     
+ Partials      297       81      -216     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/492?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [itest/matcher/matcher.go](https://codecov.io/gh/aquasecurity/starboard/pull/492/diff?src=pr&el=tree#diff-aXRlc3QvbWF0Y2hlci9tYXRjaGVyLmdv) | `82.27% <66.66%> (-1.73%)` | :arrow_down: |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/492/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/492/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/492/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/492/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/492/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/492/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/492/diff?src=pr&el=tree#diff-cGtnL2NtZC9rdWJlX2JlbmNoLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/kube\_hunter.go](https://codecov.io/gh/aquasecurity/starboard/pull/492/diff?src=pr&el=tree#diff-cGtnL2NtZC9rdWJlX2h1bnRlci5nbw==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/find\_vulnerabilities.go](https://codecov.io/gh/aquasecurity/starboard/pull/492/diff?src=pr&el=tree#diff-cGtnL2NtZC9maW5kX3Z1bG5lcmFiaWxpdGllcy5nbw==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [35 more](https://codecov.io/gh/aquasecurity/starboard/pull/492/diff?src=pr&el=tree-more) | |

------

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


Patch:
diff --git a/pkg/plugin/trivy/plugin.go b/pkg/plugin/trivy/plugin.go
index c40fc5f..68e64b5 100644
--- a/pkg/plugin/trivy/plugin.go
+++ b/pkg/plugin/trivy/plugin.go
@@ -488,8 +488,8 @@ func (s *scanner) getPodSpecForClientServerMode(spec corev1.PodSpec, credentials
 				"trivy",
 			},
 			Args: []string{
-				"client",
 				"--quiet",
+				"client",
 				"--format",
 				"json",
 				"--remote",


Test patch:
diff --git a/itest/matcher/matcher.go b/itest/matcher/matcher.go
index 7ab74c3..a0511af 100644
--- a/itest/matcher/matcher.go
+++ b/itest/matcher/matcher.go
@@ -26,6 +26,11 @@ var (
 		Vendor:  "Fairwinds Ops",
 		Version: "3.2",
 	}
+	conftestScanner = v1alpha1.Scanner{
+		Name:    "Conftest",
+		Vendor:  "Open Policy Agent",
+		Version: "v0.23.0",
+	}
 )
 
 // IsVulnerabilityReportForContainerOwnedBy succeeds if a v1alpha1.VulnerabilityReport has a valid structure,
@@ -103,14 +108,16 @@ func (m *vulnerabilityReportMatcher) NegatedFailureMessage(_ interface{}) string
 //
 // Note: This matcher is not suitable for unit tests because it does not perform a strict validation
 // of the actual v1alpha1.ConfigAuditReport.
-func IsConfigAuditReportOwnedBy(owner client.Object) types.GomegaMatcher {
+func IsConfigAuditReportOwnedBy(owner client.Object, scanner starboard.Scanner) types.GomegaMatcher {
 	return &configAuditReportMatcher{
-		owner: owner,
+		owner:   owner,
+		scanner: scanner,
 	}
 }
 
 type configAuditReportMatcher struct {
 	owner                 client.Object
+	scanner               starboard.Scanner
 	failureMessage        string
 	negatedFailureMessage string
 }
@@ -125,6 +132,11 @@ func (m *configAuditReportMatcher) Match(actual interface{}) (bool, error) {
 		return false, err
 	}
 
+	scanner := polarisScanner
+	if m.scanner == starboard.Conftest {
+		scanner = conftestScanner
+	}
+
 	matcher := MatchFields(IgnoreExtras, Fields{
 		"ObjectMeta": MatchFields(IgnoreExtras, Fields{
 			"Labels": MatchAllKeys(Keys{
@@ -142,7 +154,7 @@ func (m *configAuditReportMatcher) Match(actual interface{}) (bool, error) {
 			}),
 		}),
 		"Report": MatchFields(IgnoreExtras, Fields{
-			"Scanner": Equal(polarisScanner),
+			"Scanner": Equal(scanner),
 		}),
 	})
 	success, err := matcher.Match(actual)
diff --git a/itest/matcher/matcher_test.go b/itest/matcher/matcher_test.go
index 8ef8a63..4512200 100644
--- a/itest/matcher/matcher_test.go
+++ b/itest/matcher/matcher_test.go
@@ -71,7 +71,7 @@ func TestConfigAuditReportMatcher(t *testing.T) {
 
 	t.Run("Should return error when actual is not ConfigAuditReport", func(t *testing.T) {
 		g := NewGomegaWithT(t)
-		instance := matcher.IsConfigAuditReportOwnedBy(&appsv1.ReplicaSet{})
+		instance := matcher.IsConfigAuditReportOwnedBy(&appsv1.ReplicaSet{}, starboard.Polaris)
 		_, err := instance.Match("I AM INVALID ACTUAL")
 		g.Expect(err).To(MatchError("matcher.configAuditReportMatcher expects a v1alpha1.ConfigAuditReport"))
 	})
@@ -84,7 +84,7 @@ func TestConfigAuditReportMatcher(t *testing.T) {
 				Namespace: "default",
 				UID:       "494b2727-5d52-4057-9a9b-8b508c753fea",
 			},
-		})
+		}, starboard.Polaris)
 		success, err := instance.Match(v1alpha1.ConfigAuditReport{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "replicaset-nginx-6d4cf56db6",
diff --git a/itest/starboard/starboard_cli_test.go b/itest/starboard/starboard_cli_test.go
index 3844efa..6e287fd 100644
--- a/itest/starboard/starboard_cli_test.go
+++ b/itest/starboard/starboard_cli_test.go
@@ -9,6 +9,7 @@ import (
 
 	"context"
 	"os"
+	"strings"
 	"time"
 
 	"github.com/aquasecurity/starboard/itest/helper"
@@ -24,6 +25,7 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/types"
+	"k8s.io/apimachinery/pkg/util/rand"
 	"k8s.io/utils/pointer"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/yaml"
@@ -831,51 +833,88 @@ var _ = Describe("Starboard CLI", func() {
 	})
 
 	Describe("Command scan configauditreports", func() {
+		var object client.Object
 
 		groupByWorkloadName := func(element interface{}) string {
 			return element.(v1alpha1.ConfigAuditReport).
 				Labels[starboard.LabelResourceName]
 		}
 
-		Context("when unmanaged Pod is specified as workload", func() {
-
-			var ctx context.Context
-			var pod *corev1.Pod
+		updateScannerConfig := func(scanner starboard.Scanner) {
+			configmap := &corev1.ConfigMap{}
+			err := kubeClient.Get(context.Background(), types.NamespacedName{
+				Namespace: "starboard",
+				Name:      "starboard",
+			}, configmap)
+			Expect(err).ToNot(HaveOccurred())
 
-			BeforeEach(func() {
-				ctx = context.TODO()
-				pod = helper.NewPod().WithName("nginx-polaris").
-					WithNamespace(testNamespace.Name).
-					WithContainer("nginx-container", "nginx:1.16").
-					Build()
-				err := kubeClient.Create(ctx, pod)
-				Expect(err).ToNot(HaveOccurred())
-			})
+			configmapCopy := configmap.DeepCopy()
+			configmapCopy.Data["configAuditReports.scanner"] = string(scanner)
+			err = kubeClient.Update(context.Background(), configmapCopy)
+			Expect(err).ToNot(HaveOccurred())
+		}
 
+		assertConfigAuditReportCreated := func(scanner starboard.Scanner, kind kube.Kind) {
 			It("should create ConfigAuditReport", func() {
+				updateScannerConfig(scanner)
 				err := cmd.Run(versionInfo, []string{
 					"starboard",
-					"scan", "configauditreports", "pod/" + pod.Name,
-					"--namespace", pod.Namespace,
+					"scan", "configauditreports", strings.ToLower(string(kind)) + "/" + object.GetName(),
+					"--namespace", object.GetNamespace(),
 					"-v", starboardCLILogLevel,
 				}, GinkgoWriter, GinkgoWriter)
 				Expect(err).ToNot(HaveOccurred())
 
 				var reportList v1alpha1.ConfigAuditReportList
-				err = kubeClient.List(ctx, &reportList, client.MatchingLabels{
-					starboard.LabelResourceKind:      string(kube.KindPod),
-					starboard.LabelResourceName:      pod.Name,
-					starboard.LabelResourceNamespace: pod.Namespace,
+				err = kubeClient.List(context.TODO(), &reportList, client.MatchingLabels{
+					starboard.LabelResourceKind:      string(kind),
+					starboard.LabelResourceName:      object.GetName(),
+					starboard.LabelResourceNamespace: object.GetNamespace(),
 				})
 				Expect(err).ToNot(HaveOccurred())
 
 				Expect(reportList.Items).To(MatchAllElements(groupByWorkloadName, Elements{
-					pod.Name: IsConfigAuditReportOwnedBy(pod),
+					object.GetName(): IsConfigAuditReportOwnedBy(object, scanner),
 				}))
 			})
+		}
+
+		BeforeEach(func() {
+			configmapCopy := conftestConfigMap.DeepCopy()
+			err := kubeClient.Create(context.TODO(), configmapCopy)
+			Expect(err).ToNot(HaveOccurred())
+		})
+
+		AfterEach(func() {
+			err := kubeClient.Delete(context.TODO(), conftestConfigMap)
+			Expect(err).ToNot(HaveOccurred())
+		})
+
+		Context("when unmanaged Pod is specified as workload", func() {
+
+			var ctx context.Context
+
+			BeforeEach(func() {
+				ctx = context.TODO()
+
+				object = helper.NewPod().WithName("nginx-polaris"+"-"+rand.String(5)).
+					WithNamespace(testNamespace.Name).
+					WithContainer("nginx-container", "nginx:1.16").
+					Build()
+				err := kubeClient.Create(ctx, object)
+				Expect(err).ToNot(HaveOccurred())
+			})
+
+			Context("with Polaris as scanner", func() {
+				assertConfigAuditReportCreated(starboard.Polaris, kube.KindPod)
+			})
+
+			Context("with Conftest as scanner", func() {
+				assertConfigAuditReportCreated(starboard.Conftest, kube.KindPod)
+			})
 
 			AfterEach(func() {
-				err := kubeClient.Delete(ctx, pod)
+				err := kubeClient.Delete(ctx, object)
 				Expect(err).ToNot(HaveOccurred())
 			})
 
@@ -884,43 +923,28 @@ var _ = Describe("Starboard CLI", func() {
 		Context("when unmanaged Pod with multiple containers is specified as workload", func() {
 
 			var ctx context.Context
-			var pod *corev1.Pod
 
 			BeforeEach(func() {
 				ctx = context.TODO()
-				pod = helper.NewPod().WithName("nginx-and-tomcat-starboard").
+				object = helper.NewPod().WithName("nginx-and-tomcat-starboard"+"-"+rand.String(5)).
 					WithNamespace(testNamespace.Name).
 					WithContainer("nginx-container", "nginx:1.16").
 					WithContainer("tomcat-container", "tomcat:8").
 					Build()
-				err := kubeClient.Create(ctx, pod)
+				err := kubeClient.Create(ctx, object)
 				Expect(err).ToNot(HaveOccurred())
 			})
 
-			It("should create ConfigAuditReports", func() {
-				err := cmd.Run(versionInfo, []string{
-					"starboard",
-					"scan", "configauditreports", "pod/" + pod.Name,
-					"--namespace", pod.Namespace,
-					"-v", starboardCLILogLevel,
-				}, GinkgoWriter, GinkgoWriter)
-				Expect(err).ToNot(HaveOccurred())
-
-				var reportList v1alpha1.ConfigAuditReportList
-				err = kubeClient.List(ctx, &reportList, client.MatchingLabels{
-					starboard.LabelResourceKind:      string(kube.KindPod),
-					starboard.LabelResourceName:      pod.Name,
-					starboard.LabelResourceNamespace: pod.Namespace,
-				})
-				Expect(err).ToNot(HaveOccurred())
+			Context("with Polaris as scanner", func() {
+				assertConfigAuditReportCreated(starboard.Polaris, kube.KindPod)
+			})
 
-				Expect(reportList.Items).To(MatchAllElements(groupByWorkloadName, Elements{
-					pod.Name: IsConfigAuditReportOwnedBy(pod),
-				}))
+			Context("with Conftest as scanner", func() {
+				assertConfigAuditReportCreated(starboard.Conftest, kube.KindPod)
 			})
 
 			AfterEach(func() {
-				err := kubeClient.Delete(ctx, pod)
+				err := kubeClient.Delete(ctx, object)
 				Expect(err).ToNot(HaveOccurred())
 			})
 
@@ -929,13 +953,11 @@ var _ = Describe("Starboard CLI", func() {
 		Context("when CronJob is specified as workload", func() {
 
 			var ctx context.Context
-			var cronJob *batchv1beta1.CronJob
-
 			BeforeEach(func() {
 				ctx = context.TODO()
-				cronJob = &batchv1beta1.CronJob{
+				object = &batchv1beta1.CronJob{
 					ObjectMeta: metav1.ObjectMeta{
-						Name:      "hello",
+						Name:      "hello" + "-" + rand.String(5),
 						Namespace: testNamespace.Name,
 					},
 					Spec: batchv1beta1.CronJobSpec{
@@ -962,35 +984,20 @@ var _ = Describe("Starboard CLI", func() {
 						},
 					},
 				}
-				err := kubeClient.Create(ctx, cronJob)
+				err := kubeClient.Create(ctx, object)
 				Expect(err).ToNot(HaveOccurred())
 			})
 
-			It("should create ConfigAuditReport", func() {
-				err := cmd.Run(versionInfo, []string{
-					"starboard",
-					"scan", "configauditreports", "cronjob/" + cronJob.Name,
-					"--namespace", cronJob.Namespace,
-					"-v", starboardCLILogLevel,
-				}, GinkgoWriter, GinkgoWriter)
-				Expect(err).ToNot(HaveOccurred())
-
-				var reportList v1alpha1.ConfigAuditReportList
-
-				err = kubeClient.List(ctx, &reportList, client.MatchingLabels{
-					starboard.LabelResourceKind:      string(kube.KindCronJob),
-					starboard.LabelResourceName:      cronJob.Name,
-					starboard.LabelResourceNamespace: cronJob.Namespace,
-				})
-				Expect(err).ToNot(HaveOccurred())
-				Expect(reportList.Items).To(MatchAllElements(groupByWorkloadName, Elements{
-					cronJob.Name: IsConfigAuditReportOwnedBy(cronJob),
-				}))
+			Context("with Polaris as scanner", func() {
+				assertConfigAuditReportCreated(starboard.Polaris, kube.KindCronJob)
+			})
 
+			Context("with Conftest as scanner", func() {
+				assertConfigAuditReportCreated(starboard.Conftest, kube.KindCronJob)
 			})
 
 			AfterEach(func() {
-				err := kubeClient.Delete(ctx, cronJob)
+				err := kubeClient.Delete(ctx, object)
 				Expect(err).ToNot(HaveOccurred())
 			})
 		})
diff --git a/itest/starboard/suite_test.go b/itest/starboard/suite_test.go
index d822ba5..309e419 100644
--- a/itest/starboard/suite_test.go
+++ b/itest/starboard/suite_test.go
@@ -37,6 +37,38 @@ var (
 			Name: namespaceItest,
 		},
 	}
+
+	conftestConfigMap = &corev1.ConfigMap{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "starboard-conftest-config",
+			Namespace: "starboard",
+		},
+		Data: map[string]string{
+			"conftest.policy.kubernetes": `
+	package main
+    
+    deny[msg] {
+      input.kind == "ReplicaSet"
+      not input.spec.template.spec.securityContext.runAsNonRoot
+
+      msg := "Containers must not run as root"
+    }
+
+    deny[msg] {
+      input.kind == "Pod"
+      not input.spec.securityContext.runAsNonRoot
+
+      msg := "Containers must not run as root"
+    }
+
+    deny[msg] {
+      input.kind == "CronJob"
+      not input.spec.jobTemplate.spec.template.spec.securityContext.runAsNonRoot
+
+      msg := "Containers must not run as root"
+    }`,
+		},
+	}
 )
 
 var (
diff --git a/pkg/plugin/trivy/plugin_test.go b/pkg/plugin/trivy/plugin_test.go
index d57a93e..6295fee 100644
--- a/pkg/plugin/trivy/plugin_test.go
+++ b/pkg/plugin/trivy/plugin_test.go
@@ -350,8 +350,8 @@ func TestScanner_GetScanJobSpec(t *testing.T) {
 							"trivy",
 						},
 						Args: []string{
-							"client",
 							"--quiet",
+							"client",
 							"--format",
 							"json",
 							"--remote",