mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat(rvm): implement Azure Policy condition evaluation (#661)
Add VM support for Azure Policy's condition operators and allOf/anyOf short-circuit logic, gated behind cfg(feature = "azure_policy"). Policy conditions (equals, contains, like, match, exists, and their negations — 21 total) are encoded as a single PolicyCondition instruction with a PolicyOp sub-opcode rather than bloating the Instruction enum with 21 variants. The dispatch handles Azure Policy's quirky comparison semantics: case-insensitive string comparison, string↔number coercion, null vs undefined distinction, and element-wise collection membership. allOf/anyOf blocks use four instructions — LogicalBlockStart, AllOfNext/AnyOfNext, and LogicalBlockEnd — that wire up a result register and short-circuit on the first failing (allOf) or passing (anyOf) child. Helper functions for case-folded comparison, wildcard/glob matching, and type coercion live in builtins::azure_policy::helpers. Two YAML test suites (~2200 lines) exercise the full operator matrix and the allOf/anyOf control flow.
This commit is contained in:
committed by
GitHub
parent
83ce8c3580
commit
4d35744c4f
@@ -47,6 +47,156 @@ pub fn as_str(value: &Value) -> Option<&str> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Coerce a value to its string representation for policy comparison operators.
|
||||
///
|
||||
/// Azure Policy coerces numbers and booleans to strings when used with string
|
||||
/// operators (`like`, `match`, `contains`, `matchInsensitively`). This is
|
||||
/// needed when, for example, a count result (always a number) is compared
|
||||
/// using a string operator: `count(...) like 2`.
|
||||
pub fn coerce_to_string(value: &Value) -> Option<String> {
|
||||
match *value {
|
||||
Value::String(ref s) => Some(s.to_string()),
|
||||
Value::Number(ref n) => Some(n.format_decimal()),
|
||||
Value::Bool(b) => Some(if b { "true" } else { "false" }.to_string()),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn coerce_to_string_ci(value: &Value) -> Option<String> {
|
||||
coerce_to_string(value).map(|s| strings::case_fold::fold(&s).into_owned())
|
||||
}
|
||||
|
||||
// ── Collection helpers ────────────────────────────────────────────────
|
||||
|
||||
/// Check if an array or set contains a null sentinel.
|
||||
pub fn collection_has_null(v: &Value) -> bool {
|
||||
match *v {
|
||||
Value::Array(ref items) => items.iter().any(|i| matches!(i, Value::Null)),
|
||||
Value::Set(ref items) => items.iter().any(|i| matches!(i, Value::Null)),
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if any non-null element in a collection case-insensitively equals `target`.
|
||||
/// Scalar RHS is treated as a single-element collection.
|
||||
pub fn collection_any_ci_eq_excluding_null(collection: &Value, target: &Value) -> bool {
|
||||
match *collection {
|
||||
Value::Array(ref items) => items
|
||||
.iter()
|
||||
.filter(|i| !matches!(i, Value::Null))
|
||||
.any(|i| case_insensitive_equals(i, target)),
|
||||
Value::Set(ref items) => items
|
||||
.iter()
|
||||
.filter(|i| !matches!(i, Value::Null))
|
||||
.any(|i| case_insensitive_equals(i, target)),
|
||||
_ => case_insensitive_equals(collection, target),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_boolish(value: &Value) -> Option<bool> {
|
||||
match *value {
|
||||
Value::Bool(b) => Some(b),
|
||||
Value::String(ref s) => {
|
||||
if s.eq_ignore_ascii_case("true") {
|
||||
Some(true)
|
||||
} else if s.eq_ignore_ascii_case("false") {
|
||||
Some(false)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
// ── Comparison and coercion ───────────────────────────────────────────
|
||||
|
||||
pub fn compare_values(left: &Value, right: &Value) -> Option<i8> {
|
||||
if is_undefined(left) || is_undefined(right) {
|
||||
return None;
|
||||
}
|
||||
|
||||
#[allow(clippy::pattern_type_mismatch)]
|
||||
match (left, right) {
|
||||
(Value::String(a), Value::String(b)) => Some(match strings::case_fold::cmp(a, b) {
|
||||
core::cmp::Ordering::Less => -1,
|
||||
core::cmp::Ordering::Equal => 0,
|
||||
core::cmp::Ordering::Greater => 1,
|
||||
}),
|
||||
(Value::Number(a), Value::Number(b)) => Some(if a < b {
|
||||
-1
|
||||
} else if a > b {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}),
|
||||
(Value::Bool(a), Value::Bool(b)) => Some(if a == b {
|
||||
0
|
||||
} else if !a && *b {
|
||||
-1
|
||||
} else {
|
||||
1
|
||||
}),
|
||||
// String ↔ Number coercion
|
||||
(Value::String(s), Value::Number(n)) => try_coerce_to_number(s).map(|sn| {
|
||||
if &sn < n {
|
||||
-1
|
||||
} else if &sn > n {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}),
|
||||
(Value::Number(n), Value::String(s)) => try_coerce_to_number(s).map(|sn| {
|
||||
if n < &sn {
|
||||
-1
|
||||
} else if n > &sn {
|
||||
1
|
||||
} else {
|
||||
0
|
||||
}
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn case_insensitive_equals(left: &Value, right: &Value) -> bool {
|
||||
if is_undefined(left) || is_undefined(right) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Azure Policy treats an explicit null field value as "" (empty string)
|
||||
// for comparison purposes. Missing fields are Undefined and caught above.
|
||||
#[allow(clippy::pattern_type_mismatch)]
|
||||
match (left, right) {
|
||||
(Value::Null, Value::Null) => true,
|
||||
(Value::Null, Value::String(b)) => strings::case_fold::eq("", b),
|
||||
(Value::String(a), Value::Null) => strings::case_fold::eq(a, ""),
|
||||
(Value::String(a), Value::String(b)) => strings::case_fold::eq(a, b),
|
||||
// String ↔ Number coercion
|
||||
(Value::String(s), Value::Number(_)) | (Value::Number(_), Value::String(s)) => {
|
||||
try_coerce_to_number(s).is_some_and(|n| {
|
||||
let num_val = Value::Number(n);
|
||||
let other = if matches!(left, Value::String(_)) {
|
||||
right
|
||||
} else {
|
||||
left
|
||||
};
|
||||
&num_val == other
|
||||
})
|
||||
}
|
||||
// String ↔ Bool coercion ("true"/"false" ↔ true/false)
|
||||
(Value::String(_), Value::Bool(b)) | (Value::Bool(b), Value::String(_)) => {
|
||||
as_boolish(if matches!(left, Value::String(_)) {
|
||||
left
|
||||
} else {
|
||||
right
|
||||
}) == Some(*b)
|
||||
}
|
||||
_ => left == right,
|
||||
}
|
||||
}
|
||||
|
||||
/// Try to parse a string as a number for Azure Policy type coercion.
|
||||
pub fn try_coerce_to_number(s: &str) -> Option<crate::number::Number> {
|
||||
use core::str::FromStr as _;
|
||||
@@ -61,6 +211,103 @@ pub fn try_coerce_to_number(s: &str) -> Option<crate::number::Number> {
|
||||
})
|
||||
}
|
||||
|
||||
// ── Pattern matching ──────────────────────────────────────────────────
|
||||
|
||||
pub fn match_pattern(input_val: &Value, pattern_val: &Value, insensitive: bool) -> bool {
|
||||
let Some(mut input) = coerce_to_string(input_val) else {
|
||||
return false;
|
||||
};
|
||||
let Some(mut pattern) = coerce_to_string(pattern_val) else {
|
||||
return false;
|
||||
};
|
||||
|
||||
if insensitive {
|
||||
input = strings::case_fold::fold(&input).into_owned();
|
||||
pattern = strings::case_fold::fold(&pattern).into_owned();
|
||||
}
|
||||
|
||||
match_question_hash_pattern(&input, &pattern)
|
||||
}
|
||||
|
||||
pub fn match_like_pattern_ci(input: &str, pattern: &str) -> bool {
|
||||
wildcard_match(input, pattern)
|
||||
}
|
||||
|
||||
fn next_char(s: &str, index: usize) -> Option<(char, usize)> {
|
||||
s.get(index..)?
|
||||
.chars()
|
||||
.next()
|
||||
.map(|ch| (ch, index.saturating_add(ch.len_utf8())))
|
||||
}
|
||||
|
||||
fn wildcard_match(input: &str, pattern: &str) -> bool {
|
||||
let (mut ii, mut pi) = (0_usize, 0_usize);
|
||||
let mut star_pat: Option<usize> = None;
|
||||
let mut star_inp = 0_usize;
|
||||
|
||||
while ii < input.len() {
|
||||
let pat = next_char(pattern, pi);
|
||||
let inp = next_char(input, ii);
|
||||
|
||||
if let (Some((pc, next_pi)), Some((ic, next_ii))) = (pat, inp) {
|
||||
if pc == '?' || pc == ic {
|
||||
pi = next_pi;
|
||||
ii = next_ii;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
if matches!(pat, Some(('*', _))) {
|
||||
star_pat = Some(pi);
|
||||
star_inp = ii;
|
||||
pi = pi.saturating_add('*'.len_utf8());
|
||||
} else if let Some(saved_pi) = star_pat {
|
||||
pi = saved_pi.saturating_add('*'.len_utf8());
|
||||
if let Some((_, next_ii)) = next_char(input, star_inp) {
|
||||
star_inp = next_ii;
|
||||
ii = star_inp;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
while matches!(next_char(pattern, pi), Some(('*', _))) {
|
||||
pi = pi.saturating_add('*'.len_utf8());
|
||||
}
|
||||
|
||||
pi == pattern.len()
|
||||
}
|
||||
|
||||
pub fn match_question_hash_pattern(input: &str, pattern: &str) -> bool {
|
||||
let mut input_chars = input.chars();
|
||||
let mut pattern_chars = pattern.chars();
|
||||
|
||||
loop {
|
||||
match (input_chars.next(), pattern_chars.next()) {
|
||||
(None, None) => return true,
|
||||
(Some(_), None) | (None, Some(_)) => return false,
|
||||
(Some(input_char), Some(pattern_char)) => {
|
||||
if pattern_char == '.' {
|
||||
// '.' matches any single character (letter, digit, or special).
|
||||
} else if pattern_char == '#' {
|
||||
if !input_char.is_ascii_digit() {
|
||||
return false;
|
||||
}
|
||||
} else if pattern_char == '?' {
|
||||
if !input_char.is_ascii_alphabetic() {
|
||||
return false;
|
||||
}
|
||||
} else if input_char != pattern_char {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── Path resolution ───────────────────────────────────────────────────
|
||||
|
||||
pub fn resolve_path(root: &Value, path: &str) -> Value {
|
||||
|
||||
@@ -5,7 +5,7 @@ use alloc::format;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use super::types::GuardMode;
|
||||
use super::types::{GuardMode, LogicalBlockMode, PolicyOp};
|
||||
use super::{Instruction, InstructionData, LiteralOrRegister};
|
||||
|
||||
impl Instruction {
|
||||
@@ -291,6 +291,51 @@ impl core::fmt::Display for Instruction {
|
||||
|k| format!("COMPREHENSION_YIELD R({}) R({})", k, value_reg),
|
||||
),
|
||||
Instruction::ComprehensionEnd {} => String::from("COMPREHENSION_END"),
|
||||
|
||||
// Azure Policy consolidated instruction
|
||||
Instruction::PolicyCondition {
|
||||
dest,
|
||||
left,
|
||||
right,
|
||||
op,
|
||||
} => match op {
|
||||
PolicyOp::Not => format!("{} R({}) R({})", op.display_name(), dest, left),
|
||||
_ => format!("{} R({}) R({}) R({})", op.display_name(), dest, left, right),
|
||||
},
|
||||
|
||||
// AllOf / AnyOf structured instructions
|
||||
Instruction::LogicalBlockStart {
|
||||
mode,
|
||||
result,
|
||||
end_pc,
|
||||
} => {
|
||||
let name = match mode {
|
||||
LogicalBlockMode::AllOf => "ALL_OF_START",
|
||||
LogicalBlockMode::AnyOf => "ANY_OF_START",
|
||||
};
|
||||
format!("{} R({}) {}", name, result, end_pc)
|
||||
}
|
||||
Instruction::AllOfNext {
|
||||
check,
|
||||
result,
|
||||
end_pc,
|
||||
} => {
|
||||
format!("ALL_OF_NEXT R({}) R({}) {}", check, result, end_pc)
|
||||
}
|
||||
Instruction::AnyOfNext {
|
||||
check,
|
||||
result,
|
||||
end_pc,
|
||||
} => {
|
||||
format!("ANY_OF_NEXT R({}) R({}) {}", check, result, end_pc)
|
||||
}
|
||||
Instruction::LogicalBlockEnd { mode, result } => {
|
||||
let name = match mode {
|
||||
LogicalBlockMode::AllOf => "ALL_OF_END",
|
||||
LogicalBlockMode::AnyOf => "ANY_OF_END",
|
||||
};
|
||||
format!("{} R({})", name, result)
|
||||
}
|
||||
};
|
||||
write!(f, "{}", text)
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ pub use params::{
|
||||
FunctionCallParams, InstructionData, LoopStartParams, ObjectCreateParams, SetCreateParams,
|
||||
VirtualDataDocumentLookupParams,
|
||||
};
|
||||
pub use types::{ComprehensionMode, GuardMode, LiteralOrRegister, LoopMode};
|
||||
pub use types::{
|
||||
ComprehensionMode, GuardMode, LiteralOrRegister, LogicalBlockMode, LoopMode, PolicyOp,
|
||||
};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -355,6 +357,65 @@ pub enum Instruction {
|
||||
|
||||
/// End a comprehension block
|
||||
ComprehensionEnd {},
|
||||
|
||||
// ── Azure Policy condition operators (consolidated) ────────────────
|
||||
/// Consolidated Azure Policy condition instruction.
|
||||
///
|
||||
/// Replaces 21 separate Policy* variants. The `op` discriminant selects
|
||||
/// the specific Azure Policy condition semantics.
|
||||
///
|
||||
/// For most ops: `dest = op(left, right)`.
|
||||
/// For `PolicyOp::Not`: `dest = !is_true(left)`, `right` is unused (0).
|
||||
/// For `PolicyOp::ValueConditionGuard`: `left` = value register,
|
||||
/// `right` = condition register.
|
||||
PolicyCondition {
|
||||
dest: u8,
|
||||
left: u8,
|
||||
right: u8,
|
||||
op: PolicyOp,
|
||||
},
|
||||
|
||||
// ── AllOf / AnyOf structured short-circuit instructions ───────────
|
||||
/// Initialize allOf/anyOf: set result register to false.
|
||||
LogicalBlockStart {
|
||||
mode: LogicalBlockMode,
|
||||
/// Register that accumulates the result.
|
||||
result: u8,
|
||||
/// PC of the corresponding End instruction.
|
||||
end_pc: u16,
|
||||
},
|
||||
|
||||
/// Check one allOf child: if not true, short-circuit (result stays false),
|
||||
/// jump to end_pc.
|
||||
AllOfNext {
|
||||
/// Register holding the child condition result.
|
||||
check: u8,
|
||||
/// Register that accumulates the allOf result.
|
||||
result: u8,
|
||||
/// PC of the AllOfEnd instruction (jump target on short-circuit).
|
||||
end_pc: u16,
|
||||
},
|
||||
|
||||
/// Check one anyOf child: if true, short-circuit (set result to true),
|
||||
/// jump to end_pc.
|
||||
AnyOfNext {
|
||||
/// Register holding the child condition result.
|
||||
check: u8,
|
||||
/// Register that accumulates the anyOf result.
|
||||
result: u8,
|
||||
/// PC of the AnyOfEnd instruction.
|
||||
end_pc: u16,
|
||||
},
|
||||
|
||||
/// Finalize allOf/anyOf block.
|
||||
///
|
||||
/// For AllOf: all children passed → set result to true.
|
||||
/// For AnyOf: no child matched → result stays false (no-op).
|
||||
LogicalBlockEnd {
|
||||
mode: LogicalBlockMode,
|
||||
/// Register that accumulates the result.
|
||||
result: u8,
|
||||
},
|
||||
}
|
||||
|
||||
impl Instruction {
|
||||
|
||||
@@ -46,6 +46,110 @@ pub enum ComprehensionMode {
|
||||
Object,
|
||||
}
|
||||
|
||||
/// Azure Policy condition operator sub-opcodes.
|
||||
///
|
||||
/// Each variant maps to one of the ~21 Azure Policy condition operators.
|
||||
/// Stored inside `Instruction::PolicyCondition` to collapse 21 enum variants
|
||||
/// into a single instruction with a sub-op discriminant.
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum PolicyOp {
|
||||
Equals,
|
||||
NotEquals,
|
||||
Greater,
|
||||
GreaterOrEquals,
|
||||
Less,
|
||||
LessOrEquals,
|
||||
In,
|
||||
NotIn,
|
||||
Contains,
|
||||
NotContains,
|
||||
ContainsKey,
|
||||
NotContainsKey,
|
||||
Like,
|
||||
NotLike,
|
||||
Match,
|
||||
NotMatch,
|
||||
MatchInsensitively,
|
||||
NotMatchInsensitively,
|
||||
Exists,
|
||||
/// Guard for `value:` conditions — forces false when LHS is undefined.
|
||||
/// Uses `left` = value register, `right` = condition register.
|
||||
ValueConditionGuard,
|
||||
/// Logical negation: `!is_true(operand)`. Uses `left` = operand, `right` is unused (0).
|
||||
Not,
|
||||
}
|
||||
|
||||
impl PolicyOp {
|
||||
/// Display name used in assembly listings and Debug output.
|
||||
pub const fn display_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Equals => "POLICY_EQUALS",
|
||||
Self::NotEquals => "POLICY_NOT_EQUALS",
|
||||
Self::Greater => "POLICY_GREATER",
|
||||
Self::GreaterOrEquals => "POLICY_GREATER_OR_EQUALS",
|
||||
Self::Less => "POLICY_LESS",
|
||||
Self::LessOrEquals => "POLICY_LESS_OR_EQUALS",
|
||||
Self::In => "POLICY_IN",
|
||||
Self::NotIn => "POLICY_NOT_IN",
|
||||
Self::Contains => "POLICY_CONTAINS",
|
||||
Self::NotContains => "POLICY_NOT_CONTAINS",
|
||||
Self::ContainsKey => "POLICY_CONTAINS_KEY",
|
||||
Self::NotContainsKey => "POLICY_NOT_CONTAINS_KEY",
|
||||
Self::Like => "POLICY_LIKE",
|
||||
Self::NotLike => "POLICY_NOT_LIKE",
|
||||
Self::Match => "POLICY_MATCH",
|
||||
Self::NotMatch => "POLICY_NOT_MATCH",
|
||||
Self::MatchInsensitively => "POLICY_MATCH_INSENSITIVELY",
|
||||
Self::NotMatchInsensitively => "POLICY_NOT_MATCH_INSENSITIVELY",
|
||||
Self::Exists => "POLICY_EXISTS",
|
||||
Self::ValueConditionGuard => "VALUE_CONDITION_GUARD",
|
||||
Self::Not => "POLICY_NOT",
|
||||
}
|
||||
}
|
||||
|
||||
/// Compact name for tabular assembly listings.
|
||||
pub const fn compact_name(self) -> &'static str {
|
||||
match self {
|
||||
Self::Equals => "POLICY_EQ",
|
||||
Self::NotEquals => "POLICY_NE",
|
||||
Self::Greater => "POLICY_GT",
|
||||
Self::GreaterOrEquals => "POLICY_GE",
|
||||
Self::Less => "POLICY_LT",
|
||||
Self::LessOrEquals => "POLICY_LE",
|
||||
Self::In => "POLICY_IN",
|
||||
Self::NotIn => "POLICY_NOT_IN",
|
||||
Self::Contains => "POLICY_CONTAINS",
|
||||
Self::NotContains => "POLICY_NOT_CONTAINS",
|
||||
Self::ContainsKey => "POLICY_CONTAINS_KEY",
|
||||
Self::NotContainsKey => "POLICY_NOT_CONTAINS_KEY",
|
||||
Self::Like => "POLICY_LIKE",
|
||||
Self::NotLike => "POLICY_NOT_LIKE",
|
||||
Self::Match => "POLICY_MATCH",
|
||||
Self::NotMatch => "POLICY_NOT_MATCH",
|
||||
Self::MatchInsensitively => "POLICY_MATCH_CI",
|
||||
Self::NotMatchInsensitively => "POLICY_NOT_MATCH_CI",
|
||||
Self::Exists => "POLICY_EXISTS",
|
||||
Self::ValueConditionGuard => "VAL_COND_GUARD",
|
||||
Self::Not => "POLICY_NOT",
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns `true` for negated condition operators (NotEquals, NotIn, etc.).
|
||||
pub const fn is_negated(self) -> bool {
|
||||
matches!(
|
||||
self,
|
||||
Self::NotEquals
|
||||
| Self::NotIn
|
||||
| Self::NotContains
|
||||
| Self::NotContainsKey
|
||||
| Self::NotLike
|
||||
| Self::NotMatch
|
||||
| Self::NotMatchInsensitively
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Guard sub-modes for the consolidated `Guard` instruction.
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
@@ -57,3 +161,11 @@ pub enum GuardMode {
|
||||
/// Assert not undefined — fail (return undefined) if register is undefined.
|
||||
NotUndefined,
|
||||
}
|
||||
|
||||
/// Mode discriminant for merged AllOf/AnyOf Start and End instructions.
|
||||
#[repr(u8)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
pub enum LogicalBlockMode {
|
||||
AllOf,
|
||||
AnyOf,
|
||||
}
|
||||
|
||||
@@ -911,6 +911,15 @@ fn format_instruction_readable(
|
||||
let base = format!("{}}} CompEnd", indent);
|
||||
align_comment(&base, "End comprehension block", config.comment_column)
|
||||
}
|
||||
|
||||
// Azure Policy & allOf/anyOf instructions — use Display impl
|
||||
instruction @ Instruction::PolicyCondition { .. }
|
||||
| instruction @ Instruction::LogicalBlockStart { .. }
|
||||
| instruction @ Instruction::AllOfNext { .. }
|
||||
| instruction @ Instruction::AnyOfNext { .. }
|
||||
| instruction @ Instruction::LogicalBlockEnd { .. } => {
|
||||
format!("{}{}", indent, instruction)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1045,6 +1054,19 @@ const fn get_instruction_name(instruction: &Instruction) -> &'static str {
|
||||
Instruction::ComprehensionBegin { .. } => "COMP_BEGIN",
|
||||
Instruction::ComprehensionYield { .. } => "COMP_YIELD",
|
||||
Instruction::ComprehensionEnd {} => "COMP_END",
|
||||
// Azure Policy instructions
|
||||
Instruction::PolicyCondition { op, .. } => op.compact_name(),
|
||||
// AllOf / AnyOf
|
||||
Instruction::LogicalBlockStart { mode, .. } => match mode {
|
||||
crate::rvm::instructions::LogicalBlockMode::AllOf => "ALL_OF_START",
|
||||
crate::rvm::instructions::LogicalBlockMode::AnyOf => "ANY_OF_START",
|
||||
},
|
||||
Instruction::AllOfNext { .. } => "ALL_OF_NEXT",
|
||||
Instruction::AnyOfNext { .. } => "ANY_OF_NEXT",
|
||||
Instruction::LogicalBlockEnd { mode, .. } => match mode {
|
||||
crate::rvm::instructions::LogicalBlockMode::AllOf => "ALL_OF_END",
|
||||
crate::rvm::instructions::LogicalBlockMode::AnyOf => "ANY_OF_END",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
clippy::pattern_type_mismatch
|
||||
)] // tests unwrap conversions and slice math for brevity
|
||||
|
||||
use crate::rvm::instructions::{GuardMode, Instruction, LoopMode};
|
||||
use crate::rvm::instructions::{GuardMode, Instruction, LogicalBlockMode, LoopMode, PolicyOp};
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
@@ -83,6 +83,41 @@ pub fn parse_instruction(text: &str) -> Result<Instruction> {
|
||||
"ComprehensionYield" => parse_comprehension_add(params_text),
|
||||
"ReturnUndefinedIfNotTrue" => parse_return_undefined_if_not_true(params_text),
|
||||
"CoalesceUndefinedToNull" => parse_coalesce_undefined_to_null(params_text),
|
||||
// Azure Policy condition operators
|
||||
"PolicyEquals" => parse_policy_condition(params_text, PolicyOp::Equals),
|
||||
"PolicyNotEquals" => parse_policy_condition(params_text, PolicyOp::NotEquals),
|
||||
"PolicyGreater" => parse_policy_condition(params_text, PolicyOp::Greater),
|
||||
"PolicyGreaterOrEquals" => {
|
||||
parse_policy_condition(params_text, PolicyOp::GreaterOrEquals)
|
||||
}
|
||||
"PolicyLess" => parse_policy_condition(params_text, PolicyOp::Less),
|
||||
"PolicyLessOrEquals" => parse_policy_condition(params_text, PolicyOp::LessOrEquals),
|
||||
"PolicyIn" => parse_policy_condition(params_text, PolicyOp::In),
|
||||
"PolicyNotIn" => parse_policy_condition(params_text, PolicyOp::NotIn),
|
||||
"PolicyContains" => parse_policy_condition(params_text, PolicyOp::Contains),
|
||||
"PolicyNotContains" => parse_policy_condition(params_text, PolicyOp::NotContains),
|
||||
"PolicyContainsKey" => parse_policy_condition(params_text, PolicyOp::ContainsKey),
|
||||
"PolicyNotContainsKey" => parse_policy_condition(params_text, PolicyOp::NotContainsKey),
|
||||
"PolicyLike" => parse_policy_condition(params_text, PolicyOp::Like),
|
||||
"PolicyNotLike" => parse_policy_condition(params_text, PolicyOp::NotLike),
|
||||
"PolicyMatch" => parse_policy_condition(params_text, PolicyOp::Match),
|
||||
"PolicyNotMatch" => parse_policy_condition(params_text, PolicyOp::NotMatch),
|
||||
"PolicyMatchInsensitively" => {
|
||||
parse_policy_condition(params_text, PolicyOp::MatchInsensitively)
|
||||
}
|
||||
"PolicyNotMatchInsensitively" => {
|
||||
parse_policy_condition(params_text, PolicyOp::NotMatchInsensitively)
|
||||
}
|
||||
"PolicyExists" => parse_policy_condition(params_text, PolicyOp::Exists),
|
||||
"ValueConditionGuard" => parse_value_condition_guard(params_text),
|
||||
"PolicyNot" => parse_policy_not(params_text),
|
||||
// AllOf / AnyOf
|
||||
"AllOfStart" => parse_logical_block_start(params_text, LogicalBlockMode::AllOf),
|
||||
"AllOfNext" => parse_allof_next(params_text),
|
||||
"AllOfEnd" => parse_logical_block_end(params_text, LogicalBlockMode::AllOf),
|
||||
"AnyOfStart" => parse_logical_block_start(params_text, LogicalBlockMode::AnyOf),
|
||||
"AnyOfNext" => parse_anyof_next(params_text),
|
||||
"AnyOfEnd" => parse_logical_block_end(params_text, LogicalBlockMode::AnyOf),
|
||||
_ => bail!("Unknown instruction: {}", name),
|
||||
}
|
||||
} else {
|
||||
@@ -707,3 +742,83 @@ fn parse_coalesce_undefined_to_null(params_text: &str) -> Result<Instruction> {
|
||||
register: register.try_into().unwrap(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Generic parser for PolicyCondition instructions with { dest, left, right } fields.
|
||||
fn parse_policy_condition(params_text: &str, op: PolicyOp) -> Result<Instruction> {
|
||||
let params = parse_params(params_text)?;
|
||||
let dest: u8 = get_param_u16(¶ms, "dest")?.try_into().unwrap();
|
||||
let left: u8 = get_param_u16(¶ms, "left")?.try_into().unwrap();
|
||||
let right: u8 = get_param_u16(¶ms, "right")?.try_into().unwrap();
|
||||
Ok(Instruction::PolicyCondition {
|
||||
dest,
|
||||
left,
|
||||
right,
|
||||
op,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_value_condition_guard(params_text: &str) -> Result<Instruction> {
|
||||
let params = parse_params(params_text)?;
|
||||
let dest: u8 = get_param_u16(¶ms, "dest")?.try_into().unwrap();
|
||||
let value: u8 = get_param_u16(¶ms, "value")?.try_into().unwrap();
|
||||
let condition: u8 = get_param_u16(¶ms, "condition")?.try_into().unwrap();
|
||||
Ok(Instruction::PolicyCondition {
|
||||
dest,
|
||||
left: value,
|
||||
right: condition,
|
||||
op: PolicyOp::ValueConditionGuard,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_policy_not(params_text: &str) -> Result<Instruction> {
|
||||
let params = parse_params(params_text)?;
|
||||
let dest: u8 = get_param_u16(¶ms, "dest")?.try_into().unwrap();
|
||||
let operand: u8 = get_param_u16(¶ms, "operand")?.try_into().unwrap();
|
||||
Ok(Instruction::PolicyCondition {
|
||||
dest,
|
||||
left: operand,
|
||||
right: 0,
|
||||
op: PolicyOp::Not,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_logical_block_start(params_text: &str, mode: LogicalBlockMode) -> Result<Instruction> {
|
||||
let params = parse_params(params_text)?;
|
||||
let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap();
|
||||
let end_pc = get_param_u16(¶ms, "end_pc")?;
|
||||
Ok(Instruction::LogicalBlockStart {
|
||||
mode,
|
||||
result,
|
||||
end_pc,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_allof_next(params_text: &str) -> Result<Instruction> {
|
||||
let params = parse_params(params_text)?;
|
||||
let check: u8 = get_param_u16(¶ms, "check")?.try_into().unwrap();
|
||||
let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap();
|
||||
let end_pc = get_param_u16(¶ms, "end_pc")?;
|
||||
Ok(Instruction::AllOfNext {
|
||||
check,
|
||||
result,
|
||||
end_pc,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_logical_block_end(params_text: &str, mode: LogicalBlockMode) -> Result<Instruction> {
|
||||
let params = parse_params(params_text)?;
|
||||
let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap();
|
||||
Ok(Instruction::LogicalBlockEnd { mode, result })
|
||||
}
|
||||
|
||||
fn parse_anyof_next(params_text: &str) -> Result<Instruction> {
|
||||
let params = parse_params(params_text)?;
|
||||
let check: u8 = get_param_u16(¶ms, "check")?.try_into().unwrap();
|
||||
let result: u8 = get_param_u16(¶ms, "result")?.try_into().unwrap();
|
||||
let end_pc = get_param_u16(¶ms, "end_pc")?;
|
||||
Ok(Instruction::AnyOfNext {
|
||||
check,
|
||||
result,
|
||||
end_pc,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1059,4 +1059,10 @@ mod tests {
|
||||
fn run_loop_test_file(file: &str) {
|
||||
run_vm_test_suite(file).unwrap()
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
#[test_resources("tests/rvm/vm/suites/azure_policy/*.yaml")]
|
||||
fn run_azure_policy_test_file(file: &str) {
|
||||
run_vm_test_suite(file).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -807,6 +807,297 @@ impl RegoVM {
|
||||
let result = self.get_register(0)?.clone();
|
||||
Ok(InstructionOutcome::Return(result))
|
||||
}
|
||||
other => self.execute_policy_instruction(program, other),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "azure_policy"))]
|
||||
fn execute_policy_instruction(
|
||||
&mut self,
|
||||
program: &Program,
|
||||
instruction: Instruction,
|
||||
) -> Result<InstructionOutcome> {
|
||||
match instruction {
|
||||
instruction @ (Instruction::PolicyCondition { .. }
|
||||
| Instruction::LogicalBlockStart { .. }
|
||||
| Instruction::LogicalBlockEnd { .. }
|
||||
| Instruction::AllOfNext { .. }
|
||||
| Instruction::AnyOfNext { .. }) => Err(VmError::UnhandledInstruction {
|
||||
instruction: alloc::format!("{:?} requires the azure_policy feature", instruction),
|
||||
pc: self.pc,
|
||||
}),
|
||||
other => self.execute_virtual_instruction(program, other),
|
||||
}
|
||||
}
|
||||
|
||||
/// Check whether `l` "contains" `r` using Azure Policy semantics.
|
||||
///
|
||||
/// Works on strings (case-insensitive substring), arrays/sets (element
|
||||
/// membership), and objects (key membership). For string haystacks,
|
||||
/// non-string scalar RHS values are coerced to strings before the
|
||||
/// substring check. For non-string scalar LHS values, coercion to string
|
||||
/// only happens when the RHS is already a string.
|
||||
#[cfg(feature = "azure_policy")]
|
||||
#[inline]
|
||||
fn policy_contains_check(l: &Value, r: &Value) -> bool {
|
||||
use crate::builtins::azure_policy::helpers::{case_insensitive_equals, coerce_to_string};
|
||||
use crate::languages::azure_policy::strings;
|
||||
|
||||
match *l {
|
||||
Value::String(ref haystack) => match *r {
|
||||
Value::String(ref needle) => strings::case_fold::contains(haystack, needle),
|
||||
_ => coerce_to_string(r)
|
||||
.is_some_and(|needle| strings::case_fold::contains(haystack, &needle)),
|
||||
},
|
||||
Value::Array(ref items) => items.iter().any(|item| case_insensitive_equals(item, r)),
|
||||
Value::Set(ref items) => items.iter().any(|item| case_insensitive_equals(item, r)),
|
||||
// ARM template contains(object, key) checks key membership.
|
||||
Value::Object(ref map) => map.keys().any(|key| case_insensitive_equals(key, r)),
|
||||
// Coerce non-string scalar LHS (e.g., count result)
|
||||
// to a string only when the RHS is already a string.
|
||||
_ => {
|
||||
if let Value::String(ref needle) = *r {
|
||||
coerce_to_string(l)
|
||||
.is_some_and(|haystack| strings::case_fold::contains(&haystack, needle))
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Evaluate a Policy comparison operator. Undefined LHS → false.
|
||||
#[cfg(feature = "azure_policy")]
|
||||
fn policy_compare(
|
||||
&mut self,
|
||||
dest: u8,
|
||||
left: u8,
|
||||
right: u8,
|
||||
cmp: fn(i8) -> bool,
|
||||
) -> Result<InstructionOutcome> {
|
||||
use crate::builtins::azure_policy::helpers::{compare_values, is_undefined};
|
||||
|
||||
let l = self.get_register(left)?;
|
||||
if is_undefined(l) {
|
||||
self.set_register(dest, Value::Bool(false))?;
|
||||
} else {
|
||||
let r = self.get_register(right)?;
|
||||
let result = compare_values(l, r).is_some_and(cmp);
|
||||
self.set_register(dest, Value::Bool(result))?;
|
||||
}
|
||||
Ok(InstructionOutcome::Continue)
|
||||
}
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
fn execute_policy_instruction(
|
||||
&mut self,
|
||||
program: &Program,
|
||||
instruction: Instruction,
|
||||
) -> Result<InstructionOutcome> {
|
||||
use crate::builtins::azure_policy::helpers::{
|
||||
as_boolish, case_insensitive_equals, coerce_to_string_ci,
|
||||
collection_any_ci_eq_excluding_null, collection_has_null, is_true, is_undefined,
|
||||
match_like_pattern_ci, match_pattern,
|
||||
};
|
||||
use crate::rvm::instructions::{LogicalBlockMode, PolicyOp};
|
||||
|
||||
use Instruction::*;
|
||||
match instruction {
|
||||
PolicyCondition {
|
||||
dest,
|
||||
left,
|
||||
right,
|
||||
op,
|
||||
} => {
|
||||
let l = self.get_register(left)?;
|
||||
let result = match op {
|
||||
PolicyOp::Equals => {
|
||||
let r = self.get_register(right)?;
|
||||
if is_undefined(l) {
|
||||
matches!(r, Value::Null)
|
||||
} else {
|
||||
case_insensitive_equals(l, r)
|
||||
}
|
||||
}
|
||||
PolicyOp::NotEquals => {
|
||||
let r = self.get_register(right)?;
|
||||
if is_undefined(l) {
|
||||
!matches!(r, Value::Null)
|
||||
} else {
|
||||
!case_insensitive_equals(l, r)
|
||||
}
|
||||
}
|
||||
PolicyOp::Greater => {
|
||||
return self.policy_compare(dest, left, right, |c| c > 0);
|
||||
}
|
||||
PolicyOp::GreaterOrEquals => {
|
||||
return self.policy_compare(dest, left, right, |c| c >= 0);
|
||||
}
|
||||
PolicyOp::Less => {
|
||||
return self.policy_compare(dest, left, right, |c| c < 0);
|
||||
}
|
||||
PolicyOp::LessOrEquals => {
|
||||
return self.policy_compare(dest, left, right, |c| c <= 0);
|
||||
}
|
||||
PolicyOp::In => {
|
||||
let r = self.get_register(right)?;
|
||||
if is_undefined(l) {
|
||||
collection_has_null(r)
|
||||
} else if matches!(*l, Value::Null) || is_undefined(r) {
|
||||
false
|
||||
} else {
|
||||
collection_any_ci_eq_excluding_null(r, l)
|
||||
}
|
||||
}
|
||||
PolicyOp::NotIn => {
|
||||
let r = self.get_register(right)?;
|
||||
if is_undefined(l) {
|
||||
!collection_has_null(r)
|
||||
} else if matches!(*l, Value::Null) || is_undefined(r) {
|
||||
true
|
||||
} else {
|
||||
!collection_any_ci_eq_excluding_null(r, l)
|
||||
}
|
||||
}
|
||||
PolicyOp::Contains | PolicyOp::NotContains => {
|
||||
let negated = op.is_negated();
|
||||
if is_undefined(l) {
|
||||
negated
|
||||
} else {
|
||||
let r = self.get_register(right)?;
|
||||
if is_undefined(r) {
|
||||
// undefined RHS: positive → false, negated → false
|
||||
false
|
||||
} else {
|
||||
negated ^ Self::policy_contains_check(l, r)
|
||||
}
|
||||
}
|
||||
}
|
||||
PolicyOp::ContainsKey | PolicyOp::NotContainsKey => {
|
||||
let negated = op.is_negated();
|
||||
if is_undefined(l) {
|
||||
negated
|
||||
} else {
|
||||
let r = self.get_register(right)?;
|
||||
if is_undefined(r) {
|
||||
false
|
||||
} else {
|
||||
let found = match *l {
|
||||
Value::Object(ref map) => {
|
||||
map.keys().any(|key| case_insensitive_equals(key, r))
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
negated ^ found
|
||||
}
|
||||
}
|
||||
}
|
||||
PolicyOp::Like | PolicyOp::NotLike => {
|
||||
let negated = op.is_negated();
|
||||
if is_undefined(l) {
|
||||
negated
|
||||
} else {
|
||||
let r = self.get_register(right)?;
|
||||
let positive = match (coerce_to_string_ci(l), coerce_to_string_ci(r)) {
|
||||
(Some(input), Some(pattern)) => {
|
||||
match_like_pattern_ci(&input, &pattern)
|
||||
}
|
||||
_ => false,
|
||||
};
|
||||
negated ^ positive
|
||||
}
|
||||
}
|
||||
PolicyOp::Match
|
||||
| PolicyOp::NotMatch
|
||||
| PolicyOp::MatchInsensitively
|
||||
| PolicyOp::NotMatchInsensitively => {
|
||||
let negated = op.is_negated();
|
||||
let case_insensitive = matches!(
|
||||
op,
|
||||
PolicyOp::MatchInsensitively | PolicyOp::NotMatchInsensitively
|
||||
);
|
||||
if is_undefined(l) {
|
||||
negated
|
||||
} else {
|
||||
let r = self.get_register(right)?;
|
||||
negated ^ match_pattern(l, r, case_insensitive)
|
||||
}
|
||||
}
|
||||
PolicyOp::Exists => {
|
||||
let r = self.get_register(right)?;
|
||||
let expected = as_boolish(r).unwrap_or(false);
|
||||
let is_defined = !is_undefined(l) && !matches!(l, Value::Null);
|
||||
is_defined == expected
|
||||
}
|
||||
PolicyOp::ValueConditionGuard => {
|
||||
// left = value register, right = condition register
|
||||
if is_undefined(l) {
|
||||
self.set_register(dest, Value::Bool(false))?;
|
||||
return Ok(InstructionOutcome::Continue);
|
||||
} else {
|
||||
let c = self.get_register(right)?.clone();
|
||||
self.set_register(dest, c)?;
|
||||
return Ok(InstructionOutcome::Continue);
|
||||
}
|
||||
}
|
||||
PolicyOp::Not => {
|
||||
// left = operand, right unused
|
||||
!is_true(l)
|
||||
}
|
||||
};
|
||||
self.set_register(dest, Value::Bool(result))?;
|
||||
Ok(InstructionOutcome::Continue)
|
||||
}
|
||||
|
||||
// AllOf / AnyOf structured instructions
|
||||
LogicalBlockStart {
|
||||
mode: _,
|
||||
result,
|
||||
end_pc: _,
|
||||
} => {
|
||||
// Initialize result to false (pessimistic).
|
||||
self.set_register(result, Value::Bool(false))?;
|
||||
Ok(InstructionOutcome::Continue)
|
||||
}
|
||||
AllOfNext {
|
||||
check,
|
||||
result,
|
||||
end_pc,
|
||||
} => {
|
||||
let val = self.get_register(check)?;
|
||||
if !matches!(val, Value::Bool(true)) {
|
||||
// Child failed — short-circuit. Ensure the block result is false.
|
||||
self.set_register(result, Value::Bool(false))?;
|
||||
self.pc = usize::from(end_pc);
|
||||
}
|
||||
Ok(InstructionOutcome::Continue)
|
||||
}
|
||||
AnyOfNext {
|
||||
check,
|
||||
result,
|
||||
end_pc,
|
||||
} => {
|
||||
let val = self.get_register(check)?;
|
||||
if matches!(val, Value::Bool(true)) {
|
||||
// Child succeeded — short-circuit.
|
||||
self.set_register(result, Value::Bool(true))?;
|
||||
self.pc = usize::from(end_pc);
|
||||
}
|
||||
Ok(InstructionOutcome::Continue)
|
||||
}
|
||||
LogicalBlockEnd { mode, result } => {
|
||||
match mode {
|
||||
LogicalBlockMode::AllOf => {
|
||||
// All children passed — set result to true.
|
||||
self.set_register(result, Value::Bool(true))?;
|
||||
}
|
||||
LogicalBlockMode::AnyOf => {
|
||||
// No child matched — result stays false (set by LogicalBlockStart).
|
||||
}
|
||||
}
|
||||
Ok(InstructionOutcome::Continue)
|
||||
}
|
||||
|
||||
other => self.execute_virtual_instruction(program, other),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user