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