syncable_cli/analyzer/vulnerability/checkers/
rust.rs1use std::path::Path;
2use std::process::Command;
3use log::{info, warn};
4
5use crate::analyzer::dependency_parser::{DependencyInfo, Language};
6use crate::analyzer::tool_management::ToolDetector;
7use super::{LanguageVulnerabilityChecker};
8use crate::analyzer::vulnerability::{VulnerableDependency, VulnerabilityError, VulnerabilityInfo, VulnerabilitySeverity};
9
10pub struct RustVulnerabilityChecker {
11 tool_detector: ToolDetector,
12}
13
14impl RustVulnerabilityChecker {
15 pub fn new() -> Self {
16 Self {
17 tool_detector: ToolDetector::new(),
18 }
19 }
20}
21
22impl LanguageVulnerabilityChecker for RustVulnerabilityChecker {
23 fn check_vulnerabilities(
24 &self,
25 dependencies: &[DependencyInfo],
26 _project_path: &Path,
27 ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
28 info!("Checking Rust dependencies with cargo-audit");
29
30 let mut detector = ToolDetector::new();
32 let cargo_audit_status = detector.detect_tool("cargo-audit");
33
34 if !cargo_audit_status.available {
35 warn!("cargo-audit not installed. Install with: cargo install cargo-audit");
36 warn!("Skipping Rust vulnerability checks");
37 return Ok(vec![]);
38 }
39
40 info!("Using cargo-audit {} at {:?}",
41 cargo_audit_status.version.as_deref().unwrap_or("unknown"),
42 cargo_audit_status.path.as_deref().unwrap_or_else(|| std::path::Path::new("cargo-audit")));
43
44 let output = Command::new("cargo")
46 .args(&["audit", "--json"])
47 .output()
48 .map_err(|e| VulnerabilityError::CommandError(
49 format!("Failed to run cargo audit: {}", e)
50 ))?;
51
52 if output.stdout.is_empty() {
53 return Ok(vec![]);
54 }
55
56 let audit_data: serde_json::Value = serde_json::from_slice(&output.stdout)?;
58
59 self.parse_cargo_audit_output(&audit_data, dependencies)
60 }
61}
62
63impl RustVulnerabilityChecker {
64 fn parse_cargo_audit_output(
65 &self,
66 audit_data: &serde_json::Value,
67 dependencies: &[DependencyInfo],
68 ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
69 let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
70
71 if let Some(vulnerabilities) = audit_data.get("vulnerabilities").and_then(|v| v.get("list")).and_then(|l| l.as_array()) {
72 for vuln in vulnerabilities {
73 if let Some(advisory) = vuln.get("advisory") {
74 let package_name = advisory.get("package")
75 .and_then(|n| n.as_str())
76 .unwrap_or("");
77
78 let package_version = vuln.get("package")
79 .and_then(|p| p.get("version"))
80 .and_then(|v| v.as_str())
81 .unwrap_or("");
82
83 if let Some(dep) = dependencies.iter().find(|d| d.name == package_name) {
84 let vuln_info = VulnerabilityInfo {
85 id: advisory.get("id")
86 .and_then(|id| id.as_str())
87 .unwrap_or("unknown")
88 .to_string(),
89 severity: self.parse_rustsec_severity(
90 advisory.get("severity")
91 .and_then(|s| s.as_str())
92 ),
93 title: advisory.get("title")
94 .and_then(|t| t.as_str())
95 .unwrap_or("Unknown vulnerability")
96 .to_string(),
97 description: advisory.get("description")
98 .and_then(|d| d.as_str())
99 .unwrap_or("")
100 .to_string(),
101 cve: advisory.get("aliases")
102 .and_then(|a| a.as_array())
103 .and_then(|arr| arr.iter()
104 .filter_map(|v| v.as_str())
105 .find(|s| s.starts_with("CVE-"))
106 .map(|s| s.to_string())),
107 ghsa: advisory.get("aliases")
108 .and_then(|a| a.as_array())
109 .and_then(|arr| arr.iter()
110 .filter_map(|v| v.as_str())
111 .find(|s| s.starts_with("GHSA-"))
112 .map(|s| s.to_string())),
113 affected_versions: format!("< {}",
114 vuln.get("versions")
115 .and_then(|v| v.get("patched"))
116 .and_then(|p| p.as_array())
117 .and_then(|arr| arr.first())
118 .and_then(|s| s.as_str())
119 .unwrap_or("unknown")
120 ),
121 patched_versions: vuln.get("versions")
122 .and_then(|v| v.get("patched"))
123 .and_then(|p| p.as_array())
124 .and_then(|arr| arr.first())
125 .and_then(|s| s.as_str())
126 .map(|s| s.to_string()),
127 published_date: advisory.get("date")
128 .and_then(|d| d.as_str())
129 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
130 .map(|dt| dt.with_timezone(&chrono::Utc)),
131 references: advisory.get("references")
132 .and_then(|r| r.as_array())
133 .map(|refs| refs.iter()
134 .filter_map(|r| r.as_str().map(|s| s.to_string()))
135 .collect())
136 .unwrap_or_default(),
137 };
138
139 if let Some(existing) = vulnerable_deps.iter_mut()
141 .find(|vuln_dep: &&mut VulnerableDependency| vuln_dep.name == dep.name && vuln_dep.version == package_version)
142 {
143 existing.vulnerabilities.push(vuln_info);
144 } else {
145 vulnerable_deps.push(VulnerableDependency {
146 name: dep.name.clone(),
147 version: package_version.to_string(),
148 language: Language::Rust,
149 vulnerabilities: vec![vuln_info],
150 });
151 }
152 }
153 }
154 }
155 }
156
157 Ok(vulnerable_deps)
158 }
159
160 fn parse_rustsec_severity(&self, severity: Option<&str>) -> VulnerabilitySeverity {
161 match severity.map(|s| s.to_lowercase()).as_deref() {
162 Some("critical") => VulnerabilitySeverity::Critical,
163 Some("high") => VulnerabilitySeverity::High,
164 Some("medium") | Some("moderate") => VulnerabilitySeverity::Medium,
165 Some("low") => VulnerabilitySeverity::Low,
166 _ => VulnerabilitySeverity::Medium, }
168 }
169}