@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.
- package/dist/action-executor.d.ts +44 -0
- package/dist/action-executor.d.ts.map +1 -0
- package/dist/action-executor.js +130 -0
- package/dist/action-executor.js.map +1 -0
- package/dist/adapters/default-adapter.d.ts +24 -0
- package/dist/adapters/default-adapter.d.ts.map +1 -0
- package/dist/adapters/default-adapter.js +51 -0
- package/dist/adapters/default-adapter.js.map +1 -0
- package/dist/adapters/stdio-adapter.d.ts +30 -0
- package/dist/adapters/stdio-adapter.d.ts.map +1 -0
- package/dist/adapters/stdio-adapter.js +128 -0
- package/dist/adapters/stdio-adapter.js.map +1 -0
- package/dist/badge.d.ts +42 -0
- package/dist/badge.d.ts.map +1 -0
- package/dist/badge.js +163 -0
- package/dist/badge.js.map +1 -0
- package/dist/capability-extractor.d.ts +35 -0
- package/dist/capability-extractor.d.ts.map +1 -0
- package/dist/capability-extractor.js +91 -0
- package/dist/capability-extractor.js.map +1 -0
- package/dist/cli/scan-handler.d.ts +21 -0
- package/dist/cli/scan-handler.d.ts.map +1 -0
- package/dist/cli/scan-handler.js +276 -0
- package/dist/cli/scan-handler.js.map +1 -0
- package/dist/cli/tc-pipeline.d.ts +18 -0
- package/dist/cli/tc-pipeline.d.ts.map +1 -0
- package/dist/cli/tc-pipeline.js +295 -0
- package/dist/cli/tc-pipeline.js.map +1 -0
- package/dist/cli.d.ts +12 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +894 -0
- package/dist/cli.js.map +1 -0
- package/dist/content-hash.d.ts +7 -0
- package/dist/content-hash.d.ts.map +1 -0
- package/dist/content-hash.js +10 -0
- package/dist/content-hash.js.map +1 -0
- package/dist/converters/elastic.d.ts +36 -0
- package/dist/converters/elastic.d.ts.map +1 -0
- package/dist/converters/elastic.js +125 -0
- package/dist/converters/elastic.js.map +1 -0
- package/dist/converters/generic-regex.d.ts +37 -0
- package/dist/converters/generic-regex.d.ts.map +1 -0
- package/dist/converters/generic-regex.js +59 -0
- package/dist/converters/generic-regex.js.map +1 -0
- package/dist/converters/index.d.ts +32 -0
- package/dist/converters/index.d.ts.map +1 -0
- package/dist/converters/index.js +38 -0
- package/dist/converters/index.js.map +1 -0
- package/dist/converters/sarif.d.ts +18 -0
- package/dist/converters/sarif.d.ts.map +1 -0
- package/dist/converters/sarif.js +142 -0
- package/dist/converters/sarif.js.map +1 -0
- package/dist/converters/splunk.d.ts +19 -0
- package/dist/converters/splunk.d.ts.map +1 -0
- package/dist/converters/splunk.js +148 -0
- package/dist/converters/splunk.js.map +1 -0
- package/dist/coverage-analyzer.d.ts +43 -0
- package/dist/coverage-analyzer.d.ts.map +1 -0
- package/dist/coverage-analyzer.js +329 -0
- package/dist/coverage-analyzer.js.map +1 -0
- package/dist/embedding/build-corpus.d.ts +15 -0
- package/dist/embedding/build-corpus.d.ts.map +1 -0
- package/dist/embedding/build-corpus.js +105 -0
- package/dist/embedding/build-corpus.js.map +1 -0
- package/dist/embedding/model-loader.d.ts +41 -0
- package/dist/embedding/model-loader.d.ts.map +1 -0
- package/dist/embedding/model-loader.js +90 -0
- package/dist/embedding/model-loader.js.map +1 -0
- package/dist/embedding/vector-store.d.ts +41 -0
- package/dist/embedding/vector-store.d.ts.map +1 -0
- package/dist/embedding/vector-store.js +70 -0
- package/dist/embedding/vector-store.js.map +1 -0
- package/dist/engine.d.ts +222 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +1185 -0
- package/dist/engine.js.map +1 -0
- package/dist/eval/corpus.d.ts +42 -0
- package/dist/eval/corpus.d.ts.map +1 -0
- package/dist/eval/corpus.js +427 -0
- package/dist/eval/corpus.js.map +1 -0
- package/dist/eval/eval-harness.d.ts +44 -0
- package/dist/eval/eval-harness.d.ts.map +1 -0
- package/dist/eval/eval-harness.js +296 -0
- package/dist/eval/eval-harness.js.map +1 -0
- package/dist/eval/index.d.ts +13 -0
- package/dist/eval/index.d.ts.map +1 -0
- package/dist/eval/index.js +9 -0
- package/dist/eval/index.js.map +1 -0
- package/dist/eval/metrics.d.ts +74 -0
- package/dist/eval/metrics.d.ts.map +1 -0
- package/dist/eval/metrics.js +108 -0
- package/dist/eval/metrics.js.map +1 -0
- package/dist/eval/pint-corpus.d.ts +34 -0
- package/dist/eval/pint-corpus.d.ts.map +1 -0
- package/dist/eval/pint-corpus.js +113 -0
- package/dist/eval/pint-corpus.js.map +1 -0
- package/dist/eval/rule-corpus.d.ts +9 -0
- package/dist/eval/rule-corpus.d.ts.map +1 -0
- package/dist/eval/rule-corpus.js +4780 -0
- package/dist/eval/rule-corpus.js.map +1 -0
- package/dist/eval/rule-metrics.d.ts +34 -0
- package/dist/eval/rule-metrics.d.ts.map +1 -0
- package/dist/eval/rule-metrics.js +92 -0
- package/dist/eval/rule-metrics.js.map +1 -0
- package/dist/eval/run-eval.d.ts +7 -0
- package/dist/eval/run-eval.d.ts.map +1 -0
- package/dist/eval/run-eval.js +11 -0
- package/dist/eval/run-eval.js.map +1 -0
- package/dist/eval/run-pint-benchmark.d.ts +18 -0
- package/dist/eval/run-pint-benchmark.d.ts.map +1 -0
- package/dist/eval/run-pint-benchmark.js +159 -0
- package/dist/eval/run-pint-benchmark.js.map +1 -0
- package/dist/eval/skill-benchmark.d.ts +66 -0
- package/dist/eval/skill-benchmark.d.ts.map +1 -0
- package/dist/eval/skill-benchmark.js +194 -0
- package/dist/eval/skill-benchmark.js.map +1 -0
- package/dist/flywheel.d.ts +54 -0
- package/dist/flywheel.d.ts.map +1 -0
- package/dist/flywheel.js +121 -0
- package/dist/flywheel.js.map +1 -0
- package/dist/hook-handler.d.ts +61 -0
- package/dist/hook-handler.d.ts.map +1 -0
- package/dist/hook-handler.js +178 -0
- package/dist/hook-handler.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/{src/index.ts → dist/index.js} +1 -0
- package/dist/index.js.map +1 -0
- package/dist/layer-integration.d.ts +55 -0
- package/dist/layer-integration.d.ts.map +1 -0
- package/dist/layer-integration.js +187 -0
- package/dist/layer-integration.js.map +1 -0
- package/dist/loader.d.ts +18 -0
- package/dist/loader.d.ts.map +1 -0
- package/dist/loader.js +129 -0
- package/dist/loader.js.map +1 -0
- package/dist/mcp-server.d.ts +13 -0
- package/dist/mcp-server.d.ts.map +1 -0
- package/dist/mcp-server.js +246 -0
- package/dist/mcp-server.js.map +1 -0
- package/dist/mcp-tools/coverage-gaps.d.ts +13 -0
- package/dist/mcp-tools/coverage-gaps.d.ts.map +1 -0
- package/dist/mcp-tools/coverage-gaps.js +55 -0
- package/dist/mcp-tools/coverage-gaps.js.map +1 -0
- package/dist/mcp-tools/list-rules.d.ts +17 -0
- package/dist/mcp-tools/list-rules.d.ts.map +1 -0
- package/dist/mcp-tools/list-rules.js +45 -0
- package/dist/mcp-tools/list-rules.js.map +1 -0
- package/dist/mcp-tools/scan-skill.d.ts +17 -0
- package/dist/mcp-tools/scan-skill.d.ts.map +1 -0
- package/dist/mcp-tools/scan-skill.js +65 -0
- package/dist/mcp-tools/scan-skill.js.map +1 -0
- package/dist/mcp-tools/scan.d.ts +24 -0
- package/dist/mcp-tools/scan.d.ts.map +1 -0
- package/dist/mcp-tools/scan.js +94 -0
- package/dist/mcp-tools/scan.js.map +1 -0
- package/dist/mcp-tools/submit-proposal.d.ts +12 -0
- package/dist/mcp-tools/submit-proposal.d.ts.map +1 -0
- package/dist/mcp-tools/submit-proposal.js +103 -0
- package/dist/mcp-tools/submit-proposal.js.map +1 -0
- package/dist/mcp-tools/threat-summary.d.ts +12 -0
- package/dist/mcp-tools/threat-summary.d.ts.map +1 -0
- package/dist/mcp-tools/threat-summary.js +74 -0
- package/dist/mcp-tools/threat-summary.js.map +1 -0
- package/dist/mcp-tools/validate.d.ts +15 -0
- package/dist/mcp-tools/validate.d.ts.map +1 -0
- package/dist/mcp-tools/validate.js +51 -0
- package/dist/mcp-tools/validate.js.map +1 -0
- package/dist/modules/embedding.d.ts +71 -0
- package/dist/modules/embedding.d.ts.map +1 -0
- package/dist/modules/embedding.js +141 -0
- package/dist/modules/embedding.js.map +1 -0
- package/dist/modules/index.d.ts +144 -0
- package/dist/modules/index.d.ts.map +1 -0
- package/dist/modules/index.js +82 -0
- package/dist/modules/index.js.map +1 -0
- package/dist/modules/semantic.d.ts +106 -0
- package/dist/modules/semantic.d.ts.map +1 -0
- package/dist/modules/semantic.js +359 -0
- package/dist/modules/semantic.js.map +1 -0
- package/dist/modules/session.d.ts +70 -0
- package/dist/modules/session.d.ts.map +1 -0
- package/dist/modules/session.js +128 -0
- package/dist/modules/session.js.map +1 -0
- package/dist/quality/adapters/atr.d.ts +65 -0
- package/dist/quality/adapters/atr.d.ts.map +1 -0
- package/dist/quality/adapters/atr.js +154 -0
- package/dist/quality/adapters/atr.js.map +1 -0
- package/dist/quality/adapters/index.d.ts +10 -0
- package/dist/quality/adapters/index.d.ts.map +1 -0
- package/dist/quality/adapters/index.js +10 -0
- package/dist/quality/adapters/index.js.map +1 -0
- package/dist/quality/compute-confidence.d.ts +45 -0
- package/dist/quality/compute-confidence.d.ts.map +1 -0
- package/dist/quality/compute-confidence.js +133 -0
- package/dist/quality/compute-confidence.js.map +1 -0
- package/dist/quality/index.d.ts +36 -0
- package/dist/quality/index.d.ts.map +1 -0
- package/dist/quality/index.js +39 -0
- package/dist/quality/index.js.map +1 -0
- package/dist/quality/quality-gate.d.ts +86 -0
- package/dist/quality/quality-gate.d.ts.map +1 -0
- package/dist/quality/quality-gate.js +187 -0
- package/dist/quality/quality-gate.js.map +1 -0
- package/dist/quality/types.d.ts +129 -0
- package/dist/quality/types.d.ts.map +1 -0
- package/dist/quality/types.js +10 -0
- package/dist/quality/types.js.map +1 -0
- package/dist/quality/validate-maturity.d.ts +51 -0
- package/dist/quality/validate-maturity.d.ts.map +1 -0
- package/dist/quality/validate-maturity.js +134 -0
- package/dist/quality/validate-maturity.js.map +1 -0
- package/dist/quality.d.ts +8 -0
- package/dist/quality.d.ts.map +1 -0
- package/dist/quality.js +8 -0
- package/dist/quality.js.map +1 -0
- package/dist/rule-scaffolder.d.ts +53 -0
- package/dist/rule-scaffolder.d.ts.map +1 -0
- package/dist/rule-scaffolder.js +301 -0
- package/dist/rule-scaffolder.js.map +1 -0
- package/dist/session-tracker.d.ts +58 -0
- package/dist/session-tracker.d.ts.map +1 -0
- package/dist/session-tracker.js +176 -0
- package/dist/session-tracker.js.map +1 -0
- package/dist/shadow-evaluator.d.ts +48 -0
- package/dist/shadow-evaluator.d.ts.map +1 -0
- package/dist/shadow-evaluator.js +129 -0
- package/dist/shadow-evaluator.js.map +1 -0
- package/dist/skill-fingerprint.d.ts +85 -0
- package/dist/skill-fingerprint.d.ts.map +1 -0
- package/dist/skill-fingerprint.js +284 -0
- package/dist/skill-fingerprint.js.map +1 -0
- package/dist/tc-reporter.d.ts +50 -0
- package/dist/tc-reporter.d.ts.map +1 -0
- package/dist/tc-reporter.js +164 -0
- package/dist/tc-reporter.js.map +1 -0
- package/dist/tier0-invariant.d.ts +49 -0
- package/dist/tier0-invariant.d.ts.map +1 -0
- package/dist/tier0-invariant.js +185 -0
- package/dist/tier0-invariant.js.map +1 -0
- package/dist/tier1-blacklist.d.ts +48 -0
- package/dist/tier1-blacklist.d.ts.map +1 -0
- package/dist/tier1-blacklist.js +92 -0
- package/dist/tier1-blacklist.js.map +1 -0
- package/dist/types.d.ts +232 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +6 -0
- package/dist/types.js.map +1 -0
- package/dist/verdict.d.ts +26 -0
- package/dist/verdict.d.ts.map +1 -0
- package/dist/verdict.js +127 -0
- package/dist/verdict.js.map +1 -0
- package/package.json +16 -4
- package/.github/ISSUE_TEMPLATE/evasion-report.yml +0 -75
- package/.github/ISSUE_TEMPLATE/false-positive.yml +0 -31
- package/.github/ISSUE_TEMPLATE/mirofish-prediction.yml +0 -128
- package/.github/ISSUE_TEMPLATE/new-rule.yml +0 -37
- package/.github/PULL_REQUEST_TEMPLATE.md +0 -23
- package/.github/workflows/rule-quality.yml +0 -203
- package/.github/workflows/validate.yml +0 -42
- package/CHANGELOG.md +0 -30
- package/CONTRIBUTING.md +0 -168
- package/CONTRIBUTORS.md +0 -28
- package/COVERAGE.md +0 -135
- package/LIMITATIONS.md +0 -154
- package/SECURITY.md +0 -48
- package/THREAT-MODEL.md +0 -243
- package/docs/contribution-paths.md +0 -202
- package/docs/mirofish-prediction-guide.md +0 -304
- package/docs/quick-start.md +0 -245
- package/docs/rule-writing-guide.md +0 -647
- package/docs/schema-spec.md +0 -594
- package/examples/how-to-write-a-rule.md +0 -251
- 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
|