Skip to main content

codex_execpolicy/
execpolicycheck.rs

1use std::fs;
2use std::path::PathBuf;
3
4use anyhow::Context;
5use anyhow::Result;
6use clap::Parser;
7use serde::Serialize;
8
9use crate::Decision;
10use crate::MatchOptions;
11use crate::Policy;
12use crate::PolicyParser;
13use crate::RuleMatch;
14
15/// Arguments for evaluating a command against one or more execpolicy files.
16#[derive(Debug, Parser, Clone)]
17pub struct ExecPolicyCheckCommand {
18    /// Paths to execpolicy rule files to evaluate (repeatable).
19    #[arg(short = 'r', long = "rules", value_name = "PATH", required = true)]
20    pub rules: Vec<PathBuf>,
21
22    /// Pretty-print the JSON output.
23    #[arg(long)]
24    pub pretty: bool,
25
26    /// Resolve absolute program paths against basename rules, gated by any
27    /// `host_executable()` definitions in the loaded policy files.
28    #[arg(long)]
29    pub resolve_host_executables: bool,
30
31    /// Command tokens to check against the policy.
32    #[arg(
33        value_name = "COMMAND",
34        required = true,
35        trailing_var_arg = true,
36        allow_hyphen_values = true
37    )]
38    pub command: Vec<String>,
39}
40
41impl ExecPolicyCheckCommand {
42    /// Load the policies for this command, evaluate the command, and render JSON output.
43    pub fn run(&self) -> Result<()> {
44        let policy = load_policies(&self.rules)?;
45        let matched_rules = policy.matches_for_command_with_options(
46            &self.command,
47            /*heuristics_fallback*/ None,
48            &MatchOptions {
49                resolve_host_executables: self.resolve_host_executables,
50            },
51        );
52
53        let json = format_matches_json(&matched_rules, self.pretty)?;
54        println!("{json}");
55
56        Ok(())
57    }
58}
59
60pub fn format_matches_json(matched_rules: &[RuleMatch], pretty: bool) -> Result<String> {
61    let output = ExecPolicyCheckOutput {
62        matched_rules,
63        decision: matched_rules.iter().map(RuleMatch::decision).max(),
64    };
65
66    if pretty {
67        serde_json::to_string_pretty(&output).map_err(Into::into)
68    } else {
69        serde_json::to_string(&output).map_err(Into::into)
70    }
71}
72
73pub fn load_policies(policy_paths: &[PathBuf]) -> Result<Policy> {
74    let mut parser = PolicyParser::new();
75
76    for policy_path in policy_paths {
77        let policy_file_contents = fs::read_to_string(policy_path)
78            .with_context(|| format!("failed to read policy at {}", policy_path.display()))?;
79        let policy_identifier = policy_path.to_string_lossy().to_string();
80        parser
81            .parse(&policy_identifier, &policy_file_contents)
82            .with_context(|| format!("failed to parse policy at {}", policy_path.display()))?;
83    }
84
85    Ok(parser.build())
86}
87
88#[derive(Serialize)]
89#[serde(rename_all = "camelCase")]
90struct ExecPolicyCheckOutput<'a> {
91    #[serde(rename = "matchedRules")]
92    matched_rules: &'a [RuleMatch],
93    #[serde(skip_serializing_if = "Option::is_none")]
94    decision: Option<Decision>,
95}