1use crate::{
2 analyzer::{self, vulnerability::VulnerabilitySeverity},
3 cli::{OutputFormat, SeverityThreshold},
4};
5use std::path::PathBuf;
6
7pub async fn handle_vulnerabilities(
8 path: PathBuf,
9 severity: Option<SeverityThreshold>,
10 format: OutputFormat,
11 output: Option<PathBuf>,
12 quiet: bool,
13) -> crate::Result<()> {
14 let project_path = path.canonicalize().unwrap_or_else(|_| path.clone());
15
16 if !quiet {
17 println!(
18 "š Scanning for vulnerabilities in: {}",
19 project_path.display()
20 );
21 }
22
23 let parser = analyzer::dependency_parser::DependencyParser::new();
27 let project_dirs = parser.discover_project_dirs(&project_path);
28
29 let was_quiet = std::env::var("SYNCABLE_QUIET").is_ok();
32 if !was_quiet {
33 unsafe {
34 std::env::set_var("SYNCABLE_QUIET", "1");
35 }
36 }
37
38 let mut scannable_dirs = Vec::new();
40 for dir in &project_dirs {
41 let deps = parser.parse_deps_in_dir_standalone(dir)?;
42 if !deps.is_empty() {
43 let langs: Vec<String> = deps.keys().map(|l| format!("{:?}", l)).collect();
44 scannable_dirs.push((dir.clone(), deps, langs));
45 }
46 }
47
48 if !quiet && scannable_dirs.len() > 1 {
49 println!("\nš¦ Found {} projects to scan\n", scannable_dirs.len());
50 }
51
52 let mut merged_vulnerable_deps = Vec::new();
53 let any_deps_found = !scannable_dirs.is_empty();
54 let total_dirs = scannable_dirs.len();
55
56 for (i, (dir, deps, langs)) in scannable_dirs.into_iter().enumerate() {
57 let dir_name = dir
58 .strip_prefix(&project_path)
59 .unwrap_or(&dir)
60 .display()
61 .to_string();
62 let dir_label = if dir_name.is_empty() || dir_name == "." {
63 ".".to_string()
64 } else {
65 dir_name
66 };
67
68 if !quiet {
69 println!(
70 " [{}/{}] Scanning {} ({})",
71 i + 1,
72 total_dirs,
73 dir_label,
74 langs.join(", ")
75 );
76 }
77
78 let checker = analyzer::vulnerability::VulnerabilityChecker::new();
79 match checker.check_all_dependencies(&deps, &dir).await {
80 Ok(report) => {
81 let count = report
82 .vulnerable_dependencies
83 .iter()
84 .map(|d| d.vulnerabilities.len())
85 .sum::<usize>();
86 if !quiet && count > 0 {
87 println!(" ā ļø {} vulnerabilities found", count);
88 }
89 for mut dep in report.vulnerable_dependencies {
91 dep.source_dir = Some(dir_label.clone());
92 merged_vulnerable_deps.push(dep);
93 }
94 }
95 Err(e) => {
96 if !quiet {
97 eprintln!(" ā ļø scan failed: {}", e);
98 }
99 }
100 }
101 }
102
103 if !was_quiet {
105 unsafe {
106 std::env::remove_var("SYNCABLE_QUIET");
107 }
108 }
109
110 if !any_deps_found {
111 if !quiet {
112 println!("No dependencies found to check.");
113 }
114 return Ok(());
115 }
116
117 merged_vulnerable_deps.sort_by(|a, b| a.name.cmp(&b.name));
119 merged_vulnerable_deps.dedup_by(|a, b| a.name == b.name && a.version == b.version);
120
121 let mut critical_count = 0;
123 let mut high_count = 0;
124 let mut medium_count = 0;
125 let mut low_count = 0;
126 let mut total_vulnerabilities = 0;
127
128 for dep in &merged_vulnerable_deps {
129 for vuln in &dep.vulnerabilities {
130 total_vulnerabilities += 1;
131 match vuln.severity {
132 VulnerabilitySeverity::Critical => critical_count += 1,
133 VulnerabilitySeverity::High => high_count += 1,
134 VulnerabilitySeverity::Medium => medium_count += 1,
135 VulnerabilitySeverity::Low => low_count += 1,
136 VulnerabilitySeverity::Info => {}
137 }
138 }
139 }
140
141 let report = analyzer::vulnerability::VulnerabilityReport {
142 checked_at: chrono::Utc::now(),
143 total_vulnerabilities,
144 critical_count,
145 high_count,
146 medium_count,
147 low_count,
148 vulnerable_dependencies: merged_vulnerable_deps,
149 };
150
151 let filtered_report = if let Some(threshold) = severity {
153 filter_vulnerabilities_by_severity(report, threshold)
154 } else {
155 report
156 };
157
158 let output_string = match format {
160 OutputFormat::Table => {
161 format_vulnerabilities_table(&filtered_report, &severity, &project_path)
162 }
163 OutputFormat::Json => serde_json::to_string_pretty(&filtered_report)?,
164 };
165
166 if let Some(output_path) = output {
168 std::fs::write(&output_path, output_string)?;
169 if !quiet {
170 println!("Report saved to: {}", output_path.display());
171 }
172 } else if !quiet {
173 println!("{}", output_string);
174 }
175
176 if !quiet && (filtered_report.critical_count > 0 || filtered_report.high_count > 0) {
178 std::process::exit(1);
179 }
180
181 Ok(())
182}
183
184fn filter_vulnerabilities_by_severity(
185 report: analyzer::vulnerability::VulnerabilityReport,
186 threshold: SeverityThreshold,
187) -> analyzer::vulnerability::VulnerabilityReport {
188 let min_severity = match threshold {
189 SeverityThreshold::Low => VulnerabilitySeverity::Low,
190 SeverityThreshold::Medium => VulnerabilitySeverity::Medium,
191 SeverityThreshold::High => VulnerabilitySeverity::High,
192 SeverityThreshold::Critical => VulnerabilitySeverity::Critical,
193 };
194
195 let filtered_deps: Vec<_> = report
196 .vulnerable_dependencies
197 .into_iter()
198 .filter_map(|mut dep| {
199 dep.vulnerabilities.retain(|v| v.severity >= min_severity);
200 if dep.vulnerabilities.is_empty() {
201 None
202 } else {
203 Some(dep)
204 }
205 })
206 .collect();
207
208 use analyzer::vulnerability::VulnerabilityReport;
209 let mut filtered = VulnerabilityReport {
210 checked_at: report.checked_at,
211 total_vulnerabilities: 0,
212 critical_count: 0,
213 high_count: 0,
214 medium_count: 0,
215 low_count: 0,
216 vulnerable_dependencies: filtered_deps,
217 };
218
219 for dep in &filtered.vulnerable_dependencies {
221 for vuln in &dep.vulnerabilities {
222 filtered.total_vulnerabilities += 1;
223 match vuln.severity {
224 VulnerabilitySeverity::Critical => filtered.critical_count += 1,
225 VulnerabilitySeverity::High => filtered.high_count += 1,
226 VulnerabilitySeverity::Medium => filtered.medium_count += 1,
227 VulnerabilitySeverity::Low => filtered.low_count += 1,
228 VulnerabilitySeverity::Info => {}
229 }
230 }
231 }
232
233 filtered
234}
235
236fn format_vulnerabilities_table(
237 report: &analyzer::vulnerability::VulnerabilityReport,
238 severity: &Option<SeverityThreshold>,
239 project_path: &std::path::Path,
240) -> String {
241 let mut output = String::new();
242
243 output.push_str("\nš”ļø Vulnerability Scan Report\n");
244 output.push_str(&format!("{}\n", "=".repeat(80)));
245 output.push_str(&format!(
246 "Scanned at: {}\n",
247 report.checked_at.format("%Y-%m-%d %H:%M:%S UTC")
248 ));
249 output.push_str(&format!("Path: {}\n", project_path.display()));
250
251 if let Some(threshold) = severity {
252 output.push_str(&format!("Severity filter: >= {:?}\n", threshold));
253 }
254
255 output.push_str("\nSummary:\n");
256 output.push_str(&format!(
257 "Total vulnerabilities: {}\n",
258 report.total_vulnerabilities
259 ));
260
261 if report.total_vulnerabilities > 0 {
262 output.push_str("\nBy Severity:\n");
263 if report.critical_count > 0 {
264 output.push_str(&format!(" š“ CRITICAL: {}\n", report.critical_count));
265 }
266 if report.high_count > 0 {
267 output.push_str(&format!(" š“ HIGH: {}\n", report.high_count));
268 }
269 if report.medium_count > 0 {
270 output.push_str(&format!(" š” MEDIUM: {}\n", report.medium_count));
271 }
272 if report.low_count > 0 {
273 output.push_str(&format!(" šµ LOW: {}\n", report.low_count));
274 }
275
276 output.push_str(&format!("\n{}\n", "-".repeat(80)));
277 output.push_str("Vulnerable Dependencies:\n\n");
278
279 for vuln_dep in &report.vulnerable_dependencies {
280 let source = vuln_dep.source_dir.as_deref().unwrap_or(".");
281 output.push_str(&format!(
282 "š¦ {} v{} ({}) [{}]\n",
283 vuln_dep.name,
284 vuln_dep.version,
285 vuln_dep.language.as_str(),
286 source,
287 ));
288
289 for vuln in &vuln_dep.vulnerabilities {
290 let severity_str = match vuln.severity {
291 VulnerabilitySeverity::Critical => "CRITICAL",
292 VulnerabilitySeverity::High => "HIGH",
293 VulnerabilitySeverity::Medium => "MEDIUM",
294 VulnerabilitySeverity::Low => "LOW",
295 VulnerabilitySeverity::Info => "INFO",
296 };
297
298 output.push_str(&format!("\n ā ļø {} [{}]\n", vuln.id, severity_str));
299 output.push_str(&format!(" {}\n", vuln.title));
300
301 if !vuln.description.is_empty() && vuln.description != vuln.title {
302 let wrapped = textwrap::fill(&vuln.description, 70);
304 for line in wrapped.lines() {
305 output.push_str(&format!(" {}\n", line));
306 }
307 }
308
309 if let Some(ref cve) = vuln.cve {
310 output.push_str(&format!(" CVE: {}\n", cve));
311 }
312
313 if let Some(ref ghsa) = vuln.ghsa {
314 output.push_str(&format!(" GHSA: {}\n", ghsa));
315 }
316
317 output.push_str(&format!(" Affected: {}\n", vuln.affected_versions));
318
319 if let Some(ref patched) = vuln.patched_versions {
320 output.push_str(&format!(" ā
Fix: Upgrade to {}\n", patched));
321 }
322 }
323 output.push('\n');
324 }
325 } else {
326 output.push_str("\nā
No vulnerabilities found!\n");
327 }
328
329 output
330}