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<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 let monorepo_analysis = analyze_monorepo(&project_path)?;
27
28 let mut all_languages = Vec::new();
30 for project in &monorepo_analysis.projects {
31 all_languages.extend(project.analysis.languages.clone());
32 }
33
34 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 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 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 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 let mut deps_by_language: HashMap<analyzer::dependency_parser::Language, Vec<analyzer::dependency_parser::DependencyInfo>> = HashMap::new();
167
168 for language in all_languages {
170 let mut lang_deps = Vec::new();
171
172 for (name, info) in &dep_analysis.dependencies {
174 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 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_checker::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_checker::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_checker::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 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}