use std::path::Path;
use std::process::Command;
use serde::Deserialize;
use crate::security::scanner::{FixResult, LanguageSecurityScanner, ScanOptions};
use crate::security::vulnerability::{Advisory, AffectedPackage, Severity, Vulnerability};
pub struct JavaSecurityScanner;
impl JavaSecurityScanner {
pub fn new() -> Self {
Self
}
fn check_tool(&self) -> bool {
Command::new("dependency-check")
.args(["--version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
|| self.check_maven_plugin()
|| self.check_gradle_plugin()
}
fn check_maven_plugin(&self) -> bool {
Command::new("mvn")
.args(["--version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn check_gradle_plugin(&self) -> bool {
Command::new("gradle")
.args(["--version"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
fn parse_dependency_check(&self, json_path: &Path) -> Result<Vec<Vulnerability>, String> {
let content = std::fs::read_to_string(json_path)
.map_err(|e| format!("Failed to read dependency-check report: {}", e))?;
let report: DependencyCheckReport = serde_json::from_str(&content)
.map_err(|e| format!("Failed to parse dependency-check report: {}", e))?;
let mut vulnerabilities = Vec::new();
for dep in report.dependencies {
for vuln in dep.vulnerabilities.unwrap_or_default() {
let advisory = Advisory {
id: vuln.name.clone(),
aliases: Vec::new(),
title: vuln.description.lines().next().unwrap_or("").to_string(),
description: vuln.description.clone(),
severity: map_cvss_to_severity(vuln.cvss_v3.as_ref().or(vuln.cvss_v2.as_ref())),
cvss_score: vuln
.cvss_v3
.as_ref()
.or(vuln.cvss_v2.as_ref())
.and_then(|c| c.base_score),
cvss_vector: vuln
.cvss_v3
.as_ref()
.or(vuln.cvss_v2.as_ref())
.and_then(|c| c.vector.clone()),
url: vuln.references.first().map(|r| r.url.clone()),
published: None,
updated: None,
cwe_ids: vuln.cwes.clone().unwrap_or_default(),
references: vuln.references.iter().map(|r| r.url.clone()).collect(),
};
let affected = AffectedPackage {
name: dep.file_name.clone(),
version: dep.version.clone().unwrap_or_default(),
ecosystem: "maven".to_string(),
affected_versions: vec![],
patched_versions: vec![],
recommended_version: None,
path: vec![dep.file_path.clone()],
is_direct: true,
};
vulnerabilities.push(Vulnerability::new(
advisory,
vec![affected],
"dependency-check",
"java",
));
}
}
Ok(vulnerabilities)
}
fn run_dependency_check(&self, path: &Path) -> Result<std::path::PathBuf, String> {
let report_dir = path.join("target").join("dependency-check");
std::fs::create_dir_all(&report_dir)
.map_err(|e| format!("Failed to create report directory: {}", e))?;
let report_path = report_dir.join("dependency-check-report.json");
let cli_result = Command::new("dependency-check")
.args([
"--scan",
path.to_str().unwrap_or("."),
"--format",
"JSON",
"--out",
report_dir.to_str().unwrap_or("."),
])
.current_dir(path)
.output();
if let Ok(output) = cli_result {
if output.status.success() && report_path.exists() {
return Ok(report_path);
}
}
if path.join("pom.xml").exists() {
let mvn_result = Command::new("mvn")
.args([
"org.owasp:dependency-check-maven:check",
"-DformatsFormat=JSON",
])
.current_dir(path)
.output();
if let Ok(output) = mvn_result {
if output.status.success() {
let maven_report = path.join("target/dependency-check-report.json");
if maven_report.exists() {
return Ok(maven_report);
}
}
}
}
if path.join("build.gradle").exists() || path.join("build.gradle.kts").exists() {
let gradle_result = Command::new("gradle")
.args(["dependencyCheckAnalyze", "--info"])
.current_dir(path)
.output();
if let Ok(output) = gradle_result {
if output.status.success() {
let gradle_report = path.join("build/reports/dependency-check-report.json");
if gradle_report.exists() {
return Ok(gradle_report);
}
}
}
}
Err(
"Failed to run dependency-check. Install it via: brew install dependency-check"
.to_string(),
)
}
}
impl Default for JavaSecurityScanner {
fn default() -> Self {
Self::new()
}
}
impl LanguageSecurityScanner for JavaSecurityScanner {
fn is_available(&self) -> bool {
self.check_tool()
}
fn name(&self) -> &str {
"dependency-check"
}
fn language(&self) -> &str {
"java"
}
fn detect(&self, path: &Path) -> bool {
path.join("pom.xml").exists()
|| path.join("build.gradle").exists()
|| path.join("build.gradle.kts").exists()
}
fn scan(&self, path: &Path, _options: &ScanOptions) -> Result<Vec<Vulnerability>, String> {
let report_path = self.run_dependency_check(path)?;
self.parse_dependency_check(&report_path)
}
fn fix(&self, _path: &Path, vulnerabilities: &[Vulnerability]) -> Result<FixResult, String> {
let unfixed: Vec<String> = vulnerabilities
.iter()
.map(|v| v.advisory.id.clone())
.collect();
let result = FixResult {
needs_review: true,
messages: vec![
"Java dependency fixes require manual version updates".to_string(),
"Update versions in pom.xml or build.gradle".to_string(),
],
unfixed,
..FixResult::default()
};
Ok(result)
}
}
#[derive(Debug, Deserialize)]
struct DependencyCheckReport {
#[serde(default)]
dependencies: Vec<DependencyEntry>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct DependencyEntry {
file_name: String,
file_path: String,
version: Option<String>,
vulnerabilities: Option<Vec<VulnerabilityEntry>>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct VulnerabilityEntry {
name: String,
description: String,
cvss_v2: Option<CvssEntry>,
cvss_v3: Option<CvssEntry>,
cwes: Option<Vec<String>>,
#[serde(default)]
references: Vec<ReferenceEntry>,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct CvssEntry {
base_score: Option<f32>,
vector: Option<String>,
}
#[derive(Debug, Deserialize)]
struct ReferenceEntry {
url: String,
}
fn map_cvss_to_severity(cvss: Option<&CvssEntry>) -> Severity {
match cvss.and_then(|c| c.base_score) {
Some(score) if score >= 9.0 => Severity::Critical,
Some(score) if score >= 7.0 => Severity::High,
Some(score) if score >= 4.0 => Severity::Medium,
Some(score) if score > 0.0 => Severity::Low,
_ => Severity::Unknown,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_java_scanner_detect() {
let scanner = JavaSecurityScanner::new();
let temp_dir = tempfile::tempdir().unwrap();
assert!(!scanner.detect(temp_dir.path()));
std::fs::write(temp_dir.path().join("pom.xml"), "<project/>").unwrap();
assert!(scanner.detect(temp_dir.path()));
}
}