Repository: aquasecurity/starboard
Pull request #226 (2020-10-24 00:31:43)

feat(cli): Use deterministic names for VulnerabilityReports
Resolves: #223

Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/226?src=pr&el=h1) Report
> Merging [#226](https://codecov.io/gh/aquasecurity/starboard/pull/226?src=pr&el=desc) into [master](https://codecov.io/gh/aquasecurity/starboard/commit/e39d36a69409c27b09955fdec82197e34fc57502?el=desc) will **decrease** coverage by `18.13%`.
> The diff coverage is `39.13%`.

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

```diff
@@             Coverage Diff             @@
##           master     #226       +/-   ##
===========================================
- Coverage   36.25%   18.11%   -18.14%     
===========================================
  Files          38       39        +1     
  Lines        1851     1844        -7     
===========================================
- Hits          671      334      -337     
- Misses       1069     1479      +410     
+ Partials      111       31       -80     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/226?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/find/vulnerabilities/crd/writer.go](https://codecov.io/gh/aquasecurity/starboard/pull/226/diff?src=pr&el=tree#diff-cGtnL2ZpbmQvdnVsbmVyYWJpbGl0aWVzL2NyZC93cml0ZXIuZ28=) | `28.00% <0.00%> (+4.66%)` | :arrow_up: |
| [pkg/kube/object.go](https://codecov.io/gh/aquasecurity/starboard/pull/226/diff?src=pr&el=tree#diff-cGtnL2t1YmUvb2JqZWN0Lmdv) | `27.90% <ø> (-29.93%)` | :arrow_down: |
| [pkg/operator/etc/config.go](https://codecov.io/gh/aquasecurity/starboard/pull/226/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL2V0Yy9jb25maWcuZ28=) | `72.72% <ø> (ø)` | |
| [pkg/vulnerabilityreport/builder.go](https://codecov.io/gh/aquasecurity/starboard/pull/226/diff?src=pr&el=tree#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvYnVpbGRlci5nbw==) | `60.46% <60.46%> (ø)` | |
| [pkg/kubebench/crd/writer.go](https://codecov.io/gh/aquasecurity/starboard/pull/226/diff?src=pr&el=tree#diff-cGtnL2t1YmViZW5jaC9jcmQvd3JpdGVyLmdv) | `87.09% <100.00%> (ø)` | |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/226/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/226/diff?src=pr&el=tree#diff-cGtnL2NtZC9maW5kLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/version.go](https://codecov.io/gh/aquasecurity/starboard/pull/226/diff?src=pr&el=tree#diff-cGtnL2NtZC92ZXJzaW9uLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/root.go](https://codecov.io/gh/aquasecurity/starboard/pull/226/diff?src=pr&el=tree#diff-cGtnL2NtZC9yb290Lmdv) | `0.00% <0.00%> (-93.34%)` | :arrow_down: |
| [pkg/kube/runnable\_job.go](https://codecov.io/gh/aquasecurity/starboard/pull/226/diff?src=pr&el=tree#diff-cGtnL2t1YmUvcnVubmFibGVfam9iLmdv) | `0.00% <0.00%> (-77.15%)` | :arrow_down: |
| ... and [14 more](https://codecov.io/gh/aquasecurity/starboard/pull/226/diff?src=pr&el=tree-more) | |

------

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


Patch:
diff --git a/pkg/find/vulnerabilities/crd/writer.go b/pkg/find/vulnerabilities/crd/writer.go
index a6840df..6ed8445 100644
--- a/pkg/find/vulnerabilities/crd/writer.go
+++ b/pkg/find/vulnerabilities/crd/writer.go
@@ -2,96 +2,85 @@ package crd
 
 import (
 	"context"
-	"fmt"
 
+	"github.com/aquasecurity/starboard/pkg/kube"
+	"github.com/aquasecurity/starboard/pkg/vulnerabilityreport"
+	"k8s.io/apimachinery/pkg/api/errors"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/klog"
 
-	"github.com/aquasecurity/starboard/pkg/kube"
-
 	starboard "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
 	"github.com/aquasecurity/starboard/pkg/find/vulnerabilities"
 	clientset "github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
-	"github.com/google/uuid"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
-type ReadWriter struct {
+type readWriter struct {
 	scheme *runtime.Scheme
 	client clientset.Interface
 }
 
-func NewReadWriter(scheme *runtime.Scheme, client clientset.Interface) *ReadWriter {
-	return &ReadWriter{
+func NewReadWriter(scheme *runtime.Scheme, client clientset.Interface) vulnerabilities.ReadWriter {
+	return &readWriter{
 		scheme: scheme,
 		client: client,
 	}
 }
 
-func (s *ReadWriter) Write(ctx context.Context, reports vulnerabilities.WorkloadVulnerabilities, owner metav1.Object) (err error) {
+func (s *readWriter) Write(ctx context.Context, reports vulnerabilities.WorkloadVulnerabilities, owner metav1.Object) error {
 	for container, report := range reports {
-		err = s.createVulnerability(ctx, container, report, owner)
+		err := s.createOrUpdate(ctx, container, report, owner)
 		if err != nil {
-			return
+			return err
 		}
 	}
-	return
+	return nil
 }
 
-func (s *ReadWriter) createVulnerability(ctx context.Context, container string, report starboard.VulnerabilityScanResult, owner metav1.Object) (err error) {
+func (s *readWriter) createOrUpdate(ctx context.Context, container string, scanResult starboard.VulnerabilityScanResult, owner metav1.Object) error {
 	namespace := owner.GetNamespace()
-	name := owner.GetName()
-	kind, err := kube.KindForObject(owner, s.scheme)
+
+	reportName, err := vulnerabilityreport.NewNameBuilder(s.scheme).
+		Owner(owner).
+		Container(container).Get()
+
 	if err != nil {
 		return err
 	}
 
-	// Trying to find previously generated vulnerability report for this specific container
-	vulnsSearch, err := s.client.AquasecurityV1alpha1().VulnerabilityReports(namespace).List(ctx, metav1.ListOptions{
-		LabelSelector: labels.Set{
-			kube.LabelResourceKind:  kind,
-			kube.LabelResourceName:  name,
-			kube.LabelContainerName: container,
-		}.String(),
-	})
-	if err != nil {
-		return
+	report, err := s.client.AquasecurityV1alpha1().VulnerabilityReports(owner.GetNamespace()).
+		Get(ctx, reportName, metav1.GetOptions{})
+
+	if err == nil && report != nil {
+		klog.V(3).Infof("Updating VulnerabilityReport %q", namespace+"/"+reportName)
+		deepCopy := report.DeepCopy()
+		deepCopy.Report = scanResult
+		_, err = s.client.AquasecurityV1alpha1().VulnerabilityReports(namespace).
+			Update(ctx, deepCopy, metav1.UpdateOptions{})
+		return err
 	}
 
-	if len(vulnsSearch.Items) > 0 {
-		existingCR := vulnsSearch.Items[0]
-		klog.V(3).Infof("Updating vulnerability report: %s/%s", namespace, name)
-		deepCopy := existingCR.DeepCopy()
-		deepCopy.Report = report
-		_, err = s.client.AquasecurityV1alpha1().VulnerabilityReports(namespace).Update(ctx, deepCopy, metav1.UpdateOptions{})
-	} else {
-		klog.V(3).Infof("Creating vulnerability report: %s/%s", namespace, name)
-		report := &starboard.VulnerabilityReport{
-			ObjectMeta: metav1.ObjectMeta{
-				Name:      fmt.Sprintf(uuid.New().String()),
-				Namespace: namespace,
-				Labels: map[string]string{
-					kube.LabelResourceKind:      kind,
-					kube.LabelResourceName:      name,
-					kube.LabelResourceNamespace: namespace,
-					kube.LabelContainerName:     container,
-				},
-			},
-			Report: report,
-		}
-		err = kube.SetOwnerReference(owner, report, s.scheme)
+	if errors.IsNotFound(err) {
+		klog.V(3).Infof("Creating VulnerabilityReport %q", namespace+"/"+reportName)
+		report, err := vulnerabilityreport.NewBuilder(s.scheme).
+			Owner(owner).
+			Container(container).
+			ScanResult(scanResult).
+			ReportName(reportName).
+			Get()
 		if err != nil {
 			return err
 		}
 		_, err = s.client.AquasecurityV1alpha1().VulnerabilityReports(namespace).
-			Create(ctx, report, metav1.CreateOptions{})
+			Create(ctx, &report, metav1.CreateOptions{})
+		return err
 	}
 
 	return err
 }
 
-func (s *ReadWriter) Read(ctx context.Context, workload kube.Object) (vulnerabilities.WorkloadVulnerabilities, error) {
+func (s *readWriter) Read(ctx context.Context, workload kube.Object) (vulnerabilities.WorkloadVulnerabilities, error) {
 	list, err := s.client.AquasecurityV1alpha1().VulnerabilityReports(workload.Namespace).List(ctx, metav1.ListOptions{
 		LabelSelector: labels.Set{
 			kube.LabelResourceKind:      string(workload.Kind),
diff --git a/pkg/kube/object.go b/pkg/kube/object.go
index 42367fb..63d289d 100644
--- a/pkg/kube/object.go
+++ b/pkg/kube/object.go
@@ -120,80 +120,3 @@ func KindForObject(object metav1.Object, scheme *runtime.Scheme) (string, error)
 	}
 	return gvk.Kind, nil
 }
-
-func SetOwnerReference(owner, object metav1.Object, scheme *runtime.Scheme) error {
-	// Validate the owner.
-	ro, ok := owner.(runtime.Object)
-	if !ok {
-		return fmt.Errorf("%T is not a runtime.Object, cannot call SetOwnerReference", owner)
-	}
-	if err := validateOwner(owner, object); err != nil {
-		return err
-	}
-
-	// Create a new owner ref.
-	gvk, err := GVKForObject(ro, scheme)
-	if err != nil {
-		return err
-	}
-	ref := metav1.OwnerReference{
-		APIVersion: gvk.GroupVersion().String(),
-		Kind:       gvk.Kind,
-		UID:        owner.GetUID(),
-		Name:       owner.GetName(),
-	}
-
-	// Update owner references and return.
-	upsertOwnerRef(ref, object)
-	return nil
-}
-
-func validateOwner(owner, object metav1.Object) error {
-	ownerNs := owner.GetNamespace()
-	if ownerNs != "" {
-		objNs := object.GetNamespace()
-		if objNs == "" {
-			return fmt.Errorf("cluster-scoped resource must not have a namespace-scoped owner, owner's namespace %s", ownerNs)
-		}
-		if ownerNs != objNs {
-			return fmt.Errorf("cross-namespace owner references are disallowed, owner's namespace %s, obj's namespace %s", owner.GetNamespace(), object.GetNamespace())
-		}
-	}
-	return nil
-}
-
-func upsertOwnerRef(ref metav1.OwnerReference, object metav1.Object) {
-	owners := object.GetOwnerReferences()
-	idx := indexOwnerRef(owners, ref)
-	if idx == -1 {
-		owners = append(owners, ref)
-	} else {
-		owners[idx] = ref
-	}
-	object.SetOwnerReferences(owners)
-}
-
-// indexOwnerRef returns the index of the owner reference in the slice if found, or -1.
-func indexOwnerRef(ownerReferences []metav1.OwnerReference, ref metav1.OwnerReference) int {
-	for index, r := range ownerReferences {
-		if referSameObject(r, ref) {
-			return index
-		}
-	}
-	return -1
-}
-
-// Returns true if a and b point to the same object
-func referSameObject(a, b metav1.OwnerReference) bool {
-	aGV, err := schema.ParseGroupVersion(a.APIVersion)
-	if err != nil {
-		return false
-	}
-
-	bGV, err := schema.ParseGroupVersion(b.APIVersion)
-	if err != nil {
-		return false
-	}
-
-	return aGV.Group == bGV.Group && a.Kind == b.Kind && a.Name == b.Name
-}
diff --git a/pkg/kube/starboard.go b/pkg/kube/starboard.go
index 8cb4e10..02ee7de 100644
--- a/pkg/kube/starboard.go
+++ b/pkg/kube/starboard.go
@@ -13,6 +13,7 @@ const (
 	LabelResourceNamespace = "starboard.resource.namespace"
 
 	LabelContainerName = "starboard.container.name"
+	LabelPodSpecHash   = "pod-spec-hash"
 
 	LabelScannerName   = "starboard.scanner.name"
 	LabelScannerVendor = "starboard.scanner.vendor"
diff --git a/pkg/kubebench/crd/writer.go b/pkg/kubebench/crd/writer.go
index 4575ae1..7c30742 100644
--- a/pkg/kubebench/crd/writer.go
+++ b/pkg/kubebench/crd/writer.go
@@ -3,6 +3,8 @@ package crd
 import (
 	"context"
 
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"github.com/aquasecurity/starboard/pkg/generated/clientset/versioned/typed/aquasecurity/v1alpha1"
@@ -44,7 +46,7 @@ func (w *ReadWriter) Write(ctx context.Context, report starboard.CISKubeBenchOut
 			},
 			Report: report,
 		}
-		err = kube.SetOwnerReference(node, report, w.scheme)
+		err = controllerutil.SetOwnerReference(node, report, w.scheme)
 		if err != nil {
 			return err
 		}
diff --git a/pkg/operator/controller/job/job_controller.go b/pkg/operator/controller/job/job_controller.go
index 913a16c..71a03d3 100644
--- a/pkg/operator/controller/job/job_controller.go
+++ b/pkg/operator/controller/job/job_controller.go
@@ -86,9 +86,9 @@ func (r *JobController) processCompleteScanJob(ctx context.Context, scanJob *bat
 		return fmt.Errorf("getting container images: %w", err)
 	}
 
-	hash, ok := scanJob.Labels[etc.LabelPodSpecHash]
+	hash, ok := scanJob.Labels[kube.LabelPodSpecHash]
 	if !ok {
-		return fmt.Errorf("expected label %s not set", etc.LabelPodSpecHash)
+		return fmt.Errorf("expected label %s not set", kube.LabelPodSpecHash)
 	}
 
 	hasVulnerabilityReports, err := r.Store.HasVulnerabilityReports(ctx, workload, hash, containerImages)
@@ -124,7 +124,7 @@ func (r *JobController) processCompleteScanJob(ctx context.Context, scanJob *bat
 	}
 
 	log.Info("Writing VulnerabilityReports", "owner", workload)
-	err = r.Store.SaveVulnerabilityReports(ctx, workload, hash, vulnerabilityReports)
+	err = r.Store.SaveVulnerabilityReports(ctx, vulnerabilityReports, workload, hash)
 	if err != nil {
 		return fmt.Errorf("writing vulnerability reports: %w", err)
 	}
diff --git a/pkg/operator/controller/pod/pod_controller.go b/pkg/operator/controller/pod/pod_controller.go
index 59adf81..42b7224 100644
--- a/pkg/operator/controller/pod/pod_controller.go
+++ b/pkg/operator/controller/pod/pod_controller.go
@@ -132,7 +132,7 @@ func (r *PodController) ensureScanJob(ctx context.Context, owner kube.Object, ha
 		kube.LabelResourceNamespace: pod.Namespace,
 		kube.LabelResourceKind:      string(owner.Kind),
 		kube.LabelResourceName:      owner.Name,
-		etc.LabelPodSpecHash:        hash,
+		kube.LabelPodSpecHash:       hash,
 	}, client.InNamespace(r.Config.Namespace))
 	if err != nil {
 		return fmt.Errorf("listing jos: %w", err)
@@ -198,7 +198,7 @@ func (r *PodController) GetJobMetaFrom(owner kube.Object, hash string, spec core
 			kube.LabelResourceName:         owner.Name,
 			kube.LabelResourceNamespace:    owner.Namespace,
 			"app.kubernetes.io/managed-by": "starboard-operator",
-			etc.LabelPodSpecHash:           hash,
+			kube.LabelPodSpecHash:          hash,
 		},
 		Annotations: map[string]string{
 			kube.AnnotationContainerImages: containerImagesAsJSON,
diff --git a/pkg/operator/etc/config.go b/pkg/operator/etc/config.go
index 2dc438a..f94bb58 100644
--- a/pkg/operator/etc/config.go
+++ b/pkg/operator/etc/config.go
@@ -8,10 +8,6 @@ import (
 	"github.com/caarlos0/env/v6"
 )
 
-const (
-	LabelPodSpecHash = "pod-spec-hash"
-)
-
 type Config struct {
 	Operator       Operator
 	ScannerAquaCSP ScannerAquaCSP
diff --git a/pkg/operator/reports/store.go b/pkg/operator/reports/store.go
index 2587259..ab0b6b0 100644
--- a/pkg/operator/reports/store.go
+++ b/pkg/operator/reports/store.go
@@ -4,9 +4,8 @@ import (
 	"context"
 	"fmt"
 	"reflect"
-	"strings"
 
-	"github.com/aquasecurity/starboard/pkg/operator/etc"
+	"github.com/aquasecurity/starboard/pkg/vulnerabilityreport"
 	"k8s.io/apimachinery/pkg/api/errors"
 	ctrl "sigs.k8s.io/controller-runtime"
 
@@ -20,11 +19,8 @@ import (
 
 	starboardv1alpha1 "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
 	"github.com/aquasecurity/starboard/pkg/find/vulnerabilities"
-	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
-
 	"github.com/aquasecurity/starboard/pkg/kube"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/apimachinery/pkg/labels"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
@@ -33,85 +29,92 @@ var (
 )
 
 type StoreInterface interface {
-	SaveVulnerabilityReports(ctx context.Context, owner kube.Object, hash string, reports vulnerabilities.WorkloadVulnerabilities) error
+	SaveVulnerabilityReports(ctx context.Context, reports vulnerabilities.WorkloadVulnerabilities, owner kube.Object, hash string) error
 	GetVulnerabilityReportsByOwnerAndHash(ctx context.Context, owner kube.Object, hash string) (vulnerabilities.WorkloadVulnerabilities, error)
 	HasVulnerabilityReports(ctx context.Context, owner kube.Object, hash string, containerImages kube.ContainerImages) (bool, error)
 }
 
-type Store struct {
+type store struct {
 	client client.Client
 	scheme *runtime.Scheme
 }
 
-func NewStore(client client.Client, scheme *runtime.Scheme) *Store {
-	return &Store{
+func NewStore(client client.Client, scheme *runtime.Scheme) StoreInterface {
+	return &store{
 		client: client,
 		scheme: scheme,
 	}
 }
 
-func (s *Store) SaveVulnerabilityReports(ctx context.Context, workload kube.Object, hash string, reports vulnerabilities.WorkloadVulnerabilities) error {
-	owner, err := s.getRuntimeObjectFor(ctx, workload)
+func (s *store) SaveVulnerabilityReports(ctx context.Context, reports vulnerabilities.WorkloadVulnerabilities, owner kube.Object, hash string) error {
+	ownerObject, err := s.getRuntimeObjectFor(ctx, owner)
 	if err != nil {
 		return err
 	}
 
-	for containerName, report := range reports {
-		reportName := fmt.Sprintf("%s-%s-%s", strings.ToLower(string(workload.Kind)),
-			workload.Name, containerName)
-
-		vulnerabilityReport := &starboardv1alpha1.VulnerabilityReport{}
-
-		err := s.client.Get(ctx, types.NamespacedName{Name: reportName, Namespace: workload.Namespace}, vulnerabilityReport)
-		if errors.IsNotFound(err) {
-			vulnerabilityReport = &starboardv1alpha1.VulnerabilityReport{
-				ObjectMeta: metav1.ObjectMeta{
-					Name:      reportName,
-					Namespace: workload.Namespace,
-					Labels: labels.Set{
-						kube.LabelResourceKind:      string(workload.Kind),
-						kube.LabelResourceName:      workload.Name,
-						kube.LabelResourceNamespace: workload.Namespace,
-						kube.LabelContainerName:     containerName,
-						etc.LabelPodSpecHash:        hash,
-					},
-				},
-				Report: report,
-			}
-			err = controllerutil.SetOwnerReference(owner, vulnerabilityReport, s.scheme)
-			if err != nil {
-				return err
-			}
-			log.Info("Creating VulnerabilityReport",
-				"report", fmt.Sprintf("%s/%s", workload.Namespace, reportName),
-				"hash", hash)
-			err := s.client.Create(ctx, vulnerabilityReport)
-			if err != nil {
-				return err
-			}
-			return nil
+	for container, report := range reports {
+		err := s.createOrUpdate(ctx, container, report, ownerObject, hash)
+		if err != nil {
+			return err
 		}
+	}
+	return nil
+}
+
+func (s *store) createOrUpdate(ctx context.Context, containerName string, report starboardv1alpha1.VulnerabilityScanResult, owner metav1.Object, hash string) error {
+	namespace := owner.GetNamespace()
+
+	reportName, err := vulnerabilityreport.NewNameBuilder(s.scheme).
+		Owner(owner).
+		Container(containerName).Get()
+
+	if err != nil {
+		return err
+	}
+
+	vulnerabilityReport := starboardv1alpha1.VulnerabilityReport{}
+
+	err = s.client.Get(ctx, types.NamespacedName{Name: reportName, Namespace: namespace}, &vulnerabilityReport)
+
+	if err == nil {
+		log.Info("Updating VulnerabilityReport",
+			"report", fmt.Sprintf("%s/%s", namespace, reportName),
+			"hash", hash)
 
 		// Do not modify the object that might be cached.
 		cloned := vulnerabilityReport.DeepCopy()
-		cloned.Labels[etc.LabelPodSpecHash] = hash
+		cloned.Labels[kube.LabelPodSpecHash] = hash
 		cloned.Report = report
-		log.Info("Updating VulnerabilityReport",
-			"report", fmt.Sprintf("%s/%s", workload.Namespace, reportName),
-			"hash", hash)
+
 		return s.client.Update(ctx, cloned)
 	}
-	return nil
+
+	if errors.IsNotFound(err) {
+		log.Info("Creating VulnerabilityReport",
+			"report", fmt.Sprintf("%s/%s", namespace, reportName),
+			"hash", hash)
+
+		vulnerabilityReport, err = vulnerabilityreport.NewBuilder(s.scheme).
+			Owner(owner).
+			Container(containerName).
+			ScanResult(report).
+			ReportName(reportName).
+			PodSpecHash(hash).Get()
+
+		return s.client.Create(ctx, &vulnerabilityReport)
+	}
+
+	return err
 }
 
-func (s *Store) GetVulnerabilityReportsByOwnerAndHash(ctx context.Context, workload kube.Object, hash string) (vulnerabilities.WorkloadVulnerabilities, error) {
+func (s *store) GetVulnerabilityReportsByOwnerAndHash(ctx context.Context, workload kube.Object, hash string) (vulnerabilities.WorkloadVulnerabilities, error) {
 	vulnerabilityList := &starboardv1alpha1.VulnerabilityReportList{}
 
 	err := s.client.List(ctx, vulnerabilityList, client.MatchingLabels{
 		kube.LabelResourceKind:      string(workload.Kind),
 		kube.LabelResourceNamespace: workload.Namespace,
 		kube.LabelResourceName:      workload.Name,
-		etc.LabelPodSpecHash:        hash,
+		kube.LabelPodSpecHash:       hash,
 	}, client.InNamespace(workload.Namespace))
 	if err != nil {
 		return nil, err
@@ -126,7 +129,7 @@ func (s *Store) GetVulnerabilityReportsByOwnerAndHash(ctx context.Context, workl
 	return reports, nil
 }
 
-func (s *Store) getRuntimeObjectFor(ctx context.Context, workload kube.Object) (metav1.Object, error) {
+func (s *store) getRuntimeObjectFor(ctx context.Context, workload kube.Object) (metav1.Object, error) {
 	var obj runtime.Object
 	switch workload.Kind {
 	case kube.KindPod:
@@ -155,7 +158,7 @@ func (s *Store) getRuntimeObjectFor(ctx context.Context, workload kube.Object) (
 	return obj.(metav1.Object), nil
 }
 
-func (s *Store) HasVulnerabilityReports(ctx context.Context, owner kube.Object, hash string, containerImages kube.ContainerImages) (bool, error) {
+func (s *store) HasVulnerabilityReports(ctx context.Context, owner kube.Object, hash string, containerImages kube.ContainerImages) (bool, error) {
 	vulnerabilityReports, err := s.GetVulnerabilityReportsByOwnerAndHash(ctx, owner, hash)
 	if err != nil {
 		return false, err
diff --git a/pkg/polaris/crd/writer.go b/pkg/polaris/crd/writer.go
index b59609c..0614d3c 100644
--- a/pkg/polaris/crd/writer.go
+++ b/pkg/polaris/crd/writer.go
@@ -5,6 +5,8 @@ import (
 	"fmt"
 	"strings"
 
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+
 	"k8s.io/apimachinery/pkg/runtime"
 
 	"k8s.io/apimachinery/pkg/labels"
@@ -61,7 +63,7 @@ func (w *ReadWriter) Write(ctx context.Context, report starboard.ConfigAudit, ow
 			},
 			Report: report,
 		}
-		err = kube.SetOwnerReference(owner, report, w.scheme)
+		err = controllerutil.SetOwnerReference(owner, report, w.scheme)
 		if err != nil {
 			return
 		}
diff --git a/pkg/vulnerabilityreport/builder.go b/pkg/vulnerabilityreport/builder.go
new file mode 100644
index 0000000..9e18fe6
--- /dev/null
+++ b/pkg/vulnerabilityreport/builder.go
@@ -0,0 +1,130 @@
+package vulnerabilityreport
+
+import (
+	"fmt"
+	"strings"
+
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/kube"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"sigs.k8s.io/controller-runtime/pkg/controller/controllerutil"
+)
+
+type NameBuilder interface {
+	Owner(owner metav1.Object) NameBuilder
+	Container(name string) NameBuilder
+	Get() (string, error)
+}
+
+type nameBuilder struct {
+	scheme    *runtime.Scheme
+	owner     metav1.Object
+	container string
+}
+
+func NewNameBuilder(scheme *runtime.Scheme) NameBuilder {
+	return &nameBuilder{
+		scheme: scheme,
+	}
+}
+
+func (b *nameBuilder) Owner(owner metav1.Object) NameBuilder {
+	b.owner = owner
+	return b
+}
+
+func (b *nameBuilder) Container(name string) NameBuilder {
+	b.container = name
+	return b
+}
+
+func (b *nameBuilder) Get() (string, error) {
+	kind, err := kube.KindForObject(b.owner, b.scheme)
+	if err != nil {
+		return "", err
+	}
+	return fmt.Sprintf("%s-%s-%s", strings.ToLower(kind),
+		b.owner.GetName(), b.container), nil
+}
+
+type Builder interface {
+	Owner(owner metav1.Object) Builder
+	Container(name string) Builder
+	PodSpecHash(hash string) Builder
+	ScanResult(result v1alpha1.VulnerabilityScanResult) Builder
+	ReportName(name string) Builder
+	Get() (v1alpha1.VulnerabilityReport, error)
+}
+
+func NewBuilder(scheme *runtime.Scheme) Builder {
+	return &builder{
+		scheme: scheme,
+	}
+}
+
+type builder struct {
+	scheme     *runtime.Scheme
+	owner      metav1.Object
+	container  string
+	hash       string
+	result     v1alpha1.VulnerabilityScanResult
+	reportName string
+}
+
+func (b *builder) Owner(owner metav1.Object) Builder {
+	b.owner = owner
+	return b
+}
+
+func (b *builder) Container(name string) Builder {
+	b.container = name
+	return b
+}
+
+func (b *builder) PodSpecHash(hash string) Builder {
+	b.hash = hash
+	return b
+}
+
+func (b *builder) ScanResult(result v1alpha1.VulnerabilityScanResult) Builder {
+	b.result = result
+	return b
+}
+
+func (b *builder) ReportName(name string) Builder {
+	b.reportName = name
+	return b
+}
+
+func (b *builder) Get() (v1alpha1.VulnerabilityReport, error) {
+	kind, err := kube.KindForObject(b.owner, b.scheme)
+	if err != nil {
+		return v1alpha1.VulnerabilityReport{}, err
+	}
+
+	labels := map[string]string{
+		kube.LabelResourceKind:      kind,
+		kube.LabelResourceName:      b.owner.GetName(),
+		kube.LabelResourceNamespace: b.owner.GetNamespace(),
+		kube.LabelContainerName:     b.container,
+	}
+
+	if b.hash != "" {
+		labels[kube.LabelPodSpecHash] = b.hash
+	}
+
+	report := v1alpha1.VulnerabilityReport{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      b.reportName,
+			Namespace: b.owner.GetNamespace(),
+			Labels:    labels,
+		},
+		Report: b.result,
+	}
+	err = controllerutil.SetOwnerReference(b.owner, &report, b.scheme)
+	if err != nil {
+		return v1alpha1.VulnerabilityReport{}, err
+	}
+	return report, nil
+}


Test patch:
diff --git a/pkg/kube/object_test.go b/pkg/kube/object_test.go
index ab45657..a0fcef0 100644
--- a/pkg/kube/object_test.go
+++ b/pkg/kube/object_test.go
@@ -5,18 +5,11 @@ import (
 	"testing"
 
 	"github.com/aquasecurity/starboard/pkg/kube"
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-	appsv1 "k8s.io/api/apps/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/client-go/kubernetes/scheme"
-
 	"github.com/stretchr/testify/assert"
 	"github.com/stretchr/testify/require"
 	"k8s.io/apimachinery/pkg/labels"
 )
 
-// TODO Migrate to Ginkgo
 func TestObjectFromLabelsSet(t *testing.T) {
 	testCases := []struct {
 		name           string
@@ -80,7 +73,6 @@ func TestObjectFromLabelsSet(t *testing.T) {
 	}
 }
 
-// TODO Migrate to Ginkgo
 func TestContainerImages_AsJSON_And_FromJSON(t *testing.T) {
 	containerImages := kube.ContainerImages{
 		"nginx": "nginx:1.16",
@@ -94,82 +86,3 @@ func TestContainerImages_AsJSON_And_FromJSON(t *testing.T) {
 	require.NoError(t, err)
 	assert.Equal(t, containerImages, newContainerImages)
 }
-
-func TestKubePackage(t *testing.T) {
-	RegisterFailHandler(Fail)
-	RunSpecs(t, "Kube Package Suite")
-}
-
-var _ = Describe("Kube package", func() {
-
-	Describe("SetOwnerReference", func() {
-
-		It("should set ownerRef on an empty list", func() {
-			rs := &appsv1.ReplicaSet{}
-			dep := &appsv1.Deployment{
-				ObjectMeta: metav1.ObjectMeta{Name: "foo", UID: "foo-uid"},
-			}
-			Expect(kube.SetOwnerReference(dep, rs, scheme.Scheme)).ToNot(HaveOccurred())
-			Expect(rs.OwnerReferences).To(ConsistOf(metav1.OwnerReference{
-				Name:       "foo",
-				Kind:       "Deployment",
-				APIVersion: "apps/v1",
-				UID:        "foo-uid",
-			}))
-		})
-
-		It("should not duplicate owner references", func() {
-			rs := &appsv1.ReplicaSet{
-				ObjectMeta: metav1.ObjectMeta{
-					OwnerReferences: []metav1.OwnerReference{
-						{
-							Name:       "foo",
-							Kind:       "Deployment",
-							APIVersion: "apps/v1",
-							UID:        "foo-uid",
-						},
-					},
-				},
-			}
-			dep := &appsv1.Deployment{
-				ObjectMeta: metav1.ObjectMeta{Name: "foo", UID: "foo-uid"},
-			}
-
-			Expect(kube.SetOwnerReference(dep, rs, scheme.Scheme)).ToNot(HaveOccurred())
-			Expect(rs.OwnerReferences).To(ConsistOf(metav1.OwnerReference{
-				Name:       "foo",
-				Kind:       "Deployment",
-				APIVersion: "apps/v1",
-				UID:        "foo-uid",
-			}))
-		})
-
-		It("should update the reference", func() {
-			rs := &appsv1.ReplicaSet{
-				ObjectMeta: metav1.ObjectMeta{
-					OwnerReferences: []metav1.OwnerReference{
-						{
-							Name:       "foo",
-							Kind:       "Deployment",
-							APIVersion: "apps/v1",
-							UID:        "foo-uid-1",
-						},
-					},
-				},
-			}
-			dep := &appsv1.Deployment{
-				ObjectMeta: metav1.ObjectMeta{Name: "foo", UID: "foo-uid-2"},
-			}
-
-			Expect(kube.SetOwnerReference(dep, rs, scheme.Scheme)).ToNot(HaveOccurred())
-			Expect(rs.OwnerReferences).To(ConsistOf(metav1.OwnerReference{
-				Name:       "foo",
-				Kind:       "Deployment",
-				APIVersion: "apps/v1",
-				UID:        "foo-uid-2",
-			}))
-
-		})
-	})
-
-})
diff --git a/pkg/vulnerabilityreport/builder_test.go b/pkg/vulnerabilityreport/builder_test.go
new file mode 100644
index 0000000..97e7e32
--- /dev/null
+++ b/pkg/vulnerabilityreport/builder_test.go
@@ -0,0 +1,63 @@
+package vulnerabilityreport_test
+
+import (
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/vulnerabilityreport"
+	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	appsv1 "k8s.io/api/apps/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/client-go/kubernetes/scheme"
+
+	"testing"
+)
+
+func TestNameBuilder(t *testing.T) {
+	name, err := vulnerabilityreport.NewNameBuilder(scheme.Scheme).
+		Owner(&appsv1.ReplicaSet{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "some-owner",
+				Namespace: "qa",
+			},
+		}).
+		Container("my-container").Get()
+	require.NoError(t, err)
+	assert.Equal(t, "replicaset-some-owner-my-container", name)
+}
+
+func TestBuilder(t *testing.T) {
+	report, err := vulnerabilityreport.NewBuilder(scheme.Scheme).
+		Owner(&appsv1.ReplicaSet{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "some-owner",
+				Namespace: "qa",
+			},
+		}).
+		Container("my-container").
+		ReportName("report-name").
+		PodSpecHash("xyz").
+		ScanResult(v1alpha1.VulnerabilityScanResult{}).Get()
+
+	require.NoError(t, err)
+	assert.Equal(t, v1alpha1.VulnerabilityReport{
+		ObjectMeta: metav1.ObjectMeta{
+			Name:      "report-name",
+			Namespace: "qa",
+			OwnerReferences: []metav1.OwnerReference{
+				{
+					APIVersion: "apps/v1",
+					Kind:       "ReplicaSet",
+					Name:       "some-owner",
+				},
+			},
+			Labels: map[string]string{
+				"starboard.resource.kind":      "ReplicaSet",
+				"starboard.resource.name":      "some-owner",
+				"starboard.resource.namespace": "qa",
+				"starboard.container.name":     "my-container",
+				"pod-spec-hash":                "xyz",
+			},
+		},
+		Report: v1alpha1.VulnerabilityScanResult{},
+	}, report)
+}