syncable_cli/handlers/
dependencies.rs

1use crate::handlers::utils::format_project_category;
2use crate::{
3    analyzer::{self, analyze_monorepo, vulnerability::VulnerabilitySeverity},
4    cli::OutputFormat,
5};
6use std::collections::HashMap;
7use std::process;
8
9pub async fn handle_dependencies(
10    path: std::path::PathBuf,
11    licenses: bool,
12    vulnerabilities: bool,
13    _prod_only: bool,
14    _dev_only: bool,
15    format: OutputFormat,
16) -> crate::Result<String> {
17    let project_path = path.canonicalize().unwrap_or_else(|_| path.clone());
18
19    let mut output = String::new();
20    let header = format!("šŸ” Analyzing dependencies: {}\n", project_path.display());
21    println!("{}", header);
22    output.push_str(&header);
23
24    // First, analyze the project using monorepo analysis
25    let monorepo_analysis = analyze_monorepo(&project_path)?;
26
27    // Collect all languages from all projects
28    let mut all_languages = Vec::new();
29    for project in &monorepo_analysis.projects {
30        all_languages.extend(project.analysis.languages.clone());
31    }
32
33    // Then perform detailed dependency analysis using the collected languages
34    let dep_analysis = analyzer::dependency_parser::parse_detailed_dependencies(
35        &project_path,
36        &all_languages,
37        &analyzer::AnalysisConfig::default(),
38    )
39    .await?;
40
41    if format == OutputFormat::Table {
42        let table_output = display_dependencies_table(
43            &dep_analysis,
44            &monorepo_analysis,
45            licenses,
46            vulnerabilities,
47            &all_languages,
48            &project_path,
49        )
50        .await?;
51        output.push_str(&table_output);
52    } else if format == OutputFormat::Json {
53        // JSON output
54        let json_data = serde_json::json!({
55            "dependencies": dep_analysis.dependencies,
56            "total": dep_analysis.dependencies.len(),
57        });
58        let json_output = serde_json::to_string_pretty(&json_data)?;
59        println!("{}", json_output);
60        output.push_str(&json_output);
61    }
62
63    Ok(output)
64}
65
66async fn display_dependencies_table(
67    dep_analysis: &analyzer::dependency_parser::DependencyAnalysis,
68    monorepo_analysis: &analyzer::MonorepoAnalysis,
69    licenses: bool,
70    vulnerabilities: bool,
71    all_languages: &[analyzer::DetectedLanguage],
72    project_path: &std::path::Path,
73) -> crate::Result<String> {
74    use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
75
76    let mut output = String::new();
77    let mut stdout = StandardStream::stdout(ColorChoice::Always);
78
79    // Print summary
80    let summary_header = format!("\nšŸ“¦ Dependency Analysis Report\n{}\n", "=".repeat(80));
81    println!("{}", summary_header);
82    output.push_str(&summary_header);
83
84    let total_deps_line = format!("Total dependencies: {}\n", dep_analysis.dependencies.len());
85    println!("{}", total_deps_line);
86    output.push_str(&total_deps_line);
87
88    if monorepo_analysis.is_monorepo {
89        let projects_line = format!("Projects analyzed: {}\n", monorepo_analysis.projects.len());
90        println!("{}", projects_line);
91        output.push_str(&projects_line);
92        for project in &monorepo_analysis.projects {
93            let project_line = format!(
94                "  • {} ({})\n",
95                project.name,
96                format_project_category(&project.project_category)
97            );
98            println!("{}", project_line);
99            output.push_str(&project_line);
100        }
101    }
102
103    for (name, info) in &dep_analysis.dependencies {
104        let dep_line = format!("  {} v{}", name, info.version);
105        print!("{}", dep_line);
106        output.push_str(&dep_line);
107
108        // Color code by type
109        stdout.set_color(ColorSpec::new().set_fg(Some(if info.is_dev {
110            Color::Yellow
111        } else {
112            Color::Green
113        })))?;
114
115        let type_tag = format!(" [{}]", if info.is_dev { "dev" } else { "prod" });
116        print!("{}", type_tag);
117        output.push_str(&type_tag);
118
119        stdout.reset()?;
120
121        if licenses && info.license.is_some() {
122            let license_info = format!(
123                " - License: {}",
124                info.license.as_ref().unwrap_or(&"Unknown".to_string())
125            );
126            print!("{}", license_info);
127            output.push_str(&license_info);
128        }
129
130        println!();
131        output.push('\n');
132    }
133
134    if licenses {
135        let license_output = display_license_summary(&dep_analysis.dependencies);
136        output.push_str(&license_output);
137    }
138
139    if vulnerabilities {
140        let vuln_output =
141            check_and_display_vulnerabilities(dep_analysis, all_languages, project_path).await?;
142        output.push_str(&vuln_output);
143    }
144
145    Ok(output)
146}
147
148fn display_license_summary(
149    dependencies: &analyzer::dependency_parser::DetailedDependencyMap,
150) -> String {
151    let mut output = String::new();
152    output.push_str(&format!("\nšŸ“‹ License Summary\n{}\n", "-".repeat(80)));
153
154    let mut license_counts: HashMap<String, usize> = HashMap::new();
155
156    for info in dependencies.values() {
157        if let Some(license) = &info.license {
158            *license_counts.entry(license.clone()).or_insert(0) += 1;
159        }
160    }
161
162    let mut licenses: Vec<_> = license_counts.into_iter().collect();
163    licenses.sort_by(|a, b| b.1.cmp(&a.1));
164
165    for (license, count) in licenses {
166        output.push_str(&format!("  {}: {} packages\n", license, count));
167    }
168
169    println!("{}", output);
170    output
171}
172
173async fn check_and_display_vulnerabilities(
174    dep_analysis: &analyzer::dependency_parser::DependencyAnalysis,
175    all_languages: &[analyzer::DetectedLanguage],
176    project_path: &std::path::Path,
177) -> crate::Result<String> {
178    use termcolor::{Color, ColorChoice, ColorSpec, StandardStream, WriteColor};
179
180    let mut output = String::new();
181
182    println!("\nšŸ” Checking for vulnerabilities...");
183    output.push_str("\nšŸ” Checking for vulnerabilities...\n");
184
185    // Convert DetailedDependencyMap to the format expected by VulnerabilityChecker
186    let mut deps_by_language: HashMap<
187        analyzer::dependency_parser::Language,
188        Vec<analyzer::dependency_parser::DependencyInfo>,
189    > = HashMap::new();
190
191    // Group dependencies by detected languages
192    for language in all_languages {
193        let mut lang_deps = Vec::new();
194
195        // Filter dependencies that belong to this language
196        for (name, info) in &dep_analysis.dependencies {
197            // Simple heuristic to determine language based on source
198            let matches_language = match language.name.as_str() {
199                "Rust" => info.source == "crates.io",
200                "JavaScript" | "TypeScript" => info.source == "npm",
201                "Python" => info.source == "pypi",
202                "Go" => info.source == "go modules",
203                "Java" | "Kotlin" => info.source == "maven" || info.source == "gradle",
204                _ => false,
205            };
206
207            if matches_language {
208                // Convert to new DependencyInfo format expected by vulnerability checker
209                lang_deps.push(analyzer::dependency_parser::DependencyInfo {
210                    name: name.clone(),
211                    version: info.version.clone(),
212                    dep_type: if info.is_dev {
213                        analyzer::dependency_parser::DependencyType::Dev
214                    } else {
215                        analyzer::dependency_parser::DependencyType::Production
216                    },
217                    license: info.license.clone().unwrap_or_default(),
218                    source: Some(info.source.clone()),
219                    language: match language.name.as_str() {
220                        "Rust" => analyzer::dependency_parser::Language::Rust,
221                        "JavaScript" => analyzer::dependency_parser::Language::JavaScript,
222                        "TypeScript" => analyzer::dependency_parser::Language::TypeScript,
223                        "Python" => analyzer::dependency_parser::Language::Python,
224                        "Go" => analyzer::dependency_parser::Language::Go,
225                        "Java" => analyzer::dependency_parser::Language::Java,
226                        "Kotlin" => analyzer::dependency_parser::Language::Kotlin,
227                        _ => analyzer::dependency_parser::Language::Unknown,
228                    },
229                });
230            }
231        }
232
233        if !lang_deps.is_empty() {
234            let lang_enum = match language.name.as_str() {
235                "Rust" => analyzer::dependency_parser::Language::Rust,
236                "JavaScript" => analyzer::dependency_parser::Language::JavaScript,
237                "TypeScript" => analyzer::dependency_parser::Language::TypeScript,
238                "Python" => analyzer::dependency_parser::Language::Python,
239                "Go" => analyzer::dependency_parser::Language::Go,
240                "Java" => analyzer::dependency_parser::Language::Java,
241                "Kotlin" => analyzer::dependency_parser::Language::Kotlin,
242                _ => analyzer::dependency_parser::Language::Unknown,
243            };
244            deps_by_language.insert(lang_enum, lang_deps);
245        }
246    }
247
248    let checker = analyzer::vulnerability::VulnerabilityChecker::new();
249    match checker
250        .check_all_dependencies(&deps_by_language, project_path)
251        .await
252    {
253        Ok(report) => {
254            let mut stdout = StandardStream::stdout(ColorChoice::Always);
255
256            let report_header = format!(
257                "\nšŸ›”ļø Vulnerability Report\n{}\nChecked at: {}\nTotal vulnerabilities: {}\n",
258                "-".repeat(80),
259                report.checked_at.format("%Y-%m-%d %H:%M:%S UTC"),
260                report.total_vulnerabilities
261            );
262            println!("{}", report_header);
263            output.push_str(&report_header);
264
265            if report.total_vulnerabilities > 0 {
266                let breakdown_output = display_vulnerability_breakdown(&report, &mut stdout)?;
267                output.push_str(&breakdown_output);
268
269                let deps_output = display_vulnerable_dependencies(&report, &mut stdout)?;
270                output.push_str(&deps_output);
271            } else {
272                stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
273                let no_vulns_message = "\nāœ… No known vulnerabilities found!\n";
274                println!("{}", no_vulns_message);
275                output.push_str(no_vulns_message);
276                stdout.reset()?;
277            }
278        }
279        Err(e) => {
280            eprintln!("Error checking vulnerabilities: {}", e);
281            process::exit(1);
282        }
283    }
284
285    Ok(output)
286}
287
288fn display_vulnerability_breakdown(
289    report: &analyzer::vulnerability::VulnerabilityReport,
290    stdout: &mut termcolor::StandardStream,
291) -> crate::Result<String> {
292    use termcolor::{Color, ColorSpec, WriteColor};
293
294    let mut output = String::new();
295
296    output.push_str("\nSeverity Breakdown:\n");
297    if report.critical_count > 0 {
298        stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)).set_bold(true))?;
299        let critical_line = format!("  CRITICAL: {}\n", report.critical_count);
300        output.push_str(&critical_line);
301        print!("{}", critical_line);
302        stdout.reset()?;
303    }
304    if report.high_count > 0 {
305        stdout.set_color(ColorSpec::new().set_fg(Some(Color::Red)))?;
306        let high_line = format!("  HIGH: {}\n", report.high_count);
307        output.push_str(&high_line);
308        print!("{}", high_line);
309        stdout.reset()?;
310    }
311    if report.medium_count > 0 {
312        stdout.set_color(ColorSpec::new().set_fg(Some(Color::Yellow)))?;
313        let medium_line = format!("  MEDIUM: {}\n", report.medium_count);
314        output.push_str(&medium_line);
315        print!("{}", medium_line);
316        stdout.reset()?;
317    }
318    if report.low_count > 0 {
319        stdout.set_color(ColorSpec::new().set_fg(Some(Color::Blue)))?;
320        let low_line = format!("  LOW: {}\n", report.low_count);
321        output.push_str(&low_line);
322        print!("{}", low_line);
323        stdout.reset()?;
324    }
325
326    Ok(output)
327}
328
329fn display_vulnerable_dependencies(
330    report: &analyzer::vulnerability::VulnerabilityReport,
331    stdout: &mut termcolor::StandardStream,
332) -> crate::Result<String> {
333    use termcolor::{Color, ColorSpec, WriteColor};
334
335    let mut output = String::new();
336
337    output.push_str("\nVulnerable Dependencies:\n");
338    for vuln_dep in &report.vulnerable_dependencies {
339        let dep_line = format!(
340            "\n  šŸ“¦ {} v{} ({})\n",
341            vuln_dep.name,
342            vuln_dep.version,
343            vuln_dep.language.as_str()
344        );
345        output.push_str(&dep_line);
346        print!("{}", dep_line);
347
348        for vuln in &vuln_dep.vulnerabilities {
349            let vuln_id_line = format!("    āš ļø  {} ", vuln.id);
350            output.push_str(&vuln_id_line);
351            print!("{}", vuln_id_line);
352
353            // Color by severity
354            stdout.set_color(
355                ColorSpec::new()
356                    .set_fg(Some(match vuln.severity {
357                        VulnerabilitySeverity::Critical => Color::Red,
358                        VulnerabilitySeverity::High => Color::Red,
359                        VulnerabilitySeverity::Medium => Color::Yellow,
360                        VulnerabilitySeverity::Low => Color::Blue,
361                        VulnerabilitySeverity::Info => Color::Cyan,
362                    }))
363                    .set_bold(vuln.severity == VulnerabilitySeverity::Critical),
364            )?;
365
366            let severity_tag = match vuln.severity {
367                VulnerabilitySeverity::Critical => "[CRITICAL]",
368                VulnerabilitySeverity::High => "[HIGH]",
369                VulnerabilitySeverity::Medium => "[MEDIUM]",
370                VulnerabilitySeverity::Low => "[LOW]",
371                VulnerabilitySeverity::Info => "[INFO]",
372            };
373            output.push_str(severity_tag);
374            print!("{}", severity_tag);
375
376            stdout.reset()?;
377
378            let title_line = format!(" - {}\n", vuln.title);
379            output.push_str(&title_line);
380            print!("{}", title_line);
381
382            if let Some(ref cve) = vuln.cve {
383                let cve_line = format!("       CVE: {}\n", cve);
384                output.push_str(&cve_line);
385                println!("{}", cve_line.trim_end());
386            }
387            if let Some(ref patched) = vuln.patched_versions {
388                stdout.set_color(ColorSpec::new().set_fg(Some(Color::Green)))?;
389                let fix_line = format!("       Fix: Upgrade to {}\n", patched);
390                output.push_str(&fix_line);
391                println!("{}", fix_line.trim_end());
392                stdout.reset()?;
393            }
394        }
395    }
396
397    Ok(output)
398}