Repository: aquasecurity/starboard
Pull request #458 (2021-03-25 09:06:13)

feat: Deleting a VulnerabilityReport should trigger rescan
Resolves: #414
Resolves: #451

Signed-off-by: Daniel Pacak <pacak.daniel@gmail.com>

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/458?src=pr&el=h1) Report
> Merging [#458](https://codecov.io/gh/aquasecurity/starboard/pull/458?src=pr&el=desc) (26ae06d) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/89e3ba8c4027fad77922c1c07f0f1faf7ea0d345?el=desc) (89e3ba8) will **decrease** coverage by `36.96%`.
> The diff coverage is `1.58%`.

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

```diff
@@             Coverage Diff             @@
##             main     #458       +/-   ##
===========================================
- Coverage   69.24%   32.27%   -36.97%     
===========================================
  Files          57       56        -1     
  Lines        3473     3386       -87     
===========================================
- Hits         2405     1093     -1312     
- Misses        788     2225     +1437     
+ Partials      280       68      -212     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/458?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/kube/resources.go](https://codecov.io/gh/aquasecurity/starboard/pull/458/diff?src=pr&el=tree#diff-cGtnL2t1YmUvcmVzb3VyY2VzLmdv) | `100.00% <ø> (ø)` | |
| [pkg/operator/controller/ciskubebenchreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/458/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY2lza3ViZWJlbmNocmVwb3J0Lmdv) | `0.00% <0.00%> (-65.77%)` | :arrow_down: |
| [pkg/operator/controller/configauditreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/458/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY29uZmlnYXVkaXRyZXBvcnQuZ28=) | `0.00% <ø> (-65.72%)` | :arrow_down: |
| [pkg/operator/controller/vulnerabilityreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/458/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvdnVsbmVyYWJpbGl0eXJlcG9ydC5nbw==) | `0.00% <0.00%> (-59.82%)` | :arrow_down: |
| [pkg/operator/predicate/predicate.go](https://codecov.io/gh/aquasecurity/starboard/pull/458/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL3ByZWRpY2F0ZS9wcmVkaWNhdGUuZ28=) | `68.00% <ø> (-24.73%)` | :arrow_down: |
| [pkg/vulnerabilityreport/builder.go](https://codecov.io/gh/aquasecurity/starboard/pull/458/diff?src=pr&el=tree#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvYnVpbGRlci5nbw==) | `79.48% <100.00%> (ø)` | |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/458/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/458/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/458/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/458/diff?src=pr&el=tree#diff-cGtnL2NtZC9zY2FuLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [38 more](https://codecov.io/gh/aquasecurity/starboard/pull/458/diff?src=pr&el=tree-more) | |

------

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


Patch:
diff --git a/pkg/kube/resources.go b/pkg/kube/resources.go
index 49194d0..b826cb0 100644
--- a/pkg/kube/resources.go
+++ b/pkg/kube/resources.go
@@ -8,7 +8,6 @@ import (
 	"github.com/davecgh/go-spew/spew"
 	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
-	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/util/rand"
 )
 
@@ -41,44 +40,6 @@ func GetContainerImagesFromJob(job *batchv1.Job) (ContainerImages, error) {
 	return containerImages, nil
 }
 
-// GetImmediateOwnerReference returns the immediate owner of the specified pod.
-// For example, for a pod controlled by a Deployment it will return the active
-// ReplicaSet, whereas for an unmanaged pod the immediate owner is the pod
-// itself.
-//
-// Note that kubelet can manage pods independently by reading pod definition
-// files from a configured host directory (typically /etc/kubernetes/manifests).
-// Such pods are called *static pods* and are owned by a cluster Node.
-// In this case we treat them as unmanaged pods. (Otherwise we'd require
-// cluster-scoped permissions to get Nodes in order to set the owner reference
-// when we create an instance of custom security report.)
-//
-// Pods created and controlled by third party frameworks, such as Argo workflow
-// engine, are considered as unmanaged. Otherwise we'd need to maintain and
-// extend the list of RBAC permissions over time.
-// TODO Merge this method with ObjectResolver, which accepts kube.Object and resolves client.Object.
-func GetImmediateOwnerReference(pod *corev1.Pod) Object {
-	ownerRef := metav1.GetControllerOf(pod)
-
-	if ownerRef != nil {
-		switch ownerRef.Kind {
-		case "Pod", "ReplicaSet", "ReplicationController", "Deployment", "StatefulSet", "DaemonSet", "CronJob", "Job":
-			return Object{
-				Namespace: pod.Namespace,
-				Kind:      Kind(ownerRef.Kind),
-				Name:      ownerRef.Name,
-			}
-		}
-	}
-
-	// Pod owned by anything else is treated the same as an unmanaged pod
-	return Object{
-		Kind:      KindPod,
-		Namespace: pod.Namespace,
-		Name:      pod.Name,
-	}
-}
-
 // ComputeHash returns a hash value calculated from a given object.
 // The hash will be safe encoded to avoid bad words.
 func ComputeHash(obj interface{}) string {
diff --git a/pkg/operator/controller/ciskubebenchreport.go b/pkg/operator/controller/ciskubebenchreport.go
index d726445..a7a8033 100644
--- a/pkg/operator/controller/ciskubebenchreport.go
+++ b/pkg/operator/controller/ciskubebenchreport.go
@@ -298,6 +298,9 @@ func (r *CISKubeBenchReportReconciler) processCompleteScanJob(ctx context.Contex
 func (r *CISKubeBenchReportReconciler) deleteJob(ctx context.Context, job *batchv1.Job) error {
 	err := r.Client.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
 	if err != nil {
+		if errors.IsNotFound(err) {
+			return nil
+		}
 		return fmt.Errorf("deleting job: %w", err)
 	}
 	return nil
diff --git a/pkg/operator/controller/configauditreport.go b/pkg/operator/controller/configauditreport.go
index ad6fd50..e501712 100644
--- a/pkg/operator/controller/configauditreport.go
+++ b/pkg/operator/controller/configauditreport.go
@@ -35,8 +35,13 @@ type ConfigAuditReportReconciler struct {
 	configauditreport.ReadWriter
 }
 
-var (
-	workloads = []struct {
+func (r *ConfigAuditReportReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	installModePredicate, err := InstallModePredicate(r.Config)
+	if err != nil {
+		return err
+	}
+
+	workloads := []struct {
 		kind       kube.Kind
 		forObject  client.Object
 		ownsObject client.Object
@@ -49,13 +54,6 @@ var (
 		{kind: kube.KindCronJob, forObject: &batchv1beta1.CronJob{}, ownsObject: &v1alpha1.ConfigAuditReport{}},
 		{kind: kube.KindJob, forObject: &batchv1.Job{}, ownsObject: &v1alpha1.ConfigAuditReport{}},
 	}
-)
-
-func (r *ConfigAuditReportReconciler) SetupWithManager(mgr ctrl.Manager) error {
-	installModePredicate, err := InstallModePredicate(r.Config)
-	if err != nil {
-		return err
-	}
 
 	for _, workload := range workloads {
 		err = ctrl.NewControllerManagedBy(mgr).
@@ -97,7 +95,7 @@ func (r *ConfigAuditReportReconciler) reconcileWorkload(workloadKind kube.Kind)
 			return ctrl.Result{}, fmt.Errorf("getting %s from cache: %w", workloadKind, err)
 		}
 
-		// Skip processing if it's a Pod controlled by a standard K8s workload.
+		// Skip processing if it's a Pod controlled by a built-in K8s workload.
 		if workloadKind == kube.KindPod {
 			controller := metav1.GetControllerOf(workloadObj)
 			if kube.IsBuiltInWorkload(controller) {
diff --git a/pkg/operator/controller/vulnerabilityreport.go b/pkg/operator/controller/vulnerabilityreport.go
index a9902d4..6171b2f 100644
--- a/pkg/operator/controller/vulnerabilityreport.go
+++ b/pkg/operator/controller/vulnerabilityreport.go
@@ -12,7 +12,9 @@ import (
 	. "github.com/aquasecurity/starboard/pkg/operator/predicate"
 	"github.com/aquasecurity/starboard/pkg/vulnerabilityreport"
 	"github.com/go-logr/logr"
+	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
+	batchv1beta1 "k8s.io/api/batch/v1beta1"
 	corev1 "k8s.io/api/core/v1"
 	"k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -41,16 +43,33 @@ func (r *VulnerabilityReportReconciler) SetupWithManager(mgr ctrl.Manager) error
 	if err != nil {
 		return err
 	}
-	err = ctrl.NewControllerManagedBy(mgr).
-		For(&corev1.Pod{}, builder.WithPredicates(
-			Not(ManagedByStarboardOperator),
-			Not(IsBeingTerminated),
-			PodHasContainersReadyCondition,
-			installModePredicate,
-		)).
-		Complete(r.reconcilePods())
-	if err != nil {
-		return err
+
+	workloads := []struct {
+		kind       kube.Kind
+		forObject  client.Object
+		ownsObject client.Object
+	}{
+		{kind: kube.KindPod, forObject: &corev1.Pod{}, ownsObject: &v1alpha1.VulnerabilityReport{}},
+		{kind: kube.KindReplicaSet, forObject: &appsv1.ReplicaSet{}, ownsObject: &v1alpha1.VulnerabilityReport{}},
+		{kind: kube.KindReplicationController, forObject: &corev1.ReplicationController{}, ownsObject: &v1alpha1.VulnerabilityReport{}},
+		{kind: kube.KindStatefulSet, forObject: &appsv1.StatefulSet{}, ownsObject: &v1alpha1.VulnerabilityReport{}},
+		{kind: kube.KindDaemonSet, forObject: &appsv1.DaemonSet{}, ownsObject: &v1alpha1.VulnerabilityReport{}},
+		{kind: kube.KindCronJob, forObject: &batchv1beta1.CronJob{}, ownsObject: &v1alpha1.VulnerabilityReport{}},
+		{kind: kube.KindJob, forObject: &batchv1.Job{}, ownsObject: &v1alpha1.VulnerabilityReport{}},
+	}
+
+	for _, workload := range workloads {
+		err = ctrl.NewControllerManagedBy(mgr).
+			For(workload.forObject, builder.WithPredicates(
+				Not(ManagedByStarboardOperator),
+				Not(IsBeingTerminated),
+				installModePredicate,
+			)).
+			Owns(workload.ownsObject).
+			Complete(r.reconcileWorkload(workload.kind))
+		if err != nil {
+			return err
+		}
 	}
 	return ctrl.NewControllerManagedBy(mgr).
 		For(&batchv1.Job{}, builder.WithPredicates(
@@ -62,49 +81,69 @@ func (r *VulnerabilityReportReconciler) SetupWithManager(mgr ctrl.Manager) error
 		Complete(r.reconcileJobs())
 }
 
-func (r *VulnerabilityReportReconciler) reconcilePods() reconcile.Func {
+func (r *VulnerabilityReportReconciler) reconcileWorkload(workloadKind kube.Kind) reconcile.Func {
 	return func(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
-		log := r.Logger.WithValues("pod", req.NamespacedName)
+		log := r.Logger.WithValues("kind", workloadKind, "name", req.NamespacedName)
 
-		pod := &corev1.Pod{}
+		workloadPartial := kube.GetPartialObjectFromKindAndNamespacedName(workloadKind, req.NamespacedName)
 
-		// Retrieve the Pod from cache.
-		err := r.Get(ctx, req.NamespacedName, pod)
+		log.V(1).Info("Getting workload from cache")
+		workloadObj, err := r.GetObjectFromPartialObject(ctx, workloadPartial)
 		if err != nil {
 			if errors.IsNotFound(err) {
-				log.V(1).Info("Ignoring pod that must have been deleted")
+				log.V(1).Info("Ignoring cached workload that must have been deleted")
+				return ctrl.Result{}, nil
+			}
+			return ctrl.Result{}, fmt.Errorf("getting %s from cache: %w", workloadKind, err)
+		}
+
+		// Skip processing if it's a Pod controlled by a built-in K8s workload.
+		if workloadKind == kube.KindPod {
+			controller := metav1.GetControllerOf(workloadObj)
+			if kube.IsBuiltInWorkload(controller) {
+				log.V(1).Info("Ignoring managed pod", "controllerKind", controller.Kind, "controllerName", controller.Name)
+				return ctrl.Result{}, nil
+			}
+		}
+
+		// Skip processing if it's a Job controlled by CronJob.
+		if workloadKind == kube.KindJob {
+			controller := metav1.GetControllerOf(workloadObj)
+			if controller != nil && controller.Kind == string(kube.KindCronJob) {
+				log.V(1).Info("Ignoring managed job", "controllerKind", controller.Kind, "controllerName", controller.Name)
 				return ctrl.Result{}, nil
 			}
-			return ctrl.Result{}, fmt.Errorf("getting pod from cache: %w", err)
 		}
 
-		owner := kube.GetImmediateOwnerReference(pod)
-		containerImages := kube.GetContainerImagesFromPodSpec(pod.Spec)
-		hash := kube.ComputeHash(pod.Spec)
+		podSpec, err := kube.GetPodSpec(workloadObj)
+		if err != nil {
+			return ctrl.Result{}, err
+		}
+
+		containerImages := kube.GetContainerImagesFromPodSpec(podSpec)
+		hash := kube.ComputeHash(podSpec)
 
-		log.V(1).Info("Resolving workload properties",
-			"owner", owner, "hash", hash, "containerImages", containerImages)
+		log = log.WithValues("podSpecHash", hash)
 
 		// Check if containers of the Pod have corresponding VulnerabilityReports.
-		hasVulnerabilityReports, err := r.hasVulnerabilityReports(ctx, owner, hash, containerImages)
+		hasVulnerabilityReports, err := r.hasVulnerabilityReports(ctx, workloadPartial, hash, containerImages)
 		if err != nil {
 			return ctrl.Result{}, fmt.Errorf("getting vulnerability reports: %w", err)
 		}
 
 		if hasVulnerabilityReports {
-			log.V(1).Info("Ignoring pod that already has VulnerabilityReports", "owner", owner)
+			log.V(1).Info("VulnerabilityReports already exist")
 			return ctrl.Result{}, nil
 		}
 
-		_, job, err := r.hasActiveScanJob(ctx, owner, hash)
+		_, job, err := r.hasActiveScanJob(ctx, workloadPartial, hash)
 		if err != nil {
 			return ctrl.Result{}, fmt.Errorf("checking scan job: %w", err)
 		}
 
 		if job != nil {
 			log.V(1).Info("Scan job already exists",
-				"job", fmt.Sprintf("%s/%s", job.Namespace, job.Name),
-				"owner", owner)
+				"job", fmt.Sprintf("%s/%s", job.Namespace, job.Name))
 			return ctrl.Result{}, nil
 		}
 
@@ -119,7 +158,7 @@ func (r *VulnerabilityReportReconciler) reconcilePods() reconcile.Func {
 			return ctrl.Result{RequeueAfter: r.Config.ScanJobRetryAfter}, nil
 		}
 
-		return ctrl.Result{}, r.submitScanJob(ctx, pod.Spec, owner, containerImages, hash)
+		return ctrl.Result{}, r.submitScanJob(ctx, podSpec, workloadPartial, containerImages, hash)
 	}
 }
 
@@ -265,14 +304,14 @@ func (r *VulnerabilityReportReconciler) reconcileJobs() reconcile.Func {
 		err := r.Client.Get(ctx, req.NamespacedName, job)
 		if err != nil {
 			if errors.IsNotFound(err) {
-				log.V(1).Info("Ignoring job that must have been deleted")
+				log.V(1).Info("Ignoring cached job that must have been deleted")
 				return ctrl.Result{}, nil
 			}
 			return ctrl.Result{}, fmt.Errorf("getting job from cache: %w", err)
 		}
 
 		if len(job.Status.Conditions) == 0 {
-			log.V(1).Info("Job has no conditions despite using predicate")
+			log.V(1).Info("Ignoring Job without conditions")
 			return ctrl.Result{}, nil
 		}
 
@@ -292,6 +331,7 @@ func (r *VulnerabilityReportReconciler) reconcileJobs() reconcile.Func {
 
 func (r *VulnerabilityReportReconciler) processCompleteScanJob(ctx context.Context, job *batchv1.Job) error {
 	log := r.Logger.WithValues("job", fmt.Sprintf("%s/%s", job.Namespace, job.Name))
+
 	owner, err := kube.ObjectFromLabelsSet(job.Labels)
 	if err != nil {
 		return fmt.Errorf("getting workload from scan job labels set: %w", err)
@@ -323,7 +363,7 @@ func (r *VulnerabilityReportReconciler) processCompleteScanJob(ctx context.Conte
 	if hasVulnerabilityReports {
 		log.V(1).Info("VulnerabilityReports already exist", "owner", owner)
 		log.V(1).Info("Deleting complete scan job", "owner", owner)
-		return r.Client.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
+		return r.deleteJob(ctx, job)
 	}
 
 	var vulnerabilityReports []v1alpha1.VulnerabilityReport
@@ -357,7 +397,7 @@ func (r *VulnerabilityReportReconciler) processCompleteScanJob(ctx context.Conte
 	}
 
 	log.V(1).Info("Deleting complete scan job", "owner", owner)
-	return r.Client.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
+	return r.deleteJob(ctx, job)
 }
 
 func (r *VulnerabilityReportReconciler) processFailedScanJob(ctx context.Context, scanJob *batchv1.Job) error {
@@ -374,5 +414,16 @@ func (r *VulnerabilityReportReconciler) processFailedScanJob(ctx context.Context
 		log.Error(nil, "Scan job container", "container", container, "status.reason", status.Reason, "status.message", status.Message)
 	}
 	log.V(1).Info("Deleting failed scan job")
-	return r.Client.Delete(ctx, scanJob, client.PropagationPolicy(metav1.DeletePropagationBackground))
+	return r.deleteJob(ctx, scanJob)
+}
+
+func (r *VulnerabilityReportReconciler) deleteJob(ctx context.Context, job *batchv1.Job) error {
+	err := r.Client.Delete(ctx, job, client.PropagationPolicy(metav1.DeletePropagationBackground))
+	if err != nil {
+		if errors.IsNotFound(err) {
+			return nil
+		}
+		return fmt.Errorf("deleting job: %w", err)
+	}
+	return nil
 }
diff --git a/pkg/operator/predicate/predicate.go b/pkg/operator/predicate/predicate.go
index 56f6da4..2fb43af 100644
--- a/pkg/operator/predicate/predicate.go
+++ b/pkg/operator/predicate/predicate.go
@@ -5,7 +5,6 @@ import (
 	"github.com/aquasecurity/starboard/pkg/kube"
 	"github.com/aquasecurity/starboard/pkg/operator/etc"
 	batchv1 "k8s.io/api/batch/v1"
-	corev1 "k8s.io/api/core/v1"
 	"sigs.k8s.io/controller-runtime/pkg/client"
 	"sigs.k8s.io/controller-runtime/pkg/event"
 	"sigs.k8s.io/controller-runtime/pkg/predicate"
@@ -63,19 +62,6 @@ var ManagedByStarboardOperator = predicate.NewPredicateFuncs(func(obj client.Obj
 	return false
 })
 
-// PodHasContainersReadyCondition is a predicate.Predicate that returns true if the
-// specified client.Object is a corev1.Pod with corev1.ContainersReady condition.
-var PodHasContainersReadyCondition = predicate.NewPredicateFuncs(func(obj client.Object) bool {
-	if pod, ok := obj.(*corev1.Pod); ok {
-		for _, condition := range pod.Status.Conditions {
-			if condition.Type == corev1.ContainersReady {
-				return true
-			}
-		}
-	}
-	return false
-})
-
 // IsBeingTerminated is a predicate.Predicate that returns true if the specified
 // client.Object is being terminated, i.e. its DeletionTimestamp property is set to non nil value.
 var IsBeingTerminated = predicate.NewPredicateFuncs(func(obj client.Object) bool {
diff --git a/pkg/vulnerabilityreport/builder.go b/pkg/vulnerabilityreport/builder.go
index f750178..46e638a 100644
--- a/pkg/vulnerabilityreport/builder.go
+++ b/pkg/vulnerabilityreport/builder.go
@@ -92,7 +92,7 @@ func (b *builder) Get() (v1alpha1.VulnerabilityReport, error) {
 		},
 		Report: b.result,
 	}
-	err = controllerutil.SetOwnerReference(b.owner, &report, b.scheme)
+	err = controllerutil.SetControllerReference(b.owner, &report, b.scheme)
 	if err != nil {
 		return v1alpha1.VulnerabilityReport{}, err
 	}


Test patch:
diff --git a/itest/matcher/matcher.go b/itest/matcher/matcher.go
index 4ca2181..bf9207c 100644
--- a/itest/matcher/matcher.go
+++ b/itest/matcher/matcher.go
@@ -67,10 +67,12 @@ func (m *vulnerabilityReportMatcher) Match(actual interface{}) (bool, error) {
 				kube.LabelResourceNamespace: Equal(m.owner.GetNamespace()),
 			}),
 			"OwnerReferences": ConsistOf(metav1.OwnerReference{
-				APIVersion: gvk.GroupVersion().Identifier(),
-				Kind:       gvk.Kind,
-				Name:       m.owner.GetName(),
-				UID:        m.owner.GetUID(),
+				APIVersion:         gvk.GroupVersion().Identifier(),
+				Kind:               gvk.Kind,
+				Name:               m.owner.GetName(),
+				UID:                m.owner.GetUID(),
+				Controller:         pointer.BoolPtr(true),
+				BlockOwnerDeletion: pointer.BoolPtr(true),
 			}),
 		}),
 		"Report": MatchFields(IgnoreExtras, Fields{
diff --git a/itest/matcher/matcher_test.go b/itest/matcher/matcher_test.go
index 71aa50e..0f8985c 100644
--- a/itest/matcher/matcher_test.go
+++ b/itest/matcher/matcher_test.go
@@ -44,10 +44,12 @@ func TestVulnerabilityReportMatcher(t *testing.T) {
 				},
 				OwnerReferences: []metav1.OwnerReference{
 					{
-						APIVersion: "v1",
-						Kind:       "Pod",
-						Name:       "nginx-pod",
-						UID:        "56d53a84-c81b-4620-81a1-e226c35d3983",
+						APIVersion:         "v1",
+						Kind:               "Pod",
+						Name:               "nginx-pod",
+						UID:                "56d53a84-c81b-4620-81a1-e226c35d3983",
+						Controller:         pointer.BoolPtr(true),
+						BlockOwnerDeletion: pointer.BoolPtr(true),
 					},
 				},
 			},
diff --git a/itest/starboard-operator/starboard_operator_test.go b/itest/starboard-operator/starboard_operator_test.go
index fbc9abc..8c3375c 100644
--- a/itest/starboard-operator/starboard_operator_test.go
+++ b/itest/starboard-operator/starboard_operator_test.go
@@ -187,8 +187,7 @@ var _ = Describe("Starboard Operator", func() {
 
 		It("Should create VulnerabilityReport and ConfigAuditReport", func() {
 			Eventually(HasConfigAuditReportOwnedBy(cronJob), assertionTimeout).Should(BeTrue())
-			// FIXME(issue: #415): Assign VulnerabilityReports to CronJob instead of Jobs. The PodSpec of a CronJob does not change so there's no point in rescanning individual Jobs.
-			// Eventually(HasVulnerabilityReportOwnedBy(cronJob), assertionTimeout).Should(BeTrue())
+			Eventually(HasVulnerabilityReportOwnedBy(cronJob), assertionTimeout).Should(BeTrue())
 		})
 
 		AfterEach(func() {
diff --git a/pkg/kube/resources_test.go b/pkg/kube/resources_test.go
index 462a513..f619dd9 100644
--- a/pkg/kube/resources_test.go
+++ b/pkg/kube/resources_test.go
@@ -10,7 +10,6 @@ import (
 	batchv1 "k8s.io/api/batch/v1"
 	corev1 "k8s.io/api/core/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
-	"k8s.io/utils/pointer"
 )
 
 func TestGetContainerImagesFromPodSpec(t *testing.T) {
@@ -66,104 +65,6 @@ func TestGetContainerImagesFromJob(t *testing.T) {
 	})
 }
 
-func TestGetImmediateOwnerReference(t *testing.T) {
-
-	testCases := []struct {
-		name          string
-		pod           *corev1.Pod
-		expectedOwner kube.Object
-	}{
-		{
-			name: "Should return pod as owner of unmanaged pod",
-			pod: &corev1.Pod{
-				ObjectMeta: metav1.ObjectMeta{
-					Namespace: "foo",
-					Name:      "unmanaged-pod",
-				},
-			},
-			expectedOwner: kube.Object{
-				Kind:      "Pod",
-				Namespace: "foo",
-				Name:      "unmanaged-pod",
-			},
-		},
-		{
-			name: "Should return ReplicaSet as owner of pod managed by Deployment",
-			pod: &corev1.Pod{
-				ObjectMeta: metav1.ObjectMeta{
-					Namespace: "bar",
-					Name:      "nginx-6d4cf56db6-8g9j6",
-					OwnerReferences: []metav1.OwnerReference{
-						{
-							APIVersion: "apps/v1",
-							Kind:       "ReplicaSet",
-							Name:       "nginx-6d4cf56db6",
-							Controller: pointer.BoolPtr(true),
-						},
-					},
-				},
-			},
-			expectedOwner: kube.Object{
-				Kind:      "ReplicaSet",
-				Namespace: "bar",
-				Name:      "nginx-6d4cf56db6",
-			},
-		},
-		{
-			name: "Should return pod as owner of static pod managed by kubelet",
-			pod: &corev1.Pod{
-				ObjectMeta: metav1.ObjectMeta{
-					Namespace: "kube-system",
-					Name:      "etcd-kind-control-plane",
-					OwnerReferences: []metav1.OwnerReference{
-						{
-							APIVersion: "v1",
-							Kind:       "Node",
-							Name:       "kind-control-plane",
-							Controller: pointer.BoolPtr(true),
-						},
-					},
-				},
-			},
-			expectedOwner: kube.Object{
-				Kind:      "Pod",
-				Namespace: "kube-system",
-				Name:      "etcd-kind-control-plane",
-			},
-		},
-		{
-			name: "Should return pod as owner of pod managed by third party workload",
-			pod: &corev1.Pod{
-				ObjectMeta: metav1.ObjectMeta{
-					Namespace: "dev",
-					Name:      "hello-world-argo",
-					OwnerReferences: []metav1.OwnerReference{
-						{
-							APIVersion: "argoproj.io/v1alpha1",
-							Kind:       "Workflow",
-							Name:       "hello-world-argo-r99sq",
-							Controller: pointer.BoolPtr(true),
-						},
-					},
-				},
-			},
-			expectedOwner: kube.Object{
-				Kind:      "Pod",
-				Namespace: "dev",
-				Name:      "hello-world-argo",
-			},
-		},
-	}
-
-	for _, tc := range testCases {
-		t.Run(tc.name, func(t *testing.T) {
-			owner := kube.GetImmediateOwnerReference(tc.pod)
-			assert.Equal(t, tc.expectedOwner, owner)
-		})
-	}
-
-}
-
 func TestComputeHash(t *testing.T) {
 
 	booleanValue1 := true
diff --git a/pkg/operator/predicate/predicate_test.go b/pkg/operator/predicate/predicate_test.go
index dc14091..c371e12 100644
--- a/pkg/operator/predicate/predicate_test.go
+++ b/pkg/operator/predicate/predicate_test.go
@@ -251,48 +251,6 @@ var _ = Describe("Predicate", func() {
 		})
 	})
 
-	Describe("When checking a PodHasContainersReadyCondition predicate", func() {
-
-		instance := predicate.PodHasContainersReadyCondition
-
-		Context("Where pod has ContainersReady condition", func() {
-			It("Should return true", func() {
-				obj := &corev1.Pod{
-					Status: corev1.PodStatus{
-						Conditions: []corev1.PodCondition{
-							{Type: corev1.PodInitialized},
-							{Type: corev1.PodReady},
-							{Type: corev1.ContainersReady},
-						},
-					},
-				}
-
-				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeTrue())
-				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeTrue())
-				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeTrue())
-				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeTrue())
-			})
-		})
-
-		Context("Where pod doesn't have ContainersReady condition", func() {
-			It("Should return false", func() {
-				obj := &corev1.Pod{
-					Status: corev1.PodStatus{
-						Conditions: []corev1.PodCondition{
-							{Type: corev1.PodInitialized},
-							{Type: corev1.PodReady},
-						},
-					},
-				}
-
-				Expect(instance.Create(event.CreateEvent{Object: obj})).To(BeFalse())
-				Expect(instance.Update(event.UpdateEvent{ObjectNew: obj})).To(BeFalse())
-				Expect(instance.Delete(event.DeleteEvent{Object: obj})).To(BeFalse())
-				Expect(instance.Generic(event.GenericEvent{Object: obj})).To(BeFalse())
-			})
-		})
-	})
-
 	Describe("When checking a JobHasAnyCondition predicate", func() {
 		instance := predicate.JobHasAnyCondition
 		Context("Where job has any condition", func() {
diff --git a/pkg/vulnerabilityreport/builder_test.go b/pkg/vulnerabilityreport/builder_test.go
index 87043c8..c5f4922 100644
--- a/pkg/vulnerabilityreport/builder_test.go
+++ b/pkg/vulnerabilityreport/builder_test.go
@@ -10,6 +10,7 @@ import (
 	appsv1 "k8s.io/api/apps/v1"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/client-go/kubernetes/scheme"
+	"k8s.io/utils/pointer"
 )
 
 func TestBuilder(t *testing.T) {
@@ -31,9 +32,11 @@ func TestBuilder(t *testing.T) {
 			Namespace: "qa",
 			OwnerReferences: []metav1.OwnerReference{
 				{
-					APIVersion: "apps/v1",
-					Kind:       "ReplicaSet",
-					Name:       "some-owner",
+					APIVersion:         "apps/v1",
+					Kind:               "ReplicaSet",
+					Name:               "some-owner",
+					Controller:         pointer.BoolPtr(true),
+					BlockOwnerDeletion: pointer.BoolPtr(true),
 				},
 			},
 			Labels: map[string]string{