Skip to content

Commit 7236c8a

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 7236c8a

4 files changed

Lines changed: 151 additions & 0 deletions

File tree

pkg/policies/policies.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323
"net/url"
2424
"path/filepath"
25+
"regexp"
2526
"runtime"
2627
"slices"
2728
"strings"
@@ -591,12 +592,44 @@ func (pv *PolicyVerifier) executeScript(ctx context.Context, script *engine.Poli
591592
// Execute using the selected engine
592593
res, err := policyEngine.Verify(ctx, script, material, argsAny)
593594
if err != nil {
595+
// Gracefully degrade when a policy references a chainloop.* builtin that this
596+
// client version doesn't have registered (e.g., chainloop.vulnerability on an
597+
// older CLI). Mark as skipped so the attestation can proceed.
598+
// Intentionally blocked OPA functions (opa.runtime, trace, etc.) are NOT
599+
// caught here — those remain hard errors.
600+
if builtins := undefinedChainloopBuiltins(err); len(builtins) > 0 {
601+
pv.logger.Warn().Str("policy", script.Name).Strs("builtins", builtins).
602+
Msg("policy requires builtin functions not available in this CLI version, skipping — please upgrade")
603+
return &engine.EvaluationResult{
604+
Skipped: true,
605+
SkipReason: fmt.Sprintf("policy requires builtin functions not available in this CLI version: %s — please upgrade to the latest version", strings.Join(builtins, ", ")),
606+
Violations: make([]*engine.PolicyViolation, 0),
607+
}, nil
608+
}
594609
return nil, fmt.Errorf("failed to execute policy: %w", err)
595610
}
596611

597612
return res, nil
598613
}
599614

615+
var undefinedChainloopBuiltinRe = regexp.MustCompile(`rego_type_error: undefined function (chainloop\.\w+)`)
616+
617+
// undefinedChainloopBuiltins extracts chainloop.* function names from OPA
618+
// undefined-function type errors. Returns nil if the error doesn't involve
619+
// any chainloop builtins (e.g., intentionally blocked OPA functions like
620+
// opa.runtime or trace still produce hard errors).
621+
func undefinedChainloopBuiltins(err error) []string {
622+
matches := undefinedChainloopBuiltinRe.FindAllStringSubmatch(err.Error(), -1)
623+
if len(matches) == 0 {
624+
return nil
625+
}
626+
names := make([]string, 0, len(matches))
627+
for _, m := range matches {
628+
names = append(names, m[1])
629+
}
630+
return names
631+
}
632+
600633
// LoadPolicySpec loads and validates a policy spec from a contract
601634
func (pv *PolicyVerifier) loadPolicySpec(ctx context.Context, attachment *v1.PolicyAttachment) (*v1.Policy, *PolicyDescriptor, error) {
602635
loader, err := pv.getLoader(attachment)

pkg/policies/policies_test.go

Lines changed: 84 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,89 @@ 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 chainloop 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], "chainloop.nonexistent")
1394+
s.Contains(res[0].SkipReasons[0], "please upgrade")
1395+
s.Empty(res[0].Violations, "skipped policy should have no violations")
1396+
}
1397+
1398+
func (s *testSuite) TestUndefinedChainloopBuiltins() {
1399+
cases := []struct {
1400+
name string
1401+
err error
1402+
expect []string
1403+
}{
1404+
{
1405+
name: "single chainloop builtin",
1406+
err: fmt.Errorf("failed to evaluate policy: 1 error occurred: vulnerabilities:115: rego_type_error: undefined function chainloop.vulnerability"),
1407+
expect: []string{"chainloop.vulnerability"},
1408+
},
1409+
{
1410+
name: "wrapped chainloop builtin",
1411+
err: fmt.Errorf("failed to execute policy: %w", fmt.Errorf("rego_type_error: undefined function chainloop.nonexistent")),
1412+
expect: []string{"chainloop.nonexistent"},
1413+
},
1414+
{
1415+
name: "multiple chainloop builtins",
1416+
err: fmt.Errorf("rego_type_error: undefined function chainloop.foo\nrego_type_error: undefined function chainloop.bar"),
1417+
expect: []string{"chainloop.foo", "chainloop.bar"},
1418+
},
1419+
{
1420+
name: "blocked OPA function is not caught",
1421+
err: fmt.Errorf("rego_type_error: undefined function opa.runtime"),
1422+
expect: nil,
1423+
},
1424+
{
1425+
name: "blocked trace function is not caught",
1426+
err: fmt.Errorf("rego_type_error: undefined function trace"),
1427+
expect: nil,
1428+
},
1429+
{
1430+
name: "regular evaluation error",
1431+
err: fmt.Errorf("failed to evaluate policy: some other error"),
1432+
expect: nil,
1433+
},
1434+
{
1435+
name: "syntax error",
1436+
err: fmt.Errorf("rego_parse_error: unexpected token"),
1437+
expect: nil,
1438+
},
1439+
}
1440+
1441+
for _, tc := range cases {
1442+
s.Run(tc.name, func() {
1443+
s.Equal(tc.expect, undefinedChainloopBuiltins(tc.err))
1444+
})
1445+
}
1446+
}
1447+
13641448
type testSuite struct {
13651449
suite.Suite
13661450

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)