@chainpatrol/cli 1.5.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 +69 -0
- package/README.md +6 -1
- package/dist/{breakdown-5DJMXRK4.js → breakdown-2HAZ2MX5.js} +1 -1
- package/dist/{check-5P3EXVSN.js → check-5LJI7N5I.js} +1 -1
- package/dist/{chunk-OFUIYMPD.js → chunk-322AVMUH.js} +4 -0
- package/dist/{chunk-6VTWWGNV.js → chunk-7BZUQSRO.js} +12 -1
- package/dist/{chunk-PEX2A3IL.js → chunk-FPCN4SNK.js} +1 -1
- package/dist/{chunk-MM5OAY2U.js → chunk-YMO4QK6Q.js} +1 -1
- package/dist/cli.js +153 -44
- package/dist/{completions-ILC76OX5.js → completions-3ESPMO65.js} +1 -1
- package/dist/{configs-update-TAYT4JTU.js → configs-update-N7ZIWWKQ.js} +1 -1
- package/dist/{create-UXRULUR7.js → create-4ZJ7DXHX.js} +1 -1
- package/dist/{drift-TCZELVHO.js → drift-BGWNH6QE.js} +1 -1
- package/dist/{found-NCP6H2SD.js → found-QOARB5G2.js} +1 -1
- package/dist/{get-SM5KGJVA.js → get-ON5JOCV7.js} +1 -1
- package/dist/{healthcheck-5YEZCTXC.js → healthcheck-34LQTSVZ.js} +1 -1
- package/dist/{list-MPSB2DYU.js → list-5TKOWBC4.js} +2 -1
- package/dist/{list-2RHEVI2T.js → list-6CAKS43I.js} +1 -1
- package/dist/{list-WQBXC3QS.js → list-AG6LWNMN.js} +1 -1
- package/dist/{list-W6JZUNS2.js → list-DJFIKDY6.js} +1 -1
- package/dist/{list-4YBWU2JZ.js → list-IVFZGLKN.js} +1 -1
- package/dist/{list-23C72EH4.js → list-JG5ARDTR.js} +2 -2
- package/dist/{list-BAPTZNTM.js → list-MHOHKFXT.js} +1 -1
- package/dist/{list-TA4HYD66.js → list-PAJBOVN2.js} +1 -1
- package/dist/{list-QLMFMYU6.js → list-SJFJT4V3.js} +1 -1
- package/dist/{list-SCA73PLA.js → list-UPY6PHY4.js} +1 -1
- package/dist/{list-GEJQ6N2Y.js → list-UXYGISAK.js} +1 -1
- package/dist/{list-QJYRXPXM.js → list-W6CF25ZS.js} +1 -1
- package/dist/{list-json-NJ5IFRDE.js → list-json-ZR3XOQPJ.js} +1 -1
- package/dist/{organization-N6RMWKJM.js → organization-YAID36GZ.js} +1 -1
- package/dist/review-7A7WV73T.js +270 -0
- package/dist/{run-RRG7D4SN.js → run-AIYPHWFJ.js} +1 -1
- package/dist/{run-KOZXEYTL.js → run-LZZOV4HG.js} +1 -1
- package/dist/{run-TUADYLU7.js → run-YP4O5VXZ.js} +2 -2
- package/dist/{search-Y37B2JVP.js → search-IWS42COG.js} +2 -2
- package/dist/{search-M6EKPO4D.js → search-OGGKNPUJ.js} +1 -1
- package/dist/{setup-skill-RRTDEOR4.js → setup-skill-LP5Z3Z5T.js} +2 -2
- package/dist/{snapshot-RFGSAHO7.js → snapshot-TLSOPK7C.js} +1 -1
- package/dist/{summary-XCN3DRYP.js → summary-XA3PVJIW.js} +1 -1
- package/dist/takedown-time-ARGVSPL6.js +87 -0
- package/dist/{validate-OYTNB2ZM.js → validate-WB5367QN.js} +1 -1
- package/dist/{whoami-5WCW3FOZ.js → whoami-YE3V6I65.js} +1 -1
- package/package.json +2 -2
|
@@ -0,0 +1,270 @@
|
|
|
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
|
+
PROPOSAL_REJECT_REASONS,
|
|
11
|
+
PROPOSAL_WATCHLIST_REASONS,
|
|
12
|
+
createApiClient
|
|
13
|
+
} from "./chunk-322AVMUH.js";
|
|
14
|
+
import "./chunk-EGWK6SRQ.js";
|
|
15
|
+
import "./chunk-U73SABXK.js";
|
|
16
|
+
|
|
17
|
+
// src/commands/proposals/review.ts
|
|
18
|
+
var BRAND_REQUIRED_LABELS = ["brand impersonation", "employee impersonation"];
|
|
19
|
+
var DECISIONS = ["APPROVE", "REJECT", "WATCHLIST"];
|
|
20
|
+
function parsePositiveId(value, flag) {
|
|
21
|
+
if (value === void 0) {
|
|
22
|
+
throw new CliExitError(`proposals review requires ${flag} <id>.`, ExitCode.USAGE);
|
|
23
|
+
}
|
|
24
|
+
if (!Number.isInteger(value) || value < 1) {
|
|
25
|
+
throw new CliExitError(`${flag} must be a positive integer.`, ExitCode.USAGE);
|
|
26
|
+
}
|
|
27
|
+
return value;
|
|
28
|
+
}
|
|
29
|
+
function parseDecision(value) {
|
|
30
|
+
if (!value) {
|
|
31
|
+
throw new CliExitError(
|
|
32
|
+
"proposals review requires --decision <approve|reject|watchlist>.",
|
|
33
|
+
ExitCode.USAGE
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
const normalized = value.trim().toUpperCase();
|
|
37
|
+
if (!DECISIONS.includes(normalized)) {
|
|
38
|
+
throw new CliExitError(
|
|
39
|
+
`Invalid --decision '${value}'. Valid: approve, reject, watchlist.`,
|
|
40
|
+
ExitCode.USAGE
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
return normalized;
|
|
44
|
+
}
|
|
45
|
+
function parseRejectReason(value) {
|
|
46
|
+
if (!value) {
|
|
47
|
+
throw new CliExitError(
|
|
48
|
+
`--reject-reason is required when --decision reject. Valid: ${PROPOSAL_REJECT_REASONS.join(
|
|
49
|
+
", "
|
|
50
|
+
)}`,
|
|
51
|
+
ExitCode.USAGE
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
const normalized = value.trim().toLowerCase();
|
|
55
|
+
if (!PROPOSAL_REJECT_REASONS.includes(normalized)) {
|
|
56
|
+
throw new CliExitError(
|
|
57
|
+
`Invalid --reject-reason '${value}'. Valid: ${PROPOSAL_REJECT_REASONS.join(", ")}`,
|
|
58
|
+
ExitCode.USAGE
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
return normalized;
|
|
62
|
+
}
|
|
63
|
+
function parseWatchlistReason(value) {
|
|
64
|
+
if (!value) {
|
|
65
|
+
throw new CliExitError(
|
|
66
|
+
`--watchlist-reason is required when --decision watchlist. Valid: ${PROPOSAL_WATCHLIST_REASONS.join(
|
|
67
|
+
", "
|
|
68
|
+
)}`,
|
|
69
|
+
ExitCode.USAGE
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
const normalized = value.trim().toUpperCase();
|
|
73
|
+
if (!PROPOSAL_WATCHLIST_REASONS.includes(normalized)) {
|
|
74
|
+
throw new CliExitError(
|
|
75
|
+
`Invalid --watchlist-reason '${value}'. Valid: ${PROPOSAL_WATCHLIST_REASONS.join(
|
|
76
|
+
", "
|
|
77
|
+
)}`,
|
|
78
|
+
ExitCode.USAGE
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
return normalized;
|
|
82
|
+
}
|
|
83
|
+
function buildReviewInput(options) {
|
|
84
|
+
const proposalId = parsePositiveId(options.proposalId, "--proposal-id");
|
|
85
|
+
const assetId = parsePositiveId(options.assetId, "--asset-id");
|
|
86
|
+
const decision = parseDecision(options.decision);
|
|
87
|
+
const input = {
|
|
88
|
+
proposalId,
|
|
89
|
+
assetId,
|
|
90
|
+
slug: options.org,
|
|
91
|
+
decision
|
|
92
|
+
};
|
|
93
|
+
if (decision === "APPROVE") {
|
|
94
|
+
const label = options.label?.trim();
|
|
95
|
+
if (!label) {
|
|
96
|
+
throw new CliExitError(
|
|
97
|
+
"--label is required when --decision approve.",
|
|
98
|
+
ExitCode.USAGE
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
if (BRAND_REQUIRED_LABELS.includes(label.toLowerCase()) && options.brandId === void 0) {
|
|
102
|
+
throw new CliExitError(
|
|
103
|
+
`--brand-id is required when --label is '${label}'. List your brands with \`chainpatrol brands list\`.`,
|
|
104
|
+
ExitCode.USAGE
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
input.label = label;
|
|
108
|
+
if (options.brandId !== void 0) {
|
|
109
|
+
input.brandId = parsePositiveId(options.brandId, "--brand-id");
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (decision === "REJECT") {
|
|
113
|
+
input.rejectReason = parseRejectReason(options.rejectReason);
|
|
114
|
+
}
|
|
115
|
+
if (decision === "WATCHLIST") {
|
|
116
|
+
const watchlistReason = parseWatchlistReason(options.watchlistReason);
|
|
117
|
+
if (watchlistReason === "OTHER" && !options.note?.trim()) {
|
|
118
|
+
throw new CliExitError(
|
|
119
|
+
"--note is required when --watchlist-reason OTHER, since the note becomes the reason.",
|
|
120
|
+
ExitCode.USAGE
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
input.watchlistReason = watchlistReason;
|
|
124
|
+
}
|
|
125
|
+
const note = options.note?.trim();
|
|
126
|
+
if (note) {
|
|
127
|
+
input.note = note;
|
|
128
|
+
}
|
|
129
|
+
return input;
|
|
130
|
+
}
|
|
131
|
+
function describeReview(input) {
|
|
132
|
+
const lines = [
|
|
133
|
+
`Decision: ${input.decision}`,
|
|
134
|
+
`Proposal: #${input.proposalId}`,
|
|
135
|
+
`Asset: #${input.assetId}`,
|
|
136
|
+
`Organization: ${input.slug ?? "from credential context"}`
|
|
137
|
+
];
|
|
138
|
+
if (input.label) {
|
|
139
|
+
lines.push(`Label: ${input.label}`);
|
|
140
|
+
}
|
|
141
|
+
if (input.brandId !== void 0) {
|
|
142
|
+
lines.push(`Brand ID: ${input.brandId}`);
|
|
143
|
+
}
|
|
144
|
+
if (input.rejectReason) {
|
|
145
|
+
lines.push(`Reject reason: ${input.rejectReason}`);
|
|
146
|
+
}
|
|
147
|
+
if (input.watchlistReason) {
|
|
148
|
+
lines.push(`Watchlist reason: ${input.watchlistReason}`);
|
|
149
|
+
}
|
|
150
|
+
if (input.note) {
|
|
151
|
+
lines.push(`Note: ${input.note}`);
|
|
152
|
+
}
|
|
153
|
+
return lines;
|
|
154
|
+
}
|
|
155
|
+
async function promptForConfirmation(summary) {
|
|
156
|
+
const { createInterface } = await import("readline/promises");
|
|
157
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
158
|
+
try {
|
|
159
|
+
const answer = await rl.question(`${summary}
|
|
160
|
+
Type "yes" to continue: `);
|
|
161
|
+
const normalized = answer.trim().toLowerCase();
|
|
162
|
+
return normalized === "y" || normalized === "yes";
|
|
163
|
+
} finally {
|
|
164
|
+
rl.close();
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
async function runProposalsReview(options) {
|
|
168
|
+
const input = buildReviewInput(options);
|
|
169
|
+
const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
|
|
170
|
+
const details = describeReview(input);
|
|
171
|
+
if (options.dryRun) {
|
|
172
|
+
printOutput({
|
|
173
|
+
outputFormat,
|
|
174
|
+
json: {
|
|
175
|
+
dryRun: true,
|
|
176
|
+
mutation: "organization.proposals.review",
|
|
177
|
+
payload: input,
|
|
178
|
+
explanation: options.explain ? "This command records a customer review on the proposal and, for APPROVE, blocks the asset." : void 0
|
|
179
|
+
},
|
|
180
|
+
markdown: ["# Dry Run: Proposal Review", "", ...details.map((d) => `- ${d}`)].join(
|
|
181
|
+
"\n"
|
|
182
|
+
),
|
|
183
|
+
csv: toCsvRows([
|
|
184
|
+
{
|
|
185
|
+
proposalId: input.proposalId,
|
|
186
|
+
assetId: input.assetId,
|
|
187
|
+
decision: input.decision,
|
|
188
|
+
label: input.label ?? "",
|
|
189
|
+
rejectReason: input.rejectReason ?? "",
|
|
190
|
+
watchlistReason: input.watchlistReason ?? ""
|
|
191
|
+
}
|
|
192
|
+
]),
|
|
193
|
+
human: () => {
|
|
194
|
+
console.log("Dry run only. No review was submitted.");
|
|
195
|
+
for (const line of details) {
|
|
196
|
+
console.log(line);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
return;
|
|
201
|
+
}
|
|
202
|
+
if (!options.yes) {
|
|
203
|
+
const summary = [
|
|
204
|
+
"About to submit a review that cannot be undone over the API:",
|
|
205
|
+
...details.map((line) => ` ${line}`)
|
|
206
|
+
].join("\n");
|
|
207
|
+
if (options.confirm) {
|
|
208
|
+
if (!await options.confirm(summary)) {
|
|
209
|
+
console.log("Aborted. No review was submitted.");
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
} else if (options.noInput || outputFormat !== "human" || !process.stdin.isTTY) {
|
|
213
|
+
throw new CliExitError(
|
|
214
|
+
"proposals review cannot be undone and needs confirmation. Re-run with --yes, or run it in an interactive terminal.",
|
|
215
|
+
ExitCode.USAGE
|
|
216
|
+
);
|
|
217
|
+
} else if (!await promptForConfirmation(summary)) {
|
|
218
|
+
console.log("Aborted. No review was submitted.");
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const client = options.apiClient ?? createApiClient();
|
|
223
|
+
const result = await client.reviewOrganizationProposal(input);
|
|
224
|
+
const row = {
|
|
225
|
+
proposalId: result.proposal.id,
|
|
226
|
+
reviewStatus: result.proposal.reviewStatus,
|
|
227
|
+
assetId: result.proposal.asset.id,
|
|
228
|
+
assetType: result.proposal.asset.type,
|
|
229
|
+
assetContent: result.proposal.asset.content,
|
|
230
|
+
assetStatus: result.proposal.asset.status,
|
|
231
|
+
reviewId: result.review.id,
|
|
232
|
+
decision: result.review.decision,
|
|
233
|
+
label: result.review.label,
|
|
234
|
+
watchlisted: result.watchlisted
|
|
235
|
+
};
|
|
236
|
+
printOutput({
|
|
237
|
+
outputFormat,
|
|
238
|
+
json: {
|
|
239
|
+
...result,
|
|
240
|
+
explanation: options.explain ? "A WATCHLIST request is recorded as a REJECT review with the asset parked for re-evaluation." : void 0
|
|
241
|
+
},
|
|
242
|
+
markdown: [
|
|
243
|
+
"# Proposal Reviewed",
|
|
244
|
+
"",
|
|
245
|
+
`- Proposal: #${row.proposalId} (${row.reviewStatus})`,
|
|
246
|
+
`- Asset: #${row.assetId} ${row.assetType} ${row.assetContent} \u2192 ${row.assetStatus}`,
|
|
247
|
+
`- Review: #${row.reviewId} ${row.decision}${row.label ? ` (${row.label})` : ""}`,
|
|
248
|
+
`- Watchlisted: ${row.watchlisted ? "yes" : "no"}`
|
|
249
|
+
].join("\n"),
|
|
250
|
+
csv: toCsvRows([row]),
|
|
251
|
+
human: () => {
|
|
252
|
+
console.log(`Reviewed proposal #${row.proposalId} as ${row.decision}`);
|
|
253
|
+
console.log(`Asset: ${row.assetType} ${row.assetContent} \u2192 ${row.assetStatus}`);
|
|
254
|
+
console.log(`Review: #${row.reviewId}${row.label ? ` label=${row.label}` : ""}`);
|
|
255
|
+
if (row.watchlisted) {
|
|
256
|
+
console.log("Asset parked on the watchlist for re-evaluation.");
|
|
257
|
+
}
|
|
258
|
+
if (options.explain) {
|
|
259
|
+
console.log(
|
|
260
|
+
"Reviews over the API are limited to 10 per organization per day and are shared across all of the org's API credentials."
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
});
|
|
265
|
+
}
|
|
266
|
+
export {
|
|
267
|
+
buildReviewInput,
|
|
268
|
+
describeReview,
|
|
269
|
+
runProposalsReview
|
|
270
|
+
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getPresetDefinition,
|
|
3
3
|
runPreset
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-FPCN4SNK.js";
|
|
5
5
|
import {
|
|
6
6
|
CliExitError,
|
|
7
7
|
ExitCode
|
|
8
8
|
} from "./chunk-E2LAMILJ.js";
|
|
9
9
|
import "./chunk-VFT3TD3E.js";
|
|
10
|
-
import "./chunk-
|
|
10
|
+
import "./chunk-322AVMUH.js";
|
|
11
11
|
import "./chunk-TFCNKBRC.js";
|
|
12
12
|
import "./chunk-EGWK6SRQ.js";
|
|
13
13
|
import "./chunk-U73SABXK.js";
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./chunk-VFT3TD3E.js";
|
|
9
9
|
import {
|
|
10
10
|
createApiClient
|
|
11
|
-
} from "./chunk-
|
|
11
|
+
} from "./chunk-322AVMUH.js";
|
|
12
12
|
import "./chunk-EGWK6SRQ.js";
|
|
13
13
|
import "./chunk-U73SABXK.js";
|
|
14
14
|
|
|
@@ -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-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.
|
|
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.
|
|
36
|
+
"@chainpatrol/sdk": "1.7.0",
|
|
37
37
|
"ink": "^7.0.1",
|
|
38
38
|
"meow": "^14.1.0",
|
|
39
39
|
"open": "^11.0.0",
|