From e2e619999ea43a747ef8565555823a6ed0f18dc4 Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi Date: Thu, 21 May 2026 11:57:37 -0500 Subject: [PATCH 1/2] fix(ffi): eliminate aliasing UB via to_shared_ref migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add to_shared_ref() helper that creates &T (shared reference) from raw pointers instead of &mut T. This eliminates undefined behavior caused by violating Rust's aliasing invariant when C# SafeHandle permits concurrent FFI calls on the same handle. With &mut T, the compiler may assume exclusive (noalias) access and reorder or elide reads/writes — a miscompilation risk when another thread holds a reference to the same object. Switching to &T removes that assumption; actual mutation is mediated by the interior RwLock inside Handle, which is the sole synchronization mechanism. Migrated sites: - rvm.rs: 20 non-drop call sites - engine.rs: 30 non-drop call sites + with_unwind_guard for timer fns - compiled_policy.rs: 2 call sites - Fix null-data UB in regorus_program_deserialize_binary Drop paths retain to_ref() where exclusive access is guaranteed by the caller contract (preventing use-after-free). Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bindings/ffi/src/common.rs | 4 ++ bindings/ffi/src/compiled_policy.rs | 6 +- bindings/ffi/src/engine.rs | 99 +++++++++++++++-------------- bindings/ffi/src/rvm.rs | 51 +++++++++------ 4 files changed, 90 insertions(+), 70 deletions(-) diff --git a/bindings/ffi/src/common.rs b/bindings/ffi/src/common.rs index c6d8c599..bf8cca3d 100644 --- a/bindings/ffi/src/common.rs +++ b/bindings/ffi/src/common.rs @@ -236,6 +236,10 @@ pub(crate) fn to_ref<'a, T>(t: *mut T) -> Result<&'a mut T> { unsafe { t.as_mut().ok_or_else(|| anyhow!("null pointer")) } } +pub(crate) fn to_shared_ref<'a, T>(t: *const T) -> Result<&'a T> { + unsafe { t.as_ref().ok_or_else(|| anyhow!("null pointer")) } +} + pub(crate) fn to_regorus_result(r: Result<()>) -> RegorusResult { match r { Ok(()) => RegorusResult::ok_void(), diff --git a/bindings/ffi/src/compiled_policy.rs b/bindings/ffi/src/compiled_policy.rs index 98a15436..7abee56c 100644 --- a/bindings/ffi/src/compiled_policy.rs +++ b/bindings/ffi/src/compiled_policy.rs @@ -39,7 +39,7 @@ pub extern "C" fn regorus_compiled_policy_eval_with_input( with_unwind_guard(|| { let output = || -> Result { let input_value = regorus::Value::from_json_str(&from_c_str(input)?)?; - let result = to_ref(compiled_policy)? + let result = to_shared_ref(compiled_policy as *const RegorusCompiledPolicy)? .compiled_policy .eval_with_input(input_value)?; result.to_json_str() @@ -65,7 +65,9 @@ pub extern "C" fn regorus_compiled_policy_get_policy_info( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let info = to_ref(compiled_policy)?.compiled_policy.get_policy_info()?; + let info = to_shared_ref(compiled_policy as *const RegorusCompiledPolicy)? + .compiled_policy + .get_policy_info()?; serde_json::to_string(&info) .map_err(|e| anyhow::anyhow!("Failed to serialize policy info: {}", e)) }(); diff --git a/bindings/ffi/src/engine.rs b/bindings/ffi/src/engine.rs index 8079b8e8..3ec469cc 100644 --- a/bindings/ffi/src/engine.rs +++ b/bindings/ffi/src/engine.rs @@ -2,7 +2,8 @@ // Licensed under the MIT License. use crate::common::{ - from_c_str, to_ref, to_regorus_result, to_regorus_string_result, RegorusResult, RegorusStatus, + from_c_str, to_ref, to_regorus_result, to_regorus_string_result, to_shared_ref, RegorusResult, + RegorusStatus, }; use crate::compiled_policy::RegorusCompiledPolicy; use crate::limits::RegorusExecutionTimerConfig; @@ -193,7 +194,7 @@ pub extern "C" fn regorus_engine_new() -> *mut RegorusEngine { /// #[no_mangle] pub extern "C" fn regorus_engine_clone(engine: *mut RegorusEngine) -> *mut RegorusEngine { - match to_ref(engine) { + match to_shared_ref(engine as *const RegorusEngine) { Ok(e) => Box::into_raw(Box::new(e.clone())), _ => ptr::null_mut(), } @@ -223,7 +224,7 @@ pub extern "C" fn regorus_engine_add_policy( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_string_result(|| -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.add_policy(from_c_str(path)?, from_c_str(rego)?) }()) @@ -238,7 +239,7 @@ pub extern "C" fn regorus_engine_add_policy_from_file( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_string_result(|| -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.add_policy_from_file(from_c_str(path)?) }()) @@ -256,7 +257,7 @@ pub extern "C" fn regorus_engine_add_data_json( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.add_data(regorus::Value::from_json_str(&from_c_str(data)?)?) }()) @@ -270,7 +271,7 @@ pub extern "C" fn regorus_engine_add_data_json( pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> RegorusResult { with_unwind_guard(|| { to_regorus_string_result(|| -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let guard = engine.try_read()?; serde_json::to_string_pretty(&guard.get_packages()?).map_err(anyhow::Error::msg) }()) @@ -284,7 +285,7 @@ pub extern "C" fn regorus_engine_get_packages(engine: *mut RegorusEngine) -> Reg pub extern "C" fn regorus_engine_get_policies(engine: *mut RegorusEngine) -> RegorusResult { with_unwind_guard(|| { to_regorus_string_result(|| -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let guard = engine.try_read()?; guard.get_policies_as_json() }()) @@ -299,7 +300,7 @@ pub extern "C" fn regorus_engine_add_data_from_json_file( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.add_data(regorus::Value::from_json_file(from_c_str(path)?)?) }()) @@ -313,7 +314,7 @@ pub extern "C" fn regorus_engine_add_data_from_json_file( pub extern "C" fn regorus_engine_clear_data(engine: *mut RegorusEngine) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.clear_data(); Ok(()) @@ -332,7 +333,7 @@ pub extern "C" fn regorus_engine_set_input_json( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.set_input(regorus::Value::from_json_str(&from_c_str(input)?)?); Ok(()) @@ -348,7 +349,7 @@ pub extern "C" fn regorus_engine_set_input_from_json_file( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.set_input(regorus::Value::from_json_file(from_c_str(path)?)?); Ok(()) @@ -367,7 +368,7 @@ pub extern "C" fn regorus_engine_eval_query( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; let results = guard.eval_query(from_c_str(query)?, false)?; Ok(serde_json::to_string_pretty(&results)?) @@ -390,7 +391,7 @@ pub extern "C" fn regorus_engine_eval_rule( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.eval_rule(from_c_str(rule)?)?.to_json_str() }(); @@ -413,7 +414,7 @@ pub extern "C" fn regorus_engine_set_enable_coverage( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.set_enable_coverage(enable); Ok(()) @@ -429,7 +430,7 @@ pub extern "C" fn regorus_engine_set_enable_coverage( pub extern "C" fn regorus_engine_get_coverage_report(engine: *mut RegorusEngine) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let guard = engine.try_read()?; Ok(serde_json::to_string_pretty(&guard.get_coverage_report()?)?) }(); @@ -451,7 +452,7 @@ pub extern "C" fn regorus_engine_set_strict_builtin_errors( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.set_strict_builtin_errors(strict); Ok(()) @@ -465,18 +466,20 @@ pub extern "C" fn regorus_engine_set_execution_timer_config( engine: *mut RegorusEngine, config: *const RegorusExecutionTimerConfig, ) -> RegorusResult { - to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; - let config = unsafe { - config - .as_ref() - .copied() - .ok_or_else(|| anyhow!("execution timer config pointer is null"))? - }; - let mut guard = engine.try_write()?; - guard.set_execution_timer_config(config.to_execution_timer_config()?); - Ok(()) - }()) + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let engine = to_shared_ref(engine as *const RegorusEngine)?; + let config = unsafe { + config + .as_ref() + .copied() + .ok_or_else(|| anyhow!("execution timer config pointer is null"))? + }; + let mut guard = engine.try_write()?; + guard.set_execution_timer_config(config.to_execution_timer_config()?); + Ok(()) + }()) + }) } #[no_mangle] @@ -484,12 +487,14 @@ pub extern "C" fn regorus_engine_set_execution_timer_config( pub extern "C" fn regorus_engine_clear_execution_timer_config( engine: *mut RegorusEngine, ) -> RegorusResult { - to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; - let mut guard = engine.try_write()?; - guard.clear_execution_timer_config(); - Ok(()) - }()) + with_unwind_guard(|| { + to_regorus_result(|| -> Result<()> { + let engine = to_shared_ref(engine as *const RegorusEngine)?; + let mut guard = engine.try_write()?; + guard.clear_execution_timer_config(); + Ok(()) + }()) + }) } /// Set the policy length limits used when loading policies. @@ -500,7 +505,7 @@ pub extern "C" fn regorus_engine_set_policy_length_config( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.set_policy_length_config(config.to_policy_length_config()?); Ok(()) @@ -515,7 +520,7 @@ pub extern "C" fn regorus_engine_clear_policy_length_config( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.clear_policy_length_config(); Ok(()) @@ -533,7 +538,7 @@ pub extern "C" fn regorus_engine_get_coverage_report_pretty( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let guard = engine.try_read()?; guard.get_coverage_report()?.to_string_pretty() }(); @@ -552,7 +557,7 @@ pub extern "C" fn regorus_engine_get_coverage_report_pretty( pub extern "C" fn regorus_engine_clear_coverage_data(engine: *mut RegorusEngine) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.clear_coverage_data(); Ok(()) @@ -571,7 +576,7 @@ pub extern "C" fn regorus_engine_set_gather_prints( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.set_gather_prints(enable); Ok(()) @@ -586,7 +591,7 @@ pub extern "C" fn regorus_engine_set_gather_prints( pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; Ok(serde_json::to_string_pretty(&guard.take_prints()?)?) }(); @@ -605,7 +610,7 @@ pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> Rego pub extern "C" fn regorus_engine_get_ast_as_json(engine: *mut RegorusEngine) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let guard = engine.try_read()?; guard.get_ast_as_json() }(); @@ -626,7 +631,7 @@ pub extern "C" fn regorus_engine_get_policy_package_names( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let guard = engine.try_read()?; serde_json::to_string_pretty(&guard.get_policy_package_names()?) .map_err(anyhow::Error::msg) @@ -648,7 +653,7 @@ pub extern "C" fn regorus_engine_get_policy_parameters( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let guard = engine.try_read()?; serde_json::to_string_pretty(&guard.get_policy_parameters()?) .map_err(anyhow::Error::msg) @@ -670,7 +675,7 @@ pub extern "C" fn regorus_engine_set_rego_v0( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result<()> { - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; guard.set_rego_v0(enable); Ok(()) @@ -692,7 +697,7 @@ pub extern "C" fn regorus_engine_set_rego_v0( #[cfg(feature = "azure_policy")] pub extern "C" fn regorus_engine_compile_for_target(engine: *mut RegorusEngine) -> RegorusResult { with_unwind_guard(|| { - let engine = match to_ref(engine) { + let engine = match to_shared_ref(engine as *const RegorusEngine) { Ok(engine) => engine, Err(e) => { return RegorusResult::err_with_message( @@ -741,7 +746,7 @@ pub extern "C" fn regorus_engine_compile_with_entrypoint( let result = || -> Result { let rule_str = from_c_str(rule)?; let rule_rc: regorus::Rc = rule_str.into(); - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?; Ok(RegorusCompiledPolicy { compiled_policy }) @@ -800,7 +805,7 @@ pub extern "C" fn regorus_engine_compile_program_with_entrypoints( .ok_or_else(|| anyhow!("entry_points must contain at least one entry"))?; let rule_rc: regorus::Rc = (*rule).into(); - let engine = to_ref(engine)?; + let engine = to_shared_ref(engine as *const RegorusEngine)?; let mut guard = engine.try_write()?; let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?; diff --git a/bindings/ffi/src/rvm.rs b/bindings/ffi/src/rvm.rs index c47182d7..44831db5 100644 --- a/bindings/ffi/src/rvm.rs +++ b/bindings/ffi/src/rvm.rs @@ -2,7 +2,8 @@ // Licensed under the MIT License. use crate::common::{ - from_c_str, to_ref, to_regorus_result, RegorusBuffer, RegorusResult, RegorusStatus, + from_c_str, to_ref, to_regorus_result, to_shared_ref, RegorusBuffer, RegorusResult, + RegorusStatus, }; use crate::compile::RegorusPolicyModule; use crate::compiled_policy::RegorusCompiledPolicy; @@ -106,7 +107,8 @@ pub extern "C" fn regorus_program_compile_from_policy( let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect(); - let compiled_policy = &to_ref(compiled_policy)?.compiled_policy; + let compiled_policy = + &to_shared_ref(compiled_policy as *const RegorusCompiledPolicy)?.compiled_policy; let program = Compiler::compile_from_policy(compiled_policy, &entry_points_ref)?; Ok(Box::into_raw(Box::new(RegorusProgram { program }))) }(); @@ -187,7 +189,7 @@ pub extern "C" fn regorus_program_new() -> *mut RegorusProgram { pub extern "C" fn regorus_program_serialize_binary(program: *mut RegorusProgram) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result<*mut RegorusBuffer> { - let program = &to_ref(program)?.program; + let program = &to_shared_ref(program as *const RegorusProgram)?.program; let bytes = program.serialize_binary().map_err(|e| anyhow!(e))?; Ok(RegorusBuffer::from_vec(bytes)) }(); @@ -211,7 +213,10 @@ pub extern "C" fn regorus_program_deserialize_binary( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result<(*mut RegorusProgram, bool)> { - if data.is_null() && len > 0 { + if data.is_null() { + if len > 0 { + return Err(anyhow!("null data pointer with non-zero length")); + } return Err(anyhow!("null data pointer")); } let data = unsafe { core::slice::from_raw_parts(data, len) }; @@ -249,7 +254,7 @@ pub extern "C" fn regorus_program_deserialize_binary( pub extern "C" fn regorus_program_generate_listing(program: *mut RegorusProgram) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let program = &to_ref(program)?.program; + let program = &to_shared_ref(program as *const RegorusProgram)?.program; Ok(generate_assembly_listing( program, &AssemblyListingConfig::default(), @@ -270,7 +275,7 @@ pub extern "C" fn regorus_program_generate_tabular_listing( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let program = &to_ref(program)?.program; + let program = &to_shared_ref(program as *const RegorusProgram)?.program; Ok(generate_tabular_assembly_listing( program, &AssemblyListingConfig::default(), @@ -297,7 +302,9 @@ pub extern "C" fn regorus_rvm_new_with_policy( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result<*mut RegorusRvm> { - let policy = to_ref(compiled_policy)?.compiled_policy.clone(); + let policy = to_shared_ref(compiled_policy as *const RegorusCompiledPolicy)? + .compiled_policy + .clone(); Ok(Box::into_raw(Box::new(RegorusRvm::new( RegoVM::new_with_policy(policy), )))) @@ -318,9 +325,11 @@ pub extern "C" fn regorus_rvm_load_program( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; - let program = to_ref(program)?.program.clone(); + let program = to_shared_ref(program as *const RegorusProgram)? + .program + .clone(); guard.load_program(program); Ok(()) }()) @@ -332,7 +341,7 @@ pub extern "C" fn regorus_rvm_load_program( pub extern "C" fn regorus_rvm_set_data(vm: *mut RegorusRvm, data: *const c_char) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; let data_value = Value::from_json_str(&from_c_str(data)?)?; guard.set_data(data_value)?; @@ -349,7 +358,7 @@ pub extern "C" fn regorus_rvm_set_input( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; let input_value = Value::from_json_str(&from_c_str(input)?)?; guard.set_input(input_value); @@ -366,7 +375,7 @@ pub extern "C" fn regorus_rvm_set_max_instructions( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; guard.set_max_instructions(max_instructions); Ok(()) @@ -382,7 +391,7 @@ pub extern "C" fn regorus_rvm_set_strict_builtin_errors( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; guard.set_strict_builtin_errors(strict); Ok(()) @@ -395,7 +404,7 @@ pub extern "C" fn regorus_rvm_set_strict_builtin_errors( pub extern "C" fn regorus_rvm_set_execution_mode(vm: *mut RegorusRvm, mode: u8) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; let mode = match mode { 0 => ExecutionMode::RunToCompletion, @@ -413,7 +422,7 @@ pub extern "C" fn regorus_rvm_set_execution_mode(vm: *mut RegorusRvm, mode: u8) pub extern "C" fn regorus_rvm_set_step_mode(vm: *mut RegorusRvm, enabled: bool) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; guard.set_step_mode(enabled); Ok(()) @@ -430,7 +439,7 @@ pub extern "C" fn regorus_rvm_set_execution_timer_config( ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; if has_config { guard.set_execution_timer_config(Some(config.to_execution_timer_config()?)); @@ -447,7 +456,7 @@ pub extern "C" fn regorus_rvm_set_execution_timer_config( pub extern "C" fn regorus_rvm_execute(vm: *mut RegorusRvm) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; let result = guard.execute()?; result.to_json_str() @@ -468,7 +477,7 @@ pub extern "C" fn regorus_rvm_execute_entry_point_by_name( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; let name = from_c_str(entry_point)?; let result = guard.execute_entry_point_by_name(&name)?; @@ -490,7 +499,7 @@ pub extern "C" fn regorus_rvm_execute_entry_point_by_index( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; let result = guard.execute_entry_point_by_index(index)?; result.to_json_str() @@ -512,7 +521,7 @@ pub extern "C" fn regorus_rvm_resume( ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let mut guard = vm.try_write()?; let value = if has_value { Some(Value::from_json_str(&from_c_str(resume_value_json)?)?) @@ -535,7 +544,7 @@ pub extern "C" fn regorus_rvm_resume( pub extern "C" fn regorus_rvm_get_execution_state(vm: *mut RegorusRvm) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let vm = to_ref(vm)?; + let vm = to_shared_ref(vm as *const RegorusRvm)?; let guard = vm.try_read()?; let state: ExecutionState = guard.execution_state().clone(); Ok(format!("{:?}", state)) From c7014f8646a949aa0b1f57dea834d503904aa8aa Mon Sep 17 00:00:00 2001 From: Anand Krishnamoorthi Date: Thu, 21 May 2026 11:57:49 -0500 Subject: [PATCH 2/2] feat(ffi): add Azure Policy JSON compilation FFI and C# bindings - AliasRegistry builder pattern: RegorusAliasRegistryBuilder (mutable, single-threaded) + RegorusAliasRegistry (immutable, Arc-wrapped) - Azure Policy JSON compilation: regorus_compile_azure_policy_rule and regorus_compile_azure_policy_definition with alias registry support - regorus_rvm_set_context for host-supplied ambient data - C# AliasRegistryBuilder and AliasRegistry classes with convenience factories (FromJson, FromManifest, Empty) - C# AzurePolicyCompiler static class for policy rule/definition compilation - Compile functions take *const RegorusAliasRegistry (read-only via to_shared_ref for concurrent compilation safety) - Fix pre-existing clippy warnings across multiple crates Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- bindings/csharp/Directory.Packages.props | 2 +- bindings/csharp/README.md | 73 +++ .../Regorus.Tests/AliasRegistryTests.cs | 27 +- .../Regorus.Tests/AzurePolicyCompilerTests.cs | 436 +++++++++++++ .../csharp/Regorus.Tests/AzurePolicyTests.cs | 18 +- bindings/csharp/Regorus/AliasRegistry.cs | 103 ++- .../csharp/Regorus/AliasRegistryBuilder.cs | 69 ++ .../csharp/Regorus/AzurePolicyCompiler.cs | 183 ++++++ bindings/csharp/Regorus/NativeMethods.cs | 66 +- bindings/csharp/Regorus/Program.cs | 2 +- bindings/csharp/Regorus/ResultHelpers.cs | 24 + bindings/csharp/Regorus/Rvm.cs | 18 + bindings/csharp/Regorus/SafeHandles.cs | 32 +- bindings/csharp/TargetExampleApp/Program.cs | 79 +++ bindings/ffi/Cargo.lock | 6 +- bindings/ffi/Cargo.toml | 2 +- bindings/ffi/src/alias_registry.rs | 286 +++++--- bindings/ffi/src/compile.rs | 615 +++++++++++++++++- bindings/ffi/src/rvm.rs | 27 + bindings/java/src/lib.rs | 2 +- .../destructuring_planner/assignment.rs | 2 +- .../azure_policy/parser/policy_definition.rs | 21 +- src/tests/scheduler/analyzer/mod.rs | 2 +- 23 files changed, 1873 insertions(+), 222 deletions(-) create mode 100644 bindings/csharp/Regorus.Tests/AzurePolicyCompilerTests.cs create mode 100644 bindings/csharp/Regorus/AliasRegistryBuilder.cs create mode 100644 bindings/csharp/Regorus/AzurePolicyCompiler.cs 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..0d5669bf 100644 --- a/bindings/csharp/README.md +++ b/bindings/csharp/README.md @@ -150,3 +150,76 @@ 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 = AliasRegistry.FromJson(AliasesJson); + +// 2. Compile a JSON policy rule (the native Azure Policy language) +const string PolicyRule = """ +{ + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "equals": false } + ] + }, + "then": { "effect": "deny" } +} +"""; + +using var program = AzurePolicyCompiler.CompilePolicyRule(registry, PolicyRule); + +// 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/AliasRegistryTests.cs b/bindings/csharp/Regorus.Tests/AliasRegistryTests.cs index 3d3dc8e3..06052f41 100644 --- a/bindings/csharp/Regorus.Tests/AliasRegistryTests.cs +++ b/bindings/csharp/Regorus.Tests/AliasRegistryTests.cs @@ -43,31 +43,28 @@ public class AliasRegistryTests [TestMethod] public void Create_and_dispose_succeeds() { - using var registry = new AliasRegistry(); + using var registry = AliasRegistry.Empty(); Assert.AreEqual(0, registry.Length); } [TestMethod] public void LoadJson_populates_registry() { - using var registry = new AliasRegistry(); - registry.LoadJson(AliasesJson); + using var registry = AliasRegistry.FromJson(AliasesJson); Assert.AreEqual(1, registry.Length); } [TestMethod] public void LoadManifest_populates_registry() { - using var registry = new AliasRegistry(); - registry.LoadManifest(ManifestJson); + using var registry = AliasRegistry.FromManifest(ManifestJson); Assert.AreEqual(1, registry.Length); } [TestMethod] public void NormalizeAndWrap_produces_envelope() { - using var registry = new AliasRegistry(); - registry.LoadJson(AliasesJson); + using var registry = AliasRegistry.FromJson(AliasesJson); var resource = @"{ ""name"": ""acct1"", @@ -93,8 +90,7 @@ public void NormalizeAndWrap_produces_envelope() [TestMethod] public void NormalizeAndWrap_with_context_and_parameters() { - using var registry = new AliasRegistry(); - registry.LoadJson(AliasesJson); + using var registry = AliasRegistry.FromJson(AliasesJson); var resource = @"{ ""name"": ""acct1"", @@ -115,8 +111,7 @@ public void NormalizeAndWrap_with_context_and_parameters() [TestMethod] public void Denormalize_restores_properties() { - using var registry = new AliasRegistry(); - registry.LoadJson(AliasesJson); + using var registry = AliasRegistry.FromJson(AliasesJson); var normalized = @"{ ""name"": ""acct1"", @@ -137,8 +132,7 @@ public void Denormalize_restores_properties() [TestMethod] public void Round_trip_normalize_then_denormalize() { - using var registry = new AliasRegistry(); - registry.LoadJson(AliasesJson); + using var registry = AliasRegistry.FromJson(AliasesJson); var resource = @"{ ""name"": ""acct1"", @@ -166,8 +160,7 @@ public void Round_trip_normalize_then_denormalize() [TestMethod] public void DataPlane_manifest_normalize() { - using var registry = new AliasRegistry(); - registry.LoadManifest(ManifestJson); + using var registry = AliasRegistry.FromManifest(ManifestJson); var resource = @"{ ""type"": ""Microsoft.KeyVault.Data/vaults/certificates"", @@ -185,7 +178,7 @@ public void DataPlane_manifest_normalize() [ExpectedException(typeof(InvalidOperationException))] public void LoadJson_invalid_throws() { - using var registry = new AliasRegistry(); - registry.LoadJson("not valid json"); + using var builder = new AliasRegistryBuilder(); + builder.LoadJson("not valid json"); } } diff --git a/bindings/csharp/Regorus.Tests/AzurePolicyCompilerTests.cs b/bindings/csharp/Regorus.Tests/AzurePolicyCompilerTests.cs new file mode 100644 index 00000000..152a48bb --- /dev/null +++ b/bindings/csharp/Regorus.Tests/AzurePolicyCompilerTests.cs @@ -0,0 +1,436 @@ +// 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 rule that checks the resource type. + private const string SimpleAuditRule = @"{ + ""if"": { + ""field"": ""type"", + ""equals"": ""Microsoft.Storage/storageAccounts"" + }, + ""then"": { ""effect"": ""audit"" } + }"; + + /// Policy rule that uses an alias to check HTTPS-only. + private const string HttpsDenyRule = @"{ + ""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 rule, load it into an RVM, set input, and execute. + /// Returns the result string from ExecuteEntryPoint("main"). + /// + private static string? CompileAndEval( + AliasRegistry? registry, + string policyRuleJson, + string inputJson) + { + using var program = AzurePolicyCompiler.CompilePolicyRule(registry, policyRuleJson); + using var vm = new Rvm(); + vm.LoadProgram(program); + vm.SetInputJson(inputJson); + return vm.ExecuteEntryPoint("main"); + } + + // ----------------------------------------------------------------------- + // CompilePolicyRule tests + // ----------------------------------------------------------------------- + + [TestMethod] + public void CompilePolicyRule_no_aliases_succeeds() + { + using var program = AzurePolicyCompiler.CompilePolicyRule(null, SimpleAuditRule); + Assert.IsNotNull(program); + } + + [TestMethod] + public void CompilePolicyRule_with_aliases_succeeds() + { + using var registry = AliasRegistry.FromJson(StorageAliasesJson); + + using var program = AzurePolicyCompiler.CompilePolicyRule(registry, HttpsDenyRule); + Assert.IsNotNull(program); + } + + [TestMethod] + [ExpectedException(typeof(ArgumentNullException))] + public void CompilePolicyRule_null_json_throws() + { + AzurePolicyCompiler.CompilePolicyRule(null, null!); + } + + [TestMethod] + [ExpectedException(typeof(InvalidOperationException))] + public void CompilePolicyRule_invalid_json_throws() + { + AzurePolicyCompiler.CompilePolicyRule(null, "not valid json"); + } + + // ----------------------------------------------------------------------- + // CompilePolicyDefinition tests + // ----------------------------------------------------------------------- + + [TestMethod] + public void CompilePolicyDefinition_no_aliases_succeeds() + { + using var program = AzurePolicyCompiler.CompilePolicyDefinition(null, PolicyDefinitionWithParams); + Assert.IsNotNull(program); + } + + [TestMethod] + public void CompilePolicyDefinition_with_aliases_succeeds() + { + using var registry = AliasRegistry.FromJson(StorageAliasesJson); + + using var program = AzurePolicyCompiler.CompilePolicyDefinition(registry, PolicyDefinitionWithParams); + 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, SimpleAuditRule, 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, SimpleAuditRule, 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 = AliasRegistry.FromJson(StorageAliasesJson); + + // Non-compliant: HTTPS not enabled (normalized/lowercased form) + var input = WrapInput( + @"{""type"": ""microsoft.storage/storageaccounts"", ""supportshttpstrafficonly"": false}"); + + using var program = AzurePolicyCompiler.CompilePolicyRule(registry, HttpsDenyRule); + 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 = AliasRegistry.FromJson(StorageAliasesJson); + + // Compliant: HTTPS enabled + var input = WrapInput( + @"{""type"": ""microsoft.storage/storageaccounts"", ""supportshttpstrafficonly"": true}"); + + using var program = AzurePolicyCompiler.CompilePolicyRule(registry, HttpsDenyRule); + 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 = AliasRegistry.FromJson(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 = AliasRegistry.FromJson(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.CompilePolicyRule(registry, HttpsDenyRule); + + // 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 = AliasRegistry.FromJson(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.CompilePolicyRule(registry, HttpsDenyRule); + 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.CompilePolicyRule(null, SimpleAuditRule); + + // 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.CompilePolicyRule(null, SimpleAuditRule); + var listing = program.GenerateListing(); + Assert.IsFalse(string.IsNullOrWhiteSpace(listing), + "generated listing should not be empty"); + } + + // ----------------------------------------------------------------------- + // Context-dependent policy tests + // ----------------------------------------------------------------------- + + /// Policy rule that uses subscription() context function. + private const string ContextPolicyRule = @"{ + ""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.CompilePolicyRule(null, ContextPolicyRule); + 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.CompilePolicyRule(null, ContextPolicyRule); + 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.Tests/AzurePolicyTests.cs b/bindings/csharp/Regorus.Tests/AzurePolicyTests.cs index 1ff35c4f..86217337 100644 --- a/bindings/csharp/Regorus.Tests/AzurePolicyTests.cs +++ b/bindings/csharp/Regorus.Tests/AzurePolicyTests.cs @@ -62,8 +62,7 @@ public class AzurePolicyTests [TestMethod] public void AliasRegistry_NormalizeAndWrap_produces_input_envelope() { - using var registry = new AliasRegistry(); - registry.LoadJson(StorageAliasesJson); + using var registry = AliasRegistry.FromJson(StorageAliasesJson); var result = registry.NormalizeAndWrap( StorageResourceJson, @@ -84,8 +83,7 @@ public void AliasRegistry_NormalizeAndWrap_produces_input_envelope() [TestMethod] public void AliasRegistry_NormalizeAndWrap_flattens_properties() { - using var registry = new AliasRegistry(); - registry.LoadJson(StorageAliasesJson); + using var registry = AliasRegistry.FromJson(StorageAliasesJson); var result = registry.NormalizeAndWrap(StorageResourceJson); Assert.IsNotNull(result); @@ -107,8 +105,7 @@ public void AliasRegistry_NormalizeAndWrap_flattens_properties() [TestMethod] public void AliasRegistry_NormalizeAndWrap_preserves_type_field() { - using var registry = new AliasRegistry(); - registry.LoadJson(StorageAliasesJson); + using var registry = AliasRegistry.FromJson(StorageAliasesJson); var result = registry.NormalizeAndWrap(StorageResourceJson); var doc = JsonNode.Parse(result!); @@ -125,8 +122,7 @@ public void AliasRegistry_NormalizeAndWrap_preserves_type_field() [TestMethod] public void AliasRegistry_NormalizeAndWrap_includes_parameters() { - using var registry = new AliasRegistry(); - registry.LoadJson(StorageAliasesJson); + using var registry = AliasRegistry.FromJson(StorageAliasesJson); var parametersJson = @"{ ""effect"": ""Deny"" }"; var result = registry.NormalizeAndWrap( @@ -143,8 +139,7 @@ public void AliasRegistry_NormalizeAndWrap_includes_parameters() [TestMethod] public void AliasRegistry_Denormalize_roundtrips_correctly() { - using var registry = new AliasRegistry(); - registry.LoadJson(StorageAliasesJson); + using var registry = AliasRegistry.FromJson(StorageAliasesJson); // Normalize the ARM resource. var envelope = registry.NormalizeAndWrap(StorageResourceJson); @@ -177,8 +172,7 @@ public void AliasRegistry_loads_test_aliases_file() } var aliasesJson = File.ReadAllText(aliasesPath); - using var registry = new AliasRegistry(); - registry.LoadJson(aliasesJson); + using var registry = AliasRegistry.FromJson(aliasesJson); // The test_aliases.json file contains multiple providers. Assert.IsTrue(registry.Length > 0, diff --git a/bindings/csharp/Regorus/AliasRegistry.cs b/bindings/csharp/Regorus/AliasRegistry.cs index e14a1316..fa96dcc2 100644 --- a/bindings/csharp/Regorus/AliasRegistry.cs +++ b/bindings/csharp/Regorus/AliasRegistry.cs @@ -8,51 +8,43 @@ namespace Regorus { /// - /// Manages Azure Policy alias definitions used for resource normalization + /// Immutable Azure Policy alias registry used for resource normalization /// and policy compilation. /// public unsafe sealed class AliasRegistry : SafeHandleWrapper { + internal AliasRegistry(RegorusAliasRegistryHandle handle) + : base(handle, nameof(AliasRegistry)) + { + } + /// - /// Create an empty alias registry. + /// Create an empty immutable alias registry. /// - public AliasRegistry() - : base(RegorusAliasRegistryHandle.Create(), nameof(AliasRegistry)) + public static AliasRegistry Empty() { + using var builder = new AliasRegistryBuilder(); + return builder.Build(); } /// - /// Load control-plane alias data (array of ProviderAliases) from a JSON string. + /// Create an immutable alias registry from control-plane alias JSON. /// - /// JSON array of ProviderAliases (e.g. from Get-AzPolicyAlias or ResourceTypesAndAliases.json) - public void LoadJson(string json) + public static AliasRegistry FromJson(string json) { - Utf8Marshaller.WithUtf8(json, jsonPtr => - { - UseHandle(regPtr => - { - CheckAndDropResult(API.regorus_alias_registry_load_json( - (RegorusAliasRegistry*)regPtr, (byte*)jsonPtr)); - return 0; - }); - }); + using var builder = new AliasRegistryBuilder(); + builder.LoadJson(json); + return builder.Build(); } /// - /// Load a data-plane policy manifest from a JSON string. + /// Create an immutable alias registry from a data-plane manifest JSON document. /// - /// JSON object containing a DataPolicyManifest - public void LoadManifest(string json) + public static AliasRegistry FromManifest(string json) { - Utf8Marshaller.WithUtf8(json, jsonPtr => - { - UseHandle(regPtr => - { - CheckAndDropResult(API.regorus_alias_registry_load_manifest( - (RegorusAliasRegistry*)regPtr, (byte*)jsonPtr)); - return 0; - }); - }); + using var builder = new AliasRegistryBuilder(); + builder.LoadManifest(json); + return builder.Build(); } /// @@ -74,11 +66,6 @@ public long Length /// Normalize an ARM resource JSON and wrap it into the standard input envelope /// expected by a compiled Azure Policy program. /// - /// Raw ARM resource JSON - /// API version string (e.g. "2023-01-01"), or null to use default alias paths - /// Additional context JSON object (pass "{}" if none) - /// Policy parameter values JSON (pass "{}" if none) - /// JSON string: { "resource": <normalized>, "context": <context>, "parameters": <params> } public string? NormalizeAndWrap(string resourceJson, string? apiVersion = null, string contextJson = "{}", string parametersJson = "{}") { return Utf8Marshaller.WithUtf8(resourceJson, resPtr => @@ -96,27 +83,22 @@ public long Length (byte*)ctxPtr, (byte*)paramsPtr)); }); } - else - { - return Utf8Marshaller.WithUtf8(apiVersion, apiPtr => - UseHandle(regPtr => - { - return ResultHelpers.GetStringResult( - API.regorus_alias_registry_normalize_and_wrap( - (RegorusAliasRegistry*)regPtr, - (byte*)resPtr, (byte*)apiPtr, - (byte*)ctxPtr, (byte*)paramsPtr)); - })); - } + + return Utf8Marshaller.WithUtf8(apiVersion, apiPtr => + UseHandle(regPtr => + { + return ResultHelpers.GetStringResult( + API.regorus_alias_registry_normalize_and_wrap( + (RegorusAliasRegistry*)regPtr, + (byte*)resPtr, (byte*)apiPtr, + (byte*)ctxPtr, (byte*)paramsPtr)); + })); }))); } /// /// Denormalize a previously-normalized resource JSON back to ARM format. /// - /// The normalized resource JSON - /// API version string, or null to use default alias paths - /// Denormalized ARM JSON string public string? Denormalize(string normalizedJson, string? apiVersion = null) { return Utf8Marshaller.WithUtf8(normalizedJson, normPtr => @@ -131,23 +113,16 @@ public long Length (byte*)normPtr, null)); }); } - else - { - return Utf8Marshaller.WithUtf8(apiVersion, apiPtr => - UseHandle(regPtr => - { - return ResultHelpers.GetStringResult( - API.regorus_alias_registry_denormalize( - (RegorusAliasRegistry*)regPtr, - (byte*)normPtr, (byte*)apiPtr)); - })); - } - }); - } - private static string? CheckAndDropResult(RegorusResult result) - { - return ResultHelpers.GetStringResult(result); + return Utf8Marshaller.WithUtf8(apiVersion, apiPtr => + UseHandle(regPtr => + { + return ResultHelpers.GetStringResult( + API.regorus_alias_registry_denormalize( + (RegorusAliasRegistry*)regPtr, + (byte*)normPtr, (byte*)apiPtr)); + })); + }); } } } diff --git a/bindings/csharp/Regorus/AliasRegistryBuilder.cs b/bindings/csharp/Regorus/AliasRegistryBuilder.cs new file mode 100644 index 00000000..11b2f797 --- /dev/null +++ b/bindings/csharp/Regorus/AliasRegistryBuilder.cs @@ -0,0 +1,69 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System; +using Regorus.Internal; + +#nullable enable +namespace Regorus +{ + /// + /// Mutable, single-threaded builder for . + /// Load alias data, then call to freeze the registry. + /// + public unsafe sealed class AliasRegistryBuilder : SafeHandleWrapper + { + /// + /// Create an empty alias registry builder. + /// + public AliasRegistryBuilder() + : base(RegorusAliasRegistryBuilderHandle.Create(), nameof(AliasRegistryBuilder)) + { + } + + /// + /// Load control-plane alias data (array of ProviderAliases) from a JSON string. + /// + public void LoadJson(string json) + { + Utf8Marshaller.WithUtf8(json, jsonPtr => + { + UseHandle(builderPtr => + { + ResultHelpers.GetStringResult(API.regorus_alias_registry_builder_load_json( + (RegorusAliasRegistryBuilder*)builderPtr, + (byte*)jsonPtr)); + }); + }); + } + + /// + /// Load a data-plane policy manifest from a JSON string. + /// + public void LoadManifest(string json) + { + Utf8Marshaller.WithUtf8(json, jsonPtr => + { + UseHandle(builderPtr => + { + ResultHelpers.GetStringResult(API.regorus_alias_registry_builder_load_manifest( + (RegorusAliasRegistryBuilder*)builderPtr, + (byte*)jsonPtr)); + }); + }); + } + + /// + /// Freeze the builder into an immutable, thread-safe alias registry. + /// + public AliasRegistry Build() + { + return UseHandle(builderPtr => + { + var registryPtr = ResultHelpers.GetPointerResult( + API.regorus_alias_registry_builder_build((RegorusAliasRegistryBuilder*)builderPtr)); + return new AliasRegistry(RegorusAliasRegistryHandle.FromPointer(registryPtr)); + }); + } + } +} diff --git a/bindings/csharp/Regorus/AzurePolicyCompiler.cs b/bindings/csharp/Regorus/AzurePolicyCompiler.cs new file mode 100644 index 00000000..a58bceea --- /dev/null +++ b/bindings/csharp/Regorus/AzurePolicyCompiler.cs @@ -0,0 +1,183 @@ +// 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 with and freeze them into an . + /// Normalize the ARM resource via . + /// Compile the JSON policyRule with or the + /// full 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 an Azure Policy JSON policy rule 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 policyRule object, e.g. + /// { "if": { "field": "type", "equals": "..." }, "then": { "effect": "deny" } } + /// + /// + /// A compiled ready to be loaded into an + /// instance. + /// + /// + /// Thrown when is null. + /// + /// + /// Thrown when parsing or compilation fails. + /// + public static Program CompilePolicyRule(AliasRegistry? aliasRegistry, string policyRuleJson) + { + if (policyRuleJson is null) + { + throw new ArgumentNullException(nameof(policyRuleJson)); + } + + return Utf8Marshaller.WithUtf8(policyRuleJson, rulePtr => + { + if (aliasRegistry is null) + { + var result = API.regorus_compile_azure_policy_rule( + null, (byte*)rulePtr); + return GetProgramResult(result); + } + else + { + return aliasRegistry.UseHandleForInterop(regPtr => + { + var result = API.regorus_compile_azure_policy_rule( + (RegorusAliasRegistry*)regPtr, (byte*)rulePtr); + return GetProgramResult(result); + }); + } + }); + } + + /// + /// 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..327d5b15 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,20 @@ 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 an Azure Policy JSON policy rule into an RVM program. + /// + [DllImport(LibraryName, EntryPoint = "regorus_compile_azure_policy_rule", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_compile_azure_policy_rule( + RegorusAliasRegistry* registry, byte* policy_rule_json); + + /// + /// 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 @@ -673,28 +695,40 @@ internal static unsafe partial class API #region Alias Registry Methods /// - /// Create a new, empty AliasRegistry. + /// Create a new alias registry builder. /// - [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - internal static extern RegorusAliasRegistry* regorus_alias_registry_new(); + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_new", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusAliasRegistryBuilder* regorus_alias_registry_builder_new(); /// - /// Drop an AliasRegistry. + /// Drop an alias registry builder. /// - [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - internal static extern void regorus_alias_registry_drop(RegorusAliasRegistry* registry); + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern void regorus_alias_registry_builder_drop(RegorusAliasRegistryBuilder* builder); /// - /// Load control-plane alias data (array of ProviderAliases) into the registry. + /// Load control-plane alias data into the builder. /// - [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - internal static extern RegorusResult regorus_alias_registry_load_json(RegorusAliasRegistry* registry, byte* json); + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_load_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_alias_registry_builder_load_json(RegorusAliasRegistryBuilder* builder, byte* json); /// - /// Load a data-plane policy manifest into the registry. + /// Load a data-plane policy manifest into the builder. /// - [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_load_manifest", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] - internal static extern RegorusResult regorus_alias_registry_load_manifest(RegorusAliasRegistry* registry, byte* json); + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_load_manifest", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_alias_registry_builder_load_manifest(RegorusAliasRegistryBuilder* builder, byte* json); + + /// + /// Freeze a builder into an immutable alias registry. + /// + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_builder_build", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_alias_registry_builder_build(RegorusAliasRegistryBuilder* builder); + + /// + /// Drop an AliasRegistry. + /// + [DllImport(LibraryName, EntryPoint = "regorus_alias_registry_drop", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern void regorus_alias_registry_drop(RegorusAliasRegistry* registry); /// /// Return the number of resource types loaded in the alias registry. @@ -923,6 +957,14 @@ internal unsafe partial struct RegorusPolicyModule public byte* content; } + /// + /// Wrapper for AliasRegistryBuilder. + /// + [StructLayout(LayoutKind.Sequential)] + internal unsafe partial struct RegorusAliasRegistryBuilder + { + } + /// /// Wrapper for AliasRegistry. /// 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/ResultHelpers.cs b/bindings/csharp/Regorus/ResultHelpers.cs index 07c8e331..8ae1a2ec 100644 --- a/bindings/csharp/Regorus/ResultHelpers.cs +++ b/bindings/csharp/Regorus/ResultHelpers.cs @@ -69,5 +69,29 @@ internal static long GetIntResult(RegorusResult result) API.regorus_result_drop(result); } } + + + internal static IntPtr GetPointerResult(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 InvalidOperationException("Expected pointer result."); + } + + return (IntPtr)result.pointer_value; + } + finally + { + API.regorus_result_drop(result); + } + } } } diff --git a/bindings/csharp/Regorus/Rvm.cs b/bindings/csharp/Regorus/Rvm.cs index f38ecf21..45c2fda3 100644 --- a/bindings/csharp/Regorus/Rvm.cs +++ b/bindings/csharp/Regorus/Rvm.cs @@ -106,6 +106,24 @@ 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. + /// + 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/Regorus/SafeHandles.cs b/bindings/csharp/Regorus/SafeHandles.cs index 1808314b..c5e104bd 100644 --- a/bindings/csharp/Regorus/SafeHandles.cs +++ b/bindings/csharp/Regorus/SafeHandles.cs @@ -184,28 +184,48 @@ protected override bool ReleaseHandle() } } - internal sealed class RegorusAliasRegistryHandle : SafeHandleZeroOrMinusOneIsInvalid + internal sealed class RegorusAliasRegistryBuilderHandle : SafeHandleZeroOrMinusOneIsInvalid { - private RegorusAliasRegistryHandle() : base(ownsHandle: true) + private RegorusAliasRegistryBuilderHandle() : base(ownsHandle: true) { } - internal static RegorusAliasRegistryHandle Create() + internal static RegorusAliasRegistryBuilderHandle Create() { unsafe { - var raw = Internal.API.regorus_alias_registry_new(); + var raw = Internal.API.regorus_alias_registry_builder_new(); if (raw is null) { - throw new InvalidOperationException("Failed to create Regorus alias registry."); + throw new InvalidOperationException("Failed to create Regorus alias registry builder."); } - var handle = new RegorusAliasRegistryHandle(); + var handle = new RegorusAliasRegistryBuilderHandle(); handle.SetHandle((IntPtr)raw); return handle; } } + protected override bool ReleaseHandle() + { + if (!IsInvalid) + { + unsafe + { + Internal.API.regorus_alias_registry_builder_drop((Internal.RegorusAliasRegistryBuilder*)handle); + } + SetHandle(IntPtr.Zero); + } + return true; + } + } + + internal sealed class RegorusAliasRegistryHandle : SafeHandleZeroOrMinusOneIsInvalid + { + private RegorusAliasRegistryHandle() : base(ownsHandle: true) + { + } + internal static RegorusAliasRegistryHandle FromPointer(IntPtr pointer) { if (pointer == IntPtr.Zero) diff --git a/bindings/csharp/TargetExampleApp/Program.cs b/bindings/csharp/TargetExampleApp/Program.cs index 7d815935..49dee458 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,80 @@ 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_RULE = @"{ + ""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 = Regorus.AliasRegistry.FromJson(STORAGE_ALIASES_JSON); + Console.WriteLine("Loaded storage account aliases"); + + // 2. Compile the JSON policy rule directly (no Rego needed) + using var program = Regorus.AzurePolicyCompiler.CompilePolicyRule(registry, HTTPS_DENY_RULE); + Console.WriteLine("Compiled Azure Policy JSON rule 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..254f89d1 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", @@ -1837,9 +1837,9 @@ dependencies = [ [[package]] name = "zerofrom" -version = "0.1.8" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272" +checksum = "69faa1f2a1ea75661980b013019ed6687ed0e83d069bc1114e2cc74c6c04c4df" dependencies = [ "zerofrom-derive", ] 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..802c6025 100644 --- a/bindings/ffi/src/alias_registry.rs +++ b/bindings/ffi/src/alias_registry.rs @@ -5,66 +5,108 @@ #![cfg(feature = "azure_policy")] -use crate::common::{from_c_str, to_ref, RegorusResult, RegorusStatus}; +use crate::common::{from_c_str, to_ref, to_shared_ref, RegorusResult, RegorusStatus}; use crate::panic_guard::with_unwind_guard; use alloc::boxed::Box; use alloc::format; use alloc::string::String; -use anyhow::Result; -use core::ffi::c_char; -use core::ptr; +use alloc::sync::Arc; +use anyhow::{anyhow, Result}; +use core::ffi::{c_char, c_void}; +use core::{mem, ptr}; use regorus::languages::azure_policy::aliases::AliasRegistry; -/// Opaque wrapper for `AliasRegistry`. -pub struct RegorusAliasRegistry { +/// Mutable builder for `AliasRegistry`. +/// +/// This handle is intentionally single-threaded and must not be used +/// concurrently. Callers should finish loading alias data and then freeze it +/// into a `RegorusAliasRegistry` via `regorus_alias_registry_builder_build`. +pub struct RegorusAliasRegistryBuilder { registry: AliasRegistry, + built: bool, +} + +impl RegorusAliasRegistryBuilder { + fn new() -> Self { + Self { + registry: AliasRegistry::new(), + built: false, + } + } + + fn registry_mut(&mut self) -> Result<&mut AliasRegistry> { + if self.built { + return Err(anyhow!("alias registry builder has already been built")); + } + Ok(&mut self.registry) + } + + fn build(&mut self) -> Result { + if self.built { + return Err(anyhow!("alias registry builder has already been built")); + } + + self.built = true; + Ok(RegorusAliasRegistry { + registry: Arc::new(mem::replace(&mut self.registry, AliasRegistry::new())), + }) + } +} + +/// Frozen, immutable alias registry. +pub struct RegorusAliasRegistry { + registry: Arc, +} + +impl RegorusAliasRegistry { + /// Return a shared reference to the inner registry for use by the compiler. + pub(crate) fn inner(&self) -> Arc { + Arc::clone(&self.registry) + } } // --------------------------------------------------------------------------- -// Lifecycle +// Builder lifecycle // --------------------------------------------------------------------------- -/// Create a new, empty `AliasRegistry`. +/// Create a new, empty `AliasRegistry` builder. /// -/// The caller must eventually call `regorus_alias_registry_drop` to free the handle. +/// The caller must eventually call `regorus_alias_registry_builder_drop`. #[no_mangle] -pub extern "C" fn regorus_alias_registry_new() -> *mut RegorusAliasRegistry { - let wrapper = RegorusAliasRegistry { - registry: AliasRegistry::new(), - }; - Box::into_raw(Box::new(wrapper)) +pub extern "C" fn regorus_alias_registry_builder_new() -> *mut RegorusAliasRegistryBuilder { + Box::into_raw(Box::new(RegorusAliasRegistryBuilder::new())) } -/// Drop a `RegorusAliasRegistry`. +/// Drop a `RegorusAliasRegistryBuilder`. #[no_mangle] -pub extern "C" fn regorus_alias_registry_drop(registry: *mut RegorusAliasRegistry) { - if let Ok(r) = to_ref(registry) { +pub extern "C" fn regorus_alias_registry_builder_drop(builder: *mut RegorusAliasRegistryBuilder) { + if let Ok(builder) = to_ref(builder) { unsafe { - let _ = Box::from_raw(ptr::from_mut(r)); + let _ = Box::from_raw(ptr::from_mut(builder)); } } } // --------------------------------------------------------------------------- -// Loading +// Builder loading // --------------------------------------------------------------------------- -/// Load control-plane alias data (array of `ProviderAliases`) into the registry. +/// Load control-plane alias data (array of `ProviderAliases`) into the builder. /// /// `json` must be a valid null-terminated UTF-8 string containing the JSON /// array returned by `Get-AzPolicyAlias` or the static /// `ResourceTypesAndAliases.json` file. #[no_mangle] -pub extern "C" fn regorus_alias_registry_load_json( - registry: *mut RegorusAliasRegistry, +pub extern "C" fn regorus_alias_registry_builder_load_json( + builder: *mut RegorusAliasRegistryBuilder, json: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result<()> { let json_str = from_c_str(json)?; - to_ref(registry)?.registry.load_from_json(&json_str)?; + to_ref(builder)?.registry_mut()?.load_from_json(&json_str)?; Ok(()) }(); @@ -78,20 +120,20 @@ pub extern "C" fn regorus_alias_registry_load_json( }) } -/// Load a data-plane policy manifest into the registry. +/// Load a data-plane policy manifest into the builder. /// /// `json` must be a valid null-terminated UTF-8 string containing a single /// `DataPolicyManifest` JSON object. #[no_mangle] -pub extern "C" fn regorus_alias_registry_load_manifest( - registry: *mut RegorusAliasRegistry, +pub extern "C" fn regorus_alias_registry_builder_load_manifest( + builder: *mut RegorusAliasRegistryBuilder, json: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result<()> { let json_str = from_c_str(json)?; - to_ref(registry)? - .registry + to_ref(builder)? + .registry_mut()? .load_data_policy_manifest_json(&json_str)?; Ok(()) }(); @@ -106,16 +148,52 @@ pub extern "C" fn regorus_alias_registry_load_manifest( }) } +/// Freeze a builder into an immutable `RegorusAliasRegistry`. +#[no_mangle] +pub extern "C" fn regorus_alias_registry_builder_build( + builder: *mut RegorusAliasRegistryBuilder, +) -> RegorusResult { + with_unwind_guard(|| { + let output = || -> Result<*mut RegorusAliasRegistry> { + let registry = to_ref(builder)?.build()?; + Ok(Box::into_raw(Box::new(registry))) + }(); + + match output { + Ok(registry) => RegorusResult::ok_pointer(registry as *mut c_void), + Err(e) => { + RegorusResult::err_with_message(RegorusStatus::InvalidArgument, format!("{e}")) + } + } + }) +} + +// --------------------------------------------------------------------------- +// Frozen registry lifecycle +// --------------------------------------------------------------------------- + +/// Drop a `RegorusAliasRegistry`. +#[no_mangle] +pub extern "C" fn regorus_alias_registry_drop(registry: *mut RegorusAliasRegistry) { + if let Ok(registry) = to_ref(registry) { + unsafe { + let _ = Box::from_raw(ptr::from_mut(registry)); + } + } +} + // --------------------------------------------------------------------------- -// Queries +// Frozen registry queries // --------------------------------------------------------------------------- /// Return the number of resource types loaded in the alias registry. #[no_mangle] -pub extern "C" fn regorus_alias_registry_len(registry: *mut RegorusAliasRegistry) -> RegorusResult { +pub extern "C" fn regorus_alias_registry_len( + registry: *const RegorusAliasRegistry, +) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { - let len = to_ref(registry)?.registry.len(); + let len = to_shared_ref(registry)?.registry.len(); Ok(len as i64) }(); @@ -134,15 +212,9 @@ pub extern "C" fn regorus_alias_registry_len(registry: *mut RegorusAliasRegistry /// /// Returns a JSON string: /// `{ "resource": , "context": , "parameters": }`. -/// -/// * `resource_json` – raw ARM resource JSON -/// * `api_version` – API version string (e.g. `"2023-01-01"`), or null to use -/// the default alias paths -/// * `context_json` – JSON object for additional context (pass `"{}"` if none) -/// * `parameters_json` – JSON object of policy parameter values (pass `"{}"` if none) #[no_mangle] pub extern "C" fn regorus_alias_registry_normalize_and_wrap( - registry: *mut RegorusAliasRegistry, + registry: *const RegorusAliasRegistry, resource_json: *const c_char, api_version: *const c_char, context_json: *const c_char, @@ -168,7 +240,7 @@ pub extern "C" fn regorus_alias_registry_normalize_and_wrap( let context = regorus::Value::from_json_str(&context_str)?; let params = regorus::Value::from_json_str(¶ms_str)?; - let wrapped = to_ref(registry)?.registry.normalize_and_wrap( + let wrapped = to_shared_ref(registry)?.registry.normalize_and_wrap( &resource, api_ver.as_deref(), Some(context), @@ -185,14 +257,9 @@ pub extern "C" fn regorus_alias_registry_normalize_and_wrap( } /// Denormalize a previously-normalized resource JSON back to ARM format. -/// -/// * `normalized_json` – the normalized resource JSON -/// * `api_version` – API version string, or null to use the default alias paths -/// -/// Returns the denormalized ARM JSON string. #[no_mangle] pub extern "C" fn regorus_alias_registry_denormalize( - registry: *mut RegorusAliasRegistry, + registry: *const RegorusAliasRegistry, normalized_json: *const c_char, api_version: *const c_char, ) -> RegorusResult { @@ -212,7 +279,7 @@ pub extern "C" fn regorus_alias_registry_denormalize( let normalized = regorus::Value::from_json_str(&normalized_str)?; - let result = to_ref(registry)? + let result = to_shared_ref(registry)? .registry .denormalize(&normalized, api_ver.as_deref()); result.to_json_str() @@ -232,12 +299,10 @@ mod tests { use core::ffi::CStr; use std::ffi::CString; - /// Helper: create a C string from a Rust &str. fn c(s: &str) -> CString { CString::new(s).expect("CString::new failed") } - /// Helper: assert a RegorusResult has Ok status and extract string output. fn assert_ok_string(r: &RegorusResult) -> String { assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status"); assert!(!r.output.is_null(), "expected non-null output"); @@ -248,12 +313,51 @@ mod tests { s } - /// Helper: assert a RegorusResult has Ok status with integer output. fn assert_ok_int(r: &RegorusResult) -> i64 { assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status"); r.int_value } + fn assert_ok_pointer(r: &RegorusResult) -> *mut c_void { + assert_eq!(r.status, RegorusStatus::Ok, "expected Ok status"); + assert!(matches!( + r.data_type, + crate::common::RegorusDataType::Pointer + )); + assert!(!r.pointer_value.is_null()); + r.pointer_value + } + + fn build_registry_with_json(json: &str) -> *mut RegorusAliasRegistry { + let builder = regorus_alias_registry_builder_new(); + let json = c(json); + + let r = regorus_alias_registry_builder_load_json(builder, json.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let r = regorus_alias_registry_builder_build(builder); + let registry = assert_ok_pointer(&r) as *mut RegorusAliasRegistry; + regorus_result_drop(r); + regorus_alias_registry_builder_drop(builder); + registry + } + + fn build_registry_with_manifest(json: &str) -> *mut RegorusAliasRegistry { + let builder = regorus_alias_registry_builder_new(); + let json = c(json); + + let r = regorus_alias_registry_builder_load_manifest(builder, json.as_ptr()); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let r = regorus_alias_registry_builder_build(builder); + let registry = assert_ok_pointer(&r) as *mut RegorusAliasRegistry; + regorus_result_drop(r); + regorus_alias_registry_builder_drop(builder); + registry + } + const ALIASES: &str = r#"[{ "namespace": "Microsoft.Storage", "resourceTypes": [{ @@ -279,20 +383,21 @@ mod tests { }"#; #[test] - fn lifecycle_new_and_drop() { - let reg = regorus_alias_registry_new(); - assert!(!reg.is_null()); - regorus_alias_registry_drop(reg); + fn lifecycle_builder_build_and_drop() { + let builder = regorus_alias_registry_builder_new(); + assert!(!builder.is_null()); + + let r = regorus_alias_registry_builder_build(builder); + let registry = assert_ok_pointer(&r) as *mut RegorusAliasRegistry; + regorus_result_drop(r); + + regorus_alias_registry_builder_drop(builder); + regorus_alias_registry_drop(registry); } #[test] fn load_json_and_check_len() { - let reg = regorus_alias_registry_new(); - let json = c(ALIASES); - - let r = regorus_alias_registry_load_json(reg, json.as_ptr()); - assert_eq!(r.status, RegorusStatus::Ok); - regorus_result_drop(r); + let reg = build_registry_with_json(ALIASES); let r = regorus_alias_registry_len(reg); assert_eq!(assert_ok_int(&r), 1); @@ -303,12 +408,7 @@ mod tests { #[test] fn load_manifest_and_check_len() { - let reg = regorus_alias_registry_new(); - let json = c(MANIFEST); - - let r = regorus_alias_registry_load_manifest(reg, json.as_ptr()); - assert_eq!(r.status, RegorusStatus::Ok); - regorus_result_drop(r); + let reg = build_registry_with_manifest(MANIFEST); let r = regorus_alias_registry_len(reg); assert_eq!(assert_ok_int(&r), 1); @@ -319,24 +419,40 @@ mod tests { #[test] fn load_invalid_json_returns_error() { - let reg = regorus_alias_registry_new(); + let builder = regorus_alias_registry_builder_new(); let bad = c("not valid json"); - let r = regorus_alias_registry_load_json(reg, bad.as_ptr()); + let r = regorus_alias_registry_builder_load_json(builder, bad.as_ptr()); assert_ne!(r.status, RegorusStatus::Ok); regorus_result_drop(r); - regorus_alias_registry_drop(reg); + regorus_alias_registry_builder_drop(builder); } #[test] - fn normalize_and_wrap_round_trip() { - let reg = regorus_alias_registry_new(); + fn builder_cannot_be_reused_after_build() { + let builder = regorus_alias_registry_builder_new(); + let r = regorus_alias_registry_builder_build(builder); + let registry = assert_ok_pointer(&r) as *mut RegorusAliasRegistry; + regorus_result_drop(r); + let aliases = c(ALIASES); - let r = regorus_alias_registry_load_json(reg, aliases.as_ptr()); - assert_eq!(r.status, RegorusStatus::Ok); + let r = regorus_alias_registry_builder_load_json(builder, aliases.as_ptr()); + assert_ne!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let r = regorus_alias_registry_builder_build(builder); + assert_ne!(r.status, RegorusStatus::Ok); regorus_result_drop(r); + regorus_alias_registry_builder_drop(builder); + regorus_alias_registry_drop(registry); + } + + #[test] + fn normalize_and_wrap_round_trip() { + let reg = build_registry_with_json(ALIASES); + let resource = c(r#"{ "name": "acct1", "type": "Microsoft.Storage/storageAccounts", @@ -346,7 +462,6 @@ mod tests { let ctx = c(r#"{"resourceGroup": {"name": "rg1"}}"#); let params = c(r#"{"env": "prod"}"#); - // Normalize let r = regorus_alias_registry_normalize_and_wrap( reg, resource.as_ptr(), @@ -357,7 +472,6 @@ mod tests { let envelope_json = assert_ok_string(&r); regorus_result_drop(r); - // Parse and verify structure let envelope: serde_json::Value = serde_json::from_str(&envelope_json).expect("invalid JSON output"); assert!( @@ -373,16 +487,13 @@ mod tests { "envelope missing 'context'" ); - // The normalized resource should have lowercased alias fields let res = &envelope["resource"]; assert_eq!(res["supportshttpstrafficonly"], true); assert_eq!(res["name"], "acct1"); - // Context and parameters should be passed through assert_eq!(envelope["context"]["resourceGroup"]["name"], "rg1"); assert_eq!(envelope["parameters"]["env"], "prod"); - // Denormalize the resource portion let resource_json = serde_json::to_string(&res).expect("serialize resource"); let norm_cstr = c(&resource_json); @@ -392,7 +503,6 @@ mod tests { let denorm: serde_json::Value = serde_json::from_str(&denorm_json).expect("invalid denorm JSON"); - // Should be back under properties with restored casing assert_eq!( denorm["properties"]["supportsHttpsTrafficOnly"], true, "expected restored casing under properties" @@ -403,11 +513,7 @@ mod tests { #[test] fn denormalize_invalid_json_returns_error() { - let reg = regorus_alias_registry_new(); - let aliases = c(ALIASES); - let r = regorus_alias_registry_load_json(reg, aliases.as_ptr()); - assert_eq!(r.status, RegorusStatus::Ok); - regorus_result_drop(r); + let reg = build_registry_with_json(ALIASES); let bad = c("not json"); let api = c("2023-01-01"); @@ -420,11 +526,7 @@ mod tests { #[test] fn normalize_data_plane_manifest() { - let reg = regorus_alias_registry_new(); - let manifest = c(MANIFEST); - let r = regorus_alias_registry_load_manifest(reg, manifest.as_ptr()); - assert_eq!(r.status, RegorusStatus::Ok); - regorus_result_drop(r); + let reg = build_registry_with_manifest(MANIFEST); let resource = c(r#"{ "type": "Microsoft.KeyVault.Data/vaults/certificates", @@ -453,7 +555,12 @@ mod tests { #[test] fn empty_registry_normalize() { - let reg = regorus_alias_registry_new(); + let builder = regorus_alias_registry_builder_new(); + let r = regorus_alias_registry_builder_build(builder); + let reg = assert_ok_pointer(&r) as *mut RegorusAliasRegistry; + regorus_result_drop(r); + regorus_alias_registry_builder_drop(builder); + let resource = c(r#"{"name": "test", "type": "Unknown/type", "properties": {"foo": 1}}"#); let api = c(""); let ctx = c("{}"); @@ -470,7 +577,6 @@ mod tests { regorus_result_drop(r); let envelope: serde_json::Value = serde_json::from_str(&json).expect("invalid JSON"); - // Without aliases, properties should still be flattened assert_eq!(envelope["resource"]["foo"], 1); assert_eq!(envelope["resource"]["name"], "test"); diff --git a/bindings/ffi/src/compile.rs b/bindings/ffi/src/compile.rs index ba1ee483..f10e508f 100644 --- a/bindings/ffi/src/compile.rs +++ b/bindings/ffi/src/compile.rs @@ -1,6 +1,6 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -use crate::common::{from_c_str, RegorusResult, RegorusStatus}; +use crate::common::{from_c_str, to_shared_ref, RegorusResult, RegorusStatus}; use crate::compiled_policy::RegorusCompiledPolicy; use crate::panic_guard::with_unwind_guard; use alloc::boxed::Box; @@ -208,6 +208,220 @@ fn convert_c_modules_to_rust( Ok(policy_modules) } +// --------------------------------------------------------------------------- +// Azure Policy JSON compilation +// --------------------------------------------------------------------------- + +/// Compile an Azure Policy JSON policy rule into an RVM program. +/// +/// Parses the JSON `policyRule` (the `{ "if": ..., "then": ... }` object), +/// resolves aliases using the provided registry, and compiles the result +/// into an RVM [`Program`] that can be loaded into a [`RegorusRvm`]. +/// +/// # Parameters +/// * `registry` - Alias registry handle, or null. +/// * `policy_rule_json` - JSON string containing the policyRule object +/// +/// # Null registry behavior +/// +/// When `registry` is null, compilation proceeds **without alias resolution**. +/// Field references that correspond to Azure resource provider aliases +/// (e.g. `Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly`) will +/// be compiled as raw property paths rather than being resolved to their +/// short forms. This means: +/// +/// - Policies that rely on aliases will **silently produce incorrect +/// evaluation results** because the field paths won't match the +/// normalized resource structure. +/// - **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_rule_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_rule( + registry: *const crate::alias_registry::RegorusAliasRegistry, + policy_rule_json: *const c_char, +) -> RegorusResult { + use crate::alias_registry::RegorusAliasRegistry; + 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_rule_json).map_err(|e| { + ( + RegorusStatus::InvalidDataFormat, + format!("Invalid policy rule JSON string: {e}"), + ) + })?; + + let source = Source::from_contents("policy_rule".into(), json_str).map_err(|e| { + ( + RegorusStatus::InvalidDataFormat, + format!("Failed to create source: {e}"), + ) + })?; + + let ast = parser::parse_policy_rule(&source).map_err(|e| { + ( + RegorusStatus::InvalidPolicy, + format!("Failed to parse policy rule: {e}"), + ) + })?; + + let program = if registry.is_null() { + compiler::compile_policy_rule(&ast) + } else { + let reg: &RegorusAliasRegistry = to_shared_ref(registry).map_err(|e| { + ( + RegorusStatus::InvalidArgument, + format!("Invalid alias registry: {e}"), + ) + })?; + compiler::compile_policy_rule_with_aliases(&ast, reg.inner()) + }; + + 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 rule: {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), + } + }) +} + +/// 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: *const crate::alias_registry::RegorusAliasRegistry, + policy_definition_json: *const c_char, +) -> RegorusResult { + use crate::alias_registry::RegorusAliasRegistry; + 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_shared_ref(registry).map_err(|e| { + ( + RegorusStatus::InvalidArgument, + format!("Invalid alias registry: {e}"), + ) + })?; + compiler::compile_policy_definition_with_aliases(&defn, reg.inner()) + }; + + 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 +429,402 @@ 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; + 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": [] + }] + }] + }]"#; + + const SIMPLE_POLICY_RULE: &str = r#"{ + "if": { + "field": "type", + "equals": "Microsoft.Storage/storageAccounts" + }, + "then": { "effect": "audit" } + }"#; + + const ALIAS_POLICY_RULE: &str = r#"{ + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "field": "Microsoft.Storage/storageAccounts/supportsHttpsTrafficOnly", "equals": false } + ] + }, + "then": { "effect": "deny" } + }"#; + + const POLICY_DEFINITION: &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')]" } + } + }"#; + + /// 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}}}"#) + } + + fn build_registry_with_json( + json: &str, + ) -> *mut crate::alias_registry::RegorusAliasRegistry { + let builder = crate::alias_registry::regorus_alias_registry_builder_new(); + let json_c = c(json); + let r = crate::alias_registry::regorus_alias_registry_builder_load_json( + builder, + json_c.as_ptr(), + ); + assert_eq!(r.status, RegorusStatus::Ok); + regorus_result_drop(r); + + let r = crate::alias_registry::regorus_alias_registry_builder_build(builder); + let registry = + assert_ok_pointer(&r) as *mut crate::alias_registry::RegorusAliasRegistry; + regorus_result_drop(r); + crate::alias_registry::regorus_alias_registry_builder_drop(builder); + registry + } + + /// Helper: compile a policy rule, execute it with input, and return the + /// result string. + unsafe fn compile_and_eval_rule( + registry: *const crate::alias_registry::RegorusAliasRegistry, + policy_rule: &str, + input_json: &str, + ) -> String { + let rule_c = c(policy_rule); + let r = regorus_compile_azure_policy_rule(registry, rule_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_rule_no_aliases() { + let rule_c = c(SIMPLE_POLICY_RULE); + let r = regorus_compile_azure_policy_rule(core::ptr::null_mut(), rule_c.as_ptr()); + let ptr = assert_ok_pointer(&r); + regorus_result_drop(r); + regorus_program_drop(ptr as *mut RegorusProgram); + } + + #[test] + fn compile_rule_with_aliases() { + let reg = build_registry_with_json(ALIASES); + + let rule_c = c(ALIAS_POLICY_RULE); + let r = regorus_compile_azure_policy_rule(reg, rule_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_rule_matching() { + let input = wrap_input(r#"{"type":"microsoft.storage/storageaccounts"}"#, "{}"); + let result = + unsafe { compile_and_eval_rule(core::ptr::null_mut(), SIMPLE_POLICY_RULE, &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_rule_not_matching() { + let input = wrap_input(r#"{"type":"microsoft.compute/virtualmachines"}"#, "{}"); + let result = + unsafe { compile_and_eval_rule(core::ptr::null_mut(), SIMPLE_POLICY_RULE, &input) }; + // When the "if" condition doesn't match, the result should be undefined + assert!( + result.contains("undefined"), + "expected undefined for non-matching input, got: {result}" + ); + } + + #[test] + fn compile_and_eval_alias_rule_deny() { + let reg = build_registry_with_json(ALIASES); + + // 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_rule(reg, ALIAS_POLICY_RULE, &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_rule_compliant() { + let reg = build_registry_with_json(ALIASES); + + // Compliant resource: HTTPS enabled (normalized form) + let input = wrap_input( + r#"{"type": "microsoft.storage/storageaccounts", "supportshttpstrafficonly": true}"#, + "{}", + ); + let result = unsafe { compile_and_eval_rule(reg, ALIAS_POLICY_RULE, &input) }; + assert!( + result.contains("undefined"), + "expected undefined for compliant resource, got: {result}" + ); + + regorus_alias_registry_drop(reg); + } + + #[test] + fn compile_definition_no_aliases() { + let defn_c = c(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_and_eval() { + let reg = build_registry_with_json(ALIASES); + + let defn_c = c(POLICY_DEFINITION); + 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); + + // Evaluate with a non-compliant resource (normalized form, wrapped in envelope) + 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(); + // The default parameter value is "deny" + 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_rule(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); + } + + /// Policy rule that uses a context function (subscription()). + const CONTEXT_POLICY_RULE: &str = r#"{ + "if": { + "allOf": [ + { "field": "type", "equals": "Microsoft.Storage/storageAccounts" }, + { "value": "[subscription().subscriptionId]", "equals": "sub-123" } + ] + }, + "then": { "effect": "deny" } + }"#; + + #[test] + fn context_policy_evaluates_with_set_context() { + let rule_c = c(CONTEXT_POLICY_RULE); + let r = regorus_compile_azure_policy_rule(core::ptr::null_mut(), rule_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 rule_c = c(CONTEXT_POLICY_RULE); + let r = regorus_compile_azure_policy_rule(core::ptr::null_mut(), rule_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); + } + } +} diff --git a/bindings/ffi/src/rvm.rs b/bindings/ffi/src/rvm.rs index 44831db5..7603305c 100644 --- a/bindings/ffi/src/rvm.rs +++ b/bindings/ffi/src/rvm.rs @@ -367,6 +367,33 @@ 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. +/// +/// # 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(|| { + to_regorus_result(|| -> Result<()> { + let vm = to_shared_ref(vm as *const RegorusRvm)?; + let mut guard = vm.try_write()?; + let context_value = Value::from_json_str(&from_c_str(context_json)?)?; + guard.set_context(context_value); + Ok(()) + }()) + }) +} + /// Set the maximum number of instructions that can execute. #[no_mangle] pub extern "C" fn regorus_rvm_set_max_instructions( diff --git a/bindings/java/src/lib.rs b/bindings/java/src/lib.rs index 9bd64395..7fcb5f74 100644 --- a/bindings/java/src/lib.rs +++ b/bindings/java/src/lib.rs @@ -462,7 +462,7 @@ pub extern "system" fn Java_com_microsoft_regorus_Program_nativeCompileFromModul } let mut modules = Vec::with_capacity(ids.len()); - for (id, content) in ids.into_iter().zip(contents.into_iter()) { + for (id, content) in ids.into_iter().zip(contents) { modules.push(PolicyModule { id: Rc::from(id.as_str()), content: Rc::from(content.as_str()), diff --git a/src/compiler/destructuring_planner/assignment.rs b/src/compiler/destructuring_planner/assignment.rs index 3e88bdb3..16931828 100644 --- a/src/compiler/destructuring_planner/assignment.rs +++ b/src/compiler/destructuring_planner/assignment.rs @@ -314,7 +314,7 @@ fn order_element_pairs( if ready { let (value_expr, plan, _deps, binds) = remaining.remove(idx); - scheduled.extend(binds.into_iter()); + scheduled.extend(binds); ordered.push((value_expr, plan)); progress = true; break; diff --git a/src/languages/azure_policy/parser/policy_definition.rs b/src/languages/azure_policy/parser/policy_definition.rs index b6175edf..99d263a5 100644 --- a/src/languages/azure_policy/parser/policy_definition.rs +++ b/src/languages/azure_policy/parser/policy_definition.rs @@ -264,18 +264,17 @@ impl<'source> Parser<'source> { "metadata" => { *metadata = Some(self.parse_json_value()?); } - "parameters" => { + "parameters" if self.token_text() == "{" => { // Parameters must be a JSON object; if not, push to extra. - if self.token_text() == "{" { - *parameters = self.parse_parameter_definitions()?; - } else { - let value = self.parse_json_value()?; - extra.push(ObjectEntry { - key_span, - key: key.into(), - value, - }); - } + *parameters = self.parse_parameter_definitions()?; + } + "parameters" => { + let value = self.parse_json_value()?; + extra.push(ObjectEntry { + key_span, + key: key.into(), + value, + }); } "policyrule" => { // Parse the policyRule directly from the token stream! diff --git a/src/tests/scheduler/analyzer/mod.rs b/src/tests/scheduler/analyzer/mod.rs index 73614004..6e2524d4 100644 --- a/src/tests/scheduler/analyzer/mod.rs +++ b/src/tests/scheduler/analyzer/mod.rs @@ -88,7 +88,7 @@ fn analyze_file(regos: &[String], expected_scopes: &[Scope]) -> Result<()> { } } - scopes.sort_by(|a, b| a.0.span.line.cmp(&b.0.span.line)); + scopes.sort_by_key(|a| a.0.span.line); for (idx, (_, scope)) in scopes.iter().enumerate() { if idx > expected_scopes.len() { bail!("extra scope generated.")