syncable_cli/analyzer/vulnerability/checkers/
java.rs

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