@chainpatrol/cli 0.3.4 → 0.4.1
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 +49 -0
- package/dist/{breakdown-FFNYSXRQ.js → breakdown-EBSACUST.js} +1 -1
- package/dist/{chunk-DOL35U2S.js → chunk-F4GU6AII.js} +222 -55
- package/dist/{chunk-44FSS3CZ.js → chunk-MXUZR2BV.js} +6 -0
- package/dist/{chunk-WBKCXGLV.js → chunk-PIYOWGBZ.js} +1 -1
- 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-PCUBJJYU.js → setup-skill-2SEVH3M6.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,54 @@
|
|
|
1
1
|
# @chainpatrol/cli
|
|
2
2
|
|
|
3
|
+
## 0.4.1
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- 9126b45: Split the `reviewing.backlog` healthcheck into reviewable vs. total pending so the count matches the reviewing page in the app. The endpoint now reports `pendingProposals` (total), `reviewableProposals` (the subset the UI shows by default — assets not watchlisted, or reports from a customer), and `watchlistedHiddenProposals` (delta). Severity grades on the reviewable count; a non-zero watchlisted bucket surfaces as a warn-level finding. The bundled CLI skill is updated to document the new shape.
|
|
8
|
+
- fb0b846: Split the reviewing healthchecks along the operational divide between "Needs Review" (high-priority queue reviewers act on) and "Watchlisted" (intentionally deferred bucket):
|
|
9
|
+
|
|
10
|
+
- `reviewing.backlog` and `reviewing.old-proposals` now grade the Needs-Review bucket only — assets not on a watchlist, or reports submitted by a customer. Their counts now match what the reviewing page in the app shows by default.
|
|
11
|
+
- Two new checks cover the Watchlisted bucket as separate first-class signals: `reviewing.watchlist-backlog` (pile-up of watchlisted pending proposals) and `reviewing.watchlist-old` (watchlisted proposals deferred too long). Severity for both is **capped at warn** — watchlist cleanup matters but should never block on the same SLA as Needs Review.
|
|
12
|
+
|
|
13
|
+
The CLI skill is updated to spell out the distinction so a healthcheck report groups findings under the right bucket.
|
|
14
|
+
|
|
15
|
+
## 0.4.0
|
|
16
|
+
|
|
17
|
+
### Minor Changes
|
|
18
|
+
|
|
19
|
+
- b5de201: Add a `healthchecks` namespace to the public API and CLI.
|
|
20
|
+
|
|
21
|
+
The new `healthchecks list` returns a registry of every check the platform
|
|
22
|
+
exposes today, including planned checks that are not yet implemented on the
|
|
23
|
+
backend (marked `implemented: false` with a `notImplementedReason`). The new
|
|
24
|
+
`healthchecks run <id|--all> --org <slug>` runs a single check or every
|
|
25
|
+
implemented check in parallel and returns each result in a uniform shape
|
|
26
|
+
(`id`, `severity`, `observed`, `threshold`, `findings`, `suggestedAction`).
|
|
27
|
+
|
|
28
|
+
Implemented checks in this release:
|
|
29
|
+
|
|
30
|
+
- `detections.silent-configs` — the existing detection-config healthcheck,
|
|
31
|
+
wrapped in the uniform shape. The original `detections healthcheck`
|
|
32
|
+
command continues to work.
|
|
33
|
+
- `reviewing.backlog` — counts pending review proposals and grades severity
|
|
34
|
+
against per-org thresholds (default warn=50, fail=100).
|
|
35
|
+
- `reviewing.old-proposals` — counts proposals older than the warn / fail
|
|
36
|
+
age thresholds (default 7 / 14 days) and lists the oldest offenders.
|
|
37
|
+
- `takedowns.stale-in-progress` — counts takedowns sitting in IN_PROGRESS
|
|
38
|
+
past the staleness threshold (default 7 days).
|
|
39
|
+
|
|
40
|
+
Planned but not yet implemented (still returned by `list` with
|
|
41
|
+
`implemented: false`): `detections.coverage-gaps`, `detections.spike`,
|
|
42
|
+
`detections.drop`, `reviewing.auto-approval-spike`,
|
|
43
|
+
`blocklisting.gsb-cancelled-rate`, `takedowns.todo-volume`,
|
|
44
|
+
`takedowns.cancelled-rate`, `takedowns.automation-off`.
|
|
45
|
+
|
|
46
|
+
The bundled Claude Code skill is updated to (a) document the new namespace,
|
|
47
|
+
(b) recommend `healthchecks run --all` as the canonical entrypoint when a
|
|
48
|
+
user asks to run a healthcheck on an org, and (c) mark every not-yet-
|
|
49
|
+
implemented check in the HealthCheck Guide with a "manual check, no API
|
|
50
|
+
yet" note so the agent surfaces that gap explicitly.
|
|
51
|
+
|
|
3
52
|
## 0.3.4
|
|
4
53
|
|
|
5
54
|
### Patch Changes
|
|
@@ -297,6 +297,96 @@ Use it as the first signal in the Detection part of an org healthcheck, then
|
|
|
297
297
|
fall back to the manual checks in the HealthCheck Guide below for everything
|
|
298
298
|
else.
|
|
299
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\`, \`reviewing.watchlist-backlog\`,
|
|
327
|
+
\`reviewing.watchlist-old\`, \`takedowns.stale-in-progress\`.
|
|
328
|
+
|
|
329
|
+
### Pending proposals: "Needs Review" vs "Watchlisted"
|
|
330
|
+
|
|
331
|
+
PENDING proposals split into two operationally distinct buckets, and we
|
|
332
|
+
grade them with separate checks:
|
|
333
|
+
|
|
334
|
+
- **Needs Review** \u2014 pending proposals the reviewing UI shows by default
|
|
335
|
+
(\`excludeWatchlisted=true\`): assets that are NOT on a watchlist, OR
|
|
336
|
+
reports submitted by a customer (those stay visible even when the asset
|
|
337
|
+
is watchlisted). This is the actionable queue reviewers work from, so
|
|
338
|
+
pile-ups and old items here are high-priority signals (\`fail\` severity
|
|
339
|
+
is reachable).
|
|
340
|
+
- **Watchlisted** \u2014 pending proposals on watchlisted assets (excluding
|
|
341
|
+
customer-reported reports). The reviewing UI hides these by default
|
|
342
|
+
because watchlisting is the act of intentionally deferring an asset.
|
|
343
|
+
Pile-ups and aged items here are worth surfacing as cleanup work, but
|
|
344
|
+
severity is **capped at warn** so they never block on the same SLA as
|
|
345
|
+
Needs Review. When reporting findings, treat these as lower-priority.
|
|
346
|
+
|
|
347
|
+
Implemented checks today:
|
|
348
|
+
|
|
349
|
+
- **detections.silent-configs** \u2014 equivalent to \`detections healthcheck\`,
|
|
350
|
+
exposed under the uniform shape.
|
|
351
|
+
- **reviewing.backlog** \u2014 counts Needs Review pending proposals and grades
|
|
352
|
+
severity against per-org thresholds (default warn=50, fail=100).
|
|
353
|
+
- **reviewing.old-proposals** \u2014 counts Needs Review proposals older than
|
|
354
|
+
the warn / fail age thresholds (default 7 / 14 days) and lists the
|
|
355
|
+
oldest offenders.
|
|
356
|
+
- **reviewing.watchlist-backlog** \u2014 counts watchlisted pending proposals
|
|
357
|
+
(default warn=200). Severity capped at warn.
|
|
358
|
+
- **reviewing.watchlist-old** \u2014 counts watchlisted pending proposals older
|
|
359
|
+
than the warn-age threshold (default 30 days) and lists the oldest.
|
|
360
|
+
Severity capped at warn.
|
|
361
|
+
- **takedowns.stale-in-progress** \u2014 counts takedowns sitting in IN_PROGRESS
|
|
362
|
+
past the staleness threshold (default 7 days) and lists the oldest.
|
|
363
|
+
|
|
364
|
+
The following checks are listed by \`healthchecks list\` (\`implemented: false\`)
|
|
365
|
+
but **not yet implemented on the backend** \u2014 when the agent surfaces them in
|
|
366
|
+
a healthcheck report, mark them explicitly as "manual check, no API yet":
|
|
367
|
+
|
|
368
|
+
- **detections.coverage-gaps** \u2014 blocked assets vs. enabled-source correlation.
|
|
369
|
+
Still requires manual reasoning with \`configs list\` + \`reports list\`.
|
|
370
|
+
- **detections.spike** / **detections.drop** \u2014 require server-side baseline
|
|
371
|
+
modeling. Use \`metrics breakdown --by day\` as an interim signal.
|
|
372
|
+
- **reviewing.auto-approval-spike** \u2014 needs distinguishing automation vs.
|
|
373
|
+
human approvers in the review history. Use \`metrics breakdown\` as a proxy.
|
|
374
|
+
- **blocklisting.gsb-cancelled-rate** \u2014 Google Safe Browsing submission state
|
|
375
|
+
is not yet exposed in the public API.
|
|
376
|
+
- **takedowns.todo-volume** / **takedowns.cancelled-rate** /
|
|
377
|
+
**takedowns.automation-off** \u2014 the underlying read paths are not yet exposed.
|
|
378
|
+
|
|
379
|
+
When the user asks to "run a healthcheck on org X", the canonical command is:
|
|
380
|
+
|
|
381
|
+
\`\`\`bash
|
|
382
|
+
chainpatrol --json healthchecks run --all --org X
|
|
383
|
+
\`\`\`
|
|
384
|
+
|
|
385
|
+
This iterates the implemented entries in \`healthchecks list\`, runs them in
|
|
386
|
+
parallel, and aggregates the uniform results. Combine with the manual checks
|
|
387
|
+
in the HealthCheck Guide below for everything still marked
|
|
388
|
+
\`implemented: false\`.
|
|
389
|
+
|
|
300
390
|
### \`queues snapshot\` \u2014 Operations review/takedown queue snapshot
|
|
301
391
|
|
|
302
392
|
Server-side aggregation of the operations review queue (pending proposals,
|
|
@@ -442,16 +532,26 @@ others are soft / qualitative signals that still need you to fetch data with
|
|
|
442
532
|
|
|
443
533
|
### Quick Path: CLI commands that automate parts of this guide
|
|
444
534
|
|
|
445
|
-
|
|
446
|
-
|
|
535
|
+
The canonical first step is now the \`healthchecks\` namespace, which runs
|
|
536
|
+
every implemented check via the public API and returns a uniform shape per
|
|
537
|
+
check (\`id\`, \`severity\`, \`observed\`, \`threshold\`, \`findings\`,
|
|
538
|
+
\`suggestedAction\`):
|
|
447
539
|
|
|
448
540
|
\`\`\`bash
|
|
449
|
-
#
|
|
450
|
-
chainpatrol --json
|
|
541
|
+
# Discover every check the platform exposes, implemented or planned.
|
|
542
|
+
chainpatrol --json healthchecks list
|
|
451
543
|
|
|
452
|
-
#
|
|
453
|
-
chainpatrol --json
|
|
544
|
+
# Run every implemented healthcheck in parallel and aggregate the results.
|
|
545
|
+
chainpatrol --json healthchecks run --all --org <slug>
|
|
546
|
+
|
|
547
|
+
# Run a single named check.
|
|
548
|
+
chainpatrol --json healthchecks run reviewing.backlog --org <slug>
|
|
549
|
+
\`\`\`
|
|
550
|
+
|
|
551
|
+
After \`healthchecks run --all\`, use these complementary commands to cover
|
|
552
|
+
the signals that are not yet exposed as a uniform healthcheck endpoint:
|
|
454
553
|
|
|
554
|
+
\`\`\`bash
|
|
455
555
|
# Spikes / drops in detection volume over time (compare windows)
|
|
456
556
|
chainpatrol --json metrics breakdown --org <slug> --by day --this-week
|
|
457
557
|
|
|
@@ -461,6 +561,9 @@ chainpatrol --json reports list --org <slug> --reported-by-customer
|
|
|
461
561
|
# What's enabled vs disabled vs not configured for the org
|
|
462
562
|
chainpatrol --json configs list --org <slug>
|
|
463
563
|
|
|
564
|
+
# Snapshot of review/takedown queues \u2014 raw counts behind several healthchecks
|
|
565
|
+
chainpatrol --json queues snapshot --org <slug>
|
|
566
|
+
|
|
464
567
|
# Packaged weekly customer-success sweep (preferred when it covers the ask)
|
|
465
568
|
chainpatrol presets run cs-weekly-health --org <slug>
|
|
466
569
|
\`\`\`
|
|
@@ -469,7 +572,8 @@ Treat each command's output as one input to the healthcheck. The manual
|
|
|
469
572
|
checks below still apply \u2014 especially for signals the CLI cannot infer on
|
|
470
573
|
its own (e.g. "lots of Twitter assets are blocked but Twitter Post Search
|
|
471
574
|
is disabled", or "this drop is fine because the config isn't relevant
|
|
472
|
-
to this org").
|
|
575
|
+
to this org"). Each subsection of the guide notes whether a healthcheck
|
|
576
|
+
endpoint exists today and what to fall back on when it doesn't.
|
|
473
577
|
|
|
474
578
|
### Reporting progress while running a healthcheck
|
|
475
579
|
|
|
@@ -524,13 +628,15 @@ turned off. For example: lots of Twitter assets are on the blocklist, but
|
|
|
524
628
|
detection sources like "Twitter / X User Search" or "Twitter Post Search" are
|
|
525
629
|
disabled. Those should be turned on.
|
|
526
630
|
|
|
527
|
-
**Run via CLI:**
|
|
528
|
-
|
|
529
|
-
\`
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
631
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed in
|
|
632
|
+
\`healthchecks list\` as \`detections.coverage-gaps\` with
|
|
633
|
+
\`implemented: false\` \u2014 when reporting this signal in a healthcheck, note
|
|
634
|
+
"manual check, no API yet". Until the endpoint lands, do the correlation
|
|
635
|
+
manually: \`chainpatrol --json configs list --org <slug>\` for enabled vs
|
|
636
|
+
disabled configs, then \`chainpatrol --json reports list --org <slug>\` for
|
|
637
|
+
recent blocked-item asset types. Flag any asset type where blocked items
|
|
638
|
+
exist but the matching detection source has \`status: "disabled"\` (or
|
|
639
|
+
appears in the "Not configured" group).
|
|
534
640
|
|
|
535
641
|
#### Spike in Detections
|
|
536
642
|
|
|
@@ -538,7 +644,9 @@ A spike in recent detections is worth investigating. It could be a bad config
|
|
|
538
644
|
change, or it could be a legitimate new attack push in this area \u2014 useful
|
|
539
645
|
intel to surface to the security team as a targeted spike.
|
|
540
646
|
|
|
541
|
-
**Run via CLI:**
|
|
647
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
648
|
+
in \`healthchecks list\` as \`detections.spike\` with \`implemented: false\`.
|
|
649
|
+
Until the endpoint lands, use \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
|
|
542
650
|
(and a comparison window via \`--from\`/\`--to\`) to see daily detection
|
|
543
651
|
volume. Anything notably above the recent baseline is a spike \u2014 cross-reference
|
|
544
652
|
against recent config changes for that source.
|
|
@@ -549,40 +657,85 @@ A drop is also worth looking into. It may be a bad config change, or it may
|
|
|
549
657
|
mean the config is not really relevant and can be safely turned off (not all
|
|
550
658
|
default-on configs are relevant to every org).
|
|
551
659
|
|
|
552
|
-
**Run via CLI:** the
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
660
|
+
**Run via CLI:** the extreme case ("this source went silent") is covered
|
|
661
|
+
today by \`chainpatrol --json healthchecks run detections.silent-configs --org <slug>\`,
|
|
662
|
+
which is the canonical replacement for the older \`detections healthcheck\`.
|
|
663
|
+
It fails any config whose \`recentResultCount\` is below \`--min-results\`
|
|
664
|
+
in the \`--lookback-hours\` window; pass \`--run\` (via the lower-level
|
|
665
|
+
\`detections healthcheck --run\`) to also catch configs that error when
|
|
666
|
+
executed. For soft drops (still producing results but below baseline),
|
|
667
|
+
\`detections.drop\` is marked \`implemented: false\` in \`healthchecks list\`
|
|
668
|
+
\u2014 use \`metrics breakdown --by day\` and compare windows manually.
|
|
557
669
|
|
|
558
670
|
### Reviewing
|
|
559
671
|
|
|
560
|
-
|
|
672
|
+
PENDING proposals split into two operationally distinct buckets:
|
|
673
|
+
|
|
674
|
+
- **Needs Review** \u2014 assets not on a watchlist, or reports submitted by a
|
|
675
|
+
customer. This is the reviewing UI's default view (\`excludeWatchlisted=true\`)
|
|
676
|
+
and the actionable queue reviewers work from. Pile-ups and aged items
|
|
677
|
+
here are high priority \u2014 \`fail\` severity is reachable.
|
|
678
|
+
- **Watchlisted** \u2014 pending proposals on watchlisted assets (excluding
|
|
679
|
+
customer-reported reports). The UI hides these by default because
|
|
680
|
+
watchlisting is the act of intentionally deferring the asset. Pile-ups
|
|
681
|
+
and aged items here are worth surfacing as cleanup work but **severity
|
|
682
|
+
is capped at warn** \u2014 they should never block on the same SLA as Needs
|
|
683
|
+
Review.
|
|
684
|
+
|
|
685
|
+
Each bucket has its own pile-up and age check, so you can grade them
|
|
686
|
+
independently and tune thresholds without one drowning the other.
|
|
687
|
+
|
|
688
|
+
#### Pile Up / Backlog of Needs-Review Proposals
|
|
561
689
|
|
|
562
690
|
Too many proposals waiting in review. For most organizations this is over 100
|
|
563
691
|
reports, but really the threshold is relative to the average number of
|
|
564
692
|
confirmed threats per week. Example: if an org only adds 5 blocked threats per
|
|
565
693
|
week, then a 7-day backlog of even 10 proposals is a really big deal.
|
|
566
694
|
|
|
567
|
-
**Run via CLI:**
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
695
|
+
**Run via CLI:** **Implemented as \`healthchecks run reviewing.backlog\`.**
|
|
696
|
+
The endpoint counts the **Needs Review** subset only (assets not
|
|
697
|
+
watchlisted, or reports marked \`reportedByCustomer\`) and grades severity
|
|
698
|
+
against per-org thresholds (default warn=50, fail=100; override with
|
|
699
|
+
\`--warn-threshold\` / \`--fail-threshold\` via the run payload). This is
|
|
700
|
+
the number the reviewing page in the app shows by default, so the
|
|
701
|
+
healthcheck output matches what reviewers see.
|
|
702
|
+
|
|
703
|
+
For raw counts plus SLA / age breakdowns,
|
|
704
|
+
\`chainpatrol --json queues snapshot --org <slug>\` remains useful and
|
|
705
|
+
exposes \`reviewQueue.totalPendingProposals\` and
|
|
706
|
+
\`reviewQueue.distinctReports\` (note: \`queues snapshot\` does NOT apply
|
|
707
|
+
the watchlist filter, so its number is the sum of Needs Review +
|
|
708
|
+
Watchlisted). Compare against the org's typical weekly throughput
|
|
709
|
+
(use \`metrics summary --this-week\` for that baseline) \u2014 a backlog that
|
|
710
|
+
exceeds a week of typical confirmed-threat volume is a finding regardless
|
|
711
|
+
of the absolute number.
|
|
712
|
+
|
|
713
|
+
#### Really Old Needs-Review Proposals
|
|
714
|
+
|
|
715
|
+
Any Needs-Review proposal waiting longer than 14 days is a sign something
|
|
716
|
+
has gone wrong. Even complex investigations rarely take longer than this.
|
|
717
|
+
Except for rare cases, these should be rejected or approved to prevent a
|
|
718
|
+
backlog from building.
|
|
719
|
+
|
|
720
|
+
**Run via CLI:** **Implemented as \`healthchecks run reviewing.old-proposals\`.**
|
|
721
|
+
The endpoint counts Needs-Review proposals older than the warn / fail age
|
|
722
|
+
thresholds (default 7 / 14 days) and lists the oldest offenders in
|
|
723
|
+
\`findings\`. \`queues snapshot\` (\`reviewQueue.ageBuckets.gte168h\`) still
|
|
724
|
+
works as a raw view, and \`reviewQueue.slaBuckets.breached\` captures the
|
|
725
|
+
strictest SLA breaches separately \u2014 any non-zero value is worth raising.
|
|
726
|
+
|
|
727
|
+
#### Watchlist Pile-Up / Old Watchlisted Proposals
|
|
728
|
+
|
|
729
|
+
Watchlisted-pending proposals are deferred on purpose, but they shouldn't
|
|
730
|
+
grow unbounded \u2014 a huge pile or very-old items signal that the watchlist
|
|
731
|
+
needs a cleanup pass. These don't block on the same SLA as Needs Review.
|
|
732
|
+
|
|
733
|
+
**Run via CLI:** **Implemented as \`healthchecks run reviewing.watchlist-backlog\`**
|
|
734
|
+
(pile-up count, default warn=200) and
|
|
735
|
+
**\`healthchecks run reviewing.watchlist-old\`** (default warn-age 30
|
|
736
|
+
days). Both cap severity at warn. When reporting findings during an org
|
|
737
|
+
healthcheck, group them under "watchlist cleanup" rather than mixing with
|
|
738
|
+
Needs-Review findings \u2014 they're operationally different concerns.
|
|
586
739
|
|
|
587
740
|
#### Spike in Auto Approved Reports
|
|
588
741
|
|
|
@@ -594,8 +747,10 @@ cases, notify an engineer at ChainPatrol and check any detection configs you
|
|
|
594
747
|
adjusted recently, since those may be the cause of spam combined with a weak
|
|
595
748
|
rule.
|
|
596
749
|
|
|
597
|
-
**Run via CLI:**
|
|
598
|
-
|
|
750
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
751
|
+
in \`healthchecks list\` as \`reviewing.auto-approval-spike\` with
|
|
752
|
+
\`implemented: false\`. As a proxy until the endpoint lands, use
|
|
753
|
+
\`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
|
|
599
754
|
and look for a sudden surge in \`newThreats\` / \`threatsWatchlisted\` that
|
|
600
755
|
isn't matched by a parallel rise in reviewer activity \u2014 that gap usually
|
|
601
756
|
points at automation doing the approving.
|
|
@@ -613,9 +768,10 @@ also take a look at the org's custom detection sources \u2014 it's possible ther
|
|
|
613
768
|
are too many false positives landing on the blocklist, indicating issues with
|
|
614
769
|
detection and reviewing rules.
|
|
615
770
|
|
|
616
|
-
**Run via CLI:**
|
|
617
|
-
|
|
618
|
-
|
|
771
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
772
|
+
in \`healthchecks list\` as \`blocklisting.gsb-cancelled-rate\` with
|
|
773
|
+
\`implemented: false\`. Until Google Safe Browsing submission state is
|
|
774
|
+
exposed in the public API, this remains a manual / engineering-team check.
|
|
619
775
|
|
|
620
776
|
### Takedowns
|
|
621
777
|
|
|
@@ -625,8 +781,11 @@ Can mean a gap in automated takedowns not being implemented for some new area
|
|
|
625
781
|
of threats. It can also mean the areas that require manual takedowns are
|
|
626
782
|
being missed by the takedown team.
|
|
627
783
|
|
|
628
|
-
**Run via CLI:**
|
|
629
|
-
\`
|
|
784
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
785
|
+
in \`healthchecks list\` as \`takedowns.todo-volume\` with
|
|
786
|
+
\`implemented: false\` (needs a per-org throughput baseline). As an interim
|
|
787
|
+
signal, \`chainpatrol --json queues snapshot --org <slug>\` returns
|
|
788
|
+
\`takedownQueue.totalOpen\` and breakdowns by status; a persistently large
|
|
630
789
|
ToDo bucket relative to the org's typical takedown rate (use
|
|
631
790
|
\`metrics summary\` for that baseline) is the finding.
|
|
632
791
|
|
|
@@ -636,10 +795,12 @@ Typically means something is wrong with the submission itself. The takedown
|
|
|
636
795
|
may need to be resubmitted, the vendor asked for more evidence, or we may
|
|
637
796
|
have submitted it in the wrong place.
|
|
638
797
|
|
|
639
|
-
**Run via CLI:**
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
798
|
+
**Run via CLI:** **Implemented as \`healthchecks run takedowns.stale-in-progress\`.**
|
|
799
|
+
The endpoint counts takedowns sitting in IN_PROGRESS past the staleness
|
|
800
|
+
threshold (default 7 days) and lists the oldest offenders in \`findings\`.
|
|
801
|
+
\`queues snapshot\` (\`takedownQueue.staleInProgress\`) still works as a raw
|
|
802
|
+
view. Any non-zero value is worth investigating; a growing number across
|
|
803
|
+
snapshots strongly suggests a vendor-side or submission-format problem.
|
|
643
804
|
|
|
644
805
|
#### Too Many Cancelled Takedowns
|
|
645
806
|
|
|
@@ -648,7 +809,10 @@ do this takedown" for some reason. Cases like adding an item to the blocklist
|
|
|
648
809
|
when it's already taken down are treated as completed, not cancelled. So even
|
|
649
810
|
3 cancelled takedowns in a 7-day period is too many.
|
|
650
811
|
|
|
651
|
-
**Run via CLI:**
|
|
812
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
813
|
+
in \`healthchecks list\` as \`takedowns.cancelled-rate\` with
|
|
814
|
+
\`implemented: false\` (public API does not yet expose CANCELLED takedown
|
|
815
|
+
counts over a rolling window). As an interim signal, use
|
|
652
816
|
\`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
|
|
653
817
|
and compare \`takedownsFiled\` vs \`takedownsCompleted\` for a sudden
|
|
654
818
|
divergence \u2014 a widening gap with no In-Progress growth often shows up as
|
|
@@ -660,9 +824,12 @@ Automated takedowns should be on by default for nearly every organization.
|
|
|
660
824
|
Any issue that would make you want to turn off automated takedowns should be
|
|
661
825
|
resolved within 30 days.
|
|
662
826
|
|
|
663
|
-
**Run via CLI:**
|
|
664
|
-
|
|
665
|
-
|
|
827
|
+
**Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
|
|
828
|
+
in \`healthchecks list\` as \`takedowns.automation-off\` with
|
|
829
|
+
\`implemented: false\` (public API does not yet expose automated-takedown
|
|
830
|
+
enablement state). Check this manually with the org's takedown automation
|
|
831
|
+
settings; the CLI's role here is mostly to highlight stale state in
|
|
832
|
+
\`queues snapshot\` so you know to ask.
|
|
666
833
|
`;
|
|
667
834
|
}
|
|
668
835
|
function getBundledSkillVersion() {
|
|
@@ -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
|
}
|
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-F4GU6AII.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-2SEVH3M6.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-2SEVH3M6.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.1",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"homepage": "https://chainpatrol.com/docs/cli",
|
|
8
8
|
"keywords": [
|