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