use crate::{Error, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AuditReport {
pub vulnerabilities: Vec<Vulnerability>,
pub dependencies_count: usize,
pub passed: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Vulnerability {
pub package: String,
pub version: String,
pub severity: String,
pub title: String,
pub description: String,
pub cve: Option<String>,
pub cvss: Option<f32>,
}
impl AuditReport {
pub fn report(&self) -> String {
let mut report = String::new();
if self.passed {
report.push_str("✅ Security audit passed - No vulnerabilities found!\n");
report.push_str(&format!(
" Scanned {} dependencies\n",
self.dependencies_count
));
} else {
report.push_str(&format!(
"🚨 Security audit failed - Found {} vulnerabilities\n\n",
self.vulnerabilities.len()
));
for vuln in &self.vulnerabilities {
let severity_emoji = match vuln.severity.to_lowercase().as_str() {
"critical" => "🔴",
"high" => "🟠",
"medium" => "🟡",
"low" => "🟢",
_ => "⚪",
};
report.push_str(&format!(
"{} {} [{}] in {} v{}\n",
severity_emoji,
vuln.severity.to_uppercase(),
vuln.cve.as_ref().unwrap_or(&"N/A".to_string()),
vuln.package,
vuln.version
));
report.push_str(&format!(" {}\n", vuln.title));
report.push_str(&format!(" {}\n", vuln.description));
if let Some(cvss) = vuln.cvss {
report.push_str(&format!(" CVSS Score: {:.1}\n", cvss));
}
report.push('\n');
}
}
report
}
}
pub async fn run_security_audit(project_path: &Path) -> Result<AuditReport> {
ensure_cargo_audit_installed().await?;
let output = tokio::process::Command::new("cargo")
.args(&["audit", "--json"])
.current_dir(project_path)
.output()
.await
.map_err(|e| Error::process(format!("Failed to run cargo audit: {}", e)))?;
parse_audit_output(&output.stdout)
}
async fn ensure_cargo_audit_installed() -> Result<()> {
let check = tokio::process::Command::new("cargo")
.args(&["audit", "--version"])
.output()
.await;
if check
.as_ref()
.map_or(true, |output| !output.status.success())
{
println!("📦 Installing cargo-audit for security scanning...");
let install = tokio::process::Command::new("cargo")
.args(&["install", "cargo-audit", "--locked"])
.output()
.await
.map_err(|e| Error::process(format!("Failed to install cargo-audit: {}", e)))?;
if !install.status.success() {
return Err(Error::process("Failed to install cargo-audit"));
}
println!("✅ cargo-audit installed successfully");
}
Ok(())
}
fn parse_audit_output(output: &[u8]) -> Result<AuditReport> {
let output_str = String::from_utf8_lossy(output);
if let Ok(json) = serde_json::from_str::<serde_json::Value>(&output_str) {
parse_json_audit_output(&json)
} else {
parse_text_audit_output(&output_str)
}
}
fn parse_json_audit_output(json: &serde_json::Value) -> Result<AuditReport> {
let vulnerabilities = extract_vulnerabilities_from_json(json);
let dependencies_count = json["dependencies"]["count"].as_u64().unwrap_or(0) as usize;
Ok(AuditReport {
passed: vulnerabilities.is_empty(),
vulnerabilities,
dependencies_count,
})
}
fn extract_vulnerabilities_from_json(json: &serde_json::Value) -> Vec<Vulnerability> {
let mut vulnerabilities = Vec::new();
if let Some(vulns) = json["vulnerabilities"]["list"].as_array() {
for vuln in vulns {
if let Some(advisory) = vuln["advisory"].as_object() {
vulnerabilities.push(create_vulnerability_from_json(vuln, advisory));
}
}
}
vulnerabilities
}
fn create_vulnerability_from_json(
vuln: &serde_json::Value,
advisory: &serde_json::Map<String, serde_json::Value>,
) -> Vulnerability {
Vulnerability {
package: vuln["package"]["name"]
.as_str()
.unwrap_or("unknown")
.to_string(),
version: vuln["package"]["version"]
.as_str()
.unwrap_or("unknown")
.to_string(),
severity: advisory["severity"]
.as_str()
.unwrap_or("unknown")
.to_string(),
title: advisory["title"]
.as_str()
.unwrap_or("Security vulnerability")
.to_string(),
description: advisory["description"]
.as_str()
.unwrap_or("No description available")
.to_string(),
cve: advisory["id"].as_str().map(String::from),
cvss: advisory["cvss"].as_f64().map(|v| v as f32),
}
}
fn parse_text_audit_output(output_str: &str) -> Result<AuditReport> {
if output_str.contains("0 vulnerabilities") || output_str.contains("Success") {
Ok(AuditReport {
vulnerabilities: vec![],
dependencies_count: 0,
passed: true,
})
} else {
let vuln_count = if output_str.contains("vulnerability") {
1
} else {
0
};
Ok(AuditReport {
vulnerabilities: vec![],
dependencies_count: 0,
passed: vuln_count == 0,
})
}
}
pub async fn quick_security_check(project_path: &Path) -> Result<bool> {
let cargo_lock = project_path.join("Cargo.lock");
if !cargo_lock.exists() {
return Ok(true); }
match run_security_audit(project_path).await {
Ok(report) => Ok(report.passed),
Err(_) => Ok(true), }
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
#[test]
fn test_vulnerability_severity_classification() {
let vuln = Vulnerability {
package: "test".to_string(),
version: "1.0.0".to_string(),
severity: "critical".to_string(),
title: "Test vulnerability".to_string(),
description: "Test description".to_string(),
cve: Some("CVE-2024-0001".to_string()),
cvss: Some(9.5),
};
assert_eq!(vuln.severity, "critical");
assert!(vuln.cvss.unwrap_or(0.0) > 9.0);
}
#[test]
fn test_audit_report_passed() {
let report = AuditReport {
vulnerabilities: vec![],
dependencies_count: 10,
passed: true,
};
assert!(report.passed);
assert!(report.vulnerabilities.is_empty());
}
}