syncable_cli/handlers/
vulnerabilities.rs

1use crate::{
2    analyzer::{self, vulnerability_checker::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    // Parse dependencies
19    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    // Check vulnerabilities
27    let checker = analyzer::vulnerability_checker::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    // Filter by severity if requested
37    let filtered_report = if let Some(threshold) = severity {
38        filter_vulnerabilities_by_severity(report, threshold)
39    } else {
40        report
41    };
42    
43    // Format output
44    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    // Output results
50    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    // Exit with non-zero code if critical/high vulnerabilities found
58    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_checker::VulnerabilityReport,
67    threshold: SeverityThreshold,
68) -> analyzer::vulnerability_checker::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_checker::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    // Recalculate counts
100    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_checker::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                    // Wrap description
174                    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}