Skip to content

Commit 9f1e7ec

Browse files
committed
fix(policies): graceful degradation when policy uses undefined builtin
When a Rego policy references a custom builtin function that the current CLI doesn't have registered (e.g. chainloop.vulnerability on an older client), OPA rejects the policy at compile time with rego_type_error. Instead of hard-failing the entire attestation, catch undefined function errors and degrade the policy evaluation to skipped with a descriptive reason prompting the user to upgrade. Closes #2979 Signed-off-by: Miguel Martinez Trivino <miguel@chainloop.dev>
1 parent 7542585 commit 9f1e7ec

4 files changed

Lines changed: 121 additions & 0 deletions

File tree

pkg/policies/policies.go

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -591,12 +591,31 @@ func (pv *PolicyVerifier) executeScript(ctx context.Context, script *engine.Poli
591591
// Execute using the selected engine
592592
res, err := policyEngine.Verify(ctx, script, material, argsAny)
593593
if err != nil {
594+
// Gracefully degrade when a policy references a builtin function that this
595+
// client version doesn't have registered (e.g., chainloop.vulnerability on an
596+
// older CLI). Mark as skipped so the attestation can proceed.
597+
if isUndefinedBuiltinError(err) {
598+
pv.logger.Warn().Str("policy", script.Name).Err(err).
599+
Msg("policy uses an unsupported builtin function, skipping — upgrade your client")
600+
return &engine.EvaluationResult{
601+
Skipped: true,
602+
SkipReason: fmt.Sprintf("unsupported builtin function (upgrade your client): %s", err),
603+
Violations: make([]*engine.PolicyViolation, 0),
604+
}, nil
605+
}
594606
return nil, fmt.Errorf("failed to execute policy: %w", err)
595607
}
596608

597609
return res, nil
598610
}
599611

612+
// isUndefinedBuiltinError returns true if the error is an OPA type error
613+
// caused by an undefined function, which typically means the client doesn't
614+
// have a required custom builtin registered.
615+
func isUndefinedBuiltinError(err error) bool {
616+
return strings.Contains(err.Error(), "rego_type_error: undefined function")
617+
}
618+
600619
// LoadPolicySpec loads and validates a policy spec from a contract
601620
func (pv *PolicyVerifier) loadPolicySpec(ctx context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, *PolicyDescriptor, error) {
602621
loader, err := pv.getLoader(attachment)

pkg/policies/policies_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package policies
1717

1818
import (
1919
"context"
20+
"fmt"
2021
"io/fs"
2122
"os"
2223
"testing"
@@ -1361,6 +1362,73 @@ func (s *testSuite) TestShouldEvaluateAtPhase() {
13611362
}
13621363
}
13631364

1365+
func (s *testSuite) TestUndefinedBuiltinGracefulDegradation() {
1366+
content, err := os.ReadFile("testdata/sbom-spdx.json")
1367+
s.Require().NoError(err)
1368+
1369+
schema := &v12.CraftingSchema{
1370+
Policies: &v12.Policies{
1371+
Materials: []*v12.PolicyAttachment{
1372+
{
1373+
Policy: &v12.PolicyAttachment_Ref{Ref: "file://testdata/policy_undefined_builtin.yaml"},
1374+
},
1375+
},
1376+
},
1377+
}
1378+
material := &v1.Attestation_Material{
1379+
M: &v1.Attestation_Material_Artifact_{Artifact: &v1.Attestation_Material_Artifact{
1380+
Content: content,
1381+
}},
1382+
MaterialType: v12.CraftingSchema_Material_SBOM_SPDX_JSON,
1383+
InlineCas: true,
1384+
}
1385+
1386+
verifier := NewPolicyVerifier(schema.Policies, nil, &s.logger)
1387+
1388+
res, err := verifier.VerifyMaterial(context.TODO(), material, "")
1389+
s.Require().NoError(err, "undefined builtin should not cause a hard error")
1390+
s.Require().Len(res, 1)
1391+
s.True(res[0].Skipped, "policy should be marked as skipped")
1392+
s.Require().Len(res[0].SkipReasons, 1)
1393+
s.Contains(res[0].SkipReasons[0], "unsupported builtin function")
1394+
s.Empty(res[0].Violations, "skipped policy should have no violations")
1395+
}
1396+
1397+
func (s *testSuite) TestIsUndefinedBuiltinError() {
1398+
cases := []struct {
1399+
name string
1400+
err error
1401+
expect bool
1402+
}{
1403+
{
1404+
name: "undefined function error",
1405+
err: fmt.Errorf("failed to evaluate policy: 1 error occurred: vulnerabilities:115: rego_type_error: undefined function chainloop.vulnerability"),
1406+
expect: true,
1407+
},
1408+
{
1409+
name: "wrapped undefined function error",
1410+
err: fmt.Errorf("failed to execute policy: %w", fmt.Errorf("rego_type_error: undefined function chainloop.nonexistent")),
1411+
expect: true,
1412+
},
1413+
{
1414+
name: "regular evaluation error",
1415+
err: fmt.Errorf("failed to evaluate policy: some other error"),
1416+
expect: false,
1417+
},
1418+
{
1419+
name: "syntax error",
1420+
err: fmt.Errorf("rego_parse_error: unexpected token"),
1421+
expect: false,
1422+
},
1423+
}
1424+
1425+
for _, tc := range cases {
1426+
s.Run(tc.name, func() {
1427+
s.Equal(tc.expect, isUndefinedBuiltinError(tc.err))
1428+
})
1429+
}
1430+
}
1431+
13641432
type testSuite struct {
13651433
suite.Suite
13661434

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
package main
2+
3+
import rego.v1
4+
5+
result := {
6+
"skipped": skipped,
7+
"violations": violations,
8+
"skip_reason": skip_reason,
9+
}
10+
11+
default skip_reason := ""
12+
13+
skip_reason := m if {
14+
not valid_input
15+
m := "invalid input"
16+
}
17+
18+
default skipped := true
19+
20+
skipped := false if valid_input
21+
22+
valid_input := true
23+
24+
violations contains v if {
25+
v := chainloop.nonexistent("test")
26+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
apiVersion: chainloop.dev/v1
2+
kind: Policy
3+
metadata:
4+
name: undefined-builtin
5+
description: Test policy that uses an undefined builtin function
6+
spec:
7+
type: SBOM_SPDX_JSON
8+
path: policy_undefined_builtin.rego

0 commit comments

Comments
 (0)