agent-threat-rules 0.4.0 → 1.0.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 (291) hide show
  1. package/README.md +161 -52
  2. package/package.json +3 -1
  3. package/rules/agent-manipulation/{ATR-2026-030-cross-agent-attack.yaml → ATR-2026-00030-cross-agent-attack.yaml} +3 -1
  4. package/rules/agent-manipulation/{ATR-2026-032-goal-hijacking.yaml → ATR-2026-00032-goal-hijacking.yaml} +3 -1
  5. package/rules/agent-manipulation/{ATR-2026-074-cross-agent-privilege-escalation.yaml → ATR-2026-00074-cross-agent-privilege-escalation.yaml} +3 -1
  6. package/rules/agent-manipulation/{ATR-2026-076-inter-agent-message-spoofing.yaml → ATR-2026-00076-inter-agent-message-spoofing.yaml} +3 -1
  7. package/rules/agent-manipulation/{ATR-2026-077-human-trust-exploitation.yaml → ATR-2026-00077-human-trust-exploitation.yaml} +3 -1
  8. package/rules/agent-manipulation/{ATR-2026-108-consensus-sybil-attack.yaml → ATR-2026-00108-consensus-sybil-attack.yaml} +3 -1
  9. package/rules/agent-manipulation/{ATR-2026-116-a2a-message-validation.yaml → ATR-2026-00116-a2a-message-validation.yaml} +4 -2
  10. package/rules/agent-manipulation/{ATR-2026-117-agent-identity-spoofing.yaml → ATR-2026-00117-agent-identity-spoofing.yaml} +4 -2
  11. package/rules/agent-manipulation/{ATR-2026-118-approval-fatigue.yaml → ATR-2026-00118-approval-fatigue.yaml} +3 -1
  12. package/rules/agent-manipulation/{ATR-2026-119-social-engineering-via-agent.yaml → ATR-2026-00119-social-engineering-via-agent.yaml} +3 -1
  13. package/rules/agent-manipulation/ATR-2026-00132-casual-authority-escalation.yaml +105 -0
  14. package/rules/agent-manipulation/ATR-2026-00139-casual-authority-redirect.yaml +53 -0
  15. package/rules/context-exfiltration/{ATR-2026-020-system-prompt-leak.yaml → ATR-2026-00020-system-prompt-leak.yaml} +3 -1
  16. package/rules/context-exfiltration/{ATR-2026-021-api-key-exposure.yaml → ATR-2026-00021-api-key-exposure.yaml} +3 -1
  17. package/rules/context-exfiltration/{ATR-2026-075-agent-memory-manipulation.yaml → ATR-2026-00075-agent-memory-manipulation.yaml} +3 -1
  18. package/rules/context-exfiltration/{ATR-2026-102-disguised-analytics-exfiltration.yaml → ATR-2026-00102-disguised-analytics-exfiltration.yaml} +3 -1
  19. package/rules/context-exfiltration/{ATR-2026-113-credential-theft.yaml → ATR-2026-00113-credential-theft.yaml} +3 -1
  20. package/rules/context-exfiltration/{ATR-2026-114-oauth-token-abuse.yaml → ATR-2026-00114-oauth-token-abuse.yaml} +3 -1
  21. package/rules/context-exfiltration/{ATR-2026-115-env-var-harvesting.yaml → ATR-2026-00115-env-var-harvesting.yaml} +3 -1
  22. package/rules/context-exfiltration/ATR-2026-00136-tool-response-data-piggyback.yaml +100 -0
  23. package/rules/context-exfiltration/ATR-2026-00141-example-format-key-leak.yaml +52 -0
  24. package/rules/context-exfiltration/ATR-2026-00142-piggyback-transition-words.yaml +55 -0
  25. package/rules/context-exfiltration/ATR-2026-00145-obfuscated-key-disclosure.yaml +49 -0
  26. package/rules/context-exfiltration/ATR-2026-00146-env-var-existence-probe.yaml +49 -0
  27. package/rules/data-poisoning/{ATR-2026-070-data-poisoning.yaml → ATR-2026-00070-data-poisoning.yaml} +3 -1
  28. package/rules/excessive-autonomy/{ATR-2026-050-runaway-agent-loop.yaml → ATR-2026-00050-runaway-agent-loop.yaml} +3 -1
  29. package/rules/excessive-autonomy/{ATR-2026-051-resource-exhaustion.yaml → ATR-2026-00051-resource-exhaustion.yaml} +3 -1
  30. package/rules/excessive-autonomy/{ATR-2026-052-cascading-failure.yaml → ATR-2026-00052-cascading-failure.yaml} +3 -1
  31. package/rules/excessive-autonomy/{ATR-2026-098-unauthorized-financial-action.yaml → ATR-2026-00098-unauthorized-financial-action.yaml} +3 -1
  32. package/rules/excessive-autonomy/{ATR-2026-099-high-risk-tool-gate.yaml → ATR-2026-00099-high-risk-tool-gate.yaml} +3 -1
  33. package/rules/model-security/{ATR-2026-072-model-behavior-extraction.yaml → ATR-2026-00072-model-behavior-extraction.yaml} +3 -1
  34. package/rules/model-security/{ATR-2026-073-malicious-finetuning-data.yaml → ATR-2026-00073-malicious-finetuning-data.yaml} +3 -1
  35. package/rules/privilege-escalation/{ATR-2026-040-privilege-escalation.yaml → ATR-2026-00040-privilege-escalation.yaml} +3 -1
  36. package/rules/privilege-escalation/{ATR-2026-041-scope-creep.yaml → ATR-2026-00041-scope-creep.yaml} +3 -1
  37. package/rules/privilege-escalation/{ATR-2026-107-delayed-execution-bypass.yaml → ATR-2026-00107-delayed-execution-bypass.yaml} +3 -1
  38. package/rules/privilege-escalation/{ATR-2026-110-eval-injection.yaml → ATR-2026-00110-eval-injection.yaml} +3 -1
  39. package/rules/privilege-escalation/{ATR-2026-111-shell-escape.yaml → ATR-2026-00111-shell-escape.yaml} +5 -3
  40. package/rules/privilege-escalation/{ATR-2026-112-dynamic-import-exploitation.yaml → ATR-2026-00112-dynamic-import-exploitation.yaml} +3 -1
  41. package/rules/privilege-escalation/ATR-2026-00143-casual-privilege-escalation.yaml +53 -0
  42. package/rules/privilege-escalation/ATR-2026-00144-rationalized-safety-bypass.yaml +49 -0
  43. package/rules/prompt-injection/{ATR-2026-001-direct-prompt-injection.yaml → ATR-2026-00001-direct-prompt-injection.yaml} +3 -1
  44. package/rules/prompt-injection/{ATR-2026-002-indirect-prompt-injection.yaml → ATR-2026-00002-indirect-prompt-injection.yaml} +3 -1
  45. package/rules/prompt-injection/{ATR-2026-003-jailbreak-attempt.yaml → ATR-2026-00003-jailbreak-attempt.yaml} +3 -1
  46. package/rules/prompt-injection/{ATR-2026-004-system-prompt-override.yaml → ATR-2026-00004-system-prompt-override.yaml} +3 -1
  47. package/rules/prompt-injection/{ATR-2026-005-multi-turn-injection.yaml → ATR-2026-00005-multi-turn-injection.yaml} +3 -1
  48. package/rules/prompt-injection/{ATR-2026-080-encoding-evasion.yaml → ATR-2026-00080-encoding-evasion.yaml} +3 -1
  49. package/rules/prompt-injection/{ATR-2026-081-semantic-multi-turn.yaml → ATR-2026-00081-semantic-multi-turn.yaml} +3 -1
  50. package/rules/prompt-injection/{ATR-2026-082-fingerprint-evasion.yaml → ATR-2026-00082-fingerprint-evasion.yaml} +3 -1
  51. package/rules/prompt-injection/{ATR-2026-083-indirect-tool-injection.yaml → ATR-2026-00083-indirect-tool-injection.yaml} +3 -1
  52. package/rules/prompt-injection/{ATR-2026-084-structured-data-injection.yaml → ATR-2026-00084-structured-data-injection.yaml} +3 -1
  53. package/rules/prompt-injection/{ATR-2026-085-audit-evasion.yaml → ATR-2026-00085-audit-evasion.yaml} +3 -1
  54. package/rules/prompt-injection/{ATR-2026-086-visual-spoofing.yaml → ATR-2026-00086-visual-spoofing.yaml} +3 -1
  55. package/rules/prompt-injection/{ATR-2026-087-rule-probing.yaml → ATR-2026-00087-rule-probing.yaml} +3 -1
  56. package/rules/prompt-injection/{ATR-2026-088-adaptive-countermeasure.yaml → ATR-2026-00088-adaptive-countermeasure.yaml} +3 -1
  57. package/rules/prompt-injection/{ATR-2026-089-polymorphic-skill.yaml → ATR-2026-00089-polymorphic-skill.yaml} +3 -1
  58. package/rules/prompt-injection/{ATR-2026-090-threat-intel-exfil.yaml → ATR-2026-00090-threat-intel-exfil.yaml} +3 -1
  59. package/rules/prompt-injection/{ATR-2026-091-nested-payload.yaml → ATR-2026-00091-nested-payload.yaml} +3 -1
  60. package/rules/prompt-injection/{ATR-2026-092-consensus-poisoning.yaml → ATR-2026-00092-consensus-poisoning.yaml} +3 -1
  61. package/rules/prompt-injection/{ATR-2026-093-gradual-escalation.yaml → ATR-2026-00093-gradual-escalation.yaml} +3 -1
  62. package/rules/prompt-injection/{ATR-2026-094-audit-bypass.yaml → ATR-2026-00094-audit-bypass.yaml} +3 -1
  63. package/rules/prompt-injection/{ATR-2026-097-cjk-injection-patterns.yaml → ATR-2026-00097-cjk-injection-patterns.yaml} +3 -1
  64. package/rules/prompt-injection/{ATR-2026-104-persona-hijacking.yaml → ATR-2026-00104-persona-hijacking.yaml} +3 -1
  65. package/rules/prompt-injection/ATR-2026-00130-indirect-authority-claim.yaml +103 -0
  66. package/rules/prompt-injection/ATR-2026-00131-fictional-academic-framing.yaml +99 -0
  67. package/rules/prompt-injection/ATR-2026-00133-paraphrase-injection.yaml +117 -0
  68. package/rules/prompt-injection/ATR-2026-00137-authority-claim-injection.yaml +52 -0
  69. package/rules/prompt-injection/ATR-2026-00138-fictional-framing-bypass.yaml +51 -0
  70. package/rules/prompt-injection/ATR-2026-00140-indirect-reference-reversal.yaml +52 -0
  71. package/rules/prompt-injection/ATR-2026-00148-language-switch-injection.yaml +71 -0
  72. package/rules/skill-compromise/{ATR-2026-060-skill-impersonation.yaml → ATR-2026-00060-skill-impersonation.yaml} +3 -1
  73. package/rules/skill-compromise/{ATR-2026-061-description-behavior-mismatch.yaml → ATR-2026-00061-description-behavior-mismatch.yaml} +3 -1
  74. package/rules/skill-compromise/{ATR-2026-062-hidden-capability.yaml → ATR-2026-00062-hidden-capability.yaml} +3 -1
  75. package/rules/skill-compromise/{ATR-2026-063-skill-chain-attack.yaml → ATR-2026-00063-skill-chain-attack.yaml} +3 -1
  76. package/rules/skill-compromise/{ATR-2026-064-over-permissioned-skill.yaml → ATR-2026-00064-over-permissioned-skill.yaml} +3 -1
  77. package/rules/skill-compromise/{ATR-2026-065-skill-update-attack.yaml → ATR-2026-00065-skill-update-attack.yaml} +3 -1
  78. package/rules/skill-compromise/{ATR-2026-066-parameter-injection.yaml → ATR-2026-00066-parameter-injection.yaml} +3 -1
  79. package/rules/skill-compromise/ATR-2026-00120-skill-instruction-injection.yaml +121 -0
  80. package/rules/skill-compromise/ATR-2026-00121-skill-dangerous-script.yaml +165 -0
  81. package/rules/skill-compromise/ATR-2026-00122-skill-weaponized-instruction.yaml +114 -0
  82. package/rules/skill-compromise/ATR-2026-00123-skill-overreach-permissions.yaml +118 -0
  83. package/rules/skill-compromise/ATR-2026-00124-skill-name-squatting.yaml +98 -0
  84. package/rules/skill-compromise/ATR-2026-00125-context-poisoning-compaction.yaml +93 -0
  85. package/rules/skill-compromise/ATR-2026-00126-skill-rug-pull-setup.yaml +99 -0
  86. package/rules/skill-compromise/ATR-2026-00127-subcommand-overflow.yaml +74 -0
  87. package/rules/skill-compromise/ATR-2026-00128-html-comment-hidden-payload.yaml +79 -0
  88. package/rules/skill-compromise/ATR-2026-00129-unicode-smuggling.yaml +73 -0
  89. package/rules/skill-compromise/ATR-2026-00134-fork-claim-impersonation.yaml +93 -0
  90. package/rules/skill-compromise/ATR-2026-00135-exfil-url-in-instructions.yaml +82 -0
  91. package/rules/skill-compromise/ATR-2026-00147-fork-impersonation.yaml +48 -0
  92. package/rules/tool-poisoning/{ATR-2026-010-mcp-malicious-response.yaml → ATR-2026-00010-mcp-malicious-response.yaml} +3 -1
  93. package/rules/tool-poisoning/{ATR-2026-011-tool-output-injection.yaml → ATR-2026-00011-tool-output-injection.yaml} +3 -1
  94. package/rules/tool-poisoning/{ATR-2026-012-unauthorized-tool-call.yaml → ATR-2026-00012-unauthorized-tool-call.yaml} +3 -1
  95. package/rules/tool-poisoning/{ATR-2026-013-tool-ssrf.yaml → ATR-2026-00013-tool-ssrf.yaml} +3 -1
  96. package/rules/tool-poisoning/{ATR-2026-095-supply-chain-poisoning.yaml → ATR-2026-00095-supply-chain-poisoning.yaml} +3 -1
  97. package/rules/tool-poisoning/{ATR-2026-096-registry-poisoning.yaml → ATR-2026-00096-registry-poisoning.yaml} +3 -1
  98. package/rules/tool-poisoning/{ATR-2026-100-consent-bypass-instruction.yaml → ATR-2026-00100-consent-bypass-instruction.yaml} +3 -1
  99. package/rules/tool-poisoning/{ATR-2026-101-trust-escalation-override.yaml → ATR-2026-00101-trust-escalation-override.yaml} +3 -1
  100. package/rules/tool-poisoning/{ATR-2026-103-hidden-safety-bypass-instruction.yaml → ATR-2026-00103-hidden-safety-bypass-instruction.yaml} +3 -1
  101. package/rules/tool-poisoning/{ATR-2026-105-silent-action-concealment.yaml → ATR-2026-00105-silent-action-concealment.yaml} +3 -1
  102. package/rules/tool-poisoning/{ATR-2026-106-schema-description-contradiction.yaml → ATR-2026-00106-schema-description-contradiction.yaml} +3 -1
  103. package/spec/atr-schema.yaml +32 -3
  104. package/dist/action-executor.d.ts +0 -44
  105. package/dist/action-executor.d.ts.map +0 -1
  106. package/dist/action-executor.js +0 -130
  107. package/dist/action-executor.js.map +0 -1
  108. package/dist/adapters/default-adapter.d.ts +0 -24
  109. package/dist/adapters/default-adapter.d.ts.map +0 -1
  110. package/dist/adapters/default-adapter.js +0 -51
  111. package/dist/adapters/default-adapter.js.map +0 -1
  112. package/dist/adapters/stdio-adapter.d.ts +0 -30
  113. package/dist/adapters/stdio-adapter.d.ts.map +0 -1
  114. package/dist/adapters/stdio-adapter.js +0 -128
  115. package/dist/adapters/stdio-adapter.js.map +0 -1
  116. package/dist/badge.d.ts +0 -42
  117. package/dist/badge.d.ts.map +0 -1
  118. package/dist/badge.js +0 -158
  119. package/dist/badge.js.map +0 -1
  120. package/dist/capability-extractor.d.ts +0 -35
  121. package/dist/capability-extractor.d.ts.map +0 -1
  122. package/dist/capability-extractor.js +0 -91
  123. package/dist/capability-extractor.js.map +0 -1
  124. package/dist/cli.d.ts +0 -12
  125. package/dist/cli.d.ts.map +0 -1
  126. package/dist/cli.js +0 -892
  127. package/dist/cli.js.map +0 -1
  128. package/dist/converters/elastic.d.ts +0 -36
  129. package/dist/converters/elastic.d.ts.map +0 -1
  130. package/dist/converters/elastic.js +0 -125
  131. package/dist/converters/elastic.js.map +0 -1
  132. package/dist/converters/index.d.ts +0 -28
  133. package/dist/converters/index.d.ts.map +0 -1
  134. package/dist/converters/index.js +0 -36
  135. package/dist/converters/index.js.map +0 -1
  136. package/dist/converters/splunk.d.ts +0 -19
  137. package/dist/converters/splunk.d.ts.map +0 -1
  138. package/dist/converters/splunk.js +0 -148
  139. package/dist/converters/splunk.js.map +0 -1
  140. package/dist/coverage-analyzer.d.ts +0 -43
  141. package/dist/coverage-analyzer.d.ts.map +0 -1
  142. package/dist/coverage-analyzer.js +0 -329
  143. package/dist/coverage-analyzer.js.map +0 -1
  144. package/dist/embedding/build-corpus.d.ts +0 -15
  145. package/dist/embedding/build-corpus.d.ts.map +0 -1
  146. package/dist/embedding/build-corpus.js +0 -105
  147. package/dist/embedding/build-corpus.js.map +0 -1
  148. package/dist/embedding/model-loader.d.ts +0 -41
  149. package/dist/embedding/model-loader.d.ts.map +0 -1
  150. package/dist/embedding/model-loader.js +0 -90
  151. package/dist/embedding/model-loader.js.map +0 -1
  152. package/dist/embedding/vector-store.d.ts +0 -41
  153. package/dist/embedding/vector-store.d.ts.map +0 -1
  154. package/dist/embedding/vector-store.js +0 -70
  155. package/dist/embedding/vector-store.js.map +0 -1
  156. package/dist/engine.d.ts +0 -163
  157. package/dist/engine.d.ts.map +0 -1
  158. package/dist/engine.js +0 -869
  159. package/dist/engine.js.map +0 -1
  160. package/dist/eval/corpus.d.ts +0 -42
  161. package/dist/eval/corpus.d.ts.map +0 -1
  162. package/dist/eval/corpus.js +0 -427
  163. package/dist/eval/corpus.js.map +0 -1
  164. package/dist/eval/eval-harness.d.ts +0 -44
  165. package/dist/eval/eval-harness.d.ts.map +0 -1
  166. package/dist/eval/eval-harness.js +0 -296
  167. package/dist/eval/eval-harness.js.map +0 -1
  168. package/dist/eval/index.d.ts +0 -13
  169. package/dist/eval/index.d.ts.map +0 -1
  170. package/dist/eval/index.js +0 -9
  171. package/dist/eval/index.js.map +0 -1
  172. package/dist/eval/metrics.d.ts +0 -74
  173. package/dist/eval/metrics.d.ts.map +0 -1
  174. package/dist/eval/metrics.js +0 -108
  175. package/dist/eval/metrics.js.map +0 -1
  176. package/dist/eval/pint-corpus.d.ts +0 -34
  177. package/dist/eval/pint-corpus.d.ts.map +0 -1
  178. package/dist/eval/pint-corpus.js +0 -109
  179. package/dist/eval/pint-corpus.js.map +0 -1
  180. package/dist/eval/rule-corpus.d.ts +0 -9
  181. package/dist/eval/rule-corpus.d.ts.map +0 -1
  182. package/dist/eval/rule-corpus.js +0 -4780
  183. package/dist/eval/rule-corpus.js.map +0 -1
  184. package/dist/eval/rule-metrics.d.ts +0 -34
  185. package/dist/eval/rule-metrics.d.ts.map +0 -1
  186. package/dist/eval/rule-metrics.js +0 -92
  187. package/dist/eval/rule-metrics.js.map +0 -1
  188. package/dist/eval/run-eval.d.ts +0 -7
  189. package/dist/eval/run-eval.d.ts.map +0 -1
  190. package/dist/eval/run-eval.js +0 -11
  191. package/dist/eval/run-eval.js.map +0 -1
  192. package/dist/eval/run-pint-benchmark.d.ts +0 -18
  193. package/dist/eval/run-pint-benchmark.d.ts.map +0 -1
  194. package/dist/eval/run-pint-benchmark.js +0 -159
  195. package/dist/eval/run-pint-benchmark.js.map +0 -1
  196. package/dist/flywheel.d.ts +0 -54
  197. package/dist/flywheel.d.ts.map +0 -1
  198. package/dist/flywheel.js +0 -121
  199. package/dist/flywheel.js.map +0 -1
  200. package/dist/hook-handler.d.ts +0 -61
  201. package/dist/hook-handler.d.ts.map +0 -1
  202. package/dist/hook-handler.js +0 -178
  203. package/dist/hook-handler.js.map +0 -1
  204. package/dist/index.d.ts +0 -62
  205. package/dist/index.d.ts.map +0 -1
  206. package/dist/index.js +0 -54
  207. package/dist/index.js.map +0 -1
  208. package/dist/layer-integration.d.ts +0 -55
  209. package/dist/layer-integration.d.ts.map +0 -1
  210. package/dist/layer-integration.js +0 -185
  211. package/dist/layer-integration.js.map +0 -1
  212. package/dist/loader.d.ts +0 -21
  213. package/dist/loader.d.ts.map +0 -1
  214. package/dist/loader.js +0 -124
  215. package/dist/loader.js.map +0 -1
  216. package/dist/mcp-server.d.ts +0 -13
  217. package/dist/mcp-server.d.ts.map +0 -1
  218. package/dist/mcp-server.js +0 -220
  219. package/dist/mcp-server.js.map +0 -1
  220. package/dist/mcp-tools/coverage-gaps.d.ts +0 -13
  221. package/dist/mcp-tools/coverage-gaps.d.ts.map +0 -1
  222. package/dist/mcp-tools/coverage-gaps.js +0 -55
  223. package/dist/mcp-tools/coverage-gaps.js.map +0 -1
  224. package/dist/mcp-tools/list-rules.d.ts +0 -17
  225. package/dist/mcp-tools/list-rules.d.ts.map +0 -1
  226. package/dist/mcp-tools/list-rules.js +0 -45
  227. package/dist/mcp-tools/list-rules.js.map +0 -1
  228. package/dist/mcp-tools/scan.d.ts +0 -24
  229. package/dist/mcp-tools/scan.d.ts.map +0 -1
  230. package/dist/mcp-tools/scan.js +0 -94
  231. package/dist/mcp-tools/scan.js.map +0 -1
  232. package/dist/mcp-tools/submit-proposal.d.ts +0 -12
  233. package/dist/mcp-tools/submit-proposal.d.ts.map +0 -1
  234. package/dist/mcp-tools/submit-proposal.js +0 -103
  235. package/dist/mcp-tools/submit-proposal.js.map +0 -1
  236. package/dist/mcp-tools/threat-summary.d.ts +0 -12
  237. package/dist/mcp-tools/threat-summary.d.ts.map +0 -1
  238. package/dist/mcp-tools/threat-summary.js +0 -74
  239. package/dist/mcp-tools/threat-summary.js.map +0 -1
  240. package/dist/mcp-tools/validate.d.ts +0 -15
  241. package/dist/mcp-tools/validate.d.ts.map +0 -1
  242. package/dist/mcp-tools/validate.js +0 -45
  243. package/dist/mcp-tools/validate.js.map +0 -1
  244. package/dist/modules/embedding.d.ts +0 -71
  245. package/dist/modules/embedding.d.ts.map +0 -1
  246. package/dist/modules/embedding.js +0 -141
  247. package/dist/modules/embedding.js.map +0 -1
  248. package/dist/modules/index.d.ts +0 -144
  249. package/dist/modules/index.d.ts.map +0 -1
  250. package/dist/modules/index.js +0 -82
  251. package/dist/modules/index.js.map +0 -1
  252. package/dist/modules/semantic.d.ts +0 -106
  253. package/dist/modules/semantic.d.ts.map +0 -1
  254. package/dist/modules/semantic.js +0 -359
  255. package/dist/modules/semantic.js.map +0 -1
  256. package/dist/modules/session.d.ts +0 -70
  257. package/dist/modules/session.d.ts.map +0 -1
  258. package/dist/modules/session.js +0 -128
  259. package/dist/modules/session.js.map +0 -1
  260. package/dist/rule-scaffolder.d.ts +0 -53
  261. package/dist/rule-scaffolder.d.ts.map +0 -1
  262. package/dist/rule-scaffolder.js +0 -301
  263. package/dist/rule-scaffolder.js.map +0 -1
  264. package/dist/session-tracker.d.ts +0 -58
  265. package/dist/session-tracker.d.ts.map +0 -1
  266. package/dist/session-tracker.js +0 -176
  267. package/dist/session-tracker.js.map +0 -1
  268. package/dist/shadow-evaluator.d.ts +0 -48
  269. package/dist/shadow-evaluator.d.ts.map +0 -1
  270. package/dist/shadow-evaluator.js +0 -128
  271. package/dist/shadow-evaluator.js.map +0 -1
  272. package/dist/skill-fingerprint.d.ts +0 -85
  273. package/dist/skill-fingerprint.d.ts.map +0 -1
  274. package/dist/skill-fingerprint.js +0 -284
  275. package/dist/skill-fingerprint.js.map +0 -1
  276. package/dist/tier0-invariant.d.ts +0 -49
  277. package/dist/tier0-invariant.d.ts.map +0 -1
  278. package/dist/tier0-invariant.js +0 -184
  279. package/dist/tier0-invariant.js.map +0 -1
  280. package/dist/tier1-blacklist.d.ts +0 -48
  281. package/dist/tier1-blacklist.d.ts.map +0 -1
  282. package/dist/tier1-blacklist.js +0 -91
  283. package/dist/tier1-blacklist.js.map +0 -1
  284. package/dist/types.d.ts +0 -190
  285. package/dist/types.d.ts.map +0 -1
  286. package/dist/types.js +0 -6
  287. package/dist/types.js.map +0 -1
  288. package/dist/verdict.d.ts +0 -26
  289. package/dist/verdict.d.ts.map +0 -1
  290. package/dist/verdict.js +0 -127
  291. package/dist/verdict.js.map +0 -1
package/dist/engine.js DELETED
@@ -1,869 +0,0 @@
1
- /**
2
- * ATR Engine - Evaluates agent events against ATR rules
3
- *
4
- * Core detection engine that:
5
- * 1. Loads ATR YAML rules from disk
6
- * 2. Evaluates agent events (LLM I/O, tool calls, behaviors) against rules
7
- * 3. Returns matched rules with confidence scores
8
- * 4. Supports two condition formats:
9
- * - Array format: conditions is an array of {field, operator, value} objects
10
- * - Named format: conditions is an object map of named condition blocks
11
- *
12
- * @module agent-threat-rules/engine
13
- */
14
- import { loadRulesFromDirectory, loadRuleFile } from './loader.js';
15
- import { computeVerdict } from './verdict.js';
16
- import { SemanticModule } from './modules/semantic.js';
17
- import { resolveSkillId, runFingerprintLayer, shouldRunSemanticLayer, createSemanticModuleFromConfig, runSemanticLayer, } from './layer-integration.js';
18
- import { buildBlacklistMatch, resolveSkillId as resolveBlacklistSkillId } from './tier1-blacklist.js';
19
- /** Map agent event types to ATR source types */
20
- const EVENT_TYPE_TO_SOURCE = {
21
- llm_input: 'llm_io',
22
- llm_output: 'llm_io',
23
- tool_call: 'tool_call',
24
- tool_response: 'mcp_exchange',
25
- agent_behavior: 'agent_behavior',
26
- multi_agent_message: 'multi_agent_comm',
27
- };
28
- /** Map agent event types to default field names */
29
- const EVENT_TYPE_TO_FIELD = {
30
- llm_input: 'user_input',
31
- llm_output: 'agent_output',
32
- tool_call: 'tool_name',
33
- tool_response: 'tool_response',
34
- agent_behavior: 'metric',
35
- multi_agent_message: 'agent_message',
36
- };
37
- export class ATREngine {
38
- config;
39
- rules = [];
40
- compiledPatterns = new Map();
41
- semanticModuleInstance;
42
- constructor(config = {}) {
43
- this.config = config;
44
- // Initialize Layer 3 semantic module if config provided
45
- if (config.semanticModule) {
46
- const moduleConfig = createSemanticModuleFromConfig(config.semanticModule);
47
- this.semanticModuleInstance = new SemanticModule(moduleConfig);
48
- }
49
- else {
50
- this.semanticModuleInstance = null;
51
- }
52
- }
53
- /**
54
- * Load rules from configured directory and/or pre-loaded rules.
55
- */
56
- async loadRules() {
57
- this.rules = [];
58
- this.compiledPatterns.clear();
59
- if (this.config.rules) {
60
- this.rules.push(...this.config.rules);
61
- }
62
- if (this.config.rulesDir) {
63
- try {
64
- const fileRules = loadRulesFromDirectory(this.config.rulesDir);
65
- this.rules.push(...fileRules);
66
- }
67
- catch {
68
- // Directory may not exist yet
69
- }
70
- }
71
- // Pre-compile regex patterns for performance
72
- for (const rule of this.rules) {
73
- this.compilePatterns(rule);
74
- }
75
- return this.rules.length;
76
- }
77
- /**
78
- * Load a single rule file and add it to the engine.
79
- */
80
- addRuleFile(filePath) {
81
- const rule = loadRuleFile(filePath);
82
- this.rules.push(rule);
83
- this.compilePatterns(rule);
84
- }
85
- /**
86
- * Add a pre-parsed rule to the engine.
87
- */
88
- addRule(rule) {
89
- this.rules.push(rule);
90
- this.compilePatterns(rule);
91
- }
92
- /**
93
- * Evaluate an agent event against all loaded ATR rules.
94
- * Returns all matching rules with details.
95
- */
96
- evaluate(event) {
97
- const matches = [];
98
- const eventSourceType = EVENT_TYPE_TO_SOURCE[event.type];
99
- const allMatchedPatterns = [];
100
- const sessionId = event.sessionId;
101
- // Tier 0: Invariant enforcement (hard boundaries, pre-check)
102
- if (this.config.invariantChecker) {
103
- const violations = this.config.invariantChecker.check(event);
104
- if (violations.length > 0) {
105
- // Record denied event in session tracker for telemetry before returning
106
- if (this.config.sessionTracker && sessionId) {
107
- this.config.sessionTracker.recordEvent(sessionId, event, ['tier0-invariant-deny']);
108
- }
109
- return violations.map((v) => this.config.invariantChecker.buildDenyMatch(v));
110
- }
111
- }
112
- // Tier 1: Blacklist lookup (known-bad skills)
113
- if (this.config.blacklistProvider) {
114
- const skillId = resolveBlacklistSkillId(event);
115
- if (skillId) {
116
- const entry = this.config.blacklistProvider.lookup(skillId);
117
- if (entry) {
118
- matches.push(buildBlacklistMatch(entry));
119
- // Don't short-circuit -- continue for telemetry, but blacklist match
120
- // has critical severity which guarantees DENY verdict
121
- }
122
- }
123
- }
124
- // Tier 2: Pattern matching (existing regex rules)
125
- for (const rule of this.rules) {
126
- // Skip deprecated and draft rules
127
- if (rule.status === 'deprecated' || rule.status === 'draft')
128
- continue;
129
- // Source type filtering: skip rules that don't apply to this event type
130
- if (eventSourceType && rule.agent_source.type !== eventSourceType) {
131
- // Allow mcp_exchange rules to also match tool_call events
132
- if (!(rule.agent_source.type === 'mcp_exchange' && eventSourceType === 'tool_call')) {
133
- continue;
134
- }
135
- }
136
- const matchResult = this.evaluateRule(rule, event);
137
- if (matchResult) {
138
- matches.push(matchResult);
139
- allMatchedPatterns.push(...matchResult.matchedPatterns);
140
- }
141
- }
142
- // Record event in session tracker (always, for cross-event sequence detection)
143
- if (this.config.sessionTracker && sessionId) {
144
- this.config.sessionTracker.recordEvent(sessionId, event, allMatchedPatterns);
145
- }
146
- // Layer 2: Skill behavioral fingerprinting (optional, no LLM)
147
- const fingerprintStore = this.config.fingerprintStore;
148
- if (fingerprintStore) {
149
- const skillId = resolveSkillId(event);
150
- if (skillId) {
151
- const layer2Matches = runFingerprintLayer(fingerprintStore, event, skillId);
152
- matches.push(...layer2Matches);
153
- }
154
- }
155
- // Sort by severity (critical first) then confidence
156
- return matches.sort((a, b) => {
157
- const severityOrder = { critical: 0, high: 1, medium: 2, low: 3, informational: 4 };
158
- const aSev = severityOrder[a.rule.severity] ?? 4;
159
- const bSev = severityOrder[b.rule.severity] ?? 4;
160
- if (aSev !== bSev)
161
- return aSev - bSev;
162
- return b.confidence - a.confidence;
163
- });
164
- }
165
- /**
166
- * Evaluate a single rule against an event.
167
- * Supports both array-format and named-map-format conditions.
168
- */
169
- evaluateRule(rule, event) {
170
- const { detection } = rule;
171
- const conditions = detection.conditions;
172
- const allMatchedPatterns = [];
173
- // Detect format: array or named map
174
- if (Array.isArray(conditions)) {
175
- return this.evaluateArrayConditions(rule, conditions, detection.condition, event, allMatchedPatterns);
176
- }
177
- return this.evaluateNamedConditions(rule, conditions, detection.condition, event, allMatchedPatterns);
178
- }
179
- /**
180
- * Evaluate array-format conditions: [{field, operator, value}, ...]
181
- * with condition: "any" | "all"
182
- */
183
- evaluateArrayConditions(rule, conditions, conditionExpr, event, allMatchedPatterns) {
184
- const matchedConditionIndices = [];
185
- const isAny = conditionExpr === 'any' || conditionExpr === 'or';
186
- for (let i = 0; i < conditions.length; i++) {
187
- const cond = conditions[i];
188
- const result = this.evaluateArrayCondition(cond, event, rule.id, i, allMatchedPatterns);
189
- if (result) {
190
- matchedConditionIndices.push(i);
191
- if (isAny)
192
- break; // Short-circuit on first match for "any"
193
- }
194
- }
195
- const matched = isAny
196
- ? matchedConditionIndices.length > 0
197
- : matchedConditionIndices.length === conditions.length;
198
- if (!matched)
199
- return null;
200
- const baseConfidence = rule.tags.confidence === 'high' ? 0.9 : rule.tags.confidence === 'medium' ? 0.7 : 0.5;
201
- const matchRatio = matchedConditionIndices.length / Math.max(conditions.length, 1);
202
- const confidence = Math.min(baseConfidence + matchRatio * 0.1, 1.0);
203
- return {
204
- rule,
205
- matchedConditions: matchedConditionIndices.map(String),
206
- matchedPatterns: allMatchedPatterns,
207
- confidence,
208
- timestamp: new Date().toISOString(),
209
- };
210
- }
211
- /**
212
- * Evaluate a single array-format condition {field, operator, value}.
213
- */
214
- evaluateArrayCondition(cond, event, ruleId, index, matchedPatterns) {
215
- // Sequence condition (has 'steps' array)
216
- if (cond['steps'] && Array.isArray(cond['steps'])) {
217
- return this.evaluateSequenceCondition(cond, event);
218
- }
219
- // Behavioral condition
220
- if (cond['metric'] && cond['operator'] && cond['threshold'] !== undefined) {
221
- return this.evaluateBehavioralCondition(cond, event);
222
- }
223
- const field = cond['field'];
224
- const operator = cond['operator'];
225
- const value = cond['value'];
226
- if (!field || !operator || value === undefined)
227
- return false;
228
- const rawFieldValue = this.resolveField(field, event);
229
- if (!rawFieldValue)
230
- return false;
231
- const fieldValue = normalizeUnicode(rawFieldValue);
232
- switch (operator) {
233
- case 'regex': {
234
- // Try pre-compiled pattern first
235
- const compiled = this.compiledPatterns.get(ruleId)?.get(String(index));
236
- if (compiled && compiled.length > 0) {
237
- // Test against both normalized and raw values so that patterns
238
- // detecting zero-width/bidi characters can match before stripping
239
- if (safeRegexTest(compiled[0], fieldValue) || safeRegexTest(compiled[0], rawFieldValue)) {
240
- matchedPatterns.push(value);
241
- return true;
242
- }
243
- return false;
244
- }
245
- // Fallback: compile on the fly
246
- try {
247
- const regex = new RegExp(normalizeRegex(value), 'i');
248
- if (safeRegexTest(regex, fieldValue) || safeRegexTest(regex, rawFieldValue)) {
249
- matchedPatterns.push(value);
250
- return true;
251
- }
252
- }
253
- catch {
254
- // Invalid regex
255
- }
256
- return false;
257
- }
258
- case 'contains': {
259
- if (fieldValue.toLowerCase().includes(value.toLowerCase())) {
260
- matchedPatterns.push(value);
261
- return true;
262
- }
263
- return false;
264
- }
265
- case 'exact': {
266
- if (fieldValue === value) {
267
- matchedPatterns.push(value);
268
- return true;
269
- }
270
- return false;
271
- }
272
- case 'starts_with': {
273
- if (fieldValue.toLowerCase().startsWith(value.toLowerCase())) {
274
- matchedPatterns.push(value);
275
- return true;
276
- }
277
- return false;
278
- }
279
- default:
280
- return false;
281
- }
282
- }
283
- /**
284
- * Evaluate named-map-format conditions: {name: {field, patterns, match_type}, ...}
285
- * with condition: "name1 AND name2" | "name1 OR name2" | "name1"
286
- */
287
- evaluateNamedConditions(rule, conditions, conditionExpr, event, allMatchedPatterns) {
288
- const conditionResults = new Map();
289
- const matchedConditionNames = [];
290
- for (const [condName, condDef] of Object.entries(conditions)) {
291
- const result = this.evaluateNamedCondition(condName, condDef, event, rule, allMatchedPatterns);
292
- conditionResults.set(condName, result);
293
- if (result) {
294
- matchedConditionNames.push(condName);
295
- }
296
- }
297
- // Evaluate the boolean expression
298
- const finalResult = this.evaluateExpression(conditionExpr, conditionResults);
299
- if (!finalResult)
300
- return null;
301
- const baseConfidence = rule.tags.confidence === 'high' ? 0.9 : rule.tags.confidence === 'medium' ? 0.7 : 0.5;
302
- const matchRatio = matchedConditionNames.length / Math.max(Object.keys(conditions).length, 1);
303
- const confidence = Math.min(baseConfidence + matchRatio * 0.1, 1.0);
304
- return {
305
- rule,
306
- matchedConditions: matchedConditionNames,
307
- matchedPatterns: allMatchedPatterns,
308
- confidence,
309
- timestamp: new Date().toISOString(),
310
- };
311
- }
312
- /**
313
- * Evaluate a single named condition against an event.
314
- */
315
- evaluateNamedCondition(condName, condDef, event, rule, matchedPatterns) {
316
- const cond = condDef;
317
- // Pattern matching condition (named format with patterns array)
318
- if (cond['patterns'] && cond['field']) {
319
- return this.evaluatePatternCondition(cond, event, rule.id, condName, matchedPatterns);
320
- }
321
- // Behavioral condition
322
- if (cond['metric'] && cond['operator'] && cond['threshold'] !== undefined) {
323
- return this.evaluateBehavioralCondition(cond, event);
324
- }
325
- // Sequence condition
326
- if (cond['steps'] && Array.isArray(cond['steps'])) {
327
- return this.evaluateSequenceCondition(cond, event);
328
- }
329
- return false;
330
- }
331
- /**
332
- * Evaluate a pattern matching condition (named format with patterns array).
333
- */
334
- evaluatePatternCondition(cond, event, ruleId, condName, matchedPatterns) {
335
- const rawFieldValue = this.resolveField(cond.field, event);
336
- if (!rawFieldValue)
337
- return false;
338
- const fieldValue = normalizeUnicode(rawFieldValue);
339
- // Get pre-compiled patterns
340
- const compiled = this.compiledPatterns.get(ruleId)?.get(condName);
341
- if (compiled) {
342
- for (let i = 0; i < compiled.length; i++) {
343
- if (safeRegexTest(compiled[i], fieldValue) || (rawFieldValue && safeRegexTest(compiled[i], rawFieldValue))) {
344
- matchedPatterns.push(cond.patterns[i] ?? 'unknown');
345
- return true;
346
- }
347
- }
348
- return false;
349
- }
350
- // Fallback: direct string matching
351
- const checkValue = cond.case_sensitive ? fieldValue : fieldValue.toLowerCase();
352
- for (const pattern of cond.patterns) {
353
- const checkPattern = cond.case_sensitive ? pattern : pattern.toLowerCase();
354
- switch (cond.match_type) {
355
- case 'contains':
356
- if (checkValue.includes(checkPattern)) {
357
- matchedPatterns.push(pattern);
358
- return true;
359
- }
360
- break;
361
- case 'exact':
362
- if (checkValue === checkPattern) {
363
- matchedPatterns.push(pattern);
364
- return true;
365
- }
366
- break;
367
- case 'starts_with':
368
- if (checkValue.startsWith(checkPattern)) {
369
- matchedPatterns.push(pattern);
370
- return true;
371
- }
372
- break;
373
- case 'regex':
374
- default: {
375
- try {
376
- const flags = cond.case_sensitive ? '' : 'i';
377
- const regex = new RegExp(pattern, flags);
378
- if (safeRegexTest(regex, fieldValue)) {
379
- matchedPatterns.push(pattern);
380
- return true;
381
- }
382
- }
383
- catch {
384
- // Invalid regex, skip
385
- }
386
- break;
387
- }
388
- }
389
- }
390
- return false;
391
- }
392
- /**
393
- * Evaluate a behavioral threshold condition.
394
- * When a session tracker is available and the event has a sessionId,
395
- * supports session-derived metrics: call_frequency, pattern_frequency, event_count.
396
- */
397
- evaluateBehavioralCondition(cond, event) {
398
- const metricValue = this.resolveMetricValue(cond, event);
399
- if (metricValue === undefined)
400
- return false;
401
- switch (cond.operator) {
402
- case 'gt': return metricValue > cond.threshold;
403
- case 'lt': return metricValue < cond.threshold;
404
- case 'eq': return metricValue === cond.threshold;
405
- case 'gte': return metricValue >= cond.threshold;
406
- case 'lte': return metricValue <= cond.threshold;
407
- case 'deviation_from_baseline':
408
- return Math.abs(metricValue) > cond.threshold;
409
- default:
410
- return false;
411
- }
412
- }
413
- /**
414
- * Resolve a metric value from event metrics or session tracker.
415
- * Session-derived metrics use the format: "call_frequency:toolName" or "pattern_frequency:pattern".
416
- */
417
- resolveMetricValue(cond, event) {
418
- // Check event-level metrics first
419
- const directValue = event.metrics?.[cond.metric];
420
- if (directValue !== undefined)
421
- return directValue;
422
- // Try session tracker for session-derived metrics
423
- const tracker = this.config.sessionTracker;
424
- const sessionId = event.sessionId;
425
- if (!tracker || !sessionId)
426
- return undefined;
427
- const windowMs = this.parseWindowMs(cond.window);
428
- if (cond.metric.startsWith('call_frequency:')) {
429
- const toolName = cond.metric.slice('call_frequency:'.length);
430
- return tracker.getCallFrequency(sessionId, toolName, windowMs);
431
- }
432
- if (cond.metric.startsWith('pattern_frequency:')) {
433
- const pattern = cond.metric.slice('pattern_frequency:'.length);
434
- return tracker.getPatternFrequency(sessionId, pattern, windowMs);
435
- }
436
- if (cond.metric === 'event_count') {
437
- return tracker.getEventCount(sessionId, windowMs);
438
- }
439
- return undefined;
440
- }
441
- /**
442
- * Parse a window string (e.g. "5m", "1h", "30s") to milliseconds.
443
- * Defaults to 5 minutes if not specified or unparseable.
444
- */
445
- parseWindowMs(window) {
446
- if (!window)
447
- return 5 * 60 * 1000;
448
- const match = window.match(/^(\d+)\s*(s|m|h)$/);
449
- if (!match)
450
- return 5 * 60 * 1000;
451
- const value = parseInt(match[1], 10);
452
- const unit = match[2];
453
- switch (unit) {
454
- case 's': return value * 1000;
455
- case 'm': return value * 60 * 1000;
456
- case 'h': return value * 60 * 60 * 1000;
457
- default: return 5 * 60 * 1000;
458
- }
459
- }
460
- /**
461
- * Evaluate a sequence condition against the current event.
462
- *
463
- * Two modes:
464
- * 1. Session-aware (when SessionTracker + sessionId available):
465
- * Checks patterns across historical events in the session.
466
- * Respects `ordered` flag and `within` time window.
467
- * 2. Single-event fallback: checks if patterns co-occur in one event.
468
- */
469
- evaluateSequenceCondition(cond, event) {
470
- const steps = cond['steps'];
471
- if (!steps || steps.length === 0)
472
- return false;
473
- // Try session-aware detection first
474
- const tracker = this.config.sessionTracker;
475
- const sessionId = event.sessionId;
476
- if (tracker && sessionId) {
477
- const sessionResult = this.evaluateSequenceAcrossSession(steps, cond, tracker, sessionId, event);
478
- if (sessionResult)
479
- return true;
480
- }
481
- // Fallback: single-event check
482
- return this.evaluateSequenceSingleEvent(steps, event);
483
- }
484
- /**
485
- * Cross-event sequence detection using SessionTracker.
486
- * Checks if step patterns have been seen across events in order.
487
- */
488
- evaluateSequenceAcrossSession(steps, cond, tracker, sessionId, currentEvent) {
489
- const ordered = cond['ordered'] !== false; // default: true
490
- const withinMs = this.parseWindowMs(cond['within']);
491
- const snapshot = tracker.getSessionSnapshot(sessionId);
492
- if (!snapshot)
493
- return false;
494
- // Collect all events: historical + current
495
- const allEvents = [...snapshot.events, currentEvent];
496
- if (allEvents.length < steps.length)
497
- return false;
498
- // For each step, find the earliest event that matches
499
- const stepMatches = [];
500
- for (let si = 0; si < steps.length; si++) {
501
- const step = steps[si];
502
- const patterns = step['patterns'];
503
- if (!patterns)
504
- continue;
505
- for (let ei = 0; ei < allEvents.length; ei++) {
506
- const ev = allEvents[ei];
507
- const content = normalizeUnicode(ev.content);
508
- let matched = false;
509
- for (const pattern of patterns) {
510
- try {
511
- if (safeRegexTest(new RegExp(pattern, 'i'), content)) {
512
- matched = true;
513
- break;
514
- }
515
- }
516
- catch {
517
- // Invalid regex
518
- }
519
- }
520
- if (matched) {
521
- stepMatches.push({
522
- stepIndex: si,
523
- eventIndex: ei,
524
- timestamp: new Date(ev.timestamp).getTime(),
525
- });
526
- break; // First match per step
527
- }
528
- }
529
- }
530
- // Need all steps to match
531
- if (stepMatches.length < steps.length)
532
- return false;
533
- // Check ordering
534
- if (ordered) {
535
- for (let i = 1; i < stepMatches.length; i++) {
536
- if (stepMatches[i].eventIndex <= stepMatches[i - 1].eventIndex) {
537
- return false; // Out of order
538
- }
539
- }
540
- }
541
- // Check time window
542
- if (withinMs > 0) {
543
- const firstTs = Math.min(...stepMatches.map((m) => m.timestamp));
544
- const lastTs = Math.max(...stepMatches.map((m) => m.timestamp));
545
- if (lastTs - firstTs > withinMs)
546
- return false;
547
- }
548
- return true;
549
- }
550
- /**
551
- * Single-event fallback: check if step patterns co-occur in one event.
552
- */
553
- evaluateSequenceSingleEvent(steps, event) {
554
- const content = normalizeUnicode(event.content);
555
- let matchCount = 0;
556
- for (const step of steps) {
557
- const patterns = step['patterns'];
558
- if (patterns) {
559
- for (const pattern of patterns) {
560
- try {
561
- if (safeRegexTest(new RegExp(pattern, 'i'), content)) {
562
- matchCount++;
563
- break;
564
- }
565
- }
566
- catch {
567
- // Invalid regex
568
- }
569
- }
570
- }
571
- }
572
- return matchCount >= 2;
573
- }
574
- // parseWindowMs already defined above (behavioral conditions)
575
- /**
576
- * Resolve a field value from an agent event.
577
- */
578
- resolveField(fieldName, event) {
579
- // Check explicit fields first
580
- if (event.fields?.[fieldName]) {
581
- return event.fields[fieldName];
582
- }
583
- // Map standard field names to event properties
584
- const defaultField = EVENT_TYPE_TO_FIELD[event.type];
585
- if (fieldName === defaultField || fieldName === 'content') {
586
- return event.content;
587
- }
588
- // Common field aliases
589
- switch (fieldName) {
590
- case 'user_input':
591
- return event.type === 'llm_input' ? event.content : event.fields?.['user_input'];
592
- case 'agent_output':
593
- return event.type === 'llm_output' ? event.content : event.fields?.['agent_output'];
594
- case 'tool_response':
595
- return event.type === 'tool_response' ? event.content : event.fields?.['tool_response'];
596
- case 'tool_name':
597
- return event.fields?.['tool_name'] ?? (event.type === 'tool_call' ? event.content : undefined);
598
- case 'tool_args':
599
- return event.fields?.['tool_args'] ?? (event.type === 'tool_call' ? event.content : undefined);
600
- case 'agent_message':
601
- return event.type === 'multi_agent_message' ? event.content : event.fields?.['agent_message'];
602
- default:
603
- // Try metadata
604
- return event.metadata?.[fieldName];
605
- }
606
- }
607
- /**
608
- * Evaluate a boolean expression string against condition results.
609
- * Supports AND, OR, NOT operators.
610
- */
611
- evaluateExpression(expression, results) {
612
- const expr = expression.trim();
613
- // Simple single condition
614
- if (results.has(expr)) {
615
- return results.get(expr) ?? false;
616
- }
617
- // Handle NOT
618
- if (expr.startsWith('NOT ') || expr.startsWith('not ')) {
619
- const inner = expr.slice(4).trim();
620
- return !this.evaluateExpression(inner, results);
621
- }
622
- // Handle OR (lower precedence — split first so AND binds tighter)
623
- const orParts = this.splitByOperator(expr, 'OR');
624
- if (orParts.length > 1) {
625
- return orParts.some((part) => this.evaluateExpression(part, results));
626
- }
627
- // Handle AND (higher precedence — evaluated within each OR branch)
628
- const andParts = this.splitByOperator(expr, 'AND');
629
- if (andParts.length > 1) {
630
- return andParts.every((part) => this.evaluateExpression(part, results));
631
- }
632
- // Handle parentheses
633
- if (expr.startsWith('(') && expr.endsWith(')')) {
634
- return this.evaluateExpression(expr.slice(1, -1), results);
635
- }
636
- // Default: treat as condition name
637
- return results.get(expr) ?? false;
638
- }
639
- /**
640
- * Split expression by operator, respecting parentheses.
641
- */
642
- splitByOperator(expr, operator) {
643
- const parts = [];
644
- let depth = 0;
645
- let current = '';
646
- const op = ` ${operator} `;
647
- const opLower = ` ${operator.toLowerCase()} `;
648
- for (let i = 0; i < expr.length; i++) {
649
- const char = expr[i];
650
- if (char === '(')
651
- depth++;
652
- if (char === ')')
653
- depth--;
654
- if (depth === 0) {
655
- const remaining = expr.slice(i);
656
- if (remaining.startsWith(op) || remaining.startsWith(opLower)) {
657
- parts.push(current.trim());
658
- current = '';
659
- i += op.length - 1;
660
- continue;
661
- }
662
- }
663
- current += char;
664
- }
665
- if (current.trim()) {
666
- parts.push(current.trim());
667
- }
668
- return parts;
669
- }
670
- /**
671
- * Pre-compile regex patterns for a rule (performance optimization).
672
- * Supports both array-format and named-map-format conditions.
673
- */
674
- compilePatterns(rule) {
675
- const ruleMap = new Map();
676
- const conditions = rule.detection.conditions;
677
- if (Array.isArray(conditions)) {
678
- // Array format: compile each {operator: regex, value: "pattern"} entry
679
- for (let i = 0; i < conditions.length; i++) {
680
- const cond = conditions[i];
681
- if (cond['operator'] === 'regex' && typeof cond['value'] === 'string') {
682
- try {
683
- ruleMap.set(String(i), [new RegExp(normalizeRegex(cond['value']), 'i')]);
684
- }
685
- catch {
686
- // Invalid regex, skip
687
- }
688
- }
689
- }
690
- }
691
- else {
692
- // Named format: compile patterns arrays
693
- for (const [condName, condDef] of Object.entries(conditions)) {
694
- const cond = condDef;
695
- if (cond['patterns'] && Array.isArray(cond['patterns'])) {
696
- const matchType = cond['match_type'] ?? 'regex';
697
- const caseSensitive = cond['case_sensitive'] ?? false;
698
- const flags = caseSensitive ? '' : 'i';
699
- const compiled = [];
700
- for (const pattern of cond['patterns']) {
701
- try {
702
- if (matchType === 'regex') {
703
- compiled.push(new RegExp(normalizeRegex(pattern), flags));
704
- }
705
- else if (matchType === 'contains') {
706
- compiled.push(new RegExp(escapeRegex(pattern), flags));
707
- }
708
- else if (matchType === 'exact') {
709
- compiled.push(new RegExp(`^${escapeRegex(pattern)}$`, flags));
710
- }
711
- else if (matchType === 'starts_with') {
712
- compiled.push(new RegExp(`^${escapeRegex(pattern)}`, flags));
713
- }
714
- }
715
- catch {
716
- // Invalid regex pattern, skip
717
- }
718
- }
719
- ruleMap.set(condName, compiled);
720
- }
721
- }
722
- }
723
- this.compiledPatterns.set(rule.id, ruleMap);
724
- }
725
- /**
726
- * Evaluate an event and compute a verdict with optional action execution.
727
- *
728
- * Combines evaluate() + computeVerdict() + optional ActionExecutor
729
- * into a single call for convenience.
730
- */
731
- async evaluateWithVerdict(event, executor) {
732
- const layersUsed = ['layer1-regex'];
733
- let matches = this.evaluate(event);
734
- // Tier 0 + Tier 1 run inside evaluate(), track them
735
- if (this.config.invariantChecker)
736
- layersUsed.push('tier0-invariant');
737
- if (this.config.blacklistProvider)
738
- layersUsed.push('tier1-blacklist');
739
- // Layer 2 runs synchronously inside evaluate(), but track if it was configured
740
- if (this.config.fingerprintStore) {
741
- layersUsed.push('layer2-fingerprint');
742
- }
743
- // Tier 2.5: Embedding similarity (async, runs on all events)
744
- if (this.config.embeddingModule?.isAvailable()) {
745
- layersUsed.push('tier2.5-embedding');
746
- try {
747
- const embResult = await this.config.embeddingModule.evaluate(event, {
748
- module: 'embedding',
749
- function: 'similarity_search',
750
- args: { field: 'content' },
751
- operator: 'gte',
752
- threshold: 0.65,
753
- });
754
- if (embResult.matched) {
755
- const severity = embResult.value >= 0.95 ? 'critical'
756
- : embResult.value >= 0.88 ? 'high'
757
- : 'medium';
758
- const syntheticMatch = {
759
- rule: {
760
- title: `Embedding Match: ${embResult.description}`,
761
- id: 'tier2.5-embedding-match',
762
- status: 'experimental',
763
- description: embResult.description,
764
- author: 'atr-engine/tier2.5',
765
- date: new Date().toISOString().slice(0, 10),
766
- severity,
767
- tags: { category: 'prompt-injection', subcategory: 'semantic-similarity', confidence: 'high' },
768
- agent_source: { type: 'llm_io' },
769
- detection: { conditions: {}, condition: 'tier2.5-runtime' },
770
- response: {
771
- actions: severity === 'critical'
772
- ? ['block_input', 'alert']
773
- : ['alert'],
774
- },
775
- },
776
- matchedConditions: ['embedding_similarity'],
777
- matchedPatterns: [`similarity=${embResult.value.toFixed(3)}`],
778
- confidence: embResult.value,
779
- timestamp: new Date().toISOString(),
780
- };
781
- matches = [...matches, syntheticMatch];
782
- }
783
- }
784
- catch {
785
- // Embedding failure is non-fatal
786
- }
787
- }
788
- // Layer 3: Semantic LLM-as-judge (async, conditional)
789
- if (this.semanticModuleInstance && shouldRunSemanticLayer(matches, event)) {
790
- layersUsed.push('layer3-semantic');
791
- const semanticMatches = await runSemanticLayer(this.semanticModuleInstance, event, matches);
792
- if (semanticMatches.length > 0) {
793
- // Merge and re-sort immutably
794
- const merged = [...matches, ...semanticMatches];
795
- const severityOrder = {
796
- critical: 0, high: 1, medium: 2, low: 3, informational: 4,
797
- };
798
- merged.sort((a, b) => {
799
- const aSev = severityOrder[a.rule.severity] ?? 4;
800
- const bSev = severityOrder[b.rule.severity] ?? 4;
801
- if (aSev !== bSev)
802
- return aSev - bSev;
803
- return b.confidence - a.confidence;
804
- });
805
- matches = merged;
806
- }
807
- }
808
- const verdict = computeVerdict(matches);
809
- let actionResults = Object.freeze([]);
810
- if (executor && verdict.actions.length > 0) {
811
- const context = Object.freeze({
812
- event,
813
- matches,
814
- verdict,
815
- sessionId: event.sessionId,
816
- metadata: event.metadata ? Object.freeze({ ...event.metadata }) : undefined,
817
- });
818
- actionResults = await executor.execute(context);
819
- }
820
- return { verdict, actionResults, layersUsed: Object.freeze(layersUsed) };
821
- }
822
- /** Get loaded rule count */
823
- getRuleCount() {
824
- return this.rules.length;
825
- }
826
- /** Get all loaded rules */
827
- getRules() {
828
- return this.rules;
829
- }
830
- /** Get a rule by ID */
831
- getRuleById(id) {
832
- return this.rules.find((r) => r.id === id);
833
- }
834
- /** Get rules by category */
835
- getRulesByCategory(category) {
836
- return this.rules.filter((r) => r.tags.category === category);
837
- }
838
- }
839
- function escapeRegex(str) {
840
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
841
- }
842
- /**
843
- * Strip inline flags like (?i) from regex patterns.
844
- * JavaScript RegExp uses flags as a constructor parameter, not inline.
845
- */
846
- function normalizeRegex(pattern) {
847
- return pattern.replace(/^\(\?[imsx]+\)/, '');
848
- }
849
- /**
850
- * Normalize Unicode text to NFC form and strip zero-width characters.
851
- * This prevents evasion via combining characters, zero-width joiners, etc.
852
- */
853
- function normalizeUnicode(text) {
854
- return text
855
- .normalize('NFC')
856
- .replace(/[\u200B\u200C\u200D\uFEFF\u2060\u180E\u200E\u200F\u202A-\u202E\u2066-\u2069]/g, '');
857
- }
858
- /** Maximum input length for regex evaluation to mitigate ReDoS */
859
- const MAX_EVAL_LENGTH = 100_000;
860
- /**
861
- * Safely test a regex pattern against input with length limits.
862
- * Returns false if input exceeds MAX_EVAL_LENGTH to prevent ReDoS.
863
- */
864
- function safeRegexTest(regex, input) {
865
- if (input.length > MAX_EVAL_LENGTH)
866
- return false;
867
- return regex.test(input);
868
- }
869
- //# sourceMappingURL=engine.js.map