@chainpatrol/cli 1.6.0 → 1.7.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/CHANGELOG.md CHANGED
@@ -1,5 +1,42 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 1.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9f19c88: Adds time-to-takedown metrics: `client.getMetricsTakedownTime()` in the SDK and
8
+ `chainpatrol metrics takedown-time --org <slug>` in the CLI, both backed by the
9
+ new `POST /metrics/takedown-time` endpoint. Returns the median time from
10
+ takedown start to completion per asset type, over an optional date range.
11
+
12
+ Also adds `parseIsoDateEndOfDay` to `@chainpatrol/sdk/api`: a date-only bound
13
+ such as `2026-08-18` now covers that whole day instead of cutting off at
14
+ midnight UTC. Server-side work lives in @chainpatrol/external-trpc and
15
+ @chainpatrol/validation; both are private packages and don't need version bumps.
16
+
17
+ - b037a0b: `GET /organization/assets` now accepts an optional `slug`, so callers whose
18
+ credentials reach more than one organization (user-scoped API keys, staff and
19
+ member sessions) can pick one instead of getting a flat 401. Organization-scoped
20
+ keys keep working unchanged when they omit it.
21
+
22
+ In the SDK, `client.listOrganizationAssets()` takes a `slug`. In the CLI,
23
+ `chainpatrol orgs assets list` forwards `--org <slug>` when one is set — the
24
+ existing no-flag invocation still resolves the org from an org-scoped key.
25
+
26
+ Server-side work lives in @chainpatrol/external-trpc and @chainpatrol/validation;
27
+ both are private packages and don't need version bumps.
28
+
29
+ ### Patch Changes
30
+
31
+ - 0e5f156: Fix the install command in the CLI README. The unscoped `chainpatrol` package on npm is a
32
+ placeholder that points at `@chainpatrol/sdk`, so `npm install -g chainpatrol` never
33
+ installed the CLI — the published package is `@chainpatrol/cli`. Also link the README to
34
+ the new CLI documentation at https://chainpatrol.com/docs/cli.
35
+ - Updated dependencies [f98f547]
36
+ - Updated dependencies [9f19c88]
37
+ - Updated dependencies [b037a0b]
38
+ - @chainpatrol/sdk@1.7.0
39
+
3
40
  ## 1.6.0
4
41
 
5
42
  ### Minor Changes
package/README.md CHANGED
@@ -2,12 +2,17 @@
2
2
 
3
3
  Terminal interface for ChainPatrol threat detection.
4
4
 
5
+ Full documentation: https://chainpatrol.com/docs/cli
6
+
5
7
  ## Installation
6
8
 
7
9
  ```bash
8
- npm install -g chainpatrol
10
+ npm install -g @chainpatrol/cli
9
11
  ```
10
12
 
13
+ The unscoped `chainpatrol` package on npm is a placeholder that points at
14
+ `@chainpatrol/sdk` — it does not contain the CLI.
15
+
11
16
  ## Usage
12
17
 
13
18
  ```bash
@@ -94,6 +94,7 @@ _chainpatrol() {
94
94
  'summary:Show metrics summary'
95
95
  'found:Show found threats count'
96
96
  'breakdown:Show metrics breakdown'
97
+ 'takedown-time:Show median time to takedown by asset type'
97
98
  'organization:Get organization metrics (official endpoint)'
98
99
  )
99
100
  _describe 'subcommand' metrics_subcommands
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  installCompletions,
3
3
  uninstallCompletions
4
- } from "./chunk-P5DJRDMV.js";
4
+ } from "./chunk-7BZUQSRO.js";
5
5
 
6
6
  // src/commands/setup-skill.ts
7
7
  import { mkdirSync as mkdirSync2, writeFileSync as writeFileSync2, existsSync as existsSync2, readFileSync as readFileSync3, rmSync } from "fs";
package/dist/cli.js CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-55B34HJK.js";
17
- import "./chunk-P5DJRDMV.js";
16
+ } from "./chunk-YMO4QK6Q.js";
17
+ import "./chunk-7BZUQSRO.js";
18
18
  import {
19
19
  DateTime
20
20
  } from "./chunk-TFCNKBRC.js";
@@ -286,6 +286,7 @@ var HELP = {
286
286
  "chainpatrol metrics summary --org acme --this-week",
287
287
  "chainpatrol metrics found --org acme --from 2025-01-01 --to 2025-02-01",
288
288
  "chainpatrol metrics breakdown --org acme --by day --this-week",
289
+ "chainpatrol metrics takedown-time --org acme --from 2025-01-01 --to 2025-02-01",
289
290
  "chainpatrol metrics organization --org acme --from 2025-01-01 --to 2025-02-01"
290
291
  ]
291
292
  },
@@ -346,6 +347,17 @@ var HELP = {
346
347
  "--brand <ids> Comma-separated brand IDs"
347
348
  ]
348
349
  },
350
+ "metrics takedown-time": {
351
+ description: "Median time from takedown start to completion, per asset type. Defaults to the trailing 3 months if no date range is given.",
352
+ usage: "chainpatrol metrics takedown-time --org <slug> [--from ISO --to ISO|--this-week]",
353
+ options: [
354
+ "--org <slug> Organization slug",
355
+ "--from <iso> Start date (ISO 8601)",
356
+ "--to <iso> End date (ISO 8601); a date-only value covers the whole day",
357
+ "--this-week Use the current ISO week (overrides default)",
358
+ "--brand <ids> Comma-separated brand IDs"
359
+ ]
360
+ },
349
361
  reports: {
350
362
  description: "Create, list, and search reports from the terminal.",
351
363
  usage: "chainpatrol reports <create|list|search>",
@@ -556,9 +568,10 @@ var HELP = {
556
568
  usage: "chainpatrol orgs assets list [filters]"
557
569
  },
558
570
  "orgs assets list": {
559
- description: "List assets belonging to your organization's allowlist. Org is derived from your API key. Supports filtering by type, group, and search query.",
571
+ description: "List assets belonging to an organization's allowlist. Pass --org <slug> to choose one, or omit it to use the org tied to your API key. Supports filtering by type, group, and search query.",
560
572
  usage: "chainpatrol orgs assets list [filters]",
561
573
  options: [
574
+ "--org <slug> Organization slug (omit for org-scoped API keys)",
562
575
  "--asset-type <type> Filter by asset type (URL, ADDRESS, ...)",
563
576
  "--type <type> Alias for --asset-type",
564
577
  "--group-id <n> Filter by group ID. Use -1 for ungrouped assets only",
@@ -568,6 +581,7 @@ var HELP = {
568
581
  ],
569
582
  examples: [
570
583
  "chainpatrol orgs assets list --asset-type URL",
584
+ "chainpatrol orgs assets list --org acme --per-page 1000",
571
585
  "chainpatrol orgs assets list --group-id 12 --per-page 250 --output csv"
572
586
  ]
573
587
  },
@@ -1299,7 +1313,7 @@ async function main() {
1299
1313
  }
1300
1314
  return n;
1301
1315
  });
1302
- const { runAssetSearch } = await import("./search-W5QM2IOC.js");
1316
+ const { runAssetSearch } = await import("./search-IWS42COG.js");
1303
1317
  await runAssetSearch({
1304
1318
  contents: contents.length > 0 ? contents : void 0,
1305
1319
  ids: ids.length > 0 ? ids : void 0,
@@ -1509,6 +1523,19 @@ async function main() {
1509
1523
  });
1510
1524
  break;
1511
1525
  }
1526
+ if (subcommand === "takedown-time" || subcommand === "takedownTime") {
1527
+ const { runMetricsTakedownTime } = await import("./takedown-time-ARGVSPL6.js");
1528
+ await runMetricsTakedownTime({
1529
+ org,
1530
+ from: cli.flags.from,
1531
+ to: cli.flags.to,
1532
+ thisWeek: cli.flags.thisWeek,
1533
+ brandIds: parseBrandIds(),
1534
+ json: jsonMode,
1535
+ outputFormat: cliContext.outputFormat
1536
+ });
1537
+ break;
1538
+ }
1512
1539
  if (subcommand === "breakdown") {
1513
1540
  const by = cli.flags.by;
1514
1541
  if (!by || !["day", "type", "brand"].includes(by)) {
@@ -1527,9 +1554,15 @@ async function main() {
1527
1554
  });
1528
1555
  break;
1529
1556
  }
1530
- const hint = subcommand ? suggest(subcommand, ["summary", "found", "breakdown", "organization"]) : null;
1557
+ const hint = subcommand ? suggest(subcommand, [
1558
+ "summary",
1559
+ "found",
1560
+ "breakdown",
1561
+ "takedown-time",
1562
+ "organization"
1563
+ ]) : null;
1531
1564
  throw new Error(
1532
- subcommand ? `Unknown subcommand: metrics ${subcommand}${hint ? `. Did you mean "metrics ${hint}"?` : ""}` : "Usage: chainpatrol metrics <summary|found|breakdown|organization>"
1565
+ subcommand ? `Unknown subcommand: metrics ${subcommand}${hint ? `. Did you mean "metrics ${hint}"?` : ""}` : "Usage: chainpatrol metrics <summary|found|breakdown|takedown-time|organization>"
1533
1566
  );
1534
1567
  }
1535
1568
  case "reports": {
@@ -1739,8 +1772,11 @@ async function main() {
1739
1772
  }
1740
1773
  if (subcommand === "assets") {
1741
1774
  if (action === "list") {
1742
- const { runOrgsAssetsList } = await import("./list-H6QWWXOU.js");
1775
+ const { runOrgsAssetsList } = await import("./list-5TKOWBC4.js");
1743
1776
  await runOrgsAssetsList({
1777
+ // Optional: organization-scoped API keys resolve the org from the
1778
+ // key, so only pass a slug when one is actually available.
1779
+ slug: await tryResolveOrg(),
1744
1780
  type: cli.flags.type ?? cli.flags.assetType,
1745
1781
  groupId: cli.flags.groupId,
1746
1782
  query: cli.flags.query ?? cli.flags.search,
@@ -1860,17 +1896,17 @@ async function main() {
1860
1896
  case "setup":
1861
1897
  case "install":
1862
1898
  case "i": {
1863
- const { setupSkill } = await import("./setup-skill-2KCAXU2U.js");
1899
+ const { setupSkill } = await import("./setup-skill-LP5Z3Z5T.js");
1864
1900
  setupSkill({ json: jsonMode, cloud: cli.flags.cloud });
1865
1901
  break;
1866
1902
  }
1867
1903
  case "uninstall": {
1868
- const { uninstallSkill } = await import("./setup-skill-2KCAXU2U.js");
1904
+ const { uninstallSkill } = await import("./setup-skill-LP5Z3Z5T.js");
1869
1905
  uninstallSkill({ json: jsonMode });
1870
1906
  break;
1871
1907
  }
1872
1908
  case "completions": {
1873
- const { printCompletions } = await import("./completions-FEUVEC5N.js");
1909
+ const { printCompletions } = await import("./completions-3ESPMO65.js");
1874
1910
  printCompletions(subcommand);
1875
1911
  break;
1876
1912
  }
@@ -3,7 +3,7 @@ import {
3
3
  installCompletions,
4
4
  printCompletions,
5
5
  uninstallCompletions
6
- } from "./chunk-P5DJRDMV.js";
6
+ } from "./chunk-7BZUQSRO.js";
7
7
  export {
8
8
  getCompletionScript,
9
9
  installCompletions,
@@ -37,6 +37,7 @@ async function runOrgsAssetsList(options) {
37
37
  }
38
38
  const client = options.apiClient ?? createApiClient();
39
39
  const input = {
40
+ slug: options.slug,
40
41
  type: options.type ? options.type.toUpperCase() : void 0,
41
42
  groupId: options.groupId,
42
43
  query: options.query,
@@ -105,7 +105,7 @@ async function runAssetSearch(options) {
105
105
  console.error(
106
106
  `Error searching ${contents[i]}: ${r.reason instanceof Error ? r.reason.message : String(r.reason)}`
107
107
  );
108
- return { asset: null, reports: [], takedown: null, blockedBy: [] };
108
+ return { asset: null, reports: [], scans: [], takedown: null, blockedBy: [] };
109
109
  });
110
110
  if (errored === contents.length) {
111
111
  throw new CliExitError(
@@ -6,8 +6,8 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-55B34HJK.js";
10
- import "./chunk-P5DJRDMV.js";
9
+ } from "./chunk-YMO4QK6Q.js";
10
+ import "./chunk-7BZUQSRO.js";
11
11
  export {
12
12
  getBundledSkillContent,
13
13
  getBundledSkillVersion,
@@ -0,0 +1,87 @@
1
+ import {
2
+ resolveDateRange
3
+ } from "./chunk-S37VFIZY.js";
4
+ import {
5
+ printOutput,
6
+ toCsvRows
7
+ } from "./chunk-VFT3TD3E.js";
8
+ import {
9
+ createApiClient
10
+ } from "./chunk-322AVMUH.js";
11
+ import "./chunk-TFCNKBRC.js";
12
+ import "./chunk-EGWK6SRQ.js";
13
+ import "./chunk-U73SABXK.js";
14
+
15
+ // src/commands/metrics/takedown-time.ts
16
+ function formatDuration(hours) {
17
+ if (hours < 1 / 60) {
18
+ return `${Math.round(hours * 3600)}s`;
19
+ }
20
+ if (hours < 1) {
21
+ return `${Math.round(hours * 60)}m`;
22
+ }
23
+ if (hours < 24) {
24
+ return `${hours.toFixed(1)}h`;
25
+ }
26
+ return `${(hours / 24).toFixed(1)}d`;
27
+ }
28
+ async function runMetricsTakedownTime(options) {
29
+ const range = resolveDateRange({
30
+ from: options.from,
31
+ to: options.to,
32
+ thisWeek: options.thisWeek
33
+ });
34
+ const client = options.apiClient ?? createApiClient();
35
+ const result = await client.getMetricsTakedownTime({
36
+ slug: options.org,
37
+ // The caller's own --from/--to go through untouched when given: the API
38
+ // reads a date-only value as the whole day, while `resolveDateRange` has
39
+ // already expanded it to a midnight timestamp, which would cut the final
40
+ // day off.
41
+ startDate: options.from ?? range.startDate,
42
+ endDate: options.to ?? range.endDate,
43
+ brandIds: options.brandIds
44
+ });
45
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
46
+ printOutput({
47
+ outputFormat,
48
+ json: { ...result, range },
49
+ markdown: [
50
+ `# Median Time to Takedown (${options.org}) \u2014 ${range.label}`,
51
+ "",
52
+ `- Range: ${range.startDate} \u2192 ${range.endDate}`,
53
+ "",
54
+ ...result.byAssetType.map(
55
+ (row) => `- ${row.assetType}: ${formatDuration(row.medianTakedownHours)} (${row.count} takedowns)`
56
+ )
57
+ ].join("\n"),
58
+ csv: toCsvRows(
59
+ result.byAssetType.map((row) => ({
60
+ rangeLabel: range.label,
61
+ startDate: range.startDate,
62
+ endDate: range.endDate,
63
+ assetType: row.assetType,
64
+ medianTakedownMinutes: row.medianTakedownMinutes,
65
+ medianTakedownHours: row.medianTakedownHours,
66
+ medianTakedownDays: row.medianTakedownDays,
67
+ count: row.count
68
+ }))
69
+ ),
70
+ human: () => {
71
+ console.log(`Median time to takedown for ${options.org} \u2014 ${range.label}`);
72
+ console.log(`Range: ${range.startDate} \u2192 ${range.endDate}`);
73
+ if (result.byAssetType.length === 0) {
74
+ console.log("No completed takedowns in this range.");
75
+ return;
76
+ }
77
+ for (const row of result.byAssetType) {
78
+ console.log(
79
+ `${row.assetType}: ${formatDuration(row.medianTakedownHours)} (${row.count} takedowns)`
80
+ );
81
+ }
82
+ }
83
+ });
84
+ }
85
+ export {
86
+ runMetricsTakedownTime
87
+ };
package/package.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "name": "@chainpatrol/cli",
3
3
  "description": "The official ChainPatrol CLI — terminal interface for threat detection",
4
4
  "author": "Umar Ahmed <umar@chainpatrol.io>",
5
- "version": "1.6.0",
5
+ "version": "1.7.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [
@@ -33,7 +33,7 @@
33
33
  "lint": "npx oxlint ."
34
34
  },
35
35
  "dependencies": {
36
- "@chainpatrol/sdk": "1.6.0",
36
+ "@chainpatrol/sdk": "1.7.0",
37
37
  "ink": "^7.0.1",
38
38
  "meow": "^14.1.0",
39
39
  "open": "^11.0.0",