@clear-capabilities/agentic-security-scanner 0.143.0 → 0.145.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 (135) hide show
  1. package/CHANGELOG.md +555 -0
  2. package/bin/agentic-security.js +770 -49
  3. package/dist/1.index.js +223 -0
  4. package/dist/113.index.js +117 -18
  5. package/dist/144.index.js +163 -0
  6. package/dist/178.index.js +24 -6
  7. package/dist/238.index.js +3 -2
  8. package/dist/265.index.js +191 -0
  9. package/dist/271.index.js +165 -0
  10. package/dist/384.index.js +1 -1
  11. package/dist/435.index.js +187 -52
  12. package/dist/444.index.js +11 -2
  13. package/dist/449.index.js +76 -12
  14. package/dist/526.index.js +117 -18
  15. package/dist/552.index.js +97 -0
  16. package/dist/637.index.js +27 -5
  17. package/dist/730.index.js +311 -0
  18. package/dist/736.index.js +301 -0
  19. package/dist/824.index.js +7 -0
  20. package/dist/905.index.js +88 -22
  21. package/dist/920.index.js +491 -0
  22. package/dist/970.index.js +173 -0
  23. package/dist/agentic-security.mjs +13 -13
  24. package/dist/agentic-security.mjs.sha256 +1 -1
  25. package/dist/calibration-seed.json +2 -0
  26. package/package.json +23 -9
  27. package/src/compare.js +6 -1
  28. package/src/dataflow/CLAUDE.md +1 -1
  29. package/src/dataflow/index.js +18 -0
  30. package/src/dataflow/privacy-catalog.js +290 -0
  31. package/src/dataflow/privacy-deep-walker.js +515 -0
  32. package/src/dataflow/privacy-governance.js +126 -0
  33. package/src/dataflow/privacy-inventory.js +154 -0
  34. package/src/dataflow/privacy-sink-policy.js +125 -0
  35. package/src/dataflow/privacy-taint.js +115 -54
  36. package/src/dataflow/privacy-taxonomy.js +233 -0
  37. package/src/discovery/disprove.js +7 -3
  38. package/src/discovery/hunter.js +9 -5
  39. package/src/discovery/index.js +2 -2
  40. package/src/discovery/llm-invoke.js +69 -13
  41. package/src/egress/audit.js +147 -0
  42. package/src/egress/policy.js +313 -0
  43. package/src/egress/redact.js +180 -0
  44. package/src/engine.js +1048 -302
  45. package/src/fix/apply-fix-service.js +404 -0
  46. package/src/fix/approver-registry.js +157 -0
  47. package/src/history-scan.js +22 -5
  48. package/src/ir/CLAUDE.md +1 -1
  49. package/src/llm-validator/index.js +86 -9
  50. package/src/llm-validator/model-status.js +66 -0
  51. package/src/lsp/server.js +49 -2
  52. package/src/mcp/tools.js +177 -50
  53. package/src/pipeline/analyzer-supervisor.js +93 -0
  54. package/src/pipeline/analyzer-worker.js +26 -0
  55. package/src/pipeline/annotator-runner.js +33 -0
  56. package/src/pipeline/assurance-mode.js +154 -0
  57. package/src/pipeline/cascade-worker-pool.js +172 -0
  58. package/src/pipeline/cascade-worker.js +43 -0
  59. package/src/pipeline/coverage-ledger.js +0 -0
  60. package/src/pipeline/detector-runner.js +51 -0
  61. package/src/pipeline/enrichment-completion.js +58 -0
  62. package/src/pipeline/evidence-provenance.js +91 -0
  63. package/src/pipeline/finding-schema.js +108 -0
  64. package/src/pipeline/legacy-compat.js +101 -0
  65. package/src/pipeline/producer-collector.js +48 -0
  66. package/src/pipeline/producer-registry.js +112 -0
  67. package/src/pipeline/scan-health.js +144 -0
  68. package/src/posture/CLAUDE.md +123 -0
  69. package/src/posture/accuracy-scorecard.js +156 -1
  70. package/src/posture/adversary-agent.js +15 -3
  71. package/src/posture/artifact-registry.js +241 -0
  72. package/src/posture/auditor-walkthrough.js +186 -21
  73. package/src/posture/calibration-feedback.js +201 -0
  74. package/src/posture/calibration-seed.json +2 -0
  75. package/src/posture/calibration.js +25 -0
  76. package/src/posture/compliance-evidence-signing.js +131 -0
  77. package/src/posture/compliance-policy.js +324 -17
  78. package/src/posture/cross-repo-memory.js +7 -2
  79. package/src/posture/custom-rules.js +36 -0
  80. package/src/posture/deterministic.js +8 -1
  81. package/src/posture/encryption-provider.js +205 -0
  82. package/src/posture/evidence-grade-wording.js +71 -0
  83. package/src/posture/fix-history.js +137 -20
  84. package/src/posture/fix-honesty-gate.js +47 -6
  85. package/src/posture/fix-verify.js +65 -8
  86. package/src/posture/fleet.js +0 -0
  87. package/src/posture/flow-narration.js +7 -2
  88. package/src/posture/git-history.js +13 -5
  89. package/src/posture/legal-hold.js +140 -0
  90. package/src/posture/llm-redteam.js +10 -1
  91. package/src/posture/material-change.js +111 -2
  92. package/src/posture/mttr.js +75 -12
  93. package/src/posture/policy-bundle.js +274 -0
  94. package/src/posture/pre-incident-archaeology.js +39 -7
  95. package/src/posture/privacy-framework.js +47 -6
  96. package/src/posture/production-feedback.js +179 -0
  97. package/src/posture/provenance/ai-authorship.js +68 -0
  98. package/src/posture/provenance/branch-entry.js +80 -0
  99. package/src/posture/provenance/cache.js +143 -0
  100. package/src/posture/provenance/confidence.js +36 -0
  101. package/src/posture/provenance/coordinator.js +786 -0
  102. package/src/posture/provenance/dag-walk.js +249 -0
  103. package/src/posture/provenance/evidence-attribution.js +59 -0
  104. package/src/posture/provenance/git-evidence.js +310 -0
  105. package/src/posture/provenance/lifecycle.js +208 -0
  106. package/src/posture/provenance/missing-control-resolver.js +137 -0
  107. package/src/posture/provenance/origin-resolver.js +342 -0
  108. package/src/posture/provenance/predicate-replay.js +133 -0
  109. package/src/posture/provenance/providers/config.js +39 -0
  110. package/src/posture/provenance/providers/github.js +62 -0
  111. package/src/posture/provenance/providers/gitlab.js +58 -0
  112. package/src/posture/provenance/repo-lineage.js +74 -0
  113. package/src/posture/provenance/sca-origin.js +139 -0
  114. package/src/posture/provenance/schema.js +255 -0
  115. package/src/posture/provenance/transitive-sca.js +147 -0
  116. package/src/posture/provenance/validate.js +30 -0
  117. package/src/posture/provenance-evidence-bundle.js +144 -0
  118. package/src/posture/retention-policy.js +132 -0
  119. package/src/posture/risk-dollars.js +216 -26
  120. package/src/posture/sbom-diff.js +15 -2
  121. package/src/posture/scan-checkpoint.js +176 -31
  122. package/src/posture/secret-history.js +10 -2
  123. package/src/posture/state-dir.js +64 -5
  124. package/src/posture/state-lifecycle-report.js +77 -0
  125. package/src/posture/suppressions.js +59 -3
  126. package/src/posture/vuln-archaeology.js +8 -2
  127. package/src/pr-delta.js +25 -4
  128. package/src/privacy/ir-adapter.js +380 -0
  129. package/src/report/index.js +248 -5
  130. package/src/report/oscal.js +7 -2
  131. package/src/runScan.js +34 -5
  132. package/src/sast/cpp.js +3 -14
  133. package/src/sast/rate-limit.js +33 -3
  134. package/src/sca/llm-function-extract.js +6 -0
  135. package/src/util/git-hardening.js +128 -0
package/src/pr-delta.js CHANGED
@@ -24,18 +24,27 @@
24
24
  // comment) can transform without re-walking IR.
25
25
 
26
26
  import { spawnSync } from 'node:child_process';
27
+ import { hardenGitArgs, hardenGitEnv } from './util/git-hardening.js';
27
28
  import { runFullScan } from './engine.js';
28
29
 
29
30
  const FILE_EXT_RE = /\.(?:js|jsx|ts|tsx|mjs|cjs|py|java|cs|kt|go|rb|php|sol|swift|rs|tf|yml|yaml|json|toml|md)$/i;
30
31
  const SEVERITIES = ['critical', 'high', 'medium', 'low', 'info'];
31
32
 
33
+ // `root` is the PR's repository — a scan target, not this project's own
34
+ // trusted checkout — hardened per FR-PROV-024 / the second Finding
35
+ // Provenance PRD audit (same exposure class as
36
+ // provenance/git-evidence.js's `_run`).
32
37
  function _git(root, args) {
33
- const r = spawnSync('git', args, { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024 });
38
+ const r = spawnSync('git', hardenGitArgs(args), { cwd: root, encoding: 'utf8', maxBuffer: 64 * 1024 * 1024, env: hardenGitEnv() });
34
39
  return { ok: r.status === 0, stdout: r.stdout || '', stderr: r.stderr || '' };
35
40
  }
36
41
 
37
42
  function _readFileAtRef(root, ref, file) {
38
- const r = _git(root, ['show', `${ref}:${file}`]);
43
+ // `--no-textconv`: this blob-cat form of `show` (`<ref>:<file>`, not a
44
+ // diff) was verified NOT reachable via a hostile textconv driver in
45
+ // current git, same as git-evidence.js's getBlobAtCommit — kept for
46
+ // defense-in-depth/uniformity.
47
+ const r = _git(root, ['show', '--no-textconv', `${ref}:${file}`]);
39
48
  return r.ok ? r.stdout : null;
40
49
  }
41
50
 
@@ -56,7 +65,12 @@ async function _scanAtRef(root, ref) {
56
65
  const c = _readFileAtRef(root, ref, f);
57
66
  if (c != null) fileContents[f] = c;
58
67
  }
59
- return runFullScan({ fileContents, scanRoot: root }, () => {});
68
+ // `provenance:false` a base-ref snapshot, not the current working state.
69
+ // Beyond the wasted git walks, updateLifecycle marks every open stableId
70
+ // absent from the finding set it is handed as `remediated`; running the PR
71
+ // delta gate would silently rewrite the project's lifecycle store from the
72
+ // base ref's findings.
73
+ return runFullScan({ fileContents, scanRoot: root, provenance: false }, () => {});
60
74
  }
61
75
 
62
76
  function _summary(findings) {
@@ -71,7 +85,14 @@ function _summary(findings) {
71
85
  }
72
86
 
73
87
  function _changedFiles(root, baseRef, headRef) {
74
- const r = _git(root, ['diff', '--name-only', `${baseRef}...${headRef}`]);
88
+ // `--no-ext-diff`: found during the second-audit-remediation sweep — this
89
+ // is a `git diff` call site (`--name-only`, no content rendered, so not
90
+ // itself a verified exploit path today, same as runScan.js's
91
+ // `changedSince`), but every `diff` invocation in this codebase gets it
92
+ // uniformly per the live exploit VERIFIED in material-change.js's
93
+ // classifyGitDiff (external diff drivers fire on `git diff` even with
94
+ // `--no-textconv`, which is a surface `--no-textconv` alone does not close).
95
+ const r = _git(root, ['diff', '--name-only', '--no-ext-diff', `${baseRef}...${headRef}`]);
75
96
  if (!r.ok) return new Set();
76
97
  return new Set(r.stdout.trim().split('\n').filter(Boolean));
77
98
  }
@@ -0,0 +1,380 @@
1
+ // Privacy IR adapter (assurance-hardening PRD, Milestone 2, FR-401).
2
+ //
3
+ // dataflow/privacy-taint.js's annotatePrivacyTaint() expects a per-file map
4
+ // of `{ _content, decls: [{name, line, type}], calls: [{line, fullPath|callee,
5
+ // args: [{text}]}] }` — a FLAT, per-file shape. The real Layer-1 IR
6
+ // (ir/index.js#buildProjectIR) does not produce that shape at all: it is
7
+ // per-FUNCTION (`functions: [{params, cfg: {nodes: {...}}}]`), and CFG node
8
+ // arguments are structured expression trees (`{kind:'ident', name}`,
9
+ // `{kind:'member', object, prop}`, ...), not `{text: string}` objects. This
10
+ // was the actual reason privacy analysis was wired with hardcoded
11
+ // `decls:[], calls:[]` (A-06) — there was no adapter reconciling the two
12
+ // shapes, not a missing capability in the underlying IR/taint engine (see
13
+ // docs/implementation/assurance-hardening-decisions.md D-0003's pattern:
14
+ // verify before assuming something is unbuilt).
15
+ //
16
+ // This module is that adapter. It does NOT reimplement privacy analysis —
17
+ // it only reshapes the real IR into exactly what annotatePrivacyTaint()
18
+ // already knows how to consume.
19
+ //
20
+ // Declarations: sourced from every function's `params` (a parameter is a
21
+ // declaration) and every `assign`-kind CFG node's `target` (an assignment
22
+ // declares/rebinds a name). Both are what a real codebase would call PII
23
+ // "at the point it enters a function or gets bound to a local".
24
+ //
25
+ // Calls: sourced from every `call`-kind CFG node, AND from `assign`-kind
26
+ // nodes whose `source` is itself a call expression (`const x = fetch(...)`)
27
+ // — privacy-taint.js only walks a flat `ir.calls` list, so a call used as
28
+ // an assignment's right-hand side needs to be surfaced there too or it's
29
+ // invisible to the sink-matching pass entirely.
30
+ //
31
+ // Argument "text": privacy-taint.js's sink-matching does a literal
32
+ // \b<name>\b regex test against the joined text of a call's argument
33
+ // expressions — it does not need faithful re-rendered source code, only
34
+ // text that CONTAINS every identifier name referenced (directly or nested)
35
+ // in that argument, so a tainted variable's name is found wherever it
36
+ // appears. `_identifiersIn` recursively collects identifier names (and
37
+ // literal values, for completeness) rather than attempting full expression
38
+ // pretty-printing, which is simpler and sufficient for this consumer's
39
+ // actual matching logic.
40
+ //
41
+ // Storage: this codebase already has a separate stored-taint subsystem
42
+ // (engine.js's STORED_TAINT_FIELD_PATTERNS / buildStoredTaintRegistry) that
43
+ // tracks which ORM/model fields are written with unsanitized request input,
44
+ // for its own cross-file write-then-render correlation (crossStoredTaint).
45
+ // `_storageForFile` reindexes that ALREADY-COMPUTED, whole-project registry
46
+ // down to one file's writes rather than recomputing anything — the engine
47
+ // builds it once per scan regardless of whether privacy analysis runs at all.
48
+ //
49
+ // Types: the underlying Layer-1 IR (ir/index.js#buildProjectIR) tracks no
50
+ // type-annotation/inference data for any language — genuine cross-language
51
+ // type inference would mean extending every language parser, a separate,
52
+ // substantially larger project deliberately out of scope here. What IS in
53
+ // scope, and implemented below, is a narrow, honest, TypeScript-only sliver:
54
+ // `_extractTsTypes` does a SEPARATE, read-only `@babel/core` `parseSync` pass
55
+ // over `.ts`/`.tsx` source (never touching ir/parser-js.js's own transform
56
+ // pipeline or its shared, heavily-relied-on visitor plugin) and walks the
57
+ // resulting AST for `TSTypeAnnotation` nodes on function parameters and
58
+ // `let`/`const`/`var` declarators. Babel's `parseSync` only PARSES — it never
59
+ // runs `@babel/preset-typescript`'s stripping transform, so a `.ts` file's
60
+ // type annotations are fully intact on the AST it returns, no plugin-ordering
61
+ // trick required. Matching this SEPARATE parse's declarations back to the
62
+ // real IR's decls is done by `(line, name)` — the same real source line and
63
+ // identifier name a given parameter or declarator sits on in both parses,
64
+ // since both parse the identical source text. A `.js` file, a type Babel
65
+ // cannot express as a plain name (destructured/computed types, deeply
66
+ // generic types past a small recursion depth), or any parse/traverse failure
67
+ // all degrade to `type: null` — never a guess, matching every other
68
+ // optional field in this module.
69
+
70
+ import { parseSync as babelParseSync, traverse as babelTraverse } from '@babel/core';
71
+ import presetTypescript from '@babel/preset-typescript';
72
+
73
+ const MAX_EXPR_DEPTH = 40;
74
+ const MAX_TS_TYPE_DEPTH = 6;
75
+
76
+ function _calleeToString(callee, depth = 0) {
77
+ if (typeof callee === 'string') return callee;
78
+ if (!callee || typeof callee !== 'object' || depth > MAX_EXPR_DEPTH) return '';
79
+ switch (callee.kind) {
80
+ case 'ident': return callee.name || '';
81
+ case 'member': {
82
+ const obj = _calleeToString(callee.object, depth + 1);
83
+ return obj ? `${obj}.${callee.prop || ''}` : String(callee.prop || '');
84
+ }
85
+ case 'call': return _calleeToString(callee.callee, depth + 1);
86
+ default: return '';
87
+ }
88
+ }
89
+
90
+ function _identifiersIn(expr, out, depth = 0) {
91
+ if (!expr || typeof expr !== 'object' || depth > MAX_EXPR_DEPTH) return;
92
+ switch (expr.kind) {
93
+ case 'ident':
94
+ if (expr.name) out.push(expr.name);
95
+ return;
96
+ case 'literal':
97
+ if (expr.value != null) out.push(String(expr.value));
98
+ return;
99
+ case 'member':
100
+ _identifiersIn(expr.object, out, depth + 1);
101
+ if (expr.prop) out.push(String(expr.prop));
102
+ return;
103
+ case 'binary':
104
+ case 'logical':
105
+ _identifiersIn(expr.left, out, depth + 1);
106
+ _identifiersIn(expr.right, out, depth + 1);
107
+ return;
108
+ case 'tpl':
109
+ for (const p of expr.parts || []) _identifiersIn(p, out, depth + 1);
110
+ return;
111
+ case 'call':
112
+ _identifiersIn(expr.callee, out, depth + 1);
113
+ for (const a of expr.args || []) _identifiersIn(a, out, depth + 1);
114
+ return;
115
+ case 'array':
116
+ for (const e of expr.elements || []) _identifiersIn(e, out, depth + 1);
117
+ return;
118
+ case 'object':
119
+ for (const p of expr.props || []) _identifiersIn(p.value, out, depth + 1);
120
+ return;
121
+ case 'union':
122
+ for (const b of expr.branches || []) _identifiersIn(b, out, depth + 1);
123
+ return;
124
+ default:
125
+ return;
126
+ }
127
+ }
128
+
129
+ function _argsToTextArgs(args) {
130
+ return (args || []).map(a => {
131
+ const names = [];
132
+ _identifiersIn(a, names);
133
+ return { text: names.join(' ') };
134
+ });
135
+ }
136
+
137
+ // A TSQualifiedName is `A.B.C` (e.g. `Foo.Bar`) — dot-join it the same way
138
+ // _calleeToString dot-joins a member expression above.
139
+ function _qualifiedTypeName(node, depth = 0) {
140
+ if (!node || depth > MAX_TS_TYPE_DEPTH) return null;
141
+ if (node.type === 'Identifier') return node.name || null;
142
+ if (node.type === 'TSQualifiedName') {
143
+ const left = _qualifiedTypeName(node.left, depth + 1);
144
+ const right = node.right?.name || null;
145
+ if (!left || !right) return left || right;
146
+ return `${left}.${right}`;
147
+ }
148
+ return null;
149
+ }
150
+
151
+ /**
152
+ * Renders a TSType AST node to a short, human-readable string — never a
153
+ * best-effort re-render of the whole type, just enough to be a real,
154
+ * non-fabricated signal. Anything not recognized (mapped types, conditional
155
+ * types, indexed access, deeply nested generics past the depth guard) is
156
+ * `null`, exactly like every other "don't know" in this module — no type
157
+ * string is ever invented for a shape this doesn't genuinely understand.
158
+ */
159
+ function _tsTypeToString(node, depth = 0) {
160
+ if (!node || depth > MAX_TS_TYPE_DEPTH) return null;
161
+ switch (node.type) {
162
+ case 'TSStringKeyword': return 'string';
163
+ case 'TSNumberKeyword': return 'number';
164
+ case 'TSBooleanKeyword': return 'boolean';
165
+ case 'TSAnyKeyword': return 'any';
166
+ case 'TSUnknownKeyword': return 'unknown';
167
+ case 'TSVoidKeyword': return 'void';
168
+ case 'TSNullKeyword': return 'null';
169
+ case 'TSUndefinedKeyword': return 'undefined';
170
+ case 'TSNeverKeyword': return 'never';
171
+ case 'TSObjectKeyword': return 'object';
172
+ case 'TSBigIntKeyword': return 'bigint';
173
+ case 'TSSymbolKeyword': return 'symbol';
174
+ case 'TSTypeReference': {
175
+ const name = node.typeName?.type === 'Identifier'
176
+ ? node.typeName.name
177
+ : _qualifiedTypeName(node.typeName, depth + 1);
178
+ if (!name) return null;
179
+ // Babel's current AST names this `typeArguments`; `typeParameters` was
180
+ // the property on older @babel/parser versions for the same node —
181
+ // checking both means this doesn't silently go generic-blind on a
182
+ // dependency bump either direction.
183
+ const paramNodes = (node.typeArguments || node.typeParameters)?.params || [];
184
+ const args = paramNodes.map(p => _tsTypeToString(p, depth + 1)).filter(Boolean);
185
+ return (args.length && args.length === paramNodes.length) ? `${name}<${args.join(', ')}>` : name;
186
+ }
187
+ case 'TSArrayType': {
188
+ const el = _tsTypeToString(node.elementType, depth + 1);
189
+ return el ? `${el}[]` : null;
190
+ }
191
+ case 'TSUnionType': {
192
+ const parts = (node.types || []).map(t => _tsTypeToString(t, depth + 1)).filter(Boolean);
193
+ return parts.length === (node.types || []).length ? parts.join(' | ') : null;
194
+ }
195
+ case 'TSLiteralType': {
196
+ const lit = node.literal;
197
+ if (lit?.type === 'StringLiteral') return JSON.stringify(lit.value);
198
+ if (lit?.type === 'NumericLiteral') return String(lit.value);
199
+ if (lit?.type === 'BooleanLiteral') return String(lit.value);
200
+ return null;
201
+ }
202
+ default: return null;
203
+ }
204
+ }
205
+
206
+ /**
207
+ * FR-401's "types" element, TypeScript-only. A separate, read-only parse of
208
+ * the real source — never the transformed AST ir/parser-js.js builds its IR
209
+ * from, and never a mutation of anything that module owns. `parseSync` only
210
+ * parses; `@babel/preset-typescript`'s stripping transform never runs, so
211
+ * `.ts`/`.tsx` type annotations are fully intact on the returned AST.
212
+ *
213
+ * Keyed by the real source line a parameter or declarator's IDENTIFIER sits
214
+ * on (matching how ir/parser-js.js's own IR already records a parameter's
215
+ * line as its enclosing function's start line, and an assignment's line as
216
+ * its own CFG node's line) so `_adaptFile` can look a type up by the exact
217
+ * (line, name) pair it already has, without needing any cross-parse node
218
+ * identity.
219
+ *
220
+ * @returns {Map<number, Map<string,string>>} line -> (name -> type string)
221
+ */
222
+ function _extractTsTypes(file, content) {
223
+ const out = new Map();
224
+ if (!/\.tsx?$/i.test(file) || typeof content !== 'string' || content.length > 500_000) return out;
225
+ const record = (name, line, typeAnnotationNode) => {
226
+ if (!name || !line || !typeAnnotationNode) return;
227
+ const t = _tsTypeToString(typeAnnotationNode.typeAnnotation);
228
+ if (!t) return;
229
+ if (!out.has(line)) out.set(line, new Map());
230
+ out.get(line).set(name, t);
231
+ };
232
+ let ast;
233
+ try {
234
+ ast = babelParseSync(content, {
235
+ filename: file,
236
+ presets: [[presetTypescript, { ignoreExtensions: true }]],
237
+ // Mirrors ir/parser-js.js's own parserOpts exactly — accepting the same
238
+ // decorator syntax it accepts, so this separate parse does not reject
239
+ // (and silently lose types from) a file the real IR parser accepts.
240
+ parserOpts: { plugins: ['decorators-legacy', 'decoratorAutoAccessors'] },
241
+ babelrc: false, configFile: false, ast: true, code: false,
242
+ });
243
+ } catch { return out; }
244
+ if (!ast) return out;
245
+ try {
246
+ babelTraverse(ast, {
247
+ Function(path) {
248
+ for (const p of path.node.params || []) {
249
+ const resolved = p.type === 'AssignmentPattern' ? p.left : p;
250
+ if (resolved?.type === 'Identifier' && resolved.typeAnnotation) {
251
+ record(resolved.name, path.node.loc?.start?.line, resolved.typeAnnotation);
252
+ }
253
+ }
254
+ },
255
+ VariableDeclarator(path) {
256
+ const id = path.node.id;
257
+ if (id?.type === 'Identifier' && id.typeAnnotation) {
258
+ record(id.name, path.node.loc?.start?.line, id.typeAnnotation);
259
+ }
260
+ },
261
+ });
262
+ } catch { /* degrade to whatever was recorded before the failure */ }
263
+ return out;
264
+ }
265
+
266
+ /**
267
+ * FR-401's "storage" element. Reindexes the whole-project, field-name-keyed
268
+ * stored-taint registry (engine.js#buildStoredTaintRegistry — already
269
+ * computed once per scan for crossStoredTaint's own cross-file correlation,
270
+ * not recomputed here) down to just the writes that happened in THIS file.
271
+ * `storedRegistry` is `{ [fieldName]: [{file, line, snippet, named}, ...] }`;
272
+ * this returns the flattened, per-file subset as
273
+ * `[{field, line, snippet, named}, ...]`.
274
+ */
275
+ function _storageForFile(file, storedRegistry) {
276
+ const out = [];
277
+ if (!storedRegistry || typeof storedRegistry !== 'object') return out;
278
+ for (const [field, writes] of Object.entries(storedRegistry)) {
279
+ for (const w of writes || []) {
280
+ if (w && w.file === file) out.push({ field, line: w.line ?? 0, snippet: w.snippet || '', named: !!w.named });
281
+ }
282
+ }
283
+ return out;
284
+ }
285
+
286
+ /**
287
+ * @param {object} fileIR - one entry from ir/index.js#buildProjectIR's `perFile` map: {file, functions, topLevel}
288
+ * @param {Map<number, Map<string,string>>|null} [tsTypes] - `_extractTsTypes`'s
289
+ * output for this same file. Optional: omitted (or no match at a given
290
+ * (line, name)) leaves `type: null`, exactly as before this existed.
291
+ * @returns {{decls: Array, calls: Array}}
292
+ */
293
+ function _adaptFile(fileIR, tsTypes) {
294
+ const decls = [];
295
+ const calls = [];
296
+ const returns = [];
297
+ const typeAt = (line, name) => tsTypes?.get(line)?.get(name) ?? null;
298
+ for (const fn of fileIR?.functions || []) {
299
+ for (const paramName of fn.params || []) {
300
+ if (typeof paramName === 'string' && paramName) {
301
+ // `kind` genuinely distinguishes a parameter from a later
302
+ // assignment (FR-401's "parameters" vs "assignments" elements) —
303
+ // annotatePrivacyTaint() ignores unknown fields, so this is purely
304
+ // additive for that consumer.
305
+ decls.push({ name: paramName, line: fn.line ?? 0, type: typeAt(fn.line ?? 0, paramName), kind: 'parameter' });
306
+ }
307
+ }
308
+ const nodes = fn.cfg?.nodes || {};
309
+ for (const node of Object.values(nodes)) {
310
+ if (!node) continue;
311
+ if (node.kind === 'assign' && node.target) {
312
+ const line = node.line ?? fn.line ?? 0;
313
+ decls.push({ name: node.target, line, type: typeAt(line, node.target), kind: 'assignment' });
314
+ }
315
+ if (node.kind === 'call') {
316
+ calls.push({
317
+ line: node.line ?? fn.line ?? 0,
318
+ fullPath: _calleeToString(node.callee),
319
+ args: _argsToTextArgs(node.args),
320
+ });
321
+ } else if (node.kind === 'assign' && node.source && node.source.kind === 'call') {
322
+ calls.push({
323
+ line: node.line ?? fn.line ?? 0,
324
+ fullPath: _calleeToString(node.source.callee),
325
+ args: _argsToTextArgs(node.source.args),
326
+ });
327
+ } else if (node.kind === 'return' && node.value) {
328
+ // FR-401's "returns" element: every CFG language parser genuinely
329
+ // emits return-kind nodes (confirmed across parser-js/py/java/go/
330
+ // rb/php/cs/kt/cpp), so this is real IR data, not a stub — surfaced
331
+ // here even though annotatePrivacyTaint() (the shallow walker) does
332
+ // not consume it today, matching the adapter's stated job of
333
+ // supplying what the real IR has, not just what today's one
334
+ // consumer happens to read (see D-0052's evidence for the honest
335
+ // caveat: no current caller reads this field yet).
336
+ const names = [];
337
+ _identifiersIn(node.value, names);
338
+ returns.push({ line: node.line ?? fn.line ?? 0, names });
339
+ }
340
+ }
341
+ }
342
+ return { decls, calls, returns };
343
+ }
344
+
345
+ /**
346
+ * Build the flat, per-file map annotatePrivacyTaint() expects, from the real
347
+ * Layer-1 IR plus the raw file contents (for the `_content` field it uses
348
+ * for snippet extraction — the IR itself carries no raw source text).
349
+ *
350
+ * @param {Record<string,object>} perFileIR - ir/index.js#buildProjectIR's `perFile` (or buildProjectIRAsync's)
351
+ * @param {Record<string,string>} fileContents
352
+ * @param {object|null} [storedRegistry] - engine.js#buildStoredTaintRegistry's
353
+ * output, for FR-401's "storage" element. Optional and additive: omitted
354
+ * (or not an object) yields `storage: []` for every file, same
355
+ * degrade-gracefully convention as every other field here — this is not a
356
+ * second copy of the registry's own computation, just a per-file view of
357
+ * the one the engine already builds once per scan.
358
+ * @returns {Map<string, {_content:string, decls:Array, calls:Array, returns:Array, storage:Array}>}
359
+ */
360
+ export function adaptIRForPrivacyTaint(perFileIR, fileContents, storedRegistry) {
361
+ const out = new Map();
362
+ const fc = fileContents || {};
363
+ for (const [file, content] of Object.entries(fc)) {
364
+ if (typeof content !== 'string') continue;
365
+ const fileIR = perFileIR ? perFileIR[file] : null;
366
+ // FR-401's "types" element: a real, TypeScript-only signal via a
367
+ // separate parse (see _extractTsTypes's own header) — a no-op Map for
368
+ // every non-.ts/.tsx file, so this costs nothing on the common path.
369
+ const tsTypes = _extractTsTypes(file, content);
370
+ const { decls, calls, returns } = fileIR ? _adaptFile(fileIR, tsTypes) : { decls: [], calls: [], returns: [] };
371
+ const storage = _storageForFile(file, storedRegistry);
372
+ out.set(file, { _content: content, decls, calls, returns, storage });
373
+ }
374
+ return out;
375
+ }
376
+
377
+ export const _internals = {
378
+ _calleeToString, _identifiersIn, _argsToTextArgs, _adaptFile, _storageForFile,
379
+ _tsTypeToString, _qualifiedTypeName, _extractTsTypes,
380
+ };