@clear-capabilities/agentic-security-scanner 0.143.0 → 0.144.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 (95) hide show
  1. package/CHANGELOG.md +304 -0
  2. package/bin/agentic-security.js +477 -47
  3. package/dist/1.index.js +223 -0
  4. package/dist/113.index.js +108 -17
  5. package/dist/144.index.js +163 -0
  6. package/dist/178.index.js +1 -1
  7. package/dist/238.index.js +3 -2
  8. package/dist/265.index.js +191 -0
  9. package/dist/384.index.js +1 -1
  10. package/dist/435.index.js +165 -52
  11. package/dist/526.index.js +108 -17
  12. package/dist/552.index.js +97 -0
  13. package/dist/637.index.js +1 -1
  14. package/dist/730.index.js +311 -0
  15. package/dist/736.index.js +301 -0
  16. package/dist/824.index.js +7 -0
  17. package/dist/905.index.js +88 -22
  18. package/dist/920.index.js +491 -0
  19. package/dist/970.index.js +109 -0
  20. package/dist/agentic-security.mjs +13 -13
  21. package/dist/agentic-security.mjs.sha256 +1 -1
  22. package/dist/calibration-seed.json +2 -0
  23. package/package.json +19 -11
  24. package/src/dataflow/index.js +18 -0
  25. package/src/dataflow/privacy-catalog.js +290 -0
  26. package/src/dataflow/privacy-deep-walker.js +515 -0
  27. package/src/dataflow/privacy-governance.js +126 -0
  28. package/src/dataflow/privacy-inventory.js +154 -0
  29. package/src/dataflow/privacy-sink-policy.js +125 -0
  30. package/src/dataflow/privacy-taint.js +115 -54
  31. package/src/dataflow/privacy-taxonomy.js +233 -0
  32. package/src/discovery/disprove.js +7 -3
  33. package/src/discovery/hunter.js +9 -5
  34. package/src/discovery/index.js +2 -2
  35. package/src/discovery/llm-invoke.js +69 -13
  36. package/src/egress/audit.js +147 -0
  37. package/src/egress/policy.js +313 -0
  38. package/src/egress/redact.js +180 -0
  39. package/src/engine.js +575 -288
  40. package/src/fix/apply-fix-service.js +403 -0
  41. package/src/fix/approver-registry.js +157 -0
  42. package/src/llm-validator/index.js +86 -9
  43. package/src/llm-validator/model-status.js +66 -0
  44. package/src/mcp/tools.js +157 -50
  45. package/src/pipeline/analyzer-supervisor.js +93 -0
  46. package/src/pipeline/analyzer-worker.js +26 -0
  47. package/src/pipeline/annotator-runner.js +33 -0
  48. package/src/pipeline/assurance-mode.js +91 -0
  49. package/src/pipeline/cascade-worker-pool.js +172 -0
  50. package/src/pipeline/cascade-worker.js +43 -0
  51. package/src/pipeline/coverage-ledger.js +0 -0
  52. package/src/pipeline/detector-runner.js +51 -0
  53. package/src/pipeline/enrichment-completion.js +58 -0
  54. package/src/pipeline/evidence-provenance.js +91 -0
  55. package/src/pipeline/finding-schema.js +101 -0
  56. package/src/pipeline/legacy-compat.js +101 -0
  57. package/src/pipeline/producer-collector.js +48 -0
  58. package/src/pipeline/producer-registry.js +112 -0
  59. package/src/pipeline/scan-health.js +144 -0
  60. package/src/posture/CLAUDE.md +2 -0
  61. package/src/posture/accuracy-scorecard.js +96 -1
  62. package/src/posture/adversary-agent.js +15 -3
  63. package/src/posture/artifact-registry.js +217 -0
  64. package/src/posture/auditor-walkthrough.js +70 -8
  65. package/src/posture/calibration-feedback.js +201 -0
  66. package/src/posture/calibration-seed.json +2 -0
  67. package/src/posture/calibration.js +25 -0
  68. package/src/posture/compliance-evidence-signing.js +131 -0
  69. package/src/posture/compliance-policy.js +314 -17
  70. package/src/posture/custom-rules.js +36 -0
  71. package/src/posture/deterministic.js +8 -1
  72. package/src/posture/encryption-provider.js +205 -0
  73. package/src/posture/evidence-grade-wording.js +71 -0
  74. package/src/posture/fix-history.js +113 -19
  75. package/src/posture/fix-honesty-gate.js +47 -6
  76. package/src/posture/fix-verify.js +56 -7
  77. package/src/posture/fleet.js +0 -0
  78. package/src/posture/flow-narration.js +7 -2
  79. package/src/posture/legal-hold.js +140 -0
  80. package/src/posture/llm-redteam.js +10 -1
  81. package/src/posture/material-change.js +90 -0
  82. package/src/posture/policy-bundle.js +274 -0
  83. package/src/posture/privacy-framework.js +33 -6
  84. package/src/posture/production-feedback.js +179 -0
  85. package/src/posture/retention-policy.js +132 -0
  86. package/src/posture/risk-dollars.js +216 -26
  87. package/src/posture/scan-checkpoint.js +176 -31
  88. package/src/posture/state-dir.js +36 -1
  89. package/src/posture/state-lifecycle-report.js +77 -0
  90. package/src/posture/suppressions.js +59 -3
  91. package/src/privacy/ir-adapter.js +380 -0
  92. package/src/report/index.js +51 -2
  93. package/src/report/oscal.js +7 -2
  94. package/src/sast/cpp.js +3 -14
  95. package/src/sca/llm-function-extract.js +6 -0
package/CHANGELOG.md CHANGED
@@ -11,6 +11,310 @@
11
11
 
12
12
 
13
13
 
14
+ ## 0.144.0 — Assurance hardening closes Epic E2, and an independent audit finds what "verified" missed
15
+
16
+ The assurance-hardening PRD (`docs/implementation/assurance-hardening-*`)
17
+ closes 72 of its 73 requirements this release. Epic E2 ("analyzer supervision
18
+ and scan health" — the claim that a scan can say, honestly, whether it
19
+ actually completed) is now fully closed, 8 of 8. The one requirement left
20
+ open (FR-401's "types" element — real type inference over the Layer-1 IR) is
21
+ a deliberate, documented scope decision, not an oversight: see its own
22
+ section below.
23
+
24
+ ### An independent 16-agent audit found 4 real gaps in requirements this project had already marked "verified"
25
+
26
+ Before continuing forward implementation, the session was asked to verify the
27
+ PRD was truly complete. Sixteen independent agents, briefed only with each
28
+ requirement's literal PRD text (not this project's own tracking files), each
29
+ re-checked one previously-"verified" requirement against the real code. 12 of
30
+ 16 held up. 4 did not — all fixed and re-verified in the same pass:
31
+
32
+ - **A broken scan could still say "Safe to deploy."** `scanHealth.status`
33
+ (added by an earlier requirement) was computed correctly but never read by
34
+ the actual human-facing verdict — a scan that hit an annotator exception or
35
+ timed out on files, with zero findings, still printed a clean bill of
36
+ health. `toShipVerdict` now checks `scanHealth.status` before it says
37
+ anything is safe.
38
+ - **"Approvals, exceptions, and suppressions" only had identity verification
39
+ wired for one of the three nouns.** `posture/suppressions.js` never checked
40
+ a suppression's `justification_signed_by` against the approver registry a
41
+ sibling requirement had already built — a suppression could be filed under
42
+ anyone's name with no verification at all. Fixed by threading the same
43
+ registry through.
44
+ - **A "privacy-preserving" feedback module persisted the exact thing it
45
+ promised not to.** A caller-supplied finding id — which, for a real
46
+ finding, embeds its file path — was stored verbatim instead of hashed.
47
+ Fixed by substituting the finding's stable id or a genuine SHA-256 hash.
48
+ - **Two detectors were silently exempt from the fault-isolation work a prior
49
+ requirement claimed was complete for "every detector."** `scanWeb3Advanced`
50
+ and `scanK8sAdmission` were called directly instead of through the
51
+ isolating wrapper, so an exception thrown by either would silently discard
52
+ every OTHER detector's findings for that file. The original verification
53
+ had grepped for a call-site count that could not distinguish a wrapped call
54
+ from an unwrapped one sharing the same substring.
55
+
56
+ Three of these four were the same failure class repeating: a mechanism proven
57
+ correct in a unit test, never checked against the real, human-facing (or
58
+ caller-facing) entry point it was supposed to protect. A fifth requirement
59
+ (the privacy IR adapter, below) was found to be a genuine partial — 2 of its
60
+ 8 named elements were real, the rest were stubs or silently merged — and was
61
+ honestly downgraded rather than left mismarked.
62
+
63
+ ### Coverage ledgers: every analyzer now has one real, computed status per file
64
+
65
+ `pipeline/coverage-ledger.js` is a new, drift-guarded registry of all 121
66
+ real per-file analyzer call sites (extracted from the engine's own cascade
67
+ code, not hand-maintained), each tagged always-applicable,
68
+ extension-gated, or policy-gated. Combined with the fault-isolation and
69
+ per-file-timeout work from earlier requirements, this computes exactly one
70
+ terminal status — completed, failed, timed out, or skipped by policy — for
71
+ every (file, analyzer) pair a scan actually ran. `scanHealth.analyzers`,
72
+ previously a hardcoded `null`, is now this real summary; a detector that
73
+ throws on even one file now demotes scan status, a gap that had no signal
74
+ before.
75
+
76
+ ### `--assurance advisory|standard|strict`
77
+
78
+ A new `agentic-security ci` flag, running alongside the existing `--fail-on`
79
+ and `--policy` gates rather than replacing either. `strict` fails the build
80
+ outright when `scanHealth.status` is not `complete` — a failed, timed-out, or
81
+ silently policy-skipped analyzer; an annotator exception; a stale
82
+ vulnerability feed (see below); or a CI environment that silently downgraded
83
+ deep analysis (see below) all now have one build-failing consequence, in
84
+ addition to whatever `--fail-on`'s severity threshold already does.
85
+ `advisory` and `standard` report the same signal but never gate — the PRD's
86
+ own acceptance criterion only specifies strict mode's behavior in full.
87
+
88
+ ### CI can no longer silently downgrade deep analysis without saying so
89
+
90
+ Requesting `--deep` in a CI environment without the explicit
91
+ `AGENTIC_SECURITY_DEEP_IN_CI=1` override already fell back to pattern-only
92
+ analysis; what was missing was that decision showing up anywhere a human or
93
+ a build gate would see it. It's now a real `scanHealth` condition, visible in
94
+ the human headline (never "Safe to deploy" under a silent downgrade), in
95
+ `ci`'s own stderr, and failable under `--assurance strict`.
96
+
97
+ ### Stale vulnerability feeds, calibration data, and rulesets are now visible — and can fail strict policy
98
+
99
+ Four independent freshness signals, each reusing an already-computed
100
+ staleness check rather than inventing a new one, now feed `scanHealth`:
101
+
102
+ - The CISA KEV catalog's existing staleness tracking is wired through for
103
+ the first time.
104
+ - The engine's own EPSS enrichment path (a separate, duplicate
105
+ implementation from `posture/epss.js`'s — a pre-existing duplication, not
106
+ introduced here) gained the same kind of per-entry age tracking KEV
107
+ already had.
108
+ - The seed calibration table (`calibration-seed.json`) now carries a real,
109
+ git-derived generation timestamp and is checked against a 180-day
110
+ threshold.
111
+ - Custom rule packs (`.agentic-security/rules/*.yml`) can now opt into a
112
+ `review-interval-days` / `reviewed-at` freshness check — deliberately not
113
+ based on file mtime, since a CI checkout resets mtimes on every run,
114
+ which would make an mtime-based check silently blind in CI, the one place
115
+ a strict-mode failure matters most.
116
+
117
+ Found while wiring this: this development machine's own, real, previously
118
+ invisible EPSS disk cache had a genuine stale entry — a live demonstration
119
+ of exactly the gap this requirement closes, not a synthetic test case.
120
+
121
+ ### Checkpointed scans now invalidate only what actually changed
122
+
123
+ Interrupted-scan resume (`AGENTIC_SECURITY_RESUME=1`) previously discarded an
124
+ entire checkpoint — every already-completed file's work — if a single
125
+ scanned file changed, was added, or was removed. Run identity is now split
126
+ into a GLOBAL component (engine version, ruleset version, running bundle,
127
+ dependency manifests, environment switches — a change to any of these still
128
+ discards everything, because those affect how every file would be analyzed)
129
+ and a PER-FILE content hash on each checkpoint record. Changing one file now
130
+ re-analyzes only that file; every other already-checkpointed file still
131
+ resumes. A global discard now names what changed
132
+ (`"engine version changed (0.143.0 -> 0.144.0)"`); a per-file invalidation
133
+ names why (`"content changed since it was checkpointed"`).
134
+
135
+ ### The privacy IR adapter now supplies 7 of its 8 named elements — "types" is a deliberate exception, not a gap
136
+
137
+ An earlier requirement claimed the privacy taint engine's IR adapter
138
+ supplied "declarations, types, assignments, calls, parameters, returns,
139
+ storage, and sinks." An independent check found only 2–3 of the 8 were
140
+ real: types was a hardcoded `null`, assignments and parameters were merged
141
+ into one undifferentiated bucket, returns and storage were absent
142
+ entirely. That requirement was honestly downgraded rather than left
143
+ mismarked. This release closes parameters, assignments (now distinctly
144
+ tagged), returns, and storage — the last of these by reindexing a
145
+ stored-taint registry the engine already computes for an unrelated
146
+ correlation, not by adding new detection logic.
147
+
148
+ "Types" remains unimplemented, on purpose. A narrow, TypeScript-only signal
149
+ is technically possible — type-annotation AST nodes survive into this
150
+ codebase's own Babel visitor before TypeScript's own preset strips them —
151
+ but supplying it means modifying the shared JS/TS parser every SAST, taint,
152
+ and privacy consumer depends on, for a benefit that would only ever cover
153
+ one of roughly ten supported languages. That trade was judged not worth the
154
+ risk to already-correct, heavily-relied-on infrastructure, and is recorded
155
+ as a deliberate scope decision rather than attempted as an unproven partial
156
+ fix.
157
+
158
+ ### A real CI-only test failure, caught by hosted CI rather than the local gate
159
+
160
+ Two `privacy-ir-adapter.test.js` tests requested deep analysis via the
161
+ `{deep: true}` runScan option and asserted `scanHealth.deepAnalysis.enabled
162
+ === true`. That held locally but failed on the hosted CI runner: the engine
163
+ downgrades deep mode under `CI=true` unless a second opt-in
164
+ (`AGENTIC_SECURITY_DEEP_IN_CI`) is also set, and this codebase's own
165
+ `test:ci-parity` static checker — built specifically to catch this class of
166
+ bug after a near-identical 2026-08-19 incident — incorrectly treats the
167
+ `{deep:true}` OPTION shape as exempt from needing that opt-in ("does not go
168
+ through the env gate," per its own comment). That reasoning is wrong: the
169
+ option and the environment variable are two different ways to set the same
170
+ internal `_deepRequested` flag, and both go through the identical
171
+ `_inCi`/`_deepInCiAllowed` gate afterward. Fixed by passing `deepInCi: true`
172
+ explicitly on both tests. The checker's incorrect exemption for the
173
+ `{deep:true}` shape is a known, separately-tracked gap — roughly forty other
174
+ test files use that same option, and this incident does not establish which
175
+ of them are actually exposed to it; auditing that is future work, not done
176
+ inside this release.
177
+
178
+ ### npm publish no longer pays for the release gate twice
179
+
180
+ `.github/workflows/release.yml`'s publish job ran the full uncached release
181
+ gate as an explicit step, then called `npm publish` — which triggers
182
+ scanner/package.json's `prepublishOnly`, itself `build && sync-changelog &&
183
+ release-check.mjs`. Read from v0.143.0's actual workflow log: the explicit
184
+ step took **280s**, and the one inside `npm publish` took **318s**, ten
185
+ seconds later, on the same commit, same checkout, same runner. Caching could
186
+ not have closed that gap by design — this workflow always passes `--no-cache`
187
+ (a cross-machine cache would reintroduce the reproducibility claim
188
+ `posture/attestation.js` explicitly declines to make), so the first run never
189
+ wrote anything for the second to reuse. That is ~600s of a release entirely
190
+ spent proving the same thing twice.
191
+
192
+ `npm publish` now runs with `--ignore-scripts`, so `prepublishOnly` does not
193
+ fire on the publish step. This is safe here specifically because it would not
194
+ be safe in general: the three explicit steps immediately above it (build,
195
+ changelog sync, gate) already did everything `prepublishOnly` would, so
196
+ nothing it produces is missing from disk by the time `npm publish` runs. The
197
+ local publish path is untouched — `scanner/package.json`'s `prepublishOnly`
198
+ stays fully wired, because a local `npm publish` has no preceding gate step to
199
+ make it redundant (root `CLAUDE.md`'s "Two publish paths" section still
200
+ applies there unchanged). `test/release-workflow.test.js` pins the flag, pins
201
+ that the three steps it stands in for still run first, and adds a tripwire: if
202
+ `scanner/package.json` ever gains a `prepack`/`postpack` script,
203
+ `--ignore-scripts` would silently skip it too, and that test fails until its
204
+ effect is reproduced as an explicit step. Both new checks confirmed to fail
205
+ when the fix is reverted, and the file is restored byte-identical after.
206
+
207
+ ### `npm test`: one `node --test` invocation instead of eleven
208
+
209
+ `npm test` chained eleven separate `npm run test:<scope>` processes in series
210
+ — `node --test` already runs a multi-file invocation's files CONCURRENTLY
211
+ against the same cores, so eleven separate processes were paying eleven
212
+ startup costs while getting zero overlap ACROSS scopes: `test:posture` could
213
+ not start until `test:sast` had entirely finished. Repeated standalone timings
214
+ on this machine put the old chain at roughly 258s and a single combined
215
+ invocation over the same 397 files at 111-154s, run five times with an
216
+ identical 3955/3955/0 (later 3964/3964/0, once this section's own tests were
217
+ added) result every time — this machine has been under sustained load all
218
+ session (`uptime` reports a load average over 5 on 8 cores while this was
219
+ written), so the absolute local number is noisy; the authoritative figure is
220
+ whatever the next hosted-CI run reports, on a dedicated runner. The mechanism
221
+ is not in question: it is the same tests, run once instead of eleven times.
222
+
223
+ `scripts/run-unit-tests.mjs` derives its file list from the existing
224
+ `test:<scope>` scripts rather than hand-maintaining a second list — a
225
+ duplicated list is exactly the shape that silently drifts (add a file to
226
+ `test:sast`, forget to add it here, and the combined run quietly covers less
227
+ than `npm run test:sast` alone does). It refuses to run if a `test:*` script
228
+ using `node --test` is not accounted for in its `SCOPES` list, both as a hard
229
+ failure from the script itself and as a named test
230
+ (`test/run-unit-tests.test.js`) so the drift guard is visible in the suite,
231
+ not just as a side effect nobody reads. `test/cpp-dataflow.test.js` and
232
+ `test:python` are deliberately NOT folded in: cpp-dataflow sets its feature
233
+ flag at module load rather than inside a test, and when included in the
234
+ combined invocation its 26 tests silently contributed **zero** results to the
235
+ totals — not a failure, not a skip, just absent — for a reason not chased to
236
+ ground; python is a different runtime entirely. Both still run, as their own
237
+ separate steps, exactly as before.
238
+
239
+ `test/discovery-wiring.test.js`'s "a scope is wired into the full gate" check
240
+ previously grepped the `test` script's text for the literal substring
241
+ `test:discovery` — true when `npm test` was a literal chain, meaningless once
242
+ it calls a derivation script instead (the substring never appears in the new
243
+ `test` script whether or not discovery's files are covered). Rewritten to
244
+ assert discovery's files are actually present in the union the runner
245
+ computes, which is a stronger claim than the substring match ever was: it
246
+ would have caught the runner's own extraction regex silently missing this
247
+ scope's files, which the substring match could not have.
248
+
249
+ ### The JetBrains plugin builds again — and its support floor moved
250
+
251
+ `jetbrains-plugin` had been red in CI, classified INFORMATIONAL, and treated as
252
+ a known toolchain gap. It was not a toolchain gap. LSP4IJ dropped IntelliJ 233
253
+ support at its 0.18.0 release, so the pinned `com.redhat.devtools.lsp4ij:0.19.4`
254
+ could never resolve against the pinned IDE 2023.3.6:
255
+
256
+ ```
257
+ Plugin 'com.redhat.devtools.lsp4ij:0.19.4' is not compatible to: IC-233.15026.9
258
+ ```
259
+
260
+ There is no configuration that supports IntelliJ 2023.3 *and* a maintained
261
+ LSP4IJ. **`sinceBuild` moves 233 → 242**: IntelliJ 2023.3 and 2024.1 are no
262
+ longer supported by this plugin. That is LSP4IJ's floor, not a preference. The
263
+ build also moves off `org.jetbrains.intellij` 1.17.4 — the superseded major,
264
+ which Gradle 9 cannot apply at all — onto the IntelliJ Platform Gradle Plugin
265
+ 2.18.1, which removes the reason CI had to pin Gradle 8.10.
266
+
267
+ `untilBuild` is now open rather than `251.*`. The old cap had already gone stale
268
+ (2025.2 exists), and a stale cap reaches the user as "plugin incompatible" on an
269
+ IDE that would have worked.
270
+
271
+ **Two defects the passing build was hiding.** Once it compiled, `buildPlugin`
272
+ exited 0 and produced a zip — and `verifyPluginProjectConfiguration` reported,
273
+ in text nothing was reading, that (1) the plugin was compiled for Java 17
274
+ against a platform requiring Java 21, and (2) the Kotlin stdlib was being
275
+ double-bundled, putting a 1.7 MB `kotlin-stdlib-2.1.0.jar` in the distribution
276
+ next to 5 KB of plugin code and leaving the platform's class loader free to
277
+ resolve stdlib classes from either. Both are fixed; the distribution zip went
278
+ from 1.59 MB to 4 KB. The CI job now **fails on that verifier's output**, since a
279
+ warning nothing fails on is a warning nobody reads, and it unpacks the zip to
280
+ confirm the `factoryClass` named in `plugin.xml` is actually in the jar — an
281
+ empty zip passed the old check.
282
+
283
+ **A defect introduced by this fix, caught before it shipped.** The new CI step
284
+ pipes Gradle into `tee` so a later step can grep the verifier's output. A `run:`
285
+ step's default shell is `bash -e {0}` — no `pipefail` — so the pipeline's exit
286
+ status is *tee's*, and a failed build would have reported success. That is the
287
+ same "green gate that verifies nothing" this entire change is about, reintroduced
288
+ by the change itself. Both pipe-to-tee steps in the workflow now set
289
+ `set -o pipefail`, including the pre-existing one in `determinism-attest`: that
290
+ job is BLOCKING, and without pipefail it could upload an empty attestation —
291
+ including on `attest-fixture.mjs`'s own zero-findings refusal — for
292
+ `determinism-compare` to compare against.
293
+
294
+ The guard added for it was itself broken twice, in opposite directions, and only
295
+ running the negative control in both found them: it first matched the word
296
+ `pipefail` inside the comment explaining the fix (so deleting the real line still
297
+ passed), then matched `| tee` inside a comment (so a correct workflow failed). A
298
+ guard fooled by its own documentation is worse than no guard, because it reads as
299
+ coverage.
300
+
301
+ **A committed Gradle wrapper.** `ide/jetbrains/README.md` told contributors to
302
+ run `./gradlew` for a long time while no wrapper existed; CI pinned a Gradle
303
+ version in the workflow instead, so the two could drift. The wrapper is now
304
+ committed and pins the Gradle distribution by SHA-256 — verified to reject a
305
+ tampered checksum, which `gradle wrapper` does not configure by default and
306
+ which is not optional in this repository.
307
+
308
+ **The classification stays INFORMATIONAL, and why.** Making a job that downloads
309
+ a full IntelliJ distribution into a release blocker trades one failure mode for a
310
+ worse one. The lesson taken instead: everything checkable without the network
311
+ moves into the blocking offline gate. `test/ide-surfaces.test.js` now asserts
312
+ that the JDK CI provisions equals the `jvmToolchain` the build asks for, that the
313
+ committed wrapper exists and pins its distribution by checksum, that the README's
314
+ stated support floor is the `sinceBuild` the artifact declares, and that an
315
+ LSP4IJ ≥ 0.18 is never paired with a `sinceBuild` below 242. Each was confirmed
316
+ to fail when its subject is broken.
317
+
14
318
  ## 0.143.0 — OSCAL output, and the finding an OSCAL document must refuse to make
15
319
 
16
320
  `--format oscal` was documented in `commands/compliance.md` long before anything