feat(azure-policy): add policy rule and policy definition parsers (#660)

Extend the Azure Policy parser to handle complete policyRule and
policyDefinition JSON structures, not just standalone constraints.

Policy rule parser (policy_rule.rs):
- Parse top-level { "if": ..., "then": ... } objects
- Extract effect kind (deny, audit, append, modify, etc.) into typed AST
- Parse "details" structurally when it is an object to pull out
  existenceCondition as a first-class Constraint; fall back to opaque
  JSON for non-object details (e.g. append array form)
- Detect duplicate/missing keys for "if", "then", "effect", "details"

Policy definition parser (policy_definition.rs):
- Handle both wrapped ARM envelope ({ "properties": { ... } }) and
  unwrapped (properties-level keys at top level) forms
- Type-extract displayName, description, mode, metadata, parameters,
  and policyRule; everything else goes into extra
- Parse parameter definitions with type, defaultValue, allowedValues,
  and metadata; detect duplicate parameter names
- Duplicate key detection throughout

Grammar documentation (docs/azure-policy/azurepolicy.ebnf):
- Add formal EBNF grammar covering policy-rule, then-block,
  constraints, conditions, all 19 operators, count expressions,
  JSON values, and ARM template expressions

Test harness changes:
- Add parse_level field to YAML test cases: "constraint" (default),
  "policy_rule", or "policy_definition"
- Un-skip three parse_errors cases that needed policy_rule-level parsing
- Add policy_rule.yaml with 12 cases covering all 9 effect kinds,
  existenceCondition, parameterized effects, complex conditions, and
  extra key handling
- Add policy_definition.yaml with wrapped, unwrapped, parameterized,
  missing-policyRule, and duplicate-key error cases
This commit is contained in:
Anand Krishnamoorthi
2026-04-06 11:36:24 -05:00
committed by GitHub
parent 687be2850b
commit 8f740e2f6f
11 changed files with 1311 additions and 40 deletions
+65 -6
View File
@@ -1,7 +1,7 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
//! Recursive-descent JSON parser for Azure Policy rule constraints.
//! Custom recursive-descent JSON parser for Azure Policy rules.
//!
//! Parses Azure Policy JSON directly from [`Lexer`] tokens, building span-annotated
//! AST nodes in a single pass. No intermediate `serde_json::Value` is created.
@@ -9,19 +9,34 @@
//! 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.
//!
//! ## Usage
//!
//! ```ignore
//! use regorus::Source;
//! use regorus::languages::azure_policy::parser;
//!
//! let json = r#"{ "if": { "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
//! "then": { "effect": "deny" } }"#;
//! let source = Source::from_contents("policy.json".into(), json.into())?;
//! let rule = parser::parse_policy_rule(&source)?;
//! ```
mod constraint;
mod core;
mod error;
mod policy_definition;
mod policy_rule;
pub(super) use self::core::json_unescape;
pub use error::ParseError;
use alloc::string::ToString as _;
use crate::lexer::{Source, TokenKind};
use super::ast::{Constraint, FieldKind, OperatorKind};
use super::ast::{Constraint, FieldKind, OperatorKind, PolicyDefinition, PolicyRule};
use super::expr::ExprParser;
use self::core::Parser;
@@ -30,12 +45,56 @@ use self::core::Parser;
// Public API
// ============================================================================
/// Parse an Azure Policy rule from a JSON source.
///
/// The source should contain a complete `policyRule` JSON object:
/// ```json
/// {
/// "if": { "field": "type", "equals": "Microsoft.Compute/virtualMachines" },
/// "then": { "effect": "deny" }
/// }
/// ```
///
/// Returns a span-annotated [`PolicyRule`] AST.
pub fn parse_policy_rule(source: &Source) -> Result<PolicyRule, ParseError> {
let mut parser = Parser::new(source)?;
let rule = parser.parse_policy_rule()?;
if parser.tok.0 != TokenKind::Eof {
return Err(ParseError::UnexpectedToken {
span: parser.tok.1.clone(),
expected: "end of input",
});
}
Ok(rule)
}
/// Parse a full Azure Policy definition from a JSON source.
///
/// Accepts two forms:
/// 1. **Wrapped**: `{ "properties": { "policyRule": ..., ... }, "id": ..., ... }`
/// 2. **Unwrapped**: `{ "displayName": ..., "policyRule": ..., ... }`
///
/// Returns a [`PolicyDefinition`] with typed fields for known properties
/// and a catch-all list of `extra` entries for everything else.
pub fn parse_policy_definition(source: &Source) -> Result<PolicyDefinition, ParseError> {
let mut parser = Parser::new(source)?;
let defn = parser.parse_policy_definition()?;
if parser.tok.0 != TokenKind::Eof {
return Err(ParseError::UnexpectedToken {
span: parser.tok.1.clone(),
expected: "end of input",
});
}
Ok(defn)
}
/// 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 }`
/// Useful for parsing just the `"if"` part of a policy rule.
pub fn parse_constraint(source: &Source) -> Result<Constraint, ParseError> {
let mut parser = Parser::new(source)?;
let constraint = parser.parse_constraint()?;