diff --git a/pkg/policies/policies.go b/pkg/policies/policies.go index c29abecdc..b1dcdb9e2 100644 --- a/pkg/policies/policies.go +++ b/pkg/policies/policies.go @@ -22,6 +22,7 @@ import ( "fmt" "net/url" "path/filepath" + "regexp" "runtime" "slices" "strings" @@ -591,12 +592,44 @@ func (pv *PolicyVerifier) executeScript(ctx context.Context, script *engine.Poli // Execute using the selected engine res, err := policyEngine.Verify(ctx, script, material, argsAny) if err != nil { + // Gracefully degrade when a policy references a chainloop.* builtin that this + // client version doesn't have registered (e.g., chainloop.vulnerability on an + // older CLI). Mark as skipped so the attestation can proceed. + // Intentionally blocked OPA functions (opa.runtime, trace, etc.) are NOT + // caught here — those remain hard errors. + if builtins := undefinedChainloopBuiltins(err); len(builtins) > 0 { + pv.logger.Warn().Str("policy", script.Name).Strs("builtins", builtins). + Msg("policy requires builtin functions not available in this CLI version, skipping — please upgrade") + return &engine.EvaluationResult{ + Skipped: true, + SkipReason: fmt.Sprintf("policy requires builtin functions not available in this CLI version: %s — please upgrade to the latest version", strings.Join(builtins, ", ")), + Violations: make([]*engine.PolicyViolation, 0), + }, nil + } return nil, fmt.Errorf("failed to execute policy: %w", err) } return res, nil } +var undefinedChainloopBuiltinRe = regexp.MustCompile(`rego_type_error: undefined function (chainloop\.\w+)`) + +// undefinedChainloopBuiltins extracts chainloop.* function names from OPA +// undefined-function type errors. Returns nil if the error doesn't involve +// any chainloop builtins (e.g., intentionally blocked OPA functions like +// opa.runtime or trace still produce hard errors). +func undefinedChainloopBuiltins(err error) []string { + matches := undefinedChainloopBuiltinRe.FindAllStringSubmatch(err.Error(), -1) + if len(matches) == 0 { + return nil + } + names := make([]string, 0, len(matches)) + for _, m := range matches { + names = append(names, m[1]) + } + return names +} + // LoadPolicySpec loads and validates a policy spec from a contract func (pv *PolicyVerifier) loadPolicySpec(ctx context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, *PolicyDescriptor, error) { loader, err := pv.getLoader(attachment) diff --git a/pkg/policies/policies_test.go b/pkg/policies/policies_test.go index 1b130b50d..bdda2f3b2 100644 --- a/pkg/policies/policies_test.go +++ b/pkg/policies/policies_test.go @@ -17,6 +17,7 @@ package policies import ( "context" + "fmt" "io/fs" "os" "testing" @@ -1361,6 +1362,89 @@ func (s *testSuite) TestShouldEvaluateAtPhase() { } } +func (s *testSuite) TestUndefinedBuiltinGracefulDegradation() { + content, err := os.ReadFile("testdata/sbom-spdx.json") + s.Require().NoError(err) + + schema := &v12.CraftingSchema{ + Policies: &v12.Policies{ + Materials: []*v12.PolicyAttachment{ + { + Policy: &v12.PolicyAttachment_Ref{Ref: "file://testdata/policy_undefined_builtin.yaml"}, + }, + }, + }, + } + material := &v1.Attestation_Material{ + M: &v1.Attestation_Material_Artifact_{Artifact: &v1.Attestation_Material_Artifact{ + Content: content, + }}, + MaterialType: v12.CraftingSchema_Material_SBOM_SPDX_JSON, + InlineCas: true, + } + + verifier := NewPolicyVerifier(schema.Policies, nil, &s.logger) + + res, err := verifier.VerifyMaterial(context.TODO(), material, "") + s.Require().NoError(err, "undefined chainloop builtin should not cause a hard error") + s.Require().Len(res, 1) + s.True(res[0].Skipped, "policy should be marked as skipped") + s.Require().Len(res[0].SkipReasons, 1) + s.Contains(res[0].SkipReasons[0], "chainloop.nonexistent") + s.Contains(res[0].SkipReasons[0], "please upgrade") + s.Empty(res[0].Violations, "skipped policy should have no violations") +} + +func (s *testSuite) TestUndefinedChainloopBuiltins() { + cases := []struct { + name string + err error + expect []string + }{ + { + name: "single chainloop builtin", + err: fmt.Errorf("failed to evaluate policy: 1 error occurred: vulnerabilities:115: rego_type_error: undefined function chainloop.vulnerability"), + expect: []string{"chainloop.vulnerability"}, + }, + { + name: "wrapped chainloop builtin", + err: fmt.Errorf("failed to execute policy: %w", fmt.Errorf("rego_type_error: undefined function chainloop.nonexistent")), + expect: []string{"chainloop.nonexistent"}, + }, + { + name: "multiple chainloop builtins", + err: fmt.Errorf("rego_type_error: undefined function chainloop.foo\nrego_type_error: undefined function chainloop.bar"), + expect: []string{"chainloop.foo", "chainloop.bar"}, + }, + { + name: "blocked OPA function is not caught", + err: fmt.Errorf("rego_type_error: undefined function opa.runtime"), + expect: nil, + }, + { + name: "blocked trace function is not caught", + err: fmt.Errorf("rego_type_error: undefined function trace"), + expect: nil, + }, + { + name: "regular evaluation error", + err: fmt.Errorf("failed to evaluate policy: some other error"), + expect: nil, + }, + { + name: "syntax error", + err: fmt.Errorf("rego_parse_error: unexpected token"), + expect: nil, + }, + } + + for _, tc := range cases { + s.Run(tc.name, func() { + s.Equal(tc.expect, undefinedChainloopBuiltins(tc.err)) + }) + } +} + type testSuite struct { suite.Suite diff --git a/pkg/policies/testdata/policy_undefined_builtin.rego b/pkg/policies/testdata/policy_undefined_builtin.rego new file mode 100644 index 000000000..1f4e4113c --- /dev/null +++ b/pkg/policies/testdata/policy_undefined_builtin.rego @@ -0,0 +1,26 @@ +package main + +import rego.v1 + +result := { + "skipped": skipped, + "violations": violations, + "skip_reason": skip_reason, +} + +default skip_reason := "" + +skip_reason := m if { + not valid_input + m := "invalid input" +} + +default skipped := true + +skipped := false if valid_input + +valid_input := true + +violations contains v if { + v := chainloop.nonexistent("test") +} diff --git a/pkg/policies/testdata/policy_undefined_builtin.yaml b/pkg/policies/testdata/policy_undefined_builtin.yaml new file mode 100644 index 000000000..f52b629f1 --- /dev/null +++ b/pkg/policies/testdata/policy_undefined_builtin.yaml @@ -0,0 +1,8 @@ +apiVersion: chainloop.dev/v1 +kind: Policy +metadata: + name: undefined-builtin + description: Test policy that uses an undefined builtin function +spec: + type: SBOM_SPDX_JSON + path: policy_undefined_builtin.rego