1use std::path::Path;
4
5use crate::lockfile;
6use crate::osv;
7use crate::utils;
8
9pub fn native_audit() -> Result<Vec<osv::VulnRecord>, String> {
11 let resolved = lockfile::read_resolved_from_dir(Path::new("."))
12 .ok_or("No package-lock.json or bun.lock found. Run install first.")?;
13 let mut all = Vec::new();
14 for (name, version) in &resolved {
15 match osv::query_vulnerabilities(name, version) {
16 Ok(vulns) => all.extend(vulns),
17 Err(_) => continue,
18 }
19 }
20 Ok(all)
21}
22
23pub fn run_audit_raw(_backend: crate::backend::Backend) -> Result<Vec<u8>, String> {
25 let vulns = native_audit()?;
26 let arr: Vec<serde_json::Value> = vulns
27 .into_iter()
28 .map(|v| {
29 serde_json::json!({
30 "id": v.id,
31 "summary": v.summary,
32 "severity": v.severity,
33 "package": v.package_name,
34 "version": v.package_version,
35 })
36 })
37 .collect();
38 let out = serde_json::json!({ "vulnerabilities": arr });
39 serde_json::to_vec(&out).map_err(|e| e.to_string())
40}
41
42pub fn run_audit(_backend: crate::backend::Backend, quiet: bool) -> Result<(), String> {
44 if !Path::new("package.json").exists() {
45 return Err("No package.json found in current directory.".to_string());
46 }
47 utils::log("Running audit...");
48 let vulns = native_audit()?;
49 if quiet {
50 if !vulns.is_empty() {
51 return Err(format!("{} vulnerability(ies) found.", vulns.len()));
52 }
53 return Ok(());
54 }
55 if vulns.is_empty() {
56 println!("No vulnerabilities found.");
57 return Ok(());
58 }
59 println!("Found {} vulnerability(ies):", vulns.len());
60 for v in &vulns {
61 let sev = v.severity.as_deref().unwrap_or("unknown");
62 println!(" {}@{} ({}): {} - {}", v.package_name, v.package_version, sev, v.id, v.summary);
63 }
64 Ok(())
65}
66
67pub fn run_audit_fix(_backend: crate::backend::Backend, quiet: bool) -> Result<(), String> {
69 if !Path::new("package.json").exists() {
70 return Err("No package.json found in current directory.".to_string());
71 }
72 utils::log("Running audit fix...");
73 let vulns = native_audit()?;
74 if vulns.is_empty() {
75 if !quiet {
76 println!("No vulnerabilities to fix.");
77 }
78 return Ok(());
79 }
80 if !quiet {
81 println!("Vulnerable packages (update manually or run jhol install <pkg>@latest):");
82 let mut seen = std::collections::HashSet::new();
83 for v in &vulns {
84 let key = (v.package_name.as_str(), v.package_version.as_str());
85 if seen.insert(key) {
86 println!(" {}@{} - {}", v.package_name, v.package_version, v.id);
87 }
88 }
89 }
90 Ok(())
91}
92
93pub fn run_audit_gate(_backend: crate::backend::Backend) -> Result<(), String> {
95 let vulns = native_audit()?;
96 if vulns.is_empty() {
97 return Ok(());
98 }
99 Err(format!("audit gate failed: {} vulnerability(ies) found", vulns.len()))
100}
101
102#[derive(Clone, Copy, Debug, Eq, PartialEq)]
104pub enum SbomFormat {
105 CycloneDx,
106 Simple,
107}
108
109pub fn generate_sbom(format: SbomFormat) -> Result<String, String> {
111 let pj = Path::new("package.json");
112 if !pj.exists() {
113 return Err("No package.json in current directory.".to_string());
114 }
115 let deps = lockfile::read_package_json_deps(pj).ok_or("Could not read package.json.")?;
116 let resolved = lockfile::read_resolved_from_dir(Path::new("."));
117 let specs = lockfile::resolve_deps_for_install(&deps, resolved.as_ref());
118 let components: Vec<serde_json::Value> = specs
119 .iter()
120 .map(|s| {
121 let (name, version) = if let Some(i) = s.rfind('@') {
122 (s[..i].to_string(), s[i + 1..].to_string())
123 } else {
124 (s.clone(), "?".to_string())
125 };
126 (name, version)
127 })
128 .map(|(name, version)| {
129 serde_json::json!({
130 "name": name,
131 "version": version,
132 })
133 })
134 .collect();
135 let out = match format {
136 SbomFormat::CycloneDx => {
137 serde_json::json!({
138 "bomFormat": "CycloneDX",
139 "specVersion": "1.4",
140 "version": 1,
141 "components": components,
142 })
143 }
144 SbomFormat::Simple => serde_json::json!(components),
145 };
146 serde_json::to_string_pretty(&out).map_err(|e| e.to_string())
147}