@chainpatrol/cli 0.3.3 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,55 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - b5de201: Add a `healthchecks` namespace to the public API and CLI.
8
+
9
+ The new `healthchecks list` returns a registry of every check the platform
10
+ exposes today, including planned checks that are not yet implemented on the
11
+ backend (marked `implemented: false` with a `notImplementedReason`). The new
12
+ `healthchecks run <id|--all> --org <slug>` runs a single check or every
13
+ implemented check in parallel and returns each result in a uniform shape
14
+ (`id`, `severity`, `observed`, `threshold`, `findings`, `suggestedAction`).
15
+
16
+ Implemented checks in this release:
17
+
18
+ - `detections.silent-configs` — the existing detection-config healthcheck,
19
+ wrapped in the uniform shape. The original `detections healthcheck`
20
+ command continues to work.
21
+ - `reviewing.backlog` — counts pending review proposals and grades severity
22
+ against per-org thresholds (default warn=50, fail=100).
23
+ - `reviewing.old-proposals` — counts proposals older than the warn / fail
24
+ age thresholds (default 7 / 14 days) and lists the oldest offenders.
25
+ - `takedowns.stale-in-progress` — counts takedowns sitting in IN_PROGRESS
26
+ past the staleness threshold (default 7 days).
27
+
28
+ Planned but not yet implemented (still returned by `list` with
29
+ `implemented: false`): `detections.coverage-gaps`, `detections.spike`,
30
+ `detections.drop`, `reviewing.auto-approval-spike`,
31
+ `blocklisting.gsb-cancelled-rate`, `takedowns.todo-volume`,
32
+ `takedowns.cancelled-rate`, `takedowns.automation-off`.
33
+
34
+ The bundled Claude Code skill is updated to (a) document the new namespace,
35
+ (b) recommend `healthchecks run --all` as the canonical entrypoint when a
36
+ user asks to run a healthcheck on an org, and (c) mark every not-yet-
37
+ implemented check in the HealthCheck Guide with a "manual check, no API
38
+ yet" note so the agent surfaces that gap explicitly.
39
+
40
+ ## 0.3.4
41
+
42
+ ### Patch Changes
43
+
44
+ - e30617f: Update the bundled Claude Code skill to document the `detections healthcheck`,
45
+ `queues snapshot`, `metrics summary | found | breakdown`, and `presets`
46
+ subcommands, plus the `--no-input` / `--no-color` / `--output` / `--quiet`
47
+ global flags. The existing soft / manual signals in the Organization
48
+ HealthCheck Guide are preserved verbatim; each subsection now also points at
49
+ the matching CLI command via a "Run via CLI" note, and a new "Quick Path"
50
+ block lists the commands an agent should reach for first when asked to run
51
+ a health check.
52
+
3
53
  ## 0.3.3
4
54
 
5
55
  ### Patch Changes
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-44FSS3CZ.js";
7
+ } from "./chunk-MXUZR2BV.js";
8
8
  import "./chunk-EEG7T6WT.js";
9
9
  import "./chunk-TFCNKBRC.js";
10
10
  import "./chunk-U73SABXK.js";
@@ -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
  }
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import {
13
13
  DateTime
14
14
  } from "./chunk-TFCNKBRC.js";
@@ -264,6 +264,166 @@ You can also compare with the non-customer set using
264
264
  \`--no-reported-by-customer\` to gauge detection coverage on the same time
265
265
  window.
266
266
 
267
+ ### \`detections healthcheck\` \u2014 Validate enabled detection configs produce recent results
268
+
269
+ Server-side check. The CLI calls the ChainPatrol API; the server fetches each
270
+ enabled detection config for the org, counts results produced in the lookback
271
+ window, and FAILs configs that fall under \`--min-results\` (or whose run
272
+ errored when \`--run\` is set).
273
+
274
+ \`\`\`bash
275
+ chainpatrol --json detections healthcheck --org <slug>
276
+ \`\`\`
277
+
278
+ Flags:
279
+ - \`--source <key>\` only validate one source (e.g. \`twitter_search\`)
280
+ - \`--min-results <n>\` minimum results required in the window to pass
281
+ - \`--lookback-hours <n>\` size of the lookback window
282
+ - \`--run\` ask the server to run each config first, then validate the fresh output
283
+ - \`--include-disabled\` also validate disabled configs
284
+
285
+ What this command covers:
286
+ - Configs that have gone silent (recentResultCount below threshold)
287
+ - Configs that error when run (runOk=false) when \`--run\` is set
288
+
289
+ What it does NOT cover:
290
+ - Reviewing backlog / SLA breaches \u2192 use \`queues snapshot\`
291
+ - Takedown ToDo / In Progress / Cancelled volumes \u2192 use \`queues snapshot\`
292
+ - Spikes or drops in detection volume over time \u2192 use \`metrics breakdown\`
293
+ - Customer-reported gaps \u2192 use \`reports list --reported-by-customer\`
294
+ - Google Safe Browsing submission errors (not yet exposed in CLI)
295
+
296
+ Use it as the first signal in the Detection part of an org healthcheck, then
297
+ fall back to the manual checks in the HealthCheck Guide below for everything
298
+ else.
299
+
300
+ > Prefer the newer \`healthchecks\` namespace below. \`detections healthcheck\`
301
+ > is the original single-purpose command; the \`healthchecks\` namespace is
302
+ > the canonical place to discover and run every check we expose.
303
+
304
+ ### \`healthchecks list | run\` \u2014 Run uniform org healthchecks via the public API
305
+
306
+ The \`healthchecks\` namespace is the canonical way to run the named checks
307
+ from the Organization HealthCheck Guide below. Each implemented endpoint
308
+ returns the same uniform shape \u2014 \`{ id, ok, severity, observed, threshold,
309
+ findings, suggestedAction }\` \u2014 so the CLI / agent can render every check the
310
+ same way regardless of category.
311
+
312
+ \`\`\`bash
313
+ # Discover every check the platform exposes today, including planned checks
314
+ # that are not yet implemented on the backend.
315
+ chainpatrol --json healthchecks list
316
+
317
+ # Run a single named check.
318
+ chainpatrol --json healthchecks run reviewing.backlog --org <slug>
319
+
320
+ # Run every implemented check in parallel and aggregate the results.
321
+ chainpatrol --json healthchecks run --all --org <slug>
322
+ \`\`\`
323
+
324
+ Each implemented check has a stable id of the form \`category.name\`, e.g.
325
+ \`detections.silent-configs\`, \`reviewing.backlog\`,
326
+ \`reviewing.old-proposals\`, \`takedowns.stale-in-progress\`.
327
+
328
+ Implemented checks today:
329
+
330
+ - **detections.silent-configs** \u2014 equivalent to \`detections healthcheck\`,
331
+ exposed under the uniform shape.
332
+ - **reviewing.backlog** \u2014 counts proposals in PENDING review state and grades
333
+ severity against per-org thresholds (default warn=50, fail=100).
334
+ - **reviewing.old-proposals** \u2014 counts proposals older than the warn / fail
335
+ age thresholds (default 7 / 14 days) and lists the oldest offenders.
336
+ - **takedowns.stale-in-progress** \u2014 counts takedowns sitting in IN_PROGRESS
337
+ past the staleness threshold (default 7 days) and lists the oldest.
338
+
339
+ The following checks are listed by \`healthchecks list\` (\`implemented: false\`)
340
+ but **not yet implemented on the backend** \u2014 when the agent surfaces them in
341
+ a healthcheck report, mark them explicitly as "manual check, no API yet":
342
+
343
+ - **detections.coverage-gaps** \u2014 blocked assets vs. enabled-source correlation.
344
+ Still requires manual reasoning with \`configs list\` + \`reports list\`.
345
+ - **detections.spike** / **detections.drop** \u2014 require server-side baseline
346
+ modeling. Use \`metrics breakdown --by day\` as an interim signal.
347
+ - **reviewing.auto-approval-spike** \u2014 needs distinguishing automation vs.
348
+ human approvers in the review history. Use \`metrics breakdown\` as a proxy.
349
+ - **blocklisting.gsb-cancelled-rate** \u2014 Google Safe Browsing submission state
350
+ is not yet exposed in the public API.
351
+ - **takedowns.todo-volume** / **takedowns.cancelled-rate** /
352
+ **takedowns.automation-off** \u2014 the underlying read paths are not yet exposed.
353
+
354
+ When the user asks to "run a healthcheck on org X", the canonical command is:
355
+
356
+ \`\`\`bash
357
+ chainpatrol --json healthchecks run --all --org X
358
+ \`\`\`
359
+
360
+ This iterates the implemented entries in \`healthchecks list\`, runs them in
361
+ parallel, and aggregates the uniform results. Combine with the manual checks
362
+ in the HealthCheck Guide below for everything still marked
363
+ \`implemented: false\`.
364
+
365
+ ### \`queues snapshot\` \u2014 Operations review/takedown queue snapshot
366
+
367
+ Server-side aggregation of the operations review queue (pending proposals,
368
+ SLA breaches, age buckets) and the takedown queue (open, in-progress, stale).
369
+
370
+ \`\`\`bash
371
+ chainpatrol --json queues snapshot --org <slug>
372
+ \`\`\`
373
+
374
+ Useful for the **Reviewing** and **Takedowns** sections of the HealthCheck
375
+ Guide. Key signals in the response:
376
+ - \`reviewQueue.totalPendingProposals\` and \`reviewQueue.distinctReports\` \u2014
377
+ backlog size
378
+ - \`reviewQueue.slaBuckets.breached\` \u2014 SLA breaches (treat any breach as a
379
+ finding)
380
+ - \`reviewQueue.ageBuckets.gte168h\` \u2014 proposals older than 7 days (anything
381
+ >14 days from the manual guide should always be in here)
382
+ - \`takedownQueue.totalOpen\` and \`takedownQueue.staleInProgress\` \u2014 open
383
+ and stuck takedowns
384
+
385
+ Use \`--all\` to snapshot every org you have access to instead of a single slug.
386
+
387
+ ### \`metrics summary | found | breakdown\` \u2014 Org metrics for spike/drop analysis
388
+
389
+ \`\`\`bash
390
+ chainpatrol --json metrics summary --org <slug> --this-week
391
+ chainpatrol --json metrics breakdown --org <slug> --by day --this-week
392
+ chainpatrol --json metrics found --org <slug> --from <YYYY-MM-DD> --to <YYYY-MM-DD>
393
+ \`\`\`
394
+
395
+ \`breakdown\` is the one you usually want for healthchecks: it returns a
396
+ time series (by day or week) of reports, new threats, watchlisted threats,
397
+ and takedowns filed/completed. Compare the latest period against a prior
398
+ window to spot the **spike** or **drop** signals described in the manual
399
+ HealthCheck Guide. \`summary\` returns a single window total; \`found\` is
400
+ oriented around when threats were first discovered.
401
+
402
+ ### \`presets list | run\` \u2014 Packaged workflows for common jobs
403
+
404
+ \`\`\`bash
405
+ chainpatrol presets list
406
+ chainpatrol presets run cs-weekly-health --org <slug>
407
+ \`\`\`
408
+
409
+ Use \`presets list\` to discover packaged multi-step workflows. The bundled
410
+ \`cs-weekly-health\` preset runs the standard customer-success weekly health
411
+ sweep; prefer it over hand-rolling the same sequence of commands.
412
+
413
+ ## Headless / agent-mode tips
414
+
415
+ When running these commands from an agent or CI:
416
+
417
+ - Prefer \`--json\` (or \`--output json\`) so output is structured and the
418
+ update-check stderr nudge is suppressed automatically.
419
+ - Pass \`--no-input\` to forbid any interactive prompt (the CLI will error
420
+ out instead of waiting on a TTY).
421
+ - Pass \`--no-color\` or set \`NO_COLOR=1\` if your sink can't render ANSI.
422
+ - Set \`CHAINPATROL_NO_UPDATE_CHECK=1\` to silence the skill/npm freshness
423
+ nudge entirely.
424
+ - For \`login\` specifically, see the headless runbook in the login section
425
+ above \u2014 login is the one command that needs background + tail handling.
426
+
267
427
  ## Checking Auth Status
268
428
 
269
429
  To check if the user is logged in, read the credentials file:
@@ -303,12 +463,16 @@ machine-readable output is never polluted.
303
463
 
304
464
  ## Global Flags
305
465
 
306
- | Flag | Description |
307
- |-------------|--------------------------------------|
308
- | \`--json\` | Machine-readable JSON output |
309
- | \`--org\` | Organization slug (saved for later) |
310
- | \`--help\` | Show help |
311
- | \`--version\` | Show version |
466
+ | Flag | Description |
467
+ |------------------|--------------------------------------------------------|
468
+ | \`--json\` | Machine-readable JSON output (shortcut for \`--output json\`) |
469
+ | \`--output <fmt>\` | Output format: \`human\` (default), \`json\`, \`markdown\`, \`csv\` |
470
+ | \`--quiet\`, \`-q\` | Suppress non-essential output and the update-check nudge |
471
+ | \`--no-color\` | Disable ANSI colors (also respects the \`NO_COLOR\` env var) |
472
+ | \`--no-input\` | Disable interactive prompts (use in scripts and agents) |
473
+ | \`--org <slug>\` | Organization slug (saved as the default for later commands) |
474
+ | \`--help\`, \`-h\` | Show help |
475
+ | \`--version\` | Show version |
312
476
 
313
477
  ## Workflow
314
478
 
@@ -336,7 +500,99 @@ When the user asks for a "health check", "audit", "what's wrong with org X",
336
500
  "review org X's setup", or similar, walk through each section below and surface
337
501
  findings.
338
502
 
339
- In each section there are things you can look for that may be wrong.
503
+ In each section there are things you can look for that may be wrong. Some
504
+ checks have a dedicated CLI command that does most of the work server-side;
505
+ others are soft / qualitative signals that still need you to fetch data with
506
+ \`configs list\` or \`reports list\` and reason about it manually.
507
+
508
+ ### Quick Path: CLI commands that automate parts of this guide
509
+
510
+ The canonical first step is now the \`healthchecks\` namespace, which runs
511
+ every implemented check via the public API and returns a uniform shape per
512
+ check (\`id\`, \`severity\`, \`observed\`, \`threshold\`, \`findings\`,
513
+ \`suggestedAction\`):
514
+
515
+ \`\`\`bash
516
+ # Discover every check the platform exposes, implemented or planned.
517
+ chainpatrol --json healthchecks list
518
+
519
+ # Run every implemented healthcheck in parallel and aggregate the results.
520
+ chainpatrol --json healthchecks run --all --org <slug>
521
+
522
+ # Run a single named check.
523
+ chainpatrol --json healthchecks run reviewing.backlog --org <slug>
524
+ \`\`\`
525
+
526
+ After \`healthchecks run --all\`, use these complementary commands to cover
527
+ the signals that are not yet exposed as a uniform healthcheck endpoint:
528
+
529
+ \`\`\`bash
530
+ # Spikes / drops in detection volume over time (compare windows)
531
+ chainpatrol --json metrics breakdown --org <slug> --by day --this-week
532
+
533
+ # Customer-reported threats \u2014 gaps in our own detection
534
+ chainpatrol --json reports list --org <slug> --reported-by-customer
535
+
536
+ # What's enabled vs disabled vs not configured for the org
537
+ chainpatrol --json configs list --org <slug>
538
+
539
+ # Snapshot of review/takedown queues \u2014 raw counts behind several healthchecks
540
+ chainpatrol --json queues snapshot --org <slug>
541
+
542
+ # Packaged weekly customer-success sweep (preferred when it covers the ask)
543
+ chainpatrol presets run cs-weekly-health --org <slug>
544
+ \`\`\`
545
+
546
+ Treat each command's output as one input to the healthcheck. The manual
547
+ checks below still apply \u2014 especially for signals the CLI cannot infer on
548
+ its own (e.g. "lots of Twitter assets are blocked but Twitter Post Search
549
+ is disabled", or "this drop is fine because the config isn't relevant
550
+ to this org"). Each subsection of the guide notes whether a healthcheck
551
+ endpoint exists today and what to fall back on when it doesn't.
552
+
553
+ ### Reporting progress while running a healthcheck
554
+
555
+ When the user asks for a healthcheck, narrate each step in real time so they
556
+ can watch the run unfold. Do NOT batch results and dump everything at the
557
+ end. For every check you run (Quick Path CLI command, or a manual signal
558
+ where you're fetching data with \`configs list\` / \`reports list\` to
559
+ reason about):
560
+
561
+ 1. **Before** you call the command, emit ONE short sentence stating what
562
+ you're about to check, including the org slug. Examples:
563
+ - "Running detection config healthcheck for morpho\u2026"
564
+ - "Snapshotting review and takedown queues for morpho\u2026"
565
+ - "Fetching customer-reported reports for morpho to look for detection gaps\u2026"
566
+
567
+ 2. **As soon as** the command returns, emit ONE short result line that
568
+ starts with a status word and ends with a key number or finding:
569
+ - \`DONE\` \u2014 ran cleanly, nothing to flag
570
+ - \`WARN\` \u2014 soft signal worth surfacing (e.g. a borderline backlog,
571
+ mild spike or drop, a config you'd want a human to confirm)
572
+ - \`FAIL\` \u2014 concrete failure that the user should act on
573
+ - Examples:
574
+ - "DONE \u2014 14/14 detection configs passing."
575
+ - "WARN \u2014 23 proposals in the review queue; 4 are older than 7 days."
576
+ - "FAIL \u2014 twitter_post_search returned 0 results in the last 24h with --run."
577
+
578
+ 3. Run **independent** checks in **parallel** (single message, multiple
579
+ Bash tool calls) so progress lines arrive quickly. \`detections
580
+ healthcheck\`, \`queues snapshot\`, \`metrics breakdown\`,
581
+ \`reports list --reported-by-customer\`, and \`configs list\` are all
582
+ independent of each other and safe to fire concurrently. Dependent
583
+ follow-ups (e.g. paginating \`reports list\` with a returned cursor,
584
+ or fetching extra detail on a single failing config) run after the
585
+ first round.
586
+
587
+ 4. After every check is reported, emit a short final **Summary** section:
588
+ - A one-line status per check (\u2713 / \u26A0 / \u2717 + name + key number).
589
+ - A short "Top issues" list of the highest-priority FAIL / WARN
590
+ findings, in priority order, with the concrete next action for each.
591
+
592
+ Keep each progress line to one sentence. The goal is for the user to see
593
+ the healthcheck happening, not to read a wall of text mid-run \u2014 full
594
+ detail belongs in the final Summary or in a follow-up when the user asks
595
+ about a specific finding.
340
596
 
341
597
  ### Detection
342
598
 
@@ -347,18 +603,45 @@ turned off. For example: lots of Twitter assets are on the blocklist, but
347
603
  detection sources like "Twitter / X User Search" or "Twitter Post Search" are
348
604
  disabled. Those should be turned on.
349
605
 
606
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed in
607
+ \`healthchecks list\` as \`detections.coverage-gaps\` with
608
+ \`implemented: false\` \u2014 when reporting this signal in a healthcheck, note
609
+ "manual check, no API yet". Until the endpoint lands, do the correlation
610
+ manually: \`chainpatrol --json configs list --org <slug>\` for enabled vs
611
+ disabled configs, then \`chainpatrol --json reports list --org <slug>\` for
612
+ recent blocked-item asset types. Flag any asset type where blocked items
613
+ exist but the matching detection source has \`status: "disabled"\` (or
614
+ appears in the "Not configured" group).
615
+
350
616
  #### Spike in Detections
351
617
 
352
618
  A spike in recent detections is worth investigating. It could be a bad config
353
619
  change, or it could be a legitimate new attack push in this area \u2014 useful
354
620
  intel to surface to the security team as a targeted spike.
355
621
 
622
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
623
+ in \`healthchecks list\` as \`detections.spike\` with \`implemented: false\`.
624
+ Until the endpoint lands, use \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
625
+ (and a comparison window via \`--from\`/\`--to\`) to see daily detection
626
+ volume. Anything notably above the recent baseline is a spike \u2014 cross-reference
627
+ against recent config changes for that source.
628
+
356
629
  #### Drop in Detections
357
630
 
358
631
  A drop is also worth looking into. It may be a bad config change, or it may
359
632
  mean the config is not really relevant and can be safely turned off (not all
360
633
  default-on configs are relevant to every org).
361
634
 
635
+ **Run via CLI:** the extreme case ("this source went silent") is covered
636
+ today by \`chainpatrol --json healthchecks run detections.silent-configs --org <slug>\`,
637
+ which is the canonical replacement for the older \`detections healthcheck\`.
638
+ It fails any config whose \`recentResultCount\` is below \`--min-results\`
639
+ in the \`--lookback-hours\` window; pass \`--run\` (via the lower-level
640
+ \`detections healthcheck --run\`) to also catch configs that error when
641
+ executed. For soft drops (still producing results but below baseline),
642
+ \`detections.drop\` is marked \`implemented: false\` in \`healthchecks list\`
643
+ \u2014 use \`metrics breakdown --by day\` and compare windows manually.
644
+
362
645
  ### Reviewing
363
646
 
364
647
  #### Pile Up / Backlog of Unreviewed Proposals
@@ -368,6 +651,17 @@ reports, but really the threshold is relative to the average number of
368
651
  confirmed threats per week. Example: if an org only adds 5 blocked threats per
369
652
  week, then a 7-day backlog of even 10 proposals is a really big deal.
370
653
 
654
+ **Run via CLI:** **Implemented as \`healthchecks run reviewing.backlog\`.**
655
+ The endpoint counts proposals in PENDING review state and grades severity
656
+ against per-org thresholds (default warn=50, fail=100; override with
657
+ \`--warn-threshold\` / \`--fail-threshold\` via the run payload). For raw
658
+ counts plus SLA / age breakdowns, \`chainpatrol --json queues snapshot --org <slug>\`
659
+ remains useful and exposes \`reviewQueue.totalPendingProposals\` and
660
+ \`reviewQueue.distinctReports\`. Compare against the org's typical weekly
661
+ throughput (use \`metrics summary --this-week\` for that baseline) \u2014 a
662
+ backlog that exceeds a week of typical confirmed-threat volume is a finding
663
+ regardless of the absolute number.
664
+
371
665
  #### Really Old Proposals
372
666
 
373
667
  Any proposal waiting for review longer than 14 days is a sign something has
@@ -375,6 +669,13 @@ gone wrong. Even complex investigations rarely take longer than this. Except
375
669
  for rare cases, these should be rejected or approved to prevent a backlog
376
670
  from building.
377
671
 
672
+ **Run via CLI:** **Implemented as \`healthchecks run reviewing.old-proposals\`.**
673
+ The endpoint counts pending proposals older than the warn / fail age
674
+ thresholds (default 7 / 14 days) and lists the oldest offenders in
675
+ \`findings\`. \`queues snapshot\` (\`reviewQueue.ageBuckets.gte168h\`) still
676
+ works as a raw view, and \`reviewQueue.slaBuckets.breached\` captures the
677
+ strictest SLA breaches separately \u2014 any non-zero value is worth raising.
678
+
378
679
  #### Spike in Auto Approved Reports
379
680
 
380
681
  If suddenly a lot of proposals in an org are being approved by automation,
@@ -385,6 +686,14 @@ cases, notify an engineer at ChainPatrol and check any detection configs you
385
686
  adjusted recently, since those may be the cause of spam combined with a weak
386
687
  rule.
387
688
 
689
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
690
+ in \`healthchecks list\` as \`reviewing.auto-approval-spike\` with
691
+ \`implemented: false\`. As a proxy until the endpoint lands, use
692
+ \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
693
+ and look for a sudden surge in \`newThreats\` / \`threatsWatchlisted\` that
694
+ isn't matched by a parallel rise in reviewer activity \u2014 that gap usually
695
+ points at automation doing the approving.
696
+
388
697
  ### Blocklisting
389
698
 
390
699
  #### Google Safe Browsing (Coming Soon)
@@ -398,6 +707,11 @@ also take a look at the org's custom detection sources \u2014 it's possible ther
398
707
  are too many false positives landing on the blocklist, indicating issues with
399
708
  detection and reviewing rules.
400
709
 
710
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
711
+ in \`healthchecks list\` as \`blocklisting.gsb-cancelled-rate\` with
712
+ \`implemented: false\`. Until Google Safe Browsing submission state is
713
+ exposed in the public API, this remains a manual / engineering-team check.
714
+
401
715
  ### Takedowns
402
716
 
403
717
  #### Too Many Takedowns in ToDo
@@ -406,12 +720,27 @@ Can mean a gap in automated takedowns not being implemented for some new area
406
720
  of threats. It can also mean the areas that require manual takedowns are
407
721
  being missed by the takedown team.
408
722
 
723
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
724
+ in \`healthchecks list\` as \`takedowns.todo-volume\` with
725
+ \`implemented: false\` (needs a per-org throughput baseline). As an interim
726
+ signal, \`chainpatrol --json queues snapshot --org <slug>\` returns
727
+ \`takedownQueue.totalOpen\` and breakdowns by status; a persistently large
728
+ ToDo bucket relative to the org's typical takedown rate (use
729
+ \`metrics summary\` for that baseline) is the finding.
730
+
409
731
  #### Too Many Takedowns In Progress
410
732
 
411
733
  Typically means something is wrong with the submission itself. The takedown
412
734
  may need to be resubmitted, the vendor asked for more evidence, or we may
413
735
  have submitted it in the wrong place.
414
736
 
737
+ **Run via CLI:** **Implemented as \`healthchecks run takedowns.stale-in-progress\`.**
738
+ The endpoint counts takedowns sitting in IN_PROGRESS past the staleness
739
+ threshold (default 7 days) and lists the oldest offenders in \`findings\`.
740
+ \`queues snapshot\` (\`takedownQueue.staleInProgress\`) still works as a raw
741
+ view. Any non-zero value is worth investigating; a growing number across
742
+ snapshots strongly suggests a vendor-side or submission-format problem.
743
+
415
744
  #### Too Many Cancelled Takedowns
416
745
 
417
746
  Takedowns should rarely be cancelled. A cancelled takedown means "we will not
@@ -419,11 +748,27 @@ do this takedown" for some reason. Cases like adding an item to the blocklist
419
748
  when it's already taken down are treated as completed, not cancelled. So even
420
749
  3 cancelled takedowns in a 7-day period is too many.
421
750
 
751
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
752
+ in \`healthchecks list\` as \`takedowns.cancelled-rate\` with
753
+ \`implemented: false\` (public API does not yet expose CANCELLED takedown
754
+ counts over a rolling window). As an interim signal, use
755
+ \`chainpatrol --json metrics breakdown --org <slug> --by day --this-week\`
756
+ and compare \`takedownsFiled\` vs \`takedownsCompleted\` for a sudden
757
+ divergence \u2014 a widening gap with no In-Progress growth often shows up as
758
+ cancellations.
759
+
422
760
  #### Automated Takedowns Turned Off for Over 30 Days
423
761
 
424
762
  Automated takedowns should be on by default for nearly every organization.
425
763
  Any issue that would make you want to turn off automated takedowns should be
426
764
  resolved within 30 days.
765
+
766
+ **Run via CLI:** **Not yet implemented as a healthcheck endpoint.** Listed
767
+ in \`healthchecks list\` as \`takedowns.automation-off\` with
768
+ \`implemented: false\` (public API does not yet expose automated-takedown
769
+ enablement state). Check this manually with the org's takedown automation
770
+ settings; the CLI's role here is mostly to highlight stale state in
771
+ \`queues snapshot\` so you know to ask.
427
772
  `;
428
773
  }
429
774
  function getBundledSkillVersion() {
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-R7BPW32M.js";
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-SUZL2GBJ.js");
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-TMFLCS5U.js");
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-SIE5EXN3.js");
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-II6GH6H6.js");
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-PASGDEEX.js");
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-MGOASUON.js");
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-MGOASUON.js");
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-VOWH674W.js");
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-QORQFCMW.js");
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-TIW7T4VX.js");
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-FFNYSXRQ.js");
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-CEWBMKJ3.js");
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-ASGUBKZ7.js");
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-MQ6DWDFG.js");
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-KTIZ3UHA.js");
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-3TTLZ6HA.js");
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-KKWETU4A.js");
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-KKWETU4A.js");
1103
+ const { uninstallSkill } = await import("./setup-skill-Z5RVCWCU.js");
1041
1104
  uninstallSkill({ json: jsonMode });
1042
1105
  break;
1043
1106
  }
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-44FSS3CZ.js";
10
+ } from "./chunk-MXUZR2BV.js";
11
11
  import "./chunk-EEG7T6WT.js";
12
12
  import "./chunk-TFCNKBRC.js";
13
13
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-44FSS3CZ.js";
7
+ } from "./chunk-MXUZR2BV.js";
8
8
  import "./chunk-EEG7T6WT.js";
9
9
  import {
10
10
  DateTime
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-JCMWDZYY.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-44FSS3CZ.js";
7
+ } from "./chunk-MXUZR2BV.js";
8
8
  import {
9
9
  AuthCorruptedError,
10
10
  AuthExpiredError,
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  PRESETS
3
- } from "./chunk-WBKCXGLV.js";
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-44FSS3CZ.js";
9
+ import "./chunk-MXUZR2BV.js";
10
10
  import "./chunk-EEG7T6WT.js";
11
11
  import "./chunk-TFCNKBRC.js";
12
12
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  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
+ };
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-44FSS3CZ.js";
3
+ } from "./chunk-MXUZR2BV.js";
4
4
  import "./chunk-EEG7T6WT.js";
5
5
  import "./chunk-TFCNKBRC.js";
6
6
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -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-WBKCXGLV.js";
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-44FSS3CZ.js";
10
+ import "./chunk-MXUZR2BV.js";
11
11
  import "./chunk-EEG7T6WT.js";
12
12
  import "./chunk-TFCNKBRC.js";
13
13
  import "./chunk-U73SABXK.js";
@@ -6,7 +6,7 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-R7BPW32M.js";
9
+ } from "./chunk-S65NM7FF.js";
10
10
  import "./chunk-IUZB3DQW.js";
11
11
  export {
12
12
  getBundledSkillContent,
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-44FSS3CZ.js";
7
+ } from "./chunk-MXUZR2BV.js";
8
8
  import "./chunk-EEG7T6WT.js";
9
9
  import "./chunk-TFCNKBRC.js";
10
10
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-44FSS3CZ.js";
11
+ } from "./chunk-MXUZR2BV.js";
12
12
  import "./chunk-EEG7T6WT.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  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.3.3",
5
+ "version": "0.4.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [