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
Show file tree
Hide file tree
Changes from 3 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
Expand Up @@ -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"
Expand Down Expand Up @@ -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,
Expand All @@ -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{
Expand Down
2 changes: 2 additions & 0 deletions cmd/agent/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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))
Expand Down
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, 1*time.Minute, false, true, func(_ context.Context) (done bool, err error) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

make this poll interval 10 minutes i think

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

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
Expand Up @@ -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.

Loading