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