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 let monorepo_analysis = analyze_monorepo(&project_path)?;
26
27 let mut all_languages = Vec::new();
29 for project in &monorepo_analysis.projects {
30 all_languages.extend(project.analysis.languages.clone());
31 }
32
33 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 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 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 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 let mut deps_by_language: HashMap<
187 analyzer::dependency_parser::Language,
188 Vec<analyzer::dependency_parser::DependencyInfo>,
189 > = HashMap::new();
190
191 for language in all_languages {
193 let mut lang_deps = Vec::new();
194
195 for (name, info) in &dep_analysis.dependencies {
197 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 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 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}