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
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 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 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 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 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 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 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 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 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 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 if let Some(dep) = dependencies
143 .iter()
144 .find(|d| d.name.contains(&package_name) || package_name.contains(&d.name))
145 {
146 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(), severity,
204 title,
205 description,
206 cve,
207 ghsa: None, affected_versions: "*".to_string(), patched_versions: None, 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, }
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}