@clear-capabilities/agentic-security-scanner 0.133.0 → 0.136.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (111) hide show
  1. package/CHANGELOG.md +299 -0
  2. package/bin/agentic-security-audit.js +2 -1
  3. package/bin/agentic-security-consistency.js +2 -1
  4. package/bin/agentic-security.js +348 -40
  5. package/dist/113.index.js +6 -6
  6. package/dist/117.index.js +3 -1
  7. package/dist/178.index.js +1 -1
  8. package/dist/220.index.js +5 -3
  9. package/dist/238.index.js +4 -4
  10. package/dist/317.index.js +270 -0
  11. package/dist/384.index.js +1 -1
  12. package/dist/435.index.js +13 -7
  13. package/dist/499.index.js +86 -0
  14. package/dist/513.index.js +7 -3
  15. package/dist/526.index.js +6 -6
  16. package/dist/609.index.js +741 -0
  17. package/dist/637.index.js +1 -1
  18. package/dist/675.index.js +7 -5
  19. package/dist/839.index.js +4 -3
  20. package/dist/905.index.js +1173 -0
  21. package/dist/agentic-security.mjs +56 -56
  22. package/dist/agentic-security.mjs.sha256 +1 -1
  23. package/dist/compliance-frameworks/ccpa.json +32 -0
  24. package/dist/compliance-frameworks/eu-ai-act.json +51 -0
  25. package/dist/compliance-frameworks/gdpr.json +45 -0
  26. package/dist/compliance-frameworks/hipaa-security-rule.json +56 -0
  27. package/dist/compliance-frameworks/nist-ai-600-1.json +51 -0
  28. package/dist/compliance-frameworks/nist-csf-2.json +73 -0
  29. package/dist/compliance-frameworks/nist-privacy-1-1.json +846 -0
  30. package/dist/compliance-frameworks/owasp-asvs-5.json +79 -0
  31. package/dist/compliance-frameworks/owasp-llm-top-10.json +69 -0
  32. package/package.json +14 -6
  33. package/src/badge.js +2 -1
  34. package/src/dataflow/cross-service-taint.js +2 -1
  35. package/src/dataflow/ifds-precise.js +6 -4
  36. package/src/dataflow/incremental.js +7 -5
  37. package/src/discovery/CLAUDE.md +48 -0
  38. package/src/discovery/confirm.js +47 -0
  39. package/src/discovery/disprove.js +79 -0
  40. package/src/discovery/hunter.js +116 -0
  41. package/src/discovery/index.js +331 -0
  42. package/src/discovery/judge.js +97 -0
  43. package/src/discovery/lenses.js +69 -0
  44. package/src/discovery/llm-invoke.js +120 -0
  45. package/src/discovery/memory.js +163 -0
  46. package/src/discovery/partition.js +92 -0
  47. package/src/engine.js +163 -7
  48. package/src/integrations/tickets.js +7 -6
  49. package/src/ir/ir-stats.js +1 -1
  50. package/src/ir/parser-cpp.js +1 -1
  51. package/src/leaderboard.js +3 -2
  52. package/src/llm-validator/consistency.js +6 -2
  53. package/src/llm-validator/index.js +30 -41
  54. package/src/llm-validator/providers.js +227 -0
  55. package/src/mcp/audit.js +2 -1
  56. package/src/mcp/tools.js +4 -3
  57. package/src/posture/CLAUDE.md +102 -1
  58. package/src/posture/accuracy-scorecard.js +59 -1
  59. package/src/posture/agents-memory.js +5 -3
  60. package/src/posture/auditor-walkthrough.js +9 -7
  61. package/src/posture/auth-posture-import.js +5 -4
  62. package/src/posture/autopilot.js +225 -0
  63. package/src/posture/comparison.js +181 -0
  64. package/src/posture/compliance-frameworks/nist-privacy-1-1.json +846 -0
  65. package/src/posture/compliance-policy.js +7 -9
  66. package/src/posture/custom-rules.js +7 -5
  67. package/src/posture/cve-alert-daemon.js +6 -5
  68. package/src/posture/dep-add-guard.js +2 -1
  69. package/src/posture/deterministic.js +3 -2
  70. package/src/posture/evidence-bundle.js +246 -0
  71. package/src/posture/execution-proof.js +25 -1
  72. package/src/posture/exploitability-probability.js +2 -1
  73. package/src/posture/feature-flags.js +3 -2
  74. package/src/posture/findings-memory.js +3 -3
  75. package/src/posture/fix-history.js +5 -2
  76. package/src/posture/fix-metrics.js +5 -5
  77. package/src/posture/fix-plan.js +2 -1
  78. package/src/posture/fleet.js +0 -0
  79. package/src/posture/grader-calibration.js +3 -4
  80. package/src/posture/intent-context.js +2 -1
  81. package/src/posture/learning.js +4 -3
  82. package/src/posture/license-attributions.js +5 -7
  83. package/src/posture/license-graph.js +2 -1
  84. package/src/posture/license-policy.js +2 -1
  85. package/src/posture/logic-claims.js +266 -0
  86. package/src/posture/model-rescan.js +4 -3
  87. package/src/posture/network-policy-import.js +3 -2
  88. package/src/posture/poc-inprocess.js +404 -2
  89. package/src/posture/pqc-migration-plan.js +7 -5
  90. package/src/posture/pr-augment.js +8 -5
  91. package/src/posture/privacy-framework.js +262 -0
  92. package/src/posture/proof-artifact.js +101 -0
  93. package/src/posture/prove-findings.js +28 -4
  94. package/src/posture/risk-dollars.js +2 -2
  95. package/src/posture/router.js +5 -4
  96. package/src/posture/ruleset-version.js +2 -2
  97. package/src/posture/runtime-correlation.js +2 -1
  98. package/src/posture/sbom-diff.js +12 -3
  99. package/src/posture/sca-policy.js +7 -4
  100. package/src/posture/scan-checkpoint.js +15 -0
  101. package/src/posture/state-dir.js +34 -0
  102. package/src/posture/telemetry-ingest.js +4 -3
  103. package/src/posture/threat-model-auto.js +4 -1
  104. package/src/posture/threat-model-grounding.js +11 -1
  105. package/src/posture/time-to-fix.js +3 -2
  106. package/src/posture/triage-memory.js +3 -2
  107. package/src/posture/waf-ingest.js +6 -5
  108. package/src/posture/watch-mode.js +4 -3
  109. package/src/report/index.js +9 -0
  110. package/src/sast/code-injection-multilang.js +29 -0
  111. package/src/sca/dep-confusion.js +2 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,304 @@
1
1
  # Changelog
2
2
 
3
+ > **A note on `docs/*_PRD.md` references below.** Product requirement documents in
4
+ > this project are removed once their workstreams land — the durable parts move
5
+ > into the code and tests that enforce them, where they cannot drift out of date.
6
+ > Entries below cite PRD files that were live at the time of that release and no
7
+ > longer exist. They are left as written: a changelog records what was true when
8
+ > it was written, and rewriting past entries to hide a since-deleted file would
9
+ > make the history less accurate, not more.
10
+
11
+
12
+ ## 0.136.2 — authenticate the gate on the path that actually runs it
13
+
14
+ 0.136.1 removed the self-deadlock and the release workflow got further: every
15
+ gate check passed, then `npm publish` failed with *"the forge CLI is not
16
+ authenticated"*.
17
+
18
+ The gate runs **twice** in that job. Once as an explicit `Release gate` step,
19
+ which sets `GH_TOKEN` and passed. Then again inside `npm publish`, which
20
+ triggers `prepublishOnly` — and that step set only `NODE_AUTH_TOKEN`. The second
21
+ run had no token, could not read hosted CI, and refused.
22
+
23
+ The gate was right to refuse: unverifiable is not green, and the remedy it
24
+ suggests — `--allow-unverified-ci` — would have published without proving the
25
+ commit was green at all. So the fix is to authenticate it, not to relax it.
26
+ `checks: read` is now declared explicitly too; the gate passed without it on the
27
+ default token, but depending on an undeclared default is how a tightened default
28
+ becomes a mystery failure a year later.
29
+
30
+ The same shape as the deadlock it follows: a control that works on the path
31
+ that was tested, and fails on the path that actually ships.
32
+
33
+ ## 0.136.1 — the release gate stops deadlocking on itself
34
+
35
+ `npm publish` had been impossible since 0.135.0, and the cause was not the one I
36
+ reported. Not a missing npm token: **the release gate was waiting for itself.**
37
+
38
+ The release workflow's job is named `publish`. The gate runs inside that job,
39
+ queries hosted CI for HEAD, sees a check run named `publish` that is
40
+ `in_progress` — itself — and requires it to finish before allowing the release.
41
+ It never can. Every tag push failed this way with all nine real checks green, and
42
+ the resulting `publish: failure` then blocked local publishes too, which is the
43
+ error that finally surfaced it.
44
+
45
+ `.github/required-checks.json` gains a third category, **self**, beside blocking
46
+ and informational. A self check is EXCLUDED, not trusted: it cannot report a
47
+ conclusion until the gate it contains has already passed, so requiring it is a
48
+ deadlock and believing it would be believing a check that has not run. The file
49
+ already insisted every check be classified deliberately — this is the category
50
+ that was missing, and its absence meant `publish` fell through to the safe
51
+ default of blocking, which was exactly wrong here.
52
+
53
+ Proven in three directions, because excluding a check must not weaken a gate:
54
+ a pending self check no longer blocks, a FAILED self check no longer blocks, and
55
+ a genuinely red blocking check still does. Verified against live CI state:
56
+ `PASS Hosted CI is green for HEAD`.
57
+
58
+ v0.135.0 and v0.136.0 were tagged but never reached npm for this reason. The
59
+ tags stay where they are — a public tag is not moved — so this ships as 0.136.1.
60
+
61
+ ## 0.136.0 — NIST Privacy Framework 1.1, and the frameworks that never shipped
62
+
63
+ ### A privacy compliance scan that says what it did not check
64
+
65
+ All 104 PF 1.1 controls, assessed on every scan, artifacts at
66
+ `.agentic-security/privacy-framework.{json,md}`. Each gap is emitted as an
67
+ ordinary finding (`family: privacy-compliance`, `CWE-359`) carrying an actionable
68
+ remediation, so `/fix` handles it like anything else.
69
+
70
+ The design turns on one column in NIST's own workbook. PF 1.1 rates each control
71
+ for code-testability — **23 yes, 33 partial, 48 no** — and that rating says a
72
+ control *could* be assessed from source, not that this engine assesses it.
73
+ Collapsing the two is how a privacy report marks "the organizational mission is
74
+ communicated" as PASSED because no rule fired against it, and someone hands that
75
+ to an auditor. So every control lands in exactly one stated bucket:
76
+
77
+ | Bucket | Meaning |
78
+ |---|---|
79
+ | gap | mapped to an engine signal, and that signal is failing — the only bucket that emits a finding |
80
+ | not assessed | NIST rates it code-testable, this engine has no signal — named, never a pass |
81
+ | manual | NIST rates it not code-testable — governance, outside any scanner's reach |
82
+ | satisfied | mapped, and the signal is clean |
83
+
84
+ Measured on a live fixture: 9 gaps, 20 satisfied, 27 not assessed, 48 manual.
85
+ The satisfied rate is reported over the **29 assessed** controls, never over 104.
86
+
87
+ A **vacuous-satisfaction guard** was added after the module's own test caught it:
88
+ a `family:`-mapped control clears when no findings of that family are open, which
89
+ is equally true of a scan that read zero files. Pointing the tool at an empty
90
+ directory was reporting privacy controls as satisfied on the strength of having
91
+ looked at nothing. Now every mapped control degrades to *not assessed* and the
92
+ summary says so.
93
+
94
+ Findings are opt-in (`AGENTIC_SECURITY_PRIVACY_FRAMEWORK=1`). The assessment
95
+ always runs and persists; appending findings by default would change every
96
+ severity count and gate verdict downstream, and a compliance opinion should not
97
+ silently become someone's build failure.
98
+
99
+ ### A real `compliance` subcommand
100
+
101
+ ```
102
+ agentic-security compliance [--gap] [--list] [--walkthrough <id>]
103
+ [--format cli|json|md] [--fail-on gap]
104
+ ```
105
+
106
+ `/compliance --privacy` was documented as a mode with no binary behind it. It
107
+ reads `last-scan.json` rather than re-scanning — a compliance answer is a
108
+ statement about a scan that happened. With no scan to read it exits **2** rather
109
+ than assessing an empty project. Exit codes: 0 report produced, 1 only with
110
+ `--fail-on gap` and a failing control, 2 nothing to assess.
111
+
112
+ ### Every bundled framework had been invisible from the published artifact
113
+
114
+ Running the new subcommand from the shipped bundle printed nothing and exited 0.
115
+ `auditor-walkthrough` resolves its data directory from `import.meta.url` — inside
116
+ the bundle that is `dist/`, and `dist/compliance-frameworks/` never existed
117
+ because the build only emitted the `.mjs`. `listFrameworks` catches the readdir
118
+ failure and returns `[]`.
119
+
120
+ So GDPR, ASVS, NIST AI 600-1 — all nine — worked perfectly from source and were
121
+ **silently absent from the npm package**. Nothing caught it because every test
122
+ ran against `src/`. The build now copies the data next to the bundle, the data is
123
+ tracked in git for the same reason the bundle is, and a test drives the BUNDLE so
124
+ it cannot regress.
125
+
126
+ Twice in this release, testing the shipped artifact rather than the source found
127
+ something the whole suite was blind to.
128
+
129
+ ### Also
130
+
131
+ Removed all remaining PRD documents, repairing 18 files that referenced them.
132
+ `bench/proof-corpus`'s section-level citations (parse-coverage rule, acceptance
133
+ criterion 2, criterion 4, the disclosure boundary) are now stated inline and
134
+ owned by the files that depend on them — the rationale outlived the document.
135
+
136
+ ## 0.135.0 — a scan stops modifying what it scans, and the benchmark stops flattering
137
+
138
+ Two findings, one of which reverses something this project previously reported.
139
+
140
+ ### `--no-state`: a scan is an observation again
141
+
142
+ Pointing the engine at a directory used to write **10 files** into it. For a user
143
+ that means CI asserting a clean tree fails after a scan, and scanning a
144
+ dependency or a customer's code leaves artifacts in a tree they own. Worse, our
145
+ own output contains CWE identifiers, so a second scan could read the first
146
+ scan's conclusions as source.
147
+
148
+ `AGENTIC_SECURITY_NO_STATE=1` (and `--no-state`) now adds **zero paths** while
149
+ reporting byte-identical findings. Both halves are asserted, and the test was
150
+ proven to FAIL with the switch off — the switch must change what is written,
151
+ never what is found. The engine also skips `.agentic-security/` when walking, so
152
+ our output can never become our input.
153
+
154
+ Three defects were found in the guard meant to enforce this, each worth more
155
+ than the line that fixed it:
156
+
157
+ - **`git status` is not sufficient evidence.** Git does not track empty
158
+ directories, so an earlier revision reported a CLEAN tree while still creating
159
+ `sbom-history/` and `fix-history/`. Directory creation is mutation: it fails on
160
+ a read-only mount and is litter in someone else's repo. The acceptance test
161
+ compares full path listings.
162
+ - **The guard was blind to `bin/`** — where the three largest artifacts
163
+ (`findings.json`, `last-scan.json`, `.sig`) are written. A seam guard that
164
+ cannot see the CLI entry point misses the primary writer.
165
+ - **The detector counted documentation as a violation**, then over-corrected: a
166
+ glob inside `// .agentic-security/rules/*.yml` opened a block comment that
167
+ consumed 12,198 characters and hid a real violation. Comment-strip order is
168
+ now load-bearing and asserted in both directions.
169
+
170
+ Stated plainly: 55 modules still build state paths by hand and remain on a
171
+ migration ledger. What changed permanently is that a 56th cannot be added.
172
+
173
+ ### The independent recall figure was wrong, and the correction is downward
174
+
175
+ Benchmark trees had been contaminated by the engine's own state files (220
176
+ polluted trees, 544 carrying `CWE-` strings). Fixing that coincided with a second
177
+ change — restricting matches to the files the advisory's fix commit touched — and
178
+ recall fell from a previously reported 33.6% to 12.7%.
179
+
180
+ Attributing that fall to the wrong cause would have been the same reasoning error
181
+ as the contamination, pointed the other way, so the runner now scores both ways
182
+ in one pass:
183
+
184
+ | | advisory-local (**the claim**) | wide (diagnostic) |
185
+ |---|---|---|
186
+ | recall | **12.7%** (14/110) | 33.6% (37/110) |
187
+ | precision | **50.0%** (14/28) | 50.0% (37/74) |
188
+ | F1 | **0.203** | 0.402 |
189
+
190
+ The wide figure is identical to the pre-purge number. **The contamination was
191
+ real and had to be fixed, but it was not inflating the measurement** — every one
192
+ of the 20.9 points comes from the benchmark becoming honest about *where* a
193
+ finding has to be. 12.7% is the true recall, and it always was.
194
+
195
+ It is published as a low number rather than quietly requalified, because the
196
+ point of owning the instrument is to be able to trust it when it disagrees.
197
+
198
+
199
+ ## 0.134.0 — the loop closes, and the logic tier learns how to be wrong
200
+
201
+ The remaining PRD epics. Two of them are new capability; the other two are the
202
+ same idea applied twice — a claim nobody can disagree with is the weakest thing
203
+ this engine emits, so both new tiers ship with the machinery to refute
204
+ themselves.
205
+
206
+ ### Two more proof classes, and neither needed a running application
207
+
208
+ `sql-injection` and `path-traversal` join the three existing classes. The PRD
209
+ assumed both would wait on a running-app harness; they did not, because the
210
+ harness was never where the proof lived.
211
+
212
+ - **SQL injection is settled at the driver boundary.** Either the payload
213
+ arrives inside the query TEXT or it arrives as a bound parameter — the first
214
+ is the vulnerability by definition and the second is the fix by definition,
215
+ and no schema, rows or live server are needed to tell them apart. The PoC
216
+ stubs the driver with a recorder and writes the marker only when the payload
217
+ shows up inside something recognisably SQL. A parameterised query reaches
218
+ `proof-failed` by **execution**, not by reading the source.
219
+ - **Path traversal is settled by what comes back.** A sentinel is planted
220
+ outside the served directory and the marker is written only if its content —
221
+ or, for `sendFile`, its resolved path — comes back out of the handler. A
222
+ `basename` guard is refuted by running it.
223
+
224
+ Both directions are pinned by tests that execute in the real sandbox. Classes
225
+ still absent (IDOR, SSRF, XSS) are now documented as decisions with reasons
226
+ rather than gaps: a PoC built on invented application state proves something
227
+ about the invention.
228
+
229
+ Fixed along the way: the webhook PoC wrote its marker after a top-level `await`
230
+ guarded by an **unref'd** timer, so a handler that never replied let Node exit
231
+ with the promise pending and the marker check never ran. The test asserting
232
+ "no decision writes no marker" had been passing without reaching the line it
233
+ was testing. Timer is now ref'd and cleared, with a positive control asserting
234
+ the process reaches exit 0.
235
+
236
+ ### The autonomous loop, wired to real stages
237
+
238
+ `scripts/autopilot.mjs` connects the loop to a real scan, a real sandboxed
239
+ exploit, a deterministic-then-model fix, and the real gate. End-to-end tests run
240
+ the whole thing against a live HTTP endpoint, including the one that matters: a
241
+ patch that changes the file, reads like a fix, and would satisfy any "did the
242
+ scanner go quiet?" check is **refused and never written**, because the exploit
243
+ still fires against it.
244
+
245
+ The CLI refuses to start without a confinement backend (the verdict requires
246
+ executing something) and refuses a dirty git tree by default (the test leg
247
+ writes the candidate patch to disk and restores it in a `finally`; a clean tree
248
+ is what makes a crash recoverable). A verified fix reached with no test runner
249
+ detected is counted and reported separately — the exploit stopped firing, but
250
+ nothing checked the application still works.
251
+
252
+ ### The business-logic tier learns how to be wrong
253
+
254
+ The deterministic half already existed. The reviewing agent's half was prose:
255
+ it asserted that a handler lets one user act on another's resource, and nothing
256
+ in the finding gave a second party anything to disagree with. It was the only
257
+ tier in this engine with no way to be wrong.
258
+
259
+ `posture/logic-claims.js` adds three offline lenses that can refute one —
260
+ citation (the file exists and the line is inside it), quotation (the quoted
261
+ snippet is at the cited line), corroboration (a "no authentication" claim
262
+ against a handler that plainly authenticates). Verdicts go through the existing
263
+ producer/verifier separation, so a lens can never vote on a claim it produced;
264
+ that is why the lenses are deterministic code and not another prompt. Refuted
265
+ claims are quarantined, never deleted and never severity-touched.
266
+
267
+ ### A comparison harness that ships no opinion about who the competition is
268
+
269
+ `posture/comparison.js` + `scripts/comparison.mjs` score this engine
270
+ head-to-head against participants **the operator supplies**. The repository
271
+ ships the harness and the answer key and names no tool — a test enforces that.
272
+
273
+ Two properties are the entire module. Every rate is computed over the
274
+ **intersection** of corpus entries *all* participants completed, because a tool
275
+ that crashed on the forty hardest entries and was scored over the remaining
276
+ hundred and seventy looks like it beat one that completed everything, and the
277
+ difference is invisible in the output. And an entry a participant could not run
278
+ is **unscored**, never counted as a miss: counting a crash as a false negative
279
+ penalises a tool for a harness problem, counting it as a pass rewards it for
280
+ one. Matching is CWE-only so nobody is scored on this engine's vocabulary.
281
+
282
+ No comparison figures are published in this repository. Running other vendors'
283
+ tools and publishing the numbers is the operator's call, not the harness's.
284
+
285
+ ### The suppression pragma never worked
286
+
287
+ Found while suppressing a false positive in this release's own new code.
288
+ `// agentic-security-ignore: <rule-id>` is documented in `CLAUDE.md` and
289
+ `pr-comment.js` tells every reviewer to use it — and **nothing implemented it**.
290
+ It has been advertised and inert. A dead suppression mechanism is worse than an
291
+ absent one: the developer writes the pragma, sees the finding again, and
292
+ concludes the scanner is noisy rather than that the pragma is dead.
293
+
294
+ Now implemented in `engine.js`, applied after dedupe and after every cross-file
295
+ pass so it covers a finding whichever analysis produced it. Line-scoped, matched
296
+ against the finding's id / vuln / CWE / family, and **logged** to the same
297
+ ledger custom rules use so `--include-suppressed` can show it. Every test
298
+ carries a positive control — the same file without the pragma must still
299
+ produce the finding, or "0 findings" would prove the suppression works and
300
+ equally prove the detector stopped firing.
301
+
3
302
  ## 0.133.0 — two ways findings could be silently deleted, both closed
4
303
 
5
304
  Four rounds of adversarial premortem against this repository's own artifacts.
@@ -23,6 +23,7 @@ import * as fs from 'node:fs';
23
23
  import * as path from 'node:path';
24
24
  import { verifyAuditLog } from '../src/mcp/audit.js';
25
25
 
26
+ import { statePath } from '../src/posture/state-dir.js';
26
27
  function args() {
27
28
  const a = process.argv.slice(2);
28
29
  const sub = a[0] || 'review';
@@ -48,7 +49,7 @@ function _parseDuration(s) {
48
49
  return n * u;
49
50
  }
50
51
 
51
- function _logPath(root) { return path.join(root, '.agentic-security', 'mcp-audit.log'); }
52
+ function _logPath(root) { return statePath(root, 'mcp-audit.log'); }
52
53
 
53
54
  function _readEntries(root) {
54
55
  const fp = _logPath(root);
@@ -26,6 +26,7 @@ import * as fs from 'node:fs';
26
26
  import * as path from 'node:path';
27
27
  import { measureConsistency, summarize } from '../src/llm-validator/consistency.js';
28
28
 
29
+ import { statePath } from '../src/posture/state-dir.js';
29
30
  function args() {
30
31
  const a = process.argv.slice(2);
31
32
  const out = { trials: 5, top: 5, json: false, root: process.cwd() };
@@ -40,7 +41,7 @@ function args() {
40
41
 
41
42
  async function main() {
42
43
  const opts = args();
43
- const scanFile = path.join(opts.root, '.agentic-security', 'last-scan.json');
44
+ const scanFile = statePath(opts.root, 'last-scan.json');
44
45
  if (!fs.existsSync(scanFile)) {
45
46
  console.error(`no last-scan.json at ${scanFile} — run a scan first`);
46
47
  process.exit(2);