@clear-capabilities/agentic-security-scanner 0.84.1 → 0.118.2

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 (125) hide show
  1. package/CHANGELOG.md +404 -0
  2. package/dist/178.index.js +1 -1
  3. package/dist/384.index.js +1 -1
  4. package/dist/637.index.js +1 -1
  5. package/dist/838.index.js +1 -1
  6. package/dist/agentic-security.mjs +84 -84
  7. package/dist/agentic-security.mjs.sha256 +1 -1
  8. package/package.json +12 -6
  9. package/src/.agentic-security/dpia.md +26 -0
  10. package/src/.agentic-security/pqc-migration-plan.json +80 -0
  11. package/src/.agentic-security/pqc-migration-plan.md +32 -0
  12. package/src/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
  13. package/src/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
  14. package/src/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
  15. package/src/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
  16. package/src/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
  17. package/src/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
  18. package/src/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
  19. package/src/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
  20. package/src/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
  21. package/src/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
  22. package/src/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
  23. package/src/.agentic-security/threat-model.json +3702 -0
  24. package/src/.agentic-security/threat-model.md +118 -0
  25. package/src/dataflow/.agentic-security/dpia.md +26 -0
  26. package/src/dataflow/.agentic-security/sbom-history/15d7c4d20f9d968ebec29445c9e9b3fc6f74e6a1.json +6 -0
  27. package/src/dataflow/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
  28. package/src/dataflow/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
  29. package/src/dataflow/.agentic-security/sbom-history/8869fe64035bec0152d92ece961e9369d8637076.json +6 -0
  30. package/src/dataflow/.agentic-security/sbom-history/9c93b15e8cf3ff2ddb8cdae92371b82e178b463c.json +6 -0
  31. package/src/dataflow/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
  32. package/src/dataflow/.agentic-security/threat-model.json +161 -0
  33. package/src/dataflow/.agentic-security/threat-model.md +43 -0
  34. package/src/dataflow/CLAUDE.md +17 -5
  35. package/src/dataflow/catalog.js +9 -0
  36. package/src/dataflow/engine.js +30 -4
  37. package/src/dataflow/index.js +8 -0
  38. package/src/dataflow/proof-gate.js +95 -0
  39. package/src/dataflow/summaries.js +44 -5
  40. package/src/engine.js +199 -5
  41. package/src/ir/.agentic-security/dpia.md +26 -0
  42. package/src/ir/.agentic-security/exploit-bundles.json +77 -0
  43. package/src/ir/.agentic-security/sbom-history/73166872888d9ddcbe98645af818ecc7d8917b07.json +6 -0
  44. package/src/ir/.agentic-security/sbom-history/b5f468a158f6a145aedd8680841c106b9809f16b.json +6 -0
  45. package/src/ir/.agentic-security/threat-model.json +143 -0
  46. package/src/ir/.agentic-security/threat-model.md +39 -0
  47. package/src/ir/CLAUDE.md +1 -0
  48. package/src/ir/callgraph.js +12 -1
  49. package/src/ir/tree-sitter-loader.js +108 -0
  50. package/src/posture/.agentic-security/dpia.md +26 -0
  51. package/src/posture/.agentic-security/pqc-migration-plan.json +65 -0
  52. package/src/posture/.agentic-security/pqc-migration-plan.md +30 -0
  53. package/src/posture/.agentic-security/sbom-history/0724c133d2943c87555695b5b2adcfcfc3373a2d.json +6 -0
  54. package/src/posture/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +6 -0
  55. package/src/posture/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
  56. package/src/posture/.agentic-security/sbom-history/b7ac2dada4a9216ccfe8969fc57f988c83f46975.json +6 -0
  57. package/src/posture/.agentic-security/threat-model.json +2038 -0
  58. package/src/posture/.agentic-security/threat-model.md +73 -0
  59. package/src/posture/auditor-walkthrough.js +1 -1
  60. package/src/posture/corpus-status.js +74 -0
  61. package/src/posture/coverage-report.js +113 -0
  62. package/src/posture/holdout-eval.js +57 -0
  63. package/src/posture/pr-augment.js +1 -1
  64. package/src/posture/router.js +37 -7
  65. package/src/report/.agentic-security/dpia.md +26 -0
  66. package/src/report/.agentic-security/sbom-history/7d45b5e03804aac084b4a2b4dc8c6f10107d2005.json +6 -0
  67. package/src/report/.agentic-security/sbom-history/f94be839aa6a1011f53da016416a561698b8f338.json +6 -0
  68. package/src/report/.agentic-security/threat-model.json +7 -0
  69. package/src/report/.agentic-security/threat-model.md +22 -0
  70. package/src/report/index.js +18 -1
  71. package/src/sast/.agentic-security/dpia.md +26 -0
  72. package/src/sast/.agentic-security/pqc-migration-plan.json +65 -0
  73. package/src/sast/.agentic-security/pqc-migration-plan.md +30 -0
  74. package/src/sast/.agentic-security/sbom-history/043a7b401c68f5a298c83d4c42e94f118ff2a89a.json +6 -0
  75. package/src/sast/.agentic-security/sbom-history/0775d96ba4cf814520d02760cfb3010760c7684f.json +6 -0
  76. package/src/sast/.agentic-security/sbom-history/20d77ad7363a82777d506dedbbb1ecfed52f094a.json +6 -0
  77. package/src/sast/.agentic-security/sbom-history/29300441d3460832137a8e51a92b74068cad2f15.json +6 -0
  78. package/src/sast/.agentic-security/sbom-history/3852ad2aa0e50951bed0c930c29d1861aa13d048.json +6 -0
  79. package/src/sast/.agentic-security/sbom-history/3c6b3205f04e26f7654943d465281d00e0fa395e.json +6 -0
  80. package/src/sast/.agentic-security/sbom-history/3d96e2daec97e3226c9679bac6e64a89ca10bfe9.json +6 -0
  81. package/src/sast/.agentic-security/sbom-history/5282502e090013219eb006f7e02d8c983bc29c8e.json +6 -0
  82. package/src/sast/.agentic-security/sbom-history/5f0b136622fce09b469a1a6137c04d5b18652ea4.json +6 -0
  83. package/src/sast/.agentic-security/sbom-history/64e30595d447b62344d0d9200760269df2095332.json +6 -0
  84. package/src/sast/.agentic-security/sbom-history/71f3241902c9e7437764e3caa046ba8a3bcdd32a.json +6 -0
  85. package/src/sast/.agentic-security/sbom-history/73514b485d4770f3c5ee57d87338bccd1c61d8b7.json +6 -0
  86. package/src/sast/.agentic-security/sbom-history/96fa584fc3134512237d58aac63d6f3778b0cbcb.json +6 -0
  87. package/src/sast/.agentic-security/sbom-history/99c372cdc33ecc9dde92960b4ffadf7775860e61.json +6 -0
  88. package/src/sast/.agentic-security/sbom-history/a3544abf1863d71776b61302ca74d9ead2b26a9f.json +6 -0
  89. package/src/sast/.agentic-security/sbom-history/a4a34ab8b3d758de2adc8c3dc3e42d4cb0db00a3.json +6 -0
  90. package/src/sast/.agentic-security/sbom-history/ac236ec3602cfc8fb133f389ec3687e409084673.json +6 -0
  91. package/src/sast/.agentic-security/sbom-history/b57b3e1292f1f9f0de6c5a34a436e330c78008a3.json +6 -0
  92. package/src/sast/.agentic-security/sbom-history/dca15abdec43c508aa342db86dc5db8d7f01fbbe.json +6 -0
  93. package/src/sast/.agentic-security/sbom-history/dcbfd4935922dd165093c08a1003c338eed937c5.json +6 -0
  94. package/src/sast/.agentic-security/sbom-history/fa017f725615b0293814344c8ef7dfeb06775ca0.json +6 -0
  95. package/src/sast/.agentic-security/threat-model.json +816 -0
  96. package/src/sast/.agentic-security/threat-model.md +91 -0
  97. package/src/sast/CLAUDE.md +9 -3
  98. package/src/sast/code-injection-multilang.js +117 -0
  99. package/src/sast/crypto-protocol.js +43 -1
  100. package/src/sast/csharp-structural.js +65 -0
  101. package/src/sast/csharp.js +3 -0
  102. package/src/sast/csrf.js +24 -3
  103. package/src/sast/csv-injection.js +84 -0
  104. package/src/sast/frontend-hygiene.js +106 -0
  105. package/src/sast/go-structural.js +60 -0
  106. package/src/sast/java-structural.js +81 -0
  107. package/src/sast/js-framework-structural.js +115 -0
  108. package/src/sast/kotlin.js +72 -0
  109. package/src/sast/ldap-injection.js +73 -17
  110. package/src/sast/open-redirect.js +47 -5
  111. package/src/sast/php.js +35 -0
  112. package/src/sast/python-structural.js +83 -0
  113. package/src/sast/redos-nfa.js +75 -0
  114. package/src/sast/response-splitting.js +48 -19
  115. package/src/sast/ruby.js +38 -0
  116. package/src/sast/secret-concat.js +60 -0
  117. package/src/sast/ssrf-cloud-metadata.js +13 -0
  118. package/src/sast/stored-taint.js +88 -0
  119. package/src/sast/tree-sitter-sinks.js +62 -0
  120. package/src/sast/weak-password-hash.js +28 -0
  121. package/src/sast/weak-randomness.js +46 -5
  122. package/src/sast/wrong-context-sanitizer.js +94 -0
  123. package/src/sast/xpath-injection.js +30 -6
  124. package/src/sast/xss-reflected-multilang.js +116 -0
  125. package/src/sast/xxe.js +67 -0
@@ -0,0 +1,95 @@
1
+ // Proof-gate precision pass — roadmap item #6 ("perfect multi-language SAST").
2
+ //
3
+ // The engine produces two INDEPENDENT flow-proof signals that, until now,
4
+ // were either informational-only or wired in isolation:
5
+ //
6
+ // f.provenClean (proven-clean.js) — every reaching path to a SQL
7
+ // sink passes a parameterizer.
8
+ // f._provenUnreachable (exploit-prover.js) — a sanitizer on the path emits
9
+ // output that cannot contain the
10
+ // vuln family's metacharacters.
11
+ //
12
+ // This pass consolidates them into ONE verdict per finding and applies a
13
+ // precision demotion — the central idea of the precision gate:
14
+ //
15
+ // "Report only provably-feasible flows. A flow we can PROVE is clean or
16
+ // infeasible is demoted, not dropped — the auditor still sees it, and a
17
+ // severity-based CI gate still fires, but it stops dominating the risk
18
+ // ranking and stops tripping confidence filters."
19
+ //
20
+ // Output stamped on every finding:
21
+ // f.proof = { verdict, reasons[] }
22
+ // verdict ∈ 'proven-clean' | 'proven-infeasible' | 'feasible' | 'unproven'
23
+ // f.proofGated = true (only when a demotion was applied)
24
+ //
25
+ // Demotion policy is deliberately RECALL-PRESERVING:
26
+ // - lower `confidence` by DEMOTE_FACTOR and recompute its tier,
27
+ // - drop `confidenceTier` / `exploitabilityTier` one notch,
28
+ // - record an `exploitabilityFactors` breadcrumb,
29
+ // - leave `severity` UNTOUCHED. The proofs here are heuristic (path-
30
+ // existence / regex-exclusion), so they must never hide a finding from a
31
+ // severity gate. Confidence/exploitability are the safe levers.
32
+
33
+ const DEMOTE_FACTOR = 0.4;
34
+
35
+ // Tier ladders, lowest → highest. demoteTier moves one step down.
36
+ const CONFIDENCE_TIERS = ['very-low', 'low', 'medium', 'high'];
37
+ const EXPLOITABILITY_TIERS = ['low', 'medium', 'high', 'critical'];
38
+
39
+ function demoteTier(tier, ladder) {
40
+ const i = ladder.indexOf(tier);
41
+ if (i <= 0) return ladder[0];
42
+ return ladder[i - 1];
43
+ }
44
+
45
+ // Decide the proof verdict for a single finding from the upstream signals.
46
+ export function verdictForFinding(f) {
47
+ if (f.provenClean === true) {
48
+ const sanitizers = (f.provenanceProof && f.provenanceProof.sanitizers) || [];
49
+ return { verdict: 'proven-clean', reason: sanitizers.length ? `parameterized via ${sanitizers.join(', ')}` : 'sql-parameterizer-on-path' };
50
+ }
51
+ if (f._provenUnreachable === true) {
52
+ return { verdict: 'proven-infeasible', reason: f._provenUnreachableReason || 'sanitizer-excludes-metacharacters' };
53
+ }
54
+ // A taint finding that reached a sink with attributed sources, for which we
55
+ // could NOT discharge a clean/infeasible proof, is "feasible" — the flows
56
+ // a precision-gated report should lead with. Non-taint findings are simply
57
+ // outside the proof model → "unproven" (no demotion, no claim).
58
+ if (f.parser === 'IR-TAINT') return { verdict: 'feasible', reason: 'reaches-sink, no clean/infeasible proof discharged' };
59
+ return { verdict: 'unproven', reason: 'no flow-proof applicable to this finding class' };
60
+ }
61
+
62
+ // Annotate findings in place. Returns the same array with a non-enumerable
63
+ // `_proofGateStats` for benchmarking.
64
+ export function annotateProofGate(findings, opts = {}) {
65
+ if (!Array.isArray(findings)) return findings;
66
+ const factor = typeof opts.demoteFactor === 'number' ? opts.demoteFactor : DEMOTE_FACTOR;
67
+ const stats = { gated: 0, feasible: 0, provenClean: 0, provenInfeasible: 0, unproven: 0 };
68
+
69
+ for (const f of findings) {
70
+ if (!f || typeof f !== 'object') continue;
71
+ const { verdict, reason } = verdictForFinding(f);
72
+ f.proof = { verdict, reasons: [reason] };
73
+
74
+ if (verdict === 'feasible') { stats.feasible++; continue; }
75
+ if (verdict === 'unproven') { stats.unproven++; continue; }
76
+
77
+ // proven-clean | proven-infeasible → recall-preserving demotion.
78
+ if (verdict === 'proven-clean') stats.provenClean++; else stats.provenInfeasible++;
79
+
80
+ if (typeof f.confidence === 'number') {
81
+ f._confidenceBeforeProofGate = f.confidence;
82
+ f.confidence = Math.max(0.01, Number((f.confidence * factor).toFixed(4)));
83
+ }
84
+ if (f.confidenceTier) f.confidenceTier = demoteTier(f.confidenceTier, CONFIDENCE_TIERS);
85
+ if (f.exploitabilityTier) f.exploitabilityTier = demoteTier(f.exploitabilityTier, EXPLOITABILITY_TIERS);
86
+ if (Array.isArray(f.exploitabilityFactors)) f.exploitabilityFactors.push(`proof:${verdict}`);
87
+ f.proofGated = true;
88
+ stats.gated++;
89
+ }
90
+
91
+ Object.defineProperty(findings, '_proofGateStats', { value: stats, enumerable: false, configurable: true });
92
+ return findings;
93
+ }
94
+
95
+ export const _internals = { DEMOTE_FACTOR, CONFIDENCE_TIERS, EXPLOITABILITY_TIERS, demoteTier };
@@ -1,9 +1,18 @@
1
1
  // Function-summary cache for context-sensitive interprocedural taint.
2
2
  //
3
- // PRD §6.2: "k-CFA configurable per analysis." This module implements the
4
- // k=1 monovariant version — each function gets ONE summary per distinct
5
- // entry-taint-state, cached by hash. Higher k = exponential blowup we don't
6
- // pay yet.
3
+ // PRD §6.2: "k-CFA configurable per analysis." This module is VALUE-context
4
+ // sensitive (FR-SEM-2): a function gets a distinct summary PER distinct
5
+ // entry-taint-state, cached by hash and computed lazily at the call sites
6
+ // that need it (see engine.js). So a helper that is pure when called with
7
+ // clean args but vulnerable when called with tainted args is modelled
8
+ // correctly at each call site, not collapsed to one empty-entry result.
9
+ //
10
+ // To bound the blowup that per-context computation invites, the number of
11
+ // distinct NON-empty contexts kept per function is capped
12
+ // (`_maxContextsPerFn`, env `AGENTIC_SECURITY_KCFA_MAX_CONTEXTS`, default 16;
13
+ // 0 = pure monovariant). Over the cap we reuse the empty-entry summary.
14
+ // Call-string (k>1) sensitivity is still not modelled — context is the
15
+ // value-abstraction (which params are tainted), not the call stack.
7
16
  //
8
17
  // A summary captures, given a set of tainted parameter names at function
9
18
  // entry, what the function does:
@@ -43,6 +52,18 @@ export class SummaryCache {
43
52
  this._stack = new Set(); // qids currently being analyzed (recursion guard)
44
53
  this._iter = 0;
45
54
  this._maxIter = 5000;
55
+ // FR-SEM-2 (roadmap #2): the engine computes a distinct summary per
56
+ // entry-taint-state (value-context sensitivity), computed lazily at call
57
+ // sites. To stop that from blowing up when a hot helper is reached under
58
+ // many distinct tainted-arg combinations, cap the number of NON-empty
59
+ // entry contexts kept per function. Over the cap we reuse the empty-entry
60
+ // (monovariant) summary — the conservative base the pre-pass computed.
61
+ // AGENTIC_SECURITY_KCFA_MAX_CONTEXTS=N (default 16)
62
+ // ...=0 disables context-sensitivity (pure monovariant).
63
+ this._contextsByQid = new Map(); // qid → Set<stateHash> of computed contexts
64
+ const envCap = Number(process.env.AGENTIC_SECURITY_KCFA_MAX_CONTEXTS);
65
+ this._maxContextsPerFn = Number.isFinite(envCap) && envCap >= 0 ? envCap : 16;
66
+ this._contextCapHits = 0;
46
67
  }
47
68
 
48
69
  _key(qid, taintedParams, receiverType) {
@@ -77,6 +98,24 @@ export class SummaryCache {
77
98
  const cached = this._cache.get(k);
78
99
  if (!cached._recursive) return cached;
79
100
  }
101
+ // Context cap (FR-SEM-2). Empty entry is always allowed — it's the base
102
+ // summary the pre-pass computes for every function. For a NON-empty
103
+ // context that's new and over budget, reuse the empty-entry summary
104
+ // instead of computing a fresh one.
105
+ const stateHash = _hashState(taintedParams);
106
+ if (stateHash !== 'empty') {
107
+ let ctxs = this._contextsByQid.get(qid);
108
+ if (!ctxs) { ctxs = new Set(); this._contextsByQid.set(qid, ctxs); }
109
+ if (!ctxs.has(stateHash) && ctxs.size >= this._maxContextsPerFn) {
110
+ this._contextCapHits++;
111
+ const base = this._cache.get(this._key(qid, new Set()));
112
+ if (base && !base._recursive) return base;
113
+ // No base summary yet → fall through and compute this one (we need a
114
+ // result), but don't grow the context set.
115
+ } else {
116
+ ctxs.add(stateHash);
117
+ }
118
+ }
80
119
  if (this._stack.has(qid)) {
81
120
  this._hitRecursion = true;
82
121
  return { returnTainted: false, mutatedParams: new Set(), taintedGlobals: new Set(), findings: [], _recursive: true };
@@ -125,7 +164,7 @@ export class SummaryCache {
125
164
  }
126
165
 
127
166
  size() { return this._cache.size; }
128
- clear() { this._cache.clear(); this._iter = 0; }
167
+ clear() { this._cache.clear(); this._iter = 0; this._contextsByQid.clear(); this._contextCapHits = 0; }
129
168
  }
130
169
 
131
170
  function _summaryEq(a, b) {
package/src/engine.js CHANGED
@@ -81,6 +81,20 @@ import { scanLDAPInjection } from './sast/ldap-injection.js';
81
81
  import { scanXPathInjection } from './sast/xpath-injection.js';
82
82
  import { scanSSTI } from './sast/ssti.js';
83
83
  import { scanOpenRedirect } from './sast/open-redirect.js';
84
+ import { scanWrongContextSanitizer } from './sast/wrong-context-sanitizer.js';
85
+ import { scanFrontendHygiene } from './sast/frontend-hygiene.js';
86
+ import { scanCsvInjection } from './sast/csv-injection.js';
87
+ import { scanStoredTaint } from './sast/stored-taint.js';
88
+ import { scanTreeSitterSinks } from './sast/tree-sitter-sinks.js';
89
+ import { scanJavaStructural } from './sast/java-structural.js';
90
+ import { scanCsharpStructural } from './sast/csharp-structural.js';
91
+ import { scanJsFrameworkStructural } from './sast/js-framework-structural.js';
92
+ import { scanPythonStructural } from './sast/python-structural.js';
93
+ import { scanGoStructural } from './sast/go-structural.js';
94
+ import { scanSecretConcat } from './sast/secret-concat.js';
95
+ import { scanXssReflectedMultilang } from './sast/xss-reflected-multilang.js';
96
+ import { scanCodeInjectionMultilang } from './sast/code-injection-multilang.js';
97
+ import { scanRegexReDoS } from './sast/redos-nfa.js';
84
98
  import { scanResponseSplitting } from './sast/response-splitting.js';
85
99
  import { scanStoredPromptInjection, scanStoredPromptInjectionCrossFile } from './sast/llm-stored-prompt.js';
86
100
  import { scanRAGPoisoning } from './sast/rag-poisoning.js';
@@ -148,6 +162,8 @@ import { annotateRuntimeCorrelation } from './posture/runtime-correlation.js';
148
162
  import { applyLearnedCalibration } from './posture/triage-learning.js';
149
163
  import { annotateFormalVerification } from './dataflow/formal-verify.js';
150
164
  import { annotatePathFeasibility } from './dataflow/smt-feasibility.js';
165
+ import { annotateProofGate } from './dataflow/proof-gate.js';
166
+ import { computeAnalysisTiers, countUnmodeledSinkCandidates } from './posture/coverage-report.js';
151
167
  import { annotatePrivacyTaint, emitDpiaArtifact } from './dataflow/privacy-taint.js';
152
168
  import { buildThreatModel as buildAutoThreatModel, persistThreatModel as persistAutoThreatModel } from './posture/threat-model-auto.js';
153
169
  import { runApiContractScan } from './posture/api-contract.js';
@@ -1187,6 +1203,111 @@ function _hasPostLookupOwnershipCheck(lines, sinkLine){
1187
1203
  return false;
1188
1204
  }
1189
1205
 
1206
+ // An SSRF host allow/deny guard near the request sink: a hostname checked
1207
+ // against an allow/deny-list, an RFC1918 / loopback / link-local / metadata
1208
+ // prefix check, or a known SSRF-filter helper. Control-flow (not assignment),
1209
+ // so it isn't caught by the sanitizer-on-assignment path. Recall-safe: an
1210
+ // unguarded fetch still has no guard in its window.
1211
+ const _SSRF_HOST_GUARD_RE = /\b(?:DENY|ALLOW(?:ED)?|allow_?list|deny_?list|block_?list|allowed_?hosts?|blocked_?hosts?|ALLOWED_HOSTS)\b|\.(?:hostname|host|getHost\s*\(\s*\))\b[\s\S]{0,60}?(?:\bin\b|\.has\s*\(|\.includes\s*\(|\.contains\s*\(|[!=]=|\.startsWith\s*\(|\.startswith\s*\()|\b(?:startsWith|startswith)\s*\(\s*['"](?:10\.|192\.168\.|127\.|169\.254\.|172\.(?:1[6-9]|2\d|3[01])\.|::1|fc00|fd)|\bipaddress\b|\bis_private\b|\bis_loopback\b|getaddrinfo|gethostbyname|ssrf[-_]?req[-_]?filter|isSafeUrl|isPrivateIp|validateUrl|_is_metadata_or_private|urlparse\s*\([^)]*\)\s*\.\s*(?:hostname|netloc)/i;
1212
+
1213
+ // A comment-stripped window around the sink. Comments must not read as guards
1214
+ // (e.g. a "no host allow-list / 169.254.169.254" comment describing the vuln).
1215
+ function _guardWindow(ctx, before = 25, after = 5) {
1216
+ if (!ctx || !Array.isArray(ctx.lines)) return '';
1217
+ const line = ctx.line || 1;
1218
+ const lo = Math.max(0, line - before);
1219
+ const hi = Math.min(ctx.lines.length, line + after);
1220
+ return ctx.lines.slice(lo, hi).join('\n')
1221
+ .replace(/\/\*[\s\S]*?\*\//g, ' ')
1222
+ .replace(/\/\/[^\n]*/g, ' ')
1223
+ .replace(/(^|[^\w'"`])#[^\n]*/g, '$1 ');
1224
+ }
1225
+
1226
+ function _hasSsrfHostGuard(ctx) { return _SSRF_HOST_GUARD_RE.test(_guardWindow(ctx)); }
1227
+
1228
+ // A path-traversal containment guard near the file sink: a basename/strip
1229
+ // helper that removes directory components, a framework safe-join, or a
1230
+ // canonicalize-then-startsWith containment check.
1231
+ const _PATH_GUARD_RE = /\b(?:basename|GetFileName|secure_filename|sanitize_filename|send_from_directory|safe_join)\s*\(|\b(?:startsWith|startswith|StartsWith|HasPrefix)\s*\(|\bgetCanonicalPath\b|\btoRealPath\b|\bfilepath\s*\.\s*(?:Clean|Base|Abs)\b/;
1232
+ function _hasPathGuard(ctx) { return _PATH_GUARD_RE.test(_guardWindow(ctx)); }
1233
+
1234
+ // Reflected-XSS output-encoding guard: an HTML escaper applied near the sink.
1235
+ const _XSS_ESCAPER = String.raw`(?:escapeHtml|escape_html|escape-html|sanitizeHtml|sanitize_html|DOMPurify\.sanitize|he\.encode|he\.escape|_\.escape|validator\.escape|bleach\.clean|markupsafe|htmlspecialchars|htmlentities|html\.escape|escapeHTML|encodeURIComponent|escape)\s*\(`;
1236
+ const _XSS_ESCAPE_RE = new RegExp(_XSS_ESCAPER);
1237
+ // An escaper whose RESULT is captured (assigned / returned) — not a bare,
1238
+ // return-discarded `escapeHtml(s);` call, which does nothing for the reflected
1239
+ // value (FP-3 in smoke). Requires `… = <escaper>(` or `return <escaper>(`.
1240
+ const _XSS_ASSIGNED_ESCAPE_RE = new RegExp(String.raw`(?:[:=]|\breturn\b)\s*[^=;\n]*?` + _XSS_ESCAPER);
1241
+ // A raw request source appearing on the sink line itself (not via a prior
1242
+ // escaped var). If present, the reflection is unescaped — keep the finding.
1243
+ const _XSS_RAW_SOURCE_RE = /\breq(?:uest)?\s*\.|\bctx\s*\.\s*(?:query|params|request|body)\b|\$_(?:GET|POST|REQUEST|COOKIE)\b|\bparams\s*\[|\.(?:query|body|params)\s*\.[A-Za-z_$]/;
1244
+ function _hasXssOutputEncoding(ctx) {
1245
+ if (!ctx || !Array.isArray(ctx.lines)) return false;
1246
+ const line = ctx.lines[(ctx.line || 1) - 1] || '';
1247
+ // The reflected-XSS finding is anchored either on the escaped-assignment line
1248
+ // (`const safe = escape(req.query.x)`) or on the response sink line
1249
+ // (`res.send('…' + safe + '…')`).
1250
+ // - If the finding line itself captures the escaped value (`const safe =
1251
+ // escape(req.query.x)`), the reflection is encoded → safe. A bare,
1252
+ // return-discarded `escapeHtml(s);` does NOT count.
1253
+ if (_XSS_ASSIGNED_ESCAPE_RE.test(line)) return true;
1254
+ // - A raw, unescaped request source on the finding line is the real vuln
1255
+ // shape (`res.send('…' + req.query.x)`) → keep it.
1256
+ if (_XSS_RAW_SOURCE_RE.test(line)) return false;
1257
+ // - Otherwise the sink reflects a variable; drop only if that variable was
1258
+ // escaped (and the result captured) in the surrounding window. A bare
1259
+ // `escapeHtml(s);` with a discarded return does NOT count.
1260
+ return _XSS_ASSIGNED_ESCAPE_RE.test(_guardWindow(ctx, 8, 2));
1261
+ }
1262
+
1263
+ // Centralized guard-recognition pass (#1). SSRF (CWE-918) and path-traversal
1264
+ // (CWE-22) are emitted by several independent detectors (regex, structural,
1265
+ // per-language flow, PY-SAST, CSHARP, GO) — none of which shared a notion of
1266
+ // "the sink is hardened by a host allow/deny check or a path containment
1267
+ // guard." This single pass drops such findings uniformly, regardless of which
1268
+ // detector produced them, using the same comment-stripped window guards.
1269
+ // Recall-safe: a genuinely-unguarded sink has no guard in its window (the
1270
+ // cve-replay vulnerable-tier TP count is the invariant that proves this).
1271
+ export function dropGuardedFindings(findings, fileContents) {
1272
+ if (!Array.isArray(findings)) return findings;
1273
+ const cache = new Map();
1274
+ const linesOf = (file) => {
1275
+ if (cache.has(file)) return cache.get(file);
1276
+ const c = fileContents && fileContents[file];
1277
+ const arr = typeof c === 'string' ? c.split('\n') : null;
1278
+ cache.set(file, arr);
1279
+ return arr;
1280
+ };
1281
+ return findings.filter((f) => {
1282
+ if (!f || !f.file) return true;
1283
+ // Already-sanitized findings (isSanitized / info) are owned by the
1284
+ // sanitizer-dataflow pipeline, which records them in scan.suppressions.
1285
+ // Don't hard-drop them here or that bookkeeping is lost (smoke FP-3).
1286
+ if (f.isSanitized) return true;
1287
+ const cwe = f.cwe || '';
1288
+ const isXss = cwe === 'CWE-79' && /reflected xss/i.test(f.vuln || '');
1289
+ if (cwe !== 'CWE-918' && cwe !== 'CWE-22' && !isXss) return true;
1290
+ // Flow-pair findings (REGEX parser) carry the line on sink/source, not at
1291
+ // the top level; fall back to those before parsing it out of the id.
1292
+ let line = f.line;
1293
+ if (typeof line !== 'number') {
1294
+ line = (f.sink && f.sink.line) ?? (f.source && f.source.line);
1295
+ if (typeof line !== 'number') {
1296
+ const m = /:(\d+):/.exec(f.id || '');
1297
+ if (m) line = Number(m[1]);
1298
+ }
1299
+ }
1300
+ if (typeof line !== 'number') return true;
1301
+ const lines = linesOf(f.file);
1302
+ if (!lines) return true;
1303
+ const ctx = { lines, line };
1304
+ if (cwe === 'CWE-918' && _hasSsrfHostGuard(ctx)) return false;
1305
+ if (cwe === 'CWE-22' && _hasPathGuard(ctx)) return false;
1306
+ if (isXss && _hasXssOutputEncoding(ctx)) return false;
1307
+ return true;
1308
+ });
1309
+ }
1310
+
1190
1311
  function _detectSafeSinkShape(vuln, args, ctx){
1191
1312
  // 1.1: Reflected XSS / SSRF / Path Traversal / Open Redirect / Code Injection
1192
1313
  // sinks with empty or whitespace-only args cannot reflect user input — skip
@@ -1221,6 +1342,8 @@ function _detectSafeSinkShape(vuln, args, ctx){
1221
1342
  if (_isSafeExecFileCall(args)) return 'execFile-list';
1222
1343
  }
1223
1344
  if(/SSRF/.test(vuln) && _isStaticUrlFirstArg(args)) return 'static-url';
1345
+ if(/SSRF/.test(vuln) && _hasSsrfHostGuard(ctx)) return 'ssrf-host-guard';
1346
+ if(/Path Traversal/.test(vuln) && _hasPathGuard(ctx)) return 'path-contained';
1224
1347
  if(/Open Redirect/.test(vuln)) {
1225
1348
  // Static redirect targets (env var or string literal) are not user-controlled
1226
1349
  if (/^\s*process\.env\b/.test(args)) return 'static-redirect-target';
@@ -1959,6 +2082,17 @@ function scanStructuralVulns(fp, raw) {
1959
2082
  }
1960
2083
  effectiveSeverity = s;
1961
2084
  }
2085
+ // Guard recognition (precision, #1): a structural SSRF/path finding on
2086
+ // hardened code — an SSRF host allow/deny check, or a path
2087
+ // basename/containment guard near the sink — is a false positive.
2088
+ if (/SSRF/.test(pat.vuln) && _hasSsrfHostGuard({ lines, line })) {
2089
+ _suppressionLog.push({vuln:pat.vuln, file:fp, line, snippet, reason:'ssrf-host-guard'});
2090
+ continue;
2091
+ }
2092
+ if (/Path Traversal/.test(pat.vuln) && _hasPathGuard({ lines, line })) {
2093
+ _suppressionLog.push({vuln:pat.vuln, file:fp, line, snippet, reason:'path-contained'});
2094
+ continue;
2095
+ }
1962
2096
  const id = `struct:${fp}:${line}:${pat.vuln.replace(/\s/g, '_')}`;
1963
2097
  if (!findings.find(f => f.id === id)) {
1964
2098
  findings.push({
@@ -5512,8 +5646,17 @@ function dedupeFindingsWithEvidence(findings){
5512
5646
  const key=`${file}:${sinkLine}:${fam}`;
5513
5647
  if(!buckets.has(key)){buckets.set(key,f);continue;}
5514
5648
  const kept=buckets.get(key);
5515
- // Keep highest-severity entry; preserve evidence from both.
5516
- const keepNew = (SEV_RANK[f.severity]??9) < (SEV_RANK[kept.severity]??9);
5649
+ // Winner selection: an interprocedural flow finding (carries source→sink
5650
+ // attribution) is the better carrier than a flat structural/regex match at
5651
+ // the same sink — keep its chain/source-line attribution. When neither or
5652
+ // both carry flow, fall back to severity. The winner keeps its own severity
5653
+ // (we must not resurrect a rating that ownership/reachability analysis
5654
+ // deliberately downgraded on one of the two findings).
5655
+ const fHasFlow = !!(f.source && f.sink);
5656
+ const kHasFlow = !!(kept.source && kept.sink);
5657
+ const keepNew = (fHasFlow !== kHasFlow)
5658
+ ? fHasFlow
5659
+ : (SEV_RANK[f.severity]??9) < (SEV_RANK[kept.severity]??9);
5517
5660
  const winner = keepNew ? f : kept;
5518
5661
  const loser = keepNew ? kept : f;
5519
5662
  if(!winner.evidence)winner.evidence=[winner.parser||"UNKNOWN"];
@@ -7202,8 +7345,8 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7202
7345
  catch { _GLOBAL_JAVA_TAINTED_METHODS = new Set(); }
7203
7346
  const _perFileTimeoutMs = parseInt(process.env.AGENTIC_SECURITY_PER_FILE_TIMEOUT_MS || '10000', 10);
7204
7347
  const _fileTimings = [];
7205
- let _filesSkipped = 0, _filesTimedOut = 0;
7206
- 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));
7348
+ let _filesSkipped = 0, _filesTimedOut = 0, _filesDenseSkipped = 0;
7349
+ 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){_filesDenseSkipped++;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));if(/\.(?:java|cs|kt|py|php|phtml)$/i.test(p)){try{aLogic.push(...scanRegexReDoS(p,c));}catch(_){}}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));
7207
7350
  aF.push(...scanLLM(p,c));
7208
7351
  aF.push(...scanLLMOwasp(p,c));
7209
7352
  aLogic.push(...scanBusinessLogic(p,c));
@@ -7265,6 +7408,18 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7265
7408
  aF.push(...scanXPathInjection(p,c));
7266
7409
  aF.push(...scanSSTI(p,c));
7267
7410
  aF.push(...scanOpenRedirect(p,c));
7411
+ aF.push(...scanWrongContextSanitizer(p,c));
7412
+ aF.push(...scanFrontendHygiene(p,c));
7413
+ aF.push(...scanCsvInjection(p,c));
7414
+ aF.push(...scanStoredTaint(p,c));
7415
+ aF.push(...scanJavaStructural(p,c));
7416
+ aF.push(...scanCsharpStructural(p,c));
7417
+ aF.push(...scanJsFrameworkStructural(p,c));
7418
+ aF.push(...scanPythonStructural(p,c));
7419
+ aF.push(...scanGoStructural(p,c));
7420
+ aF.push(...scanSecretConcat(p,c));
7421
+ aF.push(...scanXssReflectedMultilang(p,c));
7422
+ aF.push(...scanCodeInjectionMultilang(p,c));
7268
7423
  aF.push(...scanResponseSplitting(p,c));
7269
7424
  aF.push(...scanStoredPromptInjection(p,c));
7270
7425
  aF.push(...scanRAGPoisoning(p,c));
@@ -7552,7 +7707,14 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7552
7707
  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};});
7553
7708
  try{aF.push(...scanDbTaintCrossFile(fc));}catch(_){}
7554
7709
  try{aF.push(...scanStoredPromptInjectionCrossFile(fc));}catch(_){}
7710
+ // Roadmap #8 — tree-sitter sinks for long-tail languages (opt-in,
7711
+ // AGENTIC_SECURITY_TREE_SITTER=1; degrades to no-op without the optional dep).
7712
+ if(process.env.AGENTIC_SECURITY_TREE_SITTER==='1'){try{aF.push(...await scanTreeSitterSinks(fc));}catch(_){}}
7555
7713
  let finalFindings;try{finalFindings=dedupeFindingsWithEvidence(aF);}catch(_){finalFindings=dd(aF,f=>f.id);}
7714
+ // #1 — centralized SSRF/path guard recognition: drop CWE-918/CWE-22 findings
7715
+ // on code hardened by a host allow/deny check or a path containment guard,
7716
+ // regardless of which detector emitted them. Opt out: AGENTIC_SECURITY_NO_GUARD_RECOGNITION=1.
7717
+ if(process.env.AGENTIC_SECURITY_NO_GUARD_RECOGNITION!=='1'){try{finalFindings=dropGuardedFindings(finalFindings,fc);}catch(_){}}
7556
7718
  // 0.34.6: filter out Java FPs where a sanitizer pattern (argv-form ProcessBuilder,
7557
7719
  // parameterized prepareStatement, constant-folded dead-branch) is present.
7558
7720
  // applyJavaBenchSuppressions is a no-op on non-.java files.
@@ -7670,6 +7832,14 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
7670
7832
  _runAnnotator("annotateCalibratedConfidence", () => { annotateCalibratedConfidence(finalFindings, { scanRoot }); });
7671
7833
  const _projectCtx = (() => { try { return detectProjectContext(fc, aR); } catch { return {}; } })();
7672
7834
  _runAnnotator("annotateExploitability", () => { annotateExploitability(finalFindings, _projectCtx); });
7835
+ // Roadmap #6 — proof-gate precision pass. Runs AFTER confidence +
7836
+ // exploitability so it can demote their tiers, and BEFORE mitigation /
7837
+ // composite-risk so the demotion flows into the canonical ranking. Default
7838
+ // on; opt out with AGENTIC_SECURITY_NO_PROOF_GATE=1. Demotes proven-clean /
7839
+ // proven-infeasible flows (confidence + tiers only, never severity).
7840
+ if (process.env.AGENTIC_SECURITY_NO_PROOF_GATE !== '1') {
7841
+ _runAnnotator("annotateProofGate", () => { annotateProofGate(finalFindings); });
7842
+ }
7673
7843
  // v3 next-gen: production-aware context ingest (Pillar 9). Must run BEFORE
7674
7844
  // the mitigation composite, persona prioritization, and final why-fired
7675
7845
  // record so those see the demotion signals.
@@ -8150,6 +8320,25 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
8150
8320
  finalFindings = finalFindings.filter(_shouldKeep);
8151
8321
  _filterInPlace(aLogic);
8152
8322
  _filterInPlace(aSecrets);
8323
+ // Drop a generic High-Entropy Credential Candidate when a named/structural
8324
+ // secret detector (e.g. secret-concat, hardcoded-secret) already flagged the
8325
+ // SAME file:line — otherwise one split/hardcoded key is reported twice (the
8326
+ // named finding lives in `findings`, the entropy candidate in `secrets`).
8327
+ {
8328
+ const _namedSecretLines = new Set();
8329
+ for (const f of finalFindings) {
8330
+ const ln = f.line ?? f.source?.line;
8331
+ if (typeof ln === 'number' && (f.cwe === 'CWE-798' || /hardcoded|credential|secret/i.test(f.vuln || ''))) {
8332
+ _namedSecretLines.add(`${f.file}:${ln}`);
8333
+ }
8334
+ }
8335
+ const _kept = aSecrets.filter((s) => {
8336
+ if (!/High-Entropy|entropy/i.test(s.vuln || '')) return true;
8337
+ return !_namedSecretLines.has(`${s.file}:${s.line}`);
8338
+ });
8339
+ aSecrets.length = 0;
8340
+ aSecrets.push(..._kept);
8341
+ }
8153
8342
  _filterInPlace(supplyChain);
8154
8343
  classifyOrphans(aSrc,aSink,finalFindings,fc);
8155
8344
  // v3 next-gen: capture scan-level reports (counterfactual, threat model,
@@ -8255,7 +8444,12 @@ async function runFullScan({fileContents={}, depFileContents={}, scanRoot=null},
8255
8444
  }
8256
8445
  }
8257
8446
 
8258
- 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}};
8447
+ // Coverage-honesty report (#5 + #6): per-language analysis tier (IR-taint vs
8448
+ // pattern-only), dense/large/timeout skips, and unmodeled-sink candidates.
8449
+ let _analysisTier = null, _unmodeledSinks = null;
8450
+ try { _analysisTier = computeAnalysisTiers(Object.keys(fc)); } catch {}
8451
+ try { _unmodeledSinks = countUnmodeledSinkCandidates(fc, finalFindings); } catch {}
8452
+ const _scanMeta={filesScanned:files.length,filesSkipped:_filesSkipped,filesDenseSkipped:_filesDenseSkipped,filesTimedOut:_filesTimedOut,analysisTier:_analysisTier,unmodeledSinkCandidates:_unmodeledSinks,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}};
8259
8453
  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,threatModel:_threatModel,sbomDiff:_sbomDiff,complianceReport:_complianceReport,exploitBundles:_exploitBundles,pqcPlan:_pqcPlan,licenseGraph:_licenseGraph,attributions:_attributions,attackTaxonomy:_taxonomySummary};}
8260
8454
 
8261
8455
  // Post-aggregation classification: every source becomes "unsafe"|"safe"; every sink becomes "confirmed"|"safe".
@@ -0,0 +1,26 @@
1
+ # Data Protection Impact Assessment (DPIA)
2
+
3
+ Generated by agentic-security scanner on 2026-05-31.
4
+
5
+ This is an automated DPIA scaffold derived from static analysis.
6
+ It must be reviewed and completed by a privacy officer before use.
7
+
8
+ ## Data classes identified
9
+
10
+ ## Privacy-related findings
11
+
12
+ | Severity | File:Line | Class → Sink | Description |
13
+ |---|---|---|---|
14
+
15
+ ## Regulatory framework mapping
16
+
17
+ - **GDPR Art. 35** — DPIA required when processing is likely to result in high risk to data subjects.
18
+ - **CCPA §1798.130** — Notice + access rights for collected personal information.
19
+
20
+ ## Reviewer checklist
21
+
22
+ - [ ] Confirm each PII field's collection has a documented lawful basis
23
+ - [ ] Confirm retention period for each class is documented
24
+ - [ ] Confirm DSAR (data subject access request) workflow exists
25
+ - [ ] Confirm encryption at rest + in transit for each class
26
+ - [ ] Confirm logging of PII access for audit (where applicable)
@@ -0,0 +1,77 @@
1
+ {
2
+ "struct:parser-cs.js:208:Mass_Assignment_(req.body_Direct_to_Model)": {
3
+ "family": "mass-assignment",
4
+ "exploitable": "unknown",
5
+ "pocs": {
6
+ "curl": null
7
+ },
8
+ "tests": {
9
+ "jest": null,
10
+ "pytest": null
11
+ },
12
+ "remediation": {
13
+ "summary": "no template for this family yet — manual review required"
14
+ },
15
+ "stub": true
16
+ },
17
+ "struct:parser-go.js:253:Mass_Assignment_(req.body_Direct_to_Model)": {
18
+ "family": "mass-assignment",
19
+ "exploitable": "unknown",
20
+ "pocs": {
21
+ "curl": null
22
+ },
23
+ "tests": {
24
+ "jest": null,
25
+ "pytest": null
26
+ },
27
+ "remediation": {
28
+ "summary": "no template for this family yet — manual review required"
29
+ },
30
+ "stub": true
31
+ },
32
+ "struct:parser-kt.js:207:Mass_Assignment_(req.body_Direct_to_Model)": {
33
+ "family": "mass-assignment",
34
+ "exploitable": "unknown",
35
+ "pocs": {
36
+ "curl": null
37
+ },
38
+ "tests": {
39
+ "jest": null,
40
+ "pytest": null
41
+ },
42
+ "remediation": {
43
+ "summary": "no template for this family yet — manual review required"
44
+ },
45
+ "stub": true
46
+ },
47
+ "struct:parser-php.js:209:Mass_Assignment_(req.body_Direct_to_Model)": {
48
+ "family": "mass-assignment",
49
+ "exploitable": "unknown",
50
+ "pocs": {
51
+ "curl": null
52
+ },
53
+ "tests": {
54
+ "jest": null,
55
+ "pytest": null
56
+ },
57
+ "remediation": {
58
+ "summary": "no template for this family yet — manual review required"
59
+ },
60
+ "stub": true
61
+ },
62
+ "struct:parser-rb.js:201:Mass_Assignment_(req.body_Direct_to_Model)": {
63
+ "family": "mass-assignment",
64
+ "exploitable": "unknown",
65
+ "pocs": {
66
+ "curl": null
67
+ },
68
+ "tests": {
69
+ "jest": null,
70
+ "pytest": null
71
+ },
72
+ "remediation": {
73
+ "summary": "no template for this family yet — manual review required"
74
+ },
75
+ "stub": true
76
+ }
77
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "sha": "73166872888d9ddcbe98645af818ecc7d8917b07",
3
+ "ts": "2026-05-31T15:37:04.152Z",
4
+ "componentCount": 0,
5
+ "components": []
6
+ }
@@ -0,0 +1,6 @@
1
+ {
2
+ "sha": "b5f468a158f6a145aedd8680841c106b9809f16b",
3
+ "ts": "2026-05-31T16:25:21.141Z",
4
+ "componentCount": 0,
5
+ "components": []
6
+ }