Add comprehensive documentation and GitHub Copilot configuration: - docs/knowledge/: 17 deep-dive knowledge files covering value semantics, RVM architecture, builtins, FFI boundary, feature composition, error handling migration, policy evaluation security, Rego semantics, interpreter/compiler architecture, Azure Policy/RBAC, engine API, time builtins, language extension guide, tooling architecture, causality/partial eval, Rego compiler, Azure Policy aliases, and telemetry/diagnostics - .github/agents/: 16 role-specific AI agent definitions (red-teamer, semantics-expert, architect, performance-engineer, test-engineer, verification-engineer, security-auditor, reliability-engineer, support-engineer, ci-engineer, refactorer, api-steward, program-manager, demo-engineer, dx-engineer, tech-lead) - .github/skills/: 6 workflow skill definitions (thorough-review, design-alternatives, add-builtin, opa-conformance, security-review, verification) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: anakrish <35780660+anakrish@users.noreply.github.com>
8.2 KiB
Knowledge: Azure Policy Aliases and Normalization
Deep knowledge about the Azure Policy alias system and ARM resource normalization. Read this before modifying alias resolution, the normalizer, or the denormalizer.
See also azure-policy-language.md for the overall Azure Policy compilation
pipeline.
What Aliases Are
Azure Policy uses "aliases" to refer to Azure resource properties in a provider-independent way:
Full alias: Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly
Short name: supportsHttpsTrafficOnly
ARM path: properties.supportsHttpsTrafficOnly
The alias system bridges between:
- Policy authors — who write conditions using alias paths
- ARM resources — which have nested JSON structures with varying casing
Alias Registry
Loading Sources
Control-plane aliases — loaded from Azure provider metadata:
GET /providers?$expand=resourceTypes/aliases
Produces ProviderAliases with resource type → alias mappings.
Data-plane aliases — loaded from data policy manifests for .Data
namespaces (e.g., Microsoft.KeyVault.Data/vaults/secrets).
Registry Structure
struct AliasRegistry {
// Maps full alias name → alias metadata
aliases: BTreeMap<String, AliasInfo>,
// Maps resource type → list of aliases
resource_type_aliases: BTreeMap<String, Vec<String>>,
}
The registry provides:
- Alias path segments (for navigating ARM JSON)
- Alias type metadata (string, array, object, etc.)
- Default path mappings when aliases are absent
Normalization Pipeline
The normalizer transforms ARM resource JSON into a flat structure that the policy compiler can evaluate directly.
Input: ARM Resource JSON
{
"type": "Microsoft.Storage/storageAccounts",
"id": "/subscriptions/.../storageAccounts/myaccount",
"name": "myaccount",
"location": "eastus",
"properties": {
"supportsHttpsTrafficOnly": true,
"networkAcls": {
"defaultAction": "Deny",
"virtualNetworkRules": [
{ "id": "/subscriptions/.../subnets/default" }
]
}
}
}
Output: Normalized Resource
{
"type": "microsoft.storage/storageaccounts",
"id": "/subscriptions/.../storageAccounts/myaccount",
"name": "myaccount",
"location": "eastus",
"supportshttpstrafficonly": true,
"networkacls.defaultaction": "Deny",
"networkacls.virtualnetworkrules": [
{ "id": "/subscriptions/.../subnets/default" }
]
}
Normalization Steps
-
Copy root fields (lowercased):
type,id,kind,name,location,identity,zones,sku,plan,tags -
Merge properties — contents of
propertiesare merged into the result at the top level -
Apply alias path resolution:
- Each alias has a path (e.g.,
properties.networkAcls.defaultAction) - The normalizer navigates the ARM JSON using path segments
- The extracted value is placed at the alias short name (lowercased)
- Each alias has a path (e.g.,
-
Handle sub-resources — sub-resource types (e.g., extensions on VMs) are extracted from arrays and normalized separately
-
Array element handling —
[*]in alias paths triggers iteration over array elements; each element is normalized independently -
Case folding — all property names are lowercased for case-insensitive matching (Azure ARM is case-insensitive)
Key Complexity: Case Preservation
ARM JSON casing is preserved through normalization and denormalization. The normalizer records original casing to enable round-trip fidelity. This matters for Modify/Append effects that construct output JSON.
Denormalization
The denormalizer converts flat normalized paths back to nested ARM JSON structure. This is needed for:
- Modify effect — construct the resource patch to apply
- Append effect — construct fields to add to the resource
Denormalization Challenge
Given a flat path like networkacls.defaultaction = "Allow", the
denormalizer must reconstruct:
{
"properties": {
"networkAcls": {
"defaultAction": "Allow"
}
}
}
This requires knowing:
- Where
propertiesnesting begins (alias metadata) - Original casing of each path segment
- Whether intermediate nodes are objects or arrays
Compiler Integration
Alias Map
The compiler receives an alias map: BTreeMap<String, String> mapping
alias short names to full ARM paths. This is populated from the
AliasRegistry for the specific resource type being evaluated.
Field Compilation
When compiling a field condition:
{ "field": "supportsHttpsTrafficOnly", "equals": true }
- Look up field name in alias map
- If found: compile as property access on normalized input
- If dynamic (
[concat(...)]): compile ARM expression, use result as key - Emit
Index/IndexLiteral/ChainedIndexinstructions
Metadata Accumulation
During compilation, the compiler tracks:
observed_aliases— all alias names referencedobserved_field_kinds— static fields, dynamic fields,[*]wildcardsobserved_resource_types— resource types from field conditionsobserved_has_dynamic_fields— whether ARM expressions appear as fields
This metadata supports policy analysis and optimization.
Wildcard Semantics
Unbound [*] (outside count)
{ "field": "securityRules[*].destinationPortRange", "equals": "443" }
Implicit allOf — every element must match. The compiler generates
a LoopStart { mode: Every } instruction.
Bound [*] (inside count)
{
"count": {
"field": "securityRules[*]",
"where": { "field": "securityRules[*].destinationPortRange", "equals": "443" }
},
"greaterOrEquals": 1
}
Iteration with counting — each element is tested, matching ones are
counted. The compiler generates LoopStart { mode: Count }.
Multi-level Wildcards
{ "field": "outer[*].inner[*].value" }
Nested loops: outer levels use ForEach, innermost carries the semantic
operator. The compiler maintains a binding stack to track scope.
current() Function
Inside count.where blocks, current() refers to the current iteration
element:
{
"count": {
"value": "[parameters('items')]",
"name": "item",
"where": {
"value": "[current('item').status]",
"equals": "active"
}
}
}
The compiler binds the loop variable and makes it accessible via
current() calls in ARM template expressions.
Existence vs Null
Azure Policy distinguishes between missing fields and null values:
- Missing field →
Undefinedin regorus Value system - Null field →
Value::Null
For most operators, the compiler emits CoalesceUndefinedToNull to
treat missing as null. The exists operator is the exception — it
specifically tests for field presence:
{ "field": "optionalProperty", "exists": true } // Field must be present
{ "field": "optionalProperty", "exists": false } // Field must be absent
Key Invariants
-
Normalization before compilation — aliases are resolved during normalization, not at compile time or runtime
-
Case-insensitive everywhere — all field name comparisons use lowercased strings
-
[*]context matters — same syntax has different semantics inside vs outsidecountexpressions -
Round-trip fidelity — normalize → denormalize must preserve original ARM JSON casing for Modify/Append effects
-
Missing = null (mostly) —
CoalesceUndefinedToNullis the default;existsis the exception
Common Pitfalls
-
Alias path segments — paths like
properties.a.bmust be split correctly. Dots in property names (rare but possible) need escaping. -
Sub-resource normalization — sub-resources have their own type and their own alias set. Don't normalize with parent's aliases.
-
Array vs scalar — some aliases point to arrays, others to scalars. The
[*]wildcard only works on arrays. Applying it to a scalar is a compile-time error. -
Dynamic field resolution order — ARM template expressions in field positions are evaluated at runtime. The alias map must be available at runtime for dynamic alias resolution.