@chainpatrol/cli 0.3.4 → 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 +37 -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-DOL35U2S.js → chunk-S65NM7FF.js} +152 -46
- 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-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,42 @@
|
|
|
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
|
+
|
|
3
40
|
## 0.3.4
|
|
4
41
|
|
|
5
42
|
### 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
|
}
|
|
@@ -297,6 +297,71 @@ 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\`, \`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
|
+
|
|
300
365
|
### \`queues snapshot\` \u2014 Operations review/takedown queue snapshot
|
|
301
366
|
|
|
302
367
|
Server-side aggregation of the operations review queue (pending proposals,
|
|
@@ -442,16 +507,26 @@ others are soft / qualitative signals that still need you to fetch data with
|
|
|
442
507
|
|
|
443
508
|
### Quick Path: CLI commands that automate parts of this guide
|
|
444
509
|
|
|
445
|
-
|
|
446
|
-
|
|
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\`):
|
|
447
514
|
|
|
448
515
|
\`\`\`bash
|
|
449
|
-
#
|
|
450
|
-
chainpatrol --json
|
|
516
|
+
# Discover every check the platform exposes, implemented or planned.
|
|
517
|
+
chainpatrol --json healthchecks list
|
|
451
518
|
|
|
452
|
-
#
|
|
453
|
-
chainpatrol --json
|
|
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
|
+
\`\`\`
|
|
454
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
|
|
455
530
|
# Spikes / drops in detection volume over time (compare windows)
|
|
456
531
|
chainpatrol --json metrics breakdown --org <slug> --by day --this-week
|
|
457
532
|
|
|
@@ -461,6 +536,9 @@ chainpatrol --json reports list --org <slug> --reported-by-customer
|
|
|
461
536
|
# What's enabled vs disabled vs not configured for the org
|
|
462
537
|
chainpatrol --json configs list --org <slug>
|
|
463
538
|
|
|
539
|
+
# Snapshot of review/takedown queues \u2014 raw counts behind several healthchecks
|
|
540
|
+
chainpatrol --json queues snapshot --org <slug>
|
|
541
|
+
|
|
464
542
|
# Packaged weekly customer-success sweep (preferred when it covers the ask)
|
|
465
543
|
chainpatrol presets run cs-weekly-health --org <slug>
|
|
466
544
|
\`\`\`
|
|
@@ -469,7 +547,8 @@ Treat each command's output as one input to the healthcheck. The manual
|
|
|
469
547
|
checks below still apply \u2014 especially for signals the CLI cannot infer on
|
|
470
548
|
its own (e.g. "lots of Twitter assets are blocked but Twitter Post Search
|
|
471
549
|
is disabled", or "this drop is fine because the config isn't relevant
|
|
472
|
-
to this org").
|
|
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.
|
|
473
552
|
|
|
474
553
|
### Reporting progress while running a healthcheck
|
|
475
554
|
|
|
@@ -524,13 +603,15 @@ turned off. For example: lots of Twitter assets are on the blocklist, but
|
|
|
524
603
|
detection sources like "Twitter / X User Search" or "Twitter Post Search" are
|
|
525
604
|
disabled. Those should be turned on.
|
|
526
605
|
|
|
527
|
-
**Run via CLI:**
|
|
528
|
-
|
|
529
|
-
\`
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
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).
|
|
534
615
|
|
|
535
616
|
#### Spike in Detections
|
|
536
617
|
|
|
@@ -538,7 +619,9 @@ A spike in recent detections is worth investigating. It could be a bad config
|
|
|
538
619
|
change, or it could be a legitimate new attack push in this area \u2014 useful
|
|
539
620
|
intel to surface to the security team as a targeted spike.
|
|
540
621
|
|
|
541
|
-
**Run via CLI:**
|
|
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\`
|
|
542
625
|
(and a comparison window via \`--from\`/\`--to\`) to see daily detection
|
|
543
626
|
volume. Anything notably above the recent baseline is a spike \u2014 cross-reference
|
|
544
627
|
against recent config changes for that source.
|
|
@@ -549,11 +632,15 @@ A drop is also worth looking into. It may be a bad config change, or it may
|
|
|
549
632
|
mean the config is not really relevant and can be safely turned off (not all
|
|
550
633
|
default-on configs are relevant to every org).
|
|
551
634
|
|
|
552
|
-
**Run via CLI:** the
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
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.
|
|
557
644
|
|
|
558
645
|
### Reviewing
|
|
559
646
|
|
|
@@ -564,12 +651,16 @@ reports, but really the threshold is relative to the average number of
|
|
|
564
651
|
confirmed threats per week. Example: if an org only adds 5 blocked threats per
|
|
565
652
|
week, then a 7-day backlog of even 10 proposals is a really big deal.
|
|
566
653
|
|
|
567
|
-
**Run via CLI:**
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
\`
|
|
571
|
-
|
|
572
|
-
|
|
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.
|
|
573
664
|
|
|
574
665
|
#### Really Old Proposals
|
|
575
666
|
|
|
@@ -578,11 +669,12 @@ gone wrong. Even complex investigations rarely take longer than this. Except
|
|
|
578
669
|
for rare cases, these should be rejected or approved to prevent a backlog
|
|
579
670
|
from building.
|
|
580
671
|
|
|
581
|
-
**Run via CLI:**
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
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.
|
|
586
678
|
|
|
587
679
|
#### Spike in Auto Approved Reports
|
|
588
680
|
|
|
@@ -594,8 +686,10 @@ cases, notify an engineer at ChainPatrol and check any detection configs you
|
|
|
594
686
|
adjusted recently, since those may be the cause of spam combined with a weak
|
|
595
687
|
rule.
|
|
596
688
|
|
|
597
|
-
**Run via CLI:**
|
|
598
|
-
|
|
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\`
|
|
599
693
|
and look for a sudden surge in \`newThreats\` / \`threatsWatchlisted\` that
|
|
600
694
|
isn't matched by a parallel rise in reviewer activity \u2014 that gap usually
|
|
601
695
|
points at automation doing the approving.
|
|
@@ -613,9 +707,10 @@ also take a look at the org's custom detection sources \u2014 it's possible ther
|
|
|
613
707
|
are too many false positives landing on the blocklist, indicating issues with
|
|
614
708
|
detection and reviewing rules.
|
|
615
709
|
|
|
616
|
-
**Run via CLI:**
|
|
617
|
-
|
|
618
|
-
|
|
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.
|
|
619
714
|
|
|
620
715
|
### Takedowns
|
|
621
716
|
|
|
@@ -625,8 +720,11 @@ Can mean a gap in automated takedowns not being implemented for some new area
|
|
|
625
720
|
of threats. It can also mean the areas that require manual takedowns are
|
|
626
721
|
being missed by the takedown team.
|
|
627
722
|
|
|
628
|
-
**Run via CLI:**
|
|
629
|
-
\`
|
|
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
|
|
630
728
|
ToDo bucket relative to the org's typical takedown rate (use
|
|
631
729
|
\`metrics summary\` for that baseline) is the finding.
|
|
632
730
|
|
|
@@ -636,10 +734,12 @@ Typically means something is wrong with the submission itself. The takedown
|
|
|
636
734
|
may need to be resubmitted, the vendor asked for more evidence, or we may
|
|
637
735
|
have submitted it in the wrong place.
|
|
638
736
|
|
|
639
|
-
**Run via CLI:**
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
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.
|
|
643
743
|
|
|
644
744
|
#### Too Many Cancelled Takedowns
|
|
645
745
|
|
|
@@ -648,7 +748,10 @@ do this takedown" for some reason. Cases like adding an item to the blocklist
|
|
|
648
748
|
when it's already taken down are treated as completed, not cancelled. So even
|
|
649
749
|
3 cancelled takedowns in a 7-day period is too many.
|
|
650
750
|
|
|
651
|
-
**Run via CLI:**
|
|
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
|
|
652
755
|
\`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
|
|
653
756
|
and compare \`takedownsFiled\` vs \`takedownsCompleted\` for a sudden
|
|
654
757
|
divergence \u2014 a widening gap with no In-Progress growth often shows up as
|
|
@@ -660,9 +763,12 @@ Automated takedowns should be on by default for nearly every organization.
|
|
|
660
763
|
Any issue that would make you want to turn off automated takedowns should be
|
|
661
764
|
resolved within 30 days.
|
|
662
765
|
|
|
663
|
-
**Run via CLI:**
|
|
664
|
-
|
|
665
|
-
|
|
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.
|
|
666
772
|
`;
|
|
667
773
|
}
|
|
668
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": [
|