use log::{info, warn};
use std::path::Path;
use std::process::Command;
use super::LanguageVulnerabilityChecker;
use crate::analyzer::dependency_parser::{DependencyInfo, Language};
use crate::analyzer::tool_management::ToolDetector;
use crate::analyzer::vulnerability::{
VulnerabilityError, VulnerabilityInfo, VulnerabilitySeverity, VulnerableDependency,
};
pub struct RustVulnerabilityChecker;
impl Default for RustVulnerabilityChecker {
fn default() -> Self {
Self
}
}
impl RustVulnerabilityChecker {
pub fn new() -> Self {
Self
}
}
impl LanguageVulnerabilityChecker for RustVulnerabilityChecker {
fn check_vulnerabilities(
&self,
dependencies: &[DependencyInfo],
_project_path: &Path,
) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
info!("Checking Rust dependencies with cargo-audit");
let mut detector = ToolDetector::new();
let cargo_audit_status = detector.detect_tool("cargo-audit");
if !cargo_audit_status.available {
warn!("cargo-audit not installed. Install with: cargo install cargo-audit");
warn!("Skipping Rust vulnerability checks");
return Ok(vec![]);
}
info!(
"Using cargo-audit {} at {:?}",
cargo_audit_status.version.as_deref().unwrap_or("unknown"),
cargo_audit_status
.path
.as_deref()
.unwrap_or_else(|| std::path::Path::new("cargo-audit"))
);
let output = Command::new("cargo")
.args(["audit", "--json"])
.output()
.map_err(|e| {
VulnerabilityError::CommandError(format!("Failed to run cargo audit: {}", e))
})?;
if output.stdout.is_empty() {
return Ok(vec![]);
}
let audit_data: serde_json::Value = serde_json::from_slice(&output.stdout)?;
self.parse_cargo_audit_output(&audit_data, dependencies)
}
}
impl RustVulnerabilityChecker {
pub fn parse_cargo_audit_output(
&self,
audit_data: &serde_json::Value,
dependencies: &[DependencyInfo],
) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
if let Some(vulnerabilities) = audit_data
.get("vulnerabilities")
.and_then(|v| v.get("list"))
.and_then(|l| l.as_array())
{
self.parse_cargo_audit_vulnerabilities(
vulnerabilities,
dependencies,
&mut vulnerable_deps,
)?;
}
if let Some(warnings) = audit_data.get("warnings") {
if let Some(unmaintained) = warnings.get("unmaintained").and_then(|w| w.as_array()) {
self.parse_cargo_audit_warnings(unmaintained, dependencies, &mut vulnerable_deps)?;
}
if let Some(yanked) = warnings.get("yanked").and_then(|w| w.as_array()) {
self.parse_cargo_audit_warnings(yanked, dependencies, &mut vulnerable_deps)?;
}
}
Ok(vulnerable_deps)
}
pub fn parse_cargo_audit_vulnerabilities(
&self,
vulnerabilities: &Vec<serde_json::Value>,
dependencies: &[DependencyInfo],
vulnerable_deps: &mut Vec<VulnerableDependency>,
) -> Result<(), VulnerabilityError> {
for vuln in vulnerabilities {
if let Some(advisory) = vuln.get("advisory") {
let package_name = advisory
.get("package")
.and_then(|n| n.as_str())
.unwrap_or("");
let package_version = vuln
.get("package")
.and_then(|p| p.get("version"))
.and_then(|v| v.as_str())
.unwrap_or("");
if let Some(dep) = dependencies.iter().find(|d| d.name == package_name) {
let vuln_info =
VulnerabilityInfo {
id: advisory
.get("id")
.and_then(|id| id.as_str())
.unwrap_or("unknown")
.to_string(),
vuln_type: "security".to_string(), severity: self.parse_rustsec_severity(
advisory.get("severity").and_then(|s| s.as_str()),
),
title: advisory
.get("title")
.and_then(|t| t.as_str())
.unwrap_or("Unknown vulnerability")
.to_string(),
description: advisory
.get("description")
.and_then(|d| d.as_str())
.unwrap_or("")
.to_string(),
cve: advisory.get("aliases").and_then(|a| a.as_array()).and_then(
|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.find(|s| s.starts_with("CVE-"))
.map(|s| s.to_string())
},
),
ghsa: advisory.get("aliases").and_then(|a| a.as_array()).and_then(
|arr| {
arr.iter()
.filter_map(|v| v.as_str())
.find(|s| s.starts_with("GHSA-"))
.map(|s| s.to_string())
},
),
affected_versions: format!(
"< {}",
vuln.get("versions")
.and_then(|v| v.get("patched"))
.and_then(|p| p.as_array())
.and_then(|arr| arr.first())
.and_then(|s| s.as_str())
.unwrap_or("unknown")
),
patched_versions: vuln
.get("versions")
.and_then(|v| v.get("patched"))
.and_then(|p| p.as_array())
.and_then(|arr| arr.first())
.and_then(|s| s.as_str())
.map(|s| s.to_string()),
published_date: advisory
.get("date")
.and_then(|d| d.as_str())
.and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
.map(|dt| dt.with_timezone(&chrono::Utc)),
references: advisory
.get("references")
.and_then(|r| r.as_array())
.map(|refs| {
refs.iter()
.filter_map(|r| r.as_str().map(|s| s.to_string()))
.collect()
})
.unwrap_or_default(),
};
if let Some(existing) =
vulnerable_deps
.iter_mut()
.find(|vuln_dep: &&mut VulnerableDependency| {
vuln_dep.name == dep.name && vuln_dep.version == package_version
})
{
existing.vulnerabilities.push(vuln_info);
} else {
vulnerable_deps.push(VulnerableDependency {
name: dep.name.clone(),
version: package_version.to_string(),
language: Language::Rust,
vulnerabilities: vec![vuln_info],
source_dir: None,
});
}
}
}
}
Ok(())
}
pub fn parse_cargo_audit_warnings(
&self,
warnings: &Vec<serde_json::Value>,
dependencies: &[DependencyInfo],
vulnerable_deps: &mut Vec<VulnerableDependency>,
) -> Result<(), VulnerabilityError> {
for warning in warnings {
let kind = warning.get("kind").and_then(|k| k.as_str()).unwrap_or("");
let (package_name, package_version) = if let Some(package_obj) = warning.get("package")
{
(
package_obj
.get("name")
.and_then(|n| n.as_str())
.unwrap_or("")
.to_string(),
package_obj
.get("version")
.and_then(|v| v.as_str())
.unwrap_or("")
.to_string(),
)
} else {
("".to_string(), "".to_string())
};
if (kind == "unmaintained" || kind == "yanked")
&& let Some(dep) = dependencies.iter().find(|d| d.name == package_name)
{
let (severity, title, description) = match kind {
"unmaintained" => (
VulnerabilitySeverity::Low,
format!("Unmaintained package: {}", package_name),
warning
.get("advisory")
.and_then(|a| a.get("description"))
.and_then(|d| d.as_str())
.unwrap_or("Package is unmaintained")
.to_string(),
),
"yanked" => (
VulnerabilitySeverity::Medium,
format!("Yanked package: {}", package_name),
"Package version has been yanked".to_string(),
),
_ => continue, };
let vuln_info = VulnerabilityInfo {
id: format!("{}-{}", kind, package_name),
vuln_type: kind.to_string(), severity,
title,
description,
cve: None,
ghsa: None,
affected_versions: package_version.to_string(),
patched_versions: None,
published_date: None,
references: vec![],
};
if let Some(existing) =
vulnerable_deps
.iter_mut()
.find(|vuln_dep: &&mut VulnerableDependency| {
vuln_dep.name == dep.name && vuln_dep.version == package_version
})
{
existing.vulnerabilities.push(vuln_info);
} else {
vulnerable_deps.push(VulnerableDependency {
name: dep.name.clone(),
version: package_version.to_string(),
language: Language::Rust,
vulnerabilities: vec![vuln_info],
source_dir: None,
});
}
}
}
Ok(())
}
fn parse_rustsec_severity(&self, severity: Option<&str>) -> VulnerabilitySeverity {
match severity.map(|s| s.to_lowercase()).as_deref() {
Some("critical") => VulnerabilitySeverity::Critical,
Some("high") => VulnerabilitySeverity::High,
Some("medium") | Some("moderate") => VulnerabilitySeverity::Medium,
Some("low") => VulnerabilitySeverity::Low,
_ => VulnerabilitySeverity::Medium, }
}
}