syncable_cli/analyzer/vulnerability/checkers/
javascript.rs

1use std::path::Path;
2use std::process::Command;
3use log::{info, warn};
4use crate::analyzer::dependency_parser::{DependencyInfo, Language};
5use crate::analyzer::runtime::{RuntimeDetector, PackageManager};
6use crate::analyzer::tool_management::ToolDetector;
7use crate::analyzer::vulnerability::{VulnerableDependency, VulnerabilityError, VulnerabilityInfo, VulnerabilitySeverity};
8use super::MutableLanguageVulnerabilityChecker;
9use serde_json::Value as JsonValue;
10
11pub struct JavaScriptVulnerabilityChecker {
12    tool_detector: ToolDetector,
13}
14
15impl JavaScriptVulnerabilityChecker {
16    pub fn new() -> Self {
17        Self {
18            tool_detector: ToolDetector::new(),
19        }
20    }
21    
22    fn execute_audit_for_manager(
23        &mut self,
24        manager: &PackageManager,
25        project_path: &Path,
26        dependencies: &[DependencyInfo],
27    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
28        match manager {
29            PackageManager::Bun => self.execute_bun_audit(project_path, dependencies),
30            PackageManager::Npm => self.execute_npm_audit(project_path, dependencies),
31            PackageManager::Yarn => self.execute_yarn_audit(project_path, dependencies),
32            PackageManager::Pnpm => self.execute_pnpm_audit(project_path, dependencies),
33            PackageManager::Unknown => Ok(None),
34        }
35    }
36    
37    fn execute_bun_audit(
38        &mut self,
39        project_path: &Path,
40        dependencies: &[DependencyInfo],
41    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
42        // Check if bun is available
43        let bun_status = self.tool_detector.detect_tool("bun");
44        if !bun_status.available {
45            warn!("bun not found, skipping bun audit");
46            return Ok(None);
47        }
48        
49        info!("Executing bun audit in {}", project_path.display());
50        
51        // Execute bun audit --json
52        let output = Command::new("bun")
53            .args(&["audit", "--json"])
54            .current_dir(project_path)
55            .output()
56            .map_err(|e| VulnerabilityError::CommandError(
57                format!("Failed to run bun audit: {}", e)
58            ))?;
59        
60        // bun audit returns non-zero exit code when vulnerabilities found
61        // This is expected behavior, not an error
62        if !output.status.success() && !output.stdout.is_empty() {
63            info!("bun audit completed with findings");
64        }
65        
66        if output.stdout.is_empty() {
67            return Ok(None);
68        }
69        
70        // Parse bun audit output
71        let audit_data: serde_json::Value = serde_json::from_slice(&output.stdout)
72            .map_err(|e| VulnerabilityError::ParseError(
73                format!("Failed to parse bun audit output: {}", e)
74            ))?;
75        
76        self.parse_bun_audit_output(&audit_data, dependencies)
77    }
78    
79    fn execute_npm_audit(
80        &mut self,
81        project_path: &Path,
82        dependencies: &[DependencyInfo],
83    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
84        // Check if npm is available
85        let npm_status = self.tool_detector.detect_tool("npm");
86        if !npm_status.available {
87            warn!("npm not found, skipping npm audit");
88            return Ok(None);
89        }
90        
91        info!("Executing npm audit in {}", project_path.display());
92        
93        // Execute npm audit --json
94        let output = Command::new("npm")
95            .args(&["audit", "--json"])
96            .current_dir(project_path)
97            .output()
98            .map_err(|e| VulnerabilityError::CommandError(
99                format!("Failed to run npm audit: {}", e)
100            ))?;
101        
102        // npm audit returns 0 even when vulnerabilities are found
103        // Non-zero exit code indicates an actual error
104        if !output.status.success() && output.stdout.is_empty() {
105            return Err(VulnerabilityError::CommandError(
106                format!("npm audit failed with exit code {}: {}", 
107                    output.status.code().unwrap_or(-1),
108                    String::from_utf8_lossy(&output.stderr))
109            ));
110        }
111        
112        if output.stdout.is_empty() {
113            return Ok(None);
114        }
115        
116        // Parse npm audit output
117        let audit_data: serde_json::Value = serde_json::from_slice(&output.stdout)
118            .map_err(|e| VulnerabilityError::ParseError(
119                format!("Failed to parse npm audit output: {}", e)
120            ))?;
121        
122        self.parse_npm_audit_output(&audit_data, dependencies)
123    }
124    
125    fn execute_yarn_audit(
126        &mut self,
127        project_path: &Path,
128        dependencies: &[DependencyInfo],
129    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
130        // Check if yarn is available
131        let yarn_status = self.tool_detector.detect_tool("yarn");
132        if !yarn_status.available {
133            warn!("yarn not found, skipping yarn audit");
134            return Ok(None);
135        }
136
137        info!("Executing yarn audit in {}", project_path.display());
138
139        // Strategy:
140        // 1) Try Yarn Berry command: yarn npm audit --json (Yarn v2+)
141        // 2) Fallback to classic: yarn audit --json (Yarn v1)
142        // 3) Handle both single-JSON and line-delimited JSON formats
143        let candidates: Vec<Vec<&str>> = vec![
144            vec!["npm", "audit", "--json"],
145            vec!["audit", "--json"],
146        ];
147
148        for args in candidates {
149            let output = match Command::new("yarn").args(&args).current_dir(project_path).output() {
150                Ok(o) => o,
151                Err(e) => {
152                    warn!("Failed to run 'yarn {}': {}", args.join(" "), e);
153                    continue;
154                }
155            };
156
157            // Non-zero with empty stdout is a hard failure; otherwise attempt to parse what we got
158            if !output.status.success() && output.stdout.is_empty() {
159                warn!(
160                    "yarn {} failed (code {:?}): {}",
161                    args.join(" "),
162                    output.status.code(),
163                    String::from_utf8_lossy(&output.stderr)
164                );
165                continue;
166            }
167
168            if output.stdout.is_empty() {
169                // Nothing to parse
170                continue;
171            }
172
173            // Try to parse as a single JSON blob first (be tolerant of banners/noise)
174            if let Some(audit_data) = try_parse_json_tolerant(&output.stdout) {
175                // If it looks like NPM's shape (common for `yarn npm audit`), reuse NPM parser
176                if audit_data.get("vulnerabilities").is_some() {
177                    if let Ok(res) = self.parse_npm_audit_output(&audit_data, dependencies) {
178                        if res.is_some() { return Ok(res); }
179                    }
180                }
181
182                // Otherwise try Yarn object shape
183                if let Ok(res) = self.parse_yarn_audit_output(&audit_data, dependencies) {
184                    if res.is_some() { return Ok(res); }
185                }
186            } else {
187                // If not a single JSON, try line-delimited JSON format (Yarn v1 classic)
188                if let Ok(res) = self.parse_yarn_streaming_audit_lines(&output.stdout, dependencies) {
189                    if res.is_some() { return Ok(res); }
190                }
191            }
192        }
193
194        // If we got here, we couldn't parse Yarn output; don't fail the whole scan
195        warn!("Unable to parse yarn audit output; skipping Yarn results");
196        Ok(None)
197    }
198    
199    fn execute_pnpm_audit(
200        &mut self,
201        project_path: &Path,
202        dependencies: &[DependencyInfo],
203    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
204        // Check if pnpm is available
205        let pnpm_status = self.tool_detector.detect_tool("pnpm");
206        if !pnpm_status.available {
207            warn!("pnpm not found, skipping pnpm audit");
208            return Ok(None);
209        }
210        
211        info!("Executing pnpm audit in {}", project_path.display());
212        
213        // Execute pnpm audit --json
214        let output = Command::new("pnpm")
215            .args(&["audit", "--json"])
216            .current_dir(project_path)
217            .output()
218            .map_err(|e| VulnerabilityError::CommandError(
219                format!("Failed to run pnpm audit: {}", e)
220            ))?;
221        
222        // pnpm audit behavior: returns 0 even when vulnerabilities are found
223        // Non-zero exit code indicates an actual error
224        if !output.status.success() && output.stdout.is_empty() {
225            return Err(VulnerabilityError::CommandError(
226                format!("pnpm audit failed with exit code {}: {}", 
227                    output.status.code().unwrap_or(-1),
228                    String::from_utf8_lossy(&output.stderr))
229            ));
230        }
231        
232        if output.stdout.is_empty() {
233            return Ok(None);
234        }
235        
236        // Parse pnpm audit output
237        let audit_data: serde_json::Value = serde_json::from_slice(&output.stdout)
238            .map_err(|e| VulnerabilityError::ParseError(
239                format!("Failed to parse pnpm audit output: {}", e)
240            ))?;
241        
242        self.parse_pnpm_audit_output(&audit_data, dependencies)
243    }
244    
245    fn parse_bun_audit_output(
246        &self,
247        audit_data: &serde_json::Value,
248        dependencies: &[DependencyInfo],
249    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
250        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
251        
252        // Bun audit JSON structure parsing
253        // Bun returns a JSON object where keys are package names and values are arrays of vulnerabilities
254        if let Some(obj) = audit_data.as_object() {
255            for (package_name, vulnerabilities) in obj {
256                if let Some(vuln_array) = vulnerabilities.as_array() {
257                    // Find matching dependency
258                    if let Some(dep) = dependencies.iter().find(|d| d.name == *package_name) {
259                        let mut package_vulns = Vec::new();
260                        
261                        for vulnerability in vuln_array {
262                            // Extract vulnerability information
263                            let id = vulnerability.get("id").and_then(|i| i.as_u64())
264                                .map(|id| id.to_string())
265                                .unwrap_or("unknown".to_string());
266                            let title = vulnerability.get("title").and_then(|t| t.as_str())
267                                .unwrap_or("Unknown vulnerability").to_string();
268                            let description = vulnerability.get("title").and_then(|t| t.as_str())
269                                .unwrap_or("").to_string();
270                            let severity = self.parse_severity(vulnerability.get("severity").and_then(|s| s.as_str()));
271                            let affected_versions = vulnerability.get("vulnerable_versions").and_then(|v| v.as_str())
272                                .unwrap_or("*").to_string();
273                            let cwe = vulnerability.get("cwe").and_then(|c| c.as_array())
274                                .and_then(|arr| arr.first())
275                                .and_then(|v| v.as_str())
276                                .map(|s| s.to_string());
277                            let url = vulnerability.get("url").and_then(|u| u.as_str())
278                                .map(|s| s.to_string());
279                            
280                            let vuln_info = VulnerabilityInfo {
281                                id,
282                                vuln_type: "security".to_string(),  // Security vulnerability
283                                severity,
284                                title,
285                                description,
286                                cve: cwe.clone(), // Using CWE as CVE for now
287                                ghsa: url.clone().filter(|u| u.contains("GHSA")).map(|u| {
288                                    u.split('/').last().unwrap_or(&u).to_string()
289                                }),
290                                affected_versions,
291                                patched_versions: None, // Bun doesn't provide this directly
292                                published_date: None, // Bun audit may not provide this
293                                references: url.map(|u| vec![u]).unwrap_or_default(),
294                            };
295                            
296                            package_vulns.push(vuln_info);
297                        }
298                        
299                        if !package_vulns.is_empty() {
300                            vulnerable_deps.push(VulnerableDependency {
301                                name: dep.name.clone(),
302                                version: dep.version.clone(),
303                                language: Language::JavaScript,
304                                vulnerabilities: package_vulns,
305                            });
306                        }
307                    }
308                }
309            }
310        }
311        
312        if vulnerable_deps.is_empty() {
313            Ok(None)
314        } else {
315            Ok(Some(vulnerable_deps))
316        }
317    }
318    
319    fn parse_npm_audit_output(
320        &self,
321        audit_data: &serde_json::Value,
322        dependencies: &[DependencyInfo],
323    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
324        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
325        
326        // NPM audit JSON structure parsing
327        // NPM returns a JSON object with a "vulnerabilities" field containing package vulnerabilities
328        if let Some(vulnerabilities) = audit_data.get("vulnerabilities").and_then(|v| v.as_object()) {
329            for (package_name, vulnerability_info) in vulnerabilities {
330                // Find matching dependency
331                if let Some(dep) = dependencies.iter().find(|d| d.name == *package_name) {
332                    let mut package_vulns = Vec::new();
333                    
334                    // Get vulnerability details from the "via" array
335                    if let Some(via) = vulnerability_info.get("via").and_then(|v| v.as_array()) {
336                        for advisory in via {
337                            if let Some(advisory_obj) = advisory.as_object() {
338                                // Skip if this is just a reference to another package
339                                if advisory_obj.contains_key("source") && !advisory_obj.contains_key("title") {
340                                    continue;
341                                }
342                                
343                                let id = advisory_obj.get("source")
344                                    .and_then(|s| s.as_u64())
345                                    .map(|id| id.to_string())
346                                    .or_else(|| advisory_obj.get("url")
347                                        .and_then(|u| u.as_str())
348                                        .and_then(|url| {
349                                            if url.contains("GHSA") {
350                                                url.split('/').last().map(|s| s.to_string())
351                                            } else {
352                                                None
353                                            }
354                                        }))
355                                    .unwrap_or("unknown".to_string());
356                                
357                                let title = advisory_obj.get("title").and_then(|t| t.as_str())
358                                    .unwrap_or("Unknown vulnerability").to_string();
359                                let description = title.clone();
360                                let severity = self.parse_severity(advisory_obj.get("severity").and_then(|s| s.as_str()));
361                                
362                                let range = advisory_obj.get("range").and_then(|r| r.as_str())
363                                    .unwrap_or("*").to_string();
364                                
365                                let cwe = advisory_obj.get("cwe").and_then(|c| c.as_array())
366                                    .and_then(|arr| arr.first())
367                                    .and_then(|v| v.as_str())
368                                    .map(|s| s.to_string());
369                                
370                                let url = advisory_obj.get("url").and_then(|u| u.as_str())
371                                    .map(|s| s.to_string());
372                                
373                                let vuln_info = VulnerabilityInfo {
374                                    id,
375                                    vuln_type: "security".to_string(),  // Security vulnerability
376                                    severity,
377                                    title,
378                                    description,
379                                    cve: cwe.clone(),
380                                    ghsa: url.clone().filter(|u| u.contains("GHSA")).map(|u| {
381                                        u.split('/').last().unwrap_or(&u).to_string()
382                                    }),
383                                    affected_versions: range,
384                                    patched_versions: None, // NPM doesn't provide this directly in via
385                                    published_date: None,
386                                    references: url.map(|u| vec![u]).unwrap_or_default(),
387                                };
388                                
389                                package_vulns.push(vuln_info);
390                            }
391                        }
392                    }
393                    
394                    if !package_vulns.is_empty() {
395                        vulnerable_deps.push(VulnerableDependency {
396                            name: dep.name.clone(),
397                            version: dep.version.clone(),
398                            language: Language::JavaScript,
399                            vulnerabilities: package_vulns,
400                        });
401                    }
402                }
403            }
404        }
405        
406        if vulnerable_deps.is_empty() {
407            Ok(None)
408        } else {
409            Ok(Some(vulnerable_deps))
410        }
411    }
412    
413    fn parse_yarn_audit_output(
414        &self,
415        audit_data: &serde_json::Value,
416        dependencies: &[DependencyInfo],
417    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
418        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
419        
420        // Yarn audit JSON structure parsing
421        // Shape 1: Single object with { data: { advisories: { id: {...} } } } (rare)
422        if let Some(data) = audit_data.get("data").and_then(|d| d.as_object()) {
423            if let Some(advisories) = data.get("advisories").and_then(|a| a.as_object()) {
424                for (advisory_id, advisory) in advisories {
425                    if let Some(advisory_obj) = advisory.as_object() {
426                        let package_name = advisory_obj.get("module_name").and_then(|n| n.as_str()).unwrap_or("").to_string();
427                        if let Some(dep) = dependencies.iter().find(|d| d.name == package_name) {
428                            let (vuln_info, pkg_name) = self.extract_yarn_advisory(advisory_id, advisory_obj);
429                            // Use dep.name to keep version/source consistent
430                            if let Some(existing) = vulnerable_deps.iter_mut().find(|v| v.name == pkg_name) {
431                                existing.vulnerabilities.push(vuln_info);
432                            } else {
433                                vulnerable_deps.push(VulnerableDependency {
434                                    name: dep.name.clone(),
435                                    version: dep.version.clone(),
436                                    language: Language::JavaScript,
437                                    vulnerabilities: vec![vuln_info],
438                                });
439                            }
440                        }
441                    }
442                }
443            }
444        }
445        
446        if vulnerable_deps.is_empty() {
447            Ok(None)
448        } else {
449            Ok(Some(vulnerable_deps))
450        }
451    }
452
453    // Parse Yarn classic line-delimited JSON output
454    fn parse_yarn_streaming_audit_lines(
455        &self,
456        stdout: &[u8],
457        dependencies: &[DependencyInfo],
458    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
459        let mut vulnerable_deps: Vec<VulnerableDependency> = Vec::new();
460        let text = String::from_utf8_lossy(stdout);
461        for line in text.lines() {
462            let line = line.trim();
463            if line.is_empty() { continue; }
464            if let Ok(json) = serde_json::from_str::<serde_json::Value>(line) {
465                if json.get("type").and_then(|t| t.as_str()) == Some("auditAdvisory") {
466                    if let Some(advisory_obj) = json
467                        .get("data")
468                        .and_then(|d| d.get("advisory"))
469                        .and_then(|a| a.as_object())
470                    {
471                        let package_name = advisory_obj
472                            .get("module_name")
473                            .and_then(|n| n.as_str())
474                            .unwrap_or("")
475                            .to_string();
476                        if let Some(dep) = dependencies.iter().find(|d| d.name == package_name) {
477                            let (vuln_info, pkg_name) = self.extract_yarn_advisory(
478                                advisory_obj
479                                    .get("id")
480                                    .and_then(|v| v.as_i64())
481                                    .map(|v| v.to_string())
482                                    .unwrap_or_else(|| "unknown".to_string())
483                                    .as_str(),
484                                advisory_obj,
485                            );
486
487                            if let Some(existing) = vulnerable_deps.iter_mut().find(|v| v.name == pkg_name) {
488                                existing.vulnerabilities.push(vuln_info);
489                            } else {
490                                vulnerable_deps.push(VulnerableDependency {
491                                    name: dep.name.clone(),
492                                    version: dep.version.clone(),
493                                    language: Language::JavaScript,
494                                    vulnerabilities: vec![vuln_info],
495                                });
496                            }
497                        }
498                    }
499                }
500            }
501        }
502
503        if vulnerable_deps.is_empty() { Ok(None) } else { Ok(Some(vulnerable_deps)) }
504    }
505
506    fn extract_yarn_advisory<'a>(
507        &self,
508        advisory_id: impl Into<String>,
509        advisory_obj: &serde_json::Map<String, serde_json::Value>,
510    ) -> (VulnerabilityInfo, String) {
511        let package_name = advisory_obj
512            .get("module_name")
513            .and_then(|n| n.as_str())
514            .unwrap_or("")
515            .to_string();
516        let id = advisory_id.into();
517        let title = advisory_obj.get("title").and_then(|t| t.as_str()).unwrap_or("Unknown vulnerability").to_string();
518        let description = advisory_obj.get("overview").and_then(|o| o.as_str()).unwrap_or("").to_string();
519        let severity = self.parse_severity(advisory_obj.get("severity").and_then(|s| s.as_str()));
520        let vulnerable_versions = advisory_obj.get("vulnerable_versions").and_then(|v| v.as_str()).unwrap_or("*").to_string();
521        let cve = advisory_obj
522            .get("cves")
523            .and_then(|c| c.as_array())
524            .and_then(|arr| arr.first())
525            .and_then(|v| v.as_str())
526            .map(|s| s.to_string());
527        let url = advisory_obj.get("url").and_then(|u| u.as_str()).map(|s| s.to_string());
528
529        let vuln_info = VulnerabilityInfo {
530            id,
531            vuln_type: "security".to_string(),
532            severity,
533            title,
534            description,
535            cve,
536            ghsa: url.clone().filter(|u| u.contains("GHSA")).map(|u| u.split('/').last().unwrap_or(&u).to_string()),
537            affected_versions: vulnerable_versions,
538            patched_versions: advisory_obj.get("patched_versions").and_then(|p| p.as_str()).map(|s| s.to_string()),
539            published_date: None,
540            references: url.map(|u| vec![u]).unwrap_or_default(),
541        };
542
543        (vuln_info, package_name)
544    }
545    
546    fn parse_pnpm_audit_output(
547        &self,
548        audit_data: &serde_json::Value,
549        dependencies: &[DependencyInfo],
550    ) -> Result<Option<Vec<VulnerableDependency>>, VulnerabilityError> {
551        // PNPM audit output can resemble NPM or provide an advisories map similar to Yarn classic
552        if audit_data.get("vulnerabilities").is_some() {
553            return self.parse_npm_audit_output(audit_data, dependencies);
554        }
555
556        if let Some(advisories) = audit_data.get("advisories").cloned() {
557            // Wrap into Yarn-like shape and reuse Yarn parser
558            let yarn_like = serde_json::json!({
559                "data": { "advisories": advisories }
560            });
561            return self.parse_yarn_audit_output(&yarn_like, dependencies);
562        }
563
564        // Some pnpm versions produce per-advisory arrays; attempt best-effort mapping if present
565        if let Some(findings) = audit_data.get("audit").or_else(|| audit_data.get("metadata")).or_else(|| audit_data.get("data")) {
566            // Try npm parser as a reasonable default
567            if let Ok(res) = self.parse_npm_audit_output(audit_data, dependencies) {
568                if res.is_some() { return Ok(res); }
569            }
570        }
571
572        Ok(None)
573    }
574    
575    fn parse_severity(&self, severity: Option<&str>) -> VulnerabilitySeverity {
576        match severity.map(|s| s.to_lowercase()).as_deref() {
577            Some("critical") => VulnerabilitySeverity::Critical,
578            Some("high") => VulnerabilitySeverity::High,
579            Some("moderate") => VulnerabilitySeverity::Medium,
580            Some("medium") => VulnerabilitySeverity::Medium,
581            Some("low") => VulnerabilitySeverity::Low,
582            _ => VulnerabilitySeverity::Medium, // Default to medium if not specified
583        }
584    }
585}
586
587impl MutableLanguageVulnerabilityChecker for JavaScriptVulnerabilityChecker {
588    fn check_vulnerabilities(
589        &mut self,
590        dependencies: &[DependencyInfo],
591        project_path: &Path,
592    ) -> Result<Vec<VulnerableDependency>, VulnerabilityError> {
593        info!("Checking JavaScript/TypeScript dependencies");
594        
595        let runtime_detector = RuntimeDetector::new(project_path.to_path_buf());
596        let detection_result = runtime_detector.detect_js_runtime_and_package_manager();
597
598        info!("Runtime detection: {}", runtime_detector.get_detection_summary());
599
600        // Build execution order: primary detected manager first, then any lockfile-based managers
601        let mut managers = Vec::new();
602        if detection_result.package_manager != crate::analyzer::runtime::PackageManager::Unknown {
603            managers.push(detection_result.package_manager.clone());
604        }
605        for m in runtime_detector.detect_all_package_managers() {
606            if !managers.contains(&m) {
607                managers.push(m);
608            }
609        }
610
611        // Always consider running Bun audit for JS projects if available,
612        // as Bun often surfaces advisories even when other managers don't.
613        if !managers.contains(&crate::analyzer::runtime::PackageManager::Bun)
614            && runtime_detector.is_js_project()
615        {
616            managers.push(crate::analyzer::runtime::PackageManager::Bun);
617        }
618
619        // If still empty but it's a JS project, default to npm as a last resort
620        if managers.is_empty() && runtime_detector.is_js_project() {
621            managers.push(crate::analyzer::runtime::PackageManager::Npm);
622        }
623
624        // Execute audit commands for each selected manager
625        let mut all_vulnerabilities = Vec::new();
626
627        for manager in managers {
628            if let Some(vulns) = self.execute_audit_for_manager(&manager, project_path, dependencies)? {
629                all_vulnerabilities.extend(vulns);
630            }
631        }
632        
633        Ok(all_vulnerabilities)
634    }
635}
636
637// Best-effort tolerant JSON extractor: handles banners/noise by
638// 1) parsing whole buffer, 2) slicing between first '{' and last '}',
639// 3) scanning lines for a valid JSON object.
640fn try_parse_json_tolerant(buf: &[u8]) -> Option<JsonValue> {
641    if let Ok(val) = serde_json::from_slice::<JsonValue>(buf) {
642        return Some(val);
643    }
644    let text = String::from_utf8_lossy(buf);
645    if let (Some(start), Some(end)) = (text.find('{'), text.rfind('}')) {
646        if start < end {
647            if let Ok(val) = serde_json::from_str::<JsonValue>(&text[start..=end]) {
648                return Some(val);
649            }
650        }
651    }
652    for line in text.lines() {
653        let line = line.trim();
654        if !line.starts_with('{') || !line.ends_with('}') { continue; }
655        if let Ok(val) = serde_json::from_str::<JsonValue>(line) {
656            return Some(val);
657        }
658    }
659    None
660}