use std::fs; use std::path::PathBuf; use anyhow::Context; use anyhow::Result; use clap::Parser; use serde::Serialize; use crate::Decision; use crate::Policy; use crate::PolicyParser; use crate::RuleMatch; /// Arguments for evaluating a command against one or more execpolicy files. #[derive(Debug, Parser, Clone)] pub struct ExecPolicyCheckCommand { /// Paths to execpolicy rule files to evaluate (repeatable). #[arg(short = 'r', long = "rules", value_name = "PATH", required = true)] pub rules: Vec, /// Pretty-print the JSON output. #[arg(long)] pub pretty: bool, /// Command tokens to check against the policy. #[arg( value_name = "COMMAND", required = true, trailing_var_arg = true, allow_hyphen_values = true )] pub command: Vec, } impl ExecPolicyCheckCommand { /// Load the policies for this command, evaluate the command, and render JSON output. pub fn run(&self) -> Result<()> { let policy = load_policies(&self.rules)?; let matched_rules = policy.matches_for_command(&self.command, None); let json = format_matches_json(&matched_rules, self.pretty)?; println!("{json}"); Ok(()) } } pub fn format_matches_json(matched_rules: &[RuleMatch], pretty: bool) -> Result { let output = ExecPolicyCheckOutput { matched_rules, decision: matched_rules.iter().map(RuleMatch::decision).max(), }; if pretty { serde_json::to_string_pretty(&output).map_err(Into::into) } else { serde_json::to_string(&output).map_err(Into::into) } } pub fn load_policies(policy_paths: &[PathBuf]) -> Result { let mut parser = PolicyParser::new(); for policy_path in policy_paths { let policy_file_contents = fs::read_to_string(policy_path) .with_context(|| format!("failed to read policy at {}", policy_path.display()))?; let policy_identifier = policy_path.to_string_lossy().to_string(); parser .parse(&policy_identifier, &policy_file_contents) .with_context(|| format!("failed to parse policy at {}", policy_path.display()))?; } Ok(parser.build()) } #[derive(Serialize)] #[serde(rename_all = "camelCase")] struct ExecPolicyCheckOutput<'a> { #[serde(rename = "matchedRules")] matched_rules: &'a [RuleMatch], #[serde(skip_serializing_if = "Option::is_none")] decision: Option, }