Repository: aquasecurity/starboard
Pull request #879 (2021-12-29 13:07:10)

TTL scan vulnerability report 
This controller is used to manage TTL of vulnerabilityreports.
If the ttl annotation passes it automatically deletes the report.

In the long run this controller could be used for other reports as well, but for now we will focus on vulnerabilityreports.

This is a PoC to continue the discussion around https://github.com/aquasecurity/starboard/pull/863.
If we think this is a okay way forward I hope we can use this PR to work towards solving: https://github.com/aquasecurity/starboard/issues/537


There are a number of TODO:s in the code and the k8s yaml needs to be added. There is probably other things as well but hopefully it's a good base to stand on.

Currently the controller only looks for a int and uses them as seconds when setting the TTL. We need to decide if we should support other values then seconds.

NissesSenap: The code isn't perfect, I'm aware of the issue in the test but I didn't want to keep on working on it unless we think this is the way forward.
NissesSenap: Thanks for giving it a try @danielpacak and I'm glad you like it.
I will continue working on this PR and the coming days.
codecov[bot]: # [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/879?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) Report
> Merging [#879](https://codecov.io/gh/aquasecurity/starboard/pull/879?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (ccb4bdd) into [main](https://codecov.io/gh/aquasecurity/starboard/commit/b8d85683bc4e347fb77c648218aba9cdbeb9fbf0?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) (b8d8568) will **decrease** coverage by `19.94%`.
> The diff coverage is `20.23%`.

[![Impacted file tree graph](https://codecov.io/gh/aquasecurity/starboard/pull/879/graphs/tree.svg?width=650&height=150&src=pr&token=qqTGKAJ0Wb&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)](https://codecov.io/gh/aquasecurity/starboard/pull/879?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)

```diff
@@             Coverage Diff             @@
##             main     #879       +/-   ##
===========================================
- Coverage   65.58%   45.64%   -19.95%     
===========================================
  Files          58       42       -16     
  Lines        6460     5359     -1101     
===========================================
- Hits         4237     2446     -1791     
- Misses       1796     2765      +969     
+ Partials      427      148      -279     
```


| [Impacted Files](https://codecov.io/gh/aquasecurity/starboard/pull/879?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) | Coverage Δ | |
|---|---|---|
| [pkg/operator/controller/vulnerabilityreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/879/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvdnVsbmVyYWJpbGl0eXJlcG9ydC5nbw==) | `0.00% <0.00%> (-63.64%)` | :arrow_down: |
| [pkg/operator/etc/config.go](https://codecov.io/gh/aquasecurity/starboard/pull/879/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2V0Yy9jb25maWcuZ28=) | `75.75% <ø> (ø)` | |
| [pkg/vulnerabilityreport/builder.go](https://codecov.io/gh/aquasecurity/starboard/pull/879/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3Z1bG5lcmFiaWxpdHlyZXBvcnQvYnVpbGRlci5nbw==) | `72.72% <11.11%> (-11.61%)` | :arrow_down: |
| [pkg/operator/controller/ttl\_report.go](https://codecov.io/gh/aquasecurity/starboard/pull/879/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvdHRsX3JlcG9ydC5nbw==) | `23.88% <23.88%> (ø)` | |
| [pkg/plugin/factory.go](https://codecov.io/gh/aquasecurity/starboard/pull/879/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL3BsdWdpbi9mYWN0b3J5Lmdv) | `0.00% <0.00%> (-81.14%)` | :arrow_down: |
| [pkg/kubehunter/scanner.go](https://codecov.io/gh/aquasecurity/starboard/pull/879/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2t1YmVodW50ZXIvc2Nhbm5lci5nbw==) | `0.00% <0.00%> (-75.72%)` | :arrow_down: |
| [pkg/operator/controller/configauditreport.go](https://codecov.io/gh/aquasecurity/starboard/pull/879/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL29wZXJhdG9yL2NvbnRyb2xsZXIvY29uZmlnYXVkaXRyZXBvcnQuZ28=) | `0.00% <0.00%> (-64.42%)` | :arrow_down: |
| [pkg/kube/runnable\_job.go](https://codecov.io/gh/aquasecurity/starboard/pull/879/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2t1YmUvcnVubmFibGVfam9iLmdv) | `0.00% <0.00%> (-63.73%)` | :arrow_down: |
| [pkg/kubehunter/model.go](https://codecov.io/gh/aquasecurity/starboard/pull/879/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity#diff-cGtnL2t1YmVodW50ZXIvbW9kZWwuZ28=) | `0.00% <0.00%> (-62.86%)` | :arrow_down: |
| ... and [34 more](https://codecov.io/gh/aquasecurity/starboard/pull/879/diff?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity) | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/879?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/aquasecurity/starboard/pull/879?src=pr&el=footer&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity). Last update [b8d8568...ccb4bdd](https://codecov.io/gh/aquasecurity/starboard/pull/879?src=pr&el=lastupdated&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=aquasecurity).

NissesSenap: One thing hits me when I'm playing around with this feature is that how should we help our users start using the feature?
If i start the operator today and I have a bunch of old reports in my cluster I get:

```
{"level":"error","ts":1641552666.1590643,"logger":"controller.vulnerabilityreport","msg":"Reconciler error","reconciler group":"aquasecurity.github.io","reconciler kind":"VulnerabilityReport","name":"replicaset-minio-5c8bcf5d48-minio","namespace":"default","error":"the annotation starboard.aquasecurity.github.io/report-ttl is missing from the report","stacktrace":"sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).Start.func2.2\n\t/home/edvin/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.10.3/pkg/internal/controller/controller.go:227"}
```

We could of course solve this with code and delete all the reports that don't have the `annotation starboard.aquasecurity.github.io/report-ttl` but I can see why people don't think that is optimal.

A simple way for us could be to just write a migration guide where we write a small bash loop ether annotates all existing reports or we just delete them all and the operator will do the rest.

No matter which solution we pick we need to document this but I don't know where: `docs/operator/getting-started.md` don't feel like correct place for this.
`docs/operator/configuration.md` could be an option but with a new header.
Should we create a new document?

What do you think @danielpacak? 
danielpacak: > One thing hits me when I'm playing around with this feature is that how should we help our users start using the feature? If i start the operator today and I have a bunch of old reports in my cluster I get:
> 
> ```
> {"level":"error","ts":1641552666.1590643,"logger":"controller.vulnerabilityreport","msg":"Reconciler error","reconciler group":"aquasecurity.github.io","reconciler kind":"VulnerabilityReport","name":"replicaset-minio-5c8bcf5d48-minio","namespace":"default","error":"the annotation starboard.aquasecurity.github.io/report-ttl is missing from the report","stacktrace":"sigs.k8s.io/controller-runtime/pkg/internal/controller.(*Controller).Start.func2.2\n\t/home/edvin/go/pkg/mod/sigs.k8s.io/controller-runtime@v0.10.3/pkg/internal/controller/controller.go:227"}
> ```
> 
> We could of course solve this with code and delete all the reports that don't have the `annotation starboard.aquasecurity.github.io/report-ttl` but I can see why people don't think that is optimal.
> 
> A simple way for us could be to just write a migration guide where we write a small bash loop ether annotates all existing reports or we just delete them all and the operator will do the rest.
> 
> No matter which solution we pick we need to document this but I don't know where: `docs/operator/getting-started.md` don't feel like correct place for this. `docs/operator/configuration.md` could be an option but with a new header. Should we create a new document?
> 
> What do you think @danielpacak?

Starboard is incubating and w are not providing (yet) an official automated upgrade experience. Hence, we could just mention any manual steps on the GitHub release page or the [Upgrade](https://aquasecurity.github.io/starboard/v0.13.2/operator/installation/upgrade/) documentation page.

To handle vulnerability reports generated by the previous release of Starboard I'd leave them intact. And instruct user that they can delete them manually (which will trigger rescan) or patch by adding a TTL annotation, which will trigger the TTL controller.
NissesSenap: @danielpacak I have updated the builder now to use *time.duration and updated some docs and yaml to match the usage.


Patch:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index a93ed76..327f216 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -239,6 +239,7 @@ basic development workflow. For other install modes see [Operator Multitenancy w
      OPERATOR_VULNERABILITY_SCANNER_ENABLED=true \
      OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS=false \
      OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED=true \
+     OPERATOR_VULNERABILITY_SCANNER_REPORT_TTL="" \
      OPERATOR_BATCH_DELETE_LIMIT=3 \
      OPERATOR_BATCH_DELETE_DELAY="30s" \
      go run cmd/starboard-operator/main.go
diff --git a/deploy/helm/templates/deployment.yaml b/deploy/helm/templates/deployment.yaml
index 1df47bc..a77dca2 100644
--- a/deploy/helm/templates/deployment.yaml
+++ b/deploy/helm/templates/deployment.yaml
@@ -75,6 +75,8 @@ spec:
               value: {{ .Values.operator.kubernetesBenchmarkEnabled | quote }}
             - name: OPERATOR_VULNERABILITY_SCANNER_ENABLED
               value: {{ .Values.operator.vulnerabilityScannerEnabled | quote }}
+            - name: OPERATOR_VULNERABILITY_SCANNER_REPORT_TTL
+              value: {{ .Values.operator.vulnerabilityScannerReportTTL | quote }}
             - name: OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED
               value: {{ .Values.operator.configAuditScannerEnabled | quote }}
             - name: OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS
diff --git a/deploy/helm/values.yaml b/deploy/helm/values.yaml
index fc04c45..afc579d 100644
--- a/deploy/helm/values.yaml
+++ b/deploy/helm/values.yaml
@@ -35,6 +35,8 @@ operator:
 
   # vulnerabilityScannerEnabled the flag to enable vulnerability scanner
   vulnerabilityScannerEnabled: true
+  # vulnerabilityScannerReportTTL the flag to set how long a vulnerability report should exist. "" means that the vulnerabilityScannerReportTTL feature is disabled
+  vulnerabilityScannerReportTTL: ""
   # configAuditScannerEnabled the flag to enable configuration audit scanner
   configAuditScannerEnabled: true
   # kubernetesBenchmarkEnabled the flag to enable CIS Kubernetes Benchmark scanner
diff --git a/deploy/static/04-starboard-operator.deployment.yaml b/deploy/static/04-starboard-operator.deployment.yaml
index c87fa9f..bb33a08 100644
--- a/deploy/static/04-starboard-operator.deployment.yaml
+++ b/deploy/static/04-starboard-operator.deployment.yaml
@@ -79,6 +79,8 @@ spec:
               value: "true"
             - name: OPERATOR_VULNERABILITY_SCANNER_ENABLED
               value: "true"
+            - name: OPERATOR_VULNERABILITY_SCANNER_REPORT_TTL
+              value: ""
             - name: OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED
               value: "true"
             - name: OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS
diff --git a/docs/operator/configuration.md b/docs/operator/configuration.md
index c82e034..6de9d3f 100644
--- a/docs/operator/configuration.md
+++ b/docs/operator/configuration.md
@@ -1,24 +1,25 @@
 Configuration of the operator's Pod is done via environment variables at startup.
 
-| NAME                                                         | DEFAULT              | DESCRIPTION                                                                                                                   |
-| ------------------------------------------------------------ | -------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
-| `OPERATOR_NAMESPACE`                                         | N/A                  | See [Install modes](#install-modes)                                                                                           |
-| `OPERATOR_TARGET_NAMESPACES`                                 | N/A                  | See [Install modes](#install-modes)                                                                                           |
-| `OPERATOR_SERVICE_ACCOUNT`                                   | `starboard-operator` | The name of the service account assigned to the operator's pod                                                                |
-| `OPERATOR_LOG_DEV_MODE`                                      | `false`              | The flag to use (or not use) development mode (more human-readable output, extra stack traces and logging information, etc).  |
-| `OPERATOR_SCAN_JOB_TIMEOUT`                                  | `5m`                 | The length of time to wait before giving up on a scan job                                                                     |
-| `OPERATOR_CONCURRENT_SCAN_JOBS_LIMIT`                        | `10`                 | The maximum number of scan jobs create by the operator                                                                        |
-| `OPERATOR_SCAN_JOB_RETRY_AFTER`                              | `30s`                | The duration to wait before retrying a failed scan job                                                                        |
-| `OPERATOR_BATCH_DELETE_LIMIT`                                | `10`                 | The maximum number of config audit reports deleted by the operator when the plugin's config has changed.                      |
-| `OPERATOR_BATCH_DELETE_DELAY`                                | `10s`                | The duration to wait before deleting another batch of config audit reports.                                                   |
-| `OPERATOR_METRICS_BIND_ADDRESS`                              | `:8080`              | The TCP address to bind to for serving [Prometheus][prometheus] metrics. It can be set to `0` to disable the metrics serving. |
-| `OPERATOR_HEALTH_PROBE_BIND_ADDRESS`                         | `:9090`              | The TCP address to bind to for serving health probes, i.e. `/healthz/` and `/readyz/` endpoints.                              |
-| `OPERATOR_CIS_KUBERNETES_BENCHMARK_ENABLED`                  | `true`               | The flag to enable CIS Kubernetes Benchmark scanner                                                                           |
-| `OPERATOR_VULNERABILITY_SCANNER_ENABLED`                     | `true`               | The flag to enable vulnerability scanner                                                                                      |
-| `OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED`                      | `true`               | The flag to enable configuration audit scanner                                                                                |
-| `OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS` | `false`              | The flag to enable vulnerability scanner to only scan the current revision of a deployment                                    |
-| `OPERATOR_LEADER_ELECTION_ENABLED`                           | `false`              | The flag to enable operator replica leader election                                                                           |
-| `OPERATOR_LEADER_ELECTION_ID`                                | `starboard-lock`     | The name of the resource lock for leader election                                                                             |
+| NAME                                                         | DEFAULT              | DESCRIPTION                                                                                                                                                                                                  |
+| ------------------------------------------------------------ | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
+| `OPERATOR_NAMESPACE`                                         | N/A                  | See [Install modes](#install-modes)                                                                                                                                                                          |
+| `OPERATOR_TARGET_NAMESPACES`                                 | N/A                  | See [Install modes](#install-modes)                                                                                                                                                                          |
+| `OPERATOR_SERVICE_ACCOUNT`                                   | `starboard-operator` | The name of the service account assigned to the operator's pod                                                                                                                                               |
+| `OPERATOR_LOG_DEV_MODE`                                      | `false`              | The flag to use (or not use) development mode (more human-readable output, extra stack traces and logging information, etc).                                                                                 |
+| `OPERATOR_SCAN_JOB_TIMEOUT`                                  | `5m`                 | The length of time to wait before giving up on a scan job                                                                                                                                                    |
+| `OPERATOR_CONCURRENT_SCAN_JOBS_LIMIT`                        | `10`                 | The maximum number of scan jobs create by the operator                                                                                                                                                       |
+| `OPERATOR_SCAN_JOB_RETRY_AFTER`                              | `30s`                | The duration to wait before retrying a failed scan job                                                                                                                                                       |
+| `OPERATOR_BATCH_DELETE_LIMIT`                                | `10`                 | The maximum number of config audit reports deleted by the operator when the plugin's config has changed.                                                                                                     |
+| `OPERATOR_BATCH_DELETE_DELAY`                                | `10s`                | The duration to wait before deleting another batch of config audit reports.                                                                                                                                  |
+| `OPERATOR_METRICS_BIND_ADDRESS`                              | `:8080`              | The TCP address to bind to for serving [Prometheus][prometheus] metrics. It can be set to `0` to disable the metrics serving.                                                                                |
+| `OPERATOR_HEALTH_PROBE_BIND_ADDRESS`                         | `:9090`              | The TCP address to bind to for serving health probes, i.e. `/healthz/` and `/readyz/` endpoints.                                                                                                             |
+| `OPERATOR_CIS_KUBERNETES_BENCHMARK_ENABLED`                  | `true`               | The flag to enable CIS Kubernetes Benchmark scanner                                                                                                                                                          |
+| `OPERATOR_VULNERABILITY_SCANNER_ENABLED`                     | `true`               | The flag to enable vulnerability scanner                                                                                                                                                                     |
+| `OPERATOR_CONFIG_AUDIT_SCANNER_ENABLED`                      | `true`               | The flag to enable configuration audit scanner                                                                                                                                                               |
+| `OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS` | `false`              | The flag to enable vulnerability scanner to only scan the current revision of a deployment                                                                                                                   |
+| `OPERATOR_VULNERABILITY_SCANNER_REPORT_TTL`                  | `""`                 | The flag to set how long a vulnerability report should exist. When a old report is deleted a new one will be created by the controller. It can be set to `""` to disabled the TTL for vulnerability scanner. |
+| `OPERATOR_LEADER_ELECTION_ENABLED`                           | `false`              | The flag to enable operator replica leader election                                                                                                                                                          |
+| `OPERATOR_LEADER_ELECTION_ID`                                | `starboard-lock`     | The name of the resource lock for leader election                                                                                                                                                            |
 
 ## Install Modes
 
diff --git a/docs/operator/getting-started.md b/docs/operator/getting-started.md
index fadf401..a215d04 100644
--- a/docs/operator/getting-started.md
+++ b/docs/operator/getting-started.md
@@ -145,6 +145,13 @@ No resources found in default namespace.
 !!! Tip
     Use `vuln` and `configaudit` as short names for `vulnerabilityreports` and `configauditreports` resources.
 
+To be sure that your vulnerabilityreports is is up to date with the latest CVE:s you can define
+how long your vulnerabilityreports should be in the cluster before automatically getting deleted.
+For example setting `OPERATOR_VULNERABILITY_SCANNER_REPORT_TTL=24h` would delete the report after 24 hours.
+When the vulnerabilityreports gets deleted starboard will automatically create a new job and scan the images again.
+Assuming that your image scan solution have updated it's DB the new vulnerabilityreports that gets created will contain the latest CVE:s.
+This feature is disabled by default.
+
 ## Infrastructure Scanning
 
 The operator discovers also Kubernetes nodes and runs CIS Kubernetes Benchmark checks on each of them. The results are
diff --git a/pkg/apis/aquasecurity/v1alpha1/common_types.go b/pkg/apis/aquasecurity/v1alpha1/common_types.go
index 8dbcffc..ba4cd72 100644
--- a/pkg/apis/aquasecurity/v1alpha1/common_types.go
+++ b/pkg/apis/aquasecurity/v1alpha1/common_types.go
@@ -1,5 +1,9 @@
 package v1alpha1
 
+const (
+	TTLReportAnnotation = "starboard.aquasecurity.github.io/report-ttl"
+)
+
 // Scanner is the spec for a scanner generating a security assessment report.
 type Scanner struct {
 	// Name the name of the scanner.
diff --git a/pkg/operator/controller/ttl_report.go b/pkg/operator/controller/ttl_report.go
new file mode 100644
index 0000000..d2493a1
--- /dev/null
+++ b/pkg/operator/controller/ttl_report.go
@@ -0,0 +1,96 @@
+package controller
+
+import (
+	"context"
+	"fmt"
+	"time"
+
+	"github.com/aquasecurity/starboard/pkg/apis/aquasecurity/v1alpha1"
+	"github.com/aquasecurity/starboard/pkg/operator/etc"
+	"github.com/aquasecurity/starboard/pkg/operator/predicate"
+	"github.com/go-logr/logr"
+	"k8s.io/apimachinery/pkg/api/errors"
+	ctrl "sigs.k8s.io/controller-runtime"
+	"sigs.k8s.io/controller-runtime/pkg/builder"
+	"sigs.k8s.io/controller-runtime/pkg/client"
+	"sigs.k8s.io/controller-runtime/pkg/reconcile"
+)
+
+type TTLReportReconciler struct {
+	logr.Logger
+	etc.Config
+	client.Client
+}
+
+func (r *TTLReportReconciler) SetupWithManager(mgr ctrl.Manager) error {
+	installModePredicate, err := predicate.InstallModePredicate(r.Config)
+	if err != nil {
+		return err
+	}
+
+	err = ctrl.NewControllerManagedBy(mgr).
+		For(&v1alpha1.VulnerabilityReport{}, builder.WithPredicates(
+			predicate.Not(predicate.IsBeingTerminated),
+			installModePredicate)).
+		Complete(r.reconcileReport())
+	if err != nil {
+		return err
+	}
+	return nil
+}
+
+func (r *TTLReportReconciler) reconcileReport() reconcile.Func {
+	return func(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
+		log := r.Logger.WithValues("report", req.NamespacedName)
+
+		report := &v1alpha1.VulnerabilityReport{}
+		err := r.Client.Get(ctx, req.NamespacedName, report)
+		if err != nil {
+			if errors.IsNotFound(err) {
+				log.V(1).Info("Ignoring cached report that must have been deleted")
+				return ctrl.Result{}, nil
+			}
+			return ctrl.Result{}, fmt.Errorf("getting report from cache: %w", err)
+		}
+
+		ttlReportAnnotationStr, ok := report.Annotations[v1alpha1.TTLReportAnnotation]
+		if !ok {
+			log.V(1).Info("Ignoring report without TTL set")
+			return ctrl.Result{}, nil
+		}
+
+		reportTTLTime, err := time.ParseDuration(ttlReportAnnotationStr)
+		if err != nil {
+			return ctrl.Result{}, fmt.Errorf("failed parsing %v with value %v %w", v1alpha1.TTLReportAnnotation, ttlReportAnnotationStr, err)
+		}
+		creationTime := report.Report.UpdateTimestamp
+		ttlExpired, durationToTTLExpiration, err := ttlIsExpired(reportTTLTime, creationTime.Time)
+		if err != nil {
+			return ctrl.Result{}, err
+		}
+		if ttlExpired {
+			log.V(1).Info("Removing vulnerabilityReport with expired TTL")
+			err := r.Client.Delete(ctx, report, &client.DeleteOptions{})
+			if err != nil && !errors.IsNotFound(err) {
+				return ctrl.Result{}, err
+			}
+			// Since the report is deleted there is no reason to requeue
+			return ctrl.Result{}, nil
+		}
+		log.V(1).Info("RequeueAfter", "durationToTTLExpiration", durationToTTLExpiration)
+		return ctrl.Result{RequeueAfter: durationToTTLExpiration}, nil
+	}
+}
+
+func ttlIsExpired(reportTTL time.Duration, creationTime time.Time) (bool, time.Duration, error) {
+	expiresAt := creationTime.Add(reportTTL)
+	currentTime := time.Now()
+	isExpired := currentTime.After(expiresAt)
+
+	if isExpired {
+		return true, time.Duration(0), nil
+	}
+
+	expiresIn := expiresAt.Sub(currentTime)
+	return false, expiresIn, nil
+}
diff --git a/pkg/operator/controller/vulnerabilityreport.go b/pkg/operator/controller/vulnerabilityreport.go
index 5e18d88..1fcd5cf 100644
--- a/pkg/operator/controller/vulnerabilityreport.go
+++ b/pkg/operator/controller/vulnerabilityreport.go
@@ -360,12 +360,18 @@ func (r *VulnerabilityReportReconciler) processCompleteScanJob(ctx context.Conte
 		}
 		_ = logsStream.Close()
 
-		report, err := vulnerabilityreport.NewReportBuilder(r.Client.Scheme()).
+		reportBuilder := vulnerabilityreport.NewReportBuilder(r.Client.Scheme()).
 			Controller(owner).
 			Container(containerName).
 			Data(reportData).
-			PodSpecHash(podSpecHash).
-			Get()
+			PodSpecHash(podSpecHash)
+
+		if r.Config.VulnerabilityScannerReportTTL != nil {
+			reportBuilder.ReportTTL(r.Config.VulnerabilityScannerReportTTL)
+		}
+
+		report, err := reportBuilder.Get()
+
 		if err != nil {
 			return err
 		}
diff --git a/pkg/operator/etc/config.go b/pkg/operator/etc/config.go
index 77f32f6..6764618 100644
--- a/pkg/operator/etc/config.go
+++ b/pkg/operator/etc/config.go
@@ -27,6 +27,7 @@ type Config struct {
 	LeaderElectionEnabled                        bool          `env:"OPERATOR_LEADER_ELECTION_ENABLED" envDefault:"false"`
 	LeaderElectionID                             string        `env:"OPERATOR_LEADER_ELECTION_ID" envDefault:"starboard-lock"`
 	VulnerabilityScannerScanOnlyCurrentRevisions bool          `env:"OPERATOR_VULNERABILITY_SCANNER_SCAN_ONLY_CURRENT_REVISIONS" envDefault:"false"`
+	VulnerabilityScannerReportTTL                *time.Duration `env:"OPERATOR_VULNERABILITY_SCANNER_REPORT_TTL"`
 }
 
 // GetOperatorConfig loads Config from environment variables.
diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go
index 2c6ba58..6d78215 100644
--- a/pkg/operator/operator.go
+++ b/pkg/operator/operator.go
@@ -160,6 +160,16 @@ func Start(ctx context.Context, buildInfo starboard.BuildInfo, operatorConfig et
 		}).SetupWithManager(mgr); err != nil {
 			return fmt.Errorf("unable to setup vulnerabilityreport reconciler: %w", err)
 		}
+
+		if operatorConfig.VulnerabilityScannerReportTTL != nil {
+			if err = (&controller.TTLReportReconciler{
+				Logger: ctrl.Log.WithName("reconciler").WithName("ttlreport"),
+				Config: operatorConfig,
+				Client: mgr.GetClient(),
+			}).SetupWithManager(mgr); err != nil {
+				return fmt.Errorf("unable to setup TTLreport reconciler: %w", err)
+			}
+		}
 	}
 
 	if operatorConfig.ConfigAuditScannerEnabled {
diff --git a/pkg/vulnerabilityreport/builder.go b/pkg/vulnerabilityreport/builder.go
index 99da6bb..5474049 100644
--- a/pkg/vulnerabilityreport/builder.go
+++ b/pkg/vulnerabilityreport/builder.go
@@ -143,6 +143,7 @@ type ReportBuilder struct {
 	container  string
 	hash       string
 	data       v1alpha1.VulnerabilityReportData
+	reportTTL  *time.Duration
 }
 
 func NewReportBuilder(scheme *runtime.Scheme) *ReportBuilder {
@@ -171,6 +172,11 @@ func (b *ReportBuilder) Data(data v1alpha1.VulnerabilityReportData) *ReportBuild
 	return b
 }
 
+func (b *ReportBuilder) ReportTTL(ttl *time.Duration) *ReportBuilder {
+	b.reportTTL = ttl
+	return b
+}
+
 func (b *ReportBuilder) reportName() string {
 	kind := b.controller.GetObjectKind().GroupVersionKind().Kind
 	name := b.controller.GetName()
@@ -199,6 +205,12 @@ func (b *ReportBuilder) Get() (v1alpha1.VulnerabilityReport, error) {
 		},
 		Report: b.data,
 	}
+
+	if b.reportTTL != nil {
+		report.Annotations = map[string]string{
+			v1alpha1.TTLReportAnnotation: b.reportTTL.String(),
+		}
+	}
 	err := kube.ObjectToObjectMetadata(b.controller, &report.ObjectMeta)
 	if err != nil {
 		return v1alpha1.VulnerabilityReport{}, err


Test patch:
diff --git a/pkg/operator/controller/ttl_report_test.go b/pkg/operator/controller/ttl_report_test.go
new file mode 100644
index 0000000..45c4a09
--- /dev/null
+++ b/pkg/operator/controller/ttl_report_test.go
@@ -0,0 +1,28 @@
+package controller
+
+import (
+	"testing"
+	"time"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestTTLIsExpired(t *testing.T) {
+	ttlReportAnnotationStr := "10h"
+	ttlReportTime, _ := time.ParseDuration(ttlReportAnnotationStr)
+	creationTime := time.Now()
+	ttlExpired, _, err := ttlIsExpired(ttlReportTime, creationTime)
+	assert.NoError(t, err)
+	assert.False(t, ttlExpired)
+}
+
+func TestTTLIsNotExpired(t *testing.T) {
+	ttlReportAnnotationStr := "10s"
+	ttlReportTime, _ := time.ParseDuration(ttlReportAnnotationStr)
+	creationTime := time.Now()
+	then := creationTime.Add(time.Duration(-10) * time.Minute)
+	ttlExpired, durationToTTLExp, err := ttlIsExpired(ttlReportTime, then)
+	t.Logf("Duration to ttl expiration %s, we should rescheduel check", durationToTTLExp)
+	assert.NoError(t, err)
+	assert.True(t, ttlExpired)
+}