From d8cd98814b60229498a90f07b6ac49d669c30985 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Tue, 4 Nov 2025 13:18:45 +0100 Subject: [PATCH 1/5] inject boilerplate policy code before evaluation Signed-off-by: Sylwester Piskozub --- app/cli/internal/policydevel/eval_test.go | 64 +++++++ app/cli/internal/policydevel/lint.go | 30 +--- app/cli/internal/policydevel/lint_test.go | 33 ---- .../policydevel/templates/example-policy.rego | 35 +--- .../sbom-metadata-component-policy.yaml | 17 ++ .../testdata/sbom-min-components-policy.yaml | 17 ++ .../testdata/sbom-multiple-checks-policy.yaml | 27 +++ .../testdata/sbom-valid-cyclonedx-policy.yaml | 22 +++ .../policydevel/testdata/test-sbom.json | 18 ++ pkg/policies/engine/rego/boilerplate.go | 161 ++++++++++++++++++ .../engine/rego/boilerplate.rego.tmpl | 22 +++ pkg/policies/engine/rego/boilerplate_test.go | 119 +++++++++++++ .../rego/testdata/custom-valid-input.rego | 12 ++ .../engine/rego/testdata/detect-rules.rego | 11 ++ .../rego/testdata/full-boilerplate.rego | 26 +++ .../rego/testdata/multiple-imports.rego | 9 + .../rego/testdata/only-package-import.rego | 3 + .../output/custom-valid-input-output.rego | 29 ++++ .../output/full-boilerplate-output.rego | 26 +++ .../output/multiple-imports-output.rego | 28 +++ .../output/only-package-import-output.rego | 23 +++ .../output/partial-boilerplate-output.rego | 26 +++ .../output/simplified-policy-output.rego | 32 ++++ .../source-commit-simplified-output.rego | 54 ++++++ .../testdata/output/with-comments-output.rego | 35 ++++ .../rego/testdata/partial-boilerplate.rego | 11 ++ .../rego/testdata/simplified-policy.rego | 13 ++ .../testdata/source-commit-simplified.rego | 35 ++++ .../engine/rego/testdata/with-comments.rego | 16 ++ pkg/policies/policies.go | 19 +++ 30 files changed, 878 insertions(+), 95 deletions(-) create mode 100644 app/cli/internal/policydevel/testdata/sbom-metadata-component-policy.yaml create mode 100644 app/cli/internal/policydevel/testdata/sbom-min-components-policy.yaml create mode 100644 app/cli/internal/policydevel/testdata/sbom-multiple-checks-policy.yaml create mode 100644 app/cli/internal/policydevel/testdata/sbom-valid-cyclonedx-policy.yaml create mode 100644 app/cli/internal/policydevel/testdata/test-sbom.json create mode 100644 pkg/policies/engine/rego/boilerplate.go create mode 100644 pkg/policies/engine/rego/boilerplate.rego.tmpl create mode 100644 pkg/policies/engine/rego/boilerplate_test.go create mode 100644 pkg/policies/engine/rego/testdata/custom-valid-input.rego create mode 100644 pkg/policies/engine/rego/testdata/detect-rules.rego create mode 100644 pkg/policies/engine/rego/testdata/full-boilerplate.rego create mode 100644 pkg/policies/engine/rego/testdata/multiple-imports.rego create mode 100644 pkg/policies/engine/rego/testdata/only-package-import.rego create mode 100644 pkg/policies/engine/rego/testdata/output/custom-valid-input-output.rego create mode 100644 pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego create mode 100644 pkg/policies/engine/rego/testdata/output/multiple-imports-output.rego create mode 100644 pkg/policies/engine/rego/testdata/output/only-package-import-output.rego create mode 100644 pkg/policies/engine/rego/testdata/output/partial-boilerplate-output.rego create mode 100644 pkg/policies/engine/rego/testdata/output/simplified-policy-output.rego create mode 100644 pkg/policies/engine/rego/testdata/output/source-commit-simplified-output.rego create mode 100644 pkg/policies/engine/rego/testdata/output/with-comments-output.rego create mode 100644 pkg/policies/engine/rego/testdata/partial-boilerplate.rego create mode 100644 pkg/policies/engine/rego/testdata/simplified-policy.rego create mode 100644 pkg/policies/engine/rego/testdata/source-commit-simplified.rego create mode 100644 pkg/policies/engine/rego/testdata/with-comments.rego diff --git a/app/cli/internal/policydevel/eval_test.go b/app/cli/internal/policydevel/eval_test.go index ac62bee3b..cc45e2fa1 100644 --- a/app/cli/internal/policydevel/eval_test.go +++ b/app/cli/internal/policydevel/eval_test.go @@ -124,3 +124,67 @@ func TestEvaluate(t *testing.T) { assert.Contains(t, err.Error(), "invalid material kind") }) } + +func TestEvaluateSimplifiedPolicies(t *testing.T) { + tempDir := t.TempDir() + logger := zerolog.New(os.Stderr) + + sbomContent, err := os.ReadFile("testdata/test-sbom.json") + require.NoError(t, err) + sbomPath := filepath.Join(tempDir, "test-sbom.json") + require.NoError(t, os.WriteFile(sbomPath, sbomContent, 0600)) + + t.Run("sbom min components policy", func(t *testing.T) { + opts := &EvalOptions{ + PolicyPath: "testdata/sbom-min-components-policy.yaml", + MaterialPath: sbomPath, + } + + result, err := Evaluate(opts, logger) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.Result.Skipped) + assert.Len(t, result.Result.Violations, 1) + assert.Contains(t, result.Result.Violations[0], "at least 2 components") + }) + + t.Run("sbom metadata component policy", func(t *testing.T) { + opts := &EvalOptions{ + PolicyPath: "testdata/sbom-metadata-component-policy.yaml", + MaterialPath: sbomPath, + } + + result, err := Evaluate(opts, logger) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.Result.Skipped) + assert.Len(t, result.Result.Violations, 0) + }) + + t.Run("sbom valid cyclonedx policy", func(t *testing.T) { + opts := &EvalOptions{ + PolicyPath: "testdata/sbom-valid-cyclonedx-policy.yaml", + MaterialPath: sbomPath, + } + + result, err := Evaluate(opts, logger) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.Result.Skipped) + assert.Len(t, result.Result.Violations, 0) + }) + + t.Run("sbom multiple checks policy", func(t *testing.T) { + opts := &EvalOptions{ + PolicyPath: "testdata/sbom-multiple-checks-policy.yaml", + MaterialPath: sbomPath, + } + + result, err := Evaluate(opts, logger) + require.NoError(t, err) + require.NotNil(t, result) + assert.False(t, result.Result.Skipped) + assert.Len(t, result.Result.Violations, 1) + assert.Contains(t, result.Result.Violations[0], "too few components") + }) +} diff --git a/app/cli/internal/policydevel/lint.go b/app/cli/internal/policydevel/lint.go index 76b57e854..87a5d1d59 100644 --- a/app/cli/internal/policydevel/lint.go +++ b/app/cli/internal/policydevel/lint.go @@ -22,7 +22,6 @@ import ( "fmt" "os" "path/filepath" - "regexp" "strings" v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1" @@ -208,10 +207,7 @@ func (p *PolicyToLint) validateAndFormatRego(content, path string) string { content = formatted } - // 2. Structural validation - p.checkResultStructure(content, path, []string{"skipped", "violations", "skip_reason"}) - - // 3. Run Regal linter + // 2. Run Regal linter p.runRegalLinter(path, content) return content @@ -226,30 +222,6 @@ func (p *PolicyToLint) applyOPAFmt(content, file string) string { return string(formatted) } -func (p *PolicyToLint) checkResultStructure(content, path string, keys []string) { - // Regex to capture result := { ... } including multiline - re := regexp.MustCompile(`(?s)result\s*:=\s*\{(.+?)\}`) - match := re.FindStringSubmatch(content) - if match == nil { - p.AddError(path, "no result literal found", 0) - return - } - - body := match[1] - // Find quoted keys inside the object literal - keyRe := regexp.MustCompile(`"([^"]+)"\s*:`) - found := make(map[string]bool) - for _, m := range keyRe.FindAllStringSubmatch(body, -1) { - found[m[1]] = true - } - - for _, want := range keys { - if !found[want] { - p.AddError(path, fmt.Sprintf("missing %q key in result", want), 0) - } - } -} - // Runs the Regal linter on the given rego content and records any violations func (p *PolicyToLint) runRegalLinter(filePath, content string) { inputModules, err := rules.InputFromText(filePath, content) diff --git a/app/cli/internal/policydevel/lint_test.go b/app/cli/internal/policydevel/lint_test.go index 0bac2c6bb..7d665def6 100644 --- a/app/cli/internal/policydevel/lint_test.go +++ b/app/cli/internal/policydevel/lint_test.go @@ -121,39 +121,6 @@ func TestPolicyToLint_processFile(t *testing.T) { }) } -func TestPolicyToLint_checkResultStructure(t *testing.T) { - t.Run("valid result structure", func(t *testing.T) { - policy := &PolicyToLint{} - content, err := os.ReadFile("testdata/valid.rego") - require.NoError(t, err) - policy.checkResultStructure(string(content), "test.rego", []string{"violations", "skip_reason", "skipped"}) - assert.False(t, policy.HasErrors()) - }) - - t.Run("missing result literal", func(t *testing.T) { - policy := &PolicyToLint{} - content := `package main - -output := { - "violations": [] -}` - policy.checkResultStructure(content, "test.rego", []string{"violations"}) - assert.True(t, policy.HasErrors()) - assert.Contains(t, policy.Errors[0].Message, "no result literal found") - }) - - t.Run("missing required keys", func(t *testing.T) { - policy := &PolicyToLint{} - content, err := os.ReadFile("testdata/missing-keys.rego") - require.NoError(t, err) - policy.checkResultStructure(string(content), "test.rego", []string{"violations", "skip_reason", "skipped"}) - assert.True(t, policy.HasErrors()) - assert.Len(t, policy.Errors, 2) - assert.Contains(t, policy.Errors[0].Message, `missing "skip_reason" key`) - assert.Contains(t, policy.Errors[1].Message, `missing "skipped" key`) - }) -} - func TestPolicyToLint_formatViolationError(t *testing.T) { policy := &PolicyToLint{} diff --git a/app/cli/internal/policydevel/templates/example-policy.rego b/app/cli/internal/policydevel/templates/example-policy.rego index bf11368ab..f1d68d7b1 100644 --- a/app/cli/internal/policydevel/templates/example-policy.rego +++ b/app/cli/internal/policydevel/templates/example-policy.rego @@ -2,42 +2,11 @@ package main import rego.v1 -################################ -# Common section do NOT change # -################################ - -result := { - "skipped": skipped, - "violations": violations, - "skip_reason": skip_reason, - "ignore": ignore, -} - -default skip_reason := "" - -skip_reason := m if { - not valid_input - m := "invalid input" -} - -default skipped := true - -skipped := false if valid_input - -default ignore := false - -######################################## -# EO Common section, custom code below # -######################################## # Validates if the input is valid and can be understood by this policy valid_input := true -# insert code here - # If the input is valid, check for any policy violation here -default violations := [] - # violations contains msg if { -# valid_input -# insert code here +# insert your validation logic here +# msg := "your violation message" # } diff --git a/app/cli/internal/policydevel/testdata/sbom-metadata-component-policy.yaml b/app/cli/internal/policydevel/testdata/sbom-metadata-component-policy.yaml new file mode 100644 index 000000000..a5bf81c18 --- /dev/null +++ b/app/cli/internal/policydevel/testdata/sbom-metadata-component-policy.yaml @@ -0,0 +1,17 @@ +apiVersion: workflowcontract.chainloop.dev/v1 +kind: Policy +metadata: + name: sbom-metadata-component + description: Policy that checks SBOM has metadata.component +spec: + policies: + - kind: SBOM_CYCLONEDX_JSON + embedded: | + package main + + import rego.v1 + + violations contains msg if { + not input.metadata.component + msg := "SBOM must have metadata.component" + } diff --git a/app/cli/internal/policydevel/testdata/sbom-min-components-policy.yaml b/app/cli/internal/policydevel/testdata/sbom-min-components-policy.yaml new file mode 100644 index 000000000..be3772436 --- /dev/null +++ b/app/cli/internal/policydevel/testdata/sbom-min-components-policy.yaml @@ -0,0 +1,17 @@ +apiVersion: workflowcontract.chainloop.dev/v1 +kind: Policy +metadata: + name: sbom-min-components + description: Policy that checks SBOM has minimum number of components +spec: + policies: + - kind: SBOM_CYCLONEDX_JSON + embedded: | + package main + + import rego.v1 + + violations contains msg if { + count(input.components) < 2 + msg := "SBOM must have at least 2 components" + } diff --git a/app/cli/internal/policydevel/testdata/sbom-multiple-checks-policy.yaml b/app/cli/internal/policydevel/testdata/sbom-multiple-checks-policy.yaml new file mode 100644 index 000000000..fb6cbceb1 --- /dev/null +++ b/app/cli/internal/policydevel/testdata/sbom-multiple-checks-policy.yaml @@ -0,0 +1,27 @@ +apiVersion: workflowcontract.chainloop.dev/v1 +kind: Policy +metadata: + name: sbom-multiple-checks + description: Policy that performs multiple SBOM validation checks +spec: + policies: + - kind: SBOM_CYCLONEDX_JSON + embedded: | + package main + + import rego.v1 + + violations contains msg if { + not input.metadata.component + msg := "missing metadata.component" + } + + violations contains msg if { + count(input.components) < 2 + msg := "too few components" + } + + violations contains msg if { + not input.bomFormat + msg := "missing bomFormat" + } diff --git a/app/cli/internal/policydevel/testdata/sbom-valid-cyclonedx-policy.yaml b/app/cli/internal/policydevel/testdata/sbom-valid-cyclonedx-policy.yaml new file mode 100644 index 000000000..1ad72e028 --- /dev/null +++ b/app/cli/internal/policydevel/testdata/sbom-valid-cyclonedx-policy.yaml @@ -0,0 +1,22 @@ +apiVersion: workflowcontract.chainloop.dev/v1 +kind: Policy +metadata: + name: sbom-valid-cyclonedx + description: Policy that validates SBOM is valid CycloneDX format +spec: + policies: + - kind: SBOM_CYCLONEDX_JSON + embedded: | + package main + + import rego.v1 + + # Custom input validation + valid_input if { + input.bomFormat == "CycloneDX" + } + + violations contains msg if { + count(input.components) == 0 + msg := "SBOM has no components" + } diff --git a/app/cli/internal/policydevel/testdata/test-sbom.json b/app/cli/internal/policydevel/testdata/test-sbom.json new file mode 100644 index 000000000..67232b3f1 --- /dev/null +++ b/app/cli/internal/policydevel/testdata/test-sbom.json @@ -0,0 +1,18 @@ +{ + "bomFormat": "CycloneDX", + "specVersion": "1.4", + "version": 1, + "metadata": { + "component": { + "type": "application", + "name": "test-app" + } + }, + "components": [ + { + "type": "library", + "name": "test-component", + "version": "1.0.0" + } + ] +} diff --git a/pkg/policies/engine/rego/boilerplate.go b/pkg/policies/engine/rego/boilerplate.go new file mode 100644 index 000000000..57c730f30 --- /dev/null +++ b/pkg/policies/engine/rego/boilerplate.go @@ -0,0 +1,161 @@ +// +// Copyright 2025 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rego + +import ( + "bytes" + _ "embed" + "fmt" + "strings" + "text/template" + + "github.com/open-policy-agent/opa/ast" +) + +const ( + ruleResult = "result" + ruleSkipped = "skipped" + ruleSkipReason = "skip_reason" + ruleValidInput = "valid_input" + ruleViolations = "violations" + ruleIgnore = "ignore" +) + +//go:embed boilerplate.rego.tmpl +var boilerplateTemplate string + +type boilerplateData struct { + NeedsResult bool + NeedsSkipReason bool + NeedsSkipped bool + NeedsValidInput bool + NeedsViolations bool +} + +// InjectBoilerplate automatically injects common policy boilerplate if it doesn't exist. +// This allows users to write simplified policies with only the violations rules. +// Requirements: Policy must have package declaration and import rego.v1 +// The function: +// - Parses the policy using OPA's AST +// - Detects which boilerplate rules are missing +// - Injects only the missing rules after package and imports +func InjectBoilerplate(policySource []byte, policyName string) ([]byte, error) { + if len(policySource) == 0 { + return nil, fmt.Errorf("empty policy source") + } + + originalPolicy := string(policySource) + + // Parse the policy + module, err := ast.ParseModule(policyName, originalPolicy) + if err != nil { + return nil, fmt.Errorf("failed to parse policy (must have 'package' and 'import rego.v1'): %w", err) + } + + // Detect which rules already exist using AST + existingRules := detectExistingRules(module) + + // If all required boilerplate rules exist, no injection needed + if existingRules[ruleResult] && existingRules[ruleSkipReason] && + existingRules[ruleSkipped] && existingRules[ruleValidInput] && + existingRules[ruleViolations] { + return policySource, nil + } + + // Build the boilerplate injection (rules only, no package/import) + injection, err := buildBoilerplate(existingRules) + if err != nil { + return nil, err + } + + // If nothing needs to be injected, return original + if injection == "" { + return policySource, nil + } + + // Inject after package and imports + injected, err := injectAfterImports(module, originalPolicy, injection) + if err != nil { + return nil, fmt.Errorf("failed to inject boilerplate: %w", err) + } + + return []byte(injected), nil +} + +// detectExistingRules scans the AST to find which rules are already defined +func detectExistingRules(module *ast.Module) map[string]bool { + existing := make(map[string]bool) + + for _, rule := range module.Rules { + ruleName := string(rule.Head.Name) + existing[ruleName] = true + } + + return existing +} + +// buildBoilerplate constructs the boilerplate template based on what's missing +func buildBoilerplate(existingRules map[string]bool) (string, error) { + data := boilerplateData{ + NeedsResult: !existingRules[ruleResult], + NeedsSkipReason: !existingRules[ruleSkipReason], + NeedsSkipped: !existingRules[ruleSkipped], + NeedsValidInput: !existingRules[ruleValidInput], + NeedsViolations: !existingRules[ruleViolations], + } + + tmpl, err := template.New("boilerplate").Parse(boilerplateTemplate) + if err != nil { + return "", fmt.Errorf("failed to parse boilerplate template: %w", err) + } + + var buf bytes.Buffer + if err := tmpl.Execute(&buf, data); err != nil { + return "", fmt.Errorf("failed to execute boilerplate template: %w", err) + } + + return buf.String(), nil +} + +// injectAfterImports inserts the injection block after the package declaration and existing imports +func injectAfterImports(module *ast.Module, originalPolicy, injection string) (string, error) { + // Get insertion line from AST - start with package line + insertionLine := module.Package.Location.Row + + // Find the last import line + for _, imp := range module.Imports { + if imp.Location.Row > insertionLine { + insertionLine = imp.Location.Row + } + } + + // Skip any blank lines after package/imports + lines := strings.Split(originalPolicy, "\n") + for insertionLine < len(lines) && strings.TrimSpace(lines[insertionLine]) == "" { + insertionLine++ + } + + // Trim trailing newline from injection to avoid double blank line when joining + injection = strings.TrimSuffix(injection, "\n") + + // Insert the injection block + result := make([]string, 0, len(lines)+1) + result = append(result, lines[:insertionLine]...) + result = append(result, injection) + result = append(result, lines[insertionLine:]...) + + return strings.Join(result, "\n"), nil +} diff --git a/pkg/policies/engine/rego/boilerplate.rego.tmpl b/pkg/policies/engine/rego/boilerplate.rego.tmpl new file mode 100644 index 000000000..bd3562445 --- /dev/null +++ b/pkg/policies/engine/rego/boilerplate.rego.tmpl @@ -0,0 +1,22 @@ +{{if .NeedsResult}}result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +{{end}}{{if .NeedsSkipReason}}default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +{{end}}{{if .NeedsSkipped}}default skipped := true + +skipped := false if valid_input + +{{end}}{{if .NeedsValidInput}}default valid_input := true + +{{end}}{{if .NeedsViolations}}default violations := [] + +{{end}} \ No newline at end of file diff --git a/pkg/policies/engine/rego/boilerplate_test.go b/pkg/policies/engine/rego/boilerplate_test.go new file mode 100644 index 000000000..32b30404b --- /dev/null +++ b/pkg/policies/engine/rego/boilerplate_test.go @@ -0,0 +1,119 @@ +// +// Copyright 2025 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. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package rego + +import ( + "os" + "path/filepath" + "testing" + + "github.com/open-policy-agent/opa/ast" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestInjectBoilerplate(t *testing.T) { + testCases := []struct { + name string + inputFile string + outputName string + }{ + { + name: "simplified policy", + inputFile: "testdata/simplified-policy.rego", + outputName: "simplified-policy-output.rego", + }, + { + name: "full boilerplate exists", + inputFile: "testdata/full-boilerplate.rego", + outputName: "full-boilerplate-output.rego", + }, + { + name: "user defined valid_input", + inputFile: "testdata/custom-valid-input.rego", + outputName: "custom-valid-input-output.rego", + }, + { + name: "partial boilerplate", + inputFile: "testdata/partial-boilerplate.rego", + outputName: "partial-boilerplate-output.rego", + }, + { + name: "preserve multiple imports", + inputFile: "testdata/multiple-imports.rego", + outputName: "multiple-imports-output.rego", + }, + { + name: "with comments", + inputFile: "testdata/with-comments.rego", + outputName: "with-comments-output.rego", + }, + { + name: "only package and import", + inputFile: "testdata/only-package-import.rego", + outputName: "only-package-import-output.rego", + }, + { + name: "real world source commit example", + inputFile: "testdata/source-commit-simplified.rego", + outputName: "source-commit-simplified-output.rego", + }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + input, err := os.ReadFile(tc.inputFile) + require.NoError(t, err) + + result, err := InjectBoilerplate(input, "test-policy") + require.NoError(t, err) + + matchesOutput(t, result, tc.outputName) + }) + } +} + +// matchesOutput compares result against expected output file +func matchesOutput(t *testing.T, result []byte, outputName string) { + t.Helper() + + outputPath := filepath.Join("testdata", "output", outputName) + + expected, err := os.ReadFile(outputPath) + require.NoError(t, err, "failed to read output file %s", outputPath) + + assert.Equal(t, string(expected), string(result), "output doesn't match expected file %s", outputPath) + + // Also verify it's valid Rego + _, err = ast.ParseModule("test", string(result)) + require.NoError(t, err, "generated Rego should be valid") +} + +func TestDetectExistingRules(t *testing.T) { + policyBytes, err := os.ReadFile("testdata/detect-rules.rego") + require.NoError(t, err) + + module, err := ast.ParseModule("test", string(policyBytes)) + require.NoError(t, err) + + existing := detectExistingRules(module) + + assert.True(t, existing["result"]) + assert.True(t, existing["skipped"]) + assert.True(t, existing["valid_input"]) + assert.True(t, existing["violations"]) + assert.False(t, existing["skip_reason"]) +} diff --git a/pkg/policies/engine/rego/testdata/custom-valid-input.rego b/pkg/policies/engine/rego/testdata/custom-valid-input.rego new file mode 100644 index 000000000..7ee0d87f8 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/custom-valid-input.rego @@ -0,0 +1,12 @@ +package main + +import rego.v1 + +valid_input if { + input.type == "attestation" +} + +violations contains msg if { + not input.subject + msg := "missing subject" +} diff --git a/pkg/policies/engine/rego/testdata/detect-rules.rego b/pkg/policies/engine/rego/testdata/detect-rules.rego new file mode 100644 index 000000000..2ed3e61ac --- /dev/null +++ b/pkg/policies/engine/rego/testdata/detect-rules.rego @@ -0,0 +1,11 @@ +package main + +import rego.v1 + +result := {"test": true} +skipped := false +valid_input := true + +violations contains msg if { + msg := "test" +} diff --git a/pkg/policies/engine/rego/testdata/full-boilerplate.rego b/pkg/policies/engine/rego/testdata/full-boilerplate.rego new file mode 100644 index 000000000..778d0b60f --- /dev/null +++ b/pkg/policies/engine/rego/testdata/full-boilerplate.rego @@ -0,0 +1,26 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +default skipped := true + +skipped := false if valid_input + +valid_input := true + +violations contains msg if { + msg := "test violation" +} diff --git a/pkg/policies/engine/rego/testdata/multiple-imports.rego b/pkg/policies/engine/rego/testdata/multiple-imports.rego new file mode 100644 index 000000000..025bfabf7 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/multiple-imports.rego @@ -0,0 +1,9 @@ +package main + +import rego.v1 +import data.lib.helpers +import future.keywords + +violations contains msg if { + msg := "test" +} diff --git a/pkg/policies/engine/rego/testdata/only-package-import.rego b/pkg/policies/engine/rego/testdata/only-package-import.rego new file mode 100644 index 000000000..424648483 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/only-package-import.rego @@ -0,0 +1,3 @@ +package main + +import rego.v1 \ No newline at end of file diff --git a/pkg/policies/engine/rego/testdata/output/custom-valid-input-output.rego b/pkg/policies/engine/rego/testdata/output/custom-valid-input-output.rego new file mode 100644 index 000000000..2476cfaa3 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/custom-valid-input-output.rego @@ -0,0 +1,29 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +default skipped := true + +skipped := false if valid_input + +valid_input if { + input.type == "attestation" +} + +violations contains msg if { + not input.subject + msg := "missing subject" +} diff --git a/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego b/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego new file mode 100644 index 000000000..778d0b60f --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego @@ -0,0 +1,26 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +default skipped := true + +skipped := false if valid_input + +valid_input := true + +violations contains msg if { + msg := "test violation" +} diff --git a/pkg/policies/engine/rego/testdata/output/multiple-imports-output.rego b/pkg/policies/engine/rego/testdata/output/multiple-imports-output.rego new file mode 100644 index 000000000..04b8f56d8 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/multiple-imports-output.rego @@ -0,0 +1,28 @@ +package main + +import rego.v1 +import data.lib.helpers +import future.keywords + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +default skipped := true + +skipped := false if valid_input + +default valid_input := true + +violations contains msg if { + msg := "test" +} diff --git a/pkg/policies/engine/rego/testdata/output/only-package-import-output.rego b/pkg/policies/engine/rego/testdata/output/only-package-import-output.rego new file mode 100644 index 000000000..8cace7fd8 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/only-package-import-output.rego @@ -0,0 +1,23 @@ +package main + +import rego.v1 +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +default skipped := true + +skipped := false if valid_input + +default valid_input := true + +default violations := [] diff --git a/pkg/policies/engine/rego/testdata/output/partial-boilerplate-output.rego b/pkg/policies/engine/rego/testdata/output/partial-boilerplate-output.rego new file mode 100644 index 000000000..e3b21f7e3 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/partial-boilerplate-output.rego @@ -0,0 +1,26 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +default valid_input := true + +default skipped := true + +skipped := false if valid_input + +violations contains msg if { + msg := "test violation" +} diff --git a/pkg/policies/engine/rego/testdata/output/simplified-policy-output.rego b/pkg/policies/engine/rego/testdata/output/simplified-policy-output.rego new file mode 100644 index 000000000..a8330a0b9 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/simplified-policy-output.rego @@ -0,0 +1,32 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +default skipped := true + +skipped := false if valid_input + +default valid_input := true + +violations contains msg if { + not has_commit + msg := "missing commit in statement" +} + +has_commit if { + some sub in input.subject + sub.name == "git.head" +} diff --git a/pkg/policies/engine/rego/testdata/output/source-commit-simplified-output.rego b/pkg/policies/engine/rego/testdata/output/source-commit-simplified-output.rego new file mode 100644 index 000000000..97819e8fb --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/source-commit-simplified-output.rego @@ -0,0 +1,54 @@ +package source_commit + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +default skipped := true + +skipped := false if valid_input + +default valid_input := true + +check_signature if { + lower(input.args.check_signature) == "true" +} + +check_signature if { + lower(input.args.check_signature) == "yes" +} + +violations contains msg if { + not has_commit + msg := "missing commit in statement" +} + +violations contains msg if { + has_commit + check_signature + not has_signature + msg := "missing signature in statement commit" +} + +has_commit if { + some sub in input.subject + sub.name == "git.head" + sub.digest.sha1 +} + +has_signature if { + some sub in input.subject + sub.name == "git.head" + sub.annotations.signature +} diff --git a/pkg/policies/engine/rego/testdata/output/with-comments-output.rego b/pkg/policies/engine/rego/testdata/output/with-comments-output.rego new file mode 100644 index 000000000..64aa8de10 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/with-comments-output.rego @@ -0,0 +1,35 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +default skipped := true + +skipped := false if valid_input + +default valid_input := true + +# This is a custom policy +# It checks for violations + +violations contains msg if { + # Check something + msg := "test violation" +} + +# Helper function +has_field if { + input.field +} diff --git a/pkg/policies/engine/rego/testdata/partial-boilerplate.rego b/pkg/policies/engine/rego/testdata/partial-boilerplate.rego new file mode 100644 index 000000000..f8fb0d1c4 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/partial-boilerplate.rego @@ -0,0 +1,11 @@ +package main + +import rego.v1 + +default skipped := true + +skipped := false if valid_input + +violations contains msg if { + msg := "test violation" +} diff --git a/pkg/policies/engine/rego/testdata/simplified-policy.rego b/pkg/policies/engine/rego/testdata/simplified-policy.rego new file mode 100644 index 000000000..15d9f0923 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/simplified-policy.rego @@ -0,0 +1,13 @@ +package main + +import rego.v1 + +violations contains msg if { + not has_commit + msg := "missing commit in statement" +} + +has_commit if { + some sub in input.subject + sub.name == "git.head" +} diff --git a/pkg/policies/engine/rego/testdata/source-commit-simplified.rego b/pkg/policies/engine/rego/testdata/source-commit-simplified.rego new file mode 100644 index 000000000..7c8861185 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/source-commit-simplified.rego @@ -0,0 +1,35 @@ +package source_commit + +import rego.v1 + +check_signature if { + lower(input.args.check_signature) == "true" +} + +check_signature if { + lower(input.args.check_signature) == "yes" +} + +violations contains msg if { + not has_commit + msg := "missing commit in statement" +} + +violations contains msg if { + has_commit + check_signature + not has_signature + msg := "missing signature in statement commit" +} + +has_commit if { + some sub in input.subject + sub.name == "git.head" + sub.digest.sha1 +} + +has_signature if { + some sub in input.subject + sub.name == "git.head" + sub.annotations.signature +} diff --git a/pkg/policies/engine/rego/testdata/with-comments.rego b/pkg/policies/engine/rego/testdata/with-comments.rego new file mode 100644 index 000000000..2044d9473 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/with-comments.rego @@ -0,0 +1,16 @@ +package main + +import rego.v1 + +# This is a custom policy +# It checks for violations + +violations contains msg if { + # Check something + msg := "test violation" +} + +# Helper function +has_field if { + input.field +} diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index c92beeb45..9639f5b3d 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -608,6 +608,11 @@ func getPolicyTypes(p *v1.Policy) []v1.CraftingSchema_Material_MaterialType { return policyTypes } +// injectBoilerplateIfNeeded automatically injects common policy boilerplate +func injectBoilerplateIfNeeded(policySource []byte, policyName string) ([]byte, error) { + return rego.InjectBoilerplate(policySource, policyName) +} + // LoadPolicyScriptsFromSpec loads all policy script that matches a given material type. It matches if: // * the policy kind is unspecified, meaning that it was forced by name selector // * the policy kind is specified, and it's equal to the material type @@ -619,6 +624,13 @@ func LoadPolicyScriptsFromSpec(policy *v1.Policy, kind v1.CraftingSchema_Materia if err != nil { return nil, fmt.Errorf("failed to load policy script: %w", err) } + + // Inject boilerplate if needed + script, err = injectBoilerplateIfNeeded(script, policy.GetMetadata().GetName()) + if err != nil { + return nil, fmt.Errorf("failed to inject boilerplate: %w", err) + } + scripts = append(scripts, &engine.Policy{Source: script, Name: policy.GetMetadata().GetName()}) } else { // multi-kind policies @@ -629,6 +641,13 @@ func LoadPolicyScriptsFromSpec(policy *v1.Policy, kind v1.CraftingSchema_Materia if err != nil { return nil, fmt.Errorf("failed to load policy script: %w", err) } + + // Inject boilerplate if needed + script, err = injectBoilerplateIfNeeded(script, policy.GetMetadata().GetName()) + if err != nil { + return nil, fmt.Errorf("failed to inject boilerplate: %w", err) + } + scripts = append(scripts, &engine.Policy{Source: script, Name: policy.GetMetadata().GetName()}) } } From c5b63fa76869e98fb92b6bdf6492461535d6869a Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Tue, 4 Nov 2025 13:54:07 +0100 Subject: [PATCH 2/5] remove unnecessary function Signed-off-by: Sylwester Piskozub --- pkg/policies/policies.go | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index 9639f5b3d..0532e6466 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -608,11 +608,6 @@ func getPolicyTypes(p *v1.Policy) []v1.CraftingSchema_Material_MaterialType { return policyTypes } -// injectBoilerplateIfNeeded automatically injects common policy boilerplate -func injectBoilerplateIfNeeded(policySource []byte, policyName string) ([]byte, error) { - return rego.InjectBoilerplate(policySource, policyName) -} - // LoadPolicyScriptsFromSpec loads all policy script that matches a given material type. It matches if: // * the policy kind is unspecified, meaning that it was forced by name selector // * the policy kind is specified, and it's equal to the material type @@ -626,7 +621,7 @@ func LoadPolicyScriptsFromSpec(policy *v1.Policy, kind v1.CraftingSchema_Materia } // Inject boilerplate if needed - script, err = injectBoilerplateIfNeeded(script, policy.GetMetadata().GetName()) + script, err = rego.InjectBoilerplate(script, policy.GetMetadata().GetName()) if err != nil { return nil, fmt.Errorf("failed to inject boilerplate: %w", err) } @@ -643,7 +638,7 @@ func LoadPolicyScriptsFromSpec(policy *v1.Policy, kind v1.CraftingSchema_Materia } // Inject boilerplate if needed - script, err = injectBoilerplateIfNeeded(script, policy.GetMetadata().GetName()) + script, err = rego.InjectBoilerplate(script, policy.GetMetadata().GetName()) if err != nil { return nil, fmt.Errorf("failed to inject boilerplate: %w", err) } From ad51d8265a8ba379ce07fdc76835eafab3a25af6 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Tue, 4 Nov 2025 13:57:46 +0100 Subject: [PATCH 3/5] use opa v1 Signed-off-by: Sylwester Piskozub --- pkg/policies/engine/rego/boilerplate.go | 2 +- pkg/policies/engine/rego/boilerplate_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/policies/engine/rego/boilerplate.go b/pkg/policies/engine/rego/boilerplate.go index 57c730f30..4c354af55 100644 --- a/pkg/policies/engine/rego/boilerplate.go +++ b/pkg/policies/engine/rego/boilerplate.go @@ -22,7 +22,7 @@ import ( "strings" "text/template" - "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/v1/ast" ) const ( diff --git a/pkg/policies/engine/rego/boilerplate_test.go b/pkg/policies/engine/rego/boilerplate_test.go index 32b30404b..249cac444 100644 --- a/pkg/policies/engine/rego/boilerplate_test.go +++ b/pkg/policies/engine/rego/boilerplate_test.go @@ -20,7 +20,7 @@ import ( "path/filepath" "testing" - "github.com/open-policy-agent/opa/ast" + "github.com/open-policy-agent/opa/v1/ast" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) From 363c478b9af0f18e308c2ef01f0c93f2d5d5803c Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Wed, 5 Nov 2025 11:56:53 +0100 Subject: [PATCH 4/5] add ignore, add trim markers Signed-off-by: Sylwester Piskozub --- pkg/policies/engine/rego/boilerplate.go | 4 ++- .../engine/rego/boilerplate.rego.tmpl | 26 ++++++++++++++----- .../rego/testdata/full-boilerplate.rego | 3 +++ .../rego/testdata/only-package-import.rego | 2 +- .../output/custom-valid-input-output.rego | 3 +++ .../output/full-boilerplate-output.rego | 3 +++ .../output/multiple-imports-output.rego | 5 +++- .../output/only-package-import-output.rego | 6 ++++- .../output/partial-boilerplate-output.rego | 3 +++ .../output/simplified-policy-output.rego | 5 +++- .../source-commit-simplified-output.rego | 5 +++- .../testdata/output/with-comments-output.rego | 5 +++- 12 files changed, 57 insertions(+), 13 deletions(-) diff --git a/pkg/policies/engine/rego/boilerplate.go b/pkg/policies/engine/rego/boilerplate.go index 4c354af55..e3f89b8cb 100644 --- a/pkg/policies/engine/rego/boilerplate.go +++ b/pkg/policies/engine/rego/boilerplate.go @@ -43,6 +43,7 @@ type boilerplateData struct { NeedsSkipped bool NeedsValidInput bool NeedsViolations bool + NeedsIgnore bool } // InjectBoilerplate automatically injects common policy boilerplate if it doesn't exist. @@ -71,7 +72,7 @@ func InjectBoilerplate(policySource []byte, policyName string) ([]byte, error) { // If all required boilerplate rules exist, no injection needed if existingRules[ruleResult] && existingRules[ruleSkipReason] && existingRules[ruleSkipped] && existingRules[ruleValidInput] && - existingRules[ruleViolations] { + existingRules[ruleViolations] && existingRules[ruleIgnore] { return policySource, nil } @@ -115,6 +116,7 @@ func buildBoilerplate(existingRules map[string]bool) (string, error) { NeedsSkipped: !existingRules[ruleSkipped], NeedsValidInput: !existingRules[ruleValidInput], NeedsViolations: !existingRules[ruleViolations], + NeedsIgnore: !existingRules[ruleIgnore], } tmpl, err := template.New("boilerplate").Parse(boilerplateTemplate) diff --git a/pkg/policies/engine/rego/boilerplate.rego.tmpl b/pkg/policies/engine/rego/boilerplate.rego.tmpl index bd3562445..68065bec7 100644 --- a/pkg/policies/engine/rego/boilerplate.rego.tmpl +++ b/pkg/policies/engine/rego/boilerplate.rego.tmpl @@ -1,22 +1,36 @@ -{{if .NeedsResult}}result := { +{{if .NeedsResult -}} +result := { "skipped": skipped, "violations": violations, "skip_reason": skip_reason, + "ignore": ignore, } -{{end}}{{if .NeedsSkipReason}}default skip_reason := "" +{{end -}} +{{if .NeedsSkipReason -}} +default skip_reason := "" skip_reason := m if { not valid_input m := "the file content is not recognized" } -{{end}}{{if .NeedsSkipped}}default skipped := true +{{end -}} +{{if .NeedsValidInput -}} +default valid_input := true + +{{end -}} +{{if .NeedsSkipped -}} +default skipped := true skipped := false if valid_input -{{end}}{{if .NeedsValidInput}}default valid_input := true +{{end -}} +{{if .NeedsIgnore -}} +default ignore := false -{{end}}{{if .NeedsViolations}}default violations := [] +{{end -}} +{{if .NeedsViolations -}} +default violations := [] -{{end}} \ No newline at end of file +{{end -}} \ No newline at end of file diff --git a/pkg/policies/engine/rego/testdata/full-boilerplate.rego b/pkg/policies/engine/rego/testdata/full-boilerplate.rego index 778d0b60f..efbe2d293 100644 --- a/pkg/policies/engine/rego/testdata/full-boilerplate.rego +++ b/pkg/policies/engine/rego/testdata/full-boilerplate.rego @@ -6,6 +6,7 @@ result := { "skipped": skipped, "violations": violations, "skip_reason": skip_reason, + "ignore": ignore, } default skip_reason := "" @@ -19,6 +20,8 @@ default skipped := true skipped := false if valid_input +default ignore := false + valid_input := true violations contains msg if { diff --git a/pkg/policies/engine/rego/testdata/only-package-import.rego b/pkg/policies/engine/rego/testdata/only-package-import.rego index 424648483..7988034e8 100644 --- a/pkg/policies/engine/rego/testdata/only-package-import.rego +++ b/pkg/policies/engine/rego/testdata/only-package-import.rego @@ -1,3 +1,3 @@ package main -import rego.v1 \ No newline at end of file +import rego.v1 diff --git a/pkg/policies/engine/rego/testdata/output/custom-valid-input-output.rego b/pkg/policies/engine/rego/testdata/output/custom-valid-input-output.rego index 2476cfaa3..13be6816e 100644 --- a/pkg/policies/engine/rego/testdata/output/custom-valid-input-output.rego +++ b/pkg/policies/engine/rego/testdata/output/custom-valid-input-output.rego @@ -6,6 +6,7 @@ result := { "skipped": skipped, "violations": violations, "skip_reason": skip_reason, + "ignore": ignore, } default skip_reason := "" @@ -19,6 +20,8 @@ default skipped := true skipped := false if valid_input +default ignore := false + valid_input if { input.type == "attestation" } diff --git a/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego b/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego index 778d0b60f..efbe2d293 100644 --- a/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego +++ b/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego @@ -6,6 +6,7 @@ result := { "skipped": skipped, "violations": violations, "skip_reason": skip_reason, + "ignore": ignore, } default skip_reason := "" @@ -19,6 +20,8 @@ default skipped := true skipped := false if valid_input +default ignore := false + valid_input := true violations contains msg if { diff --git a/pkg/policies/engine/rego/testdata/output/multiple-imports-output.rego b/pkg/policies/engine/rego/testdata/output/multiple-imports-output.rego index 04b8f56d8..d8f869c55 100644 --- a/pkg/policies/engine/rego/testdata/output/multiple-imports-output.rego +++ b/pkg/policies/engine/rego/testdata/output/multiple-imports-output.rego @@ -8,6 +8,7 @@ result := { "skipped": skipped, "violations": violations, "skip_reason": skip_reason, + "ignore": ignore, } default skip_reason := "" @@ -17,11 +18,13 @@ skip_reason := m if { m := "the file content is not recognized" } +default valid_input := true + default skipped := true skipped := false if valid_input -default valid_input := true +default ignore := false violations contains msg if { msg := "test" diff --git a/pkg/policies/engine/rego/testdata/output/only-package-import-output.rego b/pkg/policies/engine/rego/testdata/output/only-package-import-output.rego index 8cace7fd8..8b84b64d1 100644 --- a/pkg/policies/engine/rego/testdata/output/only-package-import-output.rego +++ b/pkg/policies/engine/rego/testdata/output/only-package-import-output.rego @@ -1,10 +1,12 @@ package main import rego.v1 + result := { "skipped": skipped, "violations": violations, "skip_reason": skip_reason, + "ignore": ignore, } default skip_reason := "" @@ -14,10 +16,12 @@ skip_reason := m if { m := "the file content is not recognized" } +default valid_input := true + default skipped := true skipped := false if valid_input -default valid_input := true +default ignore := false default violations := [] diff --git a/pkg/policies/engine/rego/testdata/output/partial-boilerplate-output.rego b/pkg/policies/engine/rego/testdata/output/partial-boilerplate-output.rego index e3b21f7e3..2013428d9 100644 --- a/pkg/policies/engine/rego/testdata/output/partial-boilerplate-output.rego +++ b/pkg/policies/engine/rego/testdata/output/partial-boilerplate-output.rego @@ -6,6 +6,7 @@ result := { "skipped": skipped, "violations": violations, "skip_reason": skip_reason, + "ignore": ignore, } default skip_reason := "" @@ -17,6 +18,8 @@ skip_reason := m if { default valid_input := true +default ignore := false + default skipped := true skipped := false if valid_input diff --git a/pkg/policies/engine/rego/testdata/output/simplified-policy-output.rego b/pkg/policies/engine/rego/testdata/output/simplified-policy-output.rego index a8330a0b9..797bd1ff2 100644 --- a/pkg/policies/engine/rego/testdata/output/simplified-policy-output.rego +++ b/pkg/policies/engine/rego/testdata/output/simplified-policy-output.rego @@ -6,6 +6,7 @@ result := { "skipped": skipped, "violations": violations, "skip_reason": skip_reason, + "ignore": ignore, } default skip_reason := "" @@ -15,11 +16,13 @@ skip_reason := m if { m := "the file content is not recognized" } +default valid_input := true + default skipped := true skipped := false if valid_input -default valid_input := true +default ignore := false violations contains msg if { not has_commit diff --git a/pkg/policies/engine/rego/testdata/output/source-commit-simplified-output.rego b/pkg/policies/engine/rego/testdata/output/source-commit-simplified-output.rego index 97819e8fb..7ebc34a4c 100644 --- a/pkg/policies/engine/rego/testdata/output/source-commit-simplified-output.rego +++ b/pkg/policies/engine/rego/testdata/output/source-commit-simplified-output.rego @@ -6,6 +6,7 @@ result := { "skipped": skipped, "violations": violations, "skip_reason": skip_reason, + "ignore": ignore, } default skip_reason := "" @@ -15,11 +16,13 @@ skip_reason := m if { m := "the file content is not recognized" } +default valid_input := true + default skipped := true skipped := false if valid_input -default valid_input := true +default ignore := false check_signature if { lower(input.args.check_signature) == "true" diff --git a/pkg/policies/engine/rego/testdata/output/with-comments-output.rego b/pkg/policies/engine/rego/testdata/output/with-comments-output.rego index 64aa8de10..db7dd9f6a 100644 --- a/pkg/policies/engine/rego/testdata/output/with-comments-output.rego +++ b/pkg/policies/engine/rego/testdata/output/with-comments-output.rego @@ -6,6 +6,7 @@ result := { "skipped": skipped, "violations": violations, "skip_reason": skip_reason, + "ignore": ignore, } default skip_reason := "" @@ -15,11 +16,13 @@ skip_reason := m if { m := "the file content is not recognized" } +default valid_input := true + default skipped := true skipped := false if valid_input -default valid_input := true +default ignore := false # This is a custom policy # It checks for violations From c974629b4d411ed4d07ce33d306a606e70626af3 Mon Sep 17 00:00:00 2001 From: Sylwester Piskozub Date: Fri, 7 Nov 2025 11:19:12 +0100 Subject: [PATCH 5/5] separate default rules Signed-off-by: Sylwester Piskozub --- pkg/policies/engine/rego/boilerplate.go | 68 ++++++++++++------- .../engine/rego/boilerplate.rego.tmpl | 14 ++-- pkg/policies/engine/rego/boilerplate_test.go | 20 ++++-- .../engine/rego/testdata/detect-rules.rego | 4 +- .../rego/testdata/full-boilerplate.rego | 4 +- .../output/full-boilerplate-output.rego | 4 +- 6 files changed, 76 insertions(+), 38 deletions(-) diff --git a/pkg/policies/engine/rego/boilerplate.go b/pkg/policies/engine/rego/boilerplate.go index e3f89b8cb..2e72933fc 100644 --- a/pkg/policies/engine/rego/boilerplate.go +++ b/pkg/policies/engine/rego/boilerplate.go @@ -38,12 +38,14 @@ const ( var boilerplateTemplate string type boilerplateData struct { - NeedsResult bool - NeedsSkipReason bool - NeedsSkipped bool - NeedsValidInput bool - NeedsViolations bool - NeedsIgnore bool + NeedsResult bool + NeedsDefaultSkipReason bool + NeedsSkipReasonRule bool + NeedsDefaultSkipped bool + NeedsSkippedRule bool + NeedsDefaultIgnore bool + NeedsDefaultValidInput bool + NeedsDefaultViolations bool } // InjectBoilerplate automatically injects common policy boilerplate if it doesn't exist. @@ -67,17 +69,20 @@ func InjectBoilerplate(policySource []byte, policyName string) ([]byte, error) { } // Detect which rules already exist using AST - existingRules := detectExistingRules(module) - - // If all required boilerplate rules exist, no injection needed - if existingRules[ruleResult] && existingRules[ruleSkipReason] && - existingRules[ruleSkipped] && existingRules[ruleValidInput] && - existingRules[ruleViolations] && existingRules[ruleIgnore] { + existing := detectExistingRules(module) + + // If all required boilerplate rules and defaults exist, no injection needed + if existing.hasRule[ruleResult] && + existing.hasDefault[ruleSkipReason] && existing.hasRule[ruleSkipReason] && + existing.hasDefault[ruleSkipped] && existing.hasRule[ruleSkipped] && + existing.hasDefault[ruleIgnore] && + existing.hasDefault[ruleValidInput] && + existing.hasDefault[ruleViolations] { return policySource, nil } // Build the boilerplate injection (rules only, no package/import) - injection, err := buildBoilerplate(existingRules) + injection, err := buildBoilerplate(existing) if err != nil { return nil, err } @@ -96,27 +101,42 @@ func InjectBoilerplate(policySource []byte, policyName string) ([]byte, error) { return []byte(injected), nil } +type existingRules struct { + hasRule map[string]bool + hasDefault map[string]bool +} + // detectExistingRules scans the AST to find which rules are already defined -func detectExistingRules(module *ast.Module) map[string]bool { - existing := make(map[string]bool) +func detectExistingRules(module *ast.Module) *existingRules { + rules := &existingRules{ + hasRule: make(map[string]bool), + hasDefault: make(map[string]bool), + } for _, rule := range module.Rules { ruleName := string(rule.Head.Name) - existing[ruleName] = true + rules.hasRule[ruleName] = true + + // Track if this is a default rule + if rule.Default { + rules.hasDefault[ruleName] = true + } } - return existing + return rules } // buildBoilerplate constructs the boilerplate template based on what's missing -func buildBoilerplate(existingRules map[string]bool) (string, error) { +func buildBoilerplate(rules *existingRules) (string, error) { data := boilerplateData{ - NeedsResult: !existingRules[ruleResult], - NeedsSkipReason: !existingRules[ruleSkipReason], - NeedsSkipped: !existingRules[ruleSkipped], - NeedsValidInput: !existingRules[ruleValidInput], - NeedsViolations: !existingRules[ruleViolations], - NeedsIgnore: !existingRules[ruleIgnore], + NeedsResult: !rules.hasRule[ruleResult], + NeedsDefaultSkipReason: !rules.hasDefault[ruleSkipReason] && !rules.hasRule[ruleSkipReason], + NeedsSkipReasonRule: !rules.hasRule[ruleSkipReason], + NeedsDefaultSkipped: !rules.hasDefault[ruleSkipped] && !rules.hasRule[ruleSkipped], + NeedsSkippedRule: !rules.hasRule[ruleSkipped], + NeedsDefaultIgnore: !rules.hasDefault[ruleIgnore] && !rules.hasRule[ruleIgnore], + NeedsDefaultValidInput: !rules.hasDefault[ruleValidInput] && !rules.hasRule[ruleValidInput], + NeedsDefaultViolations: !rules.hasDefault[ruleViolations] && !rules.hasRule[ruleViolations], } tmpl, err := template.New("boilerplate").Parse(boilerplateTemplate) diff --git a/pkg/policies/engine/rego/boilerplate.rego.tmpl b/pkg/policies/engine/rego/boilerplate.rego.tmpl index 68065bec7..ae0b1f2fc 100644 --- a/pkg/policies/engine/rego/boilerplate.rego.tmpl +++ b/pkg/policies/engine/rego/boilerplate.rego.tmpl @@ -7,30 +7,34 @@ result := { } {{end -}} -{{if .NeedsSkipReason -}} +{{if .NeedsDefaultSkipReason -}} default skip_reason := "" +{{end -}} +{{if .NeedsSkipReasonRule -}} skip_reason := m if { not valid_input m := "the file content is not recognized" } {{end -}} -{{if .NeedsValidInput -}} +{{if .NeedsDefaultValidInput -}} default valid_input := true {{end -}} -{{if .NeedsSkipped -}} +{{if .NeedsDefaultSkipped -}} default skipped := true +{{end -}} +{{if .NeedsSkippedRule -}} skipped := false if valid_input {{end -}} -{{if .NeedsIgnore -}} +{{if .NeedsDefaultIgnore -}} default ignore := false {{end -}} -{{if .NeedsViolations -}} +{{if .NeedsDefaultViolations -}} default violations := [] {{end -}} \ No newline at end of file diff --git a/pkg/policies/engine/rego/boilerplate_test.go b/pkg/policies/engine/rego/boilerplate_test.go index 249cac444..627ffca8d 100644 --- a/pkg/policies/engine/rego/boilerplate_test.go +++ b/pkg/policies/engine/rego/boilerplate_test.go @@ -111,9 +111,19 @@ func TestDetectExistingRules(t *testing.T) { existing := detectExistingRules(module) - assert.True(t, existing["result"]) - assert.True(t, existing["skipped"]) - assert.True(t, existing["valid_input"]) - assert.True(t, existing["violations"]) - assert.False(t, existing["skip_reason"]) + // Check rules exist + assert.True(t, existing.hasRule["result"]) + assert.True(t, existing.hasRule["skipped"]) + assert.True(t, existing.hasRule["valid_input"]) + assert.True(t, existing.hasRule["violations"]) + assert.False(t, existing.hasRule["skip_reason"]) + assert.False(t, existing.hasRule["ignore"]) + + // Check defaults for rules + assert.False(t, existing.hasDefault["result"]) + assert.True(t, existing.hasDefault["skipped"]) + assert.True(t, existing.hasDefault["valid_input"]) + assert.False(t, existing.hasDefault["violations"]) + assert.False(t, existing.hasDefault["skip_reason"]) + assert.False(t, existing.hasDefault["ignore"]) } diff --git a/pkg/policies/engine/rego/testdata/detect-rules.rego b/pkg/policies/engine/rego/testdata/detect-rules.rego index 2ed3e61ac..d093dcc3f 100644 --- a/pkg/policies/engine/rego/testdata/detect-rules.rego +++ b/pkg/policies/engine/rego/testdata/detect-rules.rego @@ -3,8 +3,8 @@ package main import rego.v1 result := {"test": true} -skipped := false -valid_input := true +default skipped := false +default valid_input := true violations contains msg if { msg := "test" diff --git a/pkg/policies/engine/rego/testdata/full-boilerplate.rego b/pkg/policies/engine/rego/testdata/full-boilerplate.rego index efbe2d293..e744c60af 100644 --- a/pkg/policies/engine/rego/testdata/full-boilerplate.rego +++ b/pkg/policies/engine/rego/testdata/full-boilerplate.rego @@ -16,13 +16,15 @@ skip_reason := m if { m := "the file content is not recognized" } +default valid_input := true + default skipped := true skipped := false if valid_input default ignore := false -valid_input := true +default violations := [] violations contains msg if { msg := "test violation" diff --git a/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego b/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego index efbe2d293..e744c60af 100644 --- a/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego +++ b/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego @@ -16,13 +16,15 @@ skip_reason := m if { m := "the file content is not recognized" } +default valid_input := true + default skipped := true skipped := false if valid_input default ignore := false -valid_input := true +default violations := [] violations contains msg if { msg := "test violation"