syncable_cli/handlers/
dependencies.rs

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