Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions pkg/policies/policies.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"fmt"
"net/url"
"path/filepath"
"regexp"
"runtime"
"slices"
"strings"
Expand Down Expand Up @@ -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)
Expand Down
84 changes: 84 additions & 0 deletions pkg/policies/policies_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ package policies

import (
"context"
"fmt"
"io/fs"
"os"
"testing"
Expand Down Expand Up @@ -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

Expand Down
26 changes: 26 additions & 0 deletions pkg/policies/testdata/policy_undefined_builtin.rego
Original file line number Diff line number Diff line change
@@ -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")
}
8 changes: 8 additions & 0 deletions pkg/policies/testdata/policy_undefined_builtin.yaml
Original file line number Diff line number Diff line change
@@ -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
Loading