From 4e16277937299ff6b73dfc36455a6bf06342c444 Mon Sep 17 00:00:00 2001 From: Tonis Tiigi Date: Mon, 9 Feb 2026 18:01:52 -0800 Subject: [PATCH] policy: update unknown keys normalization Make the code more unified between validation and test command. Normalize to key without the input prefix. Signed-off-by: Tonis Tiigi --- commands/policy/test.go | 10 +++++++- policy/tester.go | 29 +++++++++++----------- policy/utils_test.go | 53 +++++++++++++++++++++++++++++++++++++++++ policy/validate.go | 41 +++++++++++++++++++++---------- 4 files changed, 104 insertions(+), 29 deletions(-) diff --git a/commands/policy/test.go b/commands/policy/test.go index 169ebf80f..ec956b50d 100644 --- a/commands/policy/test.go +++ b/commands/policy/test.go @@ -84,7 +84,7 @@ func runTest(ctx context.Context, out io.Writer, path string, opts policy.TestOp _, _ = fmt.Fprintln(out, "decision: ") } if len(result.MissingInput) > 0 { - _, _ = fmt.Fprintf(out, "missing_input: %s\n", strings.Join(result.MissingInput, ", ")) + _, _ = fmt.Fprintf(out, "missing_input: %s\n", strings.Join(withInputPrefix(result.MissingInput), ", ")) } if len(result.MetadataNeeded) > 0 { _, _ = fmt.Fprintf(out, "metadata_resolve: %s\n", strings.Join(result.MetadataNeeded, ", ")) @@ -106,6 +106,14 @@ func writeJSON(out io.Writer, label string, v any) { _, _ = fmt.Fprintf(out, "%s:\n%s\n", label, string(dt)) } +func withInputPrefix(keys []string) []string { + out := make([]string, len(keys)) + for i, k := range keys { + out[i] = "input." + k + } + return out +} + type policyTestResolver struct { dockerCli command.Cli builderName *string diff --git a/policy/tester.go b/policy/tester.go index b5f3bfaaa..e93cc3b95 100644 --- a/policy/tester.go +++ b/policy/tester.go @@ -311,7 +311,7 @@ func runPolicyTest(ctx context.Context, policyModules map[string]*ast.Module, te result.Allow = allow result.DenyMessages = deny - missing := missingInputRefs(policyPackageModules, effectiveInput) + missing := missingInputRefs(policyPackageModules, effectiveInput, runtimeUnknownInputRefs(testState), runtimeUnknownInputRefs(decisionState)) result.MissingInput = uniqueSortedStrings(missing) result.MetadataNeeded = summarizeMetadataRequests(result.MissingInput) @@ -534,7 +534,7 @@ func hasEnv(env Env) bool { func filterResolvableMissing(missing []string) []string { out := make([]string, 0, len(missing)) for _, m := range missing { - if strings.HasPrefix(m, "input.image.") || strings.HasPrefix(m, "input.git.") { + if strings.HasPrefix(m, "image.") || strings.HasPrefix(m, "git.") { out = append(out, m) } } @@ -595,24 +595,27 @@ func modulesForPackage(modules map[string]*ast.Module, pkgPath string) []*ast.Mo return out } -func missingInputRefs(mods []*ast.Module, input *Input) []string { +func missingInputRefs(mods []*ast.Module, input *Input, extraRefs ...[]string) []string { if len(mods) == 0 { return nil } inputMap := normalizeInput(input) refs := collectUnknowns(mods, nil) + for _, er := range extraRefs { + refs = append(refs, er...) + } + seen := map[string]struct{}{} missing := make([]string, 0, len(refs)) - for _, ref := range refs { - key := strings.TrimPrefix(ref, "input.") - if key == ref { - continue - } - key = trimKey(key) + for _, key := range refs { if key == "" { continue } + if _, ok := seen[key]; ok { + continue + } + seen[key] = struct{}{} if !inputHasPath(inputMap, strings.Split(key, ".")) { - missing = append(missing, "input."+key) + missing = append(missing, key) } } return missing @@ -703,11 +706,7 @@ func summarizeMetadataRequests(missing []string) []string { return nil } req := &gwpb.ResolveSourceMetaRequest{} - trimmed := make([]string, 0, len(missing)) - for _, m := range missing { - trimmed = append(trimmed, strings.TrimPrefix(m, "input.")) - } - if err := AddUnknowns(req, trimmed); err != nil { + if err := AddUnknowns(req, missing); err != nil { return nil } var out []string diff --git a/policy/utils_test.go b/policy/utils_test.go index a0e706e8e..6b9998930 100644 --- a/policy/utils_test.go +++ b/policy/utils_test.go @@ -3,6 +3,7 @@ package policy import ( "testing" + "github.com/open-policy-agent/opa/v1/ast" "github.com/stretchr/testify/require" ) @@ -18,12 +19,16 @@ func TestTrimKey(t *testing.T) { // one separator → stays as-is {"git.tag", "git.tag"}, {"git[tag", "git[tag"}, + {"input.git.tag", "git.tag"}, + {"input.git[tag", "git[tag"}, // multiple separators → cut before second one {"git.tag.author", "git.tag"}, {"git.tag.author.email", "git.tag"}, {"git.tag[0][1]", "git.tag"}, {"git.tag[0]", "git.tag"}, + {"input.git.tag.author", "git.tag"}, + {"input.git.tag[0]", "git.tag"}, {"a.b.c", "a.b"}, } @@ -34,3 +39,51 @@ func TestTrimKey(t *testing.T) { }) } } + +func TestCollectUnknowns(t *testing.T) { + mod, err := ast.ParseModule("x.rego", ` + package x + p if { + input.git.tag[0].author == "a" + input.image.signatures[_].signer.certificateIssuer != "" + data.foo.bar == 1 + } + `) + require.NoError(t, err) + + all := collectUnknowns([]*ast.Module{mod}, nil) + require.ElementsMatch(t, []string{"git.tag", "image.signatures"}, all) + + filtered := collectUnknowns([]*ast.Module{mod}, []string{"input.image.signatures"}) + require.Equal(t, []string{"image.signatures"}, filtered) +} + +func TestRuntimeUnknownInputRefs(t *testing.T) { + require.Nil(t, runtimeUnknownInputRefs(nil)) + require.Nil(t, runtimeUnknownInputRefs(&state{})) + + st := &state{ + Unknowns: map[string]struct{}{ + funcVerifyGitSignature: {}, + }, + } + require.Equal(t, []string{"git.commit"}, runtimeUnknownInputRefs(st)) +} + +func TestMissingInputRefsWithRuntimeUnknowns(t *testing.T) { + mod, err := ast.ParseModule("x.rego", ` + package x + p if { + input.git.ref != "" + } + `) + require.NoError(t, err) + + in := &Input{ + Git: &Git{ + Ref: "refs/heads/main", + }, + } + missing := missingInputRefs([]*ast.Module{mod}, in, []string{"git.commit"}) + require.Equal(t, []string{"git.commit"}, missing) +} diff --git a/policy/validate.go b/policy/validate.go index 1deaa6d63..3cd5b8919 100644 --- a/policy/validate.go +++ b/policy/validate.go @@ -224,9 +224,7 @@ func (p *Policy) CheckPolicy(ctx context.Context, req *policysession.CheckPolicy return nil, nil, err } unk := collectUnknowns(pq.Support, unknowns) - if _, ok := st.Unknowns[funcVerifyGitSignature]; ok { - unk = append(unk, "input.git.commit") - } + unk = append(unk, runtimeUnknownInputRefs(st)...) if len(unk) > 0 { next := &gwpb.ResolveSourceMetaRequest{ @@ -621,14 +619,12 @@ func AddUnknowns(req *gwpb.ResolveSourceMetaRequest, unk []string) error { func AddUnknownsWithLogger(logf func(logrus.Level, string), req *gwpb.ResolveSourceMetaRequest, unk []string) error { unk2 := make([]string, 0, len(unk)) for _, u := range unk { - k := strings.TrimPrefix(u, "input.") - k = trimKey(k) - switch k { + switch u { case "image", "git", "http", "local": // parents are returned as unknowns for some reason, ignore continue default: - unk2 = append(unk2, k) + unk2 = append(unk2, u) } } if len(unk2) == 0 { @@ -680,8 +676,10 @@ func collectUnknowns(mods []*ast.Module, allowed []string) []string { for _, mod := range mods { ast.WalkRefs(mod, func(ref ast.Ref) bool { if ref.HasPrefix(ast.InputRootRef) { - s := ref.String() // e.g. "input.request.path" - s = "input." + trimKey(strings.TrimPrefix(s, "input.")) + s := trimKey(ref.String()) + if s == "" { + return true + } if _, ok := seen[s]; !ok { seen[s] = struct{}{} out = append(out, s) @@ -696,6 +694,10 @@ func collectUnknowns(mods []*ast.Module, allowed []string) []string { valid := map[string]struct{}{} for _, k := range allowed { + k = trimKey(k) + if k == "" { + continue + } valid[k] = struct{}{} } @@ -709,14 +711,25 @@ func collectUnknowns(mods []*ast.Module, allowed []string) []string { return filtered } +func runtimeUnknownInputRefs(st *state) []string { + if st == nil || len(st.Unknowns) == 0 { + return nil + } + var out []string + if _, ok := st.Unknowns[funcVerifyGitSignature]; ok { + out = append(out, "git.commit") + } + return out +} + func summarizeUnknownsForLog(unk []string) []string { out := make([]string, 0, len(unk)) seen := map[string]struct{}{} for _, u := range unk { - if strings.HasPrefix(u, "input.image.signatures") { - u = "input.image.signatures" + if strings.HasPrefix(u, "image.signatures") { + u = "image.signatures" } - if u == "input.image" { + if u == "image" { continue } if _, ok := seen[u]; ok { @@ -730,7 +743,7 @@ func summarizeUnknownsForLog(unk []string) []string { func hasHTTPUnknowns(unk []string) bool { for _, u := range unk { - if strings.HasPrefix(u, "input.http.") { + if strings.HasPrefix(u, "http.") { return true } } @@ -738,6 +751,8 @@ func hasHTTPUnknowns(unk []string) bool { } func trimKey(s string) string { + s = strings.TrimPrefix(s, "input.") + const ( dot = '.' sb = '['