regorus/
lib.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4// Unsafe code should not be used.
5// Hard to reason about correctness, and maintainability.
6#![forbid(unsafe_code)]
7// Ensure that all lint names are valid.
8#![deny(unknown_lints)]
9// Fail-fast lints: correctness, safety, and API surface
10#![deny(
11    // Panic sources - catch all ways code can panic
12    clippy::panic, // forbid explicit panic! macro
13    clippy::unreachable, // catches unreachable! macro usage
14    clippy::todo, // blocks remaining todo! placeholders
15    clippy::unimplemented, // blocks unimplemented! placeholders
16    clippy::unwrap_used, // reject Result/Option unwraps
17    clippy::expect_used, // reject expect with panic messages
18    clippy::manual_assert, // prefer assert! over manual if/panic
19    clippy::indexing_slicing, // reject unchecked [] indexing
20    clippy::arithmetic_side_effects, // reject overflowing/unchecked math
21    clippy::panic_in_result_fn, // disallow panic inside functions returning Result
22
23    // Rust warnings/upstream
24    dead_code, // ban unused items
25    deprecated, // prevent use of deprecated APIs
26    deprecated_in_future, // catch items scheduled for deprecation
27    exported_private_dependencies, // avoid leaking private deps in public API
28    future_incompatible, // catch patterns slated to break
29    invalid_doc_attributes, // ensure doc attributes are valid
30    keyword_idents, // disallow identifiers that are keywords
31    macro_use_extern_crate, // block legacy macro_use extern crate
32    missing_debug_implementations, // require Debug on public types
33    // TODO: Address in future pass
34    // missing_docs, // require docs on public items
35    non_ascii_idents, // disallow non-ASCII identifiers
36    nonstandard_style, // enforce idiomatic naming/style
37    noop_method_call, // catch no-op method calls
38    trivial_bounds, // forbid useless trait bounds
39    trivial_casts, // block needless casts
40    unreachable_code, // catch dead/unreachable code
41    unreachable_patterns, // catch unreachable match arms
42    // TODO: Address in future pass
43    // unreachable_pub,
44    unused_extern_crates, // remove unused extern crate declarations
45    unused_import_braces, // avoid unused braces in imports
46    absolute_paths_not_starting_with_crate, // enforce crate:: prefix for absolute paths
47
48    // Unsafe code / low-level hazards
49    clippy::unseparated_literal_suffix, // enforce underscore before literal suffixes
50    clippy::print_stderr, // discourage printing to stderr
51    clippy::use_debug, // discourage Debug formatting in display contexts
52
53    // Documentation & diagnostics
54    // TODO: Address in future pass
55    // clippy::doc_link_with_quotes, // avoid quoted intra-doc links
56    // clippy::doc_markdown, // flag bad Markdown in docs
57    // clippy::missing_docs_in_private_items, // require docs on private items
58    // clippy::missing_errors_doc, // require docs for error cases
59
60    // API correctness / style
61    clippy::missing_const_for_fn, // suggest const fn where possible
62    clippy::option_if_let_else, // prefer map_or/unwrap_or_else over if/let
63    clippy::if_then_some_else_none, // prefer Option combinators over if/else
64    clippy::semicolon_if_nothing_returned, // enforce trailing semicolon for unit
65    clippy::unused_self, // remove unused self parameters
66    clippy::used_underscore_binding, // avoid using bindings prefixed with _
67    clippy::useless_let_if_seq, // simplify let-if sequences
68    clippy::similar_names, // flag confusingly similar identifiers
69    clippy::shadow_unrelated, // discourage shadowing unrelated variables
70    clippy::redundant_pub_crate, // avoid pub(crate) on already pub items
71    clippy::wildcard_dependencies, // disallow wildcard Cargo dependency versions
72    // TODO: Address in future pass
73    // clippy::wildcard_imports, // discourage glob imports
74
75    // Numeric correctness
76    // TODO: Address in future pass
77    clippy::float_cmp, // avoid exact float equality checks
78    clippy::float_cmp_const, // avoid comparing floats to consts directly
79    clippy::float_equality_without_abs, // require tolerance in float equality
80    clippy::suspicious_operation_groupings, // catch ambiguous operator precedence
81
82    // no_std hygiene
83    clippy::std_instead_of_core, // prefer core/alloc over std in no_std
84
85    // Misc polish
86    clippy::dbg_macro, // forbid dbg! in production code
87    clippy::debug_assert_with_mut_call, // avoid mutating inside debug_assert
88    clippy::empty_line_after_outer_attr, // enforce spacing after outer attrs
89    clippy::empty_structs_with_brackets, // use unit structs without braces
90)]
91// Advisory lints: useful, but not fatal
92#![warn(
93    clippy::assertions_on_result_states, // avoid asserts on Result state
94    clippy::match_like_matches_macro, // prefer matches! macro over verbose match
95    clippy::needless_continue, // remove redundant continue statements
96    clippy::unused_trait_names, // drop unused trait imports
97    clippy::verbose_file_reads, // prefer concise file read helpers
98    clippy::as_conversions, // discourage lossy as casts
99    clippy::pattern_type_mismatch, // catch mismatched types in patterns
100)]
101#![cfg_attr(docsrs, feature(doc_cfg))]
102// Use README.md as crate documentation.
103#![doc = include_str!(concat!(env!("CARGO_MANIFEST_DIR"), "/README.md"))]
104// We'll default to building for no_std - use core, alloc instead of std.
105#![no_std]
106
107extern crate alloc;
108use serde::Serialize;
109
110// Import std crate if building with std support.
111// We don't import types or macros from std.
112// As a result, types and macros from std must be qualified via `std::`
113// making dependencies on std easier to spot.
114#[cfg(any(feature = "std", test))]
115extern crate std;
116
117#[cfg(feature = "mimalloc")]
118mimalloc::assign_global!();
119
120mod ast;
121mod builtins;
122mod compile;
123mod compiled_policy;
124mod compiler;
125mod engine;
126mod indexchecker;
127mod interpreter;
128
129pub mod languages {
130    #[cfg(feature = "azure-rbac")]
131    pub mod azure_rbac;
132
133    #[cfg(feature = "rvm")]
134    pub mod rego;
135}
136
137mod lexer;
138pub(crate) mod lookup;
139mod number;
140mod parser;
141mod policy_info;
142mod query;
143#[cfg(feature = "azure_policy")]
144pub mod registry;
145#[cfg(feature = "rvm")]
146pub mod rvm;
147mod scheduler;
148#[cfg(feature = "azure_policy")]
149mod schema;
150#[cfg(feature = "azure_policy")]
151pub mod target;
152#[cfg(any(test, all(feature = "yaml", feature = "std")))]
153pub mod test_utils;
154pub mod utils;
155mod value;
156
157#[cfg(feature = "azure_policy")]
158pub use {
159    compile::compile_policy_for_target,
160    schema::{error::ValidationError, validate::SchemaValidator, Schema},
161    target::Target,
162};
163
164pub use compile::{compile_policy_with_entrypoint, PolicyModule};
165pub use compiled_policy::CompiledPolicy;
166pub use engine::Engine;
167pub use lexer::Source;
168pub use policy_info::PolicyInfo;
169pub use utils::limits::LimitError;
170#[cfg(feature = "allocator-memory-limits")]
171pub use utils::limits::{
172    check_global_memory_limit, enforce_memory_limit, flush_thread_memory_counters,
173    global_memory_limit, set_global_memory_limit, set_thread_flush_threshold_override,
174    thread_memory_flush_threshold,
175};
176pub use value::Value;
177
178#[cfg(feature = "arc")]
179pub use alloc::sync::Arc as Rc;
180
181#[cfg(not(feature = "arc"))]
182pub use alloc::rc::Rc;
183
184#[cfg(feature = "std")]
185use std::collections::{hash_map::Entry as MapEntry, HashMap as Map, HashSet as Set};
186
187#[cfg(not(feature = "std"))]
188use alloc::collections::{btree_map::Entry as MapEntry, BTreeMap as Map, BTreeSet as Set};
189
190use alloc::{
191    borrow::ToOwned as _,
192    boxed::Box,
193    format,
194    string::{String, ToString as _},
195    vec,
196    vec::Vec,
197};
198
199use core::fmt;
200
201/// Location of an [`Expression`] in a Rego query.
202///
203/// ```
204/// # use regorus::Engine;
205/// # fn main() -> anyhow::Result<()> {
206/// // Create engine and evaluate "  \n  1 + 2".
207/// let results = Engine::new().eval_query("  \n  1 + 2".to_string(), false)?;
208///
209/// // Fetch the location for the expression.
210/// let loc = &results.result[0].expressions[0].location;
211///
212/// assert_eq!(loc.row, 2);
213/// assert_eq!(loc.col, 3);
214/// # Ok(())
215/// # }
216/// ````
217/// See also [`QueryResult`].
218#[derive(Debug, Clone, Serialize, Eq, PartialEq)]
219pub struct Location {
220    /// Line number. Starts at 1.
221    pub row: u32,
222    /// Column number. Starts at 1.
223    pub col: u32,
224}
225
226/// An expression in a Rego query.
227///
228/// ```
229/// # use regorus::*;
230/// # fn main() -> anyhow::Result<()> {
231/// // Create engine and evaluate "1 + 2".
232/// let results = Engine::new().eval_query("1 + 2".to_string(), false)?;
233///
234/// // Fetch the expression from results.
235/// let expr = &results.result[0].expressions[0];
236///
237/// assert_eq!(expr.value, Value::from(3u64));
238/// assert_eq!(expr.text.as_ref(), "1 + 2");
239/// # Ok(())
240/// # }
241/// ```
242/// See also [`QueryResult`].
243#[derive(Debug, Clone, Serialize, Eq, PartialEq)]
244pub struct Expression {
245    /// Computed value of the expression.
246    pub value: Value,
247
248    /// The Rego expression.
249    pub text: Rc<str>,
250
251    /// Location of the expression in the query string.
252    pub location: Location,
253}
254
255/// Result of evaluating a Rego query.
256///
257/// A query containing single expression.
258/// ```
259/// # use regorus::*;
260/// # fn main() -> anyhow::Result<()> {
261/// // Create engine and evaluate "1 + 2".
262/// let results = Engine::new().eval_query("1 + 2".to_string(), false)?;
263///
264/// // Fetch the first (sole) result.
265/// let result = &results.result[0];
266///
267/// assert_eq!(result.expressions[0].value, Value::from(3u64));
268/// assert_eq!(result.expressions[0].text.as_ref(), "1 + 2");
269/// # Ok(())
270/// # }
271/// ```
272///
273/// A query containing multiple expressions.
274/// ```
275/// # use regorus::*;
276/// # fn main() -> anyhow::Result<()> {
277/// // Create engine and evaluate "1 + 2; 3.5 * 4".
278/// let results = Engine::new().eval_query("1 + 2; 3.55 * 4".to_string(), false)?;
279///
280/// // Fetch the first (sole) result.
281/// let result = &results.result[0];
282///
283/// // First expression.
284/// assert_eq!(result.expressions[0].value, Value::from(3u64));
285/// assert_eq!(result.expressions[0].text.as_ref(), "1 + 2");
286///
287/// // Second expression.
288/// assert_eq!(result.expressions[1].value, Value::from(14.2));
289/// assert_eq!(result.expressions[1].text.as_ref(), "3.55 * 4");
290/// # Ok(())
291/// # }
292/// ```
293///
294/// Expressions that create bindings (i.e. associate names to values) evaluate to
295/// either true or false. The value of bindings are available in the `bindings` field.
296/// ```
297/// # use regorus::*;
298/// # fn main() -> anyhow::Result<()> {
299/// // Create engine and evaluate "x = 1; y = x > 0".
300/// let results = Engine::new().eval_query("x = 1; y = x > 0".to_string(), false)?;
301///
302/// // Fetch the first (sole) result.
303/// let result = &results.result[0];
304///
305/// // First expression is true.
306/// assert_eq!(result.expressions[0].value, Value::from(true));
307/// assert_eq!(result.expressions[0].text.as_ref(), "x = 1");
308///
309/// // Second expression is true.
310/// assert_eq!(result.expressions[1].value, Value::from(true));
311/// assert_eq!(result.expressions[1].text.as_ref(), "y = x > 0");
312///
313/// // bindings contains the value for each named expession.
314/// assert_eq!(result.bindings[&Value::from("x")], Value::from(1u64));
315/// assert_eq!(result.bindings[&Value::from("y")], Value::from(true));
316/// # Ok(())
317/// # }
318/// ```
319///
320/// If any expression evaluates to false, then no results are produced.
321/// ```
322/// # use regorus::*;
323/// # fn main() -> anyhow::Result<()> {
324/// // Create engine and evaluate "true; true; false".
325/// let results = Engine::new().eval_query("true; true; false".to_string(), false)?;
326///
327/// assert!(results.result.is_empty());
328/// # Ok(())
329/// # }
330/// ```
331#[derive(Debug, Clone, Serialize, Eq, PartialEq)]
332pub struct QueryResult {
333    /// Expressions in the query.
334    ///
335    /// Each statement in the query is treated as a separte expression.
336    ///
337    pub expressions: Vec<Expression>,
338
339    /// Bindings created in the query.
340    #[serde(skip_serializing_if = "Value::is_empty_object")]
341    pub bindings: Value,
342}
343
344impl Default for QueryResult {
345    fn default() -> Self {
346        Self {
347            bindings: Value::new_object(),
348            expressions: vec![],
349        }
350    }
351}
352
353/// Results of evaluating a Rego query.
354///
355/// Generates the same `json` representation as `opa eval`.
356///
357/// Queries typically produce a single result.
358/// ```
359/// # use regorus::*;
360/// # fn main() -> anyhow::Result<()> {
361/// // Create engine and evaluate "1 + 1".
362/// let results = Engine::new().eval_query("1 + 1".to_string(), false)?;
363///
364/// assert_eq!(results.result.len(), 1);
365/// assert_eq!(results.result[0].expressions[0].value, Value::from(2u64));
366/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 + 1");
367/// # Ok(())
368/// # }
369/// ```
370///
371/// If a query contains only one expression, and even if the expression evaluates
372/// to false, the value will be returned.
373/// ```
374/// # use regorus::*;
375/// # fn main() -> anyhow::Result<()> {
376/// // Create engine and evaluate "1 > 2" which is false.
377/// let results = Engine::new().eval_query("1 > 2".to_string(), false)?;
378///
379/// assert_eq!(results.result.len(), 1);
380/// assert_eq!(results.result[0].expressions[0].value, Value::from(false));
381/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 > 2");
382/// # Ok(())
383/// # }
384/// ```
385///
386/// In a query containing multiple expressions, if  any expression evaluates to false,
387/// then no results are produced.
388/// ```
389/// # use regorus::*;
390/// # fn main() -> anyhow::Result<()> {
391/// // Create engine and evaluate "true; true; false".
392/// let results = Engine::new().eval_query("true; true; false".to_string(), false)?;
393///
394/// assert!(results.result.is_empty());
395/// # Ok(())
396/// # }
397/// ```
398///
399/// Note that `=` is different from `==`. The former evaluates to undefined if the LHS and RHS
400/// are not equal. The latter evaluates to either true or false.
401/// ```
402/// # use regorus::*;
403/// # fn main() -> anyhow::Result<()> {
404/// // Create engine and evaluate "1 = 2" which is undefined and produces no resutl.
405/// let results = Engine::new().eval_query("1 = 2".to_string(), false)?;
406///
407/// assert_eq!(results.result.len(), 0);
408///
409/// // Create engine and evaluate "1 == 2" which evaluates to false.
410/// let results = Engine::new().eval_query("1 == 2".to_string(), false)?;
411///
412/// assert_eq!(results.result.len(), 1);
413/// assert_eq!(results.result[0].expressions[0].value, Value::from(false));
414/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 == 2");
415/// # Ok(())
416/// # }
417/// ```
418///
419/// Queries containing loops produce multiple results.
420/// ```
421/// # use regorus::*;
422/// # fn main() -> anyhow::Result<()> {
423/// let results = Engine::new().eval_query("x = [1, 2, 3][_]".to_string(), false)?;
424///
425/// // Three results are produced, one of each value of x.
426/// assert_eq!(results.result.len(), 3);
427///
428/// // Assert expressions and bindings of results.
429/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true));
430/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "x = [1, 2, 3][_]");
431/// assert_eq!(results.result[0].bindings[&Value::from("x")], Value::from(1u64));
432///
433/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true));
434/// assert_eq!(results.result[1].expressions[0].text.as_ref(), "x = [1, 2, 3][_]");
435/// assert_eq!(results.result[1].bindings[&Value::from("x")], Value::from(2u64));
436///
437/// assert_eq!(results.result[2].expressions[0].value, Value::Bool(true));
438/// assert_eq!(results.result[2].expressions[0].text.as_ref(), "x = [1, 2, 3][_]");
439/// assert_eq!(results.result[2].bindings[&Value::from("x")], Value::from(3u64));
440/// # Ok(())
441/// # }
442/// ```
443///
444/// Loop iterations that evaluate to false or undefined don't produce results.
445/// ```
446/// # use regorus::*;
447/// # fn main() -> anyhow::Result<()> {
448/// let results = Engine::new().eval_query("x = [1, 2, 3][_]; x >= 2".to_string(), false)?;
449///
450/// // Two results are produced, one for x = 2 and another for x = 3.
451/// assert_eq!(results.result.len(), 2);
452///
453/// // Assert expressions and bindings of results.
454/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true));
455/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "x = [1, 2, 3][_]");
456/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true));
457/// assert_eq!(results.result[0].expressions[1].text.as_ref(), "x >= 2");
458/// assert_eq!(results.result[0].bindings[&Value::from("x")], Value::from(2u64));
459///
460/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true));
461/// assert_eq!(results.result[1].expressions[0].text.as_ref(), "x = [1, 2, 3][_]");
462/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true));
463/// assert_eq!(results.result[1].expressions[1].text.as_ref(), "x >= 2");
464/// assert_eq!(results.result[1].bindings[&Value::from("x")], Value::from(3u64));
465/// # Ok(())
466/// # }
467/// ```
468///
469/// See [QueryResult] for examples of different kinds of results.
470#[derive(Debug, Clone, Default, Serialize, Eq, PartialEq)]
471pub struct QueryResults {
472    /// Collection of results of evaluting a query.
473    #[serde(skip_serializing_if = "Vec::is_empty")]
474    pub result: Vec<QueryResult>,
475}
476
477/// A user defined builtin function implementation.
478///
479/// It is not necessary to implement this trait directly.
480pub trait Extension: FnMut(Vec<Value>) -> anyhow::Result<Value> + Send + Sync {
481    /// Fn, FnMut etc are not sized and cannot be cloned in their boxed form.
482    /// clone_box exists to overcome that.
483    fn clone_box<'a>(&self) -> Box<dyn 'a + Extension>
484    where
485        Self: 'a;
486}
487
488/// Automatically make matching closures a valid [`Extension`].
489impl<F> Extension for F
490where
491    F: FnMut(Vec<Value>) -> anyhow::Result<Value> + Clone + Send + Sync,
492{
493    fn clone_box<'a>(&self) -> Box<dyn 'a + Extension>
494    where
495        Self: 'a,
496    {
497        Box::new(self.clone())
498    }
499}
500
501/// Implement clone for a boxed extension using [`Extension::clone_box`].
502impl Clone for Box<dyn '_ + Extension> {
503    fn clone(&self) -> Self {
504        (**self).clone_box()
505    }
506}
507
508impl fmt::Debug for dyn Extension {
509    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::result::Result<(), fmt::Error> {
510        f.write_fmt(format_args!("<extension>"))
511    }
512}
513
514#[cfg(feature = "coverage")]
515#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
516pub mod coverage {
517    use crate::*;
518
519    #[allow(missing_debug_implementations)]
520    #[derive(Default, serde::Serialize, serde::Deserialize)]
521    /// Coverage information about a rego policy file.
522    pub struct File {
523        /// Path of the policy file.
524        pub path: String,
525
526        /// The rego policy.
527        pub code: String,
528
529        /// Lines that were evaluated.
530        pub covered: alloc::collections::BTreeSet<u32>,
531
532        /// Lines that were not evaluated.
533        pub not_covered: alloc::collections::BTreeSet<u32>,
534    }
535
536    #[allow(missing_debug_implementations)]
537    #[derive(Default, serde::Serialize, serde::Deserialize)]
538    /// Policy coverage report.
539    pub struct Report {
540        /// Coverage information for files.
541        pub files: Vec<File>,
542    }
543
544    impl Report {
545        /// Produce an ANSI color encoded version of the report.
546        ///
547        /// Covered lines are green.
548        /// Lines that are not covered are red.
549        ///
550        /// <img src="https://github.com/microsoft/regorus/blob/main/docs/coverage.png?raw=true">
551        #[allow(clippy::arithmetic_side_effects)]
552        pub fn to_string_pretty(&self) -> anyhow::Result<String> {
553            let mut s = String::default();
554            s.push_str("COVERAGE REPORT:\n");
555            for file in self.files.iter() {
556                if file.not_covered.is_empty() {
557                    s.push_str(&format!("{} has full coverage\n", file.path));
558                    continue;
559                }
560
561                s.push_str(&format!("{}:\n", file.path));
562                for (line_idx, code) in file.code.split('\n').enumerate() {
563                    let line = u32::try_from(line_idx + 1).unwrap_or(u32::MAX);
564                    if file.not_covered.contains(&line) {
565                        s.push_str(&format!("\x1b[31m {line:4}  {code}\x1b[0m\n"));
566                    } else if file.covered.contains(&line) {
567                        s.push_str(&format!("\x1b[32m {line:4}  {code}\x1b[0m\n"));
568                    } else {
569                        s.push_str(&format!(" {line:4}  {code}\n"));
570                    }
571                }
572            }
573
574            s.push('\n');
575            Ok(s)
576        }
577    }
578}
579
580/// Items in `unstable` are likely to change.
581#[doc(hidden)]
582pub mod unstable {
583    pub use crate::ast::*;
584    pub use crate::lexer::*;
585    pub use crate::parser::*;
586}
587
588#[cfg(test)]
589mod tests;