#![deny(clippy::all)]
#![deny(clippy::pedantic)]
#![deny(clippy::unwrap_used)]
#![deny(clippy::expect_used)]
#![deny(clippy::panic)]
mod cli;
mod config;
mod engine;
mod outcome;
mod platform;
use crate::cli::{
Command, Invocation, ParseOutcome, PolicyCommand, PolicyRequest, RunCommand, parse_cli,
};
use crate::config::load_settings;
use crate::engine::error::Error;
use crate::engine::policy::{AccessPolicy, resolve_policy};
use crate::engine::trap::Trap;
#[cfg(unix)]
use crate::engine::trap_fd::TrapFd;
use crate::outcome::{CommandOutcome, PolicyValidationError, PolicyValidationReport};
use anyhow::Result;
use serde::Serialize;
use std::error::Error as StdError;
use std::io::{self, Write};
use std::process;
fn main() {
let invocation = match parse_cli() {
Ok(ParseOutcome::Invocation(invocation)) => invocation,
Ok(ParseOutcome::Display(text)) => {
if let Err(error) = render_display(&text) {
exit_with_error(&error);
}
return;
}
Err(error) => exit_with_error(&error.into()),
};
init_logging(invocation.debug);
#[cfg(unix)]
let trap_fd = match &invocation.command {
Command::Run(command) => TrapFd::from_fd(command.trap_fd),
_ => TrapFd::from_fd(None),
};
let outcome = match dispatch(&invocation) {
Ok(outcome) => outcome,
Err(error) => {
#[cfg(unix)]
exit_with_trap_fd(&error, &trap_fd);
#[cfg(not(unix))]
exit_with_error(&error);
}
};
if let Err(error) = render_outcome(&outcome) {
#[cfg(unix)]
exit_with_trap_fd(&error, &trap_fd);
#[cfg(not(unix))]
exit_with_error(&error);
}
let exit_code = outcome.exit_code();
if exit_code != 0 {
process::exit(exit_code);
}
}
fn init_logging(debug: bool) {
let default_filter = if debug { "debug" } else { "warn" };
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or(default_filter))
.format_timestamp(None)
.init();
}
fn dispatch(invocation: &Invocation) -> Result<CommandOutcome> {
match &invocation.command {
Command::Run(command) => run(command),
Command::Policy(command) => inspect_policy(command),
Command::Doctor => platform::doctor().map(CommandOutcome::Doctor),
#[cfg(target_os = "windows")]
Command::Windows(command) => platform::manage_windows(command).map(CommandOutcome::Windows),
#[cfg(target_os = "windows")]
Command::Worker { request } => platform::run_worker(request).map(CommandOutcome::Exit),
}
}
fn run(command: &RunCommand) -> Result<CommandOutcome> {
let policy = load_policy(&command.policy, Some(command.tool.as_os_str()))?;
platform::validate(&policy)?;
execute_run(&policy, command).map(CommandOutcome::Exit)
}
#[cfg(unix)]
fn execute_run(policy: &AccessPolicy, command: &RunCommand) -> Result<i32> {
let trap_fd = TrapFd::from_fd(command.trap_fd);
platform::execute(policy, &command.tool, &command.tool_args, &trap_fd)
}
#[cfg(not(unix))]
fn execute_run(policy: &AccessPolicy, command: &RunCommand) -> Result<i32> {
platform::execute(policy, &command.tool, &command.tool_args)
}
fn inspect_policy(command: &PolicyCommand) -> Result<CommandOutcome> {
match command {
PolicyCommand::Validate(request) => {
validate_requested_policy(request).map(CommandOutcome::PolicyValidated)
}
PolicyCommand::Resolve(request) => {
let policy = load_requested_policy(request)?;
Ok(CommandOutcome::PolicyResolved(policy))
}
}
}
fn load_requested_policy(request: &PolicyRequest) -> Result<AccessPolicy> {
load_policy(&request.policy, request.tool.as_deref())
}
fn validate_requested_policy(request: &PolicyRequest) -> Result<PolicyValidationReport> {
let result = load_requested_policy(request).and_then(|policy| platform::validate(&policy));
policy_validation_report(result)
}
fn policy_validation_report(result: Result<()>) -> Result<PolicyValidationReport> {
match result {
Ok(()) => Ok(PolicyValidationReport {
valid: true,
error: None,
}),
Err(error) => {
let Some(engine_error) = find_engine_error(&error) else {
return Err(error);
};
let Some(fallback_message) = engine_error.policy_validation_message() else {
return Err(error);
};
let code = engine_error.code();
let rendered_message = format!("{error:#}");
let message = if rendered_message == code {
fallback_message.to_owned()
} else {
rendered_message
};
Ok(PolicyValidationReport {
valid: false,
error: Some(PolicyValidationError { code, message }),
})
}
}
}
fn load_policy(
input: &crate::cli::PolicyInput,
tool: Option<&std::ffi::OsStr>,
) -> Result<AccessPolicy> {
let format = crate::cli::policy_format(input)?;
let cwd = std::env::current_dir().map_err(|source| Error::PolicyIoFailed { source })?;
log::debug!("cli: cwd: {}", cwd.display());
let settings = load_settings(&input.paths, format, tool)?;
resolve_policy(
&settings.filesystem,
&settings.network,
&settings.windows,
&cwd,
)
}
fn render_display(text: &str) -> Result<()> {
let stdout = io::stdout();
stdout.lock().write_all(text.as_bytes())?;
Ok(())
}
fn render_outcome(outcome: &CommandOutcome) -> Result<()> {
let stdout = io::stdout();
render_outcome_to(&mut stdout.lock(), outcome)
}
fn render_outcome_to(output: &mut impl Write, outcome: &CommandOutcome) -> Result<()> {
match outcome {
CommandOutcome::Exit(_) => Ok(()),
CommandOutcome::PolicyValidated(report) => write_json(output, report),
CommandOutcome::PolicyResolved(policy) => write_json(output, policy),
CommandOutcome::Doctor(report) => write_json(output, report),
#[cfg(target_os = "windows")]
CommandOutcome::Windows(report) => write_json(output, report),
}
}
fn write_json(output: &mut impl Write, value: &impl Serialize) -> Result<()> {
serde_json::to_writer(&mut *output, value)?;
writeln!(output)?;
Ok(())
}
fn exit_with_error(error: &anyhow::Error) -> ! {
let (trap, exit_code) = error_trap(error);
trap.emit();
process::exit(exit_code)
}
#[cfg(unix)]
fn exit_with_trap_fd(error: &anyhow::Error, trap_fd: &TrapFd) -> ! {
let (trap, exit_code) = error_trap(error);
trap_fd.write(&trap);
trap.emit();
process::exit(exit_code)
}
fn error_trap(error: &anyhow::Error) -> (Trap, i32) {
let engine_error = find_engine_error(error);
let trap = engine_error.map_or_else(|| Trap::internal(format!("{error:#}")), Trap::from_error);
let exit_code = if matches!(engine_error, Some(Error::Usage { .. })) {
2
} else {
1
};
(trap, exit_code)
}
fn find_engine_error(error: &anyhow::Error) -> Option<&Error> {
error
.chain()
.find_map(<dyn StdError + 'static>::downcast_ref::<Error>)
}