-
Notifications
You must be signed in to change notification settings - Fork 59
Surface volatile-excluded rules as time-bounded exceptions in report output #3419
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -280,7 +280,10 @@ func (b *basePolicyEvaluator) postProcessResults(ctx context.Context, runResults | |
| effectiveTime := b.policy.EffectiveTime() | ||
| ctx = context.WithValue(ctx, effectiveTimeKey, effectiveTime) | ||
|
|
||
| // Exceptions are not counted toward totalRules to preserve the fail-closed invariant: | ||
| // an all-excluded policy must not report success with zero real checks performed. | ||
| totalRules := 0 | ||
| allExceptions := 0 | ||
|
|
||
| missingIncludes := map[string]bool{} | ||
| for _, defaultItem := range b.include.defaultItems { | ||
|
|
@@ -306,21 +309,22 @@ func (b *basePolicyEvaluator) postProcessResults(ctx context.Context, runResults | |
| addRuleMetadata(ctx, &allResults[j], b.rules) | ||
| } | ||
|
|
||
| filteredResults, updatedMissingIncludes := unifiedFilter.FilterResults( | ||
| filteredResults, updatedMissingIncludes, configExceptions := unifiedFilter.FilterResults( | ||
| allResults, b.allRules, target.Target, target.ComponentName, missingIncludes, effectiveTime) | ||
| missingIncludes = updatedMissingIncludes | ||
|
|
||
| warnings, failures, exceptions, skipped := unifiedFilter.CategorizeResults( | ||
| warnings, failures, _, skipped := unifiedFilter.CategorizeResults( | ||
| filteredResults, result, effectiveTime) | ||
|
|
||
| result.Warnings = warnings | ||
| result.Failures = failures | ||
| result.Exceptions = exceptions | ||
| result.Exceptions = ensureNonNilSlice(configExceptions) | ||
| result.Skipped = skipped | ||
|
|
||
| result.Successes = b.computeSuccesses(result, b.rules, target.Target, target.ComponentName, missingIncludes, unifiedFilter, effectiveTime) | ||
|
|
||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [high] policy-bypass totalRules safety invariant weakened: counting exceptions toward totalRules means all-excluded policies produce success=true with no actual policy checks performed. The totalRules == 0 guard is a fail-closed invariant that catches misconfiguration. By counting exceptions (which are NOT evaluated rules), a VolatileConfig that excludes every rule now silently passes instead of erroring. For a supply chain verification tool, this converts fail-closed to fail-open. Suggested fix: Do not count exceptions in totalRules. Add a separate diagnostic if totalRules == 0 && len(allExceptions) > 0, e.g., a warning that all rules were excluded and no actual policy checks were performed. |
||
| totalRules += len(result.Warnings) + len(result.Failures) + len(result.Successes) | ||
| allExceptions += len(configExceptions) | ||
| results = append(results, result) | ||
| } | ||
|
|
||
|
|
@@ -337,6 +341,12 @@ func (b *basePolicyEvaluator) postProcessResults(ctx context.Context, runResults | |
| trim(&results) | ||
|
|
||
| if totalRules == 0 { | ||
| if allExceptions > 0 { | ||
| // All rules were excluded by policy config: warn but return the exceptions so | ||
| // downstream consumers (e.g. SVR generators) can represent the waivers. | ||
| log.Warn("all rules were excluded by policy config; no policy checks were performed") | ||
| return results, nil | ||
| } | ||
| log.Error("no successes, warnings, or failures, check input") | ||
| return nil, fmt.Errorf("no successes, warnings, or failures, check input") | ||
| } | ||
|
|
@@ -395,7 +405,7 @@ func (b *basePolicyEvaluator) computeSuccesses( | |
| } | ||
|
|
||
| if unifiedFilter != nil { | ||
| filteredResults, _ := unifiedFilter.FilterResults( | ||
| filteredResults, _, _ := unifiedFilter.FilterResults( | ||
| []Result{success}, rules, imageRef, componentName, missingIncludes, effectiveTime) | ||
| if len(filteredResults) == 0 { | ||
| log.Debugf("Skipping result success: %#v", success) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -168,7 +168,7 @@ | |
| } | ||
|
|
||
| const ( | ||
| effectiveOnFormat = "2006-01-02T15:04:05Z" | ||
| effectiveOnTimeout = -90 * 24 * time.Hour // keep effective_on metadata up to 90 days | ||
| metadataQuery = "query" | ||
| metadataCode = "code" | ||
|
|
@@ -176,8 +176,9 @@ | |
| metadataDependsOn = "depends_on" | ||
| metadataDescription = "description" | ||
| metadataSeverity = "severity" | ||
| metadataEffectiveOn = "effective_on" | ||
| metadataSolution = "solution" | ||
| metadataEffectiveOn = "effective_on" | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [low] style The new metadataEffectiveUntil and metadataSolution entries use wider tab alignment than metadataTerm and metadataTitle, creating visual inconsistency in the const block. Suggested fix: Align all entries in the const block to the same tab stop. |
||
| metadataEffectiveUntil = "effective_until" | ||
| metadataSolution = "solution" | ||
| metadataTerm = "term" | ||
| metadataTitle = "title" | ||
| ) | ||
|
|
@@ -613,8 +614,10 @@ | |
| ctx = context.WithValue(ctx, effectiveTimeKey, effectiveTime) | ||
|
|
||
| // Track how many rules have been processed. This is used later on to determine if anything | ||
| // at all was processed. | ||
| // at all was processed. Exceptions are not counted because an all-excluded policy would | ||
| // otherwise falsely report success with no real checks performed. | ||
| totalRules := 0 | ||
| allExceptions := 0 | ||
|
|
||
| // Populate a list with all the include directives specified in the | ||
| // policy config. | ||
|
|
@@ -650,25 +653,26 @@ | |
| } | ||
|
|
||
| // Filter results using the unified filter | ||
| filteredResults, updatedMissingIncludes := unifiedFilter.FilterResults( | ||
| filteredResults, updatedMissingIncludes, configExceptions := unifiedFilter.FilterResults( | ||
| allResults, allRules, target.Target, target.ComponentName, missingIncludes, effectiveTime) | ||
|
|
||
| // Update missing includes | ||
| missingIncludes = updatedMissingIncludes | ||
|
|
||
| // Categorize results using the unified filter | ||
| warnings, failures, exceptions, skipped := unifiedFilter.CategorizeResults( | ||
| warnings, failures, _, skipped := unifiedFilter.CategorizeResults( | ||
| filteredResults, result, effectiveTime) | ||
|
|
||
| result.Warnings = warnings | ||
| result.Failures = failures | ||
| result.Exceptions = exceptions | ||
| result.Exceptions = ensureNonNilSlice(configExceptions) | ||
| result.Skipped = skipped | ||
|
|
||
| // Replace the placeholder successes slice with the actual successes. | ||
| result.Successes = c.computeSuccesses(result, rules, target.Target, target.ComponentName, missingIncludes, unifiedFilter, effectiveTime) | ||
|
|
||
| totalRules += len(result.Warnings) + len(result.Failures) + len(result.Successes) | ||
| allExceptions += len(configExceptions) | ||
|
|
||
| results = append(results, result) | ||
| } | ||
|
|
@@ -688,6 +692,12 @@ | |
| // If no rules were checked, then we have effectively failed, because no tests were actually | ||
| // ran due to input error, etc. | ||
| if totalRules == 0 { | ||
| if allExceptions > 0 { | ||
| // All rules were excluded by policy config: warn but return the exceptions so | ||
| // downstream consumers (e.g. SVR generators) can represent the waivers. | ||
| log.Warn("all rules were excluded by policy config; no policy checks were performed") | ||
| return results, nil | ||
| } | ||
| log.Error("no successes, warnings, or failures, check input") | ||
| return nil, fmt.Errorf("no successes, warnings, or failures, check input") | ||
| } | ||
|
|
@@ -863,7 +873,7 @@ | |
| // Use unified filtering approach for consistency | ||
| if unifiedFilter != nil { | ||
| // Use the unified filter to check if this success should be included | ||
| filteredResults, _ := unifiedFilter.FilterResults( | ||
| filteredResults, _, _ := unifiedFilter.FilterResults( | ||
| []Result{success}, rules, imageRef, componentName, missingIncludes, effectiveTime) | ||
|
|
||
| if len(filteredResults) == 0 { | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -26,16 +26,24 @@ import ( | |
| log "github.com/sirupsen/logrus" | ||
| ) | ||
|
|
||
| // criteriaItemMeta carries optional time metadata from volatile config CRD fields. | ||
| type criteriaItemMeta struct { | ||
| effectiveOn string | ||
| effectiveUntil string | ||
| } | ||
|
|
||
| // contains include/exclude items | ||
| // digestItems stores include/exclude items that are specific with an imageRef | ||
| // - the imageRef is the key, value is the policy to include/exclude. | ||
| // componentItems stores include/exclude items that are specific to a component name | ||
| // - the component name is the key, value is the policy to include/exclude. | ||
| // defaultItems are include/exclude items without an imageRef | ||
| // meta maps criteria value strings to their time metadata (volatile config only). | ||
| type Criteria struct { | ||
| digestItems map[string][]string | ||
| componentItems map[string][]string | ||
| defaultItems []string | ||
| meta map[string]criteriaItemMeta | ||
|
Comment on lines
+41
to
+46
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🗄️ Data Integrity & Integration | 🟠 Major | 🏗️ Heavy lift Preserve metadata per scoped criterion, not per value.
Key metadata by full criterion identity/scope and carry that identity through policy resolution. Add collision tests for static+volatile and multi-component entries. As per path instructions, “Focus on major issues impacting performance, readability, maintainability and security. Avoid nitpicks and avoid verbosity.” Also applies to: 90-95, 229-243 🤖 Prompt for AI AgentsSource: Path instructions |
||
| } | ||
|
|
||
| func (c *Criteria) len() int { | ||
|
|
@@ -71,6 +79,30 @@ func (c *Criteria) addArray(key string, values []string) { | |
| } | ||
| } | ||
|
|
||
| func (c *Criteria) addItemWithMeta(key, value string, m criteriaItemMeta) { | ||
| c.addItem(key, value) | ||
| c.storeMeta(value, m) | ||
| } | ||
|
|
||
| // storeMeta records time metadata for value without adding a new default item. | ||
| // Use this for component-scoped criteria where the value is already registered | ||
| // via addComponentItem but the meta map still needs to be populated. | ||
| func (c *Criteria) storeMeta(value string, m criteriaItemMeta) { | ||
| if m.effectiveOn != "" || m.effectiveUntil != "" { | ||
| if c.meta == nil { | ||
| c.meta = make(map[string]criteriaItemMeta) | ||
| } | ||
| c.meta[value] = m | ||
| } | ||
| } | ||
|
|
||
| func (c *Criteria) getMeta(value string) criteriaItemMeta { | ||
| if c.meta == nil { | ||
| return criteriaItemMeta{} | ||
| } | ||
| return c.meta[value] | ||
| } | ||
|
|
||
| func (c *Criteria) addComponentItem(componentName, value string) { | ||
| if c.componentItems == nil { | ||
| c.componentItems = make(map[string][]string) | ||
|
|
@@ -194,19 +226,21 @@ func collectVolatileConfigItems(items *Criteria, volatileCriteria []ecc.Volatile | |
| until = at | ||
| } | ||
| if until.Compare(at) >= 0 && from.Compare(at) <= 0 { | ||
| meta := criteriaItemMeta{effectiveOn: c.EffectiveOn, effectiveUntil: c.EffectiveUntil} | ||
| // DEPRECATED: use c.ImageDigest instead | ||
| if c.ImageRef != "" { | ||
| items.addItem(c.ImageRef, c.Value) | ||
| items.addItemWithMeta(c.ImageRef, c.Value, meta) | ||
| } else if c.ImageUrl != "" { | ||
| items.addItem(c.ImageUrl, c.Value) | ||
| items.addItemWithMeta(c.ImageUrl, c.Value, meta) | ||
| } else if c.ImageDigest != "" { | ||
| items.addItem(c.ImageDigest, c.Value) | ||
| items.addItemWithMeta(c.ImageDigest, c.Value, meta) | ||
| } else if len(c.ComponentNames) > 0 { | ||
| for _, componentName := range c.ComponentNames { | ||
| items.addComponentItem(string(componentName), c.Value) | ||
| } | ||
| items.storeMeta(c.Value, meta) | ||
| } else { | ||
| items.addItem("", c.Value) | ||
| items.addItemWithMeta("", c.Value, meta) | ||
| } | ||
| } | ||
| } | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[medium] logic-error
Rego exception duplication for config-excluded rules. CategorizeResults (filters.go:1139) unconditionally appends originalResult.Exceptions. When a Rego-level exception is also config-excluded, it appears both in the CategorizeResults output (via line 1139) AND in configExceptions from FilterResults, causing duplicate entries in result.Exceptions.
Suggested fix: Either deduplicate when appending configExceptions (skip codes already present in exceptions from CategorizeResults), or condition the line 1139 append to exclude codes that are in configExceptions.