1use crate::{
2 analyzer::{
3 self, vulnerability_checker::VulnerabilitySeverity, DetectedTechnology, TechnologyCategory, LibraryType,
4 analyze_monorepo, ProjectCategory,
5 security::{TurboSecurityAnalyzer, TurboConfig, ScanMode},
6 },
7 cli::{ToolsCommand, OutputFormat, SeverityThreshold, DisplayFormat, SecurityScanMode},
8 generator,
9};
10use crate::analyzer::security::SecuritySeverity as TurboSecuritySeverity;
11use crate::analyzer::display::{display_analysis, DisplayMode, BoxDrawer};
12use std::process;
13use std::collections::HashMap;
14use std::path::PathBuf;
15
16pub fn handle_analyze(
17 path: std::path::PathBuf,
18 json: bool,
19 detailed: bool,
20 display: Option<DisplayFormat>,
21 _only: Option<Vec<String>>,
22) -> crate::Result<()> {
23 println!("š Analyzing project: {}", path.display());
24
25 let monorepo_analysis = analyze_monorepo(&path)?;
26
27 if json {
28 display_analysis(&monorepo_analysis, DisplayMode::Json);
29 } else {
30 let mode = if detailed {
32 DisplayMode::Detailed
34 } else {
35 match display {
36 Some(DisplayFormat::Matrix) | None => DisplayMode::Matrix,
37 Some(DisplayFormat::Detailed) => DisplayMode::Detailed,
38 Some(DisplayFormat::Summary) => DisplayMode::Summary,
39 }
40 };
41
42 display_analysis(&monorepo_analysis, mode);
43 }
44
45 Ok(())
46}
47
48pub fn handle_generate(
49 path: std::path::PathBuf,
50 _output: Option<std::path::PathBuf>,
51 dockerfile: bool,
52 compose: bool,
53 terraform: bool,
54 all: bool,
55 dry_run: bool,
56 _force: bool,
57) -> crate::Result<()> {
58 println!("š Analyzing project for generation: {}", path.display());
59
60 let monorepo_analysis = analyze_monorepo(&path)?;
61
62 println!("ā
Analysis complete. Generating IaC files...");
63
64 if monorepo_analysis.is_monorepo {
65 println!("š¦ Detected monorepo with {} projects", monorepo_analysis.projects.len());
66 println!("š§ Monorepo IaC generation is coming soon! For now, generating for the overall structure.");
67 println!("š” Tip: You can run generate commands on individual project directories for now.");
68 }
69
70 let main_project = &monorepo_analysis.projects[0];
73
74 let generate_all = all || (!dockerfile && !compose && !terraform);
75
76 if generate_all || dockerfile {
77 println!("\nš³ Generating Dockerfile...");
78 let dockerfile_content = generator::generate_dockerfile(&main_project.analysis)?;
79
80 if dry_run {
81 println!("--- Dockerfile (dry run) ---");
82 println!("{}", dockerfile_content);
83 } else {
84 std::fs::write("Dockerfile", dockerfile_content)?;
85 println!("ā
Dockerfile generated successfully!");
86 }
87 }
88
89 if generate_all || compose {
90 println!("\nš Generating Docker Compose file...");
91 let compose_content = generator::generate_compose(&main_project.analysis)?;
92
93 if dry_run {
94 println!("--- docker-compose.yml (dry run) ---");
95 println!("{}", compose_content);
96 } else {
97 std::fs::write("docker-compose.yml", compose_content)?;
98 println!("ā
Docker Compose file generated successfully!");
99 }
100 }
101
102 if generate_all || terraform {
103 println!("\nšļø Generating Terraform configuration...");
104 let terraform_content = generator::generate_terraform(&main_project.analysis)?;
105
106 if dry_run {
107 println!("--- main.tf (dry run) ---");
108 println!("{}", terraform_content);
109 } else {
110 std::fs::write("main.tf", terraform_content)?;
111 println!("ā
Terraform configuration generated successfully!");
112 }
113 }
114
115 if !dry_run {
116 println!("\nš Generation complete! IaC files have been created in the current directory.");
117
118 if monorepo_analysis.is_monorepo {
119 println!("š§ Note: Generated files are based on the main project structure.");
120 println!(" Advanced monorepo support with per-project generation is coming soon!");
121 }
122 }
123
124 Ok(())
125}
126
127pub fn handle_validate(
128 _path: std::path::PathBuf,
129 _types: Option<Vec<String>>,
130 _fix: bool,
131) -> crate::Result<()> {
132 println!("š Validating IaC files...");
133 println!("ā ļø Validation feature is not yet implemented.");
134 Ok(())
135}
136
137pub fn handle_support(
138 languages: bool,
139 frameworks: bool,
140 _detailed: bool,
141) -> crate::Result<()> {
142 if languages || (!languages && !frameworks) {
143 println!("š Supported Languages:");
144 println!("āāā Rust");
145 println!("āāā JavaScript/TypeScript");
146 println!("āāā Python");
147 println!("āāā Go");
148 println!("āāā Java");
149 println!("āāā (More coming soon...)");
150 }
151
152 if frameworks || (!languages && !frameworks) {
153 println!("\nš Supported Frameworks:");
154 println!("āāā Web: Express.js, Next.js, React, Vue.js, Actix Web");
155 println!("āāā Database: PostgreSQL, MySQL, MongoDB, Redis");
156 println!("āāā Build Tools: npm, yarn, cargo, maven, gradle");
157 println!("āāā (More coming soon...)");
158 }
159
160 Ok(())
161}
162
163pub async fn handle_dependencies(
164 path: std::path::PathBuf,
165 licenses: bool,
166 vulnerabilities: bool,
167 _prod_only: bool,
168 _dev_only: bool,
169 format: OutputFormat,
170) -> crate::Result<()> {
171 let project_path = path.canonicalize()
172 .unwrap_or_else(|_| path.clone());
173
174 println!("š Analyzing dependencies: {}", project_path.display());
175
176 let monorepo_analysis = analyze_monorepo(&project_path)?;
178
179 let mut all_languages = Vec::new();
181 for project in &monorepo_analysis.projects {
182 all_languages.extend(project.analysis.languages.clone());
183 }
184
185 let dep_analysis = analyzer::dependency_parser::parse_detailed_dependencies(
187 &project_path,
188 &all_languages,
189 &analyzer::AnalysisConfig::default(),
190 ).await?;
191
192 if format == OutputFormat::Table {
193 use termcolor::{ColorChoice, StandardStream, WriteColor, ColorSpec, Color};
195
196 let mut stdout = StandardStream::stdout(ColorChoice::Always);
197
198 println!("\nš¦ Dependency Analysis Report");
200 println!("{}", "=".repeat(80));
201
202 let total_deps: usize = dep_analysis.dependencies.len();
203 println!("Total dependencies: {}", total_deps);
204
205 if monorepo_analysis.is_monorepo {
206 println!("Projects analyzed: {}", monorepo_analysis.projects.len());
207 for project in &monorepo_analysis.projects {
208 println!(" ⢠{} ({})", project.name, format_project_category(&project.project_category));
209 }
210 }
211
212 for (name, info) in &dep_analysis.dependencies {
213 print!(" {} v{}", name, info.version);
214
215 stdout.set_color(ColorSpec::new().set_fg(Some(
217 if info.is_dev { Color::Yellow } else { Color::Green }
218 )))?;
219
220 print!(" [{}]", if info.is_dev { "dev" } else { "prod" });
221
222 stdout.reset()?;
223
224 if licenses && info.license.is_some() {
225 print!(" - License: {}", info.license.as_ref().unwrap_or(&"Unknown".to_string()));
226 }
227
228 println!();
229 }
230
231 if licenses {
232 println!("\nš License Summary");
234 println!("{}", "-".repeat(80));
235
236 use std::collections::HashMap;
237 let mut license_counts: HashMap<String, usize> = HashMap::new();
238
239 for (_name, info) in &dep_analysis.dependencies {
240 if let Some(license) = &info.license {
241 *license_counts.entry(license.clone()).or_insert(0) += 1;
242 }
243 }
244
245 let mut licenses: Vec<_> = license_counts.into_iter().collect();
246 licenses.sort_by(|a, b| b.1.cmp(&a.1));
247
248 for (license, count) in licenses {
249 println!(" {}: {} packages", license, count);
250 }
251 }
252
253 if vulnerabilities {
254 println!("\nš Checking for vulnerabilities...");
255
256 let mut deps_by_language: HashMap<analyzer::dependency_parser::Language, Vec<analyzer::dependency_parser::DependencyInfo>> = HashMap::new();
258
259 for language in &all_languages {
261 let mut lang_deps = Vec::new();
262
263 for (name, info) in &dep_analysis.dependencies {
265 let matches_language = match language.name.as_str() {
267 "Rust" => info.source == "crates.io",
268 "JavaScript" | "TypeScript" => info.source == "npm",
269 "Python" => info.source == "pypi",
270 "Go" => info.source == "go modules",
271 "Java" | "Kotlin" => info.source == "maven" || info.source == "gradle",
272 _ => false,
273 };
274
275 if matches_language {
276 lang_deps.push(analyzer::dependency_parser::DependencyInfo {
278 name: name.clone(),
279 version: info.version.clone(),
280 dep_type: if info.is_dev {
281 analyzer::dependency_parser::DependencyType::Dev
282 } else {
283 analyzer::dependency_parser::DependencyType::Production
284 },
285 license: info.license.clone().unwrap_or_default(),
286 source: Some(info.source.clone()),
287 language: match language.name.as_str() {
288 "Rust" => analyzer::dependency_parser::Language::Rust,
289 "JavaScript" => analyzer::dependency_parser::Language::JavaScript,
290 "TypeScript" => analyzer::dependency_parser::Language::TypeScript,
291 "Python" => analyzer::dependency_parser::Language::Python,
292 "Go" => analyzer::dependency_parser::Language::Go,
293 "Java" => analyzer::dependency_parser::Language::Java,
294 "Kotlin" => analyzer::dependency_parser::Language::Kotlin,
295 _ => analyzer::dependency_parser::Language::Unknown,
296 },
297 });
298 }
299 }
300
301 if !lang_deps.is_empty() {
302 let lang_enum = match language.name.as_str() {
303 "Rust" => analyzer::dependency_parser::Language::Rust,
304 "JavaScript" => analyzer::dependency_parser::Language::JavaScript,
305 "TypeScript" => analyzer::dependency_parser::Language::TypeScript,
306 "Python" => analyzer::dependency_parser::Language::Python,
307 "Go" => analyzer::dependency_parser::Language::Go,
308 "Java" => analyzer::dependency_parser::Language::Java,
309 "Kotlin" => analyzer::dependency_parser::Language::Kotlin,
310 _ => analyzer::dependency_parser::Language::Unknown,
311 };
312 deps_by_language.insert(lang_enum, lang_deps);
313 }
314 }
315
316 let checker = analyzer::vulnerability_checker::VulnerabilityChecker::new();
317 match checker.check_all_dependencies(&deps_by_language, &project_path).await {
318 Ok(report) => {
319 println!("\nš”ļø Vulnerability Report");
320 println!("{}", "-".repeat(80));
321 println!("Checked at: {}", report.checked_at.format("%Y-%m-%d %H:%M:%S UTC"));
322 println!("Total vulnerabilities: {}", report.total_vulnerabilities);
323
324 if report.total_vulnerabilities > 0 {
325 println!("\nSeverity Breakdown:");
326 if report.critical_count > 0 {
327 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?;
328 println!(" CRITICAL: {}", report.critical_count);
329 stdout.reset()?;
330 }
331 if report.high_count > 0 {
332 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
333 println!(" HIGH: {}", report.high_count);
334 stdout.reset()?;
335 }
336 if report.medium_count > 0 {
337 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
338 println!(" MEDIUM: {}", report.medium_count);
339 stdout.reset()?;
340 }
341 if report.low_count > 0 {
342 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?;
343 println!(" LOW: {}", report.low_count);
344 stdout.reset()?;
345 }
346
347 println!("\nVulnerable Dependencies:");
348 for vuln_dep in &report.vulnerable_dependencies {
349 println!("\n š¦ {} v{} ({})",
350 vuln_dep.name,
351 vuln_dep.version,
352 vuln_dep.language.as_str()
353 );
354
355 for vuln in &vuln_dep.vulnerabilities {
356 print!(" ā ļø {} ", vuln.id);
357
358 stdout.set_color(ColorSpec::new().set_fg(Some(
360 match vuln.severity {
361 VulnerabilitySeverity::Critical => Color::Red,
362 VulnerabilitySeverity::High => Color::Red,
363 VulnerabilitySeverity::Medium => Color::Yellow,
364 VulnerabilitySeverity::Low => Color::Blue,
365 VulnerabilitySeverity::Info => Color::Cyan,
366 }
367 )).set_bold(vuln.severity == VulnerabilitySeverity::Critical))?;
368
369 print!("[{}]", match vuln.severity {
370 VulnerabilitySeverity::Critical => "CRITICAL",
371 VulnerabilitySeverity::High => "HIGH",
372 VulnerabilitySeverity::Medium => "MEDIUM",
373 VulnerabilitySeverity::Low => "LOW",
374 VulnerabilitySeverity::Info => "INFO",
375 });
376
377 stdout.reset()?;
378
379 println!(" - {}", vuln.title);
380
381 if let Some(ref cve) = vuln.cve {
382 println!(" CVE: {}", cve);
383 }
384 if let Some(ref patched) = vuln.patched_versions {
385 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
386 println!(" Fix: Upgrade to {}", patched);
387 stdout.reset()?;
388 }
389 }
390 }
391 } else {
392 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
393 println!("\nā
No known vulnerabilities found!");
394 stdout.reset()?;
395 }
396 }
397 Err(e) => {
398 eprintln!("Error checking vulnerabilities: {}", e);
399 process::exit(1);
400 }
401 }
402 }
403 } else if format == OutputFormat::Json {
404 let output = serde_json::json!({
406 "dependencies": dep_analysis.dependencies,
407 "total": dep_analysis.dependencies.len(),
408 });
409 println!("{}", serde_json::to_string_pretty(&output)?);
410 }
411
412 Ok(())
413}
414
415pub async fn handle_vulnerabilities(
416 path: std::path::PathBuf,
417 severity: Option<SeverityThreshold>,
418 format: OutputFormat,
419 output: Option<std::path::PathBuf>,
420) -> crate::Result<()> {
421 let project_path = path.canonicalize()
422 .unwrap_or_else(|_| path.clone());
423
424 println!("š Scanning for vulnerabilities in: {}", project_path.display());
425
426 let dependencies = analyzer::dependency_parser::DependencyParser::new().parse_all_dependencies(&project_path)?;
428
429 if dependencies.is_empty() {
430 println!("No dependencies found to check.");
431 return Ok(());
432 }
433
434 let checker = analyzer::vulnerability_checker::VulnerabilityChecker::new();
436 let report = checker.check_all_dependencies(&dependencies, &project_path).await
437 .map_err(|e| crate::error::IaCGeneratorError::Analysis(
438 crate::error::AnalysisError::DependencyParsing {
439 file: "vulnerability check".to_string(),
440 reason: e.to_string(),
441 }
442 ))?;
443
444 let filtered_report = if let Some(threshold) = severity {
446 let min_severity = match threshold {
447 SeverityThreshold::Low => VulnerabilitySeverity::Low,
448 SeverityThreshold::Medium => VulnerabilitySeverity::Medium,
449 SeverityThreshold::High => VulnerabilitySeverity::High,
450 SeverityThreshold::Critical => VulnerabilitySeverity::Critical,
451 };
452
453 let filtered_deps: Vec<_> = report.vulnerable_dependencies
454 .into_iter()
455 .filter_map(|mut dep| {
456 dep.vulnerabilities.retain(|v| v.severity >= min_severity);
457 if dep.vulnerabilities.is_empty() {
458 None
459 } else {
460 Some(dep)
461 }
462 })
463 .collect();
464
465 use analyzer::vulnerability_checker::VulnerabilityReport;
466 let mut filtered = VulnerabilityReport {
467 checked_at: report.checked_at,
468 total_vulnerabilities: 0,
469 critical_count: 0,
470 high_count: 0,
471 medium_count: 0,
472 low_count: 0,
473 vulnerable_dependencies: filtered_deps,
474 };
475
476 for dep in &filtered.vulnerable_dependencies {
478 for vuln in &dep.vulnerabilities {
479 filtered.total_vulnerabilities += 1;
480 match vuln.severity {
481 VulnerabilitySeverity::Critical => filtered.critical_count += 1,
482 VulnerabilitySeverity::High => filtered.high_count += 1,
483 VulnerabilitySeverity::Medium => filtered.medium_count += 1,
484 VulnerabilitySeverity::Low => filtered.low_count += 1,
485 VulnerabilitySeverity::Info => {},
486 }
487 }
488 }
489
490 filtered
491 } else {
492 report
493 };
494
495 let output_string = match format {
497 OutputFormat::Table => {
498 let mut output = String::new();
502
503 output.push_str(&format!("\nš”ļø Vulnerability Scan Report\n"));
504 output.push_str(&format!("{}\n", "=".repeat(80)));
505 output.push_str(&format!("Scanned at: {}\n", filtered_report.checked_at.format("%Y-%m-%d %H:%M:%S UTC")));
506 output.push_str(&format!("Path: {}\n", project_path.display()));
507
508 if let Some(threshold) = severity {
509 output.push_str(&format!("Severity filter: >= {:?}\n", threshold));
510 }
511
512 output.push_str(&format!("\nSummary:\n"));
513 output.push_str(&format!("Total vulnerabilities: {}\n", filtered_report.total_vulnerabilities));
514
515 if filtered_report.total_vulnerabilities > 0 {
516 output.push_str("\nBy Severity:\n");
517 if filtered_report.critical_count > 0 {
518 output.push_str(&format!(" š“ CRITICAL: {}\n", filtered_report.critical_count));
519 }
520 if filtered_report.high_count > 0 {
521 output.push_str(&format!(" š“ HIGH: {}\n", filtered_report.high_count));
522 }
523 if filtered_report.medium_count > 0 {
524 output.push_str(&format!(" š” MEDIUM: {}\n", filtered_report.medium_count));
525 }
526 if filtered_report.low_count > 0 {
527 output.push_str(&format!(" šµ LOW: {}\n", filtered_report.low_count));
528 }
529
530 output.push_str(&format!("\n{}\n", "-".repeat(80)));
531 output.push_str("Vulnerable Dependencies:\n\n");
532
533 for vuln_dep in &filtered_report.vulnerable_dependencies {
534 output.push_str(&format!("š¦ {} v{} ({})\n",
535 vuln_dep.name,
536 vuln_dep.version,
537 vuln_dep.language.as_str()
538 ));
539
540 for vuln in &vuln_dep.vulnerabilities {
541 let severity_str = match vuln.severity {
542 VulnerabilitySeverity::Critical => "CRITICAL",
543 VulnerabilitySeverity::High => "HIGH",
544 VulnerabilitySeverity::Medium => "MEDIUM",
545 VulnerabilitySeverity::Low => "LOW",
546 VulnerabilitySeverity::Info => "INFO",
547 };
548
549 output.push_str(&format!("\n ā ļø {} [{}]\n", vuln.id, severity_str));
550 output.push_str(&format!(" {}\n", vuln.title));
551
552 if !vuln.description.is_empty() && vuln.description != vuln.title {
553 let wrapped = textwrap::fill(&vuln.description, 70);
555 for line in wrapped.lines() {
556 output.push_str(&format!(" {}\n", line));
557 }
558 }
559
560 if let Some(ref cve) = vuln.cve {
561 output.push_str(&format!(" CVE: {}\n", cve));
562 }
563
564 if let Some(ref ghsa) = vuln.ghsa {
565 output.push_str(&format!(" GHSA: {}\n", ghsa));
566 }
567
568 output.push_str(&format!(" Affected: {}\n", vuln.affected_versions));
569
570 if let Some(ref patched) = vuln.patched_versions {
571 output.push_str(&format!(" ā
Fix: Upgrade to {}\n", patched));
572 }
573 }
574 output.push_str("\n");
575 }
576 } else {
577 output.push_str("\nā
No vulnerabilities found!\n");
578 }
579
580 output
581 }
582 OutputFormat::Json => {
583 serde_json::to_string_pretty(&filtered_report)?
584 }
585 };
586
587 if let Some(output_path) = output {
589 std::fs::write(&output_path, output_string)?;
590 println!("Report saved to: {}", output_path.display());
591 } else {
592 println!("{}", output_string);
593 }
594
595 if filtered_report.critical_count > 0 || filtered_report.high_count > 0 {
597 std::process::exit(1);
598 }
599
600 Ok(())
601}
602
603fn display_technologies_detailed(technologies: &[DetectedTechnology]) {
605 if technologies.is_empty() {
606 println!("\nš ļø Technologies Detected: None");
607 return;
608 }
609
610 let mut meta_frameworks = Vec::new();
612 let mut backend_frameworks = Vec::new();
613 let mut frontend_frameworks = Vec::new();
614 let mut ui_libraries = Vec::new();
615 let mut build_tools = Vec::new();
616 let mut databases = Vec::new();
617 let mut testing = Vec::new();
618 let mut runtimes = Vec::new();
619 let mut other_libraries = Vec::new();
620
621 for tech in technologies {
622 match &tech.category {
623 TechnologyCategory::MetaFramework => meta_frameworks.push(tech),
624 TechnologyCategory::BackendFramework => backend_frameworks.push(tech),
625 TechnologyCategory::FrontendFramework => frontend_frameworks.push(tech),
626 TechnologyCategory::Library(lib_type) => match lib_type {
627 LibraryType::UI => ui_libraries.push(tech),
628 _ => other_libraries.push(tech),
629 },
630 TechnologyCategory::BuildTool => build_tools.push(tech),
631 TechnologyCategory::Database => databases.push(tech),
632 TechnologyCategory::Testing => testing.push(tech),
633 TechnologyCategory::Runtime => runtimes.push(tech),
634 _ => other_libraries.push(tech),
635 }
636 }
637
638 println!("\nš ļø Technology Stack:");
639
640 if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
642 println!(" šÆ PRIMARY: {} (confidence: {:.1}%)", primary.name, primary.confidence * 100.0);
643 println!(" Architecture driver for this project");
644 }
645
646 if !meta_frameworks.is_empty() {
648 println!("\n šļø Meta-Frameworks:");
649 for tech in meta_frameworks {
650 println!(" ⢠{} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
651 }
652 }
653
654 if !backend_frameworks.is_empty() {
656 println!("\n š„ļø Backend Frameworks:");
657 for tech in backend_frameworks {
658 println!(" ⢠{} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
659 }
660 }
661
662 if !frontend_frameworks.is_empty() {
664 println!("\n š Frontend Frameworks:");
665 for tech in frontend_frameworks {
666 println!(" ⢠{} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
667 }
668 }
669
670 if !ui_libraries.is_empty() {
672 println!("\n šØ UI Libraries:");
673 for tech in ui_libraries {
674 println!(" ⢠{} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
675 }
676 }
677
678 if !build_tools.is_empty() {
683 println!("\n šØ Build Tools:");
684 for tech in build_tools {
685 println!(" ⢠{} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
686 }
687 }
688
689 if !databases.is_empty() {
691 println!("\n šļø Database & ORM:");
692 for tech in databases {
693 println!(" ⢠{} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
694 }
695 }
696
697 if !testing.is_empty() {
699 println!("\n š§Ŗ Testing:");
700 for tech in testing {
701 println!(" ⢠{} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
702 }
703 }
704
705 if !runtimes.is_empty() {
707 println!("\n ā” Runtimes:");
708 for tech in runtimes {
709 println!(" ⢠{} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
710 }
711 }
712
713 if !other_libraries.is_empty() {
715 println!("\n š Other Libraries:");
716 for tech in other_libraries {
717 println!(" ⢠{} (confidence: {:.1}%)", tech.name, tech.confidence * 100.0);
718 }
719 }
720}
721
722fn display_technologies_summary(technologies: &[DetectedTechnology]) {
724 println!("āāā Technologies detected: {}", technologies.len());
725
726 if let Some(primary) = technologies.iter().find(|t| t.is_primary) {
728 println!("ā āāā šÆ {} (PRIMARY, {:.1}%)", primary.name, primary.confidence * 100.0);
729 }
730
731 for tech in technologies.iter().filter(|t| !t.is_primary) {
733 let icon = match &tech.category {
734 TechnologyCategory::MetaFramework => "šļø",
735 TechnologyCategory::BackendFramework => "š„ļø",
736 TechnologyCategory::FrontendFramework => "š",
737 TechnologyCategory::Library(LibraryType::UI) => "šØ",
738 TechnologyCategory::BuildTool => "šØ",
739 TechnologyCategory::Database => "šļø",
740 TechnologyCategory::Testing => "š§Ŗ",
741 TechnologyCategory::Runtime => "ā”",
742 _ => "š",
743 };
744 println!("ā āāā {} {} (confidence: {:.1}%)", icon, tech.name, tech.confidence * 100.0);
745 }
746}
747
748pub fn handle_security(
749 path: std::path::PathBuf,
750 mode: SecurityScanMode,
751 include_low: bool,
752 no_secrets: bool,
753 no_code_patterns: bool,
754 _no_infrastructure: bool,
755 _no_compliance: bool,
756 _frameworks: Vec<String>,
757 format: OutputFormat,
758 output: Option<std::path::PathBuf>,
759 fail_on_findings: bool,
760) -> crate::Result<()> {
761 let project_path = path.canonicalize()
762 .unwrap_or_else(|_| path.clone());
763
764 println!("š”ļø Running security analysis on: {}", project_path.display());
765
766 let scan_mode = if no_secrets && no_code_patterns {
768 ScanMode::Lightning
770 } else if include_low {
771 ScanMode::Paranoid
773 } else {
774 match mode {
776 SecurityScanMode::Lightning => ScanMode::Lightning,
777 SecurityScanMode::Fast => ScanMode::Fast,
778 SecurityScanMode::Balanced => ScanMode::Balanced,
779 SecurityScanMode::Thorough => ScanMode::Thorough,
780 SecurityScanMode::Paranoid => ScanMode::Paranoid,
781 }
782 };
783
784 let config = TurboConfig {
786 scan_mode,
787 max_file_size: 10 * 1024 * 1024, worker_threads: 0, use_mmap: true,
790 enable_cache: true,
791 cache_size_mb: 100,
792 max_critical_findings: if fail_on_findings { Some(1) } else { None },
793 timeout_seconds: Some(60),
794 skip_gitignored: true,
795 priority_extensions: vec![
796 "env".to_string(), "key".to_string(), "pem".to_string(),
797 "json".to_string(), "yml".to_string(), "yaml".to_string(),
798 "toml".to_string(), "ini".to_string(), "conf".to_string(),
799 "config".to_string(), "js".to_string(), "ts".to_string(),
800 "py".to_string(), "rs".to_string(), "go".to_string(),
801 ],
802 pattern_sets: if no_secrets {
803 vec![]
804 } else {
805 vec!["default".to_string(), "aws".to_string(), "gcp".to_string()]
806 },
807 };
808
809 let analyzer = TurboSecurityAnalyzer::new(config)
811 .map_err(|e| crate::error::IaCGeneratorError::Analysis(
812 crate::error::AnalysisError::InvalidStructure(
813 format!("Failed to create turbo security analyzer: {}", e)
814 )
815 ))?;
816
817 let start_time = std::time::Instant::now();
818 let security_report = analyzer.analyze_project(&project_path)
819 .map_err(|e| crate::error::IaCGeneratorError::Analysis(
820 crate::error::AnalysisError::InvalidStructure(
821 format!("Turbo security analysis failed: {}", e)
822 )
823 ))?;
824 let scan_duration = start_time.elapsed();
825
826 println!("ā” Scan completed in {:.2}s", scan_duration.as_secs_f64());
827
828 let output_string = match format {
830 OutputFormat::Table => {
831 use crate::analyzer::display::BoxDrawer;
832 use colored::*;
833
834 let mut output = String::new();
835
836 output.push_str(&format!("\n{}\n", "š”ļø Security Analysis Results".bright_white().bold()));
838 output.push_str(&format!("{}\n", "ā".repeat(80).bright_blue()));
839
840 let mut score_box = BoxDrawer::new("Security Summary");
842 score_box.add_line("Overall Score:", &format!("{:.0}/100", security_report.overall_score).bright_yellow(), true);
843 score_box.add_line("Risk Level:", &format!("{:?}", security_report.risk_level).color(match security_report.risk_level {
844 TurboSecuritySeverity::Critical => "bright_red",
845 TurboSecuritySeverity::High => "red",
846 TurboSecuritySeverity::Medium => "yellow",
847 TurboSecuritySeverity::Low => "green",
848 TurboSecuritySeverity::Info => "blue",
849 }), true);
850 score_box.add_line("Total Findings:", &security_report.total_findings.to_string().cyan(), true);
851
852 let config_files = security_report.findings.iter()
854 .filter_map(|f| f.file_path.as_ref())
855 .collect::<std::collections::HashSet<_>>()
856 .len();
857 score_box.add_line("Files Analyzed:", &config_files.max(1).to_string().green(), true);
858 score_box.add_line("Scan Mode:", &format!("{:?}", scan_mode).green(), true);
859
860 output.push_str(&format!("\n{}\n", score_box.draw()));
861
862 if !security_report.findings.is_empty() {
864 let terminal_width = if let Some((width, _)) = term_size::dimensions() {
866 width.saturating_sub(10) } else {
868 120 };
870
871 let mut findings_box = BoxDrawer::new("Security Findings");
872
873 for (i, finding) in security_report.findings.iter().enumerate() {
874 let severity_color = match finding.severity {
875 TurboSecuritySeverity::Critical => "bright_red",
876 TurboSecuritySeverity::High => "red",
877 TurboSecuritySeverity::Medium => "yellow",
878 TurboSecuritySeverity::Low => "blue",
879 TurboSecuritySeverity::Info => "green",
880 };
881
882 let file_display = if let Some(file_path) = &finding.file_path {
884 let canonical_file = file_path.canonicalize().unwrap_or_else(|_| file_path.clone());
886 let canonical_project = path.canonicalize().unwrap_or_else(|_| path.clone());
887
888 if let Ok(relative_path) = canonical_file.strip_prefix(&canonical_project) {
890 let relative_str = relative_path.to_string_lossy().replace('\\', "/");
892 format!("./{}", relative_str)
893 } else {
894 let path_str = file_path.to_string_lossy();
896 if path_str.starts_with('/') {
897 if let Some(project_name) = path.file_name().and_then(|n| n.to_str()) {
899 if let Some(project_idx) = path_str.rfind(project_name) {
900 let relative_part = &path_str[project_idx + project_name.len()..];
901 if relative_part.starts_with('/') {
902 format!(".{}", relative_part)
903 } else if !relative_part.is_empty() {
904 format!("./{}", relative_part)
905 } else {
906 format!("./{}", file_path.file_name().unwrap_or_default().to_string_lossy())
907 }
908 } else {
909 path_str.to_string()
911 }
912 } else {
913 path_str.to_string()
915 }
916 } else {
917 if path_str.starts_with("./") {
919 path_str.to_string()
920 } else {
921 format!("./{}", path_str)
922 }
923 }
924 }
925 } else {
926 "N/A".to_string()
927 };
928
929 let gitignore_status = if finding.description.contains("is tracked by git") {
931 "TRACKED".bright_red().bold()
932 } else if finding.description.contains("is NOT in .gitignore") {
933 "EXPOSED".yellow().bold()
934 } else if finding.description.contains("is protected") || finding.description.contains("properly ignored") {
935 "SAFE".bright_green().bold()
936 } else if finding.description.contains("appears safe") {
937 "OK".bright_blue().bold()
938 } else {
939 "UNKNOWN".dimmed()
940 };
941
942 let finding_type = if finding.title.contains("Environment Variable") {
944 "ENV VAR"
945 } else if finding.title.contains("Secret File") {
946 "SECRET FILE"
947 } else if finding.title.contains("API Key") || finding.title.contains("Stripe") || finding.title.contains("Firebase") {
948 "API KEY"
949 } else if finding.title.contains("Configuration") {
950 "CONFIG"
951 } else {
952 "OTHER"
953 };
954
955 let position_display = match (finding.line_number, finding.column_number) {
957 (Some(line), Some(col)) => format!("{}:{}", line, col),
958 (Some(line), None) => format!("{}", line),
959 _ => "ā".to_string(),
960 };
961
962 let box_margin = 6; let available_width = terminal_width.saturating_sub(box_margin);
965 let max_path_width = available_width.saturating_sub(20); if file_display.len() + 3 <= max_path_width {
968 findings_box.add_value_only(&format!("{}. {}",
970 format!("{}", i + 1).bright_white().bold(),
971 file_display.cyan().bold()
972 ));
973 } else if file_display.len() <= available_width.saturating_sub(4) {
974 findings_box.add_value_only(&format!("{}.",
976 format!("{}", i + 1).bright_white().bold()
977 ));
978 findings_box.add_value_only(&format!(" {}",
979 file_display.cyan().bold()
980 ));
981 } else {
982 findings_box.add_value_only(&format!("{}.",
984 format!("{}", i + 1).bright_white().bold()
985 ));
986
987 let wrap_width = available_width.saturating_sub(4);
989 let mut remaining = file_display.as_str();
990 let mut first_line = true;
991
992 while !remaining.is_empty() {
993 let prefix = if first_line { " " } else { " " };
994 let line_width = wrap_width.saturating_sub(prefix.len());
995
996 if remaining.len() <= line_width {
997 findings_box.add_value_only(&format!("{}{}",
999 prefix, remaining.cyan().bold()
1000 ));
1001 break;
1002 } else {
1003 let chunk = &remaining[..line_width];
1005 let break_point = chunk.rfind('/').unwrap_or(line_width.saturating_sub(1));
1006
1007 findings_box.add_value_only(&format!("{}{}",
1008 prefix, chunk[..break_point].cyan().bold()
1009 ));
1010 remaining = &remaining[break_point..];
1011 if remaining.starts_with('/') {
1012 remaining = &remaining[1..]; }
1014 }
1015 first_line = false;
1016 }
1017 }
1018
1019 findings_box.add_value_only(&format!(" {} {} | {} {} | {} {} | {} {}",
1020 "Type:".dimmed(),
1021 finding_type.yellow(),
1022 "Severity:".dimmed(),
1023 format!("{:?}", finding.severity).color(severity_color).bold(),
1024 "Position:".dimmed(),
1025 position_display.bright_cyan(),
1026 "Status:".dimmed(),
1027 gitignore_status
1028 ));
1029
1030 if i < security_report.findings.len() - 1 {
1032 findings_box.add_value_only("");
1033 }
1034 }
1035
1036 output.push_str(&format!("\n{}\n", findings_box.draw()));
1037
1038 let mut legend_box = BoxDrawer::new("Git Status Legend");
1040 legend_box.add_line(&"TRACKED:".bright_red().bold().to_string(), "File is tracked by git - CRITICAL RISK", false);
1041 legend_box.add_line(&"EXPOSED:".yellow().bold().to_string(), "File contains secrets but not in .gitignore", false);
1042 legend_box.add_line(&"SAFE:".bright_green().bold().to_string(), "File is properly ignored by .gitignore", false);
1043 legend_box.add_line(&"OK:".bright_blue().bold().to_string(), "File appears safe for version control", false);
1044 output.push_str(&format!("\n{}\n", legend_box.draw()));
1045 } else {
1046 let mut no_findings_box = BoxDrawer::new("Security Status");
1047 no_findings_box.add_value_only(&"ā
No security issues detected".green());
1048 no_findings_box.add_value_only("š” Regular security scanning recommended");
1049 output.push_str(&format!("\n{}\n", no_findings_box.draw()));
1050 }
1051
1052 let mut rec_box = BoxDrawer::new("Key Recommendations");
1054 if !security_report.recommendations.is_empty() {
1055 for (i, rec) in security_report.recommendations.iter().take(5).enumerate() {
1056 let clean_rec = rec.replace("Add these patterns to your .gitignore:", "Add to .gitignore:");
1058 rec_box.add_value_only(&format!("{}. {}", i + 1, clean_rec));
1059 }
1060 if security_report.recommendations.len() > 5 {
1061 rec_box.add_value_only(&format!("... and {} more recommendations",
1062 security_report.recommendations.len() - 5).dimmed());
1063 }
1064 } else {
1065 rec_box.add_value_only("ā
No immediate security concerns detected");
1066 rec_box.add_value_only("š” Consider implementing dependency scanning");
1067 rec_box.add_value_only("š” Review environment variable security practices");
1068 }
1069 output.push_str(&format!("\n{}\n", rec_box.draw()));
1070
1071 output
1072 }
1073 OutputFormat::Json => {
1074 serde_json::to_string_pretty(&security_report)?
1075 }
1076 };
1077
1078 if let Some(output_path) = output {
1080 std::fs::write(&output_path, output_string)?;
1081 println!("Security report saved to: {}", output_path.display());
1082 } else {
1083 print!("{}", output_string);
1084 }
1085
1086 if fail_on_findings && security_report.total_findings > 0 {
1088 let critical_count = security_report.findings_by_severity
1089 .get(&TurboSecuritySeverity::Critical)
1090 .unwrap_or(&0);
1091 let high_count = security_report.findings_by_severity
1092 .get(&TurboSecuritySeverity::High)
1093 .unwrap_or(&0);
1094
1095 if *critical_count > 0 {
1096 eprintln!("ā Critical security issues found. Please address immediately.");
1097 std::process::exit(1);
1098 } else if *high_count > 0 {
1099 eprintln!("ā ļø High severity security issues found. Review recommended.");
1100 std::process::exit(2);
1101 } else {
1102 eprintln!("ā¹ļø Security issues found but none are critical or high severity.");
1103 std::process::exit(3);
1104 }
1105 }
1106
1107 Ok(())
1108}
1109
1110pub async fn handle_tools(command: ToolsCommand) -> crate::Result<()> {
1111 use crate::analyzer::{tool_installer::ToolInstaller, dependency_parser::Language};
1112 use std::collections::HashMap;
1113 use termcolor::{ColorChoice, StandardStream, WriteColor, ColorSpec, Color};
1114
1115 match command {
1116 ToolsCommand::Status { format, languages } => {
1117 let installer = ToolInstaller::new();
1118
1119 let langs_to_check = if let Some(lang_names) = languages {
1121 lang_names.iter()
1122 .filter_map(|name| Language::from_string(name))
1123 .collect()
1124 } else {
1125 vec![
1126 Language::Rust,
1127 Language::JavaScript,
1128 Language::TypeScript,
1129 Language::Python,
1130 Language::Go,
1131 Language::Java,
1132 Language::Kotlin,
1133 ]
1134 };
1135
1136 println!("š§ Checking vulnerability scanning tools status...\n");
1137
1138 match format {
1139 OutputFormat::Table => {
1140 let mut stdout = StandardStream::stdout(ColorChoice::Always);
1141
1142 println!("š Vulnerability Scanning Tools Status");
1143 println!("{}", "=".repeat(50));
1144
1145 for language in &langs_to_check {
1146 let (tool_name, is_available) = match language {
1147 Language::Rust => ("cargo-audit", installer.test_tool_availability("cargo-audit")),
1148 Language::JavaScript | Language::TypeScript => ("npm", installer.test_tool_availability("npm")),
1149 Language::Python => ("pip-audit", installer.test_tool_availability("pip-audit")),
1150 Language::Go => ("govulncheck", installer.test_tool_availability("govulncheck")),
1151 Language::Java | Language::Kotlin => ("grype", installer.test_tool_availability("grype")),
1152 _ => continue,
1153 };
1154
1155 print!(" {} {:?}: ",
1156 if is_available { "ā
" } else { "ā" },
1157 language);
1158
1159 if is_available {
1160 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
1161 print!("{} installed", tool_name);
1162 } else {
1163 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
1164 print!("{} missing", tool_name);
1165 }
1166
1167 stdout.reset()?;
1168 println!();
1169 }
1170
1171 println!("\nš Universal Scanners:");
1173 let grype_available = installer.test_tool_availability("grype");
1174 print!(" {} Grype: ", if grype_available { "ā
" } else { "ā" });
1175 if grype_available {
1176 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
1177 println!("installed");
1178 } else {
1179 stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
1180 println!("missing");
1181 }
1182 stdout.reset()?;
1183 }
1184 OutputFormat::Json => {
1185 let mut status = HashMap::new();
1186
1187 for language in &langs_to_check {
1188 let (tool_name, is_available) = match language {
1189 Language::Rust => ("cargo-audit", installer.test_tool_availability("cargo-audit")),
1190 Language::JavaScript | Language::TypeScript => ("npm", installer.test_tool_availability("npm")),
1191 Language::Python => ("pip-audit", installer.test_tool_availability("pip-audit")),
1192 Language::Go => ("govulncheck", installer.test_tool_availability("govulncheck")),
1193 Language::Java | Language::Kotlin => ("grype", installer.test_tool_availability("grype")),
1194 _ => continue,
1195 };
1196
1197 status.insert(format!("{:?}", language), serde_json::json!({
1198 "tool": tool_name,
1199 "available": is_available
1200 }));
1201 }
1202
1203 println!("{}", serde_json::to_string_pretty(&status)?);
1204 }
1205 }
1206 }
1207
1208 ToolsCommand::Install { languages, include_owasp, dry_run, yes } => {
1209 let mut installer = ToolInstaller::new();
1210
1211 let langs_to_install = if let Some(lang_names) = languages {
1213 lang_names.iter()
1214 .filter_map(|name| Language::from_string(name))
1215 .collect()
1216 } else {
1217 vec![
1218 Language::Rust,
1219 Language::JavaScript,
1220 Language::TypeScript,
1221 Language::Python,
1222 Language::Go,
1223 Language::Java,
1224 ]
1225 };
1226
1227 if dry_run {
1228 println!("š Dry run: Tools that would be installed:");
1229 println!("{}", "=".repeat(50));
1230
1231 for language in &langs_to_install {
1232 let (tool_name, is_available) = match language {
1233 Language::Rust => ("cargo-audit", installer.test_tool_availability("cargo-audit")),
1234 Language::JavaScript | Language::TypeScript => ("npm", installer.test_tool_availability("npm")),
1235 Language::Python => ("pip-audit", installer.test_tool_availability("pip-audit")),
1236 Language::Go => ("govulncheck", installer.test_tool_availability("govulncheck")),
1237 Language::Java | Language::Kotlin => ("grype", installer.test_tool_availability("grype")),
1238 _ => continue,
1239 };
1240
1241 if !is_available {
1242 println!(" š¦ Would install {} for {:?}", tool_name, language);
1243 } else {
1244 println!(" ā
{} already installed for {:?}", tool_name, language);
1245 }
1246 }
1247
1248 if include_owasp && !installer.test_tool_availability("dependency-check") {
1249 println!(" š¦ Would install OWASP Dependency Check (large download)");
1250 }
1251
1252 return Ok(());
1253 }
1254
1255 if !yes {
1256 use std::io::{self, Write};
1257 print!("š§ Install missing vulnerability scanning tools? [y/N]: ");
1258 io::stdout().flush()?;
1259
1260 let mut input = String::new();
1261 io::stdin().read_line(&mut input)?;
1262
1263 if !input.trim().to_lowercase().starts_with('y') {
1264 println!("Installation cancelled.");
1265 return Ok(());
1266 }
1267 }
1268
1269 println!("š ļø Installing vulnerability scanning tools...");
1270
1271 match installer.ensure_tools_for_languages(&langs_to_install) {
1272 Ok(()) => {
1273 println!("ā
Tool installation completed!");
1274 installer.print_tool_status(&langs_to_install);
1275
1276 println!("\nš” Setup Instructions:");
1278 println!(" ⢠Add ~/.local/bin to your PATH for manually installed tools");
1279 println!(" ⢠Add ~/go/bin to your PATH for Go tools");
1280 println!(" ⢠Add to your shell profile (~/.bashrc, ~/.zshrc, etc.):");
1281 println!(" export PATH=\"$HOME/.local/bin:$HOME/go/bin:$PATH\"");
1282 }
1283 Err(e) => {
1284 eprintln!("ā Tool installation failed: {}", e);
1285 eprintln!("\nš§ Manual installation may be required for some tools.");
1286 eprintln!(" Run 'sync-ctl tools guide' for manual installation instructions.");
1287 return Err(e);
1288 }
1289 }
1290 }
1291
1292 ToolsCommand::Verify { languages, verbose } => {
1293 let installer = ToolInstaller::new();
1294
1295 let langs_to_verify = if let Some(lang_names) = languages {
1297 lang_names.iter()
1298 .filter_map(|name| Language::from_string(name))
1299 .collect()
1300 } else {
1301 vec![
1302 Language::Rust,
1303 Language::JavaScript,
1304 Language::TypeScript,
1305 Language::Python,
1306 Language::Go,
1307 Language::Java,
1308 ]
1309 };
1310
1311 println!("š Verifying vulnerability scanning tools...\n");
1312
1313 let mut all_working = true;
1314
1315 for language in &langs_to_verify {
1316 let (tool_name, is_working) = match language {
1317 Language::Rust => {
1318 let working = installer.test_tool_availability("cargo-audit");
1319 ("cargo-audit", working)
1320 }
1321 Language::JavaScript | Language::TypeScript => {
1322 let working = installer.test_tool_availability("npm");
1323 ("npm", working)
1324 }
1325 Language::Python => {
1326 let working = installer.test_tool_availability("pip-audit");
1327 ("pip-audit", working)
1328 }
1329 Language::Go => {
1330 let working = installer.test_tool_availability("govulncheck");
1331 ("govulncheck", working)
1332 }
1333 Language::Java | Language::Kotlin => {
1334 let working = installer.test_tool_availability("grype");
1335 ("grype", working)
1336 }
1337 _ => continue,
1338 };
1339
1340 print!(" {} {:?}: {}",
1341 if is_working { "ā
" } else { "ā" },
1342 language,
1343 tool_name);
1344
1345 if is_working {
1346 println!(" - working correctly");
1347
1348 if verbose {
1349 use std::process::Command;
1351 let version_result = match tool_name {
1352 "cargo-audit" => Command::new("cargo").args(&["audit", "--version"]).output(),
1353 "npm" => Command::new("npm").arg("--version").output(),
1354 "pip-audit" => Command::new("pip-audit").arg("--version").output(),
1355 "govulncheck" => Command::new("govulncheck").arg("-version").output(),
1356 "grype" => Command::new("grype").arg("version").output(),
1357 _ => continue,
1358 };
1359
1360 if let Ok(output) = version_result {
1361 if output.status.success() {
1362 let version = String::from_utf8_lossy(&output.stdout);
1363 println!(" Version: {}", version.trim());
1364 }
1365 }
1366 }
1367 } else {
1368 println!(" - not working or missing");
1369 all_working = false;
1370 }
1371 }
1372
1373 if all_working {
1374 println!("\nā
All tools are working correctly!");
1375 } else {
1376 println!("\nā Some tools are missing or not working.");
1377 println!(" Run 'sync-ctl tools install' to install missing tools.");
1378 }
1379 }
1380
1381 ToolsCommand::Guide { languages, platform } => {
1382 let target_platform = platform.unwrap_or_else(|| {
1383 match std::env::consts::OS {
1384 "macos" => "macOS".to_string(),
1385 "linux" => "Linux".to_string(),
1386 "windows" => "Windows".to_string(),
1387 other => other.to_string(),
1388 }
1389 });
1390
1391 println!("š Vulnerability Scanning Tools Installation Guide");
1392 println!("Platform: {}", target_platform);
1393 println!("{}", "=".repeat(60));
1394
1395 let langs_to_show = if let Some(lang_names) = languages {
1396 lang_names.iter()
1397 .filter_map(|name| Language::from_string(name))
1398 .collect()
1399 } else {
1400 vec![
1401 Language::Rust,
1402 Language::JavaScript,
1403 Language::TypeScript,
1404 Language::Python,
1405 Language::Go,
1406 Language::Java,
1407 ]
1408 };
1409
1410 for language in &langs_to_show {
1411 match language {
1412 Language::Rust => {
1413 println!("\nš¦ Rust - cargo-audit");
1414 println!(" Install: cargo install cargo-audit");
1415 println!(" Usage: cargo audit");
1416 }
1417 Language::JavaScript | Language::TypeScript => {
1418 println!("\nš JavaScript/TypeScript - npm audit");
1419 println!(" Install: Download Node.js from https://nodejs.org/");
1420 match target_platform.as_str() {
1421 "macOS" => println!(" Package manager: brew install node"),
1422 "Linux" => println!(" Package manager: sudo apt install nodejs npm (Ubuntu/Debian)"),
1423 _ => {}
1424 }
1425 println!(" Usage: npm audit");
1426 }
1427 Language::Python => {
1428 println!("\nš Python - pip-audit");
1429 println!(" Install: pipx install pip-audit (recommended)");
1430 println!(" Alternative: pip3 install --user pip-audit");
1431 println!(" Also available: safety (pip install safety)");
1432 println!(" Usage: pip-audit");
1433 }
1434 Language::Go => {
1435 println!("\nš¹ Go - govulncheck");
1436 println!(" Install: go install golang.org/x/vuln/cmd/govulncheck@latest");
1437 println!(" Note: Make sure ~/go/bin is in your PATH");
1438 println!(" Usage: govulncheck ./...");
1439 }
1440 Language::Java => {
1441 println!("\nā Java - Multiple options");
1442 println!(" Grype (recommended):");
1443 match target_platform.as_str() {
1444 "macOS" => println!(" Install: brew install anchore/grype/grype"),
1445 "Linux" => println!(" Install: Download from https://github.com/anchore/grype/releases"),
1446 _ => println!(" Install: Download from https://github.com/anchore/grype/releases"),
1447 }
1448 println!(" Usage: grype .");
1449 println!(" OWASP Dependency Check:");
1450 match target_platform.as_str() {
1451 "macOS" => println!(" Install: brew install dependency-check"),
1452 _ => println!(" Install: Download from https://github.com/jeremylong/DependencyCheck/releases"),
1453 }
1454 println!(" Usage: dependency-check --project myproject --scan .");
1455 }
1456 _ => {}
1457 }
1458 }
1459
1460 println!("\nš Universal Scanners:");
1461 println!(" Grype: Works with multiple ecosystems");
1462 println!(" Trivy: Container and filesystem scanning");
1463 println!(" Snyk: Commercial solution with free tier");
1464
1465 println!("\nš” Tips:");
1466 println!(" ⢠Run 'sync-ctl tools status' to check current installation");
1467 println!(" ⢠Run 'sync-ctl tools install' for automatic installation");
1468 println!(" ⢠Add tool directories to your PATH for easier access");
1469 }
1470 }
1471
1472 Ok(())
1473}
1474
1475fn format_project_category(category: &ProjectCategory) -> &'static str {
1477 match category {
1478 ProjectCategory::Frontend => "Frontend",
1479 ProjectCategory::Backend => "Backend",
1480 ProjectCategory::Api => "API",
1481 ProjectCategory::Service => "Service",
1482 ProjectCategory::Library => "Library",
1483 ProjectCategory::Tool => "Tool",
1484 ProjectCategory::Documentation => "Documentation",
1485 ProjectCategory::Infrastructure => "Infrastructure",
1486 ProjectCategory::Unknown => "Unknown",
1487 }
1488}