@clear-capabilities/agentic-security-scanner 0.136.9 → 0.137.1

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.
package/CHANGELOG.md CHANGED
@@ -9,6 +9,665 @@
9
9
  > make the history less accurate, not more.
10
10
 
11
11
 
12
+ ## 0.137.1 — Dependabot policy for the deliberately-vulnerable fixtures
13
+
14
+ Housekeeping release. Adds `.github/dependabot.yml` so Dependabot leaves the
15
+ intentionally-vulnerable fixture directories alone — `examples/demo-app`,
16
+ `scanner/test/fixtures/**`, and `bench/**` pin old, known-vulnerable
17
+ dependencies on purpose so the SCA/SBOM/CVE detectors and the tutorials have
18
+ real findings to surface (and `scanner/test/demo-app.test.js` asserts a CVE
19
+ finding on a pinned dep). An `ignore: "*"` entry suppresses both version- and
20
+ security-update PRs for those directories. The real trees (`scanner/`,
21
+ `ide/vscode/`) get no version-update entries — their currency stays enforced by
22
+ the `dependency-currency` release gate — and repo-level security updates still
23
+ cover them.
24
+
25
+ No engine or detector changes.
26
+
27
+ ## 0.137.0 — detection-gap remediation Themes B+D, C, E, plus R9, the R16 close-out, and the docs overhaul
28
+
29
+ Seven independent slices of `docs/DETECTION_GAP_REMEDIATION_PRD.md` land
30
+ together here (R6, R8, R9, R10, R11, R13, R14(a), R14(b), R16), alongside the
31
+ world-class docs overhaul (`docs/DOCS_OVERHAUL_PRD.md`). Each has its own
32
+ subsection below, and each subsection carries its own verification paragraph —
33
+ the numbers in one do not describe the other.
34
+
35
+ ### Docs overhaul — a learning layer, an accuracy pass, and an anti-rot gate
36
+
37
+ The product had a strong evidence layer (architecture, metrics, compliance
38
+ maps) and no learning layer. This release adds one, and repairs what was false.
39
+
40
+ - **A deliberately-vulnerable demo app** at `examples/demo-app/` — ~10 files
41
+ spanning every pillar (SQLi, missing auth, eval, MD5 hashing, prompt
42
+ injection, hardcoded key, Dockerfile hygiene, vulnerable deps). Its promised
43
+ findings are pinned by `scanner/test/demo-app.test.js` (wired into
44
+ `test:smoke`) so a detector change can't silently make the tutorials lie. It
45
+ is outside the self-scan gate's target set, so it never perturbs that gate.
46
+ - **A 15-minute quickstart** (`docs/guides/quickstart.md`) and **six
47
+ task-oriented how-to guides** — scanning, fixing, SBOM/AI-BOM, compliance, CI
48
+ setup, leaked-secret response — plus a **CLI reference**, a **configuration &
49
+ env-var reference**, and a **docs hub** (`docs/README.md`). Every command
50
+ shown was run against the demo app before being documented.
51
+ - **Accuracy pass** — repaired every false/contradictory claim the doc survey
52
+ found: version drift across four manifests (`gemini-extension.json` was
53
+ ~60 versions stale), the model-cost-optimizer default contradiction, the
54
+ compliance `--gap` row, a skill pointing at a deleted command file (revoke-URL
55
+ matrix restored inline), the README's `hunt`-is-a-slash-command claim, and
56
+ `secure --tour`/`--daily` documented-but-unimplemented (now implemented).
57
+ - **New anti-rot gate** — `scripts/check-doc-drift.mjs --gate` fails on any
58
+ dangling internal link across README/docs/commands/skills/agents; wired into
59
+ the release gate as `doc-links` and proven both directions. Manifest
60
+ version-sync now also covers `gemini-extension.json`.
61
+ - **Two output-correctness fixes surfaced while documenting:** the CycloneDX/
62
+ SPDX SBOM tool version was hardcoded `0.7.0` — now stamped from the real
63
+ engine version via `meta.engineVersion`. And `js-yaml` was bumped
64
+ `5.2.3 → 5.3.0` to clear the dependency-currency gate.
65
+
66
+ Verification: `test:smoke` 30/30 (includes the two demo-app contract tests),
67
+ `sbom` 3/3, `release-check` 49/49, `check-doc-drift --gate` clean and
68
+ fails-on-planted-break. Full `npm test` + the release gate run on push.
69
+
70
+ ### R9 — Java call-graph edges existed in the CFG but never reached the call graph
71
+
72
+ `ir/parser-java.js` never emitted `fn.calls`, leaving every Java function's
73
+ call-graph edges permanently empty (`callgraph.js` reads `fn.calls`
74
+ exclusively). Wired the same shared, language-agnostic call-extraction helper
75
+ six other parsers already use — no new extraction logic, matching the
76
+ identical precedent set by Ruby's earlier fix. A final-review fix wave
77
+ rebuilt the bundle, corrected doc overclaims, and added a resolution proof
78
+ test (`test/parser-java-calls.test.js`, 94 lines).
79
+
80
+ ### R16 — independent population re-measured; the finding is the absence of movement
81
+
82
+ Re-ran `bench/independent` (110 GHSA-labelled entries, fresh fetch, scan
83
+ state wiped) after seven PRD themes landed since the last measurement. Result
84
+ is identical, entry for entry, to the 2026-08-09 run — same TP/FP/FN/TN, same
85
+ per-language split, same recall across all ~40 CWE categories. Reported
86
+ plainly rather than explained away: the independent population has zero
87
+ Java/C#/Kotlin/PHP/Ruby/Go entries, so R8/R9 could not have moved it
88
+ structurally; Theme A (the plan's own hypothesized dominant lever, which also
89
+ landed after the baseline) plus R6/R10/R11/R13/R14(a) could have moved a
90
+ JS/TS/Python entry and none did. This measurement cannot distinguish "fix
91
+ doesn't occur in these 110 entries' shapes" from "effect masked elsewhere in
92
+ the same scan" — only that the net observable outcome per entry is unchanged.
93
+ The PRD backlog is closed on this basis.
94
+
95
+ ### Theme B+D (R6, R10, R11) — semantic grounding and interprocedural completeness
96
+
97
+ Closes three of the five open items in `docs/DETECTION_GAP_REMEDIATION_PRD.md`'s
98
+ Theme B ("semantic grounding of matching") and Theme D ("interprocedural
99
+ completeness"). R7 and R12 — filed under the same two themes — turned out to
100
+ already be landed (commit `553f9a5`, swept in opportunistically alongside
101
+ Theme A's nine fixes).
102
+
103
+ - **Class Hierarchy Analysis is now wired into the deep pipeline** —
104
+ prerequisite infrastructure for R6 and R11. `ir/class-hierarchy.js` and the
105
+ receiver-type heuristic (`dataflow/receiver-context.js`) were both already
106
+ built and unit-tested but never consulted at scan time; `dataflow/index.js`
107
+ now builds CHA once per scan and threads it through every `callContext`.
108
+ Landing this exposed a real, independent pre-existing bug in
109
+ `class-hierarchy.js` itself: its method-qid parser assumed a dot-joined
110
+ `"ClassName.method"` shape, but the parser's actual qid format for a class
111
+ method is `::`-joined (`file.js::ClassName::method@line`) — so `cha.classes`
112
+ was silently empty for every JS/TS class, and CHA-based resolution could
113
+ never have worked at all until this was fixed. The only prior test for
114
+ `buildClassHierarchy` had hand-mocked a qid in the wrong shape, which is why
115
+ this went unnoticed.
116
+ - **R6 — catalog sink matching is now gated by CHA-inferred receiver type.**
117
+ A bare-name sink like `.query()` or `.get()` previously matched on ANY
118
+ receiver project-wide (`cache.query(x)` scored identically to
119
+ `db.query(x)`). An opt-in `match.receiverTypeIn` catalog field is now
120
+ declared on the 5 highest-FP-risk bare-name entries (`js-sql-query`,
121
+ `js-sql-execute`, `py-requests-get` x2, `rb-erb-new`). Unknown receiver type
122
+ never suppresses a match — only a confidently resolved, non-matching type
123
+ does.
124
+
125
+ **Coverage is not uniform across those 5, and the honest summary is that
126
+ only the two JS entries do real work.** The gate can only fire when CHA
127
+ actually resolves a receiver type, and CHA's `typeOfVar` is populated from
128
+ exactly one shape: a local `let/const x = new Foo()` whose IR carries the
129
+ `isNew` marker — emitted today by the JS/TS, Java and C# parsers only.
130
+ So `rb-erb-new` is effectively inert: `ERB.new(x)`'s receiver is a bare
131
+ identifier that is never `new`-assigned, so the type is always unknown and
132
+ the entry always stays permissive. The two `py-requests-get` entries are
133
+ inert for the same reason (Python has no `new`, so its parser emits no
134
+ marker). Both are harmless — an inert gate is a permissive gate, and the
135
+ pattern layer's match survives untouched — but "applied to 5 entries"
136
+ should not be read as "gating 5 entries."
137
+ - **R10 — a call nested inside another expression now consults the callee's
138
+ own taint summary.** `sink(getUserInput())` previously only checked
139
+ `getUserInput()`'s own arguments for taint (the call's return-taint was
140
+ invisible outside assignment-RHS and bare-statement position, the only two
141
+ places the summary cache was consulted). `exprTaint`'s `'call'` case now
142
+ also resolves and consults the callee's summary, via the same shared
143
+ resolver R11 uses.
144
+ - **R11 — a JS/TS member call (`svc.save(x)`) now resolves interprocedurally
145
+ when CHA traces the receiver to one unambiguous, assignment-tracked local
146
+ variable.** Previously refused unconditionally (a bare dotted-name guess
147
+ risks inventing an edge between two unrelated same-named methods). This
148
+ landed narrower than originally scoped: it deliberately still refuses
149
+ `this.field.method()` resolution. An early implementation reused R6's full
150
+ receiver-type heuristic, including its two name-guess fallbacks
151
+ (`this.field` PascalCase-to-class guessing, bare-identifier soft-labeling) —
152
+ safe for R6's weaker consequence (mis-gating an *existing* catalog match),
153
+ but review found that reusing the same guesses for R11's stronger
154
+ consequence (fabricating a *new* interprocedural call-graph edge) let a
155
+ same-named unrelated variable resolve to the wrong class purely by name
156
+ coincidence. R11 now calls `classOfVar` directly, trusting only genuinely
157
+ assignment-tracked local types, and an ambiguous or unresolved receiver
158
+ (including every `this.field` shape) still safely refuses to resolve rather
159
+ than guessing — matching this PRD's own stated caution that R11 should stay
160
+ unimplemented rather than ship with degraded precision.
161
+
162
+ Two narrower gaps surfaced during R11 implementation and were deliberately
163
+ left unfixed as out of scope (recorded as candidate future work in
164
+ `docs/DETECTION_GAP_REMEDIATION_PRD.md`'s new "Status updates" section): CHA's
165
+ variable-type tracking is scoped to the exact enclosing function (a
166
+ module-scope instance referenced from inside a closure/route-handler can't be
167
+ typed there), and taint-argument recognition only handles bare-identifier or
168
+ one-level member-access call arguments (a two-level access like
169
+ `req.query.cmd` passed directly is invisible to it).
170
+
171
+ - **A wiring-and-verification pass on this work caught R6 suppressing a real
172
+ finding, and the fix landed the same way it was found: with a gate.**
173
+ `bench:layer-recall:check` — which exists precisely to catch a layer going
174
+ quiet on a language it used to cover — flagged `js/ts` taint recall
175
+ dropping 7 → 6. Root cause: `_receiverTypeFor` fell back to returning the
176
+ receiver's own bare identifier name (e.g. `c`) whenever `classOfVar`
177
+ couldn't verify a type, and the caller then treated that name as a
178
+ confidently-resolved non-match rather than as "unknown" — a direct
179
+ violation of R6's own "unknown != clean" rule. Concretely:
180
+ `const c = mysql.createConnection({}); c.query(tainted)` (a `mysql`
181
+ connection assigned via a factory call rather than `new X()`, the exact
182
+ shape `CVE-2021-22214-node-sqli-shape` exercises) was silently dropped by
183
+ the taint engine, because `'c'` doesn't match the SQL receiver allow-list —
184
+ even though it's a genuine, tainted SQL sink. `bench:cve-replay:check`
185
+ stayed green throughout, because a different, non-taint layer happened to
186
+ still catch this same corpus entry — the corpus gate answers "was it
187
+ detected at all," not "by which layer," which is exactly the blind spot
188
+ `bench:layer-recall:check` exists to close. Fixed by removing the
189
+ bare-identifier fallback: a non-`this` receiver is now trusted only when
190
+ `classOfVar` genuinely resolves it, mirroring the fix already applied to
191
+ R11 above. This is exactly the kind of near-miss the full gate sequence
192
+ (test, corpus, mutation, layer-recall) exists to catch before it ships, and
193
+ it did.
194
+ - **That fix was too narrow, and the whole-branch review caught it: the same
195
+ bug class had three more instances in the same function.** The round-4 fix
196
+ above removed the bare-identifier fallback but kept the `this.field`
197
+ PascalCase guess, on the reasoning that it wasn't implicated in *that*
198
+ regression. It was implicated in the identical one. `receiverTypeAtCall`'s
199
+ `this`-branch structurally cannot return `null` — it PascalCases the field
200
+ name and returns it — so every `this.<field>.method()` call was treated as a
201
+ confidently-resolved type, and only field names that happened to collide
202
+ with the allow-list vocabulary survived. `this.dbConn.query(req.query.q)`
203
+ and `this.readReplica.query(req.query.q)` are both real SQL injections, both
204
+ reported by the pre-branch base commit, and both were silently absent from
205
+ this branch — not demoted, gone, with no other layer catching them. Two more
206
+ instances alongside it: for a multi-segment chain like `svc.db.query(x)` the
207
+ code resolved `parts[0]` — the chain ROOT — answering "what type is `svc`?"
208
+ when the receiver is `svc.db`, a property path CHA never types at all; and
209
+ `buildClassHierarchy`'s `typeOfVar` walker accepted any PascalCase callee as
210
+ a constructor, so `const q = BuildCache()` was confidently mistyped as class
211
+ `BuildCache`. All three were name-or-shape guesses being trusted as
212
+ resolutions.
213
+
214
+ Rather than a fourth one-off patch, `_receiverTypeFor` now states the one
215
+ thing CHA can actually verify and refuses everything else: a receiver chain
216
+ of exactly two dot-separated parts (`x.method`), resolved through
217
+ `classOfVar`. `this`-rooted and multi-segment chains return `null` —
218
+ unknown, permissive. `parser-js.js` now emits the `isNew` marker on
219
+ `NewExpression` (matching what the Java and C# parsers already emit) and
220
+ `buildClassHierarchy` requires it, so a PascalCase *factory* call is no
221
+ longer mistaken for a constructor. Separately, the `receiverTypeIn`
222
+ vocabularies were exact-anchored (`^(?:db|pool|conn…)$`) from back when the
223
+ value reaching them could be a bare variable name; now that only real class
224
+ names arrive, `DatabaseConnection`, `PrismaClient` and `MySQLConnection` all
225
+ failed the allow-list and were suppressed, while only a class literally
226
+ named `Db` passed — the existing test passed solely because its fixture was
227
+ named `class Db`. Those four patterns are now substring matches
228
+ (`rb-erb-new`'s `^ERB$` stays anchored: one exact class, not a vocabulary),
229
+ and `Cache` still correctly suppresses.
230
+
231
+ The claim two bullets up — "unknown receiver type never suppresses a match"
232
+ — was false on the `this.field` path for the whole of this branch's life
233
+ until now. It is true again, and it is now gated rather than asserted:
234
+ `bench/mutation/` gained a detection dimension and four R6 cases, two of
235
+ them metamorphic renames (`class Db` → `class DatabaseConnection`,
236
+ `this.db` → `this.dbConn`) that a vocabulary-keyed gate cannot survive, plus
237
+ an adversarial non-DB receiver so that simply deleting the gate cannot pass
238
+ either. Both metamorphic cases fail on the pre-fix engine. Three rounds of
239
+ this same false-negative class shipped behind human review; the mutation
240
+ gate is what makes a fourth fail loudly instead.
241
+
242
+ **Verification — Theme B+D (R6, R10, R11) only:** full test gate green
243
+ (`npm test`, 3146 tests), corpus (214/214, no drift), mutation (9/9
244
+ verdict-flip, and non-zero exit confirmed against the pre-fix engine) and
245
+ layer-recall (214/214 detected, per-language taint counts equal to baseline)
246
+ all green. These figures predate the R13 work below, which was gated
247
+ separately; see R13's own verification paragraph for the current totals.
248
+
249
+ ### Theme E (R13) — flow-modeling coverage, both sub-fixes
250
+
251
+ - **R13(a) — a member-write assignment target (`el.innerHTML = tainted`) is
252
+ now consulted against the sink catalog.** The taint engine previously only
253
+ checked call expressions against the sink catalog; a plain property
254
+ assignment with no call syntax at all — the PRD's own success metric,
255
+ `el.innerHTML = req.query.x` — was structurally invisible regardless of
256
+ taint. `dataflow/catalog.js` gains a small member-write sink table and
257
+ `matchMemberWriteSink(targetPath, file)`; `dataflow/engine.js` consults it
258
+ on assignment targets alongside the existing call-sink path.
259
+ - **R13(b) — a for-of loop variable now carries the iterated expression's
260
+ taint into the loop body.** `for (const item of req.body.items) { eval(item) }`
261
+ — the PRD's other stated success metric — previously read `item` as clean:
262
+ the shared Babel loop visitor never bound the for-of loop variable to what
263
+ it iterates. `ir/parser-js.js`'s loop visitor now synthesizes an
264
+ `item = <iterated expr>` assignment in its `enter()` hook, scoped strictly
265
+ to `ForOfStatement`; the other four loop-statement types that funnel
266
+ through the same shared visitor (`for`, `while`, `do-while`, `for-in`) are
267
+ pinned byte-identical in CFG output by a dedicated regression test, since
268
+ a shared-visitor edit is the single riskiest shape of change this plan
269
+ made.
270
+
271
+ **This one took three extra fix rounds, and all three are worth recording
272
+ honestly.** First: a second, generic Babel visitor
273
+ (`VariableDeclarator`) also fires for the for-of binding's own
274
+ `const item` declarator and runs after the loop visitor's `enter()` but
275
+ before the body, silently overwriting the just-synthesized assignment
276
+ with `source:unknown` — fixed with a guard skipping that declarator.
277
+ That guard's first version was over-broad: it skipped *any*
278
+ `ForOfStatement` `left` declarator, which also deleted the same visitor's
279
+ pre-existing destructuring taint-KILL nodes and regressed
280
+ `for (const {cmd} of SAFE) eval(cmd)` to a false positive (the
281
+ destructured `cmd` should shadow and clear an outer tainted `cmd` of the
282
+ same name, and briefly stopped doing so). Narrowed to
283
+ `path.node.id?.type === 'Identifier'` so only the simple-identifier shape
284
+ the loop visitor actually synthesizes for is skipped; destructuring falls
285
+ through unaffected, now pinned by a regression test.
286
+
287
+ Second, and unrelated to the guard bug: R13(b)'s new taint capability made
288
+ a genuinely pre-existing, independent bug newly reachable inside the
289
+ scanner's *own* `ir/type-stubs.js` — `catalog.js`'s `js-exec` entry
290
+ matches any `X.exec(tainted)` by bare property name with no receiver-type
291
+ check, so `RegExp.exec()` calls newly carrying taint via the for-of fix
292
+ got misidentified as `child_process.exec` command injection. Confirmed
293
+ independent of the loop change (reproduces on a trivial non-loop fixture)
294
+ and traced to a catalog entry that predates this PRD entirely
295
+ (`f0d7e03`). Required a `bench/self-scan/BASELINE.json` update
296
+ (`dataflow/index.js: 0→5`, `ir/type-stubs.js: 6→10`), not a code fix —
297
+ logged as its own open gap in `docs/DETECTION_GAP_REMEDIATION_PRD.md`
298
+ rather than patched here, since the real fix needs CHA to type
299
+ regex-literal-assigned variables first.
300
+
301
+ Third, found by the final whole-branch review: narrowing the guard fixed
302
+ destructuring but left the shape the guard now *owns* with no kill at all.
303
+ `const`/`let` in a for-of head is a **block-scoped** binding, and this
304
+ engine's taint model has no block scoping — so once the loop variable was
305
+ bound to the iterable's taint, that state flowed straight past the loop's
306
+ exit and over-tainted a same-named OUTER variable:
307
+ `let item = 'safe'; for (const item of req.body.items) {} eval(item)`
308
+ reported a Code Injection finding that the pre-R13 engine correctly called
309
+ clean, because the generic `VariableDeclarator` visitor used to emit a
310
+ taint-KILL there and the guard suppresses it. That directly violated this
311
+ work's own "strictly additive — never remove or alter an existing finding"
312
+ constraint. The loop visitor now records the bound name in `enter()` and
313
+ re-emits the kill in `exit()`, on the loop's normal exit edge
314
+ (`header → exit-noop → kill → post-loop code`), so in-loop taint
315
+ reachability is untouched and only the post-loop read is cleared. The
316
+ bare-assignment form (`for (x of ...)`, no `const`/`let`) deliberately gets
317
+ **no** kill — that binding is function-scoped and its value legitimately
318
+ survives the loop; killing it would itself have been a regression. Both
319
+ directions are now pinned by tests.
320
+
321
+ Both sub-fixes are covered end-to-end and at the unit level by
322
+ `test/member-write-and-loop-taint.test.js` (12 tests), wired into
323
+ `test:dataflow`.
324
+
325
+ **Verification — R13 only:** full gate green — `npm test` (3158 tests, 0
326
+ failures), corpus (214/214, no drift), mutation (12/12 mutant verdicts
327
+ correct, of which 9/9 are verdict-flip cases), layer-recall (js/ts taint recall unchanged at 7/38 vs. baseline's 7/36 — R13
328
+ lands via dedicated unit tests rather than new corpus entries, so no
329
+ taint-layer increase was expected or observed here) and self-scan (green
330
+ against the baseline this same work already updated).
331
+
332
+ ### Theme E (R14(b)) — non-JS top-level IR
333
+
334
+ Closes the other half of Theme E's R14 item: Python (CST parser and regex
335
+ fallback), PHP, and Ruby now synthesize a `<module>` function wrapping
336
+ top-level statements, mirroring the JS `<module>` pattern
337
+ (`ir/parser-js.js:264,557`) that already existed. Before this, a flat
338
+ vulnerable script with no wrapping function or class — `<?php
339
+ system($_GET['cmd']);`, a bare `system(params[:cmd])` in Ruby, a bare
340
+ `os.system(request.args)` at Python module scope — had zero Layer-2
341
+ taint-analysis coverage in these three languages, regardless of how
342
+ obviously tainted the flow was, simply because the IR layer never extracted
343
+ top-level statements into any CFG at all. Unlike JS's unconditional
344
+ `<module>` creation, all three new paths only synthesize the function when
345
+ the file actually has top-level statements worth lowering, to keep the
346
+ change's blast radius on existing function-only fixtures at zero.
347
+
348
+ Landing this also surfaced (and fixed) a real, independent severity bug:
349
+ the dead-code demotion in `dataflow/engine.js` only exempts functions whose
350
+ name matches `/handler|route|controller|middleware|endpoint/i` from being
351
+ downgraded one severity tier when the call graph records no caller — but a
352
+ synthetic `<module>` function is *never* called by anything (module scope
353
+ has no caller by construction), so every finding this work would have added
354
+ was about to land one tier too low (critical → high, etc.) the moment it
355
+ shipped. `<module>`-scoped findings are now exempt from dead-code demotion
356
+ outright. **This is a severity-tier fix for existing JS `<module>` findings
357
+ too** — nothing new is detected by it, but any JS top-level finding that was
358
+ previously silently demoted now reports at its correct severity.
359
+
360
+ End-to-end coverage: `test/r14b-module-level-e2e.test.js` runs a real
361
+ `runScan` against a minimal flat script in each of the three languages
362
+ (plus Python's regex-fallback path separately) and asserts an `IR-TAINT`
363
+ finding comes back — proving the PRD's actual success metric, not just
364
+ correct IR shape. Wired into `test:dataflow`.
365
+
366
+ **Verification — R14(b) only:** `test:dataflow` (625/625, includes the 4 new
367
+ end-to-end tests), `npm test` (3176 tests, 0 failures on an isolated rerun —
368
+ two transient `spawnSync`-timeout failures surfaced under heavy parallel
369
+ system load on the first two attempts, in `audit-cli.test.js` and
370
+ `triage-command.test.js`, neither of which this work touches, and both
371
+ cleared on rerun), corpus (214/214, no drift), mutation (9/9 verdict-flip
372
+ correct), layer-recall (no taint-layer regression; python/php/ruby taint
373
+ counts unchanged from baseline, as expected — this work lands via dedicated
374
+ unit tests, not new corpus entries) and self-scan (no drift).
375
+
376
+ ### Theme E (R14(a)) — annotation/decorator-shaped framework sources
377
+
378
+ Closes the other half of Theme E's R14 item, left open when R14(b) landed.
379
+ Framework sources expressed as parameter annotations/decorators — Spring's
380
+ `@RequestParam`/`@PathVariable`/`@RequestBody`/`@RequestHeader`, ASP.NET
381
+ Core's `[FromQuery]`/`[FromBody]`/`[FromForm]`/`[FromRoute]`/`[FromHeader]`,
382
+ NestJS's `@Query()`/`@Body()`/`@Param()`/`@Headers()` — had no catalog
383
+ representation at all: the catalog only matched callables and member reads,
384
+ and an annotation is neither. A controller method whose only taint source
385
+ was a decorated parameter was invisible to deep mode regardless of how
386
+ directly it flowed to a sink.
387
+
388
+ A new `annotation` catalog match kind (`dataflow/catalog.js`) is now
389
+ consulted at every one of the taint engine's 8 `analyzeFunction` entry
390
+ points via `_unionAnnotationTaint` (`dataflow/engine.js`), against a new
391
+ IR side-channel field, `fn.paramAnnotations`, populated by three language
392
+ extractors: `ir/parser-cs.js` (C#/ASP.NET Core attributes), `ir/parser-js.js`
393
+ (NestJS decorators), and `ir/parser-java.js` (Spring annotations). The Java
394
+ extractor also fixes a genuine, independent gap-fill that came bundled with
395
+ the annotation work: Java parameter names were never extracted at all
396
+ before this (`params: []` unconditionally) — real parameter names and
397
+ Spring annotations are now both pulled from the same `formalParameterList`
398
+ CST walk.
399
+
400
+ **Accepted false-positive risk, documented rather than silently shipped:**
401
+ matching is on the *bare* decorator/attribute name only (`ANNOTATION_INDEX`
402
+ is keyed by `pa.decorator`, `dataflow/catalog.js`'s `matchAnnotationParams`)
403
+ — there is no import-binding or namespace/package check confirming the
404
+ decorator actually came from Spring/ASP.NET Core/NestJS. A user-defined
405
+ decorator or attribute that happens to share one of these names (a custom
406
+ `@Query()` in an unrelated JS library, a hand-rolled `[FromHeader]`
407
+ attribute) would be treated as a tainted parameter source. This is the same
408
+ risk class R6 (`docs/DETECTION_GAP_REMEDIATION_PRD.md`) already accepted and
409
+ documented for bare-name sink matching before it grew a `receiverTypeIn`
410
+ companion gate; R14(a) has no equivalent gate yet, and none of the three
411
+ extractors have the type/import information available to build one today.
412
+ Left as a known, accepted gap rather than blocking the whole feature on it.
413
+
414
+ Per-task summary: Task 1 (catalog schema) needed one fix round (a missing
415
+ provenance filter, a latent bug). Task 2 (engine plumbing across all 8
416
+ `analyzeFunction` call sites) needed two: round 1's own review found the
417
+ wiring solid but test coverage only jointly proved 2 of 8 sites, and while
418
+ closing that gap it also made two wrong "impossible to isolate" claims about
419
+ two further sites that the re-review refuted with real repros and round 2
420
+ fixed properly. Two genuinely pre-existing, unrelated bugs were found along
421
+ the way and logged in the PRD rather than fixed: a class-field cross-taint
422
+ pass that has been dead code since v0.66.0, and a cross-file finding
423
+ line-number mis-attribution bug. Task 3 (C# extraction) needed one fix round
424
+ (stacked attributes on one parameter only captured the first) and surfaced a
425
+ third data point for the PRD's own R8 item (parenthesized attribute
426
+ arguments break the pre-existing C# method-detection regex), logged not
427
+ fixed. Task 4 (JS/TS extraction) needed one fix round (a defaulted-parameter
428
+ decorator was silently dropped by a type-check bug). Task 5 (Java extraction
429
+ + real parameter extraction) needed one fix round (fully-qualified
430
+ annotations recorded the wrong decorator name); its own review specifically
431
+ investigated whether Java's dropped varargs parameters could cause
432
+ positional param/annotation misattribution and confirmed they cannot, for
433
+ any code that actually compiles.
434
+
435
+ Task 6 (this entry) ran the full verification gate rather than trusting each
436
+ task's own scoped tests, and it earned its keep twice over — two genuinely
437
+ new, real issues, both fixed, neither papered over.
438
+
439
+ First, the full `npm test` run (not exercised by any single task in
440
+ isolation) surfaced a real gap: the four new NestJS catalog entries
441
+ (`js-nestjs-query`, `js-nestjs-body`, `js-nestjs-param`, `js-nestjs-headers`)
442
+ were missing the `provenance` label every JS source entry is required to
443
+ carry (`test/phase7-extensions.test.js`, scoped to `test:sast`, which none
444
+ of Tasks 1-5's own isolated `test:dataflow` runs exercised). Fixed by adding
445
+ the same provenance values already used for the equivalent Express `req.*`
446
+ sources (`url-param`/`http-body`/`path-param`/`header`).
447
+
448
+ Second, `bench:self-scan:check` flagged a brand-new finding in
449
+ `ir/parser-cs.js` itself: Task 3's new `attrRegex` had two independent
450
+ `\s*` quantifiers both able to consume the same whitespace run when the
451
+ overall match fails (no closing `]`) — a textbook adjacent-quantifier
452
+ ReDoS. Verified as a genuine vulnerability, not a detector false positive,
453
+ by direct timing measurement, end-to-end reachable through
454
+ `parseCSharpFile` on an adversarial `.cs` file, not just an isolated
455
+ microbenchmark: 2.4 seconds on a 64,000-character input, extrapolating to
456
+ roughly ten minutes at 1MB.
457
+
458
+ The first fix round moved the leading `\s*` inside the optional
459
+ parenthesized-argument group — genuinely linear (re-verified: 0.49ms at
460
+ 200,000 chars) — but the engine's own `safe-regex`-backed ReDoS heuristic
461
+ still flagged that version, so that round accepted a
462
+ `bench/self-scan/BASELINE.json` bump (`ir/parser-cs.js: 3→4`) as the
463
+ resolution. A task review caught that this repo already has precedent for
464
+ a cleaner fix to the exact same situation: commit `6bd394c`
465
+ (`class-hierarchy.js`) hit an identical "safe-regex flags a pattern that's
466
+ actually safe" case and resolved it by restructuring into two
467
+ independently-safe alternatives rather than accepting the drift, reasoning
468
+ explicitly that this "avoids relying on any one detector's judgment call."
469
+ Applying that same pattern here — two alternatives (no-args and
470
+ with-args) instead of one optional group, decorator name read from
471
+ `match[1] || match[2]` — passes `safe-regex` as `true`, independently
472
+ re-verified linear (0.63ms at 256,000 chars), and produces byte-identical
473
+ matches across a 12-shape sweep against the first round's already-fixed
474
+ version. **No baseline bump was needed after all**:
475
+ `bench/self-scan/BASELINE.json` was reverted to its pre-Task-6 state
476
+ (`ir/parser-cs.js: 3`, `scanner/src` total 621) once the restructured
477
+ regex stopped tripping the heuristic.
478
+
479
+ **Verification — full gate re-run after all fixes:** `test:dataflow`
480
+ 670/670. `npm test` — all 12 scoped sub-scripts report `fail 0`
481
+ (`test:smoke` 28, `test:glob`, `test:sast` 553, `test:posture` 1330,
482
+ `test:dataflow` 670, `test:mcp` 102, `test:report` 111, `test:bench-modules`
483
+ 70, `test:lifecycle`, plus C++-dataflow and Python suites), no `npm error`
484
+ anywhere in the run. `bench:cve-replay:check` 214/214 baselined entries, no
485
+ drift. `bench:mutation:check` 9/9 verdict-flip correct (5/5 metamorphic
486
+ hold, 4/4 adversarial flip). `bench:layer-recall:check` reports no
487
+ taint-layer recall regression across any language — expected, since R14(a)
488
+ lands via dedicated unit tests, not new corpus entries, matching R13's and
489
+ R14(b)'s own precedent. `bench:self-scan:check` clean with **zero drift
490
+ from the pre-Task-6 baseline** — the ReDoS false positive is gone rather
491
+ than accepted, matching commit `6bd394c`'s own outcome exactly.
492
+ `test/parser-cs-annotations.test.js` 5/5 after the restructure. Bundle
493
+ rebuilt (`dist/agentic-security.mjs` + `.sha256`) after each regex change;
494
+ `npm run smoke` against the rebuilt bundle correctly reports critical/high
495
+ findings on the deliberately-vulnerable fixture (exit code 3, this CLI's
496
+ documented "critical findings present" convention), and the underlying
497
+ `test:smoke` suite (28/28) already passed as part of the full gate above.
498
+
499
+ **Full Theme E (R13 + R14) is now complete.**
500
+
501
+ ### Theme C (R8) — braced control-flow body recursion, four languages
502
+
503
+ Closes the single highest-leverage IR defect this project's whole
504
+ detection-gap audit found. Java, C#, Kotlin, and PHP's statement splitters
505
+ previously dropped or mangled the body of any braced control-flow statement
506
+ (`if`/`for`/`try`/`switch`/`while`/`do`/`when`) — not just losing branch
507
+ structure, but silently deleting the statements inside, or folding them into
508
+ a bogus node. Since real-world sinks in these languages overwhelmingly sit
509
+ inside exactly this shape (try-with-resources in Java, `using`/`try` in C#,
510
+ `try`/`when` in Kotlin, `try`/`foreach` in PHP), this capped deep-mode taint
511
+ recall near zero for all four regardless of any catalog or interprocedural
512
+ work already landed elsewhere in this PRD. All four now recurse into these
513
+ bodies with a real (statement-linear) CFG walk.
514
+
515
+ - **Java** (`ir/parser-java.js`) — `walkStmts` additively extended to also
516
+ recurse into `for`/`try`/`switch`/`do`/bare-block statements. Fixed a
517
+ self-caught bug in the implementation plan's own draft code before it
518
+ shipped: try-with-resources' `catch`/`finally` were being read off the
519
+ wrong CST node, which would have silently dropped catch/finally for the
520
+ single most idiomatic JDBC pattern. One fix round closed two further
521
+ gaps: enhanced-for's loop variable now carries real taint provenance
522
+ (synthesized assign, mirroring `parser-js.js`'s `ForOfStatement`
523
+ pattern), and Java 14+ arrow-form `switch` (`case 1 -> …`) is now
524
+ recognized.
525
+ - **PHP** (`ir/parser-php.js`) — the statement splitter now flushes on a
526
+ closing `}` (previously only `;`), with `try`/`switch` recognizers and a
527
+ recursion guard added. This was the hardest task in the plan (3 fix
528
+ rounds, all substantially about line-number precision, not detection
529
+ shape) — see `docs/DETECTION_GAP_REMEDIATION_PRD.md`'s R8 status entry
530
+ for the full round-by-round narration. Fixing the splitter this way also
531
+ resolved a pre-existing bug where `if`/`while`/`foreach` bodies were
532
+ already being mis-split before this task touched them.
533
+ - **C#** (`ir/parser-cs.js`) — C# had no control-flow handling at all
534
+ before this; `_buildCfg` was built from scratch, ported from
535
+ `parser-cpp.js`'s proven recurse-into-braces pattern. One fix round:
536
+ `using (...) { }` and `lock (...) { }` bodies were invisible — `using`
537
+ being the canonical ADO.NET wrapper around exactly the sinks this task
538
+ targets, this was a real, significant gap.
539
+ - **Kotlin** (`ir/parser-kt.js`) — new recursive `_buildCfg` mirroring C#'s,
540
+ with Kotlin-specific adaptations for its trailing-lambda call syntax and
541
+ `when` expression arms. Zero fix rounds during implementation — the
542
+ cleanest of the four tasks, in part because each implementer was briefed
543
+ on the previous tasks' hard-won lessons before starting.
544
+
545
+ **Verification found and fixed a genuine regression this PRD's own new code
546
+ introduced**, not papered over: `bench:self-scan:check` flagged
547
+ `ir/parser-kt.js`'s new trailing-lambda regex as a ReDoS — the identical
548
+ defect class as this same changelog's R14(a) C# `attrRegex` finding (an
549
+ optional group sandwiched between two `\s*` quantifiers), independently
550
+ confirmed genuinely quadratic by direct timing. Fixed the same way that
551
+ precedent was: restructured into two mutually-exclusive alternatives,
552
+ re-verified linear and byte-identical across a 15-shape sweep. A second,
553
+ unrelated ReDoS in the same file's variable-declaration regex was found
554
+ during that investigation and traced to a commit five months predating this
555
+ PRD — left unfixed and logged as a candidate future item, since it wasn't
556
+ introduced by this work.
557
+
558
+ **Measured `bench/layer-recall` impact: 0 of 4 languages — an honest
559
+ result, not the one originally expected, and corrected once more after
560
+ this entry's first draft mis-attributed PHP's gain.** Baseline before:
561
+ `java 1/25, kotlin 0/20, c# 1/21, php 1/23`. Measured after: `java 1/25
562
+ (unchanged), kotlin 0/20 (unchanged), c# 1/21 (unchanged), php 2/23 (+1)`.
563
+ PHP's `+1` is real, but it is **not R8's** — commit-swap A/B testing shows
564
+ the pre-R8 `parser-php.js` still reproduces 2/23, while the pre-R14(b)
565
+ `parser-php.js` (an earlier, unrelated PRD item — PHP's `<module>`
566
+ top-level lowering) drops it back to 1/23. None of the corpus's `pre/`
567
+ fixtures — the state the benchmark actually scores — place a sink
568
+ genuinely inside a control-flow body for any of the four languages this
569
+ task touched. The underlying capability this task fixed is completely real
570
+ and independently proven, just not by this corpus: each language's own new
571
+ dedicated `runScan` unit test (`test/parser-{java,cs,kt,php}-control-flow.test.js`)
572
+ directly asserts a sink nested inside an `if`/`try`/`for`/`switch` body is
573
+ now detected where it wasn't before. `bench/layer-recall/baseline.json`
574
+ still updated to the measured counts (`entriesScored` 210→214, `php` 1→2,
575
+ plus an unrelated `js/ts` 7→8 catch-up from this baseline file not having
576
+ been regenerated since 2026-08-11 — no language decreased) — the baseline
577
+ update is correct regardless of attribution, since it reflects the
578
+ engine's actual current state. A follow-up item: this corpus needs a
579
+ fixture per language with a sink genuinely inside a control-flow body (not
580
+ a guard clause) before it can measure R8's impact at all.
581
+
582
+ **Full gate:** `test:dataflow` 725/725 (rerun clean after the ReDoS fix).
583
+ `npm test` — all 12 scoped sub-scripts green (`test:smoke` 28, `test:glob`
584
+ 13, `test:sast` 553, `test:posture` 1330, `test:dataflow` 725, `test:mcp`
585
+ 102, `test:report` 111, `test:bench-modules` 70, `test:lifecycle` 216,
586
+ `test:eval` 23, `test:discovery` 79, C++-dataflow 26, plus the Python script
587
+ suite). `bench:cve-replay:check` 214/214, no drift. `bench:mutation:check`
588
+ 9/9 verdict-flip correct. `bench:self-scan:check` clean with zero drift
589
+ from the pre-R8 baseline after the fix.
590
+
591
+ ## 0.136.10 — detection-gap remediation Theme A: dedup, family, and calibration for deep mode
592
+
593
+ An architectural audit of the SAST/taint pipeline (`docs/DETECTION_GAP_REMEDIATION_PRD.md`)
594
+ found nine structural gaps behind missed real-world vulnerability classes. This
595
+ release lands Theme A, the fixes on the production detection path:
596
+
597
+ - **Dead-code demotion silently downgraded nearly every finding.** A field-name
598
+ mismatch (`.to`/`.size` vs the real `.callee`/Array shape) meant `calledQids`
599
+ was always effectively empty, so any function not named `handler`/`route`/
600
+ `controller`/`middleware`/`endpoint` — including most real sinks — got
601
+ demoted one severity notch on every deep-mode scan that ever ran.
602
+ - **Half the sanitizer catalog was unreachable.** 191 of 381 sanitizer entries
603
+ use dotted callees (`Encode.forHtml`, `filepath.Clean`, `validator.isEmail`)
604
+ indexed under the full dotted key, but every lookup path reduced to the
605
+ callee's last segment — so these entries could never be retrieved. Catalog
606
+ lookup now tries the full dotted key before falling back to the last segment,
607
+ the behavior its own header comment already promised.
608
+ - **A sanitizer-blind kill switch.** `builtin-summaries` deleted taint outright
609
+ at ~15 name-matched builtins (`parseInt`, `encodeURIComponent`,
610
+ `DOMPurify.sanitize`...) regardless of threat family, contradicting the
611
+ engine's own documented doctrine that sanitizers demote, never kill —
612
+ `x = encodeURIComponent(t); db.query(x)` silently lost its SQLi finding.
613
+ Now demotes through the same family-scoped sanitizer gate every other path
614
+ uses.
615
+ - **Three bench-shape leaks were opt-out instead of opt-in**, violating this
616
+ repo's own documented convention (`AGENTIC_SECURITY_BENCH_SHAPE=1` to enable,
617
+ never `AGENTIC_SECURITY_BLIND_BENCH` to disable): a Juliet path-prefix
618
+ category filter with no env check at all, and two Java answer-key mechanisms
619
+ gated the wrong direction.
620
+ - **Cross-file import resolution was dead code with zero callers.** `fileContents`
621
+ was never threaded into `buildCallGraph`, so its re-export/import-binding
622
+ resolution never ran — two same-named functions in unrelated files collided
623
+ by bare name on every scan.
624
+ - **The points-to graph was built and then never wired in.** `AGENTIC_SECURITY_POINTS_TO=1`
625
+ computed a real alias graph but `runTaintEngine` never copied it onto
626
+ `callContext`, so the opt-in flag caught nothing. A second bug in
627
+ `aliasesForVar` (stripping only the first `::` instead of the qid's full
628
+ prefix) was fixed alongside it, since the qid itself always contains
629
+ multiple `::` segments.
630
+ - **Guard recognition was flow-insensitive.** `dropGuardedFindings` matched a
631
+ guard-shaped regex anywhere in a −25/+5 line window with zero correlation to
632
+ the sink's actual tainted identifier, killing real SSRF/path findings
633
+ whenever unrelated guard-shaped text sat nearby. Rewritten to require the
634
+ guard match to appear near one of the sink's own argument identifiers.
635
+ Reachability annotation was also fixed to record "unknown" rather than
636
+ "unreachable" for languages with no call-graph data — only a strict `false`
637
+ should demote a finding, and absence of evidence isn't evidence of absence.
638
+ - **Deep mode was unreachable from the MCP and LSP integration surfaces.**
639
+ `scan_diff` and `scanFile` never enabled deep mode, so the interprocedural
640
+ taint engine — the thing most likely to catch a real cross-function
641
+ vulnerability — never ran from either integration.
642
+ - **Deep-mode (IR-TAINT) findings bypassed the entire finding pipeline.** They
643
+ were appended *after* dedup, clustering, stable-ID assignment, family
644
+ backfill, confidence, and calibration had already run once, so a sink caught
645
+ by both the regex layer and deep mode produced two findings — one of them
646
+ permanently unscored (no family, no calibrated confidence, no reachability
647
+ demotion, no mitigation annotation). Deep-mode findings now enter the same
648
+ pre-dedup pool as every other detector's output and ride the identical
649
+ pipeline. Fixing this exposed two further latent bugs it's now safe to state
650
+ plainly: dedup's winner-selection didn't prefer a real interprocedural
651
+ taint-walk finding over a same-severity flat pattern match at the same sink
652
+ (an IR-TAINT finding could lose a tie and take its sanitizer/chain evidence
653
+ down with it), and root-cause clustering keyed its "same sink" bucket on a
654
+ generic catalog rule id rather than a per-line signal, so two unrelated
655
+ `eval()` calls in one file could collapse into a single reported finding the
656
+ moment deep-mode findings started reaching that annotator. Both are fixed.
657
+
658
+ Verification for all nine items: full test gate green, CVE-replay corpus
659
+ 214/214 with zero drift, metamorphic/adversarial mutation gate 6/6, self-scan
660
+ precision gate re-baselined against three fully root-caused (not blindly
661
+ accepted) drifts, and the pre-push gate's per-language taint-recall check
662
+ confirmed no regression. `@vercel/ncc` (dev-only build dependency) was also
663
+ bumped 0.44.1 → 0.45.0 to clear the release gate's dependency-currency check.
664
+
665
+ R3 (route deep-mode findings through the full annotator pipeline) was the last
666
+ item in Theme A. Themes B–E of the same PRD — semantic type/import-aware
667
+ matching, control-flow-blind-parser fixes for Java/C#/Kotlin/PHP, deeper
668
+ interprocedural completeness, and DOM/loop-element flow modeling — remain open
669
+ and are tracked in `docs/DETECTION_GAP_REMEDIATION_PRD.md`.
670
+
12
671
  ## 0.136.9 — the real bug: the single-file bundle was never actually self-contained
13
672
 
14
673
  0.136.8's diagnostic logging answered the question immediately: