Skip to content

Commit ac67467

Browse files
authored
Merge pull request #2930 from joejstuart/commit-pr-spec
feat(evaluator): apply rule-level pipeline_intention filtering post-eval
2 parents 730663a + c84a050 commit ac67467

File tree

8 files changed

+834
-158
lines changed

8 files changed

+834
-158
lines changed

.cursor/rules/rule_filtering_process.mdc

Lines changed: 85 additions & 91 deletions
Original file line numberDiff line numberDiff line change
@@ -48,29 +48,9 @@ The PolicyResolver performs three phases of filtering:
4848
// that have matching pipeline_intention metadata
4949
// When pipeline_intention is NOT set in ruleData: only include packages with rules
5050
// that have NO pipeline_intention metadata (general-purpose rules)
51-
func (r *ECPolicyResolver) matchesPipelineIntention(pkgRules []rule.Info) bool {
52-
if len(r.pipelineIntentions) == 0 {
53-
// When no pipeline_intention is configured, only include packages with no pipeline_intention metadata
54-
for _, r := range pkgRules {
55-
if len(r.PipelineIntention) > 0 {
56-
return false // Exclude packages with pipeline_intention metadata
57-
}
58-
}
59-
return true // Include packages with no pipeline_intention metadata
60-
}
61-
62-
// When pipeline_intention is set, only include packages that contain rules with matching pipeline_intention metadata
63-
for _, r := range pkgRules {
64-
for _, ruleIntention := range r.PipelineIntention {
65-
for _, targetIntention := range r.pipelineIntentions {
66-
if ruleIntention == targetIntention {
67-
return true // Include packages with matching pipeline_intention metadata
68-
}
69-
}
70-
}
71-
}
72-
return false // Exclude packages with no matching pipeline_intention metadata
73-
}
51+
//
52+
// This filtering is now handled by the ruleMatchesPipelineIntention method
53+
// which evaluates each rule individually rather than at the package level.
7454
```
7555

7656
#### Phase 2: Rule-by-Rule Evaluation
@@ -82,11 +62,26 @@ for _, ruleInfo := range pkgRules {
8262
}
8363
```
8464

65+
The `baseEvaluateRuleInclusion` method:
66+
1. Creates matchers for the rule (package, rule, term combinations)
67+
2. Scores the rule against include criteria
68+
3. Scores the rule against exclude criteria
69+
4. Determines inclusion based on higher score
70+
5. Records explanations for debugging
71+
8572
#### Phase 3: Package-Level Determination
8673
```go
87-
// If ANY rule in the package is included → Package is included
88-
// If NO rules are included but SOME rules are excluded → Package is excluded
89-
// If NO rules are included and NO rules are excluded → Package is not explicitly categorized
74+
// CORRECTED LOGIC: Package inclusion should be based on whether the package
75+
// contains any rules that could potentially be included, regardless of exclusions.
76+
// Excluded rules will be filtered out at the post-evaluation stage.
77+
//
78+
// Intended behavior:
79+
// - If ANY rule in the package is included → Package is included
80+
// - If NO rules are included but SOME rules are excluded → Package is STILL included
81+
// (because excluded rules will be filtered post-evaluation)
82+
// - If NO rules are included and NO rules are excluded → Package is not explicitly categorized
83+
//
84+
// Current implementation (INCORRECT):
9085
func (r *basePolicyResolver) baseDeterminePackageInclusion(pkg string, pkgRules []rule.Info, result *PolicyResolutionResult) {
9186
hasIncludedRules := false
9287
hasExcludedRules := false
@@ -101,14 +96,35 @@ func (r *basePolicyResolver) baseDeterminePackageInclusion(pkg string, pkgRules
10196
}
10297
}
10398

99+
// CURRENT (INCORRECT) LOGIC:
104100
if hasIncludedRules {
105101
result.IncludedPackages[pkg] = true
106102
} else if hasExcludedRules {
107-
result.ExcludedPackages[pkg] = true
103+
result.ExcludedPackages[pkg] = true // This is wrong!
108104
}
105+
106+
// CORRECT LOGIC should be:
107+
// if hasIncludedRules || hasExcludedRules {
108+
// result.IncludedPackages[pkg] = true // Include package, filter rules later
109+
// }
109110
}
110111
```
111112

113+
**Note**: The current implementation incorrectly excludes packages that contain only excluded rules. This should be fixed to include such packages, allowing post-evaluation filtering to handle rule-level exclusions.
114+
115+
#### Why This Logic Matters
116+
117+
The package-level determination affects which packages are loaded for conftest evaluation:
118+
119+
1. **Pre-Evaluation**: Only packages in `IncludedPackages` are passed to conftest for evaluation
120+
2. **Post-Evaluation**: The `UnifiedPostEvaluationFilter` filters individual results based on rule-level decisions
121+
122+
**Current Problem**: If a package contains only excluded rules, it's marked as `ExcludedPackages` and never evaluated by conftest. This means:
123+
- The excluded rules never run, so they can't be filtered out post-evaluation
124+
- The package is completely skipped, which may not be the intended behavior
125+
126+
**Correct Behavior**: Packages with any rules (included or excluded) should be included for evaluation, allowing post-evaluation filtering to handle the rule-level decisions properly.
127+
112128
### 2. Initial Rule Execution
113129

114130
```go
@@ -151,78 +167,42 @@ warnings, failures, exceptions, skipped := unifiedFilter.CategorizeResults(
151167
filteredResults, result, effectiveTime)
152168
```
153169

154-
### 4. Result Processing by Type
155-
156-
For each namespace result, the following processing occurs:
157-
158-
#### Warning Processing
159-
160-
```go
161-
for i := range result.Warnings {
162-
warning := result.Warnings[i]
163-
addRuleMetadata(ctx, &warning, rules)
164-
165-
// Note: In the current implementation, this filtering is handled by UnifiedPostEvaluationFilter
166-
// The legacy isResultIncluded method is still available for backward compatibility
167-
168-
if getSeverity(warning) == severityFailure {
169-
// Promote to failure if severity indicates it should be a failure
170-
failures = append(failures, warning)
171-
} else {
172-
warnings = append(warnings, warning)
173-
}
174-
}
175-
```
176-
177-
**Filtering Logic:**
178-
1. **Metadata Addition**: Rule metadata is added from policy annotations
179-
2. **Inclusion Check**: Warning is checked against include/exclude criteria
180-
3. **Severity Promotion**: Warnings with `severity: failure` are promoted to failures
170+
### 4. UnifiedPostEvaluationFilter Implementation
181171

182-
#### Failure Processing
172+
The `UnifiedPostEvaluationFilter` provides two main methods:
183173

174+
#### FilterResults Method
184175
```go
185-
for i := range result.Failures {
186-
failure := result.Failures[i]
187-
addRuleMetadata(ctx, &failure, rules)
188-
189-
// Note: In the current implementation, this filtering is handled by UnifiedPostEvaluationFilter
190-
// The legacy isResultIncluded method is still available for backward compatibility
191-
192-
if getSeverity(failure) == severityWarning || !isResultEffective(failure, effectiveTime) {
193-
// Demote to warning if severity indicates or if not yet effective
194-
warnings = append(warnings, failure)
195-
} else {
196-
failures = append(failures, failure)
197-
}
198-
}
176+
func (f *UnifiedPostEvaluationFilter) FilterResults(
177+
results []Result,
178+
rules policyRules,
179+
target string,
180+
missingIncludes map[string]bool,
181+
effectiveTime time.Time,
182+
) ([]Result, map[string]bool)
199183
```
200184

201-
**Filtering Logic:**
202-
1. **Metadata Addition**: Rule metadata is added from policy annotations
203-
2. **Inclusion Check**: Failure is checked against include/exclude criteria
204-
3. **Severity Demotion**: Failures with `severity: warning` are demoted to warnings
205-
4. **Effective Time Check**: Failures with future `effective_on` dates are demoted to warnings
206-
207-
#### Exception and Skipped Processing
185+
This method:
186+
1. **Checks PolicyResolver Type**: Uses ECPolicyResolver for pipeline intention filtering or IncludeExcludePolicyResolver for basic filtering
187+
2. **Policy-Based Filtering**: For ECPolicyResolver, uses `ResolvePolicy` to determine which results should be included
188+
3. **Legacy Filtering**: For IncludeExcludePolicyResolver or results without codes, uses legacy `LegacyIsResultIncluded` logic
189+
4. **Missing Includes Tracking**: Updates the missing includes map as results are processed
208190

191+
#### CategorizeResults Method
209192
```go
210-
for i := range result.Exceptions {
211-
exception := result.Exceptions[i]
212-
addRuleMetadata(ctx, &exception, rules)
213-
exceptions = append(exceptions, exception)
214-
}
215-
216-
for i := range result.Skipped {
217-
skip := result.Skipped[i]
218-
addRuleMetadata(ctx, &skip, rules)
219-
skipped = append(skipped, skip)
220-
}
193+
func (f *UnifiedPostEvaluationFilter) CategorizeResults(
194+
filteredResults []Result,
195+
originalResult Outcome,
196+
effectiveTime time.Time,
197+
) (warnings []Result, failures []Result, exceptions []Result, skipped []Result)
221198
```
222199

223-
**Processing:**
224-
- Exceptions and skipped results only have metadata added
225-
- No inclusion/exclusion filtering is applied to these result types
200+
This method:
201+
1. **Determines Original Type**: Identifies whether each filtered result was originally a warning, failure, exception, or skipped
202+
2. **Applies Severity Logic**:
203+
- Warnings with `severity: failure` metadata are promoted to failures
204+
- Failures with `severity: warning` metadata or future `effective_on` dates are demoted to warnings
205+
3. **Preserves Categories**: Exceptions and skipped results maintain their original categorization
226206

227207
### 5. Term Extraction and Analysis
228208

@@ -514,4 +494,18 @@ The current implementation uses a unified filtering system:
514494
3. **Consistent Logic**: Both use the same PolicyResolver for consistent decision-making
515495
4. **Backward Compatibility**: Legacy interfaces are still supported
516496

517-
This filtering system provides fine-grained control over which policy violations are reported and how they're categorized, allowing for gradual policy rollouts and context-specific rule management with precise term-based filtering capabilities.
497+
### PolicyResolver Types
498+
499+
The system supports two types of PolicyResolver:
500+
501+
1. **ECPolicyResolver**:
502+
- Handles pipeline intention filtering
503+
- Uses `ruleMatchesPipelineIntention` for rule-level filtering
504+
- Supports both include/exclude and pipeline intention criteria
505+
506+
2. **IncludeExcludePolicyResolver**:
507+
- Ignores pipeline intention filtering
508+
- Only uses include/exclude criteria
509+
- Provides backward compatibility for systems that don't use pipeline intentions
510+
511+
This filtering system provides fine-grained control over which policy violations are reported and how they're categorized, allowing for gradual policy rollouts and context-specific rule management with precise term-based filtering capabilities.

cmd/validate/image.go

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
6060
effectiveTime string
6161
extraRuleData []string
6262
filePath string // Deprecated: images replaced this
63+
filterType string
6364
imageRef string
6465
info bool
6566
input string // Deprecated: images replaced this
@@ -86,7 +87,8 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
8687
}{
8788
strict: true,
8889
workers: 5,
89-
vsaExpiration: 168 * time.Hour, // 7 days default
90+
filterType: "include-exclude", // Default to include-exclude filter
91+
vsaExpiration: 168 * time.Hour, // 7 days default
9092
}
9193

9294
validOutputFormats := applicationsnapshot.OutputFormats
@@ -339,9 +341,9 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
339341
if utils.IsOpaEnabled() {
340342
c, err = newOPAEvaluator()
341343
} else {
342-
// Use the unified filtering approach directly
343-
c, err = evaluator.NewConftestEvaluatorWithNamespace(
344-
cmd.Context(), policySources, data.policy, sourceGroup, nil)
344+
// Use the unified filtering approach with the specified filter type
345+
c, err = evaluator.NewConftestEvaluatorWithFilterType(
346+
cmd.Context(), policySources, data.policy, sourceGroup, data.filterType)
345347
}
346348

347349
if err != nil {
@@ -657,6 +659,11 @@ func validateImageCmd(validate imageValidationFunc) *cobra.Command {
657659
cmd.Flags().IntVar(&data.workers, "workers", data.workers, hd.Doc(`
658660
Number of workers to use for validation. Defaults to 5.`))
659661

662+
cmd.Flags().StringVar(&data.filterType, "filter-type", data.filterType, hd.Doc(`
663+
Filter type to use for policy evaluation. Options: "include-exclude" (default) or "ec-policy".
664+
- "include-exclude": Uses traditional include/exclude filtering without pipeline intentions
665+
- "ec-policy": Uses Enterprise Contract policy filtering with pipeline intention support`))
666+
660667
cmd.Flags().BoolVar(&data.vsaEnabled, "vsa", false, "Generate a Verification Summary Attestation (VSA) for each validated image.")
661668
cmd.Flags().StringVar(&data.vsaSigningKey, "vsa-signing-key", "", "Path to the private key for signing the VSA.")
662669
cmd.Flags().StringSliceVar(&data.vsaUpload, "vsa-upload", nil, "Storage backends for VSA upload. Format: backend@url?param=value. Examples: rekor@https://rekor.sigstore.dev, local@./vsa-dir")

cmd/validate/input.go

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
4343
data := struct {
4444
effectiveTime string
4545
filePaths []string
46+
filterType string
4647
info bool
4748
namespaces []string
4849
output []string
@@ -51,8 +52,9 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
5152
strict bool
5253
workers int
5354
}{
54-
strict: true,
55-
workers: 5,
55+
strict: true,
56+
workers: 5,
57+
filterType: "include-exclude", // Default to include-exclude filter
5658
}
5759
cmd := &cobra.Command{
5860
Use: "input",
@@ -258,6 +260,11 @@ func validateInputCmd(validate InputValidationFunc) *cobra.Command {
258260
cmd.Flags().IntVar(&data.workers, "workers", data.workers, hd.Doc(`
259261
Number of workers to use for validation. Defaults to 5.`))
260262

263+
cmd.Flags().StringVar(&data.filterType, "filter-type", data.filterType, hd.Doc(`
264+
Filter type to use for policy evaluation. Options: "include-exclude" (default) or "ec-policy".
265+
- "include-exclude": Uses traditional include/exclude filtering without pipeline intentions
266+
- "ec-policy": Uses Enterprise Contract policy filtering with pipeline intention support`))
267+
261268
if err := cmd.MarkFlagRequired("file"); err != nil {
262269
panic(err)
263270
}

docs/modules/ROOT/pages/ec_validate_image.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,9 @@ a RFC3339 formatted value, e.g. 2022-11-18T00:00:00Z.
123123
--extra-rule-data:: Extra data to be provided to the Rego policy evaluator. Use format 'key=value'. May be used multiple times.
124124
(Default: [])
125125
-f, --file-path:: DEPRECATED - use --images: path to ApplicationSnapshot Spec JSON file
126+
--filter-type:: Filter type to use for policy evaluation. Options: "include-exclude" (default) or "ec-policy".
127+
- "include-exclude": Uses traditional include/exclude filtering without pipeline intentions
128+
- "ec-policy": Uses Enterprise Contract policy filtering with pipeline intention support (Default: include-exclude)
126129
-h, --help:: help for image (Default: false)
127130
--ignore-rekor:: Skip Rekor transparency log checks during validation. (Default: false)
128131
-i, --image:: OCI image reference

docs/modules/ROOT/pages/ec_validate_input.adoc

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,9 @@ of the git repo. For git repos not hosted on 'github.com' or 'gitlab.com', prefi
4343
effective dates in the future. The value can be "now" (default) - for
4444
current time, or a RFC3339 formatted value, e.g. 2022-11-18T00:00:00Z. (Default: now)
4545
-f, --file:: path to input YAML/JSON file (required) (Default: [])
46+
--filter-type:: Filter type to use for policy evaluation. Options: "include-exclude" (default) or "ec-policy".
47+
- "include-exclude": Uses traditional include/exclude filtering without pipeline intentions
48+
- "ec-policy": Uses Enterprise Contract policy filtering with pipeline intention support (Default: include-exclude)
4649
-h, --help:: help for input (Default: false)
4750
--info:: Include additional information on the failures. For instance for policy
4851
violations, include the title and the description of the failed policy

internal/evaluator/conftest_evaluator.go

Lines changed: 33 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -314,8 +314,20 @@ func NewConftestEvaluatorWithPostEvaluationFilter(ctx context.Context, policySou
314314
return evaluator, nil
315315
}
316316

317-
// set the policy namespace
318-
func NewConftestEvaluatorWithNamespace(ctx context.Context, policySources []source.PolicySource, p ConfigProvider, source ecc.Source, namespace []string) (Evaluator, error) {
317+
// NewConftestEvaluatorWithFilterType returns initialized conftestEvaluator with a specific filter type
318+
func NewConftestEvaluatorWithFilterType(ctx context.Context, policySources []source.PolicySource, p ConfigProvider, source ecc.Source, filterType string) (Evaluator, error) {
319+
return NewConftestEvaluatorWithNamespaceAndFilterType(ctx, policySources, p, source, []string{}, filterType)
320+
}
321+
322+
// NewConftestEvaluatorWithNamespaceAndFilterType returns initialized conftestEvaluator with namespace and filter type
323+
func NewConftestEvaluatorWithNamespaceAndFilterType(
324+
ctx context.Context,
325+
policySources []source.PolicySource,
326+
p ConfigProvider,
327+
source ecc.Source,
328+
namespace []string,
329+
filterType string,
330+
) (Evaluator, error) {
319331
if trace.IsEnabled() {
320332
r := trace.StartRegion(ctx, "ec:conftest-create-evaluator")
321333
defer r.End()
@@ -331,8 +343,15 @@ func NewConftestEvaluatorWithNamespace(ctx context.Context, policySources []sour
331343
source: source,
332344
}
333345

334-
// Initialize the unified policy resolver for both pre and post-evaluation filtering
335-
c.policyResolver = NewIncludeExcludePolicyResolver(source, p)
346+
// Initialize the policy resolver based on filter type
347+
switch filterType {
348+
case "ec-policy":
349+
c.policyResolver = NewECPolicyResolver(source, p)
350+
case "include-exclude":
351+
fallthrough
352+
default:
353+
c.policyResolver = NewIncludeExcludePolicyResolver(source, p)
354+
}
336355

337356
// Extract include/exclude criteria from the policy resolver to maintain backward compatibility
338357
// for the legacy isResultIncluded method
@@ -362,6 +381,12 @@ func NewConftestEvaluatorWithNamespace(ctx context.Context, policySources []sour
362381
return c, nil
363382
}
364383

384+
// set the policy namespace
385+
func NewConftestEvaluatorWithNamespace(ctx context.Context, policySources []source.PolicySource, p ConfigProvider, source ecc.Source, namespace []string) (Evaluator, error) {
386+
// Use default filter type (include-exclude) for backward compatibility
387+
return NewConftestEvaluatorWithNamespaceAndFilterType(ctx, policySources, p, source, namespace, "include-exclude")
388+
}
389+
365390
// Destroy removes the working directory
366391
func (c conftestEvaluator) Destroy() {
367392
if os.Getenv("EC_DEBUG") == "" {
@@ -526,10 +551,10 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
526551
filteredNamespaces = append(filteredNamespaces, pkg)
527552
}
528553

529-
log.Debugf("Policy resolution: %d packages included, %d packages excluded",
530-
len(policyResolution.IncludedPackages), len(policyResolution.ExcludedPackages))
531-
log.Debugf("Policy resolution details: included=%v, excluded=%v",
532-
policyResolution.IncludedPackages, policyResolution.ExcludedPackages)
554+
log.Debugf("Policy resolution: %d packages included",
555+
len(policyResolution.IncludedPackages))
556+
log.Debugf("Policy resolution details: included=%v",
557+
policyResolution.IncludedPackages)
533558
} else {
534559
// Legacy filtering approach - use the old namespace filtering logic
535560
// This ensures backward compatibility with existing tests

0 commit comments

Comments
 (0)