syncable_cli/analyzer/vulnerability/checkers/
rust.rs1use log::{info, warn};
2use std::path::Path;
3use std::process::Command;
4
5use super::LanguageVulnerabilityChecker;
6use crate::analyzer::dependency_parser::{DependencyInfo, Language};
7use crate::analyzer::tool_management::ToolDetector;
8use crate::analyzer::vulnerability::{
9 VulnerabilityError, VulnerabilityInfo, VulnerabilitySeverity, VulnerableDependency,
10};
11
12pub struct RustVulnerabilityChecker;
13
14impl RustVulnerabilityChecker {
15 pub fn new() -> Self {
16 Self
17 }
18}
19
20impl LanguageVulnerabilityChecker for RustVulnerabilityChecker {
21 fn check_vulnerabilities(
22 &self,
23 dependencies: &[DependencyInfo],
24 _project_path: &Path,
25 ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
26 info!("Checking Rust dependencies with cargo-audit");
27
28 let mut detector = ToolDetector::new();
30 let cargo_audit_status = detector.detect_tool("cargo-audit");
31
32 if !cargo_audit_status.available {
33 warn!("cargo-audit not installed. Install with: cargo install cargo-audit");
34 warn!("Skipping Rust vulnerability checks");
35 return Ok(vec![]);
36 }
37
38 info!(
39 "Using cargo-audit {} at {:?}",
40 cargo_audit_status.version.as_deref().unwrap_or("unknown"),
41 cargo_audit_status
42 .path
43 .as_deref()
44 .unwrap_or_else(|| std::path::Path::new("cargo-audit"))
45 );
46
47 let output = Command::new("cargo")
49 .args(&["audit", "--json"])
50 .output()
51 .map_err(|e| {
52 VulnerabilityError::CommandError(format!("Failed to run cargo audit: {}", e))
53 })?;
54
55 if output.stdout.is_empty() {
56 return Ok(vec![]);
57 }
58
59 let audit_data: serde_json::Value = serde_json::from_slice(&output.stdout)?;
61
62 self.parse_cargo_audit_output(&audit_data, dependencies)
63 }
64}
65
66impl RustVulnerabilityChecker {
67 pub fn parse_cargo_audit_output(
69 &self,
70 audit_data: &serde_json::Value,
71 dependencies: &[DependencyInfo],
72 ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
73 let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
74
75 if let Some(vulnerabilities) = audit_data
77 .get("vulnerabilities")
78 .and_then(|v| v.get("list"))
79 .and_then(|l| l.as_array())
80 {
81 self.parse_cargo_audit_vulnerabilities(
82 &vulnerabilities,
83 dependencies,
84 &mut vulnerable_deps,
85 )?;
86 }
87
88 if let Some(warnings) = audit_data.get("warnings") {
90 if let Some(unmaintained) = warnings.get("unmaintained").and_then(|w| w.as_array()) {
92 self.parse_cargo_audit_warnings(&unmaintained, dependencies, &mut vulnerable_deps)?;
93 }
94
95 if let Some(yanked) = warnings.get("yanked").and_then(|w| w.as_array()) {
97 self.parse_cargo_audit_warnings(&yanked, dependencies, &mut vulnerable_deps)?;
98 }
99 }
100
101 Ok(vulnerable_deps)
102 }
103
104 pub fn parse_cargo_audit_vulnerabilities(
106 &self,
107 vulnerabilities: &Vec<serde_json::Value>,
108 dependencies: &[DependencyInfo],
109 vulnerable_deps: &mut Vec<VulnerableDependency>,
110 ) -> Result<(), VulnerabilityError> {
111 for vuln in vulnerabilities {
112 if let Some(advisory) = vuln.get("advisory") {
113 let package_name = advisory
114 .get("package")
115 .and_then(|n| n.as_str())
116 .unwrap_or("");
117
118 let package_version = vuln
119 .get("package")
120 .and_then(|p| p.get("version"))
121 .and_then(|v| v.as_str())
122 .unwrap_or("");
123
124 if let Some(dep) = dependencies.iter().find(|d| d.name == package_name) {
125 let vuln_info =
126 VulnerabilityInfo {
127 id: advisory
128 .get("id")
129 .and_then(|id| id.as_str())
130 .unwrap_or("unknown")
131 .to_string(),
132 vuln_type: "security".to_string(), severity: self.parse_rustsec_severity(
134 advisory.get("severity").and_then(|s| s.as_str()),
135 ),
136 title: advisory
137 .get("title")
138 .and_then(|t| t.as_str())
139 .unwrap_or("Unknown vulnerability")
140 .to_string(),
141 description: advisory
142 .get("description")
143 .and_then(|d| d.as_str())
144 .unwrap_or("")
145 .to_string(),
146 cve: advisory.get("aliases").and_then(|a| a.as_array()).and_then(
147 |arr| {
148 arr.iter()
149 .filter_map(|v| v.as_str())
150 .find(|s| s.starts_with("CVE-"))
151 .map(|s| s.to_string())
152 },
153 ),
154 ghsa: advisory.get("aliases").and_then(|a| a.as_array()).and_then(
155 |arr| {
156 arr.iter()
157 .filter_map(|v| v.as_str())
158 .find(|s| s.starts_with("GHSA-"))
159 .map(|s| s.to_string())
160 },
161 ),
162 affected_versions: format!(
163 "< {}",
164 vuln.get("versions")
165 .and_then(|v| v.get("patched"))
166 .and_then(|p| p.as_array())
167 .and_then(|arr| arr.first())
168 .and_then(|s| s.as_str())
169 .unwrap_or("unknown")
170 ),
171 patched_versions: vuln
172 .get("versions")
173 .and_then(|v| v.get("patched"))
174 .and_then(|p| p.as_array())
175 .and_then(|arr| arr.first())
176 .and_then(|s| s.as_str())
177 .map(|s| s.to_string()),
178 published_date: advisory
179 .get("date")
180 .and_then(|d| d.as_str())
181 .and_then(|s| chrono::DateTime::parse_from_rfc3339(s).ok())
182 .map(|dt| dt.with_timezone(&chrono::Utc)),
183 references: advisory
184 .get("references")
185 .and_then(|r| r.as_array())
186 .map(|refs| {
187 refs.iter()
188 .filter_map(|r| r.as_str().map(|s| s.to_string()))
189 .collect()
190 })
191 .unwrap_or_default(),
192 };
193
194 if let Some(existing) =
196 vulnerable_deps
197 .iter_mut()
198 .find(|vuln_dep: &&mut VulnerableDependency| {
199 vuln_dep.name == dep.name && vuln_dep.version == package_version
200 })
201 {
202 existing.vulnerabilities.push(vuln_info);
203 } else {
204 vulnerable_deps.push(VulnerableDependency {
205 name: dep.name.clone(),
206 version: package_version.to_string(),
207 language: Language::Rust,
208 vulnerabilities: vec![vuln_info],
209 });
210 }
211 }
212 }
213 }
214
215 Ok(())
216 }
217
218 pub fn parse_cargo_audit_warnings(
220 &self,
221 warnings: &Vec<serde_json::Value>,
222 dependencies: &[DependencyInfo],
223 vulnerable_deps: &mut Vec<VulnerableDependency>,
224 ) -> Result<(), VulnerabilityError> {
225 for warning in warnings {
226 let kind = warning.get("kind").and_then(|k| k.as_str()).unwrap_or("");
227
228 let (package_name, package_version) = if let Some(package_obj) = warning.get("package")
230 {
231 (
232 package_obj
233 .get("name")
234 .and_then(|n| n.as_str())
235 .unwrap_or("")
236 .to_string(),
237 package_obj
238 .get("version")
239 .and_then(|v| v.as_str())
240 .unwrap_or("")
241 .to_string(),
242 )
243 } else {
244 ("".to_string(), "".to_string())
245 };
246
247 if kind == "unmaintained" || kind == "yanked" {
249 if let Some(dep) = dependencies.iter().find(|d| d.name == package_name) {
250 let (severity, title, description) = match kind {
251 "unmaintained" => (
252 VulnerabilitySeverity::Low,
253 format!("Unmaintained package: {}", package_name),
254 warning
255 .get("advisory")
256 .and_then(|a| a.get("description"))
257 .and_then(|d| d.as_str())
258 .unwrap_or("Package is unmaintained")
259 .to_string(),
260 ),
261 "yanked" => (
262 VulnerabilitySeverity::Medium,
263 format!("Yanked package: {}", package_name),
264 "Package version has been yanked".to_string(),
265 ),
266 _ => continue, };
268
269 let vuln_info = VulnerabilityInfo {
270 id: format!("{}-{}", kind, package_name),
271 vuln_type: kind.to_string(), severity,
273 title,
274 description,
275 cve: None,
276 ghsa: None,
277 affected_versions: package_version.to_string(),
278 patched_versions: None,
279 published_date: None,
280 references: vec![],
281 };
282
283 if let Some(existing) =
285 vulnerable_deps
286 .iter_mut()
287 .find(|vuln_dep: &&mut VulnerableDependency| {
288 vuln_dep.name == dep.name && vuln_dep.version == package_version
289 })
290 {
291 existing.vulnerabilities.push(vuln_info);
292 } else {
293 vulnerable_deps.push(VulnerableDependency {
294 name: dep.name.clone(),
295 version: package_version.to_string(),
296 language: Language::Rust,
297 vulnerabilities: vec![vuln_info],
298 });
299 }
300 }
301 }
302 }
303
304 Ok(())
305 }
306
307 fn parse_rustsec_severity(&self, severity: Option<&str>) -> VulnerabilitySeverity {
308 match severity.map(|s| s.to_lowercase()).as_deref() {
309 Some("critical") => VulnerabilitySeverity::Critical,
310 Some("high") => VulnerabilitySeverity::High,
311 Some("medium") | Some("moderate") => VulnerabilitySeverity::Medium,
312 Some("low") => VulnerabilitySeverity::Low,
313 _ => VulnerabilitySeverity::Medium, }
315 }
316}