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 let monorepo_analysis = analyze_monorepo(&project_path)?;
29
30 let mut all_languages = Vec::new();
32 for project in &monorepo_analysis.projects {
33 all_languages.extend(project.analysis.languages.clone());
34 }
35
36 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 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 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 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 let mut deps_by_language: HashMap<
192 analyzer::dependency_parser::Language,
193 Vec<analyzer::dependency_parser::DependencyInfo>,
194 > = HashMap::new();
195
196 for language in all_languages {
198 let mut lang_deps = Vec::new();
199
200 for (name, info) in &dep_analysis.dependencies {
202 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 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 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}