1use crate::analyzer::dependency_parser::Language;
2use crate::error::{AnalysisError, IaCGeneratorError, Result};
3use log::{info, warn, debug};
4use std::process::Command;
5use std::collections::HashMap;
6
7pub struct ToolInstaller {
9 installed_tools: HashMap<String, bool>,
10}
11
12impl ToolInstaller {
13 pub fn new() -> Self {
14 Self {
15 installed_tools: HashMap::new(),
16 }
17 }
18
19 pub fn ensure_tools_for_languages(&mut self, languages: &[Language]) -> Result<()> {
21 for language in languages {
22 match language {
23 Language::Rust => self.ensure_cargo_audit()?,
24 Language::JavaScript | Language::TypeScript => self.ensure_npm()?,
25 Language::Python => self.ensure_pip_audit()?,
26 Language::Go => self.ensure_govulncheck()?,
27 Language::Java | Language::Kotlin => self.ensure_grype()?,
28 _ => {} }
30 }
31 Ok(())
32 }
33
34 fn ensure_cargo_audit(&mut self) -> Result<()> {
36 if self.is_tool_installed("cargo-audit") {
37 return Ok(());
38 }
39
40 info!("š§ Installing cargo-audit for Rust vulnerability scanning...");
41
42 let output = Command::new("cargo")
43 .args(&["install", "cargo-audit"])
44 .output()
45 .map_err(|e| IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
46 file: "cargo-audit installation".to_string(),
47 reason: format!("Failed to install cargo-audit: {}", e),
48 }))?;
49
50 if output.status.success() {
51 info!("ā
cargo-audit installed successfully");
52 self.installed_tools.insert("cargo-audit".to_string(), true);
53 } else {
54 let stderr = String::from_utf8_lossy(&output.stderr);
55 warn!("ā Failed to install cargo-audit: {}", stderr);
56 return Err(IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
57 file: "cargo-audit installation".to_string(),
58 reason: format!("Installation failed: {}", stderr),
59 }));
60 }
61
62 Ok(())
63 }
64
65 fn ensure_npm(&mut self) -> Result<()> {
67 if self.is_tool_installed("npm") {
68 return Ok(());
69 }
70
71 warn!("š¦ npm not found. Please install Node.js from https://nodejs.org/");
72 warn!(" npm audit is required for JavaScript/TypeScript vulnerability scanning");
73
74 Ok(()) }
76
77 fn ensure_pip_audit(&mut self) -> Result<()> {
79 if self.is_tool_installed("pip-audit") {
80 return Ok(());
81 }
82
83 info!("š§ Installing pip-audit for Python vulnerability scanning...");
84
85 let install_commands = vec![
87 vec!["pipx", "install", "pip-audit"],
88 vec!["pip3", "install", "--user", "pip-audit"],
89 vec!["pip", "install", "--user", "pip-audit"],
90 ];
91
92 for cmd in install_commands {
93 debug!("Trying installation command: {:?}", cmd);
94
95 let output = Command::new(&cmd[0])
96 .args(&cmd[1..])
97 .output();
98
99 if let Ok(result) = output {
100 if result.status.success() {
101 info!("ā
pip-audit installed successfully using {}", cmd[0]);
102 self.installed_tools.insert("pip-audit".to_string(), true);
103 return Ok(());
104 }
105 }
106 }
107
108 warn!("š¦ Failed to auto-install pip-audit. Please install manually:");
109 warn!(" Option 1: pipx install pip-audit");
110 warn!(" Option 2: pip3 install --user pip-audit");
111
112 Ok(()) }
114
115 fn ensure_govulncheck(&mut self) -> Result<()> {
117 if self.is_tool_installed("govulncheck") {
118 return Ok(());
119 }
120
121 info!("š§ Installing govulncheck for Go vulnerability scanning...");
122
123 let output = Command::new("go")
124 .args(&["install", "golang.org/x/vuln/cmd/govulncheck@latest"])
125 .output()
126 .map_err(|e| IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
127 file: "govulncheck installation".to_string(),
128 reason: format!("Failed to install govulncheck (is Go installed?): {}", e),
129 }))?;
130
131 if output.status.success() {
132 info!("ā
govulncheck installed successfully");
133 self.installed_tools.insert("govulncheck".to_string(), true);
134
135 info!("š” Note: Make sure ~/go/bin is in your PATH to use govulncheck");
137 } else {
138 let stderr = String::from_utf8_lossy(&output.stderr);
139 warn!("ā Failed to install govulncheck: {}", stderr);
140 warn!("š¦ Please install Go from https://golang.org/ first");
141 }
142
143 Ok(())
144 }
145
146 fn ensure_grype(&mut self) -> Result<()> {
148 if self.is_tool_installed("grype") {
149 return Ok(());
150 }
151
152 info!("š§ Installing grype for vulnerability scanning...");
153
154 let os = std::env::consts::OS;
156 let arch = std::env::consts::ARCH;
157
158 match os {
160 "macos" => {
161 let output = Command::new("brew")
163 .args(&["install", "grype"])
164 .output();
165
166 match output {
167 Ok(result) if result.status.success() => {
168 info!("ā
grype installed successfully via Homebrew");
169 self.installed_tools.insert("grype".to_string(), true);
170 return Ok(());
171 }
172 _ => {
173 warn!("ā Failed to install via Homebrew. Trying manual installation...");
174 }
175 }
176 }
177 _ => {}
178 }
179
180 self.install_grype_manually(os, arch)
182 }
183
184 fn install_grype_manually(&mut self, os: &str, arch: &str) -> Result<()> {
186 use std::fs;
187 use std::path::PathBuf;
188
189 info!("š„ Downloading grype from GitHub releases...");
190
191 let version = "v0.92.2"; let home_dir = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
193 let bin_dir = PathBuf::from(&home_dir).join(".local").join("bin");
194
195 fs::create_dir_all(&bin_dir).map_err(|e| {
197 IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
198 file: "grype installation".to_string(),
199 reason: format!("Failed to create directory: {}", e),
200 })
201 })?;
202
203 let (os_name, arch_name) = match (os, arch) {
205 ("macos", "x86_64") => ("darwin", "amd64"),
206 ("macos", "aarch64") => ("darwin", "arm64"),
207 ("linux", "x86_64") => ("linux", "amd64"),
208 ("linux", "aarch64") => ("linux", "arm64"),
209 _ => {
210 warn!("ā Unsupported platform: {} {}", os, arch);
211 return Ok(());
212 }
213 };
214
215 let archive_name = format!("grype_{}_{}.tar.gz", os_name, arch_name);
216 let download_url = format!(
217 "https://github.com/anchore/grype/releases/download/{}/grype_{}_{}_{}.tar.gz",
218 version, version.trim_start_matches('v'), os_name, arch_name
219 );
220
221 let archive_path = bin_dir.join(&archive_name);
222
223 info!("š¦ Downloading from: {}", download_url);
224 let output = Command::new("curl")
225 .args(&["-L", "-o", archive_path.to_str().unwrap(), &download_url])
226 .output();
227
228 match output {
229 Ok(result) if result.status.success() => {
230 info!("ā
Download complete. Extracting...");
231
232 let extract_output = Command::new("tar")
234 .args(&["-xzf", archive_path.to_str().unwrap(), "-C", bin_dir.to_str().unwrap()])
235 .output();
236
237 if extract_output.map(|o| o.status.success()).unwrap_or(false) {
238 let grype_path = bin_dir.join("grype");
240 Command::new("chmod")
241 .args(&["+x", grype_path.to_str().unwrap()])
242 .output()
243 .ok();
244
245 info!("ā
grype installed successfully to {}", bin_dir.display());
246 info!("š” Make sure ~/.local/bin is in your PATH");
247 self.installed_tools.insert("grype".to_string(), true);
248
249 fs::remove_file(&archive_path).ok();
251
252 return Ok(());
253 }
254 }
255 _ => {}
256 }
257
258 warn!("ā Automatic installation failed. Please install manually:");
259 warn!(" ⢠macOS: brew install grype");
260 warn!(" ⢠Download: https://github.com/anchore/grype/releases");
261
262 Ok(())
263 }
264
265 fn ensure_dependency_check(&mut self) -> Result<()> {
267 if self.is_tool_installed("dependency-check") {
268 return Ok(());
269 }
270
271 info!("š§ Installing dependency-check for Java/Kotlin vulnerability scanning...");
272
273 let os = std::env::consts::OS;
275
276 match os {
277 "macos" => {
278 let output = Command::new("brew")
280 .args(&["install", "dependency-check"])
281 .output();
282
283 match output {
284 Ok(result) if result.status.success() => {
285 info!("ā
dependency-check installed successfully via Homebrew");
286 self.installed_tools.insert("dependency-check".to_string(), true);
287 return Ok(());
288 }
289 _ => {
290 warn!("ā Failed to install via Homebrew. Trying manual installation...");
291 }
292 }
293 }
294 "linux" => {
295 let output = Command::new("snap")
297 .args(&["install", "dependency-check"])
298 .output();
299
300 if output.map(|o| o.status.success()).unwrap_or(false) {
301 info!("ā
dependency-check installed successfully via snap");
302 self.installed_tools.insert("dependency-check".to_string(), true);
303 return Ok(());
304 }
305 }
306 _ => {}
307 }
308
309 self.install_dependency_check_manually()
311 }
312
313 fn install_dependency_check_manually(&mut self) -> Result<()> {
315 use std::fs;
316 use std::path::PathBuf;
317
318 info!("š„ Downloading dependency-check from GitHub releases...");
319
320 let version = "10.0.4"; let home_dir = std::env::var("HOME").unwrap_or_else(|_| ".".to_string());
322 let install_dir = PathBuf::from(&home_dir).join(".local").join("dependency-check");
323
324 fs::create_dir_all(&install_dir).map_err(|e| {
326 IaCGeneratorError::Analysis(AnalysisError::DependencyParsing {
327 file: "dependency-check installation".to_string(),
328 reason: format!("Failed to create directory: {}", e),
329 })
330 })?;
331
332 let archive_name = format!("dependency-check-{}-release.zip", version);
333 let download_url = format!(
334 "https://github.com/jeremylong/DependencyCheck/releases/download/v{}/{}",
335 version, archive_name
336 );
337
338 let archive_path = install_dir.join(&archive_name);
340
341 info!("š¦ Downloading from: {}", download_url);
342 let output = Command::new("curl")
343 .args(&["-L", "-o", archive_path.to_str().unwrap(), &download_url])
344 .output();
345
346 match output {
347 Ok(result) if result.status.success() => {
348 info!("ā
Download complete. Extracting...");
349
350 let extract_output = Command::new("unzip")
352 .args(&["-o", archive_path.to_str().unwrap(), "-d", install_dir.to_str().unwrap()])
353 .output();
354
355 if extract_output.map(|o| o.status.success()).unwrap_or(false) {
356 let bin_dir = PathBuf::from(&home_dir).join(".local").join("bin");
358 fs::create_dir_all(&bin_dir).ok();
359
360 let dc_script = install_dir.join("dependency-check").join("bin").join("dependency-check.sh");
361 let symlink = bin_dir.join("dependency-check");
362
363 fs::remove_file(&symlink).ok();
365
366 if std::os::unix::fs::symlink(&dc_script, &symlink).is_ok() {
368 info!("ā
dependency-check installed successfully to {}", install_dir.display());
369 info!("š” Added to ~/.local/bin/dependency-check");
370 info!("š” Make sure ~/.local/bin is in your PATH");
371 self.installed_tools.insert("dependency-check".to_string(), true);
372 return Ok(());
373 }
374 }
375 }
376 _ => {}
377 }
378
379 warn!("ā Automatic installation failed. Please install manually:");
380 warn!(" ⢠macOS: brew install dependency-check");
381 warn!(" ⢠Download: https://github.com/jeremylong/DependencyCheck/releases");
382 warn!(" ⢠Documentation: https://owasp.org/www-project-dependency-check/");
383
384 Ok(())
385 }
386
387 fn is_tool_installed(&mut self, tool: &str) -> bool {
389 if let Some(&cached) = self.installed_tools.get(tool) {
391 return cached;
392 }
393
394 let available = self.test_tool_availability(tool);
396 self.installed_tools.insert(tool.to_string(), available);
397 available
398 }
399
400 pub fn test_tool_availability(&self, tool: &str) -> bool {
402 let test_commands = match tool {
403 "cargo-audit" => vec!["cargo", "audit", "--version"],
404 "npm" => vec!["npm", "--version"],
405 "pip-audit" => vec!["pip-audit", "--version"],
406 "govulncheck" => vec!["govulncheck", "-version"],
407 "dependency-check" => vec!["dependency-check", "--version"],
408 "grype" => vec!["grype", "version"],
409 _ => return false,
410 };
411
412 let result = Command::new(&test_commands[0])
413 .args(&test_commands[1..])
414 .output();
415
416 match result {
417 Ok(output) => output.status.success(),
418 Err(_) => {
419 if tool == "govulncheck" {
421 let go_bin_path = std::env::var("HOME")
422 .map(|home| format!("{}/go/bin/govulncheck", home))
423 .unwrap_or_else(|_| "govulncheck".to_string());
424
425 return Command::new(&go_bin_path)
426 .arg("-version")
427 .output()
428 .map(|out| out.status.success())
429 .unwrap_or(false);
430 }
431
432 if tool == "dependency-check" {
434 let dc_path = std::env::var("HOME")
435 .map(|home| format!("{}/.local/bin/dependency-check", home))
436 .unwrap_or_else(|_| "dependency-check".to_string());
437
438 return Command::new(&dc_path)
439 .arg("--version")
440 .output()
441 .map(|out| out.status.success())
442 .unwrap_or(false);
443 }
444
445 if tool == "grype" {
447 let grype_path = std::env::var("HOME")
448 .map(|home| format!("{}/.local/bin/grype", home))
449 .unwrap_or_else(|_| "grype".to_string());
450
451 return Command::new(&grype_path)
452 .arg("version")
453 .output()
454 .map(|out| out.status.success())
455 .unwrap_or(false);
456 }
457
458 false
459 }
460 }
461 }
462
463 pub fn get_tool_status(&self) -> HashMap<String, bool> {
465 self.installed_tools.clone()
466 }
467
468 pub fn print_tool_status(&self, languages: &[Language]) {
470 println!("\nš§ Vulnerability Scanning Tools Status:");
471 println!("{}", "=".repeat(50));
472
473 for language in languages {
474 let (tool, status) = match language {
475 Language::Rust => ("cargo-audit", self.installed_tools.get("cargo-audit").unwrap_or(&false)),
476 Language::JavaScript | Language::TypeScript => ("npm", self.installed_tools.get("npm").unwrap_or(&false)),
477 Language::Python => ("pip-audit", self.installed_tools.get("pip-audit").unwrap_or(&false)),
478 Language::Go => ("govulncheck", self.installed_tools.get("govulncheck").unwrap_or(&false)),
479 Language::Java | Language::Kotlin => ("grype", self.installed_tools.get("grype").unwrap_or(&false)),
480 _ => continue,
481 };
482
483 let status_icon = if *status { "ā
" } else { "ā" };
484 println!(" {} {:?}: {} {}", status_icon, language, tool, if *status { "installed" } else { "missing" });
485 }
486 println!();
487 }
488}