@chainpatrol/cli 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +224 -0
- package/dist/breakdown-JVN66HY3.js +69 -0
- package/dist/chunk-D2QGXYXZ.js +126 -0
- package/dist/chunk-E2LAMILJ.js +48 -0
- package/dist/chunk-EEG7T6WT.js +287 -0
- package/dist/chunk-H7UKKLCV.js +6572 -0
- package/dist/chunk-IUZB3DQW.js +237 -0
- package/dist/chunk-JCMWDZYY.js +39 -0
- package/dist/chunk-U73SABXK.js +46 -0
- package/dist/chunk-VFT3TD3E.js +82 -0
- package/dist/cli.js +895 -0
- package/dist/completions-EGQIARFC.js +12 -0
- package/dist/config-Z3TASRME.js +10 -0
- package/dist/configs-update-BK2S6AZ6.js +101 -0
- package/dist/create-4SQUBQI7.js +128 -0
- package/dist/drift-VRZKQC4P.js +80 -0
- package/dist/found-4O3AISNI.js +93 -0
- package/dist/healthcheck-7DR5MGEQ.js +94 -0
- package/dist/list-6L7XR4SZ.js +94 -0
- package/dist/list-HZAHEHDM.js +40 -0
- package/dist/list-IBMM562A.js +139 -0
- package/dist/list-json-TPBLJBD3.js +24 -0
- package/dist/login-G7LPHKDR.js +162 -0
- package/dist/login-json-LKB72OFY.js +71 -0
- package/dist/logout-LA7VEKON.js +25 -0
- package/dist/logout-json-4GIJZJ46.js +18 -0
- package/dist/run-PABQKATZ.js +112 -0
- package/dist/run-U62KVNTH.js +34 -0
- package/dist/setup-skill-U24CJZ6T.js +211 -0
- package/dist/snapshot-JEVDTE74.js +88 -0
- package/dist/summary-YG5NYIOA.js +61 -0
- package/dist/validate-PI7GPT5I.js +79 -0
- package/package.json +50 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import {
|
|
2
|
+
installCompletions,
|
|
3
|
+
uninstallCompletions
|
|
4
|
+
} from "./chunk-IUZB3DQW.js";
|
|
5
|
+
|
|
6
|
+
// src/commands/setup-skill.ts
|
|
7
|
+
import { mkdirSync, writeFileSync, existsSync, readFileSync, rmSync } from "fs";
|
|
8
|
+
import { join } from "path";
|
|
9
|
+
import { homedir } from "os";
|
|
10
|
+
var SKILL_DIR = join(homedir(), ".claude", "skills", "chainpatrol");
|
|
11
|
+
var SKILL_FILE = join(SKILL_DIR, "SKILL.md");
|
|
12
|
+
var SKILL_CONTENT = `---
|
|
13
|
+
name: chainpatrol
|
|
14
|
+
description: |
|
|
15
|
+
ChainPatrol CLI assistant. Helps use the chainpatrol CLI tool: login via device
|
|
16
|
+
code flow, check auth status, list detection configs, and run CLI commands.
|
|
17
|
+
Use when: "chainpatrol cli", "login to chainpatrol", "check detection configs",
|
|
18
|
+
"am I logged in", "list configs", "use the cli".
|
|
19
|
+
allowed-tools:
|
|
20
|
+
- Bash
|
|
21
|
+
- Read
|
|
22
|
+
- Grep
|
|
23
|
+
- Glob
|
|
24
|
+
---
|
|
25
|
+
|
|
26
|
+
# ChainPatrol CLI Skill
|
|
27
|
+
|
|
28
|
+
You are a ChainPatrol CLI assistant. Help the user interact with the ChainPatrol
|
|
29
|
+
platform using the CLI tool.
|
|
30
|
+
|
|
31
|
+
## Running the CLI
|
|
32
|
+
|
|
33
|
+
IMPORTANT: Claude Code's sandbox shell has a minimal PATH (/usr/bin:/bin:/usr/sbin:/sbin)
|
|
34
|
+
that does NOT include /usr/local/bin. Always use the full path to the binary:
|
|
35
|
+
|
|
36
|
+
\`\`\`bash
|
|
37
|
+
/usr/local/bin/chainpatrol <command> [options]
|
|
38
|
+
\`\`\`
|
|
39
|
+
|
|
40
|
+
All examples below use the short name for readability, but you MUST use
|
|
41
|
+
\`/usr/local/bin/chainpatrol\` in all Bash commands.
|
|
42
|
+
|
|
43
|
+
## Available Commands
|
|
44
|
+
|
|
45
|
+
### \`login\` \u2014 Authenticate with ChainPatrol
|
|
46
|
+
|
|
47
|
+
Uses the OAuth Device Code flow (RFC 8628):
|
|
48
|
+
1. CLI requests a device code from the server
|
|
49
|
+
2. User is shown a code and a URL to visit
|
|
50
|
+
3. User authorizes in the browser
|
|
51
|
+
4. CLI polls for the token
|
|
52
|
+
|
|
53
|
+
\`\`\`bash
|
|
54
|
+
chainpatrol login
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
JSON mode (for automation):
|
|
58
|
+
\`\`\`bash
|
|
59
|
+
chainpatrol --json login
|
|
60
|
+
\`\`\`
|
|
61
|
+
|
|
62
|
+
### \`logout\` \u2014 Clear stored credentials
|
|
63
|
+
|
|
64
|
+
\`\`\`bash
|
|
65
|
+
chainpatrol logout
|
|
66
|
+
\`\`\`
|
|
67
|
+
|
|
68
|
+
### \`configs list\` \u2014 List detection configurations
|
|
69
|
+
|
|
70
|
+
Requires authentication and an organization slug.
|
|
71
|
+
|
|
72
|
+
\`\`\`bash
|
|
73
|
+
chainpatrol configs list --org <slug>
|
|
74
|
+
\`\`\`
|
|
75
|
+
|
|
76
|
+
JSON mode:
|
|
77
|
+
\`\`\`bash
|
|
78
|
+
chainpatrol --json configs list --org <slug>
|
|
79
|
+
\`\`\`
|
|
80
|
+
|
|
81
|
+
The \`--org\` flag is saved for future commands. Once set, you can omit it:
|
|
82
|
+
\`\`\`bash
|
|
83
|
+
chainpatrol configs list
|
|
84
|
+
\`\`\`
|
|
85
|
+
|
|
86
|
+
## Checking Auth Status
|
|
87
|
+
|
|
88
|
+
To check if the user is logged in, read the credentials file:
|
|
89
|
+
|
|
90
|
+
\`\`\`bash
|
|
91
|
+
cat ~/.chainpatrol/credentials.json 2>/dev/null && echo "Logged in" || echo "Not logged in"
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
Or start the login flow which will detect existing sessions:
|
|
95
|
+
\`\`\`bash
|
|
96
|
+
chainpatrol --json login
|
|
97
|
+
\`\`\`
|
|
98
|
+
|
|
99
|
+
## Configuration
|
|
100
|
+
|
|
101
|
+
Config is stored at \`~/.chainpatrol/config.json\`:
|
|
102
|
+
- \`apiUrl\` \u2014 API base URL (default: \`https://app.chainpatrol.io\`)
|
|
103
|
+
- \`defaultOrg\` \u2014 Saved organization slug
|
|
104
|
+
|
|
105
|
+
Override config dir with \`CHAINPATROL_CONFIG_DIR\` env var.
|
|
106
|
+
|
|
107
|
+
## Global Flags
|
|
108
|
+
|
|
109
|
+
| Flag | Description |
|
|
110
|
+
|-------------|--------------------------------------|
|
|
111
|
+
| \`--json\` | Machine-readable JSON output |
|
|
112
|
+
| \`--org\` | Organization slug (saved for later) |
|
|
113
|
+
| \`--help\` | Show help |
|
|
114
|
+
| \`--version\` | Show version |
|
|
115
|
+
|
|
116
|
+
## Workflow
|
|
117
|
+
|
|
118
|
+
When the user asks to use the CLI, follow this order:
|
|
119
|
+
|
|
120
|
+
1. **Check login status** \u2014 Read \`~/.chainpatrol/credentials.json\` to see if they're logged in
|
|
121
|
+
2. **Login if needed** \u2014 Run the login command and guide them through the device code flow
|
|
122
|
+
3. **Set org if needed** \u2014 Ensure \`--org\` is provided or already saved in config
|
|
123
|
+
4. **Run the requested command** \u2014 Execute the CLI command and show results
|
|
124
|
+
|
|
125
|
+
## Detection Config Output
|
|
126
|
+
|
|
127
|
+
The \`configs list\` command shows detection sources grouped by:
|
|
128
|
+
- **Configured** \u2014 Sources with org-level configs (enabled/disabled with details)
|
|
129
|
+
- **Global** \u2014 Sources that run globally for all orgs (CERTSTREAM, ASSET_CHECK, BLOCKLIST, etc.)
|
|
130
|
+
- **Not Configured** \u2014 Sources available but not yet set up for the org
|
|
131
|
+
|
|
132
|
+
Each config entry includes: title, status, cron schedule, and configuration parameters.
|
|
133
|
+
`;
|
|
134
|
+
var LOGO = `
|
|
135
|
+
\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2588\u2588\u2588\u2588\u2557
|
|
136
|
+
\u2588\u2588\u2554\u2550\u2550\u2550\u2550\u255D\u2588\u2588\u2554\u2550\u2550\u2588\u2588\u2557
|
|
137
|
+
\u2588\u2588\u2551 \u2588\u2588\u2588\u2588\u2588\u2588\u2554\u255D
|
|
138
|
+
\u2588\u2588\u2551 \u2588\u2588\u2554\u2550\u2550\u2550\u255D
|
|
139
|
+
\u255A\u2588\u2588\u2588\u2588\u2588\u2588\u2557\u2588\u2588\u2551
|
|
140
|
+
\u255A\u2550\u2550\u2550\u2550\u2550\u255D\u255A\u2550\u255D
|
|
141
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
142
|
+
C H A I N P A T R O L
|
|
143
|
+
\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
|
|
144
|
+
`;
|
|
145
|
+
function setupSkill(options) {
|
|
146
|
+
const alreadyExists = existsSync(SKILL_FILE);
|
|
147
|
+
if (alreadyExists) {
|
|
148
|
+
const existing = readFileSync(SKILL_FILE, "utf-8");
|
|
149
|
+
if (existing === SKILL_CONTENT) {
|
|
150
|
+
if (options.json) {
|
|
151
|
+
console.log(JSON.stringify({ status: "up-to-date", path: SKILL_FILE }));
|
|
152
|
+
} else {
|
|
153
|
+
console.log("Claude Code skill is already up to date.");
|
|
154
|
+
}
|
|
155
|
+
return;
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
mkdirSync(SKILL_DIR, { recursive: true });
|
|
159
|
+
writeFileSync(SKILL_FILE, SKILL_CONTENT, { mode: 420 });
|
|
160
|
+
const completionResult = installCompletions();
|
|
161
|
+
if (options.json) {
|
|
162
|
+
console.log(
|
|
163
|
+
JSON.stringify({
|
|
164
|
+
status: alreadyExists ? "updated" : "installed",
|
|
165
|
+
path: SKILL_FILE,
|
|
166
|
+
completions: completionResult
|
|
167
|
+
})
|
|
168
|
+
);
|
|
169
|
+
} else {
|
|
170
|
+
console.log(LOGO);
|
|
171
|
+
console.log(
|
|
172
|
+
alreadyExists ? `Updated Claude Code skill at ${SKILL_FILE}` : `Installed Claude Code skill at ${SKILL_FILE}`
|
|
173
|
+
);
|
|
174
|
+
console.log("You can now use /chainpatrol in Claude Code from any project.");
|
|
175
|
+
if (completionResult.installed) {
|
|
176
|
+
console.log(
|
|
177
|
+
`Installed ${completionResult.shell} completions at ${completionResult.path}`
|
|
178
|
+
);
|
|
179
|
+
if (completionResult.configuredShellRc) {
|
|
180
|
+
console.log("Added completion config to ~/.zshrc");
|
|
181
|
+
}
|
|
182
|
+
console.log('Run "exec zsh" or open a new terminal to enable tab completion.');
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
function uninstallSkill(options) {
|
|
187
|
+
if (!existsSync(SKILL_FILE)) {
|
|
188
|
+
if (options.json) {
|
|
189
|
+
console.log(JSON.stringify({ status: "not-installed" }));
|
|
190
|
+
} else {
|
|
191
|
+
console.log("Claude Code skill is not installed.");
|
|
192
|
+
}
|
|
193
|
+
return;
|
|
194
|
+
}
|
|
195
|
+
rmSync(SKILL_DIR, { recursive: true });
|
|
196
|
+
const removedCompletions = uninstallCompletions();
|
|
197
|
+
if (options.json) {
|
|
198
|
+
console.log(
|
|
199
|
+
JSON.stringify({ status: "uninstalled", path: SKILL_FILE, removedCompletions })
|
|
200
|
+
);
|
|
201
|
+
} else {
|
|
202
|
+
console.log("Removed Claude Code skill from " + SKILL_DIR);
|
|
203
|
+
if (removedCompletions) {
|
|
204
|
+
console.log("Removed shell completions.");
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
export {
|
|
209
|
+
setupSkill,
|
|
210
|
+
uninstallSkill
|
|
211
|
+
};
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CliExitError,
|
|
3
|
+
ExitCode
|
|
4
|
+
} from "./chunk-E2LAMILJ.js";
|
|
5
|
+
import {
|
|
6
|
+
printOutput,
|
|
7
|
+
toCsvRows
|
|
8
|
+
} from "./chunk-VFT3TD3E.js";
|
|
9
|
+
import {
|
|
10
|
+
createApiClient
|
|
11
|
+
} from "./chunk-H7UKKLCV.js";
|
|
12
|
+
import "./chunk-EEG7T6WT.js";
|
|
13
|
+
import "./chunk-U73SABXK.js";
|
|
14
|
+
|
|
15
|
+
// src/commands/queues/snapshot.ts
|
|
16
|
+
async function runQueuesSnapshot(options) {
|
|
17
|
+
const client = options.apiClient ?? createApiClient();
|
|
18
|
+
const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
|
|
19
|
+
const result = await client.getOperationsQueuesSnapshot({
|
|
20
|
+
slug: options.org,
|
|
21
|
+
all: options.all ?? false,
|
|
22
|
+
windowHours: options.windowHours
|
|
23
|
+
});
|
|
24
|
+
const hasRisk = result.reviewQueue.slaBuckets.breached > 0 || result.reviewQueue.ageBuckets.gte168h > 0 || result.takedownQueue.staleInProgress > 0;
|
|
25
|
+
printOutput({
|
|
26
|
+
outputFormat,
|
|
27
|
+
json: {
|
|
28
|
+
...result,
|
|
29
|
+
explanation: options.explain ? {
|
|
30
|
+
riskSignals: {
|
|
31
|
+
slaBreaches: result.reviewQueue.slaBuckets.breached,
|
|
32
|
+
reviewOlderThan168h: result.reviewQueue.ageBuckets.gte168h,
|
|
33
|
+
staleTakedowns: result.takedownQueue.staleInProgress
|
|
34
|
+
},
|
|
35
|
+
failureCondition: "reviewQueue.slaBuckets.breached > 0 || reviewQueue.ageBuckets.gte168h > 0 || takedownQueue.staleInProgress > 0"
|
|
36
|
+
} : void 0
|
|
37
|
+
},
|
|
38
|
+
markdown: [
|
|
39
|
+
"# Operations Queue Snapshot",
|
|
40
|
+
"",
|
|
41
|
+
`- Scope: ${result.scope.all ? "all orgs" : result.scope.slug ?? "unknown"}`,
|
|
42
|
+
`- Pending review proposals: ${result.reviewQueue.totalPendingProposals}`,
|
|
43
|
+
`- Open takedowns: ${result.takedownQueue.totalOpen}`,
|
|
44
|
+
`- SLA breaches: ${result.reviewQueue.slaBuckets.breached}`,
|
|
45
|
+
`- Review >=168h: ${result.reviewQueue.ageBuckets.gte168h}`,
|
|
46
|
+
`- Stale in-progress takedowns: ${result.takedownQueue.staleInProgress}`
|
|
47
|
+
].join("\n"),
|
|
48
|
+
csv: toCsvRows([
|
|
49
|
+
{
|
|
50
|
+
scope: result.scope.all ? "all" : result.scope.slug ?? "unknown",
|
|
51
|
+
pendingReviewProposals: result.reviewQueue.totalPendingProposals,
|
|
52
|
+
reviewDistinctReports: result.reviewQueue.distinctReports,
|
|
53
|
+
reviewSlaBreached: result.reviewQueue.slaBuckets.breached,
|
|
54
|
+
reviewAgeGte168h: result.reviewQueue.ageBuckets.gte168h,
|
|
55
|
+
openTakedowns: result.takedownQueue.totalOpen,
|
|
56
|
+
staleInProgress: result.takedownQueue.staleInProgress
|
|
57
|
+
}
|
|
58
|
+
]),
|
|
59
|
+
human: () => {
|
|
60
|
+
console.log(
|
|
61
|
+
`Queue snapshot (${result.scope.all ? "all orgs" : result.scope.slug ?? "unknown"})`
|
|
62
|
+
);
|
|
63
|
+
console.log(
|
|
64
|
+
`Review queue: pending=${result.reviewQueue.totalPendingProposals} distinctReports=${result.reviewQueue.distinctReports}`
|
|
65
|
+
);
|
|
66
|
+
console.log(
|
|
67
|
+
`Review SLA: breached=${result.reviewQueue.slaBuckets.breached}, due<24h=${result.reviewQueue.slaBuckets.dueWithin24h}, due<72h=${result.reviewQueue.slaBuckets.dueWithin72h}, >72h=${result.reviewQueue.slaBuckets.beyond72h}, missing=${result.reviewQueue.slaBuckets.missingSla}`
|
|
68
|
+
);
|
|
69
|
+
console.log(
|
|
70
|
+
`Takedown queue: open=${result.takedownQueue.totalOpen} todo=${result.takedownQueue.statusCounts.todo} in_progress=${result.takedownQueue.statusCounts.inProgress} pending_input=${result.takedownQueue.statusCounts.pendingInput} stale_in_progress=${result.takedownQueue.staleInProgress}`
|
|
71
|
+
);
|
|
72
|
+
if (options.explain) {
|
|
73
|
+
console.log(
|
|
74
|
+
"Snapshot fails when there are SLA breaches, very old pending reviews, or stale in-progress takedowns."
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
});
|
|
79
|
+
if (hasRisk) {
|
|
80
|
+
throw new CliExitError(
|
|
81
|
+
"Queue snapshot has SLA/aging risk signals.",
|
|
82
|
+
ExitCode.CHECK_FAILED
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
export {
|
|
87
|
+
runQueuesSnapshot
|
|
88
|
+
};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import {
|
|
2
|
+
printOutput,
|
|
3
|
+
toCsvRows
|
|
4
|
+
} from "./chunk-VFT3TD3E.js";
|
|
5
|
+
import {
|
|
6
|
+
createApiClient
|
|
7
|
+
} from "./chunk-H7UKKLCV.js";
|
|
8
|
+
import "./chunk-EEG7T6WT.js";
|
|
9
|
+
import "./chunk-U73SABXK.js";
|
|
10
|
+
|
|
11
|
+
// src/commands/metrics/summary.ts
|
|
12
|
+
async function runMetricsSummary(options) {
|
|
13
|
+
const client = options.apiClient ?? createApiClient();
|
|
14
|
+
const result = await client.getMetricsSummary({
|
|
15
|
+
slug: options.org,
|
|
16
|
+
startDate: options.from,
|
|
17
|
+
endDate: options.to,
|
|
18
|
+
brandIds: options.brandIds
|
|
19
|
+
});
|
|
20
|
+
const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
|
|
21
|
+
printOutput({
|
|
22
|
+
outputFormat,
|
|
23
|
+
json: result,
|
|
24
|
+
markdown: [
|
|
25
|
+
`# Metrics Summary (${options.org})`,
|
|
26
|
+
"",
|
|
27
|
+
`- Reports: ${result.metrics.reports}`,
|
|
28
|
+
`- New threats: ${result.metrics.newThreats}`,
|
|
29
|
+
`- Watchlisted threats: ${result.metrics.threatsWatchlisted}`,
|
|
30
|
+
`- Takedowns filed: ${result.metrics.takedownsFiled}`,
|
|
31
|
+
`- Takedowns completed: ${result.metrics.takedownsCompleted}`
|
|
32
|
+
].join("\n"),
|
|
33
|
+
csv: toCsvRows([
|
|
34
|
+
{
|
|
35
|
+
reports: result.metrics.reports,
|
|
36
|
+
newThreats: result.metrics.newThreats,
|
|
37
|
+
threatsWatchlisted: result.metrics.threatsWatchlisted,
|
|
38
|
+
takedownsFiled: result.metrics.takedownsFiled,
|
|
39
|
+
takedownsCompleted: result.metrics.takedownsCompleted,
|
|
40
|
+
domainThreats: result.metrics.domainThreats,
|
|
41
|
+
twitterThreats: result.metrics.twitterThreats,
|
|
42
|
+
telegramThreats: result.metrics.telegramThreats,
|
|
43
|
+
otherThreats: result.metrics.otherThreats
|
|
44
|
+
}
|
|
45
|
+
]),
|
|
46
|
+
human: () => {
|
|
47
|
+
console.log(`Metrics summary for ${options.org}`);
|
|
48
|
+
console.log(`Reports: ${result.metrics.reports}`);
|
|
49
|
+
console.log(`New threats: ${result.metrics.newThreats}`);
|
|
50
|
+
console.log(`Watchlisted threats: ${result.metrics.threatsWatchlisted}`);
|
|
51
|
+
console.log(`Takedowns filed: ${result.metrics.takedownsFiled}`);
|
|
52
|
+
console.log(`Takedowns completed: ${result.metrics.takedownsCompleted}`);
|
|
53
|
+
console.log(
|
|
54
|
+
`Threat breakdown: domains=${result.metrics.domainThreats}, twitter=${result.metrics.twitterThreats}, telegram=${result.metrics.telegramThreats}, other=${result.metrics.otherThreats}`
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
export {
|
|
60
|
+
runMetricsSummary
|
|
61
|
+
};
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import {
|
|
2
|
+
CliExitError,
|
|
3
|
+
ExitCode
|
|
4
|
+
} from "./chunk-E2LAMILJ.js";
|
|
5
|
+
import {
|
|
6
|
+
printOutput,
|
|
7
|
+
toCsvRows
|
|
8
|
+
} from "./chunk-VFT3TD3E.js";
|
|
9
|
+
import {
|
|
10
|
+
createApiClient
|
|
11
|
+
} from "./chunk-H7UKKLCV.js";
|
|
12
|
+
import "./chunk-EEG7T6WT.js";
|
|
13
|
+
import "./chunk-U73SABXK.js";
|
|
14
|
+
|
|
15
|
+
// src/commands/detections/validate.ts
|
|
16
|
+
async function runDetectionsValidate(options) {
|
|
17
|
+
const client = options.apiClient ?? createApiClient();
|
|
18
|
+
const result = await client.validateDetectionConfigs({
|
|
19
|
+
slug: options.org,
|
|
20
|
+
source: options.source,
|
|
21
|
+
minResults: options.minResults,
|
|
22
|
+
lookbackHours: options.lookbackHours,
|
|
23
|
+
runBeforeValidate: options.runBeforeValidate ?? false,
|
|
24
|
+
includeDisabled: options.includeDisabled ?? false
|
|
25
|
+
});
|
|
26
|
+
const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
|
|
27
|
+
const csv = toCsvRows(
|
|
28
|
+
result.validations.map((item) => ({
|
|
29
|
+
configId: item.configId,
|
|
30
|
+
source: item.source,
|
|
31
|
+
valid: item.valid,
|
|
32
|
+
runOk: item.runOk,
|
|
33
|
+
recentResultCount: item.recentResultCount
|
|
34
|
+
}))
|
|
35
|
+
);
|
|
36
|
+
const markdown = [
|
|
37
|
+
`# Detection Validation (${options.org})`,
|
|
38
|
+
"",
|
|
39
|
+
`- Status: ${result.ok ? "passed" : "failed"}`,
|
|
40
|
+
`- Passing configs: ${result.summary.passingConfigs}`,
|
|
41
|
+
`- Checked configs: ${result.summary.checkedConfigs}`,
|
|
42
|
+
"",
|
|
43
|
+
...result.validations.map(
|
|
44
|
+
(item) => `- ${item.valid ? "PASS" : "FAIL"} [${item.source}] #${item.configId} results=${item.recentResultCount} runOk=${item.runOk}`
|
|
45
|
+
)
|
|
46
|
+
].join("\n");
|
|
47
|
+
printOutput({
|
|
48
|
+
outputFormat,
|
|
49
|
+
json: {
|
|
50
|
+
...result,
|
|
51
|
+
explanation: options.explain ? {
|
|
52
|
+
checkType: "detection_validate",
|
|
53
|
+
failureCondition: "result.ok === false"
|
|
54
|
+
} : void 0
|
|
55
|
+
},
|
|
56
|
+
markdown,
|
|
57
|
+
csv,
|
|
58
|
+
human: () => {
|
|
59
|
+
console.log(
|
|
60
|
+
`Validation ${result.ok ? "passed" : "failed"} (${result.summary.passingConfigs}/${result.summary.checkedConfigs})`
|
|
61
|
+
);
|
|
62
|
+
for (const item of result.validations) {
|
|
63
|
+
const icon = item.valid ? "\u2713" : "\u2717";
|
|
64
|
+
console.log(
|
|
65
|
+
`${icon} [${item.source}] config ${item.configId} results=${item.recentResultCount} runOk=${item.runOk}`
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
if (options.explain) {
|
|
69
|
+
console.log("Validation fails when any config result count is below threshold.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
if (!result.ok) {
|
|
74
|
+
throw new CliExitError("Detection validation failed.", ExitCode.CHECK_FAILED);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
export {
|
|
78
|
+
runDetectionsValidate
|
|
79
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@chainpatrol/cli",
|
|
3
|
+
"description": "The official ChainPatrol CLI — terminal interface for threat detection",
|
|
4
|
+
"author": "Umar Ahmed <umar@chainpatrol.io>",
|
|
5
|
+
"version": "0.1.0",
|
|
6
|
+
"license": "UNLICENSED",
|
|
7
|
+
"homepage": "https://chainpatrol.com/docs/cli",
|
|
8
|
+
"keywords": [
|
|
9
|
+
"chainpatrol",
|
|
10
|
+
"cli",
|
|
11
|
+
"mcp"
|
|
12
|
+
],
|
|
13
|
+
"type": "module",
|
|
14
|
+
"bin": {
|
|
15
|
+
"chainpatrol": "./dist/cli.js"
|
|
16
|
+
},
|
|
17
|
+
"files": [
|
|
18
|
+
"./dist/**"
|
|
19
|
+
],
|
|
20
|
+
"publishConfig": {
|
|
21
|
+
"access": "public"
|
|
22
|
+
},
|
|
23
|
+
"scripts": {
|
|
24
|
+
"dev": "tsx src/cli.tsx",
|
|
25
|
+
"dev:setup": "tsx src/cli.tsx setup",
|
|
26
|
+
"build": "tsup",
|
|
27
|
+
"local:install": "tsup && npm link",
|
|
28
|
+
"local:uninstall": "npm unlink -g chainpatrol && rm -f /usr/local/bin/chainpatrol",
|
|
29
|
+
"typecheck": "tsc --noEmit",
|
|
30
|
+
"test": "vitest run --config vitest.config.unit.ts",
|
|
31
|
+
"test:watch": "vitest --config vitest.config.unit.ts",
|
|
32
|
+
"lint": "npx oxlint ."
|
|
33
|
+
},
|
|
34
|
+
"dependencies": {
|
|
35
|
+
"ink": "^7.0.0",
|
|
36
|
+
"meow": "^14.1.0",
|
|
37
|
+
"open": "^11.0.0",
|
|
38
|
+
"react": "^19.2.4",
|
|
39
|
+
"zod": "^3.25.76"
|
|
40
|
+
},
|
|
41
|
+
"devDependencies": {
|
|
42
|
+
"@types/react": "^19.2.7",
|
|
43
|
+
"ink-testing-library": "^4.0.0",
|
|
44
|
+
"msw": "^2.0.0",
|
|
45
|
+
"tsup": "^8.5.0",
|
|
46
|
+
"tsx": "^4.19.4",
|
|
47
|
+
"typescript": "6.0.2",
|
|
48
|
+
"vitest": "^3.2.4"
|
|
49
|
+
}
|
|
50
|
+
}
|