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