Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions internal/evaluator/conftest_evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -646,7 +646,7 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget

// Filter results using the unified filter
filteredResults, updatedMissingIncludes := unifiedFilter.FilterResults(
allResults, allRules, target.Target, missingIncludes, effectiveTime)
allResults, allRules, target.Target, target.ComponentName, missingIncludes, effectiveTime)

// Update missing includes
missingIncludes = updatedMissingIncludes
Expand All @@ -661,7 +661,7 @@ func (c conftestEvaluator) Evaluate(ctx context.Context, target EvaluationTarget
result.Skipped = skipped

// Replace the placeholder successes slice with the actual successes.
result.Successes = c.computeSuccesses(result, rules, target.Target, missingIncludes, unifiedFilter)
result.Successes = c.computeSuccesses(result, rules, target.Target, target.ComponentName, missingIncludes, unifiedFilter)

totalRules += len(result.Warnings) + len(result.Failures) + len(result.Successes)

Expand Down Expand Up @@ -798,7 +798,8 @@ func toRules(results []output.Result) []Result {
func (c conftestEvaluator) computeSuccesses(
result Outcome,
rules policyRules,
target string,
imageRef string,
componentName string,
missingIncludes map[string]bool,
unifiedFilter PostEvaluationFilter,
) []Result {
Expand Down Expand Up @@ -857,15 +858,15 @@ func (c conftestEvaluator) computeSuccesses(
if unifiedFilter != nil {
// Use the unified filter to check if this success should be included
filteredResults, _ := unifiedFilter.FilterResults(
[]Result{success}, rules, target, missingIncludes, time.Now())
[]Result{success}, rules, imageRef, componentName, missingIncludes, time.Now())

if len(filteredResults) == 0 {
log.Debugf("Skipping result success: %#v", success)
continue
}
} else {
// Fallback to legacy filtering for backward compatibility
if !c.isResultIncluded(success, target, missingIncludes) {
if !c.isResultIncluded(success, imageRef, componentName, missingIncludes) {
log.Debugf("Skipping result success: %#v", success)
continue
}
Expand Down Expand Up @@ -1120,10 +1121,10 @@ func isResultEffective(failure Result, now time.Time) bool {
// isResultIncluded returns whether or not the result should be included or
// discarded based on the policy configuration.
// 'missingIncludes' is a list of include directives that gets pruned if the result is matched
func (c conftestEvaluator) isResultIncluded(result Result, target string, missingIncludes map[string]bool) bool {
func (c conftestEvaluator) isResultIncluded(result Result, imageRef string, componentName string, missingIncludes map[string]bool) bool {
ruleMatchers := LegacyMakeMatchers(result)
includeScore := LegacyScoreMatches(ruleMatchers, c.include.get(target), missingIncludes)
excludeScore := LegacyScoreMatches(ruleMatchers, c.exclude.get(target), map[string]bool{})
includeScore := LegacyScoreMatches(ruleMatchers, c.include.get(imageRef, componentName), missingIncludes)
excludeScore := LegacyScoreMatches(ruleMatchers, c.exclude.get(imageRef, componentName), map[string]bool{})
return includeScore > excludeScore
}

Expand Down
201 changes: 201 additions & 0 deletions internal/evaluator/conftest_evaluator_integration_basic_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -139,3 +139,204 @@ deny contains result if {
require.NoError(t, err)
assert.NotNil(t, result)
}

func TestConftestEvaluatorIntegrationWithComponentNames(t *testing.T) {
ctx := context.Background()

// Create a temporary directory for the test
tmpDir := t.TempDir()
policyDir := filepath.Join(tmpDir, "policy")
err := os.MkdirAll(policyDir, 0o755)
require.NoError(t, err)

// Create policies that will be filtered by ComponentNames
policyContent := `package test

import rego.v1

# METADATA
# title: Check A
# custom:
# short_name: check_a
deny contains result if {
result := {
"code": "test.check_a",
"msg": "Check A always fails"
}
}

# METADATA
# title: Check B
# custom:
# short_name: check_b
deny contains result if {
result := {
"code": "test.check_b",
"msg": "Check B always fails"
}
}
`
err = os.WriteFile(filepath.Join(policyDir, "policy.rego"), []byte(policyContent), 0o600)
require.NoError(t, err)

// Create policy source
policySource := &source.PolicyUrl{
Url: "file://" + policyDir,
Kind: source.PolicyKind,
}

// Create config provider with ComponentNames filter
configProvider := &mockConfigProvider{}
configProvider.On("EffectiveTime").Return(time.Date(2024, 6, 1, 0, 0, 0, 0, time.UTC))
configProvider.On("SigstoreOpts").Return(policy.SigstoreOpts{}, nil)
configProvider.On("Spec").Return(ecc.EnterpriseContractPolicySpec{
Sources: []ecc.Source{
{
Policy: []string{"file://" + policyDir},
},
},
})

// Create evaluator with VolatileConfig that excludes check_a for comp1
evaluator, err := NewConftestEvaluator(ctx, []source.PolicySource{policySource}, configProvider, ecc.Source{
VolatileConfig: &ecc.VolatileSourceConfig{
Exclude: []ecc.VolatileCriteria{
{
Value: "test.check_a",
ComponentNames: []string{"comp1"},
EffectiveOn: "2024-01-01T00:00:00Z",
EffectiveUntil: "2025-01-01T00:00:00Z",
},
},
},
})
require.NoError(t, err)
defer evaluator.Destroy()

// Debug: Check exclude criteria
conftestEval := evaluator.(conftestEvaluator)
t.Logf("Exclude componentItems: %+v", conftestEval.exclude.componentItems)
t.Logf("Exclude defaultItems: %+v", conftestEval.exclude.defaultItems)
t.Logf("Exclude digestItems: %+v", conftestEval.exclude.digestItems)

// Create test input
inputData := map[string]interface{}{
"test": "value",
}
inputBytes, err := json.Marshal(inputData)
require.NoError(t, err)
inputPath := filepath.Join(tmpDir, "input.json")
err = os.WriteFile(inputPath, inputBytes, 0o600)
require.NoError(t, err)

// Test comp1 - check_a should be excluded
target1 := EvaluationTarget{
Inputs: []string{inputPath},
Target: "quay.io/repo/img@sha256:1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef",
ComponentName: "comp1",
}

result1, err := evaluator.Evaluate(ctx, target1)
require.NoError(t, err)
require.NotNil(t, result1)

// Debug: Print all failures
t.Logf("comp1 results: %d outcomes", len(result1))
for i, outcome := range result1 {
t.Logf(" Outcome %d: %d failures, %d successes", i, len(outcome.Failures), len(outcome.Successes))
for _, failure := range outcome.Failures {
t.Logf(" Failure: %s", failure.Metadata["code"])
}
for _, success := range outcome.Successes {
t.Logf(" Success: %s", success.Metadata["code"])
}
}

// Verify check_a is excluded, check_b is not
hasCheckA := false
hasCheckB := false
for _, outcome := range result1 {
for _, failure := range outcome.Failures {
if codeStr, ok := failure.Metadata["code"].(string); ok {
if codeStr == "test.check_a" {
hasCheckA = true
}
if codeStr == "test.check_b" {
hasCheckB = true
}
}
}
}
assert.False(t, hasCheckA, "Expected check_a to be excluded for comp1")
assert.True(t, hasCheckB, "Expected check_b to be evaluated for comp1")

// Test comp2 - check_a should NOT be excluded
target2 := EvaluationTarget{
Inputs: []string{inputPath},
Target: "quay.io/repo/img@sha256:abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890",
ComponentName: "comp2",
}

result2, err := evaluator.Evaluate(ctx, target2)
require.NoError(t, err)
require.NotNil(t, result2)

// Verify both checks are evaluated for comp2
hasCheckA2 := false
hasCheckB2 := false
for _, outcome := range result2 {
for _, failure := range outcome.Failures {
if codeStr, ok := failure.Metadata["code"].(string); ok {
if codeStr == "test.check_a" {
hasCheckA2 = true
}
if codeStr == "test.check_b" {
hasCheckB2 = true
}
}
}
}
assert.True(t, hasCheckA2, "Expected check_a to be evaluated for comp2")
assert.True(t, hasCheckB2, "Expected check_b to be evaluated for comp2")

// Test same image with different components - monorepo scenario
sameImage := "quay.io/monorepo@sha256:fedcba0987654321fedcba0987654321fedcba0987654321fedcba0987654321"

target3 := EvaluationTarget{
Inputs: []string{inputPath},
Target: sameImage,
ComponentName: "comp1",
}

result3, err := evaluator.Evaluate(ctx, target3)
require.NoError(t, err)

hasCheckA3 := false
for _, outcome := range result3 {
for _, failure := range outcome.Failures {
if codeStr, ok := failure.Metadata["code"].(string); ok && codeStr == "test.check_a" {
hasCheckA3 = true
}
}
}
assert.False(t, hasCheckA3, "Expected check_a excluded for comp1 even with different image")

target4 := EvaluationTarget{
Inputs: []string{inputPath},
Target: sameImage,
ComponentName: "comp2",
}

result4, err := evaluator.Evaluate(ctx, target4)
require.NoError(t, err)

hasCheckA4 := false
for _, outcome := range result4 {
for _, failure := range outcome.Failures {
if codeStr, ok := failure.Metadata["code"].(string); ok && codeStr == "test.check_a" {
hasCheckA4 = true
}
}
}
assert.True(t, hasCheckA4, "Expected check_a evaluated for comp2 with same image")
}
39 changes: 32 additions & 7 deletions internal/evaluator/criteria.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,17 +28,23 @@
// 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
type Criteria struct {
digestItems map[string][]string
defaultItems []string
digestItems map[string][]string
componentItems map[string][]string
defaultItems []string
}

func (c *Criteria) len() int {
totalLength := len(c.defaultItems)
for _, items := range c.digestItems {
totalLength += len(items)
}
for _, items := range c.componentItems {
totalLength += len(items)
}
return totalLength
}

Expand All @@ -64,12 +70,20 @@
}
}

// This accepts an image ref with digest
// and looks up the image url and digest separately.
func (c *Criteria) get(key string) []string {
ref, err := name.ParseReference(key)
func (c *Criteria) addComponentItem(componentName, value string) {
if c.componentItems == nil {
c.componentItems = make(map[string][]string)
}
c.componentItems[componentName] = append(c.componentItems[componentName], value)
}

// This accepts an image ref with digest and optional component name,
// and looks up the image url, digest, and component name separately.
func (c *Criteria) get(imageRef string, componentName string) []string {
ref, err := name.ParseReference(imageRef)
if err != nil {
log.Debugf("error parsing target image url: %q", key)
log.Debugf("error parsing target image url: %q", imageRef)
// Return only global defaults if image ref is invalid
return c.defaultItems
}

Expand All @@ -87,6 +101,13 @@
items = append(items, c.getWithKey(k)...)
}

// Add component-specific items if component name is provided
if componentName != "" {
if componentItems, ok := c.componentItems[componentName]; ok {
items = append(items, componentItems...)
}
}

// Add any exceptions that pertain to all images.
return append(items, c.defaultItems...)
}
Expand Down Expand Up @@ -158,6 +179,10 @@
items.addItem(c.ImageUrl, c.Value)
} else if c.ImageDigest != "" {
items.addItem(c.ImageDigest, c.Value)
} else if len(c.ComponentNames) > 0 {

Check failure on line 182 in internal/evaluator/criteria.go

View workflow job for this annotation

GitHub Actions / Lint

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)

Check failure on line 182 in internal/evaluator/criteria.go

View workflow job for this annotation

GitHub Actions / Lint

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)

Check failure on line 182 in internal/evaluator/criteria.go

View workflow job for this annotation

GitHub Actions / Lint

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)

Check failure on line 182 in internal/evaluator/criteria.go

View workflow job for this annotation

GitHub Actions / Lint

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)

Check failure on line 182 in internal/evaluator/criteria.go

View workflow job for this annotation

GitHub Actions / Acceptance

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)

Check failure on line 182 in internal/evaluator/criteria.go

View check run for this annotation

Red Hat Konflux / Red Hat Konflux / cli-main-on-pull-request

internal/evaluator/criteria.go#L182

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)
for _, componentName := range c.ComponentNames {

Check failure on line 183 in internal/evaluator/criteria.go

View workflow job for this annotation

GitHub Actions / Lint

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)) (typecheck)

Check failure on line 183 in internal/evaluator/criteria.go

View workflow job for this annotation

GitHub Actions / Lint

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)) (typecheck)

Check failure on line 183 in internal/evaluator/criteria.go

View workflow job for this annotation

GitHub Actions / Lint

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)) (typecheck)

Check failure on line 183 in internal/evaluator/criteria.go

View workflow job for this annotation

GitHub Actions / Acceptance

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)

Check failure on line 183 in internal/evaluator/criteria.go

View check run for this annotation

Red Hat Konflux / Red Hat Konflux / cli-main-on-pull-request

internal/evaluator/criteria.go#L183

c.ComponentNames undefined (type "github.com/conforma/crds/api/v1alpha1".VolatileCriteria has no field or method ComponentNames)
items.addComponentItem(string(componentName), c.Value)
}
} else {
items.addItem("", c.Value)
}
Expand Down
Loading
Loading