syncable_cli/analyzer/vulnerability/checkers/
rust.rs

1use 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        // Check if cargo-audit is installed
31        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        // Run cargo audit in JSON format
45        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        // Parse cargo audit output
57        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                        // Check if we already have this dependency
140                        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, // Default to medium if not specified
167        }
168    }
169}