@panguard-ai/atr 1.4.3 → 1.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (274) hide show
  1. package/dist/action-executor.d.ts +44 -0
  2. package/dist/action-executor.d.ts.map +1 -0
  3. package/dist/action-executor.js +130 -0
  4. package/dist/action-executor.js.map +1 -0
  5. package/dist/adapters/default-adapter.d.ts +24 -0
  6. package/dist/adapters/default-adapter.d.ts.map +1 -0
  7. package/dist/adapters/default-adapter.js +51 -0
  8. package/dist/adapters/default-adapter.js.map +1 -0
  9. package/dist/adapters/stdio-adapter.d.ts +30 -0
  10. package/dist/adapters/stdio-adapter.d.ts.map +1 -0
  11. package/dist/adapters/stdio-adapter.js +128 -0
  12. package/dist/adapters/stdio-adapter.js.map +1 -0
  13. package/dist/badge.d.ts +42 -0
  14. package/dist/badge.d.ts.map +1 -0
  15. package/dist/badge.js +163 -0
  16. package/dist/badge.js.map +1 -0
  17. package/dist/capability-extractor.d.ts +35 -0
  18. package/dist/capability-extractor.d.ts.map +1 -0
  19. package/dist/capability-extractor.js +91 -0
  20. package/dist/capability-extractor.js.map +1 -0
  21. package/dist/cli/scan-handler.d.ts +21 -0
  22. package/dist/cli/scan-handler.d.ts.map +1 -0
  23. package/dist/cli/scan-handler.js +276 -0
  24. package/dist/cli/scan-handler.js.map +1 -0
  25. package/dist/cli/tc-pipeline.d.ts +18 -0
  26. package/dist/cli/tc-pipeline.d.ts.map +1 -0
  27. package/dist/cli/tc-pipeline.js +295 -0
  28. package/dist/cli/tc-pipeline.js.map +1 -0
  29. package/dist/cli.d.ts +12 -0
  30. package/dist/cli.d.ts.map +1 -0
  31. package/dist/cli.js +894 -0
  32. package/dist/cli.js.map +1 -0
  33. package/dist/content-hash.d.ts +7 -0
  34. package/dist/content-hash.d.ts.map +1 -0
  35. package/dist/content-hash.js +10 -0
  36. package/dist/content-hash.js.map +1 -0
  37. package/dist/converters/elastic.d.ts +36 -0
  38. package/dist/converters/elastic.d.ts.map +1 -0
  39. package/dist/converters/elastic.js +125 -0
  40. package/dist/converters/elastic.js.map +1 -0
  41. package/dist/converters/generic-regex.d.ts +37 -0
  42. package/dist/converters/generic-regex.d.ts.map +1 -0
  43. package/dist/converters/generic-regex.js +59 -0
  44. package/dist/converters/generic-regex.js.map +1 -0
  45. package/dist/converters/index.d.ts +32 -0
  46. package/dist/converters/index.d.ts.map +1 -0
  47. package/dist/converters/index.js +38 -0
  48. package/dist/converters/index.js.map +1 -0
  49. package/dist/converters/sarif.d.ts +18 -0
  50. package/dist/converters/sarif.d.ts.map +1 -0
  51. package/dist/converters/sarif.js +142 -0
  52. package/dist/converters/sarif.js.map +1 -0
  53. package/dist/converters/splunk.d.ts +19 -0
  54. package/dist/converters/splunk.d.ts.map +1 -0
  55. package/dist/converters/splunk.js +148 -0
  56. package/dist/converters/splunk.js.map +1 -0
  57. package/dist/coverage-analyzer.d.ts +43 -0
  58. package/dist/coverage-analyzer.d.ts.map +1 -0
  59. package/dist/coverage-analyzer.js +329 -0
  60. package/dist/coverage-analyzer.js.map +1 -0
  61. package/dist/embedding/build-corpus.d.ts +15 -0
  62. package/dist/embedding/build-corpus.d.ts.map +1 -0
  63. package/dist/embedding/build-corpus.js +105 -0
  64. package/dist/embedding/build-corpus.js.map +1 -0
  65. package/dist/embedding/model-loader.d.ts +41 -0
  66. package/dist/embedding/model-loader.d.ts.map +1 -0
  67. package/dist/embedding/model-loader.js +90 -0
  68. package/dist/embedding/model-loader.js.map +1 -0
  69. package/dist/embedding/vector-store.d.ts +41 -0
  70. package/dist/embedding/vector-store.d.ts.map +1 -0
  71. package/dist/embedding/vector-store.js +70 -0
  72. package/dist/embedding/vector-store.js.map +1 -0
  73. package/dist/engine.d.ts +222 -0
  74. package/dist/engine.d.ts.map +1 -0
  75. package/dist/engine.js +1185 -0
  76. package/dist/engine.js.map +1 -0
  77. package/dist/eval/corpus.d.ts +42 -0
  78. package/dist/eval/corpus.d.ts.map +1 -0
  79. package/dist/eval/corpus.js +427 -0
  80. package/dist/eval/corpus.js.map +1 -0
  81. package/dist/eval/eval-harness.d.ts +44 -0
  82. package/dist/eval/eval-harness.d.ts.map +1 -0
  83. package/dist/eval/eval-harness.js +296 -0
  84. package/dist/eval/eval-harness.js.map +1 -0
  85. package/dist/eval/index.d.ts +13 -0
  86. package/dist/eval/index.d.ts.map +1 -0
  87. package/dist/eval/index.js +9 -0
  88. package/dist/eval/index.js.map +1 -0
  89. package/dist/eval/metrics.d.ts +74 -0
  90. package/dist/eval/metrics.d.ts.map +1 -0
  91. package/dist/eval/metrics.js +108 -0
  92. package/dist/eval/metrics.js.map +1 -0
  93. package/dist/eval/pint-corpus.d.ts +34 -0
  94. package/dist/eval/pint-corpus.d.ts.map +1 -0
  95. package/dist/eval/pint-corpus.js +113 -0
  96. package/dist/eval/pint-corpus.js.map +1 -0
  97. package/dist/eval/rule-corpus.d.ts +9 -0
  98. package/dist/eval/rule-corpus.d.ts.map +1 -0
  99. package/dist/eval/rule-corpus.js +4780 -0
  100. package/dist/eval/rule-corpus.js.map +1 -0
  101. package/dist/eval/rule-metrics.d.ts +34 -0
  102. package/dist/eval/rule-metrics.d.ts.map +1 -0
  103. package/dist/eval/rule-metrics.js +92 -0
  104. package/dist/eval/rule-metrics.js.map +1 -0
  105. package/dist/eval/run-eval.d.ts +7 -0
  106. package/dist/eval/run-eval.d.ts.map +1 -0
  107. package/dist/eval/run-eval.js +11 -0
  108. package/dist/eval/run-eval.js.map +1 -0
  109. package/dist/eval/run-pint-benchmark.d.ts +18 -0
  110. package/dist/eval/run-pint-benchmark.d.ts.map +1 -0
  111. package/dist/eval/run-pint-benchmark.js +159 -0
  112. package/dist/eval/run-pint-benchmark.js.map +1 -0
  113. package/dist/eval/skill-benchmark.d.ts +66 -0
  114. package/dist/eval/skill-benchmark.d.ts.map +1 -0
  115. package/dist/eval/skill-benchmark.js +194 -0
  116. package/dist/eval/skill-benchmark.js.map +1 -0
  117. package/dist/flywheel.d.ts +54 -0
  118. package/dist/flywheel.d.ts.map +1 -0
  119. package/dist/flywheel.js +121 -0
  120. package/dist/flywheel.js.map +1 -0
  121. package/dist/hook-handler.d.ts +61 -0
  122. package/dist/hook-handler.d.ts.map +1 -0
  123. package/dist/hook-handler.js +178 -0
  124. package/dist/hook-handler.js.map +1 -0
  125. package/dist/index.d.ts +8 -0
  126. package/dist/index.d.ts.map +1 -0
  127. package/{src/index.ts → dist/index.js} +1 -0
  128. package/dist/index.js.map +1 -0
  129. package/dist/layer-integration.d.ts +55 -0
  130. package/dist/layer-integration.d.ts.map +1 -0
  131. package/dist/layer-integration.js +187 -0
  132. package/dist/layer-integration.js.map +1 -0
  133. package/dist/loader.d.ts +18 -0
  134. package/dist/loader.d.ts.map +1 -0
  135. package/dist/loader.js +129 -0
  136. package/dist/loader.js.map +1 -0
  137. package/dist/mcp-server.d.ts +13 -0
  138. package/dist/mcp-server.d.ts.map +1 -0
  139. package/dist/mcp-server.js +246 -0
  140. package/dist/mcp-server.js.map +1 -0
  141. package/dist/mcp-tools/coverage-gaps.d.ts +13 -0
  142. package/dist/mcp-tools/coverage-gaps.d.ts.map +1 -0
  143. package/dist/mcp-tools/coverage-gaps.js +55 -0
  144. package/dist/mcp-tools/coverage-gaps.js.map +1 -0
  145. package/dist/mcp-tools/list-rules.d.ts +17 -0
  146. package/dist/mcp-tools/list-rules.d.ts.map +1 -0
  147. package/dist/mcp-tools/list-rules.js +45 -0
  148. package/dist/mcp-tools/list-rules.js.map +1 -0
  149. package/dist/mcp-tools/scan-skill.d.ts +17 -0
  150. package/dist/mcp-tools/scan-skill.d.ts.map +1 -0
  151. package/dist/mcp-tools/scan-skill.js +65 -0
  152. package/dist/mcp-tools/scan-skill.js.map +1 -0
  153. package/dist/mcp-tools/scan.d.ts +24 -0
  154. package/dist/mcp-tools/scan.d.ts.map +1 -0
  155. package/dist/mcp-tools/scan.js +94 -0
  156. package/dist/mcp-tools/scan.js.map +1 -0
  157. package/dist/mcp-tools/submit-proposal.d.ts +12 -0
  158. package/dist/mcp-tools/submit-proposal.d.ts.map +1 -0
  159. package/dist/mcp-tools/submit-proposal.js +103 -0
  160. package/dist/mcp-tools/submit-proposal.js.map +1 -0
  161. package/dist/mcp-tools/threat-summary.d.ts +12 -0
  162. package/dist/mcp-tools/threat-summary.d.ts.map +1 -0
  163. package/dist/mcp-tools/threat-summary.js +74 -0
  164. package/dist/mcp-tools/threat-summary.js.map +1 -0
  165. package/dist/mcp-tools/validate.d.ts +15 -0
  166. package/dist/mcp-tools/validate.d.ts.map +1 -0
  167. package/dist/mcp-tools/validate.js +51 -0
  168. package/dist/mcp-tools/validate.js.map +1 -0
  169. package/dist/modules/embedding.d.ts +71 -0
  170. package/dist/modules/embedding.d.ts.map +1 -0
  171. package/dist/modules/embedding.js +141 -0
  172. package/dist/modules/embedding.js.map +1 -0
  173. package/dist/modules/index.d.ts +144 -0
  174. package/dist/modules/index.d.ts.map +1 -0
  175. package/dist/modules/index.js +82 -0
  176. package/dist/modules/index.js.map +1 -0
  177. package/dist/modules/semantic.d.ts +106 -0
  178. package/dist/modules/semantic.d.ts.map +1 -0
  179. package/dist/modules/semantic.js +359 -0
  180. package/dist/modules/semantic.js.map +1 -0
  181. package/dist/modules/session.d.ts +70 -0
  182. package/dist/modules/session.d.ts.map +1 -0
  183. package/dist/modules/session.js +128 -0
  184. package/dist/modules/session.js.map +1 -0
  185. package/dist/quality/adapters/atr.d.ts +65 -0
  186. package/dist/quality/adapters/atr.d.ts.map +1 -0
  187. package/dist/quality/adapters/atr.js +154 -0
  188. package/dist/quality/adapters/atr.js.map +1 -0
  189. package/dist/quality/adapters/index.d.ts +10 -0
  190. package/dist/quality/adapters/index.d.ts.map +1 -0
  191. package/dist/quality/adapters/index.js +10 -0
  192. package/dist/quality/adapters/index.js.map +1 -0
  193. package/dist/quality/compute-confidence.d.ts +45 -0
  194. package/dist/quality/compute-confidence.d.ts.map +1 -0
  195. package/dist/quality/compute-confidence.js +133 -0
  196. package/dist/quality/compute-confidence.js.map +1 -0
  197. package/dist/quality/index.d.ts +36 -0
  198. package/dist/quality/index.d.ts.map +1 -0
  199. package/dist/quality/index.js +39 -0
  200. package/dist/quality/index.js.map +1 -0
  201. package/dist/quality/quality-gate.d.ts +86 -0
  202. package/dist/quality/quality-gate.d.ts.map +1 -0
  203. package/dist/quality/quality-gate.js +187 -0
  204. package/dist/quality/quality-gate.js.map +1 -0
  205. package/dist/quality/types.d.ts +129 -0
  206. package/dist/quality/types.d.ts.map +1 -0
  207. package/dist/quality/types.js +10 -0
  208. package/dist/quality/types.js.map +1 -0
  209. package/dist/quality/validate-maturity.d.ts +51 -0
  210. package/dist/quality/validate-maturity.d.ts.map +1 -0
  211. package/dist/quality/validate-maturity.js +134 -0
  212. package/dist/quality/validate-maturity.js.map +1 -0
  213. package/dist/quality.d.ts +8 -0
  214. package/dist/quality.d.ts.map +1 -0
  215. package/dist/quality.js +8 -0
  216. package/dist/quality.js.map +1 -0
  217. package/dist/rule-scaffolder.d.ts +53 -0
  218. package/dist/rule-scaffolder.d.ts.map +1 -0
  219. package/dist/rule-scaffolder.js +301 -0
  220. package/dist/rule-scaffolder.js.map +1 -0
  221. package/dist/session-tracker.d.ts +58 -0
  222. package/dist/session-tracker.d.ts.map +1 -0
  223. package/dist/session-tracker.js +176 -0
  224. package/dist/session-tracker.js.map +1 -0
  225. package/dist/shadow-evaluator.d.ts +48 -0
  226. package/dist/shadow-evaluator.d.ts.map +1 -0
  227. package/dist/shadow-evaluator.js +129 -0
  228. package/dist/shadow-evaluator.js.map +1 -0
  229. package/dist/skill-fingerprint.d.ts +85 -0
  230. package/dist/skill-fingerprint.d.ts.map +1 -0
  231. package/dist/skill-fingerprint.js +284 -0
  232. package/dist/skill-fingerprint.js.map +1 -0
  233. package/dist/tc-reporter.d.ts +50 -0
  234. package/dist/tc-reporter.d.ts.map +1 -0
  235. package/dist/tc-reporter.js +164 -0
  236. package/dist/tc-reporter.js.map +1 -0
  237. package/dist/tier0-invariant.d.ts +49 -0
  238. package/dist/tier0-invariant.d.ts.map +1 -0
  239. package/dist/tier0-invariant.js +185 -0
  240. package/dist/tier0-invariant.js.map +1 -0
  241. package/dist/tier1-blacklist.d.ts +48 -0
  242. package/dist/tier1-blacklist.d.ts.map +1 -0
  243. package/dist/tier1-blacklist.js +92 -0
  244. package/dist/tier1-blacklist.js.map +1 -0
  245. package/dist/types.d.ts +232 -0
  246. package/dist/types.d.ts.map +1 -0
  247. package/dist/types.js +6 -0
  248. package/dist/types.js.map +1 -0
  249. package/dist/verdict.d.ts +26 -0
  250. package/dist/verdict.d.ts.map +1 -0
  251. package/dist/verdict.js +127 -0
  252. package/dist/verdict.js.map +1 -0
  253. package/package.json +16 -4
  254. package/.github/ISSUE_TEMPLATE/evasion-report.yml +0 -75
  255. package/.github/ISSUE_TEMPLATE/false-positive.yml +0 -31
  256. package/.github/ISSUE_TEMPLATE/mirofish-prediction.yml +0 -128
  257. package/.github/ISSUE_TEMPLATE/new-rule.yml +0 -37
  258. package/.github/PULL_REQUEST_TEMPLATE.md +0 -23
  259. package/.github/workflows/rule-quality.yml +0 -203
  260. package/.github/workflows/validate.yml +0 -42
  261. package/CHANGELOG.md +0 -30
  262. package/CONTRIBUTING.md +0 -168
  263. package/CONTRIBUTORS.md +0 -28
  264. package/COVERAGE.md +0 -135
  265. package/LIMITATIONS.md +0 -154
  266. package/SECURITY.md +0 -48
  267. package/THREAT-MODEL.md +0 -243
  268. package/docs/contribution-paths.md +0 -202
  269. package/docs/mirofish-prediction-guide.md +0 -304
  270. package/docs/quick-start.md +0 -245
  271. package/docs/rule-writing-guide.md +0 -647
  272. package/docs/schema-spec.md +0 -594
  273. package/examples/how-to-write-a-rule.md +0 -251
  274. package/tsconfig.json +0 -17
package/dist/cli.js ADDED
@@ -0,0 +1,894 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * ATR CLI - Command-line interface for Agent Threat Rules
4
+ *
5
+ * Usage:
6
+ * npx agent-threat-rules scan <events.json> Scan events against all rules
7
+ * npx agent-threat-rules validate <rule.yaml> Validate a rule file
8
+ * npx agent-threat-rules test <rule.yaml> Run a rule's test cases
9
+ * npx agent-threat-rules stats Show rule collection stats
10
+ */
11
+ import { readFileSync, writeFileSync, readdirSync, existsSync, statSync, mkdirSync } from 'node:fs';
12
+ import { resolve, dirname, join, sep } from 'node:path';
13
+ import { homedir } from 'node:os';
14
+ import { fileURLToPath } from 'node:url';
15
+ import { ATREngine } from './engine.js';
16
+ import { loadRuleFile, loadRulesFromDirectory, validateRule } from './loader.js';
17
+ import { generateBadgeSvg, generateBadgeEndpoint, lookupPackageScan, generateBadgeMarkdown } from './badge.js';
18
+ import { cmdScanUnified } from './cli/scan-handler.js';
19
+ const __filename = fileURLToPath(import.meta.url);
20
+ const __dirname = dirname(__filename);
21
+ const RULES_DIR = resolve(__dirname, '..', 'rules');
22
+ const SEVERITY_COLORS = {
23
+ critical: '\x1b[91m', // bright red
24
+ high: '\x1b[31m', // red
25
+ medium: '\x1b[33m', // yellow
26
+ low: '\x1b[36m', // cyan
27
+ informational: '\x1b[37m', // white
28
+ };
29
+ const RESET = '\x1b[0m';
30
+ const GREEN = '\x1b[32m';
31
+ const RED = '\x1b[31m';
32
+ const DIM = '\x1b[2m';
33
+ const BOLD = '\x1b[1m';
34
+ function printUsage() {
35
+ console.log(`
36
+ ${BOLD}ATR - Agent Threat Rules${RESET}
37
+ Open detection rules for AI agent security threats.
38
+
39
+ ${BOLD}Usage:${RESET}
40
+ atr scan <file|dir> [--rules <dir>] Auto-detect and scan (JSON=MCP, .md=SKILL.md)
41
+ atr scan-skill <SKILL.md|dir> Scan SKILL.md files (alias for scan)
42
+ atr validate <rule.yaml|dir> Validate rule file(s)
43
+ atr test <rule.yaml|dir> Run embedded test cases
44
+ atr stats [--rules <dir>] Show rule collection statistics
45
+ atr convert <splunk|elastic> [--rules <dir>] [--output <file>]
46
+ Convert rules to SIEM query format
47
+ atr guard [--rules <dir>] [--dry-run] Start as Claude Code hook (stdio)
48
+ atr init [--global] Setup ATR guard hook for Claude Code
49
+ atr mcp Start MCP server (stdio transport)
50
+ atr scaffold Interactive rule scaffolding
51
+ atr badge <package> [--data <audit.json>] [--svg] [--json]
52
+ Generate ATR Scanned badge for a package
53
+
54
+ ${BOLD}Threat Cloud Pipeline:${RESET}
55
+ atr tc status Show TC state (rules, proposals, threats)
56
+ atr tc sync [--tc-key <key>] Push repo rules → TC (updates metrics + website)
57
+ atr tc pull [--since <ISO>] Pull confirmed TC rules → repo (validate + write)
58
+ atr tc crystallize Send missed attacks → TC LLM → new proposals
59
+
60
+ ${BOLD}Options:${RESET}
61
+ --rules <dir> Custom rules directory (default: bundled rules)
62
+ --json Output results as JSON
63
+ --sarif Output results as SARIF v2.1.0 (GitHub Security tab)
64
+ --output <file> Write output to file instead of stdout (convert)
65
+ --severity <s> Minimum severity to report (critical|high|medium|low|informational)
66
+ --report-to-cloud Report detections to ATR Threat Cloud (anonymous, opt-in)
67
+ --tc-url <url> Threat Cloud endpoint (default: https://tc.panguard.ai)
68
+ --dry-run Log actions without executing (guard mode)
69
+ --fail-open Default to allow on errors (guard mode, default: true)
70
+ --timeout <ms> Evaluation timeout in ms (guard mode, default: 5000)
71
+ --global Write hook to ~/.claude/settings.json instead of project (init)
72
+ --help Show this help message
73
+
74
+ ${BOLD}Examples:${RESET}
75
+ ${DIM}# Scan agent events for threats${RESET}
76
+ atr scan events.json
77
+
78
+ ${DIM}# Validate a custom rule${RESET}
79
+ atr validate my-rules/custom-rule.yaml
80
+
81
+ ${DIM}# Test all rules against their embedded test cases${RESET}
82
+ atr test rules/
83
+
84
+ ${DIM}# Show stats for bundled rules${RESET}
85
+ atr stats
86
+
87
+ ${DIM}# One-command Claude Code hook setup${RESET}
88
+ atr init
89
+
90
+ ${DIM}# Run as a Claude Code guard hook${RESET}
91
+ atr guard --rules ./my-rules
92
+
93
+ ${DIM}# Start MCP server for AI agent integration${RESET}
94
+ atr mcp
95
+
96
+ ${DIM}# Convert all rules to Splunk SPL${RESET}
97
+ atr convert splunk --output splunk-queries.txt
98
+
99
+ ${DIM}# Convert all rules to Elasticsearch Query DSL${RESET}
100
+ atr convert elastic --json --output elastic-queries.json
101
+
102
+ ${DIM}# Interactively scaffold a new rule${RESET}
103
+ atr scaffold
104
+ `);
105
+ }
106
+ function parseArgs(argv) {
107
+ const args = argv.slice(2);
108
+ const command = args[0] ?? 'help';
109
+ const options = {};
110
+ let target = '';
111
+ for (let i = 1; i < args.length; i++) {
112
+ if (args[i].startsWith('--')) {
113
+ const key = args[i].slice(2);
114
+ if (key === 'json' || key === 'sarif' || key === 'help' || key === 'dry-run' || key === 'fail-open' || key === 'global' || key === 'svg') {
115
+ options[key] = 'true';
116
+ }
117
+ else {
118
+ options[key] = args[++i] ?? '';
119
+ }
120
+ }
121
+ else if (!target) {
122
+ target = args[i];
123
+ }
124
+ }
125
+ return { command, target, options };
126
+ }
127
+ // --- VALIDATE command ---
128
+ function cmdValidate(target, options) {
129
+ if (!target) {
130
+ console.error(`${RED}Error: Missing rule file/directory. Usage: atr validate <rule.yaml|dir>${RESET}`);
131
+ process.exit(1);
132
+ }
133
+ const targetPath = resolve(target);
134
+ if (!existsSync(targetPath)) {
135
+ console.error(`${RED}Error: Not found: ${targetPath}${RESET}`);
136
+ process.exit(1);
137
+ }
138
+ const jsonOutput = options['json'] === 'true';
139
+ const files = [];
140
+ if (statSync(targetPath).isDirectory()) {
141
+ collectYamlFiles(targetPath, files);
142
+ }
143
+ else {
144
+ files.push(targetPath);
145
+ }
146
+ let passed = 0;
147
+ let failed = 0;
148
+ const results = [];
149
+ for (const file of files) {
150
+ try {
151
+ const rule = loadRuleFile(file);
152
+ const result = validateRule(rule);
153
+ results.push({ file, valid: result.valid, errors: result.errors });
154
+ if (result.valid) {
155
+ passed++;
156
+ }
157
+ else {
158
+ failed++;
159
+ }
160
+ }
161
+ catch (e) {
162
+ const msg = e instanceof Error ? e.message : String(e);
163
+ results.push({ file, valid: false, errors: [msg] });
164
+ failed++;
165
+ }
166
+ }
167
+ if (jsonOutput) {
168
+ console.log(JSON.stringify({ total: files.length, passed, failed, results }, null, 2));
169
+ return;
170
+ }
171
+ console.log(`\n${BOLD}ATR Rule Validation${RESET}`);
172
+ console.log(`${DIM}${'─'.repeat(60)}${RESET}`);
173
+ for (const r of results) {
174
+ const icon = r.valid ? `${GREEN}PASS${RESET}` : `${RED}FAIL${RESET}`;
175
+ const shortPath = r.file.replace(process.cwd() + '/', '');
176
+ console.log(` ${icon} ${shortPath}`);
177
+ if (!r.valid) {
178
+ for (const err of r.errors) {
179
+ console.log(` ${RED}${err}${RESET}`);
180
+ }
181
+ }
182
+ }
183
+ console.log(`${DIM}${'─'.repeat(60)}${RESET}`);
184
+ console.log(` ${GREEN}${passed} passed${RESET} ${failed > 0 ? RED + failed + ' failed' + RESET : ''}\n`);
185
+ if (failed > 0)
186
+ process.exit(1);
187
+ }
188
+ function collectYamlFiles(dir, out) {
189
+ const entries = readdirSync(dir);
190
+ for (const entry of entries) {
191
+ const full = join(dir, entry);
192
+ if (statSync(full).isDirectory()) {
193
+ collectYamlFiles(full, out);
194
+ }
195
+ else if (full.endsWith('.yaml') || full.endsWith('.yml')) {
196
+ out.push(full);
197
+ }
198
+ }
199
+ }
200
+ // --- TEST command ---
201
+ async function cmdTest(target, options) {
202
+ if (!target) {
203
+ // Default: test bundled rules
204
+ target = RULES_DIR;
205
+ }
206
+ const targetPath = resolve(target);
207
+ if (!existsSync(targetPath)) {
208
+ console.error(`${RED}Error: Not found: ${targetPath}${RESET}`);
209
+ process.exit(1);
210
+ }
211
+ const jsonOutput = options['json'] === 'true';
212
+ const rules = [];
213
+ if (statSync(targetPath).isDirectory()) {
214
+ rules.push(...loadRulesFromDirectory(targetPath));
215
+ }
216
+ else {
217
+ rules.push(loadRuleFile(targetPath));
218
+ }
219
+ let totalTests = 0;
220
+ let passed = 0;
221
+ let failed = 0;
222
+ const failures = [];
223
+ // Map extended agent_source types to basic event-compatible source types
224
+ // so the engine's source type filter doesn't skip rules during testing.
225
+ // The engine only recognizes: llm_io, tool_call, mcp_exchange, agent_behavior, multi_agent_comm
226
+ const EXTENDED_SOURCE_TO_BASE = {
227
+ context_window: 'llm_io',
228
+ memory_access: 'llm_io',
229
+ skill_lifecycle: 'tool_call',
230
+ skill_permission: 'tool_call',
231
+ skill_chain: 'tool_call',
232
+ };
233
+ for (const rule of rules) {
234
+ if (!rule.test_cases)
235
+ continue;
236
+ // For testing, normalize extended source types so the engine doesn't filter them out
237
+ const originalSourceType = rule.agent_source?.type;
238
+ const baseSourceType = EXTENDED_SOURCE_TO_BASE[originalSourceType ?? ''];
239
+ // Override status so draft/deprecated rules are not skipped during testing
240
+ const testRule = {
241
+ ...rule,
242
+ status: 'experimental',
243
+ ...(baseSourceType
244
+ ? { agent_source: { ...rule.agent_source, type: baseSourceType } }
245
+ : {}),
246
+ };
247
+ const engine = new ATREngine({ rules: [testRule] });
248
+ await engine.loadRules();
249
+ const tp = rule.test_cases.true_positives ?? [];
250
+ const tn = rule.test_cases.true_negatives ?? [];
251
+ // Test via both evaluate() and scanSkill() — pass if either detects
252
+ const runTest = (tcObj) => {
253
+ const event = buildEventFromTestCase(tcObj, rule);
254
+ const mcpHit = engine.evaluate(event).some(m => m.rule.id === rule.id);
255
+ if (mcpHit)
256
+ return true;
257
+ const input = String(tcObj['input'] ?? tcObj['tool_response'] ?? tcObj['agent_output'] ?? '');
258
+ if (input) {
259
+ const skillHit = engine.scanSkill(input).some(m => m.rule.id === rule.id);
260
+ if (skillHit)
261
+ return true;
262
+ }
263
+ return false;
264
+ };
265
+ for (const tc of tp) {
266
+ totalTests++;
267
+ const triggered = runTest(tc);
268
+ if (triggered) {
269
+ passed++;
270
+ }
271
+ else {
272
+ failed++;
273
+ failures.push({
274
+ ruleId: rule.id,
275
+ testType: 'true_positive',
276
+ input: String(tc.input ?? tc.tool_response ?? tc.agent_output ?? '').slice(0, 100),
277
+ expected: 'triggered',
278
+ got: 'not_triggered',
279
+ });
280
+ }
281
+ }
282
+ for (const tc of tn) {
283
+ totalTests++;
284
+ const triggered = runTest(tc);
285
+ if (!triggered) {
286
+ passed++;
287
+ }
288
+ else {
289
+ failed++;
290
+ failures.push({
291
+ ruleId: rule.id,
292
+ testType: 'true_negative',
293
+ input: String(tc.input ?? tc.tool_response ?? tc.agent_output ?? '').slice(0, 100),
294
+ expected: 'not_triggered',
295
+ got: 'triggered',
296
+ });
297
+ }
298
+ }
299
+ }
300
+ if (jsonOutput) {
301
+ console.log(JSON.stringify({ totalRules: rules.length, totalTests, passed, failed, failures }, null, 2));
302
+ return;
303
+ }
304
+ console.log(`\n${BOLD}ATR Test Runner${RESET}`);
305
+ console.log(`${DIM}${'─'.repeat(60)}${RESET}`);
306
+ console.log(` Rules tested: ${rules.length}`);
307
+ console.log(` Test cases: ${totalTests}`);
308
+ console.log(` ${GREEN}Passed:${RESET} ${passed}`);
309
+ if (failed > 0) {
310
+ console.log(` ${RED}Failed:${RESET} ${failed}`);
311
+ }
312
+ console.log(`${DIM}${'─'.repeat(60)}${RESET}`);
313
+ if (failures.length > 0) {
314
+ console.log(`\n${RED}Failures:${RESET}\n`);
315
+ for (const f of failures) {
316
+ console.log(` ${RED}FAIL${RESET} ${f.ruleId} [${f.testType}]`);
317
+ console.log(` ${DIM}Input: "${f.input}..."${RESET}`);
318
+ console.log(` Expected: ${f.expected}, Got: ${f.got}\n`);
319
+ }
320
+ process.exit(1);
321
+ }
322
+ else {
323
+ console.log(`\n${GREEN}All tests passed.${RESET}\n`);
324
+ }
325
+ }
326
+ function buildEventFromTestCase(tc, rule) {
327
+ // Stringify any object values
328
+ const str = (v) => {
329
+ if (v === undefined || v === null)
330
+ return '';
331
+ if (typeof v === 'string')
332
+ return v;
333
+ return JSON.stringify(v);
334
+ };
335
+ // Extract fields, handling multiple test case formats:
336
+ // Object-style: input: { tool_name: "...", tool_args: "...", response: "..." }
337
+ // Flat-style: input: "...", tool_response: "...", tool_name: "..."
338
+ // Tool-call-style: tool_call: { name: "...", args: "..." }
339
+ const rawInput = tc['input'];
340
+ let input = '';
341
+ let toolName = str(tc['tool_name']);
342
+ let toolArgs = str(tc['tool_args']);
343
+ let toolResponse = str(tc['tool_response']);
344
+ const agentOutput = str(tc['agent_output']);
345
+ const toolDescription = str(tc['tool_description']);
346
+ const rawContent = str(tc['content']);
347
+ // Handle tool_call: { name, args } format (used by tool_call rules like ATR-2026-00098)
348
+ const rawToolCall = tc['tool_call'];
349
+ if (rawToolCall !== null && rawToolCall !== undefined && typeof rawToolCall === 'object' && !Array.isArray(rawToolCall)) {
350
+ const tcObj = rawToolCall;
351
+ if (tcObj['name'] && !toolName)
352
+ toolName = str(tcObj['name']);
353
+ if (tcObj['args'] && !toolArgs)
354
+ toolArgs = str(tcObj['args']);
355
+ }
356
+ if (rawInput !== null && rawInput !== undefined && typeof rawInput === 'object' && !Array.isArray(rawInput)) {
357
+ const inputObj = rawInput;
358
+ if (inputObj['tool_name'] && !toolName)
359
+ toolName = str(inputObj['tool_name']);
360
+ if (inputObj['tool_args'] && !toolArgs)
361
+ toolArgs = str(inputObj['tool_args']);
362
+ // Handle 'response' as alias for 'tool_response' (used in ATR-065 etc.)
363
+ if (inputObj['response'] && !toolResponse)
364
+ toolResponse = str(inputObj['response']);
365
+ if (inputObj['tool_response'] && !toolResponse)
366
+ toolResponse = str(inputObj['tool_response']);
367
+ // For object inputs, use tool_args as the primary string input.
368
+ // If no tool_args, only use JSON-stringified input as fallback when there's
369
+ // no other meaningful field (tool_response/response) extracted.
370
+ if (toolArgs) {
371
+ input = toolArgs;
372
+ }
373
+ else if (!toolResponse) {
374
+ input = str(rawInput);
375
+ }
376
+ }
377
+ else {
378
+ input = str(rawInput);
379
+ }
380
+ // Infer event type from rule's agent_source and test case structure
381
+ const sourceType = rule.agent_source?.type ?? 'llm_io';
382
+ const SOURCE_TO_EVENT = {
383
+ llm_io: 'llm_input',
384
+ tool_call: 'tool_call',
385
+ mcp_exchange: 'tool_response',
386
+ agent_behavior: 'agent_behavior',
387
+ multi_agent_comm: 'multi_agent_message',
388
+ context_window: 'llm_input',
389
+ memory_access: 'llm_input',
390
+ skill_lifecycle: 'tool_call',
391
+ skill_permission: 'tool_call',
392
+ skill_chain: 'tool_call',
393
+ };
394
+ let type = SOURCE_TO_EVENT[sourceType] ?? 'llm_input';
395
+ // If rule expects tool_call but test case has only plain text input
396
+ // (no tool_name, tool_args, or tool_response), use llm_input event type
397
+ // since the content is natural language, not a tool invocation.
398
+ // This prevents the engine's tool_name fallback from treating text as a tool name.
399
+ if (type === 'tool_call' && !toolName && !toolArgs && !toolResponse && !toolDescription) {
400
+ type = 'llm_input';
401
+ }
402
+ // Determine the primary content based on what the rule conditions check
403
+ // Problem 2 & 3: For rules that check tool_response, the tool_response
404
+ // should be the primary content when the event type resolves tool_response from content
405
+ let content;
406
+ if (toolResponse && type === 'tool_response') {
407
+ // If event type is tool_response, engine resolves field:tool_response from event.content
408
+ content = toolResponse;
409
+ }
410
+ else {
411
+ content = input || toolDescription || rawContent || toolResponse || agentOutput || '';
412
+ }
413
+ const fields = {};
414
+ // Populate ALL known field aliases so the engine can resolve any field name
415
+ if (input)
416
+ fields['user_input'] = input;
417
+ if (toolResponse)
418
+ fields['tool_response'] = toolResponse;
419
+ if (agentOutput)
420
+ fields['agent_output'] = agentOutput;
421
+ if (toolDescription)
422
+ fields['tool_description'] = toolDescription;
423
+ // Always set tool_name (even empty) to prevent engine fallback
424
+ // from using event.content as tool_name for tool_call events
425
+ fields['tool_name'] = toolName;
426
+ if (toolArgs)
427
+ fields['tool_args'] = toolArgs;
428
+ // For content-based rules, set content and agent_message fields
429
+ fields['content'] = content;
430
+ fields['agent_message'] = content;
431
+ return {
432
+ type,
433
+ timestamp: new Date().toISOString(),
434
+ content,
435
+ fields,
436
+ sessionId: 'test-session',
437
+ agentId: 'test-agent',
438
+ };
439
+ }
440
+ // --- STATS command ---
441
+ function cmdStats(options) {
442
+ const rulesDir = options['rules'] ? resolve(options['rules']) : RULES_DIR;
443
+ const jsonOutput = options['json'] === 'true';
444
+ const rules = loadRulesFromDirectory(rulesDir);
445
+ const byCategory = {};
446
+ const bySeverity = {};
447
+ const byMaturity = {};
448
+ const byTier = {};
449
+ let totalTP = 0;
450
+ let totalTN = 0;
451
+ let totalEvasion = 0;
452
+ let withCVE = 0;
453
+ for (const rule of rules) {
454
+ const cat = rule.tags?.category ?? 'unknown';
455
+ byCategory[cat] = (byCategory[cat] ?? 0) + 1;
456
+ bySeverity[rule.severity] = (bySeverity[rule.severity] ?? 0) + 1;
457
+ const maturity = rule.maturity ?? 'experimental';
458
+ byMaturity[maturity] = (byMaturity[maturity] ?? 0) + 1;
459
+ const tier = rule.detection_tier ?? 'pattern';
460
+ byTier[tier] = (byTier[tier] ?? 0) + 1;
461
+ if (rule.test_cases) {
462
+ totalTP += rule.test_cases.true_positives?.length ?? 0;
463
+ totalTN += rule.test_cases.true_negatives?.length ?? 0;
464
+ }
465
+ const evasion = rule['evasion_tests'];
466
+ if (Array.isArray(evasion))
467
+ totalEvasion += evasion.length;
468
+ if (rule.references?.cve && rule.references.cve.length > 0)
469
+ withCVE++;
470
+ }
471
+ if (jsonOutput) {
472
+ console.log(JSON.stringify({
473
+ totalRules: rules.length,
474
+ byCategory, bySeverity, byMaturity, byTier,
475
+ testCases: { truePositives: totalTP, trueNegatives: totalTN, evasionTests: totalEvasion },
476
+ rulesWithCVE: withCVE,
477
+ }, null, 2));
478
+ return;
479
+ }
480
+ console.log(`\n${BOLD}ATR Rule Collection Statistics${RESET}`);
481
+ console.log(`${DIM}${'─'.repeat(60)}${RESET}`);
482
+ console.log(` Total rules: ${rules.length}`);
483
+ console.log(` True positives: ${totalTP}`);
484
+ console.log(` True negatives: ${totalTN}`);
485
+ console.log(` Evasion tests: ${totalEvasion}`);
486
+ console.log(` Rules with CVE: ${withCVE}`);
487
+ console.log(`\n ${BOLD}By Category:${RESET}`);
488
+ for (const [cat, count] of Object.entries(byCategory).sort((a, b) => b[1] - a[1])) {
489
+ console.log(` ${cat.padEnd(24)} ${count}`);
490
+ }
491
+ console.log(`\n ${BOLD}By Severity:${RESET}`);
492
+ for (const sev of ['critical', 'high', 'medium', 'low', 'informational']) {
493
+ if (bySeverity[sev]) {
494
+ const color = SEVERITY_COLORS[sev] ?? '';
495
+ console.log(` ${color}${sev.padEnd(24)}${RESET} ${bySeverity[sev]}`);
496
+ }
497
+ }
498
+ console.log(`\n ${BOLD}By Maturity:${RESET}`);
499
+ for (const [mat, count] of Object.entries(byMaturity).sort((a, b) => b[1] - a[1])) {
500
+ console.log(` ${mat.padEnd(24)} ${count}`);
501
+ }
502
+ console.log(`\n ${BOLD}By Detection Tier:${RESET}`);
503
+ for (const [tier, count] of Object.entries(byTier).sort((a, b) => b[1] - a[1])) {
504
+ console.log(` ${tier.padEnd(24)} ${count}`);
505
+ }
506
+ console.log('');
507
+ }
508
+ // --- GUARD command ---
509
+ async function cmdGuard(options) {
510
+ const rulesDir = options['rules'] ? resolve(options['rules']) : RULES_DIR;
511
+ const dryRun = options['dry-run'] === 'true';
512
+ const failOpen = options['fail-open'] !== 'false';
513
+ const parsedTimeout = options['timeout'] ? parseInt(options['timeout'], 10) : 5000;
514
+ const timeoutMs = (Number.isFinite(parsedTimeout) && parsedTimeout > 0) ? parsedTimeout : 5000;
515
+ const { ActionExecutor } = await import('./action-executor.js');
516
+ const { StdioAdapter } = await import('./adapters/stdio-adapter.js');
517
+ const { HookHandler } = await import('./hook-handler.js');
518
+ const engine = new ATREngine({ rulesDir });
519
+ const ruleCount = await engine.loadRules();
520
+ const adapter = new StdioAdapter();
521
+ const executor = new ActionExecutor({ adapter, dryRun });
522
+ const handler = new HookHandler({ engine, executor, timeoutMs, failOpen });
523
+ process.stderr.write(`[atr-guard] Loaded ${ruleCount} rules from ${rulesDir}` +
524
+ `${dryRun ? ' (dry-run)' : ''}\n`);
525
+ await handler.startStdioLoop();
526
+ }
527
+ // --- MCP command ---
528
+ async function cmdMcp() {
529
+ const { startMCPServer } = await import('./mcp-server.js');
530
+ await startMCPServer();
531
+ }
532
+ // --- SCAFFOLD command ---
533
+ async function cmdScaffold() {
534
+ const { createInterface } = await import('node:readline');
535
+ const { RuleScaffolder } = await import('./rule-scaffolder.js');
536
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
537
+ const ask = (question) => new Promise((resolve) => rl.question(question, resolve));
538
+ console.log(`\n${BOLD}ATR Rule Scaffolder${RESET}`);
539
+ console.log(`${DIM}Generate a draft ATR detection rule interactively.${RESET}\n`);
540
+ const title = await ask('Rule title: ');
541
+ if (!title.trim()) {
542
+ console.error(`${RED}Error: Title is required.${RESET}`);
543
+ rl.close();
544
+ process.exit(1);
545
+ }
546
+ const categories = [
547
+ 'prompt-injection', 'tool-poisoning', 'context-exfiltration',
548
+ 'agent-manipulation', 'privilege-escalation', 'excessive-autonomy',
549
+ 'data-poisoning', 'model-abuse', 'skill-compromise',
550
+ ];
551
+ console.log(`\nCategories: ${categories.join(', ')}`);
552
+ const category = await ask('Category: ');
553
+ if (!categories.includes(category.trim())) {
554
+ console.error(`${RED}Error: Invalid category.${RESET}`);
555
+ rl.close();
556
+ process.exit(1);
557
+ }
558
+ const attackDescription = await ask('Attack description: ');
559
+ if (!attackDescription.trim()) {
560
+ console.error(`${RED}Error: Description is required.${RESET}`);
561
+ rl.close();
562
+ process.exit(1);
563
+ }
564
+ console.log('\nEnter example payloads (one per line, empty line to finish):');
565
+ const payloads = [];
566
+ while (true) {
567
+ const payload = await ask(` Payload ${payloads.length + 1}: `);
568
+ if (!payload.trim())
569
+ break;
570
+ payloads.push(payload.trim());
571
+ }
572
+ if (payloads.length === 0) {
573
+ console.error(`${RED}Error: At least one example payload is required.${RESET}`);
574
+ rl.close();
575
+ process.exit(1);
576
+ }
577
+ const severities = ['critical', 'high', 'medium', 'low', 'informational'];
578
+ const severity = await ask(`Severity [${severities.join('/')}] (default: medium): `);
579
+ const finalSeverity = severity.trim() && severities.includes(severity.trim())
580
+ ? severity.trim()
581
+ : 'medium';
582
+ rl.close();
583
+ const scaffolder = new RuleScaffolder();
584
+ const result = scaffolder.scaffold({
585
+ title: title.trim(),
586
+ category: category.trim(),
587
+ attackDescription: attackDescription.trim(),
588
+ examplePayloads: payloads,
589
+ severity: finalSeverity,
590
+ });
591
+ console.log(`\n${GREEN}Generated rule ${result.id}:${RESET}\n`);
592
+ console.log(`${DIM}${'─'.repeat(60)}${RESET}`);
593
+ console.log(result.yaml);
594
+ console.log(`${DIM}${'─'.repeat(60)}${RESET}`);
595
+ if (result.warnings.length > 0) {
596
+ console.log(`\n${BOLD}Warnings:${RESET}`);
597
+ for (const w of result.warnings) {
598
+ console.log(` - ${w}`);
599
+ }
600
+ }
601
+ console.log(`\n${DIM}Copy this YAML to a .yaml file in rules/${category.trim()}/ and validate with: atr validate <file>${RESET}\n`);
602
+ }
603
+ // --- INIT command ---
604
+ function cmdInit(options) {
605
+ const isGlobal = options['global'] === 'true';
606
+ const cwd = process.cwd();
607
+ // Detect Claude Code project (unless --global)
608
+ if (!isGlobal) {
609
+ const hasClaudeDir = existsSync(join(cwd, '.claude'));
610
+ const hasClaudeMd = existsSync(join(cwd, 'CLAUDE.md'));
611
+ if (!hasClaudeDir && !hasClaudeMd) {
612
+ console.error(`${RED}Error: Not a Claude Code project (no .claude/ directory or CLAUDE.md found).${RESET}\n` +
613
+ `Run this command from your project root, or use ${BOLD}atr init --global${RESET} to configure globally.`);
614
+ process.exit(1);
615
+ }
616
+ }
617
+ const hookEntry = {
618
+ // Empty matcher means "match all tools" in Claude Code hooks —
619
+ // this is intentional so every tool call is scanned against ATR rules.
620
+ matcher: '',
621
+ command: 'npx agent-threat-rules guard',
622
+ };
623
+ // Determine target settings file
624
+ const settingsPath = isGlobal
625
+ ? join(homedir(), '.claude', 'settings.json')
626
+ : join(cwd, '.claude', 'settings.local.json');
627
+ // Ensure parent directory exists
628
+ const settingsDir = dirname(settingsPath);
629
+ if (!existsSync(settingsDir)) {
630
+ mkdirSync(settingsDir, { recursive: true });
631
+ }
632
+ // Read existing settings or start fresh
633
+ let settings = {};
634
+ if (existsSync(settingsPath)) {
635
+ let raw;
636
+ try {
637
+ raw = readFileSync(settingsPath, 'utf-8');
638
+ }
639
+ catch (e) {
640
+ const code = e.code;
641
+ console.error(`${RED}Error: Cannot read ${settingsPath} (${code ?? 'permission denied'}). Check file permissions.${RESET}`);
642
+ process.exit(1);
643
+ }
644
+ try {
645
+ settings = JSON.parse(raw);
646
+ }
647
+ catch {
648
+ console.error(`${RED}Error: Invalid JSON in ${settingsPath}. Fix the syntax manually or delete it and retry.${RESET}`);
649
+ process.exit(1);
650
+ }
651
+ }
652
+ // Navigate into hooks.PreToolUse, creating structure as needed
653
+ if (!settings['hooks'] || typeof settings['hooks'] !== 'object') {
654
+ settings['hooks'] = {};
655
+ }
656
+ const hooks = settings['hooks'];
657
+ if (!Array.isArray(hooks['PreToolUse'])) {
658
+ hooks['PreToolUse'] = [];
659
+ }
660
+ const preToolUse = hooks['PreToolUse'];
661
+ // Check if hook is already configured (validate each element is an object before accessing .command)
662
+ const alreadyConfigured = preToolUse.some((entry) => typeof entry === 'object' &&
663
+ entry !== null &&
664
+ !Array.isArray(entry) &&
665
+ entry['command'] === hookEntry.command);
666
+ if (alreadyConfigured) {
667
+ console.log(`${GREEN}ATR guard hook already configured${RESET} in ${settingsPath}`);
668
+ return;
669
+ }
670
+ // Add the hook entry
671
+ preToolUse.push(hookEntry);
672
+ // Write back
673
+ writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n', 'utf-8');
674
+ const relPath = settingsPath.startsWith(cwd + sep)
675
+ ? settingsPath.slice(cwd.length + 1)
676
+ : settingsPath;
677
+ console.log(`\n${GREEN}ATR guard hook configured successfully.${RESET}\n`);
678
+ console.log(` File: ${relPath}`);
679
+ console.log(` Hook: PreToolUse -> npx agent-threat-rules guard\n`);
680
+ console.log(`${DIM}Every tool call Claude Code makes will now be scanned against ATR`);
681
+ console.log(`threat detection rules before execution. Suspicious actions will be`);
682
+ console.log(`flagged with severity and recommendation.${RESET}\n`);
683
+ }
684
+ // --- CONVERT command ---
685
+ async function cmdConvert(target, options) {
686
+ const validFormats = ['splunk', 'elastic', 'generic-regex'];
687
+ if (!target || !validFormats.includes(target)) {
688
+ console.error(`${RED}Error: Specify format: atr convert <splunk|elastic|generic-regex>${RESET}`);
689
+ process.exit(1);
690
+ }
691
+ const rulesDir = options['rules'] ? resolve(options['rules']) : RULES_DIR;
692
+ const outputFile = options['output'];
693
+ const jsonOutput = options['json'] === 'true';
694
+ // Generic regex export (JSON array of regex patterns with metadata)
695
+ if (target === 'generic-regex') {
696
+ const { loadRulesFromDirectory } = await import('./loader.js');
697
+ const { rulesToGenericRegex } = await import('./converters/generic-regex.js');
698
+ const rules = loadRulesFromDirectory(rulesDir);
699
+ const exported = rulesToGenericRegex(rules);
700
+ if (exported.length === 0) {
701
+ console.error(`${RED}Error: No rules with regex patterns found in ${rulesDir}${RESET}`);
702
+ process.exit(1);
703
+ }
704
+ const output = JSON.stringify(exported, null, 2);
705
+ if (outputFile) {
706
+ writeFileSync(resolve(outputFile), output, 'utf-8');
707
+ console.log(`${GREEN}Exported ${exported.length} rules (${exported.reduce((n, r) => n + r.patterns.length, 0)} patterns) to generic-regex format.${RESET}`);
708
+ console.log(` Output: ${resolve(outputFile)}`);
709
+ }
710
+ else {
711
+ console.log(output);
712
+ }
713
+ return;
714
+ }
715
+ const format = target;
716
+ const { convertAllRules } = await import('./converters/index.js');
717
+ const results = convertAllRules(rulesDir, format);
718
+ if (results.length === 0) {
719
+ console.error(`${RED}Error: No rules found in ${rulesDir}${RESET}`);
720
+ process.exit(1);
721
+ }
722
+ let output;
723
+ if (jsonOutput) {
724
+ output = JSON.stringify(results, null, 2);
725
+ }
726
+ else if (format === 'elastic') {
727
+ // For Elastic, JSON output is the natural format
728
+ output = JSON.stringify(results.map(r => JSON.parse(r.query)), null, 2);
729
+ }
730
+ else {
731
+ // For Splunk, emit each query separated by blank lines
732
+ output = results.map(r => r.query).join('\n\n' + '='.repeat(80) + '\n\n');
733
+ }
734
+ if (outputFile) {
735
+ writeFileSync(resolve(outputFile), output, 'utf-8');
736
+ console.log(`${GREEN}Converted ${results.length} rules to ${format} format.${RESET}`);
737
+ console.log(` Output: ${resolve(outputFile)}`);
738
+ }
739
+ else {
740
+ console.log(output);
741
+ }
742
+ }
743
+ // --- Badge command ---
744
+ function cmdBadge(target, options) {
745
+ if (!target) {
746
+ console.error(`${RED}Usage: atr badge <package-name> [--data <audit.json>] [--svg] [--json]${RESET}`);
747
+ process.exit(1);
748
+ }
749
+ // Find audit data
750
+ const dataPath = typeof options['data'] === 'string'
751
+ ? resolve(options['data'])
752
+ : resolve(__dirname, '..', 'data', 'audit-v3-full.json');
753
+ const altDataPath = resolve(__dirname, '..', 'data', 'audit-v3-sample.json');
754
+ let summary = null;
755
+ if (existsSync(dataPath)) {
756
+ summary = lookupPackageScan(dataPath, target);
757
+ }
758
+ if (!summary && existsSync(altDataPath)) {
759
+ summary = lookupPackageScan(altDataPath, target);
760
+ }
761
+ const outputSvg = options['svg'] === 'true';
762
+ const outputJson = options['json'] === 'true';
763
+ if (outputSvg) {
764
+ console.log(generateBadgeSvg(summary));
765
+ return;
766
+ }
767
+ if (outputJson) {
768
+ console.log(JSON.stringify(generateBadgeEndpoint(summary), null, 2));
769
+ return;
770
+ }
771
+ // Default: human-readable output
772
+ const endpoint = generateBadgeEndpoint(summary);
773
+ console.log(`\n${BOLD}ATR Badge: ${target}${RESET}`);
774
+ console.log(`${'─'.repeat(50)}`);
775
+ if (summary) {
776
+ const colorMap = {
777
+ '#2ea44f': GREEN,
778
+ '#dfb317': '\x1b[33m',
779
+ '#e05d44': RED,
780
+ '#9f9f9f': DIM,
781
+ };
782
+ const termColor = colorMap[endpoint.color] ?? '';
783
+ console.log(` Status: ${termColor}${endpoint.message}${RESET}`);
784
+ console.log(` Package: ${summary.packageName}@${summary.version ?? 'unknown'}`);
785
+ console.log(` Risk: ${summary.riskLevel} (score: ${summary.riskScore})`);
786
+ console.log(` Findings: critical=${summary.findings.critical} high=${summary.findings.high} medium=${summary.findings.medium} low=${summary.findings.low}`);
787
+ if (summary.scannedAt) {
788
+ console.log(` Scanned: ${summary.scannedAt}`);
789
+ }
790
+ }
791
+ else {
792
+ console.log(` ${DIM}No scan data found for "${target}"${RESET}`);
793
+ console.log(` ${DIM}Run: atr badge ${target} --data <audit-results.json>${RESET}`);
794
+ }
795
+ console.log(`\n${BOLD}Embed:${RESET}`);
796
+ console.log(` ${DIM}Markdown:${RESET} ${generateBadgeMarkdown(target)}`);
797
+ console.log(` ${DIM}SVG:${RESET} atr badge ${target} --svg > badge.svg`);
798
+ console.log(` ${DIM}JSON:${RESET} atr badge ${target} --json`);
799
+ console.log();
800
+ }
801
+ // --- Main ---
802
+ async function main() {
803
+ const { command, target, options } = parseArgs(process.argv);
804
+ if (command === 'help' || command === '--help' || command === '-h' || options['help']) {
805
+ printUsage();
806
+ return;
807
+ }
808
+ if (command === 'version' || command === '--version' || command === '-v') {
809
+ const pkgPath = resolve(__dirname, '..', 'package.json');
810
+ const pkg = JSON.parse(readFileSync(pkgPath, 'utf-8'));
811
+ console.log(pkg.version);
812
+ return;
813
+ }
814
+ const rulesDir = options['rules'] ? resolve(options['rules']) : RULES_DIR;
815
+ switch (command) {
816
+ case 'scan':
817
+ await cmdScanUnified(target, rulesDir, {
818
+ json: options['json'] === 'true',
819
+ sarif: options['sarif'] === 'true',
820
+ severity: options['severity'],
821
+ reportToCloud: options['report-to-cloud'] === 'true',
822
+ tcUrl: options['tc-url'],
823
+ });
824
+ break;
825
+ case 'scan-skill':
826
+ await cmdScanUnified(target, rulesDir, {
827
+ json: options['json'] === 'true',
828
+ sarif: options['sarif'] === 'true',
829
+ severity: options['severity'],
830
+ forceType: 'skill',
831
+ reportToCloud: options['report-to-cloud'] === 'true',
832
+ tcUrl: options['tc-url'],
833
+ });
834
+ break;
835
+ case 'validate':
836
+ cmdValidate(target, options);
837
+ break;
838
+ case 'test':
839
+ await cmdTest(target, options);
840
+ break;
841
+ case 'stats':
842
+ cmdStats(options);
843
+ break;
844
+ case 'convert':
845
+ await cmdConvert(target, options);
846
+ break;
847
+ case 'guard':
848
+ await cmdGuard(options);
849
+ break;
850
+ case 'init':
851
+ cmdInit(options);
852
+ break;
853
+ case 'mcp':
854
+ await cmdMcp();
855
+ break;
856
+ case 'scaffold':
857
+ await cmdScaffold();
858
+ break;
859
+ case 'badge':
860
+ cmdBadge(target, options);
861
+ break;
862
+ case 'tc': {
863
+ const { cmdTCSync, cmdTCPull, cmdTCCrystallize, cmdTCStatus } = await import('./cli/tc-pipeline.js');
864
+ const subcommand = target;
865
+ switch (subcommand) {
866
+ case 'status':
867
+ await cmdTCStatus(options);
868
+ break;
869
+ case 'sync':
870
+ await cmdTCSync(options);
871
+ break;
872
+ case 'pull':
873
+ await cmdTCPull(options);
874
+ break;
875
+ case 'crystallize':
876
+ await cmdTCCrystallize(options);
877
+ break;
878
+ default:
879
+ console.error(`${RED}Unknown tc subcommand: ${subcommand}. Use: status, sync, pull, crystallize${RESET}`);
880
+ process.exit(1);
881
+ }
882
+ break;
883
+ }
884
+ default:
885
+ console.error(`${RED}Unknown command: ${command}${RESET}`);
886
+ printUsage();
887
+ process.exit(1);
888
+ }
889
+ }
890
+ main().catch(err => {
891
+ console.error(`${RED}Error: ${err instanceof Error ? err.message : String(err)}${RESET}`);
892
+ process.exit(1);
893
+ });
894
+ //# sourceMappingURL=cli.js.map