Skip to content

Commit 86b1494

Browse files
committed
feat: implement pluggable rule filtering system with pipeline intentions
- Add extensible filtering framework with RuleFilter interface - Implement PipelineIntentionFilter for filtering rules by pipeline intentions - Implement IncludeListFilter for filtering by collections, packages, and rules - Add pipeline_intention metadata field to rule annotations - Fix filtering leak by ensuring AllNamespaces=false prevents bypassing filters - Add comprehensive test coverage for all filtering components - Maintain backward compatibility with existing filtering logic The new system allows for flexible rule filtering based on pipeline intentions and include lists, while preventing any evaluation of excluded namespaces regardless of filtering results. Assisted by: Cursor (powered by Claude 4 Sonnet) https://issues.redhat.com/browse/EC-1401
1 parent 18301a2 commit 86b1494

File tree

9 files changed

+1952
-313
lines changed

9 files changed

+1952
-313
lines changed

.cursor/rules/package_filtering_process.mdc

Lines changed: 534 additions & 0 deletions
Large diffs are not rendered by default.

internal/evaluator/conftest_evaluator.go

Lines changed: 85 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -167,17 +167,18 @@ type testRunner interface {
167167
}
168168

169169
const (
170-
effectiveOnFormat = "2006-01-02T15:04:05Z"
171-
effectiveOnTimeout = -90 * 24 * time.Hour // keep effective_on metadata up to 90 days
172-
metadataCode = "code"
173-
metadataCollections = "collections"
174-
metadataDependsOn = "depends_on"
175-
metadataDescription = "description"
176-
metadataSeverity = "severity"
177-
metadataEffectiveOn = "effective_on"
178-
metadataSolution = "solution"
179-
metadataTerm = "term"
180-
metadataTitle = "title"
170+
effectiveOnFormat = "2006-01-02T15:04:05Z"
171+
effectiveOnTimeout = -90 * 24 * time.Hour // keep effective_on metadata up to 90 days
172+
metadataCode = "code"
173+
metadataCollections = "collections"
174+
metadataDependsOn = "depends_on"
175+
metadataDescription = "description"
176+
metadataPipelineIntention = "pipeline_intention"
177+
metadataSeverity = "severity"
178+
metadataEffectiveOn = "effective_on"
179+
metadataSolution = "solution"
180+
metadataTerm = "term"
181+
metadataTitle = "title"
181182
)
182183

183184
const (
@@ -205,6 +206,7 @@ type conftestEvaluator struct {
205206
exclude *Criteria
206207
fs afero.Fs
207208
namespace []string
209+
source ecc.Source
208210
}
209211

210212
type conftestRunner struct {
@@ -285,7 +287,7 @@ func (r conftestRunner) Run(ctx context.Context, fileList []string) (result []Ou
285287
// NewConftestEvaluator returns initialized conftestEvaluator implementing
286288
// Evaluator interface
287289
func NewConftestEvaluator(ctx context.Context, policySources []source.PolicySource, p ConfigProvider, source ecc.Source) (Evaluator, error) {
288-
return NewConftestEvaluatorWithNamespace(ctx, policySources, p, source, nil)
290+
return NewConftestEvaluatorWithNamespace(ctx, policySources, p, source, []string{})
289291
}
290292

291293
// set the policy namespace
@@ -302,9 +304,11 @@ func NewConftestEvaluatorWithNamespace(ctx context.Context, policySources []sour
302304
policy: p,
303305
fs: fs,
304306
namespace: namespace,
307+
source: source,
305308
}
306309

307310
c.include, c.exclude = computeIncludeExclude(source, p)
311+
308312
dir, err := utils.CreateWorkDir(fs)
309313
if err != nil {
310314
log.Debug("Failed to create work dir!")
@@ -341,6 +345,9 @@ func (c conftestEvaluator) CapabilitiesPath() string {
341345

342346
type policyRules map[string]rule.Info
343347

348+
// Add a new type to track non-annotated rules separately
349+
type nonAnnotatedRules map[string]bool
350+
344351
func (r *policyRules) collect(a *ast.AnnotationsRef) error {
345352
if a.Annotations == nil {
346353
return nil
@@ -377,6 +384,8 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
377384
// exist with the same code in two separate sources the collected rule
378385
// information is not deterministic
379386
rules := policyRules{}
387+
// Track non-annotated rules separately for filtering purposes only
388+
nonAnnotatedRules := nonAnnotatedRules{}
380389
// Download all sources
381390
for _, s := range c.policySources {
382391
dir, err := s.GetPolicy(ctx, c.workDir, false)
@@ -414,32 +423,85 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
414423
}
415424
}
416425

426+
// Collect ALL rules for filtering purposes - both with and without annotations
427+
// This ensures that rules without metadata (like fail_with_data.rego) are properly included
417428
for _, a := range annotations {
418-
if a.Annotations == nil {
419-
continue
429+
if a.Annotations != nil {
430+
// Rules with annotations - collect full metadata
431+
if err := rules.collect(a); err != nil {
432+
return nil, err
433+
}
434+
} else {
435+
// Rules without annotations - track for filtering only, not for success computation
436+
ruleRef := a.GetRule()
437+
if ruleRef != nil {
438+
// Extract package name from the rule path
439+
packageName := ""
440+
if len(a.Path) > 1 {
441+
// Path format is typically ["data", "package", "rule"]
442+
// We want the package part (index 1)
443+
if len(a.Path) >= 2 {
444+
packageName = strings.ReplaceAll(a.Path[1].String(), `"`, "")
445+
}
446+
}
447+
448+
// Extract short name from the rule head
449+
shortName := ruleRef.Head.Name.String()
450+
451+
// Generate code for filtering purposes
452+
code := fmt.Sprintf("%s.%s", packageName, shortName)
453+
454+
// Track for filtering but don't add to rules map for success computation
455+
nonAnnotatedRules[code] = true
456+
}
420457
}
421-
if err := rules.collect(a); err != nil {
422-
return nil, err
458+
}
459+
}
460+
461+
// Filter namespaces using the new pluggable filtering system
462+
filterFactory := NewIncludeFilterFactory()
463+
filters := filterFactory.CreateFilters(c.source)
464+
// Combine annotated and non-annotated rules for filtering
465+
allRules := make(policyRules)
466+
for code, rule := range rules {
467+
allRules[code] = rule
468+
}
469+
// Add non-annotated rules as minimal rule.Info for filtering
470+
for code := range nonAnnotatedRules {
471+
parts := strings.Split(code, ".")
472+
if len(parts) >= 2 {
473+
packageName := parts[len(parts)-2]
474+
shortName := parts[len(parts)-1]
475+
allRules[code] = rule.Info{
476+
Code: code,
477+
Package: packageName,
478+
ShortName: shortName,
423479
}
424480
}
425481
}
482+
filteredNamespaces := filterNamespaces(allRules, filters...)
426483

427484
var r testRunner
428485
var ok bool
429486
if r, ok = ctx.Value(runnerKey).(testRunner); r == nil || !ok {
430487

431-
// should there be a namespace defined or not
432-
allNamespaces := true
433-
if len(c.namespace) > 0 {
434-
allNamespaces = false
488+
// Determine which namespaces to use
489+
namespacesToUse := c.namespace
490+
491+
// If we have filtered namespaces from the filtering system, use those
492+
if len(filteredNamespaces) > 0 {
493+
namespacesToUse = filteredNamespaces
435494
}
436495

496+
// log the namespaces to use
497+
log.Debugf("Namespaces to use: %v", namespacesToUse)
498+
437499
r = &conftestRunner{
438500
runner.TestRunner{
439501
Data: []string{c.dataDir},
440502
Policy: []string{c.policyDir},
441-
Namespace: c.namespace,
442-
AllNamespaces: allNamespaces,
503+
Namespace: namespacesToUse,
504+
AllNamespaces: false, // Always false to prevent bypassing filtering
443505
NoFail: true,
444506
Output: c.outputFormat,
445507
Capabilities: c.CapabilitiesPath(),
@@ -504,6 +566,7 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
504566

505567
for i := range result.Failures {
506568
failure := result.Failures[i]
569+
// log the failure
507570
addRuleMetadata(ctx, &failure, rules)
508571

509572
if !c.isResultIncluded(failure, target.Target, missingIncludes) {

0 commit comments

Comments
 (0)