@chainpatrol/cli 1.7.0 → 1.8.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,32 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 1.8.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 33ba556: Adds time-to-takedown trends: `client.getMetricsTakedownTimeTrend()` in the SDK
8
+ and `chainpatrol metrics takedown-time-trend --org <slug>` in the CLI, both
9
+ backed by the new `POST /metrics/takedown-time/trend` endpoint. Returns the
10
+ median time from takedown start to completion bucketed by the month a takedown
11
+ completed, over a trailing window of whole months, plus the month-over-month
12
+ change for each series. Pass `assetTypes` (max 5) to get one series per asset
13
+ type instead of a single pooled series.
14
+
15
+ The month-over-month comparison uses the two most recent months that actually
16
+ had completed takedowns, so a quiet month in between doesn't read as "no
17
+ change", and `direction` reads lower-is-better: `"improved"` means the median
18
+ fell. Months with no completed takedowns carry a `null` median rather than zero.
19
+
20
+ Server-side work lives in @chainpatrol/external-trpc, @chainpatrol/validation
21
+ and @chainpatrol/metrics; all three are private packages and don't need version
22
+ bumps. The endpoint is gated on the new `median-time-to-takedown-trends-api`
23
+ feature flag, separate from the flag on `POST /metrics/takedown-time`.
24
+
25
+ ### Patch Changes
26
+
27
+ - Updated dependencies [33ba556]
28
+ - @chainpatrol/sdk@1.8.0
29
+
3
30
  ## 1.7.0
4
31
 
5
32
  ### 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
  },
@@ -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 },
@@ -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,
@@ -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.8.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.8.0",
37
37
  "ink": "^7.0.1",
38
38
  "meow": "^14.1.0",
39
39
  "open": "^11.0.0",