From d36f952133380474e5f3ccf60c44eccfac9e5744 Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi <35780660+anakrish@users.noreply.github.com> Date: Mon, 30 Mar 2026 18:44:36 -0500 Subject: [PATCH] feat(azure-policy): add alias normalization and denormalization (#635) * feat: add Azure Policy alias normalization/denormalization Add normalizer and denormalizer for ARM JSON resources, enabling Azure Policy alias short names to become direct paths into a flat structure. - Normalizer: flattens properties wrappers, lowercases keys, resolves per-alias versioned ARM paths, handles sub-resource array flattening, element-level field remaps, and array base renames - Denormalizer: reverses all transformations with casing restoration - AliasRegistry: loads production alias catalogs and data policy manifests - Types: serde deserialization for ARM provider alias formats - YAML test suite: 13 test files covering normalize, denormalize, round-trip, data-plane, edge cases, malformed input, sub-resources, and registry API - Benchmark suite for normalization performance * feat: add FFI and C# bindings for alias normalization - FFI: alias_registry.rs with C-compatible API for loading catalogs, normalizing resources, and denormalizing back to ARM JSON - C#: AliasRegistry wrapper class with NativeMethods P/Invoke bindings and integration tests - Updated Cargo.lock files for new serde_json dependency --- Cargo.lock | 197 +- Cargo.toml | 8 +- benches/normalization_benchmark.rs | 560 ++++ .../Regorus.Tests/AliasRegistryTests.cs | 191 ++ bindings/csharp/Regorus/AliasRegistry.cs | 153 + bindings/csharp/Regorus/NativeMethods.cs | 57 + bindings/csharp/Regorus/SafeHandles.cs | 48 + bindings/ffi/Cargo.lock | 141 +- bindings/ffi/src/alias_registry.rs | 479 +++ bindings/ffi/src/common.rs | 1 + bindings/ffi/src/lib.rs | 1 + bindings/java/Cargo.lock | 88 +- bindings/python/Cargo.lock | 96 +- bindings/wasm/Cargo.lock | 95 +- .../aliases/denormalizer/casing.rs | 114 + .../aliases/denormalizer/helpers.rs | 13 + .../azure_policy/aliases/denormalizer/mod.rs | 227 ++ .../aliases/denormalizer/sub_resource.rs | 228 ++ .../aliases/denormalizer/tests.rs | 55 + src/languages/azure_policy/aliases/mod.rs | 1310 ++++++++ .../aliases/normalizer/alias_resolution.rs | 96 + .../aliases/normalizer/element_remap.rs | 252 ++ .../aliases/normalizer/flatten.rs | 132 + .../azure_policy/aliases/normalizer/mod.rs | 157 + src/languages/azure_policy/aliases/obj_map.rs | 475 +++ src/languages/azure_policy/aliases/types.rs | 686 ++++ src/languages/azure_policy/mod.rs | 2 + src/languages/mod.rs | 7 - tests/azure_policy/aliases/test_aliases.json | 2900 +++++++++++++++++ .../aliases/versioned_aliases.json | 230 ++ tests/azure_policy/mod.rs | 4 + .../cases/data_plane_advanced.yaml | 170 + .../cases/data_plane_breadth.yaml | 154 + .../cases/data_plane_manifest.yaml | 64 + .../cases/denormalize_aliases.yaml | 319 ++ .../cases/denormalize_basic.yaml | 83 + .../normalization/cases/edge_cases.yaml | 201 ++ .../cases/envelope_pipeline.yaml | 46 + .../normalization/cases/malformed_input.yaml | 233 ++ .../normalization/cases/normalize_basic.yaml | 258 ++ .../cases/normalize_envelope.yaml | 26 + .../cases/normalize_sub_resources.yaml | 123 + .../normalization/cases/registry_api.yaml | 127 + .../normalization/cases/round_trip.yaml | 449 +++ tests/azure_policy/normalization/mod.rs | 322 ++ tests/mod.rs | 3 + 46 files changed, 11265 insertions(+), 316 deletions(-) create mode 100644 benches/normalization_benchmark.rs create mode 100644 bindings/csharp/Regorus.Tests/AliasRegistryTests.cs create mode 100644 bindings/csharp/Regorus/AliasRegistry.cs create mode 100644 bindings/ffi/src/alias_registry.rs create mode 100644 src/languages/azure_policy/aliases/denormalizer/casing.rs create mode 100644 src/languages/azure_policy/aliases/denormalizer/helpers.rs create mode 100644 src/languages/azure_policy/aliases/denormalizer/mod.rs create mode 100644 src/languages/azure_policy/aliases/denormalizer/sub_resource.rs create mode 100644 src/languages/azure_policy/aliases/denormalizer/tests.rs create mode 100644 src/languages/azure_policy/aliases/mod.rs create mode 100644 src/languages/azure_policy/aliases/normalizer/alias_resolution.rs create mode 100644 src/languages/azure_policy/aliases/normalizer/element_remap.rs create mode 100644 src/languages/azure_policy/aliases/normalizer/flatten.rs create mode 100644 src/languages/azure_policy/aliases/normalizer/mod.rs create mode 100644 src/languages/azure_policy/aliases/obj_map.rs create mode 100644 src/languages/azure_policy/aliases/types.rs delete mode 100644 src/languages/mod.rs create mode 100644 tests/azure_policy/aliases/test_aliases.json create mode 100644 tests/azure_policy/aliases/versioned_aliases.json create mode 100644 tests/azure_policy/mod.rs create mode 100644 tests/azure_policy/normalization/cases/data_plane_advanced.yaml create mode 100644 tests/azure_policy/normalization/cases/data_plane_breadth.yaml create mode 100644 tests/azure_policy/normalization/cases/data_plane_manifest.yaml create mode 100644 tests/azure_policy/normalization/cases/denormalize_aliases.yaml create mode 100644 tests/azure_policy/normalization/cases/denormalize_basic.yaml create mode 100644 tests/azure_policy/normalization/cases/edge_cases.yaml create mode 100644 tests/azure_policy/normalization/cases/envelope_pipeline.yaml create mode 100644 tests/azure_policy/normalization/cases/malformed_input.yaml create mode 100644 tests/azure_policy/normalization/cases/normalize_basic.yaml create mode 100644 tests/azure_policy/normalization/cases/normalize_envelope.yaml create mode 100644 tests/azure_policy/normalization/cases/normalize_sub_resources.yaml create mode 100644 tests/azure_policy/normalization/cases/registry_api.yaml create mode 100644 tests/azure_policy/normalization/cases/round_trip.yaml create mode 100644 tests/azure_policy/normalization/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 7caf88b..857fd78 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -63,9 +63,9 @@ checksum = "4b46cbb362ab8752921c97e041f5e366ee6297bd428a31275b9fcf1e380f7299" [[package]] name = "anstream" -version = "0.6.21" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43d5b281e737544384e969a5ccad3f1cdd24b48086a0fc1b2a5262a26b8f4f4a" +checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d" dependencies = [ "anstyle", "anstyle-parse", @@ -78,15 +78,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" -version = "0.2.7" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e7644824f0aa2c7b9384579234ef10eb7efb6a0deb83f9630a49594dd9c15c2" +checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e" dependencies = [ "utf8parse", ] @@ -140,9 +140,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "borrow-or-share" @@ -162,9 +162,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecount" @@ -186,9 +186,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -263,9 +263,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2797f34da339ce31042b27d23607e051786132987f595b02ba4f6a6dffb7030a" +checksum = "b193af5b67834b676abd72466a96c1024e6a6ad978a1f484bd90b85c94041351" dependencies = [ "clap_builder", "clap_derive", @@ -273,9 +273,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.5.60" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24a241312cea5059b13574bb9b3861cabf758b879c15190b37b6d6fd63ab6876" +checksum = "714a53001bf66416adb0e2ef5ac857140e7dc3a0c48fb28b2f10762fc4b5069f" dependencies = [ "anstream", "anstyle", @@ -285,14 +285,14 @@ dependencies = [ [[package]] name = "clap_derive" -version = "4.5.55" +version = "4.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a92793da1a46a5f2a02a6f4c46c6496b28c43638adea8306fcb0caa1634f24e5" +checksum = "1110bd8a634a1ab8cb04345d8d878267d57c3cf1b38d91b71af6686408bbca6a" dependencies = [ "heck", "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -312,9 +312,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "core-foundation-sys" @@ -433,8 +433,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -497,9 +497,9 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" [[package]] name = "flate2" -version = "1.1.8" +version = "1.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b375d6465b98090a5f25b1c7703f3859783755aa9a80433b36e0379a3ec2f369" +checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", "miniz_oxide", @@ -835,15 +835,15 @@ dependencies = [ [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -893,9 +893,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "litemap" @@ -926,9 +926,9 @@ checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "miniz_oxide" @@ -1040,9 +1040,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -1064,9 +1064,9 @@ checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" [[package]] name = "owo-colors" -version = "4.2.3" +version = "4.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c6901729fa79e91a0913333229e9ca5dc725089d1c363b2f4b4760709dc4a52" +checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" [[package]] name = "page_size" @@ -1192,7 +1192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "479ca8adacdd7ce8f1fb39ce9ecccbfe93a3f1344b3d0d97f20bc0196208f62b" dependencies = [ "proc-macro2 1.0.106", - "syn 2.0.114", + "syn 2.0.117", ] [[package]] @@ -1224,9 +1224,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2 1.0.106", ] @@ -1305,8 +1305,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7186006dcb21920990093f30e3dea63b7d6e977bf1256be20c3563a5db070da" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -1338,9 +1338,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1349,9 +1349,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "regorus" @@ -1366,6 +1366,7 @@ dependencies = [ "dashmap", "data-encoding", "globset", + "hashbrown 0.16.1", "icu_casemap", "indexmap", "ipnet", @@ -1416,9 +1417,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -1467,8 +1468,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -1505,9 +1506,9 @@ checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" [[package]] name = "simd-adler32" -version = "0.3.8" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e320a6c5ad31d271ad523dcf3ad13e2767ad8b1cb8f047f75a8aeaf8da139da2" +checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" [[package]] name = "siphasher" @@ -1558,12 +1559,12 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", + "quote 1.0.45", "unicode-ident", ] @@ -1574,8 +1575,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -1606,8 +1607,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc4ee7f67670e9b64d05fa4253e753e016c6c95ff35b89b7941d6b856dec1d5" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -1663,9 +1664,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-xid" @@ -1711,9 +1712,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", "rand", @@ -1771,9 +1772,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -1784,32 +1785,32 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ - "quote 1.0.44", + "quote 1.0.45", "wasm-bindgen-macro-support", ] [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", "wasm-bindgen-shared", ] [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -1850,9 +1851,9 @@ dependencies = [ [[package]] name = "web-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "312e32e551d92129218ea9a2452120f4aabc03529ef03e4d0d82fb2780608598" +checksum = "854ba17bb104abfb26ba36da9729addc7ce7f06f5c0f90f3c391f8461cca21f9" dependencies = [ "js-sys", "wasm-bindgen", @@ -1909,8 +1910,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -1920,8 +1921,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -1996,7 +1997,7 @@ dependencies = [ "heck", "indexmap", "prettyplease", - "syn 2.0.114", + "syn 2.0.117", "wasm-metadata", "wit-bindgen-core", "wit-component", @@ -2011,8 +2012,8 @@ dependencies = [ "anyhow", "prettyplease", "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", "wit-bindgen-core", "wit-bindgen-rust", ] @@ -2090,29 +2091,29 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b659052874eb698efe5b9e8cf382204678a0086ebf46982b79d6ca3182927e5d" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", "synstructure", ] [[package]] name = "zerocopy" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -2131,8 +2132,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", "synstructure", ] @@ -2166,8 +2167,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "eadce39539ca5cb3985590102671f2567e659fca9666581ad3411d59207951f3" dependencies = [ "proc-macro2 1.0.106", - "quote 1.0.44", - "syn 2.0.114", + "quote 1.0.45", + "syn 2.0.117", ] [[package]] @@ -2184,6 +2185,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/Cargo.toml b/Cargo.toml index dbfff42..4dab3e6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -24,7 +24,7 @@ default = ["full-opa", "arc", "rvm"] arc = [] ast = [] -azure_policy = ["dep:jsonschema", "dep:chrono", "dep:ipnet", "dep:icu_casemap", "arc", "dashmap"] +azure_policy = ["dep:jsonschema", "dep:chrono", "dep:ipnet", "dep:icu_casemap", "dep:hashbrown", "arc", "dashmap"] azure-rbac = ["regex", "time", "net"] base64 = ["dep:data-encoding"] base64url = ["dep:data-encoding"] @@ -99,6 +99,7 @@ rand = ["dep:rand"] anyhow = { version = "1.0.102", default-features = false } serde = {version = "1.0.150", default-features = false, features = ["derive", "rc", "alloc"] } serde_json = { version = "1.0.89", default-features = false, features = ["alloc"] } +hashbrown = { version = "0.16", default-features = false, features = ["default-hasher"], optional = true } lazy_static = { version = "1.4.0", default-features = false } thiserror = { version = "2.0", default-features = false } @@ -197,6 +198,11 @@ name = "rvm_benchmark" harness = false required-features = ["rvm"] +[[bench]] +name = "normalization_benchmark" +harness = false +required-features = ["azure_policy"] + [[example]] name="regorus" harness=false diff --git a/benches/normalization_benchmark.rs b/benches/normalization_benchmark.rs new file mode 100644 index 0000000..f1b71ac --- /dev/null +++ b/benches/normalization_benchmark.rs @@ -0,0 +1,560 @@ +use std::hint::black_box; + +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; +use regorus::languages::azure_policy::aliases::{denormalizer, normalizer, AliasRegistry}; +use regorus::Value; +use serde_json::json; + +// ─── Alias catalog (reused across benchmarks) ─────────────────────────────── + +const ALIASES_JSON: &str = r#"[ + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", + "defaultPath": "properties.securityRules[*].properties.access", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].priority", + "defaultPath": "properties.securityRules[*].properties.priority", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction", + "defaultPath": "properties.securityRules[*].properties.direction", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix", + "defaultPath": "properties.securityRules[*].properties.sourceAddressPrefix", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", + "defaultPath": "properties.securityRules[*].properties.destinationPortRange", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name", + "defaultPath": "properties.securityRules[*].name", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules[*].protocol", + "defaultPath": "properties.defaultSecurityRules[*].properties.protocol", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/accessTier", + "defaultPath": "properties.accessTier", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/isHnsEnabled", + "defaultPath": "properties.isHnsEnabled", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/minimumTlsVersion", + "defaultPath": "properties.minimumTlsVersion", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess", + "defaultPath": "properties.allowBlobPublicAccess", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.name", + "defaultPath": "sku.name", + "paths": [] + } + ] + } + ] + } +]"#; + +fn build_registry() -> AliasRegistry { + let mut reg = AliasRegistry::new(); + reg.load_from_json(ALIASES_JSON).unwrap(); + reg +} + +/// Convert a serde_json::Value to regorus::Value. +fn to_regorus(v: serde_json::Value) -> Value { + Value::from(v) +} + +// ─── Input resources ──────────────────────────────────────────────────────── + +fn simple_storage_resource() -> Value { + to_regorus(json!({ + "name": "myStorageAccount", + "type": "Microsoft.Storage/storageAccounts", + "location": "westus2", + "kind": "StorageV2", + "sku": { "name": "Standard_LRS", "tier": "Standard" }, + "tags": { "environment": "production", "team": "platform" }, + "properties": { + "supportsHttpsTrafficOnly": true, + "accessTier": "Hot", + "isHnsEnabled": false, + "minimumTlsVersion": "TLS1_2", + "allowBlobPublicAccess": false + } + })) +} + +fn nsg_resource(rule_count: usize) -> Value { + let rules: Vec = (0..rule_count) + .map(|i| { + json!({ + "name": format!("rule-{}", i), + "properties": { + "protocol": "Tcp", + "access": if i % 2 == 0 { "Allow" } else { "Deny" }, + "priority": 100 + i, + "direction": "Inbound", + "sourceAddressPrefix": format!("10.0.{}.0/24", i % 256), + "destinationPortRange": format!("{}", 80 + i) + } + }) + }) + .collect(); + + to_regorus(json!({ + "name": "myNsg", + "type": "Microsoft.Network/networkSecurityGroups", + "location": "eastus", + "properties": { + "securityRules": rules + } + })) +} + +// ─── Benchmarks ───────────────────────────────────────────────────────────── + +fn bench_normalize_simple(c: &mut Criterion) { + let registry = build_registry(); + let resource = simple_storage_resource(); + + c.bench_function("normalize/simple_storage", |b| { + b.iter(|| normalizer::normalize(black_box(&resource), Some(®istry), None)) + }); +} + +fn bench_normalize_no_aliases(c: &mut Criterion) { + let resource = simple_storage_resource(); + + c.bench_function("normalize/simple_no_aliases", |b| { + b.iter(|| normalizer::normalize(black_box(&resource), None, None)) + }); +} + +fn bench_normalize_nsg_scaling(c: &mut Criterion) { + let registry = build_registry(); + let mut group = c.benchmark_group("normalize/nsg_rules"); + + for rule_count in [5, 20, 100] { + let resource = nsg_resource(rule_count); + group.bench_with_input( + BenchmarkId::from_parameter(rule_count), + &resource, + |b, res| b.iter(|| normalizer::normalize(black_box(res), Some(®istry), None)), + ); + } + group.finish(); +} + +fn bench_denormalize_simple(c: &mut Criterion) { + let registry = build_registry(); + let resource = simple_storage_resource(); + let normalized = normalizer::normalize(&resource, Some(®istry), None); + + c.bench_function("denormalize/simple_storage", |b| { + b.iter(|| denormalizer::denormalize(black_box(&normalized), Some(®istry), None)) + }); +} + +fn bench_denormalize_nsg_scaling(c: &mut Criterion) { + let registry = build_registry(); + let mut group = c.benchmark_group("denormalize/nsg_rules"); + + for rule_count in [5, 20, 100] { + let resource = nsg_resource(rule_count); + let normalized = normalizer::normalize(&resource, Some(®istry), None); + group.bench_with_input( + BenchmarkId::from_parameter(rule_count), + &normalized, + |b, norm| b.iter(|| denormalizer::denormalize(black_box(norm), Some(®istry), None)), + ); + } + group.finish(); +} + +fn bench_round_trip(c: &mut Criterion) { + let registry = build_registry(); + let resource = nsg_resource(20); + + c.bench_function("round_trip/nsg_20_rules", |b| { + b.iter(|| { + let n = normalizer::normalize(black_box(&resource), Some(®istry), None); + denormalizer::denormalize(&n, Some(®istry), None) + }) + }); +} + +fn bench_normalize_and_wrap(c: &mut Criterion) { + let registry = build_registry(); + let resource = nsg_resource(20); + let context = to_regorus(json!({"resourceGroup": {"name": "rg1"}})); + let parameters = to_regorus(json!({"env": "prod"})); + + c.bench_function("normalize_and_wrap/nsg_20_rules", |b| { + b.iter(|| { + registry.normalize_and_wrap( + black_box(&resource), + None, + Some(context.clone()), + Some(parameters.clone()), + ) + }) + }); +} + +fn bench_registry_load(c: &mut Criterion) { + c.bench_function("registry/load_from_json", |b| { + b.iter(|| { + let mut reg = AliasRegistry::new(); + reg.load_from_json(black_box(ALIASES_JSON)).unwrap(); + reg + }) + }); +} + +// ─── Large-payload benchmarks ─────────────────────────────────────────────── +// +// These stress the hot paths identified in the performance analysis: +// - Nested set helpers (alias-heavy catalog with deep properties) +// - Array element remap/cleanup/rewrap (large sub-resource arrays) +// - Scalar denormalization lookups (many aliases × many fields) + +/// Build a large alias catalog with `n` scalar aliases for storage accounts. +/// Each alias maps to a nested `properties.section_i.field_j` path, creating +/// deep nested-set workloads. +fn large_alias_catalog(n: usize) -> String { + let mut aliases = Vec::new(); + for i in 0..n { + let section = i / 10; + let field = i % 10; + aliases.push(format!( + r#"{{ + "name": "Microsoft.Storage/storageAccounts/section{section}Field{field}", + "defaultPath": "properties.section{section}.field{field}", + "paths": [] + }}"#, + )); + } + format!( + r#"[{{ + "namespace": "Microsoft.Storage", + "resourceTypes": [{{ + "resourceType": "storageAccounts", + "aliases": [{aliases}] + }}] + }}]"#, + aliases = aliases.join(",") + ) +} + +/// Build a storage account resource whose `properties` contain nested sections +/// matching the large alias catalog. +fn large_storage_resource(alias_count: usize) -> Value { + let mut sections = serde_json::Map::new(); + for i in 0..alias_count { + let section = i / 10; + let field = i % 10; + let section_key = format!("section{section}"); + let section_obj = sections + .entry(section_key) + .or_insert_with(|| serde_json::Value::Object(serde_json::Map::new())); + if let serde_json::Value::Object(m) = section_obj { + m.insert(format!("field{field}"), serde_json::Value::from(i)); + } + } + Value::from(json!({ + "name": "bigStorage", + "type": "Microsoft.Storage/storageAccounts", + "location": "westus2", + "properties": sections + })) +} + +fn bench_normalize_large_catalog(c: &mut Criterion) { + let mut group = c.benchmark_group("normalize/large_catalog"); + for alias_count in [50, 200] { + let catalog_json = large_alias_catalog(alias_count); + let mut reg = AliasRegistry::new(); + reg.load_from_json(&catalog_json).unwrap(); + let resource = large_storage_resource(alias_count); + group.bench_with_input( + BenchmarkId::from_parameter(alias_count), + &(reg, resource), + |b, (reg, res)| b.iter(|| normalizer::normalize(black_box(res), Some(reg), None)), + ); + } + group.finish(); +} + +fn bench_denormalize_large_catalog(c: &mut Criterion) { + let mut group = c.benchmark_group("denormalize/large_catalog"); + for alias_count in [50, 200] { + let catalog_json = large_alias_catalog(alias_count); + let mut reg = AliasRegistry::new(); + reg.load_from_json(&catalog_json).unwrap(); + let resource = large_storage_resource(alias_count); + let normalized = normalizer::normalize(&resource, Some(®), None); + group.bench_with_input( + BenchmarkId::from_parameter(alias_count), + &(reg, normalized), + |b, (reg, norm)| b.iter(|| denormalizer::denormalize(black_box(norm), Some(reg), None)), + ); + } + group.finish(); +} + +fn bench_nsg_large_subarrays(c: &mut Criterion) { + let registry = build_registry(); + let mut group = c.benchmark_group("round_trip/nsg_sub_resource"); + for rule_count in [50, 200, 500] { + let resource = nsg_resource(rule_count); + group.bench_with_input( + BenchmarkId::from_parameter(rule_count), + &resource, + |b, res| { + b.iter(|| { + let n = normalizer::normalize(black_box(res), Some(®istry), None); + denormalizer::denormalize(&n, Some(®istry), None) + }) + }, + ); + } + group.finish(); +} + +// ─── Versioned-path benchmarks ────────────────────────────────────────────── +// +// Exercise the precomputed versioned-path aggregates by building a catalog +// where wildcard (array) aliases have version-specific paths that differ from +// the default, then running normalize/denormalize with an explicit api_version. + +/// NSG-like alias catalog where wildcard aliases have versioned paths that +/// differ from the default. This forces the normalize/denormalize path through +/// the versioned aggregate lookup rather than the default-aggregate fast path. +const VERSIONED_ALIASES_JSON: &str = r#"[ + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "paths": [ + { "path": "properties.securityRules[*].properties.transportProtocol", "apiVersions": ["2020-01-01"] }, + { "path": "properties.securityRules[*].properties.protocol", "apiVersions": ["2022-01-01"] } + ] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", + "defaultPath": "properties.securityRules[*].properties.access", + "paths": [ + { "path": "properties.securityRules[*].properties.accessLevel", "apiVersions": ["2020-01-01"] }, + { "path": "properties.securityRules[*].properties.access", "apiVersions": ["2022-01-01"] } + ] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].priority", + "defaultPath": "properties.securityRules[*].properties.priority", + "paths": [ + { "path": "properties.securityRules[*].properties.rulePriority", "apiVersions": ["2020-01-01"] }, + { "path": "properties.securityRules[*].properties.priority", "apiVersions": ["2022-01-01"] } + ] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction", + "defaultPath": "properties.securityRules[*].properties.direction", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix", + "defaultPath": "properties.securityRules[*].properties.sourceAddressPrefix", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", + "defaultPath": "properties.securityRules[*].properties.destinationPortRange", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name", + "defaultPath": "properties.securityRules[*].name", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/provisioningState", + "defaultPath": "properties.provisioningState", + "paths": [ + { "path": "properties.state", "apiVersions": ["2020-01-01"] }, + { "path": "properties.provisioningState", "apiVersions": ["2022-01-01"] } + ] + } + ] + } + ] + } +]"#; + +fn build_versioned_registry() -> AliasRegistry { + let mut reg = AliasRegistry::new(); + reg.load_from_json(VERSIONED_ALIASES_JSON).unwrap(); + reg +} + +/// Build an NSG resource for versioned-path benchmarks. +/// Uses the 2020-01-01 field names (`transportProtocol`, `accessLevel`, +/// `rulePriority`) so that versioned path resolution actually differs from +/// the default. +fn nsg_versioned_resource(rule_count: usize) -> Value { + let rules: Vec = (0..rule_count) + .map(|i| { + json!({ + "name": format!("rule-{}", i), + "properties": { + "transportProtocol": "Tcp", + "accessLevel": if i % 2 == 0 { "Allow" } else { "Deny" }, + "rulePriority": 100 + i, + "direction": "Inbound", + "sourceAddressPrefix": format!("10.0.{}.0/24", i % 256), + "destinationPortRange": format!("{}", 80 + i) + } + }) + }) + .collect(); + + to_regorus(json!({ + "name": "myNsg", + "type": "Microsoft.Network/networkSecurityGroups", + "location": "eastus", + "properties": { + "state": "Succeeded", + "securityRules": rules + } + })) +} + +fn bench_normalize_versioned(c: &mut Criterion) { + let registry = build_versioned_registry(); + let mut group = c.benchmark_group("normalize_versioned/nsg_rules"); + + for rule_count in [5, 20, 100] { + let resource = nsg_versioned_resource(rule_count); + group.bench_with_input( + BenchmarkId::from_parameter(rule_count), + &resource, + |b, res| { + b.iter(|| { + normalizer::normalize(black_box(res), Some(®istry), Some("2020-01-01")) + }) + }, + ); + } + group.finish(); +} + +fn bench_denormalize_versioned(c: &mut Criterion) { + let registry = build_versioned_registry(); + let mut group = c.benchmark_group("denormalize_versioned/nsg_rules"); + + for rule_count in [5, 20, 100] { + let resource = nsg_versioned_resource(rule_count); + let normalized = normalizer::normalize(&resource, Some(®istry), Some("2020-01-01")); + group.bench_with_input( + BenchmarkId::from_parameter(rule_count), + &normalized, + |b, norm| { + b.iter(|| { + denormalizer::denormalize(black_box(norm), Some(®istry), Some("2020-01-01")) + }) + }, + ); + } + group.finish(); +} + +fn bench_round_trip_versioned(c: &mut Criterion) { + let registry = build_versioned_registry(); + let mut group = c.benchmark_group("round_trip_versioned/nsg_rules"); + + for rule_count in [20, 100] { + let resource = nsg_versioned_resource(rule_count); + group.bench_with_input( + BenchmarkId::from_parameter(rule_count), + &resource, + |b, res| { + b.iter(|| { + let n = + normalizer::normalize(black_box(res), Some(®istry), Some("2020-01-01")); + denormalizer::denormalize(&n, Some(®istry), Some("2020-01-01")) + }) + }, + ); + } + group.finish(); +} + +criterion_group!( + normalization_benches, + bench_normalize_simple, + bench_normalize_no_aliases, + bench_normalize_nsg_scaling, + bench_denormalize_simple, + bench_denormalize_nsg_scaling, + bench_round_trip, + bench_normalize_and_wrap, + bench_registry_load, + bench_normalize_large_catalog, + bench_denormalize_large_catalog, + bench_nsg_large_subarrays, + bench_normalize_versioned, + bench_denormalize_versioned, + bench_round_trip_versioned, +); +criterion_main!(normalization_benches); diff --git a/bindings/csharp/Regorus.Tests/AliasRegistryTests.cs b/bindings/csharp/Regorus.Tests/AliasRegistryTests.cs new file mode 100644 index 0000000..3d3dc8e --- /dev/null +++ b/bindings/csharp/Regorus.Tests/AliasRegistryTests.cs @@ -0,0 +1,191 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Text.Json; +using System.Text.Json.Nodes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Regorus; + +namespace Regorus.Tests; + +[TestClass] +public class AliasRegistryTests +{ + private const string AliasesJson = @"[{ + ""namespace"": ""Microsoft.Storage"", + ""resourceTypes"": [{ + ""resourceType"": ""storageAccounts"", + ""aliases"": [{ + ""name"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"", + ""defaultPath"": ""properties.supportsHttpsTrafficOnly"", + ""paths"": [] + }, { + ""name"": ""Microsoft.Storage/storageAccounts/accessTier"", + ""defaultPath"": ""properties.accessTier"", + ""paths"": [] + }] + }] + }]"; + + private const string ManifestJson = @"{ + ""dataNamespace"": ""Microsoft.KeyVault.Data"", + ""aliases"": [], + ""resourceTypeAliases"": [{ + ""resourceType"": ""vaults/certificates"", + ""aliases"": [{ + ""name"": ""Microsoft.KeyVault.Data/vaults/certificates/keySize"", + ""paths"": [{ ""path"": ""keySize"", ""apiVersions"": [""7.0""] }] + }] + }] + }"; + + [TestMethod] + public void Create_and_dispose_succeeds() + { + using var registry = new AliasRegistry(); + Assert.AreEqual(0, registry.Length); + } + + [TestMethod] + public void LoadJson_populates_registry() + { + using var registry = new AliasRegistry(); + registry.LoadJson(AliasesJson); + Assert.AreEqual(1, registry.Length); + } + + [TestMethod] + public void LoadManifest_populates_registry() + { + using var registry = new AliasRegistry(); + registry.LoadManifest(ManifestJson); + Assert.AreEqual(1, registry.Length); + } + + [TestMethod] + public void NormalizeAndWrap_produces_envelope() + { + using var registry = new AliasRegistry(); + registry.LoadJson(AliasesJson); + + var resource = @"{ + ""name"": ""acct1"", + ""type"": ""Microsoft.Storage/storageAccounts"", + ""properties"": { ""supportsHttpsTrafficOnly"": true, ""accessTier"": ""Hot"" } + }"; + + var result = registry.NormalizeAndWrap(resource, "2023-01-01", "{}", "{}"); + Assert.IsNotNull(result); + + var envelope = JsonNode.Parse(result!)!; + Assert.IsNotNull(envelope["resource"]); + Assert.IsNotNull(envelope["parameters"]); + Assert.IsNotNull(envelope["context"]); + + // Normalized resource should have lowercased alias field names + var res = envelope["resource"]!; + Assert.AreEqual(true, res["supportshttpstrafficonly"]?.GetValue()); + Assert.AreEqual("Hot", res["accesstier"]?.GetValue()); + Assert.AreEqual("acct1", res["name"]?.GetValue()); + } + + [TestMethod] + public void NormalizeAndWrap_with_context_and_parameters() + { + using var registry = new AliasRegistry(); + registry.LoadJson(AliasesJson); + + var resource = @"{ + ""name"": ""acct1"", + ""type"": ""Microsoft.Storage/storageAccounts"", + ""properties"": { ""supportsHttpsTrafficOnly"": true } + }"; + var context = @"{""resourceGroup"": {""name"": ""rg1""}}"; + var parameters = @"{""env"": ""prod""}"; + + var result = registry.NormalizeAndWrap(resource, "2023-01-01", context, parameters); + Assert.IsNotNull(result); + + var envelope = JsonNode.Parse(result!)!; + Assert.AreEqual("rg1", envelope["context"]!["resourceGroup"]!["name"]?.GetValue()); + Assert.AreEqual("prod", envelope["parameters"]!["env"]?.GetValue()); + } + + [TestMethod] + public void Denormalize_restores_properties() + { + using var registry = new AliasRegistry(); + registry.LoadJson(AliasesJson); + + var normalized = @"{ + ""name"": ""acct1"", + ""type"": ""Microsoft.Storage/storageAccounts"", + ""supportshttpstrafficonly"": true, + ""accesstier"": ""Hot"" + }"; + + var result = registry.Denormalize(normalized, "2023-01-01"); + Assert.IsNotNull(result); + + var arm = JsonNode.Parse(result!)!; + Assert.AreEqual("acct1", arm["name"]?.GetValue()); + Assert.AreEqual(true, arm["properties"]!["supportsHttpsTrafficOnly"]?.GetValue()); + Assert.AreEqual("Hot", arm["properties"]!["accessTier"]?.GetValue()); + } + + [TestMethod] + public void Round_trip_normalize_then_denormalize() + { + using var registry = new AliasRegistry(); + registry.LoadJson(AliasesJson); + + var resource = @"{ + ""name"": ""acct1"", + ""type"": ""Microsoft.Storage/storageAccounts"", + ""properties"": { ""supportsHttpsTrafficOnly"": true, ""accessTier"": ""Hot"" } + }"; + + // Normalize + var envelopeJson = registry.NormalizeAndWrap(resource, "2023-01-01", "{}", "{}"); + Assert.IsNotNull(envelopeJson); + + var envelope = JsonNode.Parse(envelopeJson!)!; + var normalizedResource = envelope["resource"]!.ToJsonString(); + + // Denormalize + var armJson = registry.Denormalize(normalizedResource, "2023-01-01"); + Assert.IsNotNull(armJson); + + var arm = JsonNode.Parse(armJson!)!; + Assert.AreEqual(true, arm["properties"]!["supportsHttpsTrafficOnly"]?.GetValue()); + Assert.AreEqual("Hot", arm["properties"]!["accessTier"]?.GetValue()); + Assert.AreEqual("acct1", arm["name"]?.GetValue()); + } + + [TestMethod] + public void DataPlane_manifest_normalize() + { + using var registry = new AliasRegistry(); + registry.LoadManifest(ManifestJson); + + var resource = @"{ + ""type"": ""Microsoft.KeyVault.Data/vaults/certificates"", + ""keySize"": 2048 + }"; + + var result = registry.NormalizeAndWrap(resource, "7.0", "{}", "{}"); + Assert.IsNotNull(result); + + var envelope = JsonNode.Parse(result!)!; + Assert.AreEqual(2048, envelope["resource"]!["keysize"]?.GetValue()); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void LoadJson_invalid_throws() + { + using var registry = new AliasRegistry(); + registry.LoadJson("not valid json"); + } +} diff --git a/bindings/csharp/Regorus/AliasRegistry.cs b/bindings/csharp/Regorus/AliasRegistry.cs new file mode 100644 index 0000000..e14a131 --- /dev/null +++ b/bindings/csharp/Regorus/AliasRegistry.cs @@ -0,0 +1,153 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Regorus.Internal; + +#nullable enable +namespace Regorus +{ + /// + /// Manages Azure Policy alias definitions used for resource normalization + /// and policy compilation. + /// + public unsafe sealed class AliasRegistry : SafeHandleWrapper + { + /// + /// Create an empty alias registry. + /// + public AliasRegistry() + : base(RegorusAliasRegistryHandle.Create(), nameof(AliasRegistry)) + { + } + + /// + /// Load control-plane alias data (array of ProviderAliases) from a JSON string. + /// + /// JSON array of ProviderAliases (e.g. from Get-AzPolicyAlias or ResourceTypesAndAliases.json) + public void LoadJson(string json) + { + Utf8Marshaller.WithUtf8(json, jsonPtr => + { + UseHandle(regPtr => + { + CheckAndDropResult(API.regorus_alias_registry_load_json( + (RegorusAliasRegistry*)regPtr, (byte*)jsonPtr)); + return 0; + }); + }); + } + + /// + /// Load a data-plane policy manifest from a JSON string. + /// + /// JSON object containing a DataPolicyManifest + public void LoadManifest(string json) + { + Utf8Marshaller.WithUtf8(json, jsonPtr => + { + UseHandle(regPtr => + { + CheckAndDropResult(API.regorus_alias_registry_load_manifest( + (RegorusAliasRegistry*)regPtr, (byte*)jsonPtr)); + return 0; + }); + }); + } + + /// + /// Gets the number of resource types loaded in the registry. + /// + public long Length + { + get + { + return UseHandle(regPtr => + { + return ResultHelpers.GetIntResult( + API.regorus_alias_registry_len((RegorusAliasRegistry*)regPtr)); + }); + } + } + + /// + /// Normalize an ARM resource JSON and wrap it into the standard input envelope + /// expected by a compiled Azure Policy program. + /// + /// Raw ARM resource JSON + /// API version string (e.g. "2023-01-01"), or null to use default alias paths + /// Additional context JSON object (pass "{}" if none) + /// Policy parameter values JSON (pass "{}" if none) + /// JSON string: { "resource": <normalized>, "context": <context>, "parameters": <params> } + public string? NormalizeAndWrap(string resourceJson, string? apiVersion = null, string contextJson = "{}", string parametersJson = "{}") + { + return Utf8Marshaller.WithUtf8(resourceJson, resPtr => + Utf8Marshaller.WithUtf8(contextJson, ctxPtr => + Utf8Marshaller.WithUtf8(parametersJson, paramsPtr => + { + if (apiVersion is null) + { + return UseHandle(regPtr => + { + return ResultHelpers.GetStringResult( + API.regorus_alias_registry_normalize_and_wrap( + (RegorusAliasRegistry*)regPtr, + (byte*)resPtr, null, + (byte*)ctxPtr, (byte*)paramsPtr)); + }); + } + else + { + return Utf8Marshaller.WithUtf8(apiVersion, apiPtr => + UseHandle(regPtr => + { + return ResultHelpers.GetStringResult( + API.regorus_alias_registry_normalize_and_wrap( + (RegorusAliasRegistry*)regPtr, + (byte*)resPtr, (byte*)apiPtr, + (byte*)ctxPtr, (byte*)paramsPtr)); + })); + } + }))); + } + + /// + /// Denormalize a previously-normalized resource JSON back to ARM format. + /// + /// The normalized resource JSON + /// API version string, or null to use default alias paths + /// Denormalized ARM JSON string + public string? Denormalize(string normalizedJson, string? apiVersion = null) + { + return Utf8Marshaller.WithUtf8(normalizedJson, normPtr => + { + if (apiVersion is null) + { + return UseHandle(regPtr => + { + return ResultHelpers.GetStringResult( + API.regorus_alias_registry_denormalize( + (RegorusAliasRegistry*)regPtr, + (byte*)normPtr, null)); + }); + } + else + { + return Utf8Marshaller.WithUtf8(apiVersion, apiPtr => + UseHandle(regPtr => + { + return ResultHelpers.GetStringResult( + API.regorus_alias_registry_denormalize( + (RegorusAliasRegistry*)regPtr, + (byte*)normPtr, (byte*)apiPtr)); + })); + } + }); + } + + private static string? CheckAndDropResult(RegorusResult result) + { + return ResultHelpers.GetStringResult(result); + } + } +} diff --git a/bindings/csharp/Regorus/NativeMethods.cs b/bindings/csharp/Regorus/NativeMethods.cs index 6702d7f..c7a071b 100644 --- a/bindings/csharp/Regorus/NativeMethods.cs +++ b/bindings/csharp/Regorus/NativeMethods.cs @@ -669,6 +669,55 @@ namespace Regorus.Internal internal static extern RegorusResult regorus_effect_schema_clear(); #endregion + + #region Alias Registry Methods + + /// + /// Create a new, empty AliasRegistry. + /// + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusAliasRegistry* regorus_alias_registry_new(); + + /// + /// Drop an AliasRegistry. + /// + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern void regorus_alias_registry_drop(RegorusAliasRegistry* registry); + + /// + /// Load control-plane alias data (array of ProviderAliases) into the registry. + /// + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_alias_registry_load_json(RegorusAliasRegistry* registry, byte* json); + + /// + /// Load a data-plane policy manifest into the registry. + /// + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_manifest", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_alias_registry_load_manifest(RegorusAliasRegistry* registry, byte* json); + + /// + /// Return the number of resource types loaded in the alias registry. + /// + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_len", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_alias_registry_len(RegorusAliasRegistry* registry); + + /// + /// Normalize an ARM resource JSON and wrap it into the standard input envelope. + /// Returns a JSON string. + /// + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_normalize_and_wrap", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_alias_registry_normalize_and_wrap( + RegorusAliasRegistry* registry, byte* resource_json, byte* api_version, byte* context_json, byte* parameters_json); + + /// + /// Denormalize a previously-normalized resource JSON back to ARM format. + /// + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_denormalize", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_alias_registry_denormalize( + RegorusAliasRegistry* registry, byte* normalized_json, byte* api_version); + + #endregion } #region Native Structures @@ -874,5 +923,13 @@ namespace Regorus.Internal public byte* content; } + /// + /// Wrapper for AliasRegistry. + /// + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct RegorusAliasRegistry + { + } + #endregion } diff --git a/bindings/csharp/Regorus/SafeHandles.cs b/bindings/csharp/Regorus/SafeHandles.cs index 0864536..1808314 100644 --- a/bindings/csharp/Regorus/SafeHandles.cs +++ b/bindings/csharp/Regorus/SafeHandles.cs @@ -183,4 +183,52 @@ namespace Regorus return true; } } + + internal sealed class RegorusAliasRegistryHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private RegorusAliasRegistryHandle() : base(ownsHandle: true) + { + } + + internal static RegorusAliasRegistryHandle Create() + { + unsafe + { + var raw = Internal.API.regorus_alias_registry_new(); + if (raw is null) + { + throw new InvalidOperationException("Failed to create Regorus alias registry."); + } + + var handle = new RegorusAliasRegistryHandle(); + handle.SetHandle((IntPtr)raw); + return handle; + } + } + + internal static RegorusAliasRegistryHandle FromPointer(IntPtr pointer) + { + if (pointer == IntPtr.Zero) + { + throw new ArgumentException("Pointer cannot be zero.", nameof(pointer)); + } + + var handle = new RegorusAliasRegistryHandle(); + handle.SetHandle(pointer); + return handle; + } + + protected override bool ReleaseHandle() + { + if (!IsInvalid) + { + unsafe + { + Internal.API.regorus_alias_registry_drop((Internal.RegorusAliasRegistry*)handle); + } + SetHandle(IntPtr.Zero); + } + return true; + } + } } diff --git a/bindings/ffi/Cargo.lock b/bindings/ffi/Cargo.lock index 49a07d7..9667119 100644 --- a/bindings/ffi/Cargo.lock +++ b/bindings/ffi/Cargo.lock @@ -57,9 +57,9 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.13" +version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5192cca8006f1fd4f7237516f40fa183bb07f8fbdfedaa0036de5ea9b0b45e78" +checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000" [[package]] name = "anstyle-parse" @@ -119,9 +119,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "borrow-or-share" @@ -141,9 +141,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecount" @@ -172,9 +172,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.55" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -258,9 +258,9 @@ dependencies = [ [[package]] name = "colorchoice" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570" [[package]] name = "core-foundation-sys" @@ -672,15 +672,15 @@ checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -727,15 +727,15 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "linux-raw-sys" -version = "0.11.0" +version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df1d3c3b53da64cf5760482273a98e575c651a67eec7f77df96b5b642de8f039" +checksum = "32a66949e030da00e8c7d4434b251670a91556f4144941d37452769c25d58a53" [[package]] name = "litemap" @@ -766,9 +766,9 @@ checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "msvc_spectre_libs" @@ -860,9 +860,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "once_cell_polyfill" @@ -967,9 +967,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -1061,9 +1061,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -1072,9 +1072,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "regorus" @@ -1086,6 +1086,7 @@ dependencies = [ "dashmap", "data-encoding", "globset", + "hashbrown 0.16.1", "icu_casemap", "indexmap", "ipnet", @@ -1137,9 +1138,9 @@ dependencies = [ [[package]] name = "rustix" -version = "1.1.3" +version = "1.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "146c9e247ccc180c1f61615433868c99f3de3ae256a30a43b49f67c2d9171f34" +checksum = "b6fe4565b9518b83ef4f91bb47ce29620ca828bd32cb7e408f0062e9930ba190" dependencies = [ "bitflags", "errno", @@ -1156,9 +1157,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "scopeguard" @@ -1217,9 +1218,9 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "876ac351060d4f882bb1032b6369eb0aef79ad9df1ea8bc404874d8cc3d0cd98" dependencies = [ "serde_core", ] @@ -1275,9 +1276,9 @@ checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1297,12 +1298,12 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.24.0" +version = "3.27.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "655da9c7eb6305c55742045d5a8d2037996d61d8de95806335c7c86ce0f82e9c" +checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.2", "once_cell", "rustix", "windows-sys", @@ -1341,9 +1342,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.11+spec-1.1.0" +version = "0.9.12+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3afc9a848309fe1aaffaed6e1546a7a14de1f935dc9d89d32afd9a44bab7c46" +checksum = "cf92845e79fc2e2def6a5d828f0801e29a2f8acc037becc5ab08595c7d5e9863" dependencies = [ "indexmap", "serde_core", @@ -1351,7 +1352,7 @@ dependencies = [ "toml_datetime", "toml_parser", "toml_writer", - "winnow", + "winnow 0.7.15", ] [[package]] @@ -1365,18 +1366,18 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.6+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3198b4b0a8e11f09dd03e133c0280504d0801269e9afa46362ffde1cbeebf44" +checksum = "2334f11ee363607eb04df9b8fc8a13ca1715a72ba8662a26ac285c98aabb4011" dependencies = [ - "winnow", + "winnow 1.0.0", ] [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.1.0+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "d282ade6016312faf3e41e57ebbba0c073e4056dab1232ab1cb624199648f8ed" [[package]] name = "unicode-general-category" @@ -1386,9 +1387,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-xid" @@ -1428,9 +1429,9 @@ checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", "rand", @@ -1478,9 +1479,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -1491,9 +1492,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1501,9 +1502,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -1514,9 +1515,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -1625,9 +1626,15 @@ dependencies = [ [[package]] name = "winnow" -version = "0.7.14" +version = "0.7.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5364e9d77fcdeeaa6062ced926ee3381faa2ee02d3eb83a5c27a8825540829" +checksum = "df79d97927682d2fd8adb29682d1140b343be4ac0f08fd68b7765d9c059d3945" + +[[package]] +name = "winnow" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a90e88e4667264a994d34e6d1ab2d26d398dcdca8b7f52bec8668957517fc7d8" [[package]] name = "wit-bindgen" @@ -1748,18 +1755,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", @@ -1823,6 +1830,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/bindings/ffi/src/alias_registry.rs b/bindings/ffi/src/alias_registry.rs new file mode 100644 index 0000000..2d02f81 --- /dev/null +++ b/bindings/ffi/src/alias_registry.rs @@ -0,0 +1,479 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! FFI bindings for `AliasRegistry` – Azure Policy alias catalog management. + +#![cfg(feature = "azure_policy")] + +use crate::common::{from_c_str, to_ref, RegorusResult, RegorusStatus}; +use crate::panic_guard::with_unwind_guard; + +use alloc::boxed::Box; +use alloc::format; +use alloc::string::String; +use anyhow::Result; +use core::ffi::c_char; +use core::ptr; + +use regorus::languages::azure_policy::aliases::AliasRegistry; + +/// Opaque wrapper for `AliasRegistry`. +pub struct RegorusAliasRegistry { + registry: AliasRegistry, +} + +// --------------------------------------------------------------------------- +// Lifecycle +// --------------------------------------------------------------------------- + +/// Create a new, empty `AliasRegistry`. +/// +/// The caller must eventually call `regorus_alias_registry_drop` to free the handle. +#[no_mangle] +pub extern "C" fn regorus_alias_registry_new() -> *mut RegorusAliasRegistry { + let wrapper = RegorusAliasRegistry { + registry: AliasRegistry::new(), + }; + Box::into_raw(Box::new(wrapper)) +} + +/// Drop a `RegorusAliasRegistry`. +#[no_mangle] +pub extern "C" fn regorus_alias_registry_drop(registry: *mut RegorusAliasRegistry) { + if let Ok(r) = to_ref(registry) { + unsafe { + let _ = Box::from_raw(ptr::from_mut(r)); + } + } +} + +// --------------------------------------------------------------------------- +// Loading +// --------------------------------------------------------------------------- + +/// Load control-plane alias data (array of `ProviderAliases`) into the registry. +/// +/// `json` must be a valid null-terminated UTF-8 string containing the JSON +/// array returned by `Get-AzPolicyAlias` or the static +/// `ResourceTypesAndAliases.json` file. +#[no_mangle] +pub extern "C" fn regorus_alias_registry_load_json( + registry: *mut RegorusAliasRegistry, + json: *const c_char, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result<()> { + let json_str = from_c_str(json)?; + to_ref(registry)?.registry.load_from_json(&json_str)?; + Ok(()) + }(); + + match output { + Ok(()) => RegorusResult::ok_void(), + Err(e) => RegorusResult::err_with_message( + RegorusStatus::InvalidDataFormat, + format!("Failed to load alias catalog: {e}"), + ), + } + }) +} + +/// Load a data-plane policy manifest into the registry. +/// +/// `json` must be a valid null-terminated UTF-8 string containing a single +/// `DataPolicyManifest` JSON object. +#[no_mangle] +pub extern "C" fn regorus_alias_registry_load_manifest( + registry: *mut RegorusAliasRegistry, + json: *const c_char, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result<()> { + let json_str = from_c_str(json)?; + to_ref(registry)? + .registry + .load_data_policy_manifest_json(&json_str)?; + Ok(()) + }(); + + match output { + Ok(()) => RegorusResult::ok_void(), + Err(e) => RegorusResult::err_with_message( + RegorusStatus::InvalidDataFormat, + format!("Failed to load data-plane manifest: {e}"), + ), + } + }) +} + +// --------------------------------------------------------------------------- +// Queries +// --------------------------------------------------------------------------- + +/// Return the number of resource types loaded in the alias registry. +#[no_mangle] +pub extern "C" fn regorus_alias_registry_len(registry: *mut RegorusAliasRegistry) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result { + let len = to_ref(registry)?.registry.len(); + Ok(len as i64) + }(); + + match output { + Ok(n) => RegorusResult::ok_int(n), + Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")), + } + }) +} + +// --------------------------------------------------------------------------- +// Normalize / Denormalize +// --------------------------------------------------------------------------- + +/// Normalize an ARM resource JSON and wrap it into the standard input envelope. +/// +/// Returns a JSON string: +/// `{ "resource": , "context": , "parameters": }`. +/// +/// * `resource_json` – raw ARM resource JSON +/// * `api_version` – API version string (e.g. `"2023-01-01"`), or null to use +/// the default alias paths +/// * `context_json` – JSON object for additional context (pass `"{}"` if none) +/// * `parameters_json` – JSON object of policy parameter values (pass `"{}"` if none) +#[no_mangle] +pub extern "C" fn regorus_alias_registry_normalize_and_wrap( + registry: *mut RegorusAliasRegistry, + resource_json: *const c_char, + api_version: *const c_char, + context_json: *const c_char, + parameters_json: *const c_char, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result { + let resource_str = from_c_str(resource_json)?; + let api_ver = if api_version.is_null() { + None + } else { + let s = from_c_str(api_version)?; + if s.is_empty() { + None + } else { + Some(s) + } + }; + let context_str = from_c_str(context_json)?; + let params_str = from_c_str(parameters_json)?; + + let resource = regorus::Value::from_json_str(&resource_str)?; + let context = regorus::Value::from_json_str(&context_str)?; + let params = regorus::Value::from_json_str(¶ms_str)?; + + let wrapped = to_ref(registry)?.registry.normalize_and_wrap( + &resource, + api_ver.as_deref(), + Some(context), + Some(params), + ); + wrapped.to_json_str() + }(); + + match output { + Ok(s) => RegorusResult::ok_string(s), + Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")), + } + }) +} + +/// Denormalize a previously-normalized resource JSON back to ARM format. +/// +/// * `normalized_json` – the normalized resource JSON +/// * `api_version` – API version string, or null to use the default alias paths +/// +/// Returns the denormalized ARM JSON string. +#[no_mangle] +pub extern "C" fn regorus_alias_registry_denormalize( + registry: *mut RegorusAliasRegistry, + normalized_json: *const c_char, + api_version: *const c_char, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result { + let normalized_str = from_c_str(normalized_json)?; + let api_ver = if api_version.is_null() { + None + } else { + let s = from_c_str(api_version)?; + if s.is_empty() { + None + } else { + Some(s) + } + }; + + let normalized = regorus::Value::from_json_str(&normalized_str)?; + + let result = to_ref(registry)? + .registry + .denormalize(&normalized, api_ver.as_deref()); + result.to_json_str() + }(); + + match output { + Ok(s) => RegorusResult::ok_string(s), + Err(e) => RegorusResult::err_with_message(RegorusStatus::Error, format!("{e}")), + } + }) +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::regorus_result_drop; + use core::ffi::CStr; + use std::ffi::CString; + + /// Helper: create a C string from a Rust &str. + fn c(s: &str) -> CString { + CString::new(s).expect("CString::new failed") + } + + /// Helper: assert a RegorusResult has Ok status and extract string output. + fn assert_ok_string(r: &RegorusResult) -> String { + assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status"); + assert!(!r.output.is_null(), "expected non-null output"); + let s = unsafe { CStr::from_ptr(r.output) } + .to_str() + .expect("invalid UTF-8 in output") + .to_string(); + s + } + + /// Helper: assert a RegorusResult has Ok status with integer output. + fn assert_ok_int(r: &RegorusResult) -> i64 { + assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status"); + r.int_value + } + + const ALIASES: &str = r#"[{ + "namespace": "Microsoft.Storage", + "resourceTypes": [{ + "resourceType": "storageAccounts", + "aliases": [{ + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + }] + }] + }]"#; + + const MANIFEST: &str = r#"{ + "dataNamespace": "Microsoft.KeyVault.Data", + "aliases": [], + "resourceTypeAliases": [{ + "resourceType": "vaults/certificates", + "aliases": [{ + "name": "Microsoft.KeyVault.Data/vaults/certificates/keySize", + "paths": [{ "path": "keySize", "apiVersions": ["7.0"] }] + }] + }] + }"#; + + #[test] + fn lifecycle_new_and_drop() { + let reg = regorus_alias_registry_new(); + assert!(!reg.is_null()); + regorus_alias_registry_drop(reg); + } + + #[test] + fn load_json_and_check_len() { + let reg = regorus_alias_registry_new(); + let json = c(ALIASES); + + let r = regorus_alias_registry_load_json(reg, json.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let r = regorus_alias_registry_len(reg); + assert_eq!(assert_ok_int(&r), 1); + regorus_result_drop(r); + + regorus_alias_registry_drop(reg); + } + + #[test] + fn load_manifest_and_check_len() { + let reg = regorus_alias_registry_new(); + let json = c(MANIFEST); + + let r = regorus_alias_registry_load_manifest(reg, json.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let r = regorus_alias_registry_len(reg); + assert_eq!(assert_ok_int(&r), 1); + regorus_result_drop(r); + + regorus_alias_registry_drop(reg); + } + + #[test] + fn load_invalid_json_returns_error() { + let reg = regorus_alias_registry_new(); + let bad = c("not valid json"); + + let r = regorus_alias_registry_load_json(reg, bad.as_ptr()); + assert_ne!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + regorus_alias_registry_drop(reg); + } + + #[test] + fn normalize_and_wrap_round_trip() { + let reg = regorus_alias_registry_new(); + let aliases = c(ALIASES); + let r = regorus_alias_registry_load_json(reg, aliases.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let resource = c(r#"{ + "name": "acct1", + "type": "Microsoft.Storage/storageAccounts", + "properties": { "supportsHttpsTrafficOnly": true } + }"#); + let api = c("2023-01-01"); + let ctx = c(r#"{"resourceGroup": {"name": "rg1"}}"#); + let params = c(r#"{"env": "prod"}"#); + + // Normalize + let r = regorus_alias_registry_normalize_and_wrap( + reg, + resource.as_ptr(), + api.as_ptr(), + ctx.as_ptr(), + params.as_ptr(), + ); + let envelope_json = assert_ok_string(&r); + regorus_result_drop(r); + + // Parse and verify structure + let envelope: serde_json::Value = + serde_json::from_str(&envelope_json).expect("invalid JSON output"); + assert!( + envelope.get("resource").is_some(), + "envelope missing 'resource'" + ); + assert!( + envelope.get("parameters").is_some(), + "envelope missing 'parameters'" + ); + assert!( + envelope.get("context").is_some(), + "envelope missing 'context'" + ); + + // The normalized resource should have lowercased alias fields + let res = &envelope["resource"]; + assert_eq!(res["supportshttpstrafficonly"], true); + assert_eq!(res["name"], "acct1"); + + // Context and parameters should be passed through + assert_eq!(envelope["context"]["resourceGroup"]["name"], "rg1"); + assert_eq!(envelope["parameters"]["env"], "prod"); + + // Denormalize the resource portion + let resource_json = serde_json::to_string(&res).expect("serialize resource"); + let norm_cstr = c(&resource_json); + + let r = regorus_alias_registry_denormalize(reg, norm_cstr.as_ptr(), api.as_ptr()); + let denorm_json = assert_ok_string(&r); + regorus_result_drop(r); + + let denorm: serde_json::Value = + serde_json::from_str(&denorm_json).expect("invalid denorm JSON"); + // Should be back under properties with restored casing + assert_eq!( + denorm["properties"]["supportsHttpsTrafficOnly"], true, + "expected restored casing under properties" + ); + + regorus_alias_registry_drop(reg); + } + + #[test] + fn denormalize_invalid_json_returns_error() { + let reg = regorus_alias_registry_new(); + let aliases = c(ALIASES); + let r = regorus_alias_registry_load_json(reg, aliases.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let bad = c("not json"); + let api = c("2023-01-01"); + let r = regorus_alias_registry_denormalize(reg, bad.as_ptr(), api.as_ptr()); + assert_ne!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + regorus_alias_registry_drop(reg); + } + + #[test] + fn normalize_data_plane_manifest() { + let reg = regorus_alias_registry_new(); + let manifest = c(MANIFEST); + let r = regorus_alias_registry_load_manifest(reg, manifest.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let resource = c(r#"{ + "type": "Microsoft.KeyVault.Data/vaults/certificates", + "keySize": 2048 + }"#); + let api = c("7.0"); + let ctx = c("{}"); + let params = c("{}"); + + let r = regorus_alias_registry_normalize_and_wrap( + reg, + resource.as_ptr(), + api.as_ptr(), + ctx.as_ptr(), + params.as_ptr(), + ); + let envelope_json = assert_ok_string(&r); + regorus_result_drop(r); + + let envelope: serde_json::Value = + serde_json::from_str(&envelope_json).expect("invalid JSON output"); + assert_eq!(envelope["resource"]["keysize"], 2048); + + regorus_alias_registry_drop(reg); + } + + #[test] + fn empty_registry_normalize() { + let reg = regorus_alias_registry_new(); + let resource = c(r#"{"name": "test", "type": "Unknown/type", "properties": {"foo": 1}}"#); + let api = c(""); + let ctx = c("{}"); + let params = c("{}"); + + let r = regorus_alias_registry_normalize_and_wrap( + reg, + resource.as_ptr(), + api.as_ptr(), + ctx.as_ptr(), + params.as_ptr(), + ); + let json = assert_ok_string(&r); + regorus_result_drop(r); + + let envelope: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON"); + // Without aliases, properties should still be flattened + assert_eq!(envelope["resource"]["foo"], 1); + assert_eq!(envelope["resource"]["name"], "test"); + + regorus_alias_registry_drop(reg); + } +} diff --git a/bindings/ffi/src/common.rs b/bindings/ffi/src/common.rs index 10ce9f7..c6d8c59 100644 --- a/bindings/ffi/src/common.rs +++ b/bindings/ffi/src/common.rs @@ -11,6 +11,7 @@ use core::ffi::{c_char, c_longlong, c_void, CStr}; use core::{mem, ptr}; /// Status of a call on `RegorusEngine`. +#[derive(Debug, PartialEq)] #[repr(C)] pub enum RegorusStatus { /// The operation was successful. diff --git a/bindings/ffi/src/lib.rs b/bindings/ffi/src/lib.rs index 996f2f3..fa97f63 100644 --- a/bindings/ffi/src/lib.rs +++ b/bindings/ffi/src/lib.rs @@ -5,6 +5,7 @@ extern crate alloc; +mod alias_registry; mod allocator; mod common; mod compile; diff --git a/bindings/java/Cargo.lock b/bindings/java/Cargo.lock index 617cb7d..5ada1d5 100644 --- a/bindings/java/Cargo.lock +++ b/bindings/java/Cargo.lock @@ -69,9 +69,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "borrow-or-share" @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecount" @@ -109,9 +109,9 @@ checksum = "1e748733b7cbc798e1434b6ac524f0c1ff2ab456fe201501e6497c8417a4fc33" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -512,9 +512,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "jni" @@ -567,9 +567,9 @@ dependencies = [ [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -616,9 +616,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "litemap" @@ -649,9 +649,9 @@ checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "msvc_spectre_libs" @@ -743,9 +743,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "outref" @@ -842,9 +842,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -936,9 +936,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -947,9 +947,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "regorus" @@ -1024,9 +1024,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -1153,9 +1153,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1211,9 +1211,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-xid" @@ -1247,9 +1247,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", "rand", @@ -1307,9 +1307,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -1320,9 +1320,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1330,9 +1330,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -1343,9 +1343,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -1580,18 +1580,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", @@ -1654,6 +1654,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/bindings/python/Cargo.lock b/bindings/python/Cargo.lock index 1ffaf2a..1eca5f7 100644 --- a/bindings/python/Cargo.lock +++ b/bindings/python/Cargo.lock @@ -69,9 +69,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "borrow-or-share" @@ -91,9 +91,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecount" @@ -103,9 +103,9 @@ checksum = "175812e0be2bccb6abe50bb8d566126198344f707e304f45c648fd8f2cc0365e" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -496,15 +496,15 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" -version = "0.3.85" +version = "0.3.91" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8c942ebf8e95485ca0d52d97da7c5a2c387d0e7f0ba4c35e93bfcaee045955b3" +checksum = "b49715b7073f385ba4bc528e5747d02e66cb39c6146efb66b781f131f0fb399c" dependencies = [ "once_cell", "wasm-bindgen", @@ -551,9 +551,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "litemap" @@ -584,9 +584,9 @@ checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "msvc_spectre_libs" @@ -678,9 +678,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "ordered-float" @@ -746,9 +746,9 @@ dependencies = [ [[package]] name = "portable-atomic" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f89776e4d69bb58bc6993e99ffa1d11f228b839984854c7daeb5d37f87cbe950" +checksum = "c33a9471896f1c69cecef8d20cbe2f7accd12527ce60845ff44c153bb2a21b49" [[package]] name = "postcard" @@ -851,9 +851,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -945,9 +945,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -956,9 +956,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "regorus" @@ -1025,9 +1025,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "scopeguard" @@ -1129,9 +1129,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1151,9 +1151,9 @@ dependencies = [ [[package]] name = "target-lexicon" -version = "0.13.4" +version = "0.13.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1dd07eb858a2067e2f3c7155d54e929265c264e6f37efe3ee7a8d1b5a1dd0ba" +checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca" [[package]] name = "thiserror" @@ -1193,9 +1193,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-xid" @@ -1229,9 +1229,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", "rand", @@ -1279,9 +1279,9 @@ dependencies = [ [[package]] name = "wasm-bindgen" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "64024a30ec1e37399cf85a7ffefebdb72205ca1c972291c51512360d90bd8566" +checksum = "6532f9a5c1ece3798cb1c2cfdba640b9b3ba884f5db45973a6f442510a87d38e" dependencies = [ "cfg-if", "once_cell", @@ -1292,9 +1292,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "008b239d9c740232e71bd39e8ef6429d27097518b6b30bdf9086833bd5b6d608" +checksum = "18a2d50fcf105fb33bb15f00e7a77b772945a2ee45dcf454961fd843e74c18e6" dependencies = [ "quote", "wasm-bindgen-macro-support", @@ -1302,9 +1302,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-macro-support" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5256bae2d58f54820e6490f9839c49780dff84c65aeab9e772f15d5f0e913a55" +checksum = "03ce4caeaac547cdf713d280eda22a730824dd11e6b8c3ca9e42247b25c631e3" dependencies = [ "bumpalo", "proc-macro2", @@ -1315,9 +1315,9 @@ dependencies = [ [[package]] name = "wasm-bindgen-shared" -version = "0.2.108" +version = "0.2.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f01b580c9ac74c8d8f0c0e4afb04eeef2acf145458e52c03845ee9cd23e3d12" +checksum = "75a326b8c223ee17883a4251907455a2431acc2791c98c26279376490c378c16" dependencies = [ "unicode-ident", ] @@ -1534,18 +1534,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", @@ -1608,6 +1608,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/bindings/wasm/Cargo.lock b/bindings/wasm/Cargo.lock index 2592fb4..43e1f87 100644 --- a/bindings/wasm/Cargo.lock +++ b/bindings/wasm/Cargo.lock @@ -80,9 +80,9 @@ checksum = "5e764a1d40d510daf35e07be9eb06e75770908c27d411ee6c92109c9840eaaf7" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "borrow-or-share" @@ -102,9 +102,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.19.1" +version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dd9dc738b7a8311c7ade152424974d8115f2cdad61e8dab8dac9f2362298510" +checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" [[package]] name = "bytecount" @@ -120,9 +120,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.2.55" +version = "1.2.58" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" +checksum = "e1e928d4b69e3077709075a938a05ffbedfa53a84c8f766efbf8220bb1ff60e1" dependencies = [ "find-msvc-tools", "shlex", @@ -297,26 +297,25 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" +checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d" [[package]] name = "futures-task" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" +checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393" [[package]] name = "futures-util" -version = "0.3.31" +version = "0.3.32" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" +checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6" dependencies = [ "futures-core", "futures-task", "pin-project-lite", - "pin-utils", "slab", ] @@ -553,9 +552,9 @@ checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" [[package]] name = "itoa" -version = "1.0.17" +version = "1.0.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92ecc6618181def0457392ccd0ee51198e065e016d1d527a7ac1b6dc7c1f09d2" +checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682" [[package]] name = "js-sys" @@ -608,9 +607,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.180" +version = "0.2.183" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "b5b646652bf6661599e1da8901b3b9522896f01e736bad5f723fe7a3a27f899d" [[package]] name = "libm" @@ -647,9 +646,9 @@ checksum = "a1dc47f592c06f33f8e3aea9591776ec7c9f9e4124778ff8a3c3b87159f7e593" [[package]] name = "memchr" -version = "2.7.6" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" +checksum = "f8ca58f447f06ed17d5fc4043ce1b10dd205e060fb3ce5b979b8ed8e59ff3f79" [[package]] name = "minicov" @@ -761,9 +760,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.21.3" +version = "1.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" +checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50" [[package]] name = "oorandom" @@ -826,15 +825,9 @@ dependencies = [ [[package]] name = "pin-project-lite" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] name = "postcard" @@ -878,9 +871,9 @@ dependencies = [ [[package]] name = "quote" -version = "1.0.44" +version = "1.0.45" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b2ebcf727b7760c461f091f9f0f539b77b8e87f2fd88131e7f1b433b3cece4" +checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924" dependencies = [ "proc-macro2", ] @@ -972,9 +965,9 @@ dependencies = [ [[package]] name = "regex-automata" -version = "0.4.13" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5276caf25ac86c8d810222b3dbb938e512c55c6831a10f3e6ed1c93b84041f1c" +checksum = "6e1dd4122fc1595e8162618945476892eefca7b88c52820e74af6262213cae8f" dependencies = [ "aho-corasick", "memchr", @@ -983,9 +976,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.8.8" +version = "0.8.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2d987857b319362043e95f5353c0535c1f58eec5336fdfcf626430af7def58" +checksum = "dc897dd8d9e8bd1ed8cdad82b5966c3e0ecae09fb1907d58efaa013543185d0a" [[package]] name = "regorus" @@ -1042,9 +1035,9 @@ checksum = "b39cdef0fa800fc44525c84ccb54a029961a8215f9619753635a9c0d2538d46d" [[package]] name = "ryu" -version = "1.0.22" +version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a50f4cf475b65d88e057964e0e9bb1f0aa9bbb2036dc65c64596b42932536984" +checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "same-file" @@ -1148,9 +1141,9 @@ checksum = "b2aa850e253778c88a04c3d7323b043aeda9d3e30d5971937c1855769763678e" [[package]] name = "slab" -version = "0.4.11" +version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a2ae44ef20feb57a68b23d846850f861394c2e02dc425a50098ae8c90267589" +checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" [[package]] name = "smallvec" @@ -1172,9 +1165,9 @@ checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596" [[package]] name = "syn" -version = "2.0.114" +version = "2.0.117" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4d107df263a3013ef9b1879b0df87d706ff80f65a86ea879bd9c31f9b307c2a" +checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99" dependencies = [ "proc-macro2", "quote", @@ -1230,9 +1223,9 @@ checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f" [[package]] name = "unicode-ident" -version = "1.0.22" +version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9312f7c4f6ff9069b165498234ce8be658059c6728633667c526e27dc2cf1df5" +checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" [[package]] name = "unicode-xid" @@ -1266,9 +1259,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.22.0" +version = "1.23.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a68d3c8f01c0cfa54a75291d83601161799e4a89a39e0929f4b0354d88757a37" +checksum = "5ac8b6f42ead25368cf5b098aeb3dc8a1a2c05a3eee8a9a1a68c640edbfc79d9" dependencies = [ "getrandom 0.4.2", "js-sys", @@ -1670,18 +1663,18 @@ dependencies = [ [[package]] name = "zerocopy" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dafd85c832c1b68bbb4ec0c72c7f6f4fc5179627d2bc7c26b30e4c0cc11e76cc" +checksum = "efbb2a062be311f2ba113ce66f697a4dc589f85e78a4aea276200804cea0ed87" dependencies = [ "zerocopy-derive", ] [[package]] name = "zerocopy-derive" -version = "0.8.36" +version = "0.8.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cb7e4e8436d9db52fbd6625dbf2f45243ab84994a72882ec8227b99e72b439a" +checksum = "0e8bc7269b54418e7aeeef514aa68f8690b8c0489a06b0136e5f57c4c5ccab89" dependencies = [ "proc-macro2", "quote", @@ -1744,6 +1737,6 @@ dependencies = [ [[package]] name = "zmij" -version = "1.0.17" +version = "1.0.21" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "02aae0f83f69aafc94776e879363e9771d7ecbffe2c7fbb6c14c5e00dfe88439" +checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa" diff --git a/src/languages/azure_policy/aliases/denormalizer/casing.rs b/src/languages/azure_policy/aliases/denormalizer/casing.rs new file mode 100644 index 0000000..6acd9d2 --- /dev/null +++ b/src/languages/azure_policy/aliases/denormalizer/casing.rs @@ -0,0 +1,114 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Key casing restoration from alias metadata. + +use alloc::collections::BTreeMap; +use alloc::string::{String, ToString as _}; +use alloc::vec::Vec; + +use crate::Value; + +use super::super::obj_map::{make_array, make_value, new_map, obj_insert, val_str, ROOT_FIELDS}; +use super::super::types::ResolvedEntry; + +fn insert_default_casing(map: &mut BTreeMap) { + for &field in ROOT_FIELDS { + map.insert(field.to_ascii_lowercase(), field.to_string()); + } + + // Canonical casing for standard nested root-field object members that are + // not described by alias metadata but still need round-trip restoration. + for canonical in [ + "principalId", + "tenantId", + "userAssignedIdentities", + "promotionCode", + "createdBy", + "createdByType", + "createdAt", + "lastModifiedBy", + "lastModifiedByType", + "lastModifiedAt", + ] { + map.entry(canonical.to_ascii_lowercase()) + .or_insert_with(|| canonical.to_string()); + } +} + +/// Build the default casing map used when alias metadata is unavailable. +pub fn default_casing_map() -> BTreeMap { + let mut map = BTreeMap::new(); + insert_default_casing(&mut map); + map +} + +/// Build a mapping from lowercase key → original-cased key from alias entries. +pub fn build_casing_map(entries: &BTreeMap) -> BTreeMap { + let mut map = BTreeMap::new(); + insert_default_casing(&mut map); + + for entry in entries.values() { + for segment in entry.short_name.split('.') { + let clean = segment.replace("[*]", ""); + if !clean.is_empty() { + map.entry(clean.to_ascii_lowercase()) + .or_insert_with(|| clean.to_string()); + } + } + + for segment in entry.default_path.split('.') { + let clean = segment.replace("[*]", ""); + if !clean.is_empty() && !clean.eq_ignore_ascii_case("properties") { + map.entry(clean.to_ascii_lowercase()) + .or_insert_with(|| clean.to_string()); + } + } + + // Also include segments from all version-specific ARM paths so + // casing can be restored correctly for versioned aliases. + for (_ver, path) in &entry.versioned_paths { + for segment in path.split('.') { + let clean = segment.replace("[*]", ""); + if !clean.is_empty() && !clean.eq_ignore_ascii_case("properties") { + map.entry(clean.to_ascii_lowercase()) + .or_insert_with(|| clean.to_string()); + } + } + } + } + + map +} + +/// Restore the original casing of a key using the casing map. +pub fn restore_casing(key: &str, casing_map: &BTreeMap) -> String { + casing_map + .get(&key.to_ascii_lowercase()) + .cloned() + .unwrap_or_else(|| key.to_string()) +} + +/// Recursively restore key casing in a JSON value. +pub fn denormalize_value(value: &Value, casing_map: &BTreeMap) -> Value { + match value { + Value::Object(obj) => { + let mut result = new_map(); + for (k, v) in obj.iter() { + if let Some(key_s) = val_str(k) { + let restored_key = restore_casing(key_s, casing_map); + obj_insert(&mut result, &restored_key, denormalize_value(v, casing_map)); + } + } + make_value(result) + } + Value::Array(arr) => { + let items: Vec = arr + .iter() + .map(|v| denormalize_value(v, casing_map)) + .collect(); + make_array(items) + } + _ => value.clone(), + } +} diff --git a/src/languages/azure_policy/aliases/denormalizer/helpers.rs b/src/languages/azure_policy/aliases/denormalizer/helpers.rs new file mode 100644 index 0000000..22c81a3 --- /dev/null +++ b/src/languages/azure_policy/aliases/denormalizer/helpers.rs @@ -0,0 +1,13 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Small helper functions used by the denormalizer. + +use crate::Rc; + +use super::super::obj_map::ObjMap; + +/// Find a key in an ObjMap using case-insensitive comparison. +pub fn find_key_ci(obj: &ObjMap, key: &str) -> Option> { + obj.keys().find(|k| k.eq_ignore_ascii_case(key)).cloned() +} diff --git a/src/languages/azure_policy/aliases/denormalizer/mod.rs b/src/languages/azure_policy/aliases/denormalizer/mod.rs new file mode 100644 index 0000000..67f9639 --- /dev/null +++ b/src/languages/azure_policy/aliases/denormalizer/mod.rs @@ -0,0 +1,227 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Normalized `input.resource` → ARM JSON reverse transformation. + +mod casing; +pub(crate) mod helpers; +mod sub_resource; + +#[cfg(test)] +mod tests; + +use alloc::collections::BTreeSet; + +use crate::Value; + +use crate::Rc; + +use super::obj_map::{ + extract_type_field, is_root_field_collision, make_value, new_map, obj_insert, + set_nested_verbatim, val_str, ROOT_FIELDS, +}; +use super::types::ResolvedAliases; +use super::AliasRegistry; + +use super::normalizer::{apply_element_remap, ElementRemap}; + +use casing::{build_casing_map, default_casing_map, denormalize_value, restore_casing}; +use helpers::find_key_ci; + +use super::obj_map::remove_element_field; + +/// Denormalize a normalized resource back to ARM JSON structure. +pub fn denormalize( + normalized: &Value, + registry: Option<&AliasRegistry>, + api_version: Option<&str>, +) -> Value { + let aliases = registry.and_then(|r| extract_type_field(normalized).and_then(|rt| r.get(rt))); + denormalize_with_aliases(normalized, aliases, api_version) +} + +/// Internal denormalization with pre-resolved alias data. +pub fn denormalize_with_aliases( + normalized: &Value, + aliases: Option<&ResolvedAliases>, + api_version: Option<&str>, +) -> Value { + let obj = match normalized.as_object() { + Ok(o) => o, + Err(_) => return normalized.clone(), + }; + + let entries = aliases.map(|a| &a.entries); + let casing_map = entries + .map(build_casing_map) + .unwrap_or_else(default_casing_map); + let empty_set = BTreeSet::new(); + let sub_resource_set = aliases.map_or(&empty_set, |a| &a.sub_resource_arrays); + + let is_data_plane = + extract_type_field(normalized).is_some_and(|t| t.to_ascii_lowercase().contains(".data/")); + + let mut result = new_map(); + let mut properties = new_map(); + + // Phase 1: Root fields → ARM root with original casing. + for &field in ROOT_FIELDS { + let lc = field.to_ascii_lowercase(); + // Fast-path: direct BTreeMap lookup (O(log N)) for the common case + // where normalized input was produced by our normalizer with lowercase keys. + // Falls back to linear case-insensitive scan for externally-supplied mixed-case input. + let lc_key = Value::String(Rc::from(lc.as_str())); + let found = obj.get(&lc_key).or_else(|| { + obj.iter() + .find(|(k, _)| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(&lc))) + .map(|(_, v)| v) + }); + if let Some(val) = found { + let restored = denormalize_value(val, &casing_map); + obj_insert(&mut result, field, restored); + } + } + + // Phase 2a: Non-aliased, non-root fields. + for (key, val) in obj.iter() { + let key_s = match val_str(key) { + Some(s) => s, + None => continue, + }; + if ROOT_FIELDS.iter().any(|f| f.eq_ignore_ascii_case(key_s)) { + continue; + } + + let lookup_key = key_s.strip_prefix("_p_").unwrap_or(key_s); + let lookup_key_lc = lookup_key.to_ascii_lowercase(); + let has_alias = entries.is_some_and(|e| e.contains_key(lookup_key_lc.as_str())); + if has_alias { + continue; + } + + let denorm_val = denormalize_value(val, &casing_map); + + if key_s.starts_with("_p_") { + let restored = restore_casing(lookup_key, &casing_map); + obj_insert(&mut properties, &restored, denorm_val); + } else if is_data_plane { + let restored = restore_casing(key_s, &casing_map); + obj_insert(&mut result, &restored, denorm_val); + } else { + let restored = restore_casing(key_s, &casing_map); + obj_insert(&mut properties, &restored, denorm_val); + } + } + + // Phase 2b: Aliased scalar fields → versioned ARM paths. + if let Some(entries) = entries { + for (lc_key, entry) in entries { + if entry.is_wildcard { + continue; + } + + if sub_resource_set.contains(lc_key.as_str()) { + continue; + } + + let normalized_key = if is_root_field_collision(&entry.short_name, &entry.default_path) + { + alloc::format!("_p_{}", entry.short_name.to_ascii_lowercase()) + } else { + lc_key.clone() + }; + + // Fast-path: direct BTreeMap lookup for lowercase keys, + // with case-insensitive fallback for mixed-case external input. + let nk_val = Value::String(Rc::from(normalized_key.as_str())); + let val = obj.get(&nk_val).or_else(|| { + obj.iter() + .find(|(k, _)| { + val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(&normalized_key)) + }) + .map(|(_, v)| v) + }); + let val = match val { + Some(v) => v, + None => continue, + }; + + let arm_path = entry.select_path(api_version); + let denorm_val = denormalize_value(val, &casing_map); + + if let Some(props_path) = arm_path.strip_prefix("properties.") { + set_nested_verbatim(&mut properties, props_path, denorm_val); + } else { + set_nested_verbatim(&mut result, arm_path, denorm_val); + } + } + + // Phase 2c + 2d: Use precomputed renames/remaps. + // Look up versioned aggregates when api_version is provided, + // falling back to default aggregates. + if let Some(aliases) = aliases { + let agg = api_version.map_or(&aliases.default_aggregates, |ver| { + let ver_lc = ver.to_ascii_lowercase(); + aliases + .versioned_aggregates + .get(&ver_lc) + .unwrap_or(&aliases.default_aggregates) + }); + + // Phase 2c: Precomputed array base renames. + for (alias_base_lc, arm_base) in &agg.array_renames_denormalize { + if let Some(key) = find_key_ci(&properties, alias_base_lc) { + if let Some(val) = properties.remove(key.as_ref()) { + obj_insert(&mut properties, arm_base, val); + } + } + } + + // Phase 2d: Precomputed reverse element-level field remaps. + for rev in &agg.reverse_element_remaps { + let remap = ElementRemap { + array_chain: rev.array_chain.clone(), + source_field: rev.source_field.clone(), + target_field: if rev.target_field.contains('.') { + rev.target_field + .split('.') + .map(|segment| restore_casing(segment, &casing_map)) + .collect::>() + .join(".") + } else { + restore_casing(&rev.target_field, &casing_map) + }, + }; + apply_element_remap(&mut properties, &remap, false); + remove_element_field(&mut properties, &rev.array_chain, &rev.cleanup_field); + } + } + } + + // Phase 3: Re-wrap sub-resource array elements. + if let Some(aliases) = aliases { + if !aliases.sub_resource_arrays.is_empty() { + sub_resource::rewrap_sub_resource_arrays( + &mut properties, + &aliases.sub_resource_arrays, + &aliases.entries, + api_version, + ); + } + } + + // Phase 4: Attach properties to result. + if !properties.is_empty() { + if let Some(Value::Object(existing_rc)) = result.get_mut("properties") { + // Merge directly into the BTreeMap, avoiding full ObjMap round-trip. + let existing = Rc::make_mut(existing_rc); + for (k, v) in properties { + existing.entry(Value::String(k)).or_insert(v); + } + } else { + obj_insert(&mut result, "properties", make_value(properties)); + } + } + + make_value(result) +} diff --git a/src/languages/azure_policy/aliases/denormalizer/sub_resource.rs b/src/languages/azure_policy/aliases/denormalizer/sub_resource.rs new file mode 100644 index 0000000..6ab9897 --- /dev/null +++ b/src/languages/azure_policy/aliases/denormalizer/sub_resource.rs @@ -0,0 +1,228 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Sub-resource array re-wrapping during denormalization. + +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::string::String; +use alloc::vec::Vec; + +use crate::Value; + +use super::super::obj_map::{make_value, new_map, obj_insert, val_str, ObjMap}; +use super::super::types::ResolvedEntry; +use super::helpers::find_key_ci; + +/// Sub-resource array element envelope fields that remain at the element root. +const ELEMENT_ENVELOPE_FIELDS: &[&str] = &["name", "type", "id", "etag"]; + +/// Re-wrap sub-resource array elements by moving non-envelope fields back +/// under each element's `properties` object. +pub fn rewrap_sub_resource_arrays( + properties: &mut ObjMap, + sub_arrays: &BTreeSet, + entries: &BTreeMap, + api_version: Option<&str>, +) { + let mut sorted: Vec<&String> = sub_arrays.iter().collect(); + sorted.sort_by(|a, b| { + let depth_a = a.chars().filter(|&c| c == '.').count(); + let depth_b = b.chars().filter(|&c| c == '.').count(); + depth_b.cmp(&depth_a) + }); + + for sub_array_path in sorted { + let envelope_fields = classify_envelope_fields(sub_array_path, entries, api_version); + let parts: Vec<&str> = sub_array_path.split('.').collect(); + + if parts.len() == 1 { + if let Some(key) = parts.first().and_then(|p| find_key_ci(properties, p)) { + if let Some(Value::Array(arr)) = properties.get_mut(key.as_ref()) { + let inner = crate::Rc::make_mut(arr); + for elem in inner.iter_mut() { + *elem = rewrap_element(elem, &envelope_fields); + } + } + } + } else if let Some((&array_name, parent_parts)) = parts.split_last() { + rewrap_nested_array(properties, parent_parts, array_name, &envelope_fields); + } + } +} + +/// Determine which element-level fields are envelope fields for a given +/// sub-resource array. +fn classify_envelope_fields( + sub_array_path: &str, + entries: &BTreeMap, + api_version: Option<&str>, +) -> BTreeSet { + let mut envelope = BTreeSet::new(); + for &f in ELEMENT_ENVELOPE_FIELDS { + envelope.insert(f.to_ascii_lowercase()); + } + + let wildcard_prefix: String = { + let parts: Vec<&str> = sub_array_path.split('.').collect(); + let mut prefix = String::new(); + for (i, part) in parts.iter().enumerate() { + if i > 0 { + prefix.push_str("[*]."); + } + prefix.push_str(&part.to_ascii_lowercase()); + } + prefix.push_str("[*]."); + prefix + }; + + for (lc_key, entry) in entries { + if !lc_key.starts_with(&wildcard_prefix) { + continue; + } + let field = &lc_key[wildcard_prefix.len()..]; + let first_segment = field.split('.').next().unwrap_or(field); + + let arm_path = entry.select_path(api_version); + let arm_after_last_wildcard = arm_path.rsplit("[*].").next().unwrap_or(""); + + if !arm_after_last_wildcard.starts_with("properties.") { + envelope.insert(first_segment.to_ascii_lowercase()); + } + } + + envelope +} + +/// Recursively navigate nested arrays and re-wrap elements of the innermost +/// sub-resource array. +fn rewrap_nested_array( + obj: &mut ObjMap, + parent_parts: &[&str], + array_name: &str, + envelope_fields: &BTreeSet, +) { + if parent_parts.is_empty() { + return; + } + + let parent_key = match parent_parts.first().and_then(|&p| find_key_ci(obj, p)) { + Some(k) => k, + None => return, + }; + + let parent_arr = match obj.get_mut(parent_key.as_ref()) { + Some(Value::Array(arr)) => crate::Rc::make_mut(arr), + _ => return, + }; + + for element in parent_arr.iter_mut() { + if let Value::Object(obj_rc) = element { + let inner_btree = crate::Rc::make_mut(obj_rc); + + if parent_parts.len() > 1 { + rewrap_nested_array_in_btree( + inner_btree, + parent_parts.get(1..).unwrap_or_default(), + array_name, + envelope_fields, + ); + } else if let Some(arr_key) = find_key_ci_btree(inner_btree, array_name) { + if let Some(Value::Array(arr)) = inner_btree.get_mut(&arr_key) { + let inner = crate::Rc::make_mut(arr); + for inner_elem in inner.iter_mut() { + *inner_elem = rewrap_element(inner_elem, envelope_fields); + } + } + } + } + } +} + +/// BTreeMap-native recursion for nested sub-resource array re-wrapping, +/// avoiding ObjMap round-trips on each array element. +fn rewrap_nested_array_in_btree( + btree: &mut alloc::collections::BTreeMap, + parent_parts: &[&str], + array_name: &str, + envelope_fields: &BTreeSet, +) { + if parent_parts.is_empty() { + return; + } + + let parent_key = match parent_parts + .first() + .and_then(|&p| find_key_ci_btree(btree, p)) + { + Some(k) => k, + None => return, + }; + + let parent_arr = match btree.get_mut(&parent_key) { + Some(Value::Array(arr)) => crate::Rc::make_mut(arr), + _ => return, + }; + + for element in parent_arr.iter_mut() { + if let Value::Object(obj_rc) = element { + let inner_btree = crate::Rc::make_mut(obj_rc); + + if parent_parts.len() > 1 { + rewrap_nested_array_in_btree( + inner_btree, + parent_parts.get(1..).unwrap_or_default(), + array_name, + envelope_fields, + ); + } else if let Some(arr_key) = find_key_ci_btree(inner_btree, array_name) { + if let Some(Value::Array(arr)) = inner_btree.get_mut(&arr_key) { + let inner = crate::Rc::make_mut(arr); + for inner_elem in inner.iter_mut() { + *inner_elem = rewrap_element(inner_elem, envelope_fields); + } + } + } + } + } +} + +/// Find a key in a BTreeMap using case-insensitive comparison. +fn find_key_ci_btree( + btree: &alloc::collections::BTreeMap, + key: &str, +) -> Option { + btree + .keys() + .find(|k| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(key))) + .cloned() +} + +/// Re-wrap a single sub-resource array element by moving non-envelope +/// fields back under a `properties` object. +fn rewrap_element(element: &Value, envelope_fields: &BTreeSet) -> Value { + let obj = match element.as_object() { + Ok(o) => o, + Err(_) => return element.clone(), + }; + + let mut envelope = new_map(); + let mut props = new_map(); + + for (key, val) in obj.iter() { + let key_s = match val_str(key) { + Some(s) => s, + None => continue, + }; + if envelope_fields.contains(&key_s.to_ascii_lowercase()) { + obj_insert(&mut envelope, key_s, val.clone()); + } else { + obj_insert(&mut props, key_s, val.clone()); + } + } + + if !props.is_empty() { + obj_insert(&mut envelope, "properties", make_value(props)); + } + + make_value(envelope) +} diff --git a/src/languages/azure_policy/aliases/denormalizer/tests.rs b/src/languages/azure_policy/aliases/denormalizer/tests.rs new file mode 100644 index 0000000..ac0d4f1 --- /dev/null +++ b/src/languages/azure_policy/aliases/denormalizer/tests.rs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Inline denormalizer unit tests. + +use alloc::collections::BTreeMap; +use alloc::string::ToString as _; +use alloc::vec; +use alloc::vec::Vec; + +use super::super::types::ResolvedEntry; +use super::casing::build_casing_map; + +fn make_entry(short: &str, default: &str, versioned: Vec<(&str, &str)>) -> ResolvedEntry { + ResolvedEntry::new( + short.to_string(), + default.to_string(), + versioned + .into_iter() + .map(|(v, p)| (v.to_string(), p.to_string())) + .collect(), + None, + ) +} + +#[test] +fn build_casing_map_extracts_from_aliases() { + let mut entries = BTreeMap::new(); + entries.insert( + "supportshttpstrafficonly".to_string(), + make_entry( + "supportsHttpsTrafficOnly", + "properties.supportsHttpsTrafficOnly", + vec![], + ), + ); + entries.insert( + "networkacls.defaultaction".to_string(), + make_entry( + "networkAcls.defaultAction", + "properties.networkAcls.defaultAction", + vec![], + ), + ); + + let map = build_casing_map(&entries); + assert_eq!( + map.get("supportshttpstrafficonly"), + Some(&"supportsHttpsTrafficOnly".to_string()) + ); + assert_eq!(map.get("networkacls"), Some(&"networkAcls".to_string())); + assert_eq!(map.get("defaultaction"), Some(&"defaultAction".to_string())); + assert_eq!(map.get("managedby"), Some(&"managedBy".to_string())); + assert_eq!(map.get("apiversion"), Some(&"apiVersion".to_string())); +} diff --git a/src/languages/azure_policy/aliases/mod.rs b/src/languages/azure_policy/aliases/mod.rs new file mode 100644 index 0000000..f6a1466 --- /dev/null +++ b/src/languages/azure_policy/aliases/mod.rs @@ -0,0 +1,1310 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Azure Policy alias resolution and ARM resource normalization. +//! +//! This module provides: +//! - [`types`]: Data types for deserializing production alias catalogs +//! - [`normalizer`]: ARM JSON → normalized `input.resource` transformation +//! +//! # Overview +//! +//! Azure Policy aliases are short names for ARM JSON paths. For example, +//! `Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly` maps to the +//! ARM path `properties.supportsHttpsTrafficOnly`. +//! +//! The normalizer transforms raw ARM resource JSON into a flat structure where +//! alias short names are direct paths. This means the compiler and VM never +//! need to know about aliases — the normalizer handles the translation once +//! before evaluation. + +pub mod denormalizer; +pub mod normalizer; +pub(crate) mod obj_map; +pub mod types; + +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::string::{String, ToString as _}; +use alloc::vec::Vec; + +use anyhow::Result; + +use types::{ + AliasEntry, AliasPath, DataPolicyManifest, PrecomputedRemap, PrecomputedReverseRemap, + ProviderAliases, ResolvedAliases, ResolvedEntry, VersionedAggregates, +}; + +use obj_map::{collision_safe_key, is_root_field_collision}; + +/// Registry of resolved alias data, keyed by fully-qualified resource type +/// (case-insensitive, stored lowercase). +#[derive(Debug, Clone, Default)] +pub struct AliasRegistry { + /// Map from lowercase resource type → resolved alias data. + types: BTreeMap, + /// Global reverse lookup: lowercase fully-qualified alias name → short name. + /// + /// Built during [`load_provider`] so the compiler can resolve any alias + /// to its short name without knowing the resource type. + alias_to_short: BTreeMap, + /// Global lookup: lowercase fully-qualified alias name → modifiable flag. + /// + /// `true` when `defaultMetadata.attributes == "Modifiable"`, `false` otherwise. + alias_modifiable: BTreeMap, +} + +impl AliasRegistry { + /// Create a new empty registry. + pub const fn new() -> Self { + Self { + types: BTreeMap::new(), + alias_to_short: BTreeMap::new(), + alias_modifiable: BTreeMap::new(), + } + } + + /// Load alias data from a JSON string. + /// + /// Accepts either a bare JSON array of `ProviderAliases` objects or an + /// ARM-style `{ "value": [...] }` envelope, as produced by + /// `Get-AzPolicyAlias` or the ARM provider metadata API. + /// + /// Multiple calls accumulate data; duplicates overwrite earlier entries. + pub fn load_from_json(&mut self, json: &str) -> Result<()> { + let providers: Vec = types::load_auto(json)?; + for provider in providers { + self.load_provider(provider); + } + Ok(()) + } + + /// Load alias data from a data policy manifest JSON string. + /// + /// Data policy manifests describe data-plane aliases (e.g., + /// `Microsoft.KeyVault.Data`, `Microsoft.DataFactory.Data`). Their format + /// differs from the control-plane `ProviderAliases` catalog: aliases use + /// `paths[0].path` instead of `defaultPath`, and may include both + /// top-level aliases and per-resource-type groups. + /// + /// Multiple calls accumulate data; duplicates overwrite earlier entries. + pub fn load_data_policy_manifest_json(&mut self, json: &str) -> Result<()> { + let manifest: DataPolicyManifest = serde_json::from_str(json)?; + self.load_data_policy_manifest(manifest); + Ok(()) + } + + /// Load a data policy manifest. + pub fn load_data_policy_manifest(&mut self, manifest: DataPolicyManifest) { + let namespace = &manifest.data_namespace; + + // Collect known resource types so we can assign top-level aliases. + let known_rts: Vec = manifest + .resource_type_aliases + .iter() + .map(|rta| rta.resource_type.clone()) + .collect(); + + // 1. Process per-resource-type alias groups. + for rta in &manifest.resource_type_aliases { + let fq_type = alloc::format!("{}/{}", namespace, rta.resource_type); + let entries = convert_data_manifest_aliases(&fq_type, &rta.aliases); + self.ingest_alias_entries(&fq_type, &entries); + } + + // 2. Process top-level aliases — determine resource type from name. + // Group them by resource type, then merge into existing entries. + let mut grouped: BTreeMap> = BTreeMap::new(); + let ns_prefix = alloc::format!("{}/", namespace); + + for alias in &manifest.aliases { + let suffix = if alias.name.len() > ns_prefix.len() + && alias.name[..ns_prefix.len()].eq_ignore_ascii_case(&ns_prefix) + { + &alias.name[ns_prefix.len()..] + } else { + continue; + }; + + // Find the longest matching known resource type. + let mut best_rt: Option<&str> = None; + let mut best_len = 0; + for rt in &known_rts { + if suffix.len() > rt.len() + && suffix[..rt.len()].eq_ignore_ascii_case(rt) + && suffix.as_bytes().get(rt.len()) == Some(&b'/') + && rt.len() > best_len + { + best_rt = Some(rt.as_str()); + best_len = rt.len(); + } + } + + let entry = data_alias_to_alias_entry(alias); + + if let Some(rt) = best_rt { + let fq_type = alloc::format!("{}/{}", namespace, rt); + grouped.entry(fq_type).or_default().push(entry); + } + // If no matching resource type, skip (shouldn't happen in practice). + } + + // Merge grouped top-level aliases into existing resource type entries. + for (fq_type, entries) in &grouped { + self.ingest_alias_entries(fq_type, entries); + } + } + + /// Load a single provider's alias data. + pub fn load_provider(&mut self, provider: ProviderAliases) { + let namespace = &provider.namespace; + for rt in provider.resource_types { + let fq_type = alloc::format!("{}/{}", namespace, rt.resource_type); + self.ingest_alias_entries(&fq_type, &rt.aliases); + } + } + + /// Ingest a batch of alias entries for a fully-qualified resource type. + /// + /// This is the shared core used by both [`load_provider`] and + /// [`load_data_policy_manifest`]. It updates the global lookup maps + /// and merges resolved entries into the per-resource-type map. + fn ingest_alias_entries(&mut self, fq_type: &str, aliases: &[AliasEntry]) { + let prefix = alloc::format!("{}/", fq_type); + + for alias in aliases { + // Derive the short name by stripping the resource type prefix. + let raw_short = if alias.name.len() > prefix.len() + && alias.name[..prefix.len()].eq_ignore_ascii_case(&prefix) + { + alias.name[prefix.len()..].to_string() + } else if let Some(rest) = alias + .name + .rfind('/') + .and_then(|idx| alias.name.get(idx.saturating_add(1)..)) + { + rest.to_string() + } else { + continue; + }; + + let default_path = alias.default_path.as_deref().unwrap_or(""); + // The normalizer flattens `properties` into the resource root, so + // strip a leading `properties.` / `properties/` from the short name. + let raw_short_normalized = normalize_short_name(&raw_short).to_string(); + + let short = if is_root_field_collision(&raw_short_normalized, default_path) { + collision_safe_key(&raw_short_normalized) + } else { + raw_short_normalized + }; + let lc_name = alias.name.to_lowercase(); + self.alias_to_short.insert(lc_name.clone(), short); + let is_modifiable = types::has_flag( + alias + .default_metadata + .as_ref() + .and_then(|m| m.attributes.as_deref()), + "Modifiable", + ); + self.alias_modifiable.insert(lc_name, is_modifiable); + } + + let resolved = resolve_resource_type(fq_type, aliases); + let lc_type = fq_type.to_lowercase(); + + // Merge into existing entry if one exists (for data manifests that + // have both top-level and per-resource-type aliases for the same type). + if let Some(existing) = self.types.get_mut(&lc_type) { + for (key, entry) in resolved.entries { + existing.entries.insert(key, entry); + } + // Replace sub-resource arrays: redetect from the merged entry set + // so that overwritten aliases that no longer indicate sub-resource + // wrapping cause stale classifications to be removed. + existing.sub_resource_arrays = redetect_sub_resource_arrays(&existing.entries); + // Recompute aggregates after merge. + let (default_agg, versioned_agg) = + precompute_aggregates(&existing.entries, &existing.sub_resource_arrays); + existing.default_aggregates = default_agg; + existing.versioned_aggregates = versioned_agg; + } else { + self.types.insert(lc_type, resolved); + } + } + + /// Look up resolved aliases for a resource type. + /// + /// The lookup is case-insensitive. + pub fn get(&self, resource_type: &str) -> Option<&ResolvedAliases> { + self.types.get(&resource_type.to_lowercase()) + } + + /// Number of registered resource types. + pub fn len(&self) -> usize { + self.types.len() + } + + /// Whether the registry is empty. + pub fn is_empty(&self) -> bool { + self.types.is_empty() + } + + /// Resolve a fully-qualified alias name to its short name. + /// + /// The lookup is case-insensitive. Returns `None` if the alias is not + /// found in the registry (meaning it's either already a short name or + /// not a known alias). + pub fn resolve_alias(&self, fq_name: &str) -> Option<&str> { + self.alias_to_short + .get(&fq_name.to_lowercase()) + .map(String::as_str) + } + + /// Return a clone of the alias-to-short-name map for use by the compiler. + /// + /// The compiler stores this map internally so it can resolve fully-qualified + /// alias names without holding a reference to the registry. + pub fn alias_map(&self) -> BTreeMap { + self.alias_to_short.clone() + } + + /// Return a clone of the alias-to-modifiable map for use by the compiler. + /// + /// Maps lowercase fully-qualified alias names to `true` when the alias + /// has `defaultMetadata.attributes = "Modifiable"`. + pub fn alias_modifiable_map(&self) -> BTreeMap { + self.alias_modifiable.clone() + } + + /// Normalize a raw ARM resource and wrap it in the input envelope. + /// + /// Convenience method that combines alias lookup, normalization, and + /// envelope construction. The resource type is extracted from the + /// `type` field of `arm_resource` automatically. + /// + /// # Arguments + /// + /// * `arm_resource` — The raw ARM JSON for the resource. + /// * `api_version` — Optional API version to select versioned alias paths. + /// * `context` — Optional context object for the input envelope. + /// * `parameters` — Optional parameters object for the input envelope. + pub fn normalize_and_wrap( + &self, + arm_resource: &crate::Value, + api_version: Option<&str>, + context: Option, + parameters: Option, + ) -> crate::Value { + let normalized = normalizer::normalize(arm_resource, Some(self), api_version); + normalizer::build_input_envelope(normalized, context, parameters) + } + + /// Denormalize a normalized resource back to ARM JSON structure. + /// + /// Convenience method that combines alias lookup and denormalization. + /// The resource type is extracted from the `type` field of `normalized` + /// automatically. + /// + /// # Arguments + /// + /// * `normalized` — The normalized JSON object (as produced by + /// [`normalizer::normalize`]). + /// * `api_version` — Optional API version to select versioned alias paths. + pub fn denormalize( + &self, + normalized: &crate::Value, + api_version: Option<&str>, + ) -> crate::Value { + denormalizer::denormalize(normalized, Some(self), api_version) + } +} + +/// Resolve a resource type's alias entries into `ResolvedAliases`. +/// +/// This: +/// 1. Strips the resource type prefix from alias names to produce short names. +/// 2. Extracts `defaultPath` and versioned paths. +/// 3. Detects sub-resource arrays from alias path patterns. +fn resolve_resource_type(fq_type: &str, aliases: &[types::AliasEntry]) -> ResolvedAliases { + let prefix = alloc::format!("{}/", fq_type); + let mut entries = BTreeMap::new(); + let mut sub_resource_arrays: BTreeSet = BTreeSet::new(); + + for alias in aliases { + // Derive short name by stripping the resource type prefix. + // For cross-type aliases (e.g., Microsoft.Compute/imagePublisher + // under the virtualMachines resource type), the name does not + // start with the resource type prefix. In that case, take the + // part after the last '/'. + let short_name = if alias.name.len() > prefix.len() + && alias.name[..prefix.len()].eq_ignore_ascii_case(&prefix) + { + &alias.name[prefix.len()..] + } else if let Some(rest) = alias + .name + .rfind('/') + .and_then(|idx| alias.name.get(idx.saturating_add(1)..)) + { + rest + } else { + &alias.name + }; + + // The normalizer flattens `properties` into the resource root, so + // strip a leading `properties.` / `properties/` from the short name. + let short_name = normalize_short_name(short_name); + + let default_path = match &alias.default_path { + Some(p) => p.clone(), + None => continue, // Skip aliases without a default path (shouldn't happen in production) + }; + + // Detect sub-resource arrays from the alias pattern. + // If short name contains `[*]` and default_path has + // `properties.X[*].properties.Y`, then X is a sub-resource array. + detect_sub_resource_array(short_name, &default_path, &mut sub_resource_arrays); + + let versioned_paths: Vec<(String, String)> = alias + .paths + .iter() + .flat_map(|p| { + p.api_versions + .iter() + .map(move |v| (v.clone(), p.path.clone())) + }) + .collect(); + + entries.insert( + short_name.to_lowercase(), + ResolvedEntry::new( + short_name.to_string(), + default_path, + versioned_paths, + alias.default_metadata.clone(), + ), + ); + } + + // Precompute aggregate fields from entries. + let (default_aggregates, versioned_aggregates) = + precompute_aggregates(&entries, &sub_resource_arrays); + + ResolvedAliases { + resource_type: fq_type.to_string(), + entries, + sub_resource_arrays, + default_aggregates, + versioned_aggregates, + } +} + +/// Return type for [`precompute_aggregates`]. +type AggregateFields = (VersionedAggregates, BTreeMap); + +/// Precompute element remaps, reverse remaps, and array renames from resolved +/// entries for the default path AND every distinct api_version found in any +/// entry's `versioned_paths`. +/// +/// This moves O(aliases) string splitting/lowercasing out of the per-call +/// normalize/denormalize hot path into a one-time cost at registry-load time. +fn precompute_aggregates( + entries: &BTreeMap, + sub_resource_arrays: &BTreeSet, +) -> AggregateFields { + // Collect all distinct api_versions that appear in any entry. + let mut all_versions = alloc::collections::BTreeSet::new(); + for entry in entries.values() { + for (ver, _) in &entry.versioned_paths { + all_versions.insert(ver.to_lowercase()); + } + } + + // Compute default aggregates (api_version = None). + let default_agg = compute_aggregates_for_version(entries, sub_resource_arrays, None); + + // Compute per-version aggregates. + let mut versioned_map = BTreeMap::new(); + for ver in &all_versions { + let agg = compute_aggregates_for_version(entries, sub_resource_arrays, Some(ver.as_str())); + // Only store if it differs from default (saves memory for versions + // where no entry has a different path). + if agg != default_agg { + versioned_map.insert(ver.clone(), agg); + } + } + + (default_agg, versioned_map) +} + +/// Compute aggregates for a specific api_version (or None for default path). +fn compute_aggregates_for_version( + entries: &BTreeMap, + sub_resource_arrays: &BTreeSet, + api_version: Option<&str>, +) -> VersionedAggregates { + let mut element_remaps = Vec::new(); + let mut reverse_element_remaps = Vec::new(); + let mut renames_norm: Vec<(String, String)> = Vec::new(); + let mut renames_denorm: Vec<(String, String)> = Vec::new(); + + // Use BTreeSet for O(log N) dedup instead of Vec::contains. + let mut seen_norm = alloc::collections::BTreeSet::new(); + let mut seen_denorm = alloc::collections::BTreeSet::new(); + + for entry in entries.values() { + if !entry.is_wildcard { + continue; + } + + // Skip sub-resource array root entries for scalar processing. + // The set is pre-lowercased, so use a direct O(log n) lookup. + if sub_resource_arrays.contains(&entry.short_name.to_ascii_lowercase()) { + continue; + } + + // Select the ARM path for this version (or default). + let selected_path = entry.select_path(api_version); + let short_parts: Vec<&str> = entry.short_name.split("[*].").collect(); + let arm_raw_parts: Vec<&str> = selected_path.split("[*].").collect(); + + if short_parts.len() >= 2 && arm_raw_parts.len() >= 2 { + if let (Some(short_leaf), Some(arm_leaf_raw)) = + (short_parts.last(), arm_raw_parts.last()) + { + let arm_leaf = arm_leaf_raw + .strip_prefix("properties.") + .unwrap_or(arm_leaf_raw); + + if !short_leaf.eq_ignore_ascii_case(arm_leaf) { + let array_chain: Vec> = short_parts + .split_last() + .map(|(_, init)| init) + .unwrap_or_default() + .iter() + .map(|part| part.split('.').map(|s| s.to_ascii_lowercase()).collect()) + .collect(); + + let source_lc = arm_leaf.to_ascii_lowercase(); + let target_lc = short_leaf.to_ascii_lowercase(); + + element_remaps.push(PrecomputedRemap { + array_chain: array_chain.clone(), + source_field: source_lc.clone(), + target_field: target_lc.clone(), + }); + + reverse_element_remaps.push(PrecomputedReverseRemap { + array_chain: array_chain.clone(), + source_field: target_lc.clone(), + target_field: source_lc, + cleanup_field: target_lc, + }); + } + } + } + + // Compute array base rename. + if let (Some(short_base), Some(arm_base)) = ( + entry.short_name.split("[*]").next(), + selected_path.split("[*]").next(), + ) { + let arm_base_stripped = arm_base.strip_prefix("properties.").unwrap_or(arm_base); + if !short_base.eq_ignore_ascii_case(arm_base_stripped) { + // Normalize direction: (arm_base_lc, short_base_lc) + let norm_pair = ( + arm_base_stripped.to_ascii_lowercase(), + short_base.to_ascii_lowercase(), + ); + if seen_norm.insert(norm_pair.clone()) { + renames_norm.push(norm_pair); + } + + // Denormalize direction: (short_base_lc, arm_base) + let denorm_pair = ( + short_base.to_ascii_lowercase(), + arm_base_stripped.to_string(), + ); + if seen_denorm.insert(denorm_pair.clone()) { + renames_denorm.push(denorm_pair); + } + } + } + } + + VersionedAggregates { + element_remaps, + reverse_element_remaps, + array_renames_normalize: renames_norm, + array_renames_denormalize: renames_denorm, + } +} + +/// Detect sub-resource arrays from alias naming patterns. +/// +/// If the alias short name has `X[*].Y` and the default path has +/// `properties.X[*].properties.Y`, then `X` is a sub-resource array whose +/// elements need `properties` flattening during normalization. +/// +/// For nested sub-resource arrays like `X[*].Y[*].Z` mapping to +/// `properties.X[*].properties.Y[*].properties.Z`, both `X` and `X.Y` +/// (dotted path within the normalized structure) are sub-resource arrays. +fn detect_sub_resource_array( + short_name: &str, + default_path: &str, + sub_resource_arrays: &mut BTreeSet, +) { + // Split short name and default path by `[*].` + let short_parts: Vec<&str> = short_name.split("[*].").collect(); + let path_parts: Vec<&str> = default_path.split("[*].").collect(); + + if short_parts.len() < 2 || path_parts.len() < 2 { + return; // No wildcard — not a sub-resource array alias + } + + // For each `[*]` level, check if the pattern matches sub-resource wrapping. + // short_name: securityRules[*].protocol + // default_path: properties.securityRules[*].properties.protocol + // + // The first segment of the default_path after splitting should start with + // "properties." and the part after the first [*]. should start with + // "properties." to indicate sub-resource wrapping. + + // Build up the chain of sub-resource array names. + let mut accumulated_name = String::new(); + + for (i, array_field) in short_parts + .iter() + .enumerate() + .take(short_parts.len().saturating_sub(1)) + { + // For the first level, check that default_path starts with `properties.X[*].properties.` + // For nested levels, the segment after [*]. should also start with `properties.` + if let Some(next_path_segment) = path_parts.get(i.saturating_add(1)) { + if next_path_segment.starts_with("properties.") + || next_path_segment.starts_with("properties/") + { + // This is a sub-resource array! + let name = if accumulated_name.is_empty() { + array_field.to_string() + } else { + alloc::format!("{}.{}", accumulated_name, array_field) + }; + + sub_resource_arrays.insert(name.to_ascii_lowercase()); + accumulated_name = name; + } + } + } +} + +/// Redetect sub-resource arrays from the merged entry set. +/// +/// This is used after merging alias entries to ensure that stale sub-resource +/// classifications are removed when overwritten aliases no longer indicate +/// sub-resource wrapping. +fn redetect_sub_resource_arrays(entries: &BTreeMap) -> BTreeSet { + let mut sub_resource_arrays = BTreeSet::new(); + for entry in entries.values() { + detect_sub_resource_array( + &entry.short_name, + &entry.default_path, + &mut sub_resource_arrays, + ); + } + sub_resource_arrays +} + +/// Convert a data manifest alias to a standard [`AliasEntry`]. +/// +/// Data manifest aliases use `paths[0].path` as the effective default path +/// (the `defaultPath` field is absent). Both `apiVersions` and +/// `schemaVersions` are treated as version sets for versioned path lookup. +fn data_alias_to_alias_entry(dma: &types::DataManifestAlias) -> AliasEntry { + let default_path = dma.paths.first().map(|p| p.path.clone()); + + let paths: Vec = dma + .paths + .iter() + .map(|p| { + // Merge apiVersions and schemaVersions into a single version list. + let mut versions = p.api_versions.clone(); + versions.extend(p.schema_versions.iter().cloned()); + AliasPath { + path: p.path.clone(), + api_versions: versions, + metadata: None, + ..Default::default() + } + }) + .collect(); + + AliasEntry { + name: dma.name.clone(), + default_path, + default_metadata: None, + paths, + ..Default::default() + } +} + +/// Convert a slice of data manifest aliases to standard [`AliasEntry`] objects. +fn convert_data_manifest_aliases( + _fq_type: &str, + aliases: &[types::DataManifestAlias], +) -> Vec { + aliases.iter().map(data_alias_to_alias_entry).collect() +} + +/// Normalize a short name derived from an alias FQ name. +/// +/// The normalizer always flattens `properties` into the resource root, so a +/// short name like `properties.domainNames[*]` must be reduced to +/// `domainNames[*]` to match the normalized resource structure. +/// +/// This is primarily needed for data-plane aliases where the FQ name includes +/// `properties.` in the property part (e.g., +/// `Microsoft.DataFactory.Data/factories/outboundTraffic/properties.domainNames[*]`). +/// Control-plane alias names never include `properties.` in the short portion. +fn normalize_short_name(short: &str) -> &str { + short + .strip_prefix("properties.") + .or_else(|| short.strip_prefix("properties/")) + .unwrap_or(short) +} + +#[cfg(test)] +#[allow(clippy::indexing_slicing, clippy::unwrap_used, clippy::expect_used)] +mod tests { + use alloc::string::ToString as _; + use alloc::vec; + + use super::*; + + #[test] + fn test_detect_sub_resource_nsg() { + let mut subs = BTreeSet::new(); + detect_sub_resource_array( + "securityRules[*].protocol", + "properties.securityRules[*].properties.protocol", + &mut subs, + ); + assert_eq!(subs, BTreeSet::from(["securityrules".to_string()])); + } + + #[test] + fn test_detect_no_sub_resource() { + let mut subs = BTreeSet::new(); + // ipRules[*].value -> properties.networkAcls.ipRules[*].value + // No `properties.` after the `[*].` means NOT a sub-resource + detect_sub_resource_array( + "networkAcls.ipRules[*].value", + "properties.networkAcls.ipRules[*].value", + &mut subs, + ); + assert!(subs.is_empty()); + } + + #[test] + fn test_detect_nested_sub_resource() { + let mut subs = BTreeSet::new(); + detect_sub_resource_array( + "subnets[*].ipConfigurations[*].name", + "properties.subnets[*].properties.ipConfigurations[*].properties.name", + &mut subs, + ); + let expected: BTreeSet = BTreeSet::from([ + "subnets".to_string(), + "subnets.ipconfigurations".to_string(), + ]); + assert_eq!(subs, expected); + } + + #[test] + fn test_resolve_resource_type_basic() { + let aliases = vec![ + types::AliasEntry { + name: "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly".to_string(), + default_path: Some("properties.supportsHttpsTrafficOnly".to_string()), + default_metadata: None, + paths: vec![], + ..Default::default() + }, + types::AliasEntry { + name: "Microsoft.Storage/storageAccounts/sku.name".to_string(), + default_path: Some("sku.name".to_string()), + default_metadata: None, + paths: vec![], + ..Default::default() + }, + ]; + + let resolved = resolve_resource_type("Microsoft.Storage/storageAccounts", &aliases); + assert_eq!(resolved.entries.len(), 2); + + let https_entry = resolved.entries.get("supportshttpstrafficonly").unwrap(); + assert_eq!( + https_entry.default_path, + "properties.supportsHttpsTrafficOnly" + ); + + let sku_entry = resolved.entries.get("sku.name").unwrap(); + assert_eq!(sku_entry.default_path, "sku.name"); + + assert!(resolved.sub_resource_arrays.is_empty()); + } + + #[test] + fn test_resolve_nsg_has_sub_resource_arrays() { + let aliases = vec![ + types::AliasEntry { + name: "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol" + .to_string(), + default_path: Some("properties.securityRules[*].properties.protocol".to_string()), + default_metadata: None, + paths: vec![], + ..Default::default() + }, + types::AliasEntry { + name: "Microsoft.Network/networkSecurityGroups/securityRules[*].access".to_string(), + default_path: Some("properties.securityRules[*].properties.access".to_string()), + default_metadata: None, + paths: vec![], + ..Default::default() + }, + ]; + + let resolved = resolve_resource_type("Microsoft.Network/networkSecurityGroups", &aliases); + assert_eq!( + resolved.sub_resource_arrays, + BTreeSet::from(["securityrules".to_string()]) + ); + } + + #[test] + fn test_load_from_json() { + let json = r#"[ + { + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + } + ] + } + ] + } + ]"#; + + let mut registry = AliasRegistry::new(); + registry.load_from_json(json).unwrap(); + assert_eq!(registry.len(), 1); + + let resolved = registry.get("Microsoft.Storage/storageAccounts").unwrap(); + assert_eq!(resolved.entries.len(), 1); + assert!(resolved.entries.contains_key("supportshttpstrafficonly")); + } + + #[test] + fn test_registry_case_insensitive_get() { + let json = r#"[ + { + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + } + ] + } + ] + } + ]"#; + + let mut registry = AliasRegistry::new(); + registry.load_from_json(json).unwrap(); + + // Mixed-case lookup should work + assert!(registry.get("microsoft.storage/STORAGEACCOUNTS").is_some()); + assert!(registry.get("MICROSOFT.STORAGE/storageAccounts").is_some()); + } + + #[test] + fn test_resolve_with_versioned_paths() { + let aliases = vec![types::AliasEntry { + name: "Microsoft.Web/sites/siteConfig.numberOfWorkers".to_string(), + default_path: Some("properties.siteConfig.numberOfWorkers".to_string()), + default_metadata: None, + paths: vec![ + types::AliasPath { + path: "properties.siteConfig.properties.numberOfWorkers".to_string(), + api_versions: vec!["2014-04-01".to_string(), "2014-06-01".to_string()], + metadata: None, + ..Default::default() + }, + types::AliasPath { + path: "properties.siteConfig.numberOfWorkers".to_string(), + api_versions: vec!["2021-01-01".to_string()], + metadata: None, + ..Default::default() + }, + ], + ..Default::default() + }]; + + let resolved = resolve_resource_type("Microsoft.Web/sites", &aliases); + let entry = resolved.entries.get("siteconfig.numberofworkers").unwrap(); + + assert_eq!(entry.default_path, "properties.siteConfig.numberOfWorkers"); + // Has versioned paths + assert_eq!(entry.versioned_paths.len(), 3); // 2 + 1 + assert_eq!( + entry.select_path(Some("2014-04-01")), + "properties.siteConfig.properties.numberOfWorkers" + ); + assert_eq!( + entry.select_path(Some("2021-01-01")), + "properties.siteConfig.numberOfWorkers" + ); + // Unknown version falls back to default + assert_eq!( + entry.select_path(Some("9999-01-01")), + "properties.siteConfig.numberOfWorkers" + ); + } + + #[test] + fn test_resolve_alias_without_default_path_skipped() { + let aliases = vec![ + types::AliasEntry { + name: "Microsoft.Storage/storageAccounts/good".to_string(), + default_path: Some("properties.good".to_string()), + default_metadata: None, + paths: vec![], + ..Default::default() + }, + types::AliasEntry { + name: "Microsoft.Storage/storageAccounts/bad".to_string(), + default_path: None, + default_metadata: None, + paths: vec![], + ..Default::default() + }, + ]; + + let resolved = resolve_resource_type("Microsoft.Storage/storageAccounts", &aliases); + assert_eq!(resolved.entries.len(), 1); + assert!(resolved.entries.contains_key("good")); + assert!(!resolved.entries.contains_key("bad")); + } + + #[test] + fn test_empty_registry() { + let registry = AliasRegistry::new(); + assert!(registry.is_empty()); + assert_eq!(registry.len(), 0); + assert!(registry.get("Microsoft.Storage/storageAccounts").is_none()); + } + + #[test] + fn test_resolve_empty_aliases() { + let resolved = resolve_resource_type("Microsoft.Test/empty", &[]); + assert!(resolved.entries.is_empty()); + assert!(resolved.sub_resource_arrays.is_empty()); + } + + #[test] + fn test_sub_resource_dedup() { + // Multiple aliases for the same sub-resource array should deduplicate + let aliases = vec![ + types::AliasEntry { + name: "T/R/rules[*].a".to_string(), + default_path: Some("properties.rules[*].properties.a".to_string()), + default_metadata: None, + paths: vec![], + ..Default::default() + }, + types::AliasEntry { + name: "T/R/rules[*].b".to_string(), + default_path: Some("properties.rules[*].properties.b".to_string()), + default_metadata: None, + paths: vec![], + ..Default::default() + }, + types::AliasEntry { + name: "T/R/rules[*].c".to_string(), + default_path: Some("properties.rules[*].properties.c".to_string()), + default_metadata: None, + paths: vec![], + ..Default::default() + }, + ]; + + let resolved = resolve_resource_type("T/R", &aliases); + // "rules" should appear only once despite 3 aliases detecting it + assert_eq!( + resolved.sub_resource_arrays, + BTreeSet::from(["rules".to_string()]) + ); + } + + #[test] + fn test_normalize_and_wrap_full_pipeline() { + let json = r#"[ + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "paths": [] + } + ] + } + ] + } + ]"#; + + let mut registry = AliasRegistry::new(); + registry.load_from_json(json).unwrap(); + + let arm_resource = crate::Value::from_json_str( + r#"{ + "name": "myNsg", + "type": "Microsoft.Network/networkSecurityGroups", + "properties": { + "securityRules": [ + { + "name": "rule1", + "properties": { + "protocol": "Tcp" + } + } + ] + } + }"#, + ) + .unwrap(); + + let context = crate::Value::from_json_str(r#"{"resourceGroup": {"name": "rg1"}}"#).unwrap(); + let parameters = crate::Value::from_json_str(r#"{"env": "prod"}"#).unwrap(); + + let envelope = + registry.normalize_and_wrap(&arm_resource, None, Some(context), Some(parameters)); + + // Resource is normalized (all keys lowercased) + assert_eq!(envelope["resource"]["name"], crate::Value::from("myNsg")); + let rules = envelope["resource"]["securityrules"].as_array().unwrap(); + assert_eq!(rules[0]["protocol"], crate::Value::from("Tcp")); + assert_eq!(rules[0]["properties"], crate::Value::Undefined); + // Context and parameters are passed through + assert_eq!( + envelope["context"]["resourceGroup"]["name"], + crate::Value::from("rg1") + ); + assert_eq!(envelope["parameters"]["env"], crate::Value::from("prod")); + } + + #[test] + fn test_load_test_aliases_json() { + // Integration test: load the actual test_aliases.json file + let json = std::fs::read_to_string("tests/azure_policy/aliases/test_aliases.json") + .expect("test_aliases.json should exist"); + + let mut registry = AliasRegistry::new(); + registry + .load_from_json(&json) + .expect("test_aliases.json should parse"); + + // Expect 44 resource types + assert_eq!(registry.len(), 44); + + // Storage + let storage = registry + .get("Microsoft.Storage/storageAccounts") + .expect("Storage aliases should exist"); + assert!(!storage.entries.is_empty()); + assert!(storage.sub_resource_arrays.is_empty()); + + // NSG — should have sub-resource arrays + let nsg = registry + .get("Microsoft.Network/networkSecurityGroups") + .expect("NSG aliases should exist"); + assert!(!nsg.entries.is_empty()); + assert!( + nsg.sub_resource_arrays.contains("securityrules"), + "NSG should detect securityRules as sub-resource array" + ); + + // KeyVault + assert!(registry.get("Microsoft.KeyVault/vaults").is_some()); + + // SQL + assert!(registry.get("Microsoft.Sql/servers").is_some()); + + // VM + assert!(registry.get("Microsoft.Compute/virtualMachines").is_some()); + + // Web + assert!(registry.get("Microsoft.Web/sites").is_some()); + + // AKS + assert!(registry + .get("Microsoft.ContainerService/managedClusters") + .is_some()); + + // Disks + assert!(registry.get("Microsoft.Compute/disks").is_some()); + + // NIC — should have sub-resource arrays + let nic = registry + .get("Microsoft.Network/networkInterfaces") + .expect("NIC aliases should exist"); + assert!( + nic.sub_resource_arrays.contains("ipconfigurations"), + "NIC should detect ipConfigurations as sub-resource array" + ); + } + + #[test] + fn test_resolve_alias_basic() { + let json = r#"[ + { + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.name", + "defaultPath": "sku.name", + "paths": [] + } + ] + } + ] + } + ]"#; + + let mut registry = AliasRegistry::new(); + registry.load_from_json(json).unwrap(); + + assert_eq!( + registry.resolve_alias("Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"), + Some("supportsHttpsTrafficOnly") + ); + assert_eq!( + registry.resolve_alias("Microsoft.Storage/storageAccounts/sku.name"), + Some("sku.name") + ); + // Case-insensitive + assert_eq!( + registry.resolve_alias("microsoft.storage/STORAGEACCOUNTS/supportsHttpsTrafficOnly"), + Some("supportsHttpsTrafficOnly") + ); + // Unknown alias + assert_eq!( + registry.resolve_alias("Microsoft.Storage/storageAccounts/unknown"), + None + ); + // Already a short name + assert_eq!(registry.resolve_alias("supportsHttpsTrafficOnly"), None); + } + + #[test] + fn test_alias_map_for_compiler() { + let json = r#"[ + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "paths": [] + } + ] + } + ] + } + ]"#; + + let mut registry = AliasRegistry::new(); + registry.load_from_json(json).unwrap(); + + let map = registry.alias_map(); + // alias_map returns the short name derived by stripping the FQ type + // prefix from the alias name: + // `Microsoft.Network/networkSecurityGroups/securityRules[*].protocol` + // → `securityRules[*].protocol` + assert_eq!( + map.get("microsoft.network/networksecuritygroups/securityrules[*].protocol"), + Some(&"securityRules[*].protocol".to_string()) + ); + } + + #[test] + fn test_alias_modifiable_comma_separated_flags() { + // Regression: alias_modifiable_map should detect "Modifiable" even when + // the attributes field contains comma-separated flags like + // "Modifiable, SupportsCreate". + let json = r#"[ + { + "namespace": "Microsoft.Test", + "resourceTypes": [ + { + "resourceType": "widgets", + "aliases": [ + { + "name": "Microsoft.Test/widgets/singleFlag", + "defaultPath": "properties.singleFlag", + "paths": [], + "defaultMetadata": { + "attributes": "Modifiable" + } + }, + { + "name": "Microsoft.Test/widgets/multiFlag", + "defaultPath": "properties.multiFlag", + "paths": [], + "defaultMetadata": { + "attributes": "Modifiable, SupportsCreate" + } + }, + { + "name": "Microsoft.Test/widgets/notModifiable", + "defaultPath": "properties.notModifiable", + "paths": [], + "defaultMetadata": { + "attributes": "None" + } + }, + { + "name": "Microsoft.Test/widgets/noMetadata", + "defaultPath": "properties.noMetadata", + "paths": [] + } + ] + } + ] + } + ]"#; + + let mut registry = AliasRegistry::new(); + registry.load_from_json(json).unwrap(); + + let modifiable = registry.alias_modifiable_map(); + + // Single "Modifiable" flag → true + assert_eq!( + modifiable.get("microsoft.test/widgets/singleflag"), + Some(&true) + ); + // Comma-separated flags containing "Modifiable" → true + assert_eq!( + modifiable.get("microsoft.test/widgets/multiflag"), + Some(&true) + ); + // "None" → false + assert_eq!( + modifiable.get("microsoft.test/widgets/notmodifiable"), + Some(&false) + ); + // No metadata → false + assert_eq!( + modifiable.get("microsoft.test/widgets/nometadata"), + Some(&false) + ); + } + + #[test] + fn test_overwrite_removes_stale_sub_resource_arrays() { + // Regression: a second load that overwrites aliases for the same + // resource type must remove stale sub-resource array classifications + // when the new alias paths no longer indicate sub-resource wrapping. + + // First load: rules[*] aliases with `properties.` wrapping → sub-resource. + let json1 = r#"[ + { + "namespace": "Microsoft.Test", + "resourceTypes": [ + { + "resourceType": "firewalls", + "aliases": [ + { + "name": "Microsoft.Test/firewalls/rules[*].protocol", + "defaultPath": "properties.rules[*].properties.protocol", + "paths": [] + }, + { + "name": "Microsoft.Test/firewalls/rules[*].port", + "defaultPath": "properties.rules[*].properties.port", + "paths": [] + } + ] + } + ] + } + ]"#; + + let mut registry = AliasRegistry::new(); + registry.load_from_json(json1).unwrap(); + + let fw = registry.get("Microsoft.Test/firewalls").unwrap(); + assert!( + fw.sub_resource_arrays.contains("rules"), + "first load should detect 'rules' as a sub-resource array" + ); + + // Second load: same resource type, but rules[*] no longer has inner + // `properties.` wrapping → NOT a sub-resource. + let json2 = r#"[ + { + "namespace": "Microsoft.Test", + "resourceTypes": [ + { + "resourceType": "firewalls", + "aliases": [ + { + "name": "Microsoft.Test/firewalls/rules[*].protocol", + "defaultPath": "properties.rules[*].protocol", + "paths": [] + }, + { + "name": "Microsoft.Test/firewalls/rules[*].port", + "defaultPath": "properties.rules[*].port", + "paths": [] + } + ] + } + ] + } + ]"#; + + registry.load_from_json(json2).unwrap(); + + let fw2 = registry.get("Microsoft.Test/firewalls").unwrap(); + assert!( + !fw2.sub_resource_arrays.contains("rules"), + "after overwrite, 'rules' should no longer be a sub-resource array" + ); + } +} diff --git a/src/languages/azure_policy/aliases/normalizer/alias_resolution.rs b/src/languages/azure_policy/aliases/normalizer/alias_resolution.rs new file mode 100644 index 0000000..202145b --- /dev/null +++ b/src/languages/azure_policy/aliases/normalizer/alias_resolution.rs @@ -0,0 +1,96 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Per-alias path resolution: reads values from versioned ARM paths and places +//! them at alias short name paths in the normalized output. + +use alloc::string::String; + +use crate::Value; + +use super::super::obj_map::remove_element_field; +use super::super::obj_map::{ + collision_safe_key, is_root_field_collision, obj_contains, obj_insert, obj_remove, + set_nested_lowercased, ObjMap, +}; +use super::super::types::ResolvedAliases; +use super::element_remap::apply_element_remap_precomputed; +use super::flatten::normalize_value; + +/// Apply per-alias path resolution to the normalized result. +/// +/// Uses precomputed element remaps and array renames from [`ResolvedAliases`] +/// when `api_version` is `None` (the common case). Falls back to dynamic +/// computation when a specific `api_version` is provided. +pub fn apply_alias_entries( + result: &mut ObjMap, + raw: &Value, + aliases: &ResolvedAliases, + api_version: Option<&str>, +) { + let entries = &aliases.entries; + let sub_resource_set = &aliases.sub_resource_arrays; + + for (lc_key, entry) in entries { + if entry.is_wildcard { + continue; + } + + // Skip sub-resource array root entries. + if sub_resource_set.contains(lc_key.as_str()) { + continue; + } + + // Use precomputed segments for all paths (default and versioned). + let segments = entry.select_path_segments(api_version); + let value = navigate_arm_path_segments(raw, segments); + + if let Some(value) = value { + let value = normalize_value(&value, &entry.short_name, None); + + let target = if is_root_field_collision(&entry.short_name, &entry.default_path) { + collision_safe_key(&entry.short_name) + } else { + entry.short_name.clone() + }; + set_nested_lowercased(result, &target, value); + } + } + + // Look up precomputed aggregates: default or per-version. + let agg = api_version.map_or(&aliases.default_aggregates, |ver| { + let ver_lc = ver.to_ascii_lowercase(); + aliases + .versioned_aggregates + .get(&ver_lc) + .unwrap_or(&aliases.default_aggregates) + }); + + for remap in &agg.element_remaps { + apply_element_remap_precomputed(result, remap); + // Remove the original ARM field so the normalized output only has the + // alias short name. Without this, the stale source key survives and + // casing restoration during denormalization can produce a duplicate. + remove_element_field(result, &remap.array_chain, &remap.source_field); + } + + for (source_lc, target_lc) in &agg.array_renames_normalize { + if !obj_contains(result, target_lc.as_str()) { + if let Some(val) = obj_remove(result, source_lc.as_str()) { + obj_insert(result, target_lc, val); + } + } + } +} + +/// Navigate an ARM path using precomputed segments (avoids per-call split). +fn navigate_arm_path_segments(value: &Value, segments: &[String]) -> Option { + let mut current = value; + for segment in segments { + current = current + .as_object() + .ok()? + .get(&Value::from(segment.as_str()))?; + } + Some(current.clone()) +} diff --git a/src/languages/azure_policy/aliases/normalizer/element_remap.rs b/src/languages/azure_policy/aliases/normalizer/element_remap.rs new file mode 100644 index 0000000..c1b413b --- /dev/null +++ b/src/languages/azure_policy/aliases/normalizer/element_remap.rs @@ -0,0 +1,252 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Element-level field remapping for array aliases with versioned paths. + +use alloc::string::String; +use alloc::vec::Vec; + +use crate::Value; + +use super::super::obj_map::{ + obj_get, obj_get_mut, obj_insert, set_nested_in_btree, set_nested_lowercased, + set_nested_verbatim, ObjMap, +}; +use super::super::types::PrecomputedRemap; + +/// Describes a field remapping inside each element of a (possibly nested) array. +pub struct ElementRemap { + /// Chain of array navigations for nested `[*]` levels. + pub(crate) array_chain: Vec>, + /// Dot-separated path to read within the innermost array element. + pub(crate) source_field: String, + /// Dot-separated path to write within the innermost array element. + pub(crate) target_field: String, +} + +/// Apply an element-level field remap to each element of an array (or nested +/// array chain). +/// +/// When `lowercase` is `true` (normalizer), target path segments are +/// lowercased. When `false` (denormalizer), they are written verbatim +/// so that restored casing is preserved. +pub fn apply_element_remap(result: &mut ObjMap, remap: &ElementRemap, lowercase: bool) { + apply_remap_at_depth( + result, + &remap.array_chain, + 0, + &remap.source_field, + &remap.target_field, + lowercase, + ); +} + +/// Apply a precomputed element remap (from [`PrecomputedRemap`]) without +/// any per-call string splitting or allocation. +pub fn apply_element_remap_precomputed(result: &mut ObjMap, remap: &PrecomputedRemap) { + apply_remap_at_depth( + result, + &remap.array_chain, + 0, + &remap.source_field, + &remap.target_field, + true, + ); +} + +/// Recursively navigate nested arrays via `array_chain` and apply a field +/// remap in each innermost element. +fn apply_remap_at_depth( + obj: &mut ObjMap, + array_chain: &[Vec], + depth: usize, + source_field: &str, + target_field: &str, + lowercase: bool, +) { + let Some(nav) = array_chain.get(depth) else { + remap_deep_field(obj, source_field, target_field, lowercase); + return; + }; + + let first = match nav.first() { + Some(f) => f.as_str(), + None => return, + }; + + // Navigate through intermediate segments to reach the array value. + let arr_val = if nav.len() == 1 { + match obj_get_mut(obj, first) { + Some(v) => v, + None => return, + } + } else { + let mut cur: &mut Value = match obj_get_mut(obj, first) { + Some(v) => v, + None => return, + }; + for segment in nav.iter().skip(1) { + cur = match cur.as_object_mut() { + Ok(inner) => match inner.get_mut(&Value::from(segment.as_str())) { + Some(v) => v, + None => return, + }, + Err(_) => return, + }; + } + cur + }; + + if let Value::Array(elements) = arr_val { + let inner = crate::Rc::make_mut(elements); + for elem in inner.iter_mut() { + if let Value::Object(obj_rc) = elem { + let inner_btree = crate::Rc::make_mut(obj_rc); + remap_at_depth_in_btree( + inner_btree, + array_chain, + depth.saturating_add(1), + source_field, + target_field, + lowercase, + ); + } + } + } +} + +/// BTreeMap-native recursion for element-level remap, avoiding ObjMap +/// round-trips on each array element. +fn remap_at_depth_in_btree( + btree: &mut alloc::collections::BTreeMap, + array_chain: &[Vec], + depth: usize, + source_field: &str, + target_field: &str, + lowercase: bool, +) { + let Some(nav) = array_chain.get(depth) else { + remap_deep_field_in_btree(btree, source_field, target_field, lowercase); + return; + }; + + let first = match nav.first() { + Some(f) => f.as_str(), + None => return, + }; + + let key_val = Value::from(first); + let arr_val = if nav.len() == 1 { + match btree.get_mut(&key_val) { + Some(v) => v, + None => return, + } + } else { + let mut cur: &mut Value = match btree.get_mut(&key_val) { + Some(v) => v, + None => return, + }; + for segment in nav.iter().skip(1) { + cur = match cur.as_object_mut() { + Ok(inner) => match inner.get_mut(&Value::from(segment.as_str())) { + Some(v) => v, + None => return, + }, + Err(_) => return, + }; + } + cur + }; + + if let Value::Array(elements) = arr_val { + let inner = crate::Rc::make_mut(elements); + for elem in inner.iter_mut() { + if let Value::Object(obj_rc) = elem { + let inner_btree = crate::Rc::make_mut(obj_rc); + remap_at_depth_in_btree( + inner_btree, + array_chain, + depth.saturating_add(1), + source_field, + target_field, + lowercase, + ); + } + } + } +} + +/// Remap a value between dotted paths directly in a BTreeMap. +fn remap_deep_field_in_btree( + btree: &mut alloc::collections::BTreeMap, + source: &str, + target: &str, + lowercase: bool, +) { + let val = match read_dotted_path_btree(btree, source) { + Some(v) => v, + None => return, + }; + + let segments: Vec<&str> = target.split('.').collect(); + if segments.is_empty() { + return; + } + if segments.len() == 1 { + if let Some(seg) = segments.first() { + btree.insert(Value::String(crate::Rc::from(*seg)), val); + } + return; + } + set_nested_in_btree(btree, &segments, val, lowercase); +} + +/// Read a value at a dotted path from a BTreeMap. +fn read_dotted_path_btree( + btree: &alloc::collections::BTreeMap, + path: &str, +) -> Option { + let segments: Vec<&str> = path.split('.').collect(); + let first = segments.first()?; + let mut cur: &Value = btree.get(&Value::from(*first))?; + for &seg in segments.iter().skip(1) { + cur = cur.as_object().ok()?.get(&Value::from(seg))?; + } + Some(cur.clone()) +} + +/// Remap a value from one (possibly nested) dot-separated path to another +/// in an ObjMap. Used only at the top level when `depth >= array_chain.len()`. +fn remap_deep_field(obj: &mut ObjMap, source: &str, target: &str, lowercase: bool) { + let val = match read_dotted_path(obj, source) { + Some(v) => v, + None => return, + }; + + let segments: Vec<&str> = target.split('.').collect(); + if segments.is_empty() { + return; + } + if segments.len() == 1 { + if let Some(seg) = segments.first() { + obj_insert(obj, seg, val); + } + return; + } + if lowercase { + set_nested_lowercased(obj, target, val); + } else { + set_nested_verbatim(obj, target, val); + } +} + +/// Read a value at a dot-separated path from an ObjMap. +fn read_dotted_path(obj: &ObjMap, path: &str) -> Option { + let segments: Vec<&str> = path.split('.').collect(); + let first = segments.first()?; + let mut cur: &Value = obj_get(obj, first)?; + for &seg in segments.iter().skip(1) { + cur = cur.as_object().ok()?.get(&Value::from(seg))?; + } + Some(cur.clone()) +} diff --git a/src/languages/azure_policy/aliases/normalizer/flatten.rs b/src/languages/azure_policy/aliases/normalizer/flatten.rs new file mode 100644 index 0000000..37b4eab --- /dev/null +++ b/src/languages/azure_policy/aliases/normalizer/flatten.rs @@ -0,0 +1,132 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Value normalization helpers: recursive key lowercasing, sub-resource array +//! flattening, and element merging. + +use alloc::collections::BTreeSet; +use alloc::string::String; +use alloc::vec::Vec; + +use crate::Value; + +use super::super::obj_map::{make_array, make_value, new_map, obj_contains, obj_insert, val_str}; + +/// Lowercase all keys of a JSON object (shallow — values are untouched). +/// Non-object values are returned as-is. +pub fn lowercase_object_keys(value: &Value) -> Value { + match value { + Value::Object(obj) => { + let mut result = new_map(); + for (k, v) in obj.iter() { + if let Some(s) = val_str(k) { + obj_insert(&mut result, &s.to_ascii_lowercase(), v.clone()); + } + } + make_value(result) + } + _ => value.clone(), + } +} + +/// Recursively normalize a value, flattening sub-resource array elements. +pub fn normalize_value( + value: &Value, + field_path: &str, + sub_arrays: Option<&BTreeSet>, +) -> Value { + match value { + Value::Array(arr) => { + let items: Vec = if is_sub_resource_array(field_path, sub_arrays) { + arr.iter() + .map(|elem| flatten_element(elem, field_path, sub_arrays)) + .collect() + } else { + arr.iter() + .map(|elem| normalize_value(elem, field_path, sub_arrays)) + .collect() + }; + make_array(items) + } + Value::Object(obj) => { + let mut result = new_map(); + for (k, v) in obj.iter() { + let key_s = match val_str(k) { + Some(s) => s, + None => continue, + }; + let child_path = alloc::format!("{}.{}", field_path, key_s); + obj_insert( + &mut result, + &key_s.to_ascii_lowercase(), + normalize_value(v, &child_path, sub_arrays), + ); + } + make_value(result) + } + _ => value.clone(), + } +} + +/// Flatten a sub-resource array element by merging its `properties` into +/// the element root. +pub fn flatten_element( + element: &Value, + array_path: &str, + sub_arrays: Option<&BTreeSet>, +) -> Value { + let obj = match element.as_object() { + Ok(o) => o, + Err(_) => return element.clone(), + }; + + let mut result = new_map(); + + // Copy non-`properties` fields from the element envelope (keys lowercased). + for (key, val) in obj.iter() { + let key_s = match val_str(key) { + Some(s) => s, + None => continue, + }; + if key_s.eq_ignore_ascii_case("properties") { + continue; + } + let child_path = alloc::format!("{}.{}", array_path, key_s); + obj_insert( + &mut result, + &key_s.to_ascii_lowercase(), + normalize_value(val, &child_path, sub_arrays), + ); + } + + // Merge `properties` into the element (keys lowercased). + let props_val = obj + .iter() + .find(|(k, _)| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case("properties"))) + .map(|(_, v)| v); + if let Some(Value::Object(props)) = props_val { + for (key, val) in props.iter() { + let key_s = match val_str(key) { + Some(s) => s, + None => continue, + }; + let lc_key = key_s.to_ascii_lowercase(); + if obj_contains(&result, &lc_key) { + continue; + } + let child_path = alloc::format!("{}.{}", array_path, key_s); + obj_insert( + &mut result, + &lc_key, + normalize_value(val, &child_path, sub_arrays), + ); + } + } + + make_value(result) +} + +/// Check if a field path corresponds to a sub-resource array. +fn is_sub_resource_array(field_path: &str, sub_arrays: Option<&BTreeSet>) -> bool { + sub_arrays.is_some_and(|set| set.contains(&field_path.to_ascii_lowercase())) +} diff --git a/src/languages/azure_policy/aliases/normalizer/mod.rs b/src/languages/azure_policy/aliases/normalizer/mod.rs new file mode 100644 index 0000000..c577964 --- /dev/null +++ b/src/languages/azure_policy/aliases/normalizer/mod.rs @@ -0,0 +1,157 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! ARM JSON → normalized `input.resource` transformation. +//! +//! The normalizer flattens `properties` wrappers from raw ARM resource JSON so +//! that alias short names become direct paths into the normalized structure. + +mod alias_resolution; +mod element_remap; +mod flatten; + +// Re-export items used by the denormalizer. +pub(crate) use element_remap::{apply_element_remap, ElementRemap}; + +use crate::Value; + +use super::obj_map::{ + extract_type_field, make_value, new_map, obj_contains, obj_insert, val_str, ObjMap, ROOT_FIELDS, +}; +use super::types::ResolvedAliases; +use super::AliasRegistry; + +use flatten::{lowercase_object_keys, normalize_value}; + +/// Normalize a raw ARM resource JSON value into the `input.resource` structure. +/// +/// The resource type is extracted from the `type` field of `arm_resource` and +/// used to look up alias entries in the registry. +pub fn normalize( + arm_resource: &Value, + registry: Option<&AliasRegistry>, + api_version: Option<&str>, +) -> Value { + let aliases = registry.and_then(|r| extract_type_field(arm_resource).and_then(|rt| r.get(rt))); + normalize_with_aliases(arm_resource, aliases, api_version) +} + +/// Internal normalization with pre-resolved alias data. +/// +/// Core implementation used by [`normalize`] after looking up the alias +/// entries from the registry. Also used directly in unit tests. +pub fn normalize_with_aliases( + arm_resource: &Value, + aliases: Option<&ResolvedAliases>, + api_version: Option<&str>, +) -> Value { + let obj = match arm_resource.as_object() { + Ok(o) => o, + Err(_) => return arm_resource.clone(), + }; + + let sub_arrays_ref = aliases.map(|a| &a.sub_resource_arrays); + let mut result = new_map(); + + let is_data_plane = + extract_type_field(arm_resource).is_some_and(|t| t.to_ascii_lowercase().contains(".data/")); + + if is_data_plane { + for (key, val) in obj { + let key_s = match val_str(key) { + Some(s) => s, + None => continue, + }; + if key_s.eq_ignore_ascii_case("properties") { + continue; + } + let lc_key = key_s.to_ascii_lowercase(); + let val = + if key_s.eq_ignore_ascii_case("tags") || key_s.eq_ignore_ascii_case("identity") { + lowercase_object_keys(val) + } else { + normalize_value(val, key_s, sub_arrays_ref) + }; + obj_insert(&mut result, &lc_key, val); + } + merge_properties(obj, &mut result, sub_arrays_ref); + } else { + // Copy root-level fields (keys lowercased). + for &field in ROOT_FIELDS { + let found = obj + .iter() + .find(|(k, _)| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case(field))) + .map(|(_, v)| v); + if let Some(val) = found { + let val = if field.eq_ignore_ascii_case("tags") + || field.eq_ignore_ascii_case("identity") + { + // Keep these shallow to avoid lowercasing dynamic nested-map + // keys such as userAssignedIdentities member names. + lowercase_object_keys(val) + } else { + normalize_value(val, field, sub_arrays_ref) + }; + obj_insert(&mut result, &field.to_ascii_lowercase(), val); + } + } + merge_properties(obj, &mut result, sub_arrays_ref); + } + + // Per-alias path resolution. + if let Some(aliases) = aliases { + alias_resolution::apply_alias_entries(&mut result, arm_resource, aliases, api_version); + } + + make_value(result) +} + +/// Merge `properties` fields into the result map, skipping keys that already +/// exist. +fn merge_properties( + obj: &alloc::collections::BTreeMap, + result: &mut ObjMap, + sub_arrays: Option<&alloc::collections::BTreeSet>, +) { + let props_val = obj + .iter() + .find(|(k, _)| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case("properties"))) + .map(|(_, v)| v); + if let Some(Value::Object(props)) = props_val { + for (key, val) in props.iter() { + let key_s = match val_str(key) { + Some(s) => s, + None => continue, + }; + let lc_key = key_s.to_ascii_lowercase(); + if obj_contains(result, &lc_key) { + continue; + } + let normalized = normalize_value(val, key_s, sub_arrays); + obj_insert(result, &lc_key, normalized); + } + } +} + +/// Wrap a normalized resource into the full `input` envelope. +/// +/// Produces: `{ "resource": , "context": , "parameters": }` +pub fn build_input_envelope( + normalized_resource: Value, + context: Option, + parameters: Option, +) -> Value { + let mut envelope = new_map(); + obj_insert(&mut envelope, "resource", normalized_resource); + obj_insert( + &mut envelope, + "context", + context.unwrap_or_else(|| make_value(new_map())), + ); + obj_insert( + &mut envelope, + "parameters", + parameters.unwrap_or_else(|| make_value(new_map())), + ); + make_value(envelope) +} diff --git a/src/languages/azure_policy/aliases/obj_map.rs b/src/languages/azure_policy/aliases/obj_map.rs new file mode 100644 index 0000000..8e8d2bc --- /dev/null +++ b/src/languages/azure_policy/aliases/obj_map.rs @@ -0,0 +1,475 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Lightweight string-keyed map used during normalization/denormalization. +//! +//! Internally uses `hashbrown::HashMap, Value>` for O(1) lookups, +//! then converts to `Value::Object` (a `BTreeMap`) only at +//! the output boundary via [`make_value`]. + +use alloc::string::{String, ToString as _}; +use alloc::vec::Vec; + +use hashbrown::HashMap; + +use crate::Rc; +use crate::Value; + +/// A string-keyed map of JSON values. +/// +/// All normalizer / denormalizer code works with this type internally. +/// Convert to [`Value::Object`] via [`make_value`] when producing output. +pub type ObjMap = HashMap, Value>; + +/// Create an empty [`ObjMap`]. +pub fn new_map() -> ObjMap { + ObjMap::new() +} + +/// Look up a value by string key. +pub fn obj_get<'a>(map: &'a ObjMap, key: &str) -> Option<&'a Value> { + map.get(key) +} + +/// Look up a mutable value reference by string key. +pub fn obj_get_mut<'a>(map: &'a mut ObjMap, key: &str) -> Option<&'a mut Value> { + map.get_mut(key) +} + +/// Insert a key-value pair. +pub fn obj_insert(map: &mut ObjMap, key: &str, val: Value) { + map.insert(Rc::from(key), val); +} + +/// Check whether a key exists. +pub fn obj_contains(map: &ObjMap, key: &str) -> bool { + map.contains_key(key) +} + +/// Remove a key, returning its value if present. +pub fn obj_remove(map: &mut ObjMap, key: &str) -> Option { + map.remove(key) +} + +/// Convert an [`ObjMap`] into a [`Value::Object`]. +/// +/// Keys are converted from `Rc` to `Value::String` and inserted into +/// a `BTreeMap` to match the `Value::Object` representation. +pub fn make_value(map: ObjMap) -> Value { + use alloc::collections::BTreeMap; + let mut btree = BTreeMap::new(); + for (k, v) in map { + btree.insert(Value::String(k), v); + } + Value::Object(Rc::new(btree)) +} + +/// Convert a `Vec` into a `Value::Array`. +pub fn make_array(items: Vec) -> Value { + Value::Array(Rc::new(items)) +} + +/// Extract a `&str` from a `Value::String`. +pub fn val_str(v: &Value) -> Option<&str> { + match v { + Value::String(s) => Some(s.as_ref()), + _ => None, + } +} + +/// Extract the `type` field value from a resource JSON object. +/// +/// Performs a case-insensitive key lookup so both `"type"` and `"Type"` work. +pub fn extract_type_field(resource: &Value) -> Option<&str> { + resource.as_object().ok().and_then(|obj| { + obj.iter() + .find(|(k, _)| val_str(k).is_some_and(|s| s.eq_ignore_ascii_case("type"))) + .and_then(|(_, v)| val_str(v)) + }) +} + +/// Convert a `Value::Object` (BTreeMap) into an [`ObjMap`]. +/// +/// Non-string keys are silently skipped. +#[allow(dead_code)] +pub fn value_to_obj_map(value: &Value) -> Option { + let btree = value.as_object().ok()?; + let mut map = ObjMap::with_capacity(btree.len()); + for (k, v) in btree.iter() { + if let Value::String(s) = k { + map.insert(Rc::clone(s), v.clone()); + } + } + Some(map) +} + +/// Set a value at a dot-separated path in an [`ObjMap`], creating +/// intermediate `Value::Object` nodes as needed. All keys are lowercased. +pub fn set_nested_lowercased(result: &mut ObjMap, path: &str, value: Value) { + let segments: Vec<&str> = path.split('.').collect(); + if segments.is_empty() { + return; + } + if segments.len() == 1 { + if let Some(&seg) = segments.first() { + obj_insert(result, &seg.to_ascii_lowercase(), value); + } + return; + } + // Build the nested structure from inside-out. + set_nested_inner(result, &segments, value, true); +} + +/// Set a value at a dot-separated path in an [`ObjMap`], creating +/// intermediate `Value::Object` nodes as needed. Keys preserve their casing. +pub fn set_nested_verbatim(result: &mut ObjMap, path: &str, value: Value) { + let segments: Vec<&str> = path.split('.').collect(); + if segments.is_empty() { + return; + } + if segments.len() == 1 { + if let Some(&seg) = segments.first() { + obj_insert(result, seg, value); + } + return; + } + set_nested_inner(result, &segments, value, false); +} + +/// Core implementation of nested-set. Navigates the first N-1 segments, +/// creating intermediate objects, then inserts the value at the last segment. +fn set_nested_inner(obj: &mut ObjMap, segments: &[&str], value: Value, lowercase: bool) { + let Some(&first) = segments.first() else { + return; + }; + + if segments.len() == 1 { + let key = if lowercase { + first.to_ascii_lowercase() + } else { + first.to_string() + }; + obj_insert(obj, &key, value); + return; + } + + let seg = if lowercase { + first.to_ascii_lowercase() + } else { + first.to_string() + }; + + // Ensure an intermediate object exists at `seg`. + if !obj_contains(obj, &seg) { + obj_insert(obj, &seg, make_value(new_map())); + } + + // Descend directly into the BTreeMap, avoiding ObjMap round-trip. + if let Some(Value::Object(inner_rc)) = obj_get_mut(obj, &seg) { + let inner_btree = Rc::make_mut(inner_rc); + set_nested_in_btree( + inner_btree, + segments.get(1..).unwrap_or_default(), + value, + lowercase, + ); + } +} + +/// Set a value at a path directly in a `BTreeMap`, creating +/// intermediate `Value::Object` nodes as needed. +/// +/// This avoids the `btree_to_obj_map` / `obj_map_to_btree` round-trip that +/// would clone every sibling entry at each nesting level. +pub fn set_nested_in_btree( + btree: &mut alloc::collections::BTreeMap, + segments: &[&str], + value: Value, + lowercase: bool, +) { + let Some(&first) = segments.first() else { + return; + }; + + let key_str: String = if lowercase { + first.to_ascii_lowercase() + } else { + first.to_string() + }; + let key_val = Value::String(Rc::from(key_str.as_str())); + + if segments.len() == 1 { + btree.insert(key_val, value); + return; + } + + // Ensure an intermediate object exists. + if !btree.contains_key(&key_val) { + btree.insert(key_val.clone(), make_value(new_map())); + } + + if let Some(Value::Object(inner_rc)) = btree.get_mut(&key_val) { + let inner = Rc::make_mut(inner_rc); + set_nested_in_btree( + inner, + segments.get(1..).unwrap_or_default(), + value, + lowercase, + ); + } +} + +/// Fields that exist at the ARM resource root (not under `properties`). +/// +/// These are the standard ARM resource envelope fields as defined by the +/// Azure Resource Manager resource model. They are preserved at the +/// resource root during normalization and denormalization. +pub const ROOT_FIELDS: &[&str] = &[ + "name", + "type", + "location", + "kind", + "id", + "tags", + "identity", + "sku", + "plan", + "zones", + "managedBy", + "etag", + "apiVersion", + "fullName", + "systemData", + "extendedLocation", +]; + +/// Check whether an alias short name collides with a reserved ARM root field +/// and needs a collision-safe key. +pub fn is_root_field_collision(short_name: &str, default_path: &str) -> bool { + ROOT_FIELDS + .iter() + .any(|f| f.eq_ignore_ascii_case(short_name)) + && default_path.to_ascii_lowercase().starts_with("properties.") +} + +/// Return a collision-safe key for an alias whose short name collides with a +/// root ARM field. The key is `_p_` + the lowercased short name. +pub fn collision_safe_key(short_name: &str) -> String { + alloc::format!("_p_{}", short_name.to_ascii_lowercase()) +} + +// ─── Element-level field removal ──────────────────────────────────────────── +// +// Shared by both normalizer (stale source cleanup after remap) and +// denormalizer (cleanup after reverse remap). + +/// Remove a (possibly dot-separated) field from each element of a (possibly +/// nested) array, navigating via the given `array_chain`. +pub fn remove_element_field(obj: &mut ObjMap, array_chain: &[Vec], field: &str) { + remove_field_at_depth(obj, array_chain, 0, field); +} + +fn remove_field_at_depth(obj: &mut ObjMap, array_chain: &[Vec], depth: usize, field: &str) { + let Some(nav) = array_chain.get(depth) else { + let segments: Vec<&str> = field.split('.').collect(); + if segments.len() == 1 { + if let Some(&seg) = segments.first() { + obj_remove(obj, seg); + } + } else if segments.len() > 1 { + remove_at_dotted_path(obj, &segments); + } + return; + }; + + let first = match nav.first() { + Some(f) => f.as_str(), + None => return, + }; + + let arr_val = if nav.len() == 1 { + match obj_get_mut(obj, first) { + Some(v) => v, + None => return, + } + } else { + let mut cur: &mut Value = match obj_get_mut(obj, first) { + Some(v) => v, + None => return, + }; + for segment in nav.iter().skip(1) { + cur = match cur.as_object_mut() { + Ok(inner) => match inner.get_mut(&Value::from(segment.as_str())) { + Some(v) => v, + None => return, + }, + Err(_) => return, + }; + } + cur + }; + + if let Value::Array(elements) = arr_val { + let inner = Rc::make_mut(elements); + for elem in inner.iter_mut() { + if let Value::Object(obj_rc) = elem { + let inner_btree = Rc::make_mut(obj_rc); + remove_field_at_depth_in_btree( + inner_btree, + array_chain, + depth.saturating_add(1), + field, + ); + } + } + } +} + +/// BTreeMap-native recursion for element-level field removal. +fn remove_field_at_depth_in_btree( + btree: &mut alloc::collections::BTreeMap, + array_chain: &[Vec], + depth: usize, + field: &str, +) { + let Some(nav) = array_chain.get(depth) else { + let segments: Vec<&str> = field.split('.').collect(); + if segments.len() == 1 { + if let Some(&seg) = segments.first() { + btree.remove(&Value::from(seg)); + } + } else if segments.len() > 1 { + remove_at_dotted_path_in_btree(btree, &segments); + } + return; + }; + + let first = match nav.first() { + Some(f) => f.as_str(), + None => return, + }; + + let key_val = Value::from(first); + let arr_val = if nav.len() == 1 { + match btree.get_mut(&key_val) { + Some(v) => v, + None => return, + } + } else { + let mut cur: &mut Value = match btree.get_mut(&key_val) { + Some(v) => v, + None => return, + }; + for segment in nav.iter().skip(1) { + cur = match cur.as_object_mut() { + Ok(inner) => match inner.get_mut(&Value::from(segment.as_str())) { + Some(v) => v, + None => return, + }, + Err(_) => return, + }; + } + cur + }; + + if let Value::Array(elements) = arr_val { + let inner = Rc::make_mut(elements); + for elem in inner.iter_mut() { + if let Value::Object(obj_rc) = elem { + let inner_btree = Rc::make_mut(obj_rc); + remove_field_at_depth_in_btree( + inner_btree, + array_chain, + depth.saturating_add(1), + field, + ); + } + } + } +} + +/// Remove the leaf segment at a dotted path directly in a BTreeMap. +fn remove_at_dotted_path_in_btree( + btree: &mut alloc::collections::BTreeMap, + segments: &[&str], +) { + let Some((&leaf, parent_segs)) = segments.split_last() else { + return; + }; + if parent_segs.is_empty() { + btree.remove(&Value::from(leaf)); + return; + } + + let Some(&first) = parent_segs.first() else { + return; + }; + let first_key = Value::from(first); + let parent_val = match btree.get_mut(&first_key) { + Some(v) => v, + None => return, + }; + + if parent_segs.len() == 1 { + if let Value::Object(inner_rc) = parent_val { + let inner_btree = Rc::make_mut(inner_rc); + inner_btree.remove(&Value::from(leaf)); + } + } else { + let mut cur = parent_val; + for &seg in parent_segs.iter().skip(1) { + cur = match cur.as_object_mut() { + Ok(inner) => match inner.get_mut(&Value::from(seg)) { + Some(v) => v, + None => return, + }, + Err(_) => return, + }; + } + if let Value::Object(inner_rc) = cur { + let inner_btree = Rc::make_mut(inner_rc); + inner_btree.remove(&Value::from(leaf)); + } + } +} + +/// Remove the leaf segment at a dot-separated path from an ObjMap. +fn remove_at_dotted_path(obj: &mut ObjMap, segments: &[&str]) { + let Some((&leaf, parent_segs)) = segments.split_last() else { + return; + }; + if parent_segs.is_empty() { + obj_remove(obj, leaf); + return; + } + + let Some(&first) = parent_segs.first() else { + return; + }; + let parent_val = match obj_get_mut(obj, first) { + Some(v) => v, + None => return, + }; + + if parent_segs.len() == 1 { + if let Value::Object(inner_rc) = parent_val { + let inner_btree = Rc::make_mut(inner_rc); + inner_btree.remove(&Value::from(leaf)); + } + } else { + let mut cur = parent_val; + for &seg in parent_segs.iter().skip(1) { + cur = match cur.as_object_mut() { + Ok(inner) => match inner.get_mut(&Value::from(seg)) { + Some(v) => v, + None => return, + }, + Err(_) => return, + }; + } + if let Value::Object(inner_rc) = cur { + let inner_btree = Rc::make_mut(inner_rc); + inner_btree.remove(&Value::from(leaf)); + } + } +} diff --git a/src/languages/azure_policy/aliases/types.rs b/src/languages/azure_policy/aliases/types.rs new file mode 100644 index 0000000..cdab367 --- /dev/null +++ b/src/languages/azure_policy/aliases/types.rs @@ -0,0 +1,686 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! Data types for Azure Policy alias definitions. +//! +//! These types deserialize production alias catalog data from multiple sources: +//! 1. ARM API response: `GET /providers?$expand=resourceTypes/aliases` +//! 2. Static `ResourceTypesAndAliases.json` (used by PolicyTester) +//! 3. `az provider list --expand resourceTypes/aliases` CLI output +//! (where `defaultPath` may be serialized as `{ path, apiVersions }`) +//! +//! All data is captured for completeness; fields not yet used by the compiler +//! are retained so the types stay in sync with the production schema. + +use alloc::collections::{BTreeMap, BTreeSet}; +use alloc::string::String; +use alloc::vec::Vec; + +use serde::{Deserialize, Deserializer}; + +// ─── Top-level response wrappers ──────────────────────────────────────────── + +/// ARM API response envelope: `{ "value": [...] }` +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct ArmProvidersResponse { + pub value: Vec, + /// Pagination link (ARM may paginate large responses). + #[serde(rename = "nextLink", default)] + pub next_link: Option, +} + +// ─── Provider / resource type ─────────────────────────────────────────────── + +/// A resource provider's alias definitions. +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct ProviderAliases { + /// The provider namespace (e.g., `"Microsoft.Storage"`). + pub namespace: String, + + /// Resource types with their alias entries. + #[serde(default, rename = "resourceTypes")] + pub resource_types: Vec, +} + +/// Aliases for a single resource type within a provider. +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct ResourceTypeAliases { + /// The resource type name (e.g., `"storageAccounts"`). + pub resource_type: String, + + /// All alias entries for this resource type. + #[serde(default)] + pub aliases: Vec, + + /// Resource capabilities as a comma-separated string + /// (e.g., `"SupportsTags, SupportsLocation"`). + #[serde(default)] + pub capabilities: Option, + + /// Default API version for the resource type. + #[serde(default)] + pub default_api_version: Option, +} + +// ─── Alias ────────────────────────────────────────────────────────────────── + +/// A single alias entry within a resource type. +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AliasEntry { + /// Fully qualified alias name + /// (e.g., `"Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"`). + pub name: String, + + /// The default ARM JSON path used when no versioned path matches. + /// + /// In most formats this is a plain string. The `az CLI` format serializes + /// it as `{ "path": "...", "apiVersions": [...] }`. The custom + /// deserializer accepts both, extracting just the path string. + #[serde(default, deserialize_with = "deserialize_default_path")] + pub default_path: Option, + + /// Optional metadata for the default path (type, modifiability). + #[serde(default)] + pub default_metadata: Option, + + /// Extraction pattern for the default path (present in ARM responses for + /// some aliases; absent from the static file). + #[serde(default)] + pub default_pattern: Option, + + /// Alias-level type as a comma-separated string of flags: + /// `"PlainText"`, `"Mask"`, `"Deprecated"`, `"Preview"`, or combinations + /// like `"Mask, Deprecated"`. `None` when absent. + #[serde(default, rename = "type")] + pub alias_type: Option, + + /// Versioned path entries. Empty for the vast majority of aliases that + /// have only a `defaultPath`. + #[serde(default)] + pub paths: Vec, +} + +// ─── Alias path ───────────────────────────────────────────────────────────── + +/// A versioned path mapping for an alias. +/// +/// When an alias maps to different ARM JSON paths across API versions, each +/// distinct path is recorded as an `AliasPath` entry. +#[derive(Debug, Clone, Default, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AliasPath { + /// The ARM JSON path (e.g., `"properties.encryption.services.blob.enabled"`). + pub path: String, + + /// API versions for which this path is valid. Empty means all versions. + #[serde(default)] + pub api_versions: Vec, + + /// Optional per-version metadata. + #[serde(default)] + pub metadata: Option, + + /// Extraction pattern for this specific path. + #[serde(default)] + pub pattern: Option, +} + +// ─── Alias path metadata ─────────────────────────────────────────────────── + +/// Metadata associated with an alias path (default or versioned). +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct AliasPathMetadata { + /// The data type of the alias value as a string token + /// (e.g., `"String"`, `"Integer"`, `"Boolean"`, `"Array"`, `"Object"`). + #[serde(rename = "type")] + pub kind: Option, + + /// Attribute flags as a comma-separated string + /// (e.g., `"Modifiable"`, `"Modifiable, SupportsCreate, SupportsRead"`). + pub attributes: Option, +} + +// ─── Alias pattern ────────────────────────────────────────────────────────── + +/// Extraction pattern for an alias path (URI template or regex). +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AliasPattern { + /// The pattern phrase (URI template or regex string). + pub phrase: String, + + /// The variable to extract from the pattern. + #[serde(default)] + pub variable: Option, + + /// Pattern type (e.g., `"Extract"`). + #[serde(default, rename = "type")] + pub pattern_type: Option, +} + +// ─── Custom deserializer: defaultPath (string or object) ──────────────────── + +/// Accepts either a plain string `"properties.foo"` or an az CLI object +/// `{ "path": "properties.foo", "apiVersions": [...] }`, extracting just the +/// path string. Handles `null` gracefully. +fn deserialize_default_path<'de, D>(deserializer: D) -> Result, D::Error> +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + #[serde(untagged)] + enum RawDefaultPath { + Str(String), + Obj { + path: String, + #[serde(default, rename = "apiVersions")] + _api_versions: Vec, + }, + Null, + } + + match Option::::deserialize(deserializer)? { + None | Some(RawDefaultPath::Null) => Ok(None), + Some(RawDefaultPath::Str(s)) => Ok(Some(s)), + Some(RawDefaultPath::Obj { path, .. }) => Ok(Some(path)), + } +} + +// ─── Data Policy Manifest types (data-plane aliases) ──────────────────────── + +/// A single alias entry in a data policy manifest. +/// +/// Unlike control-plane [`AliasEntry`], data-plane aliases have no +/// `defaultPath` — the path is always taken from `paths[0].path`. +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +pub struct DataManifestAlias { + /// Fully qualified alias name + /// (e.g., `"Microsoft.KeyVault.Data/vaults/certificates/attributes.expiresOn"`). + pub name: String, + + /// Versioned path entries. `paths[0].path` serves as the default path. + #[serde(default)] + pub paths: Vec, +} + +/// A path entry in a data policy manifest alias. +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct DataManifestAliasPath { + /// The ARM JSON path (e.g., `"attributes.expiresOn"`). + pub path: String, + + /// API versions for which this path is valid. + #[serde(default)] + pub api_versions: Vec, + + /// Schema versions for which this path is valid (used by some data-plane + /// providers instead of `apiVersions`). + #[serde(default)] + pub schema_versions: Vec, +} + +/// Per-resource-type alias group in a data policy manifest. +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct DataManifestResourceTypeAliases { + /// Resource type suffix (e.g., `"vaults/certificates"`). + pub resource_type: String, + + /// Aliases for this resource type. + #[serde(default)] + pub aliases: Vec, +} + +/// A data policy manifest describing data-plane aliases for a namespace. +/// +/// This format is used by `dataPolicyManifests/` files, as opposed to the +/// control-plane `ProviderAliases` format used by `Get-AzPolicyAlias`. +#[derive(Debug, Clone, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct DataPolicyManifest { + /// The data namespace (e.g., `"Microsoft.KeyVault.Data"`). + pub data_namespace: String, + + /// Top-level aliases not scoped to a specific resource type. + #[serde(default)] + pub aliases: Vec, + + /// Per-resource-type alias groups. + #[serde(default)] + pub resource_type_aliases: Vec, +} + +// ─── Convenience loading functions ────────────────────────────────────────── + +/// Load from the static `ResourceTypesAndAliases.json` file (bare array). +pub fn load_from_static_file(json: &str) -> Result, serde_json::Error> { + serde_json::from_str(json) +} + +/// Load from an ARM API `GET /providers` response (`{ "value": [...] }`). +pub fn load_from_arm_response(json: &str) -> Result, serde_json::Error> { + let resp: ArmProvidersResponse = serde_json::from_str(json)?; + Ok(resp.value) +} + +/// Load from either format: tries ARM envelope first, then bare array. +pub fn load_auto(json: &str) -> Result, serde_json::Error> { + let trimmed = json.trim_start(); + if trimmed.starts_with('[') { + load_from_static_file(json) + } else { + load_from_arm_response(json) + } +} + +// ─── Utility methods ──────────────────────────────────────────────────────── + +impl AliasEntry { + /// Returns `true` if this alias is marked as deprecated (case-insensitive). + pub fn is_deprecated(&self) -> bool { + has_flag(self.alias_type.as_deref(), "Deprecated") + } + + /// Returns `true` if this alias is marked as preview. + pub fn is_preview(&self) -> bool { + has_flag(self.alias_type.as_deref(), "Preview") + } + + /// Returns `true` if this alias's value should be masked (secret). + pub fn is_secret(&self) -> bool { + has_flag(self.alias_type.as_deref(), "Mask") + } + + /// Returns the effective path string (defaultPath or first versioned path). + pub fn effective_path(&self) -> Option<&str> { + self.default_path + .as_deref() + .or_else(|| self.paths.first().map(|p| p.path.as_str())) + } +} + +impl AliasPathMetadata { + /// Returns `true` if this path supports modification (Modifiable flag). + pub fn is_modifiable(&self) -> bool { + has_flag(self.attributes.as_deref(), "Modifiable") + } +} + +/// Check whether a comma-separated flags string contains a specific flag +/// (case-insensitive). +pub(crate) fn has_flag(flags: Option<&str>, flag: &str) -> bool { + flags.is_some_and(|s| { + s.split(',') + .any(|part| part.trim().eq_ignore_ascii_case(flag)) + }) +} + +/// Parsed alias data for a single resource type, keyed by short name. +/// +/// The short name is derived by stripping the resource type prefix from the +/// fully qualified alias name: +/// `Microsoft.Storage/storageAccounts/sku.name` → `sku.name` +#[derive(Debug, Clone)] +pub struct ResolvedAliases { + /// Fully qualified resource type (e.g., `"Microsoft.Storage/storageAccounts"`). + pub resource_type: String, + /// Map from alias short name (case-insensitive key, stored lowercase) to + /// the resolved ARM path. + pub entries: BTreeMap, + /// Array field names whose elements are sub-resources (have their own + /// `properties` wrapper to flatten). Stored pre-lowercased so consumers + /// can look up directly without per-call allocation. + pub sub_resource_arrays: BTreeSet, + + // ── Precomputed aggregate fields ──────────────────────────────────── + /// Precomputed aggregates for the default path (api_version = None). + pub default_aggregates: VersionedAggregates, + /// Precomputed aggregates keyed by lowercase api_version string. + /// Computed at registry-load time for every distinct version found in + /// any entry's `versioned_paths`. + pub versioned_aggregates: BTreeMap, +} + +/// Precomputed aggregate data for a specific API version, or for the default path. +/// +/// Stored at [`ResolvedAliases`] level so it is computed once at registry-load +/// time rather than reconstructed on every normalize/denormalize call. +#[derive(Debug, Clone, Default, PartialEq, Eq)] +pub struct VersionedAggregates { + /// Precomputed element-level field remaps for wildcard aliases. + pub element_remaps: Vec, + /// Precomputed reverse element remaps for denormalization. + pub reverse_element_remaps: Vec, + /// Deduplicated array base renames for normalization: `(arm_base_lc, short_base_lc)`. + pub array_renames_normalize: Vec<(String, String)>, + /// Deduplicated array base renames for denormalization: `(short_base_lc, arm_base)`. + pub array_renames_denormalize: Vec<(String, String)>, +} + +/// A precomputed element-level field remap, stored at `ResolvedAliases` level +/// so it's computed once at registry-load time rather than per-call. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PrecomputedRemap { + /// Chain of array navigations for nested `[*]` levels. + pub array_chain: Vec>, + /// Field to read within each element. + pub source_field: String, + /// Field to write within each element. + pub target_field: String, +} + +/// A precomputed reverse remap for denormalization, including the forward +/// target field name for cleanup after remapping. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PrecomputedReverseRemap { + /// Chain of array navigations for nested `[*]` levels. + pub array_chain: Vec>, + /// Field to read within each element (was the forward target). + pub source_field: String, + /// Field to write within each element (was the forward source). + pub target_field: String, + /// The forward target field to remove after remapping. + pub cleanup_field: String, +} + +/// A resolved alias entry with its default path and optional versioned paths. +#[derive(Debug, Clone)] +pub struct ResolvedEntry { + /// The original-cased alias short name (e.g., `"accountType"`, not + /// `"accounttype"`). The entries map uses lowercase keys for + /// case-insensitive lookup, but the normalizer needs the original casing + /// to write values at correctly-cased paths in the output. + pub short_name: String, + /// The default ARM JSON path. + pub default_path: String, + /// Versioned path overrides: `(api_version, arm_path)` pairs. + pub versioned_paths: Vec<(String, String)>, + /// Optional metadata from the alias catalog (type, modifiability). + pub metadata: Option, + + // ── Precomputed fields (derived at registry-load time) ────────────── + /// Whether `short_name` contains `[*]` (i.e., this is a wildcard/array alias). + pub is_wildcard: bool, + /// Precomputed `default_path.split('.').collect()` for fast ARM path navigation. + pub default_path_segments: Vec, + /// Precomputed path segments for each versioned path, in the same order + /// as `versioned_paths`. + pub versioned_path_segments: Vec>, +} + +impl ResolvedEntry { + /// Build a `ResolvedEntry` and precompute derived fields. + pub fn new( + short_name: String, + default_path: String, + versioned_paths: Vec<(String, String)>, + metadata: Option, + ) -> Self { + let is_wildcard = short_name.contains("[*]"); + let default_path_segments = default_path.split('.').map(String::from).collect(); + let versioned_path_segments = versioned_paths + .iter() + .map(|(_, p)| p.split('.').map(String::from).collect()) + .collect(); + Self { + short_name, + default_path, + versioned_paths, + metadata, + is_wildcard, + default_path_segments, + versioned_path_segments, + } + } + + /// Select the ARM path for a given API version. + /// + /// If `api_version` is `Some` and matches a versioned path, returns that + /// path. Otherwise returns the `default_path`. + pub fn select_path(&self, api_version: Option<&str>) -> &str { + if let Some(ver) = api_version { + for (v, path) in &self.versioned_paths { + if v.eq_ignore_ascii_case(ver) { + return path; + } + } + } + &self.default_path + } + + /// Select pre-tokenized path segments for a given API version. + /// + /// Returns the versioned segments if `api_version` matches, otherwise + /// the default segments. This avoids per-call `split('.')` for both + /// default and versioned scalar alias navigation. + pub fn select_path_segments(&self, api_version: Option<&str>) -> &[String] { + if let Some(ver) = api_version { + for (i, (v, _)) in self.versioned_paths.iter().enumerate() { + if v.eq_ignore_ascii_case(ver) { + if let Some(segs) = self.versioned_path_segments.get(i) { + return segs; + } + } + } + } + &self.default_path_segments + } +} + +#[cfg(test)] +#[allow(clippy::indexing_slicing, clippy::unwrap_used)] +mod tests { + use alloc::string::ToString as _; + use alloc::vec; + + use super::*; + + fn make_entry(short: &str, default: &str, versioned: Vec<(&str, &str)>) -> ResolvedEntry { + ResolvedEntry::new( + short.to_string(), + default.to_string(), + versioned + .into_iter() + .map(|(v, p)| (v.to_string(), p.to_string())) + .collect(), + None, + ) + } + + #[test] + fn select_path_no_version_returns_default() { + let entry = make_entry( + "enabled", + "properties.enabled", + vec![("2020-01-01", "properties.isEnabled")], + ); + assert_eq!(entry.select_path(None), "properties.enabled"); + } + + #[test] + fn select_path_matching_version() { + let entry = make_entry( + "enabled", + "properties.enabled", + vec![ + ("2020-01-01", "properties.isEnabled"), + ("2021-06-01", "properties.enabled"), + ], + ); + assert_eq!( + entry.select_path(Some("2020-01-01")), + "properties.isEnabled" + ); + } + + #[test] + fn select_path_no_matching_version_returns_default() { + let entry = make_entry( + "enabled", + "properties.enabled", + vec![("2020-01-01", "properties.isEnabled")], + ); + assert_eq!(entry.select_path(Some("9999-01-01")), "properties.enabled"); + } + + #[test] + fn select_path_case_insensitive_version() { + let entry = make_entry( + "enabled", + "properties.enabled", + vec![("2020-01-01-Preview", "properties.isEnabled")], + ); + assert_eq!( + entry.select_path(Some("2020-01-01-preview")), + "properties.isEnabled" + ); + } + + #[test] + fn select_path_empty_versioned_paths() { + let entry = make_entry("enabled", "properties.enabled", vec![]); + assert_eq!(entry.select_path(Some("2020-01-01")), "properties.enabled"); + } + + #[test] + fn deserialize_provider_aliases() { + let json = r#"{ + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/sku.name", + "defaultPath": "sku.name", + "defaultMetadata": { "type": "String", "attributes": "Modifiable" }, + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/accessTier", + "defaultPath": "properties.accessTier", + "paths": [ + { + "path": "properties.accessTier", + "apiVersions": ["2021-01-01", "2020-08-01-preview"], + "metadata": { "type": "String" } + } + ] + } + ] + } + ] + }"#; + + let provider: ProviderAliases = serde_json::from_str(json).unwrap(); + assert_eq!(provider.namespace, "Microsoft.Storage"); + assert_eq!(provider.resource_types.len(), 1); + + let rt = &provider.resource_types[0]; + assert_eq!(rt.resource_type, "storageAccounts"); + assert_eq!(rt.aliases.len(), 2); + + let sku_alias = &rt.aliases[0]; + assert_eq!(sku_alias.default_path.as_deref(), Some("sku.name")); + assert!(sku_alias.paths.is_empty()); + + let access_alias = &rt.aliases[1]; + assert_eq!(access_alias.paths.len(), 1); + assert_eq!(access_alias.paths[0].api_versions.len(), 2); + } + + #[test] + fn deserialize_alias_metadata() { + let json = r#"{ + "name": "test/alias", + "defaultPath": "properties.value", + "defaultMetadata": { "type": "Integer", "attributes": "None" }, + "paths": [] + }"#; + let entry: AliasEntry = serde_json::from_str(json).unwrap(); + let meta = entry.default_metadata.unwrap(); + assert_eq!(meta.kind.as_deref(), Some("Integer")); + assert_eq!(meta.attributes.as_deref(), Some("None")); + } + + #[test] + fn deserialize_az_cli_default_path_object() { + let json = r#"{ + "name": "Microsoft.Compute/virtualMachines/sku.name", + "defaultPath": { + "path": "properties.hardwareProfile.vmSize", + "apiVersions": ["2024-07-01"] + }, + "paths": [] + }"#; + let entry: AliasEntry = serde_json::from_str(json).unwrap(); + assert_eq!( + entry.default_path.as_deref(), + Some("properties.hardwareProfile.vmSize") + ); + } + + #[test] + fn alias_type_flags() { + let json = r#"{ + "name": "test", + "type": "Mask, Deprecated", + "defaultMetadata": { "type": "String", "attributes": "Modifiable, SupportsCreate" }, + "paths": [] + }"#; + let entry: AliasEntry = serde_json::from_str(json).unwrap(); + assert!(entry.is_secret()); + assert!(entry.is_deprecated()); + assert!(!entry.is_preview()); + assert!(entry.default_metadata.as_ref().unwrap().is_modifiable()); + } + + #[test] + fn load_auto_detects_format() { + let array_json = r#"[{"namespace":"N","resourceTypes":[]}]"#; + let arm_json = r#"{"value":[{"namespace":"N","resourceTypes":[]}]}"#; + assert_eq!(load_auto(array_json).unwrap()[0].namespace, "N"); + assert_eq!(load_auto(arm_json).unwrap()[0].namespace, "N"); + } + + #[test] + fn deserialize_pattern() { + let json = r#"{ + "name": "test", + "defaultPath": "p", + "paths": [{ + "path": "p", + "apiVersions": ["2020-01-01"], + "pattern": { + "phrase": "/Subscriptions/{sub}/Providers/{prov}", + "variable": "prov", + "type": "Extract" + } + }] + }"#; + let entry: AliasEntry = serde_json::from_str(json).unwrap(); + let pattern = entry.paths[0].pattern.as_ref().unwrap(); + assert_eq!(pattern.pattern_type.as_deref(), Some("Extract")); + assert_eq!(pattern.variable.as_deref(), Some("prov")); + } + + #[test] + fn capabilities_preserved() { + let json = r#"{ + "namespace": "NS", + "resourceTypes": [{ + "resourceType": "rt", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [] + }] + }"#; + let provider: ProviderAliases = serde_json::from_str(json).unwrap(); + assert_eq!( + provider.resource_types[0].capabilities.as_deref(), + Some("SupportsTags, SupportsLocation") + ); + } +} diff --git a/src/languages/azure_policy/mod.rs b/src/languages/azure_policy/mod.rs index 477ea7c..f2256fa 100644 --- a/src/languages/azure_policy/mod.rs +++ b/src/languages/azure_policy/mod.rs @@ -3,4 +3,6 @@ //! Azure Policy language support. +#[allow(clippy::pattern_type_mismatch)] +pub mod aliases; pub mod strings; diff --git a/src/languages/mod.rs b/src/languages/mod.rs deleted file mode 100644 index ecc421a..0000000 --- a/src/languages/mod.rs +++ /dev/null @@ -1,7 +0,0 @@ -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License. - -//! Language-specific modules for specialized parsing and evaluation - -#[cfg(feature = "azure-rbac")] -pub mod azure_rbac; \ No newline at end of file diff --git a/tests/azure_policy/aliases/test_aliases.json b/tests/azure_policy/aliases/test_aliases.json new file mode 100644 index 0000000..de1f12c --- /dev/null +++ b/tests/azure_policy/aliases/test_aliases.json @@ -0,0 +1,2900 @@ +[ + { + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/isHnsEnabled", + "defaultPath": "properties.isHnsEnabled", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/customDomain.name", + "defaultPath": "properties.customDomain.name", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/customDomain.useSubDomainName", + "defaultPath": "properties.customDomain.useSubDomainName", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/customDomain", + "defaultPath": "properties.customDomain", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.resourceType", + "defaultPath": "sku.resourceType", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.kind", + "defaultPath": "sku.kind", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.locations", + "defaultPath": "sku.locations", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.capabilities", + "defaultPath": "sku.capabilities", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.locations[*]", + "defaultPath": "sku.locations[*]", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.capabilities[*].name", + "defaultPath": "sku.capabilities[*].name", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.capabilities[*]", + "defaultPath": "sku.capabilities[*]", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.restrictions[*].type", + "defaultPath": "sku.restrictions[*].type", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.restrictions[*].values[*]", + "defaultPath": "sku.restrictions[*].values[*]", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/accessTier", + "defaultPath": "properties.accessTier", + "paths": [ + { + "path": "properties.accessTier", + "apiVersions": [ + "2018-11-01", + "2018-11-09", + "2018-07-01", + "2018-03-01-Preview", + "2018-02-01", + "2017-10-01", + "2017-06-01", + "2016-12-01", + "2016-05-01", + "2016-01-01", + "2015-06-15", + "2015-05-01-preview" + ] + } + ] + }, + { + "name": "Microsoft.Storage/storageAccounts/enableBlobEncryption", + "defaultPath": "properties.encryption.services.blob.enabled", + "paths": [ + { + "path": "properties.encryption.services.blob.enabled", + "apiVersions": [ + "2018-11-01", + "2018-11-09", + "2018-07-01", + "2018-03-01-Preview", + "2018-02-01", + "2017-10-01", + "2017-06-01", + "2016-12-01", + "2016-05-01", + "2016-01-01", + "2015-06-15", + "2015-05-01-preview" + ] + } + ] + }, + { + "name": "Microsoft.Storage/storageAccounts/enableFileEncryption", + "defaultPath": "properties.encryption.services.file.enabled", + "paths": [ + { + "path": "properties.encryption.services.file.enabled", + "apiVersions": [ + "2018-11-01", + "2018-11-09", + "2018-07-01", + "2018-03-01-Preview", + "2018-02-01", + "2017-10-01", + "2017-06-01", + "2016-12-01", + "2016-05-01", + "2016-01-01", + "2015-06-15", + "2015-05-01-preview" + ] + } + ] + }, + { + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Boolean" + }, + "paths": [ + { + "path": "properties.supportsHttpsTrafficOnly", + "apiVersions": [ + "2018-11-09", + "2018-03-01-Preview", + "2016-05-01", + "2016-01-01", + "2015-06-15", + "2015-05-01-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.Storage/storageAccounts/accountType", + "defaultPath": "properties.accountType", + "paths": [ + { + "path": "properties.accountType", + "apiVersions": [ + "2015-06-15", + "2015-05-01-preview" + ] + }, + { + "path": "sku.name", + "apiVersions": [ + "2018-11-01", + "2018-11-09", + "2018-07-01", + "2018-03-01-Preview", + "2018-02-01", + "2017-10-01", + "2017-06-01", + "2016-12-01", + "2016-05-01", + "2016-01-01" + ] + } + ] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.name", + "defaultPath": "sku.name", + "paths": [ + { + "path": "properties.accountType", + "apiVersions": [ + "2015-06-15", + "2015-05-01-preview" + ] + }, + { + "path": "sku.name", + "apiVersions": [ + "2018-11-01", + "2018-11-09", + "2018-07-01", + "2018-03-01-Preview", + "2018-02-01", + "2017-10-01", + "2017-06-01", + "2016-12-01", + "2016-05-01", + "2016-01-01" + ] + } + ] + }, + { + "name": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction", + "defaultPath": "properties.networkAcls.defaultAction", + "paths": [], + "defaultMetadata": { + "attributes": "Modifiable" + } + }, + { + "name": "Microsoft.Storage/storageAccounts/networkAcls.ipRules[*]", + "defaultPath": "properties.networkAcls.ipRules[*]", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/networkAcls.ipRules[*].value", + "defaultPath": "properties.networkAcls.ipRules[*].value", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/networkAcls.ipRules[*].action", + "defaultPath": "properties.networkAcls.ipRules[*].action", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/minimumTlsVersion", + "defaultPath": "properties.minimumTlsVersion", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/primaryEndpoints.blob", + "defaultPath": "properties.primaryEndpoints.blob", + "paths": [], + "defaultMetadata": { + "attributes": "None" + } + }, + { + "name": "Microsoft.Storage/storageAccounts/networkAcls.resourceAccessRules[*]", + "defaultPath": "properties.networkAcls.resourceAccessRules[*]", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/networkAcls.virtualNetworkRules[*]", + "defaultPath": "properties.networkAcls.virtualNetworkRules[*]", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/networkAcls.virtualNetworkRules[*].id", + "defaultPath": "properties.networkAcls.virtualNetworkRules[*].id", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/networkAcls.bypass", + "defaultPath": "properties.networkAcls.bypass", + "paths": [], + "defaultMetadata": { + "attributes": "Modifiable" + } + }, + { + "name": "Microsoft.Storage/storageAccounts/allowBlobPublicAccess", + "defaultPath": "properties.allowBlobPublicAccess", + "paths": [], + "defaultMetadata": { + "attributes": "Modifiable" + } + } + ] + } + ] + }, + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules", + "defaultPath": "properties.securityRules", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Array" + }, + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/defaultSecurityRules", + "defaultPath": "properties.defaultSecurityRules", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/networkInterfaces", + "defaultPath": "properties.networkInterfaces", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/subnets", + "defaultPath": "properties.subnets", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/resourceGuid", + "defaultPath": "properties.resourceGuid", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/provisioningState", + "defaultPath": "properties.provisioningState", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/flowLogs", + "defaultPath": "properties.flowLogs", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/flushConnection", + "defaultPath": "properties.flushConnection", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].provisioningState", + "defaultPath": "properties.securityRules[*].properties.provisioningState", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name", + "defaultPath": "properties.securityRules[*].name", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].etag", + "defaultPath": "properties.securityRules[*].etag", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "defaultPath": "properties.securityRules[*]", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Object" + }, + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].id", + "defaultPath": "properties.securityRules[*].id", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [ + { + "path": "properties.securityRules[*].properties.protocol", + "apiVersions": [ + "2018-05-01", + "2018-03-01", + "2017-04-01", + "2016-11-01", + "2016-10-01", + "2016-08-01", + "2016-07-01", + "2014-12-01-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourcePortRange", + "defaultPath": "properties.securityRules[*].properties.sourcePortRange", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [ + { + "path": "properties.securityRules[*].properties.sourcePortRange", + "apiVersions": [ + "2018-05-01", + "2018-03-01", + "2017-04-01", + "2016-11-01", + "2016-10-01", + "2016-08-01", + "2016-07-01", + "2014-12-01-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourcePortRanges[*]", + "defaultPath": "properties.securityRules[*].properties.sourcePortRanges[*]", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [ + { + "path": "properties.securityRules[*].properties.sourcePortRanges[*]", + "apiVersions": [ + "2018-05-01", + "2018-03-01", + "2017-04-01", + "2017-03-01", + "2016-12-01", + "2016-11-01", + "2016-10-01", + "2016-09-01", + "2016-08-01", + "2016-07-01", + "2016-06-01", + "2016-03-30", + "2015-06-15", + "2015-05-01-preview", + "2014-12-01-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", + "defaultPath": "properties.securityRules[*].properties.destinationPortRange", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [ + { + "path": "properties.securityRules[*].properties.destinationPortRange", + "apiVersions": [ + "2018-05-01", + "2018-03-01", + "2017-04-01", + "2016-11-01", + "2016-10-01", + "2016-08-01", + "2016-07-01", + "2014-12-01-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", + "defaultPath": "properties.securityRules[*].properties.access", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction", + "defaultPath": "properties.securityRules[*].properties.direction", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix", + "defaultPath": "properties.securityRules[*].properties.sourceAddressPrefix", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules/access", + "defaultPath": "properties.access", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules/direction", + "defaultPath": "properties.direction", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules/destinationPortRange", + "defaultPath": "properties.destinationPortRange", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules/destinationPortRanges[*]", + "defaultPath": "properties.destinationPortRanges[*]", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules/sourceAddressPrefix", + "defaultPath": "properties.sourceAddressPrefix", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules/sourceAddressPrefixes[*]", + "defaultPath": "properties.sourceAddressPrefixes[*]", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.KeyVault", + "resourceTypes": [ + { + "resourceType": "vaults", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Keyvault/vaults/tenantId", + "defaultPath": "properties.tenantId", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Keyvault/vaults/sku", + "defaultPath": "properties.sku", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Object" + }, + "paths": [] + }, + { + "name": "Microsoft.Keyvault/vaults/accessPolicies", + "defaultPath": "properties.accessPolicies", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Array" + }, + "paths": [] + }, + { + "name": "Microsoft.Keyvault/vaults/vaultUri", + "defaultPath": "properties.vaultUri", + "paths": [] + }, + { + "name": "Microsoft.KeyVault/vaults/privateEndpointConnections", + "defaultPath": "properties.privateEndpointConnections", + "paths": [] + }, + { + "name": "Microsoft.KeyVault/vaults/hsmPoolResourceId", + "defaultPath": "properties.hsmPoolResourceId", + "paths": [] + }, + { + "name": "Microsoft.Keyvault/vaults/accessPolicies[*].tenantId", + "defaultPath": "properties.accessPolicies[*].tenantId", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Keyvault/vaults/accessPolicies[*].objectId", + "defaultPath": "properties.accessPolicies[*].objectId", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Keyvault/vaults/accessPolicies[*].applicationId", + "defaultPath": "properties.accessPolicies[*].applicationId", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Keyvault/vaults/accessPolicies[*].permissions.keys[*]", + "defaultPath": "properties.accessPolicies[*].permissions.keys[*]", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Keyvault/vaults/accessPolicies[*].permissions.keys", + "defaultPath": "properties.accessPolicies[*].permissions.keys", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Array" + }, + "paths": [] + }, + { + "name": "Microsoft.KeyVault/vaults/sku.name", + "defaultPath": "properties.sku.name", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [ + { + "path": "properties.sku.name", + "apiVersions": [ + "2014-12-19-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.KeyVault/vaults/sku.family", + "defaultPath": "properties.sku.family", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [ + { + "path": "properties.sku.family", + "apiVersions": [ + "2014-12-19-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.KeyVault/vaults/enabledForDeployment", + "defaultPath": "properties.enabledForDeployment", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Boolean" + }, + "paths": [ + { + "path": "properties.enabledForDeployment", + "apiVersions": [ + "2014-12-19-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.KeyVault/vaults/enabledForDiskEncryption", + "defaultPath": "properties.enabledForDiskEncryption", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Boolean" + }, + "paths": [ + { + "path": "properties.enabledForDiskEncryption", + "apiVersions": [ + "2014-12-19-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.KeyVault/vaults/createMode", + "defaultPath": "properties.createMode", + "paths": [] + }, + { + "name": "Microsoft.KeyVault/vaults/networkAcls.defaultAction", + "defaultPath": "properties.networkAcls.defaultAction", + "paths": [] + }, + { + "name": "Microsoft.KeyVault/vaults/networkAcls.ipRules[*]", + "defaultPath": "properties.networkAcls.ipRules[*]", + "paths": [] + }, + { + "name": "Microsoft.KeyVault/vaults/networkAcls.ipRules[*].value", + "defaultPath": "properties.networkAcls.ipRules[*].value", + "paths": [] + }, + { + "name": "Microsoft.KeyVault/vaults/enableSoftDelete", + "defaultPath": "properties.enableSoftDelete", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Boolean" + }, + "paths": [] + }, + { + "name": "Microsoft.KeyVault/vaults/enablePurgeProtection", + "defaultPath": "properties.enablePurgeProtection", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Boolean" + }, + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Sql", + "resourceTypes": [ + { + "resourceType": "servers", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Sql/servers/administratorLogin", + "defaultPath": "properties.administratorLogin", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/state", + "defaultPath": "properties.state", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/fullyQualifiedDomainName", + "defaultPath": "properties.fullyQualifiedDomainName", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/externalAdministratorSid", + "defaultPath": "properties.externalAdministratorSid", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/externalAdministratorLogin", + "defaultPath": "properties.externalAdministratorLogin", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/administratorLoginPassword", + "defaultPath": "properties.administratorLoginPassword", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/dnsAliases.azureDnsRecord", + "defaultPath": "properties.azureDnsRecord", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/privateEndpointConnections", + "defaultPath": "properties.privateEndpointConnections", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/privateEndpointConnections[*].id", + "defaultPath": "properties.privateEndpointConnections[*].id", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/privateEndpointConnections[*].privateEndpoint.id", + "defaultPath": "properties.privateEndpointConnections[*].properties.privateEndpoint.id", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/privateEndpointConnections[*].privateEndpoint", + "defaultPath": "properties.privateEndpointConnections[*].properties.privateEndpoint", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/privateEndpointConnections[*].privateLinkServiceConnectionState.status", + "defaultPath": "properties.privateEndpointConnections[*].properties.privateLinkServiceConnectionState.status", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/privateEndpointConnections[*].privateLinkServiceConnectionState.description", + "defaultPath": "properties.privateEndpointConnections[*].properties.privateLinkServiceConnectionState.description", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/version", + "defaultPath": "properties.version", + "paths": [ + { + "path": "properties.version", + "apiVersions": [ + "2014-01-01", + "2014-04-01", + "2014-04-01-preview", + "2015-05-01-preview", + "2017-03-01-preview", + "2017-10-01-preview", + "2018-06-01-preview" + ] + } + ] + }, + { + "name": "Microsoft.Sql/servers/minimalTlsVersion", + "defaultPath": "properties.minimalTlsVersion", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [ + { + "path": "properties.minimalTlsVersion", + "apiVersions": [ + "2015-05-01-preview", + "2014-04-01" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + } + ] + }, + { + "resourceType": "servers/auditingSettings", + "capabilities": "None", + "aliases": [ + { + "name": "Microsoft.Sql/servers/auditingSettings/auditActionsAndGroups[*]", + "defaultPath": "properties.auditActionsAndGroups[*]", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/auditingSettings/auditActionsAndGroups", + "defaultPath": "properties.auditActionsAndGroups", + "paths": [] + }, + { + "name": "Microsoft.Sql/servers/auditingSettings/state", + "defaultPath": "properties.state", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Compute", + "resourceTypes": [ + { + "resourceType": "virtualMachines", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Compute/virtualMachines/osProfile.adminPassword", + "defaultPath": "properties.osProfile.adminPassword", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.osDisk.managedDisk.storageAccountType", + "defaultPath": "properties.storageProfile.osDisk.managedDisk.storageAccountType", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.customData", + "defaultPath": "properties.osProfile.customData", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/provisioningState", + "defaultPath": "properties.provisioningState", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/hardwareProfile.vmSize", + "defaultPath": "properties.hardwareProfile.vmSize", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/hardwareProfile", + "defaultPath": "properties.hardwareProfile", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Object" + }, + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.imageReference.publisher", + "defaultPath": "properties.storageProfile.imageReference.publisher", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.imageReference.offer", + "defaultPath": "properties.storageProfile.imageReference.offer", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*].diskSizeGB", + "defaultPath": "properties.storageProfile.dataDisks[*].diskSizeGB", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*].managedDisk.id", + "defaultPath": "properties.storageProfile.dataDisks[*].managedDisk.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*].managedDisk.storageAccountType", + "defaultPath": "properties.storageProfile.dataDisks[*].managedDisk.storageAccountType", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*].vhd", + "defaultPath": "properties.storageProfile.dataDisks[*].vhd", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Object" + }, + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*].image", + "defaultPath": "properties.storageProfile.dataDisks[*].image", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Object" + }, + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*]", + "defaultPath": "properties.storageProfile.dataDisks[*]", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Object" + }, + "paths": [] + }, + { + "name": "Microsoft.Compute/licenseType", + "defaultPath": "properties.licenseType", + "paths": [ + { + "path": "properties.licenseType", + "apiVersions": [ + "2015-05-01-preview", + "2015-06-15", + "2016-03-30", + "2016-04-30-preview", + "2016-08-30", + "2017-03-30", + "2017-12-01", + "2018-04-01", + "2018-06-01", + "2018-10-01" + ] + } + ] + }, + { + "name": "Microsoft.Compute/virtualMachines/availabilitySet.id", + "defaultPath": "properties.availabilitySet.id", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [ + { + "path": "properties.availabilitySet.id", + "apiVersions": [ + "2016-08-30", + "2015-05-01-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.Compute/virtualMachines/sku.name", + "defaultPath": "properties.hardwareProfile.vmSize", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [ + { + "path": "properties.hardwareProfile.vmSize", + "apiVersions": [ + "2016-08-30", + "2015-05-01-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.Compute/virtualMachines/osDisk.Uri", + "defaultPath": "properties.storageProfile.osDisk.vhd.uri", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [ + { + "path": "properties.storageProfile.osDisk.vhd.uri", + "apiVersions": [ + "2016-08-30", + "2015-05-01-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.Compute/imageId", + "defaultPath": "properties.storageProfile.imageReference.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/imagePublisher", + "defaultPath": "properties.storageProfile.imageReference.publisher", + "paths": [] + }, + { + "name": "Microsoft.Compute/imageOffer", + "defaultPath": "properties.storageProfile.imageReference.offer", + "paths": [] + }, + { + "name": "Microsoft.Compute/imageSku", + "defaultPath": "properties.storageProfile.imageReference.sku", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/securityProfile.uefiSettings", + "defaultPath": "properties.securityProfile.uefiSettings", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.osDisk.managedDisk.id", + "defaultPath": "properties.storageProfile.osDisk.managedDisk.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/proximityPlacementGroup.id", + "defaultPath": "properties.proximityPlacementGroup.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/zones", + "defaultPath": "zones", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.osDisk.osType", + "defaultPath": "properties.storageProfile.osDisk.osType", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.osDisk.createOption", + "defaultPath": "properties.storageProfile.osDisk.createOption", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.imageReference.id", + "defaultPath": "properties.storageProfile.imageReference.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.imageReference.sku", + "defaultPath": "properties.storageProfile.imageReference.sku", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.computerName", + "defaultPath": "properties.osProfile.computerName", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.windowsConfiguration", + "defaultPath": "properties.osProfile.windowsConfiguration", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.linuxConfiguration", + "defaultPath": "properties.osProfile.linuxConfiguration", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.windowsConfiguration.patchSettings.assessmentMode", + "defaultPath": "properties.osProfile.windowsConfiguration.patchSettings.assessmentMode", + "defaultMetadata": { "attributes": "Modifiable" }, + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.linuxConfiguration.patchSettings.assessmentMode", + "defaultPath": "properties.osProfile.linuxConfiguration.patchSettings.assessmentMode", + "defaultMetadata": { "attributes": "Modifiable" }, + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.windowsConfiguration.patchSettings.patchMode", + "defaultPath": "properties.osProfile.windowsConfiguration.patchSettings.patchMode", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.linuxConfiguration.patchSettings.patchMode", + "defaultPath": "properties.osProfile.linuxConfiguration.patchSettings.patchMode", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.windowsConfiguration.patchSettings.automaticByPlatformSettings", + "defaultPath": "properties.osProfile.windowsConfiguration.patchSettings.automaticByPlatformSettings", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.windowsConfiguration.patchSettings.automaticByPlatformSettings.bypassPlatformSafetyChecksOnUserSchedule", + "defaultPath": "properties.osProfile.windowsConfiguration.patchSettings.automaticByPlatformSettings.bypassPlatformSafetyChecksOnUserSchedule", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.linuxConfiguration.patchSettings.automaticByPlatformSettings", + "defaultPath": "properties.osProfile.linuxConfiguration.patchSettings.automaticByPlatformSettings", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/osProfile.linuxConfiguration.patchSettings.automaticByPlatformSettings.bypassPlatformSafetyChecksOnUserSchedule", + "defaultPath": "properties.osProfile.linuxConfiguration.patchSettings.automaticByPlatformSettings.bypassPlatformSafetyChecksOnUserSchedule", + "paths": [] + }, + { + "name": "Microsoft.Compute/imageSKU", + "defaultPath": "properties.storageProfile.imageReference.sku", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.osDisk.managedDisk.diskEncryptionSet.id", + "defaultPath": "properties.storageProfile.osDisk.managedDisk.diskEncryptionSet.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*].managedDisk.diskEncryptionSet.id", + "defaultPath": "properties.storageProfile.dataDisks[*].managedDisk.diskEncryptionSet.id", + "paths": [] + } + ] + }, + { + "resourceType": "VirtualMachineScaleSets", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Compute/imageId", + "defaultPath": "properties.virtualMachineProfile.storageProfile.imageReference.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/imagePublisher", + "defaultPath": "properties.virtualMachineProfile.storageProfile.imageReference.publisher", + "paths": [] + }, + { + "name": "Microsoft.Compute/imageOffer", + "defaultPath": "properties.virtualMachineProfile.storageProfile.imageReference.offer", + "paths": [] + }, + { + "name": "Microsoft.Compute/imageSku", + "defaultPath": "properties.virtualMachineProfile.storageProfile.imageReference.sku", + "paths": [] + }, + { + "name": "Microsoft.Compute/VirtualMachineScaleSets/securityProfile.uefiSettings", + "defaultPath": "properties.virtualMachineProfile.securityProfile.uefiSettings", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachineScaleSets/virtualMachineProfile.storageProfile.osDisk.osType", + "defaultPath": "properties.virtualMachineProfile.storageProfile.osDisk.osType", + "paths": [] + }, + { + "name": "Microsoft.Compute/imageSKU", + "defaultPath": "properties.virtualMachineProfile.storageProfile.imageReference.sku", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachineScaleSets/virtualMachineProfile.storageProfile.osDisk.managedDisk.diskEncryptionSet.id", + "defaultPath": "properties.virtualMachineProfile.storageProfile.osDisk.managedDisk.diskEncryptionSet.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachineScaleSets/virtualMachineProfile.storageProfile.dataDisks[*]", + "defaultPath": "properties.virtualMachineProfile.storageProfile.dataDisks[*]", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachineScaleSets/virtualMachineProfile.storageProfile.dataDisks[*].managedDisk.diskEncryptionSet.id", + "defaultPath": "properties.virtualMachineProfile.storageProfile.dataDisks[*].managedDisk.diskEncryptionSet.id", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Web", + "resourceTypes": [ + { + "resourceType": "sites", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Web/sites/name", + "defaultPath": "properties.name", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/state", + "defaultPath": "properties.state", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/hostNames", + "defaultPath": "properties.hostNames", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/repositorySiteName", + "defaultPath": "properties.repositorySiteName", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/enabled", + "defaultPath": "properties.enabled", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/enabledHostNames", + "defaultPath": "properties.enabledHostNames", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/hostNameSslStates", + "defaultPath": "properties.hostNameSslStates", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/lastModifiedTimeUtc", + "defaultPath": "properties.lastModifiedTimeUtc", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/enabledHostNames[*]", + "defaultPath": "properties.enabledHostNames[*]", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/hostNameSslStates[*].name", + "defaultPath": "properties.hostNameSslStates[*].name", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/hostNameSslStates[*].virtualIP", + "defaultPath": "properties.hostNameSslStates[*].virtualIP", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/hostNameSslStates[*].thumbprint", + "defaultPath": "properties.hostNameSslStates[*].thumbprint", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/hostNameSslStates[*].toUpdate", + "defaultPath": "properties.hostNameSslStates[*].toUpdate", + "paths": [] + }, + { + "name": "Microsoft.Web/sites/serverFarmId", + "defaultPath": "properties.serverFarmId", + "paths": [ + { + "path": "properties.serverFarmId", + "apiVersions": [ + "2018-12-01-alpha", + "2018-11-01", + "2018-02-01", + "2016-08-01", + "2015-08-01-preview", + "2016-03-01", + "2015-08-01", + "2015-07-01", + "2015-06-01", + "2015-05-01", + "2015-04-01", + "2015-02-01", + "2014-11-01", + "2014-06-01", + "2014-04-01", + "2014-04-01-preview", + "2015-01-01", + "2015-11-01", + "2016-09-01", + "2017-08-01" + ] + } + ] + }, + { + "name": "Microsoft.Web/sites/clientCertEnabled", + "defaultPath": "properties.clientCertEnabled", + "paths": [ + { + "path": "properties.clientCertEnabled", + "apiVersions": [ + "2018-12-01-alpha", + "2018-11-01", + "2018-02-01", + "2016-08-01", + "2015-08-01-preview", + "2016-03-01", + "2015-08-01", + "2015-07-01", + "2015-06-01", + "2015-05-01", + "2015-04-01", + "2015-02-01", + "2014-11-01", + "2014-06-01", + "2014-04-01", + "2014-04-01-preview", + "2015-01-01", + "2015-11-01", + "2016-09-01", + "2017-08-01" + ] + } + ] + }, + { + "name": "Microsoft.Web/sites/hostNameSslStates[*].sslState", + "defaultPath": "properties.hostNameSslStates[*].sslState", + "paths": [ + { + "path": "properties.hostNameSslStates[*].sslState", + "apiVersions": [ + "2018-12-01-alpha", + "2018-11-01", + "2018-02-01", + "2016-08-01", + "2015-08-01-preview", + "2016-03-01", + "2015-08-01", + "2015-07-01", + "2015-06-01", + "2015-05-01", + "2015-04-01", + "2015-02-01", + "2014-11-01", + "2014-06-01", + "2014-04-01", + "2014-04-01-preview", + "2015-01-01", + "2015-11-01", + "2016-09-01", + "2017-08-01" + ] + } + ] + }, + { + "name": "Microsoft.Web/sites/httpsOnly", + "defaultPath": "properties.httpsOnly", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "Boolean" + }, + "paths": [ + { + "path": "properties.httpsOnly", + "apiVersions": [ + "2018-12-01-alpha", + "2017-08-01", + "2016-09-01", + "2016-03-01", + "2015-11-01", + "2015-08-01", + "2015-08-01-preview", + "2015-07-01", + "2015-06-01", + "2015-05-01", + "2015-04-01", + "2015-02-01", + "2015-01-01", + "2014-11-01", + "2014-06-01", + "2014-04-01", + "2014-04-01-preview" + ], + "metadata": { + "type": "NotSpecified", + "attributes": "None" + } + } + ] + }, + { + "name": "Microsoft.Web/sites/siteConfig.numberOfWorkers", + "defaultPath": "properties.siteConfig.numberOfWorkers", + "paths": [ + { + "path": "properties.siteConfig.numberOfWorkers", + "apiVersions": [ + "2016-08-01", + "2018-02-01", + "2018-11-01", + "2017-08-01", + "2016-09-01", + "2016-03-01", + "2015-11-01" + ] + }, + { + "path": "properties.siteConfig.properties.numberOfWorkers", + "apiVersions": [ + "2014-04-01", + "2014-04-01-preview", + "2014-06-01", + "2014-11-01", + "2015-01-01", + "2015-02-01", + "2015-04-01", + "2015-05-01", + "2015-06-01", + "2015-07-01", + "2015-08-01", + "2015-08-01-preview" + ] + } + ] + }, + { + "name": "Microsoft.Web/sites/siteConfig.defaultDocuments[*]", + "defaultPath": "properties.siteConfig.defaultDocuments[*]", + "paths": [ + { + "path": "properties.siteConfig.defaultDocuments[*]", + "apiVersions": [ + "2016-08-01", + "2018-02-01", + "2018-11-01", + "2017-08-01", + "2016-09-01", + "2016-03-01", + "2015-11-01" + ] + }, + { + "path": "properties.siteConfig.properties.defaultDocuments[*]", + "apiVersions": [ + "2014-04-01", + "2014-04-01-preview", + "2014-06-01", + "2014-11-01", + "2015-01-01", + "2015-02-01", + "2015-04-01", + "2015-05-01", + "2015-06-01", + "2015-07-01", + "2015-08-01", + "2015-08-01-preview" + ] + } + ] + }, + { + "name": "Microsoft.Web/sites/siteConfig.defaultDocuments", + "defaultPath": "properties.siteConfig.defaultDocuments", + "paths": [ + { + "path": "properties.siteConfig.defaultDocuments", + "apiVersions": [ + "2016-08-01", + "2018-02-01", + "2018-11-01", + "2017-08-01", + "2016-09-01", + "2016-03-01", + "2015-11-01" + ] + }, + { + "path": "properties.siteConfig.properties.defaultDocuments", + "apiVersions": [ + "2014-04-01", + "2014-04-01-preview", + "2014-06-01", + "2014-11-01", + "2015-01-01", + "2015-02-01", + "2015-04-01", + "2015-05-01", + "2015-06-01", + "2015-07-01", + "2015-08-01", + "2015-08-01-preview" + ] + } + ] + }, + { + "name": "Microsoft.Web/sites/siteConfig.minTlsVersion", + "defaultPath": "properties.siteConfig.minTlsVersion", + "defaultMetadata": { + "attributes": "Modifiable", + "type": "String" + }, + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.ContainerService", + "resourceTypes": [ + { + "resourceType": "managedClusters", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.ContainerService/managedClusters/kubernetesVersion", + "defaultPath": "properties.kubernetesVersion", + "paths": [] + }, + { + "name": "Microsoft.ContainerService/managedClusters/enableRBAC", + "defaultPath": "properties.enableRBAC", + "paths": [] + }, + { + "name": "Microsoft.ContainerService/managedClusters/networkProfile.networkPlugin", + "defaultPath": "properties.networkProfile.networkPlugin", + "paths": [] + }, + { + "name": "Microsoft.ContainerService/managedClusters/agentPoolProfiles[*]", + "defaultPath": "properties.agentPoolProfiles[*]", + "paths": [] + }, + { + "name": "Microsoft.ContainerService/managedClusters/agentPoolProfiles[*].availabilityZones[*]", + "defaultPath": "properties.agentPoolProfiles[*].availabilityZones[*]", + "paths": [] + }, + { + "name": "Microsoft.ContainerService/managedClusters/agentPoolProfiles[*].count", + "defaultPath": "properties.agentPoolProfiles[*].count", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Compute", + "resourceTypes": [ + { + "resourceType": "disks", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Compute/disks/encryptionSettingsCollection.enabled", + "defaultPath": "properties.encryptionSettingsCollection.enabled", + "paths": [] + }, + { + "name": "Microsoft.Compute/disks/diskSizeGB", + "defaultPath": "properties.diskSizeGB", + "paths": [] + }, + { + "name": "Microsoft.Compute/disks/encryption.diskEncryptionSetId", + "defaultPath": "properties.encryption.diskEncryptionSetId", + "paths": [] + }, + { + "name": "Microsoft.Compute/disks/managedBy", + "defaultPath": "properties.managedBy", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkInterfaces", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Network/networkInterfaces/ipConfigurations[*]", + "defaultPath": "properties.ipConfigurations[*]", + "paths": [] + }, + { + "name": "Microsoft.Network/networkInterfaces/ipConfigurations[*].privateIPAddress", + "defaultPath": "properties.ipConfigurations[*].properties.privateIPAddress", + "paths": [] + }, + { + "name": "Microsoft.Network/networkInterfaces/ipConfigurations[*].privateIPAllocationMethod", + "defaultPath": "properties.ipConfigurations[*].properties.privateIPAllocationMethod", + "paths": [] + }, + { + "name": "Microsoft.Network/networkInterfaces/ipConfigurations[*].subnet.id", + "defaultPath": "properties.ipConfigurations[*].properties.subnet.id", + "paths": [] + }, + { + "name": "Microsoft.Network/networkInterfaces/ipConfigurations[*].publicIPAddress.id", + "defaultPath": "properties.ipConfigurations[*].properties.publicIPAddress.id", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Compute", + "resourceTypes": [ + { + "resourceType": "diskEncryptionSets", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Compute/diskEncryptionSets/encryptionType", + "defaultPath": "properties.encryptionType", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Compute", + "resourceTypes": [ + { + "resourceType": "VirtualMachineScaleSets", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Compute/VirtualMachineScaleSets/osDisk.vhdContainers", + "defaultPath": "properties.virtualMachineProfile.storageProfile.osDisk.vhdContainers", + "paths": [] + }, + { + "name": "Microsoft.Compute/VirtualMachineScaleSets/osdisk.imageUrl", + "defaultPath": "properties.virtualMachineProfile.storageProfile.osDisk.image.uri", + "paths": [] + }, + { + "name": "Microsoft.Compute/imageId", + "defaultPath": "properties.virtualMachineProfile.storageProfile.imageReference.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/imagePublisher", + "defaultPath": "properties.virtualMachineProfile.storageProfile.imageReference.publisher", + "paths": [] + }, + { + "name": "Microsoft.Compute/imageOffer", + "defaultPath": "properties.virtualMachineProfile.storageProfile.imageReference.offer", + "paths": [] + }, + { + "name": "Microsoft.Compute/imageSku", + "defaultPath": "properties.virtualMachineProfile.storageProfile.imageReference.sku", + "paths": [] + }, + { + "name": "Microsoft.Compute/imageSKU", + "defaultPath": "properties.virtualMachineProfile.storageProfile.imageReference.sku", + "paths": [] + }, + { + "name": "Microsoft.Compute/VirtualMachineScaleSets/securityProfile.uefiSettings", + "defaultPath": "properties.virtualMachineProfile.securityProfile.uefiSettings", + "paths": [] + }, + { + "name": "Microsoft.Compute/virtualMachineScaleSets/virtualMachineProfile.storageProfile.osDisk.osType", + "defaultPath": "properties.virtualMachineProfile.storageProfile.osDisk.osType", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Synapse", + "resourceTypes": [ + { + "resourceType": "workspaces", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Synapse/workspaces/extraProperties", + "defaultPath": "properties.extraProperties", + "paths": [] + }, + { + "name": "Microsoft.Synapse/workspaces/azureADOnlyAuthentication", + "defaultPath": "properties.azureADOnlyAuthentication", + "paths": [] + }, + { + "name": "Microsoft.Synapse/workspaces/settings", + "defaultPath": "properties.settings", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Security", + "resourceTypes": [ + { + "resourceType": "pricings", + "capabilities": "None", + "aliases": [ + { + "name": "Microsoft.Security/pricings/pricingTier", + "defaultPath": "properties.pricingTier", + "paths": [] + }, + { + "name": "Microsoft.Security/pricings/subPlan", + "defaultPath": "properties.subPlan", + "paths": [] + }, + { + "name": "Microsoft.Security/pricings/extensions[*]", + "defaultPath": "properties.extensions[*]", + "paths": [] + }, + { + "name": "Microsoft.Security/pricings/extensions[*].name", + "defaultPath": "properties.extensions[*].name", + "paths": [] + }, + { + "name": "Microsoft.Security/pricings/extensions[*].isEnabled", + "defaultPath": "properties.extensions[*].isEnabled", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.StreamAnalytics", + "resourceTypes": [ + { + "resourceType": "streamingjobs", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.StreamAnalytics/streamingjobs/jobStorageAccount", + "defaultPath": "properties.jobStorageAccount", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/jobStorageAccount.accountName", + "defaultPath": "properties.jobStorageAccount.accountName", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/outputs/datasource.type", + "defaultPath": "properties.datasource.type", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/outputs/datasource.Microsoft-ServiceBus-EventHub.serviceBusNamespace", + "defaultPath": "properties.datasource.serviceBusNamespace", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/outputs/datasource.Microsoft-Sql-Server-Database.server", + "defaultPath": "properties.datasource.server", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/outputs/datasource.Microsoft-Storage-Table.accountName", + "defaultPath": "properties.datasource.accountName", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/outputs/datasource.Microsoft-Storage-DocumentDB.accountId", + "defaultPath": "properties.datasource.accountId", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/outputs/datasource.Microsoft-AzureFunction.functionAppName", + "defaultPath": "properties.datasource.functionAppName", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/outputs/datasource.Microsoft-Storage-Blob.storageAccounts[*]", + "defaultPath": "properties.datasource.storageAccounts[*]", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/outputs/datasource.Microsoft-Storage-Blob.storageAccounts[*].accountName", + "defaultPath": "properties.datasource.storageAccounts[*].accountName", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/inputs/Stream.datasource.type", + "defaultPath": "properties.datasource.type", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/inputs/Stream.datasource.Microsoft-ServiceBus-EventHub.serviceBusNamespace", + "defaultPath": "properties.datasource.serviceBusNamespace", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/inputs/Stream.datasource.Microsoft-Devices-IotHubs.iotHubNamespace", + "defaultPath": "properties.datasource.iotHubNamespace", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/inputs/Reference.datasource.Microsoft-Sql-Server-Database.server", + "defaultPath": "properties.datasource.server", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/inputs/Stream.datasource.Microsoft-Storage-Blob.storageAccounts[*]", + "defaultPath": "properties.datasource.storageAccounts[*]", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/inputs/Stream.datasource.Microsoft-Storage-Blob.storageAccounts[*].accountName", + "defaultPath": "properties.datasource.storageAccounts[*].accountName", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/inputs/Reference.datasource.type", + "defaultPath": "properties.datasource.type", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/inputs/Reference.datasource.Microsoft-Storage-Blob.storageAccounts[*]", + "defaultPath": "properties.datasource.storageAccounts[*]", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/inputs/Reference.datasource.Microsoft-Storage-Blob.storageAccounts[*].accountName", + "defaultPath": "properties.datasource.storageAccounts[*].accountName", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/functions[*].type", + "defaultPath": "properties.functions[*].type", + "paths": [] + }, + { + "name": "Microsoft.StreamAnalytics/streamingjobs/functions[*].binding.Microsoft-MachineLearning-WebService.endpoint", + "defaultPath": "properties.functions[*].binding.endpoint", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.DocumentDB", + "resourceTypes": [ + { + "resourceType": "databaseAccounts", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.DocumentDB/databaseAccounts/Locations[*]", + "defaultPath": "properties.Locations[*]", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/Locations[*].locationName", + "defaultPath": "properties.Locations[*].locationName", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/publicNetworkAccess", + "defaultPath": "properties.publicNetworkAccess", + "paths": [], + "defaultMetadata": { + "attributes": "Modifiable" + } + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/isVirtualNetworkFilterEnabled", + "defaultPath": "properties.isVirtualNetworkFilterEnabled", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/ipRules[*]", + "defaultPath": "properties.ipRules[*]", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/ipRules[*].ipAddressOrRange", + "defaultPath": "properties.ipRules[*].ipAddressOrRange", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/ipRules", + "defaultPath": "properties.ipRules", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/ipRangeFilter", + "defaultPath": "properties.ipRangeFilter", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/privateEndpointConnections[*]", + "defaultPath": "properties.privateEndpointConnections[*]", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/privateEndpointConnections[*].privateLinkServiceConnectionState.status", + "defaultPath": "properties.privateEndpointConnections[*].properties.privateLinkServiceConnectionState.status", + "paths": [] + } + ] + }, + { + "resourceType": "databaseAccounts/sqlDatabases", + "aliases": [ + { + "name": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/options", + "defaultPath": "properties.options", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/options.throughput", + "defaultPath": "properties.options.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/throughputSettings/default.resource.throughput", + "defaultPath": "properties.resource.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/throughputSettings/default.resource.provisionedThroughputSettings", + "defaultPath": "properties.resource.provisionedThroughputSettings", + "paths": [] + } + ] + }, + { + "resourceType": "databaseAccounts/sqlDatabases/containers", + "aliases": [ + { + "name": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/options", + "defaultPath": "properties.options", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/options.throughput", + "defaultPath": "properties.options.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/throughputSettings/default.resource.throughput", + "defaultPath": "properties.resource.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/sqlDatabases/containers/throughputSettings/default.resource.provisionedThroughputSettings", + "defaultPath": "properties.resource.provisionedThroughputSettings", + "paths": [] + } + ] + }, + { + "resourceType": "databaseAccounts/mongodbDatabases", + "aliases": [ + { + "name": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/options", + "defaultPath": "properties.options", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/options.throughput", + "defaultPath": "properties.options.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/throughputSettings/default.resource.throughput", + "defaultPath": "properties.resource.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/throughputSettings/default.resource.provisionedThroughputSettings", + "defaultPath": "properties.resource.provisionedThroughputSettings", + "paths": [] + } + ] + }, + { + "resourceType": "databaseAccounts/mongodbDatabases/collections", + "aliases": [ + { + "name": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections/options", + "defaultPath": "properties.options", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections/options.throughput", + "defaultPath": "properties.options.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections/throughputSettings/default.resource.throughput", + "defaultPath": "properties.resource.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/mongodbDatabases/collections/throughputSettings/default.resource.provisionedThroughputSettings", + "defaultPath": "properties.resource.provisionedThroughputSettings", + "paths": [] + } + ] + }, + { + "resourceType": "databaseAccounts/gremlinDatabases", + "aliases": [ + { + "name": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/options", + "defaultPath": "properties.options", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/options.throughput", + "defaultPath": "properties.options.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/throughputSettings/default.resource.throughput", + "defaultPath": "properties.resource.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/throughputSettings/default.resource.provisionedThroughputSettings", + "defaultPath": "properties.resource.provisionedThroughputSettings", + "paths": [] + } + ] + }, + { + "resourceType": "databaseAccounts/gremlinDatabases/graphs", + "aliases": [ + { + "name": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs/options", + "defaultPath": "properties.options", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs/options.throughput", + "defaultPath": "properties.options.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs/throughputSettings/default.resource.throughput", + "defaultPath": "properties.resource.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/gremlinDatabases/graphs/throughputSettings/default.resource.provisionedThroughputSettings", + "defaultPath": "properties.resource.provisionedThroughputSettings", + "paths": [] + } + ] + }, + { + "resourceType": "databaseAccounts/cassandraKeyspaces", + "aliases": [ + { + "name": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/options", + "defaultPath": "properties.options", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/options.throughput", + "defaultPath": "properties.options.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/throughputSettings/default.resource.throughput", + "defaultPath": "properties.resource.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/throughputSettings/default.resource.provisionedThroughputSettings", + "defaultPath": "properties.resource.provisionedThroughputSettings", + "paths": [] + } + ] + }, + { + "resourceType": "databaseAccounts/cassandraKeyspaces/tables", + "aliases": [ + { + "name": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables/options", + "defaultPath": "properties.options", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables/options.throughput", + "defaultPath": "properties.options.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables/throughputSettings/default.resource.throughput", + "defaultPath": "properties.resource.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/cassandraKeyspaces/tables/throughputSettings/default.resource.provisionedThroughputSettings", + "defaultPath": "properties.resource.provisionedThroughputSettings", + "paths": [] + } + ] + }, + { + "resourceType": "databaseAccounts/tables", + "aliases": [ + { + "name": "Microsoft.DocumentDB/databaseAccounts/tables/options", + "defaultPath": "properties.options", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/tables/options.throughput", + "defaultPath": "properties.options.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/tables/throughputSettings/default.resource.throughput", + "defaultPath": "properties.resource.throughput", + "paths": [] + }, + { + "name": "Microsoft.DocumentDB/databaseAccounts/tables/throughputSettings/default.resource.provisionedThroughputSettings", + "defaultPath": "properties.resource.provisionedThroughputSettings", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.ContainerInstance", + "resourceTypes": [ + { + "resourceType": "containerGroups", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.ContainerInstance/containerGroups/diagnostics.logAnalytics.workspaceId", + "defaultPath": "properties.diagnostics.logAnalytics.workspaceId", + "paths": [] + }, + { + "name": "Microsoft.ContainerInstance/containerGroups/diagnostics.logAnalytics.workspaceKey", + "defaultPath": "properties.diagnostics.logAnalytics.workspaceKey", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Portal", + "resourceTypes": [ + { + "resourceType": "dashboards", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Portal/dashboards/lenses[*].parts[*]", + "defaultPath": "properties.lenses[*].parts[*]", + "paths": [] + }, + { + "name": "Microsoft.Portal/dashboards/lenses[*].parts[*].metadata.type", + "defaultPath": "properties.lenses[*].parts[*].metadata.type", + "paths": [] + }, + { + "name": "Microsoft.Portal/dashboards/lenses[*].parts[*].metadata.Extension-HubsExtension-PartType-MarkdownPart.settings.content.settings.markdownUri", + "defaultPath": "properties.lenses[*].parts[*].metadata.Extension-HubsExtension-PartType-MarkdownPart.settings.content.settings.markdownUri", + "paths": [] + }, + { + "name": "Microsoft.Portal/dashboards/lenses[*].parts[*].metadata.Extension-HubsExtension-PartType-MarkdownPart.settings.content.settings.markdownSource", + "defaultPath": "properties.lenses[*].parts[*].metadata.Extension-HubsExtension-PartType-MarkdownPart.settings.content.settings.markdownSource", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Authorization", + "resourceTypes": [ + { + "resourceType": "roleDefinitions", + "capabilities": "", + "aliases": [ + { + "name": "Microsoft.Authorization/roleDefinitions/type", + "defaultPath": "properties.type", + "paths": [] + }, + { + "name": "Microsoft.Authorization/roleDefinitions/permissions[*].actions[*]", + "defaultPath": "properties.permissions[*].actions[*]", + "paths": [] + }, + { + "name": "Microsoft.Authorization/roleDefinitions/permissions.actions[*]", + "defaultPath": "properties.permissions.actions[*]", + "paths": [] + }, + { + "name": "Microsoft.Authorization/roleDefinitions/assignableScopes[*]", + "defaultPath": "properties.assignableScopes[*]", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "virtualNetworks", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Network/virtualNetworks/enableDdosProtection", + "defaultPath": "properties.enableDdosProtection", + "paths": [], + "defaultMetadata": { + "attributes": "Modifiable" + } + }, + { + "name": "Microsoft.Network/virtualNetworks/ddosProtectionPlan", + "defaultPath": "properties.ddosProtectionPlan", + "paths": [] + }, + { + "name": "Microsoft.Network/virtualNetworks/ddosProtectionPlan.id", + "defaultPath": "properties.ddosProtectionPlan.id", + "paths": [], + "defaultMetadata": { + "attributes": "Modifiable" + } + }, + { + "name": "Microsoft.Network/virtualNetworks/subnets[*]", + "defaultPath": "properties.subnets[*]", + "paths": [] + }, + { + "name": "Microsoft.Network/virtualNetworks/subnets[*].name", + "defaultPath": "properties.subnets[*].name", + "paths": [] + }, + { + "name": "Microsoft.Network/virtualNetworks/subnets[*].ipConfigurations[*]", + "defaultPath": "properties.subnets[*].properties.ipConfigurations[*]", + "paths": [] + }, + { + "name": "Microsoft.Network/virtualNetworks/subnets[*].ipConfigurations[*].id", + "defaultPath": "properties.subnets[*].properties.ipConfigurations[*].id", + "paths": [] + }, + { + "name": "Microsoft.Network/virtualNetworks/subnets[*].routeTable", + "defaultPath": "properties.subnets[*].properties.routeTable", + "paths": [] + }, + { + "name": "Microsoft.Network/virtualNetworks/subnets[*].routeTable.id", + "defaultPath": "properties.subnets[*].properties.routeTable.id", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.DBforPostgreSQL", + "resourceTypes": [ + { + "resourceType": "flexibleServers", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [] + }, + { + "resourceType": "flexibleServers/configurations", + "capabilities": "", + "aliases": [ + { + "name": "Microsoft.DBforPostgreSQL/flexibleServers/configurations/value", + "defaultPath": "properties.value", + "paths": [] + }, + { + "name": "Microsoft.DBforPostgreSQL/flexibleServers/configurations/source", + "defaultPath": "properties.source", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Resources", + "resourceTypes": [ + { + "resourceType": "links", + "capabilities": "None", + "aliases": [ + { + "name": "Microsoft.Resources/links/targetId", + "defaultPath": "properties.targetId", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "azureFirewalls", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Network/azureFirewalls/ipConfigurations[*]", + "defaultPath": "properties.ipConfigurations[*]", + "paths": [] + }, + { + "name": "Microsoft.Network/azureFirewalls/ipConfigurations[*].subnet.id", + "defaultPath": "properties.ipConfigurations[*].properties.subnet.id", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.SignalRService", + "resourceTypes": [ + { + "resourceType": "SignalR", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.SignalRService/SignalR/networkACLs.defaultAction", + "defaultPath": "properties.networkACLs.defaultAction", + "defaultMetadata": { "attributes": "Modifiable" }, + "paths": [] + }, + { + "name": "Microsoft.SignalRService/SignalR/networkACLs.publicNetwork.allow", + "defaultPath": "properties.networkACLs.publicNetwork.allow", + "defaultMetadata": { "attributes": "Modifiable" }, + "paths": [] + }, + { + "name": "Microsoft.SignalRService/SignalR/networkACLs.publicNetwork.allow[*]", + "defaultPath": "properties.networkACLs.publicNetwork.allow[*]", + "defaultMetadata": { "attributes": "Modifiable" }, + "paths": [] + }, + { + "name": "Microsoft.SignalRService/SignalR/publicNetworkAccess", + "defaultPath": "properties.publicNetworkAccess", + "defaultMetadata": { "attributes": "Modifiable" }, + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.ManagedIdentity", + "resourceTypes": [ + { + "resourceType": "userAssignedIdentities/federatedIdentityCredentials", + "capabilities": "", + "aliases": [ + { + "name": "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials/issuer", + "defaultPath": "properties.issuer", + "paths": [] + }, + { + "name": "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials/subject", + "defaultPath": "properties.subject", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.HybridCompute", + "resourceTypes": [ + { + "resourceType": "machines", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.HybridCompute/machines/osName", + "defaultPath": "properties.osName", + "paths": [] + }, + { + "name": "Microsoft.HybridCompute/machines/osSku", + "defaultPath": "properties.osSku", + "paths": [] + }, + { + "name": "Microsoft.HybridCompute/imageOffer", + "defaultPath": "properties.imageOffer", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Maintenance", + "resourceTypes": [ + { + "resourceType": "configurationAssignments", + "capabilities": "", + "aliases": [ + { + "name": "Microsoft.Maintenance/configurationAssignments/maintenanceConfigurationId", + "defaultPath": "properties.maintenanceConfigurationId", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Automanage", + "resourceTypes": [ + { + "resourceType": "configurationProfileAssignments", + "capabilities": "", + "aliases": [ + { + "name": "Microsoft.Automanage/configurationProfileAssignments/configurationProfile", + "defaultPath": "properties.configurationProfile", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Insights", + "resourceTypes": [ + { + "resourceType": "dataCollectionRuleAssociations", + "capabilities": "", + "aliases": [ + { + "name": "Microsoft.Insights/dataCollectionRuleAssociations/dataCollectionRuleId", + "defaultPath": "properties.dataCollectionRuleId", + "paths": [] + }, + { + "name": "Microsoft.Insights/dataCollectionRuleAssociations/dataCollectionEndpointId", + "defaultPath": "properties.dataCollectionEndpointId", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.GuestConfiguration", + "resourceTypes": [ + { + "resourceType": "guestConfigurationAssignments", + "capabilities": "", + "aliases": [ + { + "name": "Microsoft.GuestConfiguration/guestConfigurationAssignments/parameterHash", + "defaultPath": "properties.parameterHash", + "paths": [] + }, + { + "name": "Microsoft.GuestConfiguration/guestConfigurationAssignments/complianceStatus", + "defaultPath": "properties.complianceStatus", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.ConnectedVMwarevSphere", + "resourceTypes": [ + { + "resourceType": "virtualMachines", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.ConnectedVMwarevSphere/virtualMachines/osProfile.osType", + "defaultPath": "properties.osProfile.osType", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Compute", + "resourceTypes": [ + { + "resourceType": "images", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Compute/images/storageProfile.osDisk.diskEncryptionSet.id", + "defaultPath": "properties.storageProfile.osDisk.diskEncryptionSet.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/images/storageProfile.osDisk.diskEncryptionSet", + "defaultPath": "properties.storageProfile.osDisk.diskEncryptionSet", + "paths": [] + }, + { + "name": "Microsoft.Compute/images/storageProfile.dataDisks[*]", + "defaultPath": "properties.storageProfile.dataDisks[*]", + "paths": [] + }, + { + "name": "Microsoft.Compute/images/storageProfile.dataDisks[*].diskEncryptionSet.id", + "defaultPath": "properties.storageProfile.dataDisks[*].diskEncryptionSet.id", + "paths": [] + }, + { + "name": "Microsoft.Compute/images/storageProfile.dataDisks[*].diskEncryptionSet", + "defaultPath": "properties.storageProfile.dataDisks[*].diskEncryptionSet", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Compute", + "resourceTypes": [ + { + "resourceType": "galleries/images/versions", + "capabilities": "SupportsTags, SupportsLocation", + "aliases": [ + { + "name": "Microsoft.Compute/galleries/images/versions/publishingProfile.targetRegions[*]", + "defaultPath": "properties.publishingProfile.targetRegions[*]", + "paths": [] + }, + { + "name": "Microsoft.Compute/galleries/images/versions/publishingProfile.targetRegions[*].name", + "defaultPath": "properties.publishingProfile.targetRegions[*].name", + "paths": [] + }, + { + "name": "Microsoft.Compute/galleries/images/versions/publishingProfile.targetRegions[*].encryption", + "defaultPath": "properties.publishingProfile.targetRegions[*].encryption", + "paths": [] + }, + { + "name": "Microsoft.Compute/galleries/images/versions/publishingProfile.targetRegions[*].encryption.osDiskImage", + "defaultPath": "properties.publishingProfile.targetRegions[*].encryption.osDiskImage", + "paths": [] + }, + { + "name": "Microsoft.Compute/galleries/images/versions/publishingProfile.targetRegions[*].encryption.osDiskImage.diskEncryptionSetId", + "defaultPath": "properties.publishingProfile.targetRegions[*].encryption.osDiskImage.diskEncryptionSetId", + "paths": [] + }, + { + "name": "Microsoft.Compute/galleries/images/versions/publishingProfile.targetRegions[*].encryption.dataDiskImages[*]", + "defaultPath": "properties.publishingProfile.targetRegions[*].encryption.dataDiskImages[*]", + "paths": [] + }, + { + "name": "Microsoft.Compute/galleries/images/versions/publishingProfile.targetRegions[*].encryption.dataDiskImages[*].diskEncryptionSetId", + "defaultPath": "properties.publishingProfile.targetRegions[*].encryption.dataDiskImages[*].diskEncryptionSetId", + "paths": [] + }, + { + "name": "Microsoft.Compute/galleries/images/versions/publishingProfile.targetRegions[*].encryption.dataDiskImages", + "defaultPath": "properties.publishingProfile.targetRegions[*].encryption.dataDiskImages", + "paths": [] + }, + { + "name": "Microsoft.Compute/galleries/images/versions/storageProfile.dataDiskImages[*]", + "defaultPath": "properties.storageProfile.dataDiskImages[*]", + "paths": [] + }, + { + "name": "Microsoft.Compute/galleries/images/versions/storageProfile.dataDiskImages", + "defaultPath": "properties.storageProfile.dataDiskImages", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.DataFactory", + "resourceTypes": [ + { + "resourceType": "factories/linkedservices", + "capabilities": "None", + "aliases": [ + { + "name": "Microsoft.DataFactory/factories/linkedservices/type", + "defaultPath": "properties.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/typeProperties.connectionString", + "defaultPath": "properties.typeProperties.connectionString", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/typeProperties.connectionString.type", + "defaultPath": "properties.typeProperties.connectionString.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/typeProperties.encryptedCredential", + "defaultPath": "properties.typeProperties.encryptedCredential", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/SqlServer.typeProperties.password", + "defaultPath": "properties.typeProperties.password", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/SqlServer.typeProperties.password.type", + "defaultPath": "properties.typeProperties.password.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/AzureSqlDW.typeProperties.servicePrincipalKey.type", + "defaultPath": "properties.typeProperties.servicePrincipalKey.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/AzureSearch.typeProperties.key.type", + "defaultPath": "properties.typeProperties.key.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/AzureStorage.typeProperties.sasUri", + "defaultPath": "properties.typeProperties.sasUri", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/AzureStorage.typeProperties.sasUri.type", + "defaultPath": "properties.typeProperties.sasUri.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/AzureBlobStorage.typeProperties.servicePrincipalKey", + "defaultPath": "properties.typeProperties.servicePrincipalKey", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/AzureBlobStorage.typeProperties.servicePrincipalKey.type", + "defaultPath": "properties.typeProperties.servicePrincipalKey.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/AzureStorage.typeProperties.accountKey", + "defaultPath": "properties.typeProperties.accountKey", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/CosmosDb.typeProperties.accountKey.type", + "defaultPath": "properties.typeProperties.accountKey.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/AmazonMWS.typeProperties.mwsAuthToken.type", + "defaultPath": "properties.typeProperties.mwsAuthToken.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/AmazonMWS.typeProperties.secretKey.type", + "defaultPath": "properties.typeProperties.secretKey.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/AmazonS3.typeProperties.secretAccessKey.type", + "defaultPath": "properties.typeProperties.secretAccessKey.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/Dynamics.typeProperties.servicePrincipalCredential", + "defaultPath": "properties.typeProperties.servicePrincipalCredential", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/Dynamics.typeProperties.servicePrincipalCredential.type", + "defaultPath": "properties.typeProperties.servicePrincipalCredential.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/Hubspot.typeProperties.accessToken", + "defaultPath": "properties.typeProperties.accessToken", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/Hubspot.typeProperties.accessToken.type", + "defaultPath": "properties.typeProperties.accessToken.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/Odbc.typeProperties.credential.type", + "defaultPath": "properties.typeProperties.credential.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/GoogleAdWords.typeProperties.developerToken.type", + "defaultPath": "properties.typeProperties.developerToken.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/GoogleBigQuery.typeProperties.clientSecret.type", + "defaultPath": "properties.typeProperties.clientSecret.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/GoogleBigQuery.typeProperties.refreshToken.type", + "defaultPath": "properties.typeProperties.refreshToken.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/OData.typeProperties.servicePrincipalEmbeddedCert.type", + "defaultPath": "properties.typeProperties.servicePrincipalEmbeddedCert.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/OData.typeProperties.servicePrincipalEmbeddedCertPassword.type", + "defaultPath": "properties.typeProperties.servicePrincipalEmbeddedCertPassword.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/Salesforce.typeProperties.securityToken.type", + "defaultPath": "properties.typeProperties.securityToken.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/Sftp.typeProperties.passPhrase.type", + "defaultPath": "properties.typeProperties.passPhrase.type", + "paths": [] + }, + { + "name": "Microsoft.DataFactory/factories/linkedservices/Sftp.typeProperties.privateKeyContent.type", + "defaultPath": "properties.typeProperties.privateKeyContent.type", + "paths": [] + } + ] + } + ] + } +] \ No newline at end of file diff --git a/tests/azure_policy/aliases/versioned_aliases.json b/tests/azure_policy/aliases/versioned_aliases.json new file mode 100644 index 0000000..d474dd8 --- /dev/null +++ b/tests/azure_policy/aliases/versioned_aliases.json @@ -0,0 +1,230 @@ +[ + { + "namespace": "Microsoft.Test", + "resourceTypes": [ + { + "resourceType": "versionedResources", + "aliases": [ + { + "name": "Microsoft.Test/versionedResources/simpleProp", + "defaultPath": "properties.simpleProp", + "paths": [] + }, + { + "name": "Microsoft.Test/versionedResources/accountType", + "defaultPath": "properties.accountType", + "paths": [ + { + "path": "properties.accountType", + "apiVersions": ["2015-06-15", "2015-01-01"] + }, + { + "path": "sku.name", + "apiVersions": ["2020-01-01", "2019-06-01"] + } + ] + }, + { + "name": "Microsoft.Test/versionedResources/config.threshold", + "defaultPath": "properties.config.threshold", + "paths": [ + { + "path": "properties.config.properties.threshold", + "apiVersions": ["2015-06-15", "2015-01-01"] + }, + { + "path": "properties.config.threshold", + "apiVersions": ["2020-01-01", "2019-06-01"] + } + ] + }, + { + "name": "Microsoft.Test/versionedResources/tier", + "defaultPath": "sku.tier", + "paths": [ + { + "path": "properties.pricingTier", + "apiVersions": ["2015-06-15", "2015-01-01"] + }, + { + "path": "sku.tier", + "apiVersions": ["2020-01-01", "2019-06-01"] + } + ] + }, + { + "name": "Microsoft.Test/versionedResources/operationMode", + "defaultPath": "properties.mode", + "paths": [ + { + "path": "properties.legacyMode", + "apiVersions": ["2015-01-01"] + }, + { + "path": "properties.settings.mode", + "apiVersions": ["2017-06-01"] + }, + { + "path": "properties.mode", + "apiVersions": ["2020-01-01"] + } + ] + } + ] + }, + { + "resourceType": "complexResources", + "aliases": [ + { + "name": "Microsoft.Test/complexResources/enabled", + "defaultPath": "properties.enabled", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/tags", + "defaultPath": "tags", + "paths": [] + }, + + { + "name": "Microsoft.Test/complexResources/rules", + "defaultPath": "properties.rules", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*]", + "defaultPath": "properties.rules[*]", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].name", + "defaultPath": "properties.rules[*].name", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].priority", + "defaultPath": "properties.rules[*].properties.priority", + "paths": [ + { + "path": "properties.rules[*].properties.priority", + "apiVersions": ["2015-01-01"] + }, + { + "path": "properties.rules[*].properties.prio", + "apiVersions": ["2020-01-01"] + } + ] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].action", + "defaultPath": "properties.rules[*].properties.action", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].protocol", + "defaultPath": "properties.rules[*].properties.protocol", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].sourcePort", + "defaultPath": "properties.rules[*].properties.sourcePort", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].destPort", + "defaultPath": "properties.rules[*].properties.destPort", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].direction", + "defaultPath": "properties.rules[*].properties.direction", + "paths": [] + }, + + { + "name": "Microsoft.Test/complexResources/rules[*].targets", + "defaultPath": "properties.rules[*].properties.targets", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].targets[*]", + "defaultPath": "properties.rules[*].properties.targets[*]", + "paths": [] + }, + + { + "name": "Microsoft.Test/complexResources/rules[*].filters", + "defaultPath": "properties.rules[*].properties.filters", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].filters[*]", + "defaultPath": "properties.rules[*].properties.filters[*]", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].filters[*].name", + "defaultPath": "properties.rules[*].properties.filters[*].properties.name", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/rules[*].filters[*].value", + "defaultPath": "properties.rules[*].properties.filters[*].properties.value", + "paths": [] + }, + + { + "name": "Microsoft.Test/complexResources/allowedIPs", + "defaultPath": "properties.allowedIPs", + "paths": [] + }, + { + "name": "Microsoft.Test/complexResources/allowedIPs[*]", + "defaultPath": "properties.allowedIPs[*]", + "paths": [] + }, + + { + "name": "Microsoft.Test/complexResources/config.retryCount", + "defaultPath": "properties.config.retryCount", + "paths": [ + { + "path": "properties.config.properties.retryCount", + "apiVersions": ["2015-01-01"] + }, + { + "path": "properties.config.retryCount", + "apiVersions": ["2020-01-01"] + } + ] + }, + { + "name": "Microsoft.Test/complexResources/config.timeout", + "defaultPath": "properties.config.timeout", + "paths": [] + }, + + { + "name": "Microsoft.Test/complexResources/settings.encryption.enabled", + "defaultPath": "properties.settings.encryption.enabled", + "paths": [ + { + "path": "properties.settings.encryption.enabled", + "apiVersions": ["2020-01-01"] + }, + { + "path": "properties.encryptionEnabled", + "apiVersions": ["2015-01-01"] + } + ] + }, + { + "name": "Microsoft.Test/complexResources/settings.encryption.keyVaultId", + "defaultPath": "properties.settings.encryption.keyVaultId", + "paths": [] + } + ] + } + ] + } +] diff --git a/tests/azure_policy/mod.rs b/tests/azure_policy/mod.rs new file mode 100644 index 0000000..00bb5d0 --- /dev/null +++ b/tests/azure_policy/mod.rs @@ -0,0 +1,4 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +mod normalization; diff --git a/tests/azure_policy/normalization/cases/data_plane_advanced.yaml b/tests/azure_policy/normalization/cases/data_plane_advanced.yaml new file mode 100644 index 0000000..e6adeb3 --- /dev/null +++ b/tests/azure_policy/normalization/cases/data_plane_advanced.yaml @@ -0,0 +1,170 @@ +# Extended data-plane manifest tests covering shapes beyond the basic +# KeyVault case: multiple resource types, top-level aliases, nested +# objects, array aliases, and schemaVersions. + +data_manifest_json: | + { + "dataNamespace": "Microsoft.DataFactory.Data", + "aliases": [ + { + "name": "Microsoft.DataFactory.Data/factories/pipelines/enabled", + "paths": [ + { + "path": "enabled", + "apiVersions": ["2018-06-01"] + } + ] + } + ], + "resourceTypeAliases": [ + { + "resourceType": "factories/pipelines", + "aliases": [ + { + "name": "Microsoft.DataFactory.Data/factories/pipelines/activities[*].type", + "paths": [ + { + "path": "activities[*].type", + "apiVersions": ["2018-06-01"] + } + ] + }, + { + "name": "Microsoft.DataFactory.Data/factories/pipelines/activities[*].name", + "paths": [ + { + "path": "activities[*].name", + "apiVersions": ["2018-06-01"] + } + ] + }, + { + "name": "Microsoft.DataFactory.Data/factories/pipelines/concurrency", + "paths": [ + { + "path": "concurrency", + "apiVersions": ["2018-06-01"] + } + ] + }, + { + "name": "Microsoft.DataFactory.Data/factories/pipelines/folder.name", + "paths": [ + { + "path": "folder.name", + "apiVersions": ["2018-06-01"] + } + ] + } + ] + }, + { + "resourceType": "factories/datasets", + "aliases": [ + { + "name": "Microsoft.DataFactory.Data/factories/datasets/linkedServiceName.referenceName", + "paths": [ + { + "path": "linkedServiceName.referenceName", + "schemaVersions": ["1"] + } + ] + }, + { + "name": "Microsoft.DataFactory.Data/factories/datasets/description", + "paths": [ + { + "path": "description", + "schemaVersions": ["1"] + } + ] + } + ] + } + ] + } + +cases: + - note: data-plane pipeline with top-level alias and scalar fields + use_registry_api: true + input: + type: "Microsoft.DataFactory.Data/factories/pipelines" + enabled: true + concurrency: 5 + folder: + name: "etl-jobs" + expected_normalized: + type: "Microsoft.DataFactory.Data/factories/pipelines" + enabled: true + concurrency: 5 + folder: + name: "etl-jobs" + round_trip: true + + - note: data-plane pipeline with array alias + use_registry_api: true + input: + type: "Microsoft.DataFactory.Data/factories/pipelines" + activities: + - name: CopyActivity1 + type: Copy + - name: WaitActivity1 + type: Wait + expected_normalized: + type: "Microsoft.DataFactory.Data/factories/pipelines" + activities: + - name: CopyActivity1 + type: Copy + - name: WaitActivity1 + type: Wait + round_trip: true + + - note: data-plane dataset with nested alias path + use_registry_api: true + input: + type: "Microsoft.DataFactory.Data/factories/datasets" + description: "Sales data" + linkedServiceName: + referenceName: "AzureBlobStorage1" + type: LinkedServiceReference + expected_normalized: + type: "Microsoft.DataFactory.Data/factories/datasets" + description: "Sales data" + linkedservicename: + referencename: "AzureBlobStorage1" + type: LinkedServiceReference + round_trip: true + expected_round_trip: + type: "Microsoft.DataFactory.Data/factories/datasets" + description: "Sales data" + linkedServiceName: + referenceName: "AzureBlobStorage1" + type: LinkedServiceReference + + - note: data-plane without aliases (unknown resource type in namespace) + use_registry_api: true + input: + type: "Microsoft.DataFactory.Data/factories/triggers" + recurrence: + frequency: Day + interval: 1 + expected_normalized: + type: "Microsoft.DataFactory.Data/factories/triggers" + recurrence: + frequency: Day + interval: 1 + round_trip: true + + - note: data-plane denormalize dataset restores casing + use_registry_api: true + input: + type: "Microsoft.DataFactory.Data/factories/datasets" + description: "Sales data" + linkedservicename: + referencename: "AzureBlobStorage1" + expected_denormalized: + type: "Microsoft.DataFactory.Data/factories/datasets" + description: "Sales data" + linkedServiceName: + referenceName: "AzureBlobStorage1" + reverse_round_trip: true diff --git a/tests/azure_policy/normalization/cases/data_plane_breadth.yaml b/tests/azure_policy/normalization/cases/data_plane_breadth.yaml new file mode 100644 index 0000000..962859b --- /dev/null +++ b/tests/azure_policy/normalization/cases/data_plane_breadth.yaml @@ -0,0 +1,154 @@ +# Broad data-plane compatibility tests covering multiple Azure data-plane +# namespaces and resource shapes. Each case exercises a distinct namespace +# to ensure the manifest-loading and data-plane normalization paths handle +# a variety of real-world patterns. + +data_manifest_json: | + { + "dataNamespace": "Microsoft.Kubernetes.Data", + "aliases": [], + "resourceTypeAliases": [ + { + "resourceType": "namespaces", + "aliases": [ + { + "name": "Microsoft.Kubernetes.Data/namespaces/labels", + "paths": [{ "path": "labels", "apiVersions": ["v1"] }] + }, + { + "name": "Microsoft.Kubernetes.Data/namespaces/annotations", + "paths": [{ "path": "annotations", "apiVersions": ["v1"] }] + } + ] + }, + { + "resourceType": "pods", + "aliases": [ + { + "name": "Microsoft.Kubernetes.Data/pods/containers[*].image", + "paths": [{ "path": "containers[*].image", "apiVersions": ["v1"] }] + }, + { + "name": "Microsoft.Kubernetes.Data/pods/containers[*].name", + "paths": [{ "path": "containers[*].name", "apiVersions": ["v1"] }] + }, + { + "name": "Microsoft.Kubernetes.Data/pods/containers[*].resources.limits.cpu", + "paths": [{ "path": "containers[*].resources.limits.cpu", "apiVersions": ["v1"] }] + }, + { + "name": "Microsoft.Kubernetes.Data/pods/hostNetwork", + "paths": [{ "path": "hostNetwork", "apiVersions": ["v1"] }] + } + ] + } + ] + } + +cases: + # ── Kubernetes namespaces: flat labels/annotations ── + - note: "k8s namespace with labels and annotations" + use_registry_api: true + input: + type: "Microsoft.Kubernetes.Data/namespaces" + labels: + app: web-frontend + version: "v2" + annotations: + owner: team-alpha + expected_normalized: + type: "Microsoft.Kubernetes.Data/namespaces" + labels: + app: web-frontend + version: "v2" + annotations: + owner: team-alpha + round_trip: true + + # ── Kubernetes pods: arrays with deeply nested fields ── + - note: "k8s pod with containers array and resource limits" + use_registry_api: true + input: + type: "Microsoft.Kubernetes.Data/pods" + hostNetwork: false + containers: + - name: app + image: "myregistry.azurecr.io/app:latest" + resources: + limits: + cpu: "500m" + memory: "256Mi" + - name: sidecar + image: "myregistry.azurecr.io/sidecar:v1" + resources: + limits: + cpu: "100m" + expected_normalized: + type: "Microsoft.Kubernetes.Data/pods" + hostnetwork: false + containers: + - name: app + image: "myregistry.azurecr.io/app:latest" + resources: + limits: + cpu: "500m" + memory: "256Mi" + - name: sidecar + image: "myregistry.azurecr.io/sidecar:v1" + resources: + limits: + cpu: "100m" + round_trip: true + + - note: "k8s pod denormalize restores casing" + use_registry_api: true + input: + type: "Microsoft.Kubernetes.Data/pods" + hostnetwork: true + containers: + - name: app + image: "nginx" + expected_denormalized: + type: "Microsoft.Kubernetes.Data/pods" + hostNetwork: true + containers: + - name: app + image: "nginx" + reverse_round_trip: true + + # ── Unknown resource type in known namespace ── + - note: "k8s unknown resource type passes through unchanged" + use_registry_api: true + input: + type: "Microsoft.Kubernetes.Data/services" + clusterIP: "10.0.0.1" + ports: + - port: 80 + targetPort: 8080 + expected_normalized: + type: "Microsoft.Kubernetes.Data/services" + clusterip: "10.0.0.1" + ports: + - port: 80 + targetport: 8080 + round_trip: true + # No aliases for this type, so casing is lost on round-trip. + expected_round_trip: + type: "Microsoft.Kubernetes.Data/services" + clusterip: "10.0.0.1" + ports: + - port: 80 + targetport: 8080 + + # ── Empty containers array ── + - note: "k8s pod with empty containers array" + use_registry_api: true + input: + type: "Microsoft.Kubernetes.Data/pods" + hostNetwork: false + containers: [] + expected_normalized: + type: "Microsoft.Kubernetes.Data/pods" + hostnetwork: false + containers: [] + round_trip: true diff --git a/tests/azure_policy/normalization/cases/data_plane_manifest.yaml b/tests/azure_policy/normalization/cases/data_plane_manifest.yaml new file mode 100644 index 0000000..2f7a040 --- /dev/null +++ b/tests/azure_policy/normalization/cases/data_plane_manifest.yaml @@ -0,0 +1,64 @@ +# Tests that exercise data-plane manifest loading and the data-plane +# normalization path (resource type contains ".Data/"). +# This covers load_data_policy_manifest_json() and the data-plane +# normalization branch in normalizer.rs. + +data_manifest_json: | + { + "dataNamespace": "Microsoft.KeyVault.Data", + "aliases": [], + "resourceTypeAliases": [ + { + "resourceType": "vaults/certificates", + "aliases": [ + { + "name": "Microsoft.KeyVault.Data/vaults/certificates/keySize", + "paths": [ + { + "path": "keySize", + "apiVersions": ["7.0"] + } + ] + }, + { + "name": "Microsoft.KeyVault.Data/vaults/certificates/attributes.expiresOn", + "paths": [ + { + "path": "attributes.expiresOn", + "apiVersions": ["7.0"] + } + ] + } + ] + } + ] + } + +cases: + - note: data-plane manifest normalize via registry API + use_registry_api: true + input: + type: "Microsoft.KeyVault.Data/vaults/certificates" + keySize: 2048 + attributes: + expiresOn: "2025-01-01T00:00:00Z" + expected_normalized: + type: "Microsoft.KeyVault.Data/vaults/certificates" + keysize: 2048 + attributes: + expireson: "2025-01-01T00:00:00Z" + round_trip: true + + - note: data-plane manifest round-trip via registry API + use_registry_api: true + input: + type: "Microsoft.KeyVault.Data/vaults/certificates" + keySize: 2048 + attributes: + expiresOn: "2025-01-01T00:00:00Z" + round_trip: true + expected_round_trip: + type: "Microsoft.KeyVault.Data/vaults/certificates" + keySize: 2048 + attributes: + expiresOn: "2025-01-01T00:00:00Z" diff --git a/tests/azure_policy/normalization/cases/denormalize_aliases.yaml b/tests/azure_policy/normalization/cases/denormalize_aliases.yaml new file mode 100644 index 0000000..df63e2c --- /dev/null +++ b/tests/azure_policy/normalization/cases/denormalize_aliases.yaml @@ -0,0 +1,319 @@ +aliases_json: | + [ + { + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/accessTier", + "defaultPath": "properties.accessTier", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/sku.name", + "defaultPath": "sku.name", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", + "defaultPath": "properties.securityRules[*].properties.access", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name", + "defaultPath": "properties.securityRules[*].name", + "paths": [] + } + ] + }, + { + "resourceType": "virtualNetworks", + "aliases": [ + { + "name": "Microsoft.Network/virtualNetworks/subnets[*].addressPrefix", + "defaultPath": "properties.subnets[*].properties.addressPrefix", + "paths": [] + }, + { + "name": "Microsoft.Network/virtualNetworks/subnets[*].name", + "defaultPath": "properties.subnets[*].name", + "paths": [] + }, + { + "name": "Microsoft.Network/virtualNetworks/subnets[*].ipConfigurations[*].privateIPAddress", + "defaultPath": "properties.subnets[*].properties.ipConfigurations[*].properties.privateIPAddress", + "paths": [] + }, + { + "name": "Microsoft.Network/virtualNetworks/subnets[*].ipConfigurations[*].name", + "defaultPath": "properties.subnets[*].properties.ipConfigurations[*].name", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Web", + "resourceTypes": [ + { + "resourceType": "sites", + "aliases": [ + { + "name": "Microsoft.Web/sites/isEnabled", + "defaultPath": "properties.isEnabled", + "paths": [ + { + "path": "properties.enabled", + "apiVersions": ["2020-01-01"] + } + ] + } + ] + } + ] + }, + { + "namespace": "test", + "resourceTypes": [ + { + "resourceType": "resource", + "aliases": [ + { + "name": "test/resource/type", + "defaultPath": "properties.type", + "paths": [] + } + ] + }, + { + "resourceType": "versionedEnvelope", + "aliases": [ + { + "name": "test/versionedEnvelope/items[*].status", + "defaultPath": "properties.items[*].properties.status", + "paths": [ + { + "path": "properties.items[*].status", + "apiVersions": ["2025-01-01"] + } + ] + }, + { + "name": "test/versionedEnvelope/items[*].name", + "defaultPath": "properties.items[*].name", + "paths": [] + } + ] + } + ] + } + ] + +cases: + - note: restores casing from aliases + input: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + supportshttpstrafficonly: true + accesstier: Hot + expected_denormalized: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + accessTier: Hot + reverse_round_trip: true + + - note: versioned path - default + input: + type: "Microsoft.Web/sites" + isenabled: true + expected_denormalized: + type: "Microsoft.Web/sites" + properties: + isEnabled: true + reverse_round_trip: true + + - note: versioned path - matching version + input: + type: "Microsoft.Web/sites" + isenabled: true + api_version: "2020-01-01" + expected_denormalized: + type: "Microsoft.Web/sites" + properties: + enabled: true + reverse_round_trip: true + expected_reverse_round_trip: + type: "Microsoft.Web/sites" + enabled: true + isenabled: true + + - note: collision safe key + input: + type: "test/resource" + _p_type: SubType + expected_denormalized: + type: "test/resource" + properties: + type: SubType + reverse_round_trip: true + + - note: sub-resource array + input: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + securityrules: + - name: rule1 + protocol: Tcp + access: Allow + - name: rule2 + protocol: "*" + access: Deny + expected_denormalized: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: rule1 + properties: + protocol: Tcp + access: Allow + - name: rule2 + properties: + protocol: "*" + access: Deny + reverse_round_trip: true + + - note: nested sub-resource arrays + input: + name: myVnet + type: "Microsoft.Network/virtualNetworks" + subnets: + - name: subnet1 + addressprefix: "10.0.0.0/24" + ipconfigurations: + - name: ipconfig1 + privateipaddress: "10.0.0.4" + expected_denormalized: + name: myVnet + type: "Microsoft.Network/virtualNetworks" + properties: + subnets: + - name: subnet1 + properties: + addressPrefix: "10.0.0.0/24" + ipConfigurations: + - name: ipconfig1 + properties: + privateIPAddress: "10.0.0.4" + reverse_round_trip: true + + - note: root level alias (sku.name) + input: + type: "Microsoft.Storage/storageAccounts" + sku: + name: Standard_LRS + expected_denormalized: + type: "Microsoft.Storage/storageAccounts" + sku: + name: Standard_LRS + reverse_round_trip: true + + - note: versioned envelope classification — default (status under properties) + resource_type: "test/versionedEnvelope" + sub_resource_arrays: ["items"] + input: + type: "test/versionedEnvelope" + items: + - name: item1 + status: active + expected_denormalized: + type: "test/versionedEnvelope" + properties: + items: + - name: item1 + properties: + status: active + + - note: versioned envelope classification — version promotes status to envelope + resource_type: "test/versionedEnvelope" + sub_resource_arrays: ["items"] + api_version: "2025-01-01" + input: + type: "test/versionedEnvelope" + items: + - name: item1 + status: active + expected_denormalized: + type: "test/versionedEnvelope" + properties: + items: + - name: item1 + status: active + + - note: versioned casing restoration from versioned alias paths + aliases_json: | + [ + { + "namespace": "Microsoft.Test", + "resourceTypes": [ + { + "resourceType": "widgets", + "aliases": [ + { + "name": "Microsoft.Test/widgets/items[*].rating", + "defaultPath": "properties.items[*].properties.oldField", + "paths": [ + { + "path": "properties.items[*].properties.RenamedField", + "apiVersions": ["2025-06-01"] + } + ] + }, + { + "name": "Microsoft.Test/widgets/items[*].name", + "defaultPath": "properties.items[*].name", + "paths": [] + } + ] + } + ] + } + ] + api_version: "2025-06-01" + input: + type: "Microsoft.Test/widgets" + items: + - name: w1 + rating: 5 + expected_denormalized: + type: "Microsoft.Test/widgets" + properties: + items: + - name: w1 + properties: + RenamedField: 5 diff --git a/tests/azure_policy/normalization/cases/denormalize_basic.yaml b/tests/azure_policy/normalization/cases/denormalize_basic.yaml new file mode 100644 index 0000000..430d6ee --- /dev/null +++ b/tests/azure_policy/normalization/cases/denormalize_basic.yaml @@ -0,0 +1,83 @@ +cases: + - note: wraps properties + input: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + supportshttpstrafficonly: true + ishnsenabled: false + expected_denormalized: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + properties: + supportshttpstrafficonly: true + ishnsenabled: false + reverse_round_trip: true + + - note: non-object returns clone + input: "just a string" + expected_denormalized: "just a string" + reverse_round_trip: true + + - note: empty normalized + input: {} + expected_denormalized: {} + reverse_round_trip: true + + - note: preserves root fields + input: + name: r + type: t + location: l + kind: k + id: "/sub/rg/r" + tags: + env: prod + identity: + type: SystemAssigned + principalid: pid-123 + userassignedidentities: + /subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {} + sku: + name: Basic + plan: + name: p1 + zones: ["1", "2"] + managedby: "/sub/other" + etag: "W/\"abc\"" + apiversion: "2023-01-01" + fullname: parent/child + systemdata: + createdby: admin + extendedlocation: + name: edge1 + type: EdgeZone + expected_denormalized: + name: r + type: t + location: l + kind: k + id: "/sub/rg/r" + tags: + env: prod + identity: + type: SystemAssigned + principalId: pid-123 + userAssignedIdentities: + /subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {} + sku: + name: Basic + plan: + name: p1 + zones: ["1", "2"] + managedBy: "/sub/other" + etag: "W/\"abc\"" + apiVersion: "2023-01-01" + fullName: parent/child + systemData: + createdBy: admin + extendedLocation: + name: edge1 + type: EdgeZone + reverse_round_trip: true diff --git a/tests/azure_policy/normalization/cases/edge_cases.yaml b/tests/azure_policy/normalization/cases/edge_cases.yaml new file mode 100644 index 0000000..092e77a --- /dev/null +++ b/tests/azure_policy/normalization/cases/edge_cases.yaml @@ -0,0 +1,201 @@ +# Tests pinning behavior for ambiguous or malformed inputs that could +# be produced by external callers rather than the normalizer itself. +# +# The normalizer / denormalizer make assumptions about their input shape +# (e.g., keys are fully lowercased after normalization). These tests +# document what happens when those assumptions are violated, without +# asserting the behavior is "correct" per se — rather, they pin it so +# regressions are detected. + +aliases_json: | + [ + { + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/accessTier", + "defaultPath": "properties.accessTier", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", + "defaultPath": "properties.securityRules[*].properties.access", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name", + "defaultPath": "properties.securityRules[*].name", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Web", + "resourceTypes": [ + { + "resourceType": "sites", + "aliases": [ + { + "name": "Microsoft.Web/sites/isEnabled", + "defaultPath": "properties.isEnabled", + "paths": [ + { + "path": "properties.enabled", + "apiVersions": ["2020-01-01"] + } + ] + } + ] + } + ] + } + ] + +cases: + # ── Normalize: ARM input with both root and properties having same field ── + + - note: "normalize: both root 'name' and properties.name (root wins)" + input: + name: root-name + type: "Microsoft.Storage/storageAccounts" + properties: + name: props-name + accessTier: Hot + expected_normalized: + name: root-name + type: "Microsoft.Storage/storageAccounts" + accesstier: Hot + + # ── Normalize: extra fields not in alias catalog or ROOT_FIELDS ── + + - note: "normalize: extra non-aliased properties are kept (lowercased)" + input: + name: test + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + customUnknownField: 42 + expected_normalized: + name: test + type: "Microsoft.Storage/storageAccounts" + supportshttpstrafficonly: true + customunknownfield: 42 + + # ── Denormalize: input has mixed casing (externally constructed) ── + + - note: "denormalize: mixed-case keys still resolve via aliases" + input: + name: test + type: "Microsoft.Storage/storageAccounts" + SupportsHttpsTrafficOnly: true + expected_denormalized: + name: test + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + + # ── Denormalize: input has extra fields not in alias catalog ── + + - note: "denormalize: unknown fields go under properties (control-plane)" + input: + name: test + type: "Microsoft.Storage/storageAccounts" + unknownfield: 123 + accesstier: Hot + expected_denormalized: + name: test + type: "Microsoft.Storage/storageAccounts" + properties: + unknownfield: 123 + accessTier: Hot + + # ── Denormalize with versioned path: both alias-named and ARM-named present ── + + - note: "denormalize: both isenabled and enabled present, alias wins" + input: + type: "Microsoft.Web/sites" + isenabled: true + enabled: false + api_version: "2020-01-01" + expected_denormalized: + type: "Microsoft.Web/sites" + properties: + enabled: true + + - note: "denormalize: both isenabled and enabled present, default path" + input: + type: "Microsoft.Web/sites" + isenabled: true + enabled: false + expected_denormalized: + type: "Microsoft.Web/sites" + properties: + isEnabled: true + enabled: false + + # ── Normalize: ARM resource with empty type ── + + - note: "normalize: empty type field, no alias match" + input: + name: test + type: "" + properties: + foo: bar + expected_normalized: + name: test + type: "" + foo: bar + + # ── Normalize: ARM resource with null properties ── + + - note: "normalize: null properties value" + input: + name: test + properties: null + expected_normalized: + name: test + + # ── Denormalize: sub-resource array with elements already containing properties ── + + - note: "denormalize: elements already have properties wrapper (double-wrap)" + input: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + securityrules: + - name: rule1 + properties: + protocol: Tcp + expected_denormalized: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: rule1 + properties: + properties: + protocol: Tcp diff --git a/tests/azure_policy/normalization/cases/envelope_pipeline.yaml b/tests/azure_policy/normalization/cases/envelope_pipeline.yaml new file mode 100644 index 0000000..3661b30 --- /dev/null +++ b/tests/azure_policy/normalization/cases/envelope_pipeline.yaml @@ -0,0 +1,46 @@ +aliases_json: | + [ + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "paths": [] + } + ] + } + ] + } + ] + +cases: + - note: normalize-and-wrap full pipeline + input: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: rule1 + properties: + protocol: Tcp + context: + resourceGroup: + name: rg1 + parameters: + env: prod + expected_envelope: + resource: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + securityrules: + - name: rule1 + protocol: Tcp + context: + resourceGroup: + name: rg1 + parameters: + env: prod diff --git a/tests/azure_policy/normalization/cases/malformed_input.yaml b/tests/azure_policy/normalization/cases/malformed_input.yaml new file mode 100644 index 0000000..684336f --- /dev/null +++ b/tests/azure_policy/normalization/cases/malformed_input.yaml @@ -0,0 +1,233 @@ +# Tests for malformed or unexpected *normalized* input fed to the +# denormalizer. These pin the current behavior when the denormalizer +# receives externally-produced JSON that violates the normalizer's +# implicit contract (e.g., keys not lowercased, wrong value types, +# missing type field, non-object input). +# +# These are NOT correctness assertions -- they document what the +# denormalizer actually produces so regressions are caught. + +aliases_json: | + [ + { + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/accessTier", + "defaultPath": "properties.accessTier", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name", + "defaultPath": "properties.securityRules[*].name", + "paths": [] + } + ] + } + ] + } + ] + +cases: + # ── Non-object input ── + # The denormalizer should return the value as-is when it's not an object. + + - note: "denormalize: string input returned as-is" + input: "just a string" + expected_denormalized: "just a string" + + - note: "denormalize: integer input returned as-is" + input: 42 + expected_denormalized: 42 + + - note: "denormalize: array input returned as-is" + input: [1, 2, 3] + expected_denormalized: [1, 2, 3] + + - note: "denormalize: null input returned as-is" + input: null + expected_denormalized: null + + - note: "denormalize: boolean input returned as-is" + input: true + expected_denormalized: true + + # ── Missing type field ── + # Without a type, no alias resolution occurs; fields are still placed + # under `properties` (control-plane default) but casing is not restored. + + - note: "denormalize: no type field, fields go under properties" + input: + name: test + unknownfield: 42 + expected_denormalized: + name: test + properties: + unknownfield: 42 + + # ── Empty object ── + + - note: "denormalize: empty object produces empty object" + input: {} + expected_denormalized: {} + + # ── Object with only type field ── + + - note: "denormalize: only type field, empty properties" + input: + type: "Microsoft.Storage/storageAccounts" + expected_denormalized: + type: "Microsoft.Storage/storageAccounts" + + # ── Normalize: non-object input ── + + - note: "normalize: string input returned as-is" + input: "just a string" + expected_normalized: "just a string" + + - note: "normalize: integer input returned as-is" + input: 42 + expected_normalized: 42 + + - note: "normalize: null input returned as-is" + input: null + expected_normalized: null + + # ── Normalize: empty properties object ── + + - note: "normalize: empty properties object" + input: + name: test + type: "Microsoft.Storage/storageAccounts" + properties: {} + expected_normalized: + name: test + type: "Microsoft.Storage/storageAccounts" + + # ── Normalize: properties is a non-object value ── + + - note: "normalize: properties is a string (treated as non-object, ignored)" + input: + name: test + type: "Microsoft.Storage/storageAccounts" + properties: "not an object" + expected_normalized: + name: test + type: "Microsoft.Storage/storageAccounts" + + - note: "normalize: properties is an array (treated as non-object, ignored)" + input: + name: test + type: "Microsoft.Storage/storageAccounts" + properties: [1, 2, 3] + expected_normalized: + name: test + type: "Microsoft.Storage/storageAccounts" + + # ── Denormalize: value types preserved through round trip ── + + - note: "denormalize: numeric value zero" + input: + type: "Microsoft.Storage/storageAccounts" + accesstier: 0 + expected_denormalized: + type: "Microsoft.Storage/storageAccounts" + properties: + accessTier: 0 + + - note: "denormalize: boolean false aliased field" + input: + type: "Microsoft.Storage/storageAccounts" + supportshttpstrafficonly: false + expected_denormalized: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: false + + - note: "denormalize: null-valued aliased field" + input: + type: "Microsoft.Storage/storageAccounts" + supportshttpstrafficonly: null + expected_denormalized: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: null + + # ── Sub-resource array with non-array value ── + + - note: "denormalize: expected sub-resource array is a string" + input: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + securityrules: "not an array" + expected_denormalized: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: "not an array" + + - note: "denormalize: expected sub-resource array is null" + input: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + securityrules: null + expected_denormalized: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: null + + # ── Sub-resource array with non-object elements ── + + - note: "denormalize: sub-resource contains scalar elements" + input: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + securityrules: + - "just a string" + - 42 + expected_denormalized: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - "just a string" + - 42 + + # ── Deeply nested null in normalize path ── + + - note: "normalize: deeply nested null value preserved" + input: + name: test + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: null + accessTier: null + expected_normalized: + name: test + type: "Microsoft.Storage/storageAccounts" + supportshttpstrafficonly: null + accesstier: null diff --git a/tests/azure_policy/normalization/cases/normalize_basic.yaml b/tests/azure_policy/normalization/cases/normalize_basic.yaml new file mode 100644 index 0000000..b26de5e --- /dev/null +++ b/tests/azure_policy/normalization/cases/normalize_basic.yaml @@ -0,0 +1,258 @@ +cases: + - note: flattens root properties + input: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + properties: + supportsHttpsTrafficOnly: true + isHnsEnabled: false + expected_normalized: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + supportshttpstrafficonly: true + ishnsenabled: false + round_trip: true + expected_round_trip: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + properties: + supportshttpstrafficonly: true + ishnsenabled: false + + - note: preserves root level precedence + input: + name: root-name + properties: + name: props-name + expected_normalized: + name: root-name + round_trip: true + expected_round_trip: + name: root-name + + - note: leaves plain arrays alone + input: + name: test + properties: + networkAcls: + ipRules: + - value: "10.0.0.1" + action: Allow + - value: "10.0.0.2" + action: Deny + expected_normalized: + name: test + networkacls: + iprules: + - value: "10.0.0.1" + action: Allow + - value: "10.0.0.2" + action: Deny + round_trip: true + expected_round_trip: + name: test + properties: + networkacls: + iprules: + - value: "10.0.0.1" + action: Allow + - value: "10.0.0.2" + action: Deny + + - note: handles sku at root + input: + name: test + sku: + name: Standard_LRS + tier: Standard + properties: + supportsHttpsTrafficOnly: true + expected_normalized: + name: test + sku: + name: Standard_LRS + tier: Standard + supportshttpstrafficonly: true + round_trip: true + expected_round_trip: + name: test + sku: + name: Standard_LRS + tier: Standard + properties: + supportshttpstrafficonly: true + + - note: non-object returns clone + input: "just a string" + expected_normalized: "just a string" + round_trip: true + + - note: empty properties + input: + name: test + properties: {} + expected_normalized: + name: test + round_trip: true + expected_round_trip: + name: test + + - note: missing properties + input: + name: test + location: eastus + expected_normalized: + name: test + location: eastus + round_trip: true + + - note: preserves all root fields + input: + name: r + type: t + location: l + kind: k + id: "/sub/rg/r" + tags: + env: prod + identity: + type: SystemAssigned + principalId: pid-123 + userAssignedIdentities: + /subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {} + sku: + name: Basic + plan: + name: p1 + zones: ["1", "2"] + managedBy: "/sub/other" + etag: "W/\"abc\"" + apiVersion: "2023-01-01" + fullName: parent/child + systemData: + createdBy: admin + extendedLocation: + name: edge1 + type: EdgeZone + properties: + someProp: true + expected_normalized: + name: r + type: t + location: l + kind: k + id: "/sub/rg/r" + tags: + env: prod + identity: + type: SystemAssigned + principalid: pid-123 + userassignedidentities: + /subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {} + sku: + name: Basic + plan: + name: p1 + zones: ["1", "2"] + managedby: "/sub/other" + etag: "W/\"abc\"" + apiversion: "2023-01-01" + fullname: parent/child + systemdata: + createdby: admin + extendedlocation: + name: edge1 + type: EdgeZone + someprop: true + round_trip: true + expected_round_trip: + name: r + type: t + location: l + kind: k + id: "/sub/rg/r" + tags: + env: prod + identity: + type: SystemAssigned + principalId: pid-123 + userAssignedIdentities: + /subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {} + sku: + name: Basic + plan: + name: p1 + zones: ["1", "2"] + managedBy: "/sub/other" + etag: "W/\"abc\"" + apiVersion: "2023-01-01" + fullName: parent/child + systemData: + createdBy: admin + extendedLocation: + name: edge1 + type: EdgeZone + properties: + someprop: true + + - note: primitive array pass through + input: + name: test + properties: + allowedIPs: ["10.0.0.1", "10.0.0.2", "10.0.0.3"] + expected_normalized: + name: test + allowedips: ["10.0.0.1", "10.0.0.2", "10.0.0.3"] + round_trip: true + expected_round_trip: + name: test + properties: + allowedips: ["10.0.0.1", "10.0.0.2", "10.0.0.3"] + + - note: deeply nested object no sub-resource + input: + name: test + properties: + networkAcls: + defaultAction: Deny + virtualNetworkRules: + - id: "/vnet/subnet1" + action: Allow + expected_normalized: + name: test + networkacls: + defaultaction: Deny + virtualnetworkrules: + - id: "/vnet/subnet1" + action: Allow + round_trip: true + expected_round_trip: + name: test + properties: + networkacls: + defaultaction: Deny + virtualnetworkrules: + - id: "/vnet/subnet1" + action: Allow + + - note: unknown root fields are dropped (not in ROOT_FIELDS) + input: + name: test + type: "Microsoft.Foo/bars" + fooExtension: + barBaz: 1 + properties: + enabled: true + expected_normalized: + name: test + type: "Microsoft.Foo/bars" + enabled: true + round_trip: true + expected_round_trip: + name: test + type: "Microsoft.Foo/bars" + properties: + enabled: true diff --git a/tests/azure_policy/normalization/cases/normalize_envelope.yaml b/tests/azure_policy/normalization/cases/normalize_envelope.yaml new file mode 100644 index 0000000..7d8da72 --- /dev/null +++ b/tests/azure_policy/normalization/cases/normalize_envelope.yaml @@ -0,0 +1,26 @@ +cases: + - note: envelope defaults + input: + name: x + expected_envelope: + resource: + name: x + context: {} + parameters: {} + + - note: envelope with context and parameters + input: + name: x + context: + resourceGroup: + name: rg1 + parameters: + env: prod + expected_envelope: + resource: + name: x + context: + resourceGroup: + name: rg1 + parameters: + env: prod diff --git a/tests/azure_policy/normalization/cases/normalize_sub_resources.yaml b/tests/azure_policy/normalization/cases/normalize_sub_resources.yaml new file mode 100644 index 0000000..21e81f9 --- /dev/null +++ b/tests/azure_policy/normalization/cases/normalize_sub_resources.yaml @@ -0,0 +1,123 @@ +cases: + - note: flattens sub-resource arrays + input: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: rule1 + properties: + protocol: Tcp + access: Allow + - name: rule2 + properties: + protocol: "*" + access: Deny + sub_resource_arrays: ["securityRules"] + resource_type: "Microsoft.Network/networkSecurityGroups" + expected_normalized: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + securityrules: + - name: rule1 + protocol: Tcp + access: Allow + - name: rule2 + protocol: "*" + access: Deny + round_trip: true + expected_round_trip: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityrules: + - name: rule1 + properties: + protocol: Tcp + access: Allow + - name: rule2 + properties: + protocol: "*" + access: Deny + + - note: nested sub-resource arrays + input: + name: myVnet + properties: + subnets: + - name: subnet1 + properties: + addressPrefix: "10.0.0.0/24" + ipConfigurations: + - name: ipconfig1 + properties: + privateIPAddress: "10.0.0.4" + sub_resource_arrays: ["subnets", "subnets.ipConfigurations"] + resource_type: "Microsoft.Network/virtualNetworks" + expected_normalized: + name: myVnet + subnets: + - name: subnet1 + addressprefix: "10.0.0.0/24" + ipconfigurations: + - name: ipconfig1 + privateipaddress: "10.0.0.4" + round_trip: true + expected_round_trip: + name: myVnet + properties: + subnets: + - name: subnet1 + properties: + addressprefix: "10.0.0.0/24" + ipconfigurations: + - name: ipconfig1 + properties: + privateipaddress: "10.0.0.4" + + - note: sub-resource element without properties + input: + name: test + properties: + items: + - name: plain-object + enabled: true + sub_resource_arrays: ["items"] + resource_type: test + expected_normalized: + name: test + items: + - name: plain-object + enabled: true + round_trip: true + expected_round_trip: + name: test + properties: + items: + - name: plain-object + properties: + enabled: true + + - note: case-insensitive sub-resource match + input: + name: test + properties: + securityRules: + - name: r1 + properties: + protocol: Tcp + sub_resource_arrays: ["SecurityRules"] + resource_type: test + expected_normalized: + name: test + securityrules: + - name: r1 + protocol: Tcp + round_trip: true + expected_round_trip: + name: test + properties: + securityrules: + - name: r1 + properties: + protocol: Tcp diff --git a/tests/azure_policy/normalization/cases/registry_api.yaml b/tests/azure_policy/normalization/cases/registry_api.yaml new file mode 100644 index 0000000..c18fc20 --- /dev/null +++ b/tests/azure_policy/normalization/cases/registry_api.yaml @@ -0,0 +1,127 @@ +# Tests that exercise the public AliasRegistry API (normalize, denormalize, +# normalize_and_wrap) rather than the low-level *_with_aliases functions. +# This ensures the registry lookup path and public entry points are covered. + +aliases_json: | + [ + { + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/isHnsEnabled", + "defaultPath": "properties.isHnsEnabled", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name", + "defaultPath": "properties.securityRules[*].name", + "paths": [] + } + ] + } + ] + } + ] + +cases: + - note: registry API normalize + use_registry_api: true + input: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + properties: + supportsHttpsTrafficOnly: true + isHnsEnabled: false + expected_normalized: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + supportshttpstrafficonly: true + ishnsenabled: false + round_trip: true + + - note: registry API denormalize + use_registry_api: true + input: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + supportshttpstrafficonly: true + ishnsenabled: false + expected_denormalized: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + properties: + supportsHttpsTrafficOnly: true + isHnsEnabled: false + reverse_round_trip: true + + - note: registry API normalize_and_wrap (envelope) + use_registry_api: true + input: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: rule1 + properties: + protocol: Tcp + context: + resourceGroup: + name: rg1 + expected_envelope: + resource: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + securityrules: + - name: rule1 + protocol: Tcp + context: + resourceGroup: + name: rg1 + parameters: {} + + - note: registry API round-trip sub-resource + use_registry_api: true + input: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: rule1 + properties: + protocol: Tcp + round_trip: true + expected_round_trip: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: rule1 + properties: + protocol: Tcp diff --git a/tests/azure_policy/normalization/cases/round_trip.yaml b/tests/azure_policy/normalization/cases/round_trip.yaml new file mode 100644 index 0000000..990a255 --- /dev/null +++ b/tests/azure_policy/normalization/cases/round_trip.yaml @@ -0,0 +1,449 @@ +aliases_json: | + [ + { + "namespace": "Microsoft.Storage", + "resourceTypes": [ + { + "resourceType": "storageAccounts", + "aliases": [ + { + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + }, + { + "name": "Microsoft.Storage/storageAccounts/isHnsEnabled", + "defaultPath": "properties.isHnsEnabled", + "paths": [] + } + ] + } + ] + }, + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "networkSecurityGroups", + "aliases": [ + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "defaultPath": "properties.securityRules[*].properties.protocol", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", + "defaultPath": "properties.securityRules[*].properties.access", + "paths": [] + }, + { + "name": "Microsoft.Network/networkSecurityGroups/securityRules[*].name", + "defaultPath": "properties.securityRules[*].name", + "paths": [] + } + ] + } + ] + } + ] + +cases: + - note: round-trip simple resource + input: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + sku: + name: Standard_LRS + properties: + supportsHttpsTrafficOnly: true + isHnsEnabled: false + round_trip: true + expected_round_trip: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + sku: + name: Standard_LRS + properties: + supportsHttpsTrafficOnly: true + isHnsEnabled: false + + - note: round-trip sub-resource + input: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: rule1 + properties: + protocol: Tcp + access: Allow + round_trip: true + expected_round_trip: + name: myNsg + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: rule1 + properties: + protocol: Tcp + access: Allow + + - note: round-trip versioned array alias (element field remap) + aliases_json: | + [ + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "firewallPolicies", + "aliases": [ + { + "name": "Microsoft.Network/firewallPolicies/rules[*].priority", + "defaultPath": "properties.rules[*].properties.priority", + "paths": [ + { + "path": "properties.rules[*].properties.prio", + "apiVersions": ["2021-01-01"] + } + ] + }, + { + "name": "Microsoft.Network/firewallPolicies/rules[*].name", + "defaultPath": "properties.rules[*].name", + "paths": [] + } + ] + } + ] + } + ] + input: + name: myPolicy + type: "Microsoft.Network/firewallPolicies" + properties: + rules: + - name: rule1 + properties: + prio: 100 + api_version: "2021-01-01" + round_trip: true + expected_round_trip: + name: myPolicy + type: "Microsoft.Network/firewallPolicies" + properties: + rules: + - name: rule1 + properties: + prio: 100 + + # Regression: default and versioned paths both produce exactly one element + # remap (same count), but the source field differs. A length-only comparison + # would silently reuse the default aggregate, producing incorrect results for + # the versioned API version. + - note: round-trip versioned remap with same count but different source field + aliases_json: | + [ + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "firewallPolicies", + "aliases": [ + { + "name": "Microsoft.Network/firewallPolicies/rules[*].priority", + "defaultPath": "properties.rules[*].properties.oldA", + "paths": [ + { + "path": "properties.rules[*].properties.oldB", + "apiVersions": ["2023-06-01"] + } + ] + }, + { + "name": "Microsoft.Network/firewallPolicies/rules[*].name", + "defaultPath": "properties.rules[*].name", + "paths": [] + } + ] + } + ] + } + ] + input: + name: myPolicy + type: "Microsoft.Network/firewallPolicies" + properties: + rules: + - name: rule1 + properties: + oldB: 100 + api_version: "2023-06-01" + round_trip: true + expected_round_trip: + name: myPolicy + type: "Microsoft.Network/firewallPolicies" + properties: + rules: + - name: rule1 + properties: + oldB: 100 + + - note: round-trip systemData and extendedLocation + input: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + identity: + type: SystemAssigned + principalId: pid-123 + userAssignedIdentities: + /subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {} + systemData: + createdBy: user@example.com + createdByType: User + createdAt: "2023-01-01T00:00:00Z" + extendedLocation: + name: edge-site-1 + type: EdgeZone + properties: + supportsHttpsTrafficOnly: true + expected_normalized: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + identity: + type: SystemAssigned + principalid: pid-123 + userassignedidentities: + /subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {} + systemdata: + createdby: user@example.com + createdbytype: User + createdat: "2023-01-01T00:00:00Z" + extendedlocation: + name: edge-site-1 + type: EdgeZone + supportshttpstrafficonly: true + round_trip: true + expected_round_trip: + name: myStorage + type: "Microsoft.Storage/storageAccounts" + location: westus2 + identity: + type: SystemAssigned + principalId: pid-123 + userAssignedIdentities: + /subscriptions/Sub/resourceGroups/Rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/Uai1: {} + systemData: + createdBy: user@example.com + createdByType: User + createdAt: "2023-01-01T00:00:00Z" + extendedLocation: + name: edge-site-1 + type: EdgeZone + properties: + supportsHttpsTrafficOnly: true + + # Regression: element remap must remove the stale ARM source field from the + # normalized output. Without cleanup, both the alias short name AND the + # original ARM leaf key survive; casing restoration during denormalization + # then produces a duplicate key (e.g. both "prio" and "priority"). + - note: element remap removes stale ARM source field + aliases_json: | + [ + { + "namespace": "Microsoft.Network", + "resourceTypes": [ + { + "resourceType": "firewallPolicies", + "aliases": [ + { + "name": "Microsoft.Network/firewallPolicies/rules[*].priority", + "defaultPath": "properties.rules[*].properties.prio", + "paths": [] + }, + { + "name": "Microsoft.Network/firewallPolicies/rules[*].name", + "defaultPath": "properties.rules[*].name", + "paths": [] + } + ] + } + ] + } + ] + input: + name: myPolicy + type: "Microsoft.Network/firewallPolicies" + properties: + rules: + - name: rule1 + properties: + prio: 42 + # Normalize: "prio" (ARM leaf) → "priority" (alias short name). + # The stale "prio" key must be removed from the normalized element. + expected_normalized: + name: myPolicy + type: "Microsoft.Network/firewallPolicies" + rules: + - name: rule1 + priority: 42 + # Round-trip: normalize → denormalize should restore ARM structure. + round_trip: true + expected_round_trip: + name: myPolicy + type: "Microsoft.Network/firewallPolicies" + properties: + rules: + - name: rule1 + properties: + prio: 42 + + # Regression: same stale-field-cleanup scenario but with a dotted ARM leaf + # path (e.g. "config.value" rather than a single segment "prio"). + # The remove_element_field helper must navigate to the parent object and + # remove the leaf key via remove_at_dotted_path. + - note: element remap removes stale dotted ARM source field + aliases_json: | + [ + { + "namespace": "Microsoft.Test", + "resourceTypes": [ + { + "resourceType": "widgets", + "aliases": [ + { + "name": "Microsoft.Test/widgets/items[*].rating", + "defaultPath": "properties.items[*].properties.config.score", + "paths": [] + }, + { + "name": "Microsoft.Test/widgets/items[*].name", + "defaultPath": "properties.items[*].name", + "paths": [] + } + ] + } + ] + } + ] + input: + type: "Microsoft.Test/widgets" + properties: + items: + - name: w1 + properties: + config: + score: 5 + # Normalize: "config.score" (dotted ARM leaf) → "rating" (alias short name). + # The stale "config.score" path must be removed; only "rating" should remain. + expected_normalized: + type: "Microsoft.Test/widgets" + items: + - name: w1 + config: {} + rating: 5 + # Round-trip: normalize → denormalize should restore ARM structure. + round_trip: true + expected_round_trip: + type: "Microsoft.Test/widgets" + properties: + items: + - name: w1 + properties: + config: + score: 5 + + # Regression: array base rename must move (not clone) the value so that + # the stale ARM base key does not survive in the normalized output. + # Without the removal, denormalization produces a duplicate key. + - note: array base rename removes stale ARM base key + aliases_json: | + [ + { + "namespace": "Microsoft.Test", + "resourceTypes": [ + { + "resourceType": "widgets", + "aliases": [ + { + "name": "Microsoft.Test/widgets/items[*].label", + "defaultPath": "properties.entries[*].label", + "paths": [] + } + ] + } + ] + } + ] + input: + type: "Microsoft.Test/widgets" + properties: + entries: + - label: hello + # Normalize: ARM base "entries" → alias base "items". + # The stale "entries" key must be removed; only "items" should remain. + expected_normalized: + type: "Microsoft.Test/widgets" + items: + - label: hello + # Round-trip: normalize → denormalize should restore ARM structure. + round_trip: true + expected_round_trip: + type: "Microsoft.Test/widgets" + properties: + entries: + - label: hello + + # Regression: denormalize reverse element remap with a dotted ARM target + # must preserve restored casing (e.g. "Config.Score") rather than + # re-lowercasing it. + - note: round-trip dotted element remap preserves ARM casing + aliases_json: | + [ + { + "namespace": "Microsoft.Test", + "resourceTypes": [ + { + "resourceType": "widgets", + "aliases": [ + { + "name": "Microsoft.Test/widgets/items[*].rating", + "defaultPath": "properties.items[*].properties.Config.Score", + "paths": [] + }, + { + "name": "Microsoft.Test/widgets/items[*].name", + "defaultPath": "properties.items[*].name", + "paths": [] + } + ] + } + ] + } + ] + input: + type: "Microsoft.Test/widgets" + properties: + items: + - name: w1 + properties: + Config: + Score: 9 + expected_normalized: + type: "Microsoft.Test/widgets" + items: + - name: w1 + config: {} + rating: 9 + round_trip: true + expected_round_trip: + type: "Microsoft.Test/widgets" + properties: + items: + - name: w1 + properties: + Config: + Score: 9 diff --git a/tests/azure_policy/normalization/mod.rs b/tests/azure_policy/normalization/mod.rs new file mode 100644 index 0000000..31217de --- /dev/null +++ b/tests/azure_policy/normalization/mod.rs @@ -0,0 +1,322 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +//! YAML-driven normalization / denormalization tests. +//! +//! Each YAML file contains a `cases` array. Every case specifies an `input` +//! JSON value plus optional `aliases` and `api_version`. The runner then +//! checks whichever of the following fields are present: +//! +//! * `expected_normalized` – result of normalizing `input` +//! * `expected_denormalized` – result of denormalizing `input` +//! * `round_trip` – normalize then denormalize; compare with `expected_round_trip` +//! * `reverse_round_trip` – denormalize then normalize; compare with +//! `expected_reverse_round_trip` + +use std::path::Path; + +use anyhow::{bail, Result}; +use serde::Deserialize; +use test_generator::test_resources; + +use regorus::languages::azure_policy::aliases::normalizer; +use regorus::languages::azure_policy::aliases::types::ResolvedAliases; +use regorus::languages::azure_policy::aliases::{denormalizer, AliasRegistry}; +use regorus::Value; + +// ── YAML schema ────────────────────────────────────────────────────────── + +#[derive(Deserialize)] +struct YamlTest { + #[serde(default)] + aliases_json: Option, + #[serde(default)] + aliases_file: Option, + #[serde(default)] + data_manifest_json: Option, + #[serde(default)] + data_manifest_file: Option, + cases: Vec, +} + +#[derive(Deserialize)] +struct TestCase { + note: String, + input: serde_json::Value, + #[serde(default)] + api_version: Option, + #[serde(default)] + context: Option, + #[serde(default)] + parameters: Option, + #[serde(default)] + expected_normalized: Option, + #[serde(default)] + expected_denormalized: Option, + #[serde(default)] + expected_envelope: Option, + #[serde(default)] + round_trip: bool, + #[serde(default)] + expected_round_trip: Option, + #[serde(default)] + reverse_round_trip: bool, + #[serde(default)] + expected_reverse_round_trip: Option, + #[serde(default)] + aliases_json: Option, + #[serde(default)] + sub_resource_arrays: Option>, + #[serde(default)] + resource_type: Option, + #[serde(default)] + use_registry_api: bool, +} + +// ── Helpers ────────────────────────────────────────────────────────────── + +/// Convert a `serde_json::Value` to `regorus::Value`. +fn to_regorus(v: &serde_json::Value) -> Value { + Value::from(v.clone()) +} + +fn load_registry(yaml_file: &str, test: &YamlTest) -> Result> { + let mut reg = AliasRegistry::new(); + let mut loaded = false; + + if let Some(ref inline) = test.aliases_json { + reg.load_from_json(inline)?; + loaded = true; + } + if let Some(ref relpath) = test.aliases_file { + let base = Path::new(yaml_file).parent().unwrap_or(Path::new(".")); + let path = base.join(relpath); + let json = std::fs::read_to_string(&path)?; + reg.load_from_json(&json)?; + loaded = true; + } + if let Some(ref inline) = test.data_manifest_json { + reg.load_data_policy_manifest_json(inline)?; + loaded = true; + } + if let Some(ref relpath) = test.data_manifest_file { + let base = Path::new(yaml_file).parent().unwrap_or(Path::new(".")); + let path = base.join(relpath); + let json = std::fs::read_to_string(&path)?; + reg.load_data_policy_manifest_json(&json)?; + loaded = true; + } + + Ok(if loaded { Some(reg) } else { None }) +} + +fn case_override_registry(case: &TestCase) -> Result> { + if let Some(ref inline) = case.aliases_json { + let mut reg = AliasRegistry::new(); + reg.load_from_json(inline)?; + return Ok(Some(reg)); + } + Ok(None) +} + +fn resolve_aliases( + registry: Option<&AliasRegistry>, + case: &TestCase, + input: &serde_json::Value, +) -> Option { + let resource_type = case + .resource_type + .clone() + .or_else(|| input.get("type").and_then(|v| v.as_str()).map(String::from)); + + if let (Some(reg), Some(rt)) = (registry, resource_type.as_deref()) { + if let Some(resolved) = reg.get(rt) { + let mut r = resolved.clone(); + if let Some(ref subs) = case.sub_resource_arrays { + r.sub_resource_arrays = subs.iter().map(|s| s.to_ascii_lowercase()).collect(); + } + return Some(r); + } + } + + if let Some(ref subs) = case.sub_resource_arrays { + return Some(ResolvedAliases { + resource_type: resource_type.unwrap_or_default(), + entries: Default::default(), + sub_resource_arrays: subs.iter().map(|s| s.to_ascii_lowercase()).collect(), + default_aggregates: Default::default(), + versioned_aggregates: Default::default(), + }); + } + + None +} + +fn pretty_regorus(v: &Value) -> String { + v.to_json_str().unwrap_or_else(|_| format!("{v:?}")) +} + +// ── Runner ─────────────────────────────────────────────────────────────── + +fn run_yaml_test(file: &str) -> Result<()> { + let yaml_str = std::fs::read_to_string(file)?; + let test: YamlTest = serde_yaml::from_str(&yaml_str)?; + let file_registry = load_registry(file, &test)?; + + for case in &test.cases { + print!(" case: {} … ", case.note); + + let case_override = case_override_registry(case)?; + let registry = case_override.as_ref().or(file_registry.as_ref()); + let resolved = resolve_aliases(registry, case, &case.input); + let api_ver = case.api_version.as_deref(); + let input = to_regorus(&case.input); + + // ── normalize ──────────────────────────────────────────────── + if let Some(ref expected) = case.expected_normalized { + let expected = to_regorus(expected); + let actual = if case.use_registry_api { + normalizer::normalize(&input, registry, api_ver) + } else { + normalizer::normalize_with_aliases(&input, resolved.as_ref(), api_ver) + }; + if actual != expected { + bail!( + "normalize mismatch in '{}':\nexpected:\n{}\nactual:\n{}", + case.note, + pretty_regorus(&expected), + pretty_regorus(&actual), + ); + } + } + + // ── denormalize ────────────────────────────────────────────── + if let Some(ref expected) = case.expected_denormalized { + let expected = to_regorus(expected); + let actual = if case.use_registry_api { + denormalizer::denormalize(&input, registry, api_ver) + } else { + denormalizer::denormalize_with_aliases(&input, resolved.as_ref(), api_ver) + }; + if actual != expected { + bail!( + "denormalize mismatch in '{}':\nexpected:\n{}\nactual:\n{}", + case.note, + pretty_regorus(&expected), + pretty_regorus(&actual), + ); + } + } + + // ── envelope ───────────────────────────────────────────────── + if let Some(ref expected) = case.expected_envelope { + let expected = to_regorus(expected); + let actual = if case.use_registry_api { + if let Some(reg) = registry { + reg.normalize_and_wrap( + &input, + api_ver, + case.context.as_ref().map(to_regorus), + case.parameters.as_ref().map(to_regorus), + ) + } else { + let norm = normalizer::normalize(&input, None, api_ver); + normalizer::build_input_envelope( + norm, + case.context.as_ref().map(to_regorus), + case.parameters.as_ref().map(to_regorus), + ) + } + } else { + let norm = normalizer::normalize_with_aliases(&input, resolved.as_ref(), api_ver); + normalizer::build_input_envelope( + norm, + case.context.as_ref().map(to_regorus), + case.parameters.as_ref().map(to_regorus), + ) + }; + if actual != expected { + bail!( + "envelope mismatch in '{}':\nexpected:\n{}\nactual:\n{}", + case.note, + pretty_regorus(&expected), + pretty_regorus(&actual), + ); + } + } + + // ── round-trip ─────────────────────────────────────────────── + if case.round_trip { + let (normalized, denormalized) = if case.use_registry_api { + let n = normalizer::normalize(&input, registry, api_ver); + let d = denormalizer::denormalize(&n, registry, api_ver); + (n, d) + } else { + let n = normalizer::normalize_with_aliases(&input, resolved.as_ref(), api_ver); + let d = denormalizer::denormalize_with_aliases(&n, resolved.as_ref(), api_ver); + (n, d) + }; + let _ = normalized; + if let Some(ref expected) = case.expected_round_trip { + let expected = to_regorus(expected); + if denormalized != expected { + bail!( + "round-trip mismatch in '{}':\nexpected:\n{}\nactual:\n{}", + case.note, + pretty_regorus(&expected), + pretty_regorus(&denormalized), + ); + } + } else if denormalized != input { + bail!( + "round-trip mismatch in '{}' (expected original input):\ninput:\n{}\nresult:\n{}", + case.note, + pretty_regorus(&input), + pretty_regorus(&denormalized), + ); + } + } + + // ── reverse round-trip ───────────────────────────────────── + if case.reverse_round_trip { + let (denormalized, renormalized) = if case.use_registry_api { + let d = denormalizer::denormalize(&input, registry, api_ver); + let n = normalizer::normalize(&d, registry, api_ver); + (d, n) + } else { + let d = denormalizer::denormalize_with_aliases(&input, resolved.as_ref(), api_ver); + let n = normalizer::normalize_with_aliases(&d, resolved.as_ref(), api_ver); + (d, n) + }; + let _ = denormalized; + if let Some(ref expected) = case.expected_reverse_round_trip { + let expected = to_regorus(expected); + if renormalized != expected { + bail!( + "reverse round-trip mismatch in '{}':\nexpected:\n{}\nactual:\n{}", + case.note, + pretty_regorus(&expected), + pretty_regorus(&renormalized), + ); + } + } else if renormalized != input { + bail!( + "reverse round-trip mismatch in '{}' (expected original input):\ninput:\n{}\nresult:\n{}", + case.note, + pretty_regorus(&input), + pretty_regorus(&renormalized), + ); + } + } + + println!("ok"); + } + + println!(" {} cases passed in {file}", test.cases.len()); + Ok(()) +} + +#[test_resources("tests/azure_policy/normalization/cases/**/*.yaml")] +fn run(path: &str) { + run_yaml_test(path).unwrap() +} diff --git a/tests/mod.rs b/tests/mod.rs index 74fe92a..a1d11ae 100644 --- a/tests/mod.rs +++ b/tests/mod.rs @@ -12,5 +12,8 @@ mod lexer; mod parser; mod value; +#[cfg(feature = "azure_policy")] +mod azure_policy; + #[cfg(feature = "rvm")] mod rvm;