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
8pub 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
50fn 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 "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
94fn 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}