Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: opt-in creating wg-policy PolicyReport
Browse files Browse the repository at this point in the history
erikgb committed Jul 17, 2024
1 parent bb2f535 commit 946b995
Showing 12 changed files with 340 additions and 38 deletions.
1 change: 1 addition & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ func main() {
}

pflag.CommandLine.AddGoFlagSet(flag.CommandLine)
config.DefaultMutableFeatureGate.AddFlag(pflag.CommandLine)
pflag.Parse()

logger := zap.New(zap.UseFlagOptions(&opts))
2 changes: 2 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -17,6 +17,7 @@ require (
k8s.io/api v0.30.2
k8s.io/apimachinery v0.30.2
k8s.io/client-go v0.30.2
k8s.io/component-base v0.30.2
k8s.io/klog/v2 v2.130.1
k8s.io/utils v0.0.0-20240711033017-18e509b52bc8
sigs.k8s.io/cli-utils v0.37.2
@@ -29,6 +30,7 @@ require (

require (
github.com/beorn7/perks v1.0.1 // indirect
github.com/blang/semver/v4 v4.0.0 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/emicklei/go-restful/v3 v3.11.0 // indirect
4 changes: 4 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/blang/semver/v4 v4.0.0 h1:1PFHFE6yCCTv8C1TeyNNarDzntLi7wMI5i/pzqYIsAM=
github.com/blang/semver/v4 v4.0.0/go.mod h1:IbckMUScFkM3pff0VJDNKRiT6TG/YpiHIM2yvyW5YoQ=
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
@@ -231,6 +233,8 @@ k8s.io/apimachinery v0.30.2 h1:fEMcnBj6qkzzPGSVsAZtQThU62SmQ4ZymlXRC5yFSCg=
k8s.io/apimachinery v0.30.2/go.mod h1:iexa2somDaxdnj7bha06bhb43Zpa6eWH8N8dbqVjTUc=
k8s.io/client-go v0.30.2 h1:sBIVJdojUNPDU/jObC+18tXWcTJVcwyqS9diGdWHk50=
k8s.io/client-go v0.30.2/go.mod h1:JglKSWULm9xlJLx4KCkfLLQ7XwtlbflV6uFFSHTMgVs=
k8s.io/component-base v0.30.2 h1:pqGBczYoW1sno8q9ObExUqrYSKhtE5rW3y6gX88GZII=
k8s.io/component-base v0.30.2/go.mod h1:yQLkQDrkK8J6NtP+MGJOws+/PPeEXNpwFixsUI7h/OE=
k8s.io/klog/v2 v2.130.1 h1:n9Xl7H1Xvksem4KFG4PYbdQCQxqc/tTUyrgXaOhHSzk=
k8s.io/klog/v2 v2.130.1/go.mod h1:3Jpz1GvMt720eyJH1ckRHK1EDfpxISzJ7I9OYgaDtPE=
k8s.io/kube-openapi v0.0.0-20240228011516-70dd3763d340 h1:BZqlfIlq5YbRMFko6/PM7FjZpUb45WallggurYhKGag=
19 changes: 19 additions & 0 deletions internal/config/feature/feature.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package feature

import (
"k8s.io/apimachinery/pkg/util/runtime"
"k8s.io/component-base/featuregate"

"github.com/statnett/image-scanner-operator/internal/config"
)

// PolicyReport will ensure PolicyReport resources are created for completed scan jobs.
const PolicyReport featuregate.Feature = "PolicyReport"

func init() {
runtime.Must(config.DefaultMutableFeatureGate.Add(defaultFeatureGates))
}

var defaultFeatureGates = map[featuregate.Feature]featuregate.FeatureSpec{
PolicyReport: {Default: false, PreRelease: featuregate.Alpha},
}
15 changes: 15 additions & 0 deletions internal/config/feature_gate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package config

import (
"k8s.io/component-base/featuregate"
)

var (
// DefaultMutableFeatureGate is a mutable version of DefaultFeatureGate.
// Only top-level commands/options setup should make use of this.
DefaultMutableFeatureGate featuregate.MutableFeatureGate = featuregate.NewFeatureGate()

// DefaultFeatureGate is a shared global FeatureGate.
// Top-level commands/options setup that needs to modify this feature gate should use DefaultMutableFeatureGate.
DefaultFeatureGate featuregate.FeatureGate = DefaultMutableFeatureGate
)
43 changes: 30 additions & 13 deletions internal/controller/stas/containerimagescan_status.go
Original file line number Diff line number Diff line change
@@ -56,24 +56,34 @@ func (p *containerImageScanStatusPatch) withCondition(c *metav1ac.ConditionApply
return p
}

func (p *containerImageScanStatusPatch) withScanJob(job *batchv1.Job) *containerImageScanStatusPatch {
func (p *containerImageScanStatusPatch) withScanJob(job *batchv1.Job, successful bool) *containerImageScanStatusPatch {
now := metav1.Now()

p.patch.Status.
WithLastScanTime(metav1.Now()).
WithLastScanJobUID(job.UID)
WithLastScanJobUID(job.UID).
WithLastScanTime(now)

if successful {
p.patch.Status.
WithLastSuccessfulScanTime(now)
}

return p
}

func (p *containerImageScanStatusPatch) withCompletedScanJob(job *batchv1.Job, vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *containerImageScanStatusPatch {
p.minSeverity = &minSeverity
p.vulnerabilities = vulnerabilities

now := metav1.Now()
func (p *containerImageScanStatusPatch) withSummary(summary *stasv1alpha1.VulnerabilitySummary) *containerImageScanStatusPatch {
p.patch.Status.
WithVulnerabilitySummary(vulnerabilitySummary(vulnerabilities, minSeverity)).
WithLastScanTime(now).
WithLastScanJobUID(job.UID).
WithLastSuccessfulScanTime(now)
WithVulnerabilitySummary(stasv1alpha1ac.VulnerabilitySummary().
WithSeverityCount(summary.SeverityCount).
WithFixedCount(summary.FixedCount).
WithUnfixedCount(summary.UnfixedCount))

return p
}

func (p *containerImageScanStatusPatch) withResults(vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *containerImageScanStatusPatch {
p.vulnerabilities = vulnerabilities
p.minSeverity = &minSeverity

return p
}
@@ -94,11 +104,18 @@ func (p *containerImageScanStatusPatch) apply(ctx context.Context, c client.Clie
var err error
// Repeat until resource fits in api-server by increasing minimum severity on failure.
for severity := *p.minSeverity; severity <= stasv1alpha1.MaxSeverity; severity++ {
p.patch.Status.Vulnerabilities, err = filterVulnerabilities(p.vulnerabilities, severity)
var vulnerabilities []stasv1alpha1.Vulnerability

vulnerabilities, err = filterVulnerabilities(p.vulnerabilities, severity)
if err != nil {
return err
}

p.patch.Status.Vulnerabilities = make([]stasv1alpha1ac.VulnerabilityApplyConfiguration, len(vulnerabilities))
for i, v := range vulnerabilities {
p.patch.Status.Vulnerabilities[i] = *vulnerabilityPatch(v)
}

err = c.Status().Patch(ctx, p.cis, applyPatch{p.patch}, FieldValidationStrict, client.ForceOwnership, fieldOwner)
if !isResourceTooLargeError(err) {
break
134 changes: 134 additions & 0 deletions internal/controller/stas/policy_report.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,134 @@
package stas

import (
"context"
"fmt"
"maps"
"strings"

corev1 "k8s.io/api/core/v1"
"k8s.io/apimachinery/pkg/runtime"
"sigs.k8s.io/controller-runtime/pkg/client"
policyv1alpha2 "sigs.k8s.io/wg-policy-prototypes/policy-report/pkg/api/wgpolicyk8s.io/v1alpha2"

stasv1alpha1 "github.com/statnett/image-scanner-operator/api/stas/v1alpha1"
policyv1alpha2ac "github.com/statnett/image-scanner-operator/internal/wg-policy/applyconfiguration/wgpolicyk8s.io/v1alpha2"
)

func newPolicyReportPatch(cis *stasv1alpha1.ContainerImageScan) *policyReportPatch {
return &policyReportPatch{
cis: cis,
patch: policyv1alpha2ac.PolicyReport(cis.Name, cis.Namespace).
WithScope(
corev1.ObjectReference{
Kind: cis.Spec.Workload.Kind,
Name: cis.Spec.Workload.Name,
},
),
}
}

type policyReportPatch struct {
cis *stasv1alpha1.ContainerImageScan
patch *policyv1alpha2ac.PolicyReportApplyConfiguration
vulnerabilities []stasv1alpha1.Vulnerability
minSeverity stasv1alpha1.Severity
}

func (p *policyReportPatch) withSummary(summary *stasv1alpha1.VulnerabilitySummary) *policyReportPatch {
p.patch.
WithSummary(policyv1alpha2ac.PolicyReportSummary().
WithSkip(int(summary.SeverityCount[stasv1alpha1.SeverityUnknown.String()])).
WithWarn(int(summary.SeverityCount[stasv1alpha1.SeverityLow.String()] + summary.SeverityCount[stasv1alpha1.SeverityMedium.String()])).
WithFail(int(summary.SeverityCount[stasv1alpha1.SeverityHigh.String()] + summary.SeverityCount[stasv1alpha1.SeverityCritical.String()])))

return p
}

func (p *policyReportPatch) withResults(vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *policyReportPatch {
p.vulnerabilities = vulnerabilities
p.minSeverity = minSeverity

return p
}

func (p *policyReportPatch) apply(ctx context.Context, c client.Client, scheme *runtime.Scheme) error {
if err := SetControllerReference(p.cis, p.patch.ObjectMetaApplyConfiguration, scheme); err != nil {
return err
}

var err error
// Repeat until resource fits in api-server by increasing minimum severity on failure.
for severity := p.minSeverity; severity <= stasv1alpha1.MaxSeverity; severity++ {
var vulnerabilities []stasv1alpha1.Vulnerability

vulnerabilities, err = filterVulnerabilities(p.vulnerabilities, severity)
if err != nil {
return err
}

p.patch.Results = make([]policyv1alpha2ac.PolicyReportResultApplyConfiguration, len(vulnerabilities))
for i, v := range vulnerabilities {
p.patch.Results[i] = *policyReportResultPatch(v)
}

report := &policyv1alpha2.PolicyReport{}
report.Name = *p.patch.Name
report.Namespace = *p.patch.Namespace

err = c.Patch(ctx, report, applyPatch{p.patch}, FieldValidationStrict, client.ForceOwnership, fieldOwner)
if !isResourceTooLargeError(err) {
break
}
}

if err != nil {
return fmt.Errorf("when applying policy report: %w", err)
}

return nil
}

func policyReportResultPatch(v stasv1alpha1.Vulnerability) *policyv1alpha2ac.PolicyReportResultApplyConfiguration {
properties := map[string]string{
"pkgName": v.PkgName,
"pkgPath": v.PkgPath,
"installedVersion": v.InstalledVersion,
"fixedVersion": v.FixedVersion,
"primaryURL": v.PrimaryURL,
}

// Remove properties with empty values to compact report
maps.DeleteFunc(properties, func(k string, v string) bool {
return len(v) == 0
})

return policyv1alpha2ac.PolicyReportResult().
WithCategory("vulnerability scan").
WithSource("image-scanner").
WithPolicy(v.VulnerabilityID).
WithResult(mapPolicyResult(v.Severity)).
WithSeverity(mapPolicyResultSeverity(v.Severity)).
WithDescription(v.Title).
WithProperties(properties)
}

func mapPolicyResultSeverity(severity string) policyv1alpha2.PolicyResultSeverity {
switch severity {
case "UNKNOWN":
return ""
default:
return policyv1alpha2.PolicyResultSeverity(strings.ToLower(severity))
}
}

func mapPolicyResult(severity string) policyv1alpha2.PolicyResult {
switch severity {
case "UNKNOWN":
return "skip"
case "LOW", "MEDIUM":
return "warn"
default:
return "fail"
}
}
46 changes: 31 additions & 15 deletions internal/controller/stas/scan_job_controller.go
Original file line number Diff line number Diff line change
@@ -26,8 +26,8 @@ import (
"sigs.k8s.io/json"

stasv1alpha1 "github.com/statnett/image-scanner-operator/api/stas/v1alpha1"
stasv1alpha1ac "github.com/statnett/image-scanner-operator/internal/client/applyconfiguration/stas/v1alpha1"
"github.com/statnett/image-scanner-operator/internal/config"
"github.com/statnett/image-scanner-operator/internal/config/feature"
"github.com/statnett/image-scanner-operator/internal/controller"
staserrors "github.com/statnett/image-scanner-operator/internal/errors"
"github.com/statnett/image-scanner-operator/internal/pod"
@@ -168,7 +168,7 @@ func (r *ScanJobReconciler) reconcileCompleteJob(ctx context.Context, job *batch
WithReason(stasv1alpha1.ReasonScanReportDecodeError).
WithMessage(fmt.Sprintf("error decoding scan report JSON from job '%s': %s", job.Name, err)),
).
withScanJob(job).
withScanJob(job, false).
apply(ctx, r.Client)
}

@@ -182,15 +182,30 @@ func (r *ScanJobReconciler) reconcileCompleteJob(ctx context.Context, job *batch
}
}

summary := vulnerabilitySummary(vulnerabilities, minSeverity)

if config.DefaultFeatureGate.Enabled(feature.PolicyReport) {
err = newPolicyReportPatch(cis).
withResults(vulnerabilities, minSeverity).
withSummary(summary).
apply(ctx, r.Client, r.Scheme)
if err != nil {
return err
}
}

return newContainerImageStatusPatch(cis).
withCompletedScanJob(job, vulnerabilities, minSeverity).
withScanJob(job, true).
withResults(vulnerabilities, minSeverity).
withSummary(summary).
apply(ctx, r.Client)
}

func isResourceTooLargeError(err error) bool {
return apierrors.IsInternalError(err) &&
(strings.Contains(err.Error(), "ResourceExhausted") ||
strings.Contains(err.Error(), "request is too large"))
return apierrors.IsRequestEntityTooLargeError(err) ||
apierrors.IsInternalError(err) &&
(strings.Contains(err.Error(), "ResourceExhausted") ||
strings.Contains(err.Error(), "request is too large"))
}

func (r *ScanJobReconciler) reconcileFailedJob(ctx context.Context, job *batchv1.Job, log io.Reader, cis *stasv1alpha1.ContainerImageScan) error {
@@ -207,7 +222,7 @@ func (r *ScanJobReconciler) reconcileFailedJob(ctx context.Context, job *batchv1
WithReason("Error").
WithMessage(string(logBytes)),
).
withScanJob(job).
withScanJob(job, false).
apply(ctx, r.Client)
}

@@ -339,8 +354,8 @@ func (r *ScanJobReconciler) getScanJobLogs(ctx context.Context, job *batchv1.Job
return r.GetLogs(ctx, client.ObjectKeyFromObject(&jobPod), trivy.ScanJobContainerName)
}

func filterVulnerabilities(orig []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) ([]stasv1alpha1ac.VulnerabilityApplyConfiguration, error) {
var filtered []stasv1alpha1ac.VulnerabilityApplyConfiguration
func filterVulnerabilities(orig []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) ([]stasv1alpha1.Vulnerability, error) {
var filtered []stasv1alpha1.Vulnerability

for _, v := range orig {
severity, err := stasv1alpha1.NewSeverity(v.Severity)
@@ -349,14 +364,14 @@ func filterVulnerabilities(orig []stasv1alpha1.Vulnerability, minSeverity stasv1
}

if severity >= minSeverity {
filtered = append(filtered, *vulnerabilityPatch(v))
filtered = append(filtered, v)
}
}

return filtered, nil
}

func vulnerabilitySummary(vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *stasv1alpha1ac.VulnerabilitySummaryApplyConfiguration {
func vulnerabilitySummary(vulnerabilities []stasv1alpha1.Vulnerability, minSeverity stasv1alpha1.Severity) *stasv1alpha1.VulnerabilitySummary {
severityCount := make(map[string]int32)
for severity := minSeverity; severity <= stasv1alpha1.MaxSeverity; severity++ {
severityCount[severity.String()] = 0
@@ -374,8 +389,9 @@ func vulnerabilitySummary(vulnerabilities []stasv1alpha1.Vulnerability, minSever
}
}

return stasv1alpha1ac.VulnerabilitySummary().
WithSeverityCount(severityCount).
WithFixedCount(fixedCount).
WithUnfixedCount(unfixedCount)
return &stasv1alpha1.VulnerabilitySummary{
SeverityCount: severityCount,
FixedCount: fixedCount,
UnfixedCount: unfixedCount,
}
}
Loading

0 comments on commit 946b995

Please sign in to comment.