From 1c4a436a0d04a9502c9540179dff36101264706e Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Thu, 26 Feb 2026 17:12:18 -0800 Subject: [PATCH] policy: match parent unknown keys in collectUnknowns Allow child refs from partial evaluation to match allowed parent keys on path boundaries and return canonical unknowns for metadata resolution. Add tests for parent-child matching and boundary safety cases. Signed-off-by: Tonis Tiigi --- policy/utils_test.go | 44 ++++++++++++++++++++++++++++++++++++++++++++ policy/validate.go | 31 +++++++++++++++++++++++++++++-- 2 files changed, 73 insertions(+), 2 deletions(-) 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