syncable_cli/handlers/
vulnerabilities.rs1use 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) -> crate::Result<()> {
13 let project_path = path.canonicalize().unwrap_or_else(|_| path.clone());
14
15 println!(
16 "š Scanning for vulnerabilities in: {}",
17 project_path.display()
18 );
19
20 let dependencies = analyzer::dependency_parser::DependencyParser::new()
22 .parse_all_dependencies(&project_path)?;
23
24 if dependencies.is_empty() {
25 println!("No dependencies found to check.");
26 return Ok(());
27 }
28
29 let checker = analyzer::vulnerability::VulnerabilityChecker::new();
31 let report = checker
32 .check_all_dependencies(&dependencies, &project_path)
33 .await
34 .map_err(|e| {
35 crate::error::IaCGeneratorError::Analysis(
36 crate::error::AnalysisError::DependencyParsing {
37 file: "vulnerability check".to_string(),
38 reason: e.to_string(),
39 },
40 )
41 })?;
42
43 let filtered_report = if let Some(threshold) = severity {
45 filter_vulnerabilities_by_severity(report, threshold)
46 } else {
47 report
48 };
49
50 let output_string = match format {
52 OutputFormat::Table => {
53 format_vulnerabilities_table(&filtered_report, &severity, &project_path)
54 }
55 OutputFormat::Json => serde_json::to_string_pretty(&filtered_report)?,
56 };
57
58 if let Some(output_path) = output {
60 std::fs::write(&output_path, output_string)?;
61 println!("Report saved to: {}", output_path.display());
62 } else {
63 println!("{}", output_string);
64 }
65
66 if filtered_report.critical_count > 0 || filtered_report.high_count > 0 {
68 std::process::exit(1);
69 }
70
71 Ok(())
72}
73
74fn filter_vulnerabilities_by_severity(
75 report: analyzer::vulnerability::VulnerabilityReport,
76 threshold: SeverityThreshold,
77) -> analyzer::vulnerability::VulnerabilityReport {
78 let min_severity = match threshold {
79 SeverityThreshold::Low => VulnerabilitySeverity::Low,
80 SeverityThreshold::Medium => VulnerabilitySeverity::Medium,
81 SeverityThreshold::High => VulnerabilitySeverity::High,
82 SeverityThreshold::Critical => VulnerabilitySeverity::Critical,
83 };
84
85 let filtered_deps: Vec<_> = report
86 .vulnerable_dependencies
87 .into_iter()
88 .filter_map(|mut dep| {
89 dep.vulnerabilities.retain(|v| v.severity >= min_severity);
90 if dep.vulnerabilities.is_empty() {
91 None
92 } else {
93 Some(dep)
94 }
95 })
96 .collect();
97
98 use analyzer::vulnerability::VulnerabilityReport;
99 let mut filtered = VulnerabilityReport {
100 checked_at: report.checked_at,
101 total_vulnerabilities: 0,
102 critical_count: 0,
103 high_count: 0,
104 medium_count: 0,
105 low_count: 0,
106 vulnerable_dependencies: filtered_deps,
107 };
108
109 for dep in &filtered.vulnerable_dependencies {
111 for vuln in &dep.vulnerabilities {
112 filtered.total_vulnerabilities += 1;
113 match vuln.severity {
114 VulnerabilitySeverity::Critical => filtered.critical_count += 1,
115 VulnerabilitySeverity::High => filtered.high_count += 1,
116 VulnerabilitySeverity::Medium => filtered.medium_count += 1,
117 VulnerabilitySeverity::Low => filtered.low_count += 1,
118 VulnerabilitySeverity::Info => {}
119 }
120 }
121 }
122
123 filtered
124}
125
126fn format_vulnerabilities_table(
127 report: &analyzer::vulnerability::VulnerabilityReport,
128 severity: &Option<SeverityThreshold>,
129 project_path: &std::path::Path,
130) -> String {
131 let mut output = String::new();
132
133 output.push_str("\nš”ļø Vulnerability Scan Report\n");
134 output.push_str(&format!("{}\n", "=".repeat(80)));
135 output.push_str(&format!(
136 "Scanned at: {}\n",
137 report.checked_at.format("%Y-%m-%d %H:%M:%S UTC")
138 ));
139 output.push_str(&format!("Path: {}\n", project_path.display()));
140
141 if let Some(threshold) = severity {
142 output.push_str(&format!("Severity filter: >= {:?}\n", threshold));
143 }
144
145 output.push_str("\nSummary:\n");
146 output.push_str(&format!(
147 "Total vulnerabilities: {}\n",
148 report.total_vulnerabilities
149 ));
150
151 if report.total_vulnerabilities > 0 {
152 output.push_str("\nBy Severity:\n");
153 if report.critical_count > 0 {
154 output.push_str(&format!(" š“ CRITICAL: {}\n", report.critical_count));
155 }
156 if report.high_count > 0 {
157 output.push_str(&format!(" š“ HIGH: {}\n", report.high_count));
158 }
159 if report.medium_count > 0 {
160 output.push_str(&format!(" š” MEDIUM: {}\n", report.medium_count));
161 }
162 if report.low_count > 0 {
163 output.push_str(&format!(" šµ LOW: {}\n", report.low_count));
164 }
165
166 output.push_str(&format!("\n{}\n", "-".repeat(80)));
167 output.push_str("Vulnerable Dependencies:\n\n");
168
169 for vuln_dep in &report.vulnerable_dependencies {
170 output.push_str(&format!(
171 "š¦ {} v{} ({})\n",
172 vuln_dep.name,
173 vuln_dep.version,
174 vuln_dep.language.as_str()
175 ));
176
177 for vuln in &vuln_dep.vulnerabilities {
178 let severity_str = match vuln.severity {
179 VulnerabilitySeverity::Critical => "CRITICAL",
180 VulnerabilitySeverity::High => "HIGH",
181 VulnerabilitySeverity::Medium => "MEDIUM",
182 VulnerabilitySeverity::Low => "LOW",
183 VulnerabilitySeverity::Info => "INFO",
184 };
185
186 output.push_str(&format!("\n ā ļø {} [{}]\n", vuln.id, severity_str));
187 output.push_str(&format!(" {}\n", vuln.title));
188
189 if !vuln.description.is_empty() && vuln.description != vuln.title {
190 let wrapped = textwrap::fill(&vuln.description, 70);
192 for line in wrapped.lines() {
193 output.push_str(&format!(" {}\n", line));
194 }
195 }
196
197 if let Some(ref cve) = vuln.cve {
198 output.push_str(&format!(" CVE: {}\n", cve));
199 }
200
201 if let Some(ref ghsa) = vuln.ghsa {
202 output.push_str(&format!(" GHSA: {}\n", ghsa));
203 }
204
205 output.push_str(&format!(" Affected: {}\n", vuln.affected_versions));
206
207 if let Some(ref patched) = vuln.patched_versions {
208 output.push_str(&format!(" ā
Fix: Upgrade to {}\n", patched));
209 }
210 }
211 output.push('\n');
212 }
213 } else {
214 output.push_str("\nā
No vulnerabilities found!\n");
215 }
216
217 output
218}