@chainpatrol/cli 0.17.0 → 0.19.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.
Files changed (37) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/dist/{breakdown-ETSF2BZ3.js → breakdown-3LJBT3JQ.js} +1 -1
  3. package/dist/{check-EMGFDXUO.js → check-G64UQCEL.js} +1 -1
  4. package/dist/{chunk-HGAQBGGL.js → chunk-2JZAPTY6.js} +1 -1
  5. package/dist/{chunk-DPLUSURP.js → chunk-O7TMOF4W.js} +3 -0
  6. package/dist/{chunk-75H4KTIQ.js → chunk-UDGJEN4M.js} +273 -15
  7. package/dist/cli.js +59 -37
  8. package/dist/{configs-update-EKNLIRA6.js → configs-update-MVX7Q5NW.js} +1 -1
  9. package/dist/{create-M4PV5VVU.js → create-ANCO4FXB.js} +1 -1
  10. package/dist/{drift-RQKTGJWB.js → drift-SHPMEJDU.js} +1 -1
  11. package/dist/{found-LVYBEOS6.js → found-OPL4HWOW.js} +1 -1
  12. package/dist/{get-BPTWCJ6H.js → get-K7VHV5X2.js} +7 -8
  13. package/dist/{healthcheck-J6BIZEG7.js → healthcheck-QNLFKAXM.js} +1 -1
  14. package/dist/list-4VKMEKZS.js +64 -0
  15. package/dist/{list-CNF2FGLN.js → list-AQ7E23T3.js} +1 -1
  16. package/dist/{list-JCJHIJM4.js → list-FJBWSGTD.js} +2 -2
  17. package/dist/{list-O36VWTWR.js → list-FVFF4VW3.js} +1 -1
  18. package/dist/{list-S5Y6GW43.js → list-FWXQUOCP.js} +1 -1
  19. package/dist/{list-E2JVJNHV.js → list-Q5PRMAN5.js} +1 -1
  20. package/dist/{list-O62BZKQ5.js → list-QHEFB4KE.js} +1 -1
  21. package/dist/{list-OEJV4UNQ.js → list-QXA6GLCV.js} +13 -11
  22. package/dist/{list-LKNCICLT.js → list-SWHKM5R6.js} +1 -1
  23. package/dist/{list-44BQZHJD.js → list-WPPNX7G3.js} +1 -1
  24. package/dist/{list-AEM2KSO4.js → list-X4Z3CPO7.js} +1 -1
  25. package/dist/{list-2ENAEZVP.js → list-XSIXAHOG.js} +1 -1
  26. package/dist/{list-json-ZTS5GXNA.js → list-json-B5L5PXKH.js} +1 -1
  27. package/dist/{organization-PFPBZXJZ.js → organization-SOMHMGTM.js} +1 -1
  28. package/dist/{run-R3RR6XQF.js → run-J5TQ2YC2.js} +1 -1
  29. package/dist/{run-HS2EEOM6.js → run-RV6P2OCD.js} +1 -1
  30. package/dist/{run-LKPKTH3W.js → run-YAAMSWKK.js} +2 -2
  31. package/dist/{search-EBXEP7XC.js → search-FQUR75WR.js} +1 -1
  32. package/dist/{setup-skill-RPVW2CCQ.js → setup-skill-F7437RT3.js} +1 -1
  33. package/dist/{snapshot-GNZMYTXT.js → snapshot-2GDJMN3L.js} +1 -1
  34. package/dist/{summary-POVQC6B7.js → summary-LF53KXRR.js} +1 -1
  35. package/dist/{validate-QE2F4PYY.js → validate-UYKOMUEI.js} +1 -1
  36. package/dist/{whoami-24KUJUN4.js → whoami-OYOG63BX.js} +1 -1
  37. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,93 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 0.19.0
4
+
5
+ ### Minor Changes
6
+
7
+ - 477a431: Add proper brand support across metrics and listings so agents can run a
8
+ real "trend search" on an org.
9
+
10
+ ## New: list brands for an org
11
+
12
+ `GET /organization/brands` (CLI: `chainpatrol brands list`) returns every
13
+ brand belonging to the caller's org — both parent / product brands
14
+ (`type: ORGANIZATION`) and individual / employee brands (`type:
15
+ INDIVIDUAL`) — in one call. Soft-deleted brands are excluded. Use
16
+ `--type INDIVIDUAL` to filter to employee brands only. The endpoint is
17
+ org-scoped via auth, no extra args.
18
+
19
+ ## Fixed: brand filter actually applies on metrics
20
+
21
+ Two server-side bugs the trend-search docs ran into:
22
+
23
+ 1. `GET /organization/metrics` with `brandSlug` was filtering the scalar
24
+ metrics (`reports`, `newThreats`, `takedownsFiled`, ...) but **not**
25
+ `blockedByType` / `blockedByDay`, which silently returned org-wide
26
+ totals. A caller drilling into a single brand would see "this brand
27
+ is being attacked on YouTube" when YouTube was really just an
28
+ org-wide trend.
29
+ 2. `POST /metrics/breakdown` with `by: "brand"` was ignoring the
30
+ `brandIds` input filter — `getThreatsBlockedPerBrand()` always
31
+ returned every brand. The function now accepts `brandIds` and the
32
+ handler passes the input through.
33
+
34
+ Both are now wired through correctly. `metrics organization
35
+ --brand-slug <slug> --include blockedByType,blockedByDay` returns
36
+ real brand-scoped time series, and `metrics breakdown --by brand
37
+ --brand <id>` actually narrows to the requested brand(s).
38
+
39
+ ## Skill: full trend-search guide
40
+
41
+ Documents three trend signals the agent can answer from existing
42
+ commands, with concrete current-vs-baseline recipes:
43
+
44
+ 1. Spike in a specific asset type (`metrics breakdown --by type`).
45
+ 2. Spike in overall threat volume (`metrics breakdown --by day` +
46
+ `metrics organization`).
47
+ 3. Spike on a specific sub-brand (`brands list` → join → `metrics
48
+ breakdown --by brand` → `metrics organization --brand-slug`).
49
+ Employee vs. product distinction now comes straight from the brand
50
+ roster.
51
+
52
+ Server-side procedures + validation schemas live in the private
53
+ `@chainpatrol/external-trpc`, `@chainpatrol/validation`, and
54
+ `@chainpatrol/metrics` packages and ship together with this CLI
55
+ release, but are not listed in the frontmatter because they are
56
+ private.
57
+
58
+ ## 0.18.0
59
+
60
+ ### Minor Changes
61
+
62
+ - 0825606: Drop `automated` from the per-service shape of `orgs list` / `orgs get`
63
+ (and the matching `/user/orgs` and `/user/orgs/{slug}` public APIs) for
64
+ every service except `takedowns`. `automated` only ever had operational
65
+ meaning for takedowns — it controls whether ChainPatrol files the
66
+ takedown on its own. The `isAutomated*Active` columns on
67
+ reporting/reviewing/protection have no runtime effect, so exposing them
68
+ in the public API only invited misleading filters and CSV columns.
69
+
70
+ Concretely:
71
+
72
+ - API response: `services.reporting`, `services.reviewing`, and
73
+ `services.protection` now return `{ "active": boolean }` only.
74
+ `services.takedowns` still returns `{ "active": boolean, "automated":
75
+ boolean }`. `detection` and `darkWebMonitoring` are unchanged.
76
+ - API services filter: same — `reporting`, `reviewing`, and
77
+ `protection` no longer accept an `automated` filter. Only
78
+ `takedowns` does.
79
+ - CLI: `--service-automated` / `--service-manual` now only accept
80
+ `takedowns`. Passing any other service name errors out with a
81
+ clearer message.
82
+ - CLI CSV output: drops the `reportingAutomated` / `reviewingAutomated`
83
+ / `protectionAutomated` columns. Only `takedownsAutomated` remains.
84
+ - Skill description and OpenAPI endpoint descriptions explain the
85
+ new shape.
86
+
87
+ Server-side work in @chainpatrol/external-trpc and @chainpatrol/validation
88
+ supports the new shape; both are private packages and don't need
89
+ version bumps.
90
+
3
91
  ## 0.17.0
4
92
 
5
93
  ### Minor Changes
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-DPLUSURP.js";
10
+ } from "./chunk-O7TMOF4W.js";
11
11
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import {
13
13
  DateTime
14
14
  } from "./chunk-TFCNKBRC.js";
@@ -392,6 +392,9 @@ function createApiClient(options) {
392
392
  {}
393
393
  );
394
394
  },
395
+ listOrganizationBrands() {
396
+ return requestGet("/organization/brands", {});
397
+ },
395
398
  getCurrentUser() {
396
399
  return request("/user/me", {});
397
400
  },
@@ -206,7 +206,15 @@ description: |
206
206
  "across all brands", "company-wide", "total takedowns", "total threats",
207
207
  "total reports", "average takedowns", "average threats", "average per day",
208
208
  "average per customer", "average per org", "rollup across customers",
209
- "sum across orgs", "sum across customers".
209
+ "sum across orgs", "sum across customers",
210
+ "trends in org", "search for trends", "trend search", "look for trends",
211
+ "any trends", "trending threats", "spike in asset type",
212
+ "spike in threat volume", "coordinated attack", "spike check",
213
+ "anything unusual", "anything new for", "what's new for",
214
+ "employee being targeted", "spike on a sub-brand", "spike on a brand",
215
+ "sub-brand spike",
216
+ "list brands", "brands for org", "list sub-brands", "employee brands",
217
+ "individual brands", "brand list", "all brands in org".
210
218
  allowed-tools:
211
219
  - Bash
212
220
  - Read
@@ -626,10 +634,10 @@ Use \`--all\` to snapshot every org you have access to instead of a single slug.
626
634
 
627
635
  Returns every organization the caller can see, with each org's
628
636
  subscription status (\`PROSPECT\`, \`TRIAL\`, \`ACTIVE\`, \`INTEGRATION\`) and
629
- which services are active and automated. Use it to answer questions like
630
- "which customers have takedowns enabled but automation off?" or "which
631
- prospects don't have detection turned on yet?" \u2014 filters compose with AND
632
- and are applied server-side, so one call returns the final list.
637
+ which services are active. Use it to answer questions like "which
638
+ customers have takedowns enabled but automation off?" or "which prospects
639
+ don't have detection turned on yet?" \u2014 filters compose with AND and are
640
+ applied server-side, so one call returns the final list.
633
641
 
634
642
  \`\`\`bash
635
643
  chainpatrol --json orgs list \\
@@ -638,6 +646,40 @@ chainpatrol --json orgs list \\
638
646
  --service-manual takedowns
639
647
  \`\`\`
640
648
 
649
+ #### Per-service response shape \u2014 \`automated\` is takedowns-only
650
+
651
+ Every service exposes an \`active\` boolean. **Only \`takedowns\`
652
+ additionally exposes \`automated\`** \u2014 that is the one service where the
653
+ manual-vs-automated distinction changes real platform behavior (whether
654
+ ChainPatrol files the takedown on its own, or queues it for a human to
655
+ file). \`reporting\`, \`reviewing\`, and \`protection\` have backing
656
+ \`isAutomated*Active\` columns in the database, but those flags have no
657
+ operational effect today, so the API deliberately omits them from both
658
+ the response and the \`services\` filter \u2014 surfacing them would invite
659
+ misleading filters like "reporting.automated=true" that don't mean
660
+ anything. \`detection\` and \`darkWebMonitoring\` have always been
661
+ single-flag services.
662
+
663
+ So per-org JSON looks like:
664
+
665
+ \`\`\`json
666
+ {
667
+ "services": {
668
+ "reporting": { "active": true },
669
+ "reviewing": { "active": true },
670
+ "protection": { "active": false },
671
+ "takedowns": { "active": true, "automated": false },
672
+ "detection": { "active": true },
673
+ "darkWebMonitoring": { "active": false }
674
+ }
675
+ }
676
+ \`\`\`
677
+
678
+ When a user asks about "automated reporting / reviewing / protection",
679
+ explain that the flag exists in the DB but has no operational effect and
680
+ isn't exposed by the public API \u2014 only \`takedowns.automated\` carries a
681
+ real meaning.
682
+
641
683
  Filter flags (all optional, all comma-separated lists):
642
684
 
643
685
  - \`--query <text>\` partial name match (substring, case-insensitive)
@@ -646,11 +688,11 @@ Filter flags (all optional, all comma-separated lists):
646
688
  through this filter \u2014 \`orgs list\` only ever returns live customers.
647
689
  - \`--service-active <list>\` services that must be active
648
690
  - \`--service-inactive <list>\` services that must be inactive
649
- - \`--service-automated <list>\` services whose automation must be ON
650
- (\`reporting\`, \`reviewing\`, \`protection\`, \`takedowns\` \u2014 the four
651
- with an automation toggle)
652
- - \`--service-manual <list>\` services whose automation must be OFF
653
- (same four)
691
+ - \`--service-automated <list>\` services whose automation must be ON.
692
+ **Only \`takedowns\` is accepted** \u2014 the other services don't have a
693
+ meaningful automation toggle. Passing any other service name errors out.
694
+ - \`--service-manual <list>\` services whose automation must be OFF.
695
+ Same restriction \u2014 only \`takedowns\`.
654
696
 
655
697
  Service names: \`reporting\`, \`reviewing\`, \`protection\`, \`takedowns\`,
656
698
  \`detection\`, \`darkWebMonitoring\`.
@@ -662,11 +704,13 @@ enforced server-side.
662
704
  ### \`orgs get\` \u2014 Get a single organization by slug
663
705
 
664
706
  Look up one organization the caller has access to. Returns the same per-org
665
- shape as a single row from \`orgs list\` (subscription status + per-service
666
- active/automated flags), so anything you could read from \`orgs list\` you
667
- can also read here without paging or filtering. Use it when the user names
668
- a specific customer ("show me acme's setup", "is takedowns automation on
669
- for morpho?") and you don't need the rest of the catalogue.
707
+ shape as a single row from \`orgs list\` \u2014 \`active\` for every service,
708
+ plus \`automated\` on \`takedowns\` only (see the note above on why other
709
+ services don't expose an automation flag). Anything you could read from
710
+ \`orgs list\` you can also read here without paging or filtering. Use it
711
+ when the user names a specific customer ("show me acme's setup", "is
712
+ takedowns automation on for morpho?") and you don't need the rest of the
713
+ catalogue.
670
714
 
671
715
  \`\`\`bash
672
716
  chainpatrol orgs get <slug>
@@ -696,6 +740,36 @@ takedowns?", reach for \`orgs list\` \u2014 it's the only command that exposes
696
740
  service flags across multiple orgs in one call. Run it in \`--json\` mode
697
741
  and summarize patterns by service or by subscription tier.
698
742
 
743
+ ### \`brands list\` \u2014 List the brands (sub-brands) belonging to an org
744
+
745
+ Returns every brand for the org the caller is authenticated as, both
746
+ parent / product brands (\`type: "ORGANIZATION"\`) and individual /
747
+ employee brands (\`type: "INDIVIDUAL"\`). Soft-deleted brands are excluded.
748
+ The endpoint is org-scoped and inferred from auth \u2014 there's no \`--org\`
749
+ flag \u2014 so org-scoped API keys, user API keys, and user sessions all work
750
+ without extra arguments.
751
+
752
+ \`\`\`bash
753
+ chainpatrol brands list # all brands
754
+ chainpatrol brands list --type INDIVIDUAL # employee / person brands only
755
+ chainpatrol brands list --type ORGANIZATION # product / parent brands only
756
+ chainpatrol --json brands list # machine-readable
757
+ \`\`\`
758
+
759
+ Each entry has \`{ id, slug, name, type, description, brandGroupId, createdAt }\`.
760
+ Use this when you need to:
761
+
762
+ - Distinguish employee vs. product brands ahead of time \u2014 e.g. while
763
+ running a trend search and you want to highlight which spiking
764
+ sub-brands are employees so the user gets a direct heads-up to the
765
+ person involved.
766
+ - Resolve a brand name the user mentions to its \`slug\` for use with
767
+ \`metrics organization --brand-slug <slug>\` or as a \`brandId\` for
768
+ \`takedowns list --brand <id>\`.
769
+ - Audit a customer's setup \u2014 "how many employee brands does this org
770
+ protect?" or "are there brands the org set up but never wired into a
771
+ detection config?"
772
+
699
773
  ### \`metrics summary | found | breakdown | organization\` \u2014 Org metrics for spike/drop analysis
700
774
 
701
775
  #### Decision rule \u2014 read this before picking a metrics subcommand
@@ -1380,6 +1454,190 @@ Until a dedicated probe command exists:
1380
1454
  In both cases, if liveness looks miscalibrated for a class of assets,
1381
1455
  notify ChainPatrol engineering \u2014 the checker likely needs a tuning pass for
1382
1456
  that platform.
1457
+
1458
+ ## Organization Trend Search Guide
1459
+
1460
+ Trend search asks "what's changed recently for this org?" \u2014 the answer can
1461
+ surface coordinated attacks, new attack channels, or campaigns targeting
1462
+ specific people, **before** they show up as a healthcheck failure. Unlike
1463
+ the HealthCheck Guide above (which grades single signals against
1464
+ thresholds), trend search compares a recent window to a baseline window
1465
+ and flags ratios, not absolute counts. A trend is interesting even when
1466
+ the pipeline is keeping up with it \u2014 the user usually wants to know.
1467
+
1468
+ When the user asks something like:
1469
+
1470
+ - "are there any trends in org X?"
1471
+ - "search for trends in <org>"
1472
+ - "anything unusual happening with <org> lately?"
1473
+ - "any spikes for <org>?"
1474
+ - "what's new for <org> this week?"
1475
+ - "is anyone targeting <org>'s employees more than usual?"
1476
+ - "are we seeing more YouTube/Telegram/etc. threats for <org>?"
1477
+
1478
+ \u2026run the three trend checks below in parallel (single message, multiple
1479
+ Bash calls) and surface anything where the current rate is \u2265 2\xD7 the
1480
+ baseline AND clears a small absolute floor.
1481
+
1482
+ ### How to compute "current vs. baseline" rates
1483
+
1484
+ Pick two non-overlapping windows: a **current** window the user cares about
1485
+ (default last 7 days) and a **baseline** window ending where the current
1486
+ one starts (default the prior ~90 days, i.e. \`current_start - 90d\` \u2192
1487
+ \`current_start - 1d\`). For any breakdown bucket, compute:
1488
+
1489
+ \`\`\`
1490
+ current_per_day = current_count / current_window_days
1491
+ baseline_per_day = baseline_count / baseline_window_days
1492
+ ratio = current_per_day / max(baseline_per_day, epsilon)
1493
+ \`\`\`
1494
+
1495
+ Flag a bucket if \`ratio >= 2\` AND \`current_count >= 5\`. The floor
1496
+ suppresses noise on orgs with near-zero baseline activity (a single new
1497
+ YouTube report jumping the rate from 0 to 1/day shouldn't trigger). If
1498
+ the user gives a different window ("last 3 days", "this month"), adjust
1499
+ both windows proportionally and keep the same ratio threshold.
1500
+
1501
+ ### Trend 1 \u2014 Spike in a specific asset type
1502
+
1503
+ A sudden jump in threats on a platform the org doesn't usually see
1504
+ traffic on ("normally you don't get targeted on YouTube much, but now
1505
+ there's a spike there") is worth flagging \u2014 it usually means an attacker
1506
+ has discovered a new channel that works for them. Even when the absolute
1507
+ number is small, the *ratio* against the org's normal mix is what
1508
+ matters.
1509
+
1510
+ Use \`metrics breakdown --by type\` over both windows:
1511
+
1512
+ \`\`\`bash
1513
+ # Current window \u2014 last 7 days, broken out by asset type
1514
+ chainpatrol --json metrics breakdown --org <slug> --by type \\
1515
+ --from <YYYY-MM-DD 7d ago> --to <YYYY-MM-DD today>
1516
+
1517
+ # Baseline window \u2014 prior ~90 days ending where the current window starts
1518
+ chainpatrol --json metrics breakdown --org <slug> --by type \\
1519
+ --from <YYYY-MM-DD 97d ago> --to <YYYY-MM-DD 8d ago>
1520
+ \`\`\`
1521
+
1522
+ Each entry in \`.points\` has \`{ type, count }\`. Join the two windows on
1523
+ \`type\`, apply the ratio rule above, and report each flagged type.
1524
+
1525
+ Cross-reference any flagged type with \`configs list --org <slug>\` \u2014 if
1526
+ the spike is on Twitter / X but \`twitter_post_search\` is disabled, the
1527
+ detection source for that channel isn't even running for this org and
1528
+ the spike is being caught by something else (likely customer reports);
1529
+ suggest turning it on.
1530
+
1531
+ ### Trend 2 \u2014 Spike in overall threat volume
1532
+
1533
+ A sharp rise in total threats across the org \u2014 regardless of type or
1534
+ brand \u2014 usually means a coordinated attack or campaign is underway. This
1535
+ isn't a healthcheck (reviewing and takedowns may be keeping up just
1536
+ fine), but the security team still wants to know so they can warn
1537
+ customers / employees / partners.
1538
+
1539
+ \`\`\`bash
1540
+ # Daily volume for the last ~6 weeks \u2014 enough to eyeball a baseline AND
1541
+ # see the spike on the right edge of the series.
1542
+ chainpatrol --json metrics breakdown --org <slug> --by day \\
1543
+ --from <YYYY-MM-DD 42d ago> --to <YYYY-MM-DD today>
1544
+ \`\`\`
1545
+
1546
+ Read \`.points\` (one entry per day). Take the trailing 7-day average and
1547
+ compare against the prior ~5 weeks (the same 2\xD7 / floor rule). For round
1548
+ totals to quote to the user, also pull:
1549
+
1550
+ \`\`\`bash
1551
+ # Two calls \u2014 current and baseline windows \u2014 using --include to skip
1552
+ # the per-type / per-day series and keep the call cheap.
1553
+ chainpatrol --json metrics organization --org <slug> \\
1554
+ --include reports,newThreats,threatsWatchlisted \\
1555
+ --from <current_from> --to <current_to>
1556
+
1557
+ chainpatrol --json metrics organization --org <slug> \\
1558
+ --include reports,newThreats,threatsWatchlisted \\
1559
+ --from <baseline_from> --to <baseline_to>
1560
+ \`\`\`
1561
+
1562
+ When volume spikes broadly, also skim \`reports list --reported-by-customer\`
1563
+ for the same window \u2014 a wave of customer reports often arrives a few hours
1564
+ ahead of automated detection on a real coordinated push.
1565
+
1566
+ ### Trend 3 \u2014 Spike on a specific sub-brand (especially employee brands)
1567
+
1568
+ Sub-brands the org has set up for individual people \u2014 employees,
1569
+ executives, public figures \u2014 are high-signal targets. A sudden spike on
1570
+ one usually means an attacker is impersonating that person specifically,
1571
+ which is materially different from a generic phishing wave and usually
1572
+ warrants a direct heads-up to the person involved. In the database these
1573
+ are \`Brand\` rows with \`type: INDIVIDUAL\` (vs. \`ORGANIZATION\` for
1574
+ product / parent brands).
1575
+
1576
+ **Step 1: Pull the brand roster** so you can label each spike as
1577
+ "employee" or "product" without asking the user.
1578
+
1579
+ \`\`\`bash
1580
+ chainpatrol --json brands list
1581
+ \`\`\`
1582
+
1583
+ Build a map of \`slug \u2192 type\` from the response so the next step's
1584
+ findings can be tagged \`(employee)\` or \`(product)\` automatically.
1585
+
1586
+ **Step 2: Run the breakdown over both windows.**
1587
+
1588
+ \`\`\`bash
1589
+ # Current window \u2014 last 7 days, broken out by sub-brand
1590
+ chainpatrol --json metrics breakdown --org <slug> --by brand \\
1591
+ --from <YYYY-MM-DD 7d ago> --to <YYYY-MM-DD today>
1592
+
1593
+ # Baseline window \u2014 prior ~90 days
1594
+ chainpatrol --json metrics breakdown --org <slug> --by brand \\
1595
+ --from <YYYY-MM-DD 97d ago> --to <YYYY-MM-DD 8d ago>
1596
+ \`\`\`
1597
+
1598
+ Each entry in \`.points\` has \`{ brandId, brandSlug, brandName, count }\`.
1599
+ Apply the ratio rule, join against the \`slug \u2192 type\` map from Step 1,
1600
+ and report each spiking sub-brand by name plus its type.
1601
+
1602
+ **Step 3: Drill into the flagged brand** with \`metrics organization
1603
+ --brand-slug <slug>\` for a per-day / per-type breakdown scoped to that
1604
+ brand only. The server applies the brand filter to the scalar metrics
1605
+ (\`newThreats\`, \`takedownsFiled\`, etc.) AND to \`blockedByType\` /
1606
+ \`blockedByDay\`, so the time series and per-asset-type counts are real
1607
+ brand-only data:
1608
+
1609
+ \`\`\`bash
1610
+ chainpatrol --json metrics organization --org <slug> \\
1611
+ --brand-slug <brand-slug> \\
1612
+ --include newThreats,blockedByType,blockedByDay \\
1613
+ --from <YYYY-MM-DD 7d ago> --to <YYYY-MM-DD today>
1614
+ \`\`\`
1615
+
1616
+ If multiple \`INDIVIDUAL\` brands are spiking at the same time, treat that
1617
+ as a stronger signal than a single one \u2014 likely a campaign targeting the
1618
+ company's people rather than a single impersonation. Prioritize those
1619
+ findings over single-brand spikes when summarizing.
1620
+
1621
+ ### Reporting trend findings
1622
+
1623
+ Report each spiking bucket as its own finding with: the trend it falls
1624
+ under (asset type / overall volume / sub-brand), the current vs. baseline
1625
+ rate (e.g. "youtube: 12/day last 7d vs. 1.2/day prior 90d, \xD710
1626
+ baseline"), and a one-line follow-up:
1627
+
1628
+ - Asset-type spike \u2192 check \`configs list\` for the matching detection
1629
+ source and turn it on if it's off.
1630
+ - Overall-volume spike \u2192 flag a possible coordinated campaign; suggest
1631
+ the security team look at the recent \`reports list\` for shared
1632
+ infrastructure (sender, registrar, hosting).
1633
+ - Sub-brand spike \u2192 look up \`type\` via \`brands list\`; if
1634
+ \`INDIVIDUAL\`, suggest a direct heads-up to that person; if
1635
+ \`ORGANIZATION\`, suggest a product-team alert.
1636
+
1637
+ If none of the three trends fire above the 2\xD7 / floor thresholds, say so
1638
+ explicitly ("no significant trends in the last 7 days vs. the prior 90")
1639
+ rather than dumping every breakdown number \u2014 the absence is the
1640
+ finding.
1383
1641
  `;
1384
1642
  }
1385
1643
  function getBundledSkillVersion() {
package/dist/cli.js CHANGED
@@ -13,7 +13,7 @@ import {
13
13
  getCliVersion,
14
14
  isSkillInstalled,
15
15
  readInstalledSkillVersion
16
- } from "./chunk-75H4KTIQ.js";
16
+ } from "./chunk-UDGJEN4M.js";
17
17
  import "./chunk-NQT77IB7.js";
18
18
  import {
19
19
  DateTime
@@ -513,15 +513,15 @@ var HELP = {
513
513
  examples: ["chainpatrol orgs asset-groups list --output csv"]
514
514
  },
515
515
  "orgs list": {
516
- description: "List organizations with their subscription status and per-service active/automated flags. All filters compose with AND.",
516
+ description: "List organizations with their subscription status and per-service active flag (plus `automated` for takedowns only \u2014 that's the one service where manual-vs-automated changes real platform behavior). All filters compose with AND.",
517
517
  usage: "chainpatrol orgs list [filters]",
518
518
  options: [
519
519
  "--query <text> Partial name match (substring, case-insensitive)",
520
520
  "--subscription-status <l> Comma-separated list (PROSPECT,TRIAL,ACTIVE,INTEGRATION)",
521
521
  "--service-active <l> Comma-separated services that must be active",
522
522
  "--service-inactive <l> Comma-separated services that must be inactive",
523
- "--service-automated <l> Services whose automation must be ON (reporting,reviewing,protection,takedowns)",
524
- "--service-manual <l> Services whose automation must be OFF (reporting,reviewing,protection,takedowns)"
523
+ "--service-automated <l> Services whose automation must be ON (only `takedowns` is accepted \u2014 the other services have no meaningful automation toggle)",
524
+ "--service-manual <l> Services whose automation must be OFF (only `takedowns` is accepted)"
525
525
  ],
526
526
  examples: [
527
527
  "chainpatrol orgs list --subscription-status ACTIVE",
@@ -530,7 +530,7 @@ var HELP = {
530
530
  ]
531
531
  },
532
532
  "orgs get": {
533
- description: "Get a single organization by slug. Returns the same shape as one row of `orgs list` (subscription status + per-service active/automated flags). Caller must have access: org-scoped API keys must match the slug, user sessions/keys need an active membership unless the user is staff.",
533
+ description: "Get a single organization by slug. Returns the same shape as one row of `orgs list` (subscription status + per-service active flag, with `automated` exposed only for takedowns \u2014 the one service where manual-vs-automated changes real platform behavior). Caller must have access: org-scoped API keys must match the slug, user sessions/keys need an active membership unless the user is staff.",
534
534
  usage: "chainpatrol orgs get <slug>",
535
535
  options: [
536
536
  "--org <slug> Use the saved default org instead of a positional argument."
@@ -819,6 +819,7 @@ var COMMANDS = [
819
819
  "takedowns",
820
820
  "threats",
821
821
  "orgs",
822
+ "brands",
822
823
  "presets",
823
824
  "setup",
824
825
  "uninstall",
@@ -1054,12 +1055,12 @@ function parseAttachmentUrls() {
1054
1055
  }
1055
1056
  async function handleConfigsList(org) {
1056
1057
  if (jsonMode) {
1057
- const { listConfigsJson } = await import("./list-json-ZTS5GXNA.js");
1058
+ const { listConfigsJson } = await import("./list-json-B5L5PXKH.js");
1058
1059
  await listConfigsJson({ org });
1059
1060
  return;
1060
1061
  }
1061
1062
  const { render } = await import("ink");
1062
- const { default: ConfigsList } = await import("./list-AEM2KSO4.js");
1063
+ const { default: ConfigsList } = await import("./list-X4Z3CPO7.js");
1063
1064
  const { default: React } = await import("react");
1064
1065
  render(React.createElement(ConfigsList, { org }));
1065
1066
  }
@@ -1159,7 +1160,7 @@ async function main() {
1159
1160
  const positionals = cli.input.slice(2);
1160
1161
  const flagAssets = parseAssetInputs();
1161
1162
  const contents = [...positionals, ...flagAssets];
1162
- const { runAssetCheck } = await import("./check-EMGFDXUO.js");
1163
+ const { runAssetCheck } = await import("./check-G64UQCEL.js");
1163
1164
  await runAssetCheck({
1164
1165
  contents,
1165
1166
  json: jsonMode,
@@ -1169,7 +1170,7 @@ async function main() {
1169
1170
  break;
1170
1171
  }
1171
1172
  if (subcommand === "list") {
1172
- const { runAssetList } = await import("./list-2ENAEZVP.js");
1173
+ const { runAssetList } = await import("./list-XSIXAHOG.js");
1173
1174
  await runAssetList({
1174
1175
  type: cli.flags.type ?? cli.flags.assetType,
1175
1176
  status: cli.flags.assetStatus ?? cli.flags.status,
@@ -1190,7 +1191,7 @@ async function main() {
1190
1191
  case "detections": {
1191
1192
  const org = await resolveOrg();
1192
1193
  if (subcommand === "healthcheck") {
1193
- const { runDetectionsHealthcheck } = await import("./healthcheck-J6BIZEG7.js");
1194
+ const { runDetectionsHealthcheck } = await import("./healthcheck-QNLFKAXM.js");
1194
1195
  await runDetectionsHealthcheck({
1195
1196
  org,
1196
1197
  source: cli.flags.source,
@@ -1205,7 +1206,7 @@ async function main() {
1205
1206
  break;
1206
1207
  }
1207
1208
  if (subcommand === "validate") {
1208
- const { runDetectionsValidate } = await import("./validate-QE2F4PYY.js");
1209
+ const { runDetectionsValidate } = await import("./validate-UYKOMUEI.js");
1209
1210
  await runDetectionsValidate({
1210
1211
  org,
1211
1212
  source: cli.flags.source,
@@ -1220,7 +1221,7 @@ async function main() {
1220
1221
  break;
1221
1222
  }
1222
1223
  if (subcommand === "drift") {
1223
- const { runDetectionsDrift } = await import("./drift-RQKTGJWB.js");
1224
+ const { runDetectionsDrift } = await import("./drift-SHPMEJDU.js");
1224
1225
  await runDetectionsDrift({
1225
1226
  org,
1226
1227
  source: cli.flags.source,
@@ -1234,7 +1235,7 @@ async function main() {
1234
1235
  break;
1235
1236
  }
1236
1237
  if (subcommand === "run") {
1237
- const { runDetectionsRun } = await import("./run-HS2EEOM6.js");
1238
+ const { runDetectionsRun } = await import("./run-RV6P2OCD.js");
1238
1239
  await runDetectionsRun({
1239
1240
  org,
1240
1241
  configId: cli.flags.configId,
@@ -1248,7 +1249,7 @@ async function main() {
1248
1249
  break;
1249
1250
  }
1250
1251
  if (subcommand === "list") {
1251
- const { runDetectionsList } = await import("./list-LKNCICLT.js");
1252
+ const { runDetectionsList } = await import("./list-SWHKM5R6.js");
1252
1253
  await runDetectionsList({
1253
1254
  org,
1254
1255
  cursor: cli.flags.cursor,
@@ -1277,7 +1278,7 @@ async function main() {
1277
1278
  break;
1278
1279
  }
1279
1280
  if (action === "run") {
1280
- const { runDetectionsRun } = await import("./run-HS2EEOM6.js");
1281
+ const { runDetectionsRun } = await import("./run-RV6P2OCD.js");
1281
1282
  await runDetectionsRun({
1282
1283
  org,
1283
1284
  configId: cli.flags.configId,
@@ -1295,7 +1296,7 @@ async function main() {
1295
1296
  throw new Error("detections configs update requires --config-id");
1296
1297
  }
1297
1298
  const configPatch = getConfigPatchFromSetFlags();
1298
- const { runDetectionsConfigsUpdate } = await import("./configs-update-EKNLIRA6.js");
1299
+ const { runDetectionsConfigsUpdate } = await import("./configs-update-MVX7Q5NW.js");
1299
1300
  await runDetectionsConfigsUpdate({
1300
1301
  org,
1301
1302
  configId: cli.flags.configId,
@@ -1331,7 +1332,7 @@ async function main() {
1331
1332
  const slugs = parseCsvList(cli.flags.slugs);
1332
1333
  const allMyOrgs = cli.flags.allMyOrgs;
1333
1334
  const org2 = slugs && slugs.length > 0 || allMyOrgs ? void 0 : await tryResolveOrg();
1334
- const { runMetricsOrganization } = await import("./organization-PFPBZXJZ.js");
1335
+ const { runMetricsOrganization } = await import("./organization-SOMHMGTM.js");
1335
1336
  await runMetricsOrganization({
1336
1337
  org: org2,
1337
1338
  slugs,
@@ -1350,7 +1351,7 @@ async function main() {
1350
1351
  }
1351
1352
  const org = await resolveOrg();
1352
1353
  if (subcommand === "summary") {
1353
- const { runMetricsSummary } = await import("./summary-POVQC6B7.js");
1354
+ const { runMetricsSummary } = await import("./summary-LF53KXRR.js");
1354
1355
  await runMetricsSummary({
1355
1356
  org,
1356
1357
  from: cli.flags.from,
@@ -1363,7 +1364,7 @@ async function main() {
1363
1364
  break;
1364
1365
  }
1365
1366
  if (subcommand === "found") {
1366
- const { runMetricsFound } = await import("./found-LVYBEOS6.js");
1367
+ const { runMetricsFound } = await import("./found-OPL4HWOW.js");
1367
1368
  await runMetricsFound({
1368
1369
  org,
1369
1370
  from: cli.flags.from,
@@ -1380,7 +1381,7 @@ async function main() {
1380
1381
  if (!by || !["day", "type", "brand"].includes(by)) {
1381
1382
  throw new Error("metrics breakdown requires --by <day|type|brand>");
1382
1383
  }
1383
- const { runMetricsBreakdown } = await import("./breakdown-ETSF2BZ3.js");
1384
+ const { runMetricsBreakdown } = await import("./breakdown-3LJBT3JQ.js");
1384
1385
  await runMetricsBreakdown({
1385
1386
  org,
1386
1387
  by,
@@ -1401,7 +1402,7 @@ async function main() {
1401
1402
  case "reports": {
1402
1403
  if (subcommand === "list") {
1403
1404
  const org = await resolveOrg();
1404
- const { runReportsList } = await import("./list-44BQZHJD.js");
1405
+ const { runReportsList } = await import("./list-WPPNX7G3.js");
1405
1406
  await runReportsList({
1406
1407
  org,
1407
1408
  limit: cli.flags.limit,
@@ -1429,7 +1430,7 @@ async function main() {
1429
1430
  }
1430
1431
  if (subcommand === "create") {
1431
1432
  const org = await tryResolveOrg();
1432
- const { runReportsCreate } = await import("./create-M4PV5VVU.js");
1433
+ const { runReportsCreate } = await import("./create-ANCO4FXB.js");
1433
1434
  await runReportsCreate({
1434
1435
  org,
1435
1436
  title: cli.flags.title,
@@ -1450,7 +1451,7 @@ async function main() {
1450
1451
  const positionals = cli.input.slice(2);
1451
1452
  const flagAssets = parseAssetInputs();
1452
1453
  const assetContents = [...positionals, ...flagAssets];
1453
- const { runReportsSearch } = await import("./search-EBXEP7XC.js");
1454
+ const { runReportsSearch } = await import("./search-FQUR75WR.js");
1454
1455
  await runReportsSearch({
1455
1456
  assetContents,
1456
1457
  reportedByCustomer: cli.flags.reportedByCustomer,
@@ -1466,7 +1467,7 @@ async function main() {
1466
1467
  }
1467
1468
  case "threats": {
1468
1469
  if (subcommand === "list") {
1469
- const { runThreatsList } = await import("./list-E2JVJNHV.js");
1470
+ const { runThreatsList } = await import("./list-Q5PRMAN5.js");
1470
1471
  await runThreatsList({
1471
1472
  query: cli.flags.query,
1472
1473
  from: cli.flags.from,
@@ -1489,7 +1490,7 @@ async function main() {
1489
1490
  case "takedowns": {
1490
1491
  if (subcommand === "list") {
1491
1492
  const org = await tryResolveOrg();
1492
- const { runTakedownsList } = await import("./list-S5Y6GW43.js");
1493
+ const { runTakedownsList } = await import("./list-FWXQUOCP.js");
1493
1494
  await runTakedownsList({
1494
1495
  org,
1495
1496
  query: cli.flags.query,
@@ -1520,7 +1521,7 @@ async function main() {
1520
1521
  }
1521
1522
  case "queues": {
1522
1523
  if (subcommand === "snapshot") {
1523
- const { runQueuesSnapshot } = await import("./snapshot-GNZMYTXT.js");
1524
+ const { runQueuesSnapshot } = await import("./snapshot-2GDJMN3L.js");
1524
1525
  await runQueuesSnapshot({
1525
1526
  org: cli.flags.org,
1526
1527
  all: cli.flags.all,
@@ -1538,7 +1539,7 @@ async function main() {
1538
1539
  }
1539
1540
  case "orgs": {
1540
1541
  if (subcommand === "list") {
1541
- const { runOrgsList } = await import("./list-OEJV4UNQ.js");
1542
+ const { runOrgsList } = await import("./list-QXA6GLCV.js");
1542
1543
  await runOrgsList({
1543
1544
  query: cli.flags.query,
1544
1545
  subscriptionStatus: cli.flags.subscriptionStatus,
@@ -1554,7 +1555,7 @@ async function main() {
1554
1555
  if (subcommand === "get") {
1555
1556
  const positionalSlug = action;
1556
1557
  const slug = positionalSlug ?? await tryResolveOrg();
1557
- const { runOrgsGet } = await import("./get-BPTWCJ6H.js");
1558
+ const { runOrgsGet } = await import("./get-K7VHV5X2.js");
1558
1559
  await runOrgsGet({
1559
1560
  slug,
1560
1561
  json: jsonMode,
@@ -1564,7 +1565,7 @@ async function main() {
1564
1565
  }
1565
1566
  if (subcommand === "assets") {
1566
1567
  if (action === "list") {
1567
- const { runOrgsAssetsList } = await import("./list-O62BZKQ5.js");
1568
+ const { runOrgsAssetsList } = await import("./list-QHEFB4KE.js");
1568
1569
  await runOrgsAssetsList({
1569
1570
  type: cli.flags.type ?? cli.flags.assetType,
1570
1571
  groupId: cli.flags.groupId,
@@ -1580,7 +1581,7 @@ async function main() {
1580
1581
  }
1581
1582
  if (subcommand === "asset-groups") {
1582
1583
  if (action === "list") {
1583
- const { runOrgsAssetGroupsList } = await import("./list-O36VWTWR.js");
1584
+ const { runOrgsAssetGroupsList } = await import("./list-FVFF4VW3.js");
1584
1585
  await runOrgsAssetGroupsList({
1585
1586
  json: jsonMode,
1586
1587
  outputFormat: cliContext.outputFormat
@@ -1594,8 +1595,29 @@ async function main() {
1594
1595
  subcommand ? `Unknown subcommand: orgs ${subcommand}${hint ? `. Did you mean "orgs ${hint}"?` : ""}` : "Usage: chainpatrol orgs <list|get <slug>|assets list|asset-groups list> [filters]"
1595
1596
  );
1596
1597
  }
1598
+ case "brands": {
1599
+ if (subcommand === "list") {
1600
+ const rawType = cli.flags.type?.toUpperCase();
1601
+ if (rawType && rawType !== "ORGANIZATION" && rawType !== "INDIVIDUAL") {
1602
+ throw new Error(
1603
+ "brands list --type must be ORGANIZATION or INDIVIDUAL (case-insensitive)."
1604
+ );
1605
+ }
1606
+ const { runBrandsList } = await import("./list-4VKMEKZS.js");
1607
+ await runBrandsList({
1608
+ type: rawType,
1609
+ json: jsonMode,
1610
+ outputFormat: cliContext.outputFormat
1611
+ });
1612
+ break;
1613
+ }
1614
+ const hint = subcommand ? suggest(subcommand, ["list"]) : null;
1615
+ throw new Error(
1616
+ subcommand ? `Unknown subcommand: brands ${subcommand}${hint ? `. Did you mean "brands ${hint}"?` : ""}` : "Usage: chainpatrol brands list [--type ORGANIZATION|INDIVIDUAL]"
1617
+ );
1618
+ }
1597
1619
  case "whoami": {
1598
- const { runWhoami } = await import("./whoami-24KUJUN4.js");
1620
+ const { runWhoami } = await import("./whoami-OYOG63BX.js");
1599
1621
  await runWhoami({
1600
1622
  json: jsonMode,
1601
1623
  outputFormat: cliContext.outputFormat
@@ -1604,7 +1626,7 @@ async function main() {
1604
1626
  }
1605
1627
  case "healthchecks": {
1606
1628
  if (subcommand === "list") {
1607
- const { runHealthchecksList } = await import("./list-CNF2FGLN.js");
1629
+ const { runHealthchecksList } = await import("./list-AQ7E23T3.js");
1608
1630
  await runHealthchecksList({
1609
1631
  json: jsonMode,
1610
1632
  outputFormat: cliContext.outputFormat
@@ -1618,7 +1640,7 @@ async function main() {
1618
1640
  thresholds.minResults = cli.flags.minResults;
1619
1641
  if (cli.flags.lookbackHours !== void 0)
1620
1642
  thresholds.lookbackHours = cli.flags.lookbackHours;
1621
- const { runHealthchecksRun } = await import("./run-R3RR6XQF.js");
1643
+ const { runHealthchecksRun } = await import("./run-J5TQ2YC2.js");
1622
1644
  await runHealthchecksRun({
1623
1645
  org,
1624
1646
  id: action,
@@ -1636,7 +1658,7 @@ async function main() {
1636
1658
  }
1637
1659
  case "presets": {
1638
1660
  if (subcommand === "list") {
1639
- const { runPresetsList } = await import("./list-JCJHIJM4.js");
1661
+ const { runPresetsList } = await import("./list-FJBWSGTD.js");
1640
1662
  await runPresetsList({ outputFormat: cliContext.outputFormat });
1641
1663
  break;
1642
1664
  }
@@ -1647,7 +1669,7 @@ async function main() {
1647
1669
  );
1648
1670
  }
1649
1671
  const org = await resolveOrg();
1650
- const { runPresetsRun } = await import("./run-LKPKTH3W.js");
1672
+ const { runPresetsRun } = await import("./run-YAAMSWKK.js");
1651
1673
  await runPresetsRun({
1652
1674
  presetId: action,
1653
1675
  org,
@@ -1664,12 +1686,12 @@ async function main() {
1664
1686
  case "setup":
1665
1687
  case "install":
1666
1688
  case "i": {
1667
- const { setupSkill } = await import("./setup-skill-RPVW2CCQ.js");
1689
+ const { setupSkill } = await import("./setup-skill-F7437RT3.js");
1668
1690
  setupSkill({ json: jsonMode, cloud: cli.flags.cloud });
1669
1691
  break;
1670
1692
  }
1671
1693
  case "uninstall": {
1672
- const { uninstallSkill } = await import("./setup-skill-RPVW2CCQ.js");
1694
+ const { uninstallSkill } = await import("./setup-skill-F7437RT3.js");
1673
1695
  uninstallSkill({ json: jsonMode });
1674
1696
  break;
1675
1697
  }
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-DPLUSURP.js";
10
+ } from "./chunk-O7TMOF4W.js";
11
11
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-DPLUSURP.js";
10
+ } from "./chunk-O7TMOF4W.js";
11
11
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -16,11 +16,12 @@ import "./chunk-U73SABXK.js";
16
16
  // src/commands/orgs/get.ts
17
17
  function summarizeServices(org) {
18
18
  const parts = [];
19
- for (const service of ["reporting", "reviewing", "protection", "takedowns"]) {
20
- const entry = org.services[service];
21
- if (!entry.active) continue;
22
- parts.push(`${service}=${entry.automated ? "auto" : "manual"}`);
19
+ if (org.services.takedowns.active) {
20
+ parts.push(`takedowns=${org.services.takedowns.automated ? "auto" : "manual"}`);
23
21
  }
22
+ if (org.services.reporting.active) parts.push("reporting");
23
+ if (org.services.reviewing.active) parts.push("reviewing");
24
+ if (org.services.protection.active) parts.push("protection");
24
25
  if (org.services.detection.active) parts.push("detection");
25
26
  if (org.services.darkWebMonitoring.active) parts.push("darkWebMonitoring");
26
27
  return parts.length > 0 ? parts.join(" ") : "(no services enabled)";
@@ -52,12 +53,10 @@ async function runOrgsGet(options) {
52
53
  name: organization.name,
53
54
  subscriptionStatus: organization.subscriptionStatus,
54
55
  reportingActive: organization.services.reporting.active,
55
- reportingAutomated: organization.services.reporting.automated,
56
56
  reviewingActive: organization.services.reviewing.active,
57
- reviewingAutomated: organization.services.reviewing.automated,
58
57
  protectionActive: organization.services.protection.active,
59
- protectionAutomated: organization.services.protection.automated,
60
58
  takedownsActive: organization.services.takedowns.active,
59
+ // `takedowns` is the only service with a meaningful automation flag.
61
60
  takedownsAutomated: organization.services.takedowns.automated,
62
61
  detectionActive: organization.services.detection.active,
63
62
  darkWebMonitoringActive: organization.services.darkWebMonitoring.active
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -0,0 +1,64 @@
1
+ import {
2
+ printOutput,
3
+ toCsvRows
4
+ } from "./chunk-VFT3TD3E.js";
5
+ import {
6
+ createApiClient
7
+ } from "./chunk-O7TMOF4W.js";
8
+ import "./chunk-EGWK6SRQ.js";
9
+ import "./chunk-TFCNKBRC.js";
10
+ import "./chunk-U73SABXK.js";
11
+
12
+ // src/commands/brands/list.ts
13
+ function toRow(brand) {
14
+ return {
15
+ id: brand.id,
16
+ slug: brand.slug,
17
+ name: brand.name,
18
+ type: brand.type,
19
+ description: brand.description ?? "",
20
+ brandGroupId: brand.brandGroupId,
21
+ createdAt: brand.createdAt
22
+ };
23
+ }
24
+ async function runBrandsList(options) {
25
+ const outputFormat = options.outputFormat ?? (options.json ? "json" : "human");
26
+ const client = options.apiClient ?? createApiClient();
27
+ const result = await client.listOrganizationBrands();
28
+ const brands = options.type ? result.brands.filter((brand) => brand.type === options.type) : result.brands;
29
+ const rows = brands.map(toRow);
30
+ printOutput({
31
+ outputFormat,
32
+ json: options.type ? { ...result, brands } : result,
33
+ markdown: [
34
+ "# Organization brands",
35
+ "",
36
+ `- Returned: ${brands.length}${options.type ? ` (filtered to type=${options.type})` : ""}`,
37
+ "",
38
+ ...rows.map(
39
+ (row) => `- #${row.id} ${row.name} (slug=${row.slug}, type=${row.type}${row.brandGroupId !== null ? `, groupId=${row.brandGroupId}` : ""})`
40
+ )
41
+ ].join("\n"),
42
+ csv: toCsvRows(rows),
43
+ human: () => {
44
+ if (brands.length === 0) {
45
+ console.log(
46
+ options.type ? `No brands of type ${options.type} found.` : "No brands found."
47
+ );
48
+ return;
49
+ }
50
+ console.log(
51
+ `Organization brands \u2014 returned ${brands.length}${options.type ? ` (type=${options.type})` : ""}`
52
+ );
53
+ for (const row of rows) {
54
+ const group = row.brandGroupId !== null ? ` groupId=${row.brandGroupId}` : "";
55
+ console.log(
56
+ `#${row.id} ${row.name} (slug=${row.slug}, type=${row.type}${group})`
57
+ );
58
+ }
59
+ }
60
+ });
61
+ }
62
+ export {
63
+ runBrandsList
64
+ };
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-DPLUSURP.js";
7
+ } from "./chunk-O7TMOF4W.js";
8
8
  import "./chunk-EGWK6SRQ.js";
9
9
  import "./chunk-TFCNKBRC.js";
10
10
  import "./chunk-U73SABXK.js";
@@ -1,12 +1,12 @@
1
1
  import {
2
2
  PRESETS
3
- } from "./chunk-HGAQBGGL.js";
3
+ } from "./chunk-2JZAPTY6.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-DPLUSURP.js";
9
+ import "./chunk-O7TMOF4W.js";
10
10
  import "./chunk-EGWK6SRQ.js";
11
11
  import "./chunk-TFCNKBRC.js";
12
12
  import "./chunk-U73SABXK.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-VFT3TD3E.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-DPLUSURP.js";
7
+ } from "./chunk-O7TMOF4W.js";
8
8
  import "./chunk-EGWK6SRQ.js";
9
9
  import "./chunk-TFCNKBRC.js";
10
10
  import "./chunk-U73SABXK.js";
@@ -11,7 +11,7 @@ import {
11
11
  TAKEDOWN_SORT_KEYS,
12
12
  TAKEDOWN_STATUSES,
13
13
  createApiClient
14
- } from "./chunk-DPLUSURP.js";
14
+ } from "./chunk-O7TMOF4W.js";
15
15
  import "./chunk-EGWK6SRQ.js";
16
16
  import "./chunk-TFCNKBRC.js";
17
17
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -20,8 +20,14 @@ var VALID_SUBSCRIPTION_STATUSES = /* @__PURE__ */ new Set([
20
20
  "ACTIVE",
21
21
  "INTEGRATION"
22
22
  ]);
23
- var DUAL_SERVICES = ["reporting", "reviewing", "protection", "takedowns"];
24
- var SINGLE_SERVICES = ["detection", "darkWebMonitoring"];
23
+ var DUAL_SERVICES = ["takedowns"];
24
+ var SINGLE_SERVICES = [
25
+ "reporting",
26
+ "reviewing",
27
+ "protection",
28
+ "detection",
29
+ "darkWebMonitoring"
30
+ ];
25
31
  var ALL_SERVICES = [...DUAL_SERVICES, ...SINGLE_SERVICES];
26
32
  function splitCsv(value) {
27
33
  if (!value) return [];
@@ -60,7 +66,7 @@ function parseServiceList(raw, flagName) {
60
66
  function requireDualService(service, flagName) {
61
67
  if (!DUAL_SERVICES.includes(service)) {
62
68
  throw new CliExitError(
63
- `Service '${service}' does not have an automation toggle and can't be used with --${flagName}. Use --service-active / --service-inactive instead.`,
69
+ `Service '${service}' does not have a meaningful automation toggle and can't be used with --${flagName}. Only 'takedowns' supports --service-automated / --service-manual; use --service-active / --service-inactive for the others.`,
64
70
  ExitCode.USAGE
65
71
  );
66
72
  }
@@ -114,10 +120,8 @@ function buildServicesFilter(options) {
114
120
  }
115
121
  function summarizeServices(org) {
116
122
  const parts = [];
117
- for (const service of DUAL_SERVICES) {
118
- const entry = org.services[service];
119
- if (!entry.active) continue;
120
- parts.push(`${service}=${entry.automated ? "auto" : "manual"}`);
123
+ if (org.services.takedowns.active) {
124
+ parts.push(`takedowns=${org.services.takedowns.automated ? "auto" : "manual"}`);
121
125
  }
122
126
  for (const service of SINGLE_SERVICES) {
123
127
  if (org.services[service].active) parts.push(service);
@@ -150,12 +154,10 @@ async function runOrgsList(options) {
150
154
  name: org.name,
151
155
  subscriptionStatus: org.subscriptionStatus,
152
156
  reportingActive: org.services.reporting.active,
153
- reportingAutomated: org.services.reporting.automated,
154
157
  reviewingActive: org.services.reviewing.active,
155
- reviewingAutomated: org.services.reviewing.automated,
156
158
  protectionActive: org.services.protection.active,
157
- protectionAutomated: org.services.protection.automated,
158
159
  takedownsActive: org.services.takedowns.active,
160
+ // `takedowns` is the only service with a meaningful automation flag.
159
161
  takedownsAutomated: org.services.takedowns.automated,
160
162
  detectionActive: org.services.detection.active,
161
163
  darkWebMonitoringActive: org.services.darkWebMonitoring.active
@@ -12,7 +12,7 @@ import {
12
12
  LIVENESS_FILTER_VALUES,
13
13
  WATCHLIST_FILTER_VALUES,
14
14
  createApiClient
15
- } from "./chunk-DPLUSURP.js";
15
+ } from "./chunk-O7TMOF4W.js";
16
16
  import "./chunk-EGWK6SRQ.js";
17
17
  import "./chunk-TFCNKBRC.js";
18
18
  import "./chunk-U73SABXK.js";
@@ -10,7 +10,7 @@ import {
10
10
  REPORT_REVIEW_DECISIONS,
11
11
  REPORT_STATUSES,
12
12
  createApiClient
13
- } from "./chunk-DPLUSURP.js";
13
+ } from "./chunk-O7TMOF4W.js";
14
14
  import "./chunk-EGWK6SRQ.js";
15
15
  import "./chunk-TFCNKBRC.js";
16
16
  import "./chunk-U73SABXK.js";
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-JCMWDZYY.js";
5
5
  import {
6
6
  createApiClient
7
- } from "./chunk-DPLUSURP.js";
7
+ } from "./chunk-O7TMOF4W.js";
8
8
  import {
9
9
  AuthCorruptedError,
10
10
  AuthExpiredError,
@@ -8,7 +8,7 @@ import {
8
8
  } from "./chunk-VFT3TD3E.js";
9
9
  import {
10
10
  createApiClient
11
- } from "./chunk-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  createApiClient
3
- } from "./chunk-DPLUSURP.js";
3
+ } from "./chunk-O7TMOF4W.js";
4
4
  import "./chunk-EGWK6SRQ.js";
5
5
  import "./chunk-TFCNKBRC.js";
6
6
  import "./chunk-U73SABXK.js";
@@ -8,7 +8,7 @@ import {
8
8
  import {
9
9
  ORGANIZATION_METRICS_INCLUDE_FIELDS,
10
10
  createApiClient
11
- } from "./chunk-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -1,13 +1,13 @@
1
1
  import {
2
2
  getPresetDefinition,
3
3
  runPreset
4
- } from "./chunk-HGAQBGGL.js";
4
+ } from "./chunk-2JZAPTY6.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-DPLUSURP.js";
10
+ import "./chunk-O7TMOF4W.js";
11
11
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -6,7 +6,7 @@ import {
6
6
  readInstalledSkillVersion,
7
7
  setupSkill,
8
8
  uninstallSkill
9
- } from "./chunk-75H4KTIQ.js";
9
+ } from "./chunk-UDGJEN4M.js";
10
10
  import "./chunk-NQT77IB7.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -7,7 +7,7 @@ import {
7
7
  } from "./chunk-VFT3TD3E.js";
8
8
  import {
9
9
  createApiClient
10
- } from "./chunk-DPLUSURP.js";
10
+ } from "./chunk-O7TMOF4W.js";
11
11
  import "./chunk-EGWK6SRQ.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-DPLUSURP.js";
11
+ } from "./chunk-O7TMOF4W.js";
12
12
  import "./chunk-EGWK6SRQ.js";
13
13
  import "./chunk-TFCNKBRC.js";
14
14
  import "./chunk-U73SABXK.js";
@@ -3,7 +3,7 @@ import {
3
3
  } from "./chunk-VFT3TD3E.js";
4
4
  import {
5
5
  createApiClient
6
- } from "./chunk-DPLUSURP.js";
6
+ } from "./chunk-O7TMOF4W.js";
7
7
  import "./chunk-EGWK6SRQ.js";
8
8
  import "./chunk-TFCNKBRC.js";
9
9
  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.17.0",
5
+ "version": "0.19.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [