@clear-capabilities/agentic-security-scanner 0.77.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. package/bin/.agentic-security/findings.json +1907 -0
  2. package/bin/.agentic-security/last-scan.json +1907 -0
  3. package/bin/.agentic-security/last-scan.json.sig +1 -0
  4. package/bin/.agentic-security/scan-history.json +115 -0
  5. package/bin/.agentic-security/streak.json +20 -0
  6. package/bin/agentic-security.js +33 -2
  7. package/dist/178.index.js +1 -1
  8. package/dist/384.index.js +1 -1
  9. package/dist/637.index.js +1 -1
  10. package/dist/718.index.js +106 -0
  11. package/dist/824.index.js +126 -0
  12. package/dist/838.index.js +1 -1
  13. package/dist/agentic-security.mjs +32 -32
  14. package/dist/agentic-security.mjs.sha256 +1 -1
  15. package/package.json +3 -3
  16. package/src/.agentic-security/findings.json +82642 -0
  17. package/src/.agentic-security/last-scan.json +82642 -0
  18. package/src/.agentic-security/last-scan.json.sig +1 -0
  19. package/src/.agentic-security/scan-history.json +10054 -0
  20. package/src/.agentic-security/streak.json +21 -0
  21. package/src/dataflow/.agentic-security/findings.json +3515 -0
  22. package/src/dataflow/.agentic-security/last-scan.json +3515 -0
  23. package/src/dataflow/.agentic-security/last-scan.json.sig +1 -0
  24. package/src/dataflow/.agentic-security/scan-history.json +702 -0
  25. package/src/dataflow/.agentic-security/streak.json +22 -0
  26. package/src/dataflow/async-sequencing.js +16 -7
  27. package/src/dataflow/builtin-summaries.js +131 -0
  28. package/src/dataflow/catalog.js +107 -0
  29. package/src/dataflow/cross-repo.js +75 -1
  30. package/src/dataflow/engine.js +129 -0
  31. package/src/dataflow/implicit-flow.js +24 -6
  32. package/src/dataflow/stub-aware-filter.js +69 -11
  33. package/src/dataflow/summaries.js +28 -3
  34. package/src/engine-parallel.js +70 -0
  35. package/src/engine.js +165 -15
  36. package/src/ir/.agentic-security/findings.json +3777 -0
  37. package/src/ir/.agentic-security/last-scan.json +3777 -0
  38. package/src/ir/.agentic-security/last-scan.json.sig +1 -0
  39. package/src/ir/.agentic-security/scan-history.json +771 -0
  40. package/src/ir/.agentic-security/streak.json +21 -0
  41. package/src/ir/index.js +22 -1
  42. package/src/ir/parser-go.js +403 -0
  43. package/src/ir/parser-js.js +2 -0
  44. package/src/ir/parser-php.js +330 -0
  45. package/src/ir/parser-py.helper.py +137 -11
  46. package/src/ir/parser-rb.js +309 -0
  47. package/src/posture/.agentic-security/findings.json +51562 -0
  48. package/src/posture/.agentic-security/last-scan.json +51562 -0
  49. package/src/posture/.agentic-security/last-scan.json.sig +1 -0
  50. package/src/posture/.agentic-security/scan-history.json +650 -0
  51. package/src/posture/.agentic-security/streak.json +20 -0
  52. package/src/posture/calibration.js +14 -0
  53. package/src/posture/triage.js +13 -0
  54. package/src/report/.agentic-security/findings.json +80 -0
  55. package/src/report/.agentic-security/last-scan.json +80 -0
  56. package/src/report/.agentic-security/last-scan.json.sig +1 -0
  57. package/src/report/.agentic-security/scan-history.json +35 -0
  58. package/src/report/.agentic-security/streak.json +22 -0
  59. package/src/report/index.js +23 -2
  60. package/src/sast/.agentic-security/findings.json +5190 -0
  61. package/src/sast/.agentic-security/last-scan.json +5190 -0
  62. package/src/sast/.agentic-security/last-scan.json.sig +1 -0
  63. package/src/sast/.agentic-security/scan-history.json +408 -0
  64. package/src/sast/.agentic-security/streak.json +20 -0
  65. package/src/sast/cache-poisoning.js +77 -0
  66. package/src/sast/comparison-safety.js +73 -0
  67. package/src/sast/db-taint.js +54 -0
  68. package/src/sast/graphql.js +127 -0
  69. package/src/sast/llm-stored-prompt.js +57 -0
  70. package/src/sast/mutation-xss.js +43 -0
  71. package/src/sast/nosql-injection.js +5 -0
  72. package/src/sast/null-byte-injection.js +76 -0
  73. package/src/sast/redos-nfa.js +338 -0
  74. package/src/sast/sensitive-data-logging.js +73 -0
  75. package/src/sast/weak-password-hash.js +77 -0
  76. package/src/sast/weak-randomness.js +100 -0
  77. package/src/sca/.agentic-security/findings.json +1587 -0
  78. package/src/sca/.agentic-security/last-scan.json +1587 -0
  79. package/src/sca/.agentic-security/last-scan.json.sig +1 -0
  80. package/src/sca/.agentic-security/scan-history.json +36 -0
  81. package/src/sca/.agentic-security/streak.json +21 -0
  82. package/src/sca/llm-function-extract.js +107 -0
  83. package/src/sca/vendor-detect.js +91 -0
@@ -34,7 +34,8 @@
34
34
  import { addPath } from './access-paths.js';
35
35
 
36
36
  export function isImplicitFlowEnabled() {
37
- return process.env.AGENTIC_SECURITY_IMPLICIT_FLOW === '1';
37
+ if (process.env.AGENTIC_SECURITY_IMPLICIT_FLOW === '0') return false;
38
+ return true;
38
39
  }
39
40
 
40
41
  /**
@@ -62,11 +63,25 @@ export function buildImplicitContext(cfg, exprTaint) {
62
63
  const n = cfg.nodes[nid];
63
64
  if (!n) continue;
64
65
  if (n.kind === 'if' && n.cond && exprTaint(n.cond)) {
65
- // Push the consequent at depth+1. We don't have a separate alternate
66
- // edge in this v1 IR `succ` carries both. v2 should add `then`/`else`
67
- // distinguishing edges.
66
+ // Config-constant filter: if condition is `ident === literal` where
67
+ // ident is NOT tainted, skip (it's a config check, not a taint branch).
68
+ if (n.cond.kind === 'binary' && (n.cond.op === '===' || n.cond.op === '==' || n.cond.op === 'Eq') &&
69
+ n.cond.right && n.cond.right.kind === 'literal' &&
70
+ n.cond.left && n.cond.left.kind === 'ident' &&
71
+ !exprTaint(n.cond.left)) {
72
+ for (const s of (n.succ || [])) {
73
+ stack.push({ nid: s, depth, label });
74
+ }
75
+ } else {
76
+ for (const s of (n.succ || [])) {
77
+ stack.push({ nid: s, depth: depth + 1, label: _formatCondLabel(n.cond) });
78
+ }
79
+ }
80
+ } else if (n.kind === 'loop-header' && depth > 0) {
81
+ // Loop-body exclusion: don't escalate implicit depth inside loops —
82
+ // loop iteration count is not a taint channel for most vuln classes.
68
83
  for (const s of (n.succ || [])) {
69
- stack.push({ nid: s, depth: depth + 1, label: _formatCondLabel(n.cond) });
84
+ stack.push({ nid: s, depth: Math.max(depth - 1, 0), label });
70
85
  }
71
86
  } else {
72
87
  for (const s of (n.succ || [])) {
@@ -94,6 +109,9 @@ export function implicitAssignTarget(node, ctx) {
94
109
  if (!node || node.kind !== 'assign') return null;
95
110
  if (!ctx || !ctx.tainted) return null;
96
111
  if (typeof node.target !== 'string') return null;
112
+ // Literal-assignment filter: assigning a constant in a tainted branch
113
+ // is not an implicit information leak.
114
+ if (node.source && node.source.kind === 'literal') return null;
97
115
  return node.target;
98
116
  }
99
117
 
@@ -119,7 +137,7 @@ export function createImplicitFinding(node, conditionLabel) {
119
137
  return {
120
138
  kind: 'taint',
121
139
  implicit: true,
122
- confidence: 0.5,
140
+ confidence: 0.40,
123
141
  vuln: `Implicit flow — variable mutated inside tainted-conditional branch (condition: ${conditionLabel || '?'})`,
124
142
  severity: 'medium',
125
143
  cwe: 'CWE-200',
@@ -72,23 +72,81 @@ function _normalizeType(t) {
72
72
  * Returns the (mutated) findings array with `_stubFilterStats` non-
73
73
  * enumerable sidecar.
74
74
  */
75
- export function applyStubAwareFilter(findings, stubs) {
75
+ const TYPE_GUARD_PATTERNS = [
76
+ { re: /typeof\s+(\w+)\s*===?\s*['"]number['"]/, type: 'number' },
77
+ { re: /typeof\s+(\w+)\s*===?\s*['"]boolean['"]/, type: 'boolean' },
78
+ { re: /Number\.isInteger\s*\(\s*(\w+)\s*\)/, type: 'number' },
79
+ { re: /Number\.isFinite\s*\(\s*(\w+)\s*\)/, type: 'number' },
80
+ { re: /!isNaN\s*\(\s*(\w+)\s*\)/, type: 'number' },
81
+ ];
82
+
83
+ function _extractTypeGuardType(condExpr) {
84
+ if (!condExpr) return null;
85
+ const condStr = _exprToString(condExpr);
86
+ if (!condStr) return null;
87
+ for (const { re, type } of TYPE_GUARD_PATTERNS) {
88
+ if (re.test(condStr)) return type;
89
+ }
90
+ return null;
91
+ }
92
+
93
+ function _exprToString(expr) {
94
+ if (!expr) return null;
95
+ if (expr.kind === 'literal') return String(expr.value || '');
96
+ if (expr.kind === 'ident') return expr.name;
97
+ if (expr.kind === 'binary') return `${_exprToString(expr.left)} ${expr.op} ${_exprToString(expr.right)}`;
98
+ if (expr.kind === 'call') return `${typeof expr.callee === 'string' ? expr.callee : _exprToString(expr.callee)}(${(expr.args || []).map(_exprToString).join(',')})`;
99
+ if (expr.kind === 'member') return `${_exprToString(expr.object)}.${expr.prop}`;
100
+ if (expr.kind === 'unknown') return 'typeof';
101
+ return null;
102
+ }
103
+
104
+ function _hasTypeGuardOnPath(finding, perFileIR) {
105
+ if (!perFileIR || !finding.file) return null;
106
+ const ir = perFileIR[finding.file];
107
+ if (!ir || !ir.functions) return null;
108
+ const fn = ir.functions.find(f => {
109
+ const sinkLine = finding.line || 0;
110
+ return sinkLine >= f.line && sinkLine <= f.line + Object.keys(f.cfg.nodes).length * 3;
111
+ });
112
+ if (!fn) return null;
113
+ for (const node of Object.values(fn.cfg.nodes)) {
114
+ if (node.kind === 'if' && node.cond) {
115
+ const guardType = _extractTypeGuardType(node.cond);
116
+ if (guardType) return guardType;
117
+ }
118
+ }
119
+ return null;
120
+ }
121
+
122
+ export function applyStubAwareFilter(findings, stubs, perFileIR) {
76
123
  if (!Array.isArray(findings) || findings.length === 0) return findings;
77
- if (!stubs || !stubs.signatures) return findings;
78
124
  let demoted = 0;
79
125
  for (const f of findings) {
80
126
  if (!f || f.parser !== 'IR-TAINT') continue;
81
127
  const safeSet = FAMILY_SAFE_TYPES[f.cwe];
82
128
  if (!safeSet) continue;
83
- const sourceType = _sourceTypeFromStubs(f, stubs);
84
- if (!sourceType) continue;
85
- if (!safeSet.has(sourceType)) continue;
86
- f._stubTypeDemoted = true;
87
- f._stubTypeReason = `source type ${sourceType} cannot carry ${f.cwe} metacharacters`;
88
- f._stubTypeOriginalSeverity = f.severity;
89
- const downgrade = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
90
- if (downgrade[f.severity]) f.severity = downgrade[f.severity];
91
- demoted++;
129
+ // Check 1: stub-based type demotion
130
+ const sourceType = stubs ? _sourceTypeFromStubs(f, stubs) : null;
131
+ if (sourceType && safeSet.has(sourceType)) {
132
+ f._stubTypeDemoted = true;
133
+ f._stubTypeReason = `source type ${sourceType} cannot carry ${f.cwe} metacharacters`;
134
+ f._stubTypeOriginalSeverity = f.severity;
135
+ const downgrade = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
136
+ if (downgrade[f.severity]) f.severity = downgrade[f.severity];
137
+ demoted++;
138
+ continue;
139
+ }
140
+ // Check 2: type-guard narrowing on CFG path
141
+ const guardType = _hasTypeGuardOnPath(f, perFileIR);
142
+ if (guardType && safeSet.has(guardType)) {
143
+ f._stubTypeDemoted = true;
144
+ f._stubTypeReason = `type guard narrows to ${guardType}, safe for ${f.cwe}`;
145
+ f._stubTypeOriginalSeverity = f.severity;
146
+ const downgrade = { critical: 'high', high: 'medium', medium: 'low', low: 'info' };
147
+ if (downgrade[f.severity]) f.severity = downgrade[f.severity];
148
+ demoted++;
149
+ }
92
150
  }
93
151
  Object.defineProperty(findings, '_stubFilterStats', {
94
152
  value: { demoted, totalConsidered: findings.length },
@@ -68,20 +68,38 @@ export class SummaryCache {
68
68
  // Compute the summary for a function (or return cached). The `analyze`
69
69
  // callback is the per-function walker that returns
70
70
  // { returnTainted, mutatedParams: Set, taintedGlobals: Set, findings: [] }
71
+ //
72
+ // Fixed-point iteration: when a recursive call returns a bottom stub,
73
+ // re-analyze up to FP_MAX times until the summary stabilizes.
71
74
  compute(qid, taintedParams, analyze) {
72
75
  const k = this._key(qid, taintedParams);
73
- if (this._cache.has(k)) return this._cache.get(k);
76
+ if (this._cache.has(k)) {
77
+ const cached = this._cache.get(k);
78
+ if (!cached._recursive) return cached;
79
+ }
74
80
  if (this._stack.has(qid)) {
75
- // Recursion — return bottom summary; fixed-point iter will refine.
81
+ this._hitRecursion = true;
76
82
  return { returnTainted: false, mutatedParams: new Set(), taintedGlobals: new Set(), findings: [], _recursive: true };
77
83
  }
78
84
  if (++this._iter > this._maxIter) {
79
85
  return { returnTainted: false, mutatedParams: new Set(), taintedGlobals: new Set(), findings: [], _budgetExceeded: true };
80
86
  }
81
87
  this._stack.add(qid);
88
+ this._hitRecursion = false;
82
89
  try {
83
- const summary = analyze(qid, taintedParams);
90
+ let summary = analyze(qid, taintedParams);
84
91
  this._cache.set(k, summary);
92
+ if (this._hitRecursion) {
93
+ const FP_MAX = 3;
94
+ for (let fp = 0; fp < FP_MAX; fp++) {
95
+ if (++this._iter > this._maxIter) break;
96
+ const prev = summary;
97
+ summary = analyze(qid, taintedParams);
98
+ if (_summaryEq(prev, summary)) break;
99
+ this._cache.set(k, summary);
100
+ }
101
+ }
102
+ if (summary._recursive) delete summary._recursive;
85
103
  return summary;
86
104
  } finally {
87
105
  this._stack.delete(qid);
@@ -110,6 +128,13 @@ export class SummaryCache {
110
128
  clear() { this._cache.clear(); this._iter = 0; }
111
129
  }
112
130
 
131
+ function _summaryEq(a, b) {
132
+ if (!a || !b) return a === b;
133
+ if (!!a.returnTainted !== !!b.returnTainted) return false;
134
+ if ((a.mutatedParams?.size || 0) !== (b.mutatedParams?.size || 0)) return false;
135
+ return true;
136
+ }
137
+
113
138
  // Build the entry-taint-state for a callee from a call site:
114
139
  // given the callee's param names + the caller's tainted-var set + the
115
140
  // call args, return a Set of param names that are tainted at entry.
@@ -0,0 +1,70 @@
1
+ // Worker-thread parallelism infrastructure for per-file SAST analysis.
2
+ //
3
+ // Gated behind AGENTIC_SECURITY_PARALLEL=1 (default OFF).
4
+ // When enabled, distributes per-file detector execution across a bounded
5
+ // worker pool (default 2 workers, max 4).
6
+ //
7
+ // Architecture:
8
+ // - Main thread: orchestrates file distribution, collects findings
9
+ // - Workers: receive (filepath, content), run detectors, return findings[]
10
+ // - Bounded queue prevents memory exhaustion on large monorepos
11
+ //
12
+ // v1: stub infrastructure. The actual worker dispatch is deferred until
13
+ // the per-file detectors are refactored into a single function that can
14
+ // be serialized to a worker. Today the detectors import 60+ modules with
15
+ // shared state (e.g., _GLOBAL_JAVA_TAINTED_METHODS), making them
16
+ // non-trivially parallelizable.
17
+
18
+ import { availableParallelism } from 'node:os';
19
+
20
+ export function isParallelEnabled() {
21
+ return process.env.AGENTIC_SECURITY_PARALLEL === '1';
22
+ }
23
+
24
+ export function recommendedWorkerCount() {
25
+ const cpus = availableParallelism();
26
+ return Math.max(1, Math.min(4, Math.floor(cpus / 2)));
27
+ }
28
+
29
+ export function createParallelContext(opts = {}) {
30
+ const workerCount = opts.workers || recommendedWorkerCount();
31
+ return {
32
+ enabled: isParallelEnabled(),
33
+ workerCount,
34
+ filesProcessed: 0,
35
+ totalMs: 0,
36
+ _stats: {
37
+ dispatched: 0,
38
+ completed: 0,
39
+ errors: 0,
40
+ avgMs: 0,
41
+ },
42
+ };
43
+ }
44
+
45
+ export async function runParallelFileScans(files, fileContents, detectorFn, opts = {}) {
46
+ if (!isParallelEnabled()) return null;
47
+
48
+ const ctx = createParallelContext(opts);
49
+ const results = [];
50
+
51
+ // v1 stub: run sequentially but through the parallel context for testing.
52
+ // v2 will use worker_threads with a bounded queue.
53
+ for (const fp of files) {
54
+ const content = fileContents[fp];
55
+ if (!content) continue;
56
+ const t0 = Date.now();
57
+ try {
58
+ const findings = detectorFn(fp, content);
59
+ results.push(...(findings || []));
60
+ ctx._stats.completed++;
61
+ } catch {
62
+ ctx._stats.errors++;
63
+ }
64
+ ctx._stats.dispatched++;
65
+ ctx.totalMs += Date.now() - t0;
66
+ ctx.filesProcessed++;
67
+ }
68
+ ctx._stats.avgMs = ctx.filesProcessed ? Math.round(ctx.totalMs / ctx.filesProcessed) : 0;
69
+ return { findings: results, stats: ctx._stats };
70
+ }
package/src/engine.js CHANGED
@@ -23,6 +23,13 @@ import { scanSpringbootHardening } from './sast/springboot-hardening.js';
23
23
  import { scanLaravelHardening } from './sast/laravel-hardening.js';
24
24
  import { scanSwift } from './sast/swift.js';
25
25
  import { scanDartFlutter } from './sast/dart-flutter.js';
26
+ import { scanWeakRandomness } from './sast/weak-randomness.js';
27
+ import { scanGraphQL as scanGraphQLModule } from './sast/graphql.js';
28
+ import { scanSensitiveDataLogging } from './sast/sensitive-data-logging.js';
29
+ import { scanComparisonSafety } from './sast/comparison-safety.js';
30
+ import { scanWeakPasswordHash } from './sast/weak-password-hash.js';
31
+ import { scanCachePoisoning } from './sast/cache-poisoning.js';
32
+ import { scanNullByteInjection } from './sast/null-byte-injection.js';
26
33
  import { scanLlmTradingAgent } from './sast/llm-trading-agent.js';
27
34
  import { scanMobileManifest } from './sast/mobile-manifest.js';
28
35
  import { scanQuarkusHardening } from './sast/quarkus-hardening.js';
@@ -74,10 +81,10 @@ import { scanXPathInjection } from './sast/xpath-injection.js';
74
81
  import { scanSSTI } from './sast/ssti.js';
75
82
  import { scanOpenRedirect } from './sast/open-redirect.js';
76
83
  import { scanResponseSplitting } from './sast/response-splitting.js';
77
- import { scanStoredPromptInjection } from './sast/llm-stored-prompt.js';
84
+ import { scanStoredPromptInjection, scanStoredPromptInjectionCrossFile } from './sast/llm-stored-prompt.js';
78
85
  import { scanRAGPoisoning } from './sast/rag-poisoning.js';
79
86
  import { scanAgentToolEscalation } from './sast/agent-tool-escalation.js';
80
- import { scanDbTaint } from './sast/db-taint.js';
87
+ import { scanDbTaint, scanDbTaintCrossFile } from './sast/db-taint.js';
81
88
  import { scanSSRFCloudMetadata } from './sast/ssrf-cloud-metadata.js';
82
89
  import { scanMutationXSS } from './sast/mutation-xss.js';
83
90
  import { scanDeserializationGadgets, _detectGadgets } from './sast/deserialization-gadgets.js';
@@ -5444,15 +5451,60 @@ function dedupeFindingsWithEvidence(findings){
5444
5451
  // that export is actually imported or invoked in the codebase.
5445
5452
  let _VULN_FUNCTION_HINTS_DATA;
5446
5453
  try { _VULN_FUNCTION_HINTS_DATA = _require('./sca/vuln-function-hints.json'); } catch(_) { _VULN_FUNCTION_HINTS_DATA = {}; }
5454
+ let _VULN_FUNCTION_HINTS_GENERATED;
5455
+ try { _VULN_FUNCTION_HINTS_GENERATED = _require('./sca/vuln-function-hints-generated.json'); } catch(_) { _VULN_FUNCTION_HINTS_GENERATED = {}; }
5447
5456
  const VULN_FUNCTION_HINTS = {
5448
- "lodash":["merge","defaultsDeep","set","setWith","zipObjectDeep"],
5449
- "jsonwebtoken":["decode"],
5450
- "marked":["parse"],
5457
+ "lodash":["merge","defaultsDeep","set","setWith","zipObjectDeep","template"],
5458
+ "jsonwebtoken":["decode","verify","sign"],
5459
+ "marked":["parse","lexer"],
5451
5460
  "ejs":["render","renderFile","compile"],
5452
- "node-fetch":["default"],
5453
- "xml2js":["parseString"],
5454
- "js-yaml":["load"],
5461
+ "node-fetch":["default","fetch"],
5462
+ "xml2js":["parseString","parseStringPromise"],
5463
+ "js-yaml":["load","loadAll"],
5455
5464
  "minimist":["parse"],
5465
+ "express":["static","urlencoded","json"],
5466
+ "axios":["get","post","put","patch","delete","request","create"],
5467
+ "pg":["query","connect"],
5468
+ "mysql2":["query","execute","createConnection","createPool"],
5469
+ "mongoose":["connect","model","find","findOne","findById","aggregate"],
5470
+ "sequelize":["query","literal","define"],
5471
+ "handlebars":["compile","precompile","registerHelper"],
5472
+ "pug":["compile","render","renderFile"],
5473
+ "sharp":["resize","toBuffer","toFile"],
5474
+ "tar":["extract","create","list"],
5475
+ "glob":["sync","glob"],
5476
+ "cookie":["parse","serialize"],
5477
+ "cookie-parser":["default"],
5478
+ "cors":["default"],
5479
+ "helmet":["default","contentSecurityPolicy"],
5480
+ "passport":["authenticate","initialize","session"],
5481
+ "bcrypt":["hash","compare","genSalt"],
5482
+ "bcryptjs":["hash","compare","genSalt"],
5483
+ "crypto-js":["AES","DES","TripleDES","MD5","SHA1","SHA256","HmacSHA256"],
5484
+ "serialize-javascript":["default"],
5485
+ "shelljs":["exec","which","cat","sed"],
5486
+ "child_process":["exec","execSync","spawn","fork"],
5487
+ "vm2":["VM","NodeVM"],
5488
+ "yaml":["parse","parseDocument"],
5489
+ "dotenv":["config","parse"],
5490
+ "jsonwebtoken":["decode","verify","sign"],
5491
+ "jose":["jwtVerify","SignJWT","compactDecrypt"],
5492
+ "cheerio":["load"],
5493
+ "puppeteer":["launch","connect"],
5494
+ "nodemailer":["createTransport"],
5495
+ "redis":["createClient","get","set","del"],
5496
+ "ioredis":["get","set","del","eval"],
5497
+ "knex":["raw","select","where","insert","update","del"],
5498
+ "prisma":["findUnique","findFirst","findMany","create","update","delete","queryRaw"],
5499
+ "typeorm":["query","createQueryBuilder","getRepository"],
5500
+ "sqlite3":["run","get","all","exec"],
5501
+ "better-sqlite3":["prepare","exec","pragma"],
5502
+ "ws":["on","send","close"],
5503
+ "socket.io":["on","emit","to","broadcast"],
5504
+ "formidable":["parse"],
5505
+ "multer":["single","array","fields"],
5506
+ "path-to-regexp":["compile","parse"],
5507
+ ...(typeof _VULN_FUNCTION_HINTS_GENERATED === 'object' && !Array.isArray(_VULN_FUNCTION_HINTS_GENERATED) ? Object.fromEntries(Object.entries(_VULN_FUNCTION_HINTS_GENERATED).filter(([k])=>!k.startsWith('_'))) : {}),
5456
5508
  ...(typeof _VULN_FUNCTION_HINTS_DATA === 'object' && !Array.isArray(_VULN_FUNCTION_HINTS_DATA) ? Object.fromEntries(Object.entries(_VULN_FUNCTION_HINTS_DATA).filter(([k])=>!k.startsWith('_'))) : {}),
5457
5509
  };
5458
5510
  function markUsedVulnFunctions(supplyChain,fc){
@@ -5477,7 +5529,31 @@ function markUsedVulnFunctions(supplyChain,fc){
5477
5529
  }
5478
5530
  for(const sc of supplyChain||[]){
5479
5531
  if(sc.type!=='vulnerable_dep')continue;
5480
- const hints=VULN_FUNCTION_HINTS[sc.name];if(!hints)continue;
5532
+ // Merge hints: hardcoded → OSV ecosystem_specific → skip if none
5533
+ const hardcoded=VULN_FUNCTION_HINTS[sc.name]||[];
5534
+ const osvFns=Array.isArray(sc.osvVulnFunctions)?sc.osvVulnFunctions.map(f=>{const d=f.lastIndexOf('.');return d>0?f.slice(d+1):f;}):[];
5535
+ const allFns=[...new Set([...hardcoded,...osvFns])];
5536
+ if(!allFns.length){sc.functionReachable='unknown';sc.noKnownCallSite=true;sc._hintSource='none';continue;}
5537
+ sc._hintSource=osvFns.length?(hardcoded.length?'hardcoded+osv':'osv'):'hardcoded';
5538
+ // Search codebase for these functions (if not already searched via VULN_FUNCTION_HINTS)
5539
+ if(osvFns.length&&!hardcoded.length){
5540
+ for(const[fp,content] of Object.entries(fc)){
5541
+ const lines=content.split('\n');
5542
+ for(const fn of osvFns){
5543
+ const shortFn=fn.lastIndexOf('.')>0?fn.slice(fn.lastIndexOf('.')+1):fn;
5544
+ const re=new RegExp(`\\b${shortFn.replace(/\W/g,'\\$&')}\\b`,'g');
5545
+ for(let li=0;li<lines.length;li++){
5546
+ if(re.test(lines[li])){
5547
+ if(!perFile[sc.name])perFile[sc.name]=[];
5548
+ perFile[sc.name].push({pkg:sc.name,fn:shortFn,file:fp,line:li+1});
5549
+ if(!used[sc.name])used[sc.name]=new Set();
5550
+ used[sc.name].add(shortFn);
5551
+ }
5552
+ re.lastIndex=0;
5553
+ }
5554
+ }
5555
+ }
5556
+ }
5481
5557
  sc.usedVulnerableFunctions=[...(used[sc.name]||[])];
5482
5558
  const sites=(perFile[sc.name]||[]);
5483
5559
  const seen=new Set();
@@ -5971,7 +6047,10 @@ function _parsePackageLockJson(text,filePath){
5971
6047
  const parts=scoped?name.slice(1).split('/'):['',name];
5972
6048
  const group=scoped?`@${parts[0]}`:'';
5973
6049
  const pkgName=scoped?parts[1]:name;
6050
+ const depChain=path.split('node_modules/').filter(Boolean);
6051
+ const isDirect=depChain.length<=1;
5974
6052
  out.push({name,version:ver,group,scope:info.dev?'optional':'required',
6053
+ depChain,isDirect,
5975
6054
  purl:_makePurl('npm',pkgName,ver,group),ecosystem:'npm',filePath,isUnpinned:false});
5976
6055
  }
5977
6056
  }catch(_){}return out;
@@ -6393,7 +6472,13 @@ async function queryOSV(components,allFileContents){
6393
6472
  const resp=await fetch(`https://api.osv.dev/v1/vulns/${vid}`);
6394
6473
  const d=await resp.json();
6395
6474
  const fixedVersions=new Set();
6396
- for(const aff of(d.affected||[]))for(const rng of(aff.ranges||[]))for(const ev of(rng.events||[]))if(ev.fixed)fixedVersions.add(ev.fixed);
6475
+ const osvVulnFunctions=[];
6476
+ for(const aff of(d.affected||[])){
6477
+ for(const rng of(aff.ranges||[]))for(const ev of(rng.events||[]))if(ev.fixed)fixedVersions.add(ev.fixed);
6478
+ const es=aff.ecosystem_specific||aff.database_specific||{};
6479
+ if(Array.isArray(es.vulnerable_functions))osvVulnFunctions.push(...es.vulnerable_functions);
6480
+ if(Array.isArray(es.imports))for(const imp of es.imports)if(Array.isArray(imp.symbols))osvVulnFunctions.push(...imp.symbols);
6481
+ }
6397
6482
  let severity='medium';
6398
6483
  const db=d.database_specific||{};
6399
6484
  if(db.severity)severity=db.severity.toLowerCase()==='moderate'?'medium':db.severity.toLowerCase();
@@ -6409,6 +6494,7 @@ async function queryOSV(components,allFileContents){
6409
6494
  vuln={id:vid,description:(d.summary||d.details||'No description.').slice(0,300),
6410
6495
  fixedVersions:[...fixedVersions].sort(),
6411
6496
  aliases:(d.aliases||[]).filter(a=>a.startsWith('CVE-')),
6497
+ osvVulnFunctions:[...new Set(osvVulnFunctions)],
6412
6498
  severity,cvssVector,hasKnownAttackRef};
6413
6499
  _osvCacheSet('vuln:'+vid,vuln);
6414
6500
  }catch(_){continue;}
@@ -6419,7 +6505,7 @@ async function queryOSV(components,allFileContents){
6419
6505
  results.push({type:'vulnerable_dep',name:comp.name,version:comp.version,ecosystem:comp.ecosystem,
6420
6506
  purl:comp.purl,osvId:vid,cveAliases:vuln.aliases,description:vuln.description,
6421
6507
  fixedVersions:vuln.fixedVersions,severity:vuln.severity,cvssVector:vuln.cvssVector,
6422
- hasKnownAttackRef:vuln.hasKnownAttackRef,reachable:comp.reachable,scope:comp.scope,
6508
+ hasKnownAttackRef:vuln.hasKnownAttackRef,osvVulnFunctions:vuln.osvVulnFunctions||[],reachable:comp.reachable,scope:comp.scope,
6423
6509
  file:comp.filePath,
6424
6510
  // kept for generateRecs() compat
6425
6511
  advisory:`${vid}${cveStr}, ${vuln.description}`,
@@ -6605,7 +6691,10 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
6605
6691
  // variants, OWASP Benchmark's helpers package). Roadmap item #5.
6606
6692
  try { _GLOBAL_JAVA_TAINTED_METHODS = _buildGlobalJavaTaintedMethodIndex(fileContents); }
6607
6693
  catch { _GLOBAL_JAVA_TAINTED_METHODS = new Set(); }
6608
- const files=Object.keys(fileContents).filter(f=>shouldScan(f) && !_isPathIgnored(f));const fc={},pfr={};const aR=[],aF=[],aSrc=[],aSink=[],aSan=[],aLogic=[],aSupply=[],aSecrets=[],aCiphersRest=[],aCiphersTransit=[];let i=0;for(const p of files){i++;setProgress({current:i,total:files.length,file:p.split("/").pop(),phase:"Scanning"});try{const c=fileContents[p];if(!c||c.length>500000)continue;const _avgLine=c.length/Math.max(c.split('\n').length,1);if(_avgLine>400&&c.length>10000)continue;fc[p]=c;aR.push(...scanRoutes(p,c));const ta=performAnalysis(p,c);pfr[p]=ta;aF.push(...ta.findings);aSrc.push(...ta.sources);aSink.push(...ta.sinks);aSan.push(...ta.sanitizers);aLogic.push(...scanLogicVulns(p,c));aSecrets.push(...scanCredentials(p,c));aF.push(...scanStructuralVulns(p,c));aF.push(...scanExtraStructural(p,c));aF.push(...scanAliasedSinks(p,c));aF.push(...scanJavaSAST(p,c));aF.push(...scanJavaBenchExtras(p,c));aLogic.push(...scanMiddlewareOrdering(p,c));aLogic.push(...scanReDoS(p,c));aLogic.push(...scanTodosNearSecurity(p,c));aSecrets.push(...scanEntropySecrets(p,c));const cp=scanCiphers(p,c);aCiphersRest.push(...cp.atRest);aCiphersTransit.push(...cp.inTransit);if(/\.(graphql|gql)$/i.test(p))aF.push(...scanGraphQL(p,c));aF.push(...scanIaC(p,c));
6694
+ const _perFileTimeoutMs = parseInt(process.env.AGENTIC_SECURITY_PER_FILE_TIMEOUT_MS || '10000', 10);
6695
+ const _fileTimings = [];
6696
+ let _filesSkipped = 0, _filesTimedOut = 0;
6697
+ const files=Object.keys(fileContents).filter(f=>shouldScan(f) && !_isPathIgnored(f));const fc={},pfr={};const aR=[],aF=[],aSrc=[],aSink=[],aSan=[],aLogic=[],aSupply=[],aSecrets=[],aCiphersRest=[],aCiphersTransit=[];let i=0;for(const p of files){i++;const _ft0=Date.now();setProgress({current:i,total:files.length,file:p.split("/").pop(),phase:"Scanning"});try{const c=fileContents[p];if(!c||c.length>500000){_filesSkipped++;continue;}const _avgLine=c.length/Math.max(c.split('\n').length,1);if(_avgLine>400&&c.length>10000)continue;fc[p]=c;aR.push(...scanRoutes(p,c));const ta=performAnalysis(p,c);pfr[p]=ta;aF.push(...ta.findings);aSrc.push(...ta.sources);aSink.push(...ta.sinks);aSan.push(...ta.sanitizers);aLogic.push(...scanLogicVulns(p,c));aSecrets.push(...scanCredentials(p,c));aF.push(...scanStructuralVulns(p,c));aF.push(...scanExtraStructural(p,c));aF.push(...scanAliasedSinks(p,c));aF.push(...scanJavaSAST(p,c));aF.push(...scanJavaBenchExtras(p,c));aLogic.push(...scanMiddlewareOrdering(p,c));aLogic.push(...scanReDoS(p,c));aLogic.push(...scanTodosNearSecurity(p,c));aSecrets.push(...scanEntropySecrets(p,c));const cp=scanCiphers(p,c);aCiphersRest.push(...cp.atRest);aCiphersTransit.push(...cp.inTransit);if(/\.(graphql|gql)$/i.test(p))aF.push(...scanGraphQL(p,c));aF.push(...scanIaC(p,c));
6609
6698
  aF.push(...scanLLM(p,c));
6610
6699
  aF.push(...scanLLMOwasp(p,c));
6611
6700
  aLogic.push(...scanBusinessLogic(p,c));
@@ -6621,6 +6710,13 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
6621
6710
  aF.push(...scanLaravelHardening(p,c));
6622
6711
  aF.push(...scanSwift(p,c));
6623
6712
  aF.push(...scanDartFlutter(p,c));
6713
+ aF.push(...scanWeakRandomness(p,c));
6714
+ aF.push(...scanGraphQLModule(p,c));
6715
+ aF.push(...scanSensitiveDataLogging(p,c));
6716
+ aF.push(...scanComparisonSafety(p,c));
6717
+ aF.push(...scanWeakPasswordHash(p,c));
6718
+ aF.push(...scanCachePoisoning(p,c));
6719
+ aF.push(...scanNullByteInjection(p,c));
6624
6720
  aF.push(...scanLlmTradingAgent(p,c));
6625
6721
  aF.push(...scanMobileManifest(p,c));
6626
6722
  aF.push(...scanQuarkusHardening(p,c));
@@ -6669,7 +6765,11 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
6669
6765
  aF.push(...scanMutationXSS(p,c));
6670
6766
  aF.push(...scanKotlin(p,c));
6671
6767
  aF.push(...scanRuby(p,c));
6672
- aF.push(...scanPhp(p,c));}catch(_){}if(i%5===0)await new Promise(r=>setTimeout(r,0));}
6768
+ aF.push(...scanPhp(p,c));
6769
+ const _ftElapsed=Date.now()-_ft0;
6770
+ if(_ftElapsed>_perFileTimeoutMs){aF.push({id:`file-timeout:${p}`,file:p,line:0,vuln:`File analysis exceeded ${_perFileTimeoutMs}ms (${_ftElapsed}ms)`,severity:'info',parser:'ENGINE',confidence:0.5,_timeout:true});_filesTimedOut++;}
6771
+ _fileTimings.push({file:p,ms:_ftElapsed});
6772
+ }catch(_){_fileTimings.push({file:p,ms:Date.now()-_ft0,error:true});}if(i%5===0)await new Promise(r=>setTimeout(r,0));}
6673
6773
  // Deserialization-gadget detector runs once with full-tree context (it needs
6674
6774
  // manifest contents to know which gadget libs are on the classpath).
6675
6775
  try {
@@ -6817,26 +6917,48 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
6817
6917
  setProgress({current:i,total:files.length,file:"OSV vulnerability database...",phase:"SCA"});
6818
6918
  const allFileContents={...fc, ...depFileContents};
6819
6919
  const components=parseManifests(allFileContents);
6920
+ try{const{detectVendoredLibraries}=await import('./sca/vendor-detect.js');const vendored=detectVendoredLibraries(fc);for(const v of vendored){const key=`${v.ecosystem}:${v.name}:${v.version}`;if(!components.some(c=>`${c.ecosystem}:${c.name}:${c.version}`===key))components.push({...v,group:'',purl:`pkg:${v.ecosystem}/${v.name}@${v.version}`,filePath:v.file,isUnpinned:false,reachable:true});}}catch(_){}
6820
6921
  const reach=buildReachabilitySet(fc);
6821
6922
  const reachabilitySet=reach.imported;
6822
- components.forEach(c=>{c.reachable=reachabilitySet.has(c.name.toLowerCase())||(c.ecosystem==='pypi'&&reachabilitySet.has(c.name.replace(/-/g,'_').toLowerCase()));});
6923
+ components.forEach(c=>{
6924
+ c.reachable=reachabilitySet.has(c.name.toLowerCase())||(c.ecosystem==='pypi'&&reachabilitySet.has(c.name.replace(/-/g,'_').toLowerCase()));
6925
+ c.isBuildOnly=(c.scope==='optional'&&!c.reachable);
6926
+ if(!c.isDirect&&c.isDirect!==false)c.isDirect=!c.depChain||!c.depChain.length||c.depChain.length<=1;
6927
+ });
6823
6928
  let supplyChain=[];try{supplyChain=await queryOSV(components,allFileContents);}catch(_){supplyChain=[];}
6824
6929
  // Feat-9: enrich SCA findings with EPSS abuse-probability scores
6825
6930
  try{supplyChain=await _enrichWithEPSS(supplyChain);}catch(_){}
6826
6931
  // 0.10.0: enrich SCA findings with CISA KEV (CISA KEV catalog)
6827
6932
  try{supplyChain=await _enrichWithKEV(supplyChain);}catch(_){}
6828
6933
  try{markUsedVulnFunctions(supplyChain,fc);}catch(_){}
6934
+ // LLM-assisted function extraction for CVEs without hints (opt-in: AGENTIC_SECURITY_LLM_SCA=1)
6935
+ if(process.env.AGENTIC_SECURITY_LLM_SCA==='1'){try{const{extractVulnFunctionsViaLLM}=await import('./sca/llm-function-extract.js');const enriched=await extractVulnFunctionsViaLLM(supplyChain);if(enriched.length){markUsedVulnFunctions(supplyChain,fc);}}catch(_){}}
6829
6936
  setProgress({current:i,total:files.length,file:"Registry metadata...",phase:"SCA"});
6830
6937
  let registryInfo=new Map();try{registryInfo=await queryRegistries(components);}catch(_){}
6831
6938
  const dd=(a,k)=>[...new Map(a.map(x=>[k(x),x])).values()];
6832
6939
  // 0.6.0 Feat-1: annotate function-level reachability on SCA findings
6833
6940
  try { _annotateFunctionReachability(supplyChain,dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`).map(r=>({...r})),callGraph,fc); } catch(_) {}
6941
+ // Reachability tier classification for SCA findings
6942
+ for(const sc of supplyChain||[]){
6943
+ if(sc.type!=='vulnerable_dep')continue;
6944
+ if(sc.functionReachable==='reachable')sc.reachabilityTier='function-reachable';
6945
+ else if(sc.functionReachable==='unreachable')sc.reachabilityTier='unreachable';
6946
+ else if(sc.reachable)sc.reachabilityTier='import-reachable';
6947
+ else if(sc.isBuildOnly||sc.scope==='optional'&&!sc.reachable){sc.reachabilityTier='build-only';sc.isBuildOnly=true;if(sc.severity==='critical')sc.severity='medium';else if(sc.severity==='high')sc.severity='low';}
6948
+ else if(sc.scope==='required')sc.reachabilityTier='manifest-only';
6949
+ else sc.reachabilityTier='transitive-only';
6950
+ if(sc.reachabilityTier==='transitive-only'){sc.unreachable=true;sc._reachabilityDemoted=true;}
6951
+ }
6952
+ // Early dedup: collapse duplicates BEFORE the annotation pipeline to reduce work.
6953
+ try{const _earlyMap=new Map();for(const f of aF){const k=`${f.file||''}:${f.line||0}:${f.vuln||''}`;if(!_earlyMap.has(k))_earlyMap.set(k,f);}aF.length=0;aF.push(..._earlyMap.values());}catch(_){}
6834
6954
  // Sort findings: critical first, then structural patterns last within same severity
6835
6955
  aF.sort((a,b)=>({critical:0,high:1,medium:2,low:3}[a.severity]??4)-({critical:0,high:1,medium:2,low:3}[b.severity]??4));
6836
6956
  const vulnsByKey={};for(const sc of supplyChain.filter(s=>s.type==='vulnerable_dep')){const k=`${sc.ecosystem}:${sc.name}:${sc.version}`;if(!vulnsByKey[k])vulnsByKey[k]=[];vulnsByKey[k].push(sc);}
6837
6957
  const attackResult=computeAttackPathComponents(aF,components,reach.byFile);
6838
6958
  for(const[key,paths]of attackResult.pathsByKey){const[eco,name,...vp]=key.split(':');const ver=vp.join(':');for(const f of paths){if(!f.linkedComponents)f.linkedComponents=[];if(!f.linkedComponents.some(c=>c.name===name&&c.ecosystem===eco))f.linkedComponents.push({ecosystem:eco,name,version:ver});}}
6839
6959
  const annotatedComponents=components.map(c=>{const key=`${c.ecosystem}:${c.name}:${c.version}`;const vulns=vulnsByKey[key]||[];const riKey=c.ecosystem==='maven'&&c.group?`maven:${c.group}/${c.name}`:`${c.ecosystem}:${c.name}`;const ri=registryInfo.get(riKey)||{};const latestVersion=ri.latestVersion||'';const vd=(ri.versions||{})[c.version]||{};const isDeprecated=typeof vd.deprecated==='string'&&vd.deprecated.length>0;const deprecationMessage=isDeprecated?vd.deprecated:'';const isOutdated=!isDeprecated&&typeof vd.outdated==='string'&&vd.outdated.length>0;const outdatedMessage=isOutdated?vd.outdated:'';const license=ri.license||vd.license||'';return{...c,vulns,hasVulns:vulns.length>0,hasAttackPath:attackResult.flagged.has(key),attackPaths:attackResult.pathsByKey.get(key)||[],latestVersion,isDeprecated,deprecationMessage,isOutdated,outdatedMessage,license};});
6960
+ try{aF.push(...scanDbTaintCrossFile(fc));}catch(_){}
6961
+ try{aF.push(...scanStoredPromptInjectionCrossFile(fc));}catch(_){}
6840
6962
  let finalFindings;try{finalFindings=dedupeFindingsWithEvidence(aF);}catch(_){finalFindings=dd(aF,f=>f.id);}
6841
6963
  // 0.34.6: filter out Java FPs where a sanitizer pattern (argv-form ProcessBuilder,
6842
6964
  // parameterized prepareStatement, constant-folded dead-branch) is present.
@@ -7115,6 +7237,33 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7115
7237
  f.validator_verdict = 'unvalidated';
7116
7238
  }
7117
7239
  finalFindings.push(...irFindings);
7240
+ // Java SCA enrichment: use deep-mode IR call graph to improve Java function reachability
7241
+ try {
7242
+ for (const sc of supplyChain) {
7243
+ if (sc.type !== 'vulnerable_dep' || sc.ecosystem !== 'maven') continue;
7244
+ if (sc.functionReachable === 'reachable') continue;
7245
+ const allFns = [...(sc.osvVulnFunctions || []), ...(VULN_FUNCTION_HINTS[sc.name] || [])];
7246
+ if (!allFns.length) continue;
7247
+ for (const fn of callGraph.functions ? callGraph.functions.values() : []) {
7248
+ if (!fn.cfg || !fn.cfg.nodes) continue;
7249
+ for (const node of Object.values(fn.cfg.nodes)) {
7250
+ if (node.kind !== 'call') continue;
7251
+ const callee = typeof node.callee === 'string' ? node.callee : null;
7252
+ if (!callee) continue;
7253
+ const shortCallee = callee.includes('.') ? callee.split('.').pop() : callee;
7254
+ if (allFns.some(f => f === shortCallee || f === callee)) {
7255
+ sc.functionReachable = 'reachable';
7256
+ sc.reachabilityTier = 'function-reachable';
7257
+ if (!sc.vulnerableFunctionCallSites) sc.vulnerableFunctionCallSites = [];
7258
+ sc.vulnerableFunctionCallSites.push({ pkg: sc.name, fn: shortCallee, file: fn.file, line: node.line });
7259
+ sc._javaIrEnriched = true;
7260
+ break;
7261
+ }
7262
+ }
7263
+ if (sc.functionReachable === 'reachable') break;
7264
+ }
7265
+ }
7266
+ } catch { /* Java SCA enrichment is best-effort */ }
7118
7267
  } catch (e) {
7119
7268
  // Deep mode is best-effort. A parser blowup in one file shouldn't kill
7120
7269
  // the scan — fall back to the pattern-only result.
@@ -7308,7 +7457,8 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7308
7457
  // v3 next-gen: why-fired provenance is captured LAST so it reflects the
7309
7458
  // final state of each finding after every other annotator has run.
7310
7459
  _runAnnotator("annotateWhyFired", () => { annotateWhyFired(finalFindings, {}); });
7311
- return{routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,suppressions:_getSuppressions(),_v3,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors};}
7460
+ const _scanMeta={filesScanned:files.length,filesSkipped:_filesSkipped,filesTimedOut:_filesTimedOut,fileTimings:_fileTimings.sort((a,b)=>b.ms-a.ms).slice(0,20),findingsBySeverity:{critical:finalFindings.filter(f=>f.severity==='critical').length,high:finalFindings.filter(f=>f.severity==='high').length,medium:finalFindings.filter(f=>f.severity==='medium').length,low:finalFindings.filter(f=>f.severity==='low').length,info:finalFindings.filter(f=>f.severity==='info').length}};
7461
+ return{routes:dd(aR,r=>`${r.method}:${r.path}:${r.file}:${r.line}`),findings:finalFindings,sources:aSrc,sinks:aSink,sanitizers:aSan,filesScanned:files.length,crossFileCount:cf.length,logicVulns:aLogic,supplyChain,components:annotatedComponents,secrets:aSecrets,ciphers:{atRest:aCiphersRest,inTransit:aCiphersTransit},pfr,fc,suppressions:_getSuppressions(),_v3,_scanMeta,_engineErrors:{cppDataflowParseErrors:_cppDataflowParseErrors.value},annotatorErrors:_annotatorErrors};}
7312
7462
 
7313
7463
  // Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
7314
7464
  // Orphans (no finding linkage) are bucketed by file-local heuristic so the UI shows binary states only.