syncable_cli/handlers/
tools.rs

1use crate::{
2    analyzer::{dependency_parser::Language, tool_management::ToolInstaller},
3    cli::{OutputFormat, ToolsCommand},
4};
5use std::collections::HashMap;
6use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
7
8pub async fn handle_tools(command: ToolsCommand) -> crate::Result<()> {
9    match command {
10        ToolsCommand::Status { format, languages } => handle_tools_status(format, languages),
11        ToolsCommand::Install {
12            languages,
13            include_owasp,
14            dry_run,
15            yes,
16        } => handle_tools_install(languages, include_owasp, dry_run, yes),
17        ToolsCommand::Verify {
18            languages,
19            detailed,
20        } => handle_tools_verify(languages, detailed),
21        ToolsCommand::Guide {
22            languages,
23            platform,
24        } => handle_tools_guide(languages, platform),
25    }
26}
27
28fn handle_tools_status(format: OutputFormat, languages: Option<Vec<String>>) -> crate::Result<()> {
29    let mut installer = ToolInstaller::new();
30
31    // Determine which languages to check
32    let langs_to_check = get_languages_to_check(languages);
33
34    println!("šŸ”§ Checking vulnerability scanning tools status...\n");
35
36    match format {
37        OutputFormat::Table => display_status_table(&mut installer, &langs_to_check)?,
38        OutputFormat::Json => display_status_json(&mut installer, &langs_to_check),
39    }
40
41    Ok(())
42}
43
44fn handle_tools_install(
45    languages: Option<Vec<String>>,
46    include_owasp: bool,
47    dry_run: bool,
48    yes: bool,
49) -> crate::Result<()> {
50    let mut installer = ToolInstaller::new();
51
52    // Determine which languages to install tools for
53    let langs_to_install = get_languages_to_install(languages);
54
55    if dry_run {
56        return handle_dry_run(&mut installer, &langs_to_install, include_owasp);
57    }
58
59    if !yes && !confirm_installation()? {
60        println!("Installation cancelled.");
61        return Ok(());
62    }
63
64    println!("šŸ› ļø  Installing vulnerability scanning tools...");
65
66    match installer.ensure_tools_for_languages(&langs_to_install) {
67        Ok(()) => {
68            println!("āœ… Tool installation completed!");
69            installer.print_tool_status(&langs_to_install);
70            print_setup_instructions();
71        }
72        Err(e) => {
73            eprintln!("āŒ Tool installation failed: {}", e);
74            eprintln!("\nšŸ”§ Manual installation may be required for some tools.");
75            eprintln!("   Run 'sync-ctl tools guide' for manual installation instructions.");
76            return Err(e);
77        }
78    }
79
80    Ok(())
81}
82
83fn handle_tools_verify(languages: Option<Vec<String>>, detailed: bool) -> crate::Result<()> {
84    let mut installer = ToolInstaller::new();
85
86    // Determine which languages to verify
87    let langs_to_verify = get_languages_to_verify(languages);
88
89    println!("šŸ” Verifying vulnerability scanning tools...\n");
90
91    let mut all_working = true;
92
93    for language in &langs_to_verify {
94        let (tool_name, is_working) = get_tool_for_language(&mut installer, language);
95
96        print!(
97            "  {} {:?}: {}",
98            if is_working { "āœ…" } else { "āŒ" },
99            language,
100            tool_name
101        );
102
103        if is_working {
104            println!(" - working correctly");
105
106            if detailed {
107                print_version_info(tool_name);
108            }
109        } else {
110            println!(" - not working or missing");
111            all_working = false;
112        }
113    }
114
115    if all_working {
116        println!("\nāœ… All tools are working correctly!");
117    } else {
118        println!("\nāŒ Some tools are missing or not working.");
119        println!("   Run 'sync-ctl tools install' to install missing tools.");
120    }
121
122    Ok(())
123}
124
125fn handle_tools_guide(
126    languages: Option<Vec<String>>,
127    platform: Option<String>,
128) -> crate::Result<()> {
129    let target_platform = platform.unwrap_or_else(|| match std::env::consts::OS {
130        "macos" => "macOS".to_string(),
131        "linux" => "Linux".to_string(),
132        "windows" => "Windows".to_string(),
133        other => other.to_string(),
134    });
135
136    println!("šŸ“š Vulnerability Scanning Tools Installation Guide");
137    println!("Platform: {}", target_platform);
138    println!("{}", "=".repeat(60));
139
140    let langs_to_show = get_languages_to_show(languages);
141
142    for language in &langs_to_show {
143        print_language_guide(language, &target_platform);
144    }
145
146    print_universal_scanners_info();
147    print_general_tips();
148
149    Ok(())
150}
151
152// Helper functions
153
154fn get_languages_to_check(languages: Option<Vec<String>>) -> Vec<Language> {
155    if let Some(lang_names) = languages {
156        lang_names
157            .iter()
158            .filter_map(|name| Language::from_string(name))
159            .collect()
160    } else {
161        vec![
162            Language::Rust,
163            Language::JavaScript,
164            Language::TypeScript,
165            Language::Python,
166            Language::Go,
167            Language::Java,
168            Language::Kotlin,
169        ]
170    }
171}
172
173fn get_languages_to_install(languages: Option<Vec<String>>) -> Vec<Language> {
174    if let Some(lang_names) = languages {
175        lang_names
176            .iter()
177            .filter_map(|name| Language::from_string(name))
178            .collect()
179    } else {
180        vec![
181            Language::Rust,
182            Language::JavaScript,
183            Language::TypeScript,
184            Language::Python,
185            Language::Go,
186            Language::Java,
187        ]
188    }
189}
190
191fn get_languages_to_verify(languages: Option<Vec<String>>) -> Vec<Language> {
192    if let Some(lang_names) = languages {
193        lang_names
194            .iter()
195            .filter_map(|name| Language::from_string(name))
196            .collect()
197    } else {
198        vec![
199            Language::Rust,
200            Language::JavaScript,
201            Language::TypeScript,
202            Language::Python,
203            Language::Go,
204            Language::Java,
205        ]
206    }
207}
208
209fn get_languages_to_show(languages: Option<Vec<String>>) -> Vec<Language> {
210    if let Some(lang_names) = languages {
211        lang_names
212            .iter()
213            .filter_map(|name| Language::from_string(name))
214            .collect()
215    } else {
216        vec![
217            Language::Rust,
218            Language::JavaScript,
219            Language::TypeScript,
220            Language::Python,
221            Language::Go,
222            Language::Java,
223        ]
224    }
225}
226
227fn get_tool_for_language<'a>(
228    installer: &mut ToolInstaller,
229    language: &Language,
230) -> (&'a str, bool) {
231    match language {
232        Language::Rust => (
233            "cargo-audit",
234            installer.test_tool_availability("cargo-audit"),
235        ),
236        Language::JavaScript | Language::TypeScript => {
237            // Check all JavaScript package managers, prioritize bun
238            if installer.test_tool_availability("bun") {
239                ("bun", true)
240            } else if installer.test_tool_availability("npm") {
241                ("npm", true)
242            } else if installer.test_tool_availability("yarn") {
243                ("yarn", true)
244            } else if installer.test_tool_availability("pnpm") {
245                ("pnpm", true)
246            } else {
247                ("npm", false)
248            }
249        }
250        Language::Python => ("pip-audit", installer.test_tool_availability("pip-audit")),
251        Language::Go => (
252            "govulncheck",
253            installer.test_tool_availability("govulncheck"),
254        ),
255        Language::Java | Language::Kotlin => ("grype", installer.test_tool_availability("grype")),
256        _ => ("unknown", false),
257    }
258}
259
260fn display_status_table(
261    installer: &mut ToolInstaller,
262    langs_to_check: &[Language],
263) -> crate::Result<()> {
264    let mut stdout = StandardStream::stdout(ColorChoice::Always);
265
266    println!("šŸ“‹ Vulnerability Scanning Tools Status");
267    println!("{}", "=".repeat(50));
268
269    // Use the enhanced tool status display from ToolInstaller
270    installer.print_tool_status(langs_to_check);
271
272    // Also check universal tools
273    println!("šŸ” Universal Scanners:");
274    let grype_available = installer.test_tool_availability("grype");
275    print!("  {} Grype: ", if grype_available { "āœ…" } else { "āŒ" });
276    if grype_available {
277        stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
278        println!("installed");
279    } else {
280        stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
281        println!("missing - Install with: brew install grype or download from GitHub");
282    }
283    stdout.reset()?;
284
285    Ok(())
286}
287
288fn display_status_json(installer: &mut ToolInstaller, langs_to_check: &[Language]) {
289    let mut status = HashMap::new();
290
291    for language in langs_to_check {
292        let (tool_name, is_available) = get_tool_for_language(installer, language);
293
294        if tool_name == "unknown" {
295            continue;
296        }
297
298        status.insert(
299            format!("{:?}", language),
300            serde_json::json!({
301                "tool": tool_name,
302                "available": is_available
303            }),
304        );
305    }
306
307    println!("{}", serde_json::to_string_pretty(&status).unwrap());
308}
309
310fn handle_dry_run(
311    installer: &mut ToolInstaller,
312    langs_to_install: &[Language],
313    include_owasp: bool,
314) -> crate::Result<()> {
315    println!("šŸ” Dry run: Tools that would be installed:");
316    println!("{}", "=".repeat(50));
317
318    for language in langs_to_install {
319        let (tool_name, is_available) = get_tool_for_language(installer, language);
320
321        if tool_name == "unknown" {
322            continue;
323        }
324
325        if !is_available {
326            println!("  šŸ“¦ Would install {} for {:?}", tool_name, language);
327        } else {
328            println!("  āœ… {} already installed for {:?}", tool_name, language);
329        }
330    }
331
332    if include_owasp && !installer.test_tool_availability("dependency-check") {
333        println!("  šŸ“¦ Would install OWASP Dependency Check (large download)");
334    }
335
336    Ok(())
337}
338
339fn confirm_installation() -> crate::Result<bool> {
340    use std::io::{self, Write};
341    print!("šŸ”§ Install missing vulnerability scanning tools? [y/N]: ");
342    io::stdout().flush()?;
343
344    let mut input = String::new();
345    io::stdin().read_line(&mut input)?;
346
347    Ok(input.trim().to_lowercase().starts_with('y'))
348}
349
350fn print_setup_instructions() {
351    println!("\nšŸ’” Setup Instructions:");
352    println!("  • Add ~/.local/bin to your PATH for manually installed tools");
353    println!("  • Add ~/go/bin to your PATH for Go tools");
354    println!("  • Add to your shell profile (~/.bashrc, ~/.zshrc, etc.):");
355    println!("    export PATH=\"$HOME/.local/bin:$HOME/go/bin:$PATH\"");
356}
357
358fn print_version_info(tool_name: &str) {
359    use std::process::Command;
360    let version_result = match tool_name {
361        "cargo-audit" => Command::new("cargo").args(["audit", "--version"]).output(),
362        "npm" => Command::new("npm").arg("--version").output(),
363        "bun" => Command::new("bun").arg("--version").output(),
364        "yarn" => Command::new("yarn").arg("--version").output(),
365        "pnpm" => Command::new("pnpm").arg("--version").output(),
366        "pip-audit" => Command::new("pip-audit").arg("--version").output(),
367        "govulncheck" => Command::new("govulncheck").arg("-version").output(),
368        "grype" => Command::new("grype").arg("version").output(),
369        _ => return,
370    };
371
372    if let Ok(output) = version_result
373        && output.status.success()
374    {
375        let version = String::from_utf8_lossy(&output.stdout);
376        println!("    Version: {}", version.trim());
377    }
378}
379
380fn print_language_guide(language: &Language, target_platform: &str) {
381    match language {
382        Language::Rust => {
383            println!("\nšŸ¦€ Rust - cargo-audit");
384            println!("  Install: cargo install cargo-audit");
385            println!("  Usage: cargo audit");
386        }
387        Language::JavaScript | Language::TypeScript => {
388            println!("\n🌐 JavaScript/TypeScript - Multiple package managers");
389            println!("  Bun (recommended for speed):");
390            println!("    Install: curl -fsSL https://bun.sh/install | bash");
391            if target_platform == "Windows" {
392                println!("    Windows: irm bun.sh/install.ps1 | iex");
393            }
394            println!("    Usage: bun audit");
395            println!("  npm (traditional):");
396            println!("    Install: Download Node.js from https://nodejs.org/");
397            if target_platform == "macOS" {
398                println!("    Package manager: brew install node");
399            } else if target_platform == "Linux" {
400                println!("    Package manager: sudo apt install nodejs npm (Ubuntu/Debian)");
401            }
402            println!("    Usage: npm audit");
403            println!("  yarn:");
404            println!("    Install: npm install -g yarn");
405            println!("    Usage: yarn audit");
406            println!("  pnpm:");
407            println!("    Install: npm install -g pnpm");
408            println!("    Usage: pnpm audit");
409        }
410        Language::Python => {
411            println!("\nšŸ Python - pip-audit");
412            println!("  Install: pipx install pip-audit (recommended)");
413            println!("  Alternative: pip3 install --user pip-audit");
414            println!("  Also available: safety (pip install safety)");
415            println!("  Usage: pip-audit");
416        }
417        Language::Go => {
418            println!("\n🐹 Go - govulncheck");
419            println!("  Install: go install golang.org/x/vuln/cmd/govulncheck@latest");
420            println!("  Note: Make sure ~/go/bin is in your PATH");
421            println!("  Usage: govulncheck ./...");
422        }
423        Language::Java => {
424            println!("\nā˜• Java - Multiple options");
425            println!("  Grype (recommended):");
426            match target_platform {
427                "macOS" => println!("    Install: brew install anchore/grype/grype"),
428                "Linux" => {
429                    println!("    Install: Download from https://github.com/anchore/grype/releases")
430                }
431                _ => {
432                    println!("    Install: Download from https://github.com/anchore/grype/releases")
433                }
434            }
435            println!("    Usage: grype .");
436            println!("  OWASP Dependency Check:");
437            match target_platform {
438                "macOS" => println!("    Install: brew install dependency-check"),
439                _ => println!(
440                    "    Install: Download from https://github.com/jeremylong/DependencyCheck/releases"
441                ),
442            }
443            println!("    Usage: dependency-check --project myproject --scan .");
444        }
445        _ => {}
446    }
447}
448
449fn print_universal_scanners_info() {
450    println!("\nšŸ” Universal Scanners:");
451    println!("  Grype: Works with multiple ecosystems");
452    println!("  Trivy: Container and filesystem scanning");
453    println!("  Snyk: Commercial solution with free tier");
454}
455
456fn print_general_tips() {
457    println!("\nšŸ’” Tips:");
458    println!("  • Run 'sync-ctl tools status' to check current installation");
459    println!("  • Run 'sync-ctl tools install' for automatic installation");
460    println!("  • Add tool directories to your PATH for easier access");
461}