Skip to main content

sbox/
audit.rs

1use std::process::{Command, ExitCode, Stdio};
2
3use crate::cli::{AuditCommand, Cli};
4use crate::config::{LoadOptions, load_config};
5use crate::error::SboxError;
6use crate::exec::status_to_exit_code;
7
8/// `sbox audit` — scan the project's lockfile for known-malicious or vulnerable packages.
9///
10/// Delegates to the ecosystem's native audit tool and runs on the HOST (not in a sandbox)
11/// so it can reach advisory databases. This is intentional — audit only reads the lockfile
12/// and queries read-only advisory APIs; it does not execute package code.
13pub fn execute(cli: &Cli, command: &AuditCommand) -> Result<ExitCode, SboxError> {
14    let loaded = load_config(&LoadOptions {
15        workspace: cli.workspace.clone(),
16        config: cli.config.clone(),
17    })?;
18
19    let pm_name = loaded
20        .config
21        .package_manager
22        .as_ref()
23        .map(|pm| pm.name.as_str())
24        .unwrap_or_else(|| detect_pm_from_workspace(&loaded.workspace_root));
25
26    let (program, base_args, install_hint) = audit_command_for(pm_name);
27
28    let mut child = Command::new(program);
29    child.args(base_args);
30    child.args(&command.extra_args);
31    child.current_dir(&loaded.workspace_root);
32    child.stdin(Stdio::inherit());
33    child.stdout(Stdio::inherit());
34    child.stderr(Stdio::inherit());
35
36    match child.status() {
37        Ok(status) => Ok(status_to_exit_code(status)),
38        Err(e) if e.kind() == std::io::ErrorKind::NotFound => {
39            eprintln!("sbox audit: `{program}` not found.");
40            eprintln!("{install_hint}");
41            Ok(ExitCode::from(127))
42        }
43        Err(source) => Err(SboxError::CommandSpawn {
44            program: program.to_string(),
45            source,
46        }),
47    }
48}
49
50/// Returns `(program, base_args, install_hint)` for the given package manager.
51fn audit_command_for(pm_name: &str) -> (&'static str, &'static [&'static str], &'static str) {
52    match pm_name {
53        "npm" => (
54            "npm",
55            &["audit"] as &[&str],
56            "npm is required. Install Node.js from https://nodejs.org",
57        ),
58        "yarn" => (
59            "yarn",
60            &["npm", "audit"],
61            "yarn is required. Install from https://yarnpkg.com",
62        ),
63        "pnpm" => ("pnpm", &["audit"], "pnpm is required: npm install -g pnpm"),
64        "bun" => (
65            // bun does not have a native audit command; delegate to npm audit which can read
66            // package-lock.json or bun.lock
67            "npm",
68            &["audit"],
69            "npm is required for bun audit. Install Node.js from https://nodejs.org",
70        ),
71        "uv" | "pip" | "poetry" => (
72            "pip-audit",
73            &[] as &[&str],
74            "pip-audit is required: pip install pip-audit  or  uv tool install pip-audit",
75        ),
76        "cargo" => (
77            "cargo",
78            &["audit"],
79            "cargo-audit is required: cargo install cargo-audit",
80        ),
81        "go" => (
82            "govulncheck",
83            &["./..."],
84            "govulncheck is required: go install golang.org/x/vuln/cmd/govulncheck@latest",
85        ),
86        _ => (
87            "npm",
88            &["audit"],
89            "unknown package manager; defaulting to npm audit",
90        ),
91    }
92}
93
94/// Detect the package manager from lockfiles present in the workspace root.
95/// Fallback when no `package_manager:` section is configured.
96fn detect_pm_from_workspace(root: &std::path::Path) -> &'static str {
97    if root.join("package-lock.json").exists() || root.join("npm-shrinkwrap.json").exists() {
98        return "npm";
99    }
100    if root.join("yarn.lock").exists() {
101        return "yarn";
102    }
103    if root.join("pnpm-lock.yaml").exists() {
104        return "pnpm";
105    }
106    if root.join("bun.lockb").exists() || root.join("bun.lock").exists() {
107        return "bun";
108    }
109    if root.join("uv.lock").exists() {
110        return "uv";
111    }
112    if root.join("poetry.lock").exists() {
113        return "poetry";
114    }
115    if root.join("requirements.txt").exists() {
116        return "pip";
117    }
118    if root.join("Cargo.lock").exists() {
119        return "cargo";
120    }
121    if root.join("go.sum").exists() {
122        return "go";
123    }
124    "npm"
125}