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