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(any(
158 feature = "newton-crypto",
159 feature = "newton-identity",
160 feature = "newton-tlsn"
161))]
162pub mod extensions;
163
164#[cfg(feature = "azure_policy")]
165pub use {
166 compile::compile_policy_for_target,
167 schema::{error::ValidationError, validate::SchemaValidator, Schema},
168 target::Target,
169};
170
171pub use compile::{compile_policy_with_entrypoint, PolicyModule};
172pub use compiled_policy::CompiledPolicy;
173pub use engine::Engine;
174pub use lexer::Source;
175pub use policy_info::PolicyInfo;
176pub use utils::limits::LimitError;
177#[cfg(feature = "allocator-memory-limits")]
178pub use utils::limits::{
179 check_global_memory_limit, enforce_memory_limit, flush_thread_memory_counters,
180 global_memory_limit, set_global_memory_limit, set_thread_flush_threshold_override,
181 thread_memory_flush_threshold,
182};
183pub use value::Value;
184
185#[cfg(feature = "arc")]
186pub use alloc::sync::Arc as Rc;
187
188#[cfg(not(feature = "arc"))]
189pub use alloc::rc::Rc;
190
191#[cfg(feature = "std")]
192use std::collections::{hash_map::Entry as MapEntry, HashMap as Map, HashSet as Set};
193
194#[cfg(not(feature = "std"))]
195use alloc::collections::{btree_map::Entry as MapEntry, BTreeMap as Map, BTreeSet as Set};
196
197use alloc::{
198 borrow::ToOwned as _,
199 boxed::Box,
200 format,
201 string::{String, ToString as _},
202 vec,
203 vec::Vec,
204};
205
206use core::fmt;
207
208/// Location of an [`Expression`] in a Rego query.
209///
210/// ```
211/// # use regorus::Engine;
212/// # fn main() -> anyhow::Result<()> {
213/// // Create engine and evaluate " \n 1 + 2".
214/// let results = Engine::new().eval_query(" \n 1 + 2".to_string(), false)?;
215///
216/// // Fetch the location for the expression.
217/// let loc = &results.result[0].expressions[0].location;
218///
219/// assert_eq!(loc.row, 2);
220/// assert_eq!(loc.col, 3);
221/// # Ok(())
222/// # }
223/// ````
224/// See also [`QueryResult`].
225#[derive(Debug, Clone, Serialize, Eq, PartialEq)]
226pub struct Location {
227 /// Line number. Starts at 1.
228 pub row: u32,
229 /// Column number. Starts at 1.
230 pub col: u32,
231}
232
233/// An expression in a Rego query.
234///
235/// ```
236/// # use regorus::*;
237/// # fn main() -> anyhow::Result<()> {
238/// // Create engine and evaluate "1 + 2".
239/// let results = Engine::new().eval_query("1 + 2".to_string(), false)?;
240///
241/// // Fetch the expression from results.
242/// let expr = &results.result[0].expressions[0];
243///
244/// assert_eq!(expr.value, Value::from(3u64));
245/// assert_eq!(expr.text.as_ref(), "1 + 2");
246/// # Ok(())
247/// # }
248/// ```
249/// See also [`QueryResult`].
250#[derive(Debug, Clone, Serialize, Eq, PartialEq)]
251pub struct Expression {
252 /// Computed value of the expression.
253 pub value: Value,
254
255 /// The Rego expression.
256 pub text: Rc<str>,
257
258 /// Location of the expression in the query string.
259 pub location: Location,
260}
261
262/// Result of evaluating a Rego query.
263///
264/// A query containing single expression.
265/// ```
266/// # use regorus::*;
267/// # fn main() -> anyhow::Result<()> {
268/// // Create engine and evaluate "1 + 2".
269/// let results = Engine::new().eval_query("1 + 2".to_string(), false)?;
270///
271/// // Fetch the first (sole) result.
272/// let result = &results.result[0];
273///
274/// assert_eq!(result.expressions[0].value, Value::from(3u64));
275/// assert_eq!(result.expressions[0].text.as_ref(), "1 + 2");
276/// # Ok(())
277/// # }
278/// ```
279///
280/// A query containing multiple expressions.
281/// ```
282/// # use regorus::*;
283/// # fn main() -> anyhow::Result<()> {
284/// // Create engine and evaluate "1 + 2; 3.5 * 4".
285/// let results = Engine::new().eval_query("1 + 2; 3.55 * 4".to_string(), false)?;
286///
287/// // Fetch the first (sole) result.
288/// let result = &results.result[0];
289///
290/// // First expression.
291/// assert_eq!(result.expressions[0].value, Value::from(3u64));
292/// assert_eq!(result.expressions[0].text.as_ref(), "1 + 2");
293///
294/// // Second expression.
295/// assert_eq!(result.expressions[1].value, Value::from(14.2));
296/// assert_eq!(result.expressions[1].text.as_ref(), "3.55 * 4");
297/// # Ok(())
298/// # }
299/// ```
300///
301/// Expressions that create bindings (i.e. associate names to values) evaluate to
302/// either true or false. The value of bindings are available in the `bindings` field.
303/// ```
304/// # use regorus::*;
305/// # fn main() -> anyhow::Result<()> {
306/// // Create engine and evaluate "x = 1; y = x > 0".
307/// let results = Engine::new().eval_query("x = 1; y = x > 0".to_string(), false)?;
308///
309/// // Fetch the first (sole) result.
310/// let result = &results.result[0];
311///
312/// // First expression is true.
313/// assert_eq!(result.expressions[0].value, Value::from(true));
314/// assert_eq!(result.expressions[0].text.as_ref(), "x = 1");
315///
316/// // Second expression is true.
317/// assert_eq!(result.expressions[1].value, Value::from(true));
318/// assert_eq!(result.expressions[1].text.as_ref(), "y = x > 0");
319///
320/// // bindings contains the value for each named expession.
321/// assert_eq!(result.bindings[&Value::from("x")], Value::from(1u64));
322/// assert_eq!(result.bindings[&Value::from("y")], Value::from(true));
323/// # Ok(())
324/// # }
325/// ```
326///
327/// If any expression evaluates to false, then no results are produced.
328/// ```
329/// # use regorus::*;
330/// # fn main() -> anyhow::Result<()> {
331/// // Create engine and evaluate "true; true; false".
332/// let results = Engine::new().eval_query("true; true; false".to_string(), false)?;
333///
334/// assert!(results.result.is_empty());
335/// # Ok(())
336/// # }
337/// ```
338#[derive(Debug, Clone, Serialize, Eq, PartialEq)]
339pub struct QueryResult {
340 /// Expressions in the query.
341 ///
342 /// Each statement in the query is treated as a separte expression.
343 pub expressions: Vec<Expression>,
344
345 /// Bindings created in the query.
346 #[serde(skip_serializing_if = "Value::is_empty_object")]
347 pub bindings: Value,
348}
349
350impl Default for QueryResult {
351 fn default() -> Self {
352 Self {
353 bindings: Value::new_object(),
354 expressions: vec![],
355 }
356 }
357}
358
359/// Results of evaluating a Rego query.
360///
361/// Generates the same `json` representation as `opa eval`.
362///
363/// Queries typically produce a single result.
364/// ```
365/// # use regorus::*;
366/// # fn main() -> anyhow::Result<()> {
367/// // Create engine and evaluate "1 + 1".
368/// let results = Engine::new().eval_query("1 + 1".to_string(), false)?;
369///
370/// assert_eq!(results.result.len(), 1);
371/// assert_eq!(results.result[0].expressions[0].value, Value::from(2u64));
372/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 + 1");
373/// # Ok(())
374/// # }
375/// ```
376///
377/// If a query contains only one expression, and even if the expression evaluates
378/// to false, the value will be returned.
379/// ```
380/// # use regorus::*;
381/// # fn main() -> anyhow::Result<()> {
382/// // Create engine and evaluate "1 > 2" which is false.
383/// let results = Engine::new().eval_query("1 > 2".to_string(), false)?;
384///
385/// assert_eq!(results.result.len(), 1);
386/// assert_eq!(results.result[0].expressions[0].value, Value::from(false));
387/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 > 2");
388/// # Ok(())
389/// # }
390/// ```
391///
392/// In a query containing multiple expressions, if any expression evaluates to false,
393/// then no results are produced.
394/// ```
395/// # use regorus::*;
396/// # fn main() -> anyhow::Result<()> {
397/// // Create engine and evaluate "true; true; false".
398/// let results = Engine::new().eval_query("true; true; false".to_string(), false)?;
399///
400/// assert!(results.result.is_empty());
401/// # Ok(())
402/// # }
403/// ```
404///
405/// Note that `=` is different from `==`. The former evaluates to undefined if the LHS and RHS
406/// are not equal. The latter evaluates to either true or false.
407/// ```
408/// # use regorus::*;
409/// # fn main() -> anyhow::Result<()> {
410/// // Create engine and evaluate "1 = 2" which is undefined and produces no resutl.
411/// let results = Engine::new().eval_query("1 = 2".to_string(), false)?;
412///
413/// assert_eq!(results.result.len(), 0);
414///
415/// // Create engine and evaluate "1 == 2" which evaluates to false.
416/// let results = Engine::new().eval_query("1 == 2".to_string(), false)?;
417///
418/// assert_eq!(results.result.len(), 1);
419/// assert_eq!(results.result[0].expressions[0].value, Value::from(false));
420/// assert_eq!(results.result[0].expressions[0].text.as_ref(), "1 == 2");
421/// # Ok(())
422/// # }
423/// ```
424///
425/// Queries containing loops produce multiple results.
426/// ```
427/// # use regorus::*;
428/// # fn main() -> anyhow::Result<()> {
429/// let results = Engine::new().eval_query("x = [1, 2, 3][_]".to_string(), false)?;
430///
431/// // Three results are produced, one of each value of x.
432/// assert_eq!(results.result.len(), 3);
433///
434/// // Assert expressions and bindings of results.
435/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true));
436/// assert_eq!(
437/// results.result[0].expressions[0].text.as_ref(),
438/// "x = [1, 2, 3][_]"
439/// );
440/// assert_eq!(
441/// results.result[0].bindings[&Value::from("x")],
442/// Value::from(1u64)
443/// );
444///
445/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true));
446/// assert_eq!(
447/// results.result[1].expressions[0].text.as_ref(),
448/// "x = [1, 2, 3][_]"
449/// );
450/// assert_eq!(
451/// results.result[1].bindings[&Value::from("x")],
452/// Value::from(2u64)
453/// );
454///
455/// assert_eq!(results.result[2].expressions[0].value, Value::Bool(true));
456/// assert_eq!(
457/// results.result[2].expressions[0].text.as_ref(),
458/// "x = [1, 2, 3][_]"
459/// );
460/// assert_eq!(
461/// results.result[2].bindings[&Value::from("x")],
462/// Value::from(3u64)
463/// );
464/// # Ok(())
465/// # }
466/// ```
467///
468/// Loop iterations that evaluate to false or undefined don't produce results.
469/// ```
470/// # use regorus::*;
471/// # fn main() -> anyhow::Result<()> {
472/// let results = Engine::new().eval_query("x = [1, 2, 3][_]; x >= 2".to_string(), false)?;
473///
474/// // Two results are produced, one for x = 2 and another for x = 3.
475/// assert_eq!(results.result.len(), 2);
476///
477/// // Assert expressions and bindings of results.
478/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true));
479/// assert_eq!(
480/// results.result[0].expressions[0].text.as_ref(),
481/// "x = [1, 2, 3][_]"
482/// );
483/// assert_eq!(results.result[0].expressions[0].value, Value::Bool(true));
484/// assert_eq!(results.result[0].expressions[1].text.as_ref(), "x >= 2");
485/// assert_eq!(
486/// results.result[0].bindings[&Value::from("x")],
487/// Value::from(2u64)
488/// );
489///
490/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true));
491/// assert_eq!(
492/// results.result[1].expressions[0].text.as_ref(),
493/// "x = [1, 2, 3][_]"
494/// );
495/// assert_eq!(results.result[1].expressions[0].value, Value::Bool(true));
496/// assert_eq!(results.result[1].expressions[1].text.as_ref(), "x >= 2");
497/// assert_eq!(
498/// results.result[1].bindings[&Value::from("x")],
499/// Value::from(3u64)
500/// );
501/// # Ok(())
502/// # }
503/// ```
504///
505/// See [QueryResult] for examples of different kinds of results.
506#[derive(Debug, Clone, Default, Serialize, Eq, PartialEq)]
507pub struct QueryResults {
508 /// Collection of results of evaluting a query.
509 #[serde(skip_serializing_if = "Vec::is_empty")]
510 pub result: Vec<QueryResult>,
511}
512
513/// A user defined builtin function implementation.
514///
515/// It is not necessary to implement this trait directly.
516pub trait Extension: FnMut(Vec<Value>) -> anyhow::Result<Value> + Send + Sync {
517 /// Fn, FnMut etc are not sized and cannot be cloned in their boxed form.
518 /// clone_box exists to overcome that.
519 fn clone_box<'a>(&self) -> Box<dyn 'a + Extension>
520 where
521 Self: 'a;
522}
523
524/// Automatically make matching closures a valid [`Extension`].
525impl<F> Extension for F
526where
527 F: FnMut(Vec<Value>) -> anyhow::Result<Value> + Clone + Send + Sync,
528{
529 fn clone_box<'a>(&self) -> Box<dyn 'a + Extension>
530 where
531 Self: 'a,
532 {
533 Box::new(self.clone())
534 }
535}
536
537/// Implement clone for a boxed extension using [`Extension::clone_box`].
538impl Clone for Box<dyn '_ + Extension> {
539 fn clone(&self) -> Self {
540 (**self).clone_box()
541 }
542}
543
544impl fmt::Debug for dyn Extension {
545 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> core::result::Result<(), fmt::Error> {
546 f.write_fmt(format_args!("<extension>"))
547 }
548}
549
550#[cfg(feature = "coverage")]
551#[cfg_attr(docsrs, doc(cfg(feature = "coverage")))]
552pub mod coverage {
553 use crate::*;
554
555 #[allow(missing_debug_implementations)]
556 #[derive(Default, serde::Serialize, serde::Deserialize)]
557 /// Coverage information about a rego policy file.
558 pub struct File {
559 /// Path of the policy file.
560 pub path: String,
561
562 /// The rego policy.
563 pub code: String,
564
565 /// Lines that were evaluated.
566 pub covered: alloc::collections::BTreeSet<u32>,
567
568 /// Lines that were not evaluated.
569 pub not_covered: alloc::collections::BTreeSet<u32>,
570 }
571
572 #[allow(missing_debug_implementations)]
573 #[derive(Default, serde::Serialize, serde::Deserialize)]
574 /// Policy coverage report.
575 pub struct Report {
576 /// Coverage information for files.
577 pub files: Vec<File>,
578 }
579
580 impl Report {
581 /// Produce an ANSI color encoded version of the report.
582 ///
583 /// Covered lines are green.
584 /// Lines that are not covered are red.
585 ///
586 /// <img src="https://github.com/microsoft/regorus/blob/main/docs/coverage.png?raw=true">
587 #[allow(clippy::arithmetic_side_effects)]
588 pub fn to_string_pretty(&self) -> anyhow::Result<String> {
589 let mut s = String::default();
590 s.push_str("COVERAGE REPORT:\n");
591 for file in self.files.iter() {
592 if file.not_covered.is_empty() {
593 s.push_str(&format!("{} has full coverage\n", file.path));
594 continue;
595 }
596
597 s.push_str(&format!("{}:\n", file.path));
598 for (line_idx, code) in file.code.split('\n').enumerate() {
599 let line = u32::try_from(line_idx + 1).unwrap_or(u32::MAX);
600 if file.not_covered.contains(&line) {
601 s.push_str(&format!("\x1b[31m {line:4} {code}\x1b[0m\n"));
602 } else if file.covered.contains(&line) {
603 s.push_str(&format!("\x1b[32m {line:4} {code}\x1b[0m\n"));
604 } else {
605 s.push_str(&format!(" {line:4} {code}\n"));
606 }
607 }
608 }
609
610 s.push('\n');
611 Ok(s)
612 }
613 }
614}
615
616/// Items in `unstable` are likely to change.
617#[doc(hidden)]
618pub mod unstable {
619 pub use crate::{ast::*, lexer::*, parser::*};
620}
621
622#[cfg(test)]
623mod tests;