@chainpatrol/cli 0.3.3 → 0.4.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 +50 -0
- package/dist/{breakdown-FFNYSXRQ.js → breakdown-EBSACUST.js} +1 -1
- package/dist/{chunk-44FSS3CZ.js → chunk-MXUZR2BV.js} +6 -0
- package/dist/{chunk-WBKCXGLV.js → chunk-PIYOWGBZ.js} +1 -1
- package/dist/{chunk-R7BPW32M.js → chunk-S65NM7FF.js} +352 -7
- package/dist/cli.js +82 -19
- package/dist/{configs-update-VOWH674W.js → configs-update-RPN32YTL.js} +1 -1
- package/dist/{create-ASGUBKZ7.js → create-QP3M7EZM.js} +1 -1
- package/dist/{drift-PASGDEEX.js → drift-DZ6A7JL5.js} +1 -1
- package/dist/{found-TIW7T4VX.js → found-AOPBSLRD.js} +1 -1
- package/dist/{healthcheck-SIE5EXN3.js → healthcheck-KAONRGSS.js} +1 -1
- package/dist/{list-TMFLCS5U.js → list-GEMCFDD5.js} +1 -1
- package/dist/{list-KTIZ3UHA.js → list-LN6NOZIJ.js} +2 -2
- package/dist/{list-CEWBMKJ3.js → list-MWDFCHMJ.js} +1 -1
- package/dist/list-UW63DIKX.js +73 -0
- package/dist/{list-json-SUZL2GBJ.js → list-json-WTMYLZGY.js} +1 -1
- package/dist/{run-MGOASUON.js → run-43CC5AXR.js} +1 -1
- package/dist/run-7THXM7GF.js +209 -0
- package/dist/{run-3TTLZ6HA.js → run-MH5RYPWA.js} +2 -2
- package/dist/{setup-skill-KKWETU4A.js → setup-skill-Z5RVCWCU.js} +1 -1
- package/dist/{snapshot-MQ6DWDFG.js → snapshot-E3TPZOKT.js} +1 -1
- package/dist/{summary-QORQFCMW.js → summary-6NCA7PDP.js} +1 -1
- package/dist/{validate-II6GH6H6.js → validate-BJFEKI2N.js} +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,55 @@
|
|
|
1
1
|
# @chainpatrol/cli
|
|
2
2
|
|
|
3
|
+
## 0.4.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- b5de201: Add a `healthchecks` namespace to the public API and CLI.
|
|
8
|
+
|
|
9
|
+
The new `healthchecks list` returns a registry of every check the platform
|
|
10
|
+
exposes today, including planned checks that are not yet implemented on the
|
|
11
|
+
backend (marked `implemented: false` with a `notImplementedReason`). The new
|
|
12
|
+
`healthchecks run <id|--all> --org <slug>` runs a single check or every
|
|
13
|
+
implemented check in parallel and returns each result in a uniform shape
|
|
14
|
+
(`id`, `severity`, `observed`, `threshold`, `findings`, `suggestedAction`).
|
|
15
|
+
|
|
16
|
+
Implemented checks in this release:
|
|
17
|
+
|
|
18
|
+
- `detections.silent-configs` — the existing detection-config healthcheck,
|
|
19
|
+
wrapped in the uniform shape. The original `detections healthcheck`
|
|
20
|
+
command continues to work.
|
|
21
|
+
- `reviewing.backlog` — counts pending review proposals and grades severity
|
|
22
|
+
against per-org thresholds (default warn=50, fail=100).
|
|
23
|
+
- `reviewing.old-proposals` — counts proposals older than the warn / fail
|
|
24
|
+
age thresholds (default 7 / 14 days) and lists the oldest offenders.
|
|
25
|
+
- `takedowns.stale-in-progress` — counts takedowns sitting in IN_PROGRESS
|
|
26
|
+
past the staleness threshold (default 7 days).
|
|
27
|
+
|
|
28
|
+
Planned but not yet implemented (still returned by `list` with
|
|
29
|
+
`implemented: false`): `detections.coverage-gaps`, `detections.spike`,
|
|
30
|
+
`detections.drop`, `reviewing.auto-approval-spike`,
|
|
31
|
+
`blocklisting.gsb-cancelled-rate`, `takedowns.todo-volume`,
|
|
32
|
+
`takedowns.cancelled-rate`, `takedowns.automation-off`.
|
|
33
|
+
|
|
34
|
+
The bundled Claude Code skill is updated to (a) document the new namespace,
|
|
35
|
+
(b) recommend `healthchecks run --all` as the canonical entrypoint when a
|
|
36
|
+
user asks to run a healthcheck on an org, and (c) mark every not-yet-
|
|
37
|
+
implemented check in the HealthCheck Guide with a "manual check, no API
|
|
38
|
+
yet" note so the agent surfaces that gap explicitly.
|
|
39
|
+
|
|
40
|
+
## 0.3.4
|
|
41
|
+
|
|
42
|
+
### Patch Changes
|
|
43
|
+
|
|
44
|
+
- e30617f: Update the bundled Claude Code skill to document the `detections healthcheck`,
|
|
45
|
+
`queues snapshot`, `metrics summary | found | breakdown`, and `presets`
|
|
46
|
+
subcommands, plus the `--no-input` / `--no-color` / `--output` / `--quiet`
|
|
47
|
+
global flags. The existing soft / manual signals in the Organization
|
|
48
|
+
HealthCheck Guide are preserved verbatim; each subsection now also points at
|
|
49
|
+
the matching CLI command via a "Run via CLI" note, and a new "Quick Path"
|
|
50
|
+
block lists the commands an agent should reach for first when asked to run
|
|
51
|
+
a health check.
|
|
52
|
+
|
|
3
53
|
## 0.3.3
|
|
4
54
|
|
|
5
55
|
### Patch Changes
|
|
@@ -141,6 +141,12 @@ function createApiClient(options) {
|
|
|
141
141
|
searchQuery: input.searchQuery,
|
|
142
142
|
reportedByCustomer: input.reportedByCustomer
|
|
143
143
|
});
|
|
144
|
+
},
|
|
145
|
+
listHealthchecks() {
|
|
146
|
+
return request("/healthchecks/list", {});
|
|
147
|
+
},
|
|
148
|
+
runHealthcheck(endpoint, input) {
|
|
149
|
+
return request(endpoint, input);
|
|
144
150
|
}
|
|
145
151
|
};
|
|
146
152
|
}
|
|
@@ -264,6 +264,166 @@ You can also compare with the non-customer set using
|
|
|
264
264
|
\`--no-reported-by-customer\` to gauge detection coverage on the same time
|
|
265
265
|
window.
|
|
266
266
|
|
|
267
|
+
### \`detections healthcheck\` \u2014 Validate enabled detection configs produce recent results
|
|
268
|
+
|
|
269
|
+
Server-side check. The CLI calls the ChainPatrol API; the server fetches each
|
|
270
|
+
enabled detection config for the org, counts results produced in the lookback
|
|
271
|
+
window, and FAILs configs that fall under \`--min-results\` (or whose run
|
|
272
|
+
errored when \`--run\` is set).
|
|
273
|
+
|
|
274
|
+
\`\`\`bash
|
|
275
|
+
chainpatrol --json detections healthcheck --org <slug>
|
|
276
|
+
\`\`\`
|
|
277
|
+
|
|
278
|
+
Flags:
|
|
279
|
+
- \`--source <key>\` only validate one source (e.g. \`twitter_search\`)
|
|
280
|
+
- \`--min-results <n>\` minimum results required in the window to pass
|
|
281
|
+
- \`--lookback-hours <n>\` size of the lookback window
|
|
282
|
+
- \`--run\` ask the server to run each config first, then validate the fresh output
|
|
283
|
+
- \`--include-disabled\` also validate disabled configs
|
|
284
|
+
|
|
285
|
+
What this command covers:
|
|
286
|
+
- Configs that have gone silent (recentResultCount below threshold)
|
|
287
|
+
- Configs that error when run (runOk=false) when \`--run\` is set
|
|
288
|
+
|
|
289
|
+
What it does NOT cover:
|
|
290
|
+
- Reviewing backlog / SLA breaches \u2192 use \`queues snapshot\`
|
|
291
|
+
- Takedown ToDo / In Progress / Cancelled volumes \u2192 use \`queues snapshot\`
|
|
292
|
+
- Spikes or drops in detection volume over time \u2192 use \`metrics breakdown\`
|
|
293
|
+
- Customer-reported gaps \u2192 use \`reports list --reported-by-customer\`
|
|
294
|
+
- Google Safe Browsing submission errors (not yet exposed in CLI)
|
|
295
|
+
|
|
296
|
+
Use it as the first signal in the Detection part of an org healthcheck, then
|
|
297
|
+
fall back to the manual checks in the HealthCheck Guide below for everything
|
|
298
|
+
else.
|
|
299
|
+
|
|
300
|
+
> Prefer the newer \`healthchecks\` namespace below. \`detections healthcheck\`
|
|
301
|
+
> is the original single-purpose command; the \`healthchecks\` namespace is
|
|
302
|
+
> the canonical place to discover and run every check we expose.
|
|
303
|
+
|
|
304
|
+
### \`healthchecks list | run\` \u2014 Run uniform org healthchecks via the public API
|
|
305
|
+
|
|
306
|
+
The \`healthchecks\` namespace is the canonical way to run the named checks
|
|
307
|
+
from the Organization HealthCheck Guide below. Each implemented endpoint
|
|
308
|
+
returns the same uniform shape \u2014 \`{ id, ok, severity, observed, threshold,
|
|
309
|
+
findings, suggestedAction }\` \u2014 so the CLI / agent can render every check the
|
|
310
|
+
same way regardless of category.
|
|
311
|
+
|
|
312
|
+
\`\`\`bash
|
|
313
|
+
# Discover every check the platform exposes today, including planned checks
|
|
314
|
+
# that are not yet implemented on the backend.
|
|
315
|
+
chainpatrol --json healthchecks list
|
|
316
|
+
|
|
317
|
+
# Run a single named check.
|
|
318
|
+
chainpatrol --json healthchecks run reviewing.backlog --org <slug>
|
|
319
|
+
|
|
320
|
+
# Run every implemented check in parallel and aggregate the results.
|
|
321
|
+
chainpatrol --json healthchecks run --all --org <slug>
|
|
322
|
+
\`\`\`
|
|
323
|
+
|
|
324
|
+
Each implemented check has a stable id of the form \`category.name\`, e.g.
|
|
325
|
+
\`detections.silent-configs\`, \`reviewing.backlog\`,
|
|
326
|
+
\`reviewing.old-proposals\`, \`takedowns.stale-in-progress\`.
|
|
327
|
+
|
|
328
|
+
Implemented checks today:
|
|
329
|
+
|
|
330
|
+
- **detections.silent-configs** \u2014 equivalent to \`detections healthcheck\`,
|
|
331
|
+
exposed under the uniform shape.
|
|
332
|
+
- **reviewing.backlog** \u2014 counts proposals in PENDING review state and grades
|
|
333
|
+
severity against per-org thresholds (default warn=50, fail=100).
|
|
334
|
+
- **reviewing.old-proposals** \u2014 counts proposals older than the warn / fail
|
|
335
|
+
age thresholds (default 7 / 14 days) and lists the oldest offenders.
|
|
336
|
+
- **takedowns.stale-in-progress** \u2014 counts takedowns sitting in IN_PROGRESS
|
|
337
|
+
past the staleness threshold (default 7 days) and lists the oldest.
|
|
338
|
+
|
|
339
|
+
The following checks are listed by \`healthchecks list\` (\`implemented: false\`)
|
|
340
|
+
but **not yet implemented on the backend** \u2014 when the agent surfaces them in
|
|
341
|
+
a healthcheck report, mark them explicitly as "manual check, no API yet":
|
|
342
|
+
|
|
343
|
+
- **detections.coverage-gaps** \u2014 blocked assets vs. enabled-source correlation.
|
|
344
|
+
Still requires manual reasoning with \`configs list\` + \`reports list\`.
|
|
345
|
+
- **detections.spike** / **detections.drop** \u2014 require server-side baseline
|
|
346
|
+
modeling. Use \`metrics breakdown --by day\` as an interim signal.
|
|
347
|
+
- **reviewing.auto-approval-spike** \u2014 needs distinguishing automation vs.
|
|
348
|
+
human approvers in the review history. Use \`metrics breakdown\` as a proxy.
|
|
349
|
+
- **blocklisting.gsb-cancelled-rate** \u2014 Google Safe Browsing submission state
|
|
350
|
+
is not yet exposed in the public API.
|
|
351
|
+
- **takedowns.todo-volume** / **takedowns.cancelled-rate** /
|
|
352
|
+
**takedowns.automation-off** \u2014 the underlying read paths are not yet exposed.
|
|
353
|
+
|
|
354
|
+
When the user asks to "run a healthcheck on org X", the canonical command is:
|
|
355
|
+
|
|
356
|
+
\`\`\`bash
|
|
357
|
+
chainpatrol --json healthchecks run --all --org X
|
|
358
|
+
\`\`\`
|
|
359
|
+
|
|
360
|
+
This iterates the implemented entries in \`healthchecks list\`, runs them in
|
|
361
|
+
parallel, and aggregates the uniform results. Combine with the manual checks
|
|
362
|
+
in the HealthCheck Guide below for everything still marked
|
|
363
|
+
\`implemented: false\`.
|
|
364
|
+
|
|
365
|
+
### \`queues snapshot\` \u2014 Operations review/takedown queue snapshot
|
|
366
|
+
|
|
367
|
+
Server-side aggregation of the operations review queue (pending proposals,
|
|
368
|
+
SLA breaches, age buckets) and the takedown queue (open, in-progress, stale).
|
|
369
|
+
|
|
370
|
+
\`\`\`bash
|
|
371
|
+
chainpatrol --json queues snapshot --org <slug>
|
|
372
|
+
\`\`\`
|
|
373
|
+
|
|
374
|
+
Useful for the **Reviewing** and **Takedowns** sections of the HealthCheck
|
|
375
|
+
Guide. Key signals in the response:
|
|
376
|
+
- \`reviewQueue.totalPendingProposals\` and \`reviewQueue.distinctReports\` \u2014
|
|
377
|
+
backlog size
|
|
378
|
+
- \`reviewQueue.slaBuckets.breached\` \u2014 SLA breaches (treat any breach as a
|
|
379
|
+
finding)
|
|
380
|
+
- \`reviewQueue.ageBuckets.gte168h\` \u2014 proposals older than 7 days (anything
|
|
381
|
+
>14 days from the manual guide should always be in here)
|
|
382
|
+
- \`takedownQueue.totalOpen\` and \`takedownQueue.staleInProgress\` \u2014 open
|
|
383
|
+
and stuck takedowns
|
|
384
|
+
|
|
385
|
+
Use \`--all\` to snapshot every org you have access to instead of a single slug.
|
|
386
|
+
|
|
387
|
+
### \`metrics summary | found | breakdown\` \u2014 Org metrics for spike/drop analysis
|
|
388
|
+
|
|
389
|
+
\`\`\`bash
|
|
390
|
+
chainpatrol --json metrics summary --org <slug> --this-week
|
|
391
|
+
chainpatrol --json metrics breakdown --org <slug> --by day --this-week
|
|
392
|
+
chainpatrol --json metrics found --org <slug> --from <YYYY-MM-DD> --to <YYYY-MM-DD>
|
|
393
|
+
\`\`\`
|
|
394
|
+
|
|
395
|
+
\`breakdown\` is the one you usually want for healthchecks: it returns a
|
|
396
|
+
time series (by day or week) of reports, new threats, watchlisted threats,
|
|
397
|
+
and takedowns filed/completed. Compare the latest period against a prior
|
|
398
|
+
window to spot the **spike** or **drop** signals described in the manual
|
|
399
|
+
HealthCheck Guide. \`summary\` returns a single window total; \`found\` is
|
|
400
|
+
oriented around when threats were first discovered.
|
|
401
|
+
|
|
402
|
+
### \`presets list | run\` \u2014 Packaged workflows for common jobs
|
|
403
|
+
|
|
404
|
+
\`\`\`bash
|
|
405
|
+
chainpatrol presets list
|
|
406
|
+
chainpatrol presets run cs-weekly-health --org <slug>
|
|
407
|
+
\`\`\`
|
|
408
|
+
|
|
409
|
+
Use \`presets list\` to discover packaged multi-step workflows. The bundled
|
|
410
|
+
\`cs-weekly-health\` preset runs the standard customer-success weekly health
|
|
411
|
+
sweep; prefer it over hand-rolling the same sequence of commands.
|
|
412
|
+
|
|
413
|
+
## Headless / agent-mode tips
|
|
414
|
+
|
|
415
|
+
When running these commands from an agent or CI:
|
|
416
|
+
|
|
417
|
+
- Prefer \`--json\` (or \`--output json\`) so output is structured and the
|
|
418
|
+
update-check stderr nudge is suppressed automatically.
|
|
419
|
+
- Pass \`--no-input\` to forbid any interactive prompt (the CLI will error
|
|
420
|
+
out instead of waiting on a TTY).
|
|
421
|
+
- Pass \`--no-color\` or set \`NO_COLOR=1\` if your sink can't render ANSI.
|
|
422
|
+
- Set \`CHAINPATROL_NO_UPDATE_CHECK=1\` to silence the skill/npm freshness
|
|
423
|
+
nudge entirely.
|
|
424
|
+
- For \`login\` specifically, see the headless runbook in the login section
|
|
425
|
+
above \u2014 login is the one command that needs background + tail handling.
|
|
426
|
+
|
|
267
427
|
## Checking Auth Status
|
|
268
428
|
|
|
269
429
|
To check if the user is logged in, read the credentials file:
|
|
@@ -303,12 +463,16 @@ machine-readable output is never polluted.
|
|
|
303
463
|
|
|
304
464
|
## Global Flags
|
|
305
465
|
|
|
306
|
-
| Flag
|
|
307
|
-
|
|
308
|
-
| \`--json\`
|
|
309
|
-
| \`--
|
|
310
|
-
| \`--
|
|
311
|
-
| \`--
|
|
466
|
+
| Flag | Description |
|
|
467
|
+
|------------------|--------------------------------------------------------|
|
|
468
|
+
| \`--json\` | Machine-readable JSON output (shortcut for \`--output json\`) |
|
|
469
|
+
| \`--output <fmt>\` | Output format: \`human\` (default), \`json\`, \`markdown\`, \`csv\` |
|
|
470
|
+
| \`--quiet\`, \`-q\` | Suppress non-essential output and the update-check nudge |
|
|
471
|
+
| \`--no-color\` | Disable ANSI colors (also respects the \`NO_COLOR\` env var) |
|
|
472
|
+
| \`--no-input\` | Disable interactive prompts (use in scripts and agents) |
|
|
473
|
+
| \`--org <slug>\` | Organization slug (saved as the default for later commands) |
|
|
474
|
+
| \`--help\`, \`-h\` | Show help |
|
|
475
|
+
| \`--version\` | Show version |
|
|
312
476
|
|
|
313
477
|
## Workflow
|
|
314
478
|
|
|
@@ -336,7 +500,99 @@ When the user asks for a "health check", "audit", "what's wrong with org X",
|
|
|
336
500
|
"review org X's setup", or similar, walk through each section below and surface
|
|
337
501
|
findings.
|
|
338
502
|
|
|
339
|
-
In each section there are things you can look for that may be wrong.
|
|
503
|
+
In each section there are things you can look for that may be wrong. Some
|
|
504
|
+
checks have a dedicated CLI command that does most of the work server-side;
|
|
505
|
+
others are soft / qualitative signals that still need you to fetch data with
|
|
506
|
+
\`configs list\` or \`reports list\` and reason about it manually.
|
|
507
|
+
|
|
508
|
+
### Quick Path: CLI commands that automate parts of this guide
|
|
509
|
+
|
|
510
|
+
The canonical first step is now the \`healthchecks\` namespace, which runs
|
|
511
|
+
every implemented check via the public API and returns a uniform shape per
|
|
512
|
+
check (\`id\`, \`severity\`, \`observed\`, \`threshold\`, \`findings\`,
|
|
513
|
+
\`suggestedAction\`):
|
|
514
|
+
|
|
515
|
+
\`\`\`bash
|
|
516
|
+
# Discover every check the platform exposes, implemented or planned.
|
|
517
|
+
chainpatrol --json healthchecks list
|
|
518
|
+
|
|
519
|
+
# Run every implemented healthcheck in parallel and aggregate the results.
|
|
520
|
+
chainpatrol --json healthchecks run --all --org <slug>
|
|
521
|
+
|
|
522
|
+
# Run a single named check.
|
|
523
|
+
chainpatrol --json healthchecks run reviewing.backlog --org <slug>
|
|
524
|
+
\`\`\`
|
|
525
|
+
|
|
526
|
+
After \`healthchecks run --all\`, use these complementary commands to cover
|
|
527
|
+
the signals that are not yet exposed as a uniform healthcheck endpoint:
|
|
528
|
+
|
|
529
|
+
\`\`\`bash
|
|
530
|
+
# Spikes / drops in detection volume over time (compare windows)
|
|
531
|
+
chainpatrol --json metrics breakdown --org <slug> --by day --this-week
|
|
532
|
+
|
|
533
|
+
# Customer-reported threats \u2014 gaps in our own detection
|
|
534
|
+
chainpatrol --json reports list --org <slug> --reported-by-customer
|
|
535
|
+
|
|
536
|
+
# What's enabled vs disabled vs not configured for the org
|
|
537
|
+
chainpatrol --json configs list --org <slug>
|
|
538
|
+
|
|
539
|
+
# Snapshot of review/takedown queues \u2014 raw counts behind several healthchecks
|
|
540
|
+
chainpatrol --json queues snapshot --org <slug>
|
|
541
|
+
|
|
542
|
+
# Packaged weekly customer-success sweep (preferred when it covers the ask)
|
|
543
|
+
chainpatrol presets run cs-weekly-health --org <slug>
|
|
544
|
+
\`\`\`
|
|
545
|
+
|
|
546
|
+
Treat each command's output as one input to the healthcheck. The manual
|
|
547
|
+
checks below still apply \u2014 especially for signals the CLI cannot infer on
|
|
548
|
+
its own (e.g. "lots of Twitter assets are blocked but Twitter Post Search
|
|
549
|
+
is disabled", or "this drop is fine because the config isn't relevant
|
|
550
|
+
to this org"). Each subsection of the guide notes whether a healthcheck
|
|
551
|
+
endpoint exists today and what to fall back on when it doesn't.
|
|
552
|
+
|
|
553
|
+
### Reporting progress while running a healthcheck
|
|
554
|
+
|
|
555
|
+
When the user asks for a healthcheck, narrate each step in real time so they
|
|
556
|
+
can watch the run unfold. Do NOT batch results and dump everything at the
|
|
557
|
+
end. For every check you run (Quick Path CLI command, or a manual signal
|
|
558
|
+
where you're fetching data with \`configs list\` / \`reports list\` to
|
|
559
|
+
reason about):
|
|
560
|
+
|
|
561
|
+
1. **Before** you call the command, emit ONE short sentence stating what
|
|
562
|
+
you're about to check, including the org slug. Examples:
|
|
563
|
+
- "Running detection config healthcheck for morpho\u2026"
|
|
564
|
+
- "Snapshotting review and takedown queues for morpho\u2026"
|
|
565
|
+
- "Fetching customer-reported reports for morpho to look for detection gaps\u2026"
|
|
566
|
+
|
|
567
|
+
2. **As soon as** the command returns, emit ONE short result line that
|
|
568
|
+
starts with a status word and ends with a key number or finding:
|
|
569
|
+
- \`DONE\` \u2014 ran cleanly, nothing to flag
|
|
570
|
+
- \`WARN\` \u2014 soft signal worth surfacing (e.g. a borderline backlog,
|
|
571
|
+
mild spike or drop, a config you'd want a human to confirm)
|
|
572
|
+
- \`FAIL\` \u2014 concrete failure that the user should act on
|
|
573
|
+
- Examples:
|
|
574
|
+
- "DONE \u2014 14/14 detection configs passing."
|
|
575
|
+
- "WARN \u2014 23 proposals in the review queue; 4 are older than 7 days."
|
|
576
|
+
- "FAIL \u2014 twitter_post_search returned 0 results in the last 24h with --run."
|
|
577
|
+
|
|
578
|
+
3. Run **independent** checks in **parallel** (single message, multiple
|
|
579
|
+
Bash tool calls) so progress lines arrive quickly. \`detections
|
|
580
|
+
healthcheck\`, \`queues snapshot\`, \`metrics breakdown\`,
|
|
581
|
+
\`reports list --reported-by-customer\`, and \`configs list\` are all
|
|
582
|
+
independent of each other and safe to fire concurrently. Dependent
|
|
583
|
+
follow-ups (e.g. paginating \`reports list\` with a returned cursor,
|
|
584
|
+
or fetching extra detail on a single failing config) run after the
|
|
585
|
+
first round.
|
|
586
|
+
|
|
587
|
+
4. After every check is reported, emit a short final **Summary** section:
|
|
588
|
+
- A one-line status per check (\u2713 / \u26A0 / \u2717 + name + key number).
|
|
589
|
+
- A short "Top issues" list of the highest-priority FAIL / WARN
|
|
590
|
+
findings, in priority order, with the concrete next action for each.
|
|
591
|
+
|
|
592
|
+
Keep each progress line to one sentence. The goal is for the user to see
|
|
593
|
+
the healthcheck happening, not to read a wall of text mid-run \u2014 full
|
|
594
|
+
detail belongs in the final Summary or in a follow-up when the user asks
|
|
595
|
+
about a specific finding.
|
|
340
596
|
|
|
341
597
|
### Detection
|
|
342
598
|
|
|
@@ -347,18 +603,45 @@ turned off. For example: lots of Twitter assets are on the blocklist, but
|
|
|
347
603
|
detection sources like "Twitter / X User Search" or "Twitter Post Search" are
|
|
348
604
|
disabled. Those should be turned on.
|
|
349
605
|
|
|
606
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed in
|
|
607
|
+
\`healthchecks list\` as \`detections.coverage-gaps\` with
|
|
608
|
+
\`implemented: false\` \u2014 when reporting this signal in a healthcheck, note
|
|
609
|
+
"manual check, no API yet". Until the endpoint lands, do the correlation
|
|
610
|
+
manually: \`chainpatrol --json configs list --org <slug>\` for enabled vs
|
|
611
|
+
disabled configs, then \`chainpatrol --json reports list --org <slug>\` for
|
|
612
|
+
recent blocked-item asset types. Flag any asset type where blocked items
|
|
613
|
+
exist but the matching detection source has \`status: "disabled"\` (or
|
|
614
|
+
appears in the "Not configured" group).
|
|
615
|
+
|
|
350
616
|
#### Spike in Detections
|
|
351
617
|
|
|
352
618
|
A spike in recent detections is worth investigating. It could be a bad config
|
|
353
619
|
change, or it could be a legitimate new attack push in this area \u2014 useful
|
|
354
620
|
intel to surface to the security team as a targeted spike.
|
|
355
621
|
|
|
622
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
623
|
+
in \`healthchecks list\` as \`detections.spike\` with \`implemented: false\`.
|
|
624
|
+
Until the endpoint lands, use \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
|
|
625
|
+
(and a comparison window via \`--from\`/\`--to\`) to see daily detection
|
|
626
|
+
volume. Anything notably above the recent baseline is a spike \u2014 cross-reference
|
|
627
|
+
against recent config changes for that source.
|
|
628
|
+
|
|
356
629
|
#### Drop in Detections
|
|
357
630
|
|
|
358
631
|
A drop is also worth looking into. It may be a bad config change, or it may
|
|
359
632
|
mean the config is not really relevant and can be safely turned off (not all
|
|
360
633
|
default-on configs are relevant to every org).
|
|
361
634
|
|
|
635
|
+
**Run via CLI:** the extreme case ("this source went silent") is covered
|
|
636
|
+
today by \`chainpatrol --json healthchecks run detections.silent-configs --org <slug>\`,
|
|
637
|
+
which is the canonical replacement for the older \`detections healthcheck\`.
|
|
638
|
+
It fails any config whose \`recentResultCount\` is below \`--min-results\`
|
|
639
|
+
in the \`--lookback-hours\` window; pass \`--run\` (via the lower-level
|
|
640
|
+
\`detections healthcheck --run\`) to also catch configs that error when
|
|
641
|
+
executed. For soft drops (still producing results but below baseline),
|
|
642
|
+
\`detections.drop\` is marked \`implemented: false\` in \`healthchecks list\`
|
|
643
|
+
\u2014 use \`metrics breakdown --by day\` and compare windows manually.
|
|
644
|
+
|
|
362
645
|
### Reviewing
|
|
363
646
|
|
|
364
647
|
#### Pile Up / Backlog of Unreviewed Proposals
|
|
@@ -368,6 +651,17 @@ reports, but really the threshold is relative to the average number of
|
|
|
368
651
|
confirmed threats per week. Example: if an org only adds 5 blocked threats per
|
|
369
652
|
week, then a 7-day backlog of even 10 proposals is a really big deal.
|
|
370
653
|
|
|
654
|
+
**Run via CLI:** **Implemented as \`healthchecks run reviewing.backlog\`.**
|
|
655
|
+
The endpoint counts proposals in PENDING review state and grades severity
|
|
656
|
+
against per-org thresholds (default warn=50, fail=100; override with
|
|
657
|
+
\`--warn-threshold\` / \`--fail-threshold\` via the run payload). For raw
|
|
658
|
+
counts plus SLA / age breakdowns, \`chainpatrol --json queues snapshot --org <slug>\`
|
|
659
|
+
remains useful and exposes \`reviewQueue.totalPendingProposals\` and
|
|
660
|
+
\`reviewQueue.distinctReports\`. Compare against the org's typical weekly
|
|
661
|
+
throughput (use \`metrics summary --this-week\` for that baseline) \u2014 a
|
|
662
|
+
backlog that exceeds a week of typical confirmed-threat volume is a finding
|
|
663
|
+
regardless of the absolute number.
|
|
664
|
+
|
|
371
665
|
#### Really Old Proposals
|
|
372
666
|
|
|
373
667
|
Any proposal waiting for review longer than 14 days is a sign something has
|
|
@@ -375,6 +669,13 @@ gone wrong. Even complex investigations rarely take longer than this. Except
|
|
|
375
669
|
for rare cases, these should be rejected or approved to prevent a backlog
|
|
376
670
|
from building.
|
|
377
671
|
|
|
672
|
+
**Run via CLI:** **Implemented as \`healthchecks run reviewing.old-proposals\`.**
|
|
673
|
+
The endpoint counts pending proposals older than the warn / fail age
|
|
674
|
+
thresholds (default 7 / 14 days) and lists the oldest offenders in
|
|
675
|
+
\`findings\`. \`queues snapshot\` (\`reviewQueue.ageBuckets.gte168h\`) still
|
|
676
|
+
works as a raw view, and \`reviewQueue.slaBuckets.breached\` captures the
|
|
677
|
+
strictest SLA breaches separately \u2014 any non-zero value is worth raising.
|
|
678
|
+
|
|
378
679
|
#### Spike in Auto Approved Reports
|
|
379
680
|
|
|
380
681
|
If suddenly a lot of proposals in an org are being approved by automation,
|
|
@@ -385,6 +686,14 @@ cases, notify an engineer at ChainPatrol and check any detection configs you
|
|
|
385
686
|
adjusted recently, since those may be the cause of spam combined with a weak
|
|
386
687
|
rule.
|
|
387
688
|
|
|
689
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
690
|
+
in \`healthchecks list\` as \`reviewing.auto-approval-spike\` with
|
|
691
|
+
\`implemented: false\`. As a proxy until the endpoint lands, use
|
|
692
|
+
\`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
|
|
693
|
+
and look for a sudden surge in \`newThreats\` / \`threatsWatchlisted\` that
|
|
694
|
+
isn't matched by a parallel rise in reviewer activity \u2014 that gap usually
|
|
695
|
+
points at automation doing the approving.
|
|
696
|
+
|
|
388
697
|
### Blocklisting
|
|
389
698
|
|
|
390
699
|
#### Google Safe Browsing (Coming Soon)
|
|
@@ -398,6 +707,11 @@ also take a look at the org's custom detection sources \u2014 it's possible ther
|
|
|
398
707
|
are too many false positives landing on the blocklist, indicating issues with
|
|
399
708
|
detection and reviewing rules.
|
|
400
709
|
|
|
710
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
711
|
+
in \`healthchecks list\` as \`blocklisting.gsb-cancelled-rate\` with
|
|
712
|
+
\`implemented: false\`. Until Google Safe Browsing submission state is
|
|
713
|
+
exposed in the public API, this remains a manual / engineering-team check.
|
|
714
|
+
|
|
401
715
|
### Takedowns
|
|
402
716
|
|
|
403
717
|
#### Too Many Takedowns in ToDo
|
|
@@ -406,12 +720,27 @@ Can mean a gap in automated takedowns not being implemented for some new area
|
|
|
406
720
|
of threats. It can also mean the areas that require manual takedowns are
|
|
407
721
|
being missed by the takedown team.
|
|
408
722
|
|
|
723
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
724
|
+
in \`healthchecks list\` as \`takedowns.todo-volume\` with
|
|
725
|
+
\`implemented: false\` (needs a per-org throughput baseline). As an interim
|
|
726
|
+
signal, \`chainpatrol --json queues snapshot --org <slug>\` returns
|
|
727
|
+
\`takedownQueue.totalOpen\` and breakdowns by status; a persistently large
|
|
728
|
+
ToDo bucket relative to the org's typical takedown rate (use
|
|
729
|
+
\`metrics summary\` for that baseline) is the finding.
|
|
730
|
+
|
|
409
731
|
#### Too Many Takedowns In Progress
|
|
410
732
|
|
|
411
733
|
Typically means something is wrong with the submission itself. The takedown
|
|
412
734
|
may need to be resubmitted, the vendor asked for more evidence, or we may
|
|
413
735
|
have submitted it in the wrong place.
|
|
414
736
|
|
|
737
|
+
**Run via CLI:** **Implemented as \`healthchecks run takedowns.stale-in-progress\`.**
|
|
738
|
+
The endpoint counts takedowns sitting in IN_PROGRESS past the staleness
|
|
739
|
+
threshold (default 7 days) and lists the oldest offenders in \`findings\`.
|
|
740
|
+
\`queues snapshot\` (\`takedownQueue.staleInProgress\`) still works as a raw
|
|
741
|
+
view. Any non-zero value is worth investigating; a growing number across
|
|
742
|
+
snapshots strongly suggests a vendor-side or submission-format problem.
|
|
743
|
+
|
|
415
744
|
#### Too Many Cancelled Takedowns
|
|
416
745
|
|
|
417
746
|
Takedowns should rarely be cancelled. A cancelled takedown means "we will not
|
|
@@ -419,11 +748,27 @@ do this takedown" for some reason. Cases like adding an item to the blocklist
|
|
|
419
748
|
when it's already taken down are treated as completed, not cancelled. So even
|
|
420
749
|
3 cancelled takedowns in a 7-day period is too many.
|
|
421
750
|
|
|
751
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
752
|
+
in \`healthchecks list\` as \`takedowns.cancelled-rate\` with
|
|
753
|
+
\`implemented: false\` (public API does not yet expose CANCELLED takedown
|
|
754
|
+
counts over a rolling window). As an interim signal, use
|
|
755
|
+
\`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
|
|
756
|
+
and compare \`takedownsFiled\` vs \`takedownsCompleted\` for a sudden
|
|
757
|
+
divergence \u2014 a widening gap with no In-Progress growth often shows up as
|
|
758
|
+
cancellations.
|
|
759
|
+
|
|
422
760
|
#### Automated Takedowns Turned Off for Over 30 Days
|
|
423
761
|
|
|
424
762
|
Automated takedowns should be on by default for nearly every organization.
|
|
425
763
|
Any issue that would make you want to turn off automated takedowns should be
|
|
426
764
|
resolved within 30 days.
|
|
765
|
+
|
|
766
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
767
|
+
in \`healthchecks list\` as \`takedowns.automation-off\` with
|
|
768
|
+
\`implemented: false\` (public API does not yet expose automated-takedown
|
|
769
|
+
enablement state). Check this manually with the org's takedown automation
|
|
770
|
+
settings; the CLI's role here is mostly to highlight stale state in
|
|
771
|
+
\`queues snapshot\` so you know to ask.
|
|
427
772
|
`;
|
|
428
773
|
}
|
|
429
774
|
function getBundledSkillVersion() {
|
package/dist/cli.js
CHANGED
|
@@ -13,7 +13,7 @@ import {
|
|
|
13
13
|
getCliVersion,
|
|
14
14
|
isSkillInstalled,
|
|
15
15
|
readInstalledSkillVersion
|
|
16
|
-
} from "./chunk-
|
|
16
|
+
} from "./chunk-S65NM7FF.js";
|
|
17
17
|
import "./chunk-IUZB3DQW.js";
|
|
18
18
|
import {
|
|
19
19
|
DateTime
|
|
@@ -281,6 +281,35 @@ var HELP = {
|
|
|
281
281
|
"--window-hours <n> Hours used for staleness windows"
|
|
282
282
|
]
|
|
283
283
|
},
|
|
284
|
+
healthchecks: {
|
|
285
|
+
description: "Run organization healthchecks via the public API. Use `list` to see every available check (including planned ones not yet implemented on the backend) and `run` to execute one or all implemented checks.",
|
|
286
|
+
usage: "chainpatrol healthchecks <list|run <id>|run --all>",
|
|
287
|
+
options: ["--org <slug> Organization slug (required for `run`)"],
|
|
288
|
+
examples: [
|
|
289
|
+
"chainpatrol healthchecks list",
|
|
290
|
+
"chainpatrol healthchecks run --all --org acme",
|
|
291
|
+
"chainpatrol healthchecks run reviewing.backlog --org acme",
|
|
292
|
+
"chainpatrol healthchecks run takedowns.stale-in-progress --org acme"
|
|
293
|
+
]
|
|
294
|
+
},
|
|
295
|
+
"healthchecks list": {
|
|
296
|
+
description: "List every healthcheck the platform exposes. Each entry reports whether it is implemented today; not-yet-implemented entries include a `notImplementedReason` you can surface to users.",
|
|
297
|
+
usage: "chainpatrol healthchecks list"
|
|
298
|
+
},
|
|
299
|
+
"healthchecks run": {
|
|
300
|
+
description: "Run a single healthcheck by id, or pass `--all` to run every implemented check in parallel and aggregate the results. Returns the uniform healthcheck shape (id, severity, observed, threshold, findings, suggestedAction).",
|
|
301
|
+
usage: "chainpatrol healthchecks run <id|--all> --org <slug>",
|
|
302
|
+
options: [
|
|
303
|
+
"--org <slug> Organization slug (required)",
|
|
304
|
+
"--all Run every implemented healthcheck in parallel",
|
|
305
|
+
"--min-results <n> Pass through to detections.silent-configs",
|
|
306
|
+
"--lookback-hours <n> Pass through to detections.silent-configs"
|
|
307
|
+
],
|
|
308
|
+
examples: [
|
|
309
|
+
"chainpatrol healthchecks run reviewing.backlog --org acme",
|
|
310
|
+
"chainpatrol healthchecks run --all --org acme"
|
|
311
|
+
]
|
|
312
|
+
},
|
|
284
313
|
presets: {
|
|
285
314
|
description: "Run saved workflows for common jobs.",
|
|
286
315
|
usage: "chainpatrol presets <list|run <id>>",
|
|
@@ -336,6 +365,7 @@ function getTopLevelHelp() {
|
|
|
336
365
|
" logout Clear stored credentials",
|
|
337
366
|
" configs Manage detection configs",
|
|
338
367
|
" detections Validate, run, update detection configs",
|
|
368
|
+
" healthchecks List and run organization healthchecks",
|
|
339
369
|
" metrics Query organization metrics and breakdowns",
|
|
340
370
|
" reports Create and list reports from terminal",
|
|
341
371
|
" queues Snapshot operations review/takedown queues",
|
|
@@ -504,6 +534,7 @@ var COMMANDS = [
|
|
|
504
534
|
"logout",
|
|
505
535
|
"configs",
|
|
506
536
|
"detections",
|
|
537
|
+
"healthchecks",
|
|
507
538
|
"metrics",
|
|
508
539
|
"reports",
|
|
509
540
|
"queues",
|
|
@@ -686,12 +717,12 @@ function parseAttachmentUrls() {
|
|
|
686
717
|
}
|
|
687
718
|
async function handleConfigsList(org) {
|
|
688
719
|
if (jsonMode) {
|
|
689
|
-
const { listConfigsJson } = await import("./list-json-
|
|
720
|
+
const { listConfigsJson } = await import("./list-json-WTMYLZGY.js");
|
|
690
721
|
await listConfigsJson({ org });
|
|
691
722
|
return;
|
|
692
723
|
}
|
|
693
724
|
const { render } = await import("ink");
|
|
694
|
-
const { default: ConfigsList } = await import("./list-
|
|
725
|
+
const { default: ConfigsList } = await import("./list-GEMCFDD5.js");
|
|
695
726
|
const { default: React } = await import("react");
|
|
696
727
|
render(React.createElement(ConfigsList, { org }));
|
|
697
728
|
}
|
|
@@ -789,7 +820,7 @@ async function main() {
|
|
|
789
820
|
case "detections": {
|
|
790
821
|
const org = await resolveOrg();
|
|
791
822
|
if (subcommand === "healthcheck") {
|
|
792
|
-
const { runDetectionsHealthcheck } = await import("./healthcheck-
|
|
823
|
+
const { runDetectionsHealthcheck } = await import("./healthcheck-KAONRGSS.js");
|
|
793
824
|
await runDetectionsHealthcheck({
|
|
794
825
|
org,
|
|
795
826
|
source: cli.flags.source,
|
|
@@ -804,7 +835,7 @@ async function main() {
|
|
|
804
835
|
break;
|
|
805
836
|
}
|
|
806
837
|
if (subcommand === "validate") {
|
|
807
|
-
const { runDetectionsValidate } = await import("./validate-
|
|
838
|
+
const { runDetectionsValidate } = await import("./validate-BJFEKI2N.js");
|
|
808
839
|
await runDetectionsValidate({
|
|
809
840
|
org,
|
|
810
841
|
source: cli.flags.source,
|
|
@@ -819,7 +850,7 @@ async function main() {
|
|
|
819
850
|
break;
|
|
820
851
|
}
|
|
821
852
|
if (subcommand === "drift") {
|
|
822
|
-
const { runDetectionsDrift } = await import("./drift-
|
|
853
|
+
const { runDetectionsDrift } = await import("./drift-DZ6A7JL5.js");
|
|
823
854
|
await runDetectionsDrift({
|
|
824
855
|
org,
|
|
825
856
|
source: cli.flags.source,
|
|
@@ -833,7 +864,7 @@ async function main() {
|
|
|
833
864
|
break;
|
|
834
865
|
}
|
|
835
866
|
if (subcommand === "run") {
|
|
836
|
-
const { runDetectionsRun } = await import("./run-
|
|
867
|
+
const { runDetectionsRun } = await import("./run-43CC5AXR.js");
|
|
837
868
|
await runDetectionsRun({
|
|
838
869
|
org,
|
|
839
870
|
configId: cli.flags.configId,
|
|
@@ -852,7 +883,7 @@ async function main() {
|
|
|
852
883
|
break;
|
|
853
884
|
}
|
|
854
885
|
if (action === "run") {
|
|
855
|
-
const { runDetectionsRun } = await import("./run-
|
|
886
|
+
const { runDetectionsRun } = await import("./run-43CC5AXR.js");
|
|
856
887
|
await runDetectionsRun({
|
|
857
888
|
org,
|
|
858
889
|
configId: cli.flags.configId,
|
|
@@ -870,7 +901,7 @@ async function main() {
|
|
|
870
901
|
throw new Error("detections configs update requires --config-id");
|
|
871
902
|
}
|
|
872
903
|
const configPatch = getConfigPatchFromSetFlags();
|
|
873
|
-
const { runDetectionsConfigsUpdate } = await import("./configs-update-
|
|
904
|
+
const { runDetectionsConfigsUpdate } = await import("./configs-update-RPN32YTL.js");
|
|
874
905
|
await runDetectionsConfigsUpdate({
|
|
875
906
|
org,
|
|
876
907
|
configId: cli.flags.configId,
|
|
@@ -897,7 +928,7 @@ async function main() {
|
|
|
897
928
|
case "metrics": {
|
|
898
929
|
const org = await resolveOrg();
|
|
899
930
|
if (subcommand === "summary") {
|
|
900
|
-
const { runMetricsSummary } = await import("./summary-
|
|
931
|
+
const { runMetricsSummary } = await import("./summary-6NCA7PDP.js");
|
|
901
932
|
await runMetricsSummary({
|
|
902
933
|
org,
|
|
903
934
|
from: cli.flags.from,
|
|
@@ -909,7 +940,7 @@ async function main() {
|
|
|
909
940
|
break;
|
|
910
941
|
}
|
|
911
942
|
if (subcommand === "found") {
|
|
912
|
-
const { runMetricsFound } = await import("./found-
|
|
943
|
+
const { runMetricsFound } = await import("./found-AOPBSLRD.js");
|
|
913
944
|
await runMetricsFound({
|
|
914
945
|
org,
|
|
915
946
|
from: cli.flags.from,
|
|
@@ -926,7 +957,7 @@ async function main() {
|
|
|
926
957
|
if (!by || !["day", "type", "brand"].includes(by)) {
|
|
927
958
|
throw new Error("metrics breakdown requires --by <day|type|brand>");
|
|
928
959
|
}
|
|
929
|
-
const { runMetricsBreakdown } = await import("./breakdown-
|
|
960
|
+
const { runMetricsBreakdown } = await import("./breakdown-EBSACUST.js");
|
|
930
961
|
await runMetricsBreakdown({
|
|
931
962
|
org,
|
|
932
963
|
by,
|
|
@@ -946,7 +977,7 @@ async function main() {
|
|
|
946
977
|
case "reports": {
|
|
947
978
|
if (subcommand === "list") {
|
|
948
979
|
const org = await resolveOrg();
|
|
949
|
-
const { runReportsList } = await import("./list-
|
|
980
|
+
const { runReportsList } = await import("./list-MWDFCHMJ.js");
|
|
950
981
|
await runReportsList({
|
|
951
982
|
org,
|
|
952
983
|
limit: cli.flags.limit,
|
|
@@ -962,7 +993,7 @@ async function main() {
|
|
|
962
993
|
}
|
|
963
994
|
if (subcommand === "create") {
|
|
964
995
|
const org = await tryResolveOrg();
|
|
965
|
-
const { runReportsCreate } = await import("./create-
|
|
996
|
+
const { runReportsCreate } = await import("./create-QP3M7EZM.js");
|
|
966
997
|
await runReportsCreate({
|
|
967
998
|
org,
|
|
968
999
|
title: cli.flags.title,
|
|
@@ -986,7 +1017,7 @@ async function main() {
|
|
|
986
1017
|
}
|
|
987
1018
|
case "queues": {
|
|
988
1019
|
if (subcommand === "snapshot") {
|
|
989
|
-
const { runQueuesSnapshot } = await import("./snapshot-
|
|
1020
|
+
const { runQueuesSnapshot } = await import("./snapshot-E3TPZOKT.js");
|
|
990
1021
|
await runQueuesSnapshot({
|
|
991
1022
|
org: cli.flags.org,
|
|
992
1023
|
all: cli.flags.all,
|
|
@@ -1002,9 +1033,41 @@ async function main() {
|
|
|
1002
1033
|
subcommand ? `Unknown subcommand: queues ${subcommand}${hint ? `. Did you mean "queues ${hint}"?` : ""}` : "Usage: chainpatrol queues snapshot [--org <slug>|--all]"
|
|
1003
1034
|
);
|
|
1004
1035
|
}
|
|
1036
|
+
case "healthchecks": {
|
|
1037
|
+
if (subcommand === "list") {
|
|
1038
|
+
const { runHealthchecksList } = await import("./list-UW63DIKX.js");
|
|
1039
|
+
await runHealthchecksList({
|
|
1040
|
+
json: jsonMode,
|
|
1041
|
+
outputFormat: cliContext.outputFormat
|
|
1042
|
+
});
|
|
1043
|
+
break;
|
|
1044
|
+
}
|
|
1045
|
+
if (subcommand === "run") {
|
|
1046
|
+
const org = await resolveOrg();
|
|
1047
|
+
const thresholds = {};
|
|
1048
|
+
if (cli.flags.minResults !== void 0)
|
|
1049
|
+
thresholds.minResults = cli.flags.minResults;
|
|
1050
|
+
if (cli.flags.lookbackHours !== void 0)
|
|
1051
|
+
thresholds.lookbackHours = cli.flags.lookbackHours;
|
|
1052
|
+
const { runHealthchecksRun } = await import("./run-7THXM7GF.js");
|
|
1053
|
+
await runHealthchecksRun({
|
|
1054
|
+
org,
|
|
1055
|
+
id: action,
|
|
1056
|
+
all: cli.flags.all,
|
|
1057
|
+
thresholds,
|
|
1058
|
+
json: jsonMode,
|
|
1059
|
+
outputFormat: cliContext.outputFormat
|
|
1060
|
+
});
|
|
1061
|
+
break;
|
|
1062
|
+
}
|
|
1063
|
+
const hint = subcommand ? suggest(subcommand, ["list", "run"]) : null;
|
|
1064
|
+
throw new Error(
|
|
1065
|
+
subcommand ? `Unknown subcommand: healthchecks ${subcommand}${hint ? `. Did you mean "healthchecks ${hint}"?` : ""}` : "Usage: chainpatrol healthchecks <list|run <id>>"
|
|
1066
|
+
);
|
|
1067
|
+
}
|
|
1005
1068
|
case "presets": {
|
|
1006
1069
|
if (subcommand === "list") {
|
|
1007
|
-
const { runPresetsList } = await import("./list-
|
|
1070
|
+
const { runPresetsList } = await import("./list-LN6NOZIJ.js");
|
|
1008
1071
|
await runPresetsList({ outputFormat: cliContext.outputFormat });
|
|
1009
1072
|
break;
|
|
1010
1073
|
}
|
|
@@ -1015,7 +1078,7 @@ async function main() {
|
|
|
1015
1078
|
);
|
|
1016
1079
|
}
|
|
1017
1080
|
const org = await resolveOrg();
|
|
1018
|
-
const { runPresetsRun } = await import("./run-
|
|
1081
|
+
const { runPresetsRun } = await import("./run-MH5RYPWA.js");
|
|
1019
1082
|
await runPresetsRun({
|
|
1020
1083
|
presetId: action,
|
|
1021
1084
|
org,
|
|
@@ -1032,12 +1095,12 @@ async function main() {
|
|
|
1032
1095
|
case "setup":
|
|
1033
1096
|
case "install":
|
|
1034
1097
|
case "i": {
|
|
1035
|
-
const { setupSkill } = await import("./setup-skill-
|
|
1098
|
+
const { setupSkill } = await import("./setup-skill-Z5RVCWCU.js");
|
|
1036
1099
|
setupSkill({ json: jsonMode });
|
|
1037
1100
|
break;
|
|
1038
1101
|
}
|
|
1039
1102
|
case "uninstall": {
|
|
1040
|
-
const { uninstallSkill } = await import("./setup-skill-
|
|
1103
|
+
const { uninstallSkill } = await import("./setup-skill-Z5RVCWCU.js");
|
|
1041
1104
|
uninstallSkill({ json: jsonMode });
|
|
1042
1105
|
break;
|
|
1043
1106
|
}
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PRESETS
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-PIYOWGBZ.js";
|
|
4
4
|
import "./chunk-E2LAMILJ.js";
|
|
5
5
|
import {
|
|
6
6
|
printOutput,
|
|
7
7
|
toCsvRows
|
|
8
8
|
} from "./chunk-VFT3TD3E.js";
|
|
9
|
-
import "./chunk-
|
|
9
|
+
import "./chunk-MXUZR2BV.js";
|
|
10
10
|
import "./chunk-EEG7T6WT.js";
|
|
11
11
|
import "./chunk-TFCNKBRC.js";
|
|
12
12
|
import "./chunk-U73SABXK.js";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import {
|
|
2
|
+
printOutput,
|
|
3
|
+
toCsvRows
|
|
4
|
+
} from "./chunk-VFT3TD3E.js";
|
|
5
|
+
import {
|
|
6
|
+
createApiClient
|
|
7
|
+
} from "./chunk-MXUZR2BV.js";
|
|
8
|
+
import "./chunk-EEG7T6WT.js";
|
|
9
|
+
import "./chunk-TFCNKBRC.js";
|
|
10
|
+
import "./chunk-U73SABXK.js";
|
|
11
|
+
|
|
12
|
+
// src/commands/healthchecks/list.ts
|
|
13
|
+
async function runHealthchecksList(options) {
|
|
14
|
+
const client = options.apiClient ?? createApiClient();
|
|
15
|
+
const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
|
|
16
|
+
const result = await client.listHealthchecks();
|
|
17
|
+
printOutput({
|
|
18
|
+
outputFormat,
|
|
19
|
+
json: result,
|
|
20
|
+
markdown: [
|
|
21
|
+
"# Healthchecks",
|
|
22
|
+
"",
|
|
23
|
+
...result.checks.map((entry) => {
|
|
24
|
+
const status = entry.implemented ? "implemented" : "not-implemented";
|
|
25
|
+
const lines = [
|
|
26
|
+
`## ${entry.id} _(${status})_`,
|
|
27
|
+
`- Title: ${entry.title}`,
|
|
28
|
+
`- Category: ${entry.category}`,
|
|
29
|
+
`- Description: ${entry.description}`
|
|
30
|
+
];
|
|
31
|
+
if (entry.endpoint) {
|
|
32
|
+
lines.push(`- Endpoint: \`${entry.endpoint}\``);
|
|
33
|
+
}
|
|
34
|
+
if (entry.notImplementedReason) {
|
|
35
|
+
lines.push(`- Not yet implemented: ${entry.notImplementedReason}`);
|
|
36
|
+
}
|
|
37
|
+
return lines.join("\n");
|
|
38
|
+
})
|
|
39
|
+
].join("\n\n"),
|
|
40
|
+
csv: toCsvRows(
|
|
41
|
+
result.checks.map((entry) => ({
|
|
42
|
+
id: entry.id,
|
|
43
|
+
category: entry.category,
|
|
44
|
+
implemented: entry.implemented,
|
|
45
|
+
title: entry.title,
|
|
46
|
+
endpoint: entry.endpoint ?? ""
|
|
47
|
+
}))
|
|
48
|
+
),
|
|
49
|
+
human: () => {
|
|
50
|
+
const implemented = result.checks.filter((entry) => entry.implemented);
|
|
51
|
+
const notImplemented = result.checks.filter((entry) => !entry.implemented);
|
|
52
|
+
console.log(
|
|
53
|
+
`Healthchecks: ${implemented.length} implemented, ${notImplemented.length} not yet implemented (planned)`
|
|
54
|
+
);
|
|
55
|
+
console.log("");
|
|
56
|
+
console.log("Implemented:");
|
|
57
|
+
for (const entry of implemented) {
|
|
58
|
+
console.log(` \u2713 ${entry.id} [${entry.category}] ${entry.title}`);
|
|
59
|
+
}
|
|
60
|
+
console.log("");
|
|
61
|
+
console.log("Not yet implemented (data gaps; follow-up tracked):");
|
|
62
|
+
for (const entry of notImplemented) {
|
|
63
|
+
console.log(` \xB7 ${entry.id} [${entry.category}] ${entry.title}`);
|
|
64
|
+
if (entry.notImplementedReason) {
|
|
65
|
+
console.log(` ${entry.notImplementedReason}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export {
|
|
72
|
+
runHealthchecksList
|
|
73
|
+
};
|
|
@@ -0,0 +1,209 @@
|
|
|
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
|
+
createApiClient
|
|
11
|
+
} from "./chunk-MXUZR2BV.js";
|
|
12
|
+
import "./chunk-EEG7T6WT.js";
|
|
13
|
+
import "./chunk-TFCNKBRC.js";
|
|
14
|
+
import "./chunk-U73SABXK.js";
|
|
15
|
+
|
|
16
|
+
// src/commands/healthchecks/run.ts
|
|
17
|
+
function buildPayload(entry, org, thresholds) {
|
|
18
|
+
const payload = { slug: org };
|
|
19
|
+
const allowedKeys = /* @__PURE__ */ new Set([
|
|
20
|
+
...Object.keys(entry.defaultThreshold ?? {}),
|
|
21
|
+
"source",
|
|
22
|
+
"minResults",
|
|
23
|
+
"lookbackHours",
|
|
24
|
+
"runBeforeValidate",
|
|
25
|
+
"includeDisabled",
|
|
26
|
+
"warnThreshold",
|
|
27
|
+
"failThreshold",
|
|
28
|
+
"warnAgeHours",
|
|
29
|
+
"failAgeHours",
|
|
30
|
+
"staleThresholdHours"
|
|
31
|
+
]);
|
|
32
|
+
for (const [key, value] of Object.entries(thresholds)) {
|
|
33
|
+
if (allowedKeys.has(key)) {
|
|
34
|
+
payload[key] = value;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return payload;
|
|
38
|
+
}
|
|
39
|
+
function statusSymbol(severity) {
|
|
40
|
+
if (severity === "ok") return "\u2713";
|
|
41
|
+
if (severity === "warn") return "\u26A0";
|
|
42
|
+
return "\u2717";
|
|
43
|
+
}
|
|
44
|
+
function statusLabel(severity) {
|
|
45
|
+
if (severity === "ok") return "DONE";
|
|
46
|
+
if (severity === "warn") return "WARN";
|
|
47
|
+
return "FAIL";
|
|
48
|
+
}
|
|
49
|
+
function summariseObserved(result) {
|
|
50
|
+
const parts = Object.entries(result.observed).slice(0, 4).map(([key, value]) => `${key}=${value}`);
|
|
51
|
+
return parts.join(", ");
|
|
52
|
+
}
|
|
53
|
+
async function runHealthchecksRun(options) {
|
|
54
|
+
const client = options.apiClient ?? createApiClient();
|
|
55
|
+
const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
|
|
56
|
+
const thresholds = options.thresholds ?? {};
|
|
57
|
+
const isMachineFormat = outputFormat !== "human";
|
|
58
|
+
const registry = await client.listHealthchecks();
|
|
59
|
+
const targets = [];
|
|
60
|
+
if (options.all) {
|
|
61
|
+
for (const entry of registry.checks) {
|
|
62
|
+
if (entry.implemented && entry.endpoint) targets.push(entry);
|
|
63
|
+
}
|
|
64
|
+
} else if (options.id) {
|
|
65
|
+
const entry = registry.checks.find((item) => item.id === options.id);
|
|
66
|
+
if (!entry) {
|
|
67
|
+
throw new CliExitError(
|
|
68
|
+
`Unknown healthcheck id: '${options.id}'. Run 'chainpatrol healthchecks list' to see available checks.`,
|
|
69
|
+
ExitCode.USAGE
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
if (!entry.implemented || !entry.endpoint) {
|
|
73
|
+
throw new CliExitError(
|
|
74
|
+
`Healthcheck '${entry.id}' is not yet implemented on the backend.${entry.notImplementedReason ? ` Reason: ${entry.notImplementedReason}` : ""}`,
|
|
75
|
+
ExitCode.USAGE
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
targets.push(entry);
|
|
79
|
+
} else {
|
|
80
|
+
throw new CliExitError(
|
|
81
|
+
"Provide a healthcheck id or --all. Example: 'chainpatrol healthchecks run reviewing.backlog --org acme' or 'chainpatrol healthchecks run --all --org acme'.",
|
|
82
|
+
ExitCode.USAGE
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (targets.length === 0) {
|
|
86
|
+
throw new CliExitError(
|
|
87
|
+
"No implemented healthchecks available to run.",
|
|
88
|
+
ExitCode.USAGE
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
const tasks = targets.map(async (entry) => {
|
|
92
|
+
if (!isMachineFormat) {
|
|
93
|
+
console.log(`Running ${entry.id} for ${options.org}\u2026`);
|
|
94
|
+
}
|
|
95
|
+
try {
|
|
96
|
+
const result = await client.runHealthcheck(
|
|
97
|
+
entry.endpoint,
|
|
98
|
+
buildPayload(entry, options.org, thresholds)
|
|
99
|
+
);
|
|
100
|
+
if (!isMachineFormat) {
|
|
101
|
+
const detail = summariseObserved(result);
|
|
102
|
+
console.log(
|
|
103
|
+
`${statusLabel(result.severity)} \u2014 ${entry.id} ${statusSymbol(result.severity)} (${detail})`
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
return { entry, result, error: null };
|
|
107
|
+
} catch (err) {
|
|
108
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
109
|
+
if (!isMachineFormat) {
|
|
110
|
+
console.log(`FAIL \u2014 ${entry.id} \u2717 (error: ${message})`);
|
|
111
|
+
}
|
|
112
|
+
return { entry, result: null, error: message };
|
|
113
|
+
}
|
|
114
|
+
});
|
|
115
|
+
const settled = await Promise.all(tasks);
|
|
116
|
+
const results = settled.filter(
|
|
117
|
+
(entry) => entry.result !== null
|
|
118
|
+
);
|
|
119
|
+
const errors = settled.filter((entry) => entry.error !== null);
|
|
120
|
+
const overallOk = results.every((entry) => entry.result.ok) && errors.length === 0;
|
|
121
|
+
printOutput({
|
|
122
|
+
outputFormat,
|
|
123
|
+
json: {
|
|
124
|
+
org: options.org,
|
|
125
|
+
ok: overallOk,
|
|
126
|
+
results: results.map((entry) => entry.result),
|
|
127
|
+
errors: errors.map((entry) => ({ id: entry.entry.id, error: entry.error }))
|
|
128
|
+
},
|
|
129
|
+
markdown: [
|
|
130
|
+
`# Healthchecks (${options.org})`,
|
|
131
|
+
"",
|
|
132
|
+
`Overall: ${overallOk ? "OK" : "ISSUES FOUND"}`,
|
|
133
|
+
"",
|
|
134
|
+
...results.map((entry) => {
|
|
135
|
+
const lines = [
|
|
136
|
+
`## ${entry.result.id} \u2014 ${statusLabel(entry.result.severity)}`,
|
|
137
|
+
`- Title: ${entry.result.title}`,
|
|
138
|
+
`- Severity: ${entry.result.severity}`,
|
|
139
|
+
`- Observed: ${JSON.stringify(entry.result.observed)}`,
|
|
140
|
+
`- Threshold: ${JSON.stringify(entry.result.threshold)}`
|
|
141
|
+
];
|
|
142
|
+
if (entry.result.findings.length > 0) {
|
|
143
|
+
lines.push("- Findings:");
|
|
144
|
+
for (const finding of entry.result.findings) {
|
|
145
|
+
lines.push(
|
|
146
|
+
` - [${finding.severity}] ${finding.kind}${finding.ref ? ` (${finding.ref})` : ""}: ${finding.message}`
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (entry.result.suggestedAction) {
|
|
151
|
+
lines.push(`- Suggested action: ${entry.result.suggestedAction}`);
|
|
152
|
+
}
|
|
153
|
+
return lines.join("\n");
|
|
154
|
+
}),
|
|
155
|
+
...errors.map((entry) => `## ${entry.entry.id} \u2014 ERROR
|
|
156
|
+
- ${entry.error}`)
|
|
157
|
+
].join("\n\n"),
|
|
158
|
+
csv: toCsvRows(
|
|
159
|
+
results.map((entry) => ({
|
|
160
|
+
id: entry.result.id,
|
|
161
|
+
severity: entry.result.severity,
|
|
162
|
+
ok: entry.result.ok,
|
|
163
|
+
observed: JSON.stringify(entry.result.observed),
|
|
164
|
+
threshold: JSON.stringify(entry.result.threshold),
|
|
165
|
+
findings: entry.result.findings.length
|
|
166
|
+
}))
|
|
167
|
+
),
|
|
168
|
+
human: () => {
|
|
169
|
+
if (targets.length > 1) {
|
|
170
|
+
console.log("");
|
|
171
|
+
console.log("Summary:");
|
|
172
|
+
for (const entry of results) {
|
|
173
|
+
console.log(
|
|
174
|
+
` ${statusSymbol(entry.result.severity)} ${entry.result.id} ${statusLabel(entry.result.severity)} ${summariseObserved(entry.result)}`
|
|
175
|
+
);
|
|
176
|
+
}
|
|
177
|
+
for (const entry of errors) {
|
|
178
|
+
console.log(` \u2717 ${entry.entry.id} FAIL error=${entry.error}`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
for (const entry of results) {
|
|
182
|
+
if (entry.result.findings.length > 0) {
|
|
183
|
+
console.log("");
|
|
184
|
+
console.log(`Findings for ${entry.result.id}:`);
|
|
185
|
+
for (const finding of entry.result.findings) {
|
|
186
|
+
const ref = finding.ref ? ` (${finding.ref})` : "";
|
|
187
|
+
console.log(
|
|
188
|
+
` [${finding.severity}] ${finding.kind}${ref}: ${finding.message}`
|
|
189
|
+
);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
if (entry.result.suggestedAction) {
|
|
193
|
+
console.log("");
|
|
194
|
+
console.log(`Suggested action for ${entry.result.id}:`);
|
|
195
|
+
console.log(` ${entry.result.suggestedAction}`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
});
|
|
200
|
+
if (!overallOk) {
|
|
201
|
+
throw new CliExitError(
|
|
202
|
+
"One or more healthchecks failed or errored.",
|
|
203
|
+
ExitCode.CHECK_FAILED
|
|
204
|
+
);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
export {
|
|
208
|
+
runHealthchecksRun
|
|
209
|
+
};
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getPresetDefinition,
|
|
3
3
|
runPreset
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-PIYOWGBZ.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-MXUZR2BV.js";
|
|
11
11
|
import "./chunk-EEG7T6WT.js";
|
|
12
12
|
import "./chunk-TFCNKBRC.js";
|
|
13
13
|
import "./chunk-U73SABXK.js";
|
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": "0.
|
|
5
|
+
"version": "0.4.0",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"homepage": "https://chainpatrol.com/docs/cli",
|
|
8
8
|
"keywords": [
|