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