Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: add vulnerabilityreports controller #312

Merged
merged 4 commits into from
Nov 7, 2024
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions cmd/agent/kubernetes.go
Original file line number Diff line number Diff line change
@@ -6,6 +6,7 @@ import (
"os"
"strings"

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

vulnerabilityReportController := &controller.VulnerabilityReportReconciler{
Client: manager.GetClient(),
Scheme: manager.GetScheme(),
ConsoleClient: extConsoleClient,
Ctx: ctx,
}

reconcileGroups := map[schema.GroupVersionKind]controller.SetupWithManager{
{
Group: velerov1.SchemeGroupVersion.Group,
@@ -152,6 +160,11 @@ func registerKubeReconcilersOrDie(
Version: rolloutv1alpha1.SchemeGroupVersion.Version,
Kind: rollouts.RolloutKind,
}: argoRolloutController.SetupWithManager,
{
Group: trivy.SchemeGroupVersion.Group,
Version: trivy.SchemeGroupVersion.Version,
Kind: "VulnerabilityReport",
}: vulnerabilityReportController.SetupWithManager,
}

if err := (&controller.CrdRegisterControllerReconciler{
2 changes: 2 additions & 0 deletions cmd/agent/main.go
Original file line number Diff line number Diff line change
@@ -5,6 +5,7 @@ import (
"os"
"time"

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

func init() {
utilruntime.Must(trivy.AddToScheme(scheme))
utilruntime.Must(clientgoscheme.AddToScheme(scheme))
utilruntime.Must(deploymentsv1alpha1.AddToScheme(scheme))
utilruntime.Must(velerov1.AddToScheme(scheme))
99 changes: 88 additions & 11 deletions go.mod

Large diffs are not rendered by default.

1,080 changes: 1,041 additions & 39 deletions go.sum

Large diffs are not rendered by default.

202 changes: 202 additions & 0 deletions internal/controller/vulnerabilityreports_controller.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
package controller

import (
"context"
"fmt"
"strings"
"time"

trivy "github.com/aquasecurity/trivy-operator/pkg/apis/aquasecurity/v1alpha1"
cmap "github.com/orcaman/concurrent-map/v2"
console "github.com/pluralsh/console/go/client"
"github.com/pluralsh/deployment-operator/internal/helpers"
"github.com/pluralsh/deployment-operator/pkg/client"
"github.com/pluralsh/polly/algorithms"
"github.com/samber/lo"
v1 "k8s.io/apimachinery/pkg/apis/meta/v1"
"k8s.io/apimachinery/pkg/apis/meta/v1/unstructured"
"k8s.io/apimachinery/pkg/runtime"
"k8s.io/apimachinery/pkg/runtime/schema"
"sigs.k8s.io/cli-utils/pkg/inventory"
ctrl "sigs.k8s.io/controller-runtime"
k8sClient "sigs.k8s.io/controller-runtime/pkg/client"
"sigs.k8s.io/controller-runtime/pkg/log"
)

// VulnerabilityReportReconciler reconciles a Trivy VulnerabilityReport resource.
type VulnerabilityReportReconciler struct {
k8sClient.Client
Scheme *runtime.Scheme
ConsoleClient client.Client
Ctx context.Context
reports cmap.ConcurrentMap[string, console.VulnerabilityReportAttributes]
}

func (r *VulnerabilityReportReconciler) Reconcile(ctx context.Context, req ctrl.Request) (ctrl.Result, error) {
logger := log.FromContext(ctx)

vulnerabilityReport := &trivy.VulnerabilityReport{}
if err := r.Get(ctx, req.NamespacedName, vulnerabilityReport); err != nil {
logger.Error(err, "unable to fetch rollout")
return ctrl.Result{}, k8sClient.IgnoreNotFound(err)
}

if !vulnerabilityReport.DeletionTimestamp.IsZero() {
return ctrl.Result{}, nil
}

if len(vulnerabilityReport.OwnerReferences) > 0 {
k8sObj, err := r.getObjectFromOwnerReference(ctx, vulnerabilityReport.OwnerReferences[0], vulnerabilityReport.Namespace)
if err != nil {
return ctrl.Result{}, err
}
serviceID, ok := k8sObj.GetAnnotations()[inventory.OwningInventoryKey]
if !ok {
return ctrl.Result{}, nil
}

r.reports.Set(req.NamespacedName.String(), createAttribute(vulnerabilityReport, serviceID))

}
return ctrl.Result{}, nil
}

func createAttribute(vulnerabilityReport *trivy.VulnerabilityReport, serviceID string) console.VulnerabilityReportAttributes {
var namespaces []*console.NamespaceVulnAttributes
var vulnerabilityAttributes []*console.VulnerabilityAttributes
os := &console.VulnOsAttributes{
Eosl: lo.ToPtr(vulnerabilityReport.Report.OS.Eosl),
Family: lo.ToPtr(string(vulnerabilityReport.Report.OS.Family)),
Name: lo.ToPtr(vulnerabilityReport.Report.OS.Name),
}
summary := &console.VulnSummaryAttributes{
CriticalCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.CriticalCount)),
HighCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.HighCount)),
MediumCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.MediumCount)),
LowCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.LowCount)),
UnknownCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.UnknownCount)),
NoneCount: lo.ToPtr(int64(vulnerabilityReport.Report.Summary.NoneCount)),
}
artifact := &console.VulnArtifactAttributes{
Registry: lo.ToPtr(vulnerabilityReport.Report.Registry.Server),
Repository: lo.ToPtr(vulnerabilityReport.Report.Artifact.Repository),
Digest: lo.ToPtr(vulnerabilityReport.Report.Artifact.Digest),
Tag: lo.ToPtr(vulnerabilityReport.Report.Artifact.Tag),
Mime: lo.ToPtr(vulnerabilityReport.Report.Artifact.MimeType),
}
format := "%s/%s:%s"
tag := vulnerabilityReport.Report.Artifact.Tag
if tag == "" {
tag = vulnerabilityReport.Report.Artifact.Digest
format = "%s/%s@%s"
}
artifactURL := fmt.Sprintf(format, vulnerabilityReport.Report.Registry.Server, vulnerabilityReport.Report.Artifact.Repository, tag)
services := []*console.ServiceVulnAttributes{
{
ServiceID: serviceID,
},
}
if vulnerabilityReport.Namespace != "" {
namespaces = []*console.NamespaceVulnAttributes{
{
Namespace: vulnerabilityReport.Namespace,
},
}
}

for _, v := range vulnerabilityReport.Report.Vulnerabilities {
vulnerabilityAttr := &console.VulnerabilityAttributes{
Resource: lo.ToPtr(v.Resource),
FixedVersion: lo.ToPtr(v.FixedVersion),
InstalledVersion: lo.ToPtr(v.InstalledVersion),
Severity: lo.ToPtr(console.VulnSeverity(v.Severity)),
Score: v.Score,
Title: lo.ToPtr(v.Title),
Description: lo.ToPtr(v.Description),
CvssSource: lo.ToPtr(v.CVSSSource),
PrimaryLink: lo.ToPtr(v.PrimaryLink),
Links: algorithms.Map(v.Links, func(s string) *string { return &s }),
Target: lo.ToPtr(v.Target),
Class: lo.ToPtr(v.Class),
PackageType: lo.ToPtr(v.PackageType),
PkgPath: lo.ToPtr(v.PkgPath),
}
if v.PublishedDate != "" {
vulnerabilityAttr.PublishedDate = lo.ToPtr(v.PublishedDate)
}
if v.LastModifiedDate != "" {
vulnerabilityAttr.LastModifiedDate = lo.ToPtr(v.LastModifiedDate)
}
vulnerabilityAttributes = append(vulnerabilityAttributes, vulnerabilityAttr)
}

return console.VulnerabilityReportAttributes{
ArtifactURL: &artifactURL,
Os: os,
Summary: summary,
Artifact: artifact,
Vulnerabilities: vulnerabilityAttributes,
Services: services,
Namespaces: namespaces,
}
}

func (r *VulnerabilityReportReconciler) getObjectFromOwnerReference(ctx context.Context, ref v1.OwnerReference, namespace string) (*unstructured.Unstructured, error) {
gv, err := apiVersionToGroupVersion(ref.APIVersion)
if err != nil {
return nil, err
}
gvk := schema.GroupVersionKind{
Group: gv.Group,
Kind: ref.Kind,
Version: gv.Version,
}
obj := &unstructured.Unstructured{}
obj.SetGroupVersionKind(gvk)
if err := r.Get(ctx, k8sClient.ObjectKey{Name: ref.Name, Namespace: namespace}, obj); err != nil {
return nil, err
}
if ref.Kind == "ReplicaSet" {
// Get Deployment from ReplicaSet
if len(obj.GetOwnerReferences()) > 0 {
return r.getObjectFromOwnerReference(ctx, obj.GetOwnerReferences()[0], namespace)
}
}

return obj, nil
}

func (r *VulnerabilityReportReconciler) SetupWithManager(mgr ctrl.Manager) error {
logger := log.FromContext(r.Ctx)
r.reports = cmap.New[console.VulnerabilityReportAttributes]()
err := helpers.BackgroundPollUntilContextCancel(r.Ctx, 10*time.Minute, false, true, func(_ context.Context) (done bool, err error) {
if !r.reports.IsEmpty() {
apiReports := algorithms.Map(lo.Values(r.reports.Items()), func(s console.VulnerabilityReportAttributes) *console.VulnerabilityReportAttributes { return &s })
if _, err := r.ConsoleClient.UpsertVulnerabilityReports(apiReports); err != nil {
logger.Error(err, "unable to upsert vulnerability reports")
} else {
logger.Info("upsert vulnerability reports")
r.reports.Clear()
}
}
return false, nil
})
if err != nil {
return err
}

return ctrl.NewControllerManagedBy(mgr).
For(&trivy.VulnerabilityReport{}).
Complete(r)
}

func apiVersionToGroupVersion(apiVersion string) (schema.GroupVersion, error) {
parts := strings.Split(apiVersion, "/")
if len(parts) == 1 {
// If there's no group specified, it's the "core" group, e.g., "v1"
return schema.GroupVersion{Group: "", Version: parts[0]}, nil
} else if len(parts) == 2 {
return schema.GroupVersion{Group: parts[0], Version: parts[1]}, nil
}
return schema.GroupVersion{}, fmt.Errorf("invalid apiVersion: %s", apiVersion)
}
1 change: 1 addition & 0 deletions pkg/client/console.go
Original file line number Diff line number Diff line change
@@ -70,4 +70,5 @@ type Client interface {
GetUser(email string) (*console.UserFragment, error)
GetGroup(name string) (*console.GroupFragment, error)
SaveUpgradeInsights(attributes []*console.UpgradeInsightAttributes) (*console.SaveUpgradeInsights, error)
UpsertVulnerabilityReports(vulnerabilities []*console.VulnerabilityReportAttributes) (*console.UpsertVulnerabilities, error)
}
7 changes: 7 additions & 0 deletions pkg/client/vulnerability.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package client

import console "github.com/pluralsh/console/go/client"

func (c *client) UpsertVulnerabilityReports(vulnerabilities []*console.VulnerabilityReportAttributes) (*console.UpsertVulnerabilities, error) {
return c.consoleClient.UpsertVulnerabilities(c.ctx, vulnerabilities)
}
58 changes: 58 additions & 0 deletions pkg/test/mocks/Client_mock.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.