From 701749cae9c781fe3b1bd7518dd4c0e5ed352e45 Mon Sep 17 00:00:00 2001 From: Jose Donizetti Date: Mon, 13 Jun 2022 22:04:15 -0300 Subject: [PATCH] feat: exposed secrets scanning --- deploy/crd/exposedsecretreports.crd.yaml | 2 +- .../v1alpha1/exposed_secrets_types.go | 2 +- pkg/exposedsecretreport/builder.go | 112 ++++++++++++++++ pkg/exposedsecretreport/io.go | 122 ++++++++++++++++++ pkg/operator/operator.go | 24 ++-- pkg/plugin/trivy/model.go | 10 ++ pkg/plugin/trivy/plugin.go | 83 +++++++++--- pkg/plugin/trivy/plugin_test.go | 4 +- pkg/vulnerabilityreport/builder_test.go | 4 +- pkg/vulnerabilityreport/controller.go | 27 +++- pkg/vulnerabilityreport/plugin.go | 6 +- 11 files changed, 356 insertions(+), 40 deletions(-) create mode 100644 pkg/exposedsecretreport/builder.go create mode 100644 pkg/exposedsecretreport/io.go diff --git a/deploy/crd/exposedsecretreports.crd.yaml b/deploy/crd/exposedsecretreports.crd.yaml index 74424403e..42caa58ec 100644 --- a/deploy/crd/exposedsecretreports.crd.yaml +++ b/deploy/crd/exposedsecretreports.crd.yaml @@ -152,7 +152,7 @@ spec: - severity - match properties: - ruleID: + target: description: | Target is where the exposed secret was found. type: string diff --git a/pkg/apis/aquasecurity/v1alpha1/exposed_secrets_types.go b/pkg/apis/aquasecurity/v1alpha1/exposed_secrets_types.go index 3c811cc34..d48f7b35c 100644 --- a/pkg/apis/aquasecurity/v1alpha1/exposed_secrets_types.go +++ b/pkg/apis/aquasecurity/v1alpha1/exposed_secrets_types.go @@ -79,7 +79,7 @@ type ExposedSecretReportData struct { Summary ExposedSecretSummary `json:"summary"` // Secrets is a list of passwords, api keys, tokens and others items found in the Artifact. - Secrets []ExposedSecret `json:"exposed secrets"` + Secrets []ExposedSecret `json:"secrets"` } // +k8s:deepcopy-gen:interfaces=k8s.io/apimachinery/pkg/runtime.Object diff --git a/pkg/exposedsecretreport/builder.go b/pkg/exposedsecretreport/builder.go new file mode 100644 index 000000000..522398496 --- /dev/null +++ b/pkg/exposedsecretreport/builder.go @@ -0,0 +1,112 @@ +package exposedsecretreport + +import ( + "fmt" + "strings" + "time" + + "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1" + "github.com/aquasecurity/trivy-operator/pkg/kube" + "github.com/aquasecurity/trivy-operator/pkg/trivyoperator" + metav1 "k8s.io/apimachinery/pkg/apis/meta/v1" + "k8s.io/apimachinery/pkg/runtime" + "k8s.io/apimachinery/pkg/util/validation" + "k8s.io/utils/pointer" + "sigs.k8s.io/controller-runtime/pkg/client" + "sigs.k8s.io/controller-runtime/pkg/controller/controllerutil" +) + +type ReportBuilder struct { + scheme *runtime.Scheme + controller client.Object + container string + hash string + data v1alpha1.ExposedSecretReportData + reportTTL *time.Duration +} + +func NewReportBuilder(scheme *runtime.Scheme) *ReportBuilder { + return &ReportBuilder{ + scheme: scheme, + } +} + +func (b *ReportBuilder) Controller(controller client.Object) *ReportBuilder { + b.controller = controller + return b +} + +func (b *ReportBuilder) Container(name string) *ReportBuilder { + b.container = name + return b +} + +func (b *ReportBuilder) PodSpecHash(hash string) *ReportBuilder { + b.hash = hash + return b +} + +func (b *ReportBuilder) Data(data v1alpha1.ExposedSecretReportData) *ReportBuilder { + b.data = data + 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() + reportName := fmt.Sprintf("%s-%s-%s", strings.ToLower(kind), name, b.container) + if len(validation.IsValidLabelValue(reportName)) == 0 { + return reportName + } + + return fmt.Sprintf("%s-%s", strings.ToLower(kind), kube.ComputeHash(name+"-"+b.container)) +} + +func (b *ReportBuilder) Get() (v1alpha1.ExposedSecretReport, error) { + labels := map[string]string{ + trivyoperator.LabelContainerName: b.container, + } + + if b.hash != "" { + labels[trivyoperator.LabelResourceSpecHash] = b.hash + } + + report := v1alpha1.ExposedSecretReport{ + ObjectMeta: metav1.ObjectMeta{ + Name: b.reportName(), + Namespace: b.controller.GetNamespace(), + Labels: labels, + }, + Report: b.data, + } + + // TODO: do we support TTL? + if b.reportTTL != nil { + report.Annotations = map[string]string{ + v1alpha1.TTLReportAnnotation: b.reportTTL.String(), + } + } + err := kube.ObjectToObjectMeta(b.controller, &report.ObjectMeta) + if err != nil { + return v1alpha1.ExposedSecretReport{}, err + } + err = controllerutil.SetControllerReference(b.controller, &report, b.scheme) + if err != nil { + return v1alpha1.ExposedSecretReport{}, fmt.Errorf("setting controller reference: %w", err) + } + // The OwnerReferencesPermissionsEnforcement admission controller protects the + // access to metadata.ownerReferences[x].blockOwnerDeletion of an object, so + // that only users with "update" permission to the finalizers subresource of the + // referenced owner can change it. + // We set metadata.ownerReferences[x].blockOwnerDeletion to false so that + // additional RBAC permissions are not required when the OwnerReferencesPermissionsEnforcement + // is enabled. + // See https://kubernetes.io/docs/reference/access-authn-authz/admission-controllers/#ownerreferencespermissionenforcement + report.OwnerReferences[0].BlockOwnerDeletion = pointer.BoolPtr(false) + return report, nil +} diff --git a/pkg/exposedsecretreport/io.go b/pkg/exposedsecretreport/io.go new file mode 100644 index 000000000..2f2b22cd6 --- /dev/null +++ b/pkg/exposedsecretreport/io.go @@ -0,0 +1,122 @@ +package exposedsecretreport + +import ( + "context" + "fmt" + + "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1" + "github.com/aquasecurity/trivy-operator/pkg/kube" + "k8s.io/apimachinery/pkg/api/errors" + "k8s.io/apimachinery/pkg/types" + "sigs.k8s.io/controller-runtime/pkg/client" +) + +// Writer is the interface that wraps the basic Write method. +// +// Write creates or updates the given slice of v1alpha1.VulnerabilityReport +// instances. +type Writer interface { + Write(context.Context, []v1alpha1.ExposedSecretReport) error +} + +// Reader is the interface that wraps methods for finding v1alpha1.VulnerabilityReport objects. +// +// FindByOwner returns the slice of v1alpha1.VulnerabilityReport instances +// owned by the given kube.ObjectRef or an empty slice if the reports are not found. +// +// FindByOwnerInHierarchy is similar to FindByOwner except it tries to lookup +// v1alpha1.VulnerabilityReport objects owned by related Kubernetes objects. +// For example, if the given owner is a Deployment, but reports are owned by the +// active ReplicaSet (current revision) this method will return the reports. +type Reader interface { + FindByOwner(context.Context, kube.ObjectRef) ([]v1alpha1.ExposedSecretReport, error) + FindByOwnerInHierarchy(ctx context.Context, object kube.ObjectRef) ([]v1alpha1.ExposedSecretReport, error) +} + +type ReadWriter interface { + Reader + Writer +} + +type readWriter struct { + *kube.ObjectResolver +} + +// NewReadWriter constructs a new ReadWriter which is using the client package +// provided by the controller-runtime libraries for interacting with the +// Kubernetes API server. +func NewReadWriter(client client.Client) ReadWriter { + return &readWriter{ + ObjectResolver: &kube.ObjectResolver{Client: client}, + } +} + +func (r *readWriter) Write(ctx context.Context, reports []v1alpha1.ExposedSecretReport) error { + for _, report := range reports { + err := r.createOrUpdate(ctx, report) + if err != nil { + return err + } + } + return nil +} + +func (r *readWriter) createOrUpdate(ctx context.Context, report v1alpha1.ExposedSecretReport) error { + var existing v1alpha1.ExposedSecretReport + err := r.Get(ctx, types.NamespacedName{ + Name: report.Name, + Namespace: report.Namespace, + }, &existing) + + if err == nil { + copied := existing.DeepCopy() + copied.Labels = report.Labels + copied.Report = report.Report + + return r.Update(ctx, copied) + } + + if errors.IsNotFound(err) { + return r.Create(ctx, &report) + } + + return err +} + +func (r *readWriter) FindByOwner(ctx context.Context, owner kube.ObjectRef) ([]v1alpha1.ExposedSecretReport, error) { + var list v1alpha1.ExposedSecretReportList + + labels := client.MatchingLabels(kube.ObjectRefToLabels(owner)) + + err := r.List(ctx, &list, labels, client.InNamespace(owner.Namespace)) + if err != nil { + return nil, err + } + + return list.DeepCopy().Items, nil +} + +func (r *readWriter) FindByOwnerInHierarchy(ctx context.Context, owner kube.ObjectRef) ([]v1alpha1.ExposedSecretReport, error) { + reports, err := r.FindByOwner(ctx, owner) + if err != nil { + return nil, err + } + + // no reports found for provided owner, look for reports in related replicaset + if len(reports) == 0 && (owner.Kind == kube.KindDeployment || owner.Kind == kube.KindPod) { + rsName, err := r.RelatedReplicaSetName(ctx, owner) + if err != nil { + return nil, fmt.Errorf("getting replicaset related to %s/%s: %w", owner.Kind, owner.Name, err) + } + reports, err = r.FindByOwner(ctx, kube.ObjectRef{ + Kind: kube.KindReplicaSet, + Name: rsName, + Namespace: owner.Namespace, + }) + if err != nil { + return nil, err + } + } + + return reports, nil +} diff --git a/pkg/operator/operator.go b/pkg/operator/operator.go index f82c7006e..e360526fc 100644 --- a/pkg/operator/operator.go +++ b/pkg/operator/operator.go @@ -6,6 +6,7 @@ import ( "github.com/aquasecurity/trivy-operator/pkg/compliance" "github.com/aquasecurity/trivy-operator/pkg/configauditreport" + "github.com/aquasecurity/trivy-operator/pkg/exposedsecretreport" "github.com/aquasecurity/trivy-operator/pkg/ext" "github.com/aquasecurity/trivy-operator/pkg/kube" "github.com/aquasecurity/trivy-operator/pkg/metrics" @@ -140,17 +141,18 @@ func Start(ctx context.Context, buildInfo trivyoperator.BuildInfo, operatorConfi } if err = (&vulnerabilityreport.WorkloadController{ - Logger: ctrl.Log.WithName("reconciler").WithName("vulnerabilityreport"), - Config: operatorConfig, - ConfigData: trivyOperatorConfig, - Client: mgr.GetClient(), - ObjectResolver: objectResolver, - LimitChecker: limitChecker, - LogsReader: logsReader, - SecretsReader: secretsReader, - Plugin: plugin, - PluginContext: pluginContext, - ReadWriter: vulnerabilityreport.NewReadWriter(mgr.GetClient()), + Logger: ctrl.Log.WithName("reconciler").WithName("vulnerabilityreport"), + Config: operatorConfig, + ConfigData: trivyOperatorConfig, + Client: mgr.GetClient(), + ObjectResolver: objectResolver, + LimitChecker: limitChecker, + LogsReader: logsReader, + SecretsReader: secretsReader, + Plugin: plugin, + PluginContext: pluginContext, + ReadWriter: vulnerabilityreport.NewReadWriter(mgr.GetClient()), + ExposedSecretReadWriter: exposedsecretreport.NewReadWriter(mgr.GetClient()), }).SetupWithManager(mgr); err != nil { return fmt.Errorf("unable to setup vulnerabilityreport reconciler: %w", err) } diff --git a/pkg/plugin/trivy/model.go b/pkg/plugin/trivy/model.go index 37fc2da8e..5c566cb7b 100644 --- a/pkg/plugin/trivy/model.go +++ b/pkg/plugin/trivy/model.go @@ -7,6 +7,7 @@ import ( type ScanResult struct { Target string `json:"Target"` Vulnerabilities []Vulnerability `json:"Vulnerabilities"` + Secrets []Secret `json:"Secrets"` } type ScanReport struct { @@ -36,3 +37,12 @@ type Layer struct { Digest string `json:"Digest"` DiffID string `json:"DiffID"` } + +type Secret struct { + Target string `json:"Target"` + RuleID string `json:"RuleID"` + Category string `json:"Category"` + Severity v1alpha1.Severity `json:"Severity"` + Title string `json:"Title"` + Match string `json:"Match"` +} diff --git a/pkg/plugin/trivy/plugin.go b/pkg/plugin/trivy/plugin.go index ead0783b8..9fdef1477 100644 --- a/pkg/plugin/trivy/plugin.go +++ b/pkg/plugin/trivy/plugin.go @@ -1232,17 +1232,22 @@ func (p *plugin) appendTrivyNonSSLEnv(config Config, image string, env []corev1. return env, nil } -func (p *plugin) ParseVulnerabilityReportData(ctx trivyoperator.PluginContext, imageRef string, logsReader io.ReadCloser) (v1alpha1.VulnerabilityReportData, error) { +func (p *plugin) ParseReportData(ctx trivyoperator.PluginContext, imageRef string, logsReader io.ReadCloser) (v1alpha1.VulnerabilityReportData, v1alpha1.ExposedSecretReportData, error) { + var vulnReport v1alpha1.VulnerabilityReportData + var secretReport v1alpha1.ExposedSecretReportData + config, err := p.newConfigFrom(ctx) if err != nil { - return v1alpha1.VulnerabilityReportData{}, err + return vulnReport, secretReport, err } var reports ScanReport err = json.NewDecoder(logsReader).Decode(&reports) if err != nil { - return v1alpha1.VulnerabilityReportData{}, err + return vulnReport, secretReport, err } + vulnerabilities := make([]v1alpha1.Vulnerability, 0) + secrets := make([]v1alpha1.ExposedSecret, 0) for _, report := range reports.Results { for _, sr := range report.Vulnerabilities { @@ -1259,35 +1264,58 @@ func (p *plugin) ParseVulnerabilityReportData(ctx trivyoperator.PluginContext, i Target: report.Target, }) } + + for _, sr := range report.Secrets { + secrets = append(secrets, v1alpha1.ExposedSecret{ + Target: sr.Target, + RuleID: sr.RuleID, + Title: sr.Title, + Severity: sr.Severity, + Category: sr.Category, + Match: sr.Match, + }) + } } registry, artifact, err := p.parseImageRef(imageRef) if err != nil { - return v1alpha1.VulnerabilityReportData{}, err + return vulnReport, secretReport, err } trivyImageRef, err := config.GetImageRef() if err != nil { - return v1alpha1.VulnerabilityReportData{}, err + return vulnReport, secretReport, err } version, err := trivyoperator.GetVersionFromImageRef(trivyImageRef) if err != nil { - return v1alpha1.VulnerabilityReportData{}, err + return vulnReport, secretReport, err } return v1alpha1.VulnerabilityReportData{ - UpdateTimestamp: metav1.NewTime(p.clock.Now()), - Scanner: v1alpha1.Scanner{ - Name: v1alpha1.ScannerNameTrivy, - Vendor: "Aqua Security", - Version: version, - }, - Registry: registry, - Artifact: artifact, - Summary: p.toSummary(vulnerabilities), - Vulnerabilities: vulnerabilities, - }, nil + UpdateTimestamp: metav1.NewTime(p.clock.Now()), + Scanner: v1alpha1.Scanner{ + Name: v1alpha1.ScannerNameTrivy, + Vendor: "Aqua Security", + Version: version, + }, + Registry: registry, + Artifact: artifact, + Summary: p.vulnerabilitySummary(vulnerabilities), + Vulnerabilities: vulnerabilities, + }, v1alpha1.ExposedSecretReportData{ + UpdateTimestamp: metav1.NewTime(p.clock.Now()), + Scanner: v1alpha1.Scanner{ + Name: v1alpha1.ScannerNameTrivy, + Vendor: "Aqua Security", + Version: version, + }, + Registry: registry, + Artifact: artifact, + Summary: p.secretSummary(secrets), + Secrets: secrets, + }, nil + } func (p *plugin) newConfigFrom(ctx trivyoperator.PluginContext) (Config, error) { @@ -1298,7 +1326,7 @@ func (p *plugin) newConfigFrom(ctx trivyoperator.PluginContext) (Config, error) return Config{PluginConfig: pluginConfig}, nil } -func (p *plugin) toSummary(vulnerabilities []v1alpha1.Vulnerability) v1alpha1.VulnerabilitySummary { +func (p *plugin) vulnerabilitySummary(vulnerabilities []v1alpha1.Vulnerability) v1alpha1.VulnerabilitySummary { var vs v1alpha1.VulnerabilitySummary for _, v := range vulnerabilities { switch v.Severity { @@ -1317,6 +1345,25 @@ func (p *plugin) toSummary(vulnerabilities []v1alpha1.Vulnerability) v1alpha1.Vu return vs } +func (p *plugin) secretSummary(secrets []v1alpha1.ExposedSecret) v1alpha1.ExposedSecretSummary { + var s v1alpha1.ExposedSecretSummary + for _, v := range secrets { + switch v.Severity { + case v1alpha1.SeverityCritical: + s.CriticalCount++ + case v1alpha1.SeverityHigh: + s.HighCount++ + case v1alpha1.SeverityMedium: + s.MediumCount++ + case v1alpha1.SeverityLow: + s.LowCount++ + default: + s.UnknownCount++ + } + } + return s +} + func (p *plugin) parseImageRef(imageRef string) (v1alpha1.Registry, v1alpha1.Artifact, error) { ref, err := name.ParseReference(imageRef) if err != nil { diff --git a/pkg/plugin/trivy/plugin_test.go b/pkg/plugin/trivy/plugin_test.go index f9e8ec2fa..27c81c918 100644 --- a/pkg/plugin/trivy/plugin_test.go +++ b/pkg/plugin/trivy/plugin_test.go @@ -3574,7 +3574,7 @@ var ( } ) -func TestPlugin_ParseVulnerabilityReportData(t *testing.T) { +func TestPlugin_ParseReportData(t *testing.T) { config := &corev1.ConfigMap{ ObjectMeta: metav1.ObjectMeta{ Name: "trivy-operator-trivy-config", @@ -3647,7 +3647,7 @@ func TestPlugin_ParseVulnerabilityReportData(t *testing.T) { WithClient(fakeClient). Get() instance := trivy.NewPlugin(fixedClock, ext.NewSimpleIDGenerator(), fakeClient) - report, err := instance.ParseVulnerabilityReportData(ctx, tc.imageRef, io.NopCloser(strings.NewReader(tc.input))) + report, _, err := instance.ParseReportData(ctx, tc.imageRef, io.NopCloser(strings.NewReader(tc.input))) switch { case tc.expectedError == nil: require.NoError(t, err) diff --git a/pkg/vulnerabilityreport/builder_test.go b/pkg/vulnerabilityreport/builder_test.go index 02f3a03aa..e36f00480 100644 --- a/pkg/vulnerabilityreport/builder_test.go +++ b/pkg/vulnerabilityreport/builder_test.go @@ -225,6 +225,6 @@ func (p *testPlugin) GetScanJobSpec(_ trivyoperator.PluginContext, _ client.Obje return corev1.PodSpec{}, nil, nil } -func (p *testPlugin) ParseVulnerabilityReportData(_ trivyoperator.PluginContext, _ string, _ io.ReadCloser) (v1alpha1.VulnerabilityReportData, error) { - return v1alpha1.VulnerabilityReportData{}, nil +func (p *testPlugin) ParseReportData(_ trivyoperator.PluginContext, _ string, _ io.ReadCloser) (v1alpha1.VulnerabilityReportData, v1alpha1.ExposedSecretReportData, error) { + return v1alpha1.VulnerabilityReportData{}, v1alpha1.ExposedSecretReportData{}, nil } diff --git a/pkg/vulnerabilityreport/controller.go b/pkg/vulnerabilityreport/controller.go index 671e5af7d..b5df1131f 100644 --- a/pkg/vulnerabilityreport/controller.go +++ b/pkg/vulnerabilityreport/controller.go @@ -9,6 +9,7 @@ import ( "reflect" "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1" + "github.com/aquasecurity/trivy-operator/pkg/exposedsecretreport" "github.com/aquasecurity/trivy-operator/pkg/kube" "github.com/aquasecurity/trivy-operator/pkg/operator/controller" "github.com/aquasecurity/trivy-operator/pkg/operator/etc" @@ -43,6 +44,7 @@ type WorkloadController struct { trivyoperator.PluginContext ReadWriter trivyoperator.ConfigData + ExposedSecretReadWriter exposedsecretreport.ReadWriter } func (r *WorkloadController) SetupWithManager(mgr ctrl.Manager) error { @@ -369,6 +371,7 @@ func (r *WorkloadController) processCompleteScanJob(ctx context.Context, job *ba } var vulnerabilityReports []v1alpha1.VulnerabilityReport + var secretReports []v1alpha1.ExposedSecretReport for containerName, containerImage := range containerImages { logsStream, err := r.LogsReader.GetLogsByJobAndContainerName(ctx, job, containerName) @@ -383,16 +386,17 @@ func (r *WorkloadController) processCompleteScanJob(ctx context.Context, job *ba } return fmt.Errorf("getting logs for pod %q: %w", job.Namespace+"/"+job.Name, err) } - reportData, err := r.Plugin.ParseVulnerabilityReportData(r.PluginContext, containerImage, logsStream) + vulnReportData, secretReportData, err := r.Plugin.ParseReportData(r.PluginContext, containerImage, logsStream) if err != nil { return err } + _ = logsStream.Close() reportBuilder := NewReportBuilder(r.Client.Scheme()). Controller(owner). Container(containerName). - Data(reportData). + Data(vulnReportData). PodSpecHash(podSpecHash) if r.Config.VulnerabilityScannerReportTTL != nil { @@ -400,12 +404,26 @@ func (r *WorkloadController) processCompleteScanJob(ctx context.Context, job *ba } report, err := reportBuilder.Get() + if err != nil { + return err + } + // TODO: this should built a secret report + secretReportBuilder := exposedsecretreport.NewReportBuilder(r.Client.Scheme()). + Controller(owner). + Container(containerName). + Data(secretReportData). + PodSpecHash(podSpecHash) + + // TODO: need to add ReportTTL? + + secretReport, err := secretReportBuilder.Get() if err != nil { return err } vulnerabilityReports = append(vulnerabilityReports, report) + secretReports = append(secretReports, secretReport) } err = r.ReadWriter.Write(ctx, vulnerabilityReports) @@ -413,6 +431,11 @@ func (r *WorkloadController) processCompleteScanJob(ctx context.Context, job *ba return err } + err = r.ExposedSecretReadWriter.Write(ctx, secretReports) + if err != nil { + return err + } + log.V(1).Info("Deleting complete scan job", "owner", owner) return r.deleteJob(ctx, job) } diff --git a/pkg/vulnerabilityreport/plugin.go b/pkg/vulnerabilityreport/plugin.go index d0d3d014e..992d07442 100644 --- a/pkg/vulnerabilityreport/plugin.go +++ b/pkg/vulnerabilityreport/plugin.go @@ -27,8 +27,8 @@ type Plugin interface { GetScanJobSpec(ctx trivyoperator.PluginContext, workload client.Object, credentials map[string]docker.Auth) ( corev1.PodSpec, []*corev1.Secret, error) - // ParseVulnerabilityReportData is a callback to parse and convert logs of + // ParseReportData is a callback to parse and convert logs of // the pod controlled by the scan job to v1alpha1.VulnerabilityScanResult. - ParseVulnerabilityReportData(ctx trivyoperator.PluginContext, imageRef string, logsReader io.ReadCloser) ( - v1alpha1.VulnerabilityReportData, error) + ParseReportData(ctx trivyoperator.PluginContext, imageRef string, logsReader io.ReadCloser) ( + v1alpha1.VulnerabilityReportData, v1alpha1.ExposedSecretReportData, error) }