// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. use anyhow::{anyhow, bail, Result}; #[allow(dead_code)] fn read_file(path: &String) -> Result { std::fs::read_to_string(path).map_err(|_| anyhow!("could not read {path}")) } #[allow(unused_variables)] fn read_value_from_yaml_file(path: &String) -> Result { #[cfg(feature = "yaml")] return regorus::Value::from_yaml_file(path); #[cfg(not(feature = "yaml"))] bail!("regorus has not been built with yaml support"); } fn read_value_from_json_file(path: &String) -> Result { #[cfg(feature = "std")] return regorus::Value::from_json_file(path); #[cfg(not(feature = "std"))] regorus::Value::from_json_str(&read_file(path)?) } fn add_policy_from_file(engine: &mut regorus::Engine, path: String) -> Result { #[cfg(feature = "std")] return engine.add_policy_from_file(path); #[cfg(not(feature = "std"))] engine.add_policy(path.clone(), read_file(&path)?) } #[allow(clippy::too_many_arguments)] fn rego_eval( bundles: &[String], files: &[String], input: Option, query: String, enable_tracing: bool, non_strict: bool, #[cfg(feature = "coverage")] coverage: bool, v1: bool, ) -> Result<()> { // Create engine. let mut engine = regorus::Engine::new(); engine.set_strict_builtin_errors(!non_strict); #[cfg(feature = "coverage")] engine.set_enable_coverage(coverage); engine.set_rego_v1(v1); // Load files from given bundles. for dir in bundles.iter() { let entries = std::fs::read_dir(dir).or_else(|e| bail!("failed to read bundle {dir}.\n{e}"))?; // Loop through each entry in the bundle folder. for entry in entries { let entry = entry.or_else(|e| bail!("failed to unwrap entry. {e}"))?; let path = entry.path(); // Process only .rego files. match (path.is_file(), path.extension()) { (true, Some(ext)) if ext == "rego" => {} _ => continue, } let _package = add_policy_from_file(&mut engine, entry.path().display().to_string())?; } } // Load given files. for file in files.iter() { if file.ends_with(".rego") { // Read policy file. let _package = add_policy_from_file(&mut engine, file.clone())?; } else { // Read data file. let data = if file.ends_with(".json") { read_value_from_json_file(file)? } else if file.ends_with(".yaml") { read_value_from_yaml_file(file)? } else { bail!("Unsupported data file `{file}`. Must be rego, json or yaml."); }; // Merge given data. engine.add_data(data)?; } } if let Some(file) = input { let input = if file.ends_with(".json") { read_value_from_json_file(&file)? } else if file.ends_with(".yaml") { read_value_from_yaml_file(&file)? } else { bail!("Unsupported input file `{file}`. Must be json or yaml.") }; engine.set_input(input); } // Note: The `eval_query` function is used below since it produces output // in the same format as OPA. It also allows evaluating arbitrary statements // as queries. // // Most applications will want to use `eval_rule` instead. // It is faster since it does not have to parse the query string. // It also returns the value of the rule directly and thus is easier // to use. let results = engine.eval_query(query, enable_tracing)?; println!("{}", serde_json::to_string_pretty(&results)?); #[cfg(feature = "coverage")] if coverage { let report = engine.get_coverage_report()?; println!("{}", report.to_string_pretty()?); } Ok(()) } fn rego_lex(file: String, verbose: bool) -> Result<()> { use regorus::unstable::*; // Create source. #[cfg(feature = "std")] let source = Source::from_file(file)?; #[cfg(not(feature = "std"))] let source = Source::from_contents(file.clone(), read_file(&file)?)?; // Create lexer. let mut lexer = Lexer::new(&source); // Read tokens until EOF. loop { let token = lexer.next_token()?; if token.0 == TokenKind::Eof { break; } if verbose { // Print each token's line and mark with with ^. println!("{}", token.1.message("", "")); } // Print the token. println!("{token:?}"); } Ok(()) } fn rego_parse(file: String) -> Result<()> { use regorus::unstable::*; // Create source. #[cfg(feature = "std")] let source = Source::from_file(file)?; #[cfg(not(feature = "std"))] let source = Source::from_contents(file.clone(), read_file(&file)?)?; // Create a parser and parse the source. let mut parser = Parser::new(&source)?; let ast = parser.parse()?; println!("{ast:#?}"); Ok(()) } #[allow(unused_variables)] fn rego_ast(file: String) -> Result<()> { #[cfg(feature = "ast")] { // Create engine. let mut engine = regorus::Engine::new(); // Create source. #[cfg(feature = "std")] engine.add_policy_from_file(file)?; #[cfg(not(feature = "std"))] engine.add_policy(file.clone(), read_file(&file)?)?; let ast = engine.get_ast_as_json()?; println!("{ast}"); Ok(()) } #[cfg(not(feature = "ast"))] { bail!("`ast` feature must be enabled"); } } #[derive(clap::Subcommand)] enum RegorusCommand { /// Parse a Rego policy and dump AST. Ast { /// Rego policy file. file: String, }, /// Evaluate a Rego Query. Eval { /// Directories containing Rego files. #[arg(long, short, value_name = "bundle")] bundles: Vec, /// Policy or data files. Rego, json or yaml. #[arg(long, short, value_name = "policy.rego|data.json|data.yaml")] data: Vec, /// Input file. json or yaml. #[arg(long, short, value_name = "input.rego")] input: Option, /// Query. Rego query block. query: String, /// Enable tracing. #[arg(long, short)] trace: bool, /// Perform non-strict evaluation. (default behavior of OPA). #[arg(long, short)] non_strict: bool, /// Display coverage information #[cfg(feature = "coverage")] #[arg(long, short)] coverage: bool, /// Turn on rego.v1 #[arg(long)] v1: bool, }, /// Tokenize a Rego policy. Lex { /// Rego policy file. file: String, /// Verbose output. #[arg(long, short)] verbose: bool, }, /// Parse a Rego policy. Parse { /// Rego policy file. file: String, }, } #[derive(clap::Parser)] #[command(author, version, about, long_about = None)] struct Cli { #[command(subcommand)] command: RegorusCommand, } fn main() -> Result<()> { use clap::Parser; // Parse and dispatch command. let cli = Cli::parse(); match cli.command { RegorusCommand::Eval { bundles, data, input, query, trace, non_strict, #[cfg(feature = "coverage")] coverage, v1, } => rego_eval( &bundles, &data, input, query, trace, non_strict, #[cfg(feature = "coverage")] coverage, v1, ), RegorusCommand::Lex { file, verbose } => rego_lex(file, verbose), RegorusCommand::Parse { file } => rego_parse(file), RegorusCommand::Ast { file } => rego_ast(file), } }