Skip to content

Commit a93cc49

Browse files
authored
fix(cli): evaluate kind-less policy specs in policy develop eval (#3783)
1 parent 7cf9541 commit a93cc49

3 files changed

Lines changed: 105 additions & 12 deletions

File tree

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

Lines changed: 13 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -69,20 +69,17 @@ type EvalSummaryDebugInfo struct {
6969
}
7070

7171
func Evaluate(opts *EvalOptions, logger zerolog.Logger) (*EvalSummary, error) {
72-
// 1. Create crafting schema
73-
policies, err := createPolicies(opts.PolicyPath, opts.Inputs)
74-
if err != nil {
75-
return nil, err
76-
}
77-
78-
// 2. Craft material with annotations
72+
// 1. Craft material with annotations
7973
crafted, err := craftMaterial(opts.MaterialPath, opts.MaterialKind, &logger)
8074
if err != nil {
8175
return nil, err
8276
}
8377
material := crafted.Material
8478
mergeAnnotations(material, opts.Annotations, &logger)
8579

80+
// 2. Attach the policy to the crafted material
81+
policies := createPolicies(opts.PolicyPath, opts.Inputs, material.GetId())
82+
8683
// 3. Verify material against policy. A crafter that transformed the artifact
8784
// before storing it hands back what it stored, and that is what the policy
8885
// must be evaluated against — `policy devel eval` has to reproduce what
@@ -131,7 +128,11 @@ func mergeAnnotations(material *v12.Attestation_Material, annotations map[string
131128
}
132129
}
133130

134-
func createPolicies(policyPath string, inputs map[string]string) (*v1.Policies, error) {
131+
// createPolicies attaches the policy to the material named materialName, the same
132+
// way a contract attaches it with selector.name. Without the selector, a policy
133+
// whose spec has no kind would never apply, since it has no type to match the
134+
// material against. A spec with a kind still only applies to materials of that kind.
135+
func createPolicies(policyPath string, inputs map[string]string, materialName string) *v1.Policies {
135136
// Check if the policy path already has a scheme (chainloop://, http://, https://, file://)
136137
ref := policyPath
137138
scheme, _ := policies.RefParts(policyPath)
@@ -143,12 +144,12 @@ func createPolicies(policyPath string, inputs map[string]string) (*v1.Policies,
143144
return &v1.Policies{
144145
Materials: []*v1.PolicyAttachment{
145146
{
146-
Policy: &v1.PolicyAttachment_Ref{Ref: ref},
147-
With: inputs,
147+
Policy: &v1.PolicyAttachment_Ref{Ref: ref},
148+
Selector: &v1.PolicyAttachment_MaterialSelector{Name: materialName},
149+
With: inputs,
148150
},
149151
},
150-
Attestation: nil,
151-
}, nil
152+
}
152153
}
153154

154155
func verifyMaterial(pol *v1.Policies, material *v12.Attestation_Material, materialPath string, content []byte, debug bool, allowedHostnames []string, attestationClient controlplanev1.AttestationServiceClient, grpcConn *grpc.ClientConn, projectName, projectVersion string, logger *zerolog.Logger) (*EvalSummary, error) {

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

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -233,6 +233,82 @@ func TestEvaluateSimplifiedPolicies(t *testing.T) {
233233
})
234234
}
235235

236+
// `policy develop eval` selects the policy by material name, as a contract does.
237+
func TestEvaluateMaterialSelection(t *testing.T) {
238+
const (
239+
anyKindPolicy = "testdata/any-kind-policy.yaml"
240+
evidenceKind = "EVIDENCE"
241+
)
242+
243+
tempDir := t.TempDir()
244+
logger := zerolog.New(os.Stderr)
245+
246+
passing := filepath.Join(tempDir, "pass.json")
247+
require.NoError(t, os.WriteFile(passing, []byte(`{}`), 0600))
248+
failing := filepath.Join(tempDir, "fail.json")
249+
require.NoError(t, os.WriteFile(failing, []byte(`{"fail": true}`), 0600))
250+
251+
testCases := []struct {
252+
name string
253+
policyPath string
254+
kind string
255+
materialPath string
256+
wantViolations []string
257+
wantErr string
258+
}{
259+
{
260+
name: "kind-less spec with explicit kind",
261+
policyPath: anyKindPolicy,
262+
kind: evidenceKind,
263+
materialPath: passing,
264+
},
265+
{
266+
name: "kind-less spec reports violations",
267+
policyPath: anyKindPolicy,
268+
kind: evidenceKind,
269+
materialPath: failing,
270+
wantViolations: []string{"material asked to fail"},
271+
},
272+
{
273+
name: "kind-less spec with auto-detected kind",
274+
policyPath: anyKindPolicy,
275+
materialPath: "testdata/test-sbom.json",
276+
},
277+
{
278+
name: "spec with a matching kind",
279+
policyPath: "testdata/sbom-min-components-policy.yaml",
280+
kind: "SBOM_CYCLONEDX_JSON",
281+
materialPath: "testdata/test-sbom.json",
282+
wantViolations: []string{"SBOM must have at least 2 components"},
283+
},
284+
{
285+
name: "spec with a different kind is not applied",
286+
policyPath: "testdata/sbom-min-components-policy.yaml",
287+
kind: evidenceKind,
288+
materialPath: passing,
289+
wantErr: "no execution branch matched",
290+
},
291+
}
292+
293+
for _, tc := range testCases {
294+
t.Run(tc.name, func(t *testing.T) {
295+
result, err := Evaluate(&EvalOptions{
296+
PolicyPath: tc.policyPath,
297+
MaterialKind: tc.kind,
298+
MaterialPath: tc.materialPath,
299+
}, logger)
300+
if tc.wantErr != "" {
301+
require.ErrorContains(t, err, tc.wantErr)
302+
return
303+
}
304+
305+
require.NoError(t, err)
306+
assert.False(t, result.Result.Skipped)
307+
assert.ElementsMatch(t, tc.wantViolations, result.Result.Violations)
308+
})
309+
}
310+
}
311+
236312
// fixtureGitHubPAT is assembled from fragments so that this file does not itself
237313
// carry a credential-shaped literal for secret scanners to flag.
238314
const fixtureGitHubPAT = "ghp_erOZlZv0B1e3amrQ" + "ugdwZ8Ro2W4kDql9WPTf"
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
apiVersion: workflowcontract.chainloop.dev/v1
2+
kind: Policy
3+
metadata:
4+
name: any-kind
5+
description: Policy without kind, it applies to any material type
6+
spec:
7+
policies:
8+
- embedded: |
9+
package any_kind
10+
11+
import rego.v1
12+
13+
violations contains msg if {
14+
input.fail
15+
msg := "material asked to fail"
16+
}

0 commit comments

Comments
 (0)