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