mirror of
https://github.com/microsoft/regorus.git
synced 2026-08-05 02:16:11 +00:00
- Add PolicyLengthConfig struct with max_col, max_file_bytes, and max_lines fields, replacing hardcoded constants in the lexer. - Add Engine::set_policy_length_config and clear_policy_length_config to allow callers to override the default limits. - Add Source::from_contents_with_limits and from_file_with_limits for direct Source construction with custom limits; existing from_contents and from_file signatures are preserved using defaults. - Add tests for default rejection, custom limits, and engine plumbing. - Add bindings for C, C++, Python, WASM/JS, Java, Ruby, C#, Go
1594 lines
56 KiB
Rust
1594 lines
56 KiB
Rust
// Copyright (c) Microsoft Corporation.
|
|
// Licensed under the MIT License.
|
|
#![allow(clippy::print_stderr)]
|
|
|
|
use crate::ast::*;
|
|
use crate::compiled_policy::CompiledPolicy;
|
|
use crate::interpreter::*;
|
|
use crate::lexer::*;
|
|
use crate::parser::*;
|
|
use crate::scheduler::*;
|
|
use crate::utils::gather_functions;
|
|
use crate::utils::limits::PolicyLengthConfig;
|
|
use crate::utils::limits::{self, fallback_execution_timer_config, ExecutionTimerConfig};
|
|
use crate::value::*;
|
|
use crate::*;
|
|
use crate::{Extension, QueryResults};
|
|
|
|
use crate::Rc;
|
|
use anyhow::{anyhow, bail, Result};
|
|
|
|
/// The Rego evaluation engine.
|
|
///
|
|
#[derive(Debug, Clone)]
|
|
pub struct Engine {
|
|
modules: Rc<Vec<Ref<Module>>>,
|
|
interpreter: Interpreter,
|
|
prepared: bool,
|
|
rego_v1: bool,
|
|
execution_timer_config: Option<ExecutionTimerConfig>,
|
|
policy_length_config: PolicyLengthConfig,
|
|
}
|
|
|
|
#[cfg(feature = "azure_policy")]
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct PolicyPackageNameDefinition {
|
|
pub source_file: String,
|
|
pub package_name: String,
|
|
}
|
|
|
|
#[cfg(feature = "azure_policy")]
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct PolicyParameter {
|
|
pub name: String,
|
|
pub modifiable: bool,
|
|
pub required: bool,
|
|
}
|
|
|
|
#[cfg(feature = "azure_policy")]
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct PolicyModifier {
|
|
pub name: String,
|
|
}
|
|
|
|
#[cfg(feature = "azure_policy")]
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct PolicyParameters {
|
|
pub source_file: String,
|
|
pub parameters: Vec<PolicyParameter>,
|
|
pub modifiers: Vec<PolicyModifier>,
|
|
}
|
|
|
|
/// Create a default engine.
|
|
impl Default for Engine {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
impl Engine {
|
|
fn effective_execution_timer_config(&self) -> Option<ExecutionTimerConfig> {
|
|
self.execution_timer_config
|
|
.or_else(fallback_execution_timer_config)
|
|
}
|
|
|
|
fn apply_effective_execution_timer_config(&mut self) {
|
|
let config = self.effective_execution_timer_config();
|
|
self.interpreter.set_execution_timer_config(config);
|
|
}
|
|
|
|
/// Create an instance of [Engine].
|
|
pub fn new() -> Self {
|
|
let mut engine = Self {
|
|
modules: Rc::new(vec![]),
|
|
interpreter: Interpreter::new(),
|
|
prepared: false,
|
|
rego_v1: true,
|
|
execution_timer_config: None,
|
|
policy_length_config: PolicyLengthConfig::default(),
|
|
};
|
|
engine.apply_effective_execution_timer_config();
|
|
engine
|
|
}
|
|
|
|
/// Enable rego v0.
|
|
///
|
|
/// Note that regorus now defaults to v1.
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// // Enable v0 for old style policies.
|
|
/// engine.set_rego_v0(true);
|
|
///
|
|
/// engine.add_policy(
|
|
/// "test.rego".to_string(),
|
|
/// r#"
|
|
/// package test
|
|
///
|
|
/// allow { # v0 syntax does not require if keyword
|
|
/// 1 < 2
|
|
/// }
|
|
/// "#.to_string())?;
|
|
///
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
pub const fn set_rego_v0(&mut self, rego_v0: bool) {
|
|
self.rego_v1 = !rego_v0;
|
|
}
|
|
|
|
/// Configure the execution timer.
|
|
///
|
|
/// Stores the supplied configuration and ensures the next evaluation is checked against those
|
|
/// limits. Engines start without a time limit and otherwise fall back to the global
|
|
/// configuration (if provided).
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use std::num::NonZeroU32;
|
|
/// use std::time::Duration;
|
|
/// use regorus::utils::limits::ExecutionTimerConfig;
|
|
/// use regorus::Engine;
|
|
///
|
|
/// let mut engine = Engine::new();
|
|
/// let config = ExecutionTimerConfig {
|
|
/// limit: Duration::from_millis(10),
|
|
/// check_interval: NonZeroU32::new(1).unwrap(),
|
|
/// };
|
|
///
|
|
/// engine.set_execution_timer_config(config);
|
|
/// ```
|
|
pub fn set_execution_timer_config(&mut self, config: ExecutionTimerConfig) {
|
|
self.execution_timer_config = Some(config);
|
|
self.interpreter.set_execution_timer_config(Some(config));
|
|
}
|
|
|
|
/// Set the policy length limits used when loading policies.
|
|
///
|
|
/// Controls maximum file size, line count, and column width for policy files.
|
|
/// Engines start with the default limits defined by [`PolicyLengthConfig::default`].
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use core::num::{NonZeroU32, NonZeroUsize};
|
|
/// use regorus::utils::limits::PolicyLengthConfig;
|
|
/// use regorus::Engine;
|
|
///
|
|
/// let mut engine = Engine::new();
|
|
/// let config = PolicyLengthConfig {
|
|
/// max_col: NonZeroU32::new(2048).unwrap(),
|
|
/// max_file_bytes: NonZeroUsize::new(2_097_152).unwrap(),
|
|
/// max_lines: NonZeroUsize::new(40_000).unwrap(),
|
|
/// };
|
|
///
|
|
/// engine.set_policy_length_config(config);
|
|
/// ```
|
|
pub const fn set_policy_length_config(&mut self, config: PolicyLengthConfig) {
|
|
self.policy_length_config = config;
|
|
}
|
|
|
|
/// Clear the engine-specific execution timer configuration, falling back to the global value.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use std::num::NonZeroU32;
|
|
/// use std::time::Duration;
|
|
/// use regorus::utils::limits::{
|
|
/// set_fallback_execution_timer_config,
|
|
/// ExecutionTimerConfig,
|
|
/// };
|
|
/// use regorus::Engine;
|
|
///
|
|
/// let mut engine = Engine::new();
|
|
/// let global = ExecutionTimerConfig {
|
|
/// limit: Duration::from_millis(5),
|
|
/// check_interval: NonZeroU32::new(1).unwrap(),
|
|
/// };
|
|
/// set_fallback_execution_timer_config(Some(global));
|
|
///
|
|
/// engine.clear_execution_timer_config();
|
|
/// ```
|
|
pub fn clear_execution_timer_config(&mut self) {
|
|
self.execution_timer_config = None;
|
|
self.apply_effective_execution_timer_config();
|
|
}
|
|
|
|
/// Clear the policy length configuration, reverting to the defaults.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ```
|
|
/// use regorus::Engine;
|
|
///
|
|
/// let mut engine = Engine::new();
|
|
/// engine.clear_policy_length_config();
|
|
/// ```
|
|
pub fn clear_policy_length_config(&mut self) {
|
|
self.policy_length_config = PolicyLengthConfig::default();
|
|
}
|
|
|
|
/// Add a policy.
|
|
///
|
|
/// The policy file will be parsed and converted to AST representation.
|
|
/// Multiple policy files may be added to the engine.
|
|
/// Returns the Rego package name declared in the policy.
|
|
///
|
|
/// * `path`: A filename to be associated with the policy.
|
|
/// * `rego`: The rego policy code.
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// let package = engine.add_policy(
|
|
/// "test.rego".to_string(),
|
|
/// r#"
|
|
/// package test
|
|
/// allow = input.user == "root"
|
|
/// "#.to_string())?;
|
|
///
|
|
/// assert_eq!(package, "data.test");
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
pub fn add_policy(&mut self, path: String, rego: String) -> Result<String> {
|
|
let source = Source::from_contents_with_limits(
|
|
path,
|
|
rego,
|
|
self.policy_length_config.max_file_bytes,
|
|
self.policy_length_config.max_lines,
|
|
)?;
|
|
let mut parser = self.make_parser(&source)?;
|
|
let module = Ref::new(parser.parse()?);
|
|
limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
|
|
Rc::make_mut(&mut self.modules).push(module.clone());
|
|
// if policies change, interpreter needs to be prepared again
|
|
self.prepared = false;
|
|
Interpreter::get_path_string(&module.package.refr, Some("data"))
|
|
}
|
|
|
|
/// Add a policy from a given file.
|
|
///
|
|
/// The policy file will be parsed and converted to AST representation.
|
|
/// Multiple policy files may be added to the engine.
|
|
/// Returns the Rego package name declared in the policy.
|
|
///
|
|
/// * `path`: Path to the policy file (.rego).
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
/// // framework.rego does not conform to v1.
|
|
/// engine.set_rego_v0(true);
|
|
///
|
|
/// let package = engine.add_policy_from_file("tests/aci/framework.rego")?;
|
|
///
|
|
/// assert_eq!(package, "data.framework");
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
#[cfg(feature = "std")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "std")))]
|
|
pub fn add_policy_from_file<P: AsRef<std::path::Path>>(&mut self, path: P) -> Result<String> {
|
|
let source = Source::from_file_with_limits(
|
|
path,
|
|
self.policy_length_config.max_file_bytes,
|
|
self.policy_length_config.max_lines,
|
|
)?;
|
|
let mut parser = self.make_parser(&source)?;
|
|
let module = Ref::new(parser.parse()?);
|
|
limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
|
|
Rc::make_mut(&mut self.modules).push(module.clone());
|
|
// if policies change, interpreter needs to be prepared again
|
|
self.prepared = false;
|
|
Interpreter::get_path_string(&module.package.refr, Some("data"))
|
|
}
|
|
|
|
/// Get the list of packages defined by loaded policies.
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
/// // framework.rego does not conform to v1.
|
|
/// engine.set_rego_v0(true);
|
|
///
|
|
/// let _ = engine.add_policy_from_file("tests/aci/framework.rego")?;
|
|
///
|
|
/// // Package names can be different from file names.
|
|
/// let _ = engine.add_policy("policy.rego".into(), "package hello.world".into())?;
|
|
///
|
|
/// assert_eq!(engine.get_packages()?, vec!["data.framework", "data.hello.world"]);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn get_packages(&self) -> Result<Vec<String>> {
|
|
self.modules
|
|
.iter()
|
|
.map(|m| Interpreter::get_path_string(&m.package.refr, Some("data")))
|
|
.collect()
|
|
}
|
|
|
|
/// Get the list of policy files.
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// # let mut engine = Engine::new();
|
|
///
|
|
/// let pkg = engine.add_policy("hello.rego".to_string(), "package test".to_string())?;
|
|
/// assert_eq!(pkg, "data.test");
|
|
///
|
|
/// let policies = engine.get_policies()?;
|
|
///
|
|
/// assert_eq!(policies[0].get_path(), "hello.rego");
|
|
/// assert_eq!(policies[0].get_contents(), "package test");
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn get_policies(&self) -> Result<Vec<Source>> {
|
|
Ok(self
|
|
.modules
|
|
.iter()
|
|
.map(|m| m.package.refr.span().source.clone())
|
|
.collect())
|
|
}
|
|
|
|
/// Get the list of policy files as a JSON object.
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// # let mut engine = Engine::new();
|
|
///
|
|
/// let pkg = engine.add_policy("hello.rego".to_string(), "package test".to_string())?;
|
|
/// assert_eq!(pkg, "data.test");
|
|
///
|
|
/// let policies = engine.get_policies_as_json()?;
|
|
///
|
|
/// let v = Value::from_json_str(&policies)?;
|
|
/// assert_eq!(v[0]["path"].as_string()?.as_ref(), "hello.rego");
|
|
/// assert_eq!(v[0]["contents"].as_string()?.as_ref(), "package test");
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn get_policies_as_json(&self) -> Result<String> {
|
|
#[derive(Serialize)]
|
|
struct Source<'a> {
|
|
path: &'a String,
|
|
contents: &'a String,
|
|
}
|
|
|
|
let mut sources = vec![];
|
|
for m in self.modules.iter() {
|
|
let source = &m.package.refr.span().source;
|
|
sources.push(Source {
|
|
path: source.get_path(),
|
|
contents: source.get_contents(),
|
|
});
|
|
}
|
|
|
|
serde_json::to_string_pretty(&sources).map_err(anyhow::Error::msg)
|
|
}
|
|
|
|
/// Set the input document.
|
|
///
|
|
/// * `input`: Input documented. Typically this [Value] is constructed from JSON or YAML.
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// let input = Value::from_json_str(r#"
|
|
/// {
|
|
/// "role" : "admin",
|
|
/// "action": "delete"
|
|
/// }"#)?;
|
|
///
|
|
/// engine.set_input(input);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn set_input(&mut self, input: Value) {
|
|
self.interpreter.set_input(input);
|
|
}
|
|
|
|
pub fn set_input_json(&mut self, input_json: &str) -> Result<()> {
|
|
self.set_input(Value::from_json_str(input_json)?);
|
|
Ok(())
|
|
}
|
|
|
|
/// Clear the data document.
|
|
///
|
|
/// The data document will be reset to an empty object.
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// engine.clear_data();
|
|
///
|
|
/// // Evaluate data.
|
|
/// let results = engine.eval_query("data".to_string(), false)?;
|
|
///
|
|
/// // Assert that it is empty object.
|
|
/// assert_eq!(results.result.len(), 1);
|
|
/// assert_eq!(results.result[0].expressions.len(), 1);
|
|
/// assert_eq!(results.result[0].expressions[0].value, Value::new_object());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn clear_data(&mut self) {
|
|
self.interpreter.set_init_data(Value::new_object());
|
|
self.prepared = false;
|
|
}
|
|
|
|
/// Add data document.
|
|
///
|
|
/// The specified data document is merged into existing data document.
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// // Only objects can be added.
|
|
/// assert!(engine.add_data(Value::from_json_str("[]")?).is_err());
|
|
///
|
|
/// // Merge { "x" : 1, "y" : {} }
|
|
/// assert!(engine.add_data(Value::from_json_str(r#"{ "x" : 1, "y" : {}}"#)?).is_ok());
|
|
///
|
|
/// // Merge { "z" : 2 }
|
|
/// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 2 }"#)?).is_ok());
|
|
///
|
|
/// // Merge { "z" : 3 }. Conflict error.
|
|
/// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 3 }"#)?).is_err());
|
|
///
|
|
/// assert_eq!(
|
|
/// engine.eval_query("data".to_string(), false)?.result[0].expressions[0].value,
|
|
/// Value::from_json_str(r#"{ "x": 1, "y": {}, "z": 2}"#)?
|
|
/// );
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn add_data(&mut self, data: Value) -> Result<()> {
|
|
if data.as_object().is_err() {
|
|
bail!("data must be object");
|
|
}
|
|
self.prepared = false;
|
|
self.interpreter.get_init_data_mut().merge(data)
|
|
}
|
|
|
|
/// Get the data document.
|
|
///
|
|
/// The returned value is the data document that has been constructed using
|
|
/// one or more calls to [`Engine::pre`]. The values of policy rules are
|
|
/// not included in the returned document.
|
|
///
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// // If not set, data document is empty.
|
|
/// assert_eq!(engine.get_data(), Value::new_object());
|
|
///
|
|
/// // Merge { "x" : 1, "y" : {} }
|
|
/// assert!(engine.add_data(Value::from_json_str(r#"{ "x" : 1, "y" : {}}"#)?).is_ok());
|
|
///
|
|
/// // Merge { "z" : 2 }
|
|
/// assert!(engine.add_data(Value::from_json_str(r#"{ "z" : 2 }"#)?).is_ok());
|
|
///
|
|
/// let data = engine.get_data();
|
|
/// assert_eq!(data["x"], Value::from(1));
|
|
/// assert_eq!(data["y"], Value::new_object());
|
|
/// assert_eq!(data["z"], Value::from(2));
|
|
///
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn get_data(&self) -> Value {
|
|
self.interpreter.get_init_data().clone()
|
|
}
|
|
|
|
pub fn add_data_json(&mut self, data_json: &str) -> Result<()> {
|
|
self.add_data(Value::from_json_str(data_json)?)
|
|
}
|
|
|
|
/// Set whether builtins should raise errors strictly or not.
|
|
///
|
|
/// Regorus differs from OPA in that by default builtins will
|
|
/// raise errors instead of returning Undefined.
|
|
///
|
|
/// ----
|
|
/// **_NOTE:_** Currently not all builtins honor this flag and will always strictly raise errors.
|
|
/// ----
|
|
pub fn set_strict_builtin_errors(&mut self, b: bool) {
|
|
self.interpreter.set_strict_builtin_errors(b);
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
pub fn get_modules(&mut self) -> &Vec<Ref<Module>> {
|
|
&self.modules
|
|
}
|
|
|
|
/// Compiles 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. The compiled
|
|
/// policy will automatically detect and handle `__target__` declarations in the
|
|
/// loaded modules.
|
|
///
|
|
/// The engine must have been prepared with:
|
|
/// - Policy modules added via [`Engine::add_policy`]
|
|
/// - Data added via [`Engine::add_data`] (optional)
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns a [`CompiledPolicy`] that can be used for efficient policy evaluation
|
|
/// with target support, including resource type inference capabilities.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ## Basic Target-Aware Compilation
|
|
///
|
|
/// ```no_run
|
|
/// use regorus::*;
|
|
///
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
/// engine.add_data(Value::from_json_str(r#"{"allowed_sizes": ["small", "medium"]}"#)?)?;
|
|
/// engine.add_policy("policy.rego".to_string(), r#"
|
|
/// package policy.test
|
|
/// import rego.v1
|
|
/// __target__ := "target.tests.sample_test_target"
|
|
///
|
|
/// default allow := false
|
|
/// allow if {
|
|
/// input.type == "vm"
|
|
/// input.size in data.allowed_sizes
|
|
/// }
|
|
/// "#.to_string())?;
|
|
///
|
|
/// let compiled = engine.compile_for_target()?;
|
|
/// let result = compiled.eval_with_input(Value::from_json_str(r#"{"type": "vm", "size": "small"}"#)?)?;
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// ## Target Registration and Usage
|
|
///
|
|
/// ```no_run
|
|
/// use regorus::*;
|
|
/// use regorus::registry::targets;
|
|
/// use regorus::target::Target;
|
|
/// use std::sync::Arc;
|
|
///
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// // Register a target first
|
|
/// let target_json = r#"
|
|
/// {
|
|
/// "name": "target.example.vm_policy",
|
|
/// "description": "Simple VM validation target",
|
|
/// "version": "1.0.0",
|
|
/// "resource_schema_selector": "type",
|
|
/// "resource_schemas": [
|
|
/// {
|
|
/// "type": "object",
|
|
/// "properties": {
|
|
/// "name": { "type": "string" },
|
|
/// "type": { "const": "vm" },
|
|
/// "size": { "enum": ["small", "medium", "large"] }
|
|
/// },
|
|
/// "required": ["name", "type", "size"]
|
|
/// }
|
|
/// ],
|
|
/// "effects": {
|
|
/// "allow": { "type": "boolean" },
|
|
/// "deny": { "type": "boolean" }
|
|
/// }
|
|
/// }
|
|
/// "#;
|
|
///
|
|
/// let target = Target::from_json_str(target_json)?;
|
|
/// targets::register(Arc::new(target))?;
|
|
///
|
|
/// // Use the target in a policy
|
|
/// let mut engine = Engine::new();
|
|
/// engine.add_data(Value::from_json_str(r#"{"allowed_locations": ["us-east"]}"#)?)?;
|
|
/// engine.add_policy("vm_policy.rego".to_string(), r#"
|
|
/// package vm.validation
|
|
/// import rego.v1
|
|
/// __target__ := "target.example.vm_policy"
|
|
///
|
|
/// default allow := false
|
|
/// allow if {
|
|
/// input.type == "vm"
|
|
/// input.size in ["small", "medium"]
|
|
/// }
|
|
/// "#.to_string())?;
|
|
///
|
|
/// let compiled = engine.compile_for_target()?;
|
|
/// let result = compiled.eval_with_input(Value::from_json_str(r#"
|
|
/// {
|
|
/// "name": "test-vm",
|
|
/// "type": "vm",
|
|
/// "size": "small"
|
|
/// }"#)?)?;
|
|
/// assert_eq!(result, Value::from(true));
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// # Notes
|
|
///
|
|
/// - This method is only available when the `azure_policy` feature is enabled
|
|
/// - Automatically enables print gathering for debugging purposes
|
|
/// - Requires that at least one module contains a `__target__` declaration
|
|
/// - The target referenced must be registered in the target registry
|
|
///
|
|
/// # See Also
|
|
///
|
|
/// - [`Engine::compile_with_entrypoint`] for explicit rule-based compilation
|
|
/// - [`crate::compile_policy_for_target`] for a higher-level convenience function
|
|
#[cfg(feature = "azure_policy")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "azure_policy")))]
|
|
pub fn compile_for_target(&mut self) -> Result<CompiledPolicy> {
|
|
self.prepare_for_eval(false, true)?;
|
|
self.apply_effective_execution_timer_config();
|
|
self.interpreter.clean_internal_evaluation_state();
|
|
self.interpreter.compile(None).map(CompiledPolicy::new)
|
|
}
|
|
|
|
/// Compiles a policy with a specific entry point rule.
|
|
///
|
|
/// This method creates a compiled policy that evaluates a specific rule as the entry point.
|
|
/// Unlike [`Engine::compile_for_target`], this method requires you to explicitly specify which
|
|
/// rule should be evaluated and does not automatically handle target-specific features.
|
|
///
|
|
/// The engine must have been prepared with:
|
|
/// - Policy modules added via [`Engine::add_policy`]
|
|
/// - Data added via [`Engine::add_data`] (optional)
|
|
///
|
|
/// # Arguments
|
|
///
|
|
/// * `rule` - The specific rule path to evaluate (e.g., "data.policy.allow")
|
|
///
|
|
/// # Returns
|
|
///
|
|
/// Returns a [`CompiledPolicy`] that can be used for efficient policy evaluation
|
|
/// focused on the specified entry point rule.
|
|
///
|
|
/// # Examples
|
|
///
|
|
/// ## Basic Usage
|
|
///
|
|
/// ```no_run
|
|
/// use regorus::*;
|
|
/// use std::rc::Rc;
|
|
///
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
/// engine.add_data(Value::from_json_str(r#"{"allowed_users": ["alice", "bob"]}"#)?)?;
|
|
/// engine.add_policy("authz.rego".to_string(), r#"
|
|
/// package authz
|
|
/// import rego.v1
|
|
///
|
|
/// default allow := false
|
|
/// allow if {
|
|
/// input.user in data.allowed_users
|
|
/// input.action == "read"
|
|
/// }
|
|
///
|
|
/// deny if {
|
|
/// input.user == "guest"
|
|
/// }
|
|
/// "#.to_string())?;
|
|
///
|
|
/// let compiled = engine.compile_with_entrypoint(&"data.authz.allow".into())?;
|
|
/// let result = compiled.eval_with_input(Value::from_json_str(r#"{"user": "alice", "action": "read"}"#)?)?;
|
|
/// assert_eq!(result, Value::from(true));
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// ## Multi-Module Policy
|
|
///
|
|
/// ```no_run
|
|
/// use regorus::*;
|
|
/// use std::rc::Rc;
|
|
///
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
/// engine.add_data(Value::from_json_str(r#"{"departments": {"engineering": ["alice"], "hr": ["bob"]}}"#)?)?;
|
|
///
|
|
/// engine.add_policy("users.rego".to_string(), r#"
|
|
/// package users
|
|
/// import rego.v1
|
|
///
|
|
/// user_department(user) := dept if {
|
|
/// dept := [d | data.departments[d][_] == user][0]
|
|
/// }
|
|
/// "#.to_string())?;
|
|
///
|
|
/// engine.add_policy("permissions.rego".to_string(), r#"
|
|
/// package permissions
|
|
/// import rego.v1
|
|
/// import data.users
|
|
///
|
|
/// default allow := false
|
|
/// allow if {
|
|
/// users.user_department(input.user) == "engineering"
|
|
/// input.resource.type == "code"
|
|
/// }
|
|
///
|
|
/// allow if {
|
|
/// users.user_department(input.user) == "hr"
|
|
/// input.resource.type == "personnel_data"
|
|
/// }
|
|
/// "#.to_string())?;
|
|
///
|
|
/// let compiled = engine.compile_with_entrypoint(&"data.permissions.allow".into())?;
|
|
///
|
|
/// // Test engineering access to code
|
|
/// let result = compiled.eval_with_input(Value::from_json_str(r#"
|
|
/// {
|
|
/// "user": "alice",
|
|
/// "resource": {"type": "code", "name": "main.rs"}
|
|
/// }"#)?)?;
|
|
/// assert_eq!(result, Value::from(true));
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// # Entry Point Rule Format
|
|
///
|
|
/// The `rule` parameter should follow the Rego rule path format:
|
|
/// - `"data.package.rule"` - For rules in a specific package
|
|
/// - `"data.package.subpackage.rule"` - For nested packages
|
|
/// - `"allow"` - For rules in the default package (though this is not recommended)
|
|
///
|
|
/// # Notes
|
|
///
|
|
/// - Automatically enables print gathering for debugging purposes
|
|
/// - If you need target-aware compilation with automatic `__target__` handling,
|
|
/// consider using [`Engine::compile_for_target`] instead (requires `azure_policy` feature)
|
|
///
|
|
/// # See Also
|
|
///
|
|
/// - [`Engine::compile_for_target`] for target-aware compilation
|
|
/// - [`crate::compile_policy_with_entrypoint`] for a higher-level convenience function
|
|
pub fn compile_with_entrypoint(&mut self, rule: &Rc<str>) -> Result<CompiledPolicy> {
|
|
self.prepare_for_eval(false, false)?;
|
|
self.apply_effective_execution_timer_config();
|
|
self.interpreter.clean_internal_evaluation_state();
|
|
self.interpreter
|
|
.compile(Some(rule.clone()))
|
|
.map(CompiledPolicy::new)
|
|
}
|
|
|
|
/// Evaluate specified rule(s).
|
|
///
|
|
/// [`Engine::eval_rule`] is often faster than [`Engine::eval_query`] and should be preferred if
|
|
/// OPA style [`QueryResults`] are not needed.
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// // Add policy
|
|
/// engine.add_policy(
|
|
/// "policy.rego".to_string(),
|
|
/// r#"
|
|
/// package example
|
|
/// import rego.v1
|
|
///
|
|
/// x = [1, 2]
|
|
///
|
|
/// y := 5 if input.a > 2
|
|
/// "#.to_string())?;
|
|
///
|
|
/// // Evaluate rule.
|
|
/// let v = engine.eval_rule("data.example.x".to_string())?;
|
|
/// assert_eq!(v, Value::from(vec![Value::from(1), Value::from(2)]));
|
|
///
|
|
/// // y evaluates to undefined.
|
|
/// let v = engine.eval_rule("data.example.y".to_string())?;
|
|
/// assert_eq!(v, Value::Undefined);
|
|
///
|
|
/// // Evaluating a non-existent rule is an error.
|
|
/// let r = engine.eval_rule("data.exaample.x".to_string());
|
|
/// assert!(r.is_err());
|
|
///
|
|
/// // Path must be valid rule paths.
|
|
/// assert!( engine.eval_rule("data".to_string()).is_err());
|
|
/// assert!( engine.eval_rule("data.example".to_string()).is_err());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn eval_rule(&mut self, rule: String) -> Result<Value> {
|
|
self.prepare_for_eval(false, false)?;
|
|
self.apply_effective_execution_timer_config();
|
|
self.interpreter.clean_internal_evaluation_state();
|
|
self.interpreter.eval_rule_in_path(rule)
|
|
}
|
|
|
|
/// Evaluate a Rego query.
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// // Add policies
|
|
/// engine.set_rego_v0(true);
|
|
/// engine.add_policy_from_file("tests/aci/framework.rego")?;
|
|
/// engine.add_policy_from_file("tests/aci/api.rego")?;
|
|
/// engine.add_policy_from_file("tests/aci/policy.rego")?;
|
|
///
|
|
/// // Add data document (if any).
|
|
/// // If multiple data documents can be added, they will be merged together.
|
|
/// engine.add_data(Value::from_json_file("tests/aci/data.json")?)?;
|
|
///
|
|
/// // At this point the policies and data have been loaded.
|
|
/// // Either the same engine can be used to make multiple queries or the engine
|
|
/// // can be cloned to avoid having the reload the policies and data.
|
|
/// let _clone = engine.clone();
|
|
///
|
|
/// // Evaluate a query.
|
|
/// // Load input and make query.
|
|
/// engine.set_input(Value::new_object());
|
|
/// let results = engine.eval_query("data.framework.mount_overlay.allowed".to_string(), false)?;
|
|
/// assert_eq!(results.result[0].expressions[0].value, Value::from(false));
|
|
///
|
|
/// // Evaluate query with different inputs.
|
|
/// engine.set_input(Value::from_json_file("tests/aci/input.json")?);
|
|
/// let results = engine.eval_query("data.framework.mount_overlay.allowed".to_string(), false)?;
|
|
/// assert_eq!(results.result[0].expressions[0].value, Value::from(true));
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn eval_query(&mut self, query: String, enable_tracing: bool) -> Result<QueryResults> {
|
|
self.prepare_for_eval(enable_tracing, false)?;
|
|
self.apply_effective_execution_timer_config();
|
|
self.interpreter.clean_internal_evaluation_state();
|
|
|
|
self.interpreter.create_rule_prefixes()?;
|
|
let (query_module, query_node, query_schedule) = self.make_query(query)?;
|
|
if query_node.span.text() == "data" {
|
|
self.eval_modules(enable_tracing)?;
|
|
}
|
|
|
|
self.interpreter
|
|
.eval_user_query(&query_module, &query_node, query_schedule, enable_tracing)
|
|
}
|
|
|
|
/// Evaluate a Rego query that produces a boolean value.
|
|
///
|
|
///
|
|
/// This function should be preferred over [`Engine::eval_query`] if just a `true`/`false`
|
|
/// value is desired instead of [`QueryResults`].
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// # let mut engine = Engine::new();
|
|
///
|
|
/// let enable_tracing = false;
|
|
/// assert_eq!(engine.eval_bool_query("1 > 2".to_string(), enable_tracing)?, false);
|
|
/// assert_eq!(engine.eval_bool_query("1 < 2".to_string(), enable_tracing)?, true);
|
|
///
|
|
/// // Non boolean queries will raise an error.
|
|
/// assert!(engine.eval_bool_query("1+1".to_string(), enable_tracing).is_err());
|
|
///
|
|
/// // Queries producing multiple values will raise an error.
|
|
/// assert!(engine.eval_bool_query("true; true".to_string(), enable_tracing).is_err());
|
|
///
|
|
/// // Queries producing no values will raise an error.
|
|
/// assert!(engine.eval_bool_query("true; false; true".to_string(), enable_tracing).is_err());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn eval_bool_query(&mut self, query: String, enable_tracing: bool) -> Result<bool> {
|
|
let results = self.eval_query(query, enable_tracing)?;
|
|
let entries = results.result.as_slice();
|
|
let entry = entries
|
|
.first()
|
|
.ok_or_else(|| anyhow!("query did not produce any values"))?;
|
|
if entries.len() > 1 {
|
|
bail!("query produced more than one value");
|
|
}
|
|
|
|
let expressions = entry.expressions.as_slice();
|
|
let expr = expressions
|
|
.first()
|
|
.ok_or_else(|| anyhow!("query result missing expression"))?;
|
|
if expressions.len() > 1 {
|
|
bail!("query produced more than one value");
|
|
}
|
|
|
|
expr.value.as_bool().copied()
|
|
}
|
|
|
|
/// Evaluate an `allow` query.
|
|
///
|
|
/// This is a wrapper over [`Engine::eval_bool_query`] that returns true only if the
|
|
/// boolean query succeed and produced a `true` value.
|
|
///
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// # let mut engine = Engine::new();
|
|
///
|
|
/// let enable_tracing = false;
|
|
/// assert_eq!(engine.eval_allow_query("1 > 2".to_string(), enable_tracing), false);
|
|
/// assert_eq!(engine.eval_allow_query("1 < 2".to_string(), enable_tracing), true);
|
|
/// assert_eq!(engine.eval_allow_query("1+1".to_string(), enable_tracing), false);
|
|
/// assert_eq!(engine.eval_allow_query("true; true".to_string(), enable_tracing), false);
|
|
/// assert_eq!(engine.eval_allow_query("true; false; true".to_string(), enable_tracing), false);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn eval_allow_query(&mut self, query: String, enable_tracing: bool) -> bool {
|
|
matches!(self.eval_bool_query(query, enable_tracing), Ok(true))
|
|
}
|
|
|
|
/// Evaluate a `deny` query.
|
|
///
|
|
/// This is a wrapper over [`Engine::eval_bool_query`] that returns false only if the
|
|
/// boolean query succeed and produced a `false` value.
|
|
/// ```
|
|
/// # use regorus::*;
|
|
/// # fn main() -> anyhow::Result<()> {
|
|
/// # let mut engine = Engine::new();
|
|
///
|
|
/// let enable_tracing = false;
|
|
/// assert_eq!(engine.eval_deny_query("1 > 2".to_string(), enable_tracing), false);
|
|
/// assert_eq!(engine.eval_deny_query("1 < 2".to_string(), enable_tracing), true);
|
|
///
|
|
/// assert_eq!(engine.eval_deny_query("1+1".to_string(), enable_tracing), true);
|
|
/// assert_eq!(engine.eval_deny_query("true; true".to_string(), enable_tracing), true);
|
|
/// assert_eq!(engine.eval_deny_query("true; false; true".to_string(), enable_tracing), true);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn eval_deny_query(&mut self, query: String, enable_tracing: bool) -> bool {
|
|
!matches!(self.eval_bool_query(query, enable_tracing), Ok(false))
|
|
}
|
|
|
|
fn make_query(&mut self, query: String) -> Result<(NodeRef<Module>, NodeRef<Query>, Schedule)> {
|
|
let mut query_module = {
|
|
let source = Source::from_contents_with_limits(
|
|
"<query_module.rego>".to_owned(),
|
|
"package __internal_query_module".to_owned(),
|
|
self.policy_length_config.max_file_bytes,
|
|
self.policy_length_config.max_lines,
|
|
)?;
|
|
let mut parser = Parser::new(&source)?;
|
|
parser.set_max_col(self.policy_length_config.max_col);
|
|
parser.parse()?
|
|
};
|
|
|
|
// Parse the query.
|
|
let query_source = Source::from_contents_with_limits(
|
|
"<query.rego>".to_string(),
|
|
query,
|
|
self.policy_length_config.max_file_bytes,
|
|
self.policy_length_config.max_lines,
|
|
)?;
|
|
let mut parser = self.make_parser(&query_source)?;
|
|
let query_node = parser.parse_user_query()?;
|
|
query_module.num_expressions = parser.num_expressions();
|
|
query_module.num_queries = parser.num_queries();
|
|
query_module.num_statements = parser.num_statements();
|
|
let query_schedule = Analyzer::new().analyze_query_snippet(&self.modules, &query_node)?;
|
|
|
|
// Populate loop hoisting for the query snippet
|
|
// Query snippets are treated as if they're in a module appended at the end (same as analyzer)
|
|
// The loop hoisting table already has capacity for this (ensured in prepare_for_eval)
|
|
let module_idx = u32::try_from(self.modules.len())
|
|
.map_err(|_| anyhow!("module count exceeds u32::MAX"))?;
|
|
|
|
use crate::compiler::hoist::LoopHoister;
|
|
|
|
let query_schedule_rc = Rc::new(query_schedule.clone());
|
|
|
|
// Run loop hoisting for query snippet
|
|
let mut hoister = LoopHoister::new_with_schedule(query_schedule_rc.clone());
|
|
hoister.populate_query_snippet(
|
|
module_idx,
|
|
&query_node,
|
|
query_module.num_statements,
|
|
query_module.num_expressions,
|
|
)?;
|
|
let query_lookup = hoister.finalize();
|
|
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
for stmt in &query_node.stmts {
|
|
debug_assert!(
|
|
query_lookup
|
|
.get_statement_loops(module_idx, stmt.sidx)
|
|
.ok()
|
|
.and_then(|entry| entry)
|
|
.is_some(),
|
|
"missing hoisted loop entry for query statement index {}",
|
|
stmt.sidx
|
|
);
|
|
}
|
|
}
|
|
|
|
// Get the existing table, merge in the query loops, and set it back
|
|
let mut existing_table = self.interpreter.take_loop_hoisting_table();
|
|
existing_table.truncate_modules(self.modules.len());
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
debug_assert!(
|
|
existing_table.module_len() <= self.modules.len(),
|
|
"loop hoisting table should not retain extra modules before merge"
|
|
);
|
|
}
|
|
existing_table.merge_query_loops(query_lookup, self.modules.len());
|
|
#[cfg(debug_assertions)]
|
|
{
|
|
for stmt in &query_node.stmts {
|
|
debug_assert!(
|
|
existing_table
|
|
.get_statement_loops(module_idx, stmt.sidx)
|
|
.ok()
|
|
.and_then(|entry| entry)
|
|
.is_some(),
|
|
"missing hoisted loop entry after merge for module {} stmt {}",
|
|
module_idx,
|
|
stmt.sidx
|
|
);
|
|
}
|
|
}
|
|
self.interpreter.set_loop_hoisting_table(existing_table);
|
|
|
|
Ok((Ref::new(query_module), query_node, query_schedule))
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
/// Evaluate the given query and all the rules in the supplied policies.
|
|
///
|
|
/// This is mainly used for testing Regorus itself.
|
|
pub fn eval_query_and_all_rules(
|
|
&mut self,
|
|
query: String,
|
|
enable_tracing: bool,
|
|
) -> Result<QueryResults> {
|
|
self.eval_modules(enable_tracing)?;
|
|
// Restart the timer window for the user query after module evaluation.
|
|
self.apply_effective_execution_timer_config();
|
|
|
|
let (query_module, query_node, query_schedule) = self.make_query(query)?;
|
|
self.interpreter
|
|
.eval_user_query(&query_module, &query_node, query_schedule, enable_tracing)
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
fn prepare_for_eval(&mut self, enable_tracing: bool, for_target: bool) -> Result<()> {
|
|
// Fail fast if the engine already exceeds the global memory limit before evaluation work.
|
|
limits::enforce_memory_limit().map_err(|err| anyhow!(err))?;
|
|
|
|
self.interpreter.set_traces(enable_tracing);
|
|
|
|
// if the data/policies have changed or the interpreter has never been prepared
|
|
if !self.prepared {
|
|
// Analyze the modules and determine how statements must be scheduled.
|
|
let analyzer = Analyzer::new();
|
|
let schedule = Rc::new(analyzer.analyze(&self.modules)?);
|
|
|
|
self.interpreter.set_modules(self.modules.clone());
|
|
|
|
self.interpreter.clear_builtins_cache();
|
|
// clean_internal_evaluation_state will set data to an efficient clont of use supplied init_data
|
|
// Initialize the with-document with initial data values.
|
|
// with-modifiers will be applied to this document.
|
|
self.interpreter.init_with_document()?;
|
|
|
|
self.interpreter
|
|
.set_functions(gather_functions(&self.modules)?);
|
|
self.interpreter.gather_rules()?;
|
|
self.interpreter.process_imports()?;
|
|
|
|
// Populate loop hoisting table for efficient evaluation
|
|
// Reserve capacity for 1 extra module (for query modules)
|
|
use crate::compiler::hoist::LoopHoister;
|
|
|
|
// Run loop hoisting pass first
|
|
let hoister = LoopHoister::new_with_schedule(schedule.clone());
|
|
let loop_lookup = hoister.populate_with_extra_capacity(&self.modules, 0)?;
|
|
|
|
self.interpreter.set_loop_hoisting_table(loop_lookup);
|
|
|
|
// Set schedule after hoisting completes
|
|
self.interpreter.set_schedule(Some(schedule));
|
|
|
|
#[cfg(feature = "azure_policy")]
|
|
if for_target {
|
|
// Resolve and validate target specifications across all modules
|
|
crate::interpreter::target::resolve::resolve_and_apply_target(
|
|
&mut self.interpreter,
|
|
)?;
|
|
// Infer resource types
|
|
crate::interpreter::target::infer::infer_resource_type(&mut self.interpreter)?;
|
|
}
|
|
|
|
if !for_target {
|
|
// Check if any module specifies a target and warn if so
|
|
#[cfg(feature = "azure_policy")]
|
|
self.warn_if_targets_present();
|
|
}
|
|
|
|
self.prepared = true;
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
pub fn eval_rule_in_module(
|
|
&mut self,
|
|
module: &Ref<Module>,
|
|
rule: &Ref<Rule>,
|
|
enable_tracing: bool,
|
|
) -> Result<Value> {
|
|
self.prepare_for_eval(enable_tracing, false)?;
|
|
self.apply_effective_execution_timer_config();
|
|
self.interpreter.clean_internal_evaluation_state();
|
|
|
|
self.interpreter.eval_rule(module, rule)?;
|
|
|
|
Ok(self.interpreter.get_data_mut().clone())
|
|
}
|
|
|
|
#[doc(hidden)]
|
|
pub fn eval_modules(&mut self, enable_tracing: bool) -> Result<Value> {
|
|
self.prepare_for_eval(enable_tracing, false)?;
|
|
self.apply_effective_execution_timer_config();
|
|
self.interpreter.clean_internal_evaluation_state();
|
|
|
|
// Ensure that empty modules are created.
|
|
for m in self.modules.iter().filter(|m| m.policy.is_empty()) {
|
|
let path = Parser::get_path_ref_components(&m.package.refr)?;
|
|
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
|
|
let vref =
|
|
Interpreter::make_or_get_value_mut(self.interpreter.get_data_mut(), &path[..])?;
|
|
if *vref == Value::Undefined {
|
|
*vref = Value::new_object();
|
|
}
|
|
}
|
|
|
|
self.interpreter.check_default_rules()?;
|
|
for module in self.modules.clone().iter() {
|
|
for rule in &module.policy {
|
|
self.interpreter.eval_rule(module, rule)?;
|
|
}
|
|
}
|
|
// Defer the evaluation of the default rules to here
|
|
for module in self.modules.clone().iter() {
|
|
let prev_module = self.interpreter.set_current_module(Some(module.clone()))?;
|
|
for rule in &module.policy {
|
|
self.interpreter.eval_default_rule(rule)?;
|
|
}
|
|
self.interpreter.set_current_module(prev_module)?;
|
|
}
|
|
|
|
// Ensure that all modules are created.
|
|
for m in self.modules.iter() {
|
|
let path = Parser::get_path_ref_components(&m.package.refr)?;
|
|
let path: Vec<&str> = path.iter().map(|s| s.text()).collect();
|
|
let vref =
|
|
Interpreter::make_or_get_value_mut(self.interpreter.get_data_mut(), &path[..])?;
|
|
if *vref == Value::Undefined {
|
|
*vref = Value::new_object();
|
|
}
|
|
}
|
|
self.interpreter.create_rule_prefixes()?;
|
|
Ok(self.interpreter.get_data_mut().clone())
|
|
}
|
|
|
|
/// Add a custom builtin (extension).
|
|
///
|
|
/// * `path`: The fully qualified path of the builtin.
|
|
/// * `nargs`: The number of arguments the builtin takes.
|
|
/// * `extension`: The [`Extension`] instance.
|
|
///
|
|
/// ```rust
|
|
/// # use regorus::*;
|
|
/// # use anyhow::{bail, Result};
|
|
/// # fn main() -> Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// // Policy uses `do_magic` custom builtin.
|
|
/// engine.add_policy(
|
|
/// "test.rego".to_string(),
|
|
/// r#"package test
|
|
/// x = do_magic(1)
|
|
/// "#.to_string(),
|
|
/// )?;
|
|
///
|
|
/// // Evaluating fails since `do_magic` is not defined.
|
|
/// assert!(engine.eval_query("data.test.x".to_string(), false).is_err());
|
|
///
|
|
/// // Add extension to implement `do_magic`. The extension can be stateful.
|
|
/// let mut magic = 8;
|
|
/// engine.add_extension("do_magic".to_string(), 1 , Box::new(move | mut params: Vec<Value> | {
|
|
/// // params is mut and therefore individual values can be removed from it and modified.
|
|
/// // The number of parameters (1) has already been validated.
|
|
///
|
|
/// match ¶ms[0].as_i64() {
|
|
/// Ok(i) => {
|
|
/// // Compute value
|
|
/// let v = *i + magic;
|
|
/// // Update extension state.
|
|
/// magic += 1;
|
|
/// Ok(Value::from(v))
|
|
/// }
|
|
/// // Extensions can raise errors. Regorus will add location information to
|
|
/// // the error.
|
|
/// _ => bail!("do_magic expects i64 value")
|
|
/// }
|
|
/// }))?;
|
|
///
|
|
/// // Evaluation will now succeed.
|
|
/// let r = engine.eval_query("data.test.x".to_string(), false)?;
|
|
/// assert_eq!(r.result[0].expressions[0].value.as_i64()?, 9);
|
|
///
|
|
/// // Cloning the engine will also clone the extension.
|
|
/// let mut engine1 = engine.clone();
|
|
///
|
|
/// // Evaluating again will return a different value since the extension is stateful.
|
|
/// let r = engine.eval_query("data.test.x".to_string(), false)?;
|
|
/// assert_eq!(r.result[0].expressions[0].value.as_i64()?, 10);
|
|
///
|
|
/// // The second engine has a clone of the extension.
|
|
/// let r = engine1.eval_query("data.test.x".to_string(), false)?;
|
|
/// assert_eq!(r.result[0].expressions[0].value.as_i64()?, 10);
|
|
///
|
|
/// // Once added, the extension cannot be replaced or removed.
|
|
/// assert!(engine.add_extension("do_magic".to_string(), 1, Box::new(|_:Vec<Value>| {
|
|
/// Ok(Value::Undefined)
|
|
/// })).is_err());
|
|
///
|
|
/// // Extensions don't support out-parameter syntax.
|
|
/// engine.add_policy(
|
|
/// "policy.rego".to_string(),
|
|
/// r#"package invalid
|
|
/// x = y if {
|
|
/// # y = do_magic(2)
|
|
/// do_magic(2, y) # y is supplied as an out parameter.
|
|
/// }
|
|
/// "#.to_string()
|
|
/// )?;
|
|
///
|
|
/// // Evaluation fails since rule x calls an extension with out parameter.
|
|
/// assert!(engine.eval_query("data.invalid.x".to_string(), false).is_err());
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn add_extension(
|
|
&mut self,
|
|
path: String,
|
|
nargs: u8,
|
|
extension: Box<dyn Extension>,
|
|
) -> Result<()> {
|
|
self.interpreter.add_extension(path, nargs, extension)
|
|
}
|
|
|
|
#[cfg(feature = "coverage")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
|
|
/// Get the coverage report.
|
|
///
|
|
/// ```rust
|
|
/// # use regorus::*;
|
|
/// # use anyhow::{bail, Result};
|
|
/// # fn main() -> Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// engine.add_policy(
|
|
/// "policy.rego".to_string(),
|
|
/// r#"
|
|
/// package test # Line 2
|
|
///
|
|
/// x = y if { # Line 4
|
|
/// input.a > 2 # Line 5
|
|
/// y = 5 # Line 6
|
|
/// }
|
|
/// "#.to_string()
|
|
/// )?;
|
|
///
|
|
/// // Enable coverage.
|
|
/// engine.set_enable_coverage(true);
|
|
///
|
|
/// engine.eval_query("data".to_string(), false)?;
|
|
///
|
|
/// let report = engine.get_coverage_report()?;
|
|
/// assert_eq!(report.files[0].path, "policy.rego");
|
|
///
|
|
/// // Only line 5 is evaluated.
|
|
/// assert_eq!(report.files[0].covered.iter().cloned().collect::<Vec<u32>>(), vec![5]);
|
|
///
|
|
/// // Line 4 and 6 are not evaluated.
|
|
/// assert_eq!(report.files[0].not_covered.iter().cloned().collect::<Vec<u32>>(), vec![4, 6]);
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
///
|
|
/// See also [`crate::coverage::Report::to_colored_string`].
|
|
pub fn get_coverage_report(&self) -> Result<crate::coverage::Report> {
|
|
self.interpreter.get_coverage_report()
|
|
}
|
|
|
|
#[cfg(feature = "coverage")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
|
|
/// Enable/disable policy coverage.
|
|
///
|
|
/// If `enable` is different from the current value, then any existing coverage
|
|
/// information will be cleared.
|
|
pub fn set_enable_coverage(&mut self, enable: bool) {
|
|
self.interpreter.set_enable_coverage(enable);
|
|
}
|
|
|
|
#[cfg(feature = "coverage")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
|
|
/// Clear the gathered policy coverage data.
|
|
pub fn clear_coverage_data(&mut self) {
|
|
self.interpreter.clear_coverage_data();
|
|
}
|
|
|
|
/// Gather output from print statements instead of emiting to stderr.
|
|
///
|
|
/// See [`Engine::take_prints`].
|
|
pub fn set_gather_prints(&mut self, b: bool) {
|
|
self.interpreter.set_gather_prints(b);
|
|
}
|
|
|
|
/// Take the gathered output of print statements.
|
|
///
|
|
/// ```rust
|
|
/// # use regorus::*;
|
|
/// # use anyhow::{bail, Result};
|
|
/// # fn main() -> Result<()> {
|
|
/// let mut engine = Engine::new();
|
|
///
|
|
/// // Print to stderr.
|
|
/// engine.eval_query("print(\"Hello\")".to_string(), false)?;
|
|
///
|
|
/// // Configure gathering print statements.
|
|
/// engine.set_gather_prints(true);
|
|
///
|
|
/// // Execute query.
|
|
/// engine.eval_query("print(\"Hello\")".to_string(), false)?;
|
|
///
|
|
/// // Take and clear prints.
|
|
/// let prints = engine.take_prints()?;
|
|
/// assert_eq!(prints.len(), 1);
|
|
/// assert!(prints[0].contains("Hello"));
|
|
///
|
|
/// for p in prints {
|
|
/// println!("{p}");
|
|
/// }
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
pub fn take_prints(&mut self) -> Result<Vec<String>> {
|
|
self.interpreter.take_prints()
|
|
}
|
|
|
|
/// Get the policies and corresponding AST.
|
|
///
|
|
///
|
|
/// ```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())?;
|
|
///
|
|
/// let ast = engine.get_ast_as_json()?;
|
|
/// let value = Value::from_json_str(&ast)?;
|
|
///
|
|
/// assert_eq!(value[0]["ast"]["package"]["refr"]["Var"][1].as_string()?.as_ref(), "test");
|
|
/// # Ok(())
|
|
/// # }
|
|
/// ```
|
|
#[cfg(feature = "ast")]
|
|
#[cfg_attr(docsrs, doc(cfg(feature = "ast")))]
|
|
pub fn get_ast_as_json(&self) -> Result<String> {
|
|
#[derive(Serialize)]
|
|
struct Policy<'a> {
|
|
source: &'a Source,
|
|
version: u32,
|
|
ast: &'a Module,
|
|
}
|
|
let mut ast = vec![];
|
|
for m in self.modules.iter() {
|
|
ast.push(Policy {
|
|
source: &m.package.span.source,
|
|
version: 1,
|
|
ast: m,
|
|
});
|
|
}
|
|
|
|
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<Vec<PolicyPackageNameDefinition>> {
|
|
let mut package_names = vec![];
|
|
for m in self.modules.iter() {
|
|
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<Vec<PolicyParameters>> {
|
|
let mut policy_parameter_definitions = vec![];
|
|
for m in self.modules.iter() {
|
|
let mut parameters = vec![];
|
|
let mut modifiers = vec![];
|
|
|
|
for rule in &m.policy {
|
|
match *rule.as_ref() {
|
|
// Extract parameter definitions from the policy rule
|
|
// e.g. default parameters.a = 5
|
|
Rule::Default { ref 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.first().is_some_and(|p| *p == "parameters") {
|
|
if let Some(name) = paths.get(1) {
|
|
// Todo: Fetch fields other than name from rego metadoc for the parameter
|
|
parameters.push(PolicyParameter {
|
|
name: (*name).to_string(),
|
|
modifiable: false,
|
|
required: false,
|
|
});
|
|
}
|
|
}
|
|
}
|
|
// Extract modifiers to the parameters from the policy rule
|
|
// e.g. parameters.a = 5
|
|
Rule::Spec { ref head, .. } => {
|
|
match *head {
|
|
RuleHead::Compr { ref 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.first().is_some_and(|p| *p == "parameters")
|
|
{
|
|
if let Some(name) = paths.get(1) {
|
|
// Todo: Fetch fields other than name from rego metadoc for the parameter
|
|
modifiers.push(PolicyModifier {
|
|
name: (*name).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)
|
|
}
|
|
|
|
/// Emit a warning if any modules contain target specifications but we're not using target-aware compilation.
|
|
#[cfg(feature = "azure_policy")]
|
|
fn warn_if_targets_present(&self) {
|
|
let mut has_target = false;
|
|
let mut target_files = Vec::new();
|
|
|
|
for module in self.modules.iter() {
|
|
if module.target.is_some() {
|
|
has_target = true;
|
|
target_files.push(module.package.span.source.get_path());
|
|
}
|
|
}
|
|
|
|
if has_target {
|
|
std::eprintln!("Warning: Target specifications found in policy modules but not using target-aware compilation.");
|
|
std::eprintln!(" The following files contain __target__ declarations:");
|
|
for file in target_files {
|
|
std::eprintln!(" - {}", file);
|
|
}
|
|
std::eprintln!(" Consider using compile_for_target() instead of compile_with_entrypoint() for target-aware evaluation.");
|
|
}
|
|
}
|
|
|
|
fn make_parser<'a>(&self, source: &'a Source) -> Result<Parser<'a>> {
|
|
let mut parser = Parser::new(source)?;
|
|
parser.set_max_col(self.policy_length_config.max_col);
|
|
if self.rego_v1 {
|
|
parser.enable_rego_v1()?;
|
|
}
|
|
Ok(parser)
|
|
}
|
|
|
|
/// Create a new Engine from a compiled policy.
|
|
#[doc(hidden)]
|
|
pub(crate) fn new_from_compiled_policy(
|
|
compiled_policy: Rc<crate::compiled_policy::CompiledPolicyData>,
|
|
) -> Self {
|
|
let modules = compiled_policy.modules.clone();
|
|
let mut engine = Self {
|
|
modules,
|
|
interpreter: Interpreter::new_from_compiled_policy(compiled_policy),
|
|
rego_v1: true, // Value doesn't matter since this is used only for policy parsing
|
|
prepared: true,
|
|
execution_timer_config: None,
|
|
policy_length_config: PolicyLengthConfig::default(), // Compiled policies are already parsed, so these length limits are not used
|
|
};
|
|
engine.apply_effective_execution_timer_config();
|
|
engine
|
|
}
|
|
}
|