Repository: aquasecurity/starboard
Pull request #129 (2020-08-24 14:10:09)

feat: Set ownerReference for vulnerabilities resource
Resolves: #26

Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

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

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

```diff
@@            Coverage Diff             @@
##           master     #129      +/-   ##
==========================================
+ Coverage   42.42%   42.55%   +0.13%     
==========================================
  Files          30       30              
  Lines        1445     1525      +80     
==========================================
+ Hits          613      649      +36     
- Misses        774      805      +31     
- Partials       58       71      +13     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/129?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/cmd/get\_report.go](https://codecov.io/gh/aquasecurity/starboard/pull/129/diff?src=pr&el=tree#diff-cGtnL2NtZC9nZXRfcmVwb3J0Lmdv) | `37.93% <0.00%> (ø)` | |
| [pkg/find/vulnerabilities/trivy/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/129/diff?src=pr&el=tree#diff-cGtnL2ZpbmQvdnVsbmVyYWJpbGl0aWVzL3RyaXZ5L3NjYW5uZXIuZ28=) | `0.00% <0.00%> (ø)` | |
| [pkg/find/vulnerabilities/crd/writer.go](https://codecov.io/gh/aquasecurity/starboard/pull/129/diff?src=pr&el=tree#diff-cGtnL2ZpbmQvdnVsbmVyYWJpbGl0aWVzL2NyZC93cml0ZXIuZ28=) | `25.75% <9.09%> (-2.32%)` | :arrow_down: |
| [pkg/kube/object.go](https://codecov.io/gh/aquasecurity/starboard/pull/129/diff?src=pr&el=tree#diff-cGtnL2t1YmUvb2JqZWN0Lmdv) | `51.96% <45.45%> (-11.93%)` | :arrow_down: |
| [pkg/cmd/find\_vulnerabilities.go](https://codecov.io/gh/aquasecurity/starboard/pull/129/diff?src=pr&el=tree#diff-cGtnL2NtZC9maW5kX3Z1bG5lcmFiaWxpdGllcy5nbw==) | `76.27% <100.00%> (+2.19%)` | :arrow_up: |

------

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


Patch:
diff --git a/pkg/cmd/commands.go b/pkg/cmd/commands.go
index e098cd5..6a94025 100644
--- a/pkg/cmd/commands.go
+++ b/pkg/cmd/commands.go
@@ -5,6 +5,10 @@ import (
 	"strings"
 	"time"
 
+	"k8s.io/client-go/kubernetes/scheme"
+
+	"k8s.io/apimachinery/pkg/runtime"
+
 	"github.com/aquasecurity/starboard/pkg/kube"
 	"github.com/spf13/cobra"
 	"k8s.io/cli-runtime/pkg/genericclioptions"
@@ -48,6 +52,10 @@ func WorkloadFromArgs(namespace string, args []string) (workload kube.Object, er
 	return
 }
 
+func GetScheme() *runtime.Scheme {
+	return scheme.Scheme
+}
+
 const (
 	scanJobTimeoutFlagName = "scan-job-timeout"
 	deleteScanJobFlagName  = "delete-scan-job"
diff --git a/pkg/cmd/find.go b/pkg/cmd/find.go
index 5ac949c..9e59895 100644
--- a/pkg/cmd/find.go
+++ b/pkg/cmd/find.go
@@ -10,7 +10,7 @@ func NewFindCmd(executable string, cf *genericclioptions.ConfigFlags) *cobra.Com
 		Use:   "find",
 		Short: "Manage security scanners",
 	}
-	findCmd.AddCommand(GetVulnerabilitiesCmd(executable, cf))
+	findCmd.AddCommand(NewFindVulnerabilitiesCmd(executable, cf))
 
 	return findCmd
 }
diff --git a/pkg/cmd/find_vulnerabilities.go b/pkg/cmd/find_vulnerabilities.go
index 0066c4d..c5360cb 100644
--- a/pkg/cmd/find_vulnerabilities.go
+++ b/pkg/cmd/find_vulnerabilities.go
@@ -12,7 +12,7 @@ import (
 	"k8s.io/client-go/kubernetes"
 )
 
-func GetVulnerabilitiesCmd(executable string, cf *genericclioptions.ConfigFlags) *cobra.Command {
+func NewFindVulnerabilitiesCmd(executable string, cf *genericclioptions.ConfigFlags) *cobra.Command {
 	cmd := &cobra.Command{
 		Aliases: []string{"vulns", "vuln"},
 		Use:     "vulnerabilities (NAME | TYPE/NAME)",
@@ -70,7 +70,7 @@ NAME is the name of a particular Kubernetes workload.
 			if err != nil {
 				return
 			}
-			reports, err := trivy.NewScanner(opts, kubernetesClientset).Scan(ctx, workload)
+			reports, owner, err := trivy.NewScanner(opts, kubernetesClientset).Scan(ctx, workload)
 			if err != nil {
 				return
 			}
@@ -78,7 +78,7 @@ NAME is the name of a particular Kubernetes workload.
 			if err != nil {
 				return
 			}
-			err = crd.NewReadWriter(starboardClientset).Write(ctx, workload, reports)
+			err = crd.NewReadWriter(GetScheme(), starboardClientset).Write(ctx, reports, owner)
 			return
 		},
 	}
diff --git a/pkg/cmd/get_report.go b/pkg/cmd/get_report.go
index f9fde76..e3b707e 100644
--- a/pkg/cmd/get_report.go
+++ b/pkg/cmd/get_report.go
@@ -42,7 +42,7 @@ NAME is the name of a particular Kubernetes workload.
 			}
 
 			caReader := configAuditCrd.NewReadWriter(starboardClientset)
-			vulnsReader := vulnsCrd.NewReadWriter(starboardClientset)
+			vulnsReader := vulnsCrd.NewReadWriter(GetScheme(), starboardClientset)
 
 			reporter := report.NewHTMLReporter(caReader, vulnsReader, workload)
 			err = reporter.GenerateReport(os.Stdout)
diff --git a/pkg/cmd/kube_bench.go b/pkg/cmd/kube_bench.go
index 5462934..559c924 100644
--- a/pkg/cmd/kube_bench.go
+++ b/pkg/cmd/kube_bench.go
@@ -45,7 +45,7 @@ func NewKubeBenchCmd(cf *genericclioptions.ConfigFlags) *cobra.Command {
 				return
 			}
 			scanner := kubebench.NewScanner(opts, kubernetesClientset)
-			writer := crd.NewReadWriter(starboardClientset)
+			writer := crd.NewReadWriter(GetScheme(), starboardClientset)
 
 			var wg sync.WaitGroup
 
diff --git a/pkg/find/vulnerabilities/crd/writer.go b/pkg/find/vulnerabilities/crd/writer.go
index 03593ab..6d2a80c 100644
--- a/pkg/find/vulnerabilities/crd/writer.go
+++ b/pkg/find/vulnerabilities/crd/writer.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 
 	"k8s.io/apimachinery/pkg/labels"
+	"k8s.io/apimachinery/pkg/runtime"
 	"k8s.io/klog"
 
 	"github.com/aquasecurity/starboard/pkg/kube"
@@ -13,22 +14,24 @@ import (
 	"github.com/aquasecurity/starboard/pkg/find/vulnerabilities"
 	clientset "github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
 	"github.com/google/uuid"
-	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 type ReadWriter struct {
+	scheme *runtime.Scheme
 	client clientset.Interface
 }
 
-func NewReadWriter(client clientset.Interface) *ReadWriter {
+func NewReadWriter(scheme *runtime.Scheme, client clientset.Interface) *ReadWriter {
 	return &ReadWriter{
+		scheme: scheme,
 		client: client,
 	}
 }
 
-func (s *ReadWriter) Write(ctx context.Context, workload kube.Object, reports vulnerabilities.WorkloadVulnerabilities) (err error) {
+func (s *ReadWriter) Write(ctx context.Context, reports vulnerabilities.WorkloadVulnerabilities, owner metav1.Object) (err error) {
 	for container, report := range reports {
-		err = s.createVulnerability(ctx, workload, container, report)
+		err = s.createVulnerability(ctx, container, report, owner)
 		if err != nil {
 			return
 		}
@@ -36,14 +39,19 @@ func (s *ReadWriter) Write(ctx context.Context, workload kube.Object, reports vu
 	return
 }
 
-func (s *ReadWriter) createVulnerability(ctx context.Context, workload kube.Object, container string, report starboard.VulnerabilityReport) (err error) {
-	namespace := workload.Namespace
+func (s *ReadWriter) createVulnerability(ctx context.Context, container string, report starboard.VulnerabilityReport, owner metav1.Object) (err error) {
+	namespace := owner.GetNamespace()
+	name := owner.GetName()
+	kind, err := kube.KindForObject(owner, s.scheme)
+	if err != nil {
+		return err
+	}
 
 	// Trying to find previously generated vulnerability report for this specific container
-	vulnsSearch, err := s.client.AquasecurityV1alpha1().Vulnerabilities(namespace).List(ctx, meta.ListOptions{
+	vulnsSearch, err := s.client.AquasecurityV1alpha1().Vulnerabilities(namespace).List(ctx, metav1.ListOptions{
 		LabelSelector: labels.Set{
-			kube.LabelResourceKind:  string(workload.Kind),
-			kube.LabelResourceName:  workload.Name,
+			kube.LabelResourceKind:  kind,
+			kube.LabelResourceName:  name,
 			kube.LabelContainerName: container,
 		}.String(),
 	})
@@ -53,32 +61,38 @@ func (s *ReadWriter) createVulnerability(ctx context.Context, workload kube.Obje
 
 	if len(vulnsSearch.Items) > 0 {
 		existingCR := vulnsSearch.Items[0]
-		klog.V(3).Infof("Updating vulnerability report: %s/%s", namespace, workload.Name)
+		klog.V(3).Infof("Updating vulnerability report: %s/%s", namespace, name)
 		deepCopy := existingCR.DeepCopy()
 		deepCopy.Report = report
-		_, err = s.client.AquasecurityV1alpha1().Vulnerabilities(namespace).Update(ctx, deepCopy, meta.UpdateOptions{})
+		_, err = s.client.AquasecurityV1alpha1().Vulnerabilities(namespace).Update(ctx, deepCopy, metav1.UpdateOptions{})
 	} else {
-		klog.V(3).Infof("Creating vulnerability report: %s/%s", namespace, workload.Name)
-		_, err = s.client.AquasecurityV1alpha1().Vulnerabilities(namespace).
-			Create(ctx, &starboard.Vulnerability{
-				ObjectMeta: meta.ObjectMeta{
-					Name: fmt.Sprintf(uuid.New().String()),
-					Labels: map[string]string{
-						kube.LabelResourceKind:      string(workload.Kind),
-						kube.LabelResourceName:      workload.Name,
-						kube.LabelResourceNamespace: workload.Namespace,
-						kube.LabelContainerName:     container,
-					},
+		klog.V(3).Infof("Creating vulnerability report: %s/%s", namespace, name)
+		report := &starboard.Vulnerability{
+			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,
-			}, meta.CreateOptions{})
+			},
+			Report: report,
+		}
+		err = kube.SetOwnerReference(owner, report, s.scheme)
+		if err != nil {
+			return err
+		}
+		_, err = s.client.AquasecurityV1alpha1().Vulnerabilities(namespace).
+			Create(ctx, report, metav1.CreateOptions{})
 	}
 
 	return err
 }
 
 func (s *ReadWriter) Read(ctx context.Context, workload kube.Object) (vulnerabilities.WorkloadVulnerabilities, error) {
-	list, err := s.client.AquasecurityV1alpha1().Vulnerabilities(workload.Namespace).List(ctx, meta.ListOptions{
+	list, err := s.client.AquasecurityV1alpha1().Vulnerabilities(workload.Namespace).List(ctx, metav1.ListOptions{
 		LabelSelector: labels.Set{
 			kube.LabelResourceKind:      string(workload.Kind),
 			kube.LabelResourceName:      workload.Name,
diff --git a/pkg/find/vulnerabilities/trivy/scanner.go b/pkg/find/vulnerabilities/trivy/scanner.go
index 6da9374..6f30244 100644
--- a/pkg/find/vulnerabilities/trivy/scanner.go
+++ b/pkg/find/vulnerabilities/trivy/scanner.go
@@ -52,9 +52,9 @@ type Scanner struct {
 	scanners.Base
 }
 
-func (s *Scanner) Scan(ctx context.Context, workload kube.Object) (reports vulnerabilities.WorkloadVulnerabilities, err error) {
+func (s *Scanner) Scan(ctx context.Context, workload kube.Object) (reports vulnerabilities.WorkloadVulnerabilities, owner meta.Object, err error) {
 	klog.V(3).Infof("Getting Pod template for workload: %v", workload)
-	podSpec, err := s.pods.GetPodSpecByWorkload(ctx, workload)
+	podSpec, owner, err := s.pods.GetPodSpecByWorkload(ctx, workload)
 	if err != nil {
 		err = fmt.Errorf("getting Pod template: %w", err)
 		return
diff --git a/pkg/find/vulnerabilities/writer.go b/pkg/find/vulnerabilities/writer.go
index 9688255..2ad627b 100644
--- a/pkg/find/vulnerabilities/writer.go
+++ b/pkg/find/vulnerabilities/writer.go
@@ -3,11 +3,13 @@ package vulnerabilities
 import (
 	"context"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+
 	"github.com/aquasecurity/starboard/pkg/kube"
 )
 
 type Writer interface {
-	Write(ctx context.Context, workload kube.Object, reports WorkloadVulnerabilities) error
+	Write(ctx context.Context, reports WorkloadVulnerabilities, owner metav1.Object) error
 }
 
 type Reader interface {
diff --git a/pkg/kube/object.go b/pkg/kube/object.go
index f3af9f7..cca9f10 100644
--- a/pkg/kube/object.go
+++ b/pkg/kube/object.go
@@ -4,6 +4,10 @@ import (
 	"encoding/json"
 	"fmt"
 
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/apimachinery/pkg/runtime"
+	"k8s.io/apimachinery/pkg/runtime/schema"
+
 	"k8s.io/apimachinery/pkg/labels"
 )
 
@@ -84,3 +88,113 @@ func (ci ContainerImages) AsJSON() (string, error) {
 func (ci ContainerImages) FromJSON(value string) error {
 	return json.Unmarshal([]byte(value), &ci)
 }
+
+func GVKForObject(obj runtime.Object, scheme *runtime.Scheme) (schema.GroupVersionKind, error) {
+	gvks, isUnversioned, err := scheme.ObjectKinds(obj)
+	if err != nil {
+		return schema.GroupVersionKind{}, err
+	}
+	if isUnversioned {
+		return schema.GroupVersionKind{}, fmt.Errorf("cannot create group-version-kind for unversioned type %T", obj)
+	}
+
+	if len(gvks) < 1 {
+		return schema.GroupVersionKind{}, fmt.Errorf("no group-version-kinds associated with type %T", obj)
+	}
+	if len(gvks) > 1 {
+		// this should only trigger for things like metav1.XYZ --
+		// normal versioned types should be fine
+		return schema.GroupVersionKind{}, fmt.Errorf(
+			"multiple group-version-kinds associated with type %T, refusing to guess at one", obj)
+	}
+	return gvks[0], nil
+}
+
+func KindForObject(object metav1.Object, scheme *runtime.Scheme) (string, error) {
+	ro, ok := object.(runtime.Object)
+	if !ok {
+		return "", fmt.Errorf("%T is not a runtime.Object", object)
+	}
+	gvk, err := GVKForObject(ro, scheme)
+	if err != nil {
+		return "", err
+	}
+	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/pod/manager.go b/pkg/kube/pod/manager.go
index 27b7e5a..67674f2 100644
--- a/pkg/kube/pod/manager.go
+++ b/pkg/kube/pod/manager.go
@@ -27,7 +27,7 @@ func NewPodManager(clientset kubernetes.Interface) *Manager {
 }
 
 // GetPodSpecByWorkload returns a PodSpec of the specified Workload.
-func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Object) (spec core.PodSpec, err error) {
+func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Object) (spec core.PodSpec, object meta.Object, err error) {
 	ns := workload.Namespace
 	switch workload.Kind {
 	case kube.KindPod:
@@ -37,6 +37,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Objec
 			return
 		}
 		spec = pod.Spec
+		object = pod
 		return
 	case kube.KindReplicaSet:
 		var rs *apps.ReplicaSet
@@ -45,6 +46,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Objec
 			return
 		}
 		spec = rs.Spec.Template.Spec
+		object = rs
 		return
 	case kube.KindReplicationController:
 		var rc *core.ReplicationController
@@ -53,6 +55,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Objec
 			return
 		}
 		spec = rc.Spec.Template.Spec
+		object = rc
 		return
 	case kube.KindDeployment:
 		var deploy *apps.Deployment
@@ -61,6 +64,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Objec
 			return
 		}
 		spec = deploy.Spec.Template.Spec
+		object = deploy
 		return
 	case kube.KindStatefulSet:
 		var sts *apps.StatefulSet
@@ -69,6 +73,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Objec
 			return
 		}
 		spec = sts.Spec.Template.Spec
+		object = sts
 		return
 	case kube.KindDaemonSet:
 		var ds *apps.DaemonSet
@@ -77,6 +82,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Objec
 			return
 		}
 		spec = ds.Spec.Template.Spec
+		object = ds
 		return
 	case kube.KindCronJob:
 		var cj *batchv1beta1.CronJob
@@ -85,6 +91,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Objec
 			return
 		}
 		spec = cj.Spec.JobTemplate.Spec.Template.Spec
+		object = cj
 		return
 	case kube.KindJob:
 		var job *batch.Job
@@ -93,6 +100,7 @@ func (pw *Manager) GetPodSpecByWorkload(ctx context.Context, workload kube.Objec
 			return
 		}
 		spec = job.Spec.Template.Spec
+		object = job
 		return
 	}
 	err = fmt.Errorf("unrecognized workload: %s", workload.Kind)
diff --git a/pkg/kubebench/crd/writer.go b/pkg/kubebench/crd/writer.go
index 8a7a9b0..70bb5b6 100644
--- a/pkg/kubebench/crd/writer.go
+++ b/pkg/kubebench/crd/writer.go
@@ -3,6 +3,8 @@ package crd
 import (
 	"context"
 
+	"k8s.io/apimachinery/pkg/runtime"
+
 	"github.com/aquasecurity/starboard/pkg/generated/clientset/versioned/typed/aquasecurity/v1alpha1"
 	"k8s.io/klog"
 
@@ -10,48 +12,44 @@ import (
 
 	"github.com/aquasecurity/starboard/pkg/kube"
 
-	core "k8s.io/api/core/v1"
-	"k8s.io/utils/pointer"
-
 	starboard "github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
 	starboardapi "github.com/aquasecurity/starboard/pkg/generated/clientset/versioned"
-	meta "k8s.io/apimachinery/pkg/apis/meta/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 )
 
 type ReadWriter struct {
+	scheme  *runtime.Scheme
 	reports v1alpha1.CISKubeBenchReportInterface
 }
 
-func NewReadWriter(clientset starboardapi.Interface) *ReadWriter {
+func NewReadWriter(scheme *runtime.Scheme, clientset starboardapi.Interface) *ReadWriter {
 	return &ReadWriter{
+		scheme:  scheme,
 		reports: clientset.AquasecurityV1alpha1().CISKubeBenchReports(),
 	}
 }
 
-func (w *ReadWriter) Write(ctx context.Context, report starboard.CISKubeBenchOutput, node *core.Node) error {
-	reportExisting, err := w.reports.Get(ctx, node.Name, meta.GetOptions{})
+func (w *ReadWriter) Write(ctx context.Context, report starboard.CISKubeBenchOutput, node *corev1.Node) error {
+	reportExisting, err := w.reports.Get(ctx, node.Name, metav1.GetOptions{})
 	if err != nil && errors.IsNotFound(err) {
 		klog.V(3).Infof("Creating CISKubeBenchReport for %s node", node.Name)
-		_, err = w.reports.Create(ctx, &starboard.CISKubeBenchReport{
-			ObjectMeta: meta.ObjectMeta{
+		report := &starboard.CISKubeBenchReport{
+			ObjectMeta: metav1.ObjectMeta{
 				Name: node.Name,
 				Labels: map[string]string{
 					kube.LabelResourceKind:  string(kube.KindNode),
 					kube.LabelResourceName:  node.Name,
 					kube.LabelHistoryLatest: "true",
 				},
-				OwnerReferences: []meta.OwnerReference{
-					{
-						APIVersion: "v1",
-						Kind:       string(kube.KindNode),
-						Name:       node.Name,
-						UID:        node.UID,
-						Controller: pointer.BoolPtr(false),
-					},
-				},
 			},
 			Report: report,
-		}, meta.CreateOptions{})
+		}
+		err = kube.SetOwnerReference(node, report, w.scheme)
+		if err != nil {
+			return err
+		}
+		_, err = w.reports.Create(ctx, report, metav1.CreateOptions{})
 		return err
 	}
 	if err != nil {
@@ -60,12 +58,12 @@ func (w *ReadWriter) Write(ctx context.Context, report starboard.CISKubeBenchOut
 	klog.V(3).Infof("Updating existing CISKubeBenchReport for %s node", node.Name)
 	reportCopied := reportExisting.DeepCopy()
 	reportCopied.Report = report
-	_, err = w.reports.Update(ctx, reportCopied, meta.UpdateOptions{})
+	_, err = w.reports.Update(ctx, reportCopied, metav1.UpdateOptions{})
 	return err
 }
 
 func (w *ReadWriter) Read(ctx context.Context, node kube.Object) (starboard.CISKubeBenchOutput, error) {
-	report, err := w.reports.Get(ctx, node.Name, meta.GetOptions{})
+	report, err := w.reports.Get(ctx, node.Name, metav1.GetOptions{})
 	if err != nil {
 		return starboard.CISKubeBenchOutput{}, err
 	}


Test patch:
diff --git a/cmd/starboard/main_test.go b/cmd/starboard/main_test.go
new file mode 100644
index 0000000..0fee6f5
--- /dev/null
+++ b/cmd/starboard/main_test.go
@@ -0,0 +1,1 @@
+package main_test
diff --git a/pkg/cmd/find_test.go b/pkg/cmd/find_test.go
new file mode 100644
index 0000000..fedae39
--- /dev/null
+++ b/pkg/cmd/find_test.go
@@ -0,0 +1,1 @@
+package cmd_test
diff --git a/pkg/cmd/find_vulnerabilities_test.go b/pkg/cmd/find_vulnerabilities_test.go
new file mode 100644
index 0000000..fedae39
--- /dev/null
+++ b/pkg/cmd/find_vulnerabilities_test.go
@@ -0,0 +1,1 @@
+package cmd_test
diff --git a/pkg/find/vulnerabilities/crd/writer_test.go b/pkg/find/vulnerabilities/crd/writer_test.go
index c85ca2b..aad9d3a 100644
--- a/pkg/find/vulnerabilities/crd/writer_test.go
+++ b/pkg/find/vulnerabilities/crd/writer_test.go
@@ -1,9 +1,12 @@
-package crd
+package crd_test
 
 import (
 	"context"
+	"github.com/aquasecurity/starboard/pkg/find/vulnerabilities/crd"
 	"testing"
 
+	"github.com/aquasecurity/starboard/pkg/cmd"
+
 	"github.com/aquasecurity/starboard/pkg/find/vulnerabilities"
 
 	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
@@ -78,7 +81,7 @@ func TestReadWriter_Read(t *testing.T) {
 		Report: v1alpha1.VulnerabilityReport{},
 	})
 
-	reports, err := NewReadWriter(clientset).Read(context.TODO(), kube.Object{
+	reports, err := crd.NewReadWriter(cmd.GetScheme(), clientset).Read(context.TODO(), kube.Object{
 		Kind:      kube.KindDeployment,
 		Name:      "my-deploy",
 		Namespace: "my-namespace",
diff --git a/pkg/kube/object_test.go b/pkg/kube/object_test.go
index c31f845..ab45657 100644
--- a/pkg/kube/object_test.go
+++ b/pkg/kube/object_test.go
@@ -1,30 +1,38 @@
-package kube
+package kube_test
 
 import (
 	"errors"
 	"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
 		labelsSet      labels.Set
-		expectedObject Object
+		expectedObject kube.Object
 		expectedError  error
 	}{
 		{
 			name: "Should return object for namespaced object",
 			labelsSet: labels.Set{
-				LabelResourceKind:      "Deployment",
-				LabelResourceName:      "my-deployment",
-				LabelResourceNamespace: "my-namespace",
+				kube.LabelResourceKind:      "Deployment",
+				kube.LabelResourceName:      "my-deployment",
+				kube.LabelResourceNamespace: "my-namespace",
 			},
-			expectedObject: Object{
-				Kind:      KindDeployment,
+			expectedObject: kube.Object{
+				Kind:      kube.KindDeployment,
 				Name:      "my-deployment",
 				Namespace: "my-namespace",
 			},
@@ -32,11 +40,11 @@ func TestObjectFromLabelsSet(t *testing.T) {
 		{
 			name: "Should return object for cluster-scoped object",
 			labelsSet: labels.Set{
-				LabelResourceKind: "Node",
-				LabelResourceName: "my-node",
+				kube.LabelResourceKind: "Node",
+				kube.LabelResourceName: "my-node",
 			},
-			expectedObject: Object{
-				Kind:      KindNode,
+			expectedObject: kube.Object{
+				Kind:      kube.KindNode,
 				Name:      "my-node",
 				Namespace: "",
 			},
@@ -44,15 +52,15 @@ func TestObjectFromLabelsSet(t *testing.T) {
 		{
 			name: "Should return error when object kind is not specified as label",
 			labelsSet: labels.Set{
-				LabelResourceName:      "my-deployment",
-				LabelResourceNamespace: "my-namespace",
+				kube.LabelResourceName:      "my-deployment",
+				kube.LabelResourceNamespace: "my-namespace",
 			},
 			expectedError: errors.New("required label does not exist: starboard.resource.kind"),
 		},
 		{
 			name: "Should return error when object name is not specified as label",
 			labelsSet: labels.Set{
-				LabelResourceKind: "Deployment",
+				kube.LabelResourceKind: "Deployment",
 			},
 			expectedError: errors.New("required label does not exist: starboard.resource.name"),
 		},
@@ -60,7 +68,7 @@ func TestObjectFromLabelsSet(t *testing.T) {
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			obj, err := ObjectFromLabelsSet(tc.labelsSet)
+			obj, err := kube.ObjectFromLabelsSet(tc.labelsSet)
 			switch {
 			case tc.expectedError == nil:
 				require.NoError(t, err)
@@ -72,16 +80,96 @@ func TestObjectFromLabelsSet(t *testing.T) {
 	}
 }
 
+// TODO Migrate to Ginkgo
 func TestContainerImages_AsJSON_And_FromJSON(t *testing.T) {
-	containerImages := ContainerImages{
+	containerImages := kube.ContainerImages{
 		"nginx": "nginx:1.16",
 		"redis": "core.harbor.domain:8443/library/redis:5",
 	}
 	value, err := containerImages.AsJSON()
 	require.NoError(t, err)
 
-	newContainerImages := ContainerImages{}
+	newContainerImages := kube.ContainerImages{}
 	err = newContainerImages.FromJSON(value)
 	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/kubebench/crd/writer_test.go b/pkg/kubebench/crd/writer_test.go
index a5f50ed..c505688 100644
--- a/pkg/kubebench/crd/writer_test.go
+++ b/pkg/kubebench/crd/writer_test.go
@@ -4,6 +4,8 @@ import (
 	"context"
 	"testing"
 
+	"github.com/aquasecurity/starboard/pkg/cmd"
+
 	"k8s.io/apimachinery/pkg/labels"
 
 	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
@@ -50,7 +52,7 @@ var _ = Describe("ReadWriter", func() {
 	BeforeEach(func() {
 		objects = []runtime.Object{}
 		clientset = starboardClientsetFake.NewSimpleClientset(objects...)
-		rw = crd.NewReadWriter(clientset)
+		rw = crd.NewReadWriter(cmd.GetScheme(), clientset)
 	})
 
 	Describe("Writing report", func() {