From 9fce2ccc005a0a23c955180aee8ffae726153a6a Mon Sep 17 00:00:00 2001 From: Denis Komissarov <73666576+dekomissMSFT@users.noreply.github.com> Date: Mon, 4 Aug 2025 13:02:22 -0700 Subject: [PATCH] feat: Implement methods to get package names and parameters (#425) --- .gitignore | 5 +- Cargo.toml | 1 + bindings/csharp/Regorus.Tests/RegorusTests.cs | 39 ++++++ bindings/csharp/Regorus/Regorus.cs | 10 ++ bindings/csharp/Regorus/RegorusFFI.cs | 16 +++ bindings/csharp/global.json | 2 +- bindings/ffi/Cargo.toml | 3 +- bindings/ffi/src/lib.rs | 34 +++++ src/engine.rs | 132 ++++++++++++++++++ tests/engine/mod.rs | 96 +++++++++++++ 10 files changed, 335 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index 1bd9f24..783381b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,4 +27,7 @@ bindings/*/target # C# build folders **bin -**obj \ No newline at end of file +**obj + +# Visual Studio folders +**/*.vs \ No newline at end of file diff --git a/Cargo.toml b/Cargo.toml index 9ca8b6f..5184720 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,7 @@ default = ["full-opa", "arc"] arc = ["scientific/arc"] ast = [] +azure_policy = [] base64 = ["dep:data-encoding"] base64url = ["dep:data-encoding"] coverage = [] diff --git a/bindings/csharp/Regorus.Tests/RegorusTests.cs b/bindings/csharp/Regorus.Tests/RegorusTests.cs index c0b341f..b951314 100644 --- a/bindings/csharp/Regorus.Tests/RegorusTests.cs +++ b/bindings/csharp/Regorus.Tests/RegorusTests.cs @@ -173,4 +173,43 @@ public class RegorusTests Assert.IsTrue(JsonNode.DeepEquals(JsonNode.Parse(expected), JsonNode.Parse(result!)), $"Actual: {result}"); } + + [TestMethod] + public void GetPolicyPackageNames_succeeds() + { + using var engine = new Engine(); + engine.AddPolicy( + "test.rego", + "package test\nx = 1\nmessage = `Hello`"); + + engine.AddPolicy( + "test.rego", + "package test.nested.name\nx = 1\nmessage = `Hello`"); + + var result = engine.GetPolicyPackageNames(); + + var packageNames = JsonNode.Parse(result!); + + Assert.AreEqual("test", packageNames![0]["package_name"].ToString()); + Assert.AreEqual("test.nested.name", packageNames![1]["package_name"].ToString()); + } + + [TestMethod] + public void GetPolicyParameters_succeeds() + { + using var engine = new Engine(); + engine.AddPolicy( + "test.rego", + "package test\n default parameters.a = 5\nparameters.b = 10\nx = 1\nmessage = `Hello`"); + + var result = engine.GetPolicyParameters(); + + var parameters = JsonNode.Parse(result!); + + Assert.AreEqual(1, parameters![0]["parameters"].AsArray().Count); + Assert.AreEqual(1, parameters![0]["modifiers"].AsArray().Count); + + Assert.AreEqual("a", parameters![0]["parameters"][0]["name"].ToString()); + Assert.AreEqual("b", parameters![0]["modifiers"][0]["name"].ToString()); + } } \ No newline at end of file diff --git a/bindings/csharp/Regorus/Regorus.cs b/bindings/csharp/Regorus/Regorus.cs index 6afa5cb..c0b53c8 100644 --- a/bindings/csharp/Regorus/Regorus.cs +++ b/bindings/csharp/Regorus/Regorus.cs @@ -217,6 +217,16 @@ namespace Regorus return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_ast_as_json(E)); } + public string? GetPolicyPackageNames() + { + return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_package_names(E)); + } + + public string? GetPolicyParameters() + { + return CheckAndDropResult(Regorus.Internal.API.regorus_engine_get_policy_parameters(E)); + } + string? StringFromUTF8(IntPtr ptr) { diff --git a/bindings/csharp/Regorus/RegorusFFI.cs b/bindings/csharp/Regorus/RegorusFFI.cs index ebefa51..c2c2c9b 100644 --- a/bindings/csharp/Regorus/RegorusFFI.cs +++ b/bindings/csharp/Regorus/RegorusFFI.cs @@ -193,6 +193,22 @@ namespace Regorus.Internal [DllImport(__DllName, EntryPoint = "regorus_engine_get_ast_as_json", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] internal static extern RegorusResult regorus_engine_get_ast_as_json(RegorusEngine* engine); + /// + /// Gets the package names of policies added to the engine. + /// + /// See https://docs.rs/regorus/latest/regorus/coverage/struct.Engine.html#method.get_policy_package_names + /// + [DllImport(__DllName, EntryPoint = "regorus_engine_get_policy_package_names", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_engine_get_policy_package_names(RegorusEngine* engine); + + /// + /// 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 + /// + [DllImport(__DllName, EntryPoint = "regorus_engine_get_policy_parameters", CallingConvention = CallingConvention.Cdecl, ExactSpelling = true)] + internal static extern RegorusResult regorus_engine_get_policy_parameters(RegorusEngine* engine); + /// /// Enable/disable rego v1. /// diff --git a/bindings/csharp/global.json b/bindings/csharp/global.json index a811d8b..559ff1b 100644 --- a/bindings/csharp/global.json +++ b/bindings/csharp/global.json @@ -4,7 +4,7 @@ }, "sdk": { "allowPrerelease": false, - "version": "8.0.408", + "version": "8.0.412", "rollForward": "disable" } } \ No newline at end of file diff --git a/bindings/ffi/Cargo.toml b/bindings/ffi/Cargo.toml index dc3ce06..742fe92 100644 --- a/bindings/ffi/Cargo.toml +++ b/bindings/ffi/Cargo.toml @@ -23,8 +23,9 @@ lto = true codegen-units = 1 [features] -default = ["ast", "std", "coverage", "regorus/arc", "regorus/full-opa"] +default = ["ast", "azure_policy", "std", "coverage", "regorus/arc", "regorus/full-opa"] ast = ["regorus/ast"] +azure_policy = ["regorus/azure_policy"] std = ["regorus/std"] coverage = ["regorus/coverage"] custom_allocator = [] diff --git a/bindings/ffi/src/lib.rs b/bindings/ffi/src/lib.rs index c3f94ba..cea2f33 100644 --- a/bindings/ffi/src/lib.rs +++ b/bindings/ffi/src/lib.rs @@ -457,6 +457,40 @@ pub extern "C" fn regorus_engine_get_ast_as_json(engine: *mut RegorusEngine) -> } } +/// 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 { + let output = || -> Result { serde_json::to_string_pretty(&to_ref(engine)?.engine.get_policy_package_names()?).map_err(anyhow::Error::msg) }(); + match output { + Ok(out) => RegorusResult { + status: RegorusStatus::RegorusStatusOk, + output: to_c_str(out), + error_message: std::ptr::null_mut(), + }, + 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 { + let output = || -> Result { serde_json::to_string_pretty(&to_ref(engine)?.engine.get_policy_parameters()?).map_err(anyhow::Error::msg) }(); + match output { + Ok(out) => RegorusResult { + status: RegorusStatus::RegorusStatusOk, + output: to_c_str(out), + error_message: std::ptr::null_mut(), + }, + 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 diff --git a/src/engine.rs b/src/engine.rs index bf6ef45..8141dff 100644 --- a/src/engine.rs +++ b/src/engine.rs @@ -23,6 +23,31 @@ pub struct Engine { rego_v1: bool, } +#[derive(Debug, Clone, Serialize)] +pub struct PolicyPackageNameDefinition { + pub source_file: String, + pub package_name: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct PolicyParameter { + pub name: String, + pub modifiable: bool, + pub required: bool, +} + +#[derive(Debug, Clone, Serialize)] +pub struct PolicyModifier { + pub name: String, +} + +#[derive(Debug, Clone, Serialize)] +pub struct PolicyParameters { + pub source_file: String, + pub parameters: Vec, + pub modifiers: Vec, +} + /// Create a default engine. impl Default for Engine { fn default() -> Self { @@ -907,6 +932,113 @@ impl Engine { serde_json::to_string_pretty(&ast).map_err(anyhow::Error::msg) } + /// Get the package names of each policy added to the engine. + /// + /// + /// ```rust + /// # use regorus::*; + /// # use anyhow::{bail, Result}; + /// # fn main() -> Result<()> { + /// # let mut engine = Engine::new(); + /// engine.add_policy("test.rego".to_string(), "package test\n x := 1".to_string())?; + /// engine.add_policy("test2.rego".to_string(), "package test.multi.segment\n x := 1".to_string())?; + /// + /// let package_names = engine.get_policy_package_names()?; + /// + /// assert_eq!("test", package_names[0].package_name); + /// assert_eq!("test.multi.segment", package_names[1].package_name); + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "azure_policy")] + #[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))] + pub fn get_policy_package_names(&self) -> Result> { + let mut package_names = vec![]; + for m in &self.modules { + let package_name = Interpreter::get_path_string(&m.package.refr, None)?; + package_names.push(PolicyPackageNameDefinition { + source_file: m.package.span.source.file().to_string(), + package_name, + }); + } + + Ok(package_names) + } + + /// Get the parameters defined in each policy. + /// + /// + /// ```rust + /// # use regorus::*; + /// # use anyhow::{bail, Result}; + /// # fn main() -> Result<()> { + /// # let mut engine = Engine::new(); + /// engine.add_policy("test.rego".to_string(), "package test default parameters.a = 5 parameters.b = 10\n x := 1".to_string())?; + /// + /// let parameters = engine.get_policy_parameters()?; + /// + /// assert_eq!("a", parameters[0].parameters[0].name); + /// assert_eq!("b", parameters[0].modifiers[0].name); + /// + /// # Ok(()) + /// # } + /// ``` + #[cfg(feature = "azure_policy")] + #[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))] + pub fn get_policy_parameters(&self) -> Result> { + let mut policy_parameter_definitions = vec![]; + for m in &self.modules { + let mut parameters = vec![]; + let mut modifiers = vec![]; + + for rule in &m.policy { + // Extract parameter definitions from the policy rule + // e.g. default parameters.a = 5 + if let Rule::Default { refr, .. } = rule.as_ref() { + let path = Parser::get_path_ref_components(refr)?; + let paths: Vec<&str> = path.iter().map(|s| s.text()).collect(); + + if paths.len() == 2 && paths[0] == "parameters" { + // Todo: Fetch fields other than name from rego metadoc for the parameter + parameters.push(PolicyParameter { + name: paths[1].to_string(), + modifiable: false, + required: false, + }) + } + } + + // Extract modifiers to the parameters from the policy rule + // e.g. parameters.a = 5 + if let Rule::Spec { head, .. } = rule.as_ref() { + match head { + RuleHead::Compr { refr, .. } => { + let path = Parser::get_path_ref_components(refr)?; + let paths: Vec<&str> = path.iter().map(|s| s.text()).collect(); + + if paths.len() == 2 && paths[0] == "parameters" { + // Todo: Fetch fields other than name from rego metadoc for the parameter + modifiers.push(PolicyModifier { + name: paths[1].to_string(), + }) + } + } + RuleHead::Func { .. } => {} + RuleHead::Set { .. } => {} + } + } + } + + policy_parameter_definitions.push(PolicyParameters { + source_file: m.package.span.source.file().to_string(), + parameters, + modifiers, + }); + } + + Ok(policy_parameter_definitions) + } + fn make_parser<'a>(&self, source: &'a Source) -> Result> { let mut parser = Parser::new(source)?; if self.rego_v1 { diff --git a/tests/engine/mod.rs b/tests/engine/mod.rs index 7b8fa2b..292bcb9 100644 --- a/tests/engine/mod.rs +++ b/tests/engine/mod.rs @@ -101,3 +101,99 @@ fn extension_with_state() -> Result<()> { Ok(()) } + +#[test] +#[cfg(feature = "azure_policy")] +#[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))] +fn get_policy_package_names() -> Result<()> { + let mut engine = Engine::new(); + engine.add_policy( + "testPolicy1".to_string(), + r#"package test + + deny if { + 1 == 2 + } + "# + .to_string(), + )?; + + engine.add_policy( + "testPolicy2".to_string(), + r#"package test.nested.name + deny if { + 1 == 2 + } + "# + .to_string(), + )?; + + let package_names = engine.get_policy_package_names()?; + + assert_eq!(2, package_names.len()); + assert_eq!("test", package_names[0].package_name); + assert_eq!("testPolicy1", package_names[0].source_file); + + assert_eq!("test.nested.name", package_names[1].package_name); + assert_eq!("testPolicy2", package_names[1].source_file); + Ok(()) +} + +#[test] +#[cfg(feature = "azure_policy")] +#[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))] +fn get_policy_parameters() -> Result<()> { + let mut engine = Engine::new(); + engine.add_policy( + "testPolicy1".to_string(), + r#"package test + default parameters.a = 5 + default parameters.b = { asdf: 10} + + parameters.c = 10 + + deny if { + parameter.a == parameter.b.asdf + } + "# + .to_string(), + )?; + + engine.add_policy( + "testPolicy2".to_string(), + r#"package test + default parameters = { + a: 5, + b: { asdf: 10 } + } + + parameters.c = 5 + + deny if { + parameters.a == parameters.b.asdf + } + "# + .to_string(), + )?; + + let parameters = engine.get_policy_parameters()?; + // let ast = engine.get_ast_as_json()?; + // println!("ast: {}", ast); + // let parameters = Value::from_json_str(&result)?; + + assert_eq!(2, parameters.len()); + + let test_policy1_parameters = ¶meters[0]; + assert_eq!(2, test_policy1_parameters.parameters.len()); + assert_eq!("a", test_policy1_parameters.parameters[0].name); + assert_eq!("b", test_policy1_parameters.parameters[1].name); + + // We expect parameters to be defined separately, so the second policy does not have any parameters + let test_policy2_parameters = ¶meters[1]; + assert_eq!(0, test_policy2_parameters.parameters.len()); + + assert_eq!(1, test_policy2_parameters.modifiers.len()); + assert_eq!("c", test_policy2_parameters.modifiers[0].name); + + Ok(()) +}