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(())
+}