Skip to content

Commit 06f048c

Browse files
authored
feat: add vulnerabilityreports controller (#312)
* add vulnerabilityreports controller * send reports in batch * add logs * increase poll interval
1 parent 47e45e6 commit 06f048c

File tree

8 files changed

+1412
-50
lines changed

8 files changed

+1412
-50
lines changed

cmd/agent/kubernetes.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"os"
77
"strings"
88

9+
trivy "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
910
"github.com/argoproj/argo-rollouts/pkg/apis/rollouts"
1011
rolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
1112
roclientset "github.com/argoproj/argo-rollouts/pkg/client/clientset/versioned"
@@ -131,6 +132,13 @@ func registerKubeReconcilersOrDie(
131132
KubeClient: kubeClient,
132133
}
133134

135+
vulnerabilityReportController := &controller.VulnerabilityReportReconciler{
136+
Client: manager.GetClient(),
137+
Scheme: manager.GetScheme(),
138+
ConsoleClient: extConsoleClient,
139+
Ctx: ctx,
140+
}
141+
134142
reconcileGroups := map[schema.GroupVersionKind]controller.SetupWithManager{
135143
{
136144
Group: velerov1.SchemeGroupVersion.Group,
@@ -152,6 +160,11 @@ func registerKubeReconcilersOrDie(
152160
Version: rolloutv1alpha1.SchemeGroupVersion.Version,
153161
Kind: rollouts.RolloutKind,
154162
}: argoRolloutController.SetupWithManager,
163+
{
164+
Group: trivy.SchemeGroupVersion.Group,
165+
Version: trivy.SchemeGroupVersion.Version,
166+
Kind: "VulnerabilityReport",
167+
}: vulnerabilityReportController.SetupWithManager,
155168
}
156169

157170
if err := (&controller.CrdRegisterControllerReconciler{

cmd/agent/main.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"os"
66
"time"
77

8+
trivy "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
89
rolloutv1alpha1 "github.com/argoproj/argo-rollouts/pkg/apis/rollouts/v1alpha1"
910
templatesv1 "github.com/open-policy-agent/frameworks/constraint/pkg/apis/templates/v1"
1011
constraintstatusv1beta1 "github.com/open-policy-agent/gatekeeper/v3/apis/status/v1beta1"
@@ -31,6 +32,7 @@ var (
3132
)
3233

3334
func init() {
35+
utilruntime.Must(trivy.AddToScheme(scheme))
3436
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
3537
utilruntime.Must(deploymentsv1alpha1.AddToScheme(scheme))
3638
utilruntime.Must(velerov1.AddToScheme(scheme))

go.mod

Lines changed: 88 additions & 11 deletions
Large diffs are not rendered by default.

go.sum

Lines changed: 1041 additions & 39 deletions
Large diffs are not rendered by default.
Lines changed: 202 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,202 @@
1+
package controller
2+
3+
import (
4+
"context"
5+
"fmt"
6+
"strings"
7+
"time"
8+
9+
trivy "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
10+
cmap "github.com/orcaman/concurrent-map/v2"
11+
console "github.com/pluralsh/console/go/client"
12+
"github.com/pluralsh/deployment-operator/internal/helpers"
13+
"github.com/pluralsh/deployment-operator/pkg/client"
14+
"github.com/pluralsh/polly/algorithms"
15+
"github.com/samber/lo"
16+
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
17+
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
18+
"k8s.io/apimachinery/pkg/runtime"
19+
"k8s.io/apimachinery/pkg/runtime/schema"
20+
"sigs.k8s.io/cli-utils/pkg/inventory"
21+
ctrl "sigs.k8s.io/controller-runtime"
22+
k8sClient "sigs.k8s.io/controller-runtime/pkg/client"
23+
"sigs.k8s.io/controller-runtime/pkg/log"
24+
)
25+
26+
// VulnerabilityReportReconciler reconciles a Trivy VulnerabilityReport resource.
27+
type VulnerabilityReportReconciler struct {
28+
k8sClient.Client
29+
Scheme *runtime.Scheme
30+
ConsoleClient client.Client
31+
Ctx context.Context
32+
reports cmap.ConcurrentMap[string, console.VulnerabilityReportAttributes]
33+
}
34+
35+
func (r *VulnerabilityReportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
36+
logger := log.FromContext(ctx)
37+
38+
vulnerabilityReport := &trivy.VulnerabilityReport{}
39+
if err := r.Get(ctx, req.NamespacedName, vulnerabilityReport); err != nil {
40+
logger.Error(err, "unable to fetch rollout")
41+
return ctrl.Result{}, k8sClient.IgnoreNotFound(err)
42+
}
43+
44+
if !vulnerabilityReport.DeletionTimestamp.IsZero() {
45+
return ctrl.Result{}, nil
46+
}
47+
48+
if len(vulnerabilityReport.OwnerReferences) > 0 {
49+
k8sObj, err := r.getObjectFromOwnerReference(ctx, vulnerabilityReport.OwnerReferences[0], vulnerabilityReport.Namespace)
50+
if err != nil {
51+
return ctrl.Result{}, err
52+
}
53+
serviceID, ok := k8sObj.GetAnnotations()[inventory.OwningInventoryKey]
54+
if !ok {
55+
return ctrl.Result{}, nil
56+
}
57+
58+
r.reports.Set(req.NamespacedName.String(), createAttribute(vulnerabilityReport, serviceID))
59+
60+
}
61+
return ctrl.Result{}, nil
62+
}
63+
64+
func createAttribute(vulnerabilityReport *trivy.VulnerabilityReport, serviceID string) console.VulnerabilityReportAttributes {
65+
var namespaces []*console.NamespaceVulnAttributes
66+
var vulnerabilityAttributes []*console.VulnerabilityAttributes
67+
os := &console.VulnOsAttributes{
68+
Eosl: lo.ToPtr(vulnerabilityReport.Report.OS.Eosl),
69+
Family: lo.ToPtr(string(vulnerabilityReport.Report.OS.Family)),
70+
Name: lo.ToPtr(vulnerabilityReport.Report.OS.Name),
71+
}
72+
summary := &console.VulnSummaryAttributes{
73+
CriticalCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.CriticalCount)),
74+
HighCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.HighCount)),
75+
MediumCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.MediumCount)),
76+
LowCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.LowCount)),
77+
UnknownCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.UnknownCount)),
78+
NoneCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.NoneCount)),
79+
}
80+
artifact := &console.VulnArtifactAttributes{
81+
Registry: lo.ToPtr(vulnerabilityReport.Report.Registry.Server),
82+
Repository: lo.ToPtr(vulnerabilityReport.Report.Artifact.Repository),
83+
Digest: lo.ToPtr(vulnerabilityReport.Report.Artifact.Digest),
84+
Tag: lo.ToPtr(vulnerabilityReport.Report.Artifact.Tag),
85+
Mime: lo.ToPtr(vulnerabilityReport.Report.Artifact.MimeType),
86+
}
87+
format := "%s/%s:%s"
88+
tag := vulnerabilityReport.Report.Artifact.Tag
89+
if tag == "" {
90+
tag = vulnerabilityReport.Report.Artifact.Digest
91+
format = "%s/%s@%s"
92+
}
93+
artifactURL := fmt.Sprintf(format, vulnerabilityReport.Report.Registry.Server, vulnerabilityReport.Report.Artifact.Repository, tag)
94+
services := []*console.ServiceVulnAttributes{
95+
{
96+
ServiceID: serviceID,
97+
},
98+
}
99+
if vulnerabilityReport.Namespace != "" {
100+
namespaces = []*console.NamespaceVulnAttributes{
101+
{
102+
Namespace: vulnerabilityReport.Namespace,
103+
},
104+
}
105+
}
106+
107+
for _, v := range vulnerabilityReport.Report.Vulnerabilities {
108+
vulnerabilityAttr := &console.VulnerabilityAttributes{
109+
Resource: lo.ToPtr(v.Resource),
110+
FixedVersion: lo.ToPtr(v.FixedVersion),
111+
InstalledVersion: lo.ToPtr(v.InstalledVersion),
112+
Severity: lo.ToPtr(console.VulnSeverity(v.Severity)),
113+
Score: v.Score,
114+
Title: lo.ToPtr(v.Title),
115+
Description: lo.ToPtr(v.Description),
116+
CvssSource: lo.ToPtr(v.CVSSSource),
117+
PrimaryLink: lo.ToPtr(v.PrimaryLink),
118+
Links: algorithms.Map(v.Links, func(s string) *string { return &s }),
119+
Target: lo.ToPtr(v.Target),
120+
Class: lo.ToPtr(v.Class),
121+
PackageType: lo.ToPtr(v.PackageType),
122+
PkgPath: lo.ToPtr(v.PkgPath),
123+
}
124+
if v.PublishedDate != "" {
125+
vulnerabilityAttr.PublishedDate = lo.ToPtr(v.PublishedDate)
126+
}
127+
if v.LastModifiedDate != "" {
128+
vulnerabilityAttr.LastModifiedDate = lo.ToPtr(v.LastModifiedDate)
129+
}
130+
vulnerabilityAttributes = append(vulnerabilityAttributes, vulnerabilityAttr)
131+
}
132+
133+
return console.VulnerabilityReportAttributes{
134+
ArtifactURL: &artifactURL,
135+
Os: os,
136+
Summary: summary,
137+
Artifact: artifact,
138+
Vulnerabilities: vulnerabilityAttributes,
139+
Services: services,
140+
Namespaces: namespaces,
141+
}
142+
}
143+
144+
func (r *VulnerabilityReportReconciler) getObjectFromOwnerReference(ctx context.Context, ref v1.OwnerReference, namespace string) (*unstructured.Unstructured, error) {
145+
gv, err := apiVersionToGroupVersion(ref.APIVersion)
146+
if err != nil {
147+
return nil, err
148+
}
149+
gvk := schema.GroupVersionKind{
150+
Group: gv.Group,
151+
Kind: ref.Kind,
152+
Version: gv.Version,
153+
}
154+
obj := &unstructured.Unstructured{}
155+
obj.SetGroupVersionKind(gvk)
156+
if err := r.Get(ctx, k8sClient.ObjectKey{Name: ref.Name, Namespace: namespace}, obj); err != nil {
157+
return nil, err
158+
}
159+
if ref.Kind == "ReplicaSet" {
160+
// Get Deployment from ReplicaSet
161+
if len(obj.GetOwnerReferences()) > 0 {
162+
return r.getObjectFromOwnerReference(ctx, obj.GetOwnerReferences()[0], namespace)
163+
}
164+
}
165+
166+
return obj, nil
167+
}
168+
169+
func (r *VulnerabilityReportReconciler) SetupWithManager(mgr ctrl.Manager) error {
170+
logger := log.FromContext(r.Ctx)
171+
r.reports = cmap.New[console.VulnerabilityReportAttributes]()
172+
err := helpers.BackgroundPollUntilContextCancel(r.Ctx, 10*time.Minute, false, true, func(_ context.Context) (done bool, err error) {
173+
if !r.reports.IsEmpty() {
174+
apiReports := algorithms.Map(lo.Values(r.reports.Items()), func(s console.VulnerabilityReportAttributes) *console.VulnerabilityReportAttributes { return &s })
175+
if _, err := r.ConsoleClient.UpsertVulnerabilityReports(apiReports); err != nil {
176+
logger.Error(err, "unable to upsert vulnerability reports")
177+
} else {
178+
logger.Info("upsert vulnerability reports")
179+
r.reports.Clear()
180+
}
181+
}
182+
return false, nil
183+
})
184+
if err != nil {
185+
return err
186+
}
187+
188+
return ctrl.NewControllerManagedBy(mgr).
189+
For(&trivy.VulnerabilityReport{}).
190+
Complete(r)
191+
}
192+
193+
func apiVersionToGroupVersion(apiVersion string) (schema.GroupVersion, error) {
194+
parts := strings.Split(apiVersion, "/")
195+
if len(parts) == 1 {
196+
// If there's no group specified, it's the "core" group, e.g., "v1"
197+
return schema.GroupVersion{Group: "", Version: parts[0]}, nil
198+
} else if len(parts) == 2 {
199+
return schema.GroupVersion{Group: parts[0], Version: parts[1]}, nil
200+
}
201+
return schema.GroupVersion{}, fmt.Errorf("invalid apiVersion: %s", apiVersion)
202+
}

pkg/client/console.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,4 +70,5 @@ type Client interface {
7070
GetUser(email string) (*console.UserFragment, error)
7171
GetGroup(name string) (*console.GroupFragment, error)
7272
SaveUpgradeInsights(attributes []*console.UpgradeInsightAttributes) (*console.SaveUpgradeInsights, error)
73+
UpsertVulnerabilityReports(vulnerabilities []*console.VulnerabilityReportAttributes) (*console.UpsertVulnerabilities, error)
7374
}

pkg/client/vulnerability.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package client
2+
3+
import console "github.com/pluralsh/console/go/client"
4+
5+
func (c *client) UpsertVulnerabilityReports(vulnerabilities []*console.VulnerabilityReportAttributes) (*console.UpsertVulnerabilities, error) {
6+
return c.consoleClient.UpsertVulnerabilities(c.ctx, vulnerabilities)
7+
}

pkg/test/mocks/Client_mock.go

Lines changed: 58 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)