feat: add Azure Policy constraint parser (#658)

Add constraint.rs module that parses Azure Policy JSON constraints
into span-annotated AST nodes:

- Logical combinators: allOf, anyOf, not
- Leaf conditions: field/value with all 19 operators
- Count blocks: field-count and value-count with where clauses

Public API: parse_constraint() parses a standalone constraint from JSON.

Includes YAML-driven test suite with 6 test files covering operators,
fields, expressions, logical combinators, count, and parse errors.
This commit is contained in:
Anand Krishnamoorthi
2026-04-03 19:09:51 -05:00
committed by GitHub
parent 95bffcb5f9
commit 687be2850b
11 changed files with 3203 additions and 15 deletions

View File

@@ -2,3 +2,4 @@
// Licensed under the MIT License.
mod normalization;
mod parser_tests;

View File

@@ -0,0 +1,617 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Count Expressions Test Suite
# Tests field count and value count with optional where clauses and name bindings.
cases:
# =========================================================================
# Field count — direct path (core subset, no alias resolution)
# =========================================================================
- note: field_count_direct_path_core
policy_rule: |
{
"if": {
"count": {
"field": "securityRules[*]"
},
"greater": 2
},
"then": { "effect": "deny" }
}
resource:
securityRules:
- { "name": "r1" }
- { "name": "r2" }
- { "name": "r3" }
want_effect: "deny"
- note: field_count_direct_path_where_core
policy_rule: |
{
"if": {
"count": {
"field": "securityRules[*]",
"where": {
"field": "securityRules[*].access",
"equals": "Allow"
}
},
"equals": 2
},
"then": { "effect": "audit" }
}
resource:
securityRules:
- { "access": "Allow" }
- { "access": "Deny" }
- { "access": "Allow" }
want_effect: "audit"
# =========================================================================
# Field count — basic
# =========================================================================
- note: field_count_basic
policy_rule: |
{
"if": {
"count": {
"field": "securityRules[*]"
},
"greater": 10
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Network/networkSecurityGroups"
securityRules:
- { "name": "r1" }
- { "name": "r2" }
- { "name": "r3" }
- { "name": "r4" }
- { "name": "r5" }
- { "name": "r6" }
- { "name": "r7" }
- { "name": "r8" }
- { "name": "r9" }
- { "name": "r10" }
- { "name": "r11" }
want_effect: "deny"
- note: field_count_equals_zero
policy_rule: |
{
"if": {
"count": {
"field": "storageProfile.dataDisks[*]"
},
"equals": 0
},
"then": { "effect": "audit" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
want_effect: "audit"
# =========================================================================
# Field count — with where clause
# =========================================================================
- note: field_count_with_where
policy_rule: |
{
"if": {
"count": {
"field": "securityRules[*]",
"where": {
"field": "securityRules[*].access",
"equals": "Allow"
}
},
"greater": 5
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Network/networkSecurityGroups"
securityRules:
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
want_effect: "deny"
- note: field_count_where_allOf
policy_rule: |
{
"if": {
"count": {
"field": "securityRules[*]",
"where": {
"allOf": [
{
"field": "securityRules[*].access",
"equals": "Allow"
},
{
"field": "securityRules[*].direction",
"equals": "Inbound"
}
]
}
},
"greaterOrEquals": 1
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Network/networkSecurityGroups"
securityRules:
- { "access": "Allow", "direction": "Inbound" }
- { "access": "Deny", "direction": "Outbound" }
want_effect: "deny"
- note: field_count_where_anyOf
policy_rule: |
{
"if": {
"count": {
"field": "securityRules[*]",
"where": {
"anyOf": [
{
"field": "securityRules[*].destinationPortRange",
"equals": "22"
},
{
"field": "securityRules[*].destinationPortRange",
"equals": "3389"
}
]
}
},
"notEquals": 0
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Network/networkSecurityGroups"
securityRules:
- { "destinationPortRange": "22" }
- { "destinationPortRange": "443" }
want_effect: "deny"
- note: field_count_where_not
policy_rule: |
{
"if": {
"count": {
"field": "securityRules[*]",
"where": {
"not": {
"field": "securityRules[*].access",
"equals": "Deny"
}
}
},
"greater": 0
},
"then": { "effect": "audit" }
}
resource:
type: "Microsoft.Network/networkSecurityGroups"
securityRules:
- { "access": "Allow" }
- { "access": "Deny" }
want_effect: "audit"
# =========================================================================
# Value count
# =========================================================================
- note: value_count_basic
policy_rule: |
{
"if": {
"count": {
"value": ["eastus", "westus", "centralus"]
},
"equals": 3
},
"then": { "effect": "audit" }
}
resource:
type: "any"
want_effect: "audit"
- note: value_count_with_name
policy_rule: |
{
"if": {
"count": {
"value": ["eastus", "westus", "centralus"],
"name": "location"
},
"greater": 0
},
"then": { "effect": "audit" }
}
resource:
type: "any"
want_effect: "audit"
- note: value_count_with_name_and_where
policy_rule: |
{
"if": {
"count": {
"value": ["eastus", "westus", "centralus", "northeurope"],
"name": "loc",
"where": {
"value": "[current('loc')]",
"like": "*us"
}
},
"equals": 3
},
"then": { "effect": "audit" }
}
resource:
type: "any"
want_effect: "audit"
- note: value_count_expression
policy_rule: |
{
"if": {
"count": {
"value": "[parameters('allowedLocations')]",
"name": "loc"
},
"greater": 0
},
"then": { "effect": "audit" }
}
parameters:
allowedLocations:
- "eastus"
- "westus"
resource:
type: "any"
want_effect: "audit"
# =========================================================================
# Count in allOf/anyOf
# =========================================================================
- note: count_in_allOf
policy_rule: |
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Network/networkSecurityGroups" },
{
"count": {
"field": "securityRules[*]",
"where": {
"field": "securityRules[*].access",
"equals": "Allow"
}
},
"greater": 10
}
]
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Network/networkSecurityGroups"
securityRules:
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
- { "access": "Allow" }
want_effect: "deny"
- note: count_in_not
policy_rule: |
{
"if": {
"not": {
"count": {
"field": "storageProfile.dataDisks[*]"
},
"lessOrEquals": 4
}
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
storageProfile:
dataDisks:
- { "name": "d1" }
- { "name": "d2" }
- { "name": "d3" }
- { "name": "d4" }
- { "name": "d5" }
want_effect: "deny"
# =========================================================================
# Count with nested where containing count
# =========================================================================
- note: value_count_nested_where
policy_rule: |
{
"if": {
"count": {
"value": "[parameters('requiredTags')]",
"name": "tag",
"where": {
"field": "[concat('tags[', current('tag'), ']')]",
"exists": true
}
},
"notEquals": "[length(parameters('requiredTags'))]"
},
"then": { "effect": "deny" }
}
parameters:
requiredTags:
- "environment"
- "costCenter"
resource:
type: "Microsoft.Compute/virtualMachines"
tags:
environment: "prod"
want_effect: "deny"
# =========================================================================
# Alias field refs inside count resolve to current loop element
# =========================================================================
- note: field_count_multiple_alias_refs_same_element
policy_rule: |
{
"if": {
"count": {
"field": "securityRules[*]",
"where": {
"allOf": [
{ "field": "securityRules[*].access", "equals": "Allow" },
{ "field": "securityRules[*].direction", "equals": "Inbound" },
{ "field": "securityRules[*].protocol", "equals": "Tcp" }
]
}
},
"equals": 1
},
"then": { "effect": "audit" }
}
resource:
securityRules:
- { "access": "Allow", "direction": "Inbound", "protocol": "Tcp" }
- { "access": "Allow", "direction": "Outbound", "protocol": "Tcp" }
- { "access": "Deny", "direction": "Inbound", "protocol": "Tcp" }
want_effect: "audit"
- note: field_count_nested_field_access
policy_rule: |
{
"if": {
"count": {
"field": "storageProfile.dataDisks[*]",
"where": {
"field": "storageProfile.dataDisks[*].managedDisk.storageAccountType",
"notEquals": "Premium_LRS"
}
},
"greater": 0
},
"then": { "effect": "deny" }
}
resource:
storageProfile:
dataDisks:
- { "name": "d1", "managedDisk": { "storageAccountType": "Premium_LRS" } }
- { "name": "d2", "managedDisk": { "storageAccountType": "Standard_LRS" } }
want_effect: "deny"
- note: field_count_where_zero_matches
policy_rule: |
{
"if": {
"count": {
"field": "items[*]",
"where": {
"field": "items[*].status",
"equals": "failed"
}
},
"equals": 0
},
"then": { "effect": "audit" }
}
resource:
items:
- { "status": "ok" }
- { "status": "ok" }
- { "status": "ok" }
want_effect: "audit"
# =========================================================================
# Nested count: count inside another count's where clause
# =========================================================================
- note: nested_field_and_value_count
policy_rule: |
{
"if": {
"count": {
"value": "[parameters('requiredPorts')]",
"name": "port",
"where": {
"count": {
"field": "securityRules[*]",
"where": {
"allOf": [
{ "field": "securityRules[*].destinationPortRange", "equals": "[current('port')]" },
{ "field": "securityRules[*].access", "equals": "Allow" }
]
}
},
"greater": 0
}
},
"equals": "[length(parameters('requiredPorts'))]"
},
"then": { "effect": "audit" }
}
parameters:
requiredPorts:
- "443"
- "80"
resource:
securityRules:
- { "destinationPortRange": "443", "access": "Allow" }
- { "destinationPortRange": "80", "access": "Allow" }
- { "destinationPortRange": "22", "access": "Deny" }
want_effect: "audit"
- note: nested_field_and_value_count_fail
policy_rule: |
{
"if": {
"count": {
"value": "[parameters('requiredPorts')]",
"name": "port",
"where": {
"count": {
"field": "securityRules[*]",
"where": {
"allOf": [
{ "field": "securityRules[*].destinationPortRange", "equals": "[current('port')]" },
{ "field": "securityRules[*].access", "equals": "Allow" }
]
}
},
"greater": 0
}
},
"equals": "[length(parameters('requiredPorts'))]"
},
"then": { "effect": "deny" }
}
parameters:
requiredPorts:
- "443"
- "80"
- "8080"
resource:
securityRules:
- { "destinationPortRange": "443", "access": "Allow" }
- { "destinationPortRange": "80", "access": "Allow" }
- { "destinationPortRange": "22", "access": "Deny" }
want_effect: ~
# =========================================================================
# current() — zero-arg form (innermost count element)
# =========================================================================
- note: current_zero_arg_value_count
policy_rule: |
{
"if": {
"count": {
"value": ["Allow", "Allow", "Deny"],
"name": "access",
"where": {
"value": "[current()]",
"equals": "Allow"
}
},
"equals": 2
},
"then": { "effect": "deny" }
}
resource:
type: "any"
want_effect: "deny"
- note: current_zero_arg_field_count
policy_rule: |
{
"if": {
"count": {
"field": "items[*]",
"where": {
"value": "[current()]",
"equals": "yes"
}
},
"equals": 2
},
"then": { "effect": "deny" }
}
resource:
items: ["yes", "no", "yes"]
want_effect: "deny"
- note: current_zero_arg_with_function
policy_rule: |
{
"if": {
"count": {
"value": ["HELLO", "WORLD"],
"name": "word",
"where": {
"value": "[startsWith(current(), 'HE')]",
"equals": true
}
},
"equals": 1
},
"then": { "effect": "deny" }
}
resource:
type: "any"
want_effect: "deny"
- note: current_zero_arg_nested_innermost
policy_rule: |
{
"if": {
"count": {
"value": ["a", "b"],
"name": "outer",
"where": {
"count": {
"value": ["x", "y"],
"name": "inner",
"where": {
"value": "[current()]",
"equals": "x"
}
},
"greater": 0
}
},
"greater": 0
},
"then": { "effect": "deny" }
}
resource:
type: "any"
want_effect: "deny"

View File

@@ -0,0 +1,554 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# ARM Template Expressions Test Suite
# Tests [parameters(...)], [concat(...)], [field(...)], [if(...)], and other
# ARM template expression patterns in field, value, and effect positions.
cases:
# =========================================================================
# parameters() references
# =========================================================================
- note: expr_parameters_in_value
policy_rule: |
{
"if": {
"value": "[parameters('environment')]",
"equals": "production"
},
"then": { "effect": "deny" }
}
parameters:
environment: "production"
resource:
type: "any"
want_effect: "deny"
- note: expr_parameters_in_rhs
policy_rule: |
{
"if": {
"field": "location",
"in": "[parameters('allowedLocations')]"
},
"then": { "effect": "deny" }
}
parameters:
allowedLocations:
- "eastus"
- "westus"
resource:
location: "eastus"
want_effect: "deny"
- note: expr_parameters_in_effect
policy_rule: |
{
"if": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
"then": {
"effect": "[parameters('effect')]"
}
}
parameters:
effect: "deny"
resource:
type: "Microsoft.Compute/virtualMachines"
want_effect: "deny"
# =========================================================================
# concat()
# =========================================================================
- note: expr_concat_strings
policy_rule: |
{
"if": {
"value": "[concat('Microsoft.Compute/', 'virtualMachines')]",
"equals": "Microsoft.Compute/virtualMachines"
},
"then": { "effect": "audit" }
}
resource:
type: "any"
want_effect: "audit"
- note: expr_concat_with_parameters
policy_rule: |
{
"if": {
"value": "[concat(parameters('prefix'), '-vm')]",
"equals": "prod-vm"
},
"then": { "effect": "audit" }
}
parameters:
prefix: "prod"
resource:
type: "any"
want_effect: "audit"
- note: expr_concat_nested
policy_rule: |
{
"if": {
"value": "[concat(concat('a', 'b'), 'c')]",
"equals": "abc"
},
"then": { "effect": "audit" }
}
resource:
type: "any"
want_effect: "audit"
# =========================================================================
# field() function
# =========================================================================
- note: expr_field_function
policy_rule: |
{
"if": {
"value": "[field('name')]",
"contains": "prod"
},
"then": { "effect": "audit" }
}
resource:
name: "my-prod-vm"
want_effect: "audit"
- note: expr_field_in_concat
policy_rule: |
{
"if": {
"value": "[concat(field('type'), '/', field('name'))]",
"contains": "Microsoft.Compute"
},
"then": { "effect": "audit" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
name: "vm1"
want_effect: "audit"
# =========================================================================
# if() conditional
# =========================================================================
- note: expr_if_conditional
policy_rule: |
{
"if": {
"value": "[if(equals(parameters('env'), 'prod'), 'deny', 'audit')]",
"equals": "deny"
},
"then": { "effect": "audit" }
}
parameters:
env: "prod"
resource:
type: "any"
want_effect: "audit"
# =========================================================================
# String functions
# =========================================================================
- note: expr_toLower
policy_rule: |
{
"if": {
"value": "[toLower(field('name'))]",
"equals": "my-vm"
},
"then": { "effect": "audit" }
}
resource:
name: "My-VM"
want_effect: "audit"
- note: expr_toUpper
policy_rule: |
{
"if": {
"value": "[toUpper(parameters('prefix'))]",
"equals": "PROD"
},
"then": { "effect": "audit" }
}
parameters:
prefix: "prod"
resource:
type: "any"
want_effect: "audit"
- note: expr_replace
policy_rule: |
{
"if": {
"value": "[replace(field('name'), '-', '_')]",
"equals": "my_prod_vm"
},
"then": { "effect": "audit" }
}
resource:
name: "my-prod-vm"
want_effect: "audit"
- note: expr_substring
policy_rule: |
{
"if": {
"value": "[substring(field('name'), 0, 4)]",
"equals": "prod"
},
"then": { "effect": "audit" }
}
resource:
name: "prod-vm-01"
want_effect: "audit"
# =========================================================================
# Numeric functions
# =========================================================================
- note: expr_length
policy_rule: |
{
"if": {
"value": "[length(parameters('allowedLocations'))]",
"greater": 0
},
"then": { "effect": "audit" }
}
parameters:
allowedLocations:
- "eastus"
- "westus"
resource:
type: "any"
want_effect: "audit"
- note: expr_add
policy_rule: |
{
"if": {
"value": "[add(parameters('base'), 1)]",
"greater": 5
},
"then": { "effect": "audit" }
}
parameters:
base: 10
resource:
type: "any"
want_effect: "audit"
# =========================================================================
# Context functions
# =========================================================================
- note: expr_resourceGroup
policy_rule: |
{
"if": {
"value": "[resourceGroup().location]",
"notIn": "[parameters('allowedLocations')]"
},
"then": { "effect": "deny" }
}
parameters:
allowedLocations:
- "westus2"
- "centralus"
resource:
type: "any"
want_effect: "deny"
- note: expr_subscription
policy_rule: |
{
"if": {
"value": "[subscription().subscriptionId]",
"equals": "00000000-0000-0000-0000-000000000000"
},
"then": { "effect": "audit" }
}
resource:
type: "any"
want_effect: "audit"
- note: expr_requestContext_apiVersion
policy_rule: |
{
"if": {
"value": "[requestContext().apiVersion]",
"greaterOrEquals": "2021-04-01"
},
"then": { "effect": "deny" }
}
context:
resourceGroup:
name: "myResourceGroup"
location: "eastus"
subscription:
subscriptionId: "00000000-0000-0000-0000-000000000000"
requestContext:
apiVersion: "2023-01-01"
resource:
type: "any"
want_effect: "deny"
- note: expr_requestContext_apiVersion_older
policy_rule: |
{
"if": {
"value": "[requestContext().apiVersion]",
"greaterOrEquals": "2024-06-01"
},
"then": { "effect": "deny" }
}
context:
resourceGroup:
name: "myResourceGroup"
location: "eastus"
subscription:
subscriptionId: "00000000-0000-0000-0000-000000000000"
requestContext:
apiVersion: "2023-01-01"
resource:
type: "any"
want_undefined: true
- note: expr_policy_assignmentId
policy_rule: |
{
"if": {
"value": "[policy().assignmentId]",
"equals": "/subscriptions/sub1/providers/Microsoft.Authorization/policyAssignments/myAssignment"
},
"then": { "effect": "audit" }
}
context:
resourceGroup:
name: "myResourceGroup"
location: "eastus"
subscription:
subscriptionId: "00000000-0000-0000-0000-000000000000"
policy:
assignmentId: "/subscriptions/sub1/providers/Microsoft.Authorization/policyAssignments/myAssignment"
definitionId: "/providers/Microsoft.Authorization/policyDefinitions/myDefinition"
setDefinitionId: ""
definitionReferenceId: ""
resource:
type: "any"
want_effect: "audit"
- note: expr_policy_definitionId
policy_rule: |
{
"if": {
"value": "[policy().definitionId]",
"contains": "myDefinition"
},
"then": { "effect": "deny" }
}
context:
resourceGroup:
name: "myResourceGroup"
location: "eastus"
subscription:
subscriptionId: "00000000-0000-0000-0000-000000000000"
policy:
assignmentId: "/subscriptions/sub1/providers/Microsoft.Authorization/policyAssignments/myAssignment"
definitionId: "/providers/Microsoft.Authorization/policyDefinitions/myDefinition"
resource:
type: "any"
want_effect: "deny"
# =========================================================================
# current() in count contexts
# =========================================================================
- note: expr_current_in_value_count
policy_rule: |
{
"if": {
"count": {
"value": "[parameters('requiredTags')]",
"name": "tagName",
"where": {
"value": "[current('tagName')]",
"notEquals": ""
}
},
"greater": 0
},
"then": { "effect": "audit" }
}
parameters:
requiredTags:
- "environment"
- "costCenter"
resource:
type: "any"
want_effect: "audit"
# =========================================================================
# Dot and index access in expressions
# =========================================================================
- note: expr_dot_access
policy_rule: |
{
"if": {
"value": "[resourceGroup().name]",
"equals": "myResourceGroup"
},
"then": { "effect": "audit" }
}
resource:
type: "any"
want_effect: "audit"
- note: expr_index_access
policy_rule: |
{
"if": {
"value": "[parameters('allowedLocations')[0]]",
"equals": "eastus"
},
"then": { "effect": "audit" }
}
parameters:
allowedLocations:
- "eastus"
- "westus"
resource:
type: "any"
want_effect: "audit"
# =========================================================================
# Escaped bracket literals (not expressions)
# =========================================================================
- note: escaped_bracket_literal
policy_rule: |
{
"if": {
"field": "name",
"equals": "[[not-an-expression]"
},
"then": { "effect": "audit" }
}
resource:
name: "[not-an-expression]"
want_effect: "audit"
# =========================================================================
# Complex nested expressions
# =========================================================================
- note: expr_complex_nested
policy_rule: |
{
"if": {
"value": "[if(contains(toLower(field('location')), 'us'), 'allowed', 'blocked')]",
"equals": "blocked"
},
"then": { "effect": "deny" }
}
resource:
location: "northeurope"
want_effect: "deny"
- note: expr_multiple_expression_fields
policy_rule: |
{
"if": {
"allOf": [
{
"value": "[parameters('environment')]",
"equals": "production"
},
{
"value": "[concat(parameters('prefix'), '-', parameters('suffix'))]",
"notEquals": ""
},
{
"value": "[length(parameters('allowedLocations'))]",
"greater": 0
}
]
},
"then": { "effect": "deny" }
}
parameters:
environment: "production"
prefix: "prod"
suffix: "01"
allowedLocations:
- "eastus"
resource:
type: "any"
want_effect: "deny"
# =========================================================================
# Unary minus for negative number literals
# =========================================================================
- note: expr_unary_minus_literal
policy_rule: |
{
"if": {
"value": "[add(-1, 5)]",
"equals": 4
},
"then": { "effect": "deny" }
}
resource:
type: "any"
want_effect: "deny"
- note: expr_unary_minus_float
policy_rule: |
{
"if": {
"value": "[add(-2.5, 3.5)]",
"equals": 1
},
"then": { "effect": "deny" }
}
resource:
type: "any"
want_effect: "deny"
- note: expr_unary_minus_sub_expression
policy_rule: |
{
"if": {
"value": "[sub(10, -3)]",
"equals": 13
},
"then": { "effect": "deny" }
}
resource:
type: "any"
want_effect: "deny"
- note: expr_zero_arg_function_call
policy_rule: |
{
"if": {
"value": "[concat()]",
"equals": ""
},
"then": { "effect": "deny" }
}
resource:
type: "any"
want_effect: "deny"

View File

@@ -0,0 +1,323 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Built-in Fields Test Suite
# Tests all built-in field types: type, id, kind, name, location, fullName,
# tags, identity.type, and tag indexing patterns.
cases:
# =========================================================================
# Core built-in fields
# =========================================================================
- note: field_type
policy_rule: |
{
"if": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
want_effect: "deny"
- note: field_id
policy_rule: |
{
"if": {
"field": "id",
"contains": "/resourceGroups/myRg/"
},
"then": { "effect": "audit" }
}
resource:
id: "/subscriptions/sub1/resourceGroups/myRg/providers/Microsoft.Compute/virtualMachines/vm1"
want_effect: "audit"
- note: field_kind
policy_rule: |
{
"if": {
"field": "kind",
"equals": "StorageV2"
},
"then": { "effect": "audit" }
}
resource:
kind: "StorageV2"
want_effect: "audit"
- note: field_name
policy_rule: |
{
"if": {
"field": "name",
"contains": "prod"
},
"then": { "effect": "deny" }
}
resource:
name: "my-prod-vm"
want_effect: "deny"
- note: field_location
policy_rule: |
{
"if": {
"field": "location",
"equals": "eastus"
},
"then": { "effect": "deny" }
}
resource:
location: "eastus"
want_effect: "deny"
- note: field_fullName
policy_rule: |
{
"if": {
"field": "fullName",
"contains": "Microsoft.Compute"
},
"then": { "effect": "audit" }
}
resource:
fullName: "Microsoft.Compute/virtualMachines/vm1"
want_effect: "audit"
- note: field_identity_type
policy_rule: |
{
"if": {
"field": "identity.type",
"equals": "SystemAssigned"
},
"then": { "effect": "audit" }
}
resource:
identity:
type: "SystemAssigned"
want_effect: "audit"
# =========================================================================
# Tags
# =========================================================================
- note: field_tags_object
policy_rule: |
{
"if": {
"field": "tags",
"containsKey": "environment"
},
"then": { "effect": "audit" }
}
resource:
tags:
environment: "production"
want_effect: "audit"
- note: field_tags_dot_notation
policy_rule: |
{
"if": {
"field": "tags.environment",
"equals": "production"
},
"then": { "effect": "audit" }
}
resource:
tags:
environment: "production"
want_effect: "audit"
- note: field_tags_bracket_notation
policy_rule: |
{
"if": {
"field": "tags['environment']",
"equals": "production"
},
"then": { "effect": "deny" }
}
resource:
tags:
environment: "production"
want_effect: "deny"
- note: field_tags_dot_hyphen
policy_rule: |
{
"if": {
"field": "tags.cost-center",
"equals": "engineering"
},
"then": { "effect": "audit" }
}
resource:
tags:
cost-center: "engineering"
want_effect: "audit"
- note: field_tags_bracket_space
policy_rule: |
{
"if": {
"field": "tags['Created By']",
"exists": true
},
"then": { "effect": "audit" }
}
resource:
tags:
Created By: "admin"
want_effect: "audit"
- note: field_tags_missing
policy_rule: |
{
"if": {
"field": "tags.environment",
"exists": false
},
"then": { "effect": "deny" }
}
resource:
tags: {}
want_effect: "deny"
# =========================================================================
# Nested property fields (aliases)
# =========================================================================
# Note: These test parsing of alias-like dotted paths in field position.
# Actual alias resolution is out of scope; these confirm the parser
# correctly handles them.
- note: field_deep_property
policy_rule: |
{
"if": {
"field": "properties.securityProfile.uefiSettings.secureBootEnabled",
"equals": true
},
"then": { "effect": "audit" }
}
resource:
properties:
securityProfile:
uefiSettings:
secureBootEnabled: true
want_effect: "audit"
- note: field_multiple_field_conditions
policy_rule: |
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Network/networkSecurityGroups/securityRules" },
{ "field": "name", "contains": "allow" },
{ "field": "location", "in": ["eastus", "westus", "centralus"] },
{ "field": "tags.team", "equals": "security" }
]
},
"then": { "effect": "audit" }
}
resource:
type: "Microsoft.Network/networkSecurityGroups/securityRules"
name: "allow-https"
location: "eastus"
tags:
team: "security"
want_effect: "audit"
# =========================================================================
# Bracket notation in field paths
# =========================================================================
- note: field_bracket_notation_string_key
policy_rule: |
{
"if": {
"field": "properties.networkAcls['default-action']",
"equals": "Allow"
},
"then": { "effect": "deny" }
}
resource:
properties:
networkAcls:
default-action: "Allow"
want_effect: "deny"
- note: field_bracket_notation_double_quote
policy_rule: |
{
"if": {
"field": "properties.settings['log-level']",
"equals": "debug"
},
"then": { "effect": "deny" }
}
resource:
properties:
settings:
log-level: "debug"
want_effect: "deny"
# =========================================================================
# Array index access in field paths
# =========================================================================
- note: field_array_index_zero
policy_rule: |
{
"if": {
"field": "properties.ipConfigurations[0].name",
"equals": "primary"
},
"then": { "effect": "deny" }
}
resource:
properties:
ipConfigurations:
- name: "primary"
properties:
subnet: "default"
- name: "secondary"
properties:
subnet: "dmz"
want_effect: "deny"
- note: field_array_index_one
policy_rule: |
{
"if": {
"field": "properties.ipConfigurations[1].name",
"equals": "secondary"
},
"then": { "effect": "deny" }
}
resource:
properties:
ipConfigurations:
- name: "primary"
- name: "secondary"
want_effect: "deny"
- note: field_array_index_out_of_bounds
policy_rule: |
{
"if": {
"field": "properties.ipConfigurations[5].name",
"exists": true
},
"then": { "effect": "deny" }
}
resource:
properties:
ipConfigurations:
- name: "primary"
want_undefined: true

View File

@@ -0,0 +1,344 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Logical Combinators Test Suite
# Tests allOf, anyOf, not, and nested combinations.
cases:
# =========================================================================
# allOf
# =========================================================================
- note: allOf_two_conditions
policy_rule: |
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{ "field": "location", "equals": "eastus" }
]
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
location: "eastus"
want_effect: "deny"
- note: allOf_partial_match
policy_rule: |
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{ "field": "location", "equals": "westus" }
]
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
location: "eastus"
want_undefined: true
- note: allOf_three_conditions
policy_rule: |
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{ "field": "location", "equals": "eastus" },
{ "field": "name", "contains": "prod" }
]
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
location: "eastus"
name: "my-prod-vm"
want_effect: "deny"
- note: allOf_single_condition
policy_rule: |
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" }
]
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
want_effect: "deny"
- note: allOf_empty_array
policy_rule: |
{
"if": {
"allOf": []
},
"then": { "effect": "deny" }
}
resource:
type: "anything"
want_effect: "deny"
# =========================================================================
# anyOf
# =========================================================================
- note: anyOf_first_matches
policy_rule: |
{
"if": {
"anyOf": [
{ "field": "location", "equals": "eastus" },
{ "field": "location", "equals": "westus" }
]
},
"then": { "effect": "deny" }
}
resource:
location: "eastus"
want_effect: "deny"
- note: anyOf_second_matches
policy_rule: |
{
"if": {
"anyOf": [
{ "field": "location", "equals": "eastus" },
{ "field": "location", "equals": "westus" }
]
},
"then": { "effect": "deny" }
}
resource:
location: "westus"
want_effect: "deny"
- note: anyOf_no_match
policy_rule: |
{
"if": {
"anyOf": [
{ "field": "location", "equals": "eastus" },
{ "field": "location", "equals": "westus" }
]
},
"then": { "effect": "deny" }
}
resource:
location: "northeurope"
want_undefined: true
- note: anyOf_three_options
policy_rule: |
{
"if": {
"anyOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{ "field": "type", "equals": "Microsoft.Compute/virtualMachineScaleSets" },
{ "field": "type", "equals": "Microsoft.Compute/disks" }
]
},
"then": { "effect": "audit" }
}
resource:
type: "Microsoft.Compute/disks"
want_effect: "audit"
# =========================================================================
# not
# =========================================================================
- note: not_condition
policy_rule: |
{
"if": {
"not": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
}
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Storage/storageAccounts"
want_effect: "deny"
- note: not_condition_no_match
policy_rule: |
{
"if": {
"not": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
}
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
want_undefined: true
- note: not_allOf
policy_rule: |
{
"if": {
"not": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{ "field": "location", "equals": "eastus" }
]
}
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
location: "westus"
want_effect: "deny"
- note: not_anyOf
policy_rule: |
{
"if": {
"not": {
"anyOf": [
{ "field": "location", "equals": "eastus" },
{ "field": "location", "equals": "westus" }
]
}
},
"then": { "effect": "deny" }
}
resource:
location: "northeurope"
want_effect: "deny"
# =========================================================================
# Nested combinations
# =========================================================================
- note: allOf_with_nested_anyOf
policy_rule: |
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{
"anyOf": [
{ "field": "location", "equals": "eastus" },
{ "field": "location", "equals": "westus" }
]
}
]
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
location: "westus"
want_effect: "deny"
- note: anyOf_with_nested_allOf
policy_rule: |
{
"if": {
"anyOf": [
{
"allOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{ "field": "location", "equals": "eastus" }
]
},
{
"allOf": [
{ "field": "type", "equals": "Microsoft.Storage/storageAccounts" },
{ "field": "location", "equals": "westus" }
]
}
]
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Storage/storageAccounts"
location: "westus"
want_effect: "deny"
- note: allOf_with_not
policy_rule: |
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
{
"not": {
"field": "location",
"equals": "eastus"
}
}
]
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
location: "westus"
want_effect: "deny"
- note: deeply_nested_combinators
policy_rule: |
{
"if": {
"allOf": [
{ "field": "type", "equals": "Microsoft.Network/networkSecurityGroups/securityRules" },
{
"not": {
"anyOf": [
{
"allOf": [
{ "field": "properties.protocol", "equals": "TCP" },
{ "field": "properties.destinationPortRange", "in": ["443", "8443"] }
]
},
{
"allOf": [
{ "field": "properties.protocol", "equals": "UDP" },
{ "field": "properties.destinationPortRange", "equals": "53" }
]
}
]
}
}
]
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Network/networkSecurityGroups/securityRules"
properties:
protocol: "TCP"
destinationPortRange: "80"
want_effect: "deny"
- note: double_negation
policy_rule: |
{
"if": {
"not": {
"not": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
}
}
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
want_effect: "deny"

View File

@@ -0,0 +1,453 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Operators Test Suite
# Tests all 19 Azure Policy condition operators with field-based conditions.
cases:
# =========================================================================
# equals / notEquals
# =========================================================================
- note: equals_string
policy_rule: |
{
"if": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
want_effect: "deny"
- note: equals_string_no_match
policy_rule: |
{
"if": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Storage/storageAccounts"
want_undefined: true
- note: equals_number
policy_rule: |
{
"if": {
"field": "properties.count",
"equals": 5
},
"then": { "effect": "audit" }
}
resource:
properties:
count: 5
want_effect: "audit"
- note: equals_boolean
policy_rule: |
{
"if": {
"field": "properties.enabled",
"equals": true
},
"then": { "effect": "audit" }
}
resource:
properties:
enabled: true
want_effect: "audit"
- note: equals_null
policy_rule: |
{
"if": {
"field": "properties.optionalField",
"equals": null
},
"then": { "effect": "audit" }
}
resource:
properties: {}
want_effect: "audit"
- note: notEquals_string
policy_rule: |
{
"if": {
"field": "type",
"notEquals": "Microsoft.Compute/virtualMachines"
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Storage/storageAccounts"
want_effect: "deny"
- note: notEquals_no_match
policy_rule: |
{
"if": {
"field": "type",
"notEquals": "Microsoft.Compute/virtualMachines"
},
"then": { "effect": "deny" }
}
resource:
type: "Microsoft.Compute/virtualMachines"
want_undefined: true
# =========================================================================
# contains / notContains
# =========================================================================
- note: contains_string
policy_rule: |
{
"if": {
"field": "name",
"contains": "prod"
},
"then": { "effect": "audit" }
}
resource:
name: "my-prod-vm"
want_effect: "audit"
- note: contains_no_match
policy_rule: |
{
"if": {
"field": "name",
"contains": "staging"
},
"then": { "effect": "audit" }
}
resource:
name: "my-prod-vm"
want_undefined: true
- note: notContains_string
policy_rule: |
{
"if": {
"field": "name",
"notContains": "staging"
},
"then": { "effect": "audit" }
}
resource:
name: "my-prod-vm"
want_effect: "audit"
# =========================================================================
# containsKey / notContainsKey
# =========================================================================
- note: containsKey_field
policy_rule: |
{
"if": {
"field": "tags",
"containsKey": "environment"
},
"then": { "effect": "audit" }
}
resource:
tags:
environment: "production"
want_effect: "audit"
- note: notContainsKey_field
policy_rule: |
{
"if": {
"field": "tags",
"notContainsKey": "costCenter"
},
"then": { "effect": "deny" }
}
resource:
tags:
environment: "production"
want_effect: "deny"
# =========================================================================
# greater / greaterOrEquals / less / lessOrEquals
# =========================================================================
- note: greater_number
policy_rule: |
{
"if": {
"field": "properties.maxRetries",
"greater": 5
},
"then": { "effect": "deny" }
}
resource:
properties:
maxRetries: 10
want_effect: "deny"
- note: greater_no_match
policy_rule: |
{
"if": {
"field": "properties.maxRetries",
"greater": 5
},
"then": { "effect": "deny" }
}
resource:
properties:
maxRetries: 3
want_undefined: true
- note: greaterOrEquals_equal
policy_rule: |
{
"if": {
"field": "properties.minInstances",
"greaterOrEquals": 3
},
"then": { "effect": "audit" }
}
resource:
properties:
minInstances: 3
want_effect: "audit"
- note: less_number
policy_rule: |
{
"if": {
"field": "properties.retentionDays",
"less": 30
},
"then": { "effect": "deny" }
}
resource:
properties:
retentionDays: 7
want_effect: "deny"
- note: lessOrEquals_number
policy_rule: |
{
"if": {
"field": "properties.maxConnections",
"lessOrEquals": 100
},
"then": { "effect": "audit" }
}
resource:
properties:
maxConnections: 50
want_effect: "audit"
# =========================================================================
# in / notIn
# =========================================================================
- note: in_string_array
policy_rule: |
{
"if": {
"field": "location",
"in": ["eastus", "westus", "centralus"]
},
"then": { "effect": "deny" }
}
resource:
location: "eastus"
want_effect: "deny"
- note: in_no_match
policy_rule: |
{
"if": {
"field": "location",
"in": ["eastus", "westus"]
},
"then": { "effect": "deny" }
}
resource:
location: "northeurope"
want_undefined: true
- note: notIn_string_array
policy_rule: |
{
"if": {
"field": "location",
"notIn": ["eastus", "westus"]
},
"then": { "effect": "deny" }
}
resource:
location: "northeurope"
want_effect: "deny"
- note: in_number_array
policy_rule: |
{
"if": {
"field": "properties.port",
"in": [80, 443, 8080]
},
"then": { "effect": "deny" }
}
resource:
properties:
port: 443
want_effect: "deny"
# =========================================================================
# like / notLike
# =========================================================================
- note: like_wildcard
policy_rule: |
{
"if": {
"field": "name",
"like": "prod-*"
},
"then": { "effect": "audit" }
}
resource:
name: "prod-server-01"
want_effect: "audit"
- note: like_question_mark
policy_rule: |
{
"if": {
"field": "name",
"like": "vm-?"
},
"then": { "effect": "audit" }
}
resource:
name: "vm-1"
want_effect: "audit"
- note: notLike_wildcard
policy_rule: |
{
"if": {
"field": "name",
"notLike": "test-*"
},
"then": { "effect": "audit" }
}
resource:
name: "prod-server-01"
want_effect: "audit"
# =========================================================================
# match / matchInsensitively
# =========================================================================
- note: match_pattern
policy_rule: |
{
"if": {
"field": "name",
"match": "vm-##"
},
"then": { "effect": "audit" }
}
resource:
name: "vm-01"
want_effect: "audit"
- note: matchInsensitively_pattern
policy_rule: |
{
"if": {
"field": "name",
"matchInsensitively": "VM-##"
},
"then": { "effect": "audit" }
}
resource:
name: "vm-01"
want_effect: "audit"
- note: notMatch_pattern
policy_rule: |
{
"if": {
"field": "name",
"notMatch": "test-*"
},
"then": { "effect": "audit" }
}
resource:
name: "prod-server-01"
want_effect: "audit"
- note: notMatchInsensitively_pattern
policy_rule: |
{
"if": {
"field": "name",
"notMatchInsensitively": "TEST-##"
},
"then": { "effect": "audit" }
}
resource:
name: "prod-01"
want_effect: "audit"
# =========================================================================
# exists
# =========================================================================
- note: exists_true
policy_rule: |
{
"if": {
"field": "properties.optionalSetting",
"exists": true
},
"then": { "effect": "audit" }
}
resource:
properties:
optionalSetting: "value"
want_effect: "audit"
- note: exists_false
policy_rule: |
{
"if": {
"field": "properties.optionalSetting",
"exists": false
},
"then": { "effect": "audit" }
}
resource:
properties: {}
want_effect: "audit"
- note: exists_string_true
policy_rule: |
{
"if": {
"field": "properties.optionalSetting",
"exists": "true"
},
"then": { "effect": "audit" }
}
resource:
properties:
optionalSetting: "value"
want_effect: "audit"

View File

@@ -0,0 +1,273 @@
# Copyright (c) Microsoft Corporation.
# Licensed under the MIT License.
# Parse Error Test Suite
# Tests that malformed policy JSON and invalid constructs are properly rejected.
# These test cases are expected to fail parsing.
cases:
# =========================================================================
# Missing required keys
# =========================================================================
- note: missing_if_key
skip: true # Tests policy_rule-level error; needs parse_policy_rule
policy_rule: |
{
"then": { "effect": "deny" }
}
want_parse_error: true
- note: missing_then_key
skip: true # Tests policy_rule-level error; needs parse_policy_rule
policy_rule: |
{
"if": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
}
}
want_parse_error: true
- note: missing_effect_in_then
skip: true # Tests policy_rule-level error; needs parse_policy_rule
policy_rule: |
{
"if": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
"then": {}
}
want_parse_error: true
# =========================================================================
# Missing operator in condition
# =========================================================================
- note: field_without_operator
policy_rule: |
{
"if": {
"field": "type"
},
"then": { "effect": "deny" }
}
want_parse_error: true
- note: value_without_operator
policy_rule: |
{
"if": {
"value": "[parameters('x')]"
},
"then": { "effect": "deny" }
}
want_parse_error: true
# =========================================================================
# Invalid JSON structure
# =========================================================================
- note: allOf_not_array
policy_rule: |
{
"if": {
"allOf": "not-an-array"
},
"then": { "effect": "deny" }
}
want_parse_error: true
- note: anyOf_not_array
policy_rule: |
{
"if": {
"anyOf": 42
},
"then": { "effect": "deny" }
}
want_parse_error: true
- note: not_not_object
policy_rule: |
{
"if": {
"not": [1, 2, 3]
},
"then": { "effect": "deny" }
}
want_parse_error: true
# =========================================================================
# Unknown keys in condition objects
# =========================================================================
- note: unknown_key_in_condition
policy_rule: |
{
"if": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines",
"unknownKey": "value"
},
"then": { "effect": "deny" }
}
want_parse_error: true
# =========================================================================
# Count structure issues
# =========================================================================
- note: count_missing_field_and_value
policy_rule: |
{
"if": {
"count": {},
"equals": 0
},
"then": { "effect": "deny" }
}
want_parse_error: true
- note: count_with_both_field_and_value
policy_rule: |
{
"if": {
"count": {
"field": "some.alias[*]",
"value": ["a", "b"]
},
"equals": 0
},
"then": { "effect": "deny" }
}
want_parse_error: true
# =========================================================================
# Invalid ARM template expressions
# =========================================================================
- note: malformed_expression_unclosed_paren
policy_rule: |
{
"if": {
"value": "[parameters('x']",
"equals": "something"
},
"then": { "effect": "deny" }
}
want_parse_error: true
# =========================================================================
# Both field and value LHS
# =========================================================================
- note: both_field_and_value_lhs
policy_rule: |
{
"if": {
"field": "type",
"value": "something",
"equals": "Microsoft.Compute/virtualMachines"
},
"then": { "effect": "deny" }
}
want_parse_error: true
# =========================================================================
# Empty input
# =========================================================================
- note: empty_object
policy_rule: |
{}
want_parse_error: true
- note: not_an_object
policy_rule: |
"just a string"
want_parse_error: true
# =========================================================================
# Extra keys in logical operators
# =========================================================================
- note: extra_key_in_allOf
policy_rule: |
{
"if": {
"allOf": [
{ "field": "type", "equals": "X" }
],
"field": "name",
"equals": "Y"
},
"then": { "effect": "deny" }
}
want_parse_error: true
- note: extra_key_in_not
policy_rule: |
{
"if": {
"not": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines"
},
"field": "name",
"equals": "something"
},
"then": { "effect": "deny" }
}
want_parse_error: true
# =========================================================================
# count.name errors
# =========================================================================
- note: count_name_with_field_not_value
policy_rule: |
{
"if": {
"count": {
"field": "items[*]",
"name": "item"
},
"equals": 0
},
"then": { "effect": "deny" }
}
want_parse_error: true
- note: count_name_not_string
policy_rule: |
{
"if": {
"count": {
"value": ["a", "b"],
"name": 42
},
"equals": 2
},
"then": { "effect": "deny" }
}
want_parse_error: true
# =========================================================================
# Multiple operators in a single condition
# =========================================================================
- note: multiple_operators
policy_rule: |
{
"if": {
"field": "type",
"equals": "Microsoft.Compute/virtualMachines",
"in": ["Microsoft.Compute/virtualMachines"]
},
"then": { "effect": "deny" }
}
want_parse_error: true

View File

@@ -0,0 +1,175 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! YAML-driven test suite for Azure Policy constraint parser.
//!
//! Each YAML file in `tests/azure_policy/parser_tests/cases/` contains a list
//! of test cases. Each case specifies a `policy_rule` JSON string with
//! `"if"` / `"then"` structure. The test runner extracts the `"if"` constraint
//! JSON and parses it with `parse_constraint`.
use anyhow::Result;
use regorus::languages::azure_policy::parser;
use regorus::Source;
use serde::{Deserialize, Serialize};
use std::fs;
use test_generator::test_resources;
/// A single test case in the YAML file.
#[derive(Serialize, Deserialize, Debug)]
struct TestCase {
/// Short identifier for the test case.
pub note: String,
/// The Azure Policy `policyRule` JSON string.
#[serde(default)]
pub policy_rule: Option<String>,
/// If true, the constraint is expected to fail parsing.
#[serde(default)]
pub want_parse_error: Option<bool>,
/// If true, skip this test case.
#[serde(default)]
pub skip: Option<bool>,
}
/// Top-level YAML test file structure.
#[derive(Serialize, Deserialize, Debug)]
struct YamlTest {
/// Optional global policy rule JSON string.
#[serde(default)]
pub policy_rule: Option<String>,
pub cases: Vec<TestCase>,
}
/// Filter test cases by the `TEST_CASE_FILTER` environment variable.
fn should_run_test_case(case_note: &str) -> bool {
if let Ok(filter) = std::env::var("TEST_CASE_FILTER") {
case_note.contains(&filter)
} else {
true
}
}
/// Extract the `"if"` sub-object from a policy rule JSON string.
///
/// Returns `None` if parsing fails or there is no `"if"` key (the caller
/// should feed the raw string to `parse_constraint` for error tests).
fn extract_if_json(policy_rule_json: &str) -> Option<String> {
let v: serde_json::Value = serde_json::from_str(policy_rule_json).ok()?;
let if_value = v.get("if")?;
Some(if_value.to_string())
}
/// Run all test cases from a YAML file.
fn yaml_test_impl(file: &str) -> Result<()> {
let yaml_str = fs::read_to_string(file)?;
let test: YamlTest = serde_yaml::from_str(&yaml_str)?;
println!("running {file}");
if let Ok(filter) = std::env::var("TEST_CASE_FILTER") {
println!(" Test case filter active: '{filter}'");
}
let mut executed_count = 0usize;
let mut skipped_count = 0usize;
for case in &test.cases {
if !should_run_test_case(&case.note) {
println!(" case {} filtered out", case.note);
skipped_count += 1;
continue;
}
print!(" case {} ", case.note);
if case.skip == Some(true) {
println!("skipped");
skipped_count += 1;
continue;
}
executed_count += 1;
let expects_parse_error = case.want_parse_error == Some(true);
let policy_rule_json = if let Some(ref rule) = case.policy_rule {
rule.clone()
} else if let Some(ref rule) = test.policy_rule {
rule.clone()
} else {
panic!("case '{}': must specify 'policy_rule'", case.note);
};
// Extract the "if" constraint JSON. If extraction fails (malformed
// JSON or missing "if" key), feed the raw policy_rule to
// parse_constraint — it should fail, matching want_parse_error.
let constraint_json =
extract_if_json(&policy_rule_json).unwrap_or_else(|| policy_rule_json.clone());
let source = Source::from_contents(format!("test:{}", case.note), constraint_json)?;
let parse_result = parser::parse_constraint(&source).map(|_| ());
match parse_result {
Ok(()) => {
if expects_parse_error {
panic!(
"case '{}': expected parse error but parsing succeeded",
case.note
);
}
println!("passed (parsed ok)");
}
Err(e) => {
if expects_parse_error {
println!("passed (expected parse error: {})", e);
} else {
panic!("case '{}': unexpected parse error: {}", case.note, e);
}
}
}
}
println!(
" Summary: {executed_count} executed, {skipped_count} skipped, {} total",
test.cases.len()
);
Ok(())
}
#[test_resources("tests/azure_policy/parser_tests/cases/**/*.yaml")]
fn yaml_test(file: &str) {
yaml_test_impl(file).unwrap();
}
/// Test duplicate-key detection directly (bypassing serde_json which
/// silently deduplicates keys).
#[test]
fn duplicate_key_in_condition() {
// Two "field" keys in a single condition object.
let json = r#"{"field": "type", "field": "name", "equals": "X"}"#;
let source = Source::from_contents("test:dup_field".to_string(), json.to_string()).unwrap();
let err = parser::parse_constraint(&source).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("duplicate key"),
"expected duplicate key error, got: {msg}"
);
}
#[test]
fn duplicate_key_in_count() {
// Two "field" keys inside a count block.
let json = r#"{"count": {"field": "a[*]", "field": "b[*]"}, "equals": 0}"#;
let source =
Source::from_contents("test:dup_count_field".to_string(), json.to_string()).unwrap();
let err = parser::parse_constraint(&source).unwrap_err();
let msg = err.to_string();
assert!(
msg.contains("duplicate key"),
"expected duplicate key error, got: {msg}"
);
}