Repository: aquasecurity/starboard
Pull request #462 (2021-03-25 19:38:31)

feat(cli): Show top 5 failed workload configuration checks in html report for namespace
Sample output:
![config](https://user-images.githubusercontent.com/49061426/112533224-eb495680-8dd3-11eb-8dab-975c6045b213.png)

Resolves https://github.com/aquasecurity/starboard/issues/410

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/462?src=pr&el=h1) Report
> Merging [#462](https://codecov.io/gh/aquasecurity/starboard/pull/462?src=pr&el=desc) (6525b49) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/98d5ad2f3d14e8d2422b504f9c2620970d536580?el=desc) (98d5ad2) will **decrease** coverage by `36.83%`.
> The diff coverage is `0.00%`.

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

```diff
@@             Coverage Diff             @@
##             main     #462       +/-   ##
===========================================
- Coverage   69.17%   32.34%   -36.84%     
===========================================
  Files          57       57               
  Lines        3478     3432       -46     
===========================================
- Hits         2406     1110     -1296     
- Misses        788     2254     +1466     
+ Partials      284       68      -216     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/462?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/report/html.go](https://codecov.io/gh/aquasecurity/starboard/pull/462/diff?src=pr&el=tree#diff-cGtnL3JlcG9ydC9odG1sLmdv) | `0.00% <0.00%> (ø)` | |
| [pkg/report/sort.go](https://codecov.io/gh/aquasecurity/starboard/pull/462/diff?src=pr&el=tree#diff-cGtnL3JlcG9ydC9zb3J0Lmdv) | `0.00% <0.00%> (ø)` | |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/462/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/462/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/462/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/462/diff?src=pr&el=tree#diff-cGtnL2NtZC9zY2FuLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/polaris.go](https://codecov.io/gh/aquasecurity/starboard/pull/462/diff?src=pr&el=tree#diff-cGtnL2NtZC9wb2xhcmlzLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/version.go](https://codecov.io/gh/aquasecurity/starboard/pull/462/diff?src=pr&el=tree#diff-cGtnL2NtZC92ZXJzaW9uLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/kube\_bench.go](https://codecov.io/gh/aquasecurity/starboard/pull/462/diff?src=pr&el=tree#diff-cGtnL2NtZC9rdWJlX2JlbmNoLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| [pkg/cmd/kube\_hunter.go](https://codecov.io/gh/aquasecurity/starboard/pull/462/diff?src=pr&el=tree#diff-cGtnL2NtZC9rdWJlX2h1bnRlci5nbw==) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [35 more](https://codecov.io/gh/aquasecurity/starboard/pull/462/diff?src=pr&el=tree-more) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/462?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/462?src=pr&el=footer). Last update [98d5ad2...6525b49](https://codecov.io/gh/aquasecurity/starboard/pull/462?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/report/html.go b/pkg/report/html.go
index 1c36f8b..bc71849 100644
--- a/pkg/report/html.go
+++ b/pkg/report/html.go
@@ -94,10 +94,17 @@ func (r *namespaceReporter) RetrieveData(namespace kube.Object) (templates.Names
 		return templates.NamespaceReport{}, err
 	}
 
+	var configAuditReportList v1alpha1.ConfigAuditReportList
+	err = r.client.List(context.Background(), &configAuditReportList, client.InNamespace(namespace.Name))
+	if err != nil {
+		return templates.NamespaceReport{}, err
+	}
+
 	return templates.NamespaceReport{
 		Namespace:            namespace,
 		GeneratedAt:          r.clock.Now(),
 		Top5VulnerableImages: r.topNImagesBySeverityCount(vulnerabilityReportList.Items, 5),
+		Top5FailedChecks:     r.topNFailedChecksByAffectedWorkloadsCount(configAuditReportList.Items, 5),
 	}, nil
 }
 
@@ -110,6 +117,68 @@ func (r *namespaceReporter) topNImagesBySeverityCount(reports []v1alpha1.Vulnera
 	return b[:ext.MinInt(N, len(b))]
 }
 
+func (r *namespaceReporter) topNFailedChecksByAffectedWorkloadsCount(reports []v1alpha1.ConfigAuditReport, N int) []templates.CheckWithCount {
+	checksMap := make(map[string]templates.CheckWithCount)
+
+	for _, report := range reports {
+		for _, podCheck := range report.Report.PodChecks {
+			if podCheck.Success {
+				continue
+			}
+			configId := podCheck.ID
+			_, ok := checksMap[configId]
+			if ok {
+				config := checksMap[configId]
+				config.AffectedWorkloads++
+				checksMap[configId] = config
+			} else {
+				checksMap[configId] = templates.CheckWithCount{
+					Check:             podCheck,
+					AffectedWorkloads: 1,
+				}
+			}
+		}
+
+		alreadyCheckedForWorkload := make(map[string]bool)
+		for _, container := range report.Report.ContainerChecks {
+			for _, containerCheck := range container {
+				if containerCheck.Success {
+					continue
+				}
+
+				configId := containerCheck.ID
+				if alreadyCheckedForWorkload[configId] {
+					continue
+				}
+
+				alreadyCheckedForWorkload[configId] = true
+				_, ok := checksMap[configId]
+				if ok {
+					config := checksMap[configId]
+					config.AffectedWorkloads++
+					checksMap[configId] = config
+				} else {
+					checksMap[configId] = templates.CheckWithCount{
+						Check:             containerCheck,
+						AffectedWorkloads: 1,
+					}
+				}
+			}
+		}
+	}
+
+	failedChecks := make([]templates.CheckWithCount, len(checksMap))
+	i := 0
+	for _, check := range checksMap {
+		failedChecks[i] = check
+		i++
+	}
+
+	OrderedBy(checkCompareFunc...).SortDesc(failedChecks)
+
+	return failedChecks[:ext.MinInt(N, len(failedChecks))]
+}
+
 func (r *namespaceReporter) Generate(namespace kube.Object, out io.Writer) error {
 	data, err := r.RetrieveData(namespace)
 	if err != nil {
diff --git a/pkg/report/sort.go b/pkg/report/sort.go
new file mode 100644
index 0000000..ddc4bdc
--- /dev/null
+++ b/pkg/report/sort.go
@@ -0,0 +1,77 @@
+package report
+
+import (
+	"sort"
+
+	"github.com/aquasecurity/starboard/pkg/report/templates"
+)
+
+type LessFunc func(p1, p2 *templates.CheckWithCount) bool
+
+// multiSorter implements the Sort interface, sorting the reports within.
+type multiSorter struct {
+	checks []templates.CheckWithCount
+	less   []LessFunc
+}
+
+// SortDesc sorts the argument slice according to the LessFunc functions passed to OrderedBy.
+func (ms *multiSorter) SortDesc(reports []templates.CheckWithCount) {
+	ms.checks = reports
+	sort.Stable(sort.Reverse(ms))
+}
+
+// OrderedBy returns a Sorter that sorts using the LessFunc functions, in order.
+// Call its Sort method to sort the data.
+func OrderedBy(less ...LessFunc) *multiSorter {
+	return &multiSorter{
+		less: less,
+	}
+}
+
+// Len is part of sort.Interface.
+func (ms *multiSorter) Len() int {
+	return len(ms.checks)
+}
+
+// Swap is part of sort.Interface.
+func (ms *multiSorter) Swap(i, j int) {
+	ms.checks[i], ms.checks[j] = ms.checks[j], ms.checks[i]
+}
+
+// Less is part of sort.Interface. It is implemented by looping along the
+// less functions until it finds a comparison that discriminates between
+// the two items (one is less than the other). Note that it can call the
+// less functions twice per call. We could change the functions to return
+// -1, 0, 1 and reduce the number of calls for greater efficiency: an
+// exercise for the reader.
+func (ms *multiSorter) Less(i, j int) bool {
+	p, q := &ms.checks[i], &ms.checks[j]
+	// Try all but the last comparison.
+	var k int
+	for k = 0; k < len(ms.less)-1; k++ {
+		less := ms.less[k]
+		switch {
+		case less(p, q):
+			// p < q, so we have a decision.
+			return true
+		case less(q, p):
+			// p > q, so we have a decision.
+			return false
+		}
+		// p == q; try the next comparison.
+	}
+	// All comparisons to here said "equal", so just return whatever
+	// the final comparison reports.
+	return ms.less[k](p, q)
+}
+
+var (
+	checkCompareFunc = []LessFunc{
+		func(r1, r2 *templates.CheckWithCount) bool {
+			return r1.AffectedWorkloads < r2.AffectedWorkloads
+		}, func(r1, r2 *templates.CheckWithCount) bool {
+			return r1.Severity > r2.Severity
+		}, func(r1, r2 *templates.CheckWithCount) bool {
+			return r1.ID > r2.ID
+		}}
+)
diff --git a/pkg/report/templates/namespace_report.qtpl b/pkg/report/templates/namespace_report.qtpl
index 06d7eb8..b90e80a 100644
--- a/pkg/report/templates/namespace_report.qtpl
+++ b/pkg/report/templates/namespace_report.qtpl
@@ -48,6 +48,30 @@ Aqua Starboard Namespace Security Report - Namespace: {%s p.Namespace.Name %}
     </table>
   </div>
 
+   <h3>Top 5 failed configuration checks by affected workloads count</h3>
+    <div class="row">
+      <table class="table table-sm table-bordered">
+        <thead>
+          <tr>
+            <th scope="col">Name</th>
+            <th scope="col">Severity</th>
+            <th scope="col">Category</th>
+            <th scope="col">Affected Workloads</th>
+          </tr>
+        </thead>
+        <tbody>
+        {% for _, report := range p.Top5FailedChecks %}
+        <tr>
+          <td>{%s report.ID %}</td>
+          <td>{%s report.Severity %}</td>
+          <td>{%s report.Category %}</td>
+          <td>{%d report.AffectedWorkloads %}</td>
+        </tr>
+        {% endfor %}
+        </tbody>
+      </table>
+    </div>
+
 </div>
 {% endfunc %}
 
diff --git a/pkg/report/templates/namespace_report.qtpl.go b/pkg/report/templates/namespace_report.qtpl.go
index c16a85f..658c830 100644
--- a/pkg/report/templates/namespace_report.qtpl.go
+++ b/pkg/report/templates/namespace_report.qtpl.go
@@ -149,162 +149,210 @@ func (p *NamespaceReport) StreamBody(qw422016 *qt422016.Writer) {
     </table>
   </div>
 
+   <h3>Top 5 failed configuration checks by affected workloads count</h3>
+    <div class="row">
+      <table class="table table-sm table-bordered">
+        <thead>
+          <tr>
+            <th scope="col">Name</th>
+            <th scope="col">Severity</th>
+            <th scope="col">Category</th>
+            <th scope="col">Affected Workloads</th>
+          </tr>
+        </thead>
+        <tbody>
+        `)
+//line pkg/report/templates/namespace_report.qtpl:63
+	for _, report := range p.Top5FailedChecks {
+//line pkg/report/templates/namespace_report.qtpl:63
+		qw422016.N().S(`
+        <tr>
+          <td>`)
+//line pkg/report/templates/namespace_report.qtpl:65
+		qw422016.E().S(report.ID)
+//line pkg/report/templates/namespace_report.qtpl:65
+		qw422016.N().S(`</td>
+          <td>`)
+//line pkg/report/templates/namespace_report.qtpl:66
+		qw422016.E().S(report.Severity)
+//line pkg/report/templates/namespace_report.qtpl:66
+		qw422016.N().S(`</td>
+          <td>`)
+//line pkg/report/templates/namespace_report.qtpl:67
+		qw422016.E().S(report.Category)
+//line pkg/report/templates/namespace_report.qtpl:67
+		qw422016.N().S(`</td>
+          <td>`)
+//line pkg/report/templates/namespace_report.qtpl:68
+		qw422016.N().D(report.AffectedWorkloads)
+//line pkg/report/templates/namespace_report.qtpl:68
+		qw422016.N().S(`</td>
+        </tr>
+        `)
+//line pkg/report/templates/namespace_report.qtpl:70
+	}
+//line pkg/report/templates/namespace_report.qtpl:70
+	qw422016.N().S(`
+        </tbody>
+      </table>
+    </div>
+
 </div>
 `)
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 }
 
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 func (p *NamespaceReport) WriteBody(qq422016 qtio422016.Writer) {
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 	p.StreamBody(qw422016)
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 	qt422016.ReleaseWriter(qw422016)
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 }
 
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 func (p *NamespaceReport) Body() string {
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 	qb422016 := qt422016.AcquireByteBuffer()
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 	p.WriteBody(qb422016)
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 	qs422016 := string(qb422016.B)
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 	qt422016.ReleaseByteBuffer(qb422016)
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 	return qs422016
-//line pkg/report/templates/namespace_report.qtpl:52
+//line pkg/report/templates/namespace_report.qtpl:76
 }
 
-//line pkg/report/templates/namespace_report.qtpl:54
+//line pkg/report/templates/namespace_report.qtpl:78
 func streamimageReference(qw422016 *qt422016.Writer, registry v1alpha1.Registry, artifact v1alpha1.Artifact) {
-//line pkg/report/templates/namespace_report.qtpl:54
+//line pkg/report/templates/namespace_report.qtpl:78
 	qw422016.N().S(`
   `)
-//line pkg/report/templates/namespace_report.qtpl:55
+//line pkg/report/templates/namespace_report.qtpl:79
 	if artifact.Tag != "" && artifact.Digest != "" {
-//line pkg/report/templates/namespace_report.qtpl:55
+//line pkg/report/templates/namespace_report.qtpl:79
 		qw422016.N().S(`
     `)
-//line pkg/report/templates/namespace_report.qtpl:56
+//line pkg/report/templates/namespace_report.qtpl:80
 		qw422016.E().S(registry.Server)
-//line pkg/report/templates/namespace_report.qtpl:56
+//line pkg/report/templates/namespace_report.qtpl:80
 		qw422016.N().S(`/`)
-//line pkg/report/templates/namespace_report.qtpl:56
+//line pkg/report/templates/namespace_report.qtpl:80
 		qw422016.E().S(artifact.Repository)
-//line pkg/report/templates/namespace_report.qtpl:56
+//line pkg/report/templates/namespace_report.qtpl:80
 		qw422016.N().S(`:`)
-//line pkg/report/templates/namespace_report.qtpl:56
+//line pkg/report/templates/namespace_report.qtpl:80
 		qw422016.E().S(artifact.Tag)
-//line pkg/report/templates/namespace_report.qtpl:56
+//line pkg/report/templates/namespace_report.qtpl:80
 		qw422016.N().S(`@`)
-//line pkg/report/templates/namespace_report.qtpl:56
+//line pkg/report/templates/namespace_report.qtpl:80
 		qw422016.E().S(artifact.Digest)
-//line pkg/report/templates/namespace_report.qtpl:56
+//line pkg/report/templates/namespace_report.qtpl:80
 		qw422016.N().S(`
     `)
-//line pkg/report/templates/namespace_report.qtpl:57
+//line pkg/report/templates/namespace_report.qtpl:81
 		return
-//line pkg/report/templates/namespace_report.qtpl:58
+//line pkg/report/templates/namespace_report.qtpl:82
 	}
-//line pkg/report/templates/namespace_report.qtpl:58
+//line pkg/report/templates/namespace_report.qtpl:82
 	qw422016.N().S(`
 
   `)
-//line pkg/report/templates/namespace_report.qtpl:60
+//line pkg/report/templates/namespace_report.qtpl:84
 	if artifact.Tag == "" && artifact.Digest != "" {
-//line pkg/report/templates/namespace_report.qtpl:60
+//line pkg/report/templates/namespace_report.qtpl:84
 		qw422016.N().S(`
     `)
-//line pkg/report/templates/namespace_report.qtpl:61
+//line pkg/report/templates/namespace_report.qtpl:85
 		qw422016.E().S(registry.Server)
-//line pkg/report/templates/namespace_report.qtpl:61
+//line pkg/report/templates/namespace_report.qtpl:85
 		qw422016.N().S(`/`)
-//line pkg/report/templates/namespace_report.qtpl:61
+//line pkg/report/templates/namespace_report.qtpl:85
 		qw422016.E().S(artifact.Repository)
-//line pkg/report/templates/namespace_report.qtpl:61
+//line pkg/report/templates/namespace_report.qtpl:85
 		qw422016.N().S(`@`)
-//line pkg/report/templates/namespace_report.qtpl:61
+//line pkg/report/templates/namespace_report.qtpl:85
 		qw422016.E().S(artifact.Digest)
-//line pkg/report/templates/namespace_report.qtpl:61
+//line pkg/report/templates/namespace_report.qtpl:85
 		qw422016.N().S(`
     `)
-//line pkg/report/templates/namespace_report.qtpl:62
+//line pkg/report/templates/namespace_report.qtpl:86
 		return
-//line pkg/report/templates/namespace_report.qtpl:63
+//line pkg/report/templates/namespace_report.qtpl:87
 	}
-//line pkg/report/templates/namespace_report.qtpl:63
+//line pkg/report/templates/namespace_report.qtpl:87
 	qw422016.N().S(`
 
   `)
-//line pkg/report/templates/namespace_report.qtpl:65
+//line pkg/report/templates/namespace_report.qtpl:89
 	if artifact.Tag != "" && artifact.Digest == "" {
-//line pkg/report/templates/namespace_report.qtpl:65
+//line pkg/report/templates/namespace_report.qtpl:89
 		qw422016.N().S(`
     `)
-//line pkg/report/templates/namespace_report.qtpl:66
+//line pkg/report/templates/namespace_report.qtpl:90
 		qw422016.E().S(registry.Server)
-//line pkg/report/templates/namespace_report.qtpl:66
+//line pkg/report/templates/namespace_report.qtpl:90
 		qw422016.N().S(`/`)
-//line pkg/report/templates/namespace_report.qtpl:66
+//line pkg/report/templates/namespace_report.qtpl:90
 		qw422016.E().S(artifact.Repository)
-//line pkg/report/templates/namespace_report.qtpl:66
+//line pkg/report/templates/namespace_report.qtpl:90
 		qw422016.N().S(`:`)
-//line pkg/report/templates/namespace_report.qtpl:66
+//line pkg/report/templates/namespace_report.qtpl:90
 		qw422016.E().S(artifact.Tag)
-//line pkg/report/templates/namespace_report.qtpl:66
+//line pkg/report/templates/namespace_report.qtpl:90
 		qw422016.N().S(`
     `)
-//line pkg/report/templates/namespace_report.qtpl:67
+//line pkg/report/templates/namespace_report.qtpl:91
 		return
-//line pkg/report/templates/namespace_report.qtpl:68
+//line pkg/report/templates/namespace_report.qtpl:92
 	}
-//line pkg/report/templates/namespace_report.qtpl:68
+//line pkg/report/templates/namespace_report.qtpl:92
 	qw422016.N().S(`
 
   `)
-//line pkg/report/templates/namespace_report.qtpl:70
+//line pkg/report/templates/namespace_report.qtpl:94
 	qw422016.E().S(registry.Server)
-//line pkg/report/templates/namespace_report.qtpl:70
+//line pkg/report/templates/namespace_report.qtpl:94
 	qw422016.N().S(`/`)
-//line pkg/report/templates/namespace_report.qtpl:70
+//line pkg/report/templates/namespace_report.qtpl:94
 	qw422016.E().S(artifact.Repository)
-//line pkg/report/templates/namespace_report.qtpl:70
+//line pkg/report/templates/namespace_report.qtpl:94
 	qw422016.N().S(`:`)
-//line pkg/report/templates/namespace_report.qtpl:70
+//line pkg/report/templates/namespace_report.qtpl:94
 	qw422016.E().S(artifact.Tag)
-//line pkg/report/templates/namespace_report.qtpl:70
+//line pkg/report/templates/namespace_report.qtpl:94
 	qw422016.N().S(`
 `)
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 }
 
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 func writeimageReference(qq422016 qtio422016.Writer, registry v1alpha1.Registry, artifact v1alpha1.Artifact) {
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 	qw422016 := qt422016.AcquireWriter(qq422016)
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 	streamimageReference(qw422016, registry, artifact)
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 	qt422016.ReleaseWriter(qw422016)
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 }
 
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 func imageReference(registry v1alpha1.Registry, artifact v1alpha1.Artifact) string {
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 	qb422016 := qt422016.AcquireByteBuffer()
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 	writeimageReference(qb422016, registry, artifact)
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 	qs422016 := string(qb422016.B)
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 	qt422016.ReleaseByteBuffer(qb422016)
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 	return qs422016
-//line pkg/report/templates/namespace_report.qtpl:71
+//line pkg/report/templates/namespace_report.qtpl:95
 }
diff --git a/pkg/report/templates/types.go b/pkg/report/templates/types.go
index 12a0fc3..913e772 100644
--- a/pkg/report/templates/types.go
+++ b/pkg/report/templates/types.go
@@ -25,6 +25,12 @@ type NamespaceReport struct {
 	GeneratedAt time.Time
 
 	Top5VulnerableImages []v1alpha1.VulnerabilityReport
+	Top5FailedChecks     []CheckWithCount
+}
+
+type CheckWithCount struct {
+	v1alpha1.Check
+	AffectedWorkloads int
 }
 
 // NodeReport is a structure that holds data to render
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/doc.go b/itest/matcher/doc.go
new file mode 100644
index 0000000..f3dd7a4
--- /dev/null
+++ b/itest/matcher/doc.go
@@ -0,0 +1,2 @@
+// The matcher package provides domain-specific Gomega matchers.
+package matcher
diff --git a/itest/matcher/matcher.go b/itest/matcher/matcher.go
new file mode 100644
index 0000000..bf9207c
--- /dev/null
+++ b/itest/matcher/matcher.go
@@ -0,0 +1,165 @@
+package matcher
+
+import (
+	. "github.com/onsi/gomega"
+	. "github.com/onsi/gomega/gstruct"
+
+	"fmt"
+
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/kube"
+	"github.com/aquasecurity/starboard/pkg/starboard"
+	"github.com/onsi/gomega/types"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/pointer"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
+)
+
+var (
+	trivyScanner = v1alpha1.Scanner{
+		Name:    "Trivy",
+		Vendor:  "Aqua Security",
+		Version: "0.16.0",
+	}
+	polarisScanner = v1alpha1.Scanner{
+		Name:    "Polaris",
+		Vendor:  "Fairwinds Ops",
+		Version: "3.2",
+	}
+)
+
+// IsVulnerabilityReportForContainerOwnedBy succeeds if a v1alpha1.VulnerabilityReport has a valid structure,
+// corresponds to the given container and is owned by the specified client.Object.
+//
+// Note: This matcher is not suitable for unit tests because it does not perform a strict validation
+// of the actual v1alpha1.VulnerabilityReport.
+func IsVulnerabilityReportForContainerOwnedBy(containerName string, owner client.Object) types.GomegaMatcher {
+	return &vulnerabilityReportMatcher{
+		containerName: containerName,
+		owner:         owner,
+	}
+}
+
+type vulnerabilityReportMatcher struct {
+	owner                 client.Object
+	containerName         string
+	failureMessage        string
+	negatedFailureMessage string
+}
+
+func (m *vulnerabilityReportMatcher) Match(actual interface{}) (bool, error) {
+	_, ok := actual.(v1alpha1.VulnerabilityReport)
+	if !ok {
+		return false, fmt.Errorf("%T expects a %T", vulnerabilityReportMatcher{}, v1alpha1.VulnerabilityReport{})
+	}
+	gvk, err := apiutil.GVKForObject(m.owner, starboard.NewScheme())
+	if err != nil {
+		return false, err
+	}
+
+	matcher := MatchFields(IgnoreExtras, Fields{
+		"ObjectMeta": MatchFields(IgnoreExtras, Fields{
+			"Labels": MatchAllKeys(Keys{
+				kube.LabelContainerName:     Equal(m.containerName),
+				kube.LabelResourceKind:      Equal(gvk.Kind),
+				kube.LabelResourceName:      Equal(m.owner.GetName()),
+				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(),
+				Controller:         pointer.BoolPtr(true),
+				BlockOwnerDeletion: pointer.BoolPtr(true),
+			}),
+		}),
+		"Report": MatchFields(IgnoreExtras, Fields{
+			"Scanner":         Equal(trivyScanner),
+			"Vulnerabilities": Not(BeNil()),
+		}),
+	})
+
+	success, err := matcher.Match(actual)
+	if err != nil {
+		return false, err
+	}
+	m.failureMessage = matcher.FailureMessage(actual)
+	m.negatedFailureMessage = matcher.NegatedFailureMessage(actual)
+	return success, nil
+}
+
+func (m *vulnerabilityReportMatcher) FailureMessage(_ interface{}) string {
+	// TODO Add more descriptive message rather than rely on composed matchers' defaults
+	return m.failureMessage
+}
+
+func (m *vulnerabilityReportMatcher) NegatedFailureMessage(_ interface{}) string {
+	return m.negatedFailureMessage
+}
+
+// IsConfigAuditReportOwnedBy succeeds if a v1alpha1.ConfigAuditReport has a valid structure,
+// and is owned by the specified client.Object.
+//
+// Note: This matcher is not suitable for unit tests because it does not perform a strict validation
+// of the actual v1alpha1.ConfigAuditReport.
+func IsConfigAuditReportOwnedBy(owner client.Object) types.GomegaMatcher {
+	return &configAuditReportMatcher{
+		owner: owner,
+	}
+}
+
+type configAuditReportMatcher struct {
+	owner                 client.Object
+	failureMessage        string
+	negatedFailureMessage string
+}
+
+func (m *configAuditReportMatcher) Match(actual interface{}) (bool, error) {
+	_, ok := actual.(v1alpha1.ConfigAuditReport)
+	if !ok {
+		return false, fmt.Errorf("%T expects a %T", configAuditReportMatcher{}, v1alpha1.ConfigAuditReport{})
+	}
+	gvk, err := apiutil.GVKForObject(m.owner, starboard.NewScheme())
+	if err != nil {
+		return false, err
+	}
+
+	matcher := MatchFields(IgnoreExtras, Fields{
+		"ObjectMeta": MatchFields(IgnoreExtras, Fields{
+			"Labels": MatchAllKeys(Keys{
+				kube.LabelResourceKind:      Equal(gvk.Kind),
+				kube.LabelResourceName:      Equal(m.owner.GetName()),
+				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(),
+				Controller:         pointer.BoolPtr(true),
+				BlockOwnerDeletion: pointer.BoolPtr(true),
+			}),
+		}),
+		"Report": MatchFields(IgnoreExtras, Fields{
+			"Scanner": Equal(polarisScanner),
+		}),
+	})
+	success, err := matcher.Match(actual)
+	if err != nil {
+		return false, err
+	}
+	m.failureMessage = matcher.FailureMessage(actual)
+	m.negatedFailureMessage = matcher.NegatedFailureMessage(actual)
+	return success, nil
+}
+
+func (m *configAuditReportMatcher) FailureMessage(_ interface{}) string {
+	// TODO Add more descriptive message rather than rely on composed matchers' defaults
+	return m.failureMessage
+}
+
+func (m *configAuditReportMatcher) NegatedFailureMessage(_ interface{}) string {
+	return m.negatedFailureMessage
+}
diff --git a/itest/matcher/matcher_test.go b/itest/matcher/matcher_test.go
new file mode 100644
index 0000000..0f8985c
--- /dev/null
+++ b/itest/matcher/matcher_test.go
@@ -0,0 +1,119 @@
+package matcher_test
+
+import (
+	. "github.com/onsi/gomega"
+
+	"testing"
+
+	"github.com/aquasecurity/starboard/itest/matcher"
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/kube"
+	appsv1 "k8s.io/api/apps/v1"
+	corev1 "k8s.io/api/core/v1"
+	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
+	"k8s.io/utils/pointer"
+)
+
+func TestVulnerabilityReportMatcher(t *testing.T) {
+
+	t.Run("Should return error when actual is not VulnerabilityReport", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		instance := matcher.IsVulnerabilityReportForContainerOwnedBy("nginx", &corev1.Pod{})
+		_, err := instance.Match("I AM INVALID ACTUAL")
+		g.Expect(err).To(MatchError("matcher.vulnerabilityReportMatcher expects a v1alpha1.VulnerabilityReport"))
+	})
+
+	t.Run("Should return true when VulnerabilityReport matches", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		instance := matcher.IsVulnerabilityReportForContainerOwnedBy("nginx-container", &corev1.Pod{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "nginx-pod",
+				Namespace: "default",
+				UID:       "56d53a84-c81b-4620-81a1-e226c35d3983",
+			},
+		})
+		success, err := instance.Match(v1alpha1.VulnerabilityReport{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "pod-nginx-pod-nginx-container",
+				Namespace: "default",
+				Labels: map[string]string{
+					kube.LabelContainerName:     "nginx-container",
+					kube.LabelResourceKind:      "Pod",
+					kube.LabelResourceName:      "nginx-pod",
+					kube.LabelResourceNamespace: "default",
+				},
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion:         "v1",
+						Kind:               "Pod",
+						Name:               "nginx-pod",
+						UID:                "56d53a84-c81b-4620-81a1-e226c35d3983",
+						Controller:         pointer.BoolPtr(true),
+						BlockOwnerDeletion: pointer.BoolPtr(true),
+					},
+				},
+			},
+			Report: v1alpha1.VulnerabilityScanResult{
+				Scanner: v1alpha1.Scanner{
+					Name:    "Trivy",
+					Vendor:  "Aqua Security",
+					Version: "0.16.0",
+				},
+				Vulnerabilities: []v1alpha1.Vulnerability{},
+			},
+		})
+		g.Expect(err).ToNot(HaveOccurred())
+		g.Expect(success).To(BeTrue())
+	})
+}
+
+func TestConfigAuditReportMatcher(t *testing.T) {
+
+	t.Run("Should return error when actual is not ConfigAuditReport", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		instance := matcher.IsConfigAuditReportOwnedBy(&appsv1.ReplicaSet{})
+		_, err := instance.Match("I AM INVALID ACTUAL")
+		g.Expect(err).To(MatchError("matcher.configAuditReportMatcher expects a v1alpha1.ConfigAuditReport"))
+	})
+
+	t.Run("Should return true when ConfigAuditReport matches", func(t *testing.T) {
+		g := NewGomegaWithT(t)
+		instance := matcher.IsConfigAuditReportOwnedBy(&appsv1.ReplicaSet{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "nginx-6d4cf56db6",
+				Namespace: "default",
+				UID:       "494b2727-5d52-4057-9a9b-8b508c753fea",
+			},
+		})
+		success, err := instance.Match(v1alpha1.ConfigAuditReport{
+			ObjectMeta: metav1.ObjectMeta{
+				Name:      "replicaset-nginx-6d4cf56db6",
+				Namespace: "default",
+				Labels: map[string]string{
+					kube.LabelResourceKind:      "ReplicaSet",
+					kube.LabelResourceName:      "nginx-6d4cf56db6",
+					kube.LabelResourceNamespace: "default",
+				},
+				OwnerReferences: []metav1.OwnerReference{
+					{
+						APIVersion:         "apps/v1",
+						Kind:               "ReplicaSet",
+						Name:               "nginx-6d4cf56db6",
+						UID:                "494b2727-5d52-4057-9a9b-8b508c753fea",
+						Controller:         pointer.BoolPtr(true),
+						BlockOwnerDeletion: pointer.BoolPtr(true),
+					},
+				},
+			},
+			Report: v1alpha1.ConfigAuditResult{
+				Scanner: v1alpha1.Scanner{
+					Name:    "Polaris",
+					Vendor:  "Fairwinds Ops",
+					Version: "3.2",
+				},
+			},
+		})
+		g.Expect(err).ToNot(HaveOccurred())
+		g.Expect(success).To(BeTrue())
+	})
+}
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/itest/starboard/starboard_cli_test.go b/itest/starboard/starboard_cli_test.go
index 01cd3eb..9920c07 100644
--- a/itest/starboard/starboard_cli_test.go
+++ b/itest/starboard/starboard_cli_test.go
@@ -1,6 +1,12 @@
 package starboard
 
 import (
+	. "github.com/aquasecurity/starboard/itest/matcher"
+	. "github.com/onsi/ginkgo"
+	. "github.com/onsi/gomega"
+	. "github.com/onsi/gomega/gbytes"
+	. "github.com/onsi/gomega/gstruct"
+
 	"context"
 	"os"
 	"strings"
@@ -10,10 +16,6 @@ import (
 	"github.com/aquasecurity/starboard/pkg/cmd"
 	"github.com/aquasecurity/starboard/pkg/kube"
 	"github.com/aquasecurity/starboard/pkg/starboard"
-	. "github.com/onsi/ginkgo"
-	. "github.com/onsi/gomega"
-	. "github.com/onsi/gomega/gbytes"
-	. "github.com/onsi/gomega/gstruct"
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
 	batchv1beta1 "k8s.io/api/batch/v1beta1"
@@ -67,11 +69,6 @@ var (
 		Vendor:  "Aqua Security",
 		Version: "0.16.0",
 	}
-	polarisScanner = v1alpha1.Scanner{
-		Name:    "Polaris",
-		Vendor:  "Fairwinds Ops",
-		Version: "3.2",
-	}
 )
 
 var _ = Describe("Starboard CLI", func() {
@@ -195,9 +192,7 @@ var _ = Describe("Starboard CLI", func() {
 	Describe("Command scan vulnerabilityreports", func() {
 		// TODO 1. Add test cases for other types of Kubernetes controllers (StatefulSets, DaemonSets, etc.)
 
-		// containerNameAsIdFn is used as an identifier by the MatchAllElements matcher
-		// to group Vulnerability reports by container name.
-		containerNameAsIdFn := func(element interface{}) string {
+		groupByContainerName := func(element interface{}) string {
 			return element.(v1alpha1.VulnerabilityReport).
 				Labels[kube.LabelContainerName]
 		}
@@ -214,7 +209,7 @@ var _ = Describe("Starboard CLI", func() {
 				Expect(err).ToNot(HaveOccurred())
 			})
 
-			It("should create vulnerabilities resource", func() {
+			It("should create VulnerabilityReport", func() {
 				err := cmd.Run(versionInfo, []string{
 					"starboard",
 					"find", "vulnerabilities", "pod/" + podName,
@@ -232,26 +227,8 @@ var _ = Describe("Starboard CLI", func() {
 					})
 				Expect(err).ToNot(HaveOccurred())
 
-				Expect(reportList.Items).To(MatchAllElements(containerNameAsIdFn, Elements{
-					"nginx": MatchFields(IgnoreExtras, Fields{
-						"ObjectMeta": MatchFields(IgnoreExtras, Fields{
-							"Labels": MatchAllKeys(Keys{
-								kube.LabelContainerName:     Equal(podName),
-								kube.LabelResourceKind:      Equal("Pod"),
-								kube.LabelResourceName:      Equal(podName),
-								kube.LabelResourceNamespace: Equal(podNamespace),
-							}),
-							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion: "v1",
-								Kind:       "Pod",
-								Name:       podName,
-								UID:        pod.UID,
-							}),
-						}),
-						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(trivyScanner),
-						}),
-					}),
+				Expect(reportList.Items).To(MatchAllElements(groupByContainerName, Elements{
+					"nginx": IsVulnerabilityReportForContainerOwnedBy("nginx", pod),
 				}))
 			})
 
@@ -293,45 +270,9 @@ var _ = Describe("Starboard CLI", func() {
 					})
 				Expect(err).ToNot(HaveOccurred())
 
-				Expect(reportList.Items).To(MatchAllElements(containerNameAsIdFn, Elements{
-					"nginx": MatchFields(IgnoreExtras, Fields{
-						"ObjectMeta": MatchFields(IgnoreExtras, Fields{
-							"Labels": MatchAllKeys(Keys{
-								kube.LabelContainerName:     Equal("nginx"),
-								kube.LabelResourceKind:      Equal("Pod"),
-								kube.LabelResourceName:      Equal(podName),
-								kube.LabelResourceNamespace: Equal(podNamespace),
-							}),
-							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion: "v1",
-								Kind:       "Pod",
-								Name:       podName,
-								UID:        pod.UID,
-							}),
-						}),
-						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(trivyScanner),
-						}),
-					}),
-					"tomcat": MatchFields(IgnoreExtras, Fields{
-						"ObjectMeta": MatchFields(IgnoreExtras, Fields{
-							"Labels": MatchAllKeys(Keys{
-								kube.LabelContainerName:     Equal("tomcat"),
-								kube.LabelResourceKind:      Equal("Pod"),
-								kube.LabelResourceName:      Equal(podName),
-								kube.LabelResourceNamespace: Equal(podNamespace),
-							}),
-							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion: "v1",
-								Kind:       "Pod",
-								Name:       podName,
-								UID:        pod.UID,
-							}),
-						}),
-						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(trivyScanner),
-						}),
-					}),
+				Expect(reportList.Items).To(MatchAllElements(groupByContainerName, Elements{
+					"nginx":  IsVulnerabilityReportForContainerOwnedBy("nginx", pod),
+					"tomcat": IsVulnerabilityReportForContainerOwnedBy("tomcat", pod),
 				}))
 			})
 
@@ -391,26 +332,8 @@ var _ = Describe("Starboard CLI", func() {
 					})
 				Expect(err).ToNot(HaveOccurred())
 
-				Expect(reportList.Items).To(MatchAllElements(containerNameAsIdFn, Elements{
-					"nginx": MatchFields(IgnoreExtras, Fields{
-						"ObjectMeta": MatchFields(IgnoreExtras, Fields{
-							"Labels": MatchAllKeys(Keys{
-								kube.LabelContainerName:     Equal("nginx"),
-								kube.LabelResourceKind:      Equal("Pod"),
-								kube.LabelResourceName:      Equal(podName),
-								kube.LabelResourceNamespace: Equal(podNamespace),
-							}),
-							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion: "v1",
-								Kind:       "Pod",
-								Name:       podName,
-								UID:        pod.UID,
-							}),
-						}),
-						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(trivyScanner),
-						}),
-					}),
+				Expect(reportList.Items).To(MatchAllElements(groupByContainerName, Elements{
+					"nginx": IsVulnerabilityReportForContainerOwnedBy("nginx", pod),
 				}))
 			})
 
@@ -482,26 +405,8 @@ var _ = Describe("Starboard CLI", func() {
 					})
 				Expect(err).ToNot(HaveOccurred())
 
-				Expect(reportList.Items).To(MatchAllElements(containerNameAsIdFn, Elements{
-					"nginx": MatchFields(IgnoreExtras, Fields{
-						"ObjectMeta": MatchFields(IgnoreExtras, Fields{
-							"Labels": MatchAllKeys(Keys{
-								kube.LabelContainerName:     Equal("nginx"),
-								kube.LabelResourceKind:      Equal("ReplicaSet"),
-								kube.LabelResourceName:      Equal(rsName),
-								kube.LabelResourceNamespace: Equal(rsNamespace),
-							}),
-							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion: "apps/v1",
-								Kind:       "ReplicaSet",
-								Name:       rsName,
-								UID:        rs.UID,
-							}),
-						}),
-						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(trivyScanner),
-						}),
-					}),
+				Expect(reportList.Items).To(MatchAllElements(groupByContainerName, Elements{
+					"nginx": IsVulnerabilityReportForContainerOwnedBy("nginx", rs),
 				}))
 			})
 
@@ -569,26 +474,8 @@ var _ = Describe("Starboard CLI", func() {
 					})
 				Expect(err).ToNot(HaveOccurred())
 
-				Expect(reportList.Items).To(MatchAllElements(containerNameAsIdFn, Elements{
-					"nginx": MatchFields(IgnoreExtras, Fields{
-						"ObjectMeta": MatchFields(IgnoreExtras, Fields{
-							"Labels": MatchAllKeys(Keys{
-								kube.LabelContainerName:     Equal("nginx"),
-								kube.LabelResourceKind:      Equal("ReplicationController"),
-								kube.LabelResourceName:      Equal(rcName),
-								kube.LabelResourceNamespace: Equal(rcNamespace),
-							}),
-							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion: "v1",
-								Kind:       "ReplicationController",
-								Name:       rcName,
-								UID:        rc.UID,
-							}),
-						}),
-						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(trivyScanner),
-						}),
-					}),
+				Expect(reportList.Items).To(MatchAllElements(groupByContainerName, Elements{
+					"nginx": IsVulnerabilityReportForContainerOwnedBy("nginx", rc),
 				}))
 			})
 
@@ -656,26 +543,8 @@ var _ = Describe("Starboard CLI", func() {
 					})
 				Expect(err).ToNot(HaveOccurred())
 
-				Expect(reportList.Items).To(MatchAllElements(containerNameAsIdFn, Elements{
-					"nginx": MatchFields(IgnoreExtras, Fields{
-						"ObjectMeta": MatchFields(IgnoreExtras, Fields{
-							"Labels": MatchAllKeys(Keys{
-								kube.LabelContainerName:     Equal("nginx"),
-								kube.LabelResourceKind:      Equal("Deployment"),
-								kube.LabelResourceName:      Equal(deployName),
-								kube.LabelResourceNamespace: Equal(deployNamespace),
-							}),
-							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion: "apps/v1",
-								Kind:       "Deployment",
-								Name:       deployName,
-								UID:        deploy.UID,
-							}),
-						}),
-						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(trivyScanner),
-						}),
-					}),
+				Expect(reportList.Items).To(MatchAllElements(groupByContainerName, Elements{
+					"nginx": IsVulnerabilityReportForContainerOwnedBy("nginx", deploy),
 				}))
 			})
 
@@ -881,9 +750,8 @@ var _ = Describe("Starboard CLI", func() {
 	})
 
 	Describe("Command scan configauditreports", func() {
-		// containerNameAsIDFn is used as an identifier by the MatchAllElements matcher
-		// to group ConfigAuditReport by container name.
-		resourceNameAsIDFn := func(element interface{}) string {
+
+		groupByWorkloadName := func(element interface{}) string {
 			return element.(v1alpha1.ConfigAuditReport).
 				Labels[kube.LabelResourceName]
 		}
@@ -919,27 +787,8 @@ var _ = Describe("Starboard CLI", func() {
 					})
 				Expect(err).ToNot(HaveOccurred())
 
-				Expect(reportList.Items).To(MatchAllElements(resourceNameAsIDFn, Elements{
-					podName: MatchFields(IgnoreExtras, Fields{
-						"ObjectMeta": MatchFields(IgnoreExtras, Fields{
-							"Labels": MatchAllKeys(Keys{
-								kube.LabelResourceKind:      Equal("Pod"),
-								kube.LabelResourceName:      Equal(podName),
-								kube.LabelResourceNamespace: Equal(podNamespace),
-							}),
-							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion:         "v1",
-								Kind:               "Pod",
-								Name:               podName,
-								UID:                pod.UID,
-								Controller:         pointer.BoolPtr(true),
-								BlockOwnerDeletion: pointer.BoolPtr(true),
-							}),
-						}),
-						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(polarisScanner),
-						}),
-					}),
+				Expect(reportList.Items).To(MatchAllElements(groupByWorkloadName, Elements{
+					podName: IsConfigAuditReportOwnedBy(pod),
 				}))
 			})
 
@@ -982,28 +831,8 @@ var _ = Describe("Starboard CLI", func() {
 					})
 				Expect(err).ToNot(HaveOccurred())
 
-				Expect(reportList.Items).To(MatchAllElements(resourceNameAsIDFn, Elements{
-					podName: MatchFields(IgnoreExtras, Fields{
-
-						"ObjectMeta": MatchFields(IgnoreExtras, Fields{
-							"Labels": MatchAllKeys(Keys{
-								kube.LabelResourceKind:      Equal("Pod"),
-								kube.LabelResourceName:      Equal(podName),
-								kube.LabelResourceNamespace: Equal(podNamespace),
-							}),
-							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion:         "v1",
-								Kind:               "Pod",
-								Name:               podName,
-								UID:                pod.UID,
-								Controller:         pointer.BoolPtr(true),
-								BlockOwnerDeletion: pointer.BoolPtr(true),
-							}),
-						}),
-						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(polarisScanner),
-						}),
-					}),
+				Expect(reportList.Items).To(MatchAllElements(groupByWorkloadName, Elements{
+					podName: IsConfigAuditReportOwnedBy(pod),
 				}))
 			})
 
@@ -1071,28 +900,8 @@ var _ = Describe("Starboard CLI", func() {
 				})
 				Expect(err).ToNot(HaveOccurred())
 
-				Expect(reportList.Items).To(MatchAllElements(resourceNameAsIDFn, Elements{
-					"hello": MatchFields(IgnoreExtras, Fields{
-
-						"ObjectMeta": MatchFields(IgnoreExtras, Fields{
-							"Labels": MatchAllKeys(Keys{
-								kube.LabelResourceKind:      Equal("CronJob"),
-								kube.LabelResourceName:      Equal("hello"),
-								kube.LabelResourceNamespace: Equal(namespaceItest),
-							}),
-							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion:         "batch/v1beta1",
-								Kind:               "CronJob",
-								Name:               "hello",
-								UID:                cronJob.UID,
-								Controller:         pointer.BoolPtr(true),
-								BlockOwnerDeletion: pointer.BoolPtr(true),
-							}),
-						}),
-						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(polarisScanner),
-						}),
-					}),
+				Expect(reportList.Items).To(MatchAllElements(groupByWorkloadName, Elements{
+					"hello": IsConfigAuditReportOwnedBy(cronJob),
 				}))
 
 			})
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/report/sort_test.go b/pkg/report/sort_test.go
new file mode 100644
index 0000000..5b7c0ed
--- /dev/null
+++ b/pkg/report/sort_test.go
@@ -0,0 +1,98 @@
+package report
+
+import (
+	"testing"
+
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/report/templates"
+	"github.com/stretchr/testify/assert"
+)
+
+func TestOrderedBy(t *testing.T) {
+	checks := []templates.CheckWithCount{
+		{
+			Check: v1alpha1.Check{
+				ID:       "privilegeEscalationAllowed",
+				Severity: "danger",
+				Category: "Security",
+			},
+			AffectedWorkloads: 10,
+		},
+		{
+			Check: v1alpha1.Check{
+				ID:       "cpuLimitsMissing",
+				Severity: "warning",
+				Category: "Efficiency",
+			},
+			AffectedWorkloads: 12,
+		},
+		{
+			Check: v1alpha1.Check{
+				ID:       "cpuRequestsMissing",
+				Severity: "warning",
+				Category: "Efficiency",
+			},
+			AffectedWorkloads: 8,
+		},
+		{
+			Check: v1alpha1.Check{
+				ID:       "livenessProbeMissing",
+				Severity: "warning",
+				Category: "Reliability",
+			},
+			AffectedWorkloads: 5,
+		},
+		{
+			Check: v1alpha1.Check{
+				ID:       "insecureCapabilities",
+				Severity: "warning",
+				Category: "Security",
+			},
+			AffectedWorkloads: 5,
+		},
+	}
+
+	OrderedBy(checkCompareFunc...).SortDesc(checks)
+	assert.Equal(t, []templates.CheckWithCount{
+		{
+			Check: v1alpha1.Check{
+				ID:       "cpuLimitsMissing",
+				Severity: "warning",
+				Category: "Efficiency",
+			},
+			AffectedWorkloads: 12,
+		},
+		{
+			Check: v1alpha1.Check{
+				ID:       "privilegeEscalationAllowed",
+				Severity: "danger",
+				Category: "Security",
+			},
+			AffectedWorkloads: 10,
+		},
+		{
+			Check: v1alpha1.Check{
+				ID:       "cpuRequestsMissing",
+				Severity: "warning",
+				Category: "Efficiency",
+			},
+			AffectedWorkloads: 8,
+		},
+		{
+			Check: v1alpha1.Check{
+				ID:       "insecureCapabilities",
+				Severity: "warning",
+				Category: "Security",
+			},
+			AffectedWorkloads: 5,
+		},
+		{
+			Check: v1alpha1.Check{
+				ID:       "livenessProbeMissing",
+				Severity: "warning",
+				Category: "Reliability",
+			},
+			AffectedWorkloads: 5,
+		},
+	}, checks)
+}
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{