syncable-cli 0.37.1

A Rust-based CLI that analyzes code repositories and generates Infrastructure as Code configurations
Documentation
use super::MutableLanguageVulnerabilityChecker;
use crate::analyzer::dependency_parser::DependencyInfo;
use crate::analyzer::tool_management::ToolDetector;
use crate::analyzer::vulnerability::{
    VulnerabilityError, VulnerabilityInfo, VulnerabilitySeverity, VulnerableDependency,
};
use log::{info, warn};
use std::path::Path;
use std::process::Command;

#[derive(Default)]
pub struct JavaVulnerabilityChecker {
    tool_detector: ToolDetector,
}

impl JavaVulnerabilityChecker {
    pub fn new() -> Self {
        Self::default()
    }

    fn execute_owasp_dependency_check(
        &mut self,
        project_path: &Path,
        dependencies: &[DependencyInfo],
    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
        // Check if dependency-check is available
        let depcheck_status = self.tool_detector.detect_tool("dependency-check");
        if !depcheck_status.available {
            warn!(
                "dependency-check not found, skipping Java vulnerability check. Install OWASP Dependency-Check CLI."
            );
            return Ok(None);
        }

        info!(
            "Executing OWASP Dependency-Check in {}",
            project_path.display()
        );

        // Execute dependency-check --format JSON --scan .
        let output = Command::new("dependency-check")
            .args([
                "--format",
                "JSON",
                "--scan",
                ".",
                "--out",
                "dependency-check-report.json",
            ])
            .current_dir(project_path)
            .output()
            .map_err(|e| {
                VulnerabilityError::CommandError(format!("Failed to run dependency-check: {}", e))
            })?;

        // Check if command succeeded
        if !output.status.success() {
            return Err(VulnerabilityError::CommandError(format!(
                "dependency-check failed with exit code {}: {}",
                output.status.code().unwrap_or(-1),
                String::from_utf8_lossy(&output.stderr)
            )));
        }

        // Read the generated report file
        let report_path = project_path.join("dependency-check-report.json");
        if !report_path.exists() {
            return Ok(None);
        }

        let report_content =
            std::fs::read_to_string(&report_path).map_err(VulnerabilityError::Io)?;

        let audit_data: serde_json::Value = serde_json::from_str(&report_content).map_err(|e| {
            VulnerabilityError::ParseError(format!(
                "Failed to parse dependency-check output: {}",
                e
            ))
        })?;

        // Clean up the report file
        let _ = std::fs::remove_file(&report_path);

        self.parse_dependency_check_output(&audit_data, dependencies)
    }

    fn parse_dependency_check_output(
        &self,
        audit_data: &serde_json::Value,
        dependencies: &[DependencyInfo],
    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();

        // OWASP Dependency-Check JSON structure parsing
        if let Some(dependencies_array) = audit_data.get("dependencies").and_then(|d| d.as_array())
        {
            for dependency in dependencies_array {
                if let Some(dep_obj) = dependency.as_object() {
                    let file_path = dep_obj
                        .get("filePath")
                        .and_then(|f| f.as_str())
                        .unwrap_or("")
                        .to_string();

                    // Extract package name from file path or identifiers
                    let package_name = if let Some(identifiers) =
                        dep_obj.get("identifiers").and_then(|i| i.as_array())
                    {
                        identifiers
                            .iter()
                            .filter_map(|id| id.as_object())
                            .find_map(|id_obj| {
                                if let Some(type_field) =
                                    id_obj.get("type").and_then(|t| t.as_str())
                                    && (type_field == "maven" || type_field == "gradle")
                                {
                                    return id_obj
                                        .get("name")
                                        .and_then(|n| n.as_str())
                                        .map(|s| s.to_string());
                                }
                                None
                            })
                            .unwrap_or_else(|| {
                                // Fallback to file name without extension
                                std::path::Path::new(&file_path)
                                    .file_stem()
                                    .and_then(|s| s.to_str())
                                    .unwrap_or("")
                                    .to_string()
                            })
                    } else {
                        // Fallback to file name without extension
                        std::path::Path::new(&file_path)
                            .file_stem()
                            .and_then(|s| s.to_str())
                            .unwrap_or("")
                            .to_string()
                    };

                    // Find matching dependency
                    if let Some(dep) = dependencies
                        .iter()
                        .find(|d| d.name.contains(&package_name) || package_name.contains(&d.name))
                    {
                        // Check for vulnerabilities
                        if let Some(vulnerabilities) =
                            dep_obj.get("vulnerabilities").and_then(|v| v.as_array())
                        {
                            let mut package_vulns = Vec::new();

                            for vulnerability in vulnerabilities {
                                if let Some(vuln_obj) = vulnerability.as_object() {
                                    let vuln_id = vuln_obj
                                        .get("name")
                                        .and_then(|n| n.as_str())
                                        .unwrap_or("unknown")
                                        .to_string();
                                    let title = vuln_obj
                                        .get("title")
                                        .and_then(|t| t.as_str())
                                        .unwrap_or("Unknown vulnerability")
                                        .to_string();
                                    let description = vuln_obj
                                        .get("description")
                                        .and_then(|d| d.as_str())
                                        .unwrap_or("")
                                        .to_string();
                                    let severity = self.parse_severity(
                                        vuln_obj.get("severity").and_then(|s| s.as_str()),
                                    );

                                    let _cvss_score =
                                        vuln_obj.get("cvssScore").and_then(|s| s.as_f64());
                                    let _cvss_vector = vuln_obj
                                        .get("cvssVector")
                                        .and_then(|v| v.as_str())
                                        .map(|s| s.to_string());

                                    let cve = if vuln_id.starts_with("CVE-") {
                                        Some(vuln_id.clone())
                                    } else {
                                        None
                                    };

                                    let references = if let Some(refs) =
                                        vuln_obj.get("references").and_then(|r| r.as_array())
                                    {
                                        refs.iter()
                                            .filter_map(|r| r.as_object())
                                            .filter_map(|r_obj| {
                                                r_obj.get("url").and_then(|u| u.as_str())
                                            })
                                            .map(|s| s.to_string())
                                            .collect()
                                    } else {
                                        Vec::new()
                                    };

                                    let vuln_info = VulnerabilityInfo {
                                        id: vuln_id,
                                        vuln_type: "security".to_string(), // Security vulnerability
                                        severity,
                                        title,
                                        description,
                                        cve,
                                        ghsa: None, // OWASP DC doesn't provide GHSA
                                        affected_versions: "*".to_string(), // OWASP DC doesn't provide this directly
                                        patched_versions: None, // Would need to parse from description
                                        published_date: None,
                                        references,
                                    };

                                    package_vulns.push(vuln_info);
                                }
                            }

                            if !package_vulns.is_empty() {
                                vulnerable_deps.push(VulnerableDependency {
                                    name: dep.name.clone(),
                                    version: dep.version.clone(),
                                    language: crate::analyzer::dependency_parser::Language::Java,
                                    vulnerabilities: package_vulns,
                                    source_dir: None,
                                });
                            }
                        }
                    }
                }
            }
        }

        if vulnerable_deps.is_empty() {
            Ok(None)
        } else {
            Ok(Some(vulnerable_deps))
        }
    }

    fn parse_severity(&self, severity: Option<&str>) -> VulnerabilitySeverity {
        match severity.map(|s| s.to_lowercase()).as_deref() {
            Some("critical") => VulnerabilitySeverity::Critical,
            Some("high") => VulnerabilitySeverity::High,
            Some("medium") => VulnerabilitySeverity::Medium,
            Some("moderate") => VulnerabilitySeverity::Medium,
            Some("low") => VulnerabilitySeverity::Low,
            _ => VulnerabilitySeverity::Medium, // Default to medium if not specified
        }
    }
}

impl MutableLanguageVulnerabilityChecker for JavaVulnerabilityChecker {
    fn check_vulnerabilities(
        &mut self,
        dependencies: &[DependencyInfo],
        project_path: &Path,
    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
        info!("Checking Java dependencies");

        match self.execute_owasp_dependency_check(project_path, dependencies) {
            Ok(Some(vulns)) => Ok(vulns),
            Ok(None) => Ok(vec![]),
            Err(e) => Err(e),
        }
    }
}