use crate::{
analyzer::{self, vulnerability::VulnerabilitySeverity},
cli::{OutputFormat, SeverityThreshold},
};
use std::path::PathBuf;
pub async fn handle_vulnerabilities(
path: PathBuf,
severity: Option<SeverityThreshold>,
format: OutputFormat,
output: Option<PathBuf>,
quiet: bool,
) -> crate::Result<()> {
let project_path = path.canonicalize().unwrap_or_else(|_| path.clone());
if !quiet {
println!(
"🔍 Scanning for vulnerabilities in: {}",
project_path.display()
);
}
let parser = analyzer::dependency_parser::DependencyParser::new();
let project_dirs = parser.discover_project_dirs(&project_path);
let was_quiet = std::env::var("SYNCABLE_QUIET").is_ok();
if !was_quiet {
unsafe {
std::env::set_var("SYNCABLE_QUIET", "1");
}
}
let mut scannable_dirs = Vec::new();
for dir in &project_dirs {
let deps = parser.parse_deps_in_dir_standalone(dir)?;
if !deps.is_empty() {
let langs: Vec<String> = deps.keys().map(|l| format!("{:?}", l)).collect();
scannable_dirs.push((dir.clone(), deps, langs));
}
}
if !quiet && scannable_dirs.len() > 1 {
println!("\n📦 Found {} projects to scan\n", scannable_dirs.len());
}
let mut merged_vulnerable_deps = Vec::new();
let any_deps_found = !scannable_dirs.is_empty();
let total_dirs = scannable_dirs.len();
for (i, (dir, deps, langs)) in scannable_dirs.into_iter().enumerate() {
let dir_name = dir
.strip_prefix(&project_path)
.unwrap_or(&dir)
.display()
.to_string();
let dir_label = if dir_name.is_empty() || dir_name == "." {
".".to_string()
} else {
dir_name
};
if !quiet {
println!(
" [{}/{}] Scanning {} ({})",
i + 1,
total_dirs,
dir_label,
langs.join(", ")
);
}
let checker = analyzer::vulnerability::VulnerabilityChecker::new();
match checker.check_all_dependencies(&deps, &dir).await {
Ok(report) => {
let count = report
.vulnerable_dependencies
.iter()
.map(|d| d.vulnerabilities.len())
.sum::<usize>();
if !quiet && count > 0 {
println!(" ⚠️ {} vulnerabilities found", count);
}
for mut dep in report.vulnerable_dependencies {
dep.source_dir = Some(dir_label.clone());
merged_vulnerable_deps.push(dep);
}
}
Err(e) => {
if !quiet {
eprintln!(" ⚠️ scan failed: {}", e);
}
}
}
}
if !was_quiet {
unsafe {
std::env::remove_var("SYNCABLE_QUIET");
}
}
if !any_deps_found {
if !quiet {
println!("No dependencies found to check.");
}
return Ok(());
}
merged_vulnerable_deps.sort_by(|a, b| a.name.cmp(&b.name));
merged_vulnerable_deps.dedup_by(|a, b| a.name == b.name && a.version == b.version);
let mut critical_count = 0;
let mut high_count = 0;
let mut medium_count = 0;
let mut low_count = 0;
let mut total_vulnerabilities = 0;
for dep in &merged_vulnerable_deps {
for vuln in &dep.vulnerabilities {
total_vulnerabilities += 1;
match vuln.severity {
VulnerabilitySeverity::Critical => critical_count += 1,
VulnerabilitySeverity::High => high_count += 1,
VulnerabilitySeverity::Medium => medium_count += 1,
VulnerabilitySeverity::Low => low_count += 1,
VulnerabilitySeverity::Info => {}
}
}
}
let report = analyzer::vulnerability::VulnerabilityReport {
checked_at: chrono::Utc::now(),
total_vulnerabilities,
critical_count,
high_count,
medium_count,
low_count,
vulnerable_dependencies: merged_vulnerable_deps,
};
let filtered_report = if let Some(threshold) = severity {
filter_vulnerabilities_by_severity(report, threshold)
} else {
report
};
let output_string = match format {
OutputFormat::Table => {
format_vulnerabilities_table(&filtered_report, &severity, &project_path)
}
OutputFormat::Json => serde_json::to_string_pretty(&filtered_report)?,
};
if let Some(output_path) = output {
std::fs::write(&output_path, output_string)?;
if !quiet {
println!("Report saved to: {}", output_path.display());
}
} else if !quiet {
println!("{}", output_string);
}
if !quiet && (filtered_report.critical_count > 0 || filtered_report.high_count > 0) {
std::process::exit(1);
}
Ok(())
}
fn filter_vulnerabilities_by_severity(
report: analyzer::vulnerability::VulnerabilityReport,
threshold: SeverityThreshold,
) -> analyzer::vulnerability::VulnerabilityReport {
let min_severity = match threshold {
SeverityThreshold::Low => VulnerabilitySeverity::Low,
SeverityThreshold::Medium => VulnerabilitySeverity::Medium,
SeverityThreshold::High => VulnerabilitySeverity::High,
SeverityThreshold::Critical => VulnerabilitySeverity::Critical,
};
let filtered_deps: Vec<_> = report
.vulnerable_dependencies
.into_iter()
.filter_map(|mut dep| {
dep.vulnerabilities.retain(|v| v.severity >= min_severity);
if dep.vulnerabilities.is_empty() {
None
} else {
Some(dep)
}
})
.collect();
use analyzer::vulnerability::VulnerabilityReport;
let mut filtered = VulnerabilityReport {
checked_at: report.checked_at,
total_vulnerabilities: 0,
critical_count: 0,
high_count: 0,
medium_count: 0,
low_count: 0,
vulnerable_dependencies: filtered_deps,
};
for dep in &filtered.vulnerable_dependencies {
for vuln in &dep.vulnerabilities {
filtered.total_vulnerabilities += 1;
match vuln.severity {
VulnerabilitySeverity::Critical => filtered.critical_count += 1,
VulnerabilitySeverity::High => filtered.high_count += 1,
VulnerabilitySeverity::Medium => filtered.medium_count += 1,
VulnerabilitySeverity::Low => filtered.low_count += 1,
VulnerabilitySeverity::Info => {}
}
}
}
filtered
}
fn format_vulnerabilities_table(
report: &analyzer::vulnerability::VulnerabilityReport,
severity: &Option<SeverityThreshold>,
project_path: &std::path::Path,
) -> String {
let mut output = String::new();
output.push_str("\n🛡️ Vulnerability Scan Report\n");
output.push_str(&format!("{}\n", "=".repeat(80)));
output.push_str(&format!(
"Scanned at: {}\n",
report.checked_at.format("%Y-%m-%d %H:%M:%S UTC")
));
output.push_str(&format!("Path: {}\n", project_path.display()));
if let Some(threshold) = severity {
output.push_str(&format!("Severity filter: >= {:?}\n", threshold));
}
output.push_str("\nSummary:\n");
output.push_str(&format!(
"Total vulnerabilities: {}\n",
report.total_vulnerabilities
));
if report.total_vulnerabilities > 0 {
output.push_str("\nBy Severity:\n");
if report.critical_count > 0 {
output.push_str(&format!(" 🔴 CRITICAL: {}\n", report.critical_count));
}
if report.high_count > 0 {
output.push_str(&format!(" 🔴 HIGH: {}\n", report.high_count));
}
if report.medium_count > 0 {
output.push_str(&format!(" 🟡 MEDIUM: {}\n", report.medium_count));
}
if report.low_count > 0 {
output.push_str(&format!(" 🔵 LOW: {}\n", report.low_count));
}
output.push_str(&format!("\n{}\n", "-".repeat(80)));
output.push_str("Vulnerable Dependencies:\n\n");
for vuln_dep in &report.vulnerable_dependencies {
let source = vuln_dep.source_dir.as_deref().unwrap_or(".");
output.push_str(&format!(
"📦 {} v{} ({}) [{}]\n",
vuln_dep.name,
vuln_dep.version,
vuln_dep.language.as_str(),
source,
));
for vuln in &vuln_dep.vulnerabilities {
let severity_str = match vuln.severity {
VulnerabilitySeverity::Critical => "CRITICAL",
VulnerabilitySeverity::High => "HIGH",
VulnerabilitySeverity::Medium => "MEDIUM",
VulnerabilitySeverity::Low => "LOW",
VulnerabilitySeverity::Info => "INFO",
};
output.push_str(&format!("\n ⚠️ {} [{}]\n", vuln.id, severity_str));
output.push_str(&format!(" {}\n", vuln.title));
if !vuln.description.is_empty() && vuln.description != vuln.title {
let wrapped = textwrap::fill(&vuln.description, 70);
for line in wrapped.lines() {
output.push_str(&format!(" {}\n", line));
}
}
if let Some(ref cve) = vuln.cve {
output.push_str(&format!(" CVE: {}\n", cve));
}
if let Some(ref ghsa) = vuln.ghsa {
output.push_str(&format!(" GHSA: {}\n", ghsa));
}
output.push_str(&format!(" Affected: {}\n", vuln.affected_versions));
if let Some(ref patched) = vuln.patched_versions {
output.push_str(&format!(" ✅ Fix: Upgrade to {}\n", patched));
}
}
output.push('\n');
}
} else {
output.push_str("\n✅ No vulnerabilities found!\n");
}
output
}