Skip to content

Commit 5b46d0c

Browse files
committed
expose generic evaluation in policy verifier
Signed-off-by: Sylwester Piskozub <sylwesterpiskozub@gmail.com>
1 parent c8007b4 commit 5b46d0c

5 files changed

Lines changed: 230 additions & 28 deletions

File tree

‎app/cli/internal/policydevel/eval.go‎

Lines changed: 56 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ type EvalSummaryDebugInfo struct {
6161
}
6262

6363
func Evaluate(opts *EvalOptions, logger zerolog.Logger) (*EvalSummary, error) {
64+
// Check if this is a generic policy evaluation
65+
if opts.MaterialPath == "" {
66+
return evaluateGeneric(opts, logger)
67+
}
68+
69+
// Material-based evaluation
6470
// 1. Craft material with annotations
6571
material, err := craftMaterial(opts.MaterialPath, opts.MaterialKind, &logger)
6672
if err != nil {
@@ -83,6 +89,38 @@ func Evaluate(opts *EvalOptions, logger zerolog.Logger) (*EvalSummary, error) {
8389
return summary, nil
8490
}
8591

92+
func evaluateGeneric(opts *EvalOptions, logger zerolog.Logger) (*EvalSummary, error) {
93+
// Create policy attachment without material selector
94+
ref := opts.PolicyPath
95+
scheme, _ := policies.RefParts(opts.PolicyPath)
96+
if scheme == "" {
97+
// Default to file://
98+
ref = fmt.Sprintf("file://%s", opts.PolicyPath)
99+
}
100+
101+
attachment := &v1.PolicyAttachment{
102+
Policy: &v1.PolicyAttachment_Ref{Ref: ref},
103+
With: opts.Inputs,
104+
}
105+
106+
// Create policy verifier
107+
verifierOpts := buildPolicyVerifierOptions(opts.AllowedHostnames, opts.Debug)
108+
pol := &v1.Policies{}
109+
v := policies.NewPolicyVerifier(pol, opts.AttestationClient, &logger, verifierOpts...)
110+
111+
// Evaluate generic policy
112+
policyEv, err := v.EvaluateGeneric(context.Background(), attachment, nil)
113+
if err != nil {
114+
return nil, err
115+
}
116+
117+
if policyEv == nil {
118+
return nil, fmt.Errorf("no execution branch matched, or all of them were ignored")
119+
}
120+
121+
return buildEvalSummary(policyEv, opts.Debug), nil
122+
}
123+
86124
func createPolicies(policyPath string, inputs map[string]string, materialID string) (*v1.Policies, error) {
87125
// Check if the policy path already has a scheme (chainloop://, http://, https://, file://)
88126
ref := policyPath
@@ -97,25 +135,14 @@ func createPolicies(policyPath string, inputs map[string]string, materialID stri
97135
{
98136
Policy: &v1.PolicyAttachment_Ref{Ref: ref},
99137
With: inputs,
100-
// Add selector to support generic policies
101-
Selector: &v1.PolicyAttachment_MaterialSelector{
102-
Name: materialID,
103-
},
104138
},
105139
},
106140
Attestation: nil,
107141
}, nil
108142
}
109143

110144
func verifyMaterial(pol *v1.Policies, material *v12.Attestation_Material, materialPath string, debug bool, allowedHostnames []string, attestationClient controlplanev1.AttestationServiceClient, logger *zerolog.Logger) (*EvalSummary, error) {
111-
var opts []policies.PolicyVerifierOption
112-
if len(allowedHostnames) > 0 {
113-
opts = append(opts, policies.WithAllowedHostnames(allowedHostnames...))
114-
}
115-
116-
opts = append(opts, policies.WithIncludeRawData(debug))
117-
opts = append(opts, policies.WithEnablePrint(enablePrint))
118-
145+
opts := buildPolicyVerifierOptions(allowedHostnames, debug)
119146
v := policies.NewPolicyVerifier(pol, attestationClient, logger, opts...)
120147
policyEvs, err := v.VerifyMaterial(context.Background(), material, materialPath)
121148
if err != nil {
@@ -127,8 +154,22 @@ func verifyMaterial(pol *v1.Policies, material *v12.Attestation_Material, materi
127154
}
128155

129156
// Only one evaluation expected for a single policy attachment
130-
policyEv := policyEvs[0]
157+
return buildEvalSummary(policyEvs[0], debug), nil
158+
}
131159

160+
// buildPolicyVerifierOptions creates common policy verifier options
161+
func buildPolicyVerifierOptions(allowedHostnames []string, debug bool) []policies.PolicyVerifierOption {
162+
var opts []policies.PolicyVerifierOption
163+
if len(allowedHostnames) > 0 {
164+
opts = append(opts, policies.WithAllowedHostnames(allowedHostnames...))
165+
}
166+
opts = append(opts, policies.WithIncludeRawData(debug))
167+
opts = append(opts, policies.WithEnablePrint(enablePrint))
168+
return opts
169+
}
170+
171+
// buildEvalSummary converts a PolicyEvaluation to an EvalSummary
172+
func buildEvalSummary(policyEv *v12.PolicyEvaluation, debug bool) *EvalSummary {
132173
summary := &EvalSummary{
133174
Result: &EvalResult{
134175
Skipped: policyEv.GetSkipped(),
@@ -153,7 +194,7 @@ func verifyMaterial(pol *v1.Policies, material *v12.Attestation_Material, materi
153194
if rr == nil {
154195
continue
155196
}
156-
// Take the first input found, as we only allow one material input
197+
// Take the first input found
157198
if len(summary.DebugInfo.Inputs) == 0 && rr.Input != nil {
158199
summary.DebugInfo.Inputs = append(summary.DebugInfo.Inputs, json.RawMessage(rr.Input))
159200
}
@@ -164,7 +205,7 @@ func verifyMaterial(pol *v1.Policies, material *v12.Attestation_Material, materi
164205
}
165206
}
166207

167-
return summary, nil
208+
return summary
168209
}
169210

170211
func craftMaterial(materialPath, materialKind string, logger *zerolog.Logger) (*v12.Attestation_Material, error) {
@@ -174,19 +215,6 @@ func craftMaterial(materialPath, materialKind string, logger *zerolog.Logger) (*
174215
Uploader: nil, // Skip uploads
175216
}
176217

177-
// If no material path provided, create empty material
178-
if materialPath == "" {
179-
return &v12.Attestation_Material{
180-
Id: "empty-input",
181-
M: &v12.Attestation_Material_String_{
182-
String_: &v12.Attestation_Material_KeyVal{
183-
Id: "empty-input",
184-
Value: "{}",
185-
},
186-
},
187-
}, nil
188-
}
189-
190218
// Explicit kind
191219
if materialKind != "" {
192220
kind, ok := v1.CraftingSchema_Material_MaterialType_value[materialKind]

‎app/cli/internal/policydevel/eval_test.go‎

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,4 +236,86 @@ func TestEvaluateGenericPolicies(t *testing.T) {
236236
require.Error(t, err)
237237
assert.Contains(t, err.Error(), "missing required input")
238238
})
239+
240+
t.Run("kind-only policy without material - should fail", func(t *testing.T) {
241+
opts := &EvalOptions{
242+
PolicyPath: "testdata/kind-only-policy.yaml",
243+
Inputs: map[string]string{
244+
"environment": "production",
245+
},
246+
}
247+
248+
_, err := Evaluate(opts, logger)
249+
require.Error(t, err)
250+
assert.Contains(t, err.Error(), "no execution branch matched, or all of them were ignored")
251+
})
252+
}
253+
254+
func TestEvaluateMultiKindPolicies(t *testing.T) {
255+
logger := zerolog.New(os.Stderr)
256+
257+
t.Run("multi-kind policy with generic evaluation - only generic script runs", func(t *testing.T) {
258+
opts := &EvalOptions{
259+
PolicyPath: "testdata/multi-kind-policy.yaml",
260+
Inputs: map[string]string{
261+
"environment": "production",
262+
},
263+
}
264+
265+
result, err := Evaluate(opts, logger)
266+
require.NoError(t, err)
267+
require.NotNil(t, result)
268+
assert.False(t, result.Result.Skipped)
269+
270+
require.Len(t, result.Result.Violations, 1)
271+
assert.Contains(t, result.Result.Violations[0], "Generic check")
272+
assert.NotContains(t, result.Result.Violations[0], "SBOM-specific")
273+
})
274+
275+
t.Run("multi-kind policy with generic evaluation - staging no violations", func(t *testing.T) {
276+
opts := &EvalOptions{
277+
PolicyPath: "testdata/multi-kind-policy.yaml",
278+
Inputs: map[string]string{
279+
"environment": "staging",
280+
},
281+
}
282+
283+
result, err := Evaluate(opts, logger)
284+
require.NoError(t, err)
285+
require.NotNil(t, result)
286+
assert.False(t, result.Result.Skipped)
287+
288+
assert.Empty(t, result.Result.Violations)
289+
})
290+
291+
t.Run("multi-kind policy with SBOM material - only SBOM-specific policy runs", func(t *testing.T) {
292+
tempDir := t.TempDir()
293+
294+
sbomContent := `{
295+
"bomFormat": "CycloneDX",
296+
"specVersion": "1.4",
297+
"version": 1,
298+
"components": []
299+
}`
300+
sbomPath := filepath.Join(tempDir, "test-sbom.json")
301+
require.NoError(t, os.WriteFile(sbomPath, []byte(sbomContent), 0600))
302+
303+
opts := &EvalOptions{
304+
PolicyPath: "testdata/multi-kind-policy.yaml",
305+
MaterialPath: sbomPath,
306+
MaterialKind: "SBOM_CYCLONEDX_JSON",
307+
Inputs: map[string]string{
308+
"environment": "production",
309+
},
310+
}
311+
312+
result, err := Evaluate(opts, logger)
313+
require.NoError(t, err)
314+
require.NotNil(t, result)
315+
assert.False(t, result.Result.Skipped)
316+
317+
require.Len(t, result.Result.Violations, 2)
318+
assert.Contains(t, result.Result.Violations[0], "Generic check")
319+
assert.Contains(t, result.Result.Violations[1], "SBOM-specific check")
320+
})
239321
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
apiVersion: workflowcontract.chainloop.dev/v1
2+
kind: Policy
3+
metadata:
4+
name: kind-only-policy
5+
description: Policy with only kind-specific scripts (no generic)
6+
spec:
7+
inputs:
8+
- name: environment
9+
description: Deployment environment
10+
required: true
11+
policies:
12+
- kind: SBOM_CYCLONEDX_JSON
13+
embedded: |
14+
package main
15+
import rego.v1
16+
17+
result := {
18+
"violations": violations,
19+
}
20+
21+
violations contains "SBOM check failed" if {
22+
true
23+
}
24+
- kind: CONTAINER_IMAGE
25+
embedded: |
26+
package main
27+
import rego.v1
28+
29+
result := {
30+
"violations": violations,
31+
}
32+
33+
violations contains "Container check failed" if {
34+
true
35+
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
apiVersion: workflowcontract.chainloop.dev/v1
2+
kind: Policy
3+
metadata:
4+
name: multi-kind-policy
5+
description: Policy with multiple kinds - generic and material-specific
6+
spec:
7+
inputs:
8+
- name: environment
9+
description: Deployment environment
10+
required: true
11+
policies:
12+
- embedded: |
13+
package main
14+
import rego.v1
15+
16+
result := {
17+
"violations": violations,
18+
}
19+
20+
violations contains msg if {
21+
input.args.environment == "production"
22+
msg := "Generic check: production deployments require additional validation"
23+
}
24+
- kind: SBOM_CYCLONEDX_JSON
25+
embedded: |
26+
package main
27+
import rego.v1
28+
29+
result := {
30+
"violations": violations,
31+
}
32+
33+
violations contains "SBOM-specific check: this should not run for generic evaluation" if {
34+
true
35+
}

‎pkg/policies/policies.go‎

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,28 @@ func (pv *PolicyVerifier) VerifyMaterial(ctx context.Context, material *v12.Atte
159159
return result, nil
160160
}
161161

162+
// EvaluateGeneric evaluates a single policy attachment without material context.
163+
// This is used for generic policies that operate purely on input arguments.
164+
func (pv *PolicyVerifier) EvaluateGeneric(ctx context.Context, attachment *v1.PolicyAttachment, input []byte) (*v12.PolicyEvaluation, error) {
165+
// Use empty JSON if no input provided
166+
if len(input) == 0 {
167+
input = []byte("{}")
168+
}
169+
170+
// Evaluate without material context
171+
ev, err := pv.evaluatePolicyAttachment(ctx, attachment, input,
172+
&evalOpts{
173+
kind: v1.CraftingSchema_Material_MATERIAL_TYPE_UNSPECIFIED,
174+
name: "",
175+
},
176+
)
177+
if err != nil {
178+
return nil, NewPolicyError(err)
179+
}
180+
181+
return ev, nil
182+
}
183+
162184
type evalOpts struct {
163185
name string
164186
kind v1.CraftingSchema_Material_MaterialType

0 commit comments

Comments
 (0)