@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.
- package/CHANGELOG.md +88 -0
- package/dist/{breakdown-ETSF2BZ3.js → breakdown-3LJBT3JQ.js} +1 -1
- package/dist/{check-EMGFDXUO.js → check-G64UQCEL.js} +1 -1
- package/dist/{chunk-HGAQBGGL.js → chunk-2JZAPTY6.js} +1 -1
- package/dist/{chunk-DPLUSURP.js → chunk-O7TMOF4W.js} +3 -0
- package/dist/{chunk-75H4KTIQ.js → chunk-UDGJEN4M.js} +273 -15
- package/dist/cli.js +59 -37
- package/dist/{configs-update-EKNLIRA6.js → configs-update-MVX7Q5NW.js} +1 -1
- package/dist/{create-M4PV5VVU.js → create-ANCO4FXB.js} +1 -1
- package/dist/{drift-RQKTGJWB.js → drift-SHPMEJDU.js} +1 -1
- package/dist/{found-LVYBEOS6.js → found-OPL4HWOW.js} +1 -1
- package/dist/{get-BPTWCJ6H.js → get-K7VHV5X2.js} +7 -8
- package/dist/{healthcheck-J6BIZEG7.js → healthcheck-QNLFKAXM.js} +1 -1
- package/dist/list-4VKMEKZS.js +64 -0
- package/dist/{list-CNF2FGLN.js → list-AQ7E23T3.js} +1 -1
- package/dist/{list-JCJHIJM4.js → list-FJBWSGTD.js} +2 -2
- package/dist/{list-O36VWTWR.js → list-FVFF4VW3.js} +1 -1
- package/dist/{list-S5Y6GW43.js → list-FWXQUOCP.js} +1 -1
- package/dist/{list-E2JVJNHV.js → list-Q5PRMAN5.js} +1 -1
- package/dist/{list-O62BZKQ5.js → list-QHEFB4KE.js} +1 -1
- package/dist/{list-OEJV4UNQ.js → list-QXA6GLCV.js} +13 -11
- package/dist/{list-LKNCICLT.js → list-SWHKM5R6.js} +1 -1
- package/dist/{list-44BQZHJD.js → list-WPPNX7G3.js} +1 -1
- package/dist/{list-AEM2KSO4.js → list-X4Z3CPO7.js} +1 -1
- package/dist/{list-2ENAEZVP.js → list-XSIXAHOG.js} +1 -1
- package/dist/{list-json-ZTS5GXNA.js → list-json-B5L5PXKH.js} +1 -1
- package/dist/{organization-PFPBZXJZ.js → organization-SOMHMGTM.js} +1 -1
- package/dist/{run-R3RR6XQF.js → run-J5TQ2YC2.js} +1 -1
- package/dist/{run-HS2EEOM6.js → run-RV6P2OCD.js} +1 -1
- package/dist/{run-LKPKTH3W.js → run-YAAMSWKK.js} +2 -2
- package/dist/{search-EBXEP7XC.js → search-FQUR75WR.js} +1 -1
- package/dist/{setup-skill-RPVW2CCQ.js → setup-skill-F7437RT3.js} +1 -1
- package/dist/{snapshot-GNZMYTXT.js → snapshot-2GDJMN3L.js} +1 -1
- package/dist/{summary-POVQC6B7.js → summary-LF53KXRR.js} +1 -1
- package/dist/{validate-QE2F4PYY.js → validate-UYKOMUEI.js} +1 -1
- package/dist/{whoami-24KUJUN4.js → whoami-OYOG63BX.js} +1 -1
- 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
|
|
@@ -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
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
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
|
-
|
|
651
|
-
|
|
652
|
-
- \`--service-manual <list>\` services whose automation must be OFF
|
|
653
|
-
|
|
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\`
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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-
|
|
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
|
|
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 (
|
|
524
|
-
"--service-manual <l> Services whose automation must be OFF (
|
|
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
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
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-
|
|
1694
|
+
const { uninstallSkill } = await import("./setup-skill-F7437RT3.js");
|
|
1673
1695
|
uninstallSkill({ json: jsonMode });
|
|
1674
1696
|
break;
|
|
1675
1697
|
}
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
} from "./chunk-VFT3TD3E.js";
|
|
9
9
|
import {
|
|
10
10
|
createApiClient
|
|
11
|
-
} from "./chunk-
|
|
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
|
-
|
|
20
|
-
|
|
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
|
|
@@ -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
|
+
};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import {
|
|
2
2
|
PRESETS
|
|
3
|
-
} from "./chunk-
|
|
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-
|
|
9
|
+
import "./chunk-O7TMOF4W.js";
|
|
10
10
|
import "./chunk-EGWK6SRQ.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-
|
|
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 = ["
|
|
24
|
-
var SINGLE_SERVICES = [
|
|
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
|
|
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
|
-
|
|
118
|
-
|
|
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
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import {
|
|
2
2
|
getPresetDefinition,
|
|
3
3
|
runPreset
|
|
4
|
-
} from "./chunk-
|
|
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-
|
|
10
|
+
import "./chunk-O7TMOF4W.js";
|
|
11
11
|
import "./chunk-EGWK6SRQ.js";
|
|
12
12
|
import "./chunk-TFCNKBRC.js";
|
|
13
13
|
import "./chunk-U73SABXK.js";
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@chainpatrol/cli",
|
|
3
3
|
"description": "The official ChainPatrol CLI — terminal interface for threat detection",
|
|
4
4
|
"author": "Umar Ahmed <umar@chainpatrol.io>",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.19.0",
|
|
6
6
|
"license": "UNLICENSED",
|
|
7
7
|
"homepage": "https://chainpatrol.com/docs/cli",
|
|
8
8
|
"keywords": [
|