Skip to content
Merged
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
6 changes: 4 additions & 2 deletions pkg/policies/engine/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,10 @@ type PolicyEngine interface {
Verify(ctx context.Context, policy *Policy, input []byte, args map[string]any) (*EvaluationResult, error)
// MatchesParameters evaluates the matches_parameters rule to determine if evaluation parameters match expected parameters
MatchesParameters(ctx context.Context, policy *Policy, evaluationParams, expectedParams map[string]string) (bool, error)
// MatchesEvaluation evaluates the matches_evaluation rule using policy violations and expected parameters
MatchesEvaluation(ctx context.Context, policy *Policy, violations []string, expectedParams map[string]string) (bool, error)
// MatchesEvaluation evaluates the matches_evaluation rule using the policy violation messages,
// the structured findings of those violations and the expected parameters.
// findings is nil when the violations carry no structured findings.
MatchesEvaluation(ctx context.Context, policy *Policy, violations []string, findings []map[string]any, expectedParams map[string]string) (bool, error)
Comment thread
javirln marked this conversation as resolved.
}

type EvaluationResult struct {
Expand Down
14 changes: 11 additions & 3 deletions pkg/policies/engine/rego/rego.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,7 @@ const (
inputArgs = "args"
expectedArgs = "expected_args"
violationsResult = "violations"
findingsResult = "findings"
inputElements = "elements"
mainRule = "result"
matchesParametersRule = "matches_parameters"
Expand Down Expand Up @@ -418,15 +419,17 @@ func (r *Engine) MatchesParameters(ctx context.Context, policy *engine.Policy, e
}

// MatchesEvaluation evaluates the matches_evaluation rule in a rego policy.
// Creates an input object with expected parameters and policy violations.
func (r *Engine) MatchesEvaluation(ctx context.Context, policy *engine.Policy, violations []string, expectedParams map[string]string) (bool, error) {
// Creates an input object with expected parameters, policy violations and the
// structured findings of those violations. A nil list reaches the rule as an empty
// array, never as null, so that a rule that counts it raises no strict builtin error.
func (r *Engine) MatchesEvaluation(ctx context.Context, policy *engine.Policy, violations []string, findings []map[string]any, expectedParams map[string]string) (bool, error) {
Comment thread
javirln marked this conversation as resolved.
policyString := string(policy.Source)
parsedModule, err := ast.ParseModule(policy.Name, policyString)
if err != nil {
return false, fmt.Errorf("failed to parse rego policy: %w", sanitizeParseError(err))
}

// Create input expected parameters and policy violations
// Create input expected parameters, policy violations and findings
inputMap := make(map[string]interface{})
if expectedParams == nil {
inputMap[expectedArgs] = map[string]string{}
Expand All @@ -438,6 +441,11 @@ func (r *Engine) MatchesEvaluation(ctx context.Context, policy *engine.Policy, v
} else {
inputMap[violationsResult] = violations
}
if findings == nil {
inputMap[findingsResult] = []map[string]any{}
} else {
inputMap[findingsResult] = findings
}
decodedInput := r.injectProjectMetadata(inputMap)

// Evaluate matches_evaluation rule
Expand Down
130 changes: 124 additions & 6 deletions pkg/policies/engine/rego/rego_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ func TestRego_ParseErrorOmitsSource(t *testing.T) {
{
name: "matches evaluation",
run: func() error {
_, err := r.MatchesEvaluation(context.TODO(), policy, nil, nil)
_, err := r.MatchesEvaluation(context.TODO(), policy, nil, nil, nil)
return err
},
},
Expand Down Expand Up @@ -448,43 +448,161 @@ func TestRego_MatchesEvaluation(t *testing.T) {
t.Run("evaluation with violations and high severity does not match", func(t *testing.T) {
violations := []string{"test violation"}
evaluationParams := map[string]string{"severity": "high"}
matches, err := r.MatchesEvaluation(context.TODO(), policy, violations, evaluationParams)
matches, err := r.MatchesEvaluation(context.TODO(), policy, violations, nil, evaluationParams)
require.NoError(t, err)
assert.False(t, matches)
})

t.Run("evaluation without violations does matches", func(t *testing.T) {
violations := []string{}
evaluationParams := map[string]string{"severity": "high"}
matches, err := r.MatchesEvaluation(context.TODO(), policy, violations, evaluationParams)
matches, err := r.MatchesEvaluation(context.TODO(), policy, violations, nil, evaluationParams)
require.NoError(t, err)
assert.False(t, matches)
})

t.Run("evaluation with violations but wrong severity does match", func(t *testing.T) {
violations := []string{"test violation"}
evaluationParams := map[string]string{"severity": "low"}
matches, err := r.MatchesEvaluation(context.TODO(), policy, violations, evaluationParams)
matches, err := r.MatchesEvaluation(context.TODO(), policy, violations, nil, evaluationParams)
require.NoError(t, err)
assert.False(t, matches)
})

t.Run("nil evaluation matches", func(t *testing.T) {
evaluationParams := map[string]string{"severity": "high"}
matches, err := r.MatchesEvaluation(context.TODO(), policy, nil, evaluationParams)
matches, err := r.MatchesEvaluation(context.TODO(), policy, nil, nil, evaluationParams)
require.NoError(t, err)
assert.False(t, matches)
})

t.Run("empty evaluation params", func(t *testing.T) {
violations := []string{"test violation"}
evaluationParams := map[string]string{}
matches, err := r.MatchesEvaluation(context.TODO(), policy, violations, evaluationParams)
matches, err := r.MatchesEvaluation(context.TODO(), policy, violations, nil, evaluationParams)
require.NoError(t, err)
assert.False(t, matches)
})
}

func TestRego_MatchesEvaluationFindings(t *testing.T) {
findingsRule, err := os.ReadFile("testfiles/matches_evaluation_findings.rego")
require.NoError(t, err)

// Matches only when input.findings is an empty array. A null value does not match.
emptyArrayRule := []byte(`package test_findings_shape

import rego.v1

default matches_evaluation := false

matches_evaluation if {
is_array(input.findings)
count(input.findings) == 0
}
`)

// Reads only the messages, as every rule written before findings reached the input.
violationsOnlyRule := []byte(`package test_violations_only

import rego.v1

default matches_evaluation := true

matches_evaluation := false if {
count(input.violations) > 0
every v in input.violations {
not contains(v, "HIGH")
}
}
`)

const score = "score"
scoreThreshold := map[string]string{score: "7.0"}
oneViolation := []string{"first"}
twoViolations := []string{"first", "second"}

testCases := []struct {
name string
source []byte
violations []string
findings []map[string]any
expected map[string]string
want bool
}{
{
name: "a finding at the expected score matches",
source: findingsRule,
violations: twoViolations,
findings: []map[string]any{
{score: 3.0},
{score: 9.8, "location": map[string]any{"file": "main.go", "line": 7}},
},
expected: scoreThreshold,
want: true,
},
{
name: "findings below the expected score do not match",
source: findingsRule,
violations: twoViolations,
findings: []map[string]any{{score: 3.0}, {score: 6.5}},
expected: scoreThreshold,
want: false,
},
{
name: "nil findings raise no strict builtin error",
source: findingsRule,
violations: oneViolation,
expected: scoreThreshold,
want: true,
},
{
name: "empty findings raise no strict builtin error",
source: findingsRule,
violations: oneViolation,
findings: []map[string]any{},
expected: scoreThreshold,
want: true,
},
{
name: "nil findings reach the rule as an empty array",
source: emptyArrayRule,
violations: oneViolation,
want: true,
},
{
name: "findings reach the rule as an array",
source: emptyArrayRule,
violations: oneViolation,
findings: []map[string]any{{score: 9.8}},
want: false,
},
{
name: "a rule that reads only the messages ignores the findings",
source: violationsOnlyRule,
violations: []string{"LOW issue"},
findings: []map[string]any{{score: 9.8}},
want: false,
},
{
name: "a rule that reads only the messages still matches on them",
source: violationsOnlyRule,
violations: []string{"HIGH issue"},
want: true,
},
}

r := NewEngine()
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
policy := &engine.Policy{Name: "matches-evaluation-findings", Source: tc.source}
matches, err := r.MatchesEvaluation(context.TODO(), policy, tc.violations, tc.findings, tc.expected)
require.NoError(t, err)
assert.Equal(t, tc.want, matches)
})
}
}

func TestRego_CustomBuiltinsPermissiveMode(t *testing.T) {
regoContent, err := os.ReadFile("testfiles/custom_builtin_permissive.rego")
require.NoError(t, err)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package test_matches_evaluation_findings

import rego.v1

default matches_evaluation := true

# count() raises a strict builtin error when input.findings is null.
matches_evaluation := false if {
count(input.findings) > 0
not any_finding_reaches(to_number(input.expected_args.score))
}

any_finding_reaches(expected) if {
some f in input.findings
f.score >= expected
}
2 changes: 1 addition & 1 deletion pkg/policies/engine/wasm/engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ func (e *Engine) MatchesParameters(_ context.Context, _ *engine.Policy,
// MatchesEvaluation is a stub implementation for WASM policies
// WASM policies don't currently support evaluation matching
func (e *Engine) MatchesEvaluation(_ context.Context, _ *engine.Policy,
_ []string, _ map[string]string) (bool, error) {
_ []string, _ []map[string]any, _ map[string]string) (bool, error) {
// Default to true - WASM policies handle their own evaluation logic
return true, nil
}
4 changes: 2 additions & 2 deletions pkg/policies/engine/wasm/engine_test.go
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// Copyright 2025 The Chainloop Authors.
// Copyright 2025-2026 The Chainloop Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -108,7 +108,7 @@ func TestMatchesEvaluation(t *testing.T) {
}

// Should always return true for WASM policies
matches, err := eng.MatchesEvaluation(ctx, policy, []string{"violation"}, map[string]string{"key": "value"})
matches, err := eng.MatchesEvaluation(ctx, policy, []string{"violation"}, []map[string]any{{"severity": "HIGH"}}, map[string]string{"key": "value"})
assert.NoError(t, err)
assert.True(t, matches)
}
Expand Down
Loading