mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
feat(azure_policy): test runner, compiler fixes, and example program (#700)
Adds the YAML test runner that exercises the companion test data PRs, plus several compiler fixes surfaced during testing: - Removed parameter register caching that produced wrong results inside short-circuiting allOf/anyOf blocks; added literal-index caching for parameter defaults to avoid repeated O(n) literal-table scans - Simplified cross-resource effect details to only emit roleDefinitionIds and type (deployment templates are not evaluated for compliance) - Replaced guid/uniqueString builtins with clear "unsupported" errors - Normalized datetime output to ISO 8601 with Z suffix - Added azure_policy parser MAX_COL constant (8192) for long template expressions, keeping the global DEFAULT_MAX_COL at 1024 - Added rvm to azure_policy feature dependencies since the compiler targets RVM bytecode Also restructures the example binary into examples/regorus/ with new azure-policy-eval and azure-policy-aliases subcommands, adds C# alias normalization tests, and documents Azure Policy support in the README. Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com> Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
7f42115b63
commit
4c92fb4d92
156
examples/regorus/azure_policy.rs
Normal file
156
examples/regorus/azure_policy.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
// Copyright (c) Microsoft Corporation.
|
||||
// Licensed under the MIT License.
|
||||
|
||||
//! Azure Policy evaluation subcommand for the regorus example binary.
|
||||
//!
|
||||
//! Demonstrates parsing an Azure Policy definition JSON, compiling it to
|
||||
//! RVM bytecode, normalizing an ARM resource through the alias registry,
|
||||
//! and evaluating the compiled policy against the normalized input.
|
||||
//!
|
||||
//! Usage:
|
||||
//! cargo run --example regorus --features azure_policy -- \
|
||||
//! azure-policy-eval \
|
||||
//! --policy-definition policy.json \
|
||||
//! --resource resource.json \
|
||||
//! --aliases aliases.json \
|
||||
//! [--parameters '{"sku": "Standard_D2s_v3"}'] \
|
||||
//! [--api-version 2023-01-01]
|
||||
|
||||
use anyhow::{bail, Result};
|
||||
|
||||
use regorus::languages::azure_policy::aliases::normalizer;
|
||||
use regorus::languages::azure_policy::aliases::AliasRegistry;
|
||||
use regorus::languages::azure_policy::compiler;
|
||||
use regorus::languages::azure_policy::parser;
|
||||
use regorus::rvm::RegoVM;
|
||||
use regorus::Source;
|
||||
use regorus::Value;
|
||||
|
||||
/// Evaluate an Azure Policy definition against a resource.
|
||||
///
|
||||
/// This mirrors the pipeline used in production:
|
||||
/// 1. Load aliases and build the alias registry
|
||||
/// 2. Parse the policy definition JSON
|
||||
/// 3. Compile to RVM bytecode (with alias-aware field resolution)
|
||||
/// 4. Normalize the ARM resource through the alias registry
|
||||
/// 5. Run the compiled program in the Rego VM
|
||||
pub fn azure_policy_eval(
|
||||
policy_definition: String,
|
||||
resource: String,
|
||||
aliases: String,
|
||||
parameters_json: Option<String>,
|
||||
api_version: Option<String>,
|
||||
) -> Result<()> {
|
||||
// 1. Load alias registry.
|
||||
let aliases_json = std::fs::read_to_string(&aliases)
|
||||
.map_err(|e| anyhow::anyhow!("failed to read aliases file {aliases}: {e}"))?;
|
||||
let mut registry = AliasRegistry::new();
|
||||
registry.load_from_json(&aliases_json)?;
|
||||
println!(
|
||||
"Loaded {} resource type(s) from alias registry",
|
||||
registry.len()
|
||||
);
|
||||
|
||||
// 2. Parse the policy definition.
|
||||
let defn_json = std::fs::read_to_string(&policy_definition)
|
||||
.map_err(|e| anyhow::anyhow!("failed to read policy file {policy_definition}: {e}"))?;
|
||||
let source = Source::from_contents(policy_definition.clone(), defn_json)?;
|
||||
let defn = parser::parse_policy_definition(&source)
|
||||
.map_err(|e| anyhow::anyhow!("parse error: {e}"))?;
|
||||
println!("Parsed policy definition from {policy_definition}");
|
||||
|
||||
// 3. Compile to RVM bytecode.
|
||||
let program = compiler::compile_policy_definition_with_aliases(
|
||||
&defn,
|
||||
registry.alias_map(),
|
||||
registry.alias_modifiable_map(),
|
||||
)?;
|
||||
println!("Compiled policy to RVM bytecode");
|
||||
|
||||
// 4. Build normalized input.
|
||||
let resource_json = std::fs::read_to_string(&resource)
|
||||
.map_err(|e| anyhow::anyhow!("failed to read resource file {resource}: {e}"))?;
|
||||
let raw_resource = Value::from_json_str(&resource_json)?;
|
||||
let normalized = normalizer::normalize(&raw_resource, Some(®istry), api_version.as_deref());
|
||||
println!("Normalized resource ({} top-level fields)", {
|
||||
normalized.as_object().map(|m| m.len()).unwrap_or(0)
|
||||
});
|
||||
|
||||
// Inject api_version into the normalized resource (lowercased key to match
|
||||
// the host contract — policies reference `field('apiVersion')` which the
|
||||
// compiler lowercases to `apiversion`).
|
||||
let mut resource = normalized;
|
||||
if let Some(ref api_ver) = api_version {
|
||||
let map = resource.as_object_mut()?;
|
||||
map.insert(Value::from("apiversion"), Value::from(api_ver.clone()));
|
||||
}
|
||||
|
||||
// Build the input envelope: { resource, parameters }
|
||||
let parameters = if let Some(ref params) = parameters_json {
|
||||
Value::from_json_str(params)?
|
||||
} else {
|
||||
Value::new_object()
|
||||
};
|
||||
let mut input = Value::new_object();
|
||||
{
|
||||
let map = input.as_object_mut()?;
|
||||
map.insert(Value::from("resource"), resource);
|
||||
map.insert(Value::from("parameters"), parameters);
|
||||
}
|
||||
|
||||
// Build a default context with requestContext if api_version is provided.
|
||||
let mut context = Value::from_json_str(
|
||||
r#"{
|
||||
"resourceGroup": { "name": "exampleRG", "location": "eastus" },
|
||||
"subscription": { "subscriptionId": "00000000-0000-0000-0000-000000000000" }
|
||||
}"#,
|
||||
)?;
|
||||
if let Some(ref api_ver) = api_version {
|
||||
let mut req_ctx = Value::new_object();
|
||||
let rc_map = req_ctx.as_object_mut()?;
|
||||
rc_map.insert(Value::from("apiVersion"), Value::from(api_ver.clone()));
|
||||
let ctx_map = context.as_object_mut()?;
|
||||
ctx_map.insert(Value::from("requestContext"), req_ctx);
|
||||
}
|
||||
|
||||
// 5. Execute in the Rego VM.
|
||||
let mut vm = RegoVM::new();
|
||||
vm.load_program(program);
|
||||
vm.set_input(input);
|
||||
vm.set_context(context);
|
||||
|
||||
let result = vm.execute_entry_point_by_name("main")?;
|
||||
println!("\nPolicy evaluation result:");
|
||||
println!("{}", serde_json::to_string_pretty(&result)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// List available aliases for a resource type.
|
||||
pub fn azure_policy_aliases(aliases: String, resource_type: Option<String>) -> Result<()> {
|
||||
let aliases_json = std::fs::read_to_string(&aliases)
|
||||
.map_err(|e| anyhow::anyhow!("failed to read aliases file {aliases}: {e}"))?;
|
||||
let mut registry = AliasRegistry::new();
|
||||
registry.load_from_json(&aliases_json)?;
|
||||
|
||||
println!("Alias registry: {} resource type(s)", registry.len());
|
||||
|
||||
if let Some(ref rt) = resource_type {
|
||||
let rt_lower = rt.to_lowercase();
|
||||
let mut found = false;
|
||||
for (alias_name, _) in registry.alias_map() {
|
||||
if alias_name.to_lowercase().starts_with(&rt_lower) {
|
||||
println!(" {alias_name}");
|
||||
found = true;
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
bail!("no aliases found for resource type '{rt}'");
|
||||
}
|
||||
} else {
|
||||
for (alias_name, _) in registry.alias_map() {
|
||||
println!(" {alias_name}");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
15
examples/regorus/azure_policy_data/compliant_storage.json
Normal file
15
examples/regorus/azure_policy_data/compliant_storage.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"name": "securestorageaccount",
|
||||
"location": "eastus",
|
||||
"kind": "StorageV2",
|
||||
"properties": {
|
||||
"supportsHttpsTrafficOnly": true,
|
||||
"minimumTlsVersion": "TLS1_2",
|
||||
"encryption": {
|
||||
"services": {
|
||||
"blob": { "enabled": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"type": "Microsoft.Storage/storageAccounts",
|
||||
"name": "mystorageaccount",
|
||||
"location": "eastus",
|
||||
"kind": "StorageV2",
|
||||
"properties": {
|
||||
"supportsHttpsTrafficOnly": false,
|
||||
"minimumTlsVersion": "TLS1_0",
|
||||
"encryption": {
|
||||
"services": {
|
||||
"blob": { "enabled": true }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"properties": {
|
||||
"displayName": "Require HTTPS for Storage Accounts",
|
||||
"description": "Denies storage accounts that do not have HTTPS traffic only enabled.",
|
||||
"policyType": "Custom",
|
||||
"mode": "All",
|
||||
"parameters": {
|
||||
"effect": {
|
||||
"type": "String",
|
||||
"metadata": {
|
||||
"displayName": "Effect",
|
||||
"description": "Enable or disable the execution of the policy"
|
||||
},
|
||||
"allowedValues": ["Deny", "Audit", "Disabled"],
|
||||
"defaultValue": "Deny"
|
||||
}
|
||||
},
|
||||
"policyRule": {
|
||||
"if": {
|
||||
"allOf": [
|
||||
{
|
||||
"field": "type",
|
||||
"equals": "Microsoft.Storage/storageAccounts"
|
||||
},
|
||||
{
|
||||
"field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly",
|
||||
"notEquals": true
|
||||
}
|
||||
]
|
||||
},
|
||||
"then": {
|
||||
"effect": "[parameters('effect')]"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,9 @@
|
||||
|
||||
use anyhow::{anyhow, bail, Result};
|
||||
|
||||
#[cfg(feature = "azure_policy")]
|
||||
mod azure_policy;
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn read_file(path: &String) -> Result<String> {
|
||||
std::fs::read_to_string(path).map_err(|_| anyhow!("could not read {path}"))
|
||||
@@ -267,6 +270,42 @@ enum RegorusCommand {
|
||||
#[arg(long)]
|
||||
v0: bool,
|
||||
},
|
||||
|
||||
/// Evaluate an Azure Policy definition against a resource.
|
||||
#[cfg(feature = "azure_policy")]
|
||||
AzurePolicyEval {
|
||||
/// Azure Policy definition JSON file.
|
||||
#[arg(long)]
|
||||
policy_definition: String,
|
||||
|
||||
/// ARM resource JSON file to evaluate.
|
||||
#[arg(long)]
|
||||
resource: String,
|
||||
|
||||
/// Aliases JSON file (provider aliases).
|
||||
#[arg(long)]
|
||||
aliases: String,
|
||||
|
||||
/// Policy parameters as a JSON string.
|
||||
#[arg(long)]
|
||||
parameters: Option<String>,
|
||||
|
||||
/// API version for alias path selection.
|
||||
#[arg(long)]
|
||||
api_version: Option<String>,
|
||||
},
|
||||
|
||||
/// List aliases from an alias registry file.
|
||||
#[cfg(feature = "azure_policy")]
|
||||
AzurePolicyAliases {
|
||||
/// Aliases JSON file (provider aliases).
|
||||
#[arg(long)]
|
||||
aliases: String,
|
||||
|
||||
/// Filter aliases by resource type prefix.
|
||||
#[arg(long)]
|
||||
resource_type: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(clap::Parser)]
|
||||
@@ -306,5 +345,24 @@ fn main() -> Result<()> {
|
||||
RegorusCommand::Lex { file, verbose } => rego_lex(file, verbose),
|
||||
RegorusCommand::Parse { file, v0 } => rego_parse(file, v0),
|
||||
RegorusCommand::Ast { file } => rego_ast(file),
|
||||
#[cfg(feature = "azure_policy")]
|
||||
RegorusCommand::AzurePolicyEval {
|
||||
policy_definition,
|
||||
resource,
|
||||
aliases,
|
||||
parameters,
|
||||
api_version,
|
||||
} => azure_policy::azure_policy_eval(
|
||||
policy_definition,
|
||||
resource,
|
||||
aliases,
|
||||
parameters,
|
||||
api_version,
|
||||
),
|
||||
#[cfg(feature = "azure_policy")]
|
||||
RegorusCommand::AzurePolicyAliases {
|
||||
aliases,
|
||||
resource_type,
|
||||
} => azure_policy::azure_policy_aliases(aliases, resource_type),
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user