syncable_cli/handlers/
dependencies.rs

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