syncable_cli/handlers/
vulnerabilities.rs

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) -> 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    // Parse dependencies
21    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    // Check vulnerabilities
30    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    // Filter by severity if requested
44    let filtered_report = if let Some(threshold) = severity {
45        filter_vulnerabilities_by_severity(report, threshold)
46    } else {
47        report
48    };
49
50    // Format output
51    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    // Output results
59    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    // Exit with non-zero code if critical/high vulnerabilities found
67    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    // Recalculate counts
110    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                    // Wrap description
191                    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}