Skip to content

Commit 4f82f47

Browse files
anakrishCopilot
andcommitted
feat(azure_policy): test runner, compiler fixes, and example program
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 - Simplified cross-resource effect details to only emit roleDefinitionIds and type (deployment templates aren't evaluated for compliance) - Replaced guid/uniqueString builtins with clear "unsupported" errors - Normalized datetime output to ISO 8601 with Z suffix - Bumped DEFAULT_MAX_COL to 8192 for long template expressions 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. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Signed-off-by: Anand Krishnamoorthi <anakrish@microsoft.com>
1 parent afdb894 commit 4f82f47

41 files changed

Lines changed: 3341 additions & 3131 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

README.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,52 @@ $ diff <(regorus eval -b tests/aci -d tests/aci/data.json -i tests/aci/input.jso
248248
```
249249

250250

251+
## Azure Policy (Preview)
252+
253+
Regorus can evaluate [Azure Policy](https://learn.microsoft.com/en-us/azure/governance/policy/overview)
254+
definitions natively. A dedicated compiler translates Azure Policy JSON
255+
directly into RVM (Regorus Virtual Machine) bytecode — the same VM that
256+
powers Rego evaluation — so you don't have to rewrite policies in Rego.
257+
Enable it with the `azure_policy` cargo feature.
258+
259+
Most of the policy language is supported: conditions with `field`, `count`,
260+
and `value`; logical connectives (`allOf`, `anyOf`, `not`); comparison
261+
operators; template expressions like `parameters()`, `concat()`,
262+
`dateTimeAdd()`, and `utcNow()`; and effects including Deny, Audit, Modify,
263+
Append, AuditIfNotExists, and DeployIfNotExists. An alias registry handles
264+
the translation from fully-qualified alias names to the flattened ARM resource
265+
shape expected by the engine.
266+
267+
### Quick start
268+
269+
```bash
270+
cargo install --example regorus --features azure_policy --path .
271+
272+
# Evaluate a policy against a non-compliant storage account (→ Deny)
273+
regorus azure-policy-eval \
274+
--policy-definition examples/regorus/azure_policy_data/require_https_storage.json \
275+
--resource examples/regorus/azure_policy_data/non_compliant_storage.json \
276+
--aliases tests/azure_policy/aliases/test_aliases.json
277+
278+
# Same policy against a compliant resource (→ undefined, no effect)
279+
regorus azure-policy-eval \
280+
--policy-definition examples/regorus/azure_policy_data/require_https_storage.json \
281+
--resource examples/regorus/azure_policy_data/compliant_storage.json \
282+
--aliases tests/azure_policy/aliases/test_aliases.json
283+
284+
# List aliases for a resource type
285+
regorus azure-policy-aliases \
286+
--aliases tests/azure_policy/aliases/test_aliases.json \
287+
--resource-type Microsoft.Storage
288+
```
289+
290+
The test suite covers conditions, effects, template functions, alias
291+
resolution, and end-to-end scenarios across 74 YAML-driven test files:
292+
293+
```bash
294+
cargo test --features azure_policy -- azure_policy
295+
```
296+
251297
## Performance
252298

253299
To check how fast Regorus runs on your system, first install a tool like [hyperfine](https://github.com/sharkdp/hyperfine).
Lines changed: 187 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,187 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
using System;
5+
using System.IO;
6+
using System.Text.Json;
7+
using System.Text.Json.Nodes;
8+
using Microsoft.VisualStudio.TestTools.UnitTesting;
9+
using Regorus;
10+
11+
namespace Regorus.Tests;
12+
13+
/// <summary>
14+
/// Tests for Azure Policy alias normalization and denormalization
15+
/// using the AliasRegistry exposed through the C# bindings.
16+
/// </summary>
17+
[TestClass]
18+
public class AzurePolicyTests
19+
{
20+
/// <summary>
21+
/// Sample alias definitions for Microsoft.Storage provider.
22+
/// These mirror a subset of the test aliases used by the Rust test suite.
23+
/// </summary>
24+
private const string StorageAliasesJson = @"[{
25+
""namespace"": ""Microsoft.Storage"",
26+
""resourceTypes"": [{
27+
""resourceType"": ""storageAccounts"",
28+
""capabilities"": ""SupportsTags, SupportsLocation"",
29+
""aliases"": [
30+
{
31+
""name"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"",
32+
""defaultPath"": ""properties.supportsHttpsTrafficOnly"",
33+
""paths"": []
34+
},
35+
{
36+
""name"": ""Microsoft.Storage/storageAccounts/minimumTlsVersion"",
37+
""defaultPath"": ""properties.minimumTlsVersion"",
38+
""paths"": []
39+
},
40+
{
41+
""name"": ""Microsoft.Storage/storageAccounts/allowBlobPublicAccess"",
42+
""defaultPath"": ""properties.allowBlobPublicAccess"",
43+
""paths"": []
44+
}
45+
]
46+
}]
47+
}]";
48+
49+
/// <summary>
50+
/// ARM resource in its original shape (with properties wrapper).
51+
/// </summary>
52+
private const string StorageResourceJson = @"{
53+
""type"": ""Microsoft.Storage/storageAccounts"",
54+
""name"": ""mystorage"",
55+
""location"": ""eastus"",
56+
""properties"": {
57+
""supportsHttpsTrafficOnly"": true,
58+
""minimumTlsVersion"": ""TLS1_2"",
59+
""allowBlobPublicAccess"": false
60+
}
61+
}";
62+
63+
[TestMethod]
64+
public void AliasRegistry_NormalizeAndWrap_produces_input_envelope()
65+
{
66+
using var registry = new AliasRegistry();
67+
registry.LoadJson(StorageAliasesJson);
68+
69+
var result = registry.NormalizeAndWrap(
70+
StorageResourceJson,
71+
apiVersion: null,
72+
contextJson: "{}",
73+
parametersJson: "{}");
74+
75+
Assert.IsNotNull(result, "NormalizeAndWrap should return a non-null string");
76+
77+
// The result should be valid JSON with resource, parameters, and context keys.
78+
var doc = JsonNode.Parse(result);
79+
Assert.IsNotNull(doc);
80+
Assert.IsNotNull(doc["resource"], "envelope must contain 'resource'");
81+
Assert.IsNotNull(doc["parameters"], "envelope must contain 'parameters'");
82+
}
83+
84+
[TestMethod]
85+
public void AliasRegistry_NormalizeAndWrap_flattens_properties()
86+
{
87+
using var registry = new AliasRegistry();
88+
registry.LoadJson(StorageAliasesJson);
89+
90+
var result = registry.NormalizeAndWrap(StorageResourceJson);
91+
Assert.IsNotNull(result);
92+
93+
var doc = JsonNode.Parse(result);
94+
var resource = doc!["resource"];
95+
Assert.IsNotNull(resource);
96+
97+
// After normalization, alias-mapped properties should be
98+
// available at the top level of the resource (lowercased).
99+
// The normalizer flattens "properties.supportsHttpsTrafficOnly"
100+
// to "supportshttpstrafficonly" at the resource root.
101+
var httpsOnly = resource["supportshttpstrafficonly"];
102+
Assert.IsNotNull(httpsOnly,
103+
"normalized resource should have 'supportshttpstrafficonly' at top level");
104+
Assert.AreEqual(true, httpsOnly!.GetValue<bool>());
105+
}
106+
107+
[TestMethod]
108+
public void AliasRegistry_NormalizeAndWrap_preserves_type_field()
109+
{
110+
using var registry = new AliasRegistry();
111+
registry.LoadJson(StorageAliasesJson);
112+
113+
var result = registry.NormalizeAndWrap(StorageResourceJson);
114+
var doc = JsonNode.Parse(result!);
115+
var resource = doc!["resource"];
116+
117+
// The "type" field should be preserved (lowercased key).
118+
var typeField = resource!["type"];
119+
Assert.IsNotNull(typeField, "normalized resource should have 'type'");
120+
Assert.AreEqual(
121+
"microsoft.storage/storageaccounts",
122+
typeField!.GetValue<string>().ToLowerInvariant());
123+
}
124+
125+
[TestMethod]
126+
public void AliasRegistry_NormalizeAndWrap_includes_parameters()
127+
{
128+
using var registry = new AliasRegistry();
129+
registry.LoadJson(StorageAliasesJson);
130+
131+
var parametersJson = @"{ ""effect"": ""Deny"" }";
132+
var result = registry.NormalizeAndWrap(
133+
StorageResourceJson,
134+
parametersJson: parametersJson);
135+
Assert.IsNotNull(result);
136+
137+
var doc = JsonNode.Parse(result!);
138+
var parameters = doc!["parameters"];
139+
Assert.IsNotNull(parameters);
140+
Assert.AreEqual("Deny", parameters!["effect"]!.GetValue<string>());
141+
}
142+
143+
[TestMethod]
144+
public void AliasRegistry_Denormalize_roundtrips_correctly()
145+
{
146+
using var registry = new AliasRegistry();
147+
registry.LoadJson(StorageAliasesJson);
148+
149+
// Normalize the ARM resource.
150+
var envelope = registry.NormalizeAndWrap(StorageResourceJson);
151+
Assert.IsNotNull(envelope);
152+
153+
// Extract just the normalized resource from the envelope.
154+
var doc = JsonNode.Parse(envelope!);
155+
var normalizedResource = doc!["resource"]!.ToJsonString();
156+
157+
// Denormalize back to ARM shape.
158+
var denormalized = registry.Denormalize(normalizedResource);
159+
Assert.IsNotNull(denormalized, "Denormalize should return a non-null string");
160+
161+
// The denormalized result should have a "properties" wrapper again.
162+
var denormDoc = JsonNode.Parse(denormalized!);
163+
Assert.IsNotNull(denormDoc);
164+
var props = denormDoc!["properties"];
165+
Assert.IsNotNull(props, "denormalized resource should have 'properties'");
166+
}
167+
168+
[TestMethod]
169+
public void AliasRegistry_loads_test_aliases_file()
170+
{
171+
// Load the same aliases file used by the Rust test suite.
172+
var aliasesPath = Path.Combine("tests", "azure_policy", "aliases", "test_aliases.json");
173+
if (!File.Exists(aliasesPath))
174+
{
175+
Assert.Inconclusive($"Test aliases file not found at {aliasesPath}");
176+
return;
177+
}
178+
179+
var aliasesJson = File.ReadAllText(aliasesPath);
180+
using var registry = new AliasRegistry();
181+
registry.LoadJson(aliasesJson);
182+
183+
// The test_aliases.json file contains multiple providers.
184+
Assert.IsTrue(registry.Length > 0,
185+
"registry should have loaded at least one resource type");
186+
}
187+
}

examples/regorus/azure_policy.rs

Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
// Copyright (c) Microsoft Corporation.
2+
// Licensed under the MIT License.
3+
4+
//! Azure Policy evaluation subcommand for the regorus example binary.
5+
//!
6+
//! Demonstrates parsing an Azure Policy definition JSON, compiling it to
7+
//! RVM bytecode, normalizing an ARM resource through the alias registry,
8+
//! and evaluating the compiled policy against the normalized input.
9+
//!
10+
//! Usage:
11+
//! cargo run --example regorus --features azure_policy -- \
12+
//! azure-policy-eval \
13+
//! --policy-definition policy.json \
14+
//! --resource resource.json \
15+
//! --aliases aliases.json \
16+
//! [--parameters '{"sku": "Standard_D2s_v3"}'] \
17+
//! [--api-version 2023-01-01]
18+
19+
use anyhow::{bail, Result};
20+
21+
use regorus::languages::azure_policy::aliases::normalizer;
22+
use regorus::languages::azure_policy::aliases::AliasRegistry;
23+
use regorus::languages::azure_policy::compiler;
24+
use regorus::languages::azure_policy::parser;
25+
use regorus::rvm::RegoVM;
26+
use regorus::Source;
27+
use regorus::Value;
28+
29+
/// Evaluate an Azure Policy definition against a resource.
30+
///
31+
/// This mirrors the pipeline used in production:
32+
/// 1. Load aliases and build the alias registry
33+
/// 2. Parse the policy definition JSON
34+
/// 3. Compile to RVM bytecode (with alias-aware field resolution)
35+
/// 4. Normalize the ARM resource through the alias registry
36+
/// 5. Run the compiled program in the Rego VM
37+
pub fn azure_policy_eval(
38+
policy_definition: String,
39+
resource: String,
40+
aliases: String,
41+
parameters_json: Option<String>,
42+
api_version: Option<String>,
43+
) -> Result<()> {
44+
// 1. Load alias registry.
45+
let aliases_json = std::fs::read_to_string(&aliases)
46+
.map_err(|e| anyhow::anyhow!("failed to read aliases file {aliases}: {e}"))?;
47+
let mut registry = AliasRegistry::new();
48+
registry.load_from_json(&aliases_json)?;
49+
println!(
50+
"Loaded {} resource type(s) from alias registry",
51+
registry.len()
52+
);
53+
54+
// 2. Parse the policy definition.
55+
let defn_json = std::fs::read_to_string(&policy_definition)
56+
.map_err(|e| anyhow::anyhow!("failed to read policy file {policy_definition}: {e}"))?;
57+
let source = Source::from_contents(policy_definition.clone(), defn_json)?;
58+
let defn = parser::parse_policy_definition(&source)
59+
.map_err(|e| anyhow::anyhow!("parse error: {e}"))?;
60+
println!("Parsed policy definition from {policy_definition}");
61+
62+
// 3. Compile to RVM bytecode.
63+
let program = compiler::compile_policy_definition_with_aliases(
64+
&defn,
65+
registry.alias_map(),
66+
registry.alias_modifiable_map(),
67+
)?;
68+
println!("Compiled policy to RVM bytecode");
69+
70+
// 4. Build normalized input.
71+
let resource_json = std::fs::read_to_string(&resource)
72+
.map_err(|e| anyhow::anyhow!("failed to read resource file {resource}: {e}"))?;
73+
let raw_resource = Value::from_json_str(&resource_json)?;
74+
let normalized = normalizer::normalize(&raw_resource, Some(&registry), api_version.as_deref());
75+
println!("Normalized resource ({} top-level fields)", {
76+
normalized.as_object().map(|m| m.len()).unwrap_or(0)
77+
});
78+
79+
// Build the input envelope: { resource, parameters }
80+
let parameters = if let Some(ref params) = parameters_json {
81+
Value::from_json_str(params)?
82+
} else {
83+
Value::new_object()
84+
};
85+
let mut input = Value::new_object();
86+
{
87+
let map = input.as_object_mut()?;
88+
map.insert(Value::from("resource"), normalized);
89+
map.insert(Value::from("parameters"), parameters);
90+
}
91+
92+
// Build a default context.
93+
let context = Value::from_json_str(
94+
r#"{
95+
"resourceGroup": { "name": "exampleRG", "location": "eastus" },
96+
"subscription": { "subscriptionId": "00000000-0000-0000-0000-000000000000" }
97+
}"#,
98+
)?;
99+
100+
// 5. Execute in the Rego VM.
101+
let mut vm = RegoVM::new();
102+
vm.load_program(program);
103+
vm.set_input(input);
104+
vm.set_context(context);
105+
106+
let result = vm.execute_entry_point_by_name("main")?;
107+
println!("\nPolicy evaluation result:");
108+
println!("{}", serde_json::to_string_pretty(&result)?);
109+
110+
Ok(())
111+
}
112+
113+
/// List available aliases for a resource type.
114+
pub fn azure_policy_aliases(aliases: String, resource_type: Option<String>) -> Result<()> {
115+
let aliases_json = std::fs::read_to_string(&aliases)
116+
.map_err(|e| anyhow::anyhow!("failed to read aliases file {aliases}: {e}"))?;
117+
let mut registry = AliasRegistry::new();
118+
registry.load_from_json(&aliases_json)?;
119+
120+
println!("Alias registry: {} resource type(s)", registry.len());
121+
122+
if let Some(ref rt) = resource_type {
123+
let rt_lower = rt.to_lowercase();
124+
let mut found = false;
125+
for (alias_name, _) in registry.alias_map() {
126+
if alias_name.to_lowercase().starts_with(&rt_lower) {
127+
println!(" {alias_name}");
128+
found = true;
129+
}
130+
}
131+
if !found {
132+
bail!("no aliases found for resource type '{rt}'");
133+
}
134+
} else {
135+
for (alias_name, _) in registry.alias_map() {
136+
println!(" {alias_name}");
137+
}
138+
}
139+
Ok(())
140+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"type": "Microsoft.Storage/storageAccounts",
3+
"name": "securestorageaccount",
4+
"location": "eastus",
5+
"kind": "StorageV2",
6+
"properties": {
7+
"supportsHttpsTrafficOnly": true,
8+
"minimumTlsVersion": "TLS1_2",
9+
"encryption": {
10+
"services": {
11+
"blob": { "enabled": true }
12+
}
13+
}
14+
}
15+
}

0 commit comments

Comments
 (0)