Skip to content

Commit d7b3b3c

Browse files
Piskoojiparis
andauthored
feat(policies): add rego boilerplate injection (#2515)
Signed-off-by: Sylwester Piskozub <sylwesterpiskozub@gmail.com> Signed-off-by: Jose I. Paris <jiparis@chainloop.dev> Co-authored-by: Jose I. Paris <jiparis@chainloop.dev>
1 parent 6dc1ce8 commit d7b3b3c

31 files changed

+997
-95
lines changed

app/cli/internal/policydevel/eval_test.go

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -124,3 +124,67 @@ func TestEvaluate(t *testing.T) {
124124
assert.Contains(t, err.Error(), "invalid material kind")
125125
})
126126
}
127+
128+
func TestEvaluateSimplifiedPolicies(t *testing.T) {
129+
tempDir := t.TempDir()
130+
logger := zerolog.New(os.Stderr)
131+
132+
sbomContent, err := os.ReadFile("testdata/test-sbom.json")
133+
require.NoError(t, err)
134+
sbomPath := filepath.Join(tempDir, "test-sbom.json")
135+
require.NoError(t, os.WriteFile(sbomPath, sbomContent, 0600))
136+
137+
t.Run("sbom min components policy", func(t *testing.T) {
138+
opts := &EvalOptions{
139+
PolicyPath: "testdata/sbom-min-components-policy.yaml",
140+
MaterialPath: sbomPath,
141+
}
142+
143+
result, err := Evaluate(opts, logger)
144+
require.NoError(t, err)
145+
require.NotNil(t, result)
146+
assert.False(t, result.Result.Skipped)
147+
assert.Len(t, result.Result.Violations, 1)
148+
assert.Contains(t, result.Result.Violations[0], "at least 2 components")
149+
})
150+
151+
t.Run("sbom metadata component policy", func(t *testing.T) {
152+
opts := &EvalOptions{
153+
PolicyPath: "testdata/sbom-metadata-component-policy.yaml",
154+
MaterialPath: sbomPath,
155+
}
156+
157+
result, err := Evaluate(opts, logger)
158+
require.NoError(t, err)
159+
require.NotNil(t, result)
160+
assert.False(t, result.Result.Skipped)
161+
assert.Len(t, result.Result.Violations, 0)
162+
})
163+
164+
t.Run("sbom valid cyclonedx policy", func(t *testing.T) {
165+
opts := &EvalOptions{
166+
PolicyPath: "testdata/sbom-valid-cyclonedx-policy.yaml",
167+
MaterialPath: sbomPath,
168+
}
169+
170+
result, err := Evaluate(opts, logger)
171+
require.NoError(t, err)
172+
require.NotNil(t, result)
173+
assert.False(t, result.Result.Skipped)
174+
assert.Len(t, result.Result.Violations, 0)
175+
})
176+
177+
t.Run("sbom multiple checks policy", func(t *testing.T) {
178+
opts := &EvalOptions{
179+
PolicyPath: "testdata/sbom-multiple-checks-policy.yaml",
180+
MaterialPath: sbomPath,
181+
}
182+
183+
result, err := Evaluate(opts, logger)
184+
require.NoError(t, err)
185+
require.NotNil(t, result)
186+
assert.False(t, result.Result.Skipped)
187+
assert.Len(t, result.Result.Violations, 1)
188+
assert.Contains(t, result.Result.Violations[0], "too few components")
189+
})
190+
}

app/cli/internal/policydevel/lint.go

Lines changed: 1 addition & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ import (
2222
"fmt"
2323
"os"
2424
"path/filepath"
25-
"regexp"
2625
"strings"
2726

2827
v1 "github.com/chainloop-dev/chainloop/app/controlplane/api/workflowcontract/v1"
@@ -208,10 +207,7 @@ func (p *PolicyToLint) validateAndFormatRego(content, path string) string {
208207
content = formatted
209208
}
210209

211-
// 2. Structural validation
212-
p.checkResultStructure(content, path, []string{"skipped", "violations", "skip_reason"})
213-
214-
// 3. Run Regal linter
210+
// 2. Run Regal linter
215211
p.runRegalLinter(path, content)
216212

217213
return content
@@ -226,30 +222,6 @@ func (p *PolicyToLint) applyOPAFmt(content, file string) string {
226222
return string(formatted)
227223
}
228224

229-
func (p *PolicyToLint) checkResultStructure(content, path string, keys []string) {
230-
// Regex to capture result := { ... } including multiline
231-
re := regexp.MustCompile(`(?s)result\s*:=\s*\{(.+?)\}`)
232-
match := re.FindStringSubmatch(content)
233-
if match == nil {
234-
p.AddError(path, "no result literal found", 0)
235-
return
236-
}
237-
238-
body := match[1]
239-
// Find quoted keys inside the object literal
240-
keyRe := regexp.MustCompile(`"([^"]+)"\s*:`)
241-
found := make(map[string]bool)
242-
for _, m := range keyRe.FindAllStringSubmatch(body, -1) {
243-
found[m[1]] = true
244-
}
245-
246-
for _, want := range keys {
247-
if !found[want] {
248-
p.AddError(path, fmt.Sprintf("missing %q key in result", want), 0)
249-
}
250-
}
251-
}
252-
253225
// Runs the Regal linter on the given rego content and records any violations
254226
func (p *PolicyToLint) runRegalLinter(filePath, content string) {
255227
inputModules, err := rules.InputFromText(filePath, content)

app/cli/internal/policydevel/lint_test.go

Lines changed: 0 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -121,39 +121,6 @@ func TestPolicyToLint_processFile(t *testing.T) {
121121
})
122122
}
123123

124-
func TestPolicyToLint_checkResultStructure(t *testing.T) {
125-
t.Run("valid result structure", func(t *testing.T) {
126-
policy := &PolicyToLint{}
127-
content, err := os.ReadFile("testdata/valid.rego")
128-
require.NoError(t, err)
129-
policy.checkResultStructure(string(content), "test.rego", []string{"violations", "skip_reason", "skipped"})
130-
assert.False(t, policy.HasErrors())
131-
})
132-
133-
t.Run("missing result literal", func(t *testing.T) {
134-
policy := &PolicyToLint{}
135-
content := `package main
136-
137-
output := {
138-
"violations": []
139-
}`
140-
policy.checkResultStructure(content, "test.rego", []string{"violations"})
141-
assert.True(t, policy.HasErrors())
142-
assert.Contains(t, policy.Errors[0].Message, "no result literal found")
143-
})
144-
145-
t.Run("missing required keys", func(t *testing.T) {
146-
policy := &PolicyToLint{}
147-
content, err := os.ReadFile("testdata/missing-keys.rego")
148-
require.NoError(t, err)
149-
policy.checkResultStructure(string(content), "test.rego", []string{"violations", "skip_reason", "skipped"})
150-
assert.True(t, policy.HasErrors())
151-
assert.Len(t, policy.Errors, 2)
152-
assert.Contains(t, policy.Errors[0].Message, `missing "skip_reason" key`)
153-
assert.Contains(t, policy.Errors[1].Message, `missing "skipped" key`)
154-
})
155-
}
156-
157124
func TestPolicyToLint_formatViolationError(t *testing.T) {
158125
policy := &PolicyToLint{}
159126

app/cli/internal/policydevel/templates/example-policy.rego

Lines changed: 2 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -2,42 +2,11 @@ package main
22

33
import rego.v1
44

5-
################################
6-
# Common section do NOT change #
7-
################################
8-
9-
result := {
10-
"skipped": skipped,
11-
"violations": violations,
12-
"skip_reason": skip_reason,
13-
"ignore": ignore,
14-
}
15-
16-
default skip_reason := ""
17-
18-
skip_reason := m if {
19-
not valid_input
20-
m := "invalid input"
21-
}
22-
23-
default skipped := true
24-
25-
skipped := false if valid_input
26-
27-
default ignore := false
28-
29-
########################################
30-
# EO Common section, custom code below #
31-
########################################
325
# Validates if the input is valid and can be understood by this policy
336
valid_input := true
347

35-
# insert code here
36-
378
# If the input is valid, check for any policy violation here
38-
default violations := []
39-
409
# violations contains msg if {
41-
# valid_input
42-
# insert code here
10+
# insert your validation logic here
11+
# msg := "your violation message"
4312
# }
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: workflowcontract.chainloop.dev/v1
2+
kind: Policy
3+
metadata:
4+
name: sbom-metadata-component
5+
description: Policy that checks SBOM has metadata.component
6+
spec:
7+
policies:
8+
- kind: SBOM_CYCLONEDX_JSON
9+
embedded: |
10+
package main
11+
12+
import rego.v1
13+
14+
violations contains msg if {
15+
not input.metadata.component
16+
msg := "SBOM must have metadata.component"
17+
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
apiVersion: workflowcontract.chainloop.dev/v1
2+
kind: Policy
3+
metadata:
4+
name: sbom-min-components
5+
description: Policy that checks SBOM has minimum number of components
6+
spec:
7+
policies:
8+
- kind: SBOM_CYCLONEDX_JSON
9+
embedded: |
10+
package main
11+
12+
import rego.v1
13+
14+
violations contains msg if {
15+
count(input.components) < 2
16+
msg := "SBOM must have at least 2 components"
17+
}
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
apiVersion: workflowcontract.chainloop.dev/v1
2+
kind: Policy
3+
metadata:
4+
name: sbom-multiple-checks
5+
description: Policy that performs multiple SBOM validation checks
6+
spec:
7+
policies:
8+
- kind: SBOM_CYCLONEDX_JSON
9+
embedded: |
10+
package main
11+
12+
import rego.v1
13+
14+
violations contains msg if {
15+
not input.metadata.component
16+
msg := "missing metadata.component"
17+
}
18+
19+
violations contains msg if {
20+
count(input.components) < 2
21+
msg := "too few components"
22+
}
23+
24+
violations contains msg if {
25+
not input.bomFormat
26+
msg := "missing bomFormat"
27+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
apiVersion: workflowcontract.chainloop.dev/v1
2+
kind: Policy
3+
metadata:
4+
name: sbom-valid-cyclonedx
5+
description: Policy that validates SBOM is valid CycloneDX format
6+
spec:
7+
policies:
8+
- kind: SBOM_CYCLONEDX_JSON
9+
embedded: |
10+
package main
11+
12+
import rego.v1
13+
14+
# Custom input validation
15+
valid_input if {
16+
input.bomFormat == "CycloneDX"
17+
}
18+
19+
violations contains msg if {
20+
count(input.components) == 0
21+
msg := "SBOM has no components"
22+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"bomFormat": "CycloneDX",
3+
"specVersion": "1.4",
4+
"version": 1,
5+
"metadata": {
6+
"component": {
7+
"type": "application",
8+
"name": "test-app"
9+
}
10+
},
11+
"components": [
12+
{
13+
"type": "library",
14+
"name": "test-component",
15+
"version": "1.0.0"
16+
}
17+
]
18+
}

0 commit comments

Comments
 (0)