syncable_cli/analyzer/vulnerability/checkers/
java.rs1use 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 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 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 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 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 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 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 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 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 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 if let Some(dep) = dependencies
145 .iter()
146 .find(|d| d.name.contains(&package_name) || package_name.contains(&d.name))
147 {
148 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(), severity,
206 title,
207 description,
208 cve,
209 ghsa: None, affected_versions: "*".to_string(), patched_versions: None, 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, }
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}