Repository navigation
fix(azure_policy): collect nested wildcards below a count-bound prefix - #871
Merged
Anand Krishnamoorthi (anakrish) merged 1 commit intoOct 27, 2026
Conversation
Inside a count `where`, a path that extends the counted `[*]` prefix with
further `[*]` levels was navigated only up to the next wildcard
(`compile_suffix_from_binding` dropped everything after it), and `field()`
then wrapped that single value in a one-member array.
Before (no alias catalog), resource
{"properties":{"items":[{"subs":[{"v":"x"},{"v":"y"}]}]}}:
{"if":{"count":{"field":"properties.items[*]",
"where":{"value":"[field('properties.items[*].subs[*].v')]","equals":["x","y"]}},
"equals":1},"then":{"effect":"deny"}}
-> field() returned [[{"v":"x"},{"v":"y"}]] (suffix after the next [*]
dropped, then wrapped) -> Undefined.
After: field() returns ["x","y"] -> deny.
Azure docs (author-policies-for-arrays, "The field function inside where
conditions"): "Array aliases are resolved into a collection of values
selected from all array members." and "Referencing the counted array alias
inside the `where` condition returns a collection with a single value
selected from the array member that is evaluated in the current
iteration." The same page shows
`field('...objectArray[*].nestedArray[*]')` => `[1, 2, 3, 4]` (flattened).
Fix: when the suffix after the bound prefix still contains `[*]`,
`compile_suffix_from_binding` now reuses the existing unbound wildcard
collector (`compile_field_wildcard_collect`, now taking an optional base
register) relative to the current element; suffixes without `[*]` keep
single-value navigation. `field()` only wraps into a one-member array when
the path selects a single value from the bound element.
Siblings sharing compile_suffix_from_binding / compile_from_binding:
- current('a[*].b[*].c') inside count over a[*] now also collects (the
built-in CMK policy in e2e_cmk_disk_encryption.yaml relies on
`length(current('...targetRegions[*].encryption.dataDiskImages[*].diskEncryptionSetId'))`);
previously it returned the inner array itself.
- dynamic `if(cond, 'a[*].b[*].c[*]', ...)` field inside where now collects.
- field condition `"field": "a[*].b[*]"` inside where is unchanged (already
implicit allOf via has_inner_unbound_wildcard_field); pinned by a test.
Tests (tests/azure_policy/cases/count.yaml); (*) = fails pre-fix,
every _no_match twin passes both before and after:
- field_fn_nested_wildcard_under_bound_prefix_collects (*) / _no_match
- field_fn_two_level_nested_wildcard_under_bound_prefix_flattens (*) / _no_match
- field_fn_property_of_bound_member_is_single_member_array
- field_condition_nested_wildcard_under_bound_prefix_is_allof
- current_nested_wildcard_under_bound_prefix_collects (*) / _no_match
- current_property_of_bound_member_is_single_value / _no_match
- current_nested_wildcard_over_missing_inner_array_is_empty (*) / _no_match
- dynamic_if_field_nested_wildcard_under_bound_prefix_collects (*) / _no_match
- nested_count_field_fn_wildcard_under_inner_bound_prefix_collects (*) / _no_match
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Contributor
There was a problem hiding this comment.
🟢 Approval recommended
The focused change reuses existing collection logic, has targeted regression coverage, and has no identified blocking defects.
No open issues 🎉
What changed in this PR
Fixes nested wildcard resolution in Azure Policy count conditions so paths extending a count-bound prefix collect values relative to the current member.
Changes:
- Reuses the wildcard collector with an optional base register for bound paths.
- Preserves single-value wrapping for
field()while avoiding extra wrapping of collected arrays. - Adds regression coverage for nested wildcards,
current(), dynamic fields, missing arrays, and nested counts.
| File | Description |
|---|---|
tests/azure_policy/cases/count.yaml |
Adds matching and non-matching regression cases for bound-path behavior. |
src/languages/azure_policy/compiler/fields.rs |
Allows wildcard collection relative to a supplied base register. |
src/languages/azure_policy/compiler/expressions.rs |
Wraps bound field() results only when they select a single value. |
src/languages/azure_policy/compiler/count.rs |
Collects wildcard suffixes and identifies single-value bound paths. |
Review effort: Balanced
Anand Krishnamoorthi (anakrish)
approved these changes
Oct 27, 2026
Anand Krishnamoorthi (anakrish)
left a comment
Collaborator
There was a problem hiding this comment.
LGTM
Anand Krishnamoorthi (anakrish)
merged commit Oct 27, 2026
4ea1b66
into
microsoft:main
60 checks passed
This was referenced Oct 27, 2026
Closed
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Inside a count
where, a path that extends the counted[*]prefix with further[*]levels was navigated only up to the next wildcard, andfield()then wrapped that value in a one-member array.Resource
{"properties":{"items":[{"subs":[{"v":"x"},{"v":"y"}]}]}}:{"if":{"count":{"field":"properties.items[*]", "where":{"value":"[field('properties.items[*].subs[*].v')]","equals":["x","y"]}}, "equals":1},"then":{"effect":"deny"}}field()returned[[{"v":"x"},{"v":"y"}]]→ no effect.["x","y"]→ deny.Azure docs ("The field function inside where conditions"): "Array aliases are resolved into a collection of values selected from all array members." The same page shows
field('...objectArray[*].nestedArray[*]')⇒[1, 2, 3, 4].Fix
When the suffix after the bound prefix still contains
[*],compile_suffix_from_bindingreuses the existing wildcard collector (compile_field_wildcard_collect, now taking an optional base register) relative to the current element.field()wraps into a one-member array only when the path selects a single value.Paths that go through the same code:
current('a[*].b[*].c')inside a count overa[*]now collects too (the built-in CMK policy ine2e_cmk_disk_encryption.yamldepends on this). Previously it returned the inner array itself; a missing inner array now gives[].if(cond, 'a[*].b[*].c', ...)field insidewherenow collects."field": "a[*].b[*]"condition insidewherekeeps its implicit allOf behaviour; a test pins it.Testing
tests/azure_policy/cases/count.yaml: 9 scenarios. Every counterexample fails before the fix and every_no_matchtwin passes both before and after. Validated with fmt, clippy (both configs), lib and integration tests on Rust 1.95.0. Deep-reviewed (round 5: no defects; review-requested tests added).Dependencies: none (based on main).