syncable_cli/analyzer/
vulnerability_checker.rs

1use std::collections::HashMap;
2use std::process::Command;
3use std::path::Path;
4use std::fs;
5
6use chrono::{DateTime, Utc};
7use serde::{Deserialize, Serialize};
8use thiserror::Error;
9use log::{info, warn, error, debug};
10use rustsec;
11use rayon::prelude::*;
12
13use crate::analyzer::dependency_parser::{DependencyInfo, DependencyType, Language};
14use crate::analyzer::tool_installer::ToolInstaller;
15
16#[derive(Debug, Error)]
17pub enum VulnerabilityError {
18    #[error("Failed to check vulnerabilities: {0}")]
19    CheckFailed(String),
20    
21    #[error("API error: {0}")]
22    ApiError(String),
23    
24    #[error("Command execution failed: {0}")]
25    CommandError(String),
26    
27    #[error("Parse error: {0}")]
28    ParseError(String),
29    
30    #[error("IO error: {0}")]
31    Io(#[from] std::io::Error),
32    
33    #[error("Rustsec error: {0}")]
34    Rustsec(#[from] rustsec::Error),
35    
36    #[error("JSON error: {0}")]
37    Json(#[from] serde_json::Error),
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct VulnerabilityInfo {
42    pub id: String,
43    pub severity: VulnerabilitySeverity,
44    pub title: String,
45    pub description: String,
46    pub cve: Option<String>,
47    pub ghsa: Option<String>,
48    pub affected_versions: String,
49    pub patched_versions: Option<String>,
50    pub published_date: Option<DateTime<Utc>>,
51    pub references: Vec<String>,
52}
53
54#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
55pub enum VulnerabilitySeverity {
56    Critical,
57    High,
58    Medium,
59    Low,
60    Info,
61}
62
63#[derive(Debug, Serialize, Deserialize)]
64pub struct VulnerabilityReport {
65    pub checked_at: DateTime<Utc>,
66    pub total_vulnerabilities: usize,
67    pub critical_count: usize,
68    pub high_count: usize,
69    pub medium_count: usize,
70    pub low_count: usize,
71    pub vulnerable_dependencies: Vec<VulnerableDependency>,
72}
73
74#[derive(Debug, Serialize, Deserialize)]
75pub struct VulnerableDependency {
76    pub name: String,
77    pub version: String,
78    pub language: Language,
79    pub vulnerabilities: Vec<VulnerabilityInfo>,
80}
81
82pub struct VulnerabilityChecker;
83
84impl VulnerabilityChecker {
85    pub fn new() -> Self {
86        Self
87    }
88    
89    /// Check all dependencies for vulnerabilities
90    pub async fn check_all_dependencies(
91        &self,
92        dependencies: &HashMap<Language, Vec<DependencyInfo>>,
93        project_path: &Path,
94    ) -> Result<VulnerabilityReport, VulnerabilityError> {
95        info!("Starting comprehensive vulnerability check");
96        
97        // Debug: Show dependency counts by language
98        debug!("Dependencies found by language:");
99        for (lang, deps) in dependencies {
100            debug!("  {:?}: {} dependencies", lang, deps.len());
101            if deps.len() > 0 {
102                debug!("    Sample dependencies:");
103                for dep in deps.iter().take(3) {
104                    debug!("      - {} v{}", dep.name, dep.version);
105                }
106            }
107        }
108        
109        // Auto-install required tools
110        let mut installer = ToolInstaller::new();
111        let languages: Vec<Language> = dependencies.keys().cloned().collect();
112        
113        info!("🔧 Checking and installing required vulnerability scanning tools...");
114        installer.ensure_tools_for_languages(&languages)
115            .map_err(|e| VulnerabilityError::CommandError(format!("Tool installation failed: {}", e)))?;
116        
117        // Show tool status
118        installer.print_tool_status(&languages);
119        
120        let mut all_vulnerable_deps = Vec::new();
121        
122        // Process each language in parallel
123        let results: Vec<_> = dependencies.par_iter()
124            .map(|(language, deps)| {
125                self.check_language_dependencies(language, deps, project_path)
126            })
127            .collect();
128        
129        // Collect results
130        for result in results {
131            match result {
132                Ok(mut vuln_deps) => all_vulnerable_deps.append(&mut vuln_deps),
133                Err(e) => warn!("Error checking vulnerabilities: {}", e),
134            }
135        }
136        
137        // Sort by severity
138        all_vulnerable_deps.sort_by(|a, b| {
139            let a_max = a.vulnerabilities.iter()
140                .map(|v| &v.severity)
141                .max()
142                .unwrap_or(&VulnerabilitySeverity::Info);
143            let b_max = b.vulnerabilities.iter()
144                .map(|v| &v.severity)
145                .max()
146                .unwrap_or(&VulnerabilitySeverity::Info);
147            b_max.cmp(a_max)
148        });
149        
150        // Count vulnerabilities by severity
151        let mut critical_count = 0;
152        let mut high_count = 0;
153        let mut medium_count = 0;
154        let mut low_count = 0;
155        let mut total_vulnerabilities = 0;
156        
157        for dep in &all_vulnerable_deps {
158            for vuln in &dep.vulnerabilities {
159                total_vulnerabilities += 1;
160                match vuln.severity {
161                    VulnerabilitySeverity::Critical => critical_count += 1,
162                    VulnerabilitySeverity::High => high_count += 1,
163                    VulnerabilitySeverity::Medium => medium_count += 1,
164                    VulnerabilitySeverity::Low => low_count += 1,
165                    VulnerabilitySeverity::Info => {},
166                }
167            }
168        }
169        
170        Ok(VulnerabilityReport {
171            checked_at: Utc::now(),
172            total_vulnerabilities,
173            critical_count,
174            high_count,
175            medium_count,
176            low_count,
177            vulnerable_dependencies: all_vulnerable_deps,
178        })
179    }
180    
181    fn check_language_dependencies(
182        &self,
183        language: &Language,
184        dependencies: &[DependencyInfo],
185        project_path: &Path,
186    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
187        info!("Checking {} dependencies for {:?}", dependencies.len(), language);
188        
189        match language {
190            Language::Rust => self.check_rust_dependencies(dependencies),
191            Language::JavaScript | Language::TypeScript => {
192                self.check_npm_dependencies(dependencies, project_path)
193            },
194            Language::Python => self.check_python_dependencies(dependencies, project_path),
195            Language::Go => self.check_go_dependencies(dependencies, project_path),
196            Language::Java | Language::Kotlin => {
197                self.check_java_dependencies(dependencies, project_path)
198            },
199            _ => {
200                warn!("Vulnerability checking not yet implemented for {:?}", language);
201                Ok(vec![])
202            }
203        }
204    }
205    
206    /// Check Rust dependencies using RustSec database
207    fn check_rust_dependencies(
208        &self,
209        dependencies: &[DependencyInfo],
210    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
211        info!("Checking Rust dependencies with cargo-audit");
212        
213        // Check if cargo-audit is installed
214        let check_output = Command::new("cargo")
215            .args(&["audit", "--version"])
216            .output();
217            
218        if check_output.is_err() || !check_output.unwrap().status.success() {
219            warn!("cargo-audit not installed. Install with: cargo install cargo-audit");
220            warn!("Skipping Rust vulnerability checks");
221            return Ok(vec![]);
222        }
223        
224        // Run cargo audit in JSON format
225        let output = Command::new("cargo")
226            .args(&["audit", "--json"])
227            .output()
228            .map_err(|e| VulnerabilityError::CommandError(
229                format!("Failed to run cargo audit: {}", e)
230            ))?;
231        
232        if output.stdout.is_empty() {
233            return Ok(vec![]);
234        }
235        
236        // Parse cargo audit output
237        let audit_data: serde_json::Value = serde_json::from_slice(&output.stdout)?;
238        
239        self.parse_cargo_audit_output(&audit_data, dependencies)
240    }
241    
242    /// Check JavaScript/TypeScript dependencies using npm audit
243    fn check_npm_dependencies(
244        &self,
245        dependencies: &[DependencyInfo],
246        project_path: &Path,
247    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
248        info!("Checking npm dependencies with npm audit");
249        
250        // Check if package.json exists
251        let package_json_path = project_path.join("package.json");
252        if !package_json_path.exists() {
253            debug!("No package.json found, skipping npm audit");
254            return Ok(vec![]);
255        }
256        
257        // Run npm audit
258        let output = Command::new("npm")
259            .args(&["audit", "--json"])
260            .current_dir(project_path)
261            .output()
262            .map_err(|e| VulnerabilityError::CommandError(
263                format!("Failed to run npm audit: {}", e)
264            ))?;
265        
266        // Parse npm audit output
267        let audit_data: serde_json::Value = serde_json::from_slice(&output.stdout)?;
268        
269        self.parse_npm_audit_output(&audit_data, dependencies)
270    }
271    
272    /// Check Python dependencies using pip-audit
273    fn check_python_dependencies(
274        &self,
275        dependencies: &[DependencyInfo],
276        project_path: &Path,
277    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
278        info!("Checking Python dependencies with pip-audit");
279        
280        // Check for requirements.txt
281        let requirements_file = project_path.join("requirements.txt");
282        if !requirements_file.exists() {
283            debug!("No requirements.txt found, creating temporary file");
284            
285            // Create a temporary requirements file
286            let temp_req = project_path.join(".temp_requirements_for_audit.txt");
287            let mut content = String::new();
288            
289            for dep in dependencies {
290                if dep.dep_type == DependencyType::Production {
291                    content.push_str(&format!("{}=={}\n", dep.name, dep.version));
292                }
293            }
294            
295            fs::write(&temp_req, content)?;
296            
297            // Run pip-audit on temp file
298            let output = Command::new("pip-audit")
299                .args(&["-r", temp_req.to_str().unwrap(), "--format", "json"])
300                .output()
301                .map_err(|e| {
302                    // Clean up temp file
303                    let _ = fs::remove_file(&temp_req);
304                    VulnerabilityError::CommandError(
305                        format!("Failed to run pip-audit (is it installed?): {}", e)
306                    )
307                })?;
308            
309            // Clean up temp file
310            let _ = fs::remove_file(&temp_req);
311            
312            if !output.status.success() && output.stdout.is_empty() {
313                let stderr = String::from_utf8_lossy(&output.stderr);
314                return Err(VulnerabilityError::CommandError(
315                    format!("pip-audit failed: {}", stderr)
316                ));
317            }
318            
319            // Parse pip-audit output
320            let audit_data: serde_json::Value = serde_json::from_slice(&output.stdout)?;
321            return self.parse_pip_audit_output(&audit_data, dependencies);
322        }
323        
324        // Use existing requirements.txt
325        let output = Command::new("pip-audit")
326            .args(&["-r", requirements_file.to_str().unwrap(), "--format", "json"])
327            .current_dir(project_path)
328            .output()
329            .map_err(|e| VulnerabilityError::CommandError(
330                format!("Failed to run pip-audit (is it installed?): {}", e)
331            ))?;
332        
333        if !output.status.success() && output.stdout.is_empty() {
334            let stderr = String::from_utf8_lossy(&output.stderr);
335            return Err(VulnerabilityError::CommandError(
336                format!("pip-audit failed: {}", stderr)
337            ));
338        }
339        
340        // Parse pip-audit output
341        let audit_data: serde_json::Value = serde_json::from_slice(&output.stdout)?;
342        
343        self.parse_pip_audit_output(&audit_data, dependencies)
344    }
345    
346    /// Check Go dependencies using govulncheck
347    fn check_go_dependencies(
348        &self,
349        dependencies: &[DependencyInfo],
350        project_path: &Path,
351    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
352        info!("Checking Go dependencies with govulncheck");
353        
354        // Check if go.mod exists
355        let go_mod_path = project_path.join("go.mod");
356        if !go_mod_path.exists() {
357            debug!("No go.mod found, skipping govulncheck");
358            return Ok(vec![]);
359        }
360        
361        // Try different paths for govulncheck
362        let govulncheck_commands = vec![
363            "govulncheck".to_string(),
364            format!("{}/go/bin/govulncheck", std::env::var("HOME").unwrap_or_else(|_| ".".to_string())),
365        ];
366        
367        let mut last_error = None;
368        
369        for govulncheck_cmd in govulncheck_commands {
370            debug!("Trying govulncheck command: {}", govulncheck_cmd);
371            
372                         // Run govulncheck
373             let output = Command::new(&govulncheck_cmd)
374                 .args(&["-json", "./..."])
375                 .current_dir(project_path)
376                 .output();
377                
378            match output {
379                Ok(result) => {
380
381                    
382                    if result.status.success() || !result.stdout.is_empty() {
383                        info!("Successfully ran govulncheck using: {}", govulncheck_cmd);
384                        return self.parse_govulncheck_output(&result.stdout, dependencies);
385                    } else {
386                        let stderr = String::from_utf8_lossy(&result.stderr);
387                        debug!("govulncheck failed with {}: {}", govulncheck_cmd, stderr);
388                        last_error = Some(format!("govulncheck failed: {}", stderr));
389                    }
390                }
391                Err(e) => {
392                    debug!("Could not execute {}: {}", govulncheck_cmd, e);
393                    last_error = Some(format!("Failed to run govulncheck: {}", e));
394                }
395            }
396        }
397        
398        // If all attempts failed, return the last error
399        if let Some(error) = last_error {
400            warn!("govulncheck not available: {}", error);
401            warn!("Install with: go install golang.org/x/vuln/cmd/govulncheck@latest");
402            warn!("Make sure ~/go/bin is in your PATH");
403        }
404        
405        Ok(vec![])
406    }
407    
408    /// Check Java dependencies using OWASP dependency-check
409    fn check_java_dependencies(
410        &self,
411        dependencies: &[DependencyInfo],
412        project_path: &Path,
413    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
414        info!("Checking Java dependencies with multiple scanners");
415        
416        // Try grype first
417        debug!("Attempting grype scan for Java dependencies");
418        let grype_result = self.check_java_with_grype(dependencies, project_path);
419        
420        match grype_result {
421            Ok(vulnerabilities) if !vulnerabilities.is_empty() => {
422                info!("Found {} vulnerabilities with grype", vulnerabilities.len());
423                return Ok(vulnerabilities);
424            }
425            Ok(_) => {
426                warn!("grype found no vulnerabilities for {} Java dependencies", dependencies.len());
427                debug!("This could indicate:");
428                debug!("  - Dependencies are secure (unlikely for {} deps)", dependencies.len());
429                debug!("  - grype's Java vulnerability database is incomplete");
430                debug!("  - Project needs to be built for better scanning");
431            }
432            Err(e) => {
433                warn!("grype scan failed: {}", e);
434            }
435        }
436        
437        // Try OWASP Dependency Check as fallback
438        info!("Attempting OWASP Dependency Check as fallback");
439        if let Ok(owasp_vulnerabilities) = self.check_java_with_owasp_dc(dependencies, project_path) {
440            if !owasp_vulnerabilities.is_empty() {
441                info!("Found {} vulnerabilities with OWASP Dependency Check", owasp_vulnerabilities.len());
442                return Ok(owasp_vulnerabilities);
443            }
444        }
445        
446        // Try online vulnerability checking for known vulnerable packages
447        info!("Checking against known vulnerable packages");
448        let known_vulns = self.check_known_vulnerable_java_packages(dependencies);
449        if !known_vulns.is_empty() {
450            warn!("Found {} known vulnerable packages that scanners missed!", known_vulns.len());
451            return Ok(known_vulns);
452        }
453        
454        warn!("No vulnerabilities found by any scanner for {} Java dependencies", dependencies.len());
455        warn!("Consider:");
456        warn!("  1. Building the project: mvn package");
457        warn!("  2. Using a different scanner like Snyk");
458        warn!("  3. Checking dependencies manually");
459        
460        Ok(vec![])
461    }
462    
463    /// Check Java dependencies using grype (original implementation)
464    fn check_java_with_grype(
465        &self,
466        dependencies: &[DependencyInfo],
467        project_path: &Path,
468    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
469        // Try different grype locations
470        let grype_home = format!("{}/.local/bin/grype", std::env::var("HOME").unwrap_or_default());
471        let grype_cmds = vec![
472            "grype",
473            grype_home.as_str(),
474        ];
475        
476        let mut last_error = None;
477        
478        for grype_cmd in &grype_cmds {
479            // Check if grype is installed
480            let check_output = Command::new(grype_cmd)
481                .arg("version")
482                .output();
483                
484            if check_output.is_err() || !check_output.unwrap().status.success() {
485                continue;
486            }
487            
488            // Try multiple scanning approaches
489            let maven_repo_path = format!("{}/.m2/repository", std::env::var("HOME").unwrap_or_default());
490            let scan_approaches = vec![
491                // Scan project directory
492                (vec!["dir:.", "-o", "json", "--only-fixed=false", "--only-notfixed=false"], "project directory"),
493                // Scan Maven repository for specific dependencies
494                (vec![maven_repo_path.as_str(), "-o", "json"], "Maven repository"),
495            ];
496            
497            for (args, description) in scan_approaches {
498                debug!("Trying grype on {} with command: {} {}", description, grype_cmd, args.join(" "));
499                
500                let output = Command::new(grype_cmd)
501                    .args(&args)
502                    .current_dir(project_path)
503                    .output();
504                    
505                match output {
506                    Ok(result) => {
507                        if result.status.success() || !result.stdout.is_empty() {
508                            debug!("grype scan of {} completed", description);
509                            let vulnerabilities = self.parse_grype_output(&result.stdout, dependencies, Language::Java)?;
510                            if !vulnerabilities.is_empty() {
511                                info!("Found {} vulnerabilities scanning {}", vulnerabilities.len(), description);
512                                return Ok(vulnerabilities);
513                            } else {
514                                debug!("No vulnerabilities found scanning {}", description);
515                            }
516                        } else {
517                            let stderr = String::from_utf8_lossy(&result.stderr);
518                            debug!("grype scan of {} failed: {}", description, stderr);
519                            last_error = Some(format!("grype failed on {}: {}", description, stderr));
520                        }
521                    }
522                    Err(e) => {
523                        debug!("Failed to run grype {} on {}: {}", grype_cmd, description, e);
524                        last_error = Some(format!("Failed to run grype: {}", e));
525                    }
526                }
527            }
528        }
529        
530        // If no grype command worked, return error
531        if let Some(err) = last_error {
532            return Err(VulnerabilityError::CommandError(err));
533        }
534        
535        warn!("grype not installed. Install with: brew install grype");
536        Ok(vec![])
537    }
538    
539    /// Check Java dependencies using OWASP Dependency Check
540    fn check_java_with_owasp_dc(
541        &self,
542        dependencies: &[DependencyInfo],
543        project_path: &Path,
544    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
545        // Check if dependency-check is available
546        let dc_cmds = vec![
547            "dependency-check",
548            "dependency-check.sh",
549            "/opt/homebrew/bin/dependency-check",
550        ];
551        
552        for dc_cmd in dc_cmds {
553            let check_output = Command::new(dc_cmd)
554                .arg("--version")
555                .output();
556                
557            if check_output.is_ok() && check_output.unwrap().status.success() {
558                debug!("Found OWASP Dependency Check: {}", dc_cmd);
559                
560                // Run dependency check
561                let output = Command::new(dc_cmd)
562                    .args(&[
563                        "--project", "vulnerability-scan",
564                        "--scan", ".",
565                        "--format", "JSON",
566                        "--out", "./dependency-check-report",
567                        "--enableRetired",
568                    ])
569                    .current_dir(project_path)
570                    .output();
571                    
572                match output {
573                    Ok(result) if result.status.success() => {
574                        let report_file = project_path.join("dependency-check-report").join("dependency-check-report.json");
575                        if report_file.exists() {
576                            let report_content = fs::read_to_string(&report_file)?;
577                            let report_data: serde_json::Value = serde_json::from_str(&report_content)?;
578                            
579                            // Clean up report files
580                            let _ = fs::remove_dir_all(project_path.join("dependency-check-report"));
581                            
582                            return self.parse_owasp_dependency_check_output(&report_data, dependencies);
583                        }
584                    }
585                    _ => {
586                        debug!("OWASP Dependency Check failed or not configured properly");
587                    }
588                }
589            }
590        }
591        
592        debug!("OWASP Dependency Check not available");
593        Ok(vec![])
594    }
595    
596    /// Check against known vulnerable Java packages
597    fn check_known_vulnerable_java_packages(
598        &self,
599        dependencies: &[DependencyInfo],
600    ) -> Vec<VulnerableDependency> {
601        let mut vulnerable_deps = Vec::new();
602        
603        // Known vulnerable packages and versions
604        let known_vulnerabilities = vec![
605            ("io.jsonwebtoken:jjwt", "0.9.1", vec![
606                VulnerabilityInfo {
607                    id: "CVE-2019-7644".to_string(),
608                    severity: VulnerabilitySeverity::High,
609                    title: "JWT signature verification bypass in JJWT".to_string(),
610                    description: "JJWT before 0.10.5 allows attackers to bypass signature verification by providing a public key that the attacker controls.".to_string(),
611                    cve: Some("CVE-2019-7644".to_string()),
612                    ghsa: Some("GHSA-3p3g-vpw6-4w66".to_string()),
613                    affected_versions: "< 0.10.5".to_string(),
614                    patched_versions: Some(">= 0.10.5".to_string()),
615                    published_date: None,
616                    references: vec![
617                        "https://github.com/jwtk/jjwt/issues/515".to_string(),
618                        "https://nvd.nist.gov/vuln/detail/CVE-2019-7644".to_string(),
619                    ],
620                },
621            ]),
622            ("org.apache.logging.log4j:log4j-core", "2.17.1", vec![
623                VulnerabilityInfo {
624                    id: "CVE-2021-44228".to_string(),
625                    severity: VulnerabilitySeverity::Critical,
626                    title: "Log4j Remote Code Execution (Log4Shell)".to_string(),
627                    description: "Apache Log4j2 2.0-beta9 through 2.15.0 (excluding security releases 2.12.2, 2.12.3, and 2.3.1) JNDI features used in configuration, log messages, and parameters do not protect against attacker controlled LDAP and other JNDI related endpoints.".to_string(),
628                    cve: Some("CVE-2021-44228".to_string()),
629                    ghsa: Some("GHSA-jfh8-c2jp-5v3q".to_string()),
630                    affected_versions: ">= 2.0-beta9, <= 2.15.0".to_string(),
631                    patched_versions: Some(">= 2.17.1".to_string()),
632                    published_date: None,
633                    references: vec![
634                        "https://logging.apache.org/log4j/2.x/security.html".to_string(),
635                        "https://nvd.nist.gov/vuln/detail/CVE-2021-44228".to_string(),
636                    ],
637                },
638            ]),
639            ("com.fasterxml.jackson.core:jackson-databind", "2.14.2", vec![
640                VulnerabilityInfo {
641                    id: "CVE-2022-42003".to_string(),
642                    severity: VulnerabilitySeverity::High,
643                    title: "Jackson Databind deserialization vulnerability".to_string(),
644                    description: "In FasterXML jackson-databind before versions 2.13.4.1 and 2.14.0-rc1, resource exhaustion can occur because of a lack of a check in primitive value deserializers to avoid deep wrapper array nesting.".to_string(),
645                    cve: Some("CVE-2022-42003".to_string()),
646                    ghsa: Some("GHSA-jjjh-jjxp-wpff".to_string()),
647                    affected_versions: "< 2.13.4.1".to_string(),
648                    patched_versions: Some(">= 2.13.4.1".to_string()),
649                    published_date: None,
650                    references: vec![
651                        "https://github.com/FasterXML/jackson-databind/issues/3582".to_string(),
652                        "https://nvd.nist.gov/vuln/detail/CVE-2022-42003".to_string(),
653                    ],
654                },
655            ]),
656        ];
657        
658        for (package_name, _vulnerable_version, vulns) in known_vulnerabilities {
659            if let Some(dep) = dependencies.iter().find(|d| d.name == package_name) {
660                debug!("Found known vulnerable package: {} v{}", dep.name, dep.version);
661                vulnerable_deps.push(VulnerableDependency {
662                    name: dep.name.clone(),
663                    version: dep.version.clone(),
664                    language: Language::Java,
665                    vulnerabilities: vulns,
666                });
667            }
668        }
669        
670        vulnerable_deps
671    }
672    
673    #[allow(dead_code)]
674    fn map_rustsec_severity(&self, severity: &Option<rustsec::advisory::Severity>) -> VulnerabilitySeverity {
675        match severity {
676            Some(rustsec::advisory::Severity::Critical) => VulnerabilitySeverity::Critical,
677            Some(rustsec::advisory::Severity::High) => VulnerabilitySeverity::High,
678            Some(rustsec::advisory::Severity::Medium) => VulnerabilitySeverity::Medium,
679            Some(rustsec::advisory::Severity::Low) => VulnerabilitySeverity::Low,
680            Some(rustsec::advisory::Severity::None) | None => VulnerabilitySeverity::Info,
681        }
682    }
683    
684    fn parse_npm_audit_output(
685        &self,
686        audit_data: &serde_json::Value,
687        dependencies: &[DependencyInfo],
688    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
689        let mut vulnerable_deps = Vec::new();
690        
691        if let Some(vulnerabilities) = audit_data.get("vulnerabilities").and_then(|v| v.as_object()) {
692            for (pkg_name, vuln_data) in vulnerabilities {
693                if let Some(dep) = dependencies.iter().find(|d| d.name == *pkg_name) {
694                    let mut vuln_infos = Vec::new();
695                    
696                    if let Some(via) = vuln_data.get("via").and_then(|v| v.as_array()) {
697                        for item in via {
698                            if let Some(obj) = item.as_object() {
699                                vuln_infos.push(VulnerabilityInfo {
700                                    id: obj.get("source")
701                                        .and_then(|s| s.as_str())
702                                        .unwrap_or("unknown")
703                                        .to_string(),
704                                    severity: self.parse_npm_severity(
705                                        obj.get("severity")
706                                            .and_then(|s| s.as_str())
707                                            .unwrap_or("low")
708                                    ),
709                                    title: obj.get("title")
710                                        .and_then(|s| s.as_str())
711                                        .unwrap_or("Unknown vulnerability")
712                                        .to_string(),
713                                    description: obj.get("overview")
714                                        .and_then(|s| s.as_str())
715                                        .unwrap_or("")
716                                        .to_string(),
717                                    cve: obj.get("cve")
718                                        .and_then(|s| s.as_str())
719                                        .map(|s| s.to_string()),
720                                    ghsa: obj.get("ghsa")
721                                        .and_then(|s| s.as_str())
722                                        .map(|s| s.to_string()),
723                                    affected_versions: obj.get("vulnerable_versions")
724                                        .and_then(|s| s.as_str())
725                                        .unwrap_or("*")
726                                        .to_string(),
727                                    patched_versions: obj.get("patched_versions")
728                                        .and_then(|s| s.as_str())
729                                        .map(|s| s.to_string()),
730                                    published_date: None,
731                                    references: vec![],
732                                });
733                            }
734                        }
735                    }
736                    
737                    if !vuln_infos.is_empty() {
738                        vulnerable_deps.push(VulnerableDependency {
739                            name: dep.name.clone(),
740                            version: dep.version.clone(),
741                            language: Language::JavaScript,
742                            vulnerabilities: vuln_infos,
743                        });
744                    }
745                }
746            }
747        }
748        
749        Ok(vulnerable_deps)
750    }
751    
752    fn parse_pip_audit_output(
753        &self,
754        audit_data: &serde_json::Value,
755        dependencies: &[DependencyInfo],
756    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
757        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
758        
759        // pip-audit JSON format: {"dependencies": [{"name": "package", "version": "1.0", "vulns": [...]}]}
760        if let Some(deps) = audit_data.get("dependencies").and_then(|d| d.as_array()) {
761            for dep_obj in deps {
762                if let Some(dep_data) = dep_obj.as_object() {
763                    let name = dep_data.get("name")
764                        .and_then(|n| n.as_str())
765                        .unwrap_or("")
766                        .to_string();
767                    
768                    let version = dep_data.get("version")
769                        .and_then(|v| v.as_str())
770                        .unwrap_or("")
771                        .to_string();
772                    
773                    if let Some(vulns) = dep_data.get("vulns").and_then(|v| v.as_array()) {
774                        if vulns.is_empty() {
775                            continue;
776                        }
777                        
778                        if let Some(dep) = dependencies.iter().find(|d| d.name == name) {
779                            let mut vuln_infos = Vec::new();
780                            
781                            for vuln in vulns {
782                                if let Some(vuln_obj) = vuln.as_object() {
783                                    vuln_infos.push(VulnerabilityInfo {
784                                        id: vuln_obj.get("id")
785                                            .and_then(|s| s.as_str())
786                                            .unwrap_or("unknown")
787                                            .to_string(),
788                                        severity: self.parse_pip_severity(
789                                            vuln_obj.get("severity")
790                                                .and_then(|s| s.as_str())
791                                        ),
792                                        title: vuln_obj.get("description")
793                                            .and_then(|s| s.as_str())
794                                            .unwrap_or("Unknown vulnerability")
795                                            .to_string(),
796                                        description: vuln_obj.get("description")
797                                            .and_then(|s| s.as_str())
798                                            .unwrap_or("")
799                                            .to_string(),
800                                        cve: vuln_obj.get("aliases")
801                                            .and_then(|a| a.as_array())
802                                            .and_then(|arr| {
803                                                let cve_aliases: Vec<&str> = arr.iter()
804                                                    .filter_map(|v| v.as_str())
805                                                    .filter(|s| s.starts_with("CVE-"))
806                                                    .collect();
807                                                cve_aliases.first().map(|s| s.to_string())
808                                            }),
809                                        ghsa: vuln_obj.get("aliases")
810                                            .and_then(|a| a.as_array())
811                                            .and_then(|arr| {
812                                                let ghsa_aliases: Vec<&str> = arr.iter()
813                                                    .filter_map(|v| v.as_str())
814                                                    .filter(|s| s.starts_with("GHSA-"))
815                                                    .collect();
816                                                ghsa_aliases.first().map(|s| s.to_string())
817                                            }),
818                                        affected_versions: vuln_obj.get("fix_versions")
819                                            .and_then(|f| f.as_array())
820                                            .and_then(|arr| arr.first())
821                                            .and_then(|s| s.as_str())
822                                            .map(|s| format!("< {}", s))
823                                            .unwrap_or_else(|| "*".to_string()),
824                                        patched_versions: vuln_obj.get("fix_versions")
825                                            .and_then(|f| f.as_array())
826                                            .and_then(|arr| arr.first())
827                                            .and_then(|s| s.as_str())
828                                            .map(|s| s.to_string()),
829                                        published_date: None,
830                                        references: vec![],
831                                    });
832                                }
833                            }
834                            
835                            if !vuln_infos.is_empty() {
836                                vulnerable_deps.push(VulnerableDependency {
837                                    name: dep.name.clone(),
838                                    version: dep.version.clone(),
839                                    language: Language::Python,
840                                    vulnerabilities: vuln_infos,
841                                });
842                            }
843                        }
844                    }
845                }
846            }
847        }
848        
849        Ok(vulnerable_deps)
850    }
851    
852    fn parse_govulncheck_output(
853        &self,
854        output: &[u8],
855        dependencies: &[DependencyInfo],
856    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
857        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
858        let output_str = String::from_utf8_lossy(output);
859        
860        // govulncheck outputs multiple JSON objects separated by newlines
861        // We need to parse each complete JSON object
862        let mut current_json = String::new();
863        let mut brace_count = 0;
864        
865        for line in output_str.lines() {
866            let trimmed = line.trim();
867            if trimmed.is_empty() {
868                continue;
869            }
870            
871            current_json.push_str(line);
872            current_json.push('\n');
873            
874            // Count braces to determine when we have a complete JSON object
875            for ch in line.chars() {
876                match ch {
877                    '{' => brace_count += 1,
878                    '}' => brace_count -= 1,
879                    _ => {}
880                }
881            }
882            
883            // When brace count reaches 0, we have a complete JSON object
884            if brace_count == 0 && !current_json.trim().is_empty() {
885                if let Ok(json_val) = serde_json::from_str::<serde_json::Value>(&current_json) {
886
887                    
888                    if let Some(obj) = json_val.as_object() {
889                        // Look for "finding" entries which contain actual vulnerabilities affecting the code
890                        if obj.contains_key("finding") {
891                            if let Some(finding) = obj.get("finding").and_then(|f| f.as_object()) {
892                                let osv_id = finding.get("osv")
893                                    .and_then(|s| s.as_str())
894                                    .unwrap_or("unknown");
895                                
896
897                                
898                                // Skip if we've already processed this vulnerability
899                                if vulnerable_deps.iter().any(|dep| 
900                                    dep.vulnerabilities.iter().any(|v| v.id == osv_id)
901                                ) {
902
903                                    // Reset for next JSON object
904                                    current_json.clear();
905                                    continue;
906                                }
907                                
908                                // Get the trace information to find the affected module
909                                if let Some(trace) = finding.get("trace").and_then(|t| t.as_array()) {
910                                    if let Some(first_trace) = trace.first().and_then(|t| t.as_object()) {
911                                        let module_path = first_trace.get("module")
912                                            .and_then(|m| m.as_str())
913                                            .unwrap_or("");
914                                        
915                                        let module_version = first_trace.get("version")
916                                            .and_then(|v| v.as_str())
917                                            .unwrap_or("");
918                                        
919                                        // Find matching dependency
920                                        if let Some(dep) = dependencies.iter().find(|d| {
921                                            let matches = module_path.contains(&d.name) || 
922                                                d.name.contains(module_path) ||
923                                                d.name == module_path;
924                                            
925
926                                            
927                                            matches
928                                        }) {
929                                            let fixed_version = finding.get("fixed_version")
930                                                .and_then(|v| v.as_str())
931                                                .map(|v| v.to_string());
932                                            
933                                            let vuln_info = VulnerabilityInfo {
934                                                id: osv_id.to_string(),
935                                                severity: VulnerabilitySeverity::High, // Default to high for Go vulnerabilities
936                                                title: format!("Vulnerability {} in {}", osv_id, module_path),
937                                                description: format!("Vulnerability {} found in module {} version {}", osv_id, module_path, module_version),
938                                                cve: None,
939                                                ghsa: None,
940                                                affected_versions: format!("< {}", fixed_version.as_deref().unwrap_or("unknown")),
941                                                patched_versions: fixed_version,
942                                                published_date: None,
943                                                references: vec![format!("https://pkg.go.dev/vuln/{}", osv_id)],
944                                            };
945                                            
946                                            // Check if we already have this dependency
947                                            if let Some(existing) = vulnerable_deps.iter_mut()
948                                                .find(|vuln_dep: &&mut VulnerableDependency| vuln_dep.name == dep.name) 
949                                            {
950                                                existing.vulnerabilities.push(vuln_info);
951                                            } else {
952                                                vulnerable_deps.push(VulnerableDependency {
953                                                    name: dep.name.clone(),
954                                                    version: dep.version.clone(),
955                                                    language: Language::Go,
956                                                    vulnerabilities: vec![vuln_info],
957                                                });
958                                            }
959                                        }
960                                    }
961                                }
962                            }
963                        }
964                    }
965                }
966                
967                // Reset for next JSON object
968                current_json.clear();
969            }
970        }
971        
972        Ok(vulnerable_deps)
973    }
974    
975    fn parse_npm_severity(&self, severity: &str) -> VulnerabilitySeverity {
976        match severity.to_lowercase().as_str() {
977            "critical" => VulnerabilitySeverity::Critical,
978            "high" => VulnerabilitySeverity::High,
979            "moderate" | "medium" => VulnerabilitySeverity::Medium,
980            "low" => VulnerabilitySeverity::Low,
981            _ => VulnerabilitySeverity::Info,
982        }
983    }
984    
985    fn parse_pip_severity(&self, severity: Option<&str>) -> VulnerabilitySeverity {
986        match severity.map(|s| s.to_lowercase()).as_deref() {
987            Some("critical") => VulnerabilitySeverity::Critical,
988            Some("high") => VulnerabilitySeverity::High,
989            Some("medium") | Some("moderate") => VulnerabilitySeverity::Medium,
990            Some("low") => VulnerabilitySeverity::Low,
991            _ => VulnerabilitySeverity::Medium, // Default to medium if not specified
992        }
993    }
994    
995    fn parse_osv_severity(&self, osv: &serde_json::Map<String, serde_json::Value>) -> VulnerabilitySeverity {
996        // OSV format uses CVSS scores or database_specific severity
997        if let Some(severity) = osv.get("database_specific")
998            .and_then(|d| d.get("severity"))
999            .and_then(|s| s.as_str()) 
1000        {
1001            return self.parse_npm_severity(severity);
1002        }
1003        
1004        // Default to high for Go vulnerabilities
1005        VulnerabilitySeverity::High
1006    }
1007    
1008    fn parse_cargo_audit_output(
1009        &self,
1010        audit_data: &serde_json::Value,
1011        dependencies: &[DependencyInfo],
1012    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
1013        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
1014        
1015        if let Some(vulnerabilities) = audit_data.get("vulnerabilities").and_then(|v| v.get("list")).and_then(|l| l.as_array()) {
1016            for vuln in vulnerabilities {
1017                if let Some(advisory) = vuln.get("advisory") {
1018                    let package_name = advisory.get("package")
1019                        .and_then(|n| n.as_str())
1020                        .unwrap_or("");
1021                    
1022                    let package_version = vuln.get("package")
1023                        .and_then(|p| p.get("version"))
1024                        .and_then(|v| v.as_str())
1025                        .unwrap_or("");
1026                    
1027                    if let Some(dep) = dependencies.iter().find(|d| d.name == package_name) {
1028                        let vuln_info = VulnerabilityInfo {
1029                            id: advisory.get("id")
1030                                .and_then(|id| id.as_str())
1031                                .unwrap_or("unknown")
1032                                .to_string(),
1033                            severity: self.parse_rustsec_severity(
1034                                advisory.get("severity")
1035                                    .and_then(|s| s.as_str())
1036                            ),
1037                            title: advisory.get("title")
1038                                .and_then(|t| t.as_str())
1039                                .unwrap_or("Unknown vulnerability")
1040                                .to_string(),
1041                            description: advisory.get("description")
1042                                .and_then(|d| d.as_str())
1043                                .unwrap_or("")
1044                                .to_string(),
1045                            cve: advisory.get("aliases")
1046                                .and_then(|a| a.as_array())
1047                                .and_then(|arr| arr.iter()
1048                                    .filter_map(|v| v.as_str())
1049                                    .find(|s| s.starts_with("CVE-"))
1050                                    .map(|s| s.to_string())),
1051                            ghsa: advisory.get("aliases")
1052                                .and_then(|a| a.as_array())
1053                                .and_then(|arr| arr.iter()
1054                                    .filter_map(|v| v.as_str())
1055                                    .find(|s| s.starts_with("GHSA-"))
1056                                    .map(|s| s.to_string())),
1057                            affected_versions: format!("< {}", 
1058                                vuln.get("versions")
1059                                    .and_then(|v| v.get("patched"))
1060                                    .and_then(|p| p.as_array())
1061                                    .and_then(|arr| arr.first())
1062                                    .and_then(|s| s.as_str())
1063                                    .unwrap_or("unknown")
1064                            ),
1065                            patched_versions: vuln.get("versions")
1066                                .and_then(|v| v.get("patched"))
1067                                .and_then(|p| p.as_array())
1068                                .and_then(|arr| arr.first())
1069                                .and_then(|s| s.as_str())
1070                                .map(|s| s.to_string()),
1071                            published_date: advisory.get("date")
1072                                .and_then(|d| d.as_str())
1073                                .and_then(|s| DateTime::parse_from_rfc3339(s).ok())
1074                                .map(|dt| dt.with_timezone(&Utc)),
1075                            references: advisory.get("references")
1076                                .and_then(|r| r.as_array())
1077                                .map(|refs| refs.iter()
1078                                    .filter_map(|r| r.as_str().map(|s| s.to_string()))
1079                                    .collect())
1080                                .unwrap_or_default(),
1081                        };
1082                        
1083                        // Check if we already have this dependency
1084                        if let Some(existing) = vulnerable_deps.iter_mut()
1085                            .find(|vuln_dep: &&mut VulnerableDependency| vuln_dep.name == dep.name && vuln_dep.version == package_version) 
1086                        {
1087                            existing.vulnerabilities.push(vuln_info);
1088                        } else {
1089                            vulnerable_deps.push(VulnerableDependency {
1090                                name: dep.name.clone(),
1091                                version: package_version.to_string(),
1092                                language: Language::Rust,
1093                                vulnerabilities: vec![vuln_info],
1094                            });
1095                        }
1096                    }
1097                }
1098            }
1099        }
1100        
1101        Ok(vulnerable_deps)
1102    }
1103    
1104    fn parse_rustsec_severity(&self, severity: Option<&str>) -> VulnerabilitySeverity {
1105        match severity.map(|s| s.to_lowercase()).as_deref() {
1106            Some("critical") => VulnerabilitySeverity::Critical,
1107            Some("high") => VulnerabilitySeverity::High,
1108            Some("medium") | Some("moderate") => VulnerabilitySeverity::Medium,
1109            Some("low") => VulnerabilitySeverity::Low,
1110            _ => VulnerabilitySeverity::Medium, // Default to medium if not specified
1111        }
1112    }
1113    
1114    fn parse_grype_output(
1115        &self,
1116        output: &[u8],
1117        dependencies: &[DependencyInfo],
1118        language: Language,
1119    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
1120        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
1121        let output_str = String::from_utf8_lossy(output);
1122        
1123        // Parse grype JSON output
1124        let grype_data: serde_json::Value = serde_json::from_str(&output_str)
1125            .map_err(|e| VulnerabilityError::ParseError(
1126                format!("Failed to parse grype output: {}", e)
1127            ))?;
1128        
1129        // Grype JSON structure has a "matches" array
1130        if let Some(matches) = grype_data.get("matches").and_then(|m| m.as_array()) {
1131            for match_obj in matches {
1132                if let Some(obj) = match_obj.as_object() {
1133                    // Get artifact information
1134                    let artifact_name = obj.get("artifact")
1135                        .and_then(|a| a.get("name"))
1136                        .and_then(|n| n.as_str())
1137                        .unwrap_or("");
1138                    
1139                    let artifact_version = obj.get("artifact")
1140                        .and_then(|a| a.get("version"))
1141                        .and_then(|v| v.as_str())
1142                        .unwrap_or("");
1143                    
1144                    // Check if this matches one of our dependencies
1145                    if let Some(dep) = dependencies.iter().find(|d| {
1146                        // Match by artifact name or group:artifact format
1147                        artifact_name.contains(&d.name) || 
1148                        d.name.contains(artifact_name) ||
1149                        d.name.split(':').last() == Some(artifact_name)
1150                    }) {
1151                        // Get vulnerability details
1152                        if let Some(vuln_obj) = obj.get("vulnerability").and_then(|v| v.as_object()) {
1153                            let vuln_id = vuln_obj.get("id")
1154                                .and_then(|id| id.as_str())
1155                                .unwrap_or("unknown")
1156                                .to_string();
1157                            
1158                            let severity = vuln_obj.get("severity")
1159                                .and_then(|s| s.as_str())
1160                                .map(|s| self.parse_grype_severity(s))
1161                                .unwrap_or(VulnerabilitySeverity::Medium);
1162                            
1163                            let description = vuln_obj.get("description")
1164                                .and_then(|d| d.as_str())
1165                                .unwrap_or("")
1166                                .to_string();
1167                            
1168                            let fix_versions = vuln_obj.get("fix")
1169                                .and_then(|f| f.get("versions"))
1170                                .and_then(|v| v.as_array())
1171                                .map(|versions| {
1172                                    versions.iter()
1173                                        .filter_map(|v| v.as_str())
1174                                        .collect::<Vec<_>>()
1175                                        .join(", ")
1176                                });
1177                            
1178                            let vuln_info = VulnerabilityInfo {
1179                                id: vuln_id.clone(),
1180                                severity,
1181                                title: description.clone(),
1182                                description,
1183                                cve: if vuln_id.starts_with("CVE-") {
1184                                    Some(vuln_id.clone())
1185                                } else {
1186                                    None
1187                                },
1188                                ghsa: if vuln_id.starts_with("GHSA-") {
1189                                    Some(vuln_id.clone())
1190                                } else {
1191                                    None
1192                                },
1193                                affected_versions: artifact_version.to_string(),
1194                                patched_versions: fix_versions,
1195                                published_date: None,
1196                                references: vec![],
1197                            };
1198                            
1199                            // Check if we already have this dependency
1200                            if let Some(existing) = vulnerable_deps.iter_mut()
1201                                .find(|vuln_dep| vuln_dep.name == dep.name) 
1202                            {
1203                                // Avoid duplicate vulnerabilities
1204                                if !existing.vulnerabilities.iter().any(|v| v.id == vuln_info.id) {
1205                                    existing.vulnerabilities.push(vuln_info);
1206                                }
1207                            } else {
1208                                vulnerable_deps.push(VulnerableDependency {
1209                                    name: dep.name.clone(),
1210                                    version: dep.version.clone(),
1211                                    language: language.clone(),
1212                                    vulnerabilities: vec![vuln_info],
1213                                });
1214                            }
1215                        }
1216                    }
1217                }
1218            }
1219        }
1220        
1221        Ok(vulnerable_deps)
1222    }
1223    
1224    fn parse_grype_severity(&self, severity: &str) -> VulnerabilitySeverity {
1225        match severity.to_lowercase().as_str() {
1226            "critical" => VulnerabilitySeverity::Critical,
1227            "high" => VulnerabilitySeverity::High,
1228            "medium" => VulnerabilitySeverity::Medium,
1229            "low" => VulnerabilitySeverity::Low,
1230            "negligible" => VulnerabilitySeverity::Info,
1231            _ => VulnerabilitySeverity::Medium,
1232        }
1233    }
1234    
1235    fn parse_owasp_dependency_check_output(
1236        &self,
1237        report_data: &serde_json::Value,
1238        dependencies: &[DependencyInfo],
1239    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
1240        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
1241        
1242        if let Some(deps_array) = report_data.get("dependencies").and_then(|d| d.as_array()) {
1243            for dep_obj in deps_array {
1244                if let Some(vulns) = dep_obj.get("vulnerabilities").and_then(|v| v.as_array()) {
1245                    if vulns.is_empty() {
1246                        continue;
1247                    }
1248                    
1249                    // Extract dependency information
1250                    let file_name = dep_obj.get("fileName")
1251                        .and_then(|f| f.as_str())
1252                        .unwrap_or("");
1253                    
1254                    // Try to match with our dependencies
1255                    let matched_dep = dependencies.iter().find(|d| {
1256                        file_name.contains(&d.name) || 
1257                        dep_obj.get("packages").and_then(|p| p.as_array())
1258                            .map(|packages| packages.iter().any(|pkg| {
1259                                pkg.get("id").and_then(|id| id.as_str())
1260                                    .map(|id| id.contains(&d.name))
1261                                    .unwrap_or(false)
1262                            }))
1263                            .unwrap_or(false)
1264                    });
1265                    
1266                    if let Some(dep) = matched_dep {
1267                        let mut vuln_infos = Vec::new();
1268                        
1269                        for vuln in vulns {
1270                            let severity = vuln.get("severity")
1271                                .and_then(|s| s.as_str())
1272                                .unwrap_or("MEDIUM");
1273                            
1274                            vuln_infos.push(VulnerabilityInfo {
1275                                id: vuln.get("name")
1276                                    .and_then(|n| n.as_str())
1277                                    .unwrap_or("unknown")
1278                                    .to_string(),
1279                                severity: self.parse_owasp_severity(severity),
1280                                title: vuln.get("description")
1281                                    .and_then(|d| d.as_str())
1282                                    .unwrap_or("Unknown vulnerability")
1283                                    .to_string(),
1284                                description: vuln.get("notes")
1285                                    .and_then(|n| n.as_str())
1286                                    .unwrap_or("")
1287                                    .to_string(),
1288                                cve: vuln.get("name")
1289                                    .and_then(|n| n.as_str())
1290                                    .filter(|n| n.starts_with("CVE-"))
1291                                    .map(|s| s.to_string()),
1292                                ghsa: None,
1293                                affected_versions: vuln.get("vulnerableSoftware")
1294                                    .and_then(|vs| vs.as_array())
1295                                    .and_then(|arr| arr.first())
1296                                    .and_then(|v| v.get("versionEndIncluding"))
1297                                    .and_then(|v| v.as_str())
1298                                    .map(|v| format!("<= {}", v))
1299                                    .unwrap_or_else(|| "*".to_string()),
1300                                patched_versions: None, // OWASP DC doesn't provide this directly
1301                                published_date: None,
1302                                references: vuln.get("references")
1303                                    .and_then(|r| r.as_array())
1304                                    .map(|refs| refs.iter()
1305                                        .filter_map(|r| r.get("url").and_then(|u| u.as_str()).map(|s| s.to_string()))
1306                                        .collect())
1307                                    .unwrap_or_default(),
1308                            });
1309                        }
1310                        
1311                        if !vuln_infos.is_empty() {
1312                            vulnerable_deps.push(VulnerableDependency {
1313                                name: dep.name.clone(),
1314                                version: dep.version.clone(),
1315                                language: Language::Java,
1316                                vulnerabilities: vuln_infos,
1317                            });
1318                        }
1319                    }
1320                }
1321            }
1322        }
1323        
1324        Ok(vulnerable_deps)
1325    }
1326    
1327    fn parse_owasp_severity(&self, severity: &str) -> VulnerabilitySeverity {
1328        match severity.to_uppercase().as_str() {
1329            "CRITICAL" => VulnerabilitySeverity::Critical,
1330            "HIGH" => VulnerabilitySeverity::High,
1331            "MEDIUM" | "MODERATE" => VulnerabilitySeverity::Medium,
1332            "LOW" => VulnerabilitySeverity::Low,
1333            _ => VulnerabilitySeverity::Medium, // Default to medium
1334        }
1335    }
1336}
1337
1338#[cfg(test)]
1339mod tests {
1340    use super::*;
1341    
1342    #[test]
1343    fn test_vulnerability_severity_ordering() {
1344        assert!(VulnerabilitySeverity::Critical > VulnerabilitySeverity::High);
1345        assert!(VulnerabilitySeverity::High > VulnerabilitySeverity::Medium);
1346        assert!(VulnerabilitySeverity::Medium > VulnerabilitySeverity::Low);
1347        assert!(VulnerabilitySeverity::Low > VulnerabilitySeverity::Info);
1348    }
1349    
1350    #[test]
1351    fn test_severity_parsing() {
1352        let checker = VulnerabilityChecker::new();
1353        
1354        assert_eq!(
1355            checker.parse_npm_severity("critical"),
1356            VulnerabilitySeverity::Critical
1357        );
1358        assert_eq!(
1359            checker.parse_npm_severity("MODERATE"),
1360            VulnerabilitySeverity::Medium
1361        );
1362    }
1363}