@chainpatrol/cli 1.2.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,132 @@
1
1
  # @chainpatrol/cli
2
2
 
3
+ ## 1.4.0
4
+
5
+ ### Minor Changes
6
+
7
+ - ccd3811: Adds `reporterKind` and `reviewerKind` filters (`human` | `automation`) to organization report listing so callers can separate automation vs human report sources and approvals.
8
+
9
+ The previous `excludeAutomation` boolean remains supported as a deprecated alias for `reporterKind: "human"`. CLI adds `--reporter-kind` and `--reviewer-kind`.
10
+
11
+ Server-side work in `@chainpatrol/core`, `@chainpatrol/trpc`, and `@chainpatrol/external-trpc` backs the new filters; those packages are private and don't need version bumps.
12
+
13
+ - 9dd02cf: Third and final tranche of the API-surface expansion — the join-required
14
+ slices. All additive, no removed fields.
15
+
16
+ **Takedowns — provider names on `listTakedowns` and `getTakedown`.** Each
17
+ `TakedownItem` and the `getTakedown` `takedown` object now carries
18
+ `domainRegistrar`, `hostingProvider`, and `tldRegistrar` (each
19
+ `{ id, name } | null`) alongside the existing `assignee` / `brand`.
20
+ Answers "who hosts phish.example?" without a follow-up call and unblocks
21
+ "which takedowns are stuck at registrar X?" cross-org queries. Distinct
22
+ from the per-task `takedownProvider` — the trio here is the
23
+ takedown-level assignment for the domain itself, whereas the task-level
24
+ provider is the entity handling the current step.
25
+
26
+ **Detection Configs — `lastResultAt` aggregate on
27
+ `listDetectionConfigs`.** Each `ConfigEntry` now carries `lastResultAt`
28
+ (nullable ISO timestamp) — the `MAX(createdAt)` of `ThreatDetectionResult`
29
+ rows tied to that config. Surfaces the same silent-config signal the
30
+ drift healthcheck already computes internally. A config with a stale
31
+ `lastResultAt` is going silent regardless of what its `updatedAt` says.
32
+
33
+ Computed with a single `groupBy` on `ThreatDetectionResult` scoped to the
34
+ response's config IDs — one extra query per call, index-friendly via
35
+ `ThreatDetectionResult.@@index([configId])`.
36
+
37
+ Server-side work in `@chainpatrol/external-trpc` and
38
+ `@chainpatrol/validation` supports the new fields; both are private
39
+ packages and don't need version bumps.
40
+
41
+ - 725d32c: `listOrganizationReports` now calls `GET /organization/reports` instead of the deprecated `POST /public/getOrganizationReports`. Inputs, outputs, and the CLI's `reports list --org <slug>` behaviour are unchanged.
42
+
43
+ Server-side work in `@chainpatrol/external-trpc` makes the switch safe. `GET /organization/reports` now accepts an optional `slug`, authorized the same way the deprecated endpoint authorized it, so callers whose credentials cover more than one organization — user-scoped API keys, staff and member sessions, which is how `chainpatrol login` authenticates — can still choose an organization instead of having it derived from an org-scoped API key. It also fixes two query-string bugs that made the endpoint's filters unusable: `?<flag>=false` was read as `true`, and array filters passed once rather than repeated (`?brandIds=12`, `?assetTypes=URL`) were rejected with a validation error. `@chainpatrol/external-trpc` is private and does not need a version bump.
44
+
45
+ - 725d32c: Adds a `needsCustomerReview` filter to organization report listing so callers can pull just the reports an organization still has to approve. CLI adds `--needs-customer-review` / `--no-needs-customer-review` to `reports list`.
46
+
47
+ The filter resolves what "waiting on the customer" means per organization: with Obligatory Organization Admin Approval on, every report with a pending proposal whose asset type is in the approval scope (all asset types when the scope is not narrowed) plus anything escalated to the customer; with it off, only reports escalated to the customer. Reports the organization submitted itself are excluded.
48
+
49
+ Server-side work in `@chainpatrol/core` and `@chainpatrol/external-trpc` backs the new filter; those packages are private and don't need version bumps.
50
+
51
+ - e73955d: Fixes three problems with report search (`POST /reports/search`, `chainpatrol reports search`):
52
+
53
+ - **It did not work for logged-in CLI users.** The endpoint resolved the organization only from an organization-scoped API key, so anyone authenticated with the bearer token `chainpatrol login` issues got `API Organization not found. Invalid or missing API key.` It now accepts an optional `slug`, authorized the same way the rest of the API authorizes one, and the CLI passes the resolved organization.
54
+ - **Results were unbounded.** An asset content shared across many reports returned every one of them. Responses are now capped by a `limit` (default 50, max 100, newest first) and carry `totalCount` so callers can see when results were truncated. The CLI gains `--limit` and reports "Matched 3 of 41 report(s)".
55
+ - **The endpoint's purpose was unclear next to report listing.** Both endpoints' descriptions now state the question each answers and point at the other: search looks reports up by asset content in one batched call to avoid filing a duplicate; `GET /organization/reports` browses and filters an organization's reports with full records and pagination.
56
+
57
+ Server-side work in `@chainpatrol/external-trpc`, which is private and does not need a version bump.
58
+
59
+ ### Patch Changes
60
+
61
+ - Updated dependencies [ccd3811]
62
+ - Updated dependencies [9dd02cf]
63
+ - Updated dependencies [725d32c]
64
+ - Updated dependencies [725d32c]
65
+ - Updated dependencies [e73955d]
66
+ - @chainpatrol/sdk@1.4.0
67
+
68
+ ## 1.3.0
69
+
70
+ ### Minor Changes
71
+
72
+ - 08b9976: Broaden the public API surface across Reports, Takedowns, Detections,
73
+ Detection Configs, and Assets. Everything additive — no removed fields,
74
+ no renames — so existing callers are unaffected.
75
+
76
+ **Detections (`listDetections`)** — new fields on each `DetectionListItem`:
77
+ `reason` (free-text explanation of why the detection fired), `score` (raw
78
+ 0–1 model score), `configId` (which `ThreatDetectionConfig` produced the
79
+ hit), `reportedAt` (when it transitioned to `REPORTED`, nullable), and
80
+ `brands[]` (brand associations).
81
+
82
+ **Detection Configs (`listDetectionConfigs`)** — each `ConfigEntry` now
83
+ carries `description` (nullable), `createdAt`, and `updatedAt` so
84
+ integrators can spot silent or stale configs.
85
+
86
+ **Reports** — `listOrganizationReports` gains `slaDueAt` (SLA deadline),
87
+ `externalSubmissionLink` (original external source URL when submitted from
88
+ a third-party surface), and `duplicateOfId` (canonical report ID when this
89
+ one is a duplicate). Type gaps are also closed: `imageDisplay`,
90
+ `favoritedAt`, `attachments[]`, and `proposals.asset.scans[]` were already
91
+ returned by the handler but omitted from the SDK type — they're now
92
+ declared. `searchReports` (`report/search-external`) response now
93
+ surfaces the `title` and `createdAt` fields the handler already fetched.
94
+
95
+ **Takedowns (`listTakedowns`)** — each `TakedownItem` gains `externalId`
96
+ (vendor's own reference — CleanDNS report ID / registrar ticket, for
97
+ reconciliation), `hasFilingDelay`, `hasLivenessCheckDelay` (staff-set
98
+ flags used by median-time metrics), and `asset.blockedAt`.
99
+
100
+ **Takedowns (`getTakedown`)** — the response was previously bare
101
+ (`{ assetContent, takedownStatus, createdAt, updatedAt }`). It's now
102
+ fully built out with `asset` details (id / content / type /
103
+ `livenessStatus` / `blockedAt`) and a nested `takedown` record (id /
104
+ status / `externalId` / delay flags / assignee / brand / tasks[]). The
105
+ original top-level fields (`takedownStatus`, `createdAt`, `updatedAt`)
106
+ are preserved for existing callers.
107
+
108
+ **Assets — `livenessStatus` + `blockedAt` parity.** `listAssets`,
109
+ `listOrganizationAssets`, `assetSearch` (`.asset`), and `listThreats`
110
+ (`livenessStatus` only, `blockedAt` already there) now consistently
111
+ return both fields. `listAssets` also fixes a bug where each returned
112
+ row's `id` was stripped from the response mapper (the column was already
113
+ selected). Note: `assetCheck` is intentionally deferred — its response
114
+ goes through `@chainpatrol/asset`'s own `checkAsset()` contract; parity
115
+ there needs a scoped follow-up in the asset package.
116
+
117
+ Server-side work in `@chainpatrol/external-trpc`, `@chainpatrol/core`,
118
+ `@chainpatrol/validation`, and `@chainpatrol/database` backs the new
119
+ fields; those are private packages and don't need version bumps. Also
120
+ adds a `/// @deprecated` doc-comment to `Report.threatLevel` in the
121
+ Prisma schema — the field is not surfaced through the public API and no
122
+ in-app UI reads it; the column stays for now, removal is a separate
123
+ destructive migration.
124
+
125
+ ### Patch Changes
126
+
127
+ - Updated dependencies [08b9976]
128
+ - @chainpatrol/sdk@1.3.0
129
+
3
130
  ## 1.2.0
4
131
 
5
132
  ### Minor Changes
package/dist/cli.js CHANGED
@@ -356,16 +356,19 @@ var HELP = {
356
356
  ]
357
357
  },
358
358
  "reports search": {
359
- description: "Search existing reports in your organization by asset contents. Useful to check for an existing report before creating a duplicate.",
359
+ description: "Look up reports by asset content \u2014 'do reports already exist for these assets?'. Checks a batch of assets in one call, so it is the fast way to avoid filing a duplicate. To browse or filter an org's reports instead, use `reports list`.",
360
360
  usage: "chainpatrol reports search <content> [<content> ...]",
361
361
  options: [
362
+ "--org <slug> Organization slug (defaults to saved/env)",
362
363
  "--asset <content> Asset content (repeatable; alternative to positional args)",
363
364
  "--reported-by-customer Match only customer-submitted reports",
364
- "--no-reported-by-customer Match only non-customer reports"
365
+ "--no-reported-by-customer Match only non-customer reports",
366
+ "--limit <n> Max reports to return, newest first (1-100, default 50)"
365
367
  ],
366
368
  examples: [
367
369
  "chainpatrol reports search https://bad.site",
368
- "chainpatrol reports search https://a.site https://b.site --reported-by-customer"
370
+ "chainpatrol reports search https://a.site https://b.site --reported-by-customer",
371
+ "chainpatrol reports search https://bad.site --limit 100"
369
372
  ]
370
373
  },
371
374
  "reports create": {
@@ -388,7 +391,7 @@ var HELP = {
388
391
  ]
389
392
  },
390
393
  "reports list": {
391
- description: "List recent reports for an organization with the same level of filtering as the app's Reports view.",
394
+ description: "List recent reports for an organization with the same level of filtering as the app's Reports view. To check whether a report already exists for specific assets, use `reports search` instead.",
392
395
  usage: "chainpatrol reports list --org <slug> [filters]",
393
396
  options: [
394
397
  "--org <slug> Organization slug",
@@ -399,7 +402,11 @@ var HELP = {
399
402
  "--reporter-query <q> Search reports by reporter name (legacy)",
400
403
  "--reported-by-customer Show only customer-submitted reports",
401
404
  "--no-reported-by-customer Show only non-customer reports",
402
- "--exclude-automation Exclude reports submitted by automation",
405
+ "--needs-customer-review Show only reports awaiting the org's own approval",
406
+ "--no-needs-customer-review Show only reports not awaiting the org's approval",
407
+ "--exclude-automation Deprecated; prefer --reporter-kind human",
408
+ "--reporter-kind <kind> Filter by reporter: human|automation",
409
+ "--reviewer-kind <kind> Filter by approver: human|automation",
403
410
  "--only-rejected Only reports whose latest review decision was REJECT",
404
411
  "--review-status <list> Comma list: APPROVE|REJECT|SKIP|ESCALATE",
405
412
  "--asset-type <list> Comma list of asset types (URL,TWITTER,...)",
@@ -413,9 +420,10 @@ var HELP = {
413
420
  ],
414
421
  examples: [
415
422
  "chainpatrol reports list --org acme --status TODO --review-status REJECT",
416
- "chainpatrol reports list --org acme --exclude-automation --reported-by-customer",
423
+ "chainpatrol reports list --org acme --reporter-kind human --reviewer-kind automation",
417
424
  "chainpatrol reports list --org acme --from 2026-05-01 --updated-from 2026-05-10",
418
- "chainpatrol reports list --org acme --brand 12,34 --country-code US,GB"
425
+ "chainpatrol reports list --org acme --brand 12,34 --country-code US,GB",
426
+ "chainpatrol reports list --org acme --needs-customer-review"
419
427
  ]
420
428
  },
421
429
  queues: {
@@ -927,6 +935,7 @@ ${getTopLevelHelp()}
927
935
  status: { type: "string" },
928
936
  search: { type: "string" },
929
937
  reportedByCustomer: { type: "boolean" },
938
+ needsCustomerReview: { type: "boolean" },
930
939
  attachmentUrl: { type: "string" },
931
940
  contactInfo: { type: "string" },
932
941
  externalSubmissionLink: { type: "string" },
@@ -967,6 +976,8 @@ ${getTopLevelHelp()}
967
976
  hideAutomatedLivenessChecks: { type: "boolean" },
968
977
  reporterQuery: { type: "string" },
969
978
  excludeAutomation: { type: "boolean" },
979
+ reporterKind: { type: "string" },
980
+ reviewerKind: { type: "string" },
970
981
  onlyRejected: { type: "boolean" },
971
982
  reviewStatus: { type: "string" },
972
983
  reviewedByUserId: { type: "number" },
@@ -1073,6 +1084,17 @@ function parseCsvList(value) {
1073
1084
  const items = value.split(",").map((item) => item.trim()).filter(Boolean);
1074
1085
  return items.length > 0 ? items : void 0;
1075
1086
  }
1087
+ function parseActorKindFlag(value, flagName) {
1088
+ if (!value) return void 0;
1089
+ const normalized = value.trim().toLowerCase();
1090
+ if (normalized === "human" || normalized === "automation") {
1091
+ return normalized;
1092
+ }
1093
+ throw new CliExitError(
1094
+ `--${flagName} must be "human" or "automation"; got '${value}'.`,
1095
+ ExitCode.USAGE
1096
+ );
1097
+ }
1076
1098
  function parseIntList(value, flagName) {
1077
1099
  const items = parseCsvList(value);
1078
1100
  if (!items) return void 0;
@@ -1469,7 +1491,7 @@ async function main() {
1469
1491
  case "reports": {
1470
1492
  if (subcommand === "list") {
1471
1493
  const org = await resolveOrg();
1472
- const { runReportsList } = await import("./list-TULFPPG3.js");
1494
+ const { runReportsList } = await import("./list-WQBXC3QS.js");
1473
1495
  await runReportsList({
1474
1496
  org,
1475
1497
  limit: cli.flags.limit,
@@ -1478,7 +1500,10 @@ async function main() {
1478
1500
  searchQuery: cli.flags.search,
1479
1501
  reporterQuery: cli.flags.reporterQuery,
1480
1502
  reportedByCustomer: cli.flags.reportedByCustomer,
1503
+ needsCustomerReview: cli.flags.needsCustomerReview,
1481
1504
  excludeAutomation: cli.flags.excludeAutomation,
1505
+ reporterKind: parseActorKindFlag(cli.flags.reporterKind, "reporterKind"),
1506
+ reviewerKind: parseActorKindFlag(cli.flags.reviewerKind, "reviewerKind"),
1482
1507
  onlyRejected: cli.flags.onlyRejected,
1483
1508
  reviewStatuses: parseCsvList(cli.flags.reviewStatus),
1484
1509
  assetTypes: parseCsvList(cli.flags.assetType),
@@ -1515,13 +1540,16 @@ async function main() {
1515
1540
  break;
1516
1541
  }
1517
1542
  if (subcommand === "search") {
1543
+ const org = await tryResolveOrg();
1518
1544
  const positionals = cli.input.slice(2);
1519
1545
  const flagAssets = parseAssetInputs();
1520
1546
  const assetContents = [...positionals, ...flagAssets];
1521
- const { runReportsSearch } = await import("./search-5DOXBWPB.js");
1547
+ const { runReportsSearch } = await import("./search-M6EKPO4D.js");
1522
1548
  await runReportsSearch({
1523
1549
  assetContents,
1550
+ org,
1524
1551
  reportedByCustomer: cli.flags.reportedByCustomer,
1552
+ limit: cli.flags.limit,
1525
1553
  json: jsonMode,
1526
1554
  outputFormat: cliContext.outputFormat
1527
1555
  });
@@ -75,7 +75,10 @@ async function runReportsList(options) {
75
75
  searchQuery: options.searchQuery,
76
76
  reporterQuery: options.reporterQuery,
77
77
  reportedByCustomer: options.reportedByCustomer,
78
+ needsCustomerReview: options.needsCustomerReview,
78
79
  excludeAutomation: options.excludeAutomation,
80
+ reporterKind: options.reporterKind,
81
+ reviewerKind: options.reviewerKind,
79
82
  onlyRejected: options.onlyRejected,
80
83
  reviewStatuses,
81
84
  assetTypes: options.assetTypes?.length ? options.assetTypes.map((t) => t.toUpperCase()) : void 0,
@@ -99,7 +102,8 @@ async function runReportsList(options) {
99
102
  cursor: options.cursor,
100
103
  status: options.status,
101
104
  searchQuery: options.searchQuery,
102
- reportedByCustomer: options.reportedByCustomer
105
+ reportedByCustomer: options.reportedByCustomer,
106
+ needsCustomerReview: options.needsCustomerReview
103
107
  }
104
108
  } : void 0
105
109
  },
@@ -29,10 +29,18 @@ async function runReportsSearch(options) {
29
29
  ExitCode.USAGE
30
30
  );
31
31
  }
32
+ if (options.limit !== void 0 && (options.limit < 1 || options.limit > 100)) {
33
+ throw new CliExitError(
34
+ "reports search requires --limit between 1 and 100.",
35
+ ExitCode.USAGE
36
+ );
37
+ }
32
38
  const client = options.apiClient ?? createApiClient();
33
39
  const input = {
34
40
  assetContents: options.assetContents,
35
- reportedByCustomer: options.reportedByCustomer
41
+ slug: options.org,
42
+ reportedByCustomer: options.reportedByCustomer,
43
+ limit: options.limit
36
44
  };
37
45
  const result = await client.searchReports(input);
38
46
  const rows = result.reports.map(toRow);
@@ -43,7 +51,7 @@ async function runReportsSearch(options) {
43
51
  "# Report search results",
44
52
  "",
45
53
  `- Searched: ${options.assetContents.length} asset(s)`,
46
- `- Matched: ${result.reports.length} report(s)`,
54
+ `- Matched: ${result.reports.length} of ${result.totalCount} report(s)`,
47
55
  "",
48
56
  ...rows.map(
49
57
  (row) => `- #${row.id} [${row.status}]${row.reportedByCustomer ? " (customer)" : ""} assets=${row.assets}`
@@ -56,12 +64,17 @@ async function runReportsSearch(options) {
56
64
  return;
57
65
  }
58
66
  console.log(
59
- `Matched ${result.reports.length} report(s) for ${options.assetContents.length} asset(s)`
67
+ `Matched ${result.reports.length} of ${result.totalCount} report(s) for ${options.assetContents.length} asset(s)`
60
68
  );
61
69
  for (const row of rows) {
62
70
  const customerTag = row.reportedByCustomer ? " (customer)" : "";
63
71
  console.log(`#${row.id} [${row.status}]${customerTag} assets=${row.assets}`);
64
72
  }
73
+ if (result.totalCount > result.reports.length) {
74
+ console.log(
75
+ `Showing the ${result.reports.length} newest. Raise --limit (max 100) to see more.`
76
+ );
77
+ }
65
78
  }
66
79
  });
67
80
  }
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": "1.2.0",
5
+ "version": "1.4.0",
6
6
  "license": "UNLICENSED",
7
7
  "homepage": "https://chainpatrol.com/docs/cli",
8
8
  "keywords": [
@@ -33,7 +33,7 @@
33
33
  "lint": "npx oxlint ."
34
34
  },
35
35
  "dependencies": {
36
- "@chainpatrol/sdk": "1.2.0",
36
+ "@chainpatrol/sdk": "1.4.0",
37
37
  "ink": "^7.0.1",
38
38
  "meow": "^14.1.0",
39
39
  "open": "^11.0.0",