anodizer_core/user_command.rs
1//! Spawn a user-supplied command (e.g. `publisher.cmd`) with a clean,
2//! whitelisted environment.
3//!
4//! Centralised here so the `Command::new(<arbitrary>)` shell-out lives
5//! inside the module-boundaries allow-list
6//! (`.claude/rules/module-boundaries.md`). Inlining this in the CLI
7//! crate would put `Command::new` outside the allow-list and counts
8//! as a boundary violation.
9
10use std::ffi::OsStr;
11use std::process::Command;
12
13/// Environment variables that are inherited from the parent process
14/// when constructing a sandboxed `Command`. Anything else must be
15/// explicitly added via `Command::env`.
16///
17/// This whitelist exists to prevent accidental leakage of release
18/// credentials (`GITHUB_TOKEN`, `COSIGN_*`, signing keys, etc.) into
19/// arbitrary user-supplied commands.
20pub const ENV_WHITELIST: &[&str] = &[
21 "HOME",
22 "USER",
23 "USERPROFILE",
24 "TMPDIR",
25 "TMP",
26 "TEMP",
27 "PATH",
28 "SYSTEMROOT",
29];
30
31/// Construct a `Command` whose argv is `argv` and whose environment is
32/// reset to the [`ENV_WHITELIST`] subset of the parent's env. The first
33/// element of `argv` is the program; the rest are arguments. The caller
34/// is responsible for adding any further env vars / cwd / I/O config
35/// before invoking `output()`.
36///
37/// Panics: returns an empty `Command` (program = empty string) when
38/// `argv` is empty; callers should reject that case before reaching
39/// this helper. The CLI's publisher command does so explicitly.
40pub fn whitelisted<S: AsRef<OsStr>>(argv: &[S]) -> Command {
41 let program = argv.first().map(AsRef::as_ref).unwrap_or(OsStr::new(""));
42 let mut cmd = Command::new(program);
43 if argv.len() > 1 {
44 cmd.args(&argv[1..]);
45 }
46 cmd.env_clear();
47 for key in ENV_WHITELIST {
48 if let Ok(val) = std::env::var(key) {
49 cmd.env(key, val);
50 }
51 }
52 cmd
53}