diff --git a/tests/azure_policy/cases/aliases.yaml b/tests/azure_policy/cases/aliases.yaml new file mode 100644 index 0000000..13b9ab1 --- /dev/null +++ b/tests/azure_policy/cases/aliases.yaml @@ -0,0 +1,1131 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Alias Resolution Test Suite +# Tests alias-to-short-name resolution, normalization of ARM resource JSON, +# and correct field access through aliases across different resource types +# and alias patterns (scalar, nested, wildcard arrays, sub-resources, sku.*). + +aliases: test_aliases.json + +cases: + # ========================================================================= + # Scalar alias — properties-flattened path + # ========================================================================= + + - note: alias_scalar_properties_flat + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + want_effect: "deny" + + - note: alias_scalar_properties_flat_false + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: false + want_undefined: true + + - note: alias_scalar_properties_missing_field + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: {} + want_undefined: true + + # ========================================================================= + # Nested alias — dotted path inside properties + # ========================================================================= + + - note: alias_nested_dotted_path + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/customDomain.name", + "equals": "cdn.contoso.com" + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + customDomain: + name: "cdn.contoso.com" + want_effect: "audit" + + - note: alias_nested_dotted_path_mismatch + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/customDomain.name", + "equals": "cdn.contoso.com" + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + customDomain: + name: "other.example.com" + want_undefined: true + + # ========================================================================= + # SKU alias — non-properties root path + # ========================================================================= + + - note: alias_sku_root_path + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/sku.name", + "equals": "Standard_LRS" + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + sku: + name: "Standard_LRS" + want_effect: "deny" + + - note: alias_sku_not_in_list + policy_rule: | + { + "if": { + "not": { + "field": "Microsoft.Storage/storageAccounts/sku.name", + "in": ["Standard_LRS", "Standard_GRS"] + } + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + sku: + name: "Premium_LRS" + want_effect: "deny" + + # ========================================================================= + # Encryption alias — deeply nested properties path + # ========================================================================= + + - note: alias_deep_nested_encryption + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/enableBlobEncryption", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + encryption: + services: + blob: + enabled: true + want_effect: "audit" + + - note: alias_deep_nested_encryption_false + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/enableBlobEncryption", + "notEquals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + encryption: + services: + blob: + enabled: false + want_effect: "deny" + + # ========================================================================= + # Network ACLs alias — nested non-array path + # ========================================================================= + + - note: alias_network_acls_default_action + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction", + "notEquals": "Deny" + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + networkAcls: + defaultAction: "Allow" + want_effect: "audit" + + # ========================================================================= + # Wildcard array alias — ipRules[*] (NOT a sub-resource; no properties flattening) + # ========================================================================= + + - note: alias_wildcard_array_ip_rules_count + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Storage/storageAccounts/networkAcls.ipRules[*]" + }, + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + networkAcls: + ipRules: + - value: "10.0.0.0/24" + action: "Allow" + want_effect: "deny" + + - note: alias_wildcard_array_ip_rules_where + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Storage/storageAccounts/networkAcls.ipRules[*]", + "where": { + "field": "Microsoft.Storage/storageAccounts/networkAcls.ipRules[*].value", + "equals": "10.0.0.0/24" + } + }, + "equals": 1 + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + networkAcls: + ipRules: + - value: "10.0.0.0/24" + action: "Allow" + - value: "192.168.1.0/24" + action: "Allow" + want_effect: "audit" + + - note: alias_wildcard_array_ip_rules_empty + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Storage/storageAccounts/networkAcls.ipRules[*]" + }, + "equals": 0 + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + networkAcls: + ipRules: [] + want_effect: "audit" + + # ========================================================================= + # Sub-resource alias — NSG securityRules[*] (properties flattening) + # ========================================================================= + + - note: alias_sub_resource_nsg_protocol + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "equals": "Tcp" + } + }, + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: "allow-ssh" + properties: + protocol: "Tcp" + access: "Allow" + direction: "Inbound" + want_effect: "deny" + + - note: alias_sub_resource_nsg_no_match + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].protocol", + "equals": "Udp" + } + }, + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: "allow-ssh" + properties: + protocol: "Tcp" + access: "Allow" + want_undefined: true + + - note: alias_sub_resource_nsg_multiple_rules + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "allOf": [ + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", + "equals": "Allow" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction", + "equals": "Inbound" + } + ] + } + }, + "equals": 2 + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: "rule1" + properties: + protocol: "Tcp" + access: "Allow" + direction: "Inbound" + - name: "rule2" + properties: + protocol: "Udp" + access: "Allow" + direction: "Inbound" + - name: "rule3" + properties: + protocol: "Tcp" + access: "Deny" + direction: "Outbound" + want_effect: "audit" + + # ========================================================================= + # Child resource alias — securityRules (non-array, single rule resource) + # ========================================================================= + + - note: alias_child_resource_security_rule_access + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/networkSecurityGroups/securityRules" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/access", + "equals": "Allow" + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups/securityRules" + properties: + access: "Allow" + direction: "Inbound" + want_effect: "deny" + + - note: alias_child_resource_security_rule_direction + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/networkSecurityGroups/securityRules" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules/direction", + "equals": "Outbound" + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups/securityRules" + properties: + direction: "Inbound" + want_undefined: true + + # ========================================================================= + # VM alias — deeply nested storage/OS profiles + # ========================================================================= + + - note: alias_vm_hardware_profile + policy_rule: | + { + "if": { + "field": "Microsoft.Compute/virtualMachines/hardwareProfile.vmSize", + "equals": "Standard_D2s_v3" + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + properties: + hardwareProfile: + vmSize: "Standard_D2s_v3" + want_effect: "audit" + + - note: alias_vm_managed_disk_type + policy_rule: | + { + "if": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.osDisk.managedDisk.storageAccountType", + "notEquals": "Premium_LRS" + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + properties: + storageProfile: + osDisk: + managedDisk: + storageAccountType: "Standard_LRS" + want_effect: "deny" + + - note: alias_vm_data_disks_wildcard + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*]", + "where": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*].managedDisk.storageAccountType", + "notEquals": "Premium_LRS" + } + }, + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + properties: + storageProfile: + dataDisks: + - diskSizeGB: 128 + managedDisk: + storageAccountType: "Standard_LRS" + - diskSizeGB: 256 + managedDisk: + storageAccountType: "Premium_LRS" + want_effect: "deny" + + - note: alias_vm_data_disks_all_premium + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*]", + "where": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*].managedDisk.storageAccountType", + "notEquals": "Premium_LRS" + } + }, + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + properties: + storageProfile: + dataDisks: + - diskSizeGB: 128 + managedDisk: + storageAccountType: "Premium_LRS" + - diskSizeGB: 256 + managedDisk: + storageAccountType: "Premium_LRS" + want_undefined: true + + # ========================================================================= + # KeyVault alias — scalar + nested + # ========================================================================= + + - note: alias_keyvault_create_mode + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.KeyVault/vaults" + }, + { + "field": "Microsoft.KeyVault/vaults/createMode", + "notEquals": "recover" + } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.KeyVault/vaults" + properties: + createMode: "default" + want_effect: "audit" + + - note: alias_keyvault_recover_mode_no_match + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.KeyVault/vaults" + }, + { + "field": "Microsoft.KeyVault/vaults/createMode", + "notEquals": "recover" + } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.KeyVault/vaults" + properties: + createMode: "recover" + want_undefined: true + + - note: alias_keyvault_network_acls + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.KeyVault/vaults" + }, + { + "field": "Microsoft.KeyVault/vaults/networkAcls.defaultAction", + "notEquals": "Deny" + } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.KeyVault/vaults" + properties: + networkAcls: + defaultAction: "Allow" + want_effect: "audit" + + # ========================================================================= + # Case-insensitive alias matching + # ========================================================================= + + - note: alias_case_insensitive_match + policy_rule: | + { + "if": { + "field": "microsoft.storage/storageaccounts/supportsHttpsTrafficOnly", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + want_effect: "deny" + + - note: alias_mixed_case_match + policy_rule: | + { + "if": { + "field": "MICROSOFT.STORAGE/STORAGEACCOUNTS/supportsHttpsTrafficOnly", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + want_effect: "deny" + + # ========================================================================= + # Multiple aliases in same condition (allOf) + # ========================================================================= + + - note: alias_multiple_aliases_allof + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + { + "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "equals": true + }, + { + "field": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction", + "equals": "Deny" + } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + networkAcls: + defaultAction: "Deny" + want_effect: "audit" + + - note: alias_multiple_aliases_one_fails + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + { + "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "equals": true + }, + { + "field": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction", + "equals": "Deny" + } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + networkAcls: + defaultAction: "Allow" + want_undefined: true + + # ========================================================================= + # Alias with exists operator + # ========================================================================= + + - note: alias_exists_true + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/isHnsEnabled", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + isHnsEnabled: true + want_effect: "audit" + + - note: alias_exists_false_when_missing + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/isHnsEnabled", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: {} + want_effect: "audit" + + - note: alias_exists_true_when_missing + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/isHnsEnabled", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: {} + want_undefined: true + + # ========================================================================= + # Alias with not operator + # ========================================================================= + + - note: alias_not_operator + policy_rule: | + { + "if": { + "not": { + "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "equals": true + } + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: false + want_effect: "deny" + + - note: alias_not_operator_negation + policy_rule: | + { + "if": { + "not": { + "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "equals": true + } + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + want_undefined: true + + # ========================================================================= + # Alias with contains/like operators + # ========================================================================= + + - note: alias_contains_operator + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/accessTier", + "contains": "Ho" + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + accessTier: "Hot" + want_effect: "audit" + + - note: alias_like_operator + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/accessTier", + "like": "H*" + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + accessTier: "Hot" + want_effect: "audit" + + # ========================================================================= + # Alias with object parent (whole object) + # ========================================================================= + + - note: alias_object_parent + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/customDomain", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + customDomain: + name: "cdn.contoso.com" + want_effect: "audit" + + - note: alias_object_parent_missing + policy_rule: | + { + "if": { + "field": "Microsoft.Storage/storageAccounts/customDomain", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: {} + want_effect: "audit" + + # ========================================================================= + # VM image reference alias (deep nested, no sub-resource) + # ========================================================================= + + - note: alias_vm_image_publisher + policy_rule: | + { + "if": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.imageReference.publisher", + "equals": "Canonical" + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + properties: + storageProfile: + imageReference: + publisher: "Canonical" + offer: "UbuntuServer" + want_effect: "audit" + + - note: alias_vm_image_offer + policy_rule: | + { + "if": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.imageReference.offer", + "notEquals": "WindowsServer" + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + properties: + storageProfile: + imageReference: + publisher: "Canonical" + offer: "UbuntuServer" + want_effect: "deny" + + # ========================================================================= + # Cross-resource-type alias test (SQL Server auditing) + # ========================================================================= + + - note: alias_sql_server_auditing + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Sql/servers" + }, + { + "field": "Microsoft.Sql/servers/state", + "equals": "Ready" + } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Sql/servers" + properties: + state: "Ready" + want_effect: "audit" + + # ========================================================================= + # NSG sub-resource: securityRules[*].name — not under properties in ARM + # ========================================================================= + + - note: alias_nsg_rule_name_not_in_properties + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].name", + "equals": "allow-rdp" + } + }, + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: "allow-rdp" + properties: + protocol: "Tcp" + access: "Allow" + - name: "allow-ssh" + properties: + protocol: "Tcp" + access: "Allow" + want_effect: "deny" + + # ========================================================================= + # NSG sub-resource: complex where with nested allOf/anyOf + # ========================================================================= + + - note: alias_nsg_complex_where + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "allOf": [ + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", + "equals": "Allow" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction", + "equals": "Inbound" + }, + { + "anyOf": [ + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", + "equals": "3389" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", + "equals": "22" + } + ] + } + ] + } + }, + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: "allow-rdp" + properties: + protocol: "Tcp" + access: "Allow" + direction: "Inbound" + destinationPortRange: "3389" + - name: "allow-https" + properties: + protocol: "Tcp" + access: "Allow" + direction: "Inbound" + destinationPortRange: "443" + want_effect: "deny" + + - note: alias_nsg_complex_where_no_dangerous_ports + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "allOf": [ + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", + "equals": "Allow" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction", + "equals": "Inbound" + }, + { + "anyOf": [ + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", + "equals": "3389" + }, + { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", + "equals": "22" + } + ] + } + ] + } + }, + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + properties: + securityRules: + - name: "allow-https" + properties: + protocol: "Tcp" + access: "Allow" + direction: "Inbound" + destinationPortRange: "443" + - name: "allow-http" + properties: + protocol: "Tcp" + access: "Allow" + direction: "Inbound" + destinationPortRange: "80" + want_undefined: true + + # ========================================================================= + # Alias combined with type gate + # ========================================================================= + + - note: alias_with_type_gate_match + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + { + "field": "Microsoft.Compute/virtualMachines/hardwareProfile.vmSize", + "notIn": ["Standard_D2s_v3", "Standard_D4s_v3"] + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + properties: + hardwareProfile: + vmSize: "Standard_E64i_v3" + want_effect: "deny" + + - note: alias_with_type_gate_no_match_wrong_type + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + { + "field": "Microsoft.Compute/virtualMachines/hardwareProfile.vmSize", + "notIn": ["Standard_D2s_v3", "Standard_D4s_v3"] + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + hardwareProfile: + vmSize: "Standard_E64i_v3" + want_undefined: true + + # ========================================================================= + # Alias with greaterOrEquals / lessOrEquals number operators + # ========================================================================= + + - note: alias_numeric_greater_or_equals + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Storage/storageAccounts/networkAcls.ipRules[*]" + }, + "greaterOrEquals": 3 + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + networkAcls: + ipRules: + - value: "10.0.0.0/24" + - value: "10.0.1.0/24" + - value: "10.0.2.0/24" + want_effect: "deny" + + - note: alias_numeric_less_or_equals + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Storage/storageAccounts/networkAcls.ipRules[*]" + }, + "lessOrEquals": 1 + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + networkAcls: + ipRules: + - value: "10.0.0.0/24" + want_effect: "audit" diff --git a/tests/azure_policy/cases/azure_policies.yaml b/tests/azure_policy/cases/azure_policies.yaml new file mode 100644 index 0000000..d113da8 --- /dev/null +++ b/tests/azure_policy/cases/azure_policies.yaml @@ -0,0 +1,1214 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Azure Policy Scenarios with ARM-shaped resources +# +# These tests model real-world Azure Policy rules that enterprise customers +# commonly deploy. Policy rules use fully-qualified alias names as required by +# the Azure Policy language. Resources use raw ARM shapes with `properties` +# wrappers; the normalizer flattens them before evaluation. + +aliases: test_aliases.json + +cases: + # ========================================================================= + # VM SKU Restriction — Deny non-allowed VM sizes + # ========================================================================= + + - note: vm_sku_restriction_denied + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { + "not": { + "field": "Microsoft.Compute/virtualMachines/sku.name", + "in": "[parameters('allowedSKUs')]" + } + } + ] + }, + "then": { "effect": "deny" } + } + parameters: + allowedSKUs: + - "Standard_D2s_v3" + - "Standard_D4s_v3" + - "Standard_D8s_v3" + resource: + type: "Microsoft.Compute/virtualMachines" + name: "myVM" + location: "eastus" + properties: + hardwareProfile: + vmSize: "Standard_E64i_v3" + want_effect: "deny" + + - note: vm_sku_restriction_allowed + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { + "not": { + "field": "Microsoft.Compute/virtualMachines/sku.name", + "in": "[parameters('allowedSKUs')]" + } + } + ] + }, + "then": { "effect": "deny" } + } + parameters: + allowedSKUs: + - "Standard_D2s_v3" + - "Standard_D4s_v3" + resource: + type: "Microsoft.Compute/virtualMachines" + name: "myVM" + location: "eastus" + properties: + hardwareProfile: + vmSize: "Standard_D2s_v3" + want_effect: ~ + + # ========================================================================= + # Storage — Require blob encryption enabled + # ========================================================================= + + - note: storage_blob_encryption_required_deny + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { + "field": "Microsoft.Storage/storageAccounts/enableBlobEncryption", + "notEquals": true + } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage123" + location: "westus2" + properties: + encryption: + services: + blob: + enabled: false + file: + enabled: true + want_effect: "audit" + + - note: storage_blob_encryption_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { + "field": "Microsoft.Storage/storageAccounts/enableBlobEncryption", + "notEquals": true + } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage123" + location: "westus2" + properties: + encryption: + services: + blob: + enabled: true + file: + enabled: true + want_effect: ~ + + # ========================================================================= + # Key Vault — Require soft delete enabled + # ========================================================================= + + - note: keyvault_soft_delete_required + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.KeyVault/vaults" }, + { "field": "Microsoft.KeyVault/vaults/enableSoftDelete", "notEquals": true } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.KeyVault/vaults" + name: "myvault" + location: "eastus" + properties: + enableSoftDelete: false + enablePurgeProtection: true + want_effect: "deny" + + - note: keyvault_soft_delete_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.KeyVault/vaults" }, + { "field": "Microsoft.KeyVault/vaults/enableSoftDelete", "notEquals": true } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.KeyVault/vaults" + name: "myvault" + location: "eastus" + properties: + enableSoftDelete: true + enablePurgeProtection: true + want_effect: ~ + + # ========================================================================= + # Web App — Enforce minimum TLS version + # ========================================================================= + + - note: webapp_min_tls_version_denied + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Web/sites" }, + { + "field": "Microsoft.Web/sites/siteConfig.minTlsVersion", + "less": "1.2" + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Web/sites" + name: "mywebapp" + location: "eastus" + kind: "app" + properties: + siteConfig: + minTlsVersion: "1.0" + ftpsState: "Disabled" + want_effect: "deny" + + - note: webapp_min_tls_version_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Web/sites" }, + { + "field": "Microsoft.Web/sites/siteConfig.minTlsVersion", + "less": "1.2" + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Web/sites" + name: "mywebapp" + location: "eastus" + kind: "app" + properties: + siteConfig: + minTlsVersion: "1.2" + ftpsState: "Disabled" + want_effect: ~ + + # ========================================================================= + # AKS — Require RBAC enabled + # ========================================================================= + + - note: aks_rbac_required_denied + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.ContainerService/managedClusters" }, + { "field": "Microsoft.ContainerService/managedClusters/enableRBAC", "notEquals": true } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.ContainerService/managedClusters" + name: "myaks" + location: "eastus" + properties: + enableRBAC: false + kubernetesVersion: "1.28.0" + want_effect: "deny" + + - note: aks_rbac_required_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.ContainerService/managedClusters" }, + { "field": "Microsoft.ContainerService/managedClusters/enableRBAC", "notEquals": true } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.ContainerService/managedClusters" + name: "myaks" + location: "eastus" + properties: + enableRBAC: true + kubernetesVersion: "1.28.0" + want_effect: ~ + + # ========================================================================= + # Disk Encryption — Require managed disk encryption + # ========================================================================= + + - note: disk_encryption_required + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/disks" }, + { + "field": "Microsoft.Compute/disks/encryptionSettingsCollection.enabled", + "notEquals": true + } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/disks" + name: "mydisk" + location: "eastus" + sku: + name: "Premium_LRS" + tier: "Premium" + properties: + encryptionSettingsCollection: + enabled: false + want_effect: "audit" + + - note: disk_encryption_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/disks" }, + { + "field": "Microsoft.Compute/disks/encryptionSettingsCollection.enabled", + "notEquals": true + } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/disks" + name: "mydisk" + location: "eastus" + sku: + name: "Premium_LRS" + tier: "Premium" + properties: + encryptionSettingsCollection: + enabled: true + want_effect: ~ + + # ========================================================================= + # Storage — Network ACLs default deny + # ========================================================================= + + - note: storage_network_acls_default_deny + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction", "notEquals": "Deny" } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage123" + location: "westus2" + sku: + name: "Standard_LRS" + tier: "Standard" + properties: + networkAcls: + defaultAction: "Allow" + bypass: "AzureServices" + ipRules: [] + virtualNetworkRules: [] + want_effect: "audit" + + - note: storage_network_acls_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "Microsoft.Storage/storageAccounts/networkAcls.defaultAction", "notEquals": "Deny" } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage123" + location: "westus2" + sku: + name: "Standard_LRS" + tier: "Standard" + properties: + networkAcls: + defaultAction: "Deny" + bypass: "AzureServices" + ipRules: + - value: "10.0.0.0/24" + action: "Allow" + want_effect: ~ + + # ========================================================================= + # NSG — Deny SSH from Internet (realistic multi-condition) + # ========================================================================= + + - note: nsg_deny_ssh_from_internet + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Network/networkSecurityGroups" }, + { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", "equals": "Allow" }, + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction", "equals": "Inbound" }, + { + "anyOf": [ + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", "equals": "22" }, + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", "equals": "*" } + ] + }, + { + "anyOf": [ + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix", "equals": "*" }, + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix", "equals": "Internet" }, + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix", "equals": "0.0.0.0/0" } + ] + } + ] + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + name: "myNsg" + location: "eastus" + properties: + securityRules: + - name: "AllowHTTPS" + properties: + access: "Allow" + direction: "Inbound" + protocol: "Tcp" + destinationPortRange: "443" + sourceAddressPrefix: "*" + priority: 100 + - name: "AllowSSH" + properties: + access: "Allow" + direction: "Inbound" + protocol: "Tcp" + destinationPortRange: "22" + sourceAddressPrefix: "Internet" + priority: 200 + want_effect: "deny" + + - note: nsg_deny_ssh_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Network/networkSecurityGroups" }, + { + "count": { + "field": "Microsoft.Network/networkSecurityGroups/securityRules[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].access", "equals": "Allow" }, + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].direction", "equals": "Inbound" }, + { + "anyOf": [ + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", "equals": "22" }, + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].destinationPortRange", "equals": "*" } + ] + }, + { + "anyOf": [ + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix", "equals": "*" }, + { "field": "Microsoft.Network/networkSecurityGroups/securityRules[*].sourceAddressPrefix", "equals": "Internet" } + ] + } + ] + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + name: "myNsg" + location: "eastus" + properties: + securityRules: + - name: "AllowHTTPS" + properties: + access: "Allow" + direction: "Inbound" + protocol: "Tcp" + destinationPortRange: "443" + sourceAddressPrefix: "*" + priority: 100 + - name: "AllowSSHRestricted" + properties: + access: "Allow" + direction: "Inbound" + protocol: "Tcp" + destinationPortRange: "22" + sourceAddressPrefix: "10.0.0.0/24" + priority: 200 + want_effect: ~ + + # ========================================================================= + # VM — Require managed disks (check storageProfile) + # ========================================================================= + + - note: vm_require_managed_disks + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { + "count": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*]", + "where": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*].managedDisk.id", + "exists": false + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + name: "myVM" + location: "eastus" + sku: + name: "Standard_D4s_v3" + properties: + storageProfile: + osDisk: + osType: "Linux" + managedDisk: + storageAccountType: "Premium_LRS" + dataDisks: + - lun: 0 + name: "datadisk0" + managedDisk: + id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/disks/datadisk0" + storageAccountType: "Premium_LRS" + - lun: 1 + name: "datadisk1" + vhd: + uri: "https://mystorage.blob.core.windows.net/vhds/datadisk1.vhd" + want_effect: "deny" + + - note: vm_all_managed_disks_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { + "count": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*]", + "where": { + "field": "Microsoft.Compute/virtualMachines/storageProfile.dataDisks[*].managedDisk.id", + "exists": false + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + name: "myVM" + location: "eastus" + properties: + storageProfile: + dataDisks: + - lun: 0 + name: "datadisk0" + managedDisk: + id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/disks/datadisk0" + - lun: 1 + name: "datadisk1" + managedDisk: + id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Compute/disks/datadisk1" + want_effect: ~ + + # ========================================================================= + # Allowed resource types — deny everything not in allow-list + # ========================================================================= + + - note: allowed_resource_types_denied + policy_rule: | + { + "if": { + "not": { + "field": "type", + "in": "[parameters('allowedTypes')]" + } + }, + "then": { "effect": "deny" } + } + parameters: + allowedTypes: + - "Microsoft.Compute/virtualMachines" + - "Microsoft.Storage/storageAccounts" + - "Microsoft.Network/networkSecurityGroups" + - "Microsoft.Network/virtualNetworks" + resource: + type: "Microsoft.Sql/servers" + name: "myserver" + location: "eastus" + want_effect: "deny" + + - note: allowed_resource_types_allowed + policy_rule: | + { + "if": { + "not": { + "field": "type", + "in": "[parameters('allowedTypes')]" + } + }, + "then": { "effect": "deny" } + } + parameters: + allowedTypes: + - "Microsoft.Compute/virtualMachines" + - "Microsoft.Storage/storageAccounts" + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage" + location: "eastus" + want_effect: ~ + + # ========================================================================= + # Multi-tag requirement with value count + current() + concat + # ========================================================================= + + - note: multi_tag_requirement_missing_tags + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "notEquals": "Microsoft.Resources/subscriptions" }, + { + "count": { + "value": "[parameters('requiredTags')]", + "name": "tagName", + "where": { + "field": "[concat('tags[', current('tagName'), ']')]", + "exists": true + } + }, + "notEquals": "[length(parameters('requiredTags'))]" + } + ] + }, + "then": { "effect": "deny" } + } + parameters: + requiredTags: + - "Environment" + - "CostCenter" + - "Owner" + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage" + location: "eastus" + tags: + Environment: "Production" + CostCenter: "12345" + want_effect: "deny" + + - note: multi_tag_requirement_all_present + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "notEquals": "Microsoft.Resources/subscriptions" }, + { + "count": { + "value": "[parameters('requiredTags')]", + "name": "tagName", + "where": { + "field": "[concat('tags[', current('tagName'), ']')]", + "exists": true + } + }, + "notEquals": "[length(parameters('requiredTags'))]" + } + ] + }, + "then": { "effect": "deny" } + } + parameters: + requiredTags: + - "Environment" + - "CostCenter" + - "Owner" + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage" + location: "eastus" + tags: + Environment: "Production" + CostCenter: "12345" + Owner: "teamA" + want_effect: ~ + + # ========================================================================= + # Storage — Require minimum TLS version + # ========================================================================= + + - note: storage_min_tls_version_denied + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { + "anyOf": [ + { "field": "Microsoft.Storage/storageAccounts/minimumTlsVersion", "exists": false }, + { "field": "Microsoft.Storage/storageAccounts/minimumTlsVersion", "less": "TLS1_2" } + ] + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage" + location: "eastus" + sku: + name: "Standard_LRS" + properties: + minimumTlsVersion: "TLS1_0" + supportsHttpsTrafficOnly: true + want_effect: "deny" + + - note: storage_min_tls_version_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { + "anyOf": [ + { "field": "Microsoft.Storage/storageAccounts/minimumTlsVersion", "exists": false }, + { "field": "Microsoft.Storage/storageAccounts/minimumTlsVersion", "less": "TLS1_2" } + ] + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage" + location: "eastus" + sku: + name: "Standard_LRS" + properties: + minimumTlsVersion: "TLS1_2" + supportsHttpsTrafficOnly: true + want_effect: ~ + + # ========================================================================= + # SQL Server — Require TLS 1.2 + # ========================================================================= + + - note: sql_server_min_tls_denied + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Sql/servers" }, + { "field": "Microsoft.Sql/servers/minimalTlsVersion", "less": "1.2" } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Sql/servers" + name: "myserver" + location: "eastus" + kind: "v12.0" + properties: + minimalTlsVersion: "1.0" + administratorLogin: "sqladmin" + want_effect: "audit" + + - note: sql_server_min_tls_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Sql/servers" }, + { "field": "Microsoft.Sql/servers/minimalTlsVersion", "less": "1.2" } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Sql/servers" + name: "myserver" + location: "eastus" + kind: "v12.0" + properties: + minimalTlsVersion: "1.2" + administratorLogin: "sqladmin" + want_effect: ~ + + # ========================================================================= + # Combined: type check + parameterized condition + expression + # Deny storage accounts with wrong naming convention + # ========================================================================= + + - note: storage_naming_convention_denied + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { + "not": { + "field": "name", + "like": "[concat(parameters('prefix'), '*')]" + } + } + ] + }, + "then": { "effect": "deny" } + } + parameters: + prefix: "sa" + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorageaccount" + location: "eastus" + want_effect: "deny" + + - note: storage_naming_convention_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { + "not": { + "field": "name", + "like": "[concat(parameters('prefix'), '*')]" + } + } + ] + }, + "then": { "effect": "deny" } + } + parameters: + prefix: "sa" + resource: + type: "Microsoft.Storage/storageAccounts" + name: "saprodeast01" + location: "eastus" + want_effect: ~ + + # ========================================================================= + # NIC — Deny public IP association (count over ipConfigurations) + # ========================================================================= + + - note: nic_deny_public_ip + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Network/networkInterfaces" }, + { + "count": { + "field": "Microsoft.Network/networkInterfaces/ipConfigurations[*]", + "where": { + "field": "Microsoft.Network/networkInterfaces/ipConfigurations[*].publicIPAddress.id", + "exists": true + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkInterfaces" + name: "myNic" + location: "eastus" + properties: + ipConfigurations: + - name: "ipconfig1" + properties: + privateIPAddress: "10.0.0.4" + privateIPAllocationMethod: "Dynamic" + subnet: + id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1" + publicIPAddress: + id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/publicIPAddresses/pip1" + want_effect: "deny" + + - note: nic_no_public_ip_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Network/networkInterfaces" }, + { + "count": { + "field": "Microsoft.Network/networkInterfaces/ipConfigurations[*]", + "where": { + "field": "Microsoft.Network/networkInterfaces/ipConfigurations[*].publicIPAddress.id", + "exists": true + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkInterfaces" + name: "myNic" + location: "eastus" + properties: + ipConfigurations: + - name: "ipconfig1" + properties: + privateIPAddress: "10.0.0.4" + privateIPAllocationMethod: "Dynamic" + subnet: + id: "/subscriptions/sub1/resourceGroups/rg1/providers/Microsoft.Network/virtualNetworks/vnet1/subnets/subnet1" + want_effect: ~ + + # ========================================================================= + # API Version — Require minimum API version for storage accounts + # ========================================================================= + + - note: storage_require_min_api_version_denied + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "apiVersion", "less": "2019-06-01" } + ] + }, + "then": { "effect": "deny" } + } + api_version: "2018-11-01" + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage" + location: "eastus" + properties: + supportsHttpsTrafficOnly: true + want_effect: "deny" + + - note: storage_require_min_api_version_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "apiVersion", "less": "2019-06-01" } + ] + }, + "then": { "effect": "deny" } + } + api_version: "2023-01-01" + resource: + type: "Microsoft.Storage/storageAccounts" + name: "mystorage" + location: "eastus" + properties: + supportsHttpsTrafficOnly: true + want_effect: ~ + + # ========================================================================= + # API Version — Deny old SQL Server API versions + # ========================================================================= + + - note: sql_deny_old_api_version + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Sql/servers" }, + { "field": "apiVersion", "less": "2020-01-01" } + ] + }, + "then": { "effect": "audit" } + } + api_version: "2019-06-01-preview" + resource: + type: "Microsoft.Sql/servers" + name: "myserver" + location: "eastus" + kind: "v12.0" + properties: + minimalTlsVersion: "1.2" + want_effect: "audit" + + - note: sql_deny_old_api_version_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Sql/servers" }, + { "field": "apiVersion", "less": "2020-01-01" } + ] + }, + "then": { "effect": "audit" } + } + api_version: "2023-05-01-preview" + resource: + type: "Microsoft.Sql/servers" + name: "myserver" + location: "eastus" + kind: "v12.0" + properties: + minimalTlsVersion: "1.2" + want_effect: ~ + + # ========================================================================= + # API Version + properties — VM with api_version and nested check + # ========================================================================= + + - note: vm_old_api_with_encryption_denied + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { "field": "apiVersion", "less": "2020-06-01" }, + { + "field": "Microsoft.Compute/virtualMachines/storageProfile.osDisk.managedDisk.storageAccountType", + "notEquals": "Premium_LRS" + } + ] + }, + "then": { "effect": "deny" } + } + api_version: "2019-07-01" + resource: + type: "Microsoft.Compute/virtualMachines" + name: "myVM" + location: "eastus" + sku: + name: "Standard_D4s_v3" + properties: + storageProfile: + osDisk: + osType: "Linux" + managedDisk: + storageAccountType: "Standard_LRS" + want_effect: "deny" + + - note: vm_new_api_with_encryption_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { "field": "apiVersion", "less": "2020-06-01" }, + { + "field": "Microsoft.Compute/virtualMachines/storageProfile.osDisk.managedDisk.storageAccountType", + "notEquals": "Premium_LRS" + } + ] + }, + "then": { "effect": "deny" } + } + api_version: "2023-03-01" + resource: + type: "Microsoft.Compute/virtualMachines" + name: "myVM" + location: "eastus" + sku: + name: "Standard_D4s_v3" + properties: + storageProfile: + osDisk: + osType: "Linux" + managedDisk: + storageAccountType: "Standard_LRS" + want_effect: ~ + + # ========================================================================= + # API Version — Key Vault require recent API version + # ========================================================================= + + - note: keyvault_require_recent_api_version_denied + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.KeyVault/vaults" }, + { + "anyOf": [ + { "field": "apiVersion", "exists": false }, + { "field": "apiVersion", "less": "2021-06-01-preview" } + ] + } + ] + }, + "then": { "effect": "audit" } + } + api_version: "2019-09-01" + resource: + type: "Microsoft.KeyVault/vaults" + name: "myvault" + location: "eastus" + properties: + enableSoftDelete: true + enablePurgeProtection: true + want_effect: "audit" + + - note: keyvault_require_recent_api_version_compliant + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.KeyVault/vaults" }, + { + "anyOf": [ + { "field": "apiVersion", "exists": false }, + { "field": "apiVersion", "less": "2021-06-01-preview" } + ] + } + ] + }, + "then": { "effect": "audit" } + } + api_version: "2023-07-01" + resource: + type: "Microsoft.KeyVault/vaults" + name: "myvault" + location: "eastus" + properties: + enableSoftDelete: true + enablePurgeProtection: true + want_effect: ~ + + # ========================================================================= + # API Version — Web app deny old API versions with TLS check + # ========================================================================= + + - note: webapp_old_api_tls_denied + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Web/sites" }, + { "field": "apiVersion", "lessOrEquals": "2018-11-01" }, + { + "field": "Microsoft.Web/sites/siteConfig.minTlsVersion", + "less": "1.2" + } + ] + }, + "then": { "effect": "deny" } + } + api_version: "2018-02-01" + resource: + type: "Microsoft.Web/sites" + name: "mywebapp" + location: "eastus" + kind: "app" + properties: + siteConfig: + minTlsVersion: "1.0" + want_effect: "deny" + + - note: webapp_new_api_tls_skip + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Web/sites" }, + { "field": "apiVersion", "lessOrEquals": "2018-11-01" }, + { + "field": "Microsoft.Web/sites/siteConfig.minTlsVersion", + "less": "1.2" + } + ] + }, + "then": { "effect": "deny" } + } + api_version: "2023-01-01" + resource: + type: "Microsoft.Web/sites" + name: "mywebapp" + location: "eastus" + kind: "app" + properties: + siteConfig: + minTlsVersion: "1.0" + want_effect: ~ diff --git a/tests/azure_policy/cases/casing.yaml b/tests/azure_policy/cases/casing.yaml new file mode 100644 index 0000000..a0e7f6f --- /dev/null +++ b/tests/azure_policy/cases/casing.yaml @@ -0,0 +1,1155 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Casing & Case-Insensitive Comparison Test Suite +# +# Locks down the two-tier case-insensitivity semantics: +# 1. ARM property keys: OrdinalIgnoreCase (ASCII fold only) +# 2. Azure Policy condition values: InvariantCultureIgnoreCase (full Unicode +# case folding via ICU — ß=SS, ffi=FFI, Σ/σ/ς unified, etc.) +# +# See docs/azure-policy/casing.md for the full specification. + +cases: + # ========================================================================= + # equals — ASCII case-insensitive string values + # ========================================================================= + + - note: equals_ascii_case_insensitive + policy_rule: | + { + "if": { + "field": "type", + "equals": "microsoft.compute/virtualmachines" + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "deny" + + - note: equals_ascii_mixed_case + policy_rule: | + { + "if": { + "field": "location", + "equals": "WESTUS2" + }, + "then": { "effect": "audit" } + } + resource: + location: "westus2" + want_effect: "audit" + + - note: equals_ascii_exact_match + policy_rule: | + { + "if": { + "field": "name", + "equals": "MyVM" + }, + "then": { "effect": "audit" } + } + resource: + name: "myvm" + want_effect: "audit" + + - note: equals_ascii_no_match + policy_rule: | + { + "if": { + "field": "name", + "equals": "MyVM" + }, + "then": { "effect": "audit" } + } + resource: + name: "OtherVM" + want_undefined: true + + # ========================================================================= + # equals — Unicode case folding (InvariantCultureIgnoreCase) + # ========================================================================= + + - note: equals_unicode_sharp_s + policy_rule: | + { + "if": { + "field": "tags.description", + "equals": "Straße" + }, + "then": { "effect": "deny" } + } + resource: + tags: + description: "STRASSE" + want_effect: "deny" + + - note: equals_unicode_sharp_s_reverse + policy_rule: | + { + "if": { + "field": "tags.description", + "equals": "STRASSE" + }, + "then": { "effect": "deny" } + } + resource: + tags: + description: "Straße" + want_effect: "deny" + + - note: equals_unicode_ligature_ffi + policy_rule: | + { + "if": { + "field": "tags.label", + "equals": "ffice" + }, + "then": { "effect": "audit" } + } + resource: + tags: + label: "FFICE" + want_effect: "audit" + + # ========================================================================= + # notEquals — casing + # ========================================================================= + + - note: notEquals_case_insensitive_no_trigger + policy_rule: | + { + "if": { + "field": "location", + "notEquals": "westus2" + }, + "then": { "effect": "deny" } + } + resource: + location: "WestUS2" + want_undefined: true + + - note: notEquals_case_insensitive_trigger + policy_rule: | + { + "if": { + "field": "location", + "notEquals": "eastus" + }, + "then": { "effect": "deny" } + } + resource: + location: "WestUS2" + want_effect: "deny" + + # ========================================================================= + # Tag name case insensitivity + # ========================================================================= + + - note: tag_name_case_insensitive_lowercase_policy + policy_rule: | + { + "if": { + "field": "tags.environment", + "equals": "production" + }, + "then": { "effect": "deny" } + } + resource: + tags: + Environment: "production" + want_effect: "deny" + + - note: tag_name_case_insensitive_uppercase_policy + policy_rule: | + { + "if": { + "field": "tags.Environment", + "equals": "production" + }, + "then": { "effect": "deny" } + } + resource: + tags: + environment: "production" + want_effect: "deny" + + - note: tag_name_case_insensitive_mixed + policy_rule: | + { + "if": { + "field": "tags.CostCenter", + "equals": "finance" + }, + "then": { "effect": "audit" } + } + resource: + tags: + costcenter: "finance" + want_effect: "audit" + + - note: tag_value_case_insensitive + policy_rule: | + { + "if": { + "field": "tags.environment", + "equals": "PRODUCTION" + }, + "then": { "effect": "deny" } + } + resource: + tags: + environment: "Production" + want_effect: "deny" + + # ========================================================================= + # contains — string case insensitivity + # ========================================================================= + + - note: contains_string_case_insensitive + policy_rule: | + { + "if": { + "field": "name", + "contains": "PROD" + }, + "then": { "effect": "audit" } + } + resource: + name: "my-prod-vm" + want_effect: "audit" + + - note: contains_string_unicode_sharp_s + policy_rule: | + { + "if": { + "field": "tags.description", + "contains": "STRASSE" + }, + "then": { "effect": "audit" } + } + resource: + tags: + description: "Die Straße ist lang" + want_effect: "audit" + + - note: notContains_case_insensitive + policy_rule: | + { + "if": { + "field": "name", + "notContains": "PROD" + }, + "then": { "effect": "audit" } + } + resource: + name: "my-prod-vm" + want_undefined: true + + # ========================================================================= + # containsKey — key name case insensitivity + # ========================================================================= + + - note: containsKey_case_insensitive + policy_rule: | + { + "if": { + "field": "tags", + "containsKey": "Environment" + }, + "then": { "effect": "audit" } + } + resource: + tags: + environment: "production" + want_effect: "audit" + + - note: notContainsKey_case_insensitive + policy_rule: | + { + "if": { + "field": "tags", + "notContainsKey": "Environment" + }, + "then": { "effect": "deny" } + } + resource: + tags: + environment: "production" + want_undefined: true + + # ========================================================================= + # in / notIn — case-insensitive membership + # ========================================================================= + + - note: in_case_insensitive + policy_rule: | + { + "if": { + "field": "location", + "in": ["EASTUS", "WESTUS2", "CENTRALUS"] + }, + "then": { "effect": "audit" } + } + resource: + location: "westus2" + want_effect: "audit" + + - note: in_no_match + policy_rule: | + { + "if": { + "field": "location", + "in": ["eastus", "westus2"] + }, + "then": { "effect": "audit" } + } + resource: + location: "northeurope" + want_undefined: true + + - note: notIn_case_insensitive + policy_rule: | + { + "if": { + "field": "location", + "notIn": ["EASTUS", "WESTUS2"] + }, + "then": { "effect": "deny" } + } + resource: + location: "westus2" + want_undefined: true + + # ========================================================================= + # greater / less — case-insensitive string ordering + # ========================================================================= + + - note: greater_string_case_insensitive + policy_rule: | + { + "if": { + "field": "name", + "greater": "ALPHA" + }, + "then": { "effect": "audit" } + } + resource: + name: "beta" + want_effect: "audit" + + - note: greater_string_equal_different_case + policy_rule: | + { + "if": { + "field": "name", + "greater": "HELLO" + }, + "then": { "effect": "audit" } + } + resource: + name: "hello" + want_undefined: true + + - note: less_string_case_insensitive + policy_rule: | + { + "if": { + "field": "name", + "less": "BETA" + }, + "then": { "effect": "audit" } + } + resource: + name: "alpha" + want_effect: "audit" + + - note: greaterOrEquals_string_case_insensitive + policy_rule: | + { + "if": { + "field": "name", + "greaterOrEquals": "HELLO" + }, + "then": { "effect": "audit" } + } + resource: + name: "hello" + want_effect: "audit" + + - note: lessOrEquals_string_case_insensitive + policy_rule: | + { + "if": { + "field": "name", + "lessOrEquals": "HELLO" + }, + "then": { "effect": "audit" } + } + resource: + name: "hello" + want_effect: "audit" + + # ========================================================================= + # like / notLike — case-insensitive wildcard matching + # ========================================================================= + + - note: like_case_insensitive + policy_rule: | + { + "if": { + "field": "name", + "like": "MY-*-VM" + }, + "then": { "effect": "audit" } + } + resource: + name: "my-prod-vm" + want_effect: "audit" + + - note: like_case_insensitive_no_match + policy_rule: | + { + "if": { + "field": "name", + "like": "MY-*-VM" + }, + "then": { "effect": "audit" } + } + resource: + name: "your-prod-vm" + want_undefined: true + + - note: notLike_case_insensitive + policy_rule: | + { + "if": { + "field": "name", + "notLike": "MY-*-VM" + }, + "then": { "effect": "audit" } + } + resource: + name: "my-prod-vm" + want_undefined: true + + # ========================================================================= + # match / matchInsensitively + # ========================================================================= + + - note: match_is_case_sensitive + policy_rule: | + { + "if": { + "field": "name", + "match": "abc-##" + }, + "then": { "effect": "audit" } + } + resource: + name: "abc-12" + want_effect: "audit" + + - note: match_case_sensitive_literal_no_match + policy_rule: | + { + "if": { + "field": "name", + "match": "abc-##" + }, + "then": { "effect": "audit" } + } + resource: + name: "ABC-12" + want_undefined: true + + - note: matchInsensitively_case_insensitive + policy_rule: | + { + "if": { + "field": "name", + "matchInsensitively": "abc-##" + }, + "then": { "effect": "audit" } + } + resource: + name: "ABC-12" + want_effect: "audit" + + # ========================================================================= + # Property key casing (ARM OrdinalIgnoreCase) + # ========================================================================= + + - note: property_key_case_insensitive + policy_rule: | + { + "if": { + "field": "properties.enableHttpsTrafficOnly", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + EnableHttpsTrafficOnly: true + want_effect: "audit" + + # ========================================================================= + # type field — case-insensitive resource type matching + # ========================================================================= + + - note: type_case_insensitive_upper + policy_rule: | + { + "if": { + "field": "type", + "equals": "MICROSOFT.COMPUTE/VIRTUALMACHINES" + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "deny" + + - note: type_case_insensitive_lower + policy_rule: | + { + "if": { + "field": "type", + "equals": "microsoft.compute/virtualmachines" + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "deny" + + # ========================================================================= + # contains with array — case-insensitive element matching + # ========================================================================= + + - note: contains_array_case_insensitive + policy_rule: | + { + "if": { + "field": "properties.allowedLocations", + "contains": "WESTUS2" + }, + "then": { "effect": "audit" } + } + resource: + properties: + allowedLocations: + - "eastus" + - "westus2" + - "centralus" + want_effect: "audit" + + - note: notContains_array_case_insensitive + policy_rule: | + { + "if": { + "field": "properties.allowedLocations", + "notContains": "WESTUS2" + }, + "then": { "effect": "deny" } + } + resource: + properties: + allowedLocations: + - "eastus" + - "westus2" + want_undefined: true + + # ========================================================================= + # exists — not affected by casing (boolean check) + # ========================================================================= + + - note: exists_tag_case_insensitive_name + policy_rule: | + { + "if": { + "field": "tags.Environment", + "exists": true + }, + "then": { "effect": "deny" } + } + resource: + tags: + environment: "production" + want_effect: "deny" + + - note: exists_tag_missing + policy_rule: | + { + "if": { + "field": "tags.costCenter", + "exists": false + }, + "then": { "effect": "deny" } + } + resource: + tags: + environment: "production" + want_effect: "deny" + + # ========================================================================= + # Edge cases: empty strings, numbers, booleans + # ========================================================================= + + - note: equals_empty_string + policy_rule: | + { + "if": { + "field": "tags.label", + "equals": "" + }, + "then": { "effect": "audit" } + } + resource: + tags: + label: "" + want_effect: "audit" + + - note: equals_number_not_affected_by_casing + policy_rule: | + { + "if": { + "field": "properties.count", + "equals": 42 + }, + "then": { "effect": "audit" } + } + resource: + properties: + count: 42 + want_effect: "audit" + + - note: equals_boolean_not_affected_by_casing + policy_rule: | + { + "if": { + "field": "properties.enabled", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + enabled: true + want_effect: "audit" + + - note: equals_string_number_coercion_with_casing + policy_rule: | + { + "if": { + "field": "properties.port", + "equals": "443" + }, + "then": { "effect": "audit" } + } + resource: + properties: + port: 443 + want_effect: "audit" + + # ========================================================================= + # Corner cases: undefined / missing fields + # ========================================================================= + + - note: equals_missing_field_is_undefined + policy_rule: | + { + "if": { + "field": "tags.missing", + "equals": "anything" + }, + "then": { "effect": "deny" } + } + resource: + tags: + environment: "production" + want_undefined: true + + - note: notEquals_missing_field_fires + policy_rule: | + { + "if": { + "field": "tags.missing", + "notEquals": "anything" + }, + "then": { "effect": "deny" } + } + resource: + tags: + environment: "production" + want_effect: "deny" + + - note: contains_string_missing_field_is_undefined + policy_rule: | + { + "if": { + "field": "tags.missing", + "contains": "X" + }, + "then": { "effect": "audit" } + } + resource: + tags: + environment: "production" + want_undefined: true + + - note: in_missing_field_is_undefined + policy_rule: | + { + "if": { + "field": "tags.missing", + "in": ["a", "b"] + }, + "then": { "effect": "audit" } + } + resource: + tags: + environment: "production" + want_undefined: true + + - note: greater_missing_field_is_undefined + policy_rule: | + { + "if": { + "field": "tags.missing", + "greater": "z" + }, + "then": { "effect": "audit" } + } + resource: + tags: + environment: "production" + want_undefined: true + + # ========================================================================= + # Tag bracket notation — tags['Key'] equivalent to tags.Key + # ========================================================================= + + - note: tag_bracket_notation_case_insensitive + policy_rule: | + { + "if": { + "field": "tags['Environment']", + "equals": "production" + }, + "then": { "effect": "deny" } + } + resource: + tags: + environment: "production" + want_effect: "deny" + + - note: tag_bracket_notation_value_ci + policy_rule: | + { + "if": { + "field": "tags['costCenter']", + "equals": "FINANCE" + }, + "then": { "effect": "audit" } + } + resource: + tags: + CostCenter: "finance" + want_effect: "audit" + + # ========================================================================= + # String ↔ Bool coercion with mixed casing + # ========================================================================= + + - note: equals_string_true_uppercase_coercion + policy_rule: | + { + "if": { + "field": "properties.flag", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + flag: "TRUE" + want_effect: "audit" + + - note: equals_string_false_mixed_case_coercion + policy_rule: | + { + "if": { + "field": "properties.flag", + "equals": false + }, + "then": { "effect": "audit" } + } + resource: + properties: + flag: "False" + want_effect: "audit" + + # ========================================================================= + # Unicode: Greek sigma variants (Σ/σ/ς all fold to σ) + # ========================================================================= + + - note: equals_greek_sigma_upper_vs_lower + policy_rule: | + { + "if": { + "field": "tags.label", + "equals": "ΣΕΛΑΣ" + }, + "then": { "effect": "audit" } + } + resource: + tags: + label: "σελας" + want_effect: "audit" + + - note: equals_greek_final_sigma + policy_rule: | + { + "if": { + "field": "tags.label", + "equals": "ΚΟΣΜΟΣ" + }, + "then": { "effect": "audit" } + } + resource: + tags: + label: "κοσμος" + want_effect: "audit" + + # ========================================================================= + # notEquals with Unicode — should NOT fire when values match after folding + # ========================================================================= + + - note: notEquals_unicode_sharp_s_equal + policy_rule: | + { + "if": { + "field": "tags.description", + "notEquals": "STRASSE" + }, + "then": { "effect": "deny" } + } + resource: + tags: + description: "Straße" + want_undefined: true + + # ========================================================================= + # in/notIn with Unicode values + # ========================================================================= + + - note: in_unicode_sharp_s_in_array + policy_rule: | + { + "if": { + "field": "tags.street", + "in": ["STRASSE", "AVENUE", "BOULEVARD"] + }, + "then": { "effect": "audit" } + } + resource: + tags: + street: "Straße" + want_effect: "audit" + + - note: notIn_unicode_value_present + policy_rule: | + { + "if": { + "field": "tags.street", + "notIn": ["STRASSE", "AVENUE"] + }, + "then": { "effect": "deny" } + } + resource: + tags: + street: "Straße" + want_undefined: true + + # ========================================================================= + # contains — empty needle/haystack edge cases + # ========================================================================= + + - note: contains_empty_needle_always_matches + policy_rule: | + { + "if": { + "field": "name", + "contains": "" + }, + "then": { "effect": "audit" } + } + resource: + name: "anything" + want_effect: "audit" + + - note: contains_empty_haystack_no_match + policy_rule: | + { + "if": { + "field": "name", + "contains": "something" + }, + "then": { "effect": "audit" } + } + resource: + name: "" + want_undefined: true + + - note: notContains_string_truly_absent + policy_rule: | + { + "if": { + "field": "name", + "notContains": "STAGING" + }, + "then": { "effect": "audit" } + } + resource: + name: "my-prod-vm" + want_effect: "audit" + + # ========================================================================= + # contains with Unicode ligature as substring + # ========================================================================= + + - note: contains_unicode_ligature_substring + policy_rule: | + { + "if": { + "field": "tags.description", + "contains": "ffice" + }, + "then": { "effect": "audit" } + } + resource: + tags: + description: "The OFFICE is closed" + want_effect: "audit" + + # ========================================================================= + # containsKey — key truly missing vs case mismatch + # ========================================================================= + + - note: containsKey_key_truly_missing + policy_rule: | + { + "if": { + "field": "tags", + "containsKey": "billing" + }, + "then": { "effect": "audit" } + } + resource: + tags: + environment: "production" + want_undefined: true + + - note: notContainsKey_key_truly_missing + policy_rule: | + { + "if": { + "field": "tags", + "notContainsKey": "billing" + }, + "then": { "effect": "deny" } + } + resource: + tags: + environment: "production" + want_effect: "deny" + + # ========================================================================= + # greater/less with Unicode — ß folds to "ss" for ordering + # ========================================================================= + + - note: greater_unicode_sharp_s_equal_after_fold + policy_rule: | + { + "if": { + "field": "tags.street", + "greater": "strasse" + }, + "then": { "effect": "audit" } + } + resource: + tags: + street: "Straße" + want_undefined: true + + - note: lessOrEquals_unicode_sharp_s + policy_rule: | + { + "if": { + "field": "tags.street", + "lessOrEquals": "STRASSE" + }, + "then": { "effect": "audit" } + } + resource: + tags: + street: "Straße" + want_effect: "audit" + + # ========================================================================= + # like with Unicode — input folded before wildcard matching + # ========================================================================= + + - note: like_unicode_folded_match + policy_rule: | + { + "if": { + "field": "tags.description", + "like": "die strasse*" + }, + "then": { "effect": "audit" } + } + resource: + tags: + description: "Die Straße ist lang" + want_effect: "audit" + + # ========================================================================= + # matchInsensitively with Unicode literals + # ========================================================================= + + - note: matchInsensitively_unicode_literal + policy_rule: | + { + "if": { + "field": "tags.code", + "matchInsensitively": "strasse-##" + }, + "then": { "effect": "audit" } + } + resource: + tags: + code: "STRASSE-42" + want_effect: "audit" + + # ========================================================================= + # Nested property key casing (deep paths) + # ========================================================================= + + - note: nested_property_key_deep_path + policy_rule: | + { + "if": { + "field": "properties.networkProfile.primaryNic", + "equals": "nic-01" + }, + "then": { "effect": "audit" } + } + resource: + properties: + NetworkProfile: + PrimaryNic: "NIC-01" + want_effect: "audit" + + # ========================================================================= + # Multiple tags — lookup finds correct tag by name + # ========================================================================= + + - note: multiple_tags_lookup_correct_one + policy_rule: | + { + "if": { + "allOf": [ + { "field": "tags.Environment", "equals": "production" }, + { "field": "tags.CostCenter", "equals": "FINANCE" } + ] + }, + "then": { "effect": "deny" } + } + resource: + tags: + environment: "PRODUCTION" + costCenter: "finance" + want_effect: "deny" + + # ========================================================================= + # in with single-element array + # ========================================================================= + + - note: in_single_element_case_insensitive + policy_rule: | + { + "if": { + "field": "location", + "in": ["WestUS2"] + }, + "then": { "effect": "audit" } + } + resource: + location: "westus2" + want_effect: "audit" + + # ========================================================================= + # Null value interactions + # ========================================================================= + + - note: equals_null_explicit + policy_rule: | + { + "if": { + "field": "properties.optionalField", + "equals": null + }, + "then": { "effect": "audit" } + } + resource: + properties: + optionalField: null + want_effect: "audit" + + - note: equals_null_missing_field + policy_rule: | + { + "if": { + "field": "properties.optionalField", + "equals": null + }, + "then": { "effect": "audit" } + } + resource: + properties: {} + want_effect: "audit" + + - note: notEquals_null_with_string_value + policy_rule: | + { + "if": { + "field": "properties.optionalField", + "notEquals": null + }, + "then": { "effect": "audit" } + } + resource: + properties: + optionalField: "has-a-value" + want_effect: "audit" + + # ========================================================================= + # String self-comparison with different casing + # ========================================================================= + + - note: equals_self_different_case + policy_rule: | + { + "if": { + "field": "name", + "equals": "My-Production-VM" + }, + "then": { "effect": "audit" } + } + resource: + name: "MY-PRODUCTION-VM" + want_effect: "audit" diff --git a/tests/azure_policy/cases/complex_policies.yaml b/tests/azure_policy/cases/complex_policies.yaml new file mode 100644 index 0000000..ade884b --- /dev/null +++ b/tests/azure_policy/cases/complex_policies.yaml @@ -0,0 +1,482 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Complex Policies Test Suite +# Tests realistic, multi-layer Azure Policy definitions covering combinations of +# operators, logical combinators, expressions, fields, counts, and effects. + +cases: + # ========================================================================= + # Require HTTPS for storage accounts + # ========================================================================= + + - note: require_https_storage + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + { + "field": "properties.supportsHttpsTrafficOnly", + "notEquals": true + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: false + want_effect: "deny" + + # ========================================================================= + # Allowed locations with parameterized effect + # ========================================================================= + + - note: allowed_locations_parameterized + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "location", + "notIn": "[parameters('allowedLocations')]" + }, + { + "field": "location", + "notEquals": "global" + }, + { + "field": "type", + "notEquals": "Microsoft.AzureActiveDirectory/b2cDirectories" + } + ] + }, + "then": { + "effect": "[parameters('effect')]" + } + } + parameters: + allowedLocations: + - "eastus" + - "westus" + effect: "deny" + resource: + type: "Microsoft.Compute/virtualMachines" + location: "northeurope" + want_effect: "deny" + + # ========================================================================= + # Require tags with parameter-driven enforcement + # ========================================================================= + + - note: require_tag_environment + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "notEquals": "Microsoft.Resources/subscriptions" + }, + { + "field": "[concat('tags[', parameters('tagName'), ']')]", + "exists": false + } + ] + }, + "then": { + "effect": "deny", + "details": { + "message": "Required tag is missing" + } + } + } + parameters: + tagName: "environment" + resource: + type: "Microsoft.Compute/virtualMachines" + tags: {} + want_effect: "deny" + + # ========================================================================= + # NSG rule restriction — deny risky inbound ports + # ========================================================================= + + - note: deny_risky_inbound_nsg + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/networkSecurityGroups/securityRules" + }, + { + "field": "properties.direction", + "equals": "Inbound" + }, + { + "field": "properties.access", + "equals": "Allow" + }, + { + "anyOf": [ + { + "field": "properties.destinationPortRange", + "in": ["22", "3389", "*"] + }, + { + "field": "properties.sourceAddressPrefix", + "in": ["*", "Internet", "0.0.0.0/0"] + } + ] + } + ] + }, + "then": { + "effect": "deny", + "details": { + "message": "Risky inbound NSG rules are not allowed" + } + } + } + resource: + type: "Microsoft.Network/networkSecurityGroups/securityRules" + properties: + direction: "Inbound" + access: "Allow" + destinationPortRange: "22" + sourceAddressPrefix: "*" + want_effect: "deny" + + # ========================================================================= + # Modify — add tags if missing + # ========================================================================= + + - note: modify_add_tags + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + { + "anyOf": [ + { "field": "tags.environment", "exists": false }, + { "field": "tags.costCenter", "exists": false } + ] + } + ] + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['environment']", + "value": "[if(empty(field('tags.environment')), 'unknown', field('tags.environment'))]" + }, + { + "operation": "addOrReplace", + "field": "tags['costCenter']", + "value": "[if(empty(field('tags.costCenter')), 'unassigned', field('tags.costCenter'))]" + } + ] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + tags: + environment: "prod" + want_effect: "modify" + + # ========================================================================= + # Count — deny if too many open NSG rules + # ========================================================================= + + - note: deny_excessive_open_nsg_rules + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Network/networkSecurityGroups" + }, + { + "count": { + "field": "securityRules[*]", + "where": { + "allOf": [ + { + "field": "securityRules[*].access", + "equals": "Allow" + }, + { + "field": "securityRules[*].direction", + "equals": "Inbound" + }, + { + "field": "securityRules[*].sourceAddressPrefix", + "equals": "*" + } + ] + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + securityRules: + - + access: "Allow" + direction: "Inbound" + sourceAddressPrefix: "*" + want_effect: "deny" + + # ========================================================================= + # AuditIfNotExists — require diagnostics settings + # ========================================================================= + + - note: audit_diagnostics_settings + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.KeyVault/vaults" + }, + "then": { + "effect": "auditIfNotExists", + "details": { + "type": "Microsoft.Insights/diagnosticSettings", + "existenceCondition": { + "allOf": [ + { + "field": "properties.logs.enabled", + "equals": true + }, + { + "field": "properties.logs.retentionPolicy.enabled", + "equals": true + }, + { + "field": "properties.logs.retentionPolicy.days", + "greaterOrEquals": 90 + } + ] + } + } + } + } + resource: + type: "Microsoft.KeyVault/vaults" + host_await: + - key: + operation: "lookup_related_resources" + type: "Microsoft.Insights/diagnosticSettings" + response: null + want_effect: "auditIfNotExists" + + # ========================================================================= + # DeployIfNotExists — deploy monitoring agent + # ========================================================================= + + - note: deploy_monitoring_agent + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + { + "field": "properties.storageProfile.imageReference.publisher", + "equals": "Canonical" + } + ] + }, + "then": { + "effect": "deployIfNotExists", + "details": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/9980e02c-c2be-4d73-94e8-173b1dc7cf3c" + ], + "existenceCondition": { + "allOf": [ + { + "field": "properties.publisher", + "equals": "Microsoft.Azure.Monitor" + }, + { + "field": "properties.type", + "equals": "AzureMonitorLinuxAgent" + } + ] + }, + "deployment": { + "properties": { + "mode": "incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "parameters": {}, + "resources": [] + } + } + } + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + properties: + storageProfile: + imageReference: + publisher: "Canonical" + host_await: + - response: null + want_effect: "deployIfNotExists" + + # ========================================================================= + # Value count with complex where — required tags + # ========================================================================= + + - note: required_tags_value_count + policy_rule: | + { + "if": { + "allOf": [ + { + "field": "type", + "notEquals": "Microsoft.Resources/subscriptions" + }, + { + "count": { + "value": "[parameters('requiredTags')]", + "name": "tagName", + "where": { + "field": "[concat('tags[', current('tagName'), ']')]", + "exists": true + } + }, + "notEquals": "[length(parameters('requiredTags'))]" + } + ] + }, + "then": { + "effect": "deny", + "details": { + "message": "Not all required tags are present" + } + } + } + parameters: + requiredTags: + - "environment" + - "costCenter" + - "owner" + resource: + type: "Microsoft.Compute/virtualMachines" + tags: + environment: "prod" + costCenter: "12345" + want_effect: "deny" + + # ========================================================================= + # Multi-resource type policy with not + # ========================================================================= + + - note: multi_type_with_not + policy_rule: | + { + "if": { + "allOf": [ + { + "not": { + "anyOf": [ + { "field": "type", "equals": "Microsoft.Resources/subscriptions" }, + { "field": "type", "equals": "Microsoft.Resources/subscriptions/resourceGroups" }, + { "field": "type", "equals": "Microsoft.Authorization/roleAssignments" } + ] + } + }, + { + "field": "location", + "notIn": "[parameters('allowedLocations')]" + } + ] + }, + "then": { + "effect": "[parameters('effect')]", + "details": { + "message": "Resource location is not in the allowed list" + } + } + } + parameters: + allowedLocations: + - "eastus" + - "westus" + - "centralus" + effect: "deny" + resource: + type: "Microsoft.Compute/virtualMachines" + location: "southeastasia" + want_effect: "deny" + + # ========================================================================= + # Condition-free policy (always true if block is trivially satisfied) + # ========================================================================= + + - note: trivial_allOf_empty + policy_rule: | + { + "if": { + "allOf": [] + }, + "then": { "effect": "audit" } + } + resource: + type: "anything" + want_effect: "audit" + + # ========================================================================= + # Exists mixed with value comparisons + # ========================================================================= + + - note: exists_and_value_check + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "properties.networkAcls", "exists": true }, + { "field": "properties.networkAcls.defaultAction", "notEquals": "Deny" } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + networkAcls: + defaultAction: "Allow" + want_effect: "deny" diff --git a/tests/azure_policy/cases/count.yaml b/tests/azure_policy/cases/count.yaml new file mode 100644 index 0000000..76b52e2 --- /dev/null +++ b/tests/azure_policy/cases/count.yaml @@ -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" diff --git a/tests/azure_policy/cases/deep_nesting.yaml b/tests/azure_policy/cases/deep_nesting.yaml new file mode 100644 index 0000000..caee96b --- /dev/null +++ b/tests/azure_policy/cases/deep_nesting.yaml @@ -0,0 +1,684 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Deep Nesting & Edge-Case Test Suite +# Tests deeply nested logical combinators, nested ARM expressions, +# nested count loops, and edge cases around condition-no-match paths. + +cases: + # ========================================================================= + # Deep logical combinator nesting (4+ levels) + # ========================================================================= + + - note: four_level_nesting + policy_rule: | + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "not": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { "field": "location", "equals": "westus" } + ] + } + }, + { + "field": "kind", "equals": "linux" + } + ] + }, + { "field": "name", "notEquals": "" } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + location: "eastus" + kind: "windows" + name: "my-vm" + want_effect: "audit" + + - note: four_level_nesting_no_match + policy_rule: | + { + "if": { + "allOf": [ + { + "anyOf": [ + { + "not": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { "field": "location", "equals": "eastus" } + ] + } + }, + { + "field": "kind", "equals": "linux" + } + ] + }, + { "field": "name", "notEquals": "" } + ] + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + location: "eastus" + kind: "windows" + name: "my-vm" + want_undefined: true + + - note: five_level_nesting + policy_rule: | + { + "if": { + "not": { + "allOf": [ + { + "anyOf": [ + { + "not": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "properties.supportsHttpsTrafficOnly", "equals": true } + ] + } + } + ] + } + ] + } + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + want_effect: "deny" + + # ========================================================================= + # Deeply nested ARM template expressions + # ========================================================================= + + - note: nested_toLower_concat + policy_rule: | + { + "if": { + "value": "[toLower(concat(parameters('prefix'), '-', field('name')))]", + "equals": "prod-myvm" + }, + "then": { "effect": "audit" } + } + parameters: + prefix: "PROD" + resource: + name: "MYVM" + want_effect: "audit" + + - note: nested_if_equals_contains + policy_rule: | + { + "if": { + "value": "[if(contains(field('location'), 'us'), 'allowed', 'blocked')]", + "equals": "blocked" + }, + "then": { "effect": "deny" } + } + resource: + location: "northeurope" + want_effect: "deny" + + - note: nested_if_equals_allowed + policy_rule: | + { + "if": { + "value": "[if(contains(field('location'), 'us'), 'allowed', 'blocked')]", + "equals": "blocked" + }, + "then": { "effect": "deny" } + } + resource: + location: "eastus" + want_undefined: true + + - note: nested_length_of_concat + policy_rule: | + { + "if": { + "value": "[length(concat(parameters('a'), parameters('b')))]", + "greater": 6 + }, + "then": { "effect": "audit" } + } + parameters: + a: "hello" + b: "world" + resource: + type: "any" + want_effect: "audit" + + - note: nested_add_length_length + policy_rule: | + { + "if": { + "value": "[add(length(parameters('list1')), length(parameters('list2')))]", + "equals": 5 + }, + "then": { "effect": "audit" } + } + parameters: + list1: ["a", "b"] + list2: ["c", "d", "e"] + resource: + type: "any" + want_effect: "audit" + + - note: triple_nested_replace_toLower + policy_rule: | + { + "if": { + "value": "[replace(toLower(field('name')), '-', '_')]", + "equals": "my_vm" + }, + "then": { "effect": "audit" } + } + resource: + name: "My-VM" + want_effect: "audit" + + - note: triple_nested_substring_concat + policy_rule: | + { + "if": { + "value": "[substring(concat(parameters('prefix'), '-', field('name')), 0, 4)]", + "equals": "prod" + }, + "then": { "effect": "audit" } + } + parameters: + prefix: "prod" + resource: + name: "myvm" + want_effect: "audit" + + # ========================================================================= + # Count inside anyOf (not just allOf) + # ========================================================================= + + - note: count_inside_anyOf + policy_rule: | + { + "if": { + "anyOf": [ + { + "count": { + "field": "securityRules[*]", + "where": { + "field": "securityRules[*].access", + "equals": "Allow" + } + }, + "greater": 5 + }, + { + "field": "type", + "equals": "something-else" + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + securityRules: + - { access: "Allow" } + - { access: "Allow" } + - { access: "Allow" } + - { access: "Allow" } + - { access: "Allow" } + - { access: "Allow" } + want_effect: "deny" + + # ========================================================================= + # Count edge cases + # ========================================================================= + + - note: field_count_empty_array + policy_rule: | + { + "if": { + "count": { + "field": "items[*]" + }, + "equals": 0 + }, + "then": { "effect": "audit" } + } + resource: + items: [] + want_effect: "audit" + + - note: field_count_where_matches_none + policy_rule: | + { + "if": { + "count": { + "field": "securityRules[*]", + "where": { + "field": "securityRules[*].access", + "equals": "SuperAllow" + } + }, + "equals": 0 + }, + "then": { "effect": "audit" } + } + resource: + securityRules: + - { access: "Allow" } + - { access: "Deny" } + want_effect: "audit" + + - note: field_count_where_matches_all + policy_rule: | + { + "if": { + "count": { + "field": "items[*]", + "where": { + "field": "items[*].enabled", + "equals": true + } + }, + "equals": 3 + }, + "then": { "effect": "audit" } + } + resource: + items: + - { enabled: true } + - { enabled: true } + - { enabled: true } + want_effect: "audit" + + - note: value_count_empty_parameter_array + policy_rule: | + { + "if": { + "count": { + "value": "[parameters('emptyList')]" + }, + "equals": 0 + }, + "then": { "effect": "audit" } + } + parameters: + emptyList: [] + resource: + type: "any" + want_effect: "audit" + + - note: value_count_greater_no_match + policy_rule: | + { + "if": { + "count": { + "value": ["a", "b"] + }, + "greater": 5 + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_undefined: true + + # ========================================================================= + # Count with deeply nested where clauses + # ========================================================================= + + - note: count_where_allOf_nested_anyOf + policy_rule: | + { + "if": { + "count": { + "field": "rules[*]", + "where": { + "allOf": [ + { "field": "rules[*].enabled", "equals": true }, + { + "anyOf": [ + { "field": "rules[*].priority", "equals": "high" }, + { "field": "rules[*].priority", "equals": "critical" } + ] + } + ] + } + }, + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + rules: + - { enabled: true, priority: "low" } + - { enabled: true, priority: "critical" } + - { enabled: false, priority: "high" } + want_effect: "deny" + + - note: count_where_not_nested + policy_rule: | + { + "if": { + "count": { + "field": "items[*]", + "where": { + "not": { + "anyOf": [ + { "field": "items[*].status", "equals": "approved" }, + { "field": "items[*].status", "equals": "pending" } + ] + } + } + }, + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + items: + - { status: "approved" } + - { status: "rejected" } + - { status: "pending" } + want_effect: "deny" + + # ========================================================================= + # Operators with missing/null fields + # ========================================================================= + + - note: greater_on_missing_field + policy_rule: | + { + "if": { + "field": "properties.missingProp", + "greater": 10 + }, + "then": { "effect": "deny" } + } + resource: + properties: {} + want_undefined: true + + - note: less_on_missing_field + policy_rule: | + { + "if": { + "field": "properties.missingProp", + "less": 100 + }, + "then": { "effect": "deny" } + } + resource: + properties: {} + want_undefined: true + + - note: contains_on_missing_field + policy_rule: | + { + "if": { + "field": "properties.missingProp", + "contains": "anything" + }, + "then": { "effect": "deny" } + } + resource: + properties: {} + want_undefined: true + + - note: in_on_missing_field + policy_rule: | + { + "if": { + "field": "properties.missingProp", + "in": ["a", "b", "c"] + }, + "then": { "effect": "deny" } + } + resource: + properties: {} + want_undefined: true + + - note: like_on_missing_field + policy_rule: | + { + "if": { + "field": "properties.missingProp", + "like": "any*" + }, + "then": { "effect": "deny" } + } + resource: + properties: {} + want_undefined: true + + - note: match_on_missing_field + policy_rule: | + { + "if": { + "field": "properties.missingProp", + "match": "test-##" + }, + "then": { "effect": "deny" } + } + resource: + properties: {} + want_undefined: true + + - note: notEquals_on_missing_field + policy_rule: | + { + "if": { + "field": "properties.missingProp", + "notEquals": "something" + }, + "then": { "effect": "audit" } + } + resource: + properties: {} + want_effect: "audit" + + - note: notIn_on_missing_field + policy_rule: | + { + "if": { + "field": "properties.missingProp", + "notIn": ["a", "b"] + }, + "then": { "effect": "audit" } + } + resource: + properties: {} + want_effect: "audit" + + - note: exists_true_on_missing_field + policy_rule: | + { + "if": { + "field": "properties.missingProp", + "exists": true + }, + "then": { "effect": "deny" } + } + resource: + properties: {} + want_undefined: true + + # ========================================================================= + # ARM expression functions: equals() and contains() as function calls + # ========================================================================= + + - note: expr_func_equals + policy_rule: | + { + "if": { + "value": "[if(equals(field('type'), 'Microsoft.Compute/virtualMachines'), 'vm', 'other')]", + "equals": "vm" + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "audit" + + - note: expr_func_contains_array + policy_rule: | + { + "if": { + "value": "[if(contains(parameters('allowedTypes'), field('type')), 'yes', 'no')]", + "equals": "no" + }, + "then": { "effect": "deny" } + } + parameters: + allowedTypes: + - "Microsoft.Storage/storageAccounts" + - "Microsoft.Compute/virtualMachines" + resource: + type: "Microsoft.Network/virtualNetworks" + want_effect: "deny" + + # ========================================================================= + # Type coercion edge cases: greaterOrEquals, lessOrEquals, notEquals, notIn + # ========================================================================= + + - note: coercion_notEquals_string_number + policy_rule: | + { + "if": { + "field": "properties.port", + "notEquals": 443 + }, + "then": { "effect": "deny" } + } + resource: + properties: + port: "80" + want_effect: "deny" + + - note: coercion_greaterOrEquals_string_number + policy_rule: | + { + "if": { + "field": "properties.port", + "greaterOrEquals": 80 + }, + "then": { "effect": "deny" } + } + resource: + properties: + port: "80" + want_effect: "deny" + + - note: coercion_lessOrEquals_string_number + policy_rule: | + { + "if": { + "field": "properties.port", + "lessOrEquals": 443 + }, + "then": { "effect": "deny" } + } + resource: + properties: + port: "80" + want_effect: "deny" + + - note: coercion_notIn_mixed_types + policy_rule: | + { + "if": { + "field": "properties.port", + "notIn": [80, 443] + }, + "then": { "effect": "deny" } + } + resource: + properties: + port: "8080" + want_effect: "deny" + + # ========================================================================= + # Complex real-world: multiple count + combinator + expression + # ========================================================================= + + - note: complex_nsg_with_tag_and_count + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Network/networkSecurityGroups" }, + { + "not": { + "field": "tags.exception", + "equals": "true" + } + }, + { + "count": { + "field": "securityRules[*]", + "where": { + "allOf": [ + { "field": "securityRules[*].access", "equals": "Allow" }, + { "field": "securityRules[*].direction", "equals": "Inbound" }, + { + "anyOf": [ + { "field": "securityRules[*].sourceAddressPrefix", "equals": "*" }, + { "field": "securityRules[*].sourceAddressPrefix", "equals": "Internet" } + ] + } + ] + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Network/networkSecurityGroups" + tags: + environment: "prod" + securityRules: + - { access: "Allow", direction: "Inbound", sourceAddressPrefix: "Internet" } + - { access: "Deny", direction: "Outbound", sourceAddressPrefix: "10.0.0.0/8" } + want_effect: "deny" + + - note: complex_value_count_with_expr_where + policy_rule: | + { + "if": { + "count": { + "value": "[parameters('requiredPorts')]", + "name": "port", + "where": { + "value": "[current('port')]", + "greater": 1024 + } + }, + "equals": 2 + }, + "then": { "effect": "audit" } + } + parameters: + requiredPorts: [80, 8080, 9090] + resource: + type: "any" + want_effect: "audit" diff --git a/tests/azure_policy/cases/effect_details.yaml b/tests/azure_policy/cases/effect_details.yaml new file mode 100644 index 0000000..1a6c1ef --- /dev/null +++ b/tests/azure_policy/cases/effect_details.yaml @@ -0,0 +1,539 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Effect Details Test Suite +# Tests that effects produce structured result objects: +# { "effect": "", "details": { ... } } +# and that want_details validation works correctly. + +cases: + # ========================================================================= + # Simple effects — structured result with no details + # ========================================================================= + + - note: deny_structured_result + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "deny" + + - note: audit_structured_result + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "audit" + + # ========================================================================= + # Modify — single operation + # ========================================================================= + + - note: modify_single_operation_details + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['environment']", + "value": "production" + } + ] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "modify" + want_details: + roleDefinitionIds: + - "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + operations: + - operation: "addOrReplace" + field: "tags['environment']" + value: "production" + + # ========================================================================= + # Modify — multiple operations + # ========================================================================= + + - note: modify_multiple_operations_details + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['environment']", + "value": "production" + }, + { + "operation": "add", + "field": "tags['managedBy']", + "value": "policy" + } + ] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "modify" + want_details: + roleDefinitionIds: + - "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + operations: + - operation: "addOrReplace" + field: "tags['environment']" + value: "production" + - operation: "add" + field: "tags['managedBy']" + value: "policy" + + # ========================================================================= + # Modify — operation with condition + # ========================================================================= + + - note: modify_operation_with_condition + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['environment']", + "value": "production", + "condition": "[equals(field('tags.environment'), '')]" + } + ] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "modify" + want_details: + roleDefinitionIds: + - "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + operations: + - operation: "addOrReplace" + field: "tags['environment']" + value: "production" + condition: "[equals(field('tags.environment'), '')]" + + # ========================================================================= + # Modify — template expression in value + # ========================================================================= + + - note: modify_template_expression_value + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { "field": "tags.environment", "exists": false } + ] + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['environment']", + "value": "[if(empty(field('tags.environment')), 'unknown', field('tags.environment'))]" + } + ] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + tags: {} + want_effect: "modify" + want_details: + roleDefinitionIds: + - "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + operations: + - operation: "addOrReplace" + field: "tags['environment']" + value: "unknown" + + # ========================================================================= + # Modify — template expression with existing tag value + # ========================================================================= + + - note: modify_template_expression_existing_value + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['environment']", + "value": "[if(empty(field('tags.environment')), 'unknown', field('tags.environment'))]" + } + ] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + tags: + environment: "staging" + want_effect: "modify" + want_details: + roleDefinitionIds: + - "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + operations: + - operation: "addOrReplace" + field: "tags['environment']" + value: "staging" + + # ========================================================================= + # Modify — no details (bare effect) + # ========================================================================= + + - note: modify_no_details + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "modify" + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "modify" + + # ========================================================================= + # Append — single item + # ========================================================================= + + - note: append_single_item_details + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "append", + "details": [ + { + "field": "properties.supportsHttpsTrafficOnly", + "value": true + } + ] + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + want_effect: "append" + want_details: + - field: "properties.supportsHttpsTrafficOnly" + value: true + + # ========================================================================= + # Append — multiple items + # ========================================================================= + + - note: append_multiple_items_details + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "append", + "details": [ + { + "field": "properties.supportsHttpsTrafficOnly", + "value": true + }, + { + "field": "properties.minimumTlsVersion", + "value": "TLS1_2" + } + ] + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + want_effect: "append" + want_details: + - field: "properties.supportsHttpsTrafficOnly" + value: true + - field: "properties.minimumTlsVersion" + value: "TLS1_2" + + # ========================================================================= + # Append — template expression in value + # ========================================================================= + + - note: append_template_expression_value + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "append", + "details": [ + { + "field": "properties.networkAcls.defaultAction", + "value": "[toLower('Deny')]" + } + ] + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + want_effect: "append" + want_details: + - field: "properties.networkAcls.defaultAction" + value: "deny" + + # ========================================================================= + # Cross-resource effect — auditIfNotExists with details + # ========================================================================= + + - note: aine_structured_details + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "auditIfNotExists", + "details": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/9980e02c-c2be-4d73-94e8-173b1dc7cf3c" + ], + "existenceCondition": { + "field": "properties.publisher", + "equals": "Microsoft.Azure.Security" + } + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + host_await: + - response: null + want_effect: "auditIfNotExists" + want_details: + roleDefinitionIds: + - "/providers/Microsoft.Authorization/roleDefinitions/9980e02c-c2be-4d73-94e8-173b1dc7cf3c" + type: "Microsoft.Compute/virtualMachines/extensions" + + # ========================================================================= + # Cross-resource effect — deployIfNotExists with details + # ========================================================================= + + - note: dine_structured_details + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "deployIfNotExists", + "details": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/9980e02c-c2be-4d73-94e8-173b1dc7cf3c" + ], + "existenceCondition": { + "field": "properties.publisher", + "equals": "Microsoft.Azure.Monitoring" + } + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + host_await: + - response: null + want_effect: "deployIfNotExists" + want_details: + roleDefinitionIds: + - "/providers/Microsoft.Authorization/roleDefinitions/9980e02c-c2be-4d73-94e8-173b1dc7cf3c" + type: "Microsoft.Compute/virtualMachines/extensions" + + # ========================================================================= + # Parameterized effect resolving to Modify + # ========================================================================= + + - note: parameterized_modify_details + policy_definition: | + { + "properties": { + "parameters": { + "effect": { + "type": "String", + "defaultValue": "Modify" + } + }, + "policyRule": { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "[parameters('effect')]", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['owner']", + "value": "platform-team" + } + ] + } + } + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "Modify" + want_details: + roleDefinitionIds: + - "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + operations: + - operation: "addOrReplace" + field: "tags['owner']" + value: "platform-team" + + # ========================================================================= + # Parameterized effect resolving to Append + # ========================================================================= + + - note: parameterized_append_details + policy_definition: | + { + "properties": { + "parameters": { + "effect": { + "type": "String", + "defaultValue": "Append" + } + }, + "policyRule": { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "[parameters('effect')]", + "details": [ + { + "field": "properties.supportsHttpsTrafficOnly", + "value": true + } + ] + } + } + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + want_effect: "Append" + want_details: + - field: "properties.supportsHttpsTrafficOnly" + value: true + + # ========================================================================= + # Condition not matching → undefined (no details) + # ========================================================================= + + - note: modify_condition_not_met + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['environment']", + "value": "production" + } + ] + } + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + want_undefined: true diff --git a/tests/azure_policy/cases/effects.yaml b/tests/azure_policy/cases/effects.yaml new file mode 100644 index 0000000..198baf8 --- /dev/null +++ b/tests/azure_policy/cases/effects.yaml @@ -0,0 +1,428 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Effects Test Suite +# Tests all 9 Azure Policy effect types and parameterized effects. + +cases: + # ========================================================================= + # Simple effects + # ========================================================================= + + - note: effect_deny + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "deny" + + - note: effect_audit + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "audit" + + - note: effect_disabled + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { "effect": "disabled" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + # Azure Policy: "disabled" means the policy is inactive — no compliance + # result is produced. The compiler correctly returns undefined. + want_undefined: true + + - note: effect_manual + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { "effect": "manual" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "manual" + + - note: effect_denyAction + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "denyAction", + "details": { + "actionNames": ["delete"] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "denyAction" + + # ========================================================================= + # Effects with details + # ========================================================================= + + - note: effect_deny_with_message + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "deny", + "details": { + "message": "Storage accounts must use HTTPS" + } + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + want_effect: "deny" + + - note: effect_append + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "append", + "details": [ + { + "field": "properties.supportsHttpsTrafficOnly", + "value": true + } + ] + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + want_effect: "append" + + - note: effect_modify + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { "field": "tags.environment", "exists": false } + ] + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['environment']", + "value": "production" + } + ] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + tags: {} + want_effect: "modify" + + - note: effect_modify_multiple_operations + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['environment']", + "value": "production" + }, + { + "operation": "add", + "field": "tags['managedBy']", + "value": "policy" + } + ] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "modify" + + - note: effect_auditIfNotExists + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "auditIfNotExists", + "details": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "existenceCondition": { + "allOf": [ + { "field": "properties.publisher", "equals": "Microsoft.Azure.Security" }, + { "field": "properties.type", "equals": "IaaSAntimalware" } + ] + } + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + host_await: + - key: + operation: "lookup_related_resources" + type: "Microsoft.Compute/virtualMachines/extensions" + response: null + want_effect: "auditIfNotExists" + + # Related resource found and existenceCondition matches → compliant (undefined) + - note: effect_auditIfNotExists_compliant + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "auditIfNotExists", + "details": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "existenceCondition": { + "allOf": [ + { "field": "properties.publisher", "equals": "Microsoft.Azure.Security" }, + { "field": "properties.type", "equals": "IaaSAntimalware" } + ] + } + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + host_await: + - response: + properties: + publisher: "Microsoft.Azure.Security" + type: "IaaSAntimalware" + want_undefined: true + + # Related resource found but existenceCondition fails → non-compliant + - note: effect_auditIfNotExists_condition_fails + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "auditIfNotExists", + "details": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "existenceCondition": { + "allOf": [ + { "field": "properties.publisher", "equals": "Microsoft.Azure.Security" }, + { "field": "properties.type", "equals": "IaaSAntimalware" } + ] + } + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + host_await: + - response: + properties: + publisher: "SomeOtherPublisher" + type: "SomeOtherExtension" + want_effect: "auditIfNotExists" + + # No existenceCondition — just check if resource exists + - note: effect_auditIfNotExists_no_condition_exists + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "auditIfNotExists", + "details": { + "type": "Microsoft.Compute/virtualMachines/extensions" + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + host_await: + - response: + name: "some-extension" + properties: {} + want_undefined: true + + # No existenceCondition, resource not found → non-compliant + - note: effect_auditIfNotExists_no_condition_missing + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "auditIfNotExists", + "details": { + "type": "Microsoft.Compute/virtualMachines/extensions" + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + host_await: + - response: null + want_effect: "auditIfNotExists" + + - note: effect_deployIfNotExists + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "deployIfNotExists", + "details": { + "type": "Microsoft.Compute/virtualMachines/extensions", + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/9980e02c-c2be-4d73-94e8-173b1dc7cf3c" + ], + "existenceCondition": { + "field": "properties.publisher", + "equals": "Microsoft.Azure.Monitoring" + }, + "deployment": { + "properties": { + "mode": "incremental", + "template": { + "$schema": "https://schema.management.azure.com/schemas/2019-04-01/deploymentTemplate.json#", + "contentVersion": "1.0.0.0", + "resources": [] + } + } + } + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + host_await: + - response: null + want_effect: "deployIfNotExists" + + # ========================================================================= + # Parameterized effects + # ========================================================================= + + - note: effect_parameterized + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "[parameters('effect')]" + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + parameters: + effect: "deny" + want_effect: "deny" + + - note: effect_parameterized_with_details + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "[parameters('effect')]", + "details": { + "message": "HTTPS required for storage accounts" + } + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + parameters: + effect: "audit" + want_effect: "audit" + + # ========================================================================= + # Case-insensitive effect names + # ========================================================================= + + - note: effect_case_insensitive_Deny + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { "effect": "Deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "Deny" + + - note: effect_case_insensitive_AUDIT + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { "effect": "AUDIT" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "AUDIT" diff --git a/tests/azure_policy/cases/exists.yaml b/tests/azure_policy/cases/exists.yaml new file mode 100644 index 0000000..078f45f --- /dev/null +++ b/tests/azure_policy/cases/exists.yaml @@ -0,0 +1,463 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Exists Operator – comprehensive tests +# +# Covers bare-field paths, dotted nested paths, null values, missing fields, +# object-valued fields, array-valued fields, and edge cases. These lock down +# the behaviour exercised by the external csharp-converted test suite. + +cases: + # ========================================================================= + # Basic exists true / false on properties path + # ========================================================================= + + - note: exists_true_field_present + policy_rule: | + { + "if": { + "field": "properties.setting", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + setting: "hello" + want_effect: "audit" + + - note: exists_true_field_absent + policy_rule: | + { + "if": { + "field": "properties.setting", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: {} + want_undefined: true + + - note: exists_false_field_absent + policy_rule: | + { + "if": { + "field": "properties.setting", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + properties: {} + want_effect: "audit" + + - note: exists_false_field_present + policy_rule: | + { + "if": { + "field": "properties.setting", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + properties: + setting: "hello" + want_undefined: true + + # ========================================================================= + # Null-valued fields + # ========================================================================= + + - note: exists_true_null_value + policy_rule: | + { + "if": { + "field": "properties.setting", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + setting: null + want_undefined: true + + - note: exists_false_null_value + policy_rule: | + { + "if": { + "field": "properties.setting", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + properties: + setting: null + want_effect: "audit" + + # ========================================================================= + # Null parent object + # ========================================================================= + + - note: exists_true_null_parent + policy_rule: | + { + "if": { + "field": "properties.nested.value", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + nested: null + want_undefined: true + + - note: exists_false_null_parent + policy_rule: | + { + "if": { + "field": "properties.nested.value", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + properties: + nested: null + want_effect: "audit" + + # ========================================================================= + # Object-valued fields (exists checks the field itself, not its children) + # ========================================================================= + + - note: exists_true_object_value + policy_rule: | + { + "if": { + "field": "properties.config", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + config: + enabled: true + want_effect: "audit" + + - note: exists_false_object_value + policy_rule: | + { + "if": { + "field": "properties.config", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + properties: + config: + enabled: true + want_undefined: true + + - note: exists_true_empty_object + policy_rule: | + { + "if": { + "field": "properties.config", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + config: {} + want_effect: "audit" + + # ========================================================================= + # Array-valued fields + # ========================================================================= + + - note: exists_true_array_value + policy_rule: | + { + "if": { + "field": "properties.items", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + items: + - "a" + - "b" + want_effect: "audit" + + - note: exists_true_empty_array + policy_rule: | + { + "if": { + "field": "properties.items", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + items: [] + want_effect: "audit" + + - note: exists_false_array_absent + policy_rule: | + { + "if": { + "field": "properties.items", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + properties: {} + want_effect: "audit" + + # ========================================================================= + # Deeply nested dotted paths + # ========================================================================= + + - note: exists_true_deep_path + policy_rule: | + { + "if": { + "field": "properties.network.subnet.cidr", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + network: + subnet: + cidr: "10.0.0.0/24" + want_effect: "audit" + + - note: exists_false_deep_path_missing_leaf + policy_rule: | + { + "if": { + "field": "properties.network.subnet.cidr", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + properties: + network: + subnet: {} + want_effect: "audit" + + - note: exists_false_deep_path_missing_intermediate + policy_rule: | + { + "if": { + "field": "properties.network.subnet.cidr", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + properties: + network: {} + want_effect: "audit" + + # ========================================================================= + # Root-level fields (type, name, location, etc.) + # ========================================================================= + + - note: exists_true_root_type + policy_rule: | + { + "if": { + "field": "type", + "exists": true + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "deny" + + - note: exists_false_root_location_absent + policy_rule: | + { + "if": { + "field": "location", + "exists": false + }, + "then": { "effect": "deny" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "deny" + + - note: exists_true_root_tags + policy_rule: | + { + "if": { + "field": "tags", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + tags: + env: "prod" + want_effect: "audit" + + - note: exists_false_root_tags_absent + policy_rule: | + { + "if": { + "field": "tags", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "audit" + + # ========================================================================= + # Numeric & boolean values (non-null truthy values ⇒ exists true) + # ========================================================================= + + - note: exists_true_zero + policy_rule: | + { + "if": { + "field": "properties.count", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + count: 0 + want_effect: "audit" + + - note: exists_true_false_boolean + policy_rule: | + { + "if": { + "field": "properties.enabled", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + enabled: false + want_effect: "audit" + + - note: exists_true_empty_string + policy_rule: | + { + "if": { + "field": "properties.label", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + label: "" + want_effect: "audit" + + # ========================================================================= + # Combined with allOf / anyOf + # ========================================================================= + + - note: exists_allof_both_present + policy_rule: | + { + "if": { + "allOf": [ + { "field": "properties.a", "exists": true }, + { "field": "properties.b", "exists": true } + ] + }, + "then": { "effect": "audit" } + } + resource: + properties: + a: 1 + b: 2 + want_effect: "audit" + + - note: exists_allof_one_missing + policy_rule: | + { + "if": { + "allOf": [ + { "field": "properties.a", "exists": true }, + { "field": "properties.b", "exists": true } + ] + }, + "then": { "effect": "audit" } + } + resource: + properties: + a: 1 + want_undefined: true + + - note: exists_anyof_one_present + policy_rule: | + { + "if": { + "anyOf": [ + { "field": "properties.a", "exists": true }, + { "field": "properties.b", "exists": true } + ] + }, + "then": { "effect": "audit" } + } + resource: + properties: + b: "yes" + want_effect: "audit" + + # ========================================================================= + # exists with string value ("true"/"false") – must behave same as boolean + # ========================================================================= + + - note: exists_string_false_field_absent + policy_rule: | + { + "if": { + "field": "properties.setting", + "exists": "false" + }, + "then": { "effect": "audit" } + } + resource: + properties: {} + want_effect: "audit" + + - note: exists_string_false_field_present + policy_rule: | + { + "if": { + "field": "properties.setting", + "exists": "false" + }, + "then": { "effect": "audit" } + } + resource: + properties: + setting: "val" + want_undefined: true diff --git a/tests/azure_policy/cases/expressions.yaml b/tests/azure_policy/cases/expressions.yaml new file mode 100644 index 0000000..9dc7e5a --- /dev/null +++ b/tests/azure_policy/cases/expressions.yaml @@ -0,0 +1,541 @@ +# 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" diff --git a/tests/azure_policy/cases/field_wildcard_collect.yaml b/tests/azure_policy/cases/field_wildcard_collect.yaml new file mode 100644 index 0000000..bf35129 --- /dev/null +++ b/tests/azure_policy/cases/field_wildcard_collect.yaml @@ -0,0 +1,253 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Field Wildcard Collection Test Suite +# Tests field('path[*]') and field('path[*].prop') as value expressions +# that return arrays, including missing-array → empty-array handling. + +cases: + # ========================================================================= + # field('path[*]') — collect all elements + # ========================================================================= + + - note: field_wildcard_collect_all_elements + policy_rule: | + { + "if": { + "value": "[length(field('items[*]'))]", + "equals": 3 + }, + "then": { "effect": "deny" } + } + resource: + items: + - "a" + - "b" + - "c" + want_effect: "deny" + + - note: field_wildcard_collect_all_elements_no_match + policy_rule: | + { + "if": { + "value": "[length(field('items[*]'))]", + "equals": 5 + }, + "then": { "effect": "deny" } + } + resource: + items: + - "a" + - "b" + - "c" + want_undefined: true + + # ========================================================================= + # field('path[*].prop') — collect property from each element + # ========================================================================= + + - note: field_wildcard_collect_property + policy_rule: | + { + "if": { + "value": "[first(field('rules[*].action'))]", + "equals": "Allow" + }, + "then": { "effect": "deny" } + } + resource: + rules: + - { "action": "Allow", "priority": 100 } + - { "action": "Deny", "priority": 200 } + want_effect: "deny" + + - note: field_wildcard_collect_property_length + policy_rule: | + { + "if": { + "value": "[length(field('securityRules[*].access'))]", + "equals": 4 + }, + "then": { "effect": "audit" } + } + resource: + securityRules: + - { "access": "Allow" } + - { "access": "Deny" } + - { "access": "Allow" } + - { "access": "Deny" } + want_effect: "audit" + + # ========================================================================= + # Nested property path after [*] + # ========================================================================= + + - note: field_wildcard_nested_property + policy_rule: | + { + "if": { + "value": "[length(field('rules[*].target.name'))]", + "greater": 0 + }, + "then": { "effect": "deny" } + } + resource: + rules: + - target: + name: "web-app" + - target: + name: "api-app" + want_effect: "deny" + + # ========================================================================= + # Dotted prefix before [*] + # ========================================================================= + + - note: field_wildcard_dotted_prefix + policy_rule: | + { + "if": { + "value": "[length(field('properties.ipRules[*].value'))]", + "equals": 2 + }, + "then": { "effect": "deny" } + } + resource: + properties: + ipRules: + - { "value": "10.0.0.0/8" } + - { "value": "192.168.0.0/16" } + want_effect: "deny" + + # ========================================================================= + # Missing array → empty collection + # ========================================================================= + + - note: field_wildcard_missing_array_length_zero + policy_rule: | + { + "if": { + "value": "[length(field('items[*]'))]", + "equals": 0 + }, + "then": { "effect": "deny" } + } + resource: + type: "some.type" + want_effect: "deny" + + - note: field_wildcard_missing_array_empty_true + policy_rule: | + { + "if": { + "value": "[empty(field('properties.ipRules[*]'))]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + properties: + enabled: true + want_effect: "deny" + + - note: field_wildcard_missing_nested_prefix + policy_rule: | + { + "if": { + "value": "[length(field('config.logging.entries[*].level'))]", + "equals": 0 + }, + "then": { "effect": "deny" } + } + resource: + type: "some.type" + want_effect: "deny" + + # ========================================================================= + # Empty array → empty collection (not missing, but zero elements) + # ========================================================================= + + - note: field_wildcard_empty_array + policy_rule: | + { + "if": { + "value": "[length(field('items[*]'))]", + "equals": 0 + }, + "then": { "effect": "deny" } + } + resource: + items: [] + want_effect: "deny" + + # ========================================================================= + # Doubly-nested wildcards: field('a[*].b[*].c') — flat-map + # ========================================================================= + + - note: field_wildcard_doubly_nested + policy_rule: | + { + "if": { + "value": "[length(field('groups[*].members[*].name'))]", + "equals": 4 + }, + "then": { "effect": "deny" } + } + resource: + groups: + - members: + - { "name": "alice" } + - { "name": "bob" } + - members: + - { "name": "carol" } + - { "name": "dave" } + want_effect: "deny" + + - note: field_wildcard_doubly_nested_no_suffix + policy_rule: | + { + "if": { + "value": "[length(field('groups[*].tags[*]'))]", + "equals": 5 + }, + "then": { "effect": "deny" } + } + resource: + groups: + - tags: ["a", "b", "c"] + - tags: ["d", "e"] + want_effect: "deny" + + - note: field_wildcard_doubly_nested_partial_missing + policy_rule: | + { + "if": { + "value": "[length(field('groups[*].members[*].name'))]", + "equals": 2 + }, + "then": { "effect": "deny" } + } + resource: + groups: + - members: + - { "name": "alice" } + - { "name": "bob" } + - other_field: "no members here" + want_effect: "deny" + + # ========================================================================= + # Real-world pattern: field() with [*] used in condition comparisons + # ========================================================================= + + - note: field_wildcard_first_equals + policy_rule: | + { + "if": { + "value": "[first(field('ports[*]'))]", + "equals": 80 + }, + "then": { "effect": "deny" } + } + resource: + ports: [80, 443, 8080] + want_effect: "deny" diff --git a/tests/azure_policy/cases/fields.yaml b/tests/azure_policy/cases/fields.yaml new file mode 100644 index 0000000..6b78cf5 --- /dev/null +++ b/tests/azure_policy/cases/fields.yaml @@ -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 diff --git a/tests/azure_policy/cases/implicit_allof.yaml b/tests/azure_policy/cases/implicit_allof.yaml new file mode 100644 index 0000000..c68f7c4 --- /dev/null +++ b/tests/azure_policy/cases/implicit_allof.yaml @@ -0,0 +1,555 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Implicit allOf Test Suite +# When a field with [*] appears in a condition outside count, Azure Policy +# applies implicit allOf semantics: ALL elements must satisfy the operator. +# Empty arrays evaluate to true (vacuous truth); missing arrays evaluate to +# false because the field is not iterable and the condition is not met. + +cases: + # ========================================================================= + # Basic allOf — all elements match + # ========================================================================= + + - note: allof_equals_all_match + policy_rule: | + { + "if": { + "field": "items[*].status", + "equals": "active" + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "status": "active" } + - { "status": "active" } + - { "status": "active" } + want_effect: "deny" + + - note: allof_equals_one_mismatch + policy_rule: | + { + "if": { + "field": "items[*].status", + "equals": "active" + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "status": "active" } + - { "status": "inactive" } + - { "status": "active" } + want_undefined: true + + # ========================================================================= + # Empty array → true (vacuous truth) + # ========================================================================= + + - note: allof_empty_array_is_true + policy_rule: | + { + "if": { + "field": "items[*].status", + "equals": "active" + }, + "then": { "effect": "deny" } + } + resource: + items: [] + want_effect: "deny" + + # ========================================================================= + # Missing array → false (not iterable → condition not met) + # Azure Policy: field[*] on null/missing/non-array → false + # ========================================================================= + + - note: allof_missing_array_is_false + policy_rule: | + { + "if": { + "field": "items[*].status", + "equals": "active" + }, + "then": { "effect": "deny" } + } + resource: + type: "some.type" + want_undefined: true + + # ========================================================================= + # No suffix — elements compared directly + # ========================================================================= + + - note: allof_no_suffix_all_match + policy_rule: | + { + "if": { + "field": "ports[*]", + "equals": 443 + }, + "then": { "effect": "deny" } + } + resource: + ports: [443, 443, 443] + want_effect: "deny" + + - note: allof_no_suffix_one_mismatch + policy_rule: | + { + "if": { + "field": "ports[*]", + "equals": 443 + }, + "then": { "effect": "deny" } + } + resource: + ports: [443, 80, 443] + want_undefined: true + + # ========================================================================= + # Dotted prefix before [*] + # ========================================================================= + + - note: allof_dotted_prefix + policy_rule: | + { + "if": { + "field": "properties.rules[*].enabled", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + properties: + rules: + - { "enabled": true } + - { "enabled": true } + want_effect: "deny" + + - note: allof_dotted_prefix_mismatch + policy_rule: | + { + "if": { + "field": "properties.rules[*].enabled", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + properties: + rules: + - { "enabled": true } + - { "enabled": false } + want_undefined: true + + # ========================================================================= + # Various operators + # ========================================================================= + + - note: allof_not_equals + policy_rule: | + { + "if": { + "field": "items[*].value", + "notEquals": "bad" + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "value": "good" } + - { "value": "fine" } + want_effect: "deny" + + - note: allof_not_equals_one_matches + policy_rule: | + { + "if": { + "field": "items[*].value", + "notEquals": "bad" + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "value": "good" } + - { "value": "bad" } + want_undefined: true + + - note: allof_greater + policy_rule: | + { + "if": { + "field": "scores[*]", + "greater": 50 + }, + "then": { "effect": "deny" } + } + resource: + scores: [80, 90, 100] + want_effect: "deny" + + - note: allof_greater_one_fails + policy_rule: | + { + "if": { + "field": "scores[*]", + "greater": 50 + }, + "then": { "effect": "deny" } + } + resource: + scores: [80, 30, 100] + want_undefined: true + + - note: allof_in_operator + policy_rule: | + { + "if": { + "field": "regions[*]", + "in": ["eastus", "westus", "centralus"] + }, + "then": { "effect": "deny" } + } + resource: + regions: ["eastus", "westus"] + want_effect: "deny" + + - note: allof_in_operator_one_outside + policy_rule: | + { + "if": { + "field": "regions[*]", + "in": ["eastus", "westus", "centralus"] + }, + "then": { "effect": "deny" } + } + resource: + regions: ["eastus", "northeurope"] + want_undefined: true + + - note: allof_contains + policy_rule: | + { + "if": { + "field": "tags[*]", + "contains": "prod" + }, + "then": { "effect": "deny" } + } + resource: + tags: ["prod-web", "prod-api"] + want_effect: "deny" + + - note: allof_like + policy_rule: | + { + "if": { + "field": "names[*]", + "like": "vm-*" + }, + "then": { "effect": "deny" } + } + resource: + names: ["vm-001", "vm-002"] + want_effect: "deny" + + - note: allof_exists_true + policy_rule: | + { + "if": { + "field": "items[*].name", + "exists": true + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "name": "a" } + - { "name": "b" } + want_effect: "deny" + + - note: allof_exists_false_missing_field + policy_rule: | + { + "if": { + "field": "items[*].name", + "exists": true + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "name": "a" } + - { "other": "b" } + want_undefined: true + + # -- exists false with [*] ------------------------------------------------ + + - note: allof_exists_false_all_present + # All elements have the field → "exists false" is false for each → compliant + policy_rule: | + { + "if": { + "field": "items[*].name", + "exists": "false" + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "name": "a" } + - { "name": "b" } + want_undefined: true + + - note: allof_exists_false_all_missing + # No element has the field → "exists false" is true for each → non-compliant + policy_rule: | + { + "if": { + "field": "items[*].name", + "exists": "false" + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "other": "x" } + - { "other": "y" } + want_effect: "deny" + + - note: allof_exists_false_mixed + # Mixed — one has field, one doesn't → not all satisfy → compliant + policy_rule: | + { + "if": { + "field": "items[*].name", + "exists": "false" + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "name": "a" } + - { "other": "b" } + want_undefined: true + + - note: allof_exists_true_null_values + # Null values → "exists true" is false (null = not defined) → compliant + policy_rule: | + { + "if": { + "field": "items[*].name", + "exists": "true" + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "name": null } + - { "name": null } + want_undefined: true + + - note: allof_exists_false_null_values + # Null values → "exists false" is true (null = not defined) → non-compliant + policy_rule: | + { + "if": { + "field": "items[*].name", + "exists": "false" + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "name": null } + - { "name": null } + want_effect: "deny" + + - note: allof_exists_false_empty_array + # Empty array → vacuous truth → non-compliant + policy_rule: | + { + "if": { + "field": "items[*].name", + "exists": "false" + }, + "then": { "effect": "deny" } + } + resource: + items: [] + want_effect: "deny" + + # ========================================================================= + # Real-world pattern: not + [*] + notEquals + # "at least one element equals X" via double negation + # ========================================================================= + + - note: allof_not_notequals_pattern_match + policy_rule: | + { + "if": { + "not": { + "field": "ports[*]", + "notEquals": "*" + } + }, + "then": { "effect": "deny" } + } + resource: + ports: ["80", "*", "443"] + want_effect: "deny" + + - note: allof_not_notequals_pattern_no_match + policy_rule: | + { + "if": { + "not": { + "field": "ports[*]", + "notEquals": "*" + } + }, + "then": { "effect": "deny" } + } + resource: + ports: ["80", "443"] + want_undefined: true + + # ========================================================================= + # Doubly-nested [*] — allOf over nested arrays + # ========================================================================= + + - note: allof_doubly_nested_all_match + policy_rule: | + { + "if": { + "field": "groups[*].tags[*]", + "notEquals": "deprecated" + }, + "then": { "effect": "deny" } + } + resource: + groups: + - tags: ["prod", "web"] + - tags: ["staging", "api"] + want_effect: "deny" + + - note: allof_doubly_nested_one_fails + policy_rule: | + { + "if": { + "field": "groups[*].tags[*]", + "notEquals": "deprecated" + }, + "then": { "effect": "deny" } + } + resource: + groups: + - tags: ["prod", "web"] + - tags: ["deprecated", "api"] + want_undefined: true + + - note: allof_doubly_nested_with_suffix + policy_rule: | + { + "if": { + "field": "groups[*].members[*].role", + "equals": "admin" + }, + "then": { "effect": "deny" } + } + resource: + groups: + - members: + - { "role": "admin" } + - { "role": "admin" } + - members: + - { "role": "admin" } + want_effect: "deny" + + - note: allof_doubly_nested_with_suffix_fail + policy_rule: | + { + "if": { + "field": "groups[*].members[*].role", + "equals": "admin" + }, + "then": { "effect": "deny" } + } + resource: + groups: + - members: + - { "role": "admin" } + - members: + - { "role": "viewer" } + want_undefined: true + + # ========================================================================= + # Doubly-nested — inner array missing/empty + # ========================================================================= + + - note: allof_doubly_nested_inner_empty + policy_rule: | + { + "if": { + "field": "groups[*].tags[*]", + "equals": "ok" + }, + "then": { "effect": "deny" } + } + resource: + groups: + - tags: [] + - tags: [] + want_effect: "deny" + + - note: allof_doubly_nested_inner_missing + # Inner [*] on missing field → false (not iterable), outer Every fails + policy_rule: | + { + "if": { + "field": "groups[*].tags[*]", + "equals": "ok" + }, + "then": { "effect": "deny" } + } + resource: + groups: + - other: "no tags" + - other: "also no tags" + want_undefined: true + + # ========================================================================= + # Single element — trivially allOf + # ========================================================================= + + - note: allof_single_element_match + policy_rule: | + { + "if": { + "field": "items[*].v", + "equals": 42 + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "v": 42 } + want_effect: "deny" + + - note: allof_single_element_no_match + policy_rule: | + { + "if": { + "field": "items[*].v", + "equals": 42 + }, + "then": { "effect": "deny" } + } + resource: + items: + - { "v": 99 } + want_undefined: true diff --git a/tests/azure_policy/cases/logical_combinators.yaml b/tests/azure_policy/cases/logical_combinators.yaml new file mode 100644 index 0000000..fec2d3f --- /dev/null +++ b/tests/azure_policy/cases/logical_combinators.yaml @@ -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" diff --git a/tests/azure_policy/cases/modifiable_check.yaml b/tests/azure_policy/cases/modifiable_check.yaml new file mode 100644 index 0000000..6ee42cf --- /dev/null +++ b/tests/azure_policy/cases/modifiable_check.yaml @@ -0,0 +1,221 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Modifiable Check Test Suite +# When an alias catalog is loaded, the compiler validates that aliases used +# in Modify operations have `defaultMetadata.attributes = "Modifiable"`. +# +# Non-modifiable aliases should cause a compile-time error. +# Modifiable aliases, tag paths, and built-in fields should compile fine. + +aliases: test_aliases.json + +cases: + # ========================================================================= + # Modifiable alias in Modify — should compile and evaluate + # ========================================================================= + + - note: modify_modifiable_alias_ok + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "value": true + } + ] + } + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: false + want_effect: "modify" + want_details: + roleDefinitionIds: + - "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + operations: + - operation: "addOrReplace" + field: "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly" + value: true + + # ========================================================================= + # Non-modifiable alias in Modify — should fail compilation + # ========================================================================= + + - note: modify_non_modifiable_alias_error + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "Microsoft.Storage/storageAccounts/primaryEndpoints.blob", + "value": "https://example.blob.core.windows.net" + } + ] + } + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + want_compile_error: true + + # ========================================================================= + # Tag field in Modify — always allowed (no alias check needed) + # ========================================================================= + + - note: modify_tag_field_ok + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "tags['environment']", + "value": "production" + } + ] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "modify" + + # ========================================================================= + # Properties path in Modify — no alias match, allowed + # ========================================================================= + + - note: modify_properties_path_ok + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "properties.someCustomField", + "value": "custom-value" + } + ] + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "modify" + + # ========================================================================= + # Multiple operations — one non-modifiable should fail entire compilation + # ========================================================================= + + - note: modify_mixed_modifiable_non_modifiable_error + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "value": true + }, + { + "operation": "addOrReplace", + "field": "Microsoft.Storage/storageAccounts/primaryEndpoints.blob", + "value": "https://override.blob.core.windows.net" + } + ] + } + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + want_compile_error: true + + # ========================================================================= + # Modify with modifiable alias — details include resolved short name + # ========================================================================= + + - note: modify_modifiable_alias_details_check + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { + "effect": "modify", + "details": { + "roleDefinitionIds": [ + "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + ], + "operations": [ + { + "operation": "addOrReplace", + "field": "Microsoft.Storage/storageAccounts/minimumTlsVersion", + "value": "TLS1_2" + } + ] + } + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + minimumTlsVersion: "TLS1_0" + want_effect: "modify" + want_details: + roleDefinitionIds: + - "/providers/Microsoft.Authorization/roleDefinitions/b24988ac-6180-42a0-ab88-20f7382dd24c" + operations: + - operation: "addOrReplace" + field: "Microsoft.Storage/storageAccounts/minimumTlsVersion" + value: "TLS1_2" diff --git a/tests/azure_policy/cases/operators.yaml b/tests/azure_policy/cases/operators.yaml new file mode 100644 index 0000000..5029405 --- /dev/null +++ b/tests/azure_policy/cases/operators.yaml @@ -0,0 +1,453 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Operators Test Suite +# Tests all 20 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" diff --git a/tests/azure_policy/cases/parse_errors.yaml b/tests/azure_policy/cases/parse_errors.yaml new file mode 100644 index 0000000..10a1667 --- /dev/null +++ b/tests/azure_policy/cases/parse_errors.yaml @@ -0,0 +1,269 @@ +# 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 + policy_rule: | + { + "then": { "effect": "deny" } + } + want_parse_error: true + + - note: missing_then_key + policy_rule: | + { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + } + } + want_parse_error: true + + - note: missing_effect_in_then + 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 + + # ========================================================================= + # Edge cases: expressions that look malformed but are actually valid + # ========================================================================= + + - note: concat_zero_args_is_valid + policy_rule: | + { + "if": { + "value": "[concat()]", + "equals": "" + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" diff --git a/tests/azure_policy/cases/policy_definition.yaml b/tests/azure_policy/cases/policy_definition.yaml new file mode 100644 index 0000000..11f8b4c --- /dev/null +++ b/tests/azure_policy/cases/policy_definition.yaml @@ -0,0 +1,399 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Full Policy Definition Test Suite +# Tests parsing of the complete Azure Policy definition envelope, +# including parameters, displayName, description, mode, and metadata. + +cases: + # ========================================================================= + # Unwrapped form (properties-level) + # ========================================================================= + + - note: defn_unwrapped_basic + policy_definition: | + { + "displayName": "Deny Public IPs", + "description": "Prevents creation of public IP addresses", + "mode": "All", + "parameters": {}, + "policyRule": { + "if": { + "field": "type", + "equals": "Microsoft.Network/publicIPAddresses" + }, + "then": { + "effect": "deny" + } + } + } + resource: + type: "Microsoft.Network/publicIPAddresses" + want_effect: "deny" + + - note: defn_unwrapped_no_match + policy_definition: | + { + "displayName": "Deny Public IPs", + "mode": "All", + "policyRule": { + "if": { + "field": "type", + "equals": "Microsoft.Network/publicIPAddresses" + }, + "then": { + "effect": "deny" + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_undefined: true + + - note: defn_unwrapped_with_parameters + policy_definition: | + { + "displayName": "Require tag", + "description": "Requires a specified tag on resources", + "mode": "Indexed", + "parameters": { + "tagName": { + "type": "String", + "metadata": { + "displayName": "Tag Name", + "description": "Name of the tag to require" + } + }, + "tagValue": { + "type": "String", + "defaultValue": "production", + "allowedValues": ["production", "staging", "development"] + } + }, + "policyRule": { + "if": { + "field": "[concat('tags.', parameters('tagName'))]", + "notEquals": "[parameters('tagValue')]" + }, + "then": { + "effect": "deny" + } + } + } + parameters: + tagName: "environment" + tagValue: "production" + resource: + tags: + environment: "production" + want_undefined: true + + - note: defn_unwrapped_param_tag_mismatch + policy_definition: | + { + "displayName": "Require tag", + "parameters": { + "tagName": { + "type": "String" + }, + "tagValue": { + "type": "String" + } + }, + "policyRule": { + "if": { + "field": "[concat('tags.', parameters('tagName'))]", + "notEquals": "[parameters('tagValue')]" + }, + "then": { + "effect": "deny" + } + } + } + parameters: + tagName: "environment" + tagValue: "production" + resource: + tags: + environment: "staging" + want_effect: "deny" + + # ========================================================================= + # Wrapped form (with "properties" envelope) + # ========================================================================= + + - note: defn_wrapped_basic + policy_definition: | + { + "id": "/providers/Microsoft.Authorization/policyDefinitions/test-001", + "name": "test-001", + "type": "Microsoft.Authorization/policyDefinitions", + "properties": { + "displayName": "Deny Storage without HTTPS", + "description": "Storage accounts should only allow HTTPS traffic", + "policyType": "BuiltIn", + "mode": "All", + "parameters": {}, + "policyRule": { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + { + "field": "properties.supportsHttpsTrafficOnly", + "notEquals": true + } + ] + }, + "then": { + "effect": "deny" + } + } + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: false + want_effect: "deny" + + - note: defn_wrapped_compliant + policy_definition: | + { + "id": "/providers/Microsoft.Authorization/policyDefinitions/test-001", + "name": "test-001", + "type": "Microsoft.Authorization/policyDefinitions", + "properties": { + "displayName": "Deny Storage without HTTPS", + "mode": "All", + "parameters": {}, + "policyRule": { + "if": { + "allOf": [ + { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + { + "field": "properties.supportsHttpsTrafficOnly", + "notEquals": true + } + ] + }, + "then": { + "effect": "deny" + } + } + } + } + resource: + type: "Microsoft.Storage/storageAccounts" + properties: + supportsHttpsTrafficOnly: true + want_undefined: true + + # ========================================================================= + # Parameterized effect + # ========================================================================= + + - note: defn_parameterized_effect + policy_definition: | + { + "displayName": "Allowed locations", + "parameters": { + "allowedLocations": { + "type": "Array", + "metadata": { + "displayName": "Allowed locations", + "description": "The list of allowed locations for resources" + } + }, + "effect": { + "type": "String", + "defaultValue": "deny", + "allowedValues": ["audit", "deny", "disabled"] + } + }, + "policyRule": { + "if": { + "not": { + "field": "location", + "in": "[parameters('allowedLocations')]" + } + }, + "then": { + "effect": "[parameters('effect')]" + } + } + } + parameters: + allowedLocations: + - "eastus" + - "westus" + effect: "audit" + resource: + location: "northeurope" + want_effect: "audit" + + # ========================================================================= + # Extra / unknown fields preserved + # ========================================================================= + + - note: defn_extra_fields_ignored + policy_definition: | + { + "displayName": "Test Policy", + "policyType": "Custom", + "category": "General", + "version": "1.0.0", + "policyRule": { + "if": { + "field": "location", + "equals": "eastus" + }, + "then": { + "effect": "audit" + } + } + } + resource: + location: "eastus" + want_effect: "audit" + + # ========================================================================= + # Metadata + # ========================================================================= + + - note: defn_with_metadata + policy_definition: | + { + "displayName": "Audit VMs", + "metadata": { + "version": "2.0.0", + "category": "Compute", + "preview": true + }, + "parameters": {}, + "policyRule": { + "if": { + "field": "type", + "equals": "Microsoft.Compute/virtualMachines" + }, + "then": { + "effect": "audit" + } + } + } + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "audit" + + # ========================================================================= + # Missing policyRule should fail + # ========================================================================= + + - note: defn_missing_policy_rule + policy_definition: | + { + "displayName": "No Rule", + "description": "This definition is missing policyRule", + "parameters": {} + } + want_parse_error: true + + # ========================================================================= + # Complex: wrapped with all fields + # ========================================================================= + + - note: defn_wrapped_full + policy_definition: | + { + "id": "/subscriptions/xxx/providers/Microsoft.Authorization/policyDefinitions/require-tag", + "name": "require-tag", + "type": "Microsoft.Authorization/policyDefinitions", + "properties": { + "displayName": "Require a tag and its value on resources", + "description": "Enforces a required tag and its value.", + "policyType": "Custom", + "mode": "Indexed", + "metadata": { + "version": "1.0.0", + "category": "Tags" + }, + "parameters": { + "tagName": { + "type": "String", + "metadata": { + "displayName": "Tag Name", + "description": "Name of the tag, such as costCenter" + } + }, + "tagValue": { + "type": "String", + "metadata": { + "displayName": "Tag Value", + "description": "Value of the tag, such as IT" + } + } + }, + "policyRule": { + "if": { + "not": { + "field": "[concat('tags[', parameters('tagName'), ']')]", + "equals": "[parameters('tagValue')]" + } + }, + "then": { + "effect": "deny" + } + } + } + } + parameters: + tagName: "costCenter" + tagValue: "IT" + resource: + tags: + costCenter: "IT" + want_undefined: true + + - note: defn_wrapped_full_non_compliant + policy_definition: | + { + "id": "/subscriptions/xxx/providers/Microsoft.Authorization/policyDefinitions/require-tag", + "name": "require-tag", + "type": "Microsoft.Authorization/policyDefinitions", + "properties": { + "displayName": "Require a tag and its value on resources", + "policyType": "Custom", + "mode": "Indexed", + "parameters": { + "tagName": { + "type": "String" + }, + "tagValue": { + "type": "String" + } + }, + "policyRule": { + "if": { + "not": { + "field": "[concat('tags[', parameters('tagName'), ']')]", + "equals": "[parameters('tagValue')]" + } + }, + "then": { + "effect": "deny" + } + } + } + } + parameters: + tagName: "costCenter" + tagValue: "IT" + resource: + tags: + costCenter: "Finance" + want_effect: "deny" diff --git a/tests/azure_policy/cases/template_functions.yaml b/tests/azure_policy/cases/template_functions.yaml new file mode 100644 index 0000000..9a1c5ff --- /dev/null +++ b/tests/azure_policy/cases/template_functions.yaml @@ -0,0 +1,548 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# ARM Template Functions Test Suite +# Tests: split, empty, first, last, createArray, startsWith, endsWith, +# int, string, bool. + +cases: + # ========================================================================= + # split(inputString, delimiter) + # ========================================================================= + + - note: fn_split_basic + policy_rule: | + { + "if": { + "value": "[first(split('a-b-c', '-'))]", + "equals": "a" + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_split_last_element + policy_rule: | + { + "if": { + "value": "[last(split('foo/bar/baz', '/'))]", + "equals": "baz" + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_split_length + policy_rule: | + { + "if": { + "value": "[length(split('a,b,c,d', ','))]", + "equals": 4 + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_split_no_match_delimiter + policy_rule: | + { + "if": { + "value": "[length(split('hello', ','))]", + "equals": 1 + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + # ========================================================================= + # empty(item) + # ========================================================================= + + - note: fn_empty_string_true + policy_rule: | + { + "if": { + "value": "[empty('')]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_empty_string_false + policy_rule: | + { + "if": { + "value": "[empty('hello')]", + "equals": false + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_empty_with_parameter_array + policy_rule: | + { + "if": { + "value": "[empty(parameters('items'))]", + "equals": true + }, + "then": { "effect": "deny" } + } + parameters: + items: [] + resource: + type: "any" + want_effect: "deny" + + - note: fn_empty_nonempty_array + policy_rule: | + { + "if": { + "value": "[empty(parameters('items'))]", + "equals": false + }, + "then": { "effect": "deny" } + } + parameters: + items: + - "a" + resource: + type: "any" + want_effect: "deny" + + # ========================================================================= + # first(arg) / last(arg) + # ========================================================================= + + - note: fn_first_string + policy_rule: | + { + "if": { + "value": "[first('hello')]", + "equals": "h" + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_last_string + policy_rule: | + { + "if": { + "value": "[last('hello')]", + "equals": "o" + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_first_array + policy_rule: | + { + "if": { + "value": "[first(parameters('items'))]", + "equals": "alpha" + }, + "then": { "effect": "deny" } + } + parameters: + items: + - "alpha" + - "beta" + - "gamma" + resource: + type: "any" + want_effect: "deny" + + - note: fn_last_array + policy_rule: | + { + "if": { + "value": "[last(parameters('items'))]", + "equals": "gamma" + }, + "then": { "effect": "deny" } + } + parameters: + items: + - "alpha" + - "beta" + - "gamma" + resource: + type: "any" + want_effect: "deny" + + # ========================================================================= + # createArray(items...) + # ========================================================================= + + - note: fn_createArray_basic + policy_rule: | + { + "if": { + "value": "[length(createArray('a', 'b', 'c'))]", + "equals": 3 + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_createArray_first + policy_rule: | + { + "if": { + "value": "[first(createArray('x', 'y'))]", + "equals": "x" + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + # ========================================================================= + # startsWith / endsWith + # ========================================================================= + + - note: fn_startsWith_true + policy_rule: | + { + "if": { + "value": "[startsWith('abcdef', 'abc')]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_startsWith_false + policy_rule: | + { + "if": { + "value": "[startsWith('abcdef', 'xyz')]", + "equals": false + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_startsWith_case_insensitive + policy_rule: | + { + "if": { + "value": "[startsWith('AbCdEf', 'abc')]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_endsWith_true + policy_rule: | + { + "if": { + "value": "[endsWith('abcdef', 'def')]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_endsWith_false + policy_rule: | + { + "if": { + "value": "[endsWith('abcdef', 'xyz')]", + "equals": false + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_endsWith_case_insensitive + policy_rule: | + { + "if": { + "value": "[endsWith('AbCdEf', 'DEF')]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_startsWith_field_value + policy_rule: | + { + "if": { + "value": "[startsWith(field('name'), 'test-')]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + name: "test-resource" + want_effect: "deny" + + # ========================================================================= + # int(value) + # ========================================================================= + + - note: fn_int_from_string + policy_rule: | + { + "if": { + "value": "[int('42')]", + "equals": 42 + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_int_from_float_string + policy_rule: | + { + "if": { + "value": "[int('3.7')]", + "equals": 3 + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_int_identity + policy_rule: | + { + "if": { + "value": "[int(parameters('num'))]", + "equals": 10 + }, + "then": { "effect": "deny" } + } + parameters: + num: 10 + resource: + type: "any" + want_effect: "deny" + + # ========================================================================= + # string(value) + # ========================================================================= + + - note: fn_string_from_int + policy_rule: | + { + "if": { + "value": "[string(42)]", + "equals": "42" + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_string_from_bool + policy_rule: | + { + "if": { + "value": "[string(true)]", + "equals": "true" + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_string_identity + policy_rule: | + { + "if": { + "value": "[string('hello')]", + "equals": "hello" + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + # ========================================================================= + # bool(value) + # ========================================================================= + + - note: fn_bool_from_true_string + policy_rule: | + { + "if": { + "value": "[bool('true')]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_bool_from_false_string + policy_rule: | + { + "if": { + "value": "[bool('false')]", + "equals": false + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_bool_from_1_string + policy_rule: | + { + "if": { + "value": "[bool('1')]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_bool_from_number + policy_rule: | + { + "if": { + "value": "[bool(1)]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_bool_zero_is_false + policy_rule: | + { + "if": { + "value": "[bool(0)]", + "equals": false + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + # ========================================================================= + # Composed / nested ARM functions + # ========================================================================= + + - note: fn_composed_split_startsWith + policy_rule: | + { + "if": { + "value": "[startsWith(first(split(field('name'), '-')), 'prod')]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + name: "production-vm-001" + want_effect: "deny" + + - note: fn_composed_int_add + policy_rule: | + { + "if": { + "value": "[add(int('10'), int('20'))]", + "equals": 30 + }, + "then": { "effect": "deny" } + } + resource: + type: "any" + want_effect: "deny" + + - note: fn_composed_if_empty + policy_rule: | + { + "if": { + "value": "[if(empty(parameters('label')), 'default', parameters('label'))]", + "equals": "default" + }, + "then": { "effect": "deny" } + } + parameters: + label: "" + resource: + type: "any" + want_effect: "deny" + + - note: fn_composed_concat_with_string + policy_rule: | + { + "if": { + "value": "[concat('count=', string(length(parameters('items'))))]", + "equals": "count=3" + }, + "then": { "effect": "deny" } + } + parameters: + items: + - "a" + - "b" + - "c" + resource: + type: "any" + want_effect: "deny" + + - note: fn_composed_endsWith_toLower + policy_rule: | + { + "if": { + "value": "[endsWith(toLower(field('name')), '.json')]", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + name: "Config.JSON" + want_effect: "deny" diff --git a/tests/azure_policy/cases/template_functions_datetime_ip.yaml b/tests/azure_policy/cases/template_functions_datetime_ip.yaml new file mode 100644 index 0000000..31d5ff6 --- /dev/null +++ b/tests/azure_policy/cases/template_functions_datetime_ip.yaml @@ -0,0 +1,364 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Date/Time ARM template functions + ipRangeContains tests +# +# Tests: utcNow, dateTimeAdd, dateTimeFromEpoch, dateTimeToEpoch, addDays, +# ipRangeContains + +cases: + # ── utcNow ─────────────────────────────────────────────────────────── + + - note: utcNow_returns_context_timestamp + policy_rule: | + { + "if": { + "value": "[utcNow()]", + "equals": "2025-02-20T12:00:00Z" + }, + "then": { "effect": "audit" } + } + context: + utcNow: "2025-02-20T12:00:00Z" + resourceGroup: + name: myRG + location: eastus + subscription: + subscriptionId: "00000000-0000-0000-0000-000000000000" + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "audit" + + - note: utcNow_in_comparison + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { "value": "[utcNow()]", "greater": "2024-01-01T00:00:00Z" } + ] + }, + "then": { "effect": "audit" } + } + context: + utcNow: "2025-02-20T12:00:00Z" + resourceGroup: + name: myRG + location: eastus + subscription: + subscriptionId: "00000000-0000-0000-0000-000000000000" + resource: + type: "Microsoft.Compute/virtualMachines" + want_effect: "audit" + + # ── dateTimeAdd ────────────────────────────────────────────────────── + + - note: dateTimeAdd_P1D + policy_rule: | + { + "if": { + "value": "[dateTimeAdd('2024-01-15T12:00:00Z', 'P1D')]", + "equals": "2024-01-16T12:00:00Z" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: dateTimeAdd_PT2H + policy_rule: | + { + "if": { + "value": "[dateTimeAdd('2024-01-15T10:00:00Z', 'PT2H')]", + "equals": "2024-01-15T12:00:00Z" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: dateTimeAdd_negative_duration + policy_rule: | + { + "if": { + "value": "[dateTimeAdd('2024-01-15T12:00:00Z', '-P1D')]", + "equals": "2024-01-14T12:00:00Z" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: dateTimeAdd_complex_P1DT2H30M + policy_rule: | + { + "if": { + "value": "[dateTimeAdd('2024-01-15T10:00:00Z', 'P1DT2H30M')]", + "equals": "2024-01-16T12:30:00Z" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + # ── dateTimeFromEpoch ──────────────────────────────────────────────── + + - note: dateTimeFromEpoch_basic + policy_rule: | + { + "if": { + "value": "[dateTimeFromEpoch(1705312800)]", + "equals": "2024-01-15T10:00:00Z" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: dateTimeFromEpoch_zero + policy_rule: | + { + "if": { + "value": "[dateTimeFromEpoch(0)]", + "equals": "1970-01-01T00:00:00Z" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + # ── dateTimeToEpoch ────────────────────────────────────────────────── + + - note: dateTimeToEpoch_basic + policy_rule: | + { + "if": { + "value": "[dateTimeToEpoch('2024-01-15T10:00:00Z')]", + "equals": 1705312800 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: dateTimeToEpoch_zero + policy_rule: | + { + "if": { + "value": "[dateTimeToEpoch('1970-01-01T00:00:00Z')]", + "equals": 0 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: dateTimeToEpoch_roundtrip + policy_rule: | + { + "if": { + "value": "[dateTimeToEpoch(dateTimeFromEpoch(1705312800))]", + "equals": 1705312800 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + # ── addDays ────────────────────────────────────────────────────────── + + - note: addDays_positive + policy_rule: | + { + "if": { + "value": "[addDays('2024-01-15T12:00:00Z', 5)]", + "equals": "2024-01-20T12:00:00Z" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: addDays_negative + policy_rule: | + { + "if": { + "value": "[addDays('2024-01-15T12:00:00Z', -3)]", + "equals": "2024-01-12T12:00:00Z" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: addDays_cross_month + policy_rule: | + { + "if": { + "value": "[addDays('2024-01-30T00:00:00Z', 5)]", + "equals": "2024-02-04T00:00:00Z" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: addDays_with_utcNow + policy_rule: | + { + "if": { + "value": "[addDays(utcNow(), -1)]", + "equals": "2025-02-19T12:00:00Z" + }, + "then": { "effect": "audit" } + } + context: + utcNow: "2025-02-20T12:00:00Z" + resourceGroup: + name: myRG + location: eastus + subscription: + subscriptionId: "00000000-0000-0000-0000-000000000000" + resource: + type: "any" + want_effect: "audit" + + # ── ipRangeContains ────────────────────────────────────────────────── + + - note: ipRangeContains_ip_in_range + policy_rule: | + { + "if": { + "value": "[ipRangeContains('10.0.0.0/24', '10.0.0.5')]", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.KeyVault/vaults" + want_effect: "audit" + + - note: ipRangeContains_ip_not_in_range + policy_rule: | + { + "if": { + "value": "[ipRangeContains('10.0.0.0/24', '10.0.1.5')]", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.KeyVault/vaults" + want_undefined: true + + - note: ipRangeContains_subnet_contained + policy_rule: | + { + "if": { + "value": "[ipRangeContains('10.0.0.0/16', '10.0.1.0/24')]", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.KeyVault/vaults" + want_effect: "audit" + + - note: ipRangeContains_subnet_not_contained + policy_rule: | + { + "if": { + "value": "[ipRangeContains('10.0.0.0/24', '10.0.0.0/16')]", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.KeyVault/vaults" + want_undefined: true + + - note: ipRangeContains_exact_match_32 + policy_rule: | + { + "if": { + "value": "[ipRangeContains('10.0.0.5/32', '10.0.0.5')]", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.KeyVault/vaults" + want_effect: "audit" + + - note: ipRangeContains_ipv6_in_range + policy_rule: | + { + "if": { + "value": "[ipRangeContains('2001:db8::/32', '2001:db8::1')]", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.KeyVault/vaults" + want_effect: "audit" + + - note: ipRangeContains_ipv6_not_in_range + policy_rule: | + { + "if": { + "value": "[ipRangeContains('2001:db8::/32', '2001:db9::1')]", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + type: "Microsoft.KeyVault/vaults" + want_undefined: true + + # ── Composition tests ─────────────────────────────────────────────── + + - note: addDays_with_mul_negative_days + policy_rule: | + { + "if": { + "value": "[addDays(utcNow(), mul(int('30'), -1))]", + "less": "2025-02-20T12:00:00Z" + }, + "then": { "effect": "audit" } + } + context: + utcNow: "2025-02-20T12:00:00Z" + resourceGroup: + name: myRG + location: eastus + subscription: + subscriptionId: "00000000-0000-0000-0000-000000000000" + resource: + type: "Microsoft.DocumentDB/databaseAccounts" + want_effect: "audit" + + - note: dateTimeToEpoch_with_dateTimeAdd + policy_rule: | + { + "if": { + "value": "[dateTimeToEpoch(dateTimeAdd('2024-01-15T00:00:00Z', 'P7D'))]", + "greater": "[dateTimeToEpoch('2024-01-20T00:00:00Z')]" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" diff --git a/tests/azure_policy/cases/template_functions_extra.yaml b/tests/azure_policy/cases/template_functions_extra.yaml new file mode 100644 index 0000000..d320bc7 --- /dev/null +++ b/tests/azure_policy/cases/template_functions_extra.yaml @@ -0,0 +1,561 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# ARM Template Functions — Additional String, Collection, and Numeric Functions +# Tests indexOf, lastIndexOf, trim, format, base64, base64ToString, base64ToJson, +# uri, uriComponent, uriComponentToString, dataUri, dataUriToString, +# intersection, union, take, skip, range, array, coalesce, createObject, +# sub, mul, div, mod, min, max, float. + +cases: + # ========================================================================= + # String functions + # ========================================================================= + + - note: fn_indexOf_found + policy_rule: | + { + "if": { + "value": "[indexOf('hello world', 'world')]", + "equals": 6 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_indexOf_not_found + policy_rule: | + { + "if": { + "value": "[indexOf('hello world', 'xyz')]", + "equals": -1 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_lastIndexOf_found + policy_rule: | + { + "if": { + "value": "[lastIndexOf('hello world hello', 'hello')]", + "equals": 12 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_lastIndexOf_not_found + policy_rule: | + { + "if": { + "value": "[lastIndexOf('hello', 'xyz')]", + "equals": -1 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_trim + policy_rule: | + { + "if": { + "value": "[trim(' hello ')]", + "equals": "hello" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_format_simple + policy_rule: | + { + "if": { + "value": "[format('Hello {0}, you are {1}', 'World', 'great')]", + "equals": "Hello World, you are great" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_format_numbers + policy_rule: | + { + "if": { + "value": "[format('{0} + {1} = {2}', 1, 2, 3)]", + "equals": "1 + 2 = 3" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_base64_encode + policy_rule: | + { + "if": { + "value": "[base64('hello')]", + "equals": "aGVsbG8=" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_base64ToString + policy_rule: | + { + "if": { + "value": "[base64ToString('aGVsbG8=')]", + "equals": "hello" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_base64ToJson + policy_rule: | + { + "if": { + "value": "[base64ToJson('eyJrZXkiOiJ2YWx1ZSJ9').key]", + "equals": "value" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_uriComponent_encode + policy_rule: | + { + "if": { + "value": "[uriComponent('hello world')]", + "equals": "hello%20world" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_uriComponentToString + policy_rule: | + { + "if": { + "value": "[uriComponentToString('hello%20world')]", + "equals": "hello world" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_uri_combine + policy_rule: | + { + "if": { + "value": "[uri('https://example.com/path/', 'api/v1')]", + "equals": "https://example.com/path/api/v1" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_dataUri + policy_rule: | + { + "if": { + "value": "[dataUri('Hello')]", + "contains": "base64," + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_dataUriToString + policy_rule: | + { + "if": { + "value": "[dataUriToString(dataUri('Hello'))]", + "equals": "Hello" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + # ========================================================================= + # Collection functions + # ========================================================================= + + - note: fn_intersection_arrays + policy_rule: | + { + "if": { + "value": "[length(intersection(createArray('a','b','c'), createArray('b','c','d')))]", + "equals": 2 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_union_arrays + policy_rule: | + { + "if": { + "value": "[length(union(createArray('a','b'), createArray('b','c')))]", + "equals": 3 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_take_array + policy_rule: | + { + "if": { + "value": "[length(take(createArray('a','b','c','d'), 2))]", + "equals": 2 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_take_string + policy_rule: | + { + "if": { + "value": "[take('Hello World', 5)]", + "equals": "Hello" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_skip_array + policy_rule: | + { + "if": { + "value": "[length(skip(createArray('a','b','c','d'), 2))]", + "equals": 2 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_skip_string + policy_rule: | + { + "if": { + "value": "[skip('Hello World', 6)]", + "equals": "World" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_range + policy_rule: | + { + "if": { + "value": "[length(range(0, 5))]", + "equals": 5 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_array_wrap + policy_rule: | + { + "if": { + "value": "[length(array('hello'))]", + "equals": 1 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_coalesce_first_non_null + policy_rule: | + { + "if": { + "value": "[coalesce('first', 'second')]", + "equals": "first" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_createObject + policy_rule: | + { + "if": { + "value": "[string(createObject('key', 'value'))]", + "contains": "key" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + # ========================================================================= + # Numeric functions + # ========================================================================= + + - note: fn_sub + policy_rule: | + { + "if": { + "value": "[sub(10, 3)]", + "equals": 7 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_mul + policy_rule: | + { + "if": { + "value": "[mul(3, 4)]", + "equals": 12 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_div + policy_rule: | + { + "if": { + "value": "[div(10, 3)]", + "equals": 3 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_mod + policy_rule: | + { + "if": { + "value": "[mod(10, 3)]", + "equals": 1 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_min_args + policy_rule: | + { + "if": { + "value": "[min(5, 3, 8, 1)]", + "equals": 1 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_max_args + policy_rule: | + { + "if": { + "value": "[max(5, 3, 8, 1)]", + "equals": 8 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_min_array + policy_rule: | + { + "if": { + "value": "[min(createArray(5, 3, 8, 1))]", + "equals": 1 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_max_array + policy_rule: | + { + "if": { + "value": "[max(createArray(5, 3, 8, 1))]", + "equals": 8 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_float_from_int + policy_rule: | + { + "if": { + "value": "[float(42)]", + "equals": 42 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_float_from_string + policy_rule: | + { + "if": { + "value": "[float('3.14')]", + "equals": 3.14 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + # ========================================================================= + # Composition tests — functions calling functions + # ========================================================================= + + - note: fn_composition_trim_tolower + policy_rule: | + { + "if": { + "value": "[toLower(trim(' Hello '))]", + "equals": "hello" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_composition_indexOf_with_param + policy_rule: | + { + "if": { + "value": "[indexOf(parameters('input'), parameters('search'))]", + "greaterOrEquals": 0 + }, + "then": { "effect": "deny" } + } + parameters: + input: "Microsoft.Compute/virtualMachines" + search: "Compute" + resource: + type: "any" + want_effect: "deny" + + - note: fn_composition_base64_roundtrip + policy_rule: | + { + "if": { + "value": "[base64ToString(base64('round trip'))]", + "equals": "round trip" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_composition_uri_roundtrip + policy_rule: | + { + "if": { + "value": "[uriComponentToString(uriComponent('hello world&foo=bar'))]", + "equals": "hello world&foo=bar" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_composition_padleft_format + policy_rule: | + { + "if": { + "value": "[format('Days: {0}', padLeft('5', 3, '0'))]", + "equals": "Days: 005" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: fn_composition_arithmetic + policy_rule: | + { + "if": { + "value": "[add(mul(3, 4), sub(10, 5))]", + "equals": 17 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" diff --git a/tests/azure_policy/cases/type_coercion.yaml b/tests/azure_policy/cases/type_coercion.yaml new file mode 100644 index 0000000..b55e137 --- /dev/null +++ b/tests/azure_policy/cases/type_coercion.yaml @@ -0,0 +1,358 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Type Coercion Test Suite +# Tests string↔number coercion, null/undefined handling, and other +# implicit type behaviors covered by this test suite. + +cases: + # ========================================================================= + # String ↔ Number coercion + # ========================================================================= + + - note: coercion_string_number_equals + policy_rule: | + { + "if": { + "field": "properties.destinationPortRange", + "equals": 80 + }, + "then": { "effect": "deny" } + } + resource: + properties: + destinationPortRange: "80" + want_effect: "deny" + + - note: coercion_number_string_equals + policy_rule: | + { + "if": { + "field": "properties.port", + "equals": "443" + }, + "then": { "effect": "deny" } + } + resource: + properties: + port: 443 + want_effect: "deny" + + - note: coercion_string_number_greater + policy_rule: | + { + "if": { + "field": "properties.destinationPortRange", + "greater": 1024 + }, + "then": { "effect": "deny" } + } + resource: + properties: + destinationPortRange: "8080" + want_effect: "deny" + + - note: coercion_string_number_less + policy_rule: | + { + "if": { + "field": "properties.port", + "less": 100 + }, + "then": { "effect": "deny" } + } + resource: + properties: + port: "80" + want_effect: "deny" + + - note: coercion_in_with_mixed_types + policy_rule: | + { + "if": { + "field": "properties.destinationPortRange", + "in": [80, 443, 8080] + }, + "then": { "effect": "deny" } + } + resource: + properties: + destinationPortRange: "443" + want_effect: "deny" + + - note: coercion_non_numeric_string + policy_rule: | + { + "if": { + "field": "properties.port", + "equals": 80 + }, + "then": { "effect": "deny" } + } + resource: + properties: + port: "not-a-number" + want_undefined: true + + # ========================================================================= + # Null and undefined + # ========================================================================= + + - note: null_field_exists_false + policy_rule: | + { + "if": { + "field": "properties.optionalField", + "exists": false + }, + "then": { "effect": "audit" } + } + resource: + properties: {} + want_effect: "audit" + + - note: null_field_equals_null + policy_rule: | + { + "if": { + "field": "properties.nullableField", + "equals": null + }, + "then": { "effect": "audit" } + } + resource: + properties: + nullableField: null + want_effect: "audit" + + - note: empty_string_not_undefined + policy_rule: | + { + "if": { + "field": "properties.emptyField", + "exists": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + emptyField: "" + want_effect: "audit" + + # ========================================================================= + # Boolean semantics + # ========================================================================= + + - note: bool_string_coercion_matches + policy_rule: | + { + "if": { + "field": "properties.enabled", + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + properties: + enabled: "true" + want_effect: "audit" + + - note: bool_equals_direct + policy_rule: | + { + "if": { + "field": "properties.httpsOnly", + "equals": false + }, + "then": { "effect": "deny" } + } + resource: + properties: + httpsOnly: false + want_effect: "deny" + + # ========================================================================= + # Array values in conditions + # ========================================================================= + + - note: array_rhs_in_operator + policy_rule: | + { + "if": { + "field": "location", + "in": ["eastus", "westus", "centralus"] + }, + "then": { "effect": "deny" } + } + resource: + location: "eastus" + want_effect: "deny" + + - note: array_rhs_from_parameter + policy_rule: | + { + "if": { + "field": "location", + "in": "[parameters('allowedLocations')]" + }, + "then": { "effect": "deny" } + } + parameters: + allowedLocations: + - "eastus" + - "westus" + resource: + location: "northeurope" + want_undefined: true + + # ========================================================================= + # Negative numbers in conditions + # ========================================================================= + + - note: negative_number_rhs + policy_rule: | + { + "if": { + "field": "properties.offset", + "equals": -10 + }, + "then": { "effect": "audit" } + } + resource: + properties: + offset: -10 + want_effect: "audit" + + - note: fractional_number_rhs + policy_rule: | + { + "if": { + "field": "properties.threshold", + "greater": 0.5 + }, + "then": { "effect": "audit" } + } + resource: + properties: + threshold: 0.75 + want_effect: "audit" + + # ========================================================================= + # String ↔ Bool coercion + # ========================================================================= + + - note: coercion_string_true_equals_bool_true + policy_rule: | + { + "if": { + "field": "properties.enabled", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + properties: + enabled: "true" + want_effect: "deny" + + - note: coercion_string_false_equals_bool_false + policy_rule: | + { + "if": { + "field": "properties.enabled", + "equals": false + }, + "then": { "effect": "deny" } + } + resource: + properties: + enabled: "false" + want_effect: "deny" + + - note: coercion_bool_true_equals_string_true + policy_rule: | + { + "if": { + "field": "properties.enabled", + "equals": "True" + }, + "then": { "effect": "deny" } + } + resource: + properties: + enabled: true + want_effect: "deny" + + - note: coercion_bool_false_notEquals_string_true + policy_rule: | + { + "if": { + "field": "properties.enabled", + "notEquals": "true" + }, + "then": { "effect": "deny" } + } + resource: + properties: + enabled: false + want_effect: "deny" + + - note: coercion_string_TRUE_case_insensitive + policy_rule: | + { + "if": { + "field": "properties.flag", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + properties: + flag: "TRUE" + want_effect: "deny" + + - note: coercion_string_random_not_bool + policy_rule: | + { + "if": { + "field": "properties.flag", + "equals": true + }, + "then": { "effect": "deny" } + } + resource: + properties: + flag: "yes" + want_undefined: true + + # ========================================================================= + # Unicode case-insensitivity + # ========================================================================= + + - note: unicode_case_equals + policy_rule: | + { + "if": { + "field": "properties.city", + "equals": "MÜNCHEN" + }, + "then": { "effect": "deny" } + } + resource: + properties: + city: "münchen" + want_effect: "deny" + + - note: unicode_case_contains + policy_rule: | + { + "if": { + "field": "properties.name", + "contains": "CAFÉ" + }, + "then": { "effect": "deny" } + } + resource: + properties: + name: "my-café-app" + want_effect: "deny" diff --git a/tests/azure_policy/cases/value_conditions.yaml b/tests/azure_policy/cases/value_conditions.yaml new file mode 100644 index 0000000..5007915 --- /dev/null +++ b/tests/azure_policy/cases/value_conditions.yaml @@ -0,0 +1,264 @@ +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. + +# Value Conditions Test Suite +# Tests the "value" LHS in conditions (as opposed to "field" or "count"). + +cases: + # ========================================================================= + # Value with literal values + # ========================================================================= + + - note: value_string_literal + policy_rule: | + { + "if": { + "value": "hello", + "equals": "hello" + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: value_number_literal + policy_rule: | + { + "if": { + "value": 42, + "equals": 42 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: value_boolean_literal + policy_rule: | + { + "if": { + "value": true, + "equals": true + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: value_null_literal + policy_rule: | + { + "if": { + "value": null, + "equals": null + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + # ========================================================================= + # Value with expressions + # ========================================================================= + + - note: value_parameters_expression + policy_rule: | + { + "if": { + "value": "[parameters('environment')]", + "equals": "production" + }, + "then": { "effect": "deny" } + } + parameters: + environment: "production" + resource: + type: "any" + want_effect: "deny" + + - note: value_concat_expression + policy_rule: | + { + "if": { + "value": "[concat(parameters('prefix'), '-resource')]", + "contains": "prod" + }, + "then": { "effect": "audit" } + } + parameters: + prefix: "prod" + resource: + type: "any" + want_effect: "audit" + + - note: value_field_expression + policy_rule: | + { + "if": { + "value": "[field('name')]", + "contains": "prod" + }, + "then": { "effect": "audit" } + } + resource: + name: "my-prod-vm" + want_effect: "audit" + + - note: value_length_expression + policy_rule: | + { + "if": { + "value": "[length(field('name'))]", + "greater": 5 + }, + "then": { "effect": "audit" } + } + resource: + name: "my-long-resource-name" + want_effect: "audit" + + # ========================================================================= + # Value with array RHS + # ========================================================================= + + - note: value_in_array + policy_rule: | + { + "if": { + "value": "[parameters('location')]", + "in": ["eastus", "westus", "centralus"] + }, + "then": { "effect": "deny" } + } + parameters: + location: "eastus" + resource: + type: "any" + want_effect: "deny" + + - note: value_not_in_array + policy_rule: | + { + "if": { + "value": "[parameters('location')]", + "notIn": ["eastus", "westus"] + }, + "then": { "effect": "deny" } + } + parameters: + location: "northeurope" + resource: + type: "any" + want_effect: "deny" + + # ========================================================================= + # Value in allOf / anyOf + # ========================================================================= + + - note: value_in_allOf + policy_rule: | + { + "if": { + "allOf": [ + { "value": "[parameters('env')]", "equals": "production" }, + { "value": "[parameters('region')]", "in": ["eastus", "westus"] } + ] + }, + "then": { "effect": "deny" } + } + parameters: + env: "production" + region: "eastus" + resource: + type: "any" + want_effect: "deny" + + - note: value_in_anyOf + policy_rule: | + { + "if": { + "anyOf": [ + { "value": "[parameters('tier')]", "equals": "free" }, + { "value": "[parameters('tier')]", "equals": "basic" } + ] + }, + "then": { "effect": "deny" } + } + parameters: + tier: "free" + resource: + type: "any" + want_effect: "deny" + + # ========================================================================= + # Value mixed with field conditions + # ========================================================================= + + - note: value_mixed_with_field + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Compute/virtualMachines" }, + { "value": "[parameters('enforcePolicy')]", "equals": true }, + { "field": "location", "notIn": "[parameters('allowedLocations')]" } + ] + }, + "then": { "effect": "deny" } + } + parameters: + enforcePolicy: true + allowedLocations: + - "eastus" + - "westus" + resource: + type: "Microsoft.Compute/virtualMachines" + location: "northeurope" + want_effect: "deny" + + # ========================================================================= + # Value with negative numbers + # ========================================================================= + + - note: value_negative_number + policy_rule: | + { + "if": { + "value": -1, + "less": 0 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: value_zero + policy_rule: | + { + "if": { + "value": 0, + "equals": 0 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" + + - note: value_float + policy_rule: | + { + "if": { + "value": 3.14, + "greater": 3 + }, + "then": { "effect": "audit" } + } + resource: + type: "any" + want_effect: "audit" diff --git a/tests/azure_policy/cases/versioned_normalization.yaml b/tests/azure_policy/cases/versioned_normalization.yaml new file mode 100644 index 0000000..bb6577d --- /dev/null +++ b/tests/azure_policy/cases/versioned_normalization.yaml @@ -0,0 +1,2129 @@ +# Versioned alias normalization tests. +# +# Each test verifies that the normalizer picks the correct ARM path based on +# the api_version. Resources contain DIFFERENT values at each possible path +# so that if the wrong path is selected, the "equals" check fails and the +# test fails with an unexpected undefined / wrong effect. +# +# Aliases file: versioned_aliases.json (Microsoft.Test/versionedResources) +# +# Alias | Old API (2015) | New API (2020) | Default (no match) +# ----------------------|----------------------------------------|----------------------------------|------------------- +# accountType | properties.accountType | sku.name | properties.accountType +# config.threshold | properties.config.properties.threshold | properties.config.threshold | properties.config.threshold +# tier | properties.pricingTier | sku.tier | sku.tier +# operationMode | properties.legacyMode | properties.mode | properties.mode +# | (2015-01-01 only) | (2020-01-01 only) | +# | properties.settings.mode (2017-06-01) | | + +aliases: versioned_aliases.json + +cases: + # ========================================================================= + # 1. simpleProp — no versioning, baseline sanity check + # ========================================================================= + - note: simple_prop_baseline + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/simpleProp", + "equals": "hello" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + simpleProp: "hello" + want_effect: "Deny" + + # ========================================================================= + # 2. accountType — properties.accountType (old) vs sku.name (new) + # Resource has DIFFERENT values at each path. + # ========================================================================= + + # Old API → should read properties.accountType = "Premium_LRS" + - note: accountType_old_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/accountType", + "equals": "Premium_LRS" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + name: "Standard_LRS" + properties: + accountType: "Premium_LRS" + api_version: "2015-06-15" + want_effect: "Deny" + + # Old API — verify wrong value is NOT matched + - note: accountType_old_api_wrong_value + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/accountType", + "equals": "Standard_LRS" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + name: "Standard_LRS" + properties: + accountType: "Premium_LRS" + api_version: "2015-06-15" + want_undefined: true + + # New API → should read sku.name = "Standard_LRS" + - note: accountType_new_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/accountType", + "equals": "Standard_LRS" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + name: "Standard_LRS" + properties: + accountType: "Premium_LRS" + api_version: "2020-01-01" + want_effect: "Deny" + + # New API — verify wrong value is NOT matched + - note: accountType_new_api_wrong_value + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/accountType", + "equals": "Premium_LRS" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + name: "Standard_LRS" + properties: + accountType: "Premium_LRS" + api_version: "2020-01-01" + want_undefined: true + + # No API version → default path = properties.accountType + - note: accountType_no_api_version + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/accountType", + "equals": "Premium_LRS" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + name: "Standard_LRS" + properties: + accountType: "Premium_LRS" + want_effect: "Deny" + + # Unknown API version → falls back to default = properties.accountType + - note: accountType_unknown_api_version + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/accountType", + "equals": "Premium_LRS" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + name: "Standard_LRS" + properties: + accountType: "Premium_LRS" + api_version: "2099-01-01" + want_effect: "Deny" + + # ========================================================================= + # 3. accountType with fully-qualified alias name in policy + # Tests both compiler FQ→short resolution AND normalizer versioned path. + # ========================================================================= + + - note: fq_accountType_old_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/accountType", + "equals": "Premium_LRS" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + name: "Standard_LRS" + properties: + accountType: "Premium_LRS" + api_version: "2015-06-15" + want_effect: "Deny" + + - note: fq_accountType_new_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/accountType", + "equals": "Standard_LRS" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + name: "Standard_LRS" + properties: + accountType: "Premium_LRS" + api_version: "2020-01-01" + want_effect: "Deny" + + # ========================================================================= + # 4. config.threshold — extra properties nesting in old API + # Old: properties.config.properties.threshold = 200 + # New: properties.config.threshold = 100 + # ========================================================================= + + - note: config_threshold_old_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/config.threshold", + "equals": 200 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + config: + threshold: 100 + properties: + threshold: 200 + api_version: "2015-06-15" + want_effect: "Deny" + + - note: config_threshold_old_api_wrong + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/config.threshold", + "equals": 100 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + config: + threshold: 100 + properties: + threshold: 200 + api_version: "2015-06-15" + want_undefined: true + + - note: config_threshold_new_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/config.threshold", + "equals": 100 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + config: + threshold: 100 + properties: + threshold: 200 + api_version: "2020-01-01" + want_effect: "Deny" + + - note: config_threshold_new_api_wrong + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/config.threshold", + "equals": 200 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + config: + threshold: 100 + properties: + threshold: 200 + api_version: "2020-01-01" + want_undefined: true + + # ========================================================================= + # 5. tier — properties.pricingTier (old) vs sku.tier (new) + # ========================================================================= + + - note: tier_old_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/tier", + "equals": "Premium" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + tier: "Standard" + properties: + pricingTier: "Premium" + api_version: "2015-06-15" + want_effect: "Deny" + + - note: tier_old_api_wrong + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/tier", + "equals": "Standard" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + tier: "Standard" + properties: + pricingTier: "Premium" + api_version: "2015-06-15" + want_undefined: true + + - note: tier_new_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/tier", + "equals": "Standard" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + tier: "Standard" + properties: + pricingTier: "Premium" + api_version: "2020-01-01" + want_effect: "Deny" + + - note: tier_new_api_wrong + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/tier", + "equals": "Premium" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + sku: + tier: "Standard" + properties: + pricingTier: "Premium" + api_version: "2020-01-01" + want_undefined: true + + # ========================================================================= + # 6. operationMode — three distinct paths across versions + # 2015-01-01: properties.legacyMode = "compat" + # 2017-06-01: properties.settings.mode = "advanced" + # 2020-01-01: properties.mode = "standard" + # default: properties.mode = "standard" + # ========================================================================= + + - note: operationMode_oldest_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/operationMode", + "equals": "compat" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + legacyMode: "compat" + mode: "standard" + settings: + mode: "advanced" + api_version: "2015-01-01" + want_effect: "Deny" + + - note: operationMode_oldest_api_wrong + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/operationMode", + "equals": "standard" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + legacyMode: "compat" + mode: "standard" + settings: + mode: "advanced" + api_version: "2015-01-01" + want_undefined: true + + - note: operationMode_middle_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/operationMode", + "equals": "advanced" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + legacyMode: "compat" + mode: "standard" + settings: + mode: "advanced" + api_version: "2017-06-01" + want_effect: "Deny" + + - note: operationMode_middle_api_wrong + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/operationMode", + "equals": "compat" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + legacyMode: "compat" + mode: "standard" + settings: + mode: "advanced" + api_version: "2017-06-01" + want_undefined: true + + - note: operationMode_newest_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/operationMode", + "equals": "standard" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + legacyMode: "compat" + mode: "standard" + settings: + mode: "advanced" + api_version: "2020-01-01" + want_effect: "Deny" + + - note: operationMode_default_no_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/operationMode", + "equals": "standard" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + legacyMode: "compat" + mode: "standard" + settings: + mode: "advanced" + want_effect: "Deny" + + # ========================================================================= + # 7. FQ alias with operationMode (mixed-case short name + versioned paths) + # ========================================================================= + + - note: fq_operationMode_middle_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/versionedResources/operationMode", + "equals": "advanced" + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/versionedResources + name: testResource + properties: + legacyMode: "compat" + mode: "standard" + settings: + mode: "advanced" + api_version: "2017-06-01" + want_effect: "Deny" + + # ========================================================================= + # PART 2 — Complex resource type: Microsoft.Test/complexResources + # + # Alias catalog for this type includes: + # - Sub-resource array: rules[*] (with properties flattening) + # - Nested sub-resource array: rules[*].filters[*] (2-level flattening) + # - Primitive array: allowedIPs[*] (strings, no properties wrapper) + # - Sub-resource element fields: rules[*].action, rules[*].protocol, etc. + # - Versioned sub-resource field: rules[*].priority (old vs new path) + # - Nested object alias: config.retryCount (versioned), config.timeout + # - Deeply nested alias: settings.encryption.enabled (versioned) + # - Primitive array inside sub-resource: rules[*].targets[*] + # ========================================================================= + + # ----------------------------------------------------------------------- + # 8. Scalar baseline — enabled field + # ----------------------------------------------------------------------- + - note: complex_enabled_baseline + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/enabled", + "equals": true + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + enabled: true + want_effect: "Deny" + + - note: complex_enabled_false + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/enabled", + "equals": true + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + enabled: false + want_undefined: true + + # ----------------------------------------------------------------------- + # 9. Sub-resource array: count rules[*] + # ----------------------------------------------------------------------- + - note: count_rules_basic + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]" + }, + "greater": 2 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + - name: r2 + properties: + action: "Deny" + - name: r3 + properties: + action: "Allow" + want_effect: "Deny" + + - note: count_rules_not_enough + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]" + }, + "greater": 5 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + - name: r2 + properties: + action: "Deny" + want_undefined: true + + # ----------------------------------------------------------------------- + # 10. Sub-resource with where — access flattened properties + # rules[*].action lives under properties wrapper in ARM. + # After normalization, it should be at rules[*].action directly. + # ----------------------------------------------------------------------- + - note: count_rules_where_action + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].action", + "equals": "Allow" + } + }, + "equals": 2 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + - name: r2 + properties: + action: "Deny" + - name: r3 + properties: + action: "Allow" + want_effect: "Deny" + + - note: count_rules_where_action_zero + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].action", + "equals": "Block" + } + }, + "equals": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + - name: r2 + properties: + action: "Deny" + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 11. Sub-resource where with allOf — multiple flattened fields + # ----------------------------------------------------------------------- + - note: count_rules_where_allOf + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].action", "equals": "Allow" }, + { "field": "Microsoft.Test/complexResources/rules[*].direction", "equals": "Inbound" }, + { "field": "Microsoft.Test/complexResources/rules[*].protocol", "equals": "Tcp" } + ] + } + }, + "greaterOrEquals": 1 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + direction: "Inbound" + protocol: "Tcp" + - name: r2 + properties: + action: "Allow" + direction: "Outbound" + protocol: "Tcp" + - name: r3 + properties: + action: "Deny" + direction: "Inbound" + protocol: "Tcp" + want_effect: "Deny" + + - note: count_rules_where_allOf_no_match + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].action", "equals": "Allow" }, + { "field": "Microsoft.Test/complexResources/rules[*].direction", "equals": "Inbound" }, + { "field": "Microsoft.Test/complexResources/rules[*].protocol", "equals": "Udp" } + ] + } + }, + "greaterOrEquals": 1 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + direction: "Inbound" + protocol: "Tcp" + want_undefined: true + + # ----------------------------------------------------------------------- + # 12. Sub-resource where with anyOf + # ----------------------------------------------------------------------- + - note: count_rules_where_anyOf_destPort + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "anyOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].destPort", "equals": "22" }, + { "field": "Microsoft.Test/complexResources/rules[*].destPort", "equals": "3389" } + ] + } + }, + "notEquals": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + destPort: "443" + - name: r2 + properties: + destPort: "22" + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 13. Sub-resource where with not + # ----------------------------------------------------------------------- + - note: count_rules_where_not + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "not": { + "field": "Microsoft.Test/complexResources/rules[*].action", + "equals": "Deny" + } + } + }, + "greater": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + - name: r2 + properties: + action: "Deny" + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 14. Primitive array: count allowedIPs[*] + # Unlike rules[*], allowedIPs is a plain string array — no properties + # flattening. + # ----------------------------------------------------------------------- + - note: count_allowedIPs_basic + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/allowedIPs[*]", + "where": { + "field": "Microsoft.Test/complexResources/allowedIPs[*]", + "like": "10.*" + } + }, + "equals": 2 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + allowedIPs: + - "10.0.0.1" + - "192.168.1.1" + - "10.0.0.2" + want_effect: "Deny" + + - note: count_allowedIPs_empty + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/allowedIPs[*]" + }, + "equals": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + allowedIPs: [] + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 15. Primitive array inside sub-resource: rules[*].targets[*] + # ----------------------------------------------------------------------- + - note: count_rules_targets_nested + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*].targets[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].targets[*]", + "like": "*.prod.*" + } + }, + "greater": 0 + } + }, + "greater": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + targets: + - "app.prod.east" + - "app.dev.east" + - name: r2 + properties: + action: "Allow" + targets: + - "api.dev.west" + want_effect: "Deny" + + - note: count_rules_targets_nested_no_prod + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*].targets[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].targets[*]", + "like": "*.prod.*" + } + }, + "greater": 0 + } + }, + "greater": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + targets: + - "app.dev.east" + - name: r2 + properties: + action: "Allow" + targets: + - "api.staging.west" + want_undefined: true + + # ----------------------------------------------------------------------- + # 16. Nested sub-resource arrays: rules[*].filters[*] + # filters[*] is a sub-resource (has properties wrapper) nested inside + # rules[*] — two-level properties flattening. + # ----------------------------------------------------------------------- + - note: nested_sub_resource_count + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*].filters[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].filters[*].name", + "equals": "source-ip" + } + }, + "greater": 0 + } + }, + "greater": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + filters: + - properties: + name: "source-ip" + value: "10.0.0.0/8" + - properties: + name: "protocol" + value: "tcp" + - name: r2 + properties: + action: "Deny" + filters: + - properties: + name: "dest-port" + value: "22" + want_effect: "Deny" + + - note: nested_sub_resource_count_no_match + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*].filters[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].filters[*].name", + "equals": "geo-location" + } + }, + "greater": 0 + } + }, + "greater": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + filters: + - properties: + name: "source-ip" + value: "10.0.0.0/8" + want_undefined: true + + - note: nested_sub_resource_filter_value + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*].filters[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].filters[*].name", "equals": "protocol" }, + { "field": "Microsoft.Test/complexResources/rules[*].filters[*].value", "equals": "tcp" } + ] + } + }, + "greater": 0 + } + }, + "equals": 1 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + filters: + - properties: + name: "protocol" + value: "tcp" + - name: r2 + properties: + action: "Deny" + filters: + - properties: + name: "protocol" + value: "udp" + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 17. Count in allOf: type check + sub-resource count + # ----------------------------------------------------------------------- + - note: count_in_allOf_with_type_check + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Test/complexResources" }, + { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].action", "equals": "Allow" }, + { "field": "Microsoft.Test/complexResources/rules[*].direction", "equals": "Inbound" } + ] + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + direction: "Inbound" + - name: r2 + properties: + action: "Deny" + direction: "Outbound" + want_effect: "Deny" + + - note: count_in_allOf_wrong_type + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Other/resources" }, + { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].action", + "equals": "Allow" + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + want_undefined: true + + # ----------------------------------------------------------------------- + # 18. Nested object: config.retryCount — versioned + # Old API: properties.config.properties.retryCount + # New API: properties.config.retryCount + # ----------------------------------------------------------------------- + - note: config_retryCount_old_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/config.retryCount", + "greater": 3 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + config: + retryCount: 2 + properties: + retryCount: 5 + api_version: "2015-01-01" + want_effect: "Deny" + + - note: config_retryCount_old_api_wrong + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/config.retryCount", + "greater": 3 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + config: + retryCount: 2 + properties: + retryCount: 1 + api_version: "2015-01-01" + want_undefined: true + + - note: config_retryCount_new_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/config.retryCount", + "greater": 3 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + config: + retryCount: 5 + properties: + retryCount: 1 + api_version: "2020-01-01" + want_effect: "Deny" + + - note: config_retryCount_new_api_wrong + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/config.retryCount", + "greater": 3 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + config: + retryCount: 2 + properties: + retryCount: 99 + api_version: "2020-01-01" + want_undefined: true + + # ----------------------------------------------------------------------- + # 19. Non-versioned nested object: config.timeout + # ----------------------------------------------------------------------- + - note: config_timeout_baseline + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/config.timeout", + "greater": 30 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + config: + timeout: 60 + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 20. Deep nested alias: settings.encryption.enabled — versioned + # Old API (2015): properties.encryptionEnabled (flat) + # New API (2020): properties.settings.encryption.enabled (deep) + # ----------------------------------------------------------------------- + - note: encryption_enabled_old_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/settings.encryption.enabled", + "equals": true + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + encryptionEnabled: true + settings: + encryption: + enabled: false + api_version: "2015-01-01" + want_effect: "Deny" + + - note: encryption_enabled_old_api_wrong + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/settings.encryption.enabled", + "equals": true + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + encryptionEnabled: false + settings: + encryption: + enabled: true + api_version: "2015-01-01" + want_undefined: true + + - note: encryption_enabled_new_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/settings.encryption.enabled", + "equals": true + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + encryptionEnabled: false + settings: + encryption: + enabled: true + api_version: "2020-01-01" + want_effect: "Deny" + + - note: encryption_enabled_new_api_wrong + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/settings.encryption.enabled", + "equals": true + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + encryptionEnabled: true + settings: + encryption: + enabled: false + api_version: "2020-01-01" + want_undefined: true + + - note: encryption_enabled_no_api_default + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/settings.encryption.enabled", + "equals": true + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + encryptionEnabled: false + settings: + encryption: + enabled: true + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 21. FQ alias: fully-qualified with complex resource + # ----------------------------------------------------------------------- + - note: fq_config_retryCount_old_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/config.retryCount", + "greater": 3 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + config: + retryCount: 2 + properties: + retryCount: 5 + api_version: "2015-01-01" + want_effect: "Deny" + + - note: fq_encryption_enabled_new_api + policy_rule: | + { + "if": { + "field": "Microsoft.Test/complexResources/settings.encryption.enabled", + "equals": true + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + encryptionEnabled: false + settings: + encryption: + enabled: true + api_version: "2020-01-01" + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 22. Complex NSG-like: type + count + where(allOf) + anyOf inside where + # ----------------------------------------------------------------------- + - note: complex_nsg_like_deny_ssh + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Test/complexResources" }, + { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].action", "equals": "Allow" }, + { "field": "Microsoft.Test/complexResources/rules[*].direction", "equals": "Inbound" }, + { + "anyOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].destPort", "equals": "22" }, + { "field": "Microsoft.Test/complexResources/rules[*].destPort", "equals": "*" } + ] + }, + { + "anyOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].sourcePort", "equals": "*" }, + { "field": "Microsoft.Test/complexResources/rules[*].sourcePort", "equals": "0" } + ] + } + ] + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: allowHTTPS + properties: + action: "Allow" + direction: "Inbound" + protocol: "Tcp" + destPort: "443" + sourcePort: "*" + - name: allowSSH + properties: + action: "Allow" + direction: "Inbound" + protocol: "Tcp" + destPort: "22" + sourcePort: "*" + want_effect: "Deny" + + - note: complex_nsg_like_no_ssh + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Test/complexResources" }, + { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].action", "equals": "Allow" }, + { "field": "Microsoft.Test/complexResources/rules[*].direction", "equals": "Inbound" }, + { + "anyOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].destPort", "equals": "22" }, + { "field": "Microsoft.Test/complexResources/rules[*].destPort", "equals": "*" } + ] + } + ] + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: allowHTTPS + properties: + action: "Allow" + direction: "Inbound" + protocol: "Tcp" + destPort: "443" + sourcePort: "*" + - name: denySSH + properties: + action: "Deny" + direction: "Inbound" + protocol: "Tcp" + destPort: "22" + sourcePort: "*" + want_undefined: true + + # ----------------------------------------------------------------------- + # 23. Value count with nested field count across sub-resources + # "For each required port, at least one Allow rule must exist" + # ----------------------------------------------------------------------- + - note: value_count_nested_field_count_rules + policy_rule: | + { + "if": { + "count": { + "value": "[parameters('requiredPorts')]", + "name": "port", + "where": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].destPort", "equals": "[current('port')]" }, + { "field": "Microsoft.Test/complexResources/rules[*].action", "equals": "Allow" } + ] + } + }, + "greater": 0 + } + }, + "equals": "[length(parameters('requiredPorts'))]" + }, + "then": { "effect": "Deny" } + } + parameters: + requiredPorts: + - "443" + - "80" + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: allowHTTPS + properties: + action: "Allow" + destPort: "443" + - name: allowHTTP + properties: + action: "Allow" + destPort: "80" + - name: denySSH + properties: + action: "Deny" + destPort: "22" + want_effect: "Deny" + + - note: value_count_nested_field_count_rules_missing + policy_rule: | + { + "if": { + "count": { + "value": "[parameters('requiredPorts')]", + "name": "port", + "where": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].destPort", "equals": "[current('port')]" }, + { "field": "Microsoft.Test/complexResources/rules[*].action", "equals": "Allow" } + ] + } + }, + "greater": 0 + } + }, + "equals": "[length(parameters('requiredPorts'))]" + }, + "then": { "effect": "Deny" } + } + parameters: + requiredPorts: + - "443" + - "80" + - "8080" + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: allowHTTPS + properties: + action: "Allow" + destPort: "443" + - name: allowHTTP + properties: + action: "Allow" + destPort: "80" + want_undefined: true + + # ----------------------------------------------------------------------- + # 24. Empty sub-resource array — count = 0 + # ----------------------------------------------------------------------- + - note: count_rules_empty_array + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]" + }, + "equals": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: [] + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 25. Missing sub-resource array — count = 0 + # ----------------------------------------------------------------------- + - note: count_rules_missing_array + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]" + }, + "equals": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + enabled: true + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 26. Sub-resource where accessing root-level field (name on element) + # The "name" field lives at the element root, not under properties. + # ----------------------------------------------------------------------- + - note: count_rules_where_name_field + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].name", + "like": "allow*" + } + }, + "equals": 2 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: allowHTTPS + properties: + action: "Allow" + - name: allowHTTP + properties: + action: "Allow" + - name: denySSH + properties: + action: "Deny" + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 27. Mixed: scalar alias AND count in allOf + # ----------------------------------------------------------------------- + - note: enabled_and_rule_count_allOf + policy_rule: | + { + "if": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/enabled", "equals": true }, + { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].action", + "equals": "Allow" + } + }, + "greater": 3 + } + ] + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + enabled: true + rules: + - name: r1 + properties: { action: "Allow" } + - name: r2 + properties: { action: "Allow" } + - name: r3 + properties: { action: "Allow" } + - name: r4 + properties: { action: "Allow" } + want_effect: "Deny" + + - note: enabled_false_short_circuits + policy_rule: | + { + "if": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/enabled", "equals": true }, + { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].action", + "equals": "Allow" + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + enabled: false + rules: + - name: r1 + properties: { action: "Allow" } + want_undefined: true + + # ----------------------------------------------------------------------- + # 28. Count in not — negated sub-resource count + # ----------------------------------------------------------------------- + - note: not_count_rules + policy_rule: | + { + "if": { + "not": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]" + }, + "lessOrEquals": 2 + } + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: { action: "Allow" } + - name: r2 + properties: { action: "Allow" } + - name: r3 + properties: { action: "Deny" } + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 29. Sub-resource element without properties wrapper + # Some elements may just have top-level fields, no properties. + # The normalizer should handle this gracefully. + # ----------------------------------------------------------------------- + - note: sub_resource_no_properties_wrapper + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].name", + "equals": "direct" + } + }, + "greater": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: "direct" + action: "Allow" + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 30. Deep nesting: allOf > anyOf > not > count + # ----------------------------------------------------------------------- + - note: deep_logical_with_count + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Test/complexResources" }, + { + "anyOf": [ + { + "not": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].action", + "equals": "Deny" + } + }, + "greaterOrEquals": 1 + } + } + ] + } + ] + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + - name: r2 + properties: + action: "Allow" + want_effect: "Deny" + + - note: deep_logical_with_count_has_deny + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Test/complexResources" }, + { + "anyOf": [ + { + "not": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].action", + "equals": "Deny" + } + }, + "greaterOrEquals": 1 + } + } + ] + } + ] + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + - name: r2 + properties: + action: "Deny" + want_undefined: true + + # ----------------------------------------------------------------------- + # 31. Missing field in sub-resource element — exists false + # ----------------------------------------------------------------------- + - note: exists_false_in_sub_resource + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].protocol", + "exists": false + } + }, + "greater": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + protocol: "Tcp" + - name: r2 + properties: + action: "Deny" + want_effect: "Deny" + + - note: exists_true_in_sub_resource_all_present + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].protocol", + "exists": false + } + }, + "equals": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + action: "Allow" + protocol: "Tcp" + - name: r2 + properties: + action: "Deny" + protocol: "Udp" + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 32. Versioned sub-resource field: rules[*].priority + # Old API (2015): properties.rules[*].properties.priority + # New API (2020): properties.rules[*].properties.prio + # The "priority" alias always maps to a field under the element's + # properties wrapper — the difference is the field NAME changes + # across versions. + # + # NOTE: Versioned paths for sub-resource element fields are handled + # at the structural level (the alias defines the full ARM path). + # After normalization, the alias short name "priority" should resolve + # correctly. We place DIFFERENT values at "priority" vs "prio" in the + # elements to test path selection. + # ----------------------------------------------------------------------- + + # Versioned sub-resource element fields use per-element alias resolution + # during normalization. After structural flattening, the normalizer remaps + # element-level field names when the selected API path differs from the + # alias short name (e.g., "prio" → "priority" for new API versions). + - note: rules_priority_old_api + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].priority", + "greater": 100 + } + }, + "greater": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + priority: 200 + prio: 50 + api_version: "2015-01-01" + want_effect: "Deny" + + - note: rules_priority_new_api + policy_rule: | + { + "if": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "field": "Microsoft.Test/complexResources/rules[*].priority", + "greater": 100 + } + }, + "greater": 0 + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + rules: + - name: r1 + properties: + priority: 50 + prio: 200 + api_version: "2020-01-01" + want_effect: "Deny" + + # ----------------------------------------------------------------------- + # 33. Three-level nesting: allOf > count(rules) > where > count(filters) + # > where > allOf (filter name + value) + # ----------------------------------------------------------------------- + - note: three_level_nesting_count_in_allof + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Test/complexResources" }, + { "field": "Microsoft.Test/complexResources/enabled", "equals": true }, + { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].action", "equals": "Allow" }, + { + "count": { + "field": "Microsoft.Test/complexResources/rules[*].filters[*]", + "where": { + "allOf": [ + { "field": "Microsoft.Test/complexResources/rules[*].filters[*].name", "equals": "protocol" }, + { "field": "Microsoft.Test/complexResources/rules[*].filters[*].value", "equals": "tcp" } + ] + } + }, + "greater": 0 + } + ] + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + enabled: true + rules: + - name: r1 + properties: + action: "Allow" + filters: + - properties: + name: "protocol" + value: "tcp" + - properties: + name: "source-ip" + value: "10.0.0.0/8" + - name: r2 + properties: + action: "Deny" + filters: + - properties: + name: "protocol" + value: "udp" + want_effect: "Deny" + + - note: three_level_nesting_enabled_false + policy_rule: | + { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Test/complexResources" }, + { "field": "Microsoft.Test/complexResources/enabled", "equals": true }, + { + "count": { + "field": "Microsoft.Test/complexResources/rules[*]", + "where": { + "count": { + "field": "Microsoft.Test/complexResources/rules[*].filters[*]" + }, + "greater": 0 + } + }, + "greater": 0 + } + ] + }, + "then": { "effect": "Deny" } + } + resource: + type: Microsoft.Test/complexResources + name: myResource + properties: + enabled: false + rules: + - name: r1 + properties: + action: "Allow" + filters: + - properties: { name: "x", value: "y" } + want_undefined: true