agent-threat-rules 4.1.5 → 4.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (248) hide show
  1. package/CHANGELOG.md +751 -1
  2. package/README.md +356 -125
  3. package/dist/adapters/openshell-filter.d.ts +10 -3
  4. package/dist/adapters/openshell-filter.d.ts.map +1 -1
  5. package/dist/adapters/openshell-filter.js +10 -3
  6. package/dist/adapters/openshell-filter.js.map +1 -1
  7. package/dist/adapters/stdio-adapter.js +3 -3
  8. package/dist/adapters/stdio-adapter.js.map +1 -1
  9. package/dist/cli/mcp-tools-document.d.ts +40 -0
  10. package/dist/cli/mcp-tools-document.d.ts.map +1 -0
  11. package/dist/cli/mcp-tools-document.js +95 -0
  12. package/dist/cli/mcp-tools-document.js.map +1 -0
  13. package/dist/cli/scan-handler.d.ts +8 -0
  14. package/dist/cli/scan-handler.d.ts.map +1 -1
  15. package/dist/cli/scan-handler.js +126 -34
  16. package/dist/cli/scan-handler.js.map +1 -1
  17. package/dist/cli.d.ts.map +1 -1
  18. package/dist/cli.js +80 -43
  19. package/dist/cli.js.map +1 -1
  20. package/dist/converters/sarif.d.ts +14 -2
  21. package/dist/converters/sarif.d.ts.map +1 -1
  22. package/dist/converters/sarif.js +7 -9
  23. package/dist/converters/sarif.js.map +1 -1
  24. package/dist/corpus-event.d.ts +47 -5
  25. package/dist/corpus-event.d.ts.map +1 -1
  26. package/dist/corpus-event.js +68 -29
  27. package/dist/corpus-event.js.map +1 -1
  28. package/dist/embedding/model-loader.d.ts +2 -0
  29. package/dist/embedding/model-loader.d.ts.map +1 -1
  30. package/dist/embedding/model-loader.js +3 -1
  31. package/dist/embedding/model-loader.js.map +1 -1
  32. package/dist/enforcement.d.ts +8 -0
  33. package/dist/enforcement.d.ts.map +1 -1
  34. package/dist/enforcement.js +10 -0
  35. package/dist/enforcement.js.map +1 -1
  36. package/dist/engine.d.ts +150 -6
  37. package/dist/engine.d.ts.map +1 -1
  38. package/dist/engine.js +337 -76
  39. package/dist/engine.js.map +1 -1
  40. package/dist/eval/embedding-provenance.d.ts +40 -0
  41. package/dist/eval/embedding-provenance.d.ts.map +1 -0
  42. package/dist/eval/embedding-provenance.js +71 -0
  43. package/dist/eval/embedding-provenance.js.map +1 -0
  44. package/dist/eval/eval-harness.d.ts +34 -0
  45. package/dist/eval/eval-harness.d.ts.map +1 -1
  46. package/dist/eval/eval-harness.js +49 -2
  47. package/dist/eval/eval-harness.js.map +1 -1
  48. package/dist/eval/hackaprompt-corpus.d.ts +12 -0
  49. package/dist/eval/hackaprompt-corpus.d.ts.map +1 -1
  50. package/dist/eval/hackaprompt-corpus.js +15 -1
  51. package/dist/eval/hackaprompt-corpus.js.map +1 -1
  52. package/dist/eval/hackaprompt-provenance.d.ts +52 -0
  53. package/dist/eval/hackaprompt-provenance.d.ts.map +1 -0
  54. package/dist/eval/hackaprompt-provenance.js +108 -0
  55. package/dist/eval/hackaprompt-provenance.js.map +1 -0
  56. package/dist/eval/run-hackaprompt-benchmark.d.ts +18 -8
  57. package/dist/eval/run-hackaprompt-benchmark.d.ts.map +1 -1
  58. package/dist/eval/run-hackaprompt-benchmark.js +154 -64
  59. package/dist/eval/run-hackaprompt-benchmark.js.map +1 -1
  60. package/dist/eval/run-pint-benchmark.js +11 -1
  61. package/dist/eval/run-pint-benchmark.js.map +1 -1
  62. package/dist/eval/skill-benchmark.d.ts +26 -6
  63. package/dist/eval/skill-benchmark.d.ts.map +1 -1
  64. package/dist/eval/skill-benchmark.js +49 -16
  65. package/dist/eval/skill-benchmark.js.map +1 -1
  66. package/dist/flywheel.d.ts +4 -0
  67. package/dist/flywheel.d.ts.map +1 -1
  68. package/dist/flywheel.js +12 -4
  69. package/dist/flywheel.js.map +1 -1
  70. package/dist/hook-handler.d.ts +11 -1
  71. package/dist/hook-handler.d.ts.map +1 -1
  72. package/dist/hook-handler.js +84 -16
  73. package/dist/hook-handler.js.map +1 -1
  74. package/dist/hook-tool-output.d.ts +47 -0
  75. package/dist/hook-tool-output.d.ts.map +1 -0
  76. package/dist/hook-tool-output.js +110 -0
  77. package/dist/hook-tool-output.js.map +1 -0
  78. package/dist/index.d.ts +5 -3
  79. package/dist/index.d.ts.map +1 -1
  80. package/dist/index.js +3 -2
  81. package/dist/index.js.map +1 -1
  82. package/dist/layer-integration.d.ts +4 -2
  83. package/dist/layer-integration.d.ts.map +1 -1
  84. package/dist/layer-integration.js +6 -2
  85. package/dist/layer-integration.js.map +1 -1
  86. package/dist/mcp-server.d.ts +8 -0
  87. package/dist/mcp-server.d.ts.map +1 -1
  88. package/dist/mcp-server.js +1 -0
  89. package/dist/mcp-server.js.map +1 -1
  90. package/dist/mcp-tools/scan.d.ts.map +1 -1
  91. package/dist/mcp-tools/scan.js +9 -1
  92. package/dist/mcp-tools/scan.js.map +1 -1
  93. package/dist/measurement/enforce-lane.d.ts +66 -0
  94. package/dist/measurement/enforce-lane.d.ts.map +1 -0
  95. package/dist/measurement/enforce-lane.js +89 -0
  96. package/dist/measurement/enforce-lane.js.map +1 -0
  97. package/dist/measurement/from-eval-harness.d.ts +16 -3
  98. package/dist/measurement/from-eval-harness.d.ts.map +1 -1
  99. package/dist/measurement/from-eval-harness.js +15 -5
  100. package/dist/measurement/from-eval-harness.js.map +1 -1
  101. package/dist/measurement/in-sample-ablation.d.ts +29 -0
  102. package/dist/measurement/in-sample-ablation.d.ts.map +1 -0
  103. package/dist/measurement/in-sample-ablation.js +45 -0
  104. package/dist/measurement/in-sample-ablation.js.map +1 -0
  105. package/dist/measurement/schema.d.ts +38 -1
  106. package/dist/measurement/schema.d.ts.map +1 -1
  107. package/dist/measurement/schema.js +43 -1
  108. package/dist/measurement/schema.js.map +1 -1
  109. package/dist/quality/action-eligibility.d.ts +4 -2
  110. package/dist/quality/action-eligibility.d.ts.map +1 -1
  111. package/dist/quality/action-eligibility.js.map +1 -1
  112. package/dist/rule-confidence.d.ts +31 -0
  113. package/dist/rule-confidence.d.ts.map +1 -0
  114. package/dist/rule-confidence.js +49 -0
  115. package/dist/rule-confidence.js.map +1 -0
  116. package/dist/rule-scaffolder.d.ts.map +1 -1
  117. package/dist/rule-scaffolder.js +43 -4
  118. package/dist/rule-scaffolder.js.map +1 -1
  119. package/dist/semantic-evaluator.d.ts.map +1 -1
  120. package/dist/semantic-evaluator.js +15 -13
  121. package/dist/semantic-evaluator.js.map +1 -1
  122. package/dist/shadow-evaluator.d.ts +7 -2
  123. package/dist/shadow-evaluator.d.ts.map +1 -1
  124. package/dist/shadow-evaluator.js +29 -25
  125. package/dist/shadow-evaluator.js.map +1 -1
  126. package/dist/testcase-event.d.ts +82 -1
  127. package/dist/testcase-event.d.ts.map +1 -1
  128. package/dist/testcase-event.js +135 -16
  129. package/dist/testcase-event.js.map +1 -1
  130. package/dist/tier0-invariant.d.ts.map +1 -1
  131. package/dist/tier0-invariant.js +1 -0
  132. package/dist/tier0-invariant.js.map +1 -1
  133. package/dist/tier1-blacklist.d.ts.map +1 -1
  134. package/dist/tier1-blacklist.js +1 -0
  135. package/dist/tier1-blacklist.js.map +1 -1
  136. package/dist/tool-definition-scope.d.ts +45 -0
  137. package/dist/tool-definition-scope.d.ts.map +1 -0
  138. package/dist/tool-definition-scope.js +68 -0
  139. package/dist/tool-definition-scope.js.map +1 -0
  140. package/dist/types.d.ts +42 -2
  141. package/dist/types.d.ts.map +1 -1
  142. package/dist/verdict.d.ts +46 -2
  143. package/dist/verdict.d.ts.map +1 -1
  144. package/dist/verdict.js +99 -14
  145. package/dist/verdict.js.map +1 -1
  146. package/package.json +2 -1
  147. package/rules/agent-manipulation/ATR-2026-00030-cross-agent-attack.yaml +2 -2
  148. package/rules/agent-manipulation/ATR-2026-00139-casual-authority-redirect.yaml +1 -1
  149. package/rules/agent-manipulation/ATR-2026-00328-named-mode-persona-override.yaml +1 -1
  150. package/rules/agent-manipulation/ATR-2026-00330-yoja-allcaps-settings-block-persona.yaml +1 -1
  151. package/rules/agent-manipulation/ATR-2026-00418-weknora-mcp-config-rce.yaml +5 -5
  152. package/rules/agent-manipulation/ATR-2026-00432-superagi-output-handler-eval-rce.yaml +1 -1
  153. package/rules/agent-manipulation/ATR-2026-00440-semantic-kernel-vector-store-eval-rce.yaml +3 -3
  154. package/rules/context-exfiltration/ATR-2026-00426-nl-output-injection-credential-leak.yaml +1 -1
  155. package/rules/context-exfiltration/ATR-2026-00501-data-exfiltration-via-markdown-image-and-link-url-injection.yaml +16 -2
  156. package/rules/context-exfiltration/ATR-2026-00505-system-prompt-extraction-instruction-dump-request.yaml +6 -4
  157. package/rules/context-exfiltration/ATR-2026-00702-ipi-credential-exfil-via-agent-action.yaml +1 -1
  158. package/rules/context-exfiltration/ATR-2026-00707-ipi-file-exfil-with-destroy-or-tamper.yaml +1 -1
  159. package/rules/context-exfiltration/ATR-2026-00851-indirect-pi-financial-payment-exfil.yaml +1 -1
  160. package/rules/context-exfiltration/ATR-2026-01457-sysprompt-completion-clone-attack.yaml +1 -1
  161. package/rules/context-exfiltration/ATR-2026-01462-sysprompt-override-pseudo-admin.yaml +1 -1
  162. package/rules/context-exfiltration/ATR-2026-01463-imend-imsystem-prompt-injection-format.yaml +1 -1
  163. package/rules/context-exfiltration/ATR-2026-01606-ssrf-internal-network-scan.yaml +1 -1
  164. package/rules/context-exfiltration/ATR-2026-02847-policy-justified-exfil-to-email.yaml +1 -1
  165. package/rules/context-exfiltration/ATR-2026-02855-cross-email-exfil-relay.yaml +79 -0
  166. package/rules/context-exfiltration/ATR-2026-02861-exfil-send-email-with-body-directive.yaml +83 -0
  167. package/rules/context-exfiltration/ATR-2026-02866-idna-label-separator-in-fetch-url-host.yaml +104 -0
  168. package/rules/excessive-autonomy/ATR-2026-00500-ssrf-via-agent-url-fetch-instruction.yaml +12 -2
  169. package/rules/excessive-autonomy/ATR-2026-00710-ipi-physical-access-grant.yaml +1 -1
  170. package/rules/model-abuse/ATR-2026-01897-host-sabotage.yaml +2 -2
  171. package/rules/model-security/ATR-2026-00433-modelcache-torch-load-deserialization-rce.yaml +1 -1
  172. package/rules/privilege-escalation/ATR-2026-00040-privilege-escalation.yaml +1 -1
  173. package/rules/privilege-escalation/ATR-2026-00436-enclave-vm-sandbox-escape-rce.yaml +1 -1
  174. package/rules/privilege-escalation/ATR-2026-01601-sql-injection-destructive-ddl.yaml +3 -3
  175. package/rules/privilege-escalation/ATR-2026-01612-debug-mode-privilege-escalation.yaml +1 -1
  176. package/rules/privilege-escalation/ATR-2026-02867-identifier-typed-tool-argument-dot-dot-path-segment.yaml +106 -0
  177. package/rules/prompt-injection/ATR-2026-00004-system-prompt-override.yaml +2 -2
  178. package/rules/prompt-injection/ATR-2026-00080-encoding-evasion.yaml +1 -1
  179. package/rules/prompt-injection/ATR-2026-00086-visual-spoofing.yaml +1 -1
  180. package/rules/prompt-injection/ATR-2026-00203-context-pollution-skill-description.yaml +1 -1
  181. package/rules/prompt-injection/ATR-2026-00228-structured-jailbreak.yaml +2 -2
  182. package/rules/prompt-injection/ATR-2026-00233-structured-jailbreak.yaml +2 -2
  183. package/rules/prompt-injection/ATR-2026-00256-base-n-encoding-jailbreak.yaml +1 -1
  184. package/rules/prompt-injection/ATR-2026-00264-latent-injection-translation.yaml +3 -3
  185. package/rules/prompt-injection/ATR-2026-00265-latent-injection-rag-document.yaml +1 -1
  186. package/rules/prompt-injection/ATR-2026-00282-perez-prompt-injection-hijack.yaml +4 -4
  187. package/rules/prompt-injection/ATR-2026-00321-hyphenated-system-instruction-injection.yaml +1 -1
  188. package/rules/prompt-injection/ATR-2026-00394-backspace-deletion-character-injection.yaml +2 -2
  189. package/rules/prompt-injection/ATR-2026-00446-variable-assignment-payload-injection.yaml +3 -3
  190. package/rules/prompt-injection/ATR-2026-00476-garak-web-injection-javascript-alert-1.yaml +3 -3
  191. package/rules/prompt-injection/ATR-2026-00481-garak-dan-mixedunassigned.yaml +1 -1
  192. package/rules/prompt-injection/ATR-2026-00489-garak-encoding-javascript-alert-1.yaml +3 -3
  193. package/rules/prompt-injection/ATR-2026-00503-fake-error-state-takeover-unrestricted-replacement-bot.yaml +4 -2
  194. package/rules/prompt-injection/ATR-2026-00508-escape-delimiter-wrapped-goal-hijacking-in-user-input-prompt.yaml +3 -3
  195. package/rules/prompt-injection/ATR-2026-00512-rules-file-backdoor-injection.yaml +1 -1
  196. package/rules/prompt-injection/ATR-2026-01005-many-shot-repetition-override.yaml +2 -2
  197. package/rules/prompt-injection/ATR-2026-01006-payload-split-string-concat-injection.yaml +2 -2
  198. package/rules/prompt-injection/ATR-2026-01015-evasion-refusal-suppression.yaml +1 -1
  199. package/rules/prompt-injection/ATR-2026-01016-evasion-response-priming-forced-opener.yaml +1 -1
  200. package/rules/prompt-injection/ATR-2026-01026-evasion-unicode-glitch-token-anomaly.yaml +2 -2
  201. package/rules/prompt-injection/ATR-2026-01304-indirect-injection-carrier-important-override.yaml +1 -1
  202. package/rules/prompt-injection/ATR-2026-01759-dos-new-important-instructions-injection-in-document.yaml +1 -1
  203. package/rules/prompt-injection/ATR-2026-01865-llmail-fake-email-boundary-marker.yaml +1 -1
  204. package/rules/prompt-injection/ATR-2026-02010-emoji-only-payload.yaml +2 -2
  205. package/rules/prompt-injection/ATR-2026-02815-injected-halt-order-canned-reply-dos.yaml +80 -0
  206. package/rules/prompt-injection/ATR-2026-02820-tool-result-orders-agent-to-hide-its-own-action.yaml +106 -0
  207. package/rules/prompt-injection/ATR-2026-02858-context-suppression-directive.yaml +73 -0
  208. package/rules/prompt-injection/ATR-2026-02859-tool-output-agent-role-reassignment.yaml +71 -0
  209. package/rules/prompt-injection/ATR-2026-02860-forged-instruction-boundary-marker.yaml +70 -0
  210. package/rules/prompt-injection/ATR-2026-02862-fake-chat-template-turn-injection.yaml +79 -0
  211. package/rules/skill-compromise/ATR-2026-00060-skill-impersonation.yaml +1 -1
  212. package/rules/skill-compromise/ATR-2026-00063-skill-chain-attack.yaml +2 -2
  213. package/rules/skill-compromise/ATR-2026-00064-over-permissioned-skill.yaml +1 -1
  214. package/rules/skill-compromise/ATR-2026-00066-parameter-injection.yaml +2 -2
  215. package/rules/skill-compromise/ATR-2026-00149-skill-exfil-compound.yaml +6 -6
  216. package/rules/skill-compromise/ATR-2026-00223-reverse-shell-dropper.yaml +2 -2
  217. package/rules/skill-compromise/ATR-2026-01755-backdoor-pot-linguistic-trigger-phrase.yaml +1 -1
  218. package/rules/skill-compromise/ATR-2026-01756-backdoor-pot-symbol-emoticon-trigger.yaml +1 -1
  219. package/rules/skill-compromise/ATR-2026-02261-code-credential-exfil-chain.yaml +1 -1
  220. package/rules/tool-poisoning/ATR-2026-00012-unauthorized-tool-call.yaml +23 -23
  221. package/rules/tool-poisoning/ATR-2026-00013-tool-ssrf.yaml +16 -5
  222. package/rules/tool-poisoning/ATR-2026-00161-important-tag-cross-tool-shadowing.yaml +1 -1
  223. package/rules/tool-poisoning/ATR-2026-00277-echo-template-command-injection.yaml +2 -2
  224. package/rules/tool-poisoning/ATR-2026-00494-garak-exploitation-mixedunassigned.yaml +4 -4
  225. package/rules/tool-poisoning/ATR-2026-00521-shell-command-injection-agent-tool-context.yaml +1 -1
  226. package/rules/tool-poisoning/ATR-2026-00581-mcp-tool-rug-pull-post-approval-redefinition.yaml +1 -1
  227. package/rules/tool-poisoning/ATR-2026-01303-tool-schema-enumeration-social-engineering.yaml +1 -1
  228. package/rules/tool-poisoning/ATR-2026-01307-mcp-dns-rebinding-attack.yaml +2 -2
  229. package/rules/tool-poisoning/ATR-2026-01775-semantic-mcp-tool-manifest-poisoning.yaml +1 -1
  230. package/rules/tool-poisoning/ATR-2026-01932-shadow-undeclared-mcp-server-registration.yaml +1 -1
  231. package/rules/tool-poisoning/ATR-2026-02025-mcp-full-schema-poisoning-nondescription-field.yaml +1 -1
  232. package/rules/tool-poisoning/ATR-2026-02817-untrusted-content-dictates-tool-arguments.yaml +83 -0
  233. package/rules/tool-poisoning/ATR-2026-02864-non-canonical-ipv4-host-in-url-tool-argument.yaml +102 -0
  234. package/rules/tool-poisoning/ATR-2026-02865-read-only-utility-write-flag-in-command-argument.yaml +157 -0
  235. package/rules/tool-poisoning/ATR-2026-02868-ssrf-ipv6-transition-address-embedded-private-ipv4.yaml +107 -0
  236. package/rules/tool-poisoning/ATR-2026-02870-invisible-control-before-directive.yaml +94 -0
  237. package/spec/README.md +6 -3
  238. package/spec/atr-event-v1.0.md +13 -8
  239. package/spec/atr-method-v1.1.md +2 -2
  240. package/spec/category-registry/gemara/README.md +102 -0
  241. package/spec/category-registry/gemara/ai-agent-capability-catalog.yaml +157 -0
  242. package/spec/category-registry/gemara/atr-attack-vector-catalog.yaml +130 -0
  243. package/spec/category-registry/gemara/atr-detection-control-catalog.yaml +327 -0
  244. package/spec/category-registry/gemara/atr-threat-catalog.yaml +246 -0
  245. package/spec/category-registry/gemara/src/editorial.yaml +743 -0
  246. package/spec/mappings/README.md +1 -1
  247. package/spec/mappings/atr-to-nist-csf-2.0.md +5 -4
  248. package/spec/schema/event.schema.json +1 -1
package/CHANGELOG.md CHANGED
@@ -4,6 +4,733 @@ All notable changes to ATR will be documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [4.2.1] - 2026-10-24
8
+
9
+ A patch release of rule fixes and one new rule, plus two release-tooling
10
+ fixes. The rule format, identifier scheme and Match output are unchanged.
11
+
12
+ Default behaviour that changes:
13
+
14
+ - ATR-2026-01601 (SQL injection with destructive DDL, maturity `stable`, so it
15
+ runs in the enforce lane) now also matches when whitespace separates the
16
+ single quote from the semicolon, as in `1' ; TRUNCATE TABLE accounts; --` and
17
+ `7' ; DELETE FROM sessions; --`, which it missed before. An integration that
18
+ blocks on the enforce lane blocks these too. Its benign false positives were
19
+ re-measured: none over the 14,172 samples of the benign gate (#748, issue
20
+ #620).
21
+
22
+ ### Added
23
+
24
+ - **ATR-2026-02870: an invisible control before a directive in a tool
25
+ description or skill file.** A zero-width, soft-hyphen or bidi control
26
+ character at the start of a clause that tells the agent to ignore or
27
+ override instructions, send or read secrets, conversations or credential
28
+ files, or keep something from the user. Presence of the character alone
29
+ does not match, so soft hyphens in German, ZWNJ in Persian, LRM/RLM in Hebrew
30
+ and Arabic, emoji ZWJ sequences and a leading BOM stay clear. It reads
31
+ `tool_description`, so it runs in a `tools/list` scan, on `mcp_exchange`
32
+ events and on SKILL.md files. Maturity `test` (hunt and alert lanes). No
33
+ benign false positives on the gate corpora or on the 710 tool definitions in
34
+ `data/test-corpora/mcp-tools-list-benign/`, which contain no such characters,
35
+ so that last figure is weak evidence. A control inside a word, rather than at
36
+ the start of a clause, is ATR-2026-00276's case and not covered here (#749,
37
+ from issue #653 by @Paraphern).
38
+
39
+ ### Fixed
40
+
41
+ - **ATR-2026-00040 and -00064 matched the wrong chmod modes.** Their chmod
42
+ condition matched the owner digit of `chmod 755 deploy.sh` or `chmod 700
43
+ ~/.ssh` and missed `chmod -R 777 /`. They now match a world-writable mode or
44
+ a setuid/setgid/sticky four-digit mode; with options such as `-R` before the
45
+ mode, only when the target is the root, a system directory or the home
46
+ directory, since `chmod -R 777` on an application subdirectory is documented
47
+ practice in the benign corpus. Group-writable modes are not matched. Both
48
+ rules are maturity `test` (#740, issue #735, from the probe in #658 by
49
+ @PHM-Audit).
50
+ - **ATR-2026-00500, -00501 and -00505 are grounded in the vendor test cases
51
+ they cite.** 00500 and 00501 matched 1 and 0 of promptfoo's SSRF and
52
+ data-exfiltration test cases, and 00505 missed an llm-guard case with a
53
+ typographic apostrophe. They now match all of them (3 of 3, 3 of 3, 6 of 6),
54
+ their true positives that did not occur in any vendor repository were
55
+ replaced by verbatim vendor cases with repository, commit, file and line,
56
+ and -00503 and -00521 gained the vendor originals behind their shortened
57
+ samples. On the pinned vendor suites, hunt-lane recall goes from 97 to 100 of
58
+ 126 attack cases; the ablation reference is unchanged (#709, measurement in
59
+ #750).
60
+ - **ATR-2026-00223's samples use a documentation address** (203.0.113.30)
61
+ instead of an address from a real dropper. Detection is unchanged (#738).
62
+ - **The release workflow commits the reconciled stats cache with the version
63
+ bump.** The 4.2.0 bump reached `main` with `data/stats.json` still at 4.1.5,
64
+ which failed CI on `main`; the published package was not affected (#746).
65
+ - **The Gemara catalogs' first publish reads oras's "not found" as
66
+ unpublished.** The first publish from v4.2.0 stopped at the
67
+ already-published check; this release tag is the first that can publish the
68
+ bundle (#747).
69
+
70
+ ## [4.2.0] - 2026-10-24
71
+
72
+ The first release since 4.1.5. For an integrator it changes three things at
73
+ runtime: the Claude Code guard reads what a tool returned instead of what it
74
+ was given, rules written for `context_window` and `skill_*` events now run on
75
+ the events their data arrives in, and a match that a model decided is
76
+ reported instead of enforced unless the integrator opts in. `atr scan` now
77
+ scans an MCP config file it used to skip, and an MCP `tools/list` document
78
+ as one event per tool, without running the rules written for a tool being
79
+ called. Many rule conditions that took polynomial time on long input were
80
+ rewritten, and three rules left the enforce
81
+ lane after matching benign samples. Like 4.1.5, the npm package carries the
82
+ rules without their test samples; the repository keeps them.
83
+
84
+ Default behaviour that changes, and how to keep the old one:
85
+
86
+ - Model-decided matches are advisory. A match from a semantic rule's judge
87
+ that its own pattern fallback does not corroborate, from the embedding
88
+ module or from the semantic layer is listed in `verdict.advisory` and no
89
+ longer sets the verdict's outcome or dispatches more than observe-tier
90
+ actions. To keep the old behaviour pass `modelDecisions: 'enforce'` to
91
+ `ATREngine` or `computeVerdict()`, or use `--model-decisions enforce` or
92
+ `ATR_MODEL_DECISIONS=enforce` with `atr scan` and `atr mcp`. Integrations
93
+ that wire no model, including `atr guard` and the bundled adapters, see no
94
+ change (#720). `verdict.highestSeverity` and `highestConfidence` now
95
+ describe only the matches the outcome came from (read `advisorySeverity`
96
+ for the rest), and `atr scan --json` gives every match `decided_by` and
97
+ `enforced`.
98
+ - More of what an agent sees reaches the rules. A guard registered under
99
+ Claude Code's PostToolUse now scans the tool's output, where it used to scan
100
+ the tool's input; registering it under PostToolUseFailure as well also scans
101
+ failed calls (#715, #721). Rules of type `context_window` and `skill_*` now
102
+ run on typed runtime events (#701). The OpenShell filter
103
+ and NemoClaw preflight adapters evaluate in the hunt lane and deny at `high`
104
+ and above by default (`ATR_MIN_SEVERITY`), so they can now deny on
105
+ ATR-2026-00070 and -01774 (high, `llm_input`/`llm_output`) and
106
+ ATR-2026-00157 and -02405 (critical, `tool_call`), where 4.1.5 did not. Both raise matches in
107
+ the hunt lane, the engine default, including on benign text (measured
108
+ figures under Fixed below); neither moves the enforce lane. There is no
109
+ switch back. An integration that blocks on PostToolUse should block on the
110
+ enforce lane: `atr guard --blocking --lane enforce` (or `ATR_BLOCKING=1
111
+ ATR_LANE=enforce`), or `lane: 'enforce'` on `ATREngine`. The guard's default
112
+ lane is hunt, so `--blocking` alone blocks on the hunt-lane matches above.
113
+ - ATR-2026-00080, -00436 and -01606 moved from maturity `stable` to `test`
114
+ after matching benign samples in the grown corpus (#625). They still alert;
115
+ an integration that blocks only in the enforce lane no longer blocks on
116
+ them.
117
+ - ATR-2026-00012 moved from maturity `test` to `experimental` (#683), so it
118
+ runs in the hunt lane only and no longer in the alert lane. ATR-2026-00233,
119
+ a duplicate of ATR-2026-00228, is deprecated and runs in no lane (#666).
120
+ - `atr scan` on an MCP config file scans one event per server (#581, by
121
+ @ciceroyang), and on an MCP `tools/list` document, bare or in a JSON-RPC
122
+ response, one event per tool (#737). 4.1.5 skipped both and reported them
123
+ clean. Any JSON file `atr scan` reads in which no event reaches the engine
124
+ now exits 1, in text, `--json` and `--sarif` output alike; `events_scanned`
125
+ counts only events that did, beside a new `events_skipped`. A CI step that ran
126
+ `atr scan` on such a file and always passed may now fail, on the exit code or
127
+ through `--fail-on`. The GitHub Action ignores the exit code (#575) but counts
128
+ the new findings toward `fail-on-finding`. Scanning a directory that holds
129
+ only an MCP config is not fixed yet (#575).
130
+
131
+ Under SPEC §10 this is a minor release: the rule format, identifier scheme and
132
+ Match output are unchanged (matches gain an optional `decidedBy`, which §7
133
+ allows). SPEC.md moves to 1.0.1, a [Compatible] correction of §6.4 (#699).
134
+ SPEC §10 versions the format, not the package's defaults, and some defaults
135
+ change above. 4.0.0 went major because every `atr guard` user lost a block
136
+ they had by default. Here an integration that wires no model loses no block;
137
+ one that wired a model judge and blocked on its decisions does, and one that
138
+ blocks in the hunt lane, as `atr guard --blocking` does on PostToolUse and the
139
+ OpenShell and NemoClaw adapters do, can gain blocks. If that describes your
140
+ integration, apply the switches above or stay on 4.1.5.
141
+
142
+ ### Added
143
+
144
+ - **New rules, all at maturity `test`** (hunt and alert lanes, not the
145
+ enforce lane): ATR-2026-02815, -02817 and -02820 (#612); -02858, -02859 and
146
+ -02860, technique rules for llm-guard samples lost to earlier FP repairs
147
+ (#668); -02855, -02861 and -02862 from the FN-mine sweeps (#650, #671);
148
+ -02864, -02865, -02866, -02867 and -02868 from agent-related CVEs published
149
+ 2026-09-22 to 2026-10-21 (#730, #731).
150
+
151
+ - **The category registry as Gemara catalogs, generated and published from
152
+ here.** `spec/category-registry/gemara/` holds a vector, capability, threat
153
+ and control catalog validated against the Gemara v1.6.0 CUE schemas. The
154
+ vector catalog is generated from `spec/category-registry/v1.0.yaml` (ids and
155
+ descriptions verbatim) by `scripts/generate-gemara-catalogs.ts`;
156
+ `tests/gemara-catalogs.test.ts` fails when the committed catalogs are stale.
157
+ `.github/workflows/publish-gemara.yml` publishes them as one keyless-signed
158
+ OCI bundle, `ghcr.io/agent-threat-rule/agent-threat-rules/gemara:<bundle
159
+ version>`, on release tags only, and pushes nothing when that version is
160
+ already published with the same catalogs.
161
+
162
+ - **`atr test` reports a true positive that matches only through a copy the
163
+ test event makes.** The test event offers a case's input under every alias a
164
+ rule might read, whatever the event type: as `user_input` and `tool_args`, and
165
+ its content as `agent_message`. `buildEventFromTestCase` gains a `runtime`
166
+ field set, which holds what this repository's producers put on an event of the
167
+ type: content, the fields the case names (taken as given), `user_input` on an
168
+ `llm_input` event, and on a tool event the `tool_name` and `tool_args`
169
+ `src/hook-handler.ts` fills. `caseMatchesAtRuntime` runs a case on that set
170
+ for every event type, the engine's source filter deciding which of them the
171
+ rule runs on, or as a skill document. `atr test` lists a passing true positive
172
+ that matches none of them under `harnessOnly` (JSON) and "Harness-only"
173
+ (text); it still counts as passed. No true positive on main is one.
174
+ ATR-2026-01610's two cases declared as `user_input` match their `llm_input`
175
+ event only through the input copied into `tool_args`; the engine also runs
176
+ `llm_io` rules on `tool_response` events, where the hook fills `tool_args`,
177
+ and there they match. `tests/testcase-runtime-parity-rules.test.ts` runs every
178
+ rule's true positives the way `atr test` does, leaving out a type the engine
179
+ withholds as `atr test` does, and fails on one.
180
+
181
+ - **Every benchmark measurement records the enforce lane beside its
182
+ headline.** A measurement's `metrics.recall` is the `hunt` lane (the engine
183
+ default, every maturity). Each runner the weekly measurement calls now also
184
+ runs the same samples through its own detection loop on an engine
185
+ constructed with `lane: 'enforce'` (only `maturity: stable` rules, the lane
186
+ for blocking) and records the result as `breakdown.enforce_lane`. Where a
187
+ measurement records it, README's Recall cell cites both lanes and root
188
+ `stats.json` cites the enforce figure as `enforceLaneRecall`;
189
+ `scripts/check-benchmark-citations.ts` fails CI when either disagrees with
190
+ the measurement or leaves the enforce figure out. Before this, the enforce
191
+ figures in README were one-off replays.
192
+ - **Recall figures outside README are rendered from the measurement files.**
193
+ `scripts/render-recall-citations.ts` fills the figures between recall
194
+ markers in `LIMITATIONS.md` and the `docs/owasp-pr/` texts, from the file each
195
+ source's `latest.json` points at, in both lanes (every rendering is held to
196
+ the rule README's table is checked by). `scripts/sync-citation-garak.ts`
197
+ generates the garak sentence in `CITATION.cff` from root `stats.json`, with
198
+ both lanes and the closed-book caveat. The weekly measurement run re-renders
199
+ and stages both and lists what it could not render in the rolling PR's body;
200
+ CI (`eval.yml`) fails on a stale or unrenderable figure.
201
+ - **`scripts/bench-evaluate-latency.ts` times single `ATREngine.evaluate()`
202
+ calls** in the hunt and enforce lanes on three committed corpora of
203
+ different input length, so the latency wording in LIMITATIONS.md and
204
+ THREAT-MODEL.md can be checked on any hardware. The `latency_ms` blocks in
205
+ `data/measurements/` time whole corpus samples (several event shapes, a
206
+ skill scan and, when installed, an embedding pass) inside a full benchmark
207
+ run, and are not comparable.
208
+ - **CI re-checks the /red-team page's links against GitHub.** The page's list
209
+ of offensive tools and corpora moved out of the page into
210
+ `website/data/red-team-tools.json`. `scripts/verify-red-team-links.mjs`, run
211
+ by `verify-red-team-links.yml` when the list or the checker changes and
212
+ weekly, fails when an in-review entry's PR or issue is no longer open, an
213
+ in-production entry's PR is not merged, a corpus entry's link is neither a
214
+ merged PR nor a commit on the default branch, or the `stillPresent` file an
215
+ in-production or corpus entry must name no longer holds its text. It
216
+ reports drift, links that are gone, and lookups GitHub could not answer
217
+ separately, and counts none of them as a pass; a failure on main or on the schedule opens a tracking issue.
218
+ It would have flagged NVIDIA/garak#1676 within a week of its withdrawal on
219
+ 2026-08-04, instead of the ten weeks it stayed listed before #688. It cannot
220
+ flag an open proposal nobody answers, which is why the page calls that group
221
+ "Open proposals". The three corpus entries linked to ATR PRs #51, #52 and
222
+ #53, which do not contain the rules they named; they now link to the commits
223
+ that added ATR-2026-00452..00456, 00500..00505 and 00510..00517, the second
224
+ no longer names NeMo-Guardrails as a rule source, and every figure they and
225
+ the page's 2026-05-12 sprint section quote now comes from a measurement file
226
+ or a committed re-run record. HackAPrompt is quoted per lane on its pinned
227
+ 4,780-prompt subset, followed by its ablation reference (the same prompts
228
+ without the rules whose YAML names HackAPrompt), read from
229
+ `breakdown.in_sample_ablation` of the measurement file and called what it is,
230
+ an approximation of out-of-sample recall rather than an out-of-sample result;
231
+ the vendor-suite entry is followed by its own. The sprint's 28.6% to 66.2%
232
+ becomes the re-measured 28.6% to 66.0%: 66.2% could not be reproduced at the
233
+ sprint commits `e9efefdd` and `92c0d0fe` (66.0% at best), and the 28.6% is
234
+ itself in-sample. The paper is described as listed under Best Paper Awards at
235
+ EMNLP 2023, on gpt-3.5-turbo, FlanT5-XXL and text-davinci-003 (not
236
+ GPT/Claude/PaLM). The 2026-05-12 import is described as 79 of 94 samples
237
+ occurring verbatim in the vendor file they name. The sprint added 71 rule
238
+ files, not 75 (the 2.2.0 entry also counted four earlier CVE rules), and the
239
+ claims that the sprint's and the 2026-06 mega-scan's rules all passed the
240
+ quality gate with 0 benign FP are replaced by the gate re-run on 2026-10-19
241
+ and again on 2026-10-22 (`data/gate-reruns/2026-10-19_red-team-rule-batches.json`):
242
+ at 92c0d0fe, 10 of the 71 failed (own true positives) and none flagged a benign
243
+ SKILL.md file; at main b9d16840 on 2026-10-22, 33 of 71 and 4 of 29 fail, and
244
+ 26 and 3 flag at least one benign sample, none of them in the enforce lane,
245
+ the same as at 9d74fc50 on 2026-10-19 although nine of the recorded rules
246
+ changed in between. Each corpus entry links its
247
+ measurement file.
248
+
249
+ ### Changed
250
+
251
+ - **Three rules left the enforce lane.** ATR-2026-00080, -00436 and -01606
252
+ each matched benign samples in the grown corpus and were demoted from
253
+ maturity `stable` to `test` (#625). ATR-2026-00233, a duplicate of another
254
+ rule, is deprecated (#666).
255
+ - **Six rules follow MITRE ATLAS v2026.07.** AML.T0019 and AML.T0104, retired
256
+ into AML.T0115 as sub-techniques, are replaced by AML.T0115.000 and
257
+ AML.T0115.002 in the rules' `references.mitre_atlas` (#543).
258
+
259
+ - **A match a model decided is reported, not enforced, unless the integrator
260
+ opts in.** Three engine paths turn a model's output into a match: a semantic
261
+ rule's `semanticJudge`, the `embeddingModule` similarity signal (which in the
262
+ hunt and alert lanes could produce a critical `block_input` match), and the
263
+ `semanticModule` layer. No evidence a response tier is earned from measures
264
+ them: the benign-corpus evidence runs the synchronous engine, where a semantic
265
+ rule is its pattern fallback, and neither the embedding module nor the
266
+ semantic layer. Every match now carries `decidedBy`, and
267
+ `computeVerdict()` keeps a model-decided one out of the outcome and dispatches
268
+ only its observe-tier actions; it is listed in the new `verdict.advisory`
269
+ (with `verdict.advisorySeverity`), and `highestSeverity` and
270
+ `highestConfidence` now describe the matches the outcome came from. A
271
+ semantic rule whose judge matches is enforced when the rule's own pattern
272
+ fallback matches the same input (reported as the fallback's match); a judge
273
+ match the fallback does not corroborate is advisory. Rule, invariant,
274
+ blacklist and fingerprint matches decide as before. `modelDecisions:
275
+ 'enforce'` on the engine (or on `computeVerdict`) restores the previous
276
+ behaviour; any other value throws. `computeVerdict` now sorts the matches it
277
+ is given instead of trusting their order.
278
+ - **`atr scan` and the MCP server follow the same default.** `atr scan
279
+ --semantic` lists a judge-only match but leaves it out of `--fail-on`, and
280
+ writes it to SARIF at level `note` with `properties.enforced: false`, which the
281
+ GitHub Action no longer counts toward `fail-on-finding`. JSON and SARIF output
282
+ name what decided each match (`decided_by`). The MCP scan tool reports a
283
+ judge-only match with outcome `allow`, `enforced: false` and observe-tier
284
+ recommended actions. `--model-decisions enforce` or
285
+ `ATR_MODEL_DECISIONS=enforce` restores counting for `atr scan` and `atr mcp`.
286
+ `atr guard` and the bundled adapters wire no model, so nothing they do
287
+ changes.
288
+
289
+ - **The ReDoS gate now times long inputs, every branch of an alternation
290
+ and input tiled with a pattern's own openers, and its baseline grew from 9
291
+ to 439 conditions because of it.** Nothing got slower; the gate started
292
+ looking. Two integrations packaging the corpus found conditions that took
293
+ seconds to minutes on 8,000- and 50,000-character inputs while
294
+ `scripts/gate-redos.py` reported them clean: its inputs were 1,500 to
295
+ 3,000 characters, where a quadratic condition costs about a tenth of a
296
+ second, and only the first branch of an alternation was ever pumped. Each
297
+ condition is now also run on 20,000-character inputs built per quantifier
298
+ -- the text that reaches the quantifier, through whichever branch,
299
+ lookbehind or back-reference leads there, followed by a long run of its
300
+ own body -- and on its openers and pumped near-match tiled to length
301
+ (#601). Each worker calibrates per chunk and times are reported in
302
+ reference-machine seconds, which reduces, but does not remove, the effect
303
+ of load that changes during a run. `data/redos-baseline.json` now lists
304
+ 439 conditions in 259 rules: 350 over the budget when it was written (201
305
+ of them did not return within the wall even timed alone) and 89 under the
306
+ budget but over a third of it, recorded too because one condition's
307
+ readings spread by more than 2x across runs and machines. 41 are in the
308
+ enforce lane and 412 are high or critical.
309
+ Each entry carries a fingerprint of its pattern, so editing a baselined
310
+ condition makes it new again instead of inheriting the exemption; an
311
+ unchanged baselined hang is not re-run. The gate stays a ratchet: it fails
312
+ on a slow condition outside that file. `scripts/verify_rule_redos.py` now
313
+ uses the gate's own inputs and timing -- it had reported PASS on
314
+ ATR-2026-02372 with both conditions hanging -- and with `--base REF` also
315
+ times a changed condition on the inputs derived from its previous pattern.
316
+ Twelve conditions that Python `re` cannot compile, and that the gate
317
+ skipped without saying so, are now listed by key. pyatr runs eleven of
318
+ them through its `regex` fallback and the TypeScript engine compiles all
319
+ twelve, so they run in production and are still untimed here; the twelfth,
320
+ ATR-2026-00290#3, back-references a group that does not exist, which pyatr
321
+ refuses and JavaScript reads as the character U+0001 (#331). The semantic
322
+ lane's per-draft ReDoS precheck now allows 300 s instead of 120 s.
323
+
324
+ - **Every recall figure the site, LIMITATIONS.md, CITATION.cff and the docs
325
+ above take from the measurement files now names its lane** and gives the
326
+ enforce figure wherever the measurement records one. Figures no measurement
327
+ file records are labelled instead: the about page's historical garak figure
328
+ as taken with every rule loaded, the neutral-benchmarks post's runs as having
329
+ no recorded lane. The website reads `breakdown.enforce_lane` from the measurement
330
+ files (home, coverage, research, red-team, garak integration, ATD and
331
+ sovereign-AI pages, the PINT blog post); it no longer reads recall
332
+ from the old eval report files or their typed fallbacks, and no longer
333
+ hydrates recall in the browser. The garak figures carry the closed-book
334
+ caveat, and so now do the PINT-format and HackAPrompt figures, which have
335
+ rules mined from them too. The SKILL.md benchmark is described as the
336
+ internal, labelled corpus it is, with the harness's layer names. An erratum
337
+ (`e-2026-10-16-recall-lanes`) records the change on the site.
338
+ - `sync-stats.ts` now fails when CITATION.cff has no garak sentence to
339
+ rewrite, instead of leaving an old figure in place.
340
+ - **`scripts/check-rules-safety.ts` checks that a modified rule still matches
341
+ its own true positives, and checks renamed rules.** A new rule's true
342
+ positives had to match; a modified rule only had to add no benign false
343
+ positive, and a rule that matches nothing adds none. `npm run
344
+ gate:generalization` covers pattern rules only, so a semantic rule whose
345
+ `fallback_method` was set to `none` passed every check. The new own-TP check
346
+ runs each true positive on both paths `atr test` tries, replaying every case
347
+ to the changed rule as the event the base rule would receive: a case the
348
+ base rule matched on a path (`evaluate()`, `scanSkill()`) must still match
349
+ there, one the change adds must match, and the changed rule must still match
350
+ one of the cases the base rule matched. That also catches re-routing a rule
351
+ away from its cases. A rule of a type the engine withholds from typed events
352
+ (`WITHHELD_SOURCE_TYPES`), which `atr test` reports as unevaluable, is
353
+ skipped while it stays that type. A renamed rule file was on neither the new
354
+ nor the modified list and skipped both checks; it is now compared with the
355
+ file it was at the base, and a changed file whose rule id differs is checked
356
+ as a new rule. `npm run validate` also checks a semantic rule's
357
+ `fallback_method` (`pattern` or `none`, compared exactly, as the engine
358
+ does), its `threshold` (a number from 0 to 1), and that a pattern fallback
359
+ has a condition to match.
360
+ - **`scripts/check-rules-safety.ts` runs a new rule's true positives the way
361
+ `atr test` does (Check 2).** It read only a case's `input` and ran it as the
362
+ text of the benign corpus shapes. On main, as if each rule past Check 1 were
363
+ new, 250 true positives in 54 rules carry their payload elsewhere
364
+ (`tool_response`, `tool_args`, `tool_description`, a `user_input` key, an
365
+ object `input`, ...) and were never read; 47 of those rules had none read
366
+ and passed with nothing run. The routing a rule declares was never
367
+ exercised. Each case now runs as the event built from it for the rule's own
368
+ `agent_source.type`, and through `scanSkill()`, with the code the own-TP
369
+ check for modified rules uses (`scripts/lib/own-true-positives.ts`), and its
370
+ text must still match on the corpus shapes, so a case that fires only
371
+ through a tool name, which no benign sample exercises, fails. The semantic
372
+ lane's pre-gate runs the same code. A case that cannot be run fails, and so
373
+ does a rule of a type the engine withholds from typed events
374
+ (`memory_access`), whose cases `atr test` reports as unevaluable. A new rule
375
+ the benign checks cannot run at all (deprecated, or a trace, behavioral or
376
+ signature method) is refused with the reason; Check 2 used to fail most of
377
+ them only because it could not match their cases either, and one whose cases
378
+ carried no `input` passed the whole gate.
379
+ - **README §8 discloses that the `garak` and `garak-full` figures are partly
380
+ closed-book.** A replay without the rules whose own YAML shows they were
381
+ written from the in-the-wild corpus is given in the new `[^garakcorpus]`
382
+ footnote, and the paragraph on the two `garak` rows no longer calls them
383
+ independent: `garak-full` contains that corpus twice.
384
+ - `data/stats.json` no longer carries the orphaned `benchmark.pintRecall`
385
+ block, a 3.5.11 figure nothing has written since #600 and nothing reads.
386
+
387
+ ### Fixed
388
+
389
+ - **`atr scan` scans an MCP config file instead of skipping it.** The file was
390
+ parsed, wrapped as one event without `content` and skipped, while the
391
+ summary counted it as scanned, so a config that sets `NODE_OPTIONS` to a
392
+ `require` payload came back as no threats. Each server in the document is
393
+ now its own event, `events_scanned` counts what reached the engine beside
394
+ `events_skipped`, and a file with nothing scannable exits non-zero (#581,
395
+ by @ciceroyang; issue #575, where directory scans and the Action's exit
396
+ handling remain open).
397
+
398
+ - **Rule conditions that took polynomial time on long input were
399
+ rewritten.** 86 conditions in 48 rules found by the extended ReDoS gate
400
+ (#704), eight conditions quadratic in a long run of one character (#723),
401
+ and ATR-2026-00063 and -00228 (#666). The rewrites keep the language of
402
+ each condition, except four deliberate changes in #704:
403
+ ATR-2026-00446 condition 0 widens, and ATR-2026-01005 condition 1 and
404
+ ATR-2026-01307 conditions 0 and 1 narrow. ATR-2026-02261 condition 0
405
+ narrows on purpose in #723 (its back-reference no longer ties two different
406
+ variables), and ATR-2026-00228 condition 1 in #666 (on JSON-encoded content
407
+ the two labels must be on one line). Conditions still over the gate's
408
+ budget stay listed in `data/redos-baseline.json`.
409
+ - **Rule false positives.** ATR-2026-00012 matched 1,817 of 14,157 benign
410
+ samples and now matches 150, with every match of the new version also a
411
+ match of the old one (#683). ATR-2026-02010 no longer matches blank or
412
+ whitespace-only input (#704). ATR-2026-00013 also flags apex
413
+ loopback-wildcard domains (CVE-2026-55096) and its IPv6 condition matches
414
+ IPv4-mapped obfuscation only (#732, #733).
415
+ - **`atr scan` scans an MCP `tools/list` document, one event per tool.** A file
416
+ holding `{"tools": [...]}`, or the JSON-RPC response `{"jsonrpc": "2.0",
417
+ "result": {"tools": [...]}}`, was wrapped as one event with no content,
418
+ skipped, and reported as "nothing was scanned", so the usual input for a
419
+ tool-poisoning check reached no rule. Each tool is now an `mcp_exchange`
420
+ event (`src/cli/mcp-tools-document.ts`): the content is the tool definition as
421
+ JSON, input schema and per-parameter descriptions included, and
422
+ `tool_name` / `tool_description` are set from the definition. A JSON array of
423
+ such responses is accepted too. A document that lists no tools fails with
424
+ "nothing was scanned" rather than reporting clean. `mcpServers` documents and
425
+ event files are read as before. Tests: `tests/cli-mcp-tools-list-scan.test.ts`.
426
+ - **A tools/list scan does not run the rules written for a tool being called.**
427
+ A definition defines a tool and calls none, but an `mcp_exchange` event admits
428
+ every rule, so call rules (ATR-2026-00099 and others, which read the called
429
+ tool's name and arguments) flagged every definition named `delete_*`,
430
+ `*_reset` or `exec*`. New engine option `toolDefinitions` (`ATREngineConfig`,
431
+ default false), which `atr scan` sets only for a tools/list document: a rule
432
+ with `agent_source.type: tool_call` then runs only if one of its conditions
433
+ reads `tool_description` (the test is per rule: such a rule runs with all its
434
+ conditions), or if its subject is the tool's identity (`TOOL_IDENTITY_RULES`
435
+ in `src/tool-definition-scope.ts`: ATR-2026-00060, impersonation); every other
436
+ rule runs as before. No rule file changes; hooks, adapters, config documents
437
+ and event files are unaffected. `atr scan --json` reports
438
+ `tool_definition_scope`, `rules_evaluated` and `rules_skipped_by_scope` (SARIF:
439
+ run properties) and the `tool_name` of each result, so a rule that was not run
440
+ does not read as a clean result. An event that also carries a `tools` array
441
+ is still scanned as an event. Documented in `docs/ARCHITECTURE.md` §3.2,
442
+ `docs/schema-spec.md` (agent_source.type) and `docs/rule-writing-guide.md`.
443
+ On 710 benign tool definitions from 45 official and vendor MCP servers,
444
+ flagged definitions go from 58 to 6 (hunt) and 57 to 5 (alert); enforce is 0
445
+ before and after, which says nothing here, since none of the rules involved is
446
+ stable. The true positives of the rules that read `tool_description` give the
447
+ same result with and without the scope: of 41, 39 fire (35 description texts,
448
+ and 4 ATR-2026-00441 code snippets through its content conditions) and 2 do
449
+ not (ATR-2026-00441 tool calls given as JSON with arguments, which are not
450
+ definitions). The 6 remaining hunt-lane false positives come from rules this
451
+ change does not touch: ATR-2026-01987, -00225, -00217, -00161 and -00569 (over-
452
+ broad conditions) and a form match of ATR-2026-00579 (experimental); see
453
+ `data/measurements/mcp-tools-list-benign/README.md`. The scope also costs
454
+ detection: a poisoned definition that only a call rule catches (for example
455
+ ATR-2026-00114) is missed in a tools/list scan; no malicious tool-definition
456
+ corpus measures this yet. Tests:
457
+ `tests/tool-definition-scope.test.ts`, `tests/cli-mcp-tools-list-scan.test.ts`.
458
+
459
+ - **On Claude Code, the guard scans what a tool returned on PostToolUse.**
460
+ Claude Code sends a tool's output at the top level of the PostToolUse hook
461
+ input, as `tool_response` (checked against the shipped 2.1.280 binary).
462
+ `src/hook-handler.ts` read an output only from `tool_input.output` or
463
+ `tool_input.response`, where Claude Code never puts one, so the event's content
464
+ was the tool's input, and a prompt injection in a command's output, a file or a
465
+ fetched page reached no rule. The event's content and `tool_response` are now
466
+ the text of the output: `src/hook-tool-output.ts` keeps every string in it, as
467
+ written, and leaves out binary data, what Claude Code keeps from the model,
468
+ and labels and strings the tool only repeats from its input. A tool that edits
469
+ a file returns the edit it made, so its event is still built from its input.
470
+ `tool_args` stays the JSON of the input, and `tool_input.output` and
471
+ `.response` are still read for a host that sends the output there, now as raw
472
+ text in content too. Measured through the hook in the default hunt lane, each
473
+ sample as a Bash command's output: the own true positives of the 746 rules the
474
+ engine runs on a tool response (`llm_io`, `mcp_exchange` and `context_window`)
475
+ match 3,852 of 3,890 times (0 before, and the same as a Read or MCP output);
476
+ 289 of PINT's 451 attacks match (0 before) and none of its 399 benign samples;
477
+ 1,246 of the 14,157 benign-corpus samples match a rule (0 before), most of them
478
+ through rules at maturity `test`. In the alert lane 1,183 match, in the enforce
479
+ lane none. Not covered: an output whose text exceeds 100,000 characters
480
+ taken together (`MAX_EVAL_LENGTH`); and some rules take time quadratic in a
481
+ long run of one character. `atr init` registers the guard on
482
+ PreToolUse only, so this reaches an installation that added it under
483
+ PostToolUse.
484
+
485
+ - **The guard also scans a failed tool call's output (PostToolUseFailure).**
486
+ When a tool call fails, a Bash command whose exit code Claude Code counts as
487
+ a failure among them (most non-zero codes; `grep`'s exit 1 is not one),
488
+ Claude Code sends `PostToolUseFailure` instead of `PostToolUse`. Its input
489
+ carries `tool_name`, `tool_input`, `tool_use_id`, `error`, `is_interrupt`
490
+ and `duration_ms`, and no `tool_response` (checked against the shipped
491
+ 2.1.280 binary). The guard answered it "Unknown hook type" and allowed it,
492
+ so ending a command with `; exit 1` kept its output from every rule. It now
493
+ builds the event from `error`, the failure as the model sees it: for a shell
494
+ command, its stderr and stdout after Claude Code's `Exit code N` line and
495
+ abort note, which are left out. A command that printed nothing leaves no
496
+ text, and its event is built from the input, as an output-less PostToolUse
497
+ is. It answers in the PostToolUse contract under the `PostToolUseFailure`
498
+ name, which Claude Code accepts, and with blocking on its `decision:
499
+ "block"` reaches the model as a blocking error. Measured through the hook in
500
+ the default hunt lane, each sample as a failed Bash command's error: the own
501
+ true positives of the 746 rules the engine runs on a tool response match
502
+ 3,852 of 3,890 times (0 before); 289 of PINT's 451 attacks match (0 before)
503
+ and none of its 399 benign samples; 1,246 of the 14,157 benign-corpus
504
+ samples match a rule (0 before). On every sample the verdict and the rules
505
+ matched are those of the same text as a Bash command's output on
506
+ PostToolUse. Claude Code cuts a failed call's error to about 10,000
507
+ characters; 1,117 of the benign samples are longer and were sent whole. To
508
+ use it, register the guard under `PostToolUseFailure` too.
509
+
510
+ - **Flywheel shadow rules can match.** `ShadowEvaluator` compiled each
511
+ condition with `new RegExp(value, 'i')`. A value that starts with `(?i)` is
512
+ a SyntaxError in JavaScript, the error was swallowed, and the condition
513
+ never matched. `RuleScaffolder` starts every pattern it writes with `(?i)`,
514
+ so no rule the flywheel generated could match in shadow mode or reach
515
+ promotion. Shadow conditions now go through the engine's own
516
+ `compileRegexCondition()` (leading flag group removed per SPEC §6.4, `u`
517
+ when needed, ReDoS gate) and `safeRegexTest()` (the engine's input-length
518
+ limit), and a condition whose value is not a string is skipped instead of
519
+ throwing. Shadow evaluation still differs from `ATREngine` in field
520
+ resolution, operators, `condition: all`, Unicode normalisation and
521
+ `agent_source` routing; those differences are unchanged.
522
+ - **Flywheel promotion needs a reviewed true positive, and promotes a rule
523
+ once.** `ShadowEvaluator.getPromotionCandidates()` divided confirmed false
524
+ positives by all matches, so a rule with no recorded feedback had an FP rate
525
+ of 0 and became a candidate after its first match. Once shadow rules could
526
+ match (above), `FlywheelManager.promoteReady()` would have handed such rules
527
+ to `onPromote` with status `stable`, and the same rule again on every call.
528
+ A rule is now a candidate only after at least `minReviewedMatches` (new
529
+ `FlywheelConfig` option, default 1) matches with recorded feedback including
530
+ a confirmed true positive; the FP rate is computed over reviewed matches; and
531
+ `promoteReady()` hands each rule to `onPromote` once. A promoted rule keeps
532
+ `maturity: draft`.
533
+ - **`RuleScaffolder` no longer writes a pattern that misses its own example
534
+ payload.** An attack indicator is chosen by a broad test and writes a
535
+ narrower pattern: "Ignore all previous instructions", "please ignore the
536
+ rules above" and "disregard everything you were told" all selected the
537
+ instruction-override indicator, whose pattern did not match them. An
538
+ indicator now counts only when its pattern matches the payload, and the
539
+ exact payload is used if nothing else does. Keyword patterns are now tried
540
+ only at line starts, `(?:^|[\n\r\u2028\u2029])(?=[^\n\r\u2028\u2029]*word)...`,
541
+ and exact patterns are the escaped payload without a surrounding `.*`. Both
542
+ match the same inputs as before, but neither is quadratic on a long
543
+ single-line input any more (about 7-12 seconds per pattern on 100,000
544
+ characters before, about a millisecond now). An empty or whitespace-only
545
+ example payload, which produced a pattern that matched everything, is now
546
+ rejected. Output of `atr scaffold` and the `atr_submit_proposal` MCP tool
547
+ changes accordingly.
548
+
549
+ - **SPEC §6.4 corrected: SPEC.md 1.0.1 [Compatible].** A `regex` pattern
550
+ condition matches case-insensitively whether or not it begins with `(?i)`,
551
+ and `m`, `s` and `x` in a leading flag group have no effect. The 1.0.0 text,
552
+ added on 2026-07-19 (#340), said an engine applies the flags named in that
553
+ group, which made a pattern without `(?i)` case-sensitive and gave `(?s)`
554
+ and `(?m)` their dotAll and multiline meaning. Compatible: an engine that
555
+ passes the conformance suite needs no change, because its true-positive
556
+ fixtures already require case-insensitive matching; an implementation built
557
+ to the 1.0.0 wording does. 1.0.1 also admits `x` in the group, which both
558
+ engines already removed, says when the `u` flag applies, advises against
559
+ modifier groups such as `(?-i:...)`, and adds a revision history (Appendix
560
+ D). Issue #698 has the evidence; erratum `e-2026-10-19-regex-dialect`
561
+ records the error. `tests/spec-regex-dialect.test.ts` and pyATR's
562
+ `TestRegexDialectSpec64` pin the statements. `scripts/generate-sigma.py` no
563
+ longer copies `m` and `s` from that group into exported Sigma patterns,
564
+ where a backend would honour them; of the three rules on main that carry
565
+ such a group, only ATR-2026-00450's `(?im)^...` condition matched
566
+ differently because of it. Other exports that still follow the 1.0.0
567
+ wording are listed in #698.
568
+ - **Rules with a `context_window` or `skill_*` `agent_source.type` run on the
569
+ events their data arrives in.** `src/engine.ts` maps an event type to one of
570
+ five source types and skipped every rule whose type differed, so a rule of an
571
+ extended type never ran on a typed event: on the `tool_call` and
572
+ `tool_response` events `src/hook-handler.ts` builds, ATR-2026-00070, -00075,
573
+ -01155, -01774, -00134, -00157 and -02405 never fired, while `atr test` mapped
574
+ their types to a base type and passed every case. The engine now does that
575
+ mapping itself (`RUNTIME_SOURCE_TYPE`): `context_window` runs where `llm_io`
576
+ rules run, the `skill_*` types where `tool_call` rules run, and a rule without
577
+ `agent_source` no longer throws a `TypeError` that aborted the whole
578
+ evaluation of a typed event. `memory_access` is withheld
579
+ (`WITHHELD_SOURCE_TYPES`): ATR-2026-00075 also matches standing instructions
580
+ in an ordinary chat turn ("From now on, always answer in French."), which the
581
+ Mastra processor and the OpenShell filter, both blocking by default, would
582
+ refuse; -00075 and -01155 still run only on an untyped event and in a skill
583
+ scan, and a new `memory_access` rule fails the test suite until the type is
584
+ mapped. `atr test` no longer maps anything, so what passes there is what the
585
+ engine runs, and it reports a withheld type's cases as unevaluable, with the
586
+ reason. The rule-latency gate profiles the five rules instead of listing them
587
+ as unexercised. The benign-corpus measurement does not change, because its
588
+ untyped shape already ran these rules; what changes is that their matches now
589
+ surface at runtime in the hunt lane, the default: ATR-2026-00070 (high)
590
+ matches 20 of the 14,157 benign samples. On the weekly benchmark suite three
591
+ hunt-lane recalls rise: garak 601 to 602 of 650 and garak-full 1,989 to 1,991
592
+ of 3,475 (one prompt, which -00070 catches on the tool-response channel and
593
+ garak-full holds twice), and atr-self-test 310 to 312 of 321 (-00070's own
594
+ true positives). No precision or false-positive figure moves (the new matches
595
+ fall on attack samples), and no enforce-lane figure moves (none of the five
596
+ rules is stable).
597
+
598
+ - **A tool event built from a test case always names a tool.**
599
+ `buildEventFromTestCase` (`src/testcase-event.ts`, behind `atr test`,
600
+ `scripts/validate-rule-testcases.ts` and the case-sensitivity gate) gave a case
601
+ that names no tool the `tool_name` `""`, meant to keep the case's text out of
602
+ `tool_name`. `src/engine.ts` reads a field only when it is truthy and, on a
603
+ `tool_call` event, then falls back to the event's content, so a `tool_name`
604
+ condition read the payload as the tool's name and a rule keyed on the tool
605
+ could pass its cases without naming one. A tool event now carries
606
+ `atr-test-tool` (`UNNAMED_TOOL`) when the case names no tool, as
607
+ `src/hook-handler.ts` fills it from the host's tool call, and the
608
+ case-sensitivity gate leaves that name out of the text it checks a condition
609
+ against. No rule on main relied on the old behaviour: every true positive and
610
+ true negative of every rule tests the same.
611
+
612
+ - **A test case's own `agent_message` is no longer replaced by its content.**
613
+ `buildEventFromTestCase` set the field from the case and then copied the
614
+ event's content over it, so a case that names an `agent_message` beside its
615
+ input was run on the input there. No case on main names one.
616
+
617
+ - **A `medium` confidence label no longer depends on float rounding to stay
618
+ out of `deny`.** A pattern match scored `0.7 + 0.1 * coverage`, and a
619
+ `high`-severity match denies at `>= 0.8`. At full coverage that sum is
620
+ `0.7999999999999999` in IEEE doubles, so the verdict was `ask` only because
621
+ of the rounding direction; the same quantity as an integer percentage is
622
+ `0.8`. Label-derived confidence is now computed in one place
623
+ (`src/rule-confidence.ts`) on a basis-point scale, and a label below the
624
+ deny threshold is capped one basis point under it. No verdict or
625
+ auto-response decision changes. The raw `confidence` of a `medium` match at
626
+ full coverage is `0.7999` instead of `0.7999999999999999`, and other
627
+ partial-coverage values lose trailing float noise (`0.9500000000000001`
628
+ becomes `0.95`). One displayed percentage moves as a result: a
629
+ named-condition rule labelled `medium` at 3/4 coverage shows 78% (`0.775`)
630
+ where the noise used to show 77%. No shipped rule reaches that cell.
631
+
632
+ - **A semantic rule without a pattern fallback is no longer recorded as
633
+ measured clean.** The benign-corpus measurement
634
+ (`scripts/gate-promotion-fp.ts`, and `scripts/remeasure-benign-fp.ts`
635
+ through it) drives the synchronous engine, which returns null for
636
+ `method: semantic` unless `detection.semantic.fallback_method` is exactly
637
+ `pattern`; the judge runs only in `evaluateAsync()`. `fieldCoverage()`
638
+ named trace, behavioral and signature as methods it cannot run, but not
639
+ this one, and it skipped a rule with no condition field, such as a pattern
640
+ fallback with nothing in it. Either was written down as `fp_count: 0,
641
+ partial_measurement: false`, which `scripts/gate-action-eligibility.ts`
642
+ graded `terminate` at maturity `stable` and `degrade` at `test`. Both are
643
+ now recorded partial and capped at `observe`. No rule on main is in either
644
+ state, so the committed evidence is unchanged. The skill-path check uses
645
+ the same predicate; it used to read `fallback_method: Pattern` as
646
+ `pattern`, which the engine does not, and it exempted a
647
+ `scan_target: both` rule with no conditions.
648
+ - **NIST-related statements corrected across the docs and the website.**
649
+ Pages described a NIST CAISI dialogue and position paper that did not
650
+ happen, an RFI response ATR never filed, an OSCAL "Path 1" acceptance,
651
+ and a quote NIST never published; they listed NIST among adopters and
652
+ in a trust bar, and showed "100%" or "4/4 functions" for an AI RMF
653
+ mapping that covers a subset of subcategories. usnistgov/oscal-content#338
654
+ is described as what it is: open, reworked against NIST's AI RMF catalog,
655
+ awaiting NIST review. The website changelog page now shows the notes on
656
+ historical entries, which it used to drop.
657
+ - **Integration status, provenance, evasion and latency claims corrected.**
658
+ The red-team page listed seven proposals as under review that their ATR
659
+ author had withdrawn unmerged (NVIDIA/garak#1676 on 2026-08-04; HarmBench,
660
+ AgentDojo, JailbreakBench, Counterfit, PromptInject and Damn Vulnerable MCP
661
+ Server on 2026-10-14 and 15, six of them never answered). They are removed,
662
+ along with the lapsed "On deck" schedule, and the garak, sovereign-AI,
663
+ responsible-use, integrate and STANDARDIZATION-STATUS surfaces no longer
664
+ call the garak integration or any OWASP PR open. The garak page and the
665
+ contributor docs (QUALITY-GATE.md, TSC-ONBOARDING.md, REVIEWER-GUIDE.md,
666
+ RULE-PRODUCTION.md, the issue-to-proposal PR text) no longer describe
667
+ auto-merge, which has been off since 2026-06-03. The red-team and governance
668
+ pages no longer promise a `metadata_provenance.discovered_by` field no rule
669
+ has, and the home, red-team, about and quality-standard pages no longer
670
+ credit daily auto-crystallization for growth that came from CVE sweeps,
671
+ benchmark-miss mining and contributors. The sovereign-AI page no longer says
672
+ ATR detects intent and cannot be evaded; LIMITATIONS.md and the
673
+ responsible-use, integrate and research pages no longer say every rule
674
+ carries evasion tests or that there are 64; LIMITATIONS.md and
675
+ THREAT-MODEL.md now agree that the optional judge and embedding modules ship
676
+ but are off by default. "Sub-millisecond", "< 5ms", "5ms pattern match" and
677
+ "Done in 47ms" are replaced by how evaluation cost scales; the OpenShell
678
+ example is marked as a sketch of a proposal OpenShell closed, and its
679
+ timeouts now exceed a one-shot cold start. CITATION.cff no longer calls the
680
+ rules hand-authored and says what the safety gate checks for added and
681
+ changed rules; the OWASP PR text drops its 76-rule count and notes the PR
682
+ was withdrawn. Errata `e-2026-10-16-integration-status`,
683
+ `e-2026-10-16-evasion`, `e-2026-10-16-latency` and `e-2026-10-16-provenance`
684
+ record the site changes.
685
+ - **A rule the engine does not evaluate no longer gets a false-positive
686
+ record.** `scripts/gate-promotion-fp.ts --emit-measurement` wrote
687
+ `fp_count: 0` for draft and deprecated rules, which the engine skips, so
688
+ nothing was counted for them. `detection_fingerprint` does not cover
689
+ `status`, so a one-line revival handed the rule that 0, and
690
+ `scripts/gate-action-eligibility.ts` trusted it: at maturity `test`, enough
691
+ for `block_tool`. Seven records in `data/benign-fp-measurement.json` were of
692
+ this kind. The measurement now asks the engine (`ATREngine.evaluates`) and
693
+ lists such a rule, or an id two loaded files share, under NOT MEASURED
694
+ instead. Without `--filter-mode` that fails the gate: promoting a rule the
695
+ engine does not evaluate no longer passes as clean.
696
+ `scripts/remeasure-benign-fp.ts` leaves the same rules out, and measures in
697
+ full, rather than extends, a record for a rule that was inert at its base
698
+ commit. `scripts/promote-to-stable.ts` no longer counts a draft rule at
699
+ maturity `test` as FP-clean.
700
+ - **The action gate refuses evidence that holds a record for a rule that is
701
+ not live** (inert, or its file gone): exit 2, as for evidence taken on
702
+ another corpus. A record carries no status, so the gate cannot tell a count
703
+ taken while the rule was live from a 0 nothing measured, and left in place
704
+ either is trusted the day the rule is revived or restored. Deprecating or
705
+ deleting a rule now means re-emitting the evidence in the same change. The
706
+ gate also reads a status that is not a string (`status: [draft]`) as live,
707
+ as the engine does; it used to skip such a rule while the engine ran it.
708
+ - **Two rule files with the same name are both measured.** The measurement
709
+ copied rule files into its scratch directory by file name, so of two
710
+ same-named files in different directories only the last was read, and the
711
+ other was reported clean.
712
+ - **`scripts/promote-to-stable.ts` no longer promotes on a 0 it did not
713
+ measure.** A `maturity: test` rule whose conditions the benign corpus cannot
714
+ fully exercise (a field no sample fills, such as `tool_name`, or a method the
715
+ synchronous engine cannot run, such as `trace`) scored 0 false positives
716
+ without being read, and `--write` promoted it to `stable`. Such a rule is now
717
+ reported as not promotable; one that did false-positive stays with the rules
718
+ that have false positives. No `maturity: test` rule on main is affected today.
719
+ - **verify-adopters.yml no longer fails when other commits land on main during
720
+ a run, and its failures get noticed.** The job commits the refreshed
721
+ `data/adopters-verification.json` straight to main with a plain push, which
722
+ was rejected whenever another commit landed first, failing the run with
723
+ nothing wrong in ADOPTERS.md (2026-10-07, 2026-10-15). It now rebases its
724
+ one-file commit onto the moved main and retries, at most three times; runs
725
+ one verification at a time per branch; and checks out the branch tip rather
726
+ than the triggering commit, so a run that waited behind another starts from
727
+ the data the other pushed instead of conflicting with it (the 2026-10-07
728
+ shape: two adopter merges, each run writing the same file). A conflict with
729
+ a hand edit of the generated file still fails the run. Four red runs in a
730
+ row (2026-10-12 to 10-15) went unnoticed for days; a failure on main now
731
+ opens one tracking issue, or rewrites the open one, and the next green run
732
+ closes it. The issue lists drift only from a verification that finished.
733
+
7
734
  ## [4.1.5] - 2026-10-10
8
735
 
9
736
  4.1.4 was staged on npm and removed by publish-time scanning before it could be
@@ -788,6 +1515,14 @@ No rule content changed and no threshold was loosened in any of the above.
788
1515
  ### Metrics
789
1516
 
790
1517
  - HackAPrompt (4,780 samples): recall 28.6% → 66.2% (+37.6pp), 100% precision maintained.
1518
+ - Erratum (2026-10-19): re-run on the same 4,780 samples at the sprint commits,
1519
+ recall was 28.6% (1,368, at `3a6145b7`) → 65.0% (3,108, at `e9efefdd`) and
1520
+ 66.0% (3,154, at `92c0d0fe`); 66.2% could not be reproduced at either. The
1521
+ 28.6% starting point was itself in-sample (ATR-2026-00442..00447 were mined
1522
+ from this subset). "100% precision" was not a measurement: the corpus has
1523
+ no benign samples, so precision is undefined on it. Procedure and figures:
1524
+ `data/hackaprompt/METHOD.md`; current measurement, with the ablation
1525
+ reference: `data/measurements/hackaprompt/`.
791
1526
  - garak (3,475 prompts): ATR-core families ~80%+; per-family breakdown: latentinjection 34.4%, sysprompt_extraction 67.9%, dan 90.2%.
792
1527
  - PINT (850 samples): 0.25% FP maintained, 0 FP regression.
793
1528
  - SKILL.md (341 samples): 100% precision, 0% FP, 0 FP regression.
@@ -856,6 +1591,13 @@ No rule content changed and no threshold was loosened in any of the above.
856
1591
 
857
1592
  ## [2.1.0] - 2026-05-09
858
1593
 
1594
+ > **Historical entry — the AI RMF line below overstated coverage.**
1595
+ > "100% coverage of all 4 AI RMF functions" described the community OSCAL
1596
+ > catalog, which encodes all 72 AI RMF subcategories as controls. ATR's
1597
+ > rules map to a subset of those subcategories. The catalog is a community
1598
+ > contribution and is not endorsed by NIST. Do not quote the line as a
1599
+ > coverage claim. (Note added 2026-10-16.)
1600
+
859
1601
  ### Added
860
1602
 
861
1603
  - **NIST AI RMF mapping v0.2** — 100% coverage of all 4 AI RMF functions (Govern / Map / Measure / Manage), 72 controls + 31 cross-reference links + worked example profile. Schema-valid OSCAL catalog published under `Agent-Threat-Rule/ai-rmf-oscal-catalog` (PR #46).
@@ -867,6 +1609,14 @@ No rule content changed and no threshold was loosened in any of the above.
867
1609
 
868
1610
  ## [2.0.11] - 2026-04-21
869
1611
 
1612
+ > **Historical entry — do not quote the garak or SKILL.md figures below.**
1613
+ > The 97.1% in-the-wild recall was measured on a garak corpus that many of
1614
+ > this release's new rules were written from, so it is not an out-of-sample
1615
+ > result. The SKILL.md precision and FP figures are in-sample: the benign
1616
+ > set is the one the rules were tuned against. Current figures, reported
1617
+ > per lane and with their closed-book share, are in README §8 Evaluation.
1618
+ > (Note added 2026-10-16.)
1619
+
870
1620
  ### Added
871
1621
 
872
1622
  - **193 new rules** covering the full NVIDIA garak probe corpus (ATR-00300 ~ ATR-00414), bringing total to **311 rules**
@@ -966,7 +1716,7 @@ No rule content changed and no threshold was loosened in any of the above.
966
1716
  - Wild scan (96K): 1.35% flag rate, 751 confirmed malware
967
1717
  - OWASP Agentic Top 10: 10/10 coverage
968
1718
  - MITRE ATLAS: 100/113 rules mapped
969
- - SAFE-MCP: 91.8% technique coverage
1719
+ - SAFE-MCP: technique coverage figure (withdrawn 2026-10: it was not backed by rule-level data; see docs/SAFE-MCP-MAPPING.md)
970
1720
  - Avg scan latency: 14ms per file
971
1721
 
972
1722
  ### Ecosystem