diff --git a/bindings/csharp/Directory.Packages.props b/bindings/csharp/Directory.Packages.props index bcd820c3..691e379d 100644 --- a/bindings/csharp/Directory.Packages.props +++ b/bindings/csharp/Directory.Packages.props @@ -1,7 +1,7 @@ true - 0.10.0 + 0.10.1 -$(VersionSuffix) diff --git a/bindings/csharp/README.md b/bindings/csharp/README.md index 74c0aa52..eef3189f 100644 --- a/bindings/csharp/README.md +++ b/bindings/csharp/README.md @@ -150,3 +150,79 @@ const string ContextJson = """ var allowed = RbacEngine.EvaluateCondition(Condition, ContextJson); Console.WriteLine($"RBAC condition allowed: {allowed}"); ``` + +## Azure Policy JSON Evaluation + +Compile and evaluate Azure Policy JSON `policyRule` definitions directly — no Rego translation required. +The `AzurePolicyCompiler` compiles JSON policy rules into RVM programs that can be executed with the `Rvm` engine. + +```csharp +using Regorus; + +// 1. Load alias definitions for the resource provider +const string AliasesJson = """ +[{ + "namespace": "Microsoft.Storage", + "resourceTypes": [{ + "resourceType": "storageAccounts", + "aliases": [{ + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + }] + }] +}] +"""; + +using var registry = new AliasRegistry(); +registry.LoadJson(AliasesJson); + +// 2. Compile a JSON policy definition (the native Azure Policy language) +const string PolicyDefinition = """ +{ + "policyRule": { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "equals": false } + ] + }, + "then": { "effect": "deny" } + } +} +"""; + +using var program = AzurePolicyCompiler.CompilePolicyDefinition(registry, PolicyDefinition); + +// 3. Normalize an ARM resource and evaluate +var armResource = """ +{ + "type": "Microsoft.Storage/storageAccounts", + "name": "mystorage", + "properties": { "supportsHttpsTrafficOnly": false } +} +"""; +var envelope = registry.NormalizeAndWrap(armResource); + +using var vm = new Rvm(); +vm.LoadProgram(program); +vm.SetInputJson(envelope!); + +var result = vm.ExecuteEntryPoint("main"); +// result: {"effect": "deny"} for non-compliant, "" for compliant +Console.WriteLine($"Policy result: {result}"); +``` + +**Context-dependent policies:** If your policy uses context functions like +`subscription()`, `resourceGroup()`, or `requestContext()`, you must also set +the VM context separately: + +```csharp +// The context JSON from NormalizeAndWrap is in the input envelope, +// but must also be provided to the VM's ambient context: +vm.SetContextJson(contextJson); +``` + +You can also compile full policy definitions (with parameters) using +`AzurePolicyCompiler.CompilePolicyDefinition()`. See +`bindings/csharp/Regorus.Tests/AzurePolicyCompilerTests.cs` for comprehensive examples. diff --git a/bindings/csharp/Regorus.Tests/AzurePolicyCompilerTests.cs b/bindings/csharp/Regorus.Tests/AzurePolicyCompilerTests.cs new file mode 100644 index 00000000..fd0f6492 --- /dev/null +++ b/bindings/csharp/Regorus.Tests/AzurePolicyCompilerTests.cs @@ -0,0 +1,414 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using System.Text.Json.Nodes; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Regorus; + +namespace Regorus.Tests; + +/// +/// Tests for — compiling Azure Policy JSON +/// policyRule and policyDefinition into RVM programs and evaluating them. +/// +[TestClass] +public class AzurePolicyCompilerTests +{ + // ----------------------------------------------------------------------- + // Test data + // ----------------------------------------------------------------------- + + private const string StorageAliasesJson = @"[{ + ""namespace"": ""Microsoft.Storage"", + ""resourceTypes"": [{ + ""resourceType"": ""storageAccounts"", + ""capabilities"": ""SupportsTags, SupportsLocation"", + ""aliases"": [ + { + ""name"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"", + ""defaultPath"": ""properties.supportsHttpsTrafficOnly"", + ""paths"": [] + }, + { + ""name"": ""Microsoft.Storage/storageAccounts/minimumTlsVersion"", + ""defaultPath"": ""properties.minimumTlsVersion"", + ""paths"": [] + } + ] + }] + }]"; + + /// Simple policy definition that checks the resource type. + private const string SimpleAuditDefinition = @"{ + ""policyRule"": { + ""if"": { + ""field"": ""type"", + ""equals"": ""Microsoft.Storage/storageAccounts"" + }, + ""then"": { ""effect"": ""audit"" } + } + }"; + + /// Policy definition that uses an alias to check HTTPS-only. + private const string HttpsDenyDefinition = @"{ + ""policyRule"": { + ""if"": { + ""allOf"": [ + { ""field"": ""type"", ""equals"": ""Microsoft.Storage/storageAccounts"" }, + { ""field"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"", ""equals"": false } + ] + }, + ""then"": { ""effect"": ""deny"" } + } + }"; + + /// Full policy definition with parameters. + private const string PolicyDefinitionWithParams = @"{ + ""displayName"": ""Require HTTPS for storage accounts"", + ""policyType"": ""Custom"", + ""mode"": ""Indexed"", + ""parameters"": { + ""effect"": { + ""type"": ""String"", + ""defaultValue"": ""deny"" + } + }, + ""policyRule"": { + ""if"": { + ""allOf"": [ + { ""field"": ""type"", ""equals"": ""Microsoft.Storage/storageAccounts"" }, + { ""field"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"", ""equals"": false } + ] + }, + ""then"": { ""effect"": ""[parameters('effect')]"" } + } + }"; + + // ----------------------------------------------------------------------- + // Helper + // ----------------------------------------------------------------------- + + /// + /// Wrap a normalized resource JSON and parameters into the input envelope + /// expected by compiled Azure Policy RVM programs. + /// + private static string WrapInput(string resourceJson, string parametersJson = "{}") + { + return $@"{{""resource"": {resourceJson}, ""parameters"": {parametersJson}}}"; + } + + /// + /// Compile a policy definition, load it into an RVM, set input, and execute. + /// Returns the result string from ExecuteEntryPoint("main"). + /// + private static string? CompileAndEval( + AliasRegistry? registry, + string policyDefinitionJson, + string inputJson) + { + using var program = AzurePolicyCompiler.CompilePolicyDefinition(registry, policyDefinitionJson); + using var vm = new Rvm(); + vm.LoadProgram(program); + vm.SetInputJson(inputJson); + return vm.ExecuteEntryPoint("main"); + } + + // ----------------------------------------------------------------------- + // CompilePolicyDefinition tests + // ----------------------------------------------------------------------- + + [TestMethod] + public void CompilePolicyDefinition_no_aliases_succeeds() + { + using var program = AzurePolicyCompiler.CompilePolicyDefinition(null, SimpleAuditDefinition); + Assert.IsNotNull(program); + } + + [TestMethod] + public void CompilePolicyDefinition_with_aliases_succeeds() + { + using var registry = new AliasRegistry(); + registry.LoadJson(StorageAliasesJson); + + using var program = AzurePolicyCompiler.CompilePolicyDefinition(registry, HttpsDenyDefinition); + Assert.IsNotNull(program); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void CompilePolicyDefinition_null_json_throws() + { + AzurePolicyCompiler.CompilePolicyDefinition(null, null!); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void CompilePolicyDefinition_invalid_json_throws() + { + AzurePolicyCompiler.CompilePolicyDefinition(null, @"{""not"": ""a definition""}"); + } + + // ----------------------------------------------------------------------- + // End-to-end evaluation tests + // ----------------------------------------------------------------------- + + [TestMethod] + public void Eval_simple_rule_matching_resource_returns_effect() + { + var input = WrapInput( + @"{""type"": ""microsoft.storage/storageaccounts""}"); + + var result = CompileAndEval(null, SimpleAuditDefinition, input); + Assert.IsNotNull(result, "expected a result for matching resource"); + + var doc = JsonNode.Parse(result!)!; + Assert.AreEqual("audit", doc["effect"]?.GetValue(), + $"expected 'audit' effect, got: {result}"); + } + + [TestMethod] + public void Eval_simple_rule_non_matching_resource_returns_undefined() + { + var input = WrapInput( + @"{""type"": ""microsoft.compute/virtualmachines""}"); + + var result = CompileAndEval(null, SimpleAuditDefinition, input); + Assert.IsNotNull(result); + StringAssert.Contains(result!, "undefined", + "expected undefined for non-matching resource type"); + } + + [TestMethod] + public void Eval_alias_rule_non_compliant_returns_deny() + { + using var registry = new AliasRegistry(); + registry.LoadJson(StorageAliasesJson); + + // Non-compliant: HTTPS not enabled (normalized/lowercased form) + var input = WrapInput( + @"{""type"": ""microsoft.storage/storageaccounts"", ""supportshttpstrafficonly"": false}"); + + using var program = AzurePolicyCompiler.CompilePolicyDefinition(registry, HttpsDenyDefinition); + using var vm = new Rvm(); + vm.LoadProgram(program); + vm.SetInputJson(input); + + var result = vm.ExecuteEntryPoint("main"); + Assert.IsNotNull(result); + + var doc = JsonNode.Parse(result!)!; + Assert.AreEqual("deny", doc["effect"]?.GetValue(), + $"expected 'deny' for non-compliant resource, got: {result}"); + } + + [TestMethod] + public void Eval_alias_rule_compliant_returns_undefined() + { + using var registry = new AliasRegistry(); + registry.LoadJson(StorageAliasesJson); + + // Compliant: HTTPS enabled + var input = WrapInput( + @"{""type"": ""microsoft.storage/storageaccounts"", ""supportshttpstrafficonly"": true}"); + + using var program = AzurePolicyCompiler.CompilePolicyDefinition(registry, HttpsDenyDefinition); + using var vm = new Rvm(); + vm.LoadProgram(program); + vm.SetInputJson(input); + + var result = vm.ExecuteEntryPoint("main"); + Assert.IsNotNull(result); + StringAssert.Contains(result!, "undefined", + "expected undefined for compliant resource"); + } + + [TestMethod] + public void Eval_definition_with_default_parameters() + { + using var registry = new AliasRegistry(); + registry.LoadJson(StorageAliasesJson); + + using var program = AzurePolicyCompiler.CompilePolicyDefinition( + registry, PolicyDefinitionWithParams); + using var vm = new Rvm(); + vm.LoadProgram(program); + + // Non-compliant resource + var input = WrapInput( + @"{""type"": ""microsoft.storage/storageaccounts"", ""supportshttpstrafficonly"": false}"); + vm.SetInputJson(input); + + var result = vm.ExecuteEntryPoint("main"); + Assert.IsNotNull(result); + + var doc = JsonNode.Parse(result!)!; + // Default parameter value is "deny" + Assert.AreEqual("deny", doc["effect"]?.GetValue(), + $"expected default 'deny' effect, got: {result}"); + } + + [TestMethod] + public void Eval_with_normalized_arm_resource_end_to_end() + { + using var registry = new AliasRegistry(); + registry.LoadJson(StorageAliasesJson); + + // Simulate the full production flow: + // 1. Start with an ARM resource + var armResource = @"{ + ""type"": ""Microsoft.Storage/storageAccounts"", + ""name"": ""mystorage"", + ""location"": ""eastus"", + ""properties"": { + ""supportsHttpsTrafficOnly"": false, + ""minimumTlsVersion"": ""TLS1_0"" + } + }"; + + // 2. Normalize via AliasRegistry + var normalizedEnvelope = registry.NormalizeAndWrap( + armResource, + apiVersion: null, + contextJson: "{}", + parametersJson: "{}"); + Assert.IsNotNull(normalizedEnvelope); + + // 3. Compile the policy rule + using var program = AzurePolicyCompiler.CompilePolicyDefinition(registry, HttpsDenyDefinition); + + // 4. Execute + using var vm = new Rvm(); + vm.LoadProgram(program); + vm.SetInputJson(normalizedEnvelope!); + + var result = vm.ExecuteEntryPoint("main"); + Assert.IsNotNull(result); + + var doc = JsonNode.Parse(result!)!; + Assert.AreEqual("deny", doc["effect"]?.GetValue(), + $"expected 'deny' for non-HTTPS storage account, got: {result}"); + } + + [TestMethod] + public void Eval_normalized_compliant_resource_end_to_end() + { + using var registry = new AliasRegistry(); + registry.LoadJson(StorageAliasesJson); + + var armResource = @"{ + ""type"": ""Microsoft.Storage/storageAccounts"", + ""name"": ""secureastorage"", + ""location"": ""westus"", + ""properties"": { + ""supportsHttpsTrafficOnly"": true, + ""minimumTlsVersion"": ""TLS1_2"" + } + }"; + + var normalizedEnvelope = registry.NormalizeAndWrap( + armResource, + apiVersion: null, + contextJson: "{}", + parametersJson: "{}"); + Assert.IsNotNull(normalizedEnvelope); + + using var program = AzurePolicyCompiler.CompilePolicyDefinition(registry, HttpsDenyDefinition); + using var vm = new Rvm(); + vm.LoadProgram(program); + vm.SetInputJson(normalizedEnvelope!); + + var result = vm.ExecuteEntryPoint("main"); + Assert.IsNotNull(result); + StringAssert.Contains(result!, "undefined", + "expected undefined for compliant HTTPS storage account"); + } + + [TestMethod] + public void Program_can_be_serialized_and_reloaded() + { + using var program = AzurePolicyCompiler.CompilePolicyDefinition(null, SimpleAuditDefinition); + + // Serialize to binary + var binary = program.SerializeBinary(); + Assert.IsTrue(binary.Length > 0, "serialized program should not be empty"); + + // Deserialize and run + using var restored = Program.DeserializeBinary(binary, out var isPartial); + Assert.IsFalse(isPartial, "program should not be partial"); + + using var vm = new Rvm(); + vm.LoadProgram(restored); + var input = WrapInput(@"{""type"": ""microsoft.storage/storageaccounts""}"); + vm.SetInputJson(input); + + var result = vm.ExecuteEntryPoint("main"); + Assert.IsNotNull(result); + var doc = JsonNode.Parse(result!)!; + Assert.AreEqual("audit", doc["effect"]?.GetValue()); + } + + [TestMethod] + public void Program_generates_listing() + { + using var program = AzurePolicyCompiler.CompilePolicyDefinition(null, SimpleAuditDefinition); + var listing = program.GenerateListing(); + Assert.IsFalse(string.IsNullOrWhiteSpace(listing), + "generated listing should not be empty"); + } + + // ----------------------------------------------------------------------- + // Context-dependent policy tests + // ----------------------------------------------------------------------- + + /// Policy definition that uses subscription() context function. + private const string ContextPolicyDefinition = @"{ + ""policyRule"": { + ""if"": { + ""allOf"": [ + { ""field"": ""type"", ""equals"": ""Microsoft.Storage/storageAccounts"" }, + { ""value"": ""[subscription().subscriptionId]"", ""equals"": ""sub-123"" } + ] + }, + ""then"": { ""effect"": ""deny"" } + } + }"; + + [TestMethod] + public void Eval_context_policy_with_set_context_returns_effect() + { + using var program = AzurePolicyCompiler.CompilePolicyDefinition(null, ContextPolicyDefinition); + using var vm = new Rvm(); + vm.LoadProgram(program); + + vm.SetContextJson(@"{""subscription"": {""subscriptionId"": ""sub-123""}}"); + + var input = WrapInput( + @"{""type"": ""microsoft.storage/storageaccounts""}"); + vm.SetInputJson(input); + + var result = vm.ExecuteEntryPoint("main"); + Assert.IsNotNull(result); + var doc = JsonNode.Parse(result!)!; + Assert.AreEqual("deny", doc["effect"]?.GetValue(), + $"expected 'deny' with matching context, got: {result}"); + } + + [TestMethod] + public void Eval_context_policy_without_context_returns_undefined() + { + using var program = AzurePolicyCompiler.CompilePolicyDefinition(null, ContextPolicyDefinition); + using var vm = new Rvm(); + vm.LoadProgram(program); + + // No context set — subscription() will be undefined + var input = WrapInput( + @"{""type"": ""microsoft.storage/storageaccounts""}"); + vm.SetInputJson(input); + + var result = vm.ExecuteEntryPoint("main"); + Assert.IsNotNull(result); + StringAssert.Contains(result!, "undefined", + "expected undefined without context set"); + } +} diff --git a/bindings/csharp/Regorus/AzurePolicyCompiler.cs b/bindings/csharp/Regorus/AzurePolicyCompiler.cs new file mode 100644 index 00000000..23ee9467 --- /dev/null +++ b/bindings/csharp/Regorus/AzurePolicyCompiler.cs @@ -0,0 +1,126 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Regorus.Internal; + +#nullable enable +namespace Regorus +{ + /// + /// Provides static methods for compiling Azure Policy JSON definitions + /// into RVM programs that can be executed by . + /// + /// + /// + /// This class bridges the gap between Azure Policy JSON (the native + /// Azure policy language with policyRule, field, + /// equals, etc.) and Regorus's RVM execution engine. + /// + /// + /// + /// Typical workflow: + /// + /// + /// Load alias definitions into an . + /// Normalize the ARM resource via . + /// Compile the JSON policy definition with . + /// Execute the resulting in an + /// instance with the normalized input. + /// + /// + /// + /// Context-dependent policies: Policies that use context functions + /// such as subscription(), resourceGroup(), or + /// requestContext() require the VM context to be set separately via + /// before execution. The context JSON + /// returned by is passed as + /// input.context but is not automatically wired into the VM's + /// ambient context — the caller must do both: + /// vm.SetInputJson(envelope) and vm.SetContextJson(contextJson). + /// + /// + public static unsafe class AzurePolicyCompiler + { + /// + /// Compile a full Azure Policy definition JSON into an RVM . + /// + /// + /// Alias registry for resolving fully-qualified alias names in field + /// references. Pass null if no alias resolution is needed. + /// + /// Warning: When null, alias field references compile as raw + /// property paths and will silently produce incorrect evaluation results for + /// policies that use aliases. Modify/Append effect policies will also skip + /// the compile-time modifiability validation. Only pass null when the + /// policy is known to contain no alias references (e.g. simple type/location + /// checks or unit-test scenarios). + /// + /// + /// + /// JSON string containing the full policy definition, which includes + /// policyRule, parameters, displayName, etc. + /// Accepted in both wrapped and unwrapped forms. + /// + /// + /// A compiled ready to be loaded into an + /// instance. + /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when parsing or compilation fails. + /// + public static Program CompilePolicyDefinition(AliasRegistry? aliasRegistry, string policyDefinitionJson) + { + if (policyDefinitionJson is null) + { + throw new ArgumentNullException(nameof(policyDefinitionJson)); + } + + return Utf8Marshaller.WithUtf8(policyDefinitionJson, defnPtr => + { + if (aliasRegistry is null) + { + var result = API.regorus_compile_azure_policy_definition( + null, (byte*)defnPtr); + return GetProgramResult(result); + } + else + { + return aliasRegistry.UseHandleForInterop(regPtr => + { + var result = API.regorus_compile_azure_policy_definition( + (RegorusAliasRegistry*)regPtr, (byte*)defnPtr); + return GetProgramResult(result); + }); + } + }); + } + + private static Program GetProgramResult(RegorusResult result) + { + try + { + if (result.status != RegorusStatus.Ok) + { + var message = Utf8Marshaller.FromUtf8(result.error_message); + throw result.status.CreateException(message); + } + + if (result.data_type != RegorusDataType.Pointer || result.pointer_value == null) + { + throw new Exception("Expected program pointer but got different data type"); + } + + var handle = RegorusProgramHandle.FromPointer((IntPtr)result.pointer_value); + return new Program(handle); + } + finally + { + API.regorus_result_drop(result); + } + } + } +} diff --git a/bindings/csharp/Regorus/NativeMethods.cs b/bindings/csharp/Regorus/NativeMethods.cs index c7a071b9..6ea664a7 100644 --- a/bindings/csharp/Regorus/NativeMethods.cs +++ b/bindings/csharp/Regorus/NativeMethods.cs @@ -178,6 +178,14 @@ internal static unsafe partial class API [DllImport(LibraryName, EntryPoint = "regorus_rvm_set_input", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern RegorusResult regorus_rvm_set_input(RegorusRvm* vm, byte* input_json); + /// + /// Set the context document for the RVM. + /// The context provides host-supplied ambient data (e.g. resourceGroup(), subscription()) + /// that Azure Policy functions can access. + /// + [DllImport(LibraryName, EntryPoint = "regorus_rvm_set_context", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_rvm_set_context(RegorusRvm* vm, byte* context_json); + /// /// Execute the program. /// @@ -490,6 +498,13 @@ internal static unsafe partial class API [DllImport(LibraryName, EntryPoint = "regorus_compile_policy_for_target", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern RegorusResult regorus_compile_policy_for_target(byte* data_json, RegorusPolicyModule* modules, UIntPtr modules_len); + /// + /// Compile a full Azure Policy definition JSON into an RVM program. + /// + [DllImport(LibraryName, EntryPoint = "regorus_compile_azure_policy_definition", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_compile_azure_policy_definition( + RegorusAliasRegistry* registry, byte* policy_definition_json); + #endregion #region Compiled Policy Methods diff --git a/bindings/csharp/Regorus/Program.cs b/bindings/csharp/Regorus/Program.cs index 27eb79dd..26448075 100644 --- a/bindings/csharp/Regorus/Program.cs +++ b/bindings/csharp/Regorus/Program.cs @@ -15,7 +15,7 @@ namespace Regorus /// public unsafe sealed class Program : SafeHandleWrapper { - private Program(RegorusProgramHandle handle) + internal Program(RegorusProgramHandle handle) : base(handle, nameof(Program)) { } diff --git a/bindings/csharp/Regorus/Rvm.cs b/bindings/csharp/Regorus/Rvm.cs index f38ecf21..f0e0c866 100644 --- a/bindings/csharp/Regorus/Rvm.cs +++ b/bindings/csharp/Regorus/Rvm.cs @@ -106,6 +106,31 @@ public void SetInputJson(string inputJson) }); } + /// + /// Set the context document for the VM. + /// The context provides host-supplied ambient data (e.g. resourceGroup(), + /// subscription()) that Azure Policy functions can access via LoadContext + /// instructions. + /// + /// + /// The must be a JSON object. Non-object + /// values (arrays, strings, numbers, etc.) will throw a . + /// Persistence: The context persists across multiple Execute + /// calls and is not cleared by loading a new program. Call again with "{}" + /// to clear it before evaluating policies with different ambient data. + /// + public void SetContextJson(string contextJson) + { + Utf8Marshaller.WithUtf8(contextJson, contextPtr => + { + UseHandle(vmPtr => + { + CheckAndDropResult(API.regorus_rvm_set_context((RegorusRvm*)vmPtr, (byte*)contextPtr)); + return 0; + }); + }); + } + /// /// Set the execution mode (0 = run-to-completion, 1 = suspendable). /// diff --git a/bindings/csharp/TargetExampleApp/Program.cs b/bindings/csharp/TargetExampleApp/Program.cs index 7d815935..06b5e51b 100644 --- a/bindings/csharp/TargetExampleApp/Program.cs +++ b/bindings/csharp/TargetExampleApp/Program.cs @@ -232,6 +232,9 @@ static void DemonstrateTargetFunctionality() Console.WriteLine("\n8. RVM host await (suspend/resume):"); DemonstrateRvmHostAwait(); + + Console.WriteLine("\n9. Azure Policy JSON compilation:"); + DemonstrateAzurePolicyJsonCompilation(); } static void DemonstrateConcurrentEvaluation(Regorus.CompiledPolicy compiledPolicy) @@ -492,4 +495,83 @@ static void DemonstrateRvmHostAwait() var resumed = vm.Resume("{\"tier\":\"gold\"}"); Console.WriteLine($"HostAwait resumed result: {resumed}"); } + + // Azure Policy JSON constants + private const string STORAGE_ALIASES_JSON = @"[{ + ""namespace"": ""Microsoft.Storage"", + ""resourceTypes"": [{ + ""resourceType"": ""storageAccounts"", + ""capabilities"": ""SupportsTags, SupportsLocation"", + ""aliases"": [ + { + ""name"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"", + ""defaultPath"": ""properties.supportsHttpsTrafficOnly"", + ""paths"": [] + } + ] + }] + }]"; + + private const string HTTPS_DENY_DEFINITION = @"{ + ""policyRule"": { + ""if"": { + ""allOf"": [ + { ""field"": ""type"", ""equals"": ""Microsoft.Storage/storageAccounts"" }, + { ""field"": ""Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly"", ""equals"": false } + ] + }, + ""then"": { ""effect"": ""deny"" } + } + }"; + + static void DemonstrateAzurePolicyJsonCompilation() + { + // 1. Set up alias registry + using var registry = new Regorus.AliasRegistry(); + registry.LoadJson(STORAGE_ALIASES_JSON); + Console.WriteLine("Loaded storage account aliases"); + + // 2. Compile the JSON policy definition directly (no Rego needed) + using var program = Regorus.AzurePolicyCompiler.CompilePolicyDefinition(registry, HTTPS_DENY_DEFINITION); + Console.WriteLine("Compiled Azure Policy JSON definition to RVM program"); + + // 3. Normalize an ARM resource + var armResource = @"{ + ""type"": ""Microsoft.Storage/storageAccounts"", + ""name"": ""insecurestorage"", + ""location"": ""eastus"", + ""properties"": { ""supportsHttpsTrafficOnly"": false } + }"; + var envelope = registry.NormalizeAndWrap(armResource, apiVersion: null, contextJson: "{}", parametersJson: "{}"); + Console.WriteLine($"Normalized ARM resource to evaluation envelope"); + + // 4. Execute in the RVM + // Note: For policies using context functions (subscription(), resourceGroup()), + // call vm.SetContextJson(contextJson) before execution. The context from + // NormalizeAndWrap is in the envelope but must also be set on the VM separately. + using var vm = new Regorus.Rvm(); + vm.LoadProgram(program); + vm.SetInputJson(envelope!); + // vm.SetContextJson(contextJson); // ← required for context-dependent policies + var result = vm.ExecuteEntryPoint("main"); + Console.WriteLine($"Evaluation result (non-compliant): {result}"); + + // 5. Test with a compliant resource + var compliantResource = @"{ + ""type"": ""Microsoft.Storage/storageAccounts"", + ""name"": ""securestorage"", + ""location"": ""eastus"", + ""properties"": { ""supportsHttpsTrafficOnly"": true } + }"; + var compliantEnvelope = registry.NormalizeAndWrap(compliantResource, apiVersion: null, contextJson: "{}", parametersJson: "{}"); + using var vm2 = new Regorus.Rvm(); + vm2.LoadProgram(program); + vm2.SetInputJson(compliantEnvelope!); + var compliantResult = vm2.ExecuteEntryPoint("main"); + Console.WriteLine($"Evaluation result (compliant): {compliantResult}"); + + // 6. Demonstrate program serialization + var binary = program.SerializeBinary(); + Console.WriteLine($"Serialized program size: {binary.Length} bytes"); + } } diff --git a/bindings/ffi/Cargo.lock b/bindings/ffi/Cargo.lock index d4386a04..d0a4c400 100644 --- a/bindings/ffi/Cargo.lock +++ b/bindings/ffi/Cargo.lock @@ -1163,7 +1163,7 @@ dependencies = [ [[package]] name = "regorus-ffi" -version = "0.10.0" +version = "0.10.1" dependencies = [ "anyhow", "cbindgen", diff --git a/bindings/ffi/Cargo.toml b/bindings/ffi/Cargo.toml index a020cf8c..2fd61017 100644 --- a/bindings/ffi/Cargo.toml +++ b/bindings/ffi/Cargo.toml @@ -2,7 +2,7 @@ [package] name = "regorus-ffi" -version = "0.10.0" +version = "0.10.1" edition = "2021" license = "MIT AND Apache-2.0 AND BSD-3-Clause" diff --git a/bindings/ffi/src/alias_registry.rs b/bindings/ffi/src/alias_registry.rs index 2d02f818..9104b037 100644 --- a/bindings/ffi/src/alias_registry.rs +++ b/bindings/ffi/src/alias_registry.rs @@ -22,6 +22,13 @@ pub struct RegorusAliasRegistry { registry: AliasRegistry, } +impl RegorusAliasRegistry { + /// Return an `Rc` clone for use by the compiler. + pub(crate) fn clone_rc(&self) -> regorus::Rc { + regorus::Rc::new(self.registry.clone()) + } +} + // --------------------------------------------------------------------------- // Lifecycle // --------------------------------------------------------------------------- diff --git a/bindings/ffi/src/compile.rs b/bindings/ffi/src/compile.rs index ba1ee483..934e0bcd 100644 --- a/bindings/ffi/src/compile.rs +++ b/bindings/ffi/src/compile.rs @@ -208,6 +208,117 @@ fn convert_c_modules_to_rust( Ok(policy_modules) } +// --------------------------------------------------------------------------- +// Azure Policy JSON compilation +// --------------------------------------------------------------------------- + +/// Compile a full Azure Policy definition JSON into an RVM program. +/// +/// Parses the JSON policy definition (which includes `policyRule`, `parameters`, +/// `displayName`, etc.), resolves aliases using the provided registry, and +/// compiles the result into an RVM [`Program`]. +/// +/// The definition JSON may be in either wrapped or unwrapped form: +/// - **Wrapped**: `{ "properties": { "policyRule": ..., "parameters": ... }, "id": ... }` +/// - **Unwrapped**: `{ "policyRule": ..., "parameters": ..., "displayName": ... }` +/// +/// # Parameters +/// * `registry` - Alias registry handle, or null. +/// * `policy_definition_json` - JSON string containing the full policy definition +/// +/// # Null registry behavior +/// +/// When `registry` is null, compilation proceeds **without alias resolution**. +/// Field references that correspond to Azure resource provider aliases will +/// be compiled as raw property paths rather than being resolved. This means: +/// +/// - Policies that rely on aliases will **silently produce incorrect +/// evaluation results**. +/// - **Modify / Append** effect policies will **skip the modifiability +/// validation** that normally rejects writes to non-modifiable aliases +/// at compile time. +/// +/// Pass null only when the policy is known to contain no alias references +/// (e.g. simple `type` / `location` checks, or in unit-test scenarios). +/// +/// # Returns +/// Returns a `RegorusResult` containing a `RegorusProgram` pointer on success. +/// +/// # Safety +/// `policy_definition_json` must be a valid null-terminated UTF-8 string. +/// If `registry` is non-null it must be a valid `RegorusAliasRegistry` pointer. +/// The caller must eventually call `regorus_program_drop` on the returned handle. +#[cfg(all(feature = "azure_policy", feature = "rvm"))] +#[no_mangle] +pub extern "C" fn regorus_compile_azure_policy_definition( + registry: *mut crate::alias_registry::RegorusAliasRegistry, + policy_definition_json: *const c_char, +) -> RegorusResult { + use crate::alias_registry::RegorusAliasRegistry; + use crate::common::to_ref; + use crate::rvm::RegorusProgram; + use alloc::sync::Arc; + use regorus::languages::azure_policy::{compiler, parser}; + use regorus::Rc; + use regorus::Source; + + with_unwind_guard(|| { + let result = || -> Result { + let json_str = from_c_str(policy_definition_json).map_err(|e| { + ( + RegorusStatus::InvalidDataFormat, + format!("Invalid policy definition JSON string: {e}"), + ) + })?; + + let source = + Source::from_contents("policy_definition".into(), json_str).map_err(|e| { + ( + RegorusStatus::InvalidDataFormat, + format!("Failed to create source: {e}"), + ) + })?; + + let defn = parser::parse_policy_definition(&source).map_err(|e| { + ( + RegorusStatus::InvalidPolicy, + format!("Failed to parse policy definition: {e}"), + ) + })?; + + let program = if registry.is_null() { + compiler::compile_policy_definition(&defn) + } else { + let reg: &RegorusAliasRegistry = to_ref(registry).map_err(|e| { + ( + RegorusStatus::InvalidArgument, + format!("Invalid alias registry: {e}"), + ) + })?; + compiler::compile_policy_definition_with_aliases(&defn, reg.clone_rc()) + }; + + program + .map(|p| RegorusProgram { + program: Arc::new(Rc::try_unwrap(p).unwrap_or_else(|rc| (*rc).clone())), + }) + .map_err(|e| { + ( + RegorusStatus::CompilationFailed, + format!("Failed to compile policy definition: {e}"), + ) + }) + }(); + + match result { + Ok(program) => { + RegorusResult::ok_pointer(Box::into_raw(Box::new(program)) as *mut c_void) + } + Err((status, msg)) => RegorusResult::err_with_message(status, msg), + } + }) +} + #[cfg(feature = "std")] fn report_module_error(index: usize, kind: &str, err: &anyhow::Error) { eprintln!("Invalid {} at index {}: {}", kind, index, err); @@ -215,3 +326,426 @@ fn report_module_error(index: usize, kind: &str, err: &anyhow::Error) { #[cfg(not(feature = "std"))] fn report_module_error(_index: usize, _kind: &str, _err: &anyhow::Error) {} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use crate::common::regorus_result_drop; + use core::ffi::CStr; + use std::ffi::CString; + + fn c(s: &str) -> CString { + CString::new(s).expect("CString::new failed") + } + + fn assert_ok_pointer(r: &RegorusResult) -> *mut c_void { + assert_eq!( + r.status, + RegorusStatus::Ok, + "expected Ok, got {:?}", + r.status + ); + assert!(!r.pointer_value.is_null(), "expected non-null pointer"); + r.pointer_value + } + + #[cfg(all(feature = "azure_policy", feature = "rvm"))] + mod azure_policy_json { + use super::*; + use crate::alias_registry::{ + regorus_alias_registry_drop, regorus_alias_registry_load_json, + regorus_alias_registry_new, + }; + use crate::rvm::{ + regorus_program_drop, regorus_rvm_drop, regorus_rvm_execute_entry_point_by_name, + regorus_rvm_load_program, regorus_rvm_new, regorus_rvm_set_context, + regorus_rvm_set_input, RegorusProgram, + }; + + const ALIASES: &str = r#"[{ + "namespace": "Microsoft.Storage", + "resourceTypes": [{ + "resourceType": "storageAccounts", + "aliases": [{ + "name": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", + "defaultPath": "properties.supportsHttpsTrafficOnly", + "paths": [] + }, { + "name": "Microsoft.Storage/storageAccounts/minimumTlsVersion", + "defaultPath": "properties.minimumTlsVersion", + "paths": [] + }] + }] + }]"#; + + /// Simple policy definition (no aliases, no parameters). + const SIMPLE_POLICY_DEFINITION: &str = r#"{ + "policyRule": { + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { "effect": "audit" } + } + }"#; + + /// Policy definition that uses an alias (no parameters). + const ALIAS_POLICY_DEFINITION: &str = r#"{ + "policyRule": { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "equals": false } + ] + }, + "then": { "effect": "deny" } + } + }"#; + + /// Policy definition with parameters. + const POLICY_DEFINITION_WITH_PARAMS: &str = r#"{ + "displayName": "Require HTTPS for storage accounts", + "policyType": "Custom", + "mode": "Indexed", + "parameters": { + "effect": { + "type": "String", + "defaultValue": "deny" + } + }, + "policyRule": { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "equals": false } + ] + }, + "then": { "effect": "[parameters('effect')]" } + } + }"#; + + /// Policy definition that uses a context function (subscription()). + const CONTEXT_POLICY_DEFINITION: &str = r#"{ + "policyRule": { + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "value": "[subscription().subscriptionId]", "equals": "sub-123" } + ] + }, + "then": { "effect": "deny" } + } + }"#; + + /// Wrap a normalized resource JSON into the input envelope expected by + /// the compiled Azure Policy RVM program. + fn wrap_input(resource_json: &str, parameters_json: &str) -> String { + format!(r#"{{"resource": {resource_json}, "parameters": {parameters_json}}}"#) + } + + /// Helper: compile a policy definition, execute it with input, and return the + /// result string. + unsafe fn compile_and_eval( + registry: *mut crate::alias_registry::RegorusAliasRegistry, + policy_definition: &str, + input_json: &str, + ) -> String { + let defn_c = c(policy_definition); + let r = regorus_compile_azure_policy_definition(registry, defn_c.as_ptr()); + let program_ptr = assert_ok_pointer(&r) as *mut RegorusProgram; + regorus_result_drop(r); + + let vm = regorus_rvm_new(); + assert!(!vm.is_null()); + + let r = regorus_rvm_load_program(vm, program_ptr); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let input_c = c(input_json); + let r = regorus_rvm_set_input(vm, input_c.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let entry = c("main"); + let r = regorus_rvm_execute_entry_point_by_name(vm, entry.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok, "execute failed"); + let output = CStr::from_ptr(r.output) + .to_str() + .expect("invalid UTF-8") + .to_string(); + regorus_result_drop(r); + + regorus_rvm_drop(vm); + regorus_program_drop(program_ptr); + output + } + + #[test] + fn compile_simple_definition_no_aliases() { + let defn_c = c(SIMPLE_POLICY_DEFINITION); + let r = regorus_compile_azure_policy_definition(core::ptr::null_mut(), defn_c.as_ptr()); + let ptr = assert_ok_pointer(&r); + regorus_result_drop(r); + regorus_program_drop(ptr as *mut RegorusProgram); + } + + #[test] + fn compile_definition_with_aliases() { + let reg = regorus_alias_registry_new(); + let aliases_c = c(ALIASES); + let r = regorus_alias_registry_load_json(reg, aliases_c.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let defn_c = c(ALIAS_POLICY_DEFINITION); + let r = regorus_compile_azure_policy_definition(reg, defn_c.as_ptr()); + let ptr = assert_ok_pointer(&r); + regorus_result_drop(r); + + regorus_program_drop(ptr as *mut RegorusProgram); + regorus_alias_registry_drop(reg); + } + + #[test] + fn compile_and_eval_simple_definition_matching() { + let input = wrap_input(r#"{"type":"microsoft.storage/storageaccounts"}"#, "{}"); + let result = unsafe { + compile_and_eval(core::ptr::null_mut(), SIMPLE_POLICY_DEFINITION, &input) + }; + let parsed: serde_json::Value = + serde_json::from_str(&result).expect("result should be valid JSON"); + assert_eq!( + parsed["effect"], "audit", + "expected audit effect, got: {result}" + ); + } + + #[test] + fn compile_and_eval_simple_definition_not_matching() { + let input = wrap_input(r#"{"type":"microsoft.compute/virtualmachines"}"#, "{}"); + let result = unsafe { + compile_and_eval(core::ptr::null_mut(), SIMPLE_POLICY_DEFINITION, &input) + }; + assert!( + result.contains("undefined"), + "expected undefined for non-matching input, got: {result}" + ); + } + + #[test] + fn compile_and_eval_alias_definition_deny() { + let reg = regorus_alias_registry_new(); + let aliases_c = c(ALIASES); + let r = regorus_alias_registry_load_json(reg, aliases_c.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + // Non-compliant resource: HTTPS not enabled (normalized form) + let input = wrap_input( + r#"{"type": "microsoft.storage/storageaccounts", "supportshttpstrafficonly": false}"#, + "{}", + ); + let result = unsafe { compile_and_eval(reg, ALIAS_POLICY_DEFINITION, &input) }; + let parsed: serde_json::Value = serde_json::from_str(&result).expect("valid JSON"); + assert_eq!(parsed["effect"], "deny", "expected deny, got: {result}"); + + regorus_alias_registry_drop(reg); + } + + #[test] + fn compile_and_eval_alias_definition_compliant() { + let reg = regorus_alias_registry_new(); + let aliases_c = c(ALIASES); + let r = regorus_alias_registry_load_json(reg, aliases_c.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + // Compliant resource: HTTPS enabled (normalized form) + let input = wrap_input( + r#"{"type": "microsoft.storage/storageaccounts", "supportshttpstrafficonly": true}"#, + "{}", + ); + let result = unsafe { compile_and_eval(reg, ALIAS_POLICY_DEFINITION, &input) }; + assert!( + result.contains("undefined"), + "expected undefined for compliant resource, got: {result}" + ); + + regorus_alias_registry_drop(reg); + } + + #[test] + fn compile_definition_with_params_and_eval() { + let reg = regorus_alias_registry_new(); + let aliases_c = c(ALIASES); + let r = regorus_alias_registry_load_json(reg, aliases_c.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let defn_c = c(POLICY_DEFINITION_WITH_PARAMS); + let r = regorus_compile_azure_policy_definition(reg, defn_c.as_ptr()); + let program_ptr = assert_ok_pointer(&r) as *mut RegorusProgram; + regorus_result_drop(r); + + unsafe { + let vm = regorus_rvm_new(); + let r = regorus_rvm_load_program(vm, program_ptr); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let input_json = wrap_input( + r#"{"type": "microsoft.storage/storageaccounts", "supportshttpstrafficonly": false}"#, + "{}", + ); + let input = c(&input_json); + let r = regorus_rvm_set_input(vm, input.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let entry = c("main"); + let r = regorus_rvm_execute_entry_point_by_name(vm, entry.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + let result = CStr::from_ptr(r.output) + .to_str() + .expect("UTF-8") + .to_string(); + regorus_result_drop(r); + + let parsed: serde_json::Value = serde_json::from_str(&result).unwrap(); + assert_eq!(parsed["effect"], "deny", "got: {result}"); + + regorus_rvm_drop(vm); + regorus_program_drop(program_ptr); + } + + regorus_alias_registry_drop(reg); + } + + #[test] + fn invalid_json_returns_error() { + let bad = c("not valid json"); + let r = regorus_compile_azure_policy_definition(core::ptr::null_mut(), bad.as_ptr()); + assert_ne!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + } + + #[test] + fn invalid_definition_returns_error() { + let bad = c(r#"{"not": "a policy definition"}"#); + let r = regorus_compile_azure_policy_definition(core::ptr::null_mut(), bad.as_ptr()); + assert_ne!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + } + + #[test] + fn context_policy_evaluates_with_set_context() { + let defn_c = c(CONTEXT_POLICY_DEFINITION); + let r = regorus_compile_azure_policy_definition(core::ptr::null_mut(), defn_c.as_ptr()); + let program = assert_ok_pointer(&r) as *mut RegorusProgram; + regorus_result_drop(r); + + let vm = regorus_rvm_new(); + assert!(!vm.is_null()); + + let r = regorus_rvm_load_program(vm, program); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + // Set the context with subscription info + let context = c(r#"{"subscription": {"subscriptionId": "sub-123"}}"#); + let r = regorus_rvm_set_context(vm, context.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + // Set matching input + let input = c(&wrap_input( + r#"{"type": "microsoft.storage/storageaccounts"}"#, + "{}", + )); + let r = regorus_rvm_set_input(vm, input.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let entry = c("main"); + let r = regorus_rvm_execute_entry_point_by_name(vm, entry.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + let output = unsafe { CStr::from_ptr(r.output) }.to_str().unwrap(); + assert!( + output.contains("deny"), + "expected deny effect with matching context, got: {output}" + ); + regorus_result_drop(r); + + regorus_rvm_drop(vm); + regorus_program_drop(program); + } + + #[test] + fn context_policy_undefined_without_context() { + let defn_c = c(CONTEXT_POLICY_DEFINITION); + let r = regorus_compile_azure_policy_definition(core::ptr::null_mut(), defn_c.as_ptr()); + let program = assert_ok_pointer(&r) as *mut RegorusProgram; + regorus_result_drop(r); + + let vm = regorus_rvm_new(); + assert!(!vm.is_null()); + + let r = regorus_rvm_load_program(vm, program); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + // No context set — subscription() will be undefined + let input = c(&wrap_input( + r#"{"type": "microsoft.storage/storageaccounts"}"#, + "{}", + )); + let r = regorus_rvm_set_input(vm, input.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let entry = c("main"); + let r = regorus_rvm_execute_entry_point_by_name(vm, entry.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + let output = unsafe { CStr::from_ptr(r.output) }.to_str().unwrap(); + assert!( + output.contains("undefined"), + "expected undefined without context, got: {output}" + ); + regorus_result_drop(r); + + regorus_rvm_drop(vm); + regorus_program_drop(program); + } + + #[test] + fn set_context_rejects_non_object() { + let vm = regorus_rvm_new(); + assert!(!vm.is_null()); + + // Array should be rejected with InvalidDataFormat + let array_json = c(r#"[1, 2, 3]"#); + let r = regorus_rvm_set_context(vm, array_json.as_ptr()); + assert_eq!(r.status, RegorusStatus::InvalidDataFormat); + regorus_result_drop(r); + + // String should be rejected with InvalidDataFormat + let str_json = c(r#""hello""#); + let r = regorus_rvm_set_context(vm, str_json.as_ptr()); + assert_eq!(r.status, RegorusStatus::InvalidDataFormat); + regorus_result_drop(r); + + // Object should succeed + let obj_json = c(r#"{"key": "value"}"#); + let r = regorus_rvm_set_context(vm, obj_json.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + regorus_rvm_drop(vm); + } + } +} diff --git a/bindings/ffi/src/rvm.rs b/bindings/ffi/src/rvm.rs index c47182d7..e014f7ae 100644 --- a/bindings/ffi/src/rvm.rs +++ b/bindings/ffi/src/rvm.rs @@ -358,6 +358,68 @@ pub extern "C" fn regorus_rvm_set_input( }) } +/// Set the VM context document from JSON. +/// +/// The context provides host-supplied ambient data (e.g. `resourceGroup()`, +/// `subscription()`) that Azure Policy functions can access via `LoadContext` +/// instructions. This must be called before `regorus_rvm_execute` when +/// evaluating policies that reference context functions. +/// +/// The `context_json` must be a JSON **object**. Non-object values (arrays, +/// strings, numbers, booleans, null) are rejected with an `InvalidDataFormat` +/// error because context functions expect named fields. +/// +/// **Persistence:** The context persists across multiple `regorus_rvm_execute` +/// calls — it is *not* cleared by execution or by loading a new program. This +/// is the same behavior as `regorus_rvm_set_input`. Callers should call this +/// function again (or pass `"{}"`) to update or clear the context before +/// evaluating a different policy that requires different ambient data. +/// +/// # Safety +/// - `vm` must be a valid pointer to a `RegorusRvm` created by `regorus_rvm_new`. +/// - `context_json` must be a valid null-terminated UTF-8 string. +#[cfg(feature = "azure_policy")] +#[no_mangle] +pub extern "C" fn regorus_rvm_set_context( + vm: *mut RegorusRvm, + context_json: *const c_char, +) -> RegorusResult { + with_unwind_guard(|| { + let result = || -> Result<(), (RegorusStatus, alloc::string::String)> { + let vm = to_ref(vm) + .map_err(|e| (RegorusStatus::InvalidArgument, format!("Invalid VM: {e}")))?; + let mut guard = vm + .try_write() + .map_err(|e| (RegorusStatus::Error, format!("VM lock failed: {e}")))?; + let context_value = Value::from_json_str(&from_c_str(context_json).map_err(|e| { + ( + RegorusStatus::InvalidDataFormat, + format!("Invalid context JSON string: {e}"), + ) + })?) + .map_err(|e| { + ( + RegorusStatus::InvalidDataFormat, + format!("Invalid context JSON: {e}"), + ) + })?; + if !matches!(context_value, Value::Object(_)) { + return Err(( + RegorusStatus::InvalidDataFormat, + alloc::string::String::from("context must be a JSON object"), + )); + } + guard.set_context(context_value); + Ok(()) + }(); + + match result { + Ok(()) => RegorusResult::ok_void(), + Err((status, msg)) => RegorusResult::err_with_message(status, msg), + } + }) +} + /// Set the maximum number of instructions that can execute. #[no_mangle] pub extern "C" fn regorus_rvm_set_max_instructions(