@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.
@@ -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
+ }