@clear-capabilities/agentic-security-scanner 0.137.1 → 0.139.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.
Files changed (44) hide show
  1. package/CHANGELOG.md +244 -0
  2. package/dist/113.index.js +2 -2
  3. package/dist/178.index.js +1 -1
  4. package/dist/384.index.js +1 -1
  5. package/dist/435.index.js +29 -1
  6. package/dist/526.index.js +2 -2
  7. package/dist/637.index.js +1 -1
  8. package/dist/agentic-security.mjs +14 -14
  9. package/dist/agentic-security.mjs.sha256 +1 -1
  10. package/package.json +20 -7
  11. package/src/dataflow/CLAUDE.md +30 -0
  12. package/src/dataflow/catalog.js +512 -14
  13. package/src/dataflow/engine.js +275 -27
  14. package/src/dataflow/summaries.js +30 -5
  15. package/src/engine.js +512 -120
  16. package/src/ir/CLAUDE.md +20 -5
  17. package/src/ir/balanced-call.js +11 -1
  18. package/src/ir/callgraph.js +34 -0
  19. package/src/ir/parser-cs.js +55 -6
  20. package/src/ir/parser-go.js +106 -2
  21. package/src/ir/parser-java.js +111 -10
  22. package/src/ir/parser-js.js +40 -0
  23. package/src/ir/parser-kt.js +194 -10
  24. package/src/ir/parser-php.js +108 -6
  25. package/src/ir/parser-py.helper.py +199 -10
  26. package/src/ir/parser-rb.js +405 -31
  27. package/src/mcp/tools.js +29 -1
  28. package/src/posture/accuracy-scorecard.js +103 -0
  29. package/src/runScan.js +5 -2
  30. package/src/sast/CLAUDE.md +1 -1
  31. package/src/sast/_auth-signals.js +141 -0
  32. package/src/sast/_comment-strip.js +80 -13
  33. package/src/sast/codegen-sink.js +110 -0
  34. package/src/sast/convention-deviation.js +235 -0
  35. package/src/sast/fastapi-hardening.js +45 -6
  36. package/src/sast/file-upload.js +29 -1
  37. package/src/sast/ownership-authz.js +245 -0
  38. package/src/sast/php.js +12 -2
  39. package/src/sast/rate-limit.js +2 -0
  40. package/src/sast/rbac-consistency.js +1 -1
  41. package/src/sast/redirect-toctou.js +167 -0
  42. package/src/sast/resource-exhaustion.js +217 -0
  43. package/src/sast/sibling-guard.js +176 -0
  44. package/src/sast/zip-slip.js +53 -2
package/CHANGELOG.md CHANGED
@@ -9,6 +9,250 @@
9
9
  > make the history less accurate, not more.
10
10
 
11
11
 
12
+ ## 0.139.1 — The same scanner, published with provenance
13
+
14
+ **The shipped artifact is functionally identical to 0.139.0.** Only two commits
15
+ separate them, and neither is in the published package: a test-harness timeout
16
+ (`test/` does not ship) and a dependency hold (`.dependency-holds.json` does not
17
+ ship). Nothing about how the scanner behaves has changed.
18
+
19
+ This release exists for one reason: **0.139.0 was published without a provenance
20
+ attestation.** It went out from a maintainer's laptop because the CI token
21
+ lacked write access on the scoped package, so every tag-triggered publish failed
22
+ at `PUT` with `E404` — after having already signed provenance to the Sigstore
23
+ transparency log. The token has been replaced and verified (a re-run now returns
24
+ `E403 "cannot publish over the previously published versions"`, which is the
25
+ registry authenticating the write and rejecting only the duplicate version), so
26
+ this is the first release published by CI with `--provenance`.
27
+
28
+ For a security scanner that argues its case on *provable, measurable,
29
+ reproducible*, "you can cryptographically verify this artifact was built from
30
+ this commit in this repository" is a product property rather than a formality.
31
+ Provenance is signed at publish time and cannot be retrofitted, which is why it
32
+ takes a version number rather than an amendment to the last one.
33
+
34
+ Fixes carried along, neither user-visible:
35
+
36
+ - **`test/audit-cli.test.js` budgeted 4 s for a CLI that takes ~2.7 s idle** — a
37
+ 1.5× margin in a suite that runs files concurrently. It failed as `status:
38
+ null` on a *different* test each run, was unreproducible in isolation, and
39
+ blocked a release push on a commit whose entire diff was 8 lines of JSON.
40
+ Now 30 s (~11× idle), still bounded so a genuinely hung CLI fails rather than
41
+ stalls.
42
+ - **`@types/vscode` is held rather than upgraded.** It declares the VS Code API
43
+ surface the extension compiles against and should track `engines.vscode` — the
44
+ *oldest* supported host — not the newest published types. Chasing latest lets
45
+ the extension compile against APIs absent from hosts it claims to support, and
46
+ that failure lands as a runtime `TypeError` in a user's editor. The hold
47
+ records a pre-existing mismatch (`engines ^1.95.0` vs types `^1.125.0`) for a
48
+ deliberate decision at review time, since narrowing the supported host range
49
+ is a product call, not a dependency bump.
50
+
51
+
52
+ ## 0.139.0 — Two detectors that were dead, a confinement rule that was documented but unenforced, and a new Go rule
53
+
54
+ Every defect in this release is the same shape: **a control that exists, is
55
+ documented, and does not hold end-to-end.** Unit tests passed in every case.
56
+ Only checks that exercised the whole pipeline found them.
57
+
58
+ ### Two detectors produced nothing through a real scan
59
+
60
+ Both worked when called directly and returned zero through `runScan` — the
61
+ `rate-limit.js` signature (a rule that discarded 100% of its own findings,
62
+ project-wide, from the day it was written).
63
+
64
+ - **`k8s-admission`** — `_isIaCFile` admitted YAML as Kubernetes only when the
65
+ path contained a directory literally named `k8s/`. Its own comment claimed the
66
+ caller also checked for `kind:`; nothing ever did. **Manifests under
67
+ `deploy/`, `manifests/`, `charts/`, `kubernetes/` or the repository root were
68
+ invisible** — which is where most projects actually put them. Now admitted on
69
+ CONTENT (`apiVersion:` and `kind:` at line-start within the first 2 KB),
70
+ because the set of directory names people use is unbounded and the file
71
+ format is not.
72
+
73
+ The fix had to be applied at **both** admission gates: `runScan` admits a file
74
+ into `fileContents`, and then `runFullScan` independently re-filters that same
75
+ list with `shouldScan()`. With only the first opened, the predicate returned
76
+ true, the walker collected the files, and the scan still returned zero.
77
+
78
+ - **`install-script`** — `package.json` is routed to `depFileContents` and fails
79
+ `shouldScan()`, so the per-file SAST loop never visited it and
80
+ `scanInstallScripts` was wired in but never invoked. Fixed on the manifest
81
+ path rather than by admitting `package.json` into the SAST loop, which would
82
+ push a manifest through all ~117 code detectors to satisfy one rule.
83
+
84
+ Measured blast radius rather than assumed: 16 of 1129 YAML files in this tree
85
+ are newly admitted (1.4%), all in fixtures and caches. Self-scan unchanged.
86
+
87
+ ### `_CONFINEMENT` rule 3 was documented and unimplemented
88
+
89
+ `agents/_CONFINEMENT.md` states three refusal rules for every edit-capable agent
90
+ and for the MCP write tools. The code enforced two. Rule 3 — *"a backup, lock, or
91
+ build-output file (`*.bak`, `*.lock`, `dist/`, `build/`, `target/`)"* — was not
92
+ enforced at all.
93
+
94
+ The tests failed by **successfully writing**, which is what makes this more than
95
+ pedantry: `scanner/dist/` holds the shipped bundle, which carries its own
96
+ SHA-256 integrity sidecar precisely because its contents matter. `apply_fix`
97
+ would have rewritten it and reported success. A contract an agent is instructed
98
+ to follow, enforced less strictly than it claims, is worse than no contract.
99
+
100
+ Build output is matched as a **path segment at any depth** — `packages/web/dist/`
101
+ is the normal shape — while a source file *named* `dist.js` or `build.py` stays
102
+ writable.
103
+
104
+ ### New: `sibling-guard-omission` (CWE-22, Go)
105
+
106
+ From `GHSA-95cv-r8x4-vh75`: two fields of one request struct reach a filesystem
107
+ rename and the project's **own** guard is applied to only one. High precision by
108
+ construction — the rule never decides what a guard *is*, it observes one being
109
+ applied to a sibling, so every finding reduces to "this file guards X and forgets
110
+ Y", falsifiable from a single screen of code, with the guard name and both field
111
+ names carried as evidence.
112
+
113
+ Fires on the real advisory in `pre` and goes silent there in `post`. FP budget
114
+ measured across 72 real Go packages: **1.12%** of findings, max 5 per repo.
115
+
116
+ **It could never have shipped without a second fix.** `dropGuardedFindings` drops
117
+ a CWE-22 finding when its window contains a containment guard — and for this
118
+ family the window *always* contains one, because the guard on the sibling **is**
119
+ the finding. The centralized precision filter was reading the evidence of the bug
120
+ as proof of safety and would have deleted 100% of the family regardless of
121
+ detector quality. Exemption keyed on `family`, deliberately narrow.
122
+
123
+ A false positive in this rule was then found on real code (an `\n` escape read as
124
+ a path separator) and fixed, with a regression test written from that code.
125
+
126
+ ### Instruments
127
+
128
+ - **The shipped LSP is smoke-tested.** `bin/agentic-security-lsp.js` ships inside
129
+ the JetBrains and Neovim plugins and was referenced by no test, script or
130
+ workflow. It now runs as a subprocess speaking real LSP over stdio —
131
+ `initialize` → `didOpen` → `publishDiagnostics` → `shutdown` → `exit` — plus a
132
+ malformed-frame survival case.
133
+ - **Per-language layer attribution** on the independent population, with taint's
134
+ attributable share stated outright each run.
135
+ - **Gate integrity** (P0 of `docs/WORLD_CLASS_HARNESS_PRD.md`): CI-parity checks
136
+ so the local gate cannot disagree with hosted CI; layer-recall gates on
137
+ equality rather than a floor, so an unrecorded *improvement* fails too; a
138
+ shared per-entry bench watchdog; and a detector-liveness guard that requires
139
+ every rule fixture to produce a finding *through* `runScan`. That guard is what
140
+ found both dead detectors above.
141
+
142
+
143
+ ## 0.138.0 — Findings come from code, not comments; container taint; a benchmark that can no longer hang
144
+
145
+ ### Comment blindness, enforced instead of documented
146
+
147
+ `src/sast/CLAUDE.md` has always named this as its first gotcha for detector
148
+ authors — "Comments confuse detectors. Always go through `blankComments()`" —
149
+ but nothing enforced it, so whether a rule ignored comments depended on whether
150
+ its author remembered. A probe over nine languages, in which *every* dangerous
151
+ construct sat inside a comment and the only executable statement was
152
+ `return 1`, produced **15 findings from code that can never run**. Three
153
+ independent defects, each fixed after a failing test proved it:
154
+
155
+ - `runFullScan` handed the **raw** file to every SAST module. 64 of 117 modules
156
+ called `blankComments()` themselves; roughly 40 of the rest ran regexes over
157
+ raw text. The per-file dispatch now computes one comment-blanked,
158
+ offset-preserving view and passes it to 99 detector call sites.
159
+ - The engine's own `stripNoise()` understood `//` and `/* */` but **not `#`**,
160
+ so every engine-internal regex pass leaked on Python/Ruby/shell/HCL comments.
161
+ All 15 call sites now pass a path, and the mapping is language-aware because
162
+ it has to be: `#` opens a comment in Python but is `#include`/`#region` in the
163
+ C family, and `//` is a comment in C-family languages but **floor division in
164
+ Python**, where blanking it would delete real code.
165
+ - `blankComments()` let a `'`/`"` string span a newline. An odd number of quotes
166
+ on one line — which regex literals produce routinely; `/"(?:sh|bash)"\s*,\s*(?!"[^"]*")/`
167
+ has seven — left the scanner stuck in string mode for the **entire rest of the
168
+ file**, silently disabling comment stripping from that point on. Found on this
169
+ repository's own `sast/go-extended.js`, where it resurrected a false positive
170
+ from a comment six lines below such a regex.
171
+
172
+ Ruby `=begin`/`=end` blocks are now stripped too (a `#`-only pass left them
173
+ wholly intact).
174
+
175
+ **Measured, finding-by-finding rather than by totals.** Self-scan on this
176
+ repository fell 458 → 427: **33 findings removed, all 33 on comment lines, zero
177
+ real findings lost**; the 2 added were previously *masked* by the string-mode
178
+ defect. On the 315-entry independent population of real upstream advisories,
179
+ localized precision rose **40.74% → 44.00%** with true positives unchanged at
180
+ 11 — the fix buys precision, not recall, which is exactly what was predicted
181
+ once it was established that none of the credited true positives sat on a
182
+ comment line. Deliberately still reading raw source: the prompt-injection
183
+ family (for an agentic tool, instructions hidden in a comment *are* the attack),
184
+ the secrets scanners (a committed credential is leaked whether or not its line
185
+ executes), and `scanTodosNearSecurity`, whose entire subject is comments.
186
+
187
+ The property is pinned end-to-end by `test/comment-blindness.test.js`, including
188
+ a positive control and a Python-floor-division guard, so a regression surfaces
189
+ as a test failure rather than as quietly inflated findings.
190
+
191
+ ### Container / collection-element taint (PRD T3.3)
192
+
193
+ Probed ten container shapes before writing anything, which showed the JS array
194
+ cases already worked. Coverage went from 5/10 to 9/10:
195
+
196
+ - A computed write with a non-literal key (`bag[k] = tainted`) lowers to access
197
+ path `bag.*`, and `'*'` is a *literal* property name, not a wildcard — the
198
+ lattice propagates only downward from a prefix, so the write was unreachable
199
+ from every correctly-computed read. An unknown key could be any key, so it now
200
+ widens to the container.
201
+ - `Map.set` / `Set.add` joined the mutator list.
202
+ - **The mutator rule required `callee.kind === 'member'`, a shape only Babel
203
+ emits**, so container writes in Python/Ruby/PHP/Go/Java/C#/Kotlin never matched
204
+ at all. The miss was invisible because a PY-SAST pattern rule caught the same
205
+ sink.
206
+ - `__setitem__` is in the mutator list because `parser-py.js` lowers a Python
207
+ subscript *assignment* to a call node, not an assign node — established by
208
+ dumping the IR, not by inference.
209
+
210
+ **Reported as capability, not as a recall gain:** `bench:layer-recall` is
211
+ unchanged at 116/215 and the dataflow-shaped subset unchanged at 115/137,
212
+ because the corpus holds no container-shaped entries. Pinned by
213
+ `test/container-taint.test.js`, including two precision cases that must *not*
214
+ fire.
215
+
216
+ ### Performance
217
+
218
+ The comment work initially cost **19.7%** of end-to-end scan time on a 307-file
219
+ entry, because `stripNoise` is called ~15× per file and an interpreted character
220
+ loop had replaced two native regex passes. A single-slot memo (safe because the
221
+ engine processes one file at a time and `fileContents[p]` returns the same
222
+ string *reference*) plus a rewrite of `blankComments` to emit bulk slices
223
+ instead of per-character appends brought that to **5.7%**. Byte offsets and line
224
+ counts are preserved exactly — every finding's line number depends on it.
225
+
226
+ ### The independent benchmark could hang indefinitely
227
+
228
+ A full run wedged at entry 186 of 315 on `GHSA-hcm8-x79p-wx2w` (apache/camel,
229
+ 649 MB): process alive, state `S`, 0.0% CPU, no progress for six hours, killed
230
+ with 129 entries never scored. Reproduced on a clean checkout, so it was
231
+ pre-existing. `runScan` carries a deep-mode walltime budget and a per-file
232
+ timeout, but nothing bounded a whole-entry scan. There is now a per-entry
233
+ watchdog (`AGENTIC_SECURITY_BENCH_ENTRY_TIMEOUT_MS`, default 600 s) that marks
234
+ the entry UNSCORED — the harness's own documented doctrine for an entry that
235
+ could not be run, never a miss. The run that produced this release's numbers
236
+ named 6 such entries (all Java, all very large) and completed.
237
+
238
+ Worth stating plainly: **every independent-population figure published before
239
+ this release came from a harness that could silently stall partway through.**
240
+
241
+ ### Documentation corrected
242
+
243
+ `docs/METRICS.md`'s taint table was ~5× stale (23/210 = 11%; actual 116/215 =
244
+ 54%) and its central claim that kotlin still had **0%** taint recall was false
245
+ (48%). Both `bench/layer-recall/baseline.json` and the table are regenerated.
246
+ The cause is recorded alongside the fix, because it is a gate-design lesson:
247
+ `bench:layer-recall:check` compares against a **floor**, so an improvement from
248
+ 31 to 116 passed it exactly as a no-change run would.
249
+
250
+ Also fixed two long-standing lifecycle failures: `jsUnits` — Theme 6's JS/TS
251
+ extractor, which carries 6 of the 10 known sibling-omission entries — was wired
252
+ into production with **no test at all**, and now has four; and a stale
253
+ `profile.js::DEFAULTS` dead-code allowlist entry was removed.
254
+
255
+
12
256
  ## 0.137.1 — Dependabot policy for the deliberately-vulnerable fixtures
13
257
 
14
258
  Housekeeping release. Adds `.github/dependabot.yml` so Dependabot leaves the
package/dist/113.index.js CHANGED
@@ -415,8 +415,8 @@ var external_node_child_process_ = __webpack_require__(1421);
415
415
  var external_node_fs_ = __webpack_require__(3024);
416
416
  // EXTERNAL MODULE: external "node:path"
417
417
  var external_node_path_ = __webpack_require__(6760);
418
- // EXTERNAL MODULE: ./src/engine.js + 196 modules
419
- var engine = __webpack_require__(2739);
418
+ // EXTERNAL MODULE: ./src/engine.js + 203 modules
419
+ var engine = __webpack_require__(6622);
420
420
  ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
421
421
  // Deterministic honesty gates on fix / finding output (#7).
422
422
  //
package/dist/178.index.js CHANGED
@@ -13,7 +13,7 @@ export const modules = {
13
13
  /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
14
14
  /* harmony import */ var node_fs__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3024);
15
15
  /* harmony import */ var node_path__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(6760);
16
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(2739);
16
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(6622);
17
17
  // Time-travel + counterfactual scanning (v0.68).
18
18
  //
19
19
  // Two new modes that exploit the pure-input shape of runFullScan:
package/dist/384.index.js CHANGED
@@ -8,7 +8,7 @@ export const modules = {
8
8
  /* harmony export */ __webpack_require__.d(__webpack_exports__, {
9
9
  /* harmony export */ scanCredentials: () => (/* reexport safe */ _engine_js__WEBPACK_IMPORTED_MODULE_0__.Sv)
10
10
  /* harmony export */ });
11
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2739);
11
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(6622);
12
12
  // Secrets submodule view of the engine — credential + entropy + TODO scanning.
13
13
 
14
14
 
package/dist/435.index.js CHANGED
@@ -572,7 +572,29 @@ const RESERVED_WRITE_SUFFIXES = [
572
572
  '.tfvars',
573
573
  'docker-compose.yml',
574
574
  'docker-compose.yaml',
575
+ // _CONFINEMENT rule 3 — backup and lock files. The specific lock BASENAMES
576
+ // above cover the ecosystems we know; this catches the rest (`deps.lock`,
577
+ // `foo.bak`) without needing to enumerate them. Nothing an autofix should
578
+ // ever be rewriting: a `.bak` is someone's safety copy and a `.lock` is
579
+ // generated state.
580
+ '.bak',
581
+ '.lock',
575
582
  ];
583
+ // _CONFINEMENT rule 3 — build output. Matched as a PATH SEGMENT at any depth,
584
+ // not as a top-level prefix, because build output is routinely nested
585
+ // (`packages/web/dist/`, `services/api/target/`) and a top-level-only check
586
+ // would refuse the monorepo root and allow every package inside it.
587
+ //
588
+ // This matters most in THIS repository: `scanner/dist/` holds the shipped
589
+ // bundle, which carries its own SHA-256 integrity sidecar precisely because
590
+ // what it contains matters. Before this, `apply_fix` would rewrite it and
591
+ // report success.
592
+ //
593
+ // NOTE for a future change, deliberately not made here: the PREFIX list above
594
+ // (`node_modules/`, `.git/`, …) is still top-level-only, so a nested
595
+ // `packages/a/node_modules/` is not covered by it. That is a separate widening
596
+ // with its own blast radius and belongs in its own change with its own tests.
597
+ const RESERVED_WRITE_DIR_SEGMENTS = new Set(['dist', 'build', 'target']);
576
598
  function _isReservedWritePath(sessionRoot, absFile) {
577
599
  // Resolve sessionRoot symlinks so the relative path is computed against
578
600
  // the same canonical root as `absFile` (which _confine already realpath'd).
@@ -581,9 +603,15 @@ function _isReservedWritePath(sessionRoot, absFile) {
581
603
  const rootReal = external_node_fs_.realpathSync(external_node_path_.resolve(sessionRoot));
582
604
  const rel = external_node_path_.relative(rootReal, absFile).replace(/\\/g, '/');
583
605
  if (RESERVED_WRITE_PREFIXES.some(p => rel === p.replace(/\/$/, '') || rel.startsWith(p))) return true;
584
- const base = rel.split('/').pop() || '';
606
+ const segments = rel.split('/');
607
+ const base = segments[segments.length - 1] || '';
585
608
  if (RESERVED_WRITE_BASENAMES.has(base)) return true;
586
609
  if (RESERVED_WRITE_SUFFIXES.some(s => base === s || base.endsWith(s))) return true;
610
+ // Any DIRECTORY segment that names build output — checked over
611
+ // `segments.length - 1` so a source file legitimately called `build` or
612
+ // `dist` is not refused for its own name; only living inside such a
613
+ // directory counts.
614
+ if (segments.slice(0, -1).some(seg => RESERVED_WRITE_DIR_SEGMENTS.has(seg))) return true;
587
615
  return false;
588
616
  }
589
617
 
package/dist/526.index.js CHANGED
@@ -234,8 +234,8 @@ var external_node_child_process_ = __webpack_require__(1421);
234
234
  var external_node_fs_ = __webpack_require__(3024);
235
235
  // EXTERNAL MODULE: external "node:path"
236
236
  var external_node_path_ = __webpack_require__(6760);
237
- // EXTERNAL MODULE: ./src/engine.js + 196 modules
238
- var engine = __webpack_require__(2739);
237
+ // EXTERNAL MODULE: ./src/engine.js + 203 modules
238
+ var engine = __webpack_require__(6622);
239
239
  ;// CONCATENATED MODULE: ./src/posture/fix-honesty-gate.js
240
240
  // Deterministic honesty gates on fix / finding output (#7).
241
241
  //
package/dist/637.index.js CHANGED
@@ -11,7 +11,7 @@ export const modules = {
11
11
  /* harmony export */ renderPrDeltaText: () => (/* binding */ renderPrDeltaText)
12
12
  /* harmony export */ });
13
13
  /* harmony import */ var node_child_process__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(1421);
14
- /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2739);
14
+ /* harmony import */ var _engine_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(6622);
15
15
  // Shadowscan / security-DELTA on PR (v0.72).
16
16
  //
17
17
  // Most SAST PR-comment integrations show absolute counts — "12 findings