Skip to main content

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