feat: add Azure Policy constraint parser (#658)

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

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

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

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

View File

@@ -0,0 +1,411 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Constraint parsing: logical combinators, leaf conditions, and count blocks.
use alloc::boxed::Box;
use alloc::string::String;
use alloc::vec::Vec;
use crate::lexer::Span;
use crate::languages::azure_policy::ast::{
Condition, Constraint, CountNode, FieldNode, JsonValue, Lhs, NameNode, OperatorNode,
};
use super::classify_field;
use super::core::{CountInner, EntryValue, Parser};
use super::error::ParseError;
use super::parse_operator_kind;
/// Set `slot` to `val`, returning a [`ParseError::DuplicateKey`] if it was already set.
fn set_once<T>(slot: &mut Option<T>, val: T, key: &str, span: &Span) -> Result<(), ParseError> {
if slot.is_some() {
return Err(ParseError::DuplicateKey {
span: span.clone(),
key: String::from(key),
});
}
*slot = Some(val);
Ok(())
}
impl<'source> Parser<'source> {
/// Parse a constraint (a JSON object: logical combinator or leaf condition).
pub fn parse_constraint(&mut self) -> Result<Constraint, ParseError> {
let open = self.expect_symbol("{")?;
let mut entries: Vec<(Span, String, EntryValue)> = Vec::new();
if self.token_text() != "}" {
loop {
let (key_span, key) = self.expect_string()?;
self.expect_symbol(":")?;
let key_lower = key.to_lowercase();
let value = match key_lower.as_str() {
"allof" | "anyof" => {
if self.token_text() != "[" {
return Err(ParseError::LogicalOperatorNotArray {
span: key_span,
operator: key,
});
}
EntryValue::ConstraintArray(self.parse_constraint_array()?)
}
"not" => EntryValue::SingleConstraint(self.parse_constraint()?),
"count" => EntryValue::CountInner(Box::new(self.parse_count_inner()?)),
_ => EntryValue::Json(self.parse_json_value()?),
};
// Detect duplicate keys during collection (case-insensitive).
if entries
.iter()
.any(|entry| entry.1.eq_ignore_ascii_case(&key))
{
return Err(ParseError::DuplicateKey {
span: key_span,
key,
});
}
entries.push((key_span, key, value));
if self.token_text() == "," {
self.advance()?;
} else {
break;
}
}
}
let close = self.expect_symbol("}")?;
let span = Span {
source: open.source.clone(),
line: open.line,
col: open.col,
start: open.start,
end: close.end,
};
Self::build_constraint(span, entries)
}
/// Parse a `[constraint, constraint, ...]` array for `allOf`/`anyOf`.
fn parse_constraint_array(&mut self) -> Result<Vec<Constraint>, ParseError> {
self.expect_symbol("[")?;
let mut constraints = Vec::new();
if self.token_text() != "]" {
constraints.push(self.parse_constraint()?);
while self.token_text() == "," {
self.advance()?;
constraints.push(self.parse_constraint()?);
}
}
self.expect_symbol("]")?;
Ok(constraints)
}
/// Dispatch on collected entries to build the appropriate constraint.
fn build_constraint(
span: Span,
entries: Vec<(Span, String, EntryValue)>,
) -> Result<Constraint, ParseError> {
// Check for logical operators with extra keys.
for entry in entries.iter() {
let lower = entry.1.to_lowercase();
if matches!(lower.as_str(), "allof" | "anyof" | "not") && entries.len() > 1 {
return Err(ParseError::ExtraKeysInLogical {
span: entry.0.clone(),
operator: entry.1.clone(),
});
}
}
// Single-entry logical operators.
if entries.len() == 1 {
// Safe: we just checked len() == 1.
let mut entries = entries;
let (key_span, key, value) = entries
.pop()
.ok_or_else(|| ParseError::MissingLhsOperand { span: span.clone() })?;
match key.to_lowercase().as_str() {
"allof" => {
// Non-array values are caught during parsing (LogicalOperatorNotArray).
let EntryValue::ConstraintArray(constraints) = value else {
return Err(ParseError::LogicalOperatorNotArray {
span: key_span,
operator: key,
});
};
return Ok(Constraint::AllOf { span, constraints });
}
"anyof" => {
let EntryValue::ConstraintArray(constraints) = value else {
return Err(ParseError::LogicalOperatorNotArray {
span: key_span,
operator: key,
});
};
return Ok(Constraint::AnyOf { span, constraints });
}
"not" => {
let EntryValue::SingleConstraint(constraint) = value else {
// `not` is always parsed as SingleConstraint in parse_constraint,
// so this branch is structurally unreachable.
return Err(ParseError::UnexpectedToken {
span: key_span,
expected: "constraint for 'not'",
});
};
return Ok(Constraint::Not {
span,
constraint: Box::new(constraint),
});
}
_ => {
return Self::build_condition(span, alloc::vec![(key_span, key, value)]);
}
}
}
Self::build_condition(span, entries)
}
/// Build a leaf condition from collected object entries.
fn build_condition(
span: Span,
entries: Vec<(Span, String, EntryValue)>,
) -> Result<Constraint, ParseError> {
let mut field: Option<(Span, JsonValue)> = None;
let mut value: Option<(Span, JsonValue)> = None;
let mut count: Option<(Span, Box<CountInner>)> = None;
let mut operator: Option<OperatorNode> = None;
let mut rhs: Option<JsonValue> = None;
for (key_span, key, entry_value) in entries {
match key.to_lowercase().as_str() {
"field" => {
let EntryValue::Json(jv) = entry_value else {
return Err(ParseError::UnexpectedToken {
span: key_span,
expected: "JSON value for 'field'",
});
};
set_once(&mut field, (key_span.clone(), jv), &key, &key_span)?;
}
"value" => {
let EntryValue::Json(jv) = entry_value else {
return Err(ParseError::UnexpectedToken {
span: key_span,
expected: "JSON value for 'value'",
});
};
set_once(&mut value, (key_span.clone(), jv), &key, &key_span)?;
}
"count" => {
let EntryValue::CountInner(ci) = entry_value else {
return Err(ParseError::UnexpectedToken {
span: key_span,
expected: "object for 'count'",
});
};
set_once(&mut count, (key_span.clone(), ci), &key, &key_span)?;
}
_ => {
if let Some(op_kind) = parse_operator_kind(&key.to_lowercase()) {
let EntryValue::Json(jv) = entry_value else {
return Err(ParseError::UnexpectedToken {
span: key_span,
expected: "JSON value for operator",
});
};
if operator.is_some() {
return Err(ParseError::MultipleOperators { span: key_span });
}
operator = Some(OperatorNode {
span: key_span,
kind: op_kind,
});
rhs = Some(jv);
} else {
return Err(ParseError::UnrecognizedKey {
span: key_span,
key,
});
}
}
}
}
let operator =
operator.ok_or_else(|| ParseError::MissingOperator { span: span.clone() })?;
let rhs_json = rhs.ok_or_else(|| ParseError::MissingOperator { span: span.clone() })?;
let rhs_value = Self::json_to_value_or_expr(rhs_json)?;
let lhs = match (field, value, count) {
(Some((_, fv)), None, None) => Lhs::Field(Self::json_to_field(fv)?),
(None, Some((key_span, vv)), None) => Lhs::Value {
key_span,
value: Self::json_to_value_or_expr(vv)?,
},
(None, None, Some((_, ci))) => Lhs::Count(Self::finalize_count(*ci)?),
(None, None, None) => {
return Err(ParseError::MissingLhsOperand { span: span.clone() });
}
_ => {
return Err(ParseError::MultipleLhsOperands { span: span.clone() });
}
};
Ok(Constraint::Condition(Box::new(Condition {
span,
lhs,
operator,
rhs: rhs_value,
})))
}
// ========================================================================
// Count parsing
// ========================================================================
/// Parse the inner object of a `"count": { ... }` block.
pub fn parse_count_inner(&mut self) -> Result<CountInner, ParseError> {
let open = self.expect_symbol("{")?;
let mut field: Option<(Span, JsonValue)> = None;
let mut value: Option<(Span, JsonValue)> = None;
let mut name: Option<(Span, JsonValue)> = None;
let mut where_: Option<Constraint> = None;
if self.token_text() != "}" {
loop {
let (key_span, key) = self.expect_string()?;
self.expect_symbol(":")?;
let key_lower = key.to_lowercase();
match key_lower.as_str() {
"field" => {
let jv = self.parse_json_value()?;
set_once(&mut field, (key_span.clone(), jv), &key_lower, &key_span)?;
}
"value" => {
let jv = self.parse_json_value()?;
set_once(&mut value, (key_span.clone(), jv), &key_lower, &key_span)?;
}
"name" => {
let jv = self.parse_json_value()?;
set_once(&mut name, (key_span.clone(), jv), &key_lower, &key_span)?;
}
"where" => {
let c = self.parse_constraint()?;
set_once(&mut where_, c, &key_lower, &key_span)?;
}
_ => {
return Err(ParseError::UnrecognizedKey {
span: key_span,
key: key_lower,
});
}
}
if self.token_text() == "," {
self.advance()?;
} else {
break;
}
}
}
let close = self.expect_symbol("}")?;
let span = Span {
source: open.source.clone(),
line: open.line,
col: open.col,
start: open.start,
end: close.end,
};
Ok(CountInner {
span,
field,
value,
name,
where_,
})
}
// ========================================================================
// Helpers
// ========================================================================
/// Convert a JSON value (expected to be a string) into a [`FieldNode`].
pub fn json_to_field(jv: JsonValue) -> Result<FieldNode, ParseError> {
let (span, text) = match jv {
JsonValue::Str(span, text) => (span, text),
other => {
return Err(ParseError::UnexpectedToken {
span: other.span().clone(),
expected: "string for 'field' value",
});
}
};
let kind = classify_field(&text, &span)?;
Ok(FieldNode { span, kind })
}
/// Finalize a [`CountInner`] into a [`CountNode`].
pub fn finalize_count(ci: CountInner) -> Result<CountNode, ParseError> {
let CountInner {
span,
field,
value,
name,
where_,
} = ci;
let where_box = where_.map(Box::new);
let name_node = match name {
Some((key_span, jv)) => {
if value.is_none() {
return Err(ParseError::MisplacedCountName { span: key_span });
}
match jv {
JsonValue::Str(name_span, text) => Some(NameNode {
span: name_span,
name: text,
}),
_ => {
return Err(ParseError::InvalidCountName {
span: jv.span().clone(),
});
}
}
}
None => None,
};
match (field, value) {
(None, None) => Err(ParseError::MissingCountCollection { span }),
(Some((_, fv)), None) => {
let field_node = Self::json_to_field(fv)?;
Ok(CountNode::Field {
span,
field: field_node,
where_: where_box,
})
}
(None, Some((_, vv))) => {
let val = Self::json_to_value_or_expr(vv)?;
Ok(CountNode::Value {
span,
value: val,
name: name_node,
where_: where_box,
})
}
(Some(_), Some(_)) => Err(ParseError::MultipleCountCollections { span }),
}
}
}

View File

@@ -39,6 +39,10 @@ pub enum ParseError {
InvalidCountName { span: Span },
/// `name` used without `value` in count.
MisplacedCountName { span: Span },
/// Multiple operator keys in a single condition.
MultipleOperators { span: Span },
/// A duplicate key was found in a JSON object.
DuplicateKey { span: Span, key: String },
/// A custom error message (e.g., from sub-parsing).
Custom { span: Span, message: String },
}
@@ -137,6 +141,16 @@ impl core::fmt::Display for ParseError {
span.error("'name' can only be used with count-value")
)
}
ParseError::MultipleOperators { ref span } => {
write!(
f,
"{}",
span.error("only one operator key allowed in a condition")
)
}
ParseError::DuplicateKey { ref span, ref key } => {
write!(f, "{}", span.error(&format!("duplicate key \"{}\"", key)))
}
ParseError::Custom {
ref span,
ref message,

View File

@@ -1,19 +1,17 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Core recursive-descent JSON parser for Azure Policy.
//! Recursive-descent JSON parser for Azure Policy rule constraints.
//!
//! Provides the low-level token-driven parser (`core::Parser`) that reads JSON from
//! [`Lexer`] tokens, building span-annotated AST values in a single pass.
//! No intermediate `serde_json::Value` is created.
//! Parses Azure Policy JSON directly from [`Lexer`] tokens, building span-annotated
//! AST nodes in a single pass. No intermediate `serde_json::Value` is created.
//!
//! Higher-level policy-aware parsing (constraints, policy rules, policy
//! definitions) is layered on top by sibling modules.
//! The parser is policy-aware: when parsing JSON objects, it dispatches on key names
//! (`allOf`, `anyOf`, `not`, `field`, `value`, `count`, operator names) to build
//! the appropriate AST nodes.
// Parser internals are consumed by constraint/policy_rule/policy_definition
// modules added in a subsequent PR.
#[allow(dead_code)]
pub(crate) mod core;
mod constraint;
mod core;
mod error;
pub(super) use self::core::json_unescape;
@@ -21,15 +19,42 @@ pub use error::ParseError;
use alloc::string::ToString as _;
use super::ast::{FieldKind, OperatorKind};
use crate::lexer::{Source, TokenKind};
use super::ast::{Constraint, FieldKind, OperatorKind};
use super::expr::ExprParser;
use self::core::Parser;
// ============================================================================
// Helper functions (used by constraint/policy_rule/policy_definition modules)
// Public API
// ============================================================================
/// Parse a standalone constraint from a JSON source.
///
/// A constraint is one of:
/// - Logical combinator: `{ "allOf": [...] }`, `{ "anyOf": [...] }`, `{ "not": {...} }`
/// - Leaf condition: `{ "field": "...", "equals": "..." }`
/// - Count condition: `{ "count": { "field": "..." }, "greater": 0 }`
pub fn parse_constraint(source: &Source) -> Result<Constraint, ParseError> {
let mut parser = Parser::new(source)?;
let constraint = parser.parse_constraint()?;
if parser.tok.0 != TokenKind::Eof {
return Err(ParseError::UnexpectedToken {
span: parser.tok.1.clone(),
expected: "end of input",
});
}
Ok(constraint)
}
// ============================================================================
// Helper functions (used across submodules)
// ============================================================================
/// Check if a string is an ARM template expression (`[...]` but not `[[...`).
#[allow(dead_code)]
pub(super) fn is_template_expr(s: &str) -> bool {
s.starts_with('[') && s.ends_with(']') && !s.starts_with("[[")
}
@@ -51,7 +76,6 @@ fn unwrap(s: &str, prefix_len: usize, suffix_len: usize) -> &str {
}
/// Classify a field string into a [`FieldKind`].
#[allow(dead_code)]
pub(super) fn classify_field(
text: &str,
span: &crate::lexer::Span,
@@ -96,7 +120,6 @@ pub(super) fn classify_field(
}
/// Try to parse a lowercase key as an operator kind.
#[allow(dead_code)]
pub(super) fn parse_operator_kind(key: &str) -> Option<OperatorKind> {
match key {
"contains" => Some(OperatorKind::Contains),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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