Repository: aquasecurity/starboard
Pull request #428 (2021-03-12 15:44:41)

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

codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/428?src=pr&el=h1) Report
> Merging [#428](https://codecov.io/gh/aquasecurity/starboard/pull/428?src=pr&el=desc) (13a34d7) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/30b95b28460c70955a975961f6f29ae1eb6f9fe4?el=desc) (30b95b2) will **decrease** coverage by `37.64%`.
> The diff coverage is `13.33%`.

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

```diff
@@             Coverage Diff             @@
##             main     #428       +/-   ##
===========================================
- Coverage   66.94%   29.29%   -37.65%     
===========================================
  Files          59       58        -1     
  Lines        3406     3328       -78     
===========================================
- Hits         2280      975     -1305     
- Misses        847     2292     +1445     
+ Partials      279       61      -218     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/428?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [pkg/configauditreport/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/428/diff?src=pr&el=tree#diff-cGtnL2NvbmZpZ2F1ZGl0cmVwb3J0L3NjYW5uZXIuZ28=) | `0.00% <0.00%> (-81.95%)` | :arrow_down: |
| [pkg/kube/object.go](https://codecov.io/gh/aquasecurity/starboard/pull/428/diff?src=pr&el=tree#diff-cGtnL2t1YmUvb2JqZWN0Lmdv) | `37.50% <ø> (-25.00%)` | :arrow_down: |
| [pkg/operator/controller/configauditreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/428/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY29uZmlnYXVkaXRyZXBvcnQuZ28=) | `0.00% <0.00%> (-61.12%)` | :arrow_down: |
| [pkg/operator/controller/owner\_resolver.go](https://codecov.io/gh/aquasecurity/starboard/pull/428/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvb3duZXJfcmVzb2x2ZXIuZ28=) | `0.00% <0.00%> (-25.00%)` | :arrow_down: |
| [pkg/operator/controller/vulnerabilityreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/428/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvdnVsbmVyYWJpbGl0eXJlcG9ydC5nbw==) | `0.00% <0.00%> (-63.21%)` | :arrow_down: |
| [pkg/starboard/config.go](https://codecov.io/gh/aquasecurity/starboard/pull/428/diff?src=pr&el=tree#diff-cGtnL3N0YXJib2FyZC9jb25maWcuZ28=) | `70.49% <0.00%> (-0.59%)` | :arrow_down: |
| [pkg/configauditreport/builder.go](https://codecov.io/gh/aquasecurity/starboard/pull/428/diff?src=pr&el=tree#diff-cGtnL2NvbmZpZ2F1ZGl0cmVwb3J0L2J1aWxkZXIuZ28=) | `77.77% <100.00%> (ø)` | |
| [pkg/operator/predicate/predicate.go](https://codecov.io/gh/aquasecurity/starboard/pull/428/diff?src=pr&el=tree#diff-cGtnL29wZXJhdG9yL3ByZWRpY2F0ZS9wcmVkaWNhdGUuZ28=) | `70.90% <100.00%> (-20.32%)` | :arrow_down: |
| [pkg/cmd/get.go](https://codecov.io/gh/aquasecurity/starboard/pull/428/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/428/diff?src=pr&el=tree#diff-cGtnL2NtZC9maW5kLmdv) | `0.00% <0.00%> (-100.00%)` | :arrow_down: |
| ... and [42 more](https://codecov.io/gh/aquasecurity/starboard/pull/428/diff?src=pr&el=tree-more) | |

------

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


Patch:
diff --git a/pkg/configauditreport/builder.go b/pkg/configauditreport/builder.go
index 75dfbbd..95e9ed7 100644
--- a/pkg/configauditreport/builder.go
+++ b/pkg/configauditreport/builder.go
@@ -12,7 +12,7 @@ import (
 )
 
 type Builder interface {
-	Owner(owner metav1.Object) Builder
+	Controller(controller metav1.Object) Builder
 	PodSpecHash(hash string) Builder
 	Result(result v1alpha1.ConfigAuditResult) Builder
 	Get() (v1alpha1.ConfigAuditReport, error)
@@ -25,14 +25,14 @@ func NewBuilder(scheme *runtime.Scheme) Builder {
 }
 
 type builder struct {
-	scheme *runtime.Scheme
-	owner  metav1.Object
-	hash   string
-	result v1alpha1.ConfigAuditResult
+	scheme     *runtime.Scheme
+	controller metav1.Object
+	hash       string
+	result     v1alpha1.ConfigAuditResult
 }
 
-func (b *builder) Owner(owner metav1.Object) Builder {
-	b.owner = owner
+func (b *builder) Controller(controller metav1.Object) Builder {
+	b.controller = controller
 	return b
 }
 
@@ -47,24 +47,24 @@ func (b *builder) Result(result v1alpha1.ConfigAuditResult) Builder {
 }
 
 func (b *builder) reportName() (string, error) {
-	kind, err := kube.KindForObject(b.owner, b.scheme)
+	kind, err := kube.KindForObject(b.controller, b.scheme)
 	if err != nil {
 		return "", err
 	}
 	return fmt.Sprintf("%s-%s", strings.ToLower(kind),
-		b.owner.GetName()), nil
+		b.controller.GetName()), nil
 }
 
 func (b *builder) Get() (v1alpha1.ConfigAuditReport, error) {
-	kind, err := kube.KindForObject(b.owner, b.scheme)
+	kind, err := kube.KindForObject(b.controller, b.scheme)
 	if err != nil {
 		return v1alpha1.ConfigAuditReport{}, err
 	}
 
 	labels := map[string]string{
 		kube.LabelResourceKind:      kind,
-		kube.LabelResourceName:      b.owner.GetName(),
-		kube.LabelResourceNamespace: b.owner.GetNamespace(),
+		kube.LabelResourceName:      b.controller.GetName(),
+		kube.LabelResourceNamespace: b.controller.GetNamespace(),
 	}
 
 	if b.hash != "" {
@@ -79,12 +79,12 @@ func (b *builder) Get() (v1alpha1.ConfigAuditReport, error) {
 	report := v1alpha1.ConfigAuditReport{
 		ObjectMeta: metav1.ObjectMeta{
 			Name:      reportName,
-			Namespace: b.owner.GetNamespace(),
+			Namespace: b.controller.GetNamespace(),
 			Labels:    labels,
 		},
 		Report: b.result,
 	}
-	err = controllerutil.SetOwnerReference(b.owner, &report, b.scheme)
+	err = controllerutil.SetControllerReference(b.controller, &report, b.scheme)
 	if err != nil {
 		return v1alpha1.ConfigAuditReport{}, err
 	}
diff --git a/pkg/configauditreport/scanner.go b/pkg/configauditreport/scanner.go
index a1a2388..f7d88cf 100644
--- a/pkg/configauditreport/scanner.go
+++ b/pkg/configauditreport/scanner.go
@@ -94,7 +94,7 @@ func (s *Scanner) Scan(ctx context.Context, workload kube.Object, gvk schema.Gro
 	}()
 
 	return NewBuilder(s.scheme).
-		Owner(owner).
+		Controller(owner).
 		Result(result).
 		Get()
 }
diff --git a/pkg/kube/object.go b/pkg/kube/object.go
index c4f97ed..c36fe31 100644
--- a/pkg/kube/object.go
+++ b/pkg/kube/object.go
@@ -13,6 +13,7 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 )
 
+// TODO Rename from Object to PartialObject (consider embedding types.NamespacedName struct)
 // Object is a simplified representation of a Kubernetes object.
 // Each object has kind, which designates the type of the entity it represents.
 // Objects have names and many of them live in namespaces.
diff --git a/pkg/operator/controller/ciskubebenchreport.go b/pkg/operator/controller/ciskubebenchreport.go
index 3f30b8a..4c3014c 100644
--- a/pkg/operator/controller/ciskubebenchreport.go
+++ b/pkg/operator/controller/ciskubebenchreport.go
@@ -71,7 +71,7 @@ func (r *CISKubeBenchReportReconciler) reconcileNodes() reconcile.Func {
 		err := r.Client.Get(ctx, req.NamespacedName, node)
 		if err != nil {
 			if errors.IsNotFound(err) {
-				log.V(1).Info("Ignoring node read from cache that must have been deleted")
+				log.V(1).Info("Ignoring cached node that must have been deleted")
 				return ctrl.Result{}, nil
 			}
 			return ctrl.Result{}, fmt.Errorf("getting node from cache: %w", err)
@@ -91,7 +91,7 @@ func (r *CISKubeBenchReportReconciler) reconcileNodes() reconcile.Func {
 		log.V(1).Info("Checking whether CIS Kubernetes Benchmark checks have been scheduled")
 		_, job, err := r.hasScanJob(ctx, node)
 		if err != nil {
-			return ctrl.Result{}, nil
+			return ctrl.Result{}, err
 		}
 		if job != nil {
 			log.V(1).Info("CIS Kubernetes Benchmark have been scheduled",
@@ -193,17 +193,18 @@ func (r *CISKubeBenchReportReconciler) reconcileJobs() reconcile.Func {
 		log := r.Logger.WithValues("job", req.NamespacedName)
 
 		job := &batchv1.Job{}
+		log.V(1).Info("Getting job from cache")
 		err := r.Client.Get(ctx, req.NamespacedName, job)
 		if err != nil {
 			if errors.IsNotFound(err) {
-				log.V(1).Info("Ignoring job read from cache 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
 		}
 
@@ -213,7 +214,7 @@ func (r *CISKubeBenchReportReconciler) reconcileJobs() reconcile.Func {
 		case batchv1.JobFailed:
 			err = r.processFailedScanJob(ctx, job)
 		default:
-			err = fmt.Errorf("unrecognized scan job condition: %v", jobCondition)
+			err = fmt.Errorf("unrecognized job condition: %v", jobCondition)
 		}
 
 		return ctrl.Result{}, err
diff --git a/pkg/operator/controller/configauditreport.go b/pkg/operator/controller/configauditreport.go
index d14b636..47154a4 100644
--- a/pkg/operator/controller/configauditreport.go
+++ b/pkg/operator/controller/configauditreport.go
@@ -4,17 +4,19 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
 	"github.com/aquasecurity/starboard/pkg/configauditreport"
 	"github.com/aquasecurity/starboard/pkg/kube"
 	"github.com/aquasecurity/starboard/pkg/operator/etc"
 	. "github.com/aquasecurity/starboard/pkg/operator/predicate"
 	"github.com/aquasecurity/starboard/pkg/resources"
 	"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"
-	"k8s.io/apimachinery/pkg/runtime/schema"
 	"k8s.io/utils/pointer"
 	ctrl "sigs.k8s.io/controller-runtime"
 	"sigs.k8s.io/controller-runtime/pkg/builder"
@@ -35,22 +37,42 @@ type ConfigAuditReportReconciler struct {
 	configauditreport.ReadWriter
 }
 
+var (
+	workloads = []struct {
+		kind       kube.Kind
+		forObject  client.Object
+		ownsObject client.Object
+	}{
+		{kind: kube.KindPod, forObject: &corev1.Pod{}, ownsObject: &v1alpha1.ConfigAuditReport{}},
+		{kind: kube.KindReplicaSet, forObject: &appsv1.ReplicaSet{}, ownsObject: &v1alpha1.ConfigAuditReport{}},
+		{kind: kube.KindReplicationController, forObject: &corev1.ReplicationController{}, ownsObject: &v1alpha1.ConfigAuditReport{}},
+		{kind: kube.KindStatefulSet, forObject: &appsv1.StatefulSet{}, ownsObject: &v1alpha1.ConfigAuditReport{}},
+		{kind: kube.KindDaemonSet, forObject: &appsv1.DaemonSet{}, ownsObject: &v1alpha1.ConfigAuditReport{}},
+		{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
 	}
-	err = ctrl.NewControllerManagedBy(mgr).
-		For(&corev1.Pod{}, builder.WithPredicates(
-			Not(ManagedByStarboardOperator),
-			Not(PodBeingTerminated),
-			PodHasContainersReadyCondition,
-			installModePredicate,
-		)).
-		Complete(r.reconcilePods())
-	if err != nil {
-		return err
+
+	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(
 			InNamespace(r.Config.Namespace),
@@ -61,49 +83,64 @@ func (r *ConfigAuditReportReconciler) SetupWithManager(mgr ctrl.Manager) error {
 		Complete(r.reconcileJobs())
 }
 
-func (r *ConfigAuditReportReconciler) reconcilePods() reconcile.Func {
+func (r *ConfigAuditReportReconciler) 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 := GetPartialObjectFromKindAndNamespacedName(workloadKind, req.NamespacedName)
 
-		err := r.Client.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 pod from cache: %w", err)
+			return ctrl.Result{}, fmt.Errorf("getting %s from cache: %w", workloadKind, err)
 		}
 
-		owner := resources.GetImmediateOwnerReference(pod)
-		hash := resources.ComputeHash(pod.Spec)
-
-		log.Info("Resolving workload properties",
-			"owner", owner, "hash", hash)
+		// Skip processing if it's a Pod controlled by a standard K8s workload.
+		if workloadKind == kube.KindPod {
+			controller := metav1.GetControllerOf(workloadObj)
+			if controller != nil &&
+				(controller.Kind == string(kube.KindReplicaSet) ||
+					controller.Kind == string(kube.KindReplicationController) ||
+					controller.Kind == string(kube.KindStatefulSet) ||
+					controller.Kind == string(kube.KindDaemonSet) ||
+					controller.Kind == string(kube.KindCronJob) ||
+					controller.Kind == string(kube.KindJob)) {
+				log.V(1).Info("Ignoring managed pod", "controllerKind", controller.Kind, "controllerName", controller.Name)
+				return ctrl.Result{}, nil
+			}
+		}
 
-		hasConfigAuditReport, err := r.hasConfigAuditReport(ctx, owner, hash)
+		podSpec, err := GetPodSpec(workloadObj)
 		if err != nil {
 			return ctrl.Result{}, err
 		}
+		podSpecHash := resources.ComputeHash(podSpec)
 
-		if hasConfigAuditReport {
-			return ctrl.Result{}, nil
-		}
+		log = log.WithValues("podSpecHash", podSpecHash)
 
-		ownerObj, err := r.OwnerResolver.Resolve(ctx, owner)
+		log.V(1).Info("Checking whether configuration audit report exists")
+		hasReport, err := r.hasReport(ctx, workloadPartial, podSpecHash)
 		if err != nil {
 			return ctrl.Result{}, err
 		}
 
-		_, job, err := r.hasActiveScanJob(ctx, owner, hash)
+		if hasReport {
+			log.V(1).Info("Configuration audit report exists")
+			return ctrl.Result{}, nil
+		}
+
+		log.V(1).Info("Checking whether configuration audit has been scheduled")
+		_, job, err := r.hasActiveScanJob(ctx, workloadPartial, podSpecHash)
 		if err != nil {
 			return ctrl.Result{}, nil
 		}
 		if job != nil {
-			log.V(1).Info("Scan job already exists",
-				"job", fmt.Sprintf("%s/%s", job.Namespace, job.Name),
-				"owner", owner)
+			log.V(1).Info("Configuration audit has been scheduled",
+				"job", fmt.Sprintf("%s/%s", job.Namespace, job.Name))
 			return ctrl.Result{}, nil
 		}
 
@@ -118,11 +155,7 @@ func (r *ConfigAuditReportReconciler) reconcilePods() reconcile.Func {
 			return ctrl.Result{RequeueAfter: r.Config.ScanJobRetryAfter}, nil
 		}
 
-		gvk, err := apiutil.GVKForObject(ownerObj, r.Client.Scheme())
-		if err != nil {
-			return ctrl.Result{}, err
-		}
-		job, secrets, err := r.getScanJob(owner, ownerObj, gvk, hash)
+		job, secrets, err := r.getScanJob(workloadPartial, workloadObj, podSpecHash)
 		if err != nil {
 			return ctrl.Result{}, err
 		}
@@ -135,6 +168,7 @@ func (r *ConfigAuditReportReconciler) reconcilePods() reconcile.Func {
 			}
 		}
 
+		log.V(1).Info("Scheduling configuration audit", "secrets", len(secrets))
 		err = r.Client.Create(ctx, job)
 		if err != nil {
 			return ctrl.Result{}, fmt.Errorf("creating job: %w", err)
@@ -155,7 +189,7 @@ func (r *ConfigAuditReportReconciler) reconcilePods() reconcile.Func {
 	}
 }
 
-func (r *ConfigAuditReportReconciler) hasConfigAuditReport(ctx context.Context, owner kube.Object, hash string) (bool, error) {
+func (r *ConfigAuditReportReconciler) hasReport(ctx context.Context, owner kube.Object, hash string) (bool, error) {
 	report, err := r.ReadWriter.FindByOwner(ctx, owner)
 	if err != nil {
 		return false, err
@@ -188,7 +222,12 @@ func (r *ConfigAuditReportReconciler) getScanJobName(workload kube.Object) strin
 	return fmt.Sprintf("scan-configauditreport-%s", resources.ComputeHash(workload))
 }
 
-func (r *ConfigAuditReportReconciler) getScanJob(workload kube.Object, obj client.Object, gvk schema.GroupVersionKind, hash string) (*batchv1.Job, []*corev1.Secret, error) {
+func (r *ConfigAuditReportReconciler) getScanJob(workload kube.Object, obj client.Object, hash string) (*batchv1.Job, []*corev1.Secret, error) {
+	gvk, err := apiutil.GVKForObject(obj, r.Client.Scheme())
+	if err != nil {
+		return nil, nil, err
+	}
+
 	jobSpec, secrets, err := r.Plugin.GetScanJobSpec(workload, obj, gvk)
 
 	if err != nil {
@@ -238,17 +277,18 @@ func (r *ConfigAuditReportReconciler) reconcileJobs() reconcile.Func {
 		log := r.Logger.WithValues("job", req.NamespacedName)
 
 		job := &batchv1.Job{}
+		log.V(1).Info("Getting job from cache")
 		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
 		}
 
@@ -258,7 +298,7 @@ func (r *ConfigAuditReportReconciler) reconcileJobs() reconcile.Func {
 		case batchv1.JobFailed:
 			err = r.processFailedScanJob(ctx, job)
 		default:
-			err = fmt.Errorf("unrecognized scan job condition: %v", jobCondition)
+			err = fmt.Errorf("unrecognized job condition: %v", jobCondition)
 		}
 
 		return ctrl.Result{}, err
@@ -274,19 +314,19 @@ func (r *ConfigAuditReportReconciler) processCompleteScanJob(ctx context.Context
 		return fmt.Errorf("getting workload from scan job labels set: %w", err)
 	}
 
-	log.Info("Processing complete scan job", "owner", owner)
+	log.V(1).Info("Processing complete scan job", "owner", owner)
 
-	ownerObj, err := r.OwnerResolver.Resolve(ctx, owner)
+	ownerObj, err := r.GetObjectFromPartialObject(ctx, owner)
 	if err != nil {
 		return err
 	}
 
-	hash, ok := job.Labels[kube.LabelPodSpecHash]
+	podSpecHash, ok := job.Labels[kube.LabelPodSpecHash]
 	if !ok {
 		return fmt.Errorf("expected label %s not set", kube.LabelPodSpecHash)
 	}
 
-	hasConfigAuditReport, err := r.hasConfigAuditReport(ctx, owner, hash)
+	hasConfigAuditReport, err := r.hasReport(ctx, owner, podSpecHash)
 	if err != nil {
 		return err
 	}
@@ -308,8 +348,8 @@ func (r *ConfigAuditReportReconciler) processCompleteScanJob(ctx context.Context
 	}()
 
 	report, err := configauditreport.NewBuilder(r.Client.Scheme()).
-		Owner(ownerObj).
-		PodSpecHash(hash).
+		Controller(ownerObj).
+		PodSpecHash(podSpecHash).
 		Result(result).
 		Get()
 	if err != nil {
diff --git a/pkg/operator/controller/owner_resolver.go b/pkg/operator/controller/owner_resolver.go
index 886f4431..97f182e 100644
--- a/pkg/operator/controller/owner_resolver.go
+++ b/pkg/operator/controller/owner_resolver.go
@@ -13,11 +13,20 @@ import (
 	"sigs.k8s.io/controller-runtime/pkg/client"
 )
 
+// TODO Rename from OwnerResolver to ObjectResolver.
 type OwnerResolver struct {
 	client.Client
 }
 
-func (o *OwnerResolver) Resolve(ctx context.Context, workload kube.Object) (client.Object, error) {
+func GetPartialObjectFromKindAndNamespacedName(kind kube.Kind, name types.NamespacedName) kube.Object {
+	return kube.Object{
+		Kind:      kind,
+		Name:      name.Name,
+		Namespace: name.Namespace,
+	}
+}
+
+func (o *OwnerResolver) GetObjectFromPartialObject(ctx context.Context, workload kube.Object) (client.Object, error) {
 	var obj client.Object
 	switch workload.Kind {
 	case kube.KindPod:
@@ -37,7 +46,7 @@ func (o *OwnerResolver) Resolve(ctx context.Context, workload kube.Object) (clie
 	case kube.KindJob:
 		obj = &batchv1.Job{}
 	default:
-		return nil, fmt.Errorf("unknown workload kind: %s", workload.Kind)
+		return nil, fmt.Errorf("unknown kind: %s", workload.Kind)
 	}
 	err := o.Client.Get(ctx, types.NamespacedName{Name: workload.Name, Namespace: workload.Namespace}, obj)
 	if err != nil {
@@ -45,3 +54,22 @@ func (o *OwnerResolver) Resolve(ctx context.Context, workload kube.Object) (clie
 	}
 	return obj, nil
 }
+
+func GetPodSpec(obj client.Object) (corev1.PodSpec, error) {
+	switch t := obj.(type) {
+	case *corev1.Pod:
+		return (obj.(*corev1.Pod)).Spec, nil
+	case *appsv1.ReplicaSet:
+		return (obj.(*appsv1.ReplicaSet)).Spec.Template.Spec, nil
+	case *corev1.ReplicationController:
+		return (obj.(*corev1.ReplicationController)).Spec.Template.Spec, nil
+	case *appsv1.StatefulSet:
+		return (obj.(*appsv1.StatefulSet)).Spec.Template.Spec, nil
+	case *appsv1.DaemonSet:
+		return (obj.(*appsv1.DaemonSet)).Spec.Template.Spec, nil
+	case *batchv1beta1.CronJob:
+		return (obj.(*batchv1beta1.CronJob)).Spec.JobTemplate.Spec.Template.Spec, nil
+	default:
+		return corev1.PodSpec{}, fmt.Errorf("cannot get PodSpec for %v", t)
+	}
+}
diff --git a/pkg/operator/controller/vulnerabilityreport.go b/pkg/operator/controller/vulnerabilityreport.go
index 8a87896..c84ac42 100644
--- a/pkg/operator/controller/vulnerabilityreport.go
+++ b/pkg/operator/controller/vulnerabilityreport.go
@@ -45,7 +45,7 @@ func (r *VulnerabilityReportReconciler) SetupWithManager(mgr ctrl.Manager) error
 	err = ctrl.NewControllerManagedBy(mgr).
 		For(&corev1.Pod{}, builder.WithPredicates(
 			Not(ManagedByStarboardOperator),
-			Not(PodBeingTerminated),
+			Not(IsBeingTerminated),
 			PodHasContainersReadyCondition,
 			installModePredicate,
 		)).
@@ -294,7 +294,7 @@ func (r *VulnerabilityReportReconciler) processCompleteScanJob(ctx context.Conte
 		return fmt.Errorf("getting workload from scan job labels set: %w", err)
 	}
 
-	ownerObj, err := r.OwnerResolver.Resolve(ctx, owner)
+	ownerObj, err := r.GetObjectFromPartialObject(ctx, owner)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/operator/predicate/predicate.go b/pkg/operator/predicate/predicate.go
index aa916e9..56f6da4 100644
--- a/pkg/operator/predicate/predicate.go
+++ b/pkg/operator/predicate/predicate.go
@@ -76,14 +76,10 @@ var PodHasContainersReadyCondition = predicate.NewPredicateFuncs(func(obj client
 	return false
 })
 
-// PodBeingTerminated is a predicate.Predicate that returns true if the specified
-// client.Object is a corev1.Pod that is being terminated, i.e. its
-// DeletionTimestamp property is set to non nil value.
-var PodBeingTerminated = predicate.NewPredicateFuncs(func(obj client.Object) bool {
-	if pod, ok := obj.(*corev1.Pod); ok {
-		return pod.DeletionTimestamp != nil
-	}
-	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 {
+	return obj.GetDeletionTimestamp() != nil
 })
 
 // JobHasConditions is a predicate.Predicate that returns true if the
diff --git a/pkg/starboard/config.go b/pkg/starboard/config.go
index c155d01..e1f5914 100644
--- a/pkg/starboard/config.go
+++ b/pkg/starboard/config.go
@@ -8,6 +8,7 @@ import (
 	"github.com/google/go-containerregistry/pkg/name"
 	appsv1 "k8s.io/api/apps/v1"
 	batchv1 "k8s.io/api/batch/v1"
+	batchv1beta1 "k8s.io/api/batch/v1beta1"
 	corev1 "k8s.io/api/core/v1"
 	apierrors "k8s.io/apimachinery/pkg/api/errors"
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
@@ -206,8 +207,9 @@ exemptions:
 func NewScheme() *runtime.Scheme {
 	scheme := runtime.NewScheme()
 	_ = corev1.AddToScheme(scheme)
-	_ = batchv1.AddToScheme(scheme)
 	_ = appsv1.AddToScheme(scheme)
+	_ = batchv1.AddToScheme(scheme)
+	_ = batchv1beta1.AddToScheme(scheme)
 	_ = v1alpha1.AddToScheme(scheme)
 	return scheme
 }


Test patch:
diff --git a/itest/starboard-operator/starboard_operator_test.go b/itest/starboard-operator/starboard_operator_test.go
index b3d4e23..043f84c 100644
--- a/itest/starboard-operator/starboard_operator_test.go
+++ b/itest/starboard-operator/starboard_operator_test.go
@@ -5,6 +5,7 @@ import (
 	"fmt"
 	"time"
 
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
 	"github.com/aquasecurity/starboard/pkg/kube"
 	. "github.com/onsi/ginkgo"
 	. "github.com/onsi/gomega"
@@ -13,6 +14,8 @@ import (
 	metav1 "k8s.io/apimachinery/pkg/apis/meta/v1"
 	"k8s.io/apimachinery/pkg/labels"
 	"k8s.io/utils/pointer"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/client/apiutil"
 )
 
 const (
@@ -26,7 +29,43 @@ var _ = Describe("Starboard Operator", func() {
 		deploymentName = "wordpress"
 	)
 
-	Describe("When a new Deployment is created", func() {
+	Describe("When unmanaged Pod is created", func() {
+
+		ctx := context.Background()
+		var pod *corev1.Pod
+
+		BeforeEach(func() {
+			pod = &corev1.Pod{
+				ObjectMeta: metav1.ObjectMeta{
+					Name:      "unmanaged-nginx",
+					Namespace: corev1.NamespaceDefault,
+				},
+				Spec: corev1.PodSpec{
+					Containers: []corev1.Container{
+						{
+							Name:  "nginx",
+							Image: "nginx:1.16",
+						},
+					},
+				},
+			}
+			err := kubeClient.Create(ctx, pod)
+			Expect(err).ToNot(HaveOccurred())
+		})
+
+		It("Should create VulnerabilityReport and ConfigAuditReport", func() {
+			Eventually(HasConfigAuditReportOwnedBy(pod), assertionTimeout).Should(BeTrue())
+			Eventually(HasVulnerabilityReportOwnedBy(pod), assertionTimeout).Should(BeTrue())
+		})
+
+		AfterEach(func() {
+			err := kubeClient.Delete(ctx, pod)
+			Expect(err).ToNot(HaveOccurred())
+		})
+
+	})
+
+	Describe("When Deployment is created", func() {
 
 		ctx := context.Background()
 
@@ -66,20 +105,13 @@ var _ = Describe("Starboard Operator", func() {
 			Eventually(HasActiveReplicaSet(namespaceName, deploymentName), assertionTimeout).Should(BeTrue())
 		})
 
-		It("Should create VulnerabilityReport", func() {
-			rs, err := GetActiveReplicaSetForDeployment(namespaceName, deploymentName)
-			Expect(err).ToNot(HaveOccurred())
-			Expect(rs).ToNot(BeNil())
-
-			Eventually(HasVulnerabilityReportOwnedBy(rs), assertionTimeout).Should(BeTrue())
-		})
-
-		It("Should create ConfigAuditReport", func() {
+		It("Should create VulnerabilityReport and ConfigAuditReport", func() {
 			rs, err := GetActiveReplicaSetForDeployment(namespaceName, deploymentName)
 			Expect(err).ToNot(HaveOccurred())
 			Expect(rs).ToNot(BeNil())
 
 			Eventually(HasConfigAuditReportOwnedBy(rs), assertionTimeout).Should(BeTrue())
+			Eventually(HasVulnerabilityReportOwnedBy(rs), assertionTimeout).Should(BeTrue())
 		})
 
 		AfterEach(func() {
@@ -114,37 +146,44 @@ func HasActiveReplicaSet(namespace, name string) func() bool {
 	}
 }
 
-func HasVulnerabilityReportOwnedBy(rs *appsv1.ReplicaSet) func() bool {
+func HasVulnerabilityReportOwnedBy(obj client.Object) func() bool {
 	return func() bool {
-		list, err := starboardClientset.AquasecurityV1alpha1().VulnerabilityReports(rs.Namespace).
-			List(context.Background(), metav1.ListOptions{
-				LabelSelector: labels.Set{
-					kube.LabelResourceKind:      "ReplicaSet",
-					kube.LabelResourceName:      rs.Name,
-					kube.LabelResourceNamespace: rs.Namespace,
-				}.String(),
-			})
+		gvk, err := apiutil.GVKForObject(obj, scheme)
 		if err != nil {
+			// TODO Report error
 			return false
 		}
-		return len(list.Items) == 1
+		var reportList v1alpha1.VulnerabilityReportList
+		err = kubeClient.List(context.Background(), &reportList, client.MatchingLabels{
+			kube.LabelResourceKind:      gvk.Kind,
+			kube.LabelResourceName:      obj.GetName(),
+			kube.LabelResourceNamespace: obj.GetNamespace(),
+		})
+		if err != nil {
+			// TODO Report error
+			return false
+		}
+		return len(reportList.Items) == 1
 	}
 }
 
-func HasConfigAuditReportOwnedBy(rs *appsv1.ReplicaSet) func() bool {
+func HasConfigAuditReportOwnedBy(obj client.Object) func() bool {
 	return func() bool {
-		list, err := starboardClientset.AquasecurityV1alpha1().ConfigAuditReports(rs.Namespace).
-			List(context.Background(), metav1.ListOptions{
-				LabelSelector: labels.Set{
-					kube.LabelResourceKind:      "ReplicaSet",
-					kube.LabelResourceName:      rs.Name,
-					kube.LabelResourceNamespace: rs.Namespace,
-				}.String(),
-			})
+		gvk, err := apiutil.GVKForObject(obj, scheme)
+		if err != nil {
+			// TODO Report error
+			return false
+		}
+		var reportsList v1alpha1.ConfigAuditReportList
+		err = kubeClient.List(context.Background(), &reportsList, client.MatchingLabels{
+			kube.LabelResourceKind:      gvk.Kind,
+			kube.LabelResourceName:      obj.GetName(),
+			kube.LabelResourceNamespace: obj.GetNamespace(),
+		})
 		if err != nil {
 			return false
 		}
-		return len(list.Items) == 1
+		return len(reportsList.Items) == 1
 	}
 }
 
diff --git a/itest/starboard-operator/suite_test.go b/itest/starboard-operator/suite_test.go
index 05304ca..95ec103 100644
--- a/itest/starboard-operator/suite_test.go
+++ b/itest/starboard-operator/suite_test.go
@@ -1,6 +1,7 @@
 package starboard_operator
 
 import (
+	"k8s.io/apimachinery/pkg/runtime"
 	"path/filepath"
 	"testing"
 
@@ -29,6 +30,7 @@ var (
 )
 
 var (
+	scheme             *runtime.Scheme
 	kubeClientset      kubernetes.Interface
 	kubeClient         client.Client
 	starboardClientset versioned.Interface
@@ -58,7 +60,8 @@ var _ = BeforeSuite(func(done Done) {
 	kubeClientset, err = kubernetes.NewForConfig(kubeConfig)
 	Expect(err).ToNot(HaveOccurred())
 
-	kubeClient, err = client.New(kubeConfig, client.Options{Scheme: starboard.NewScheme()})
+	scheme = starboard.NewScheme()
+	kubeClient, err = client.New(kubeConfig, client.Options{Scheme: scheme})
 	Expect(err).ToNot(HaveOccurred())
 
 	starboardClientset, err = versioned.NewForConfig(kubeConfig)
diff --git a/itest/starboard/starboard_cli_test.go b/itest/starboard/starboard_cli_test.go
index 91c3bba..15f4076 100644
--- a/itest/starboard/starboard_cli_test.go
+++ b/itest/starboard/starboard_cli_test.go
@@ -64,6 +64,11 @@ var (
 		Vendor:  "Aqua Security",
 		Version: "0.16.0",
 	}
+	polarisScanner = v1alpha1.Scanner{
+		Name:    "Polaris",
+		Vendor:  "Fairwinds Ops",
+		Version: "3.0",
+	}
 )
 
 var _ = Describe("Starboard CLI", func() {
@@ -72,8 +77,7 @@ var _ = Describe("Starboard CLI", func() {
 		err := cmd.Run(versionInfo, []string{
 			"starboard",
 			"init",
-			"-v",
-			starboardCLILogLevel,
+			"-v", starboardCLILogLevel,
 		}, GinkgoWriter, GinkgoWriter)
 		Expect(err).ToNot(HaveOccurred())
 	})
@@ -185,7 +189,7 @@ var _ = Describe("Starboard CLI", func() {
 
 	})
 
-	Describe("Command find vulnerabilities", 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
@@ -873,7 +877,7 @@ var _ = Describe("Starboard CLI", func() {
 		})
 	})
 
-	Describe("Command polaris", 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 {
@@ -896,8 +900,9 @@ var _ = Describe("Starboard CLI", func() {
 			It("should create configaudit resource", func() {
 				err := cmd.Run(versionInfo, []string{
 					"starboard",
-					"polaris", "pod/" + podName,
-					"-v", starboardCLILogLevel, "--namespace", namespaceItest,
+					"scan", "configauditreports", "pod/" + podName,
+					"--namespace", namespaceItest,
+					"-v", starboardCLILogLevel,
 				}, GinkgoWriter, GinkgoWriter)
 				Expect(err).ToNot(HaveOccurred())
 
@@ -920,18 +925,16 @@ var _ = Describe("Starboard CLI", func() {
 								kube.LabelResourceNamespace: Equal(podNamespace),
 							}),
 							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion: "v1",
-								Kind:       "Pod",
-								Name:       podName,
-								UID:        pod.UID,
+								APIVersion:         "v1",
+								Kind:               "Pod",
+								Name:               podName,
+								UID:                pod.UID,
+								Controller:         pointer.BoolPtr(true),
+								BlockOwnerDeletion: pointer.BoolPtr(true),
 							}),
 						}),
 						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(v1alpha1.Scanner{
-								Name:    "Polaris",
-								Vendor:  "Fairwinds Ops",
-								Version: "3.0",
-							}),
+							"Scanner": Equal(polarisScanner),
 						}),
 					}),
 				}))
@@ -960,8 +963,9 @@ var _ = Describe("Starboard CLI", func() {
 			It("should create configaudit resources", func() {
 				err := cmd.Run(versionInfo, []string{
 					"starboard",
-					"polaris", "pod/" + podName,
-					"-v", starboardCLILogLevel, "--namespace", namespaceItest,
+					"scan", "configauditreports", "pod/" + podName,
+					"--namespace", namespaceItest,
+					"-v", starboardCLILogLevel,
 				}, GinkgoWriter, GinkgoWriter)
 				Expect(err).ToNot(HaveOccurred())
 
@@ -985,18 +989,16 @@ var _ = Describe("Starboard CLI", func() {
 								kube.LabelResourceNamespace: Equal(podNamespace),
 							}),
 							"OwnerReferences": ConsistOf(metav1.OwnerReference{
-								APIVersion: "v1",
-								Kind:       "Pod",
-								Name:       podName,
-								UID:        pod.UID,
+								APIVersion:         "v1",
+								Kind:               "Pod",
+								Name:               podName,
+								UID:                pod.UID,
+								Controller:         pointer.BoolPtr(true),
+								BlockOwnerDeletion: pointer.BoolPtr(true),
 							}),
 						}),
 						"Report": MatchFields(IgnoreExtras, Fields{
-							"Scanner": Equal(v1alpha1.Scanner{
-								Name:    "Polaris",
-								Vendor:  "Fairwinds Ops",
-								Version: "3.0",
-							}),
+							"Scanner": Equal(polarisScanner),
 						}),
 					}),
 				}))
@@ -1012,7 +1014,7 @@ var _ = Describe("Starboard CLI", func() {
 
 	})
 
-	Describe("Command generate ciskubebenchreports", func() {
+	Describe("Command scan ciskubebenchreports", func() {
 
 		It("should run kube-bench", func() {
 			err := cmd.Run(versionInfo, []string{
diff --git a/pkg/configauditreport/builder_test.go b/pkg/configauditreport/builder_test.go
index 1ef6cf8..d192e46 100644
--- a/pkg/configauditreport/builder_test.go
+++ b/pkg/configauditreport/builder_test.go
@@ -10,11 +10,12 @@ 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) {
 	report, err := configauditreport.NewBuilder(scheme.Scheme).
-		Owner(&appsv1.ReplicaSet{
+		Controller(&appsv1.ReplicaSet{
 			ObjectMeta: metav1.ObjectMeta{
 				Name:      "some-owner",
 				Namespace: "qa",
@@ -30,9 +31,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{
diff --git a/pkg/operator/predicate/predicate_test.go b/pkg/operator/predicate/predicate_test.go
index 69c3803..dc14091 100644
--- a/pkg/operator/predicate/predicate_test.go
+++ b/pkg/operator/predicate/predicate_test.go
@@ -217,7 +217,7 @@ var _ = Describe("Predicate", func() {
 
 	Describe("When checking a PodBeingTerminated predicate", func() {
 
-		instance := predicate.PodBeingTerminated
+		instance := predicate.IsBeingTerminated
 		deletionTimestamp := metav1.NewTime(time.Now())
 
 		Context("Where pod is being terminated", func() {