diff --git a/api/v1alpha1/kubescapevalidator_types.go b/api/v1alpha1/kubescapevalidator_types.go index dcff65b..2cad735 100644 --- a/api/v1alpha1/kubescapevalidator_types.go +++ b/api/v1alpha1/kubescapevalidator_types.go @@ -29,13 +29,28 @@ import ( type KubescapeValidatorSpec struct { //+kubebuilder:default=kubescape Namespace string `json:"namespace,omitempty" yaml:"namespace,omitempty"` + // global ignored CVEs + IgnoredCVEs []IgnoredCVE `json:"ignoredCVEs,omitempty" yaml:"ignoredCVERule,omitempty"` + // +kubebuilder:validation:MaxItems=5 + // +kubebuilder:validation:XValidation:message="KubescapeRules must have a unique RuleName",rule="self.all(e, size(self.filter(x, x.name == e.name)) == 1)" + SeverityLimitRules []SeverityLimitRule `json:"kubescapeRules,omitempty" yaml:"kubescapeRules,omitempty"` + + FlaggedCVERules []FlaggedCVE `json:"flaggedCVEs,omitempty" yaml:"flaggedCVERule,omitempty"` +} + +type SeverityLimitRule struct { // Global Severity Limit Rule - SeverityLimitRule SeverityLimitRule `json:"severityLimitRule,omitempty" yaml:"severityLimitRule,omitempty"` + SeverityLimits SeverityLimits `json:"severityLimitRule,omitempty" yaml:"severityLimitRule,omitempty"` + // Global Ignore CVEs + IgnoredCVEs []IgnoredCVE `json:"ignoredCVEs,omitempty" yaml:"ignoredCVERule,omitempty"` // Rule for Flagged CVEs - FlaggedCVERule []FlaggedCVE `json:"flaggedCVERule,omitempty" yaml:"flaggedCVERule,omitempty"` } -// FlaggedCVE is a flagged CVE rule. +func (r SeverityLimitRule) Name() string { + return "SeverityLimitRule" +} + +type IgnoredCVE string type FlaggedCVE string // Name returns the formatted name of the flagged CVE. @@ -43,20 +58,20 @@ func (r FlaggedCVE) Name() string { return fmt.Sprintf("FLAG-%s", string(r)) } +func (r IgnoredCVE) Name() string { + return fmt.Sprintf("IGNORED-%s", string(r)) +} + // ResultCount returns the number of validation results expected for an KubescapeValidatorSpec. func (s KubescapeValidatorSpec) ResultCount() int { count := 0 - if s.SeverityLimitRule != (SeverityLimitRule{}) { - count++ + if s.SeverityLimitRules != nil { + count += len(s.SeverityLimitRules) } - count += len(s.FlaggedCVERule) - return count } -// SeverityLimitRule verifies that the number of vulnerabilities of each severity level does not -// exceed the specified limit. -type SeverityLimitRule struct { +type SeverityLimits struct { Critical *int `json:"critical,omitempty"` High *int `json:"high,omitempty"` Medium *int `json:"medium,omitempty"` @@ -65,9 +80,8 @@ type SeverityLimitRule struct { Unknown *int `json:"unknown,omitempty"` } -// Name is the name of all severity limit rules. -func (r SeverityLimitRule) Name() string { - return "SeverityLimitRule" +func (r SeverityLimits) Name() string { + return "SeverityLimits" } // KubescapeValidatorStatus defines the observed state of KubescapeValidator diff --git a/api/v1alpha1/zz_generated.deepcopy.go b/api/v1alpha1/zz_generated.deepcopy.go index b68b1c2..bd27a9e 100644 --- a/api/v1alpha1/zz_generated.deepcopy.go +++ b/api/v1alpha1/zz_generated.deepcopy.go @@ -86,9 +86,20 @@ func (in *KubescapeValidatorList) DeepCopyObject() runtime.Object { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *KubescapeValidatorSpec) DeepCopyInto(out *KubescapeValidatorSpec) { *out = *in - in.SeverityLimitRule.DeepCopyInto(&out.SeverityLimitRule) - if in.FlaggedCVERule != nil { - in, out := &in.FlaggedCVERule, &out.FlaggedCVERule + if in.IgnoredCVEs != nil { + in, out := &in.IgnoredCVEs, &out.IgnoredCVEs + *out = make([]IgnoredCVE, len(*in)) + copy(*out, *in) + } + if in.SeverityLimitRules != nil { + in, out := &in.SeverityLimitRules, &out.SeverityLimitRules + *out = make([]SeverityLimitRule, len(*in)) + for i := range *in { + (*in)[i].DeepCopyInto(&(*out)[i]) + } + } + if in.FlaggedCVERules != nil { + in, out := &in.FlaggedCVERules, &out.FlaggedCVERules *out = make([]FlaggedCVE, len(*in)) copy(*out, *in) } @@ -121,6 +132,27 @@ func (in *KubescapeValidatorStatus) DeepCopy() *KubescapeValidatorStatus { // DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. func (in *SeverityLimitRule) DeepCopyInto(out *SeverityLimitRule) { + *out = *in + in.SeverityLimits.DeepCopyInto(&out.SeverityLimits) + if in.IgnoredCVEs != nil { + in, out := &in.IgnoredCVEs, &out.IgnoredCVEs + *out = make([]IgnoredCVE, len(*in)) + copy(*out, *in) + } +} + +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeverityLimitRule. +func (in *SeverityLimitRule) DeepCopy() *SeverityLimitRule { + if in == nil { + return nil + } + out := new(SeverityLimitRule) + in.DeepCopyInto(out) + return out +} + +// DeepCopyInto is an autogenerated deepcopy function, copying the receiver, writing into out. in must be non-nil. +func (in *SeverityLimits) DeepCopyInto(out *SeverityLimits) { *out = *in if in.Critical != nil { in, out := &in.Critical, &out.Critical @@ -154,12 +186,12 @@ func (in *SeverityLimitRule) DeepCopyInto(out *SeverityLimitRule) { } } -// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeverityLimitRule. -func (in *SeverityLimitRule) DeepCopy() *SeverityLimitRule { +// DeepCopy is an autogenerated deepcopy function, copying the receiver, creating a new SeverityLimits. +func (in *SeverityLimits) DeepCopy() *SeverityLimits { if in == nil { return nil } - out := new(SeverityLimitRule) + out := new(SeverityLimits) in.DeepCopyInto(out) return out } diff --git a/config/crd/bases/validation.spectrocloud.labs_kubescapevalidators.yaml b/config/crd/bases/validation.spectrocloud.labs_kubescapevalidators.yaml index 28abf8b..ef4bb89 100644 --- a/config/crd/bases/validation.spectrocloud.labs_kubescapevalidators.yaml +++ b/config/crd/bases/validation.spectrocloud.labs_kubescapevalidators.yaml @@ -40,31 +40,48 @@ spec: spec: description: KubescapeValidatorSpec defines the desired state of KubescapeValidator properties: - flaggedCVERule: - description: Rule for Flagged CVEs + flaggedCVEs: items: - description: FlaggedCVE is a flagged CVE rule. type: string type: array + ignoredCVEs: + description: global ignored CVEs + items: + type: string + type: array + kubescapeRules: + items: + properties: + ignoredCVEs: + description: Global Ignore CVEs + items: + type: string + type: array + severityLimitRule: + description: Global Severity Limit Rule + properties: + critical: + type: integer + high: + type: integer + low: + type: integer + medium: + type: integer + negligible: + type: integer + unknown: + type: integer + type: object + type: object + maxItems: 5 + type: array + x-kubernetes-validations: + - message: KubescapeRules must have a unique RuleName + rule: self.all(e, size(self.filter(x, x.name == e.name)) == 1) namespace: default: kubescape type: string - severityLimitRule: - description: Global Severity Limit Rule - properties: - critical: - type: integer - high: - type: integer - low: - type: integer - medium: - type: integer - negligible: - type: integer - unknown: - type: integer - type: object type: object status: description: KubescapeValidatorStatus defines the observed state of KubescapeValidator diff --git a/internal/controller/kubescapevalidator_controller.go b/internal/controller/kubescapevalidator_controller.go index 16f3689..74edfc4 100644 --- a/internal/controller/kubescapevalidator_controller.go +++ b/internal/controller/kubescapevalidator_controller.go @@ -117,28 +117,32 @@ func (r *KubescapeValidatorReconciler) Reconcile(ctx context.Context, req ctrl.R kubescapeService := validators.NewKubescapeService(r.Log, kubescape) - manifests, err := kubescapeService.Manifests() + manifests, err := kubescapeService.Manifests(validator.Spec.Namespace) if err != nil { return ctrl.Result{RequeueAfter: time.Second * 120}, errors.New("no manifests found") } - // Reconcile Severity Rule - vrr, err := kubescapeService.ReconcileSeverityRule(validator.Spec.SeverityLimitRule, manifests) - if err != nil { - l.Error(err, "failed to reconcile Severity rule") - } - resp.AddResult(vrr, err) - - // Reconcile Flagged CVE Rule - for _, rule := range validator.Spec.FlaggedCVERule { - fmt.Println("ahash") - vrr, err := kubescapeService.ReconcileFlaggedCVERule(rule, manifests) + // Reconcile Kubescape Rule + for _, rule := range validator.Spec.SeverityLimitRules { + vrr, err := kubescapeService.ReconcileSeverityRule(rule, validator.Spec.IgnoredCVEs, manifests) if err != nil { l.Error(err, "failed to reconcile Severity rule") } resp.AddResult(vrr, err) } + /* + // Reconcile Flagged CVE Rule + for _, rule := range validator.Spec.FlaggedCVERule { + fmt.Println("ahash") + vrr, err := kubescapeService.ReconcileFlaggedCVERule(nn, rule, manifests) + if err != nil { + l.Error(err, "failed to reconcile Severity rule") + } + resp.AddResult(vrr, err) + } + */ + if err := vres.SafeUpdateValidationResult(ctx, p, vr, resp, r.Log); err != nil { return ctrl.Result{}, err } diff --git a/internal/validators/kubescape.go b/internal/validators/kubescape.go index 3f0842f..249603f 100644 --- a/internal/validators/kubescape.go +++ b/internal/validators/kubescape.go @@ -4,6 +4,7 @@ package validators import ( "context" "fmt" + "slices" "github.com/go-logr/logr" kubevuln "github.com/kubescape/kubevuln/repositories" @@ -34,28 +35,25 @@ func NewKubescapeService(log logr.Logger, kvAPI *kubevuln.APIServerStore) *Kubes } } -// Manifests retrieves vulnerability data. -func (n *KubescapeService) Manifests() ([]kubescapev1.VulnerabilityManifest, error) { - manifestList, err := n.API.StorageClient.VulnerabilityManifests("kubescape").List(context.Background(), metav1.ListOptions{}) +func (n *KubescapeService) Manifests(namespace string) ([]kubescapev1.VulnerabilityManifest, error) { + manifestList, err := n.API.StorageClient.VulnerabilityManifests(namespace).List(context.Background(), metav1.ListOptions{}) if err != nil { return nil, err } manifests := make([]kubescapev1.VulnerabilityManifest, 0, len(manifestList.Items)) for _, v := range manifestList.Items { - manifest, err := n.API.StorageClient.VulnerabilityManifests("kubescape").Get(context.Background(), v.Name, metav1.GetOptions{}) + manifest, err := n.API.StorageClient.VulnerabilityManifests(namespace).Get(context.Background(), v.Name, metav1.GetOptions{}) if err != nil { return nil, err } - manifests = append(manifests, *manifest) } return manifests, nil } -// ReconcileSeverityRule reconciles a severity limit rule. -func (n *KubescapeService) ReconcileSeverityRule(rule validationv1.SeverityLimitRule, manifests []kubescapev1.VulnerabilityManifest) (*types.ValidationRuleResult, error) { +func (n *KubescapeService) ReconcileSeverityRule(rule validationv1.SeverityLimitRule, ignoredCVEs []validationv1.IgnoredCVE, manifests []kubescapev1.VulnerabilityManifest) (*types.ValidationRuleResult, error) { vr := buildValidationResult(rule, constants.ValidationTypeSeverity) critical := 0 @@ -65,7 +63,7 @@ func (n *KubescapeService) ReconcileSeverityRule(rule validationv1.SeverityLimit unknown := 0 negligible := 0 - foundVulns := validationv1.SeverityLimitRule{ + foundVulns := validationv1.SeverityLimits{ Critical: &critical, High: &high, Medium: &medium, @@ -83,6 +81,11 @@ func (n *KubescapeService) ReconcileSeverityRule(rule validationv1.SeverityLimit continue } + // ignore global and rule scoped CVE + if slices.Contains(ignoredCVEs, validationv1.IgnoredCVE(match.Vulnerability.ID)) || slices.Contains(rule.IgnoredCVEs, validationv1.IgnoredCVE(match.Vulnerability.ID)) { + continue + } + uniqueCVEs[match.Vulnerability.ID] = true switch match.Vulnerability.Severity { @@ -103,43 +106,43 @@ func (n *KubescapeService) ReconcileSeverityRule(rule validationv1.SeverityLimit } var diff int - if rule.Critical != nil && *foundVulns.Critical > *rule.Critical { + if rule.SeverityLimits.Critical != nil && *foundVulns.Critical > *rule.SeverityLimits.Critical { vr.Condition.Status = v1.ConditionFalse - diff = *foundVulns.Critical - *rule.Critical - vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d unique Critical severity vulnerabilities. %d higher then %d limit.", *foundVulns.Critical, diff, *rule.Critical)) + diff = *foundVulns.Critical - *rule.SeverityLimits.Critical + vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d unique Critical severity vulnerabilities. %d higher then %d limit.", *foundVulns.Critical, diff, *rule.SeverityLimits.Critical)) } - if rule.High != nil && *foundVulns.High > *rule.High { + if rule.SeverityLimits.High != nil && *foundVulns.High > *rule.SeverityLimits.High { vr.Condition.Status = v1.ConditionFalse - diff = *foundVulns.High - *rule.High - vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d High severity vulnerabilities. %d higher then %d limit.", *foundVulns.High, diff, *rule.High)) + diff = *foundVulns.High - *rule.SeverityLimits.High + vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d High severity vulnerabilities. %d higher then %d limit.", *foundVulns.High, diff, *rule.SeverityLimits.High)) } - if rule.Medium != nil && *foundVulns.Medium > *rule.Medium { + if rule.SeverityLimits.Medium != nil && *foundVulns.Medium > *rule.SeverityLimits.Medium { vr.Condition.Status = v1.ConditionFalse - diff = *foundVulns.Medium - *rule.Medium - vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d Medium severity vulnerabilities. %d higher then %d limit.", *foundVulns.Medium, diff, *rule.Medium)) + diff = *foundVulns.Medium - *rule.SeverityLimits.Medium + vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d Medium severity vulnerabilities. %d higher then %d limit.", *foundVulns.Medium, diff, *rule.SeverityLimits.Medium)) } - if rule.Low != nil && *foundVulns.Low > *rule.Low { + if rule.SeverityLimits.Low != nil && *foundVulns.Low > *rule.SeverityLimits.Low { vr.Condition.Status = v1.ConditionFalse - diff = *foundVulns.Low - *rule.Low - vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d Low severity vulnerabilities. %d higher then %d limit.", *foundVulns.Low, diff, *rule.Low)) + diff = *foundVulns.Low - *rule.SeverityLimits.Low + vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d Low severity vulnerabilities. %d higher then %d limit.", *foundVulns.Low, diff, *rule.SeverityLimits.Low)) } - if rule.Unknown != nil && *foundVulns.Unknown > *rule.Unknown { + if rule.SeverityLimits.Unknown != nil && *foundVulns.Unknown > *rule.SeverityLimits.Unknown { vr.Condition.Status = v1.ConditionFalse - diff = *foundVulns.Unknown - *rule.Unknown - vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d Unknown severity vulnerabilities. %d higher then %d limit.", *foundVulns.Unknown, diff, *rule.Unknown)) + diff = *foundVulns.Unknown - *rule.SeverityLimits.Unknown + vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d Unknown severity vulnerabilities. %d higher then %d limit.", *foundVulns.Unknown, diff, *rule.SeverityLimits.Unknown)) } - if rule.Negligible != nil && *foundVulns.Negligible > *rule.Negligible { + if rule.SeverityLimits.Negligible != nil && *foundVulns.Negligible > *rule.SeverityLimits.Negligible { vr.Condition.Status = v1.ConditionFalse - diff = *foundVulns.Negligible - *rule.Negligible - vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d Negligible severity vulnerabilities. %d higher then %d limit.", *foundVulns.Negligible, diff, *rule.Negligible)) + diff = *foundVulns.Negligible - *rule.SeverityLimits.Negligible + vr.Condition.Details = append(vr.Condition.Details, fmt.Sprintf("Found %d Negligible severity vulnerabilities. %d higher then %d limit.", *foundVulns.Negligible, diff, *rule.SeverityLimits.Negligible)) } if vr.Condition.Status == v1.ConditionFalse { diff --git a/internal/validators/kubescape_test.go b/internal/validators/kubescape_test.go index c540e71..5edea0f 100644 --- a/internal/validators/kubescape_test.go +++ b/internal/validators/kubescape_test.go @@ -17,8 +17,9 @@ func TestKubescapeService_ReconcileSeverityRule(t *testing.T) { API *kubevuln.APIServerStore } type args struct { - rule validationv1.SeverityLimitRule - manifests []kubescapev1.VulnerabilityManifest + rule validationv1.SeverityLimitRule + ignoredCVEs []validationv1.IgnoredCVE + manifests []kubescapev1.VulnerabilityManifest } tests := []struct { name string @@ -35,7 +36,7 @@ func TestKubescapeService_ReconcileSeverityRule(t *testing.T) { Log: tt.fields.Log, API: tt.fields.API, } - got, err := n.ReconcileSeverityRule(tt.args.rule, tt.args.manifests) + got, err := n.ReconcileSeverityRule(tt.args.rule, tt.args.ignoredCVEs, tt.args.manifests) if (err != nil) != tt.wantErr { t.Errorf("KubescapeService.ReconcileSeverityRule() error = %v, wantErr %v", err, tt.wantErr) return