diff --git a/policy/utils_test.go b/policy/utils_test.go index 37a692e5a..3f7ffb250 100644 --- a/policy/utils_test.go +++ b/policy/utils_test.go @@ -31,6 +31,9 @@ func TestTrimKey(t *testing.T) { {"input.git.tag[0]", "git.tag"}, {"input.image.provenance.materials[0].image.hasProvenance", "image.provenance.materials[0].image.hasProvenance"}, {"image.provenance.materials[0].image.labels", "image.provenance.materials[0].image.labels"}, + {"input.image.provenance.materials[0].image.provenance.predicateType", "image.provenance.materials[0].image.provenance.predicateType"}, + {"input.image.provenance.materials[0].image.signatures[0].signer.certificateIssuer", "image.provenance.materials[0].image.signatures[0].signer.certificateIssuer"}, + {"input.image.provenance.materials[10].image.hasProvenance", "image.provenance.materials[10].image.hasProvenance"}, {"a.b.c", "a.b"}, } @@ -61,6 +64,47 @@ func TestCollectUnknowns(t *testing.T) { require.ElementsMatch(t, []string{"image.signatures", "image.provenance.materials[0].image.hasProvenance"}, filtered) } +func TestCollectUnknownsParentAllowedMatchesChildRef(t *testing.T) { + mod, err := ast.ParseModule("x.rego", ` + package x + p if { + input.image.provenance.materials[0].image.provenance.predicateType != "" + input.image.provenance.materials[0].image.signatures[0].signer.certificateIssuer != "" + input.image.provenance.materials[0].git.tag.name != "" + input.foo.bar != "" + input.image.provenance.materials[10].image.hasProvenance + } + `) + require.NoError(t, err) + + filtered := collectUnknowns([]*ast.Module{mod}, []string{ + "input.image.provenance.materials[0].image.provenance", + "input.image.provenance.materials[0].image.signatures", + "input.image.provenance.materials[0].git.tag", + "input.foo.b", + "input.image.provenance.materials[1].image", + }) + + require.ElementsMatch(t, []string{ + "image.provenance.materials[0].image.provenance", + "image.provenance.materials[0].image.signatures", + "image.provenance.materials[0].git.tag", + }, filtered) +} + +func TestMatchAllowedOrParentBoundary(t *testing.T) { + allowed := map[string]struct{}{ + "foo.b": {}, + "image.provenance.materials[1].image": {}, + } + + _, ok := matchAllowedOrParent("foo.bar", allowed) + require.False(t, ok) + + _, ok = matchAllowedOrParent("image.provenance.materials[10].image.hasProvenance", allowed) + require.False(t, ok) +} + func TestRuntimeUnknownInputRefs(t *testing.T) { require.Nil(t, runtimeUnknownInputRefs(nil)) require.Nil(t, runtimeUnknownInputRefs(&state{})) diff --git a/policy/validate.go b/policy/validate.go index 643502458..69599cb59 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -808,15 +808,42 @@ func collectUnknowns(mods []*ast.Module, allowed []string) []string { } filtered := make([]string, 0, len(out)) + filteredSeen := map[string]struct{}{} for _, k := range out { - if _, ok := valid[k]; ok { - filtered = append(filtered, k) + matched, ok := matchAllowedOrParent(k, valid) + if !ok { + continue } + if _, exists := filteredSeen[matched]; exists { + continue + } + filteredSeen[matched] = struct{}{} + filtered = append(filtered, matched) } return filtered } +func matchAllowedOrParent(key string, allowed map[string]struct{}) (string, bool) { + if _, ok := allowed[key]; ok { + return key, true + } + // Find the nearest parent on a component boundary. + for i := len(key) - 1; i >= 0; i-- { + switch key[i] { + case '.', '[': + if i == 0 { + continue + } + candidate := key[:i] + if _, ok := allowed[candidate]; ok { + return candidate, true + } + } + } + return "", false +} + func runtimeUnknownInputRefs(st *state) []string { if st == nil || len(st.Unknowns) == 0 { return nil