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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 64 additions & 0 deletions app/cli/internal/policydevel/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}
30 changes: 1 addition & 29 deletions app/cli/internal/policydevel/lint.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,6 @@ import (
"fmt"
"os"
"path/filepath"
"regexp"
"strings"

v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
33 changes: 0 additions & 33 deletions app/cli/internal/policydevel/lint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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{}

Expand Down
35 changes: 2 additions & 33 deletions app/cli/internal/policydevel/templates/example-policy.rego
Original file line number Diff line number Diff line change
Expand Up @@ -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"
# }
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
Original file line number Diff line number Diff line change
@@ -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"
}
18 changes: 18 additions & 0 deletions app/cli/internal/policydevel/testdata/test-sbom.json
Original file line number Diff line number Diff line change
@@ -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"
}
]
}
Loading
Loading