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..2e72933fc --- /dev/null +++ b/pkg/policies/engine/rego/boilerplate.go @@ -0,0 +1,183 @@ +// +// 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/v1/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 + 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. +// 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 + 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(existing) + 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 +} + +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) *existingRules { + rules := &existingRules{ + hasRule: make(map[string]bool), + hasDefault: make(map[string]bool), + } + + for _, rule := range module.Rules { + ruleName := string(rule.Head.Name) + rules.hasRule[ruleName] = true + + // Track if this is a default rule + if rule.Default { + rules.hasDefault[ruleName] = true + } + } + + return rules +} + +// buildBoilerplate constructs the boilerplate template based on what's missing +func buildBoilerplate(rules *existingRules) (string, error) { + data := boilerplateData{ + 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) + 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..ae0b1f2fc --- /dev/null +++ b/pkg/policies/engine/rego/boilerplate.rego.tmpl @@ -0,0 +1,40 @@ +{{if .NeedsResult -}} +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, +} + +{{end -}} +{{if .NeedsDefaultSkipReason -}} +default skip_reason := "" + +{{end -}} +{{if .NeedsSkipReasonRule -}} +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +{{end -}} +{{if .NeedsDefaultValidInput -}} +default valid_input := true + +{{end -}} +{{if .NeedsDefaultSkipped -}} +default skipped := true + +{{end -}} +{{if .NeedsSkippedRule -}} +skipped := false if valid_input + +{{end -}} +{{if .NeedsDefaultIgnore -}} +default ignore := false + +{{end -}} +{{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 new file mode 100644 index 000000000..627ffca8d --- /dev/null +++ b/pkg/policies/engine/rego/boilerplate_test.go @@ -0,0 +1,129 @@ +// +// 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/v1/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) + + // 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/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..d093dcc3f --- /dev/null +++ b/pkg/policies/engine/rego/testdata/detect-rules.rego @@ -0,0 +1,11 @@ +package main + +import rego.v1 + +result := {"test": 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 new file mode 100644 index 000000000..e744c60af --- /dev/null +++ b/pkg/policies/engine/rego/testdata/full-boilerplate.rego @@ -0,0 +1,31 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, +} + +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 + +default ignore := false + +default violations := [] + +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..7988034e8 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/only-package-import.rego @@ -0,0 +1,3 @@ +package main + +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 new file mode 100644 index 000000000..13be6816e --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/custom-valid-input-output.rego @@ -0,0 +1,32 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, +} + +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 ignore := false + +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..e744c60af --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/full-boilerplate-output.rego @@ -0,0 +1,31 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, +} + +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 + +default ignore := false + +default violations := [] + +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..d8f869c55 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/multiple-imports-output.rego @@ -0,0 +1,31 @@ +package main + +import rego.v1 +import data.lib.helpers +import future.keywords + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, +} + +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 + +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 new file mode 100644 index 000000000..8b84b64d1 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/only-package-import-output.rego @@ -0,0 +1,27 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, +} + +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 + +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 new file mode 100644 index 000000000..2013428d9 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/partial-boilerplate-output.rego @@ -0,0 +1,29 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "the file content is not recognized" +} + +default valid_input := true + +default ignore := false + +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..797bd1ff2 --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/simplified-policy-output.rego @@ -0,0 +1,35 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, +} + +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 + +default ignore := false + +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..7ebc34a4c --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/source-commit-simplified-output.rego @@ -0,0 +1,57 @@ +package source_commit + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, +} + +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 + +default ignore := false + +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..db7dd9f6a --- /dev/null +++ b/pkg/policies/engine/rego/testdata/output/with-comments-output.rego @@ -0,0 +1,38 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, + "ignore": ignore, +} + +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 + +default ignore := false + +# 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..0532e6466 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -619,6 +619,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 = rego.InjectBoilerplate(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 +636,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 = rego.InjectBoilerplate(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()}) } }