// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use crate::common::{ from_c_str, to_ref, to_regorus_result, to_regorus_string_result, RegorusResult, RegorusStatus, }; use crate::compiled_policy::RegorusCompiledPolicy; use crate::limits::RegorusExecutionTimerConfig; use crate::limits::RegorusPolicyLengthConfig; use crate::lock::{new_handle, read, try_read, try_write, Handle, ReadGuard, WriteGuard}; use crate::panic_guard::with_unwind_guard; use alloc::boxed::Box; use alloc::format; use alloc::string::String; #[cfg(feature = "rvm")] use alloc::sync::Arc; use alloc::vec::Vec; use anyhow::{anyhow, Result}; use core::ffi::{c_char, c_void}; use core::ptr; #[cfg(feature = "rvm")] use regorus::languages::rego::compiler::Compiler; #[cfg(feature = "rvm")] use regorus::rvm::program::Program; /// Wrapper for `regorus::Engine`. pub struct RegorusEngine { engine: Handle<::regorus::Engine>, } impl RegorusEngine { fn new(engine: ::regorus::Engine) -> Self { Self { engine: new_handle(engine), } } fn contention_error() -> anyhow::Error { anyhow!( "regorus engine handle is already in use; clone the engine before sharing across threads" ) } fn try_write(&self) -> Result> { try_write(&self.engine).ok_or_else(Self::contention_error) } fn try_read(&self) -> Result> { try_read(&self.engine).ok_or_else(Self::contention_error) } } impl Clone for RegorusEngine { fn clone(&self) -> Self { let guard = read(&self.engine); Self::new((*guard).clone()) } } #[cfg(all(test, feature = "contention_checks", feature = "std"))] mod tests { use super::RegorusEngine; #[test] fn detects_handle_contention() { let engine = RegorusEngine::new(::regorus::Engine::new()); let _first_guard = engine.try_write().expect("initial lock should succeed"); let err = engine .try_write() .expect_err("contention detection must reject the second lock"); assert!( err.to_string().contains("engine handle is already in use"), "unexpected error message: {err}" ); } } #[cfg(all(test, feature = "std"))] mod panic_tests { use super::{ regorus_engine_drop, regorus_engine_eval_query, regorus_engine_get_policies, regorus_engine_new, }; use crate::common::{regorus_result_drop, RegorusStatus}; use crate::panic_guard::{is_poisoned, reset_poison}; use alloc::boxed::Box; use regorus::Value; use std::ffi::{CStr, CString}; #[test] fn catches_extension_panics_and_marks_poison() { reset_poison(); let engine_ptr = regorus_engine_new(); assert!(!engine_ptr.is_null(), "engine allocation must succeed"); assert!(!is_poisoned(), "guard must start unpoisoned"); unsafe { let engine = &mut *engine_ptr; { let mut guard = engine .try_write() .expect("exclusive access to configure engine"); guard .add_extension( "panic_extension".to_string(), 0, Box::new(|_| -> anyhow::Result { panic!("ffi extension panic") }), ) .expect("extension registration must succeed"); guard .add_policy( "panic.rego".to_string(), "package panic\n\ndefault allow = false\n\nallow if {\n panic_extension()\n}" .to_string(), ) .expect("policy registration must succeed"); } } let query = CString::new("data.panic.allow").expect("valid query string"); let panic_result = regorus_engine_eval_query(engine_ptr, query.as_ptr()); assert!(matches!(panic_result.status, RegorusStatus::Panic)); unsafe { assert!( !panic_result.error_message.is_null(), "panic details must be present" ); let message = CStr::from_ptr(panic_result.error_message) .to_str() .expect("error message utf8"); assert!( message.contains("ffi extension panic"), "panic payload must bubble across guard" ); } regorus_result_drop(panic_result); assert!(is_poisoned(), "engine must be marked poisoned after panic"); let poisoned_result = regorus_engine_get_policies(engine_ptr); assert!(matches!(poisoned_result.status, RegorusStatus::Poisoned)); unsafe { assert!( !poisoned_result.error_message.is_null(), "poison message must be present" ); let message = CStr::from_ptr(poisoned_result.error_message) .to_str() .expect("poison message utf8"); assert!( message.contains("regorus is poisoned"), "poison message must inform callers" ); } regorus_result_drop(poisoned_result); regorus_engine_drop(engine_ptr); reset_poison(); } } #[no_mangle] #[cfg(feature = "std")] pub extern "C" fn regorus_engine_test_trigger_panic() -> RegorusResult { with_unwind_guard(|| panic!("regorus ffi test panic")) } #[no_mangle] pub extern "C" fn regorus_engine_test_reset_poison() { crate::panic_guard::reset_poison(); } #[no_mangle] /// Construct a new Engine /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html pub extern "C" fn regorus_engine_new() -> *mut RegorusEngine { let mut engine = ::regorus::Engine::new(); // For more OPA compatibility out of the box, we ask builtins to return undefined // instead of raising errors in certain failure scenarios. engine.set_strict_builtin_errors(false); Box::into_raw(Box::new(RegorusEngine::new(engine))) } /// Clone a [`RegorusEngine`] /// /// To avoid having to parse same policy again, the engine can be cloned /// after policies and data have been added. /// #[no_mangle] pub extern "C" fn regorus_engine_clone(engine: *mut RegorusEngine) -> *mut RegorusEngine { match to_ref(engine) { Ok(e) => Box::into_raw(Box::new(e.clone())), _ => ptr::null_mut(), } } #[no_mangle] pub extern "C" fn regorus_engine_drop(engine: *mut RegorusEngine) { if let Ok(e) = to_ref(engine) { unsafe { let _ = Box::from_raw(ptr::from_mut(e)); } } } /// Add a policy /// /// The policy is parsed into AST. /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_policy /// /// * `path`: A filename to be associated with the policy. /// * `rego`: Rego policy. #[no_mangle] pub extern "C" fn regorus_engine_add_policy( engine: *mut RegorusEngine, path: *const c_char, rego: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_string_result(|| -> Result { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.add_policy(from_c_str(path)?, from_c_str(rego)?) }()) }) } #[cfg(feature = "std")] #[no_mangle] pub extern "C" fn regorus_engine_add_policy_from_file( engine: *mut RegorusEngine, path: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_string_result(|| -> Result { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.add_policy_from_file(from_c_str(path)?) }()) }) } /// Add policy data. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.add_data /// * `data`: JSON encoded value to be used as policy data. #[no_mangle] pub extern "C" fn regorus_engine_add_data_json( engine: *mut RegorusEngine, data: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.add_data(regorus::Value::from_json_str(&from_c_str(data)?)?) }()) }) } /// Get list of loaded Rego packages as JSON. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_packages #[no_mangle] 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 guard = engine.try_read()?; serde_json::to_string_pretty(&guard.get_packages()?).map_err(anyhow::Error::msg) }()) }) } /// Get list of policies as JSON. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_policies #[no_mangle] 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 guard = engine.try_read()?; guard.get_policies_as_json() }()) }) } #[cfg(feature = "std")] #[no_mangle] pub extern "C" fn regorus_engine_add_data_from_json_file( engine: *mut RegorusEngine, path: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.add_data(regorus::Value::from_json_file(from_c_str(path)?)?) }()) }) } /// Clear policy data. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_data #[no_mangle] 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 mut guard = engine.try_write()?; guard.clear_data(); Ok(()) }()) }) } /// Set input. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_input /// * `input`: JSON encoded value to be used as input to query. #[no_mangle] pub extern "C" fn regorus_engine_set_input_json( engine: *mut RegorusEngine, input: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.set_input(regorus::Value::from_json_str(&from_c_str(input)?)?); Ok(()) }()) }) } #[cfg(feature = "std")] #[no_mangle] pub extern "C" fn regorus_engine_set_input_from_json_file( engine: *mut RegorusEngine, path: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.set_input(regorus::Value::from_json_file(from_c_str(path)?)?); Ok(()) }()) }) } /// Evaluate query. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.eval_query /// * `query`: Rego expression to be evaluate. #[no_mangle] pub extern "C" fn regorus_engine_eval_query( engine: *mut RegorusEngine, query: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; let results = guard.eval_query(from_c_str(query)?, false)?; Ok(serde_json::to_string_pretty(&results)?) }(); match output { Ok(out) => RegorusResult::ok_string(out), Err(e) => to_regorus_result(Err(e)), } }) } /// Evaluate specified rule. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.eval_rule /// * `rule`: Path to the rule. #[no_mangle] pub extern "C" fn regorus_engine_eval_rule( engine: *mut RegorusEngine, rule: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.eval_rule(from_c_str(rule)?)?.to_json_str() }(); match output { Ok(out) => RegorusResult::ok_string(out), Err(e) => to_regorus_result(Err(e)), } }) } /// Enable/disable coverage. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_enable_coverage /// * `enable`: Whether to enable or disable coverage. #[no_mangle] #[cfg(feature = "coverage")] pub extern "C" fn regorus_engine_set_enable_coverage( engine: *mut RegorusEngine, enable: bool, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.set_enable_coverage(enable); Ok(()) }()) }) } /// Get coverage report. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.get_coverage_report #[no_mangle] #[cfg(feature = "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 guard = engine.try_read()?; Ok(serde_json::to_string_pretty(&guard.get_coverage_report()?)?) }(); match output { Ok(out) => RegorusResult::ok_string(out), Err(e) => to_regorus_result(Err(e)), } }) } /// Enable/disable strict builtin errors. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_strict_builtin_errors /// * `strict`: Whether to raise errors or return undefined on certain scenarios. #[no_mangle] pub extern "C" fn regorus_engine_set_strict_builtin_errors( engine: *mut RegorusEngine, strict: bool, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.set_strict_builtin_errors(strict); Ok(()) }()) }) } #[no_mangle] /// Configure the execution timer for a specific engine instance. 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(()) }()) } #[no_mangle] /// Clear the engine-specific execution timer configuration. 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(()) }()) } /// Set the policy length limits used when loading policies. #[no_mangle] pub extern "C" fn regorus_engine_set_policy_length_config( engine: *mut RegorusEngine, config: RegorusPolicyLengthConfig, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.set_policy_length_config(config.to_policy_length_config()?); Ok(()) }()) }) } /// Clear the policy length configuration, reverting to defaults. #[no_mangle] pub extern "C" fn regorus_engine_clear_policy_length_config( engine: *mut RegorusEngine, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.clear_policy_length_config(); Ok(()) }()) }) } /// Get pretty printed coverage report. /// /// See https://docs.rs/regorus/latest/regorus/coverage/struct.Report.html#method.to_string_pretty #[no_mangle] #[cfg(feature = "coverage")] pub extern "C" fn regorus_engine_get_coverage_report_pretty( engine: *mut RegorusEngine, ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { let engine = to_ref(engine)?; let guard = engine.try_read()?; guard.get_coverage_report()?.to_string_pretty() }(); match output { Ok(out) => RegorusResult::ok_string(out), Err(e) => to_regorus_result(Err(e)), } }) } /// Clear coverage data. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.clear_coverage_data #[no_mangle] #[cfg(feature = "coverage")] 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 mut guard = engine.try_write()?; guard.clear_coverage_data(); Ok(()) }()) }) } /// Whether to gather output of print statements. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_gather_prints /// * `enable`: Whether to enable or disable gathering print statements. #[no_mangle] pub extern "C" fn regorus_engine_set_gather_prints( engine: *mut RegorusEngine, enable: bool, ) -> RegorusResult { with_unwind_guard(|| { to_regorus_result(|| -> Result<()> { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.set_gather_prints(enable); Ok(()) }()) }) } /// Take all the gathered print statements. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.take_prints #[no_mangle] pub extern "C" fn regorus_engine_take_prints(engine: *mut RegorusEngine) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; Ok(serde_json::to_string_pretty(&guard.take_prints()?)?) }(); match output { Ok(out) => RegorusResult::ok_string(out), Err(e) => to_regorus_result(Err(e)), } }) } /// Get AST of policies. /// /// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_ast_as_json #[no_mangle] #[cfg(feature = "ast")] 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 guard = engine.try_read()?; guard.get_ast_as_json() }(); match output { Ok(out) => RegorusResult::ok_string(out), Err(e) => to_regorus_result(Err(e)), } }) } /// Gets the package names defined in each policy added to the engine. /// /// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_package_names #[no_mangle] #[cfg(feature = "azure_policy")] pub extern "C" fn regorus_engine_get_policy_package_names( engine: *mut RegorusEngine, ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { let engine = to_ref(engine)?; let guard = engine.try_read()?; serde_json::to_string_pretty(&guard.get_policy_package_names()?) .map_err(anyhow::Error::msg) }(); match output { Ok(out) => RegorusResult::ok_string(out), Err(e) => to_regorus_result(Err(e)), } }) } /// Gets the parameters defined in each policy added to the engine. /// /// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_parameters #[no_mangle] #[cfg(feature = "azure_policy")] pub extern "C" fn regorus_engine_get_policy_parameters( engine: *mut RegorusEngine, ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result { let engine = to_ref(engine)?; let guard = engine.try_read()?; serde_json::to_string_pretty(&guard.get_policy_parameters()?) .map_err(anyhow::Error::msg) }(); match output { Ok(out) => RegorusResult::ok_string(out), Err(e) => to_regorus_result(Err(e)), } }) } /// Enable/disable rego v1. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.set_rego_v0 #[no_mangle] pub extern "C" fn regorus_engine_set_rego_v0( engine: *mut RegorusEngine, enable: bool, ) -> RegorusResult { with_unwind_guard(|| { let output = || -> Result<()> { let engine = to_ref(engine)?; let mut guard = engine.try_write()?; guard.set_rego_v0(enable); Ok(()) }(); match output { Ok(()) => RegorusResult::ok_void(), Err(e) => to_regorus_result(Err(e)), } }) } /// Compile a target-aware policy from the current engine state. /// /// This method creates a compiled policy that can work with Azure Policy targets, /// enabling resource type inference and target-specific evaluation. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.compile_for_target #[no_mangle] #[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) { Ok(engine) => engine, Err(e) => { return RegorusResult::err_with_message( RegorusStatus::InvalidArgument, format!("Failed to get engine reference: {e}"), ) } }; let mut guard = match engine.try_write() { Ok(guard) => guard, Err(e) => { return RegorusResult::err_with_message( RegorusStatus::Error, format!("Failed to lock engine: {e}"), ) } }; match guard.compile_for_target() { Ok(compiled_policy) => { let wrapped_policy = RegorusCompiledPolicy { compiled_policy }; let boxed_policy = Box::new(wrapped_policy); RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void) } Err(e) => RegorusResult::err_with_message( RegorusStatus::CompilationFailed, format!("Failed to compile for target: {e}"), ), } }) } /// Compile a policy with a specific entry point rule. /// /// This method creates a compiled policy that evaluates a specific rule as the entry point. /// /// See https://docs.rs/regorus/latest/regorus/struct.Engine.html#method.compile_with_entrypoint /// * `rule`: The specific rule path to evaluate (e.g., "data.policy.allow") #[no_mangle] pub extern "C" fn regorus_engine_compile_with_entrypoint( engine: *mut RegorusEngine, rule: *const c_char, ) -> RegorusResult { with_unwind_guard(|| { let result = || -> Result { let rule_str = from_c_str(rule)?; let rule_rc: regorus::Rc = rule_str.into(); let engine = to_ref(engine)?; let mut guard = engine.try_write()?; let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?; Ok(RegorusCompiledPolicy { compiled_policy }) }(); match result { Ok(wrapped_policy) => { let boxed_policy = Box::new(wrapped_policy); RegorusResult::ok_pointer(Box::into_raw(boxed_policy) as *mut c_void) } Err(e) => RegorusResult::err_with_message( RegorusStatus::CompilationFailed, format!("Failed to compile with entrypoint: {e}"), ), } }) } /// Compile an RVM program from the engine state with entry points. /// /// * `entry_points` - Array of entry point rule paths /// * `entry_points_len` - Number of entry points #[cfg(feature = "rvm")] #[no_mangle] pub extern "C" fn regorus_engine_compile_program_with_entrypoints( engine: *mut RegorusEngine, entry_points: *const *const c_char, entry_points_len: usize, ) -> RegorusResult { with_unwind_guard(|| { let result = || -> Result> { if entry_points_len == 0 { return Err(anyhow!("entry_points must contain at least one entry")); } if entry_points.is_null() && entry_points_len > 0 { return Err(anyhow!("null entry_points pointer")); } let mut entry_points_vec = Vec::with_capacity(entry_points_len); for i in 0..entry_points_len { unsafe { let entry_ptr = entry_points.add(i); if entry_ptr.is_null() { return Err(anyhow!("null entry point at index {i}")); } let entry = from_c_str(*entry_ptr)?; entry_points_vec.push(entry); } } let entry_points_ref: Vec<&str> = entry_points_vec.iter().map(|s| s.as_str()).collect(); let rule = entry_points_ref .first() .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 mut guard = engine.try_write()?; let compiled_policy = guard.compile_with_entrypoint(&rule_rc)?; let program = Compiler::compile_from_policy(&compiled_policy, &entry_points_ref)?; Ok(program) }(); match result { Ok(program) => { let wrapped = crate::rvm::RegorusProgram { program }; let boxed = Box::new(wrapped); RegorusResult::ok_pointer(Box::into_raw(boxed) as *mut c_void) } Err(e) => RegorusResult::err_with_message( RegorusStatus::CompilationFailed, format!("Failed to compile RVM program: {e}"), ), } }) }