@chainpatrol/cli 1.7.0 → 1.9.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,43 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 1.9.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 9347b6a: Add `takedownsInProgress` and `takedownsCancelled` to the `/organization/metrics` endpoint, available as `--include` values and as fields on the metrics payload.
8
+
9
+ ### Patch Changes
10
+
11
+ - Updated dependencies [9347b6a]
12
+ - @chainpatrol/sdk@1.9.0
13
+
14
+ ## 1.8.0
15
+
16
+ ### Minor Changes
17
+
18
+ - 33ba556: Adds time-to-takedown trends: `client.getMetricsTakedownTimeTrend()` in the SDK
19
+ and `chainpatrol metrics takedown-time-trend --org <slug>` in the CLI, both
20
+ backed by the new `POST /metrics/takedown-time/trend` endpoint. Returns the
21
+ median time from takedown start to completion bucketed by the month a takedown
22
+ completed, over a trailing window of whole months, plus the month-over-month
23
+ change for each series. Pass `assetTypes` (max 5) to get one series per asset
24
+ type instead of a single pooled series.
25
+
26
+ The month-over-month comparison uses the two most recent months that actually
27
+ had completed takedowns, so a quiet month in between doesn't read as "no
28
+ change", and `direction` reads lower-is-better: `"improved"` means the median
29
+ fell. Months with no completed takedowns carry a `null` median rather than zero.
30
+
31
+ Server-side work lives in @chainpatrol/external-trpc, @chainpatrol/validation
32
+ and @chainpatrol/metrics; all three are private packages and don't need version
33
+ bumps. The endpoint is gated on the new `median-time-to-takedown-trends-api`
34
+ feature flag, separate from the flag on `POST /metrics/takedown-time`.
35
+
36
+ ### Patch Changes
37
+
38
+ - Updated dependencies [33ba556]
39
+ - @chainpatrol/sdk@1.8.0
40
+
3
41
  ## 1.7.0
4
42
 
5
43
  ### Minor Changes
@@ -1,7 +1,7 @@
1
1
  import {
2
2
  installCompletions,
3
3
  uninstallCompletions
4
- } from "./chunk-7BZUQSRO.js";
4
+ } from "./chunk-TI2HK3NM.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";
@@ -95,6 +95,7 @@ _chainpatrol() {
95
95
  'found:Show found threats count'
96
96
  'breakdown:Show metrics breakdown'
97
97
  'takedown-time:Show median time to takedown by asset type'
98
+ 'takedown-time-trend:Show month-over-month median time-to-takedown trend'
98
99
  'organization:Get organization metrics (official endpoint)'
99
100
  )
100
101
  _describe 'subcommand' metrics_subcommands
package/dist/cli.js CHANGED
@@ -13,8 +13,8 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-YMO4QK6Q.js";
17
- import "./chunk-7BZUQSRO.js";
16
+ } from "./chunk-LE5ZOOUI.js";
17
+ import "./chunk-TI2HK3NM.js";
18
18
  import {
19
19
  DateTime
20
20
  } from "./chunk-TFCNKBRC.js";
@@ -287,6 +287,7 @@ var HELP = {
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
289
  "chainpatrol metrics takedown-time --org acme --from 2025-01-01 --to 2025-02-01",
290
+ "chainpatrol metrics takedown-time-trend --org acme --months 12",
290
291
  "chainpatrol metrics organization --org acme --from 2025-01-01 --to 2025-02-01"
291
292
  ]
292
293
  },
@@ -303,7 +304,7 @@ var HELP = {
303
304
  "--from <iso> Start date (ISO 8601, e.g. 2026-01-01)",
304
305
  "--to <iso> End date (ISO 8601)",
305
306
  "--this-week Use the current ISO week (overrides default)",
306
- "--include <list> Comma list of fields to compute: reports,newThreats,threatsWatchlisted,takedownsFiled,takedownsCompleted,domainThreats,twitterThreats,telegramThreats,otherThreats,blockedByType,blockedByDay (omit for all)"
307
+ "--include <list> Comma list of fields to compute: reports,newThreats,threatsWatchlisted,takedownsFiled,takedownsInProgress,takedownsCompleted,takedownsCancelled,domainThreats,twitterThreats,telegramThreats,otherThreats,blockedByType,blockedByDay (omit for all)"
307
308
  ],
308
309
  examples: [
309
310
  "chainpatrol metrics organization --org acme # defaults to last 3 months",
@@ -358,6 +359,16 @@ var HELP = {
358
359
  "--brand <ids> Comma-separated brand IDs"
359
360
  ]
360
361
  },
362
+ "metrics takedown-time-trend": {
363
+ description: "Median time from takedown start to completion bucketed by month, over a trailing window, with the month-over-month change.",
364
+ usage: "chainpatrol metrics takedown-time-trend --org <slug> [--months 12] [--asset-type TWITTER,URL]",
365
+ options: [
366
+ "--org <slug> Organization slug",
367
+ "--months <n> Trailing window in whole months (2-36, default 12)",
368
+ "--asset-type <types> Comma-separated asset types to break the trend down by (max 5)",
369
+ "--brand <ids> Comma-separated brand IDs"
370
+ ]
371
+ },
361
372
  reports: {
362
373
  description: "Create, list, and search reports from the terminal.",
363
374
  usage: "chainpatrol reports <create|list|search>",
@@ -967,6 +978,7 @@ ${getTopLevelHelp()}
967
978
  to: { type: "string" },
968
979
  thisWeek: { type: "boolean", default: false },
969
980
  by: { type: "string" },
981
+ months: { type: "number" },
970
982
  include: { type: "string" },
971
983
  slugs: { type: "string" },
972
984
  allMyOrgs: { type: "boolean", default: false },
@@ -1479,7 +1491,7 @@ async function main() {
1479
1491
  const slugs = parseCsvList(cli.flags.slugs);
1480
1492
  const allMyOrgs = cli.flags.allMyOrgs;
1481
1493
  const org2 = slugs && slugs.length > 0 || allMyOrgs ? void 0 : await tryResolveOrg();
1482
- const { runMetricsOrganization } = await import("./organization-YAID36GZ.js");
1494
+ const { runMetricsOrganization } = await import("./organization-SSCPM7XN.js");
1483
1495
  await runMetricsOrganization({
1484
1496
  org: org2,
1485
1497
  slugs,
@@ -1536,6 +1548,18 @@ async function main() {
1536
1548
  });
1537
1549
  break;
1538
1550
  }
1551
+ if (subcommand === "takedown-time-trend" || subcommand === "takedownTimeTrend") {
1552
+ const { runMetricsTakedownTimeTrend } = await import("./takedown-time-trend-LL3ZCCSD.js");
1553
+ await runMetricsTakedownTimeTrend({
1554
+ org,
1555
+ months: cli.flags.months,
1556
+ assetTypes: parseCsvList(cli.flags.assetType),
1557
+ brandIds: parseBrandIds(),
1558
+ json: jsonMode,
1559
+ outputFormat: cliContext.outputFormat
1560
+ });
1561
+ break;
1562
+ }
1539
1563
  if (subcommand === "breakdown") {
1540
1564
  const by = cli.flags.by;
1541
1565
  if (!by || !["day", "type", "brand"].includes(by)) {
@@ -1559,10 +1583,11 @@ async function main() {
1559
1583
  "found",
1560
1584
  "breakdown",
1561
1585
  "takedown-time",
1586
+ "takedown-time-trend",
1562
1587
  "organization"
1563
1588
  ]) : null;
1564
1589
  throw new Error(
1565
- subcommand ? `Unknown subcommand: metrics ${subcommand}${hint ? `. Did you mean "metrics ${hint}"?` : ""}` : "Usage: chainpatrol metrics <summary|found|breakdown|takedown-time|organization>"
1590
+ subcommand ? `Unknown subcommand: metrics ${subcommand}${hint ? `. Did you mean "metrics ${hint}"?` : ""}` : "Usage: chainpatrol metrics <summary|found|breakdown|takedown-time|takedown-time-trend|organization>"
1566
1591
  );
1567
1592
  }
1568
1593
  case "reports": {
@@ -1896,17 +1921,17 @@ async function main() {
1896
1921
  case "setup":
1897
1922
  case "install":
1898
1923
  case "i": {
1899
- const { setupSkill } = await import("./setup-skill-LP5Z3Z5T.js");
1924
+ const { setupSkill } = await import("./setup-skill-ESJ4HIKR.js");
1900
1925
  setupSkill({ json: jsonMode, cloud: cli.flags.cloud });
1901
1926
  break;
1902
1927
  }
1903
1928
  case "uninstall": {
1904
- const { uninstallSkill } = await import("./setup-skill-LP5Z3Z5T.js");
1929
+ const { uninstallSkill } = await import("./setup-skill-ESJ4HIKR.js");
1905
1930
  uninstallSkill({ json: jsonMode });
1906
1931
  break;
1907
1932
  }
1908
1933
  case "completions": {
1909
- const { printCompletions } = await import("./completions-3ESPMO65.js");
1934
+ const { printCompletions } = await import("./completions-7DD5E5HL.js");
1910
1935
  printCompletions(subcommand);
1911
1936
  break;
1912
1937
  }
@@ -3,7 +3,7 @@ import {
3
3
  installCompletions,
4
4
  printCompletions,
5
5
  uninstallCompletions
6
- } from "./chunk-7BZUQSRO.js";
6
+ } from "./chunk-TI2HK3NM.js";
7
7
  export {
8
8
  getCompletionScript,
9
9
  installCompletions,
@@ -42,7 +42,9 @@ var METRIC_LABELS = [
42
42
  ["newThreats", "New threats"],
43
43
  ["threatsWatchlisted", "Watchlisted threats"],
44
44
  ["takedownsFiled", "Takedowns filed"],
45
+ ["takedownsInProgress", "Takedowns in-progress"],
45
46
  ["takedownsCompleted", "Takedowns completed"],
47
+ ["takedownsCancelled", "Takedowns cancelled"],
46
48
  ["domainThreats", "Domain threats"],
47
49
  ["twitterThreats", "Twitter threats"],
48
50
  ["telegramThreats", "Telegram threats"],
@@ -66,10 +68,9 @@ function formatAverage(value) {
66
68
  return Number.isInteger(value) ? String(value) : value.toFixed(2);
67
69
  }
68
70
  function populatedMetricRows(metrics, format = (v) => String(v)) {
69
- return METRIC_LABELS.filter(([key]) => metrics[key] !== null).map(([key, label]) => [
70
- label,
71
- format(metrics[key])
72
- ]);
71
+ return METRIC_LABELS.filter(([key]) => (metrics[key] ?? null) !== null).map(
72
+ ([key, label]) => [label, format(metrics[key] ?? null)]
73
+ );
73
74
  }
74
75
  async function runMetricsOrganization(options) {
75
76
  const explicitForms = [
@@ -231,7 +232,9 @@ function buildCsvRows({
231
232
  newThreats: metrics.newThreats,
232
233
  threatsWatchlisted: metrics.threatsWatchlisted,
233
234
  takedownsFiled: metrics.takedownsFiled,
235
+ takedownsInProgress: metrics.takedownsInProgress,
234
236
  takedownsCompleted: metrics.takedownsCompleted,
237
+ takedownsCancelled: metrics.takedownsCancelled,
235
238
  domainThreats: metrics.domainThreats,
236
239
  twitterThreats: metrics.twitterThreats,
237
240
  telegramThreats: metrics.telegramThreats,
@@ -6,8 +6,8 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-YMO4QK6Q.js";
10
- import "./chunk-7BZUQSRO.js";
9
+ } from "./chunk-LE5ZOOUI.js";
10
+ import "./chunk-TI2HK3NM.js";
11
11
  export {
12
12
  getBundledSkillContent,
13
13
  getBundledSkillVersion,
@@ -0,0 +1,103 @@
1
+ import {
2
+ printOutput,
3
+ toCsvRows
4
+ } from "./chunk-VFT3TD3E.js";
5
+ import {
6
+ createApiClient
7
+ } from "./chunk-322AVMUH.js";
8
+ import "./chunk-EGWK6SRQ.js";
9
+ import "./chunk-U73SABXK.js";
10
+
11
+ // src/commands/metrics/takedown-time-trend.ts
12
+ function formatDuration(hours) {
13
+ if (hours === null) {
14
+ return "\u2014";
15
+ }
16
+ if (hours < 1 / 60) {
17
+ return `${Math.round(hours * 3600)}s`;
18
+ }
19
+ if (hours < 1) {
20
+ return `${Math.round(hours * 60)}m`;
21
+ }
22
+ if (hours < 24) {
23
+ return `${hours.toFixed(1)}h`;
24
+ }
25
+ return `${(hours / 24).toFixed(1)}d`;
26
+ }
27
+ function formatPercentChange(percentChange) {
28
+ if (percentChange === null) {
29
+ return "no comparable earlier month";
30
+ }
31
+ const sign = percentChange > 0 ? "+" : "";
32
+ return `${sign}${percentChange.toFixed(1)}%`;
33
+ }
34
+ async function runMetricsTakedownTimeTrend(options) {
35
+ const client = options.apiClient ?? createApiClient();
36
+ const result = await client.getMetricsTakedownTimeTrend({
37
+ slug: options.org,
38
+ months: options.months,
39
+ assetTypes: options.assetTypes,
40
+ brandIds: options.brandIds
41
+ });
42
+ const seriesLabel = (assetType) => assetType ?? "All asset types";
43
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
44
+ printOutput({
45
+ outputFormat,
46
+ json: result,
47
+ markdown: [
48
+ `# Median Time to Takedown Trend (${options.org}) \u2014 last ${result.months} months`,
49
+ "",
50
+ ...result.series.flatMap((series) => [
51
+ `## ${seriesLabel(series.assetType)}`,
52
+ "",
53
+ `- Month over month: ${formatPercentChange(series.change.percentChange)}${series.change.direction ? ` (${series.change.direction})` : ""}`,
54
+ "",
55
+ "| Month | Median | Takedowns |",
56
+ "| --- | --- | --- |",
57
+ ...series.points.map(
58
+ (point) => `| ${point.period} | ${formatDuration(point.medianTakedownHours)} | ${point.count} |`
59
+ ),
60
+ ""
61
+ ])
62
+ ].join("\n"),
63
+ csv: toCsvRows(
64
+ result.series.flatMap(
65
+ (series) => series.points.map((point) => ({
66
+ assetType: series.assetType ?? "",
67
+ period: point.period,
68
+ medianTakedownMinutes: point.medianTakedownMinutes,
69
+ medianTakedownHours: point.medianTakedownHours,
70
+ medianTakedownDays: point.medianTakedownDays,
71
+ count: point.count,
72
+ percentChange: series.change.percentChange,
73
+ direction: series.change.direction
74
+ }))
75
+ )
76
+ ),
77
+ human: () => {
78
+ console.log(
79
+ `Median time to takedown trend for ${options.org} \u2014 last ${result.months} months`
80
+ );
81
+ for (const series of result.series) {
82
+ console.log("");
83
+ console.log(seriesLabel(series.assetType));
84
+ for (const point of series.points) {
85
+ console.log(
86
+ ` ${point.period}: ${formatDuration(point.medianTakedownHours)} (${point.count} takedowns)`
87
+ );
88
+ }
89
+ const { latest, previous, percentChange, direction } = series.change;
90
+ if (latest && previous && percentChange !== null) {
91
+ console.log(
92
+ ` Month over month (${previous.period} \u2192 ${latest.period}): ${formatPercentChange(percentChange)} \u2014 ${direction}`
93
+ );
94
+ } else {
95
+ console.log(` Month over month: ${formatPercentChange(percentChange)}`);
96
+ }
97
+ }
98
+ }
99
+ });
100
+ }
101
+ export {
102
+ runMetricsTakedownTimeTrend
103
+ };
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.7.0",
5
+ "version": "1.9.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.7.0",
36
+ "@chainpatrol/sdk": "1.9.0",
37
37
  "ink": "^7.0.1",
38
38
  "meow": "^14.1.0",
39
39
  "open": "^11.0.0",