syncable_cli/analyzer/vulnerability/checkers/
java.rs1use 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 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 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 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 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 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 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 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 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 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 if let Some(dep) = dependencies.iter().find(|d|
120 d.name.contains(&package_name) || package_name.contains(&d.name)) {
121
122 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(), severity,
160 title,
161 description,
162 cve,
163 ghsa: None, affected_versions: "*".to_string(), patched_versions: None, 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, }
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}