@chainpatrol/cli 1.6.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 +64 -0
- package/README.md +6 -1
- package/dist/{chunk-55B34HJK.js → chunk-LE5ZOOUI.js} +1 -1
- package/dist/{chunk-P5DJRDMV.js → chunk-TI2HK3NM.js} +2 -0
- package/dist/cli.js +71 -10
- package/dist/{completions-FEUVEC5N.js → completions-7DD5E5HL.js} +1 -1
- package/dist/{list-H6QWWXOU.js → list-5TKOWBC4.js} +1 -0
- package/dist/{search-W5QM2IOC.js → search-IWS42COG.js} +1 -1
- package/dist/{setup-skill-2KCAXU2U.js → setup-skill-ESJ4HIKR.js} +2 -2
- package/dist/takedown-time-ARGVSPL6.js +87 -0
- package/dist/takedown-time-trend-LL3ZCCSD.js +103 -0
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,69 @@
|
|
|
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
|
+
|
|
30
|
+
## 1.7.0
|
|
31
|
+
|
|
32
|
+
### Minor Changes
|
|
33
|
+
|
|
34
|
+
- 9f19c88: Adds time-to-takedown metrics: `client.getMetricsTakedownTime()` in the SDK and
|
|
35
|
+
`chainpatrol metrics takedown-time --org <slug>` in the CLI, both backed by the
|
|
36
|
+
new `POST /metrics/takedown-time` endpoint. Returns the median time from
|
|
37
|
+
takedown start to completion per asset type, over an optional date range.
|
|
38
|
+
|
|
39
|
+
Also adds `parseIsoDateEndOfDay` to `@chainpatrol/sdk/api`: a date-only bound
|
|
40
|
+
such as `2026-08-18` now covers that whole day instead of cutting off at
|
|
41
|
+
midnight UTC. Server-side work lives in @chainpatrol/external-trpc and
|
|
42
|
+
@chainpatrol/validation; both are private packages and don't need version bumps.
|
|
43
|
+
|
|
44
|
+
- b037a0b: `GET /organization/assets` now accepts an optional `slug`, so callers whose
|
|
45
|
+
credentials reach more than one organization (user-scoped API keys, staff and
|
|
46
|
+
member sessions) can pick one instead of getting a flat 401. Organization-scoped
|
|
47
|
+
keys keep working unchanged when they omit it.
|
|
48
|
+
|
|
49
|
+
In the SDK, `client.listOrganizationAssets()` takes a `slug`. In the CLI,
|
|
50
|
+
`chainpatrol orgs assets list` forwards `--org <slug>` when one is set — the
|
|
51
|
+
existing no-flag invocation still resolves the org from an org-scoped key.
|
|
52
|
+
|
|
53
|
+
Server-side work lives in @chainpatrol/external-trpc and @chainpatrol/validation;
|
|
54
|
+
both are private packages and don't need version bumps.
|
|
55
|
+
|
|
56
|
+
### Patch Changes
|
|
57
|
+
|
|
58
|
+
- 0e5f156: Fix the install command in the CLI README. The unscoped `chainpatrol` package on npm is a
|
|
59
|
+
placeholder that points at `@chainpatrol/sdk`, so `npm install -g chainpatrol` never
|
|
60
|
+
installed the CLI — the published package is `@chainpatrol/cli`. Also link the README to
|
|
61
|
+
the new CLI documentation at https://chainpatrol.com/docs/cli.
|
|
62
|
+
- Updated dependencies [f98f547]
|
|
63
|
+
- Updated dependencies [9f19c88]
|
|
64
|
+
- Updated dependencies [b037a0b]
|
|
65
|
+
- @chainpatrol/sdk@1.7.0
|
|
66
|
+
|
|
3
67
|
## 1.6.0
|
|
4
68
|
|
|
5
69
|
### 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
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {
|
|
2
2
|
installCompletions,
|
|
3
3
|
uninstallCompletions
|
|
4
|
-
} from "./chunk-
|
|
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";
|
|
@@ -94,6 +94,8 @@ _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'
|
|
98
|
+
'takedown-time-trend:Show month-over-month median time-to-takedown trend'
|
|
97
99
|
'organization:Get organization metrics (official endpoint)'
|
|
98
100
|
)
|
|
99
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-
|
|
17
|
-
import "./chunk-
|
|
16
|
+
} from "./chunk-LE5ZOOUI.js";
|
|
17
|
+
import "./chunk-TI2HK3NM.js";
|
|
18
18
|
import {
|
|
19
19
|
DateTime
|
|
20
20
|
} from "./chunk-TFCNKBRC.js";
|
|
@@ -286,6 +286,8 @@ 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",
|
|
290
|
+
"chainpatrol metrics takedown-time-trend --org acme --months 12",
|
|
289
291
|
"chainpatrol metrics organization --org acme --from 2025-01-01 --to 2025-02-01"
|
|
290
292
|
]
|
|
291
293
|
},
|
|
@@ -346,6 +348,27 @@ var HELP = {
|
|
|
346
348
|
"--brand <ids> Comma-separated brand IDs"
|
|
347
349
|
]
|
|
348
350
|
},
|
|
351
|
+
"metrics takedown-time": {
|
|
352
|
+
description: "Median time from takedown start to completion, per asset type. Defaults to the trailing 3 months if no date range is given.",
|
|
353
|
+
usage: "chainpatrol metrics takedown-time --org <slug> [--from ISO --to ISO|--this-week]",
|
|
354
|
+
options: [
|
|
355
|
+
"--org <slug> Organization slug",
|
|
356
|
+
"--from <iso> Start date (ISO 8601)",
|
|
357
|
+
"--to <iso> End date (ISO 8601); a date-only value covers the whole day",
|
|
358
|
+
"--this-week Use the current ISO week (overrides default)",
|
|
359
|
+
"--brand <ids> Comma-separated brand IDs"
|
|
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
|
+
},
|
|
349
372
|
reports: {
|
|
350
373
|
description: "Create, list, and search reports from the terminal.",
|
|
351
374
|
usage: "chainpatrol reports <create|list|search>",
|
|
@@ -556,9 +579,10 @@ var HELP = {
|
|
|
556
579
|
usage: "chainpatrol orgs assets list [filters]"
|
|
557
580
|
},
|
|
558
581
|
"orgs assets list": {
|
|
559
|
-
description: "List assets belonging to
|
|
582
|
+
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
583
|
usage: "chainpatrol orgs assets list [filters]",
|
|
561
584
|
options: [
|
|
585
|
+
"--org <slug> Organization slug (omit for org-scoped API keys)",
|
|
562
586
|
"--asset-type <type> Filter by asset type (URL, ADDRESS, ...)",
|
|
563
587
|
"--type <type> Alias for --asset-type",
|
|
564
588
|
"--group-id <n> Filter by group ID. Use -1 for ungrouped assets only",
|
|
@@ -568,6 +592,7 @@ var HELP = {
|
|
|
568
592
|
],
|
|
569
593
|
examples: [
|
|
570
594
|
"chainpatrol orgs assets list --asset-type URL",
|
|
595
|
+
"chainpatrol orgs assets list --org acme --per-page 1000",
|
|
571
596
|
"chainpatrol orgs assets list --group-id 12 --per-page 250 --output csv"
|
|
572
597
|
]
|
|
573
598
|
},
|
|
@@ -953,6 +978,7 @@ ${getTopLevelHelp()}
|
|
|
953
978
|
to: { type: "string" },
|
|
954
979
|
thisWeek: { type: "boolean", default: false },
|
|
955
980
|
by: { type: "string" },
|
|
981
|
+
months: { type: "number" },
|
|
956
982
|
include: { type: "string" },
|
|
957
983
|
slugs: { type: "string" },
|
|
958
984
|
allMyOrgs: { type: "boolean", default: false },
|
|
@@ -1299,7 +1325,7 @@ async function main() {
|
|
|
1299
1325
|
}
|
|
1300
1326
|
return n;
|
|
1301
1327
|
});
|
|
1302
|
-
const { runAssetSearch } = await import("./search-
|
|
1328
|
+
const { runAssetSearch } = await import("./search-IWS42COG.js");
|
|
1303
1329
|
await runAssetSearch({
|
|
1304
1330
|
contents: contents.length > 0 ? contents : void 0,
|
|
1305
1331
|
ids: ids.length > 0 ? ids : void 0,
|
|
@@ -1509,6 +1535,31 @@ async function main() {
|
|
|
1509
1535
|
});
|
|
1510
1536
|
break;
|
|
1511
1537
|
}
|
|
1538
|
+
if (subcommand === "takedown-time" || subcommand === "takedownTime") {
|
|
1539
|
+
const { runMetricsTakedownTime } = await import("./takedown-time-ARGVSPL6.js");
|
|
1540
|
+
await runMetricsTakedownTime({
|
|
1541
|
+
org,
|
|
1542
|
+
from: cli.flags.from,
|
|
1543
|
+
to: cli.flags.to,
|
|
1544
|
+
thisWeek: cli.flags.thisWeek,
|
|
1545
|
+
brandIds: parseBrandIds(),
|
|
1546
|
+
json: jsonMode,
|
|
1547
|
+
outputFormat: cliContext.outputFormat
|
|
1548
|
+
});
|
|
1549
|
+
break;
|
|
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
|
+
}
|
|
1512
1563
|
if (subcommand === "breakdown") {
|
|
1513
1564
|
const by = cli.flags.by;
|
|
1514
1565
|
if (!by || !["day", "type", "brand"].includes(by)) {
|
|
@@ -1527,9 +1578,16 @@ async function main() {
|
|
|
1527
1578
|
});
|
|
1528
1579
|
break;
|
|
1529
1580
|
}
|
|
1530
|
-
const hint = subcommand ? suggest(subcommand, [
|
|
1581
|
+
const hint = subcommand ? suggest(subcommand, [
|
|
1582
|
+
"summary",
|
|
1583
|
+
"found",
|
|
1584
|
+
"breakdown",
|
|
1585
|
+
"takedown-time",
|
|
1586
|
+
"takedown-time-trend",
|
|
1587
|
+
"organization"
|
|
1588
|
+
]) : null;
|
|
1531
1589
|
throw new Error(
|
|
1532
|
-
subcommand ? `Unknown subcommand: metrics ${subcommand}${hint ? `. Did you mean "metrics ${hint}"?` : ""}` : "Usage: chainpatrol metrics <summary|found|breakdown|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>"
|
|
1533
1591
|
);
|
|
1534
1592
|
}
|
|
1535
1593
|
case "reports": {
|
|
@@ -1739,8 +1797,11 @@ async function main() {
|
|
|
1739
1797
|
}
|
|
1740
1798
|
if (subcommand === "assets") {
|
|
1741
1799
|
if (action === "list") {
|
|
1742
|
-
const { runOrgsAssetsList } = await import("./list-
|
|
1800
|
+
const { runOrgsAssetsList } = await import("./list-5TKOWBC4.js");
|
|
1743
1801
|
await runOrgsAssetsList({
|
|
1802
|
+
// Optional: organization-scoped API keys resolve the org from the
|
|
1803
|
+
// key, so only pass a slug when one is actually available.
|
|
1804
|
+
slug: await tryResolveOrg(),
|
|
1744
1805
|
type: cli.flags.type ?? cli.flags.assetType,
|
|
1745
1806
|
groupId: cli.flags.groupId,
|
|
1746
1807
|
query: cli.flags.query ?? cli.flags.search,
|
|
@@ -1860,17 +1921,17 @@ async function main() {
|
|
|
1860
1921
|
case "setup":
|
|
1861
1922
|
case "install":
|
|
1862
1923
|
case "i": {
|
|
1863
|
-
const { setupSkill } = await import("./setup-skill-
|
|
1924
|
+
const { setupSkill } = await import("./setup-skill-ESJ4HIKR.js");
|
|
1864
1925
|
setupSkill({ json: jsonMode, cloud: cli.flags.cloud });
|
|
1865
1926
|
break;
|
|
1866
1927
|
}
|
|
1867
1928
|
case "uninstall": {
|
|
1868
|
-
const { uninstallSkill } = await import("./setup-skill-
|
|
1929
|
+
const { uninstallSkill } = await import("./setup-skill-ESJ4HIKR.js");
|
|
1869
1930
|
uninstallSkill({ json: jsonMode });
|
|
1870
1931
|
break;
|
|
1871
1932
|
}
|
|
1872
1933
|
case "completions": {
|
|
1873
|
-
const { printCompletions } = await import("./completions-
|
|
1934
|
+
const { printCompletions } = await import("./completions-7DD5E5HL.js");
|
|
1874
1935
|
printCompletions(subcommand);
|
|
1875
1936
|
break;
|
|
1876
1937
|
}
|
|
@@ -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-
|
|
10
|
-
import "./chunk-
|
|
9
|
+
} from "./chunk-LE5ZOOUI.js";
|
|
10
|
+
import "./chunk-TI2HK3NM.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
|
+
};
|
|
@@ -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.
|
|
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.
|
|
36
|
+
"@chainpatrol/sdk": "1.8.0",
|
|
37
37
|
"ink": "^7.0.1",
|
|
38
38
|
"meow": "^14.1.0",
|
|
39
39
|
"open": "^11.0.0",
|