syncable_cli/analyzer/vulnerability/checkers/
java.rs

1use std::path::Path;
2use std::process::Command;
3use log::{info, warn};
4use crate::analyzer::dependency_parser::DependencyInfo;
5use crate::analyzer::tool_management::ToolDetector;
6use crate::analyzer::vulnerability::{VulnerableDependency, VulnerabilityError, VulnerabilityInfo, VulnerabilitySeverity};
7use super::MutableLanguageVulnerabilityChecker;
8
9pub struct JavaVulnerabilityChecker {
10    tool_detector: ToolDetector,
11}
12
13impl JavaVulnerabilityChecker {
14    pub fn new() -> Self {
15        Self {
16            tool_detector: ToolDetector::new(),
17        }
18    }
19    
20    fn execute_owasp_dependency_check(
21        &mut self,
22        project_path: &Path,
23        dependencies: &[DependencyInfo],
24    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
25        // Check if dependency-check is available
26        let depcheck_status = self.tool_detector.detect_tool("dependency-check");
27        if !depcheck_status.available {
28            warn!("dependency-check not found, skipping Java vulnerability check. Install OWASP Dependency-Check CLI.");
29            return Ok(None);
30        }
31        
32        info!("Executing OWASP Dependency-Check in {}", project_path.display());
33        
34        // Execute dependency-check --format JSON --scan .
35        let output = Command::new("dependency-check")
36            .args(&["--format", "JSON", "--scan", ".", "--out", "dependency-check-report.json"])
37            .current_dir(project_path)
38            .output()
39            .map_err(|e| VulnerabilityError::CommandError(
40                format!("Failed to run dependency-check: {}", e)
41            ))?;
42        
43        // Check if command succeeded
44        if !output.status.success() {
45            return Err(VulnerabilityError::CommandError(
46                format!("dependency-check failed with exit code {}: {}", 
47                    output.status.code().unwrap_or(-1),
48                    String::from_utf8_lossy(&output.stderr))
49            ));
50        }
51        
52        // Read the generated report file
53        let report_path = project_path.join("dependency-check-report.json");
54        if !report_path.exists() {
55            return Ok(None);
56        }
57        
58        let report_content = std::fs::read_to_string(&report_path)
59            .map_err(|e| VulnerabilityError::Io(e))?;
60        
61        let audit_data: serde_json::Value = serde_json::from_str(&report_content)
62            .map_err(|e| VulnerabilityError::ParseError(
63                format!("Failed to parse dependency-check output: {}", e)
64            ))?;
65        
66        // Clean up the report file
67        let _ = std::fs::remove_file(&report_path);
68        
69        self.parse_dependency_check_output(&audit_data, dependencies)
70    }
71    
72    fn parse_dependency_check_output(
73        &self,
74        audit_data: &serde_json::Value,
75        dependencies: &[DependencyInfo],
76    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
77        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
78        
79        // OWASP Dependency-Check JSON structure parsing
80        if let Some(dependencies_array) = audit_data
81            .get("dependencies")
82            .and_then(|d| d.as_array()) {
83            
84            for dependency in dependencies_array {
85                if let Some(dep_obj) = dependency.as_object() {
86                    let file_path = dep_obj.get("filePath").and_then(|f| f.as_str())
87                        .unwrap_or("").to_string();
88                    
89                    // Extract package name from file path or identifiers
90                    let package_name = if let Some(identifiers) = dep_obj.get("identifiers").and_then(|i| i.as_array()) {
91                        identifiers.iter()
92                            .filter_map(|id| id.as_object())
93                            .find_map(|id_obj| {
94                                if let Some(type_field) = id_obj.get("type").and_then(|t| t.as_str()) {
95                                    if type_field == "maven" || type_field == "gradle" {
96                                        return id_obj.get("name").and_then(|n| n.as_str()).map(|s| s.to_string());
97                                    }
98                                }
99                                None
100                            })
101                            .unwrap_or_else(|| {
102                                // Fallback to file name without extension
103                                std::path::Path::new(&file_path)
104                                    .file_stem()
105                                    .and_then(|s| s.to_str())
106                                    .unwrap_or("")
107                                    .to_string()
108                            })
109                    } else {
110                        // Fallback to file name without extension
111                        std::path::Path::new(&file_path)
112                            .file_stem()
113                            .and_then(|s| s.to_str())
114                            .unwrap_or("")
115                            .to_string()
116                    };
117                    
118                    // Find matching dependency
119                    if let Some(dep) = dependencies.iter().find(|d| 
120                        d.name.contains(&package_name) || package_name.contains(&d.name)) {
121                        
122                        // Check for vulnerabilities
123                        if let Some(vulnerabilities) = dep_obj.get("vulnerabilities").and_then(|v| v.as_array()) {
124                            let mut package_vulns = Vec::new();
125                            
126                            for vulnerability in vulnerabilities {
127                                if let Some(vuln_obj) = vulnerability.as_object() {
128                                    let vuln_id = vuln_obj.get("name").and_then(|n| n.as_str())
129                                        .unwrap_or("unknown").to_string();
130                                    let title = vuln_obj.get("title").and_then(|t| t.as_str())
131                                        .unwrap_or("Unknown vulnerability").to_string();
132                                    let description = vuln_obj.get("description").and_then(|d| d.as_str())
133                                        .unwrap_or("").to_string();
134                                    let severity = self.parse_severity(vuln_obj.get("severity").and_then(|s| s.as_str()));
135                                    
136                                    let _cvss_score = vuln_obj.get("cvssScore").and_then(|s| s.as_f64());
137                                    let _cvss_vector = vuln_obj.get("cvssVector").and_then(|v| v.as_str())
138                                        .map(|s| s.to_string());
139                                    
140                                    let cve = if vuln_id.starts_with("CVE-") {
141                                        Some(vuln_id.clone())
142                                    } else {
143                                        None
144                                    };
145                                    
146                                    let references = if let Some(refs) = vuln_obj.get("references").and_then(|r| r.as_array()) {
147                                        refs.iter()
148                                            .filter_map(|r| r.as_object())
149                                            .filter_map(|r_obj| r_obj.get("url").and_then(|u| u.as_str()))
150                                            .map(|s| s.to_string())
151                                            .collect()
152                                    } else {
153                                        Vec::new()
154                                    };
155                                    
156                                    let vuln_info = VulnerabilityInfo {
157                                        id: vuln_id,
158                                        vuln_type: "security".to_string(),  // Security vulnerability
159                                        severity,
160                                        title,
161                                        description,
162                                        cve,
163                                        ghsa: None, // OWASP DC doesn't provide GHSA
164                                        affected_versions: "*".to_string(), // OWASP DC doesn't provide this directly
165                                        patched_versions: None, // Would need to parse from description
166                                        published_date: None,
167                                        references,
168                                    };
169                                    
170                                    package_vulns.push(vuln_info);
171                                }
172                            }
173                            
174                            if !package_vulns.is_empty() {
175                                vulnerable_deps.push(VulnerableDependency {
176                                    name: dep.name.clone(),
177                                    version: dep.version.clone(),
178                                    language: crate::analyzer::dependency_parser::Language::Java,
179                                    vulnerabilities: package_vulns,
180                                });
181                            }
182                        }
183                    }
184                }
185            }
186        }
187        
188        if vulnerable_deps.is_empty() {
189            Ok(None)
190        } else {
191            Ok(Some(vulnerable_deps))
192        }
193    }
194    
195    fn parse_severity(&self, severity: Option<&str>) -> VulnerabilitySeverity {
196        match severity.map(|s| s.to_lowercase()).as_deref() {
197            Some("critical") => VulnerabilitySeverity::Critical,
198            Some("high") => VulnerabilitySeverity::High,
199            Some("medium") => VulnerabilitySeverity::Medium,
200            Some("moderate") => VulnerabilitySeverity::Medium,
201            Some("low") => VulnerabilitySeverity::Low,
202            _ => VulnerabilitySeverity::Medium, // Default to medium if not specified
203        }
204    }
205}
206
207impl MutableLanguageVulnerabilityChecker for JavaVulnerabilityChecker {
208    fn check_vulnerabilities(
209        &mut self,
210        dependencies: &[DependencyInfo],
211        project_path: &Path,
212    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
213        info!("Checking Java dependencies");
214        
215        match self.execute_owasp_dependency_check(project_path, dependencies) {
216            Ok(Some(vulns)) => Ok(vulns),
217            Ok(None) => Ok(vec![]),
218            Err(e) => Err(e),
219        }
220    }
221}