@clear-capabilities/agentic-security-scanner 0.143.0 → 0.145.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/CHANGELOG.md +555 -0
  2. package/bin/agentic-security.js +770 -49
  3. package/dist/1.index.js +223 -0
  4. package/dist/113.index.js +117 -18
  5. package/dist/144.index.js +163 -0
  6. package/dist/178.index.js +24 -6
  7. package/dist/238.index.js +3 -2
  8. package/dist/265.index.js +191 -0
  9. package/dist/271.index.js +165 -0
  10. package/dist/384.index.js +1 -1
  11. package/dist/435.index.js +187 -52
  12. package/dist/444.index.js +11 -2
  13. package/dist/449.index.js +76 -12
  14. package/dist/526.index.js +117 -18
  15. package/dist/552.index.js +97 -0
  16. package/dist/637.index.js +27 -5
  17. package/dist/730.index.js +311 -0
  18. package/dist/736.index.js +301 -0
  19. package/dist/824.index.js +7 -0
  20. package/dist/905.index.js +88 -22
  21. package/dist/920.index.js +491 -0
  22. package/dist/970.index.js +173 -0
  23. package/dist/agentic-security.mjs +13 -13
  24. package/dist/agentic-security.mjs.sha256 +1 -1
  25. package/dist/calibration-seed.json +2 -0
  26. package/package.json +23 -9
  27. package/src/compare.js +6 -1
  28. package/src/dataflow/CLAUDE.md +1 -1
  29. package/src/dataflow/index.js +18 -0
  30. package/src/dataflow/privacy-catalog.js +290 -0
  31. package/src/dataflow/privacy-deep-walker.js +515 -0
  32. package/src/dataflow/privacy-governance.js +126 -0
  33. package/src/dataflow/privacy-inventory.js +154 -0
  34. package/src/dataflow/privacy-sink-policy.js +125 -0
  35. package/src/dataflow/privacy-taint.js +115 -54
  36. package/src/dataflow/privacy-taxonomy.js +233 -0
  37. package/src/discovery/disprove.js +7 -3
  38. package/src/discovery/hunter.js +9 -5
  39. package/src/discovery/index.js +2 -2
  40. package/src/discovery/llm-invoke.js +69 -13
  41. package/src/egress/audit.js +147 -0
  42. package/src/egress/policy.js +313 -0
  43. package/src/egress/redact.js +180 -0
  44. package/src/engine.js +1048 -302
  45. package/src/fix/apply-fix-service.js +404 -0
  46. package/src/fix/approver-registry.js +157 -0
  47. package/src/history-scan.js +22 -5
  48. package/src/ir/CLAUDE.md +1 -1
  49. package/src/llm-validator/index.js +86 -9
  50. package/src/llm-validator/model-status.js +66 -0
  51. package/src/lsp/server.js +49 -2
  52. package/src/mcp/tools.js +177 -50
  53. package/src/pipeline/analyzer-supervisor.js +93 -0
  54. package/src/pipeline/analyzer-worker.js +26 -0
  55. package/src/pipeline/annotator-runner.js +33 -0
  56. package/src/pipeline/assurance-mode.js +154 -0
  57. package/src/pipeline/cascade-worker-pool.js +172 -0
  58. package/src/pipeline/cascade-worker.js +43 -0
  59. package/src/pipeline/coverage-ledger.js +0 -0
  60. package/src/pipeline/detector-runner.js +51 -0
  61. package/src/pipeline/enrichment-completion.js +58 -0
  62. package/src/pipeline/evidence-provenance.js +91 -0
  63. package/src/pipeline/finding-schema.js +108 -0
  64. package/src/pipeline/legacy-compat.js +101 -0
  65. package/src/pipeline/producer-collector.js +48 -0
  66. package/src/pipeline/producer-registry.js +112 -0
  67. package/src/pipeline/scan-health.js +144 -0
  68. package/src/posture/CLAUDE.md +123 -0
  69. package/src/posture/accuracy-scorecard.js +156 -1
  70. package/src/posture/adversary-agent.js +15 -3
  71. package/src/posture/artifact-registry.js +241 -0
  72. package/src/posture/auditor-walkthrough.js +186 -21
  73. package/src/posture/calibration-feedback.js +201 -0
  74. package/src/posture/calibration-seed.json +2 -0
  75. package/src/posture/calibration.js +25 -0
  76. package/src/posture/compliance-evidence-signing.js +131 -0
  77. package/src/posture/compliance-policy.js +324 -17
  78. package/src/posture/cross-repo-memory.js +7 -2
  79. package/src/posture/custom-rules.js +36 -0
  80. package/src/posture/deterministic.js +8 -1
  81. package/src/posture/encryption-provider.js +205 -0
  82. package/src/posture/evidence-grade-wording.js +71 -0
  83. package/src/posture/fix-history.js +137 -20
  84. package/src/posture/fix-honesty-gate.js +47 -6
  85. package/src/posture/fix-verify.js +65 -8
  86. package/src/posture/fleet.js +0 -0
  87. package/src/posture/flow-narration.js +7 -2
  88. package/src/posture/git-history.js +13 -5
  89. package/src/posture/legal-hold.js +140 -0
  90. package/src/posture/llm-redteam.js +10 -1
  91. package/src/posture/material-change.js +111 -2
  92. package/src/posture/mttr.js +75 -12
  93. package/src/posture/policy-bundle.js +274 -0
  94. package/src/posture/pre-incident-archaeology.js +39 -7
  95. package/src/posture/privacy-framework.js +47 -6
  96. package/src/posture/production-feedback.js +179 -0
  97. package/src/posture/provenance/ai-authorship.js +68 -0
  98. package/src/posture/provenance/branch-entry.js +80 -0
  99. package/src/posture/provenance/cache.js +143 -0
  100. package/src/posture/provenance/confidence.js +36 -0
  101. package/src/posture/provenance/coordinator.js +786 -0
  102. package/src/posture/provenance/dag-walk.js +249 -0
  103. package/src/posture/provenance/evidence-attribution.js +59 -0
  104. package/src/posture/provenance/git-evidence.js +310 -0
  105. package/src/posture/provenance/lifecycle.js +208 -0
  106. package/src/posture/provenance/missing-control-resolver.js +137 -0
  107. package/src/posture/provenance/origin-resolver.js +342 -0
  108. package/src/posture/provenance/predicate-replay.js +133 -0
  109. package/src/posture/provenance/providers/config.js +39 -0
  110. package/src/posture/provenance/providers/github.js +62 -0
  111. package/src/posture/provenance/providers/gitlab.js +58 -0
  112. package/src/posture/provenance/repo-lineage.js +74 -0
  113. package/src/posture/provenance/sca-origin.js +139 -0
  114. package/src/posture/provenance/schema.js +255 -0
  115. package/src/posture/provenance/transitive-sca.js +147 -0
  116. package/src/posture/provenance/validate.js +30 -0
  117. package/src/posture/provenance-evidence-bundle.js +144 -0
  118. package/src/posture/retention-policy.js +132 -0
  119. package/src/posture/risk-dollars.js +216 -26
  120. package/src/posture/sbom-diff.js +15 -2
  121. package/src/posture/scan-checkpoint.js +176 -31
  122. package/src/posture/secret-history.js +10 -2
  123. package/src/posture/state-dir.js +64 -5
  124. package/src/posture/state-lifecycle-report.js +77 -0
  125. package/src/posture/suppressions.js +59 -3
  126. package/src/posture/vuln-archaeology.js +8 -2
  127. package/src/pr-delta.js +25 -4
  128. package/src/privacy/ir-adapter.js +380 -0
  129. package/src/report/index.js +248 -5
  130. package/src/report/oscal.js +7 -2
  131. package/src/runScan.js +34 -5
  132. package/src/sast/cpp.js +3 -14
  133. package/src/sast/rate-limit.js +33 -3
  134. package/src/sca/llm-function-extract.js +6 -0
  135. package/src/util/git-hardening.js +128 -0
package/CHANGELOG.md CHANGED
@@ -11,6 +11,561 @@
11
11
 
12
12
 
13
13
 
14
+ ## 0.145.0 — Finding Provenance ships, and two audits find what the first one missed
15
+
16
+ Every finding now carries a `findingProvenance` record answering "when did this
17
+ enter the codebase, and how sure are we?" — resolved from Git history, not
18
+ guessed. It is **opt-in**: pass `--provenance` (or any provenance flag, e.g.
19
+ `--provenance-since`, `--require-provenance`) on a scan of a Git repository.
20
+
21
+ It is opt-in rather than on-by-default because the release gate measured what
22
+ on-by-default costs: time-to-first-finding over a 207-file tree went 4.5s to
23
+ 45s. That is a 7.6x regression on the one metric this product's own benchmark
24
+ calls the binding constraint for its ICP — "how long until the FIRST useful
25
+ result, not aggregate F1" — and resolving commit history for every finding is
26
+ not what a first-time user is waiting on. CI, compliance and triage callers
27
+ that want provenance ask for it explicitly and pay the cost knowingly.
28
+
29
+ **What it resolves.** For a SAST finding: the commit that introduced it, found
30
+ by replaying the finding's own predicate against historical blobs (`git blame`
31
+ answers "who last touched this line", which is a different and usually wrong
32
+ question) and then confirming the predicate was FALSE in that commit's parent.
33
+ For a direct dependency: the commit that moved the declared version in
34
+ `package.json`/`requirements.txt` into an advisory's vulnerable range. Alongside
35
+ the origin commit: the branch/PR the change entered through, the evidence nodes
36
+ (source/sink/manifest, each as a path:line:commit triple), a confidence level
37
+ with its reasons, and a lifecycle ledger of introduce/remediate/reintroduce
38
+ events at `.agentic-security/provenance/lifecycle.json`.
39
+
40
+ **Only a complete scan can close a finding.** The ledger's remediation pass turns
41
+ a finding's *absence* into the claim "this was fixed," which is sound only when
42
+ the scan actually looked everywhere it could have found it. A `--changed-since` /
43
+ `--pr` scan, or any caller-supplied file list (the MCP `scan_diff` tool, the LSP's
44
+ on-save scan), therefore records new and reintroduced findings normally but closes
45
+ nothing — the entries it did not look at stay open until a full scan says
46
+ otherwise. For the same reason the ledger is written only when the scan target is
47
+ a real directory: a scan of a path that does not exist resolves its state
48
+ directory by walking up from the current working directory, and would otherwise
49
+ write a verdict about a project it never read.
50
+
51
+ **It refuses to guess.** Every record carries a terminal `status`, and there is
52
+ no path that leaves the field absent: `complete`, `partial` (history could not
53
+ confirm a parent boundary — a shallow clone, or an advisory with no `introduced`
54
+ bound), `uncommitted` (the finding exists only in the working tree),
55
+ `not_available`, `budget_exhausted`, or `error`. A shallow clone can never reach
56
+ `complete`. Author emails are redacted from every output format unless
57
+ `--include-author-email` is passed.
58
+
59
+ **New flags** (`agentic-security scan --help` documents all seven):
60
+ `--provenance <standard|deep>`, `--no-provenance`, `--provenance-since <ref>`,
61
+ `--provenance-timeout <ms>`, `--include-author-email`, `--pseudonymize-authors`,
62
+ `--require-provenance`. `deep` mode performs real non-linear DAG analysis (see
63
+ the M3 subsection below) — it is no longer a stub that silently runs `standard`.
64
+ `--require-provenance` reports unresolved provenance as a scan-health condition
65
+ and downgrades `scanHealth.status` to `partial`; it never changes the exit code
66
+ by itself — see the M2 subsection below for the mechanism that does.
67
+ `--verbose --firehose` prints the provenance block per finding in text output.
68
+ See [`docs/guides/finding-provenance.md`](docs/guides/finding-provenance.md)
69
+ for the full user-facing writeup.
70
+
71
+ ### M2: format parity, compliance/MTTR/fix-lifecycle surfacing, and `--assurance strict` can now fail the build
72
+
73
+ M0+M1 landed provenance resolution and the JSON/text surfaces above. M2 threads
74
+ that data through every other output and, for the first time, gives provenance
75
+ completeness the power to fail a CI build.
76
+
77
+ - **Format parity.** SARIF, CSV, Markdown, and HTML output now all carry each
78
+ finding's `findingProvenance`, matching what JSON already had — a consumer no
79
+ longer has to switch formats to see it.
80
+ - **Compliance evidence.** Auditor-walkthrough and NIST Privacy Framework gap
81
+ findings now carry `controlRefs` (the finding ids backing a control's gap
82
+ determination) and `derivedProvenance` (the earliest proven open condition
83
+ among those findings, with a `confidence` level and stated `limitations`).
84
+ - **MTTR.** `mttr.js` reports `ageBasis` (`finding_origin` | `earliest_observable`
85
+ | `uncommitted` | `first_observed`) and `provenAgeDays` alongside the existing
86
+ wall-clock `ageDays`, so age-to-remediate can be read against the commit that
87
+ actually introduced the finding, not just when the scanner first saw it.
88
+ - **Fix records.** Every fix-history entry now carries a `provenanceAtFix`
89
+ snapshot — the finding's `findingProvenance` as it stood at the moment the fix
90
+ was applied — so a later audit can see what was known at fix time, not just
91
+ what is knowable now.
92
+ - **`--assurance strict` can now fail the build over incomplete provenance.**
93
+ This is the behavior change that matters most, and it is a genuinely new
94
+ failure mode, not a rewording of `--require-provenance` above: strict mode
95
+ (`--assurance strict`) now treats any finding whose `findingProvenance.status`
96
+ is outside `[complete, uncommitted]` as making the scan incomplete, and fails
97
+ the same way it already fails on a failed or skipped analyzer. `--require-provenance`
98
+ is unchanged by this — it still only flags and never fails the exit code.
99
+ `--assurance strict` is the new, separate mechanism that does fail it. See the
100
+ `KNOWN INTERACTION` comment in `scanner/src/pipeline/assurance-mode.js` for a
101
+ known, disclosed consequence: transitive `vulnerable_dep`, `unpinned_dep`, and
102
+ `no_lockfile` supply-chain findings are stamped `not_available` today (the
103
+ latter two are a category error — there is no commit that introduced a
104
+ *missing* lockfile — not merely a deferral), so `--assurance strict` will fail
105
+ on nearly any real project with a dependency manifest until that gap closes.
106
+
107
+ ### M3: real non-linear history, transitive-dependency provenance, missing-control regressions, PR/CODEOWNERS enrichment
108
+
109
+ - **`--provenance deep` now does real work.** Instead of the M0–M2 stub that
110
+ accepted the flag and silently ran `standard`, deep mode walks every parent
111
+ of a merge commit (not just the first), which resolves origins standard
112
+ mode's first-parent-only walk cannot see, and detects reverts and
113
+ cherry-picks — surfaced as `findingOrigin.revertOf` / `.cherryPickOf` — via
114
+ a real unified-diff inversion.
115
+ - **Transitive-dependency provenance is now live-wired**, not merely
116
+ modeled: `transitive-sca.js` re-derives lockfile ancestry per historical
117
+ commit to find the commit that moved a *lockfile-resolved* (not
118
+ manifest-declared) dependency into an advisory's vulnerable range.
119
+ - **Missing-control regressions** — a previously-observed safeguard (today:
120
+ `sast/rate-limit.js`'s findings) disappearing — can now resolve a real
121
+ origin via `missing-control-resolver.js`.
122
+ - **Optional GitHub/GitLab PR-metadata + CODEOWNERS enrichment**
123
+ (`findingProvenance.providerEnrichment`) is live for `complete`-status
124
+ findings, configured via `.agentic-security/provenance-providers.yml` /
125
+ `AGENTIC_SECURITY_GITHUB_TOKEN` / `AGENTIC_SECURITY_GITLAB_TOKEN`, capped
126
+ per scan.
127
+
128
+ ### M4: signed provenance evidence bundles, cross-repository lineage, an AI-authorship hook
129
+
130
+ - **`agentic-security attest --provenance`** signs a per-finding provenance
131
+ record (origin commit, confidence, evidence attribution) with the same
132
+ Ed25519 key material as the existing finding-evidence bundle mechanism;
133
+ `verify-attestation` auto-detects and verifies it against a public key
134
+ alone. Note the flag-shape collision with `scan`: on `attest`, `--provenance`
135
+ takes an optional *finding id*, not a mode — `attest --provenance deep`
136
+ looks for a finding literally named `deep`.
137
+ - **Cross-repository lineage.** An operator-declared
138
+ `.agentic-security/repo-lineage.json` can link a root-commit origin (a
139
+ finding whose earliest commit has no parent in the current repo) across a
140
+ prior, local-clone-only fork/split history. Resolution is conservative:
141
+ content at the linked line must actually match, not merely exist, before
142
+ an origin is reported, and the record discloses the boundary crossing
143
+ explicitly.
144
+ - **An extensible AI-authorship verifier registry**
145
+ (`registerAIAuthorshipVerifier` / `resolveAIAuthorship`) now stamps every
146
+ SAST `findingOrigin` with an `aiAuthorship` field; no verifier is
147
+ registered today, so it defaults honestly to `{status:'unknown', verifier:null}`.
148
+ - **Fleet-wide rollups** (`fleet.js`) now surface provenance-proven
149
+ remediation debt. Fleet MTTR is honestly disclosed rather than fabricated:
150
+ real remediation-timing data isn't reachable from the production fleet
151
+ driver without new state, so `rollupFleet` distinguishes "never tracked"
152
+ from "tracked, zero remediations" instead of reporting a misleading number.
153
+
154
+ ### PRD completion: injection hardening, evidence-digest binding, retention split, and the first real coverage/accuracy measurements
155
+
156
+ - Author names and commit summaries are now sanitized against terminal
157
+ control-character and Markdown/HTML injection everywhere they reach a
158
+ human (FR-PROV-026).
159
+ - The run-attestation digest and provenance cache key are now genuinely
160
+ bound to the PRD-named inputs they claim to cover, including
161
+ detector/ruleset version.
162
+ - Symlink-escape protection added to the git evidence layer.
163
+ - **`--pseudonymize-authors`** (new flag, listed above) replaces raw
164
+ commit-author names with a stable `Contributor-XXXXXXXX` id — for when you
165
+ need to compare "who introduced what" without a raw name in the output.
166
+ Honored everywhere `--include-author-email`'s redaction already was,
167
+ including PR-reviewer logins and CODEOWNERS entries from provider
168
+ enrichment.
169
+ - The provenance cache now lives in its own top-level, independently-retained
170
+ state directory (`.agentic-security/provenance-cache/`), split from the
171
+ permanent lifecycle ledger (`.agentic-security/provenance/lifecycle.json`),
172
+ so cache eviction can never touch permanent history.
173
+ - **Two PRD success metrics are now genuinely measured and published, not
174
+ just designed:** known-origin accuracy (12/13 = 92.3% on the labeled
175
+ corpus, against a ≥98% target) and provenance coverage (311/341 = 91.2% on
176
+ this repository's own tree, against a ≥95% target — all 30 shortfall
177
+ findings resolve to `partial`, not `error`/`not_available`, so the gap is
178
+ reduced confidence rather than pipeline failure).
179
+ - `scan.secrets` and blameable `scan.logicVulns` entries now go through real
180
+ origin resolution (real stableIds, real git history) instead of a
181
+ permanent `not_available` placeholder.
182
+
183
+ ### Second-audit remediation: a hostile-repository RCE closed, and further honesty fixes
184
+
185
+ An independent second audit of the completed Finding Provenance PRD found
186
+ one security-critical gap and several places where a metric or a claim
187
+ wasn't as real as it read. All fixed this release:
188
+
189
+ - **Security fix (1 of 2).** Git subprocess calls in the provenance pipeline
190
+ are now hardened against a hostile repository's own `.git/config` (e.g. a
191
+ malicious `core.fsmonitor`), `.gitattributes` `textconv` drivers, and
192
+ external `diff` drivers — a repository could previously trigger arbitrary
193
+ code execution merely by being scanned, provenance on or off. The sweep
194
+ covers every git invocation in the scanner, not just the provenance
195
+ module, and a source-level guard now fails the build if a future `git
196
+ diff` call site omits `--no-ext-diff`. Removing the shell from two
197
+ remaining `execSync` call sites also closed a command-injection path via
198
+ attacker-controlled filenames.
199
+ - **Security fix (2 of 2), found while reviewing a performance optimization
200
+ in this same release.** A change that fused the parent-commit lookup into
201
+ an existing `git show` placed that field after the author name in a
202
+ delimiter-separated record. Git preserves a literal `0x1f` inside an
203
+ author name, so an outside contributor choosing their own author name
204
+ could shift the parse and select the bytes read as the parent commit.
205
+ Because a parent whose blobs cannot be fetched is indistinguishable from
206
+ one that genuinely lacks the finding, this could manufacture a
207
+ `status: 'complete'` origin with **HIGH confidence** for a boundary that
208
+ was never verified — a fabricated certainty claim that would then flow
209
+ into signed evidence bundles. The field now sits behind the commit hash
210
+ only, with hex validation as defense in depth. Both directions are pinned
211
+ by a regression test.
212
+ - The provenance cache key and evidence digest are now genuinely bound to
213
+ the running detector/ruleset version (previously always `null` in
214
+ practice), so upgrading the scanner correctly invalidates stale cached
215
+ provenance.
216
+ - `ageBasis` and `provenAgeDays` are now rendered wherever a finding's age is
217
+ shown, not only written to `last-scan.json`.
218
+ - Provenance coverage is now wired into the real, running scorecard-generation
219
+ path (see "PRD completion" above for the number) instead of always
220
+ reporting "unmeasured."
221
+ - **Performance measurement is now honest end-to-end**, and the honest
222
+ numbers are a real miss against target: real p95 (n=20) over both cold and
223
+ warm cache arms, and a genuine two-sided memory comparison (not a one-arm
224
+ heap delta). Measured this release — cold-cache time ~27x wall-clock p95
225
+ against a ≤1.3x (≤30% overhead) target, warm-cache ~2x; cold-cache memory
226
+ ~13x against a ≤1.2x target. `bench:provenance-accuracy:check` is now
227
+ wired into the pre-push gate so the known-origin-accuracy number can no
228
+ longer silently rot.
229
+ - The PRD's **required compliance-evidence disclaimer** ("Provenance
230
+ establishes repository history for technical evidence. It does not prove
231
+ developer intent, control operation outside code, organizational
232
+ compliance, or certification.") now appears next to every
233
+ provenance-derived origin the auditor walkthrough renders
234
+ (`compliance --walkthrough`), and user-facing documentation for the whole
235
+ feature now exists at
236
+ [`docs/guides/finding-provenance.md`](docs/guides/finding-provenance.md).
237
+ - **Not fixed, disclosed honestly:** a finding in a file renamed after
238
+ introduction still degrades to `status: 'partial'` rather than resolving
239
+ its true pre-rename origin commit. Known and traced, not fixed this round.
240
+
241
+ ### Breaking: SCA finding ids change for manifest-declared dependencies
242
+
243
+ Direct dependencies declared in `package.json` / `requirements.txt` now carry the
244
+ manifest **line number** where they are declared, and `report/index.js`'s
245
+ `fingerprint()` folds that line into the finding id. This was necessary for
246
+ provenance — an SCA finding had no line, so every `vulnerable_dep` from one
247
+ manifest previously hashed to the SAME id and could not be told apart — but it
248
+ means those ids are **not stable across this upgrade**.
249
+
250
+ Concretely: any triage verdict, baseline entry, or suppression keyed on the OLD
251
+ id of a `package.json`- or `requirements.txt`-declared dependency finding will no
252
+ longer match and is effectively orphaned. Affected state:
253
+ `.agentic-security/baseline.json`, triage memory, and `disable:`/suppression
254
+ entries naming an SCA finding id. Transitive dependencies (resolved from a
255
+ lockfile, not declared in a manifest) are unaffected, as are all SAST, secrets,
256
+ and business-logic findings.
257
+
258
+ **There is no automatic migration**, and that is deliberate rather than an
259
+ oversight — id aliasing would have to be carried indefinitely to be safe, and
260
+ the orphaned entries fail open (a finding reappears) rather than closed (a real
261
+ finding stays hidden). Re-triage or re-baseline the affected SCA findings after
262
+ upgrading; `agentic-security scan --set-baseline` regenerates the baseline in one
263
+ step.
264
+
265
+ ## 0.144.0 — Assurance hardening closes Epic E2, and an independent audit finds what "verified" missed
266
+
267
+ The assurance-hardening PRD (`docs/implementation/assurance-hardening-*`)
268
+ closes 72 of its 73 requirements this release. Epic E2 ("analyzer supervision
269
+ and scan health" — the claim that a scan can say, honestly, whether it
270
+ actually completed) is now fully closed, 8 of 8. The one requirement left
271
+ open (FR-401's "types" element — real type inference over the Layer-1 IR) is
272
+ a deliberate, documented scope decision, not an oversight: see its own
273
+ section below.
274
+
275
+ ### An independent 16-agent audit found 4 real gaps in requirements this project had already marked "verified"
276
+
277
+ Before continuing forward implementation, the session was asked to verify the
278
+ PRD was truly complete. Sixteen independent agents, briefed only with each
279
+ requirement's literal PRD text (not this project's own tracking files), each
280
+ re-checked one previously-"verified" requirement against the real code. 12 of
281
+ 16 held up. 4 did not — all fixed and re-verified in the same pass:
282
+
283
+ - **A broken scan could still say "Safe to deploy."** `scanHealth.status`
284
+ (added by an earlier requirement) was computed correctly but never read by
285
+ the actual human-facing verdict — a scan that hit an annotator exception or
286
+ timed out on files, with zero findings, still printed a clean bill of
287
+ health. `toShipVerdict` now checks `scanHealth.status` before it says
288
+ anything is safe.
289
+ - **"Approvals, exceptions, and suppressions" only had identity verification
290
+ wired for one of the three nouns.** `posture/suppressions.js` never checked
291
+ a suppression's `justification_signed_by` against the approver registry a
292
+ sibling requirement had already built — a suppression could be filed under
293
+ anyone's name with no verification at all. Fixed by threading the same
294
+ registry through.
295
+ - **A "privacy-preserving" feedback module persisted the exact thing it
296
+ promised not to.** A caller-supplied finding id — which, for a real
297
+ finding, embeds its file path — was stored verbatim instead of hashed.
298
+ Fixed by substituting the finding's stable id or a genuine SHA-256 hash.
299
+ - **Two detectors were silently exempt from the fault-isolation work a prior
300
+ requirement claimed was complete for "every detector."** `scanWeb3Advanced`
301
+ and `scanK8sAdmission` were called directly instead of through the
302
+ isolating wrapper, so an exception thrown by either would silently discard
303
+ every OTHER detector's findings for that file. The original verification
304
+ had grepped for a call-site count that could not distinguish a wrapped call
305
+ from an unwrapped one sharing the same substring.
306
+
307
+ Three of these four were the same failure class repeating: a mechanism proven
308
+ correct in a unit test, never checked against the real, human-facing (or
309
+ caller-facing) entry point it was supposed to protect. A fifth requirement
310
+ (the privacy IR adapter, below) was found to be a genuine partial — 2 of its
311
+ 8 named elements were real, the rest were stubs or silently merged — and was
312
+ honestly downgraded rather than left mismarked.
313
+
314
+ ### Coverage ledgers: every analyzer now has one real, computed status per file
315
+
316
+ `pipeline/coverage-ledger.js` is a new, drift-guarded registry of all 121
317
+ real per-file analyzer call sites (extracted from the engine's own cascade
318
+ code, not hand-maintained), each tagged always-applicable,
319
+ extension-gated, or policy-gated. Combined with the fault-isolation and
320
+ per-file-timeout work from earlier requirements, this computes exactly one
321
+ terminal status — completed, failed, timed out, or skipped by policy — for
322
+ every (file, analyzer) pair a scan actually ran. `scanHealth.analyzers`,
323
+ previously a hardcoded `null`, is now this real summary; a detector that
324
+ throws on even one file now demotes scan status, a gap that had no signal
325
+ before.
326
+
327
+ ### `--assurance advisory|standard|strict`
328
+
329
+ A new `agentic-security ci` flag, running alongside the existing `--fail-on`
330
+ and `--policy` gates rather than replacing either. `strict` fails the build
331
+ outright when `scanHealth.status` is not `complete` — a failed, timed-out, or
332
+ silently policy-skipped analyzer; an annotator exception; a stale
333
+ vulnerability feed (see below); or a CI environment that silently downgraded
334
+ deep analysis (see below) all now have one build-failing consequence, in
335
+ addition to whatever `--fail-on`'s severity threshold already does.
336
+ `advisory` and `standard` report the same signal but never gate — the PRD's
337
+ own acceptance criterion only specifies strict mode's behavior in full.
338
+
339
+ ### CI can no longer silently downgrade deep analysis without saying so
340
+
341
+ Requesting `--deep` in a CI environment without the explicit
342
+ `AGENTIC_SECURITY_DEEP_IN_CI=1` override already fell back to pattern-only
343
+ analysis; what was missing was that decision showing up anywhere a human or
344
+ a build gate would see it. It's now a real `scanHealth` condition, visible in
345
+ the human headline (never "Safe to deploy" under a silent downgrade), in
346
+ `ci`'s own stderr, and failable under `--assurance strict`.
347
+
348
+ ### Stale vulnerability feeds, calibration data, and rulesets are now visible — and can fail strict policy
349
+
350
+ Four independent freshness signals, each reusing an already-computed
351
+ staleness check rather than inventing a new one, now feed `scanHealth`:
352
+
353
+ - The CISA KEV catalog's existing staleness tracking is wired through for
354
+ the first time.
355
+ - The engine's own EPSS enrichment path (a separate, duplicate
356
+ implementation from `posture/epss.js`'s — a pre-existing duplication, not
357
+ introduced here) gained the same kind of per-entry age tracking KEV
358
+ already had.
359
+ - The seed calibration table (`calibration-seed.json`) now carries a real,
360
+ git-derived generation timestamp and is checked against a 180-day
361
+ threshold.
362
+ - Custom rule packs (`.agentic-security/rules/*.yml`) can now opt into a
363
+ `review-interval-days` / `reviewed-at` freshness check — deliberately not
364
+ based on file mtime, since a CI checkout resets mtimes on every run,
365
+ which would make an mtime-based check silently blind in CI, the one place
366
+ a strict-mode failure matters most.
367
+
368
+ Found while wiring this: this development machine's own, real, previously
369
+ invisible EPSS disk cache had a genuine stale entry — a live demonstration
370
+ of exactly the gap this requirement closes, not a synthetic test case.
371
+
372
+ ### Checkpointed scans now invalidate only what actually changed
373
+
374
+ Interrupted-scan resume (`AGENTIC_SECURITY_RESUME=1`) previously discarded an
375
+ entire checkpoint — every already-completed file's work — if a single
376
+ scanned file changed, was added, or was removed. Run identity is now split
377
+ into a GLOBAL component (engine version, ruleset version, running bundle,
378
+ dependency manifests, environment switches — a change to any of these still
379
+ discards everything, because those affect how every file would be analyzed)
380
+ and a PER-FILE content hash on each checkpoint record. Changing one file now
381
+ re-analyzes only that file; every other already-checkpointed file still
382
+ resumes. A global discard now names what changed
383
+ (`"engine version changed (0.143.0 -> 0.144.0)"`); a per-file invalidation
384
+ names why (`"content changed since it was checkpointed"`).
385
+
386
+ ### The privacy IR adapter now supplies 7 of its 8 named elements — "types" is a deliberate exception, not a gap
387
+
388
+ An earlier requirement claimed the privacy taint engine's IR adapter
389
+ supplied "declarations, types, assignments, calls, parameters, returns,
390
+ storage, and sinks." An independent check found only 2–3 of the 8 were
391
+ real: types was a hardcoded `null`, assignments and parameters were merged
392
+ into one undifferentiated bucket, returns and storage were absent
393
+ entirely. That requirement was honestly downgraded rather than left
394
+ mismarked. This release closes parameters, assignments (now distinctly
395
+ tagged), returns, and storage — the last of these by reindexing a
396
+ stored-taint registry the engine already computes for an unrelated
397
+ correlation, not by adding new detection logic.
398
+
399
+ "Types" remains unimplemented, on purpose. A narrow, TypeScript-only signal
400
+ is technically possible — type-annotation AST nodes survive into this
401
+ codebase's own Babel visitor before TypeScript's own preset strips them —
402
+ but supplying it means modifying the shared JS/TS parser every SAST, taint,
403
+ and privacy consumer depends on, for a benefit that would only ever cover
404
+ one of roughly ten supported languages. That trade was judged not worth the
405
+ risk to already-correct, heavily-relied-on infrastructure, and is recorded
406
+ as a deliberate scope decision rather than attempted as an unproven partial
407
+ fix.
408
+
409
+ ### A real CI-only test failure, caught by hosted CI rather than the local gate
410
+
411
+ Two `privacy-ir-adapter.test.js` tests requested deep analysis via the
412
+ `{deep: true}` runScan option and asserted `scanHealth.deepAnalysis.enabled
413
+ === true`. That held locally but failed on the hosted CI runner: the engine
414
+ downgrades deep mode under `CI=true` unless a second opt-in
415
+ (`AGENTIC_SECURITY_DEEP_IN_CI`) is also set, and this codebase's own
416
+ `test:ci-parity` static checker — built specifically to catch this class of
417
+ bug after a near-identical 2026-08-19 incident — incorrectly treats the
418
+ `{deep:true}` OPTION shape as exempt from needing that opt-in ("does not go
419
+ through the env gate," per its own comment). That reasoning is wrong: the
420
+ option and the environment variable are two different ways to set the same
421
+ internal `_deepRequested` flag, and both go through the identical
422
+ `_inCi`/`_deepInCiAllowed` gate afterward. Fixed by passing `deepInCi: true`
423
+ explicitly on both tests. The checker's incorrect exemption for the
424
+ `{deep:true}` shape is a known, separately-tracked gap — roughly forty other
425
+ test files use that same option, and this incident does not establish which
426
+ of them are actually exposed to it; auditing that is future work, not done
427
+ inside this release.
428
+
429
+ ### npm publish no longer pays for the release gate twice
430
+
431
+ `.github/workflows/release.yml`'s publish job ran the full uncached release
432
+ gate as an explicit step, then called `npm publish` — which triggers
433
+ scanner/package.json's `prepublishOnly`, itself `build && sync-changelog &&
434
+ release-check.mjs`. Read from v0.143.0's actual workflow log: the explicit
435
+ step took **280s**, and the one inside `npm publish` took **318s**, ten
436
+ seconds later, on the same commit, same checkout, same runner. Caching could
437
+ not have closed that gap by design — this workflow always passes `--no-cache`
438
+ (a cross-machine cache would reintroduce the reproducibility claim
439
+ `posture/attestation.js` explicitly declines to make), so the first run never
440
+ wrote anything for the second to reuse. That is ~600s of a release entirely
441
+ spent proving the same thing twice.
442
+
443
+ `npm publish` now runs with `--ignore-scripts`, so `prepublishOnly` does not
444
+ fire on the publish step. This is safe here specifically because it would not
445
+ be safe in general: the three explicit steps immediately above it (build,
446
+ changelog sync, gate) already did everything `prepublishOnly` would, so
447
+ nothing it produces is missing from disk by the time `npm publish` runs. The
448
+ local publish path is untouched — `scanner/package.json`'s `prepublishOnly`
449
+ stays fully wired, because a local `npm publish` has no preceding gate step to
450
+ make it redundant (root `CLAUDE.md`'s "Two publish paths" section still
451
+ applies there unchanged). `test/release-workflow.test.js` pins the flag, pins
452
+ that the three steps it stands in for still run first, and adds a tripwire: if
453
+ `scanner/package.json` ever gains a `prepack`/`postpack` script,
454
+ `--ignore-scripts` would silently skip it too, and that test fails until its
455
+ effect is reproduced as an explicit step. Both new checks confirmed to fail
456
+ when the fix is reverted, and the file is restored byte-identical after.
457
+
458
+ ### `npm test`: one `node --test` invocation instead of eleven
459
+
460
+ `npm test` chained eleven separate `npm run test:<scope>` processes in series
461
+ — `node --test` already runs a multi-file invocation's files CONCURRENTLY
462
+ against the same cores, so eleven separate processes were paying eleven
463
+ startup costs while getting zero overlap ACROSS scopes: `test:posture` could
464
+ not start until `test:sast` had entirely finished. Repeated standalone timings
465
+ on this machine put the old chain at roughly 258s and a single combined
466
+ invocation over the same 397 files at 111-154s, run five times with an
467
+ identical 3955/3955/0 (later 3964/3964/0, once this section's own tests were
468
+ added) result every time — this machine has been under sustained load all
469
+ session (`uptime` reports a load average over 5 on 8 cores while this was
470
+ written), so the absolute local number is noisy; the authoritative figure is
471
+ whatever the next hosted-CI run reports, on a dedicated runner. The mechanism
472
+ is not in question: it is the same tests, run once instead of eleven times.
473
+
474
+ `scripts/run-unit-tests.mjs` derives its file list from the existing
475
+ `test:<scope>` scripts rather than hand-maintaining a second list — a
476
+ duplicated list is exactly the shape that silently drifts (add a file to
477
+ `test:sast`, forget to add it here, and the combined run quietly covers less
478
+ than `npm run test:sast` alone does). It refuses to run if a `test:*` script
479
+ using `node --test` is not accounted for in its `SCOPES` list, both as a hard
480
+ failure from the script itself and as a named test
481
+ (`test/run-unit-tests.test.js`) so the drift guard is visible in the suite,
482
+ not just as a side effect nobody reads. `test/cpp-dataflow.test.js` and
483
+ `test:python` are deliberately NOT folded in: cpp-dataflow sets its feature
484
+ flag at module load rather than inside a test, and when included in the
485
+ combined invocation its 26 tests silently contributed **zero** results to the
486
+ totals — not a failure, not a skip, just absent — for a reason not chased to
487
+ ground; python is a different runtime entirely. Both still run, as their own
488
+ separate steps, exactly as before.
489
+
490
+ `test/discovery-wiring.test.js`'s "a scope is wired into the full gate" check
491
+ previously grepped the `test` script's text for the literal substring
492
+ `test:discovery` — true when `npm test` was a literal chain, meaningless once
493
+ it calls a derivation script instead (the substring never appears in the new
494
+ `test` script whether or not discovery's files are covered). Rewritten to
495
+ assert discovery's files are actually present in the union the runner
496
+ computes, which is a stronger claim than the substring match ever was: it
497
+ would have caught the runner's own extraction regex silently missing this
498
+ scope's files, which the substring match could not have.
499
+
500
+ ### The JetBrains plugin builds again — and its support floor moved
501
+
502
+ `jetbrains-plugin` had been red in CI, classified INFORMATIONAL, and treated as
503
+ a known toolchain gap. It was not a toolchain gap. LSP4IJ dropped IntelliJ 233
504
+ support at its 0.18.0 release, so the pinned `com.redhat.devtools.lsp4ij:0.19.4`
505
+ could never resolve against the pinned IDE 2023.3.6:
506
+
507
+ ```
508
+ Plugin 'com.redhat.devtools.lsp4ij:0.19.4' is not compatible to: IC-233.15026.9
509
+ ```
510
+
511
+ There is no configuration that supports IntelliJ 2023.3 *and* a maintained
512
+ LSP4IJ. **`sinceBuild` moves 233 → 242**: IntelliJ 2023.3 and 2024.1 are no
513
+ longer supported by this plugin. That is LSP4IJ's floor, not a preference. The
514
+ build also moves off `org.jetbrains.intellij` 1.17.4 — the superseded major,
515
+ which Gradle 9 cannot apply at all — onto the IntelliJ Platform Gradle Plugin
516
+ 2.18.1, which removes the reason CI had to pin Gradle 8.10.
517
+
518
+ `untilBuild` is now open rather than `251.*`. The old cap had already gone stale
519
+ (2025.2 exists), and a stale cap reaches the user as "plugin incompatible" on an
520
+ IDE that would have worked.
521
+
522
+ **Two defects the passing build was hiding.** Once it compiled, `buildPlugin`
523
+ exited 0 and produced a zip — and `verifyPluginProjectConfiguration` reported,
524
+ in text nothing was reading, that (1) the plugin was compiled for Java 17
525
+ against a platform requiring Java 21, and (2) the Kotlin stdlib was being
526
+ double-bundled, putting a 1.7 MB `kotlin-stdlib-2.1.0.jar` in the distribution
527
+ next to 5 KB of plugin code and leaving the platform's class loader free to
528
+ resolve stdlib classes from either. Both are fixed; the distribution zip went
529
+ from 1.59 MB to 4 KB. The CI job now **fails on that verifier's output**, since a
530
+ warning nothing fails on is a warning nobody reads, and it unpacks the zip to
531
+ confirm the `factoryClass` named in `plugin.xml` is actually in the jar — an
532
+ empty zip passed the old check.
533
+
534
+ **A defect introduced by this fix, caught before it shipped.** The new CI step
535
+ pipes Gradle into `tee` so a later step can grep the verifier's output. A `run:`
536
+ step's default shell is `bash -e {0}` — no `pipefail` — so the pipeline's exit
537
+ status is *tee's*, and a failed build would have reported success. That is the
538
+ same "green gate that verifies nothing" this entire change is about, reintroduced
539
+ by the change itself. Both pipe-to-tee steps in the workflow now set
540
+ `set -o pipefail`, including the pre-existing one in `determinism-attest`: that
541
+ job is BLOCKING, and without pipefail it could upload an empty attestation —
542
+ including on `attest-fixture.mjs`'s own zero-findings refusal — for
543
+ `determinism-compare` to compare against.
544
+
545
+ The guard added for it was itself broken twice, in opposite directions, and only
546
+ running the negative control in both found them: it first matched the word
547
+ `pipefail` inside the comment explaining the fix (so deleting the real line still
548
+ passed), then matched `| tee` inside a comment (so a correct workflow failed). A
549
+ guard fooled by its own documentation is worse than no guard, because it reads as
550
+ coverage.
551
+
552
+ **A committed Gradle wrapper.** `ide/jetbrains/README.md` told contributors to
553
+ run `./gradlew` for a long time while no wrapper existed; CI pinned a Gradle
554
+ version in the workflow instead, so the two could drift. The wrapper is now
555
+ committed and pins the Gradle distribution by SHA-256 — verified to reject a
556
+ tampered checksum, which `gradle wrapper` does not configure by default and
557
+ which is not optional in this repository.
558
+
559
+ **The classification stays INFORMATIONAL, and why.** Making a job that downloads
560
+ a full IntelliJ distribution into a release blocker trades one failure mode for a
561
+ worse one. The lesson taken instead: everything checkable without the network
562
+ moves into the blocking offline gate. `test/ide-surfaces.test.js` now asserts
563
+ that the JDK CI provisions equals the `jvmToolchain` the build asks for, that the
564
+ committed wrapper exists and pins its distribution by checksum, that the README's
565
+ stated support floor is the `sinceBuild` the artifact declares, and that an
566
+ LSP4IJ ≥ 0.18 is never paired with a `sinceBuild` below 242. Each was confirmed
567
+ to fail when its subject is broken.
568
+
14
569
  ## 0.143.0 — OSCAL output, and the finding an OSCAL document must refuse to make
15
570
 
16
571
  `--format oscal` was documented in `commands/compliance.md` long before anything