Repository: aquasecurity/starboard
Pull request #643 (2021-07-23 21:29:13)

feat: Define interface for saving and finding ClusterConfigAuditReports
Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/643?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) Report
> Merging [#643](https://codecov.io/gh/aquasecurity/starboard/pull/643?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (fd62ef1) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/3e56c16c7d1b2a79e6a8e926b25a253baefdbd12?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (3e56c16) will **decrease** coverage by `22.96%`.
> The diff coverage is `0.00%`.

[![Impacted file tree graph](https://codecov.io/gh/aquasecurity/starboard/pull/643/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/643?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)

```diff
@@             Coverage Diff             @@
##             main     #643       +/-   ##
===========================================
- Coverage   68.34%   45.37%   -22.97%     
===========================================
  Files          58       41       -17     
  Lines        4489     3656      -833     
===========================================
- Hits         3068     1659     -1409     
- Misses       1061     1892      +831     
+ Partials      360      105      -255     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/643?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) | Coverage Δ | |
|---|---|---|
| [pkg/configauditreport/io.go](https://codecov.io/gh/aquasecurity/starboard/pull/643/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2NvbmZpZ2F1ZGl0cmVwb3J0L2lvLmdv) | `34.92% <0.00%> (-21.18%)` | :arrow_down: |
| [pkg/operator/controller/configauditreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/643/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY29uZmlnYXVkaXRyZXBvcnQuZ28=) | `0.00% <0.00%> (-64.82%)` | :arrow_down: |
| [pkg/report/html.go](https://codecov.io/gh/aquasecurity/starboard/pull/643/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3JlcG9ydC9odG1sLmdv) | `19.56% <0.00%> (ø)` | |
| [pkg/kubehunter/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/643/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2t1YmVodW50ZXIvc2Nhbm5lci5nbw==) | `0.00% <0.00%> (-80.51%)` | :arrow_down: |
| [pkg/plugin/factory.go](https://codecov.io/gh/aquasecurity/starboard/pull/643/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3BsdWdpbi9mYWN0b3J5Lmdv) | `0.00% <0.00%> (-80.49%)` | :arrow_down: |
| [pkg/configauditreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/643/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/vulnerabilityreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/643/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvc2Nhbm5lci5nbw==) | `0.00% <0.00%> (-70.54%)` | :arrow_down: |
| [pkg/operator/controller/vulnerabilityreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/643/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%> (-68.04%)` | :arrow_down: |
| [pkg/kube/runnable\_job.go](https://codecov.io/gh/aquasecurity/starboard/pull/643/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2t1YmUvcnVubmFibGVfam9iLmdv) | `0.00% <0.00%> (-64.39%)` | :arrow_down: |
| ... and [29 more](https://codecov.io/gh/aquasecurity/starboard/pull/643/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/643?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/643?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity). Last update [3e56c16...fd62ef1](https://codecov.io/gh/aquasecurity/starboard/pull/643?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/get_configaudit.go b/pkg/cmd/get_configaudit.go
index ec7e71c..fba7e41 100644
--- a/pkg/cmd/get_configaudit.go
+++ b/pkg/cmd/get_configaudit.go
@@ -57,7 +57,7 @@ NAME is the name of a particular Kubernetes workload.
 				return err
 			}
 			reader := configauditreport.NewReadWriter(kubeClient)
-			report, err := reader.FindByOwnerInHierarchy(ctx, workload)
+			report, err := reader.FindReportByOwnerInHierarchy(ctx, workload)
 			if err != nil {
 				return nil
 			}
diff --git a/pkg/cmd/scan_configauditreports.go b/pkg/cmd/scan_configauditreports.go
index d3c3d86..4b1e6d9 100644
--- a/pkg/cmd/scan_configauditreports.go
+++ b/pkg/cmd/scan_configauditreports.go
@@ -78,6 +78,6 @@ func ScanConfigAuditReports(buildInfo starboard.BuildInfo, cf *genericclioptions
 			return err
 		}
 		writer := configauditreport.NewReadWriter(kubeClient)
-		return writer.Write(ctx, report)
+		return writer.WriteReport(ctx, report)
 	}
 }
diff --git a/pkg/configauditreport/io.go b/pkg/configauditreport/io.go
index eb2f955..1bf2cb6 100644
--- a/pkg/configauditreport/io.go
+++ b/pkg/configauditreport/io.go
@@ -12,25 +12,35 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
-// Writer is the interface that wraps the basic Write method.
-//
-// Write creates or updates the given v1alpha1.ConfigAuditReport instance.
+// Writer is the interface for saving v1alpha1.ClusterConfigAuditReport
+// and v1alpha1.ConfigAuditReport instances.
 type Writer interface {
-	Write(ctx context.Context, report v1alpha1.ConfigAuditReport) error
+
+	// WriteReport creates or updates the given v1alpha1.ConfigAuditReport instance.
+	WriteReport(ctx context.Context, report v1alpha1.ConfigAuditReport) error
+
+	// WriteClusterReport creates or updates the given v1alpha1.ClusterConfigAuditReport instance.
+	WriteClusterReport(ctx context.Context, report v1alpha1.ClusterConfigAuditReport) error
 }
 
-// Reader is the interface that wraps methods for finding v1alpha1.ConfigAuditReport objects.
-//
-// FindByOwner returns a v1alpha1.ConfigAuditReport owned by the given
-// kube.Object or nil if the report is not found.
-//
-// FindByOwnerInHierarchy is similar to FindByOwner except that it tries to lookup
-// a v1alpha1.ConfigAuditReport object owned by related Kubernetes objects.
-// For example, if the given owner is a Deployment, but a report is owned by the
-// active ReplicaSet (current revision) this method will return the report.
+// Reader is the interface that wraps methods for finding v1alpha1.ConfigAuditReport
+// and v1alpha1.ClusterConfigAuditReport objects.
+// TODO(danielpacak): Consider returning starboard.ResourceNotFound error instead of returning nil.
 type Reader interface {
-	FindByOwner(ctx context.Context, owner kube.Object) (*v1alpha1.ConfigAuditReport, error)
-	FindByOwnerInHierarchy(ctx context.Context, owner kube.Object) (*v1alpha1.ConfigAuditReport, error)
+
+	// FindReportByOwner returns a v1alpha1.ConfigAuditReport owned by the given
+	// kube.Object or nil if the report is not found.
+	FindReportByOwner(ctx context.Context, owner kube.Object) (*v1alpha1.ConfigAuditReport, error)
+
+	// FindReportByOwnerInHierarchy is similar to FindReportByOwner except that it tries to lookup
+	// a v1alpha1.ConfigAuditReport object owned by related Kubernetes objects.
+	// For example, if the given owner is a Deployment, but a report is owned by the
+	// active ReplicaSet (current revision) this method will return the report.
+	FindReportByOwnerInHierarchy(ctx context.Context, owner kube.Object) (*v1alpha1.ConfigAuditReport, error)
+
+	// FindClusterReportByOwner returns a v1alpha1.ClusterConfigAuditReport owned by the given
+	// kube.Object or nil if the report is not found.
+	FindClusterReportByOwner(ctx context.Context, owner kube.Object) (*v1alpha1.ClusterConfigAuditReport, error)
 }
 
 type ReadWriter interface {
@@ -51,7 +61,7 @@ func NewReadWriter(client client.Client) ReadWriter {
 	}
 }
 
-func (r *readWriter) Write(ctx context.Context, report v1alpha1.ConfigAuditReport) error {
+func (r *readWriter) WriteReport(ctx context.Context, report v1alpha1.ConfigAuditReport) error {
 	var existing v1alpha1.ConfigAuditReport
 	err := r.Get(ctx, types.NamespacedName{
 		Name:      report.Name,
@@ -73,7 +83,28 @@ func (r *readWriter) Write(ctx context.Context, report v1alpha1.ConfigAuditRepor
 	return err
 }
 
-func (r *readWriter) FindByOwner(ctx context.Context, owner kube.Object) (*v1alpha1.ConfigAuditReport, error) {
+func (r *readWriter) WriteClusterReport(ctx context.Context, report v1alpha1.ClusterConfigAuditReport) error {
+	var existing v1alpha1.ClusterConfigAuditReport
+	err := r.Get(ctx, types.NamespacedName{
+		Name: report.Name,
+	}, &existing)
+
+	if err == nil {
+		copied := existing.DeepCopy()
+		copied.Labels = report.Labels
+		copied.Report = report.Report
+
+		return r.Update(ctx, copied)
+	}
+
+	if errors.IsNotFound(err) {
+		return r.Create(ctx, &report)
+	}
+
+	return err
+}
+
+func (r *readWriter) FindReportByOwner(ctx context.Context, owner kube.Object) (*v1alpha1.ConfigAuditReport, error) {
 	var list v1alpha1.ConfigAuditReportList
 
 	err := r.List(ctx, &list, client.MatchingLabels{
@@ -92,8 +123,8 @@ func (r *readWriter) FindByOwner(ctx context.Context, owner kube.Object) (*v1alp
 	return nil, nil
 }
 
-func (r *readWriter) FindByOwnerInHierarchy(ctx context.Context, owner kube.Object) (*v1alpha1.ConfigAuditReport, error) {
-	report, err := r.FindByOwner(ctx, owner)
+func (r *readWriter) FindReportByOwnerInHierarchy(ctx context.Context, owner kube.Object) (*v1alpha1.ConfigAuditReport, error) {
+	report, err := r.FindReportByOwner(ctx, owner)
 	if err != nil {
 		return nil, err
 	}
@@ -104,7 +135,7 @@ func (r *readWriter) FindByOwnerInHierarchy(ctx context.Context, owner kube.Obje
 		if err != nil {
 			return nil, fmt.Errorf("getting replicaset related to %s/%s: %w", owner.Kind, owner.Name, err)
 		}
-		report, err = r.FindByOwner(ctx, kube.Object{
+		report, err = r.FindReportByOwner(ctx, kube.Object{
 			Kind:      kube.KindReplicaSet,
 			Name:      rsName,
 			Namespace: owner.Namespace,
@@ -117,3 +148,21 @@ func (r *readWriter) FindByOwnerInHierarchy(ctx context.Context, owner kube.Obje
 	}
 	return nil, nil
 }
+
+func (r *readWriter) FindClusterReportByOwner(ctx context.Context, owner kube.Object) (*v1alpha1.ClusterConfigAuditReport, error) {
+	var list v1alpha1.ClusterConfigAuditReportList
+
+	err := r.List(ctx, &list, client.MatchingLabels{
+		starboard.LabelResourceKind: string(owner.Kind),
+		starboard.LabelResourceName: owner.Name,
+	})
+	if err != nil {
+		return nil, err
+	}
+
+	// Only one config audit per specific workload exists on the cluster
+	if len(list.Items) > 0 {
+		return &list.DeepCopy().Items[0], nil
+	}
+	return nil, nil
+}
diff --git a/pkg/operator/controller/configauditreport.go b/pkg/operator/controller/configauditreport.go
index 55a761f..c510503 100644
--- a/pkg/operator/controller/configauditreport.go
+++ b/pkg/operator/controller/configauditreport.go
@@ -222,7 +222,7 @@ func (r *ConfigAuditReportReconciler) reconcileWorkload(workloadKind kube.Kind)
 
 func (r *ConfigAuditReportReconciler) hasReport(ctx context.Context, owner kube.Object, podSpecHash string, pluginConfigHash string) (bool, error) {
 	// TODO FindByOwner should accept optional label selector to further narrow down search results
-	report, err := r.ReadWriter.FindByOwner(ctx, owner)
+	report, err := r.ReadWriter.FindReportByOwner(ctx, owner)
 	if err != nil {
 		return false, err
 	}
@@ -342,7 +342,7 @@ func (r *ConfigAuditReportReconciler) processCompleteScanJob(ctx context.Context
 		return err
 	}
 
-	err = r.ReadWriter.Write(ctx, report)
+	err = r.ReadWriter.WriteReport(ctx, report)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/report/html.go b/pkg/report/html.go
index 37d0f57..4d388f2 100644
--- a/pkg/report/html.go
+++ b/pkg/report/html.go
@@ -34,7 +34,7 @@ func NewWorkloadReporter(clock ext.Clock, client client.Client) WorkloadReporter
 
 func (h *workloadReporter) RetrieveData(workload kube.Object) (templates.WorkloadReport, error) {
 	ctx := context.Background()
-	configAuditReport, err := h.configAuditReportsReader.FindByOwnerInHierarchy(ctx, workload)
+	configAuditReport, err := h.configAuditReportsReader.FindReportByOwnerInHierarchy(ctx, workload)
 	if err != nil {
 		return templates.WorkloadReport{}, err
 	}


Test patch:
diff --git a/pkg/configauditreport/io_test.go b/pkg/configauditreport/io_test.go
index e37c856..2a5f44b 100644
--- a/pkg/configauditreport/io_test.go
+++ b/pkg/configauditreport/io_test.go
@@ -22,14 +22,14 @@ func TestReadWriter(t *testing.T) {
 	t.Run("Should create ConfigAuditReport", func(t *testing.T) {
 		client := fake.NewClientBuilder().WithScheme(kubernetesScheme).Build()
 		readWriter := configauditreport.NewReadWriter(client)
-		err := readWriter.Write(context.TODO(), v1alpha1.ConfigAuditReport{
+		err := readWriter.WriteReport(context.TODO(), v1alpha1.ConfigAuditReport{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "deployment-app",
 				Namespace: "qa",
 				Labels: map[string]string{
-					"starboard.resource.kind":      "Deployment",
-					"starboard.resource.name":      "app",
-					"starboard.resource.namespace": "qa",
+					starboard.LabelResourceKind:      "Deployment",
+					starboard.LabelResourceName:      "app",
+					starboard.LabelResourceNamespace: "qa",
 				},
 			},
 			Report: v1alpha1.ConfigAuditReportData{
@@ -54,9 +54,9 @@ func TestReadWriter(t *testing.T) {
 				Name:      "deployment-app",
 				Namespace: "qa",
 				Labels: map[string]string{
-					"starboard.resource.kind":      "Deployment",
-					"starboard.resource.name":      "app",
-					"starboard.resource.namespace": "qa",
+					starboard.LabelResourceKind:      "Deployment",
+					starboard.LabelResourceName:      "app",
+					starboard.LabelResourceNamespace: "qa",
 				},
 				ResourceVersion: "1",
 			},
@@ -76,10 +76,10 @@ func TestReadWriter(t *testing.T) {
 				Namespace:       "qa",
 				ResourceVersion: "0",
 				Labels: map[string]string{
-					"starboard.resource.kind":      "Deployment",
-					"starboard.resource.name":      "app",
-					"starboard.resource.namespace": "qa",
-					"pod-spec-hash":                "h1",
+					starboard.LabelResourceKind:      "Deployment",
+					starboard.LabelResourceName:      "app",
+					starboard.LabelResourceNamespace: "qa",
+					starboard.LabelPodSpecHash:       "h1",
 				},
 			},
 			Report: v1alpha1.ConfigAuditReportData{
@@ -90,15 +90,15 @@ func TestReadWriter(t *testing.T) {
 			},
 		}).Build()
 		readWriter := configauditreport.NewReadWriter(client)
-		err := readWriter.Write(context.TODO(), v1alpha1.ConfigAuditReport{
+		err := readWriter.WriteReport(context.TODO(), v1alpha1.ConfigAuditReport{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "deployment-app",
 				Namespace: "qa",
 				Labels: map[string]string{
-					"starboard.resource.kind":      "Deployment",
-					"starboard.resource.name":      "app",
-					"starboard.resource.namespace": "qa",
-					"pod-spec-hash":                "h2",
+					starboard.LabelResourceKind:      "Deployment",
+					starboard.LabelResourceName:      "app",
+					starboard.LabelResourceNamespace: "qa",
+					starboard.LabelPodSpecHash:       "h2",
 				},
 			},
 			Report: v1alpha1.ConfigAuditReportData{
@@ -123,10 +123,10 @@ func TestReadWriter(t *testing.T) {
 				Name:      "deployment-app",
 				Namespace: "qa",
 				Labels: map[string]string{
-					"starboard.resource.kind":      "Deployment",
-					"starboard.resource.name":      "app",
-					"starboard.resource.namespace": "qa",
-					"pod-spec-hash":                "h2",
+					starboard.LabelResourceKind:      "Deployment",
+					starboard.LabelResourceName:      "app",
+					starboard.LabelResourceNamespace: "qa",
+					starboard.LabelPodSpecHash:       "h2",
 				},
 				ResourceVersion: "1",
 			},
@@ -166,7 +166,7 @@ func TestReadWriter(t *testing.T) {
 		}).Build()
 
 		readWriter := configauditreport.NewReadWriter(client)
-		found, err := readWriter.FindByOwner(context.TODO(), kube.Object{
+		found, err := readWriter.FindReportByOwner(context.TODO(), kube.Object{
 			Kind:      kube.KindDeployment,
 			Name:      "my-deploy",
 			Namespace: "my-namespace",
@@ -186,4 +186,165 @@ func TestReadWriter(t *testing.T) {
 			Report: v1alpha1.ConfigAuditReportData{},
 		}, found)
 	})
+
+	t.Run("Should create ClusterConfigAuditReport", func(t *testing.T) {
+		client := fake.NewClientBuilder().WithScheme(kubernetesScheme).Build()
+		readWriter := configauditreport.NewReadWriter(client)
+		err := readWriter.WriteClusterReport(context.TODO(), v1alpha1.ClusterConfigAuditReport{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "clusterrole-admin",
+				Labels: map[string]string{
+					starboard.LabelResourceKind: "ClusterRole",
+					starboard.LabelResourceName: "admin",
+				},
+			},
+			Report: v1alpha1.ConfigAuditReportData{
+				Summary: v1alpha1.ConfigAuditSummary{
+					WarningCount: 8,
+					DangerCount:  3,
+				},
+			},
+		})
+		require.NoError(t, err)
+
+		var found v1alpha1.ClusterConfigAuditReport
+		err = client.Get(context.TODO(), types.NamespacedName{Name: "clusterrole-admin"}, &found)
+		require.NoError(t, err)
+
+		assert.Equal(t, v1alpha1.ClusterConfigAuditReport{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "ClusterConfigAuditReport",
+				APIVersion: "aquasecurity.github.io/v1alpha1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "clusterrole-admin",
+				Labels: map[string]string{
+					starboard.LabelResourceKind: "ClusterRole",
+					starboard.LabelResourceName: "admin",
+				},
+				ResourceVersion: "1",
+			},
+			Report: v1alpha1.ConfigAuditReportData{
+				Summary: v1alpha1.ConfigAuditSummary{
+					WarningCount: 8,
+					DangerCount:  3,
+				},
+			},
+		}, found)
+	})
+
+	t.Run("Should update ClusterConfigAuditReport", func(t *testing.T) {
+		client := fake.NewClientBuilder().
+			WithScheme(kubernetesScheme).
+			WithObjects(
+				&v1alpha1.ClusterConfigAuditReport{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            "clusterrole-admin",
+						ResourceVersion: "0",
+						Labels: map[string]string{
+							starboard.LabelResourceKind: "ClusterRole",
+							starboard.LabelResourceName: "admin",
+							starboard.LabelPodSpecHash:  "h1",
+						},
+					},
+					Report: v1alpha1.ConfigAuditReportData{
+						Summary: v1alpha1.ConfigAuditSummary{
+							WarningCount: 8,
+							DangerCount:  3,
+						},
+					},
+				}).
+			Build()
+		readWriter := configauditreport.NewReadWriter(client)
+		err := readWriter.WriteClusterReport(context.TODO(), v1alpha1.ClusterConfigAuditReport{
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "clusterrole-admin",
+				Labels: map[string]string{
+					starboard.LabelResourceKind: "ClusterRole",
+					starboard.LabelResourceName: "admin",
+					starboard.LabelPodSpecHash:  "h2",
+				},
+			},
+			Report: v1alpha1.ConfigAuditReportData{
+				Summary: v1alpha1.ConfigAuditSummary{
+					WarningCount: 9,
+					DangerCount:  2,
+				},
+			},
+		})
+		require.NoError(t, err)
+
+		var found v1alpha1.ClusterConfigAuditReport
+		err = client.Get(context.TODO(), types.NamespacedName{Name: "clusterrole-admin"}, &found)
+		require.NoError(t, err)
+
+		assert.Equal(t, v1alpha1.ClusterConfigAuditReport{
+			TypeMeta: metav1.TypeMeta{
+				Kind:       "ClusterConfigAuditReport",
+				APIVersion: "aquasecurity.github.io/v1alpha1",
+			},
+			ObjectMeta: metav1.ObjectMeta{
+				Name: "clusterrole-admin",
+				Labels: map[string]string{
+					starboard.LabelResourceKind: "ClusterRole",
+					starboard.LabelResourceName: "admin",
+					starboard.LabelPodSpecHash:  "h2",
+				},
+				ResourceVersion: "1",
+			},
+			Report: v1alpha1.ConfigAuditReportData{
+				Summary: v1alpha1.ConfigAuditSummary{
+					WarningCount: 9,
+					DangerCount:  2,
+				},
+			},
+		}, found)
+	})
+
+	t.Run("Should find ClusterConfigAuditReport by owner", func(t *testing.T) {
+		client := fake.NewClientBuilder().
+			WithScheme(kubernetesScheme).
+			WithObjects(
+				&v1alpha1.ClusterConfigAuditReport{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            "clusterrole-viewer",
+						ResourceVersion: "1",
+						Labels: map[string]string{
+							starboard.LabelResourceKind: "ClusterRole",
+							starboard.LabelResourceName: "viewer",
+						},
+					},
+					Report: v1alpha1.ConfigAuditReportData{},
+				},
+				&v1alpha1.ClusterConfigAuditReport{
+					ObjectMeta: metav1.ObjectMeta{
+						Name:            "clusterrole-editor",
+						ResourceVersion: "1",
+						Labels: map[string]string{
+							starboard.LabelResourceKind: "ClusterRole",
+							starboard.LabelResourceName: "editor",
+						},
+					},
+					Report: v1alpha1.ConfigAuditReportData{},
+				}).
+			Build()
+
+		readWriter := configauditreport.NewReadWriter(client)
+		found, err := readWriter.FindClusterReportByOwner(context.TODO(), kube.Object{
+			Kind: "ClusterRole",
+			Name: "editor",
+		})
+		require.NoError(t, err)
+		assert.Equal(t, &v1alpha1.ClusterConfigAuditReport{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:            "clusterrole-editor",
+				ResourceVersion: "1",
+				Labels: map[string]string{
+					starboard.LabelResourceKind: "ClusterRole",
+					starboard.LabelResourceName: "editor",
+				},
+			},
+			Report: v1alpha1.ConfigAuditReportData{},
+		}, found)
+	})
 }