@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1

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 (300) hide show
  1. package/NOTICE +2 -2
  2. package/cli/argument-values.mjs +55 -0
  3. package/cli/arguments.mjs +24 -470
  4. package/cli/artifact-validation/case-response.mjs +21 -6
  5. package/cli/artifact-validation/lookup-evidence.mjs +3 -3
  6. package/cli/artifact-validation/signed-review.mjs +59 -13
  7. package/cli/artifact-verify.mjs +2 -0
  8. package/cli/assurance-arguments.mjs +56 -0
  9. package/cli/assurance-command-definitions.mjs +179 -0
  10. package/cli/assurance-command-runner.mjs +1 -1
  11. package/cli/bounded-json.mjs +1 -1
  12. package/cli/bulk-checkpoint.mjs +1 -1
  13. package/cli/bulk-command-runner.mjs +2 -2
  14. package/cli/bulk.mjs +2 -3
  15. package/cli/case-command.mjs +20 -3
  16. package/cli/case-pack.mjs +12 -2
  17. package/cli/ci-report.mjs +6 -3
  18. package/cli/collection-arguments.mjs +123 -0
  19. package/cli/collection-command-definitions.mjs +330 -0
  20. package/cli/collection-preflight.mjs +8 -1
  21. package/cli/command-argument-grammar.mjs +8 -210
  22. package/cli/command-definition.mjs +329 -0
  23. package/cli/command-reference.mjs +55 -1388
  24. package/cli/command-seeds.mjs +18 -0
  25. package/cli/compare.mjs +1 -1
  26. package/cli/config-profile.mjs +1 -1
  27. package/cli/ct-event-intake.mjs +4 -4
  28. package/cli/discover.mjs +2 -2
  29. package/cli/discovery-observation-snapshot.mjs +2 -2
  30. package/cli/discovery-scan-command-runner.mjs +3 -3
  31. package/cli/discovery-scan.mjs +1 -1
  32. package/cli/discovery-snapshot.mjs +2 -2
  33. package/cli/discovery-workflow.mjs +4 -4
  34. package/cli/domain-control-monitor.mjs +6 -6
  35. package/cli/domain-control-observations.mjs +2 -1
  36. package/cli/errors.mjs +5 -6
  37. package/cli/evidence-arguments.mjs +46 -0
  38. package/cli/evidence-command-definitions.mjs +69 -0
  39. package/cli/evidence-command-runner.mjs +2 -3
  40. package/cli/evidence-signing.mjs +3 -2
  41. package/cli/external-observation-mapping.mjs +2 -2
  42. package/cli/formatters/terminal-command-formats.mjs +6 -0
  43. package/cli/formatters/terminal-lookup.mjs +6 -0
  44. package/cli/history-arguments.mjs +39 -0
  45. package/cli/history-command-definitions.mjs +88 -0
  46. package/cli/indicator-set-command.mjs +33 -0
  47. package/cli/inline-command-families.mjs +6 -47
  48. package/cli/inline-command-runner.mjs +4 -31
  49. package/cli/input.mjs +2 -2
  50. package/cli/intake-command.mjs +107 -0
  51. package/cli/interchange-report.mjs +4 -2
  52. package/cli/investigation-artifacts.mjs +1 -1
  53. package/cli/investigation-plan.mjs +32 -154
  54. package/cli/investigation-recipes.mjs +177 -0
  55. package/cli/investigation-run.mjs +78 -12
  56. package/cli/local-mmdb-review.mjs +197 -17
  57. package/cli/lookalike-calibration.mjs +1 -1
  58. package/cli/lookup-brief.mjs +2 -2
  59. package/cli/lookup-command-runner.mjs +18 -35
  60. package/cli/lookup-diff.mjs +10 -9
  61. package/cli/lookup-plan.mjs +10 -14
  62. package/cli/lookup-reconcile.mjs +6 -5
  63. package/cli/lookup-timeline.mjs +7 -6
  64. package/cli/mail-header-review.mjs +24 -247
  65. package/cli/mail-review.mjs +9 -7
  66. package/cli/network-arguments.mjs +51 -0
  67. package/cli/network-command-definitions.mjs +130 -0
  68. package/cli/network-command-runner.mjs +43 -31
  69. package/cli/offline-evidence-review.mjs +39 -6
  70. package/cli/posture.mjs +1 -1
  71. package/cli/registry-fixture-scaffold.mjs +1 -1
  72. package/cli/retained-artifact-diff.mjs +8 -7
  73. package/cli/review-arguments.mjs +131 -0
  74. package/cli/review-command-definitions.mjs +273 -0
  75. package/cli/review-command-runner.mjs +68 -158
  76. package/cli/risk-calibration.mjs +8 -8
  77. package/cli/runner.mjs +20 -32
  78. package/cli/saved-lookup.mjs +2 -2
  79. package/cli/selected-input-worker.mjs +63 -0
  80. package/cli/sharing-review.mjs +5 -64
  81. package/cli/source-reliability.mjs +2 -2
  82. package/cli/support-arguments.mjs +49 -0
  83. package/cli/support-command-definitions.mjs +209 -0
  84. package/cli/support-command-runner.mjs +2 -2
  85. package/cli/workflow-arguments.mjs +86 -0
  86. package/cli/workflow-command-definitions.mjs +78 -0
  87. package/cli/workflow-command-runner.mjs +8 -1
  88. package/docs/cli-reference.md +27 -0
  89. package/docs/cli.md +223 -2
  90. package/docs/contextual-reviews.md +155 -0
  91. package/lib/analyst-taxonomy.mjs +2 -40
  92. package/lib/application-version.mjs +1 -1
  93. package/lib/availability.mjs +13 -789
  94. package/lib/bounded-contract-normalizers.mjs +4 -0
  95. package/lib/bounded-json.mjs +2 -330
  96. package/lib/browser-library-profile.mjs +62 -84
  97. package/lib/candidate-provenance-bounds.mjs +2 -0
  98. package/lib/classify.mjs +1 -1
  99. package/lib/ct-query.mjs +2 -27
  100. package/lib/ct-response-bounds.mjs +2 -9
  101. package/lib/ct-search.mjs +83 -47
  102. package/lib/dmarc-discovery.mjs +62 -0
  103. package/lib/dns-convergence-review.mjs +21 -8
  104. package/lib/dns-delegation-health.mjs +12 -8
  105. package/lib/dns-inheritance-review.mjs +53 -63
  106. package/lib/dns-intelligence.mjs +9 -7
  107. package/lib/dns-query-coverage.mjs +40 -0
  108. package/lib/domain-assurance.mjs +1 -1
  109. package/lib/domain-change-packet.mjs +12 -13
  110. package/lib/domain-change-review.mjs +32 -14
  111. package/lib/domain-control-flight-recorder.mjs +103 -27
  112. package/lib/domain-control-manifest.mjs +25 -17
  113. package/lib/domain-posture-analysis.mjs +22 -5
  114. package/lib/domain-posture.mjs +16 -15
  115. package/lib/domain-registration.mjs +392 -0
  116. package/lib/evidence-export-privacy.mjs +22 -15
  117. package/lib/evidence-export.mjs +3 -3
  118. package/lib/evidence-report.mjs +4 -1
  119. package/lib/external-intelligence-risk.mjs +2 -124
  120. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  121. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  122. package/lib/generated/retire-browser-catalog.mjs +332 -19
  123. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  124. package/lib/html-signals.mjs +7 -2
  125. package/lib/http-evidence-bounds.mjs +2 -8
  126. package/lib/http-intelligence.mjs +13 -1
  127. package/lib/idn-confusable-policy.mjs +6 -4
  128. package/lib/idn-confusables.mjs +1 -1
  129. package/lib/interchange-fidelity-registry.mjs +10 -0
  130. package/lib/lookup-contract-primitives.mjs +1 -1
  131. package/lib/lookup-diagnostics.mjs +1 -23
  132. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  133. package/lib/lookup-progress.mjs +50 -1
  134. package/lib/lookup-response-contract.mjs +69 -40
  135. package/lib/lookup-response.mjs +5 -15
  136. package/lib/lookup-source-progress.mjs +8 -22
  137. package/lib/lookup.mjs +1 -1
  138. package/lib/opportunity-scoring.mjs +2 -107
  139. package/lib/outbound-identity.mjs +1 -1
  140. package/lib/page-fingerprints.mjs +18 -1
  141. package/lib/perceptual-hash-comparison.mjs +2 -34
  142. package/lib/perceptual-hash.mjs +36 -10
  143. package/lib/portable-generator.mjs +2 -29
  144. package/lib/project-metadata.mjs +2 -8
  145. package/lib/rdap-client.mjs +11 -7
  146. package/lib/registrable-domain.mjs +2 -56
  147. package/lib/registrar-standing-contract.mjs +1 -1
  148. package/lib/registrar-standing.mjs +9 -13
  149. package/lib/registry-capabilities.mjs +2 -2
  150. package/lib/registry-capability-catalogue.mjs +27 -19
  151. package/lib/risk-calibration-summary.mjs +2 -352
  152. package/lib/risk-scoring.mjs +2 -345
  153. package/lib/scoring-evidence-quality.mjs +2 -107
  154. package/lib/semantic-version.mjs +2 -54
  155. package/lib/service-binding-dns.mjs +27 -0
  156. package/lib/smtp-transport-review.mjs +48 -19
  157. package/lib/sslbl-certificates.generated.mjs +141 -139
  158. package/lib/static-html-analysis.mjs +1 -1
  159. package/lib/structured-data-identity.mjs +1 -1
  160. package/lib/threat-intelligence-manifests.mjs +1 -1
  161. package/lib/threat-intelligence-runtime.mjs +1 -1
  162. package/lib/threat-intelligence-targets.mjs +1 -1
  163. package/lib/threat-intelligence-types.mjs +2 -75
  164. package/lib/typosquat-generator.mjs +11 -2
  165. package/lib/website-enrichment.mjs +388 -0
  166. package/lib/whois-authority.mjs +43 -5
  167. package/lib/whois-normalization.mjs +3 -1
  168. package/lib/whois-parser.mjs +5 -3
  169. package/package.json +13 -14
  170. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  171. package/packages/analysis/bounded-json.mjs +330 -0
  172. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  173. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  174. package/packages/analysis/ct-query.mjs +27 -0
  175. package/packages/analysis/ct-response-bounds.mjs +9 -0
  176. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  177. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  178. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  179. package/packages/analysis/opportunity-scoring.mjs +107 -0
  180. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  181. package/packages/analysis/portable-generator.mjs +29 -0
  182. package/packages/analysis/project-metadata.mjs +8 -0
  183. package/packages/analysis/registrable-domain.mjs +56 -0
  184. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  185. package/packages/analysis/risk-scoring.mjs +345 -0
  186. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  187. package/packages/analysis/semantic-version.mjs +54 -0
  188. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  189. package/packages/cases/case-evidence-links.mjs +135 -0
  190. package/packages/cases/case-evidence-model.mjs +78 -12
  191. package/packages/cases/case-evidence-quality.mjs +5 -0
  192. package/packages/cases/case-export-input.mjs +7 -5
  193. package/packages/cases/case-incident-context.mjs +52 -0
  194. package/packages/cases/case-markdown.mjs +11 -0
  195. package/packages/cases/case-migration-model.mjs +40 -24
  196. package/packages/cases/case-recheck-model.mjs +90 -32
  197. package/packages/cases/case-record-contracts.mjs +1 -1
  198. package/packages/cases/case-record-core.mjs +1 -1
  199. package/packages/cases/case-record-operations.mjs +40 -112
  200. package/packages/cases/case-record-projection.mjs +8 -1
  201. package/packages/cases/case-record-version-input.mjs +19 -0
  202. package/packages/cases/case-report.mjs +63 -27
  203. package/packages/cases/case-requested-evidence.mjs +146 -0
  204. package/packages/cases/case-response-actions.mjs +103 -1
  205. package/packages/cases/case-response-model.mjs +9 -1
  206. package/packages/cases/case-response-outcomes.mjs +29 -8
  207. package/packages/cases/case-response-packet-render.mjs +13 -9
  208. package/packages/cases/case-response-packet.mjs +25 -11
  209. package/packages/cases/case-response-records.mjs +0 -1
  210. package/packages/cases/case-response-review-inputs.mjs +14 -3
  211. package/packages/cases/case-storage-model.mjs +1 -1
  212. package/packages/cases/case-workflow-metadata.mjs +225 -0
  213. package/packages/cases/case-workflow-migration.mjs +43 -0
  214. package/packages/cases/http-summary.mjs +6 -1
  215. package/packages/comparison/favicon-similarity.mjs +1 -1
  216. package/packages/comparison/page-similarity.mjs +1 -1
  217. package/packages/comparison/relationship-evidence.mjs +21 -9
  218. package/packages/comparison/relationship-provenance.mjs +7 -1
  219. package/packages/contracts/analyst-interchange.mjs +28 -10
  220. package/packages/contracts/case-portability.mjs +118 -25
  221. package/packages/contracts/cli-command-semantics.mjs +2 -0
  222. package/packages/contracts/cli-grammar.mjs +6 -0
  223. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  224. package/packages/contracts/context-review.mjs +24 -0
  225. package/packages/contracts/document-intake.mjs +11 -0
  226. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  227. package/packages/contracts/domain-control-manifest.mjs +1 -1
  228. package/packages/contracts/domain-control-monitor.mjs +13 -2
  229. package/packages/contracts/domain-control-review.mjs +36 -15
  230. package/packages/contracts/har-review.mjs +2 -0
  231. package/packages/contracts/identity-events.mjs +4 -0
  232. package/packages/contracts/investigation-run.mjs +2 -0
  233. package/packages/contracts/ip-address.mjs +10 -0
  234. package/packages/contracts/mail-authentication.mjs +5 -0
  235. package/packages/contracts/message-intake.mjs +31 -0
  236. package/packages/contracts/offline-comparison.mjs +4 -1
  237. package/packages/contracts/passive-mail.mjs +10 -0
  238. package/packages/contracts/web-capture.mjs +2 -2
  239. package/packages/contracts/workspace-portability.mjs +63 -17
  240. package/packages/evidence/collection-quality.mjs +67 -0
  241. package/packages/evidence/dns-query-outcome.mjs +72 -0
  242. package/packages/evidence/email-recipient.mjs +18 -0
  243. package/packages/evidence/lookup-target.mjs +5 -1
  244. package/packages/evidence/observation.mjs +2 -0
  245. package/packages/evidence/png-pixels.mjs +117 -0
  246. package/packages/evidence/public-address-policy.mjs +83 -0
  247. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  248. package/packages/interchange/external-findings-import.mjs +4 -12
  249. package/packages/interchange/managed-indicator-export.mjs +35 -0
  250. package/packages/interchange/managed-indicator-set.mjs +210 -0
  251. package/packages/interchange/misp-indicator-export.mjs +93 -0
  252. package/packages/interchange/sharing-policy.mjs +56 -0
  253. package/packages/interchange/stix-indicator-export.mjs +129 -0
  254. package/packages/interchange/web-capture-import.mjs +22 -15
  255. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  256. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  257. package/packages/investigation/capture-coverage.mjs +57 -0
  258. package/packages/investigation/connector-provenance-review.mjs +100 -0
  259. package/packages/investigation/context-review.mjs +35 -0
  260. package/packages/investigation/document-intake.mjs +81 -0
  261. package/packages/investigation/docx-intake.mjs +133 -0
  262. package/packages/investigation/domain-history-review.mjs +100 -0
  263. package/packages/investigation/domain-transition-review.mjs +47 -0
  264. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  265. package/packages/investigation/har-intake.mjs +68 -0
  266. package/packages/investigation/identity-event-intake.mjs +131 -0
  267. package/packages/investigation/identity-incident-review.mjs +39 -0
  268. package/packages/investigation/incident-sequence-review.mjs +58 -0
  269. package/packages/investigation/intake-report.mjs +19 -0
  270. package/packages/investigation/investigation-manifest.mjs +2 -2
  271. package/packages/investigation/link-intake.mjs +179 -0
  272. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  273. package/packages/investigation/mail-authentication-review.mjs +222 -0
  274. package/packages/investigation/mail-header-identity.mjs +209 -0
  275. package/packages/investigation/message-intake.mjs +195 -0
  276. package/packages/investigation/page-behaviour.mjs +113 -0
  277. package/packages/investigation/pdf-intake.mjs +155 -0
  278. package/packages/investigation/platform-continuity-review.mjs +60 -0
  279. package/packages/investigation/qr-intake.mjs +56 -0
  280. package/packages/investigation/requested-action-hints.mjs +11 -0
  281. package/packages/investigation/selected-input-review.mjs +17 -0
  282. package/packages/investigation/storefront-review.mjs +70 -0
  283. package/packages/workspace/brand-profile-model.mjs +2 -2
  284. package/packages/workspace/bulk-review-model.mjs +28 -26
  285. package/packages/workspace/bulk-session-model.mjs +23 -13
  286. package/packages/workspace/campaign-model.mjs +1 -1
  287. package/packages/workspace/detection-rule-model.mjs +1 -1
  288. package/packages/workspace/hostile-input.mjs +1 -1
  289. package/packages/workspace/investigation-guide.mjs +2 -2
  290. package/packages/workspace/investigation-template-model.mjs +26 -2
  291. package/packages/workspace/page-baseline.mjs +2 -2
  292. package/packages/workspace/relationship-observation-model.mjs +17 -13
  293. package/packages/workspace/shortlist-model.mjs +14 -5
  294. package/packages/workspace/watchlist-history.mjs +28 -12
  295. package/packages/workspace/watchlist-store.mjs +17 -5
  296. package/packages/workspace/website-snapshot-model.mjs +28 -3
  297. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  298. package/packages/workspace/workspace-archive.mjs +5 -4
  299. package/third-party-notices.txt +549 -16
  300. package/lib/hostname.mjs +0 -2
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "../lib/bounded-json.mjs";
2
+ import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "../packages/analysis/bounded-json.mjs";
3
3
  import { canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
4
4
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
5
5
  import { MAX_CASE_CHECKPOINT_FACTS, MAX_CASE_OBJECTIVE_LENGTH, MAX_CASE_STORE_BYTES, MAX_EDITABLE_CASE_INPUT_BYTES, MAX_NOTE_LENGTH, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../packages/contracts/case-portability.mjs";
@@ -8,6 +8,7 @@ import { createCaseIncident, openOrCreateCase, recordCaseConclusion, recordCaseR
8
8
  import { buildCaseExport, serializeCaseStore } from "../packages/cases/case-storage-model.mjs";
9
9
  import { appendCaseEvidencePin } from "../packages/cases/case-response-model.mjs";
10
10
  import { readCaseRecheckAnswerContext } from "../packages/cases/case-recheck-model.mjs";
11
+ import { caseEvidenceLinkIssues, mergeCaseEvidenceLinks } from "../packages/cases/case-evidence-links.mjs";
11
12
  import { dispositionLabel, statusLabel } from "../packages/cases/case-record-decisions.mjs";
12
13
  import { normalizeDomain } from "../packages/cases/case-record-core.mjs";
13
14
  import { safeTerminalValue } from "./formatters/terminal.mjs";
@@ -52,6 +53,10 @@ function pin(input, now) {
52
53
  return raw;
53
54
  }
54
55
  function retainExistingEntries(before, after) {
56
+ if (canonicalArtifactJsonV2(mergeCaseEvidenceLinks(before.evidenceLinks, after.evidenceLinks) ?? [])
57
+ !== canonicalArtifactJsonV2(mergeCaseEvidenceLinks(undefined, after.evidenceLinks) ?? [])) {
58
+ throw new CliUsageError('The operation would remove retained evidence relationships. Nothing was written.');
59
+ }
55
60
  const pairs = [
56
61
  [before.notes, after.notes], [before.evidenceHistory, after.evidenceHistory], [before.evidencePins, after.evidencePins],
57
62
  [before.decisions, after.decisions], [before.actions, after.actions], [before.assertions, after.assertions],
@@ -66,7 +71,7 @@ function retainExistingEntries(before, after) {
66
71
  }
67
72
  }
68
73
  }
69
- function mutate(cases, args, input, note, now) {
74
+ export function applyCliCaseOperation(cases, args, input, note, now) {
70
75
  if (args.operation === 'open') {
71
76
  const title = args.title === null ? undefined : prose(args.title, MAX_CASE_OBJECTIVE_LENGTH, 'Incident title');
72
77
  const result = args.newIncident
@@ -84,6 +89,9 @@ function mutate(cases, args, input, note, now) {
84
89
  else if (args.operation === 'pin') {
85
90
  result = updateCase(cases, current.id, { evidencePin: pin(input, now) }, now);
86
91
  }
92
+ else if (args.operation === 'link' || args.operation === 'withdraw-link') {
93
+ result = updateCase(cases, current.id, args.operation === 'link' ? { evidenceLink: input } : { evidenceLinkWithdrawal: input }, now);
94
+ }
87
95
  else if (args.operation === 'assess') {
88
96
  if (!input)
89
97
  throw new CliUsageError('An assessment requires JSON input.');
@@ -149,6 +157,15 @@ function formatCases(cases, digest) {
149
157
  lines.push(`Pin ${safeTerminalValue(pin.id)} · ${safeTerminalValue(pin.label)}`, safeTerminalValue(pin.value, '—', MAX_RESPONSE_VALUE_LENGTH), ` Source ${safeTerminalValue(pin.source)} · ${safeTerminalValue(pin.observedAt, 'time unavailable')} · ${pin.completeness}`);
150
158
  for (const decision of record.decisions)
151
159
  lines.push(`Assessment ${decision.createdAt}`, safeTerminalValue(decision.summary, '—', MAX_RESPONSE_VALUE_LENGTH), safeTerminalValue(decision.rationale, '—', MAX_RESPONSE_RATIONALE_LENGTH));
160
+ for (const { link, missingPinIds, cyclic } of caseEvidenceLinkIssues(record.evidenceLinks ?? [], record.evidencePins)) {
161
+ lines.push(`Evidence relationship ${safeTerminalValue(link.id)} · analyst declaration`, ` ${safeTerminalValue(link.fromPinId)} ${link.kind.replaceAll('_', ' ')} ${safeTerminalValue(link.toPinId)}`, ` ${safeTerminalValue(link.basis, '—', MAX_RESPONSE_RATIONALE_LENGTH)}`);
162
+ if (link.withdrawal)
163
+ lines.push(` Withdrawn ${link.withdrawal.at}: ${safeTerminalValue(link.withdrawal.reason, '—', MAX_RESPONSE_RATIONALE_LENGTH)}`);
164
+ if (missingPinIds.length)
165
+ lines.push(` Referenced pins not retained: ${missingPinIds.map(id => safeTerminalValue(id)).join(', ')}`);
166
+ if (cyclic)
167
+ lines.push(' Conflicting imported derivation cycle; no order is inferred.');
168
+ }
152
169
  for (const review of record.observedEffects.reviews)
153
170
  lines.push(`Recheck ${safeTerminalValue(review.observedAt, 'time unavailable')} · ${review.state} · ${review.completeness} · ${safeTerminalValue(review.source)}`);
154
171
  lines.push(`Other retained records: ${record.evidenceHistory.length} snapshots, ${record.actions.length} actions, ${record.assertions.length} assertions, ${record.attachments?.length ?? 0} file references.`, '');
@@ -176,7 +193,7 @@ export async function runCaseCommand(args, dependencies, context) {
176
193
  maximumBytes: MAX_EDITABLE_CASE_INPUT_BYTES, limits: boundedJsonLimitsForBytes(MAX_CASE_STORE_BYTES), label: 'Case operation input',
177
194
  }) : null;
178
195
  const note = args.noteSource ? await context.readInput(args.noteSource, MAX_NOTE_LENGTH * 4 + 4, 'Case note') : args.text;
179
- const next = mutate(cases, args, input, note, now);
196
+ const next = applyCliCaseOperation(cases, args, input, note, now);
180
197
  if (Buffer.byteLength(serializeCaseStore(next)) > MAX_CASE_STORE_BYTES)
181
198
  throw new CliUsageError('The updated Case store exceeds its byte budget. No evidence was pruned and nothing was written.');
182
199
  const output = `${JSON.stringify(buildCaseExport(next, now))}\n`;
package/cli/case-pack.mjs CHANGED
@@ -3,8 +3,12 @@ import { Buffer } from 'node:buffer';
3
3
  import { canonicalArtifactJson, canonicalArtifactJsonV2, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
4
4
  import { buildCaseReport, buildCaseReportVerificationProjection } from "../packages/cases/case-report.mjs";
5
5
  import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
6
- import { assertBoundedJsonStructure, scanBoundedJson } from "../lib/bounded-json.mjs";
7
- import { assertCaseBrandProfileIds, CASE_AUDIENCE_SENSITIVE_FIELD_NAMES, caseAudienceExclusions, normalizeDomain, normalizeCaseStore, projectCaseForAudience, safeId, } from "../packages/cases/case-model.mjs";
6
+ import { assertBoundedJsonStructure, scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
7
+ import { assertCaseBrandProfileIds } from "../packages/cases/case-brand-profile-references.mjs";
8
+ import { CASE_AUDIENCE_SENSITIVE_FIELD_NAMES, caseAudienceExclusions, projectCaseForAudience, } from "../packages/cases/case-record-projection.mjs";
9
+ import { normalizeDomain } from "../packages/evidence/domain-name.mjs";
10
+ import { normalizeCaseStore } from "../packages/cases/case-migration-model.mjs";
11
+ import { safeId } from "../packages/cases/case-record-core.mjs";
8
12
  import { CASE_REPORT_SCHEMA, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_INPUT_CASE_VERSIONS, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS as INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS as PACKET_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS as PACKET_KEYS, CLI_CASE_PACK_REPORT_KEYS as REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS as ROOT_KEYS, MAX_CASE_IMPORT_BYTES, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, PUBLIC_CASE_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, } from "../packages/contracts/case-portability.mjs";
9
13
  import { CliUsageError } from "./errors.mjs";
10
14
  import { canonicalCaseExportProjection as canonicalPackProjection } from "../packages/cases/case-export-input.mjs";
@@ -62,6 +66,11 @@ function allowedSensitiveFieldPath(key, path) {
62
66
  return topCasePath(path) || reportCasePath(path);
63
67
  if (key === 'notes')
64
68
  return topCasePath(path) || reportCasePath(path);
69
+ if (key === 'workflowMetadata')
70
+ return topCasePath(path) || analystResponsePath(path);
71
+ if (key === 'incidentTargets' || key === 'investigationContext') {
72
+ return path.at(-1) === 'workflowMetadata' && (topCasePath(path.slice(0, -1)) || analystResponsePath(path.slice(0, -1)));
73
+ }
65
74
  if (key === 'actions' || key === 'assertions' || key === 'manualTrail' || key === 'observedEffects' || key === 'closures' || key === 'branches') {
66
75
  return topCasePath(path) || analystResponsePath(path);
67
76
  }
@@ -473,6 +482,7 @@ export function formatCliCasePack(document) {
473
482
  `Cases ${document.cases.length}`,
474
483
  `Reports ${document.packet.reports.length}`,
475
484
  `Digest ${document.integrity.digestSha256}`,
485
+ 'Included for every audience: domain evidence, Case, pin and decision identifiers, tags, decision summaries and rationale. Review these before sharing.',
476
486
  '',
477
487
  ...document.packet.redactionManifest.excluded.map((item) => `Excluded: ${item}`),
478
488
  '',
package/cli/ci-report.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { WHOISLEUTH_REQUEST_POLICY_URL } from "../lib/project-metadata.mjs";
1
+ import { WHOISLEUTH_REQUEST_POLICY_URL } from "../packages/analysis/project-metadata.mjs";
2
2
  import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA } from "../packages/contracts/domain-control-monitor.mjs";
3
3
  function record(value) {
4
4
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
@@ -45,9 +45,12 @@ function junitCases(document) {
45
45
  return [{ name: 'WHOISleuth result', failure: 'Unsupported document for JUnit conversion.' }];
46
46
  }
47
47
  export function formatCliJunit(document) {
48
- const cases = junitCases(record(document));
48
+ const root = record(document);
49
+ const cases = junitCases(root);
49
50
  const failures = cases.filter((item) => item.failure).length;
50
- return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="WHOISleuth" tests="${cases.length}" failures="${failures}">\n${cases.map((item) => ` <testcase classname="whoisleuth" name="${xml(item.name)}">${item.failure ? `<failure message="${xml(item.failure)}" />` : ''}</testcase>`).join('\n')}\n</testsuite>\n`;
51
+ const reminders = root.schema === CLI_DOMAIN_CONTROL_MONITOR_SCHEMA
52
+ ? `\n <system-out>${xml(`Renewal reviews due: ${Number(record(record(root.review).counts).due ?? 0)}`)}</system-out>` : '';
53
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="WHOISleuth" tests="${cases.length}" failures="${failures}">\n${cases.map((item) => ` <testcase classname="whoisleuth" name="${xml(item.name)}">${item.failure ? `<failure message="${xml(item.failure)}" />` : ''}</testcase>`).join('\n')}${reminders}\n</testsuite>\n`;
51
54
  }
52
55
  export function buildPostureSarif(document) {
53
56
  const root = record(document);
@@ -0,0 +1,123 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import {} from "./fail-policy.mjs";
3
+ import { commandDefaultNumber, commandDefaultText } from "./command-reference.mjs";
4
+ import { COLLECTION_COMMAND_DEFINITIONS } from "./collection-command-definitions.mjs";
5
+ import { terminalOptions, normalizedLabel, failPolicies, parseOutput, } from "./argument-values.mjs";
6
+ function parseLookupArguments(parsed) {
7
+ const output = parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS.lookup);
8
+ const selectedFailPolicies = failPolicies(parsed, 'lookup');
9
+ return {
10
+ action: 'lookup',
11
+ query: parsed.positionalValue('target'),
12
+ output,
13
+ deep: parsed.hasOption('--deep'),
14
+ detail: parsed.hasOption('--summary')
15
+ ? 'summary'
16
+ : parsed.hasOption('--verbose')
17
+ ? 'verbose'
18
+ : 'standard',
19
+ strictExit: parsed.hasOption('--strict-exit'),
20
+ events: parsed.hasOption('--events'),
21
+ plan: parsed.hasOption('--plan'),
22
+ includeAttribution: !parsed.hasOption('--no-attribution'),
23
+ observerLabel: normalizedLabel(parsed, '--observer', 80),
24
+ vantageLabel: normalizedLabel(parsed, '--vantage', 80),
25
+ ...terminalOptions(parsed),
26
+ ...(parsed.hasOption('--browse') ? { browse: true } : {}),
27
+ ...(parsed.hasOption('--exact-url') ? { exactUrl: true } : {}),
28
+ ...(parsed.optionValue('--save-lookup')
29
+ ? { saveLookup: parsed.optionValue('--save-lookup') }
30
+ : {}),
31
+ ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
32
+ };
33
+ }
34
+ function parseBulkArguments(parsed) {
35
+ const deep = parsed.hasOption('--deep');
36
+ const selectedFailPolicies = failPolicies(parsed, 'bulk');
37
+ const output = parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS.bulk);
38
+ const filter = parsed.hasOption('--registered-only')
39
+ ? 'registered'
40
+ : parsed.hasOption('--inconclusive-only')
41
+ ? 'inconclusive'
42
+ : parsed.hasOption('--errors-only')
43
+ ? 'errors'
44
+ : 'all';
45
+ return {
46
+ action: 'bulk',
47
+ source: parsed.positionalValue('source'),
48
+ output,
49
+ deep,
50
+ concurrency: parsed.integerOption('--concurrency') ?? commandDefaultNumber('bulk', '--concurrency', deep),
51
+ checkpoint: parsed.optionValue('--checkpoint'),
52
+ resume: parsed.hasOption('--resume'),
53
+ events: parsed.hasOption('--events'),
54
+ plan: parsed.hasOption('--plan'),
55
+ filter,
56
+ ...terminalOptions(parsed),
57
+ ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
58
+ };
59
+ }
60
+ function parseDiscoverArguments(parsed) {
61
+ return {
62
+ action: 'discover',
63
+ seed: parsed.positionalValue('subject'),
64
+ output: parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS.discover),
65
+ ...discoveryValues(parsed),
66
+ snapshotSource: parsed.optionValue('--snapshot'),
67
+ ...terminalOptions(parsed),
68
+ };
69
+ }
70
+ function discoveryValues(parsed) {
71
+ const familyText = parsed.optionValue('--families');
72
+ return {
73
+ preset: (familyText
74
+ ? 'custom'
75
+ : (parsed.optionValue('--preset') ?? commandDefaultText('discover', '--preset'))),
76
+ keyboardLayout: (parsed.optionValue('--keyboard') ??
77
+ commandDefaultText('discover', '--keyboard')),
78
+ tldText: parsed.optionValue('--tlds'),
79
+ dictionarySource: parsed.optionValue('--dictionary'),
80
+ familyText,
81
+ };
82
+ }
83
+ function parseDiscoverScanArguments(parsed) {
84
+ const deep = parsed.hasOption('--deep');
85
+ const selectedFailPolicies = failPolicies(parsed, 'discover-scan');
86
+ const filter = parsed.hasOption('--registered-only')
87
+ ? 'registered'
88
+ : parsed.hasOption('--inconclusive-only')
89
+ ? 'inconclusive'
90
+ : parsed.hasOption('--acquisition-only')
91
+ ? 'acquisition'
92
+ : parsed.hasOption('--suppressed-only')
93
+ ? 'suppressed'
94
+ : 'all';
95
+ return {
96
+ action: 'discover-scan',
97
+ seed: parsed.positionalValue('subject'),
98
+ output: parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS['discover-scan']),
99
+ ...discoveryValues(parsed),
100
+ deep,
101
+ scanLimit: parsed.integerOption('--scan-limit') ??
102
+ commandDefaultNumber('discover-scan', '--scan-limit', deep),
103
+ chunkSize: parsed.integerOption('--chunk-size') ?? commandDefaultNumber('discover-scan', '--chunk-size'),
104
+ concurrency: parsed.integerOption('--concurrency') ??
105
+ commandDefaultNumber('discover-scan', '--concurrency', deep),
106
+ checkpoint: parsed.optionValue('--checkpoint'),
107
+ resume: parsed.hasOption('--resume'),
108
+ resolverText: parsed.optionValue('--resolver'),
109
+ observationSnapshot: parsed.optionValue('--observation-snapshot'),
110
+ allowlistSource: parsed.optionValue('--allowlist'),
111
+ filter,
112
+ events: parsed.hasOption('--events'),
113
+ plan: parsed.hasOption('--plan'),
114
+ ...terminalOptions(parsed),
115
+ ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
116
+ };
117
+ }
118
+ export const COLLECTION_ARGUMENT_PARSERS = Object.freeze({
119
+ lookup: parseLookupArguments,
120
+ bulk: parseBulkArguments,
121
+ discover: parseDiscoverArguments,
122
+ 'discover-scan': parseDiscoverScanArguments,
123
+ });
@@ -0,0 +1,330 @@
1
+ import { positional, OPTIONAL_FILE_POSITIONAL, OPTIONAL_TEXT_POSITIONAL, constraint, commandSeed, } from "./command-definition.mjs";
2
+ import { bulkQueryLimit, bulkConcurrencyLimit } from "../lib/bulk-limits.mjs";
3
+ const COLLECTION_CEILINGS = `${bulkQueryLimit('fast')} fast or ${bulkQueryLimit('deep')} deep`;
4
+ const CONCURRENCY_CEILINGS = `${bulkConcurrencyLimit('fast')} fast or ${bulkConcurrencyLimit('deep')} deep`;
5
+ export const COLLECTION_COMMAND_DEFINITIONS = Object.freeze({
6
+ lookup: commandSeed({
7
+ reference: {
8
+ description: 'Collect registration evidence for one domain, IP, or ASN.',
9
+ example: 'whoisleuth lookup example.test --deep --browse',
10
+ boundary: 'Fast is the default. An ICANN-recognised public domain, reserved documentation domain, IP, or ASN may occupy command position as shorthand; it delegates to this same parser and URL-like input requires the explicit lookup command. Deep mode adds bounded WHOIS, DNS, HTTP, TLS, technology, posture, and network context where applicable. --deep --exact-url explicitly sends the input URL path and query to the website, without its fragment; ordinary URL input sends only the hostname. --plan remains offline and omits the selected URL. Retained paths and page-derived text require review before sharing. A full Deep homepage observation can derive fixed publication and delivery/cache summaries from the same response without retaining raw metadata values or making another request. --browse opens before collection, shows aggregate Fast progress or independently settled planned Deep sources, and then navigates allowlisted retained fields in the completed document. Press ? for help and / to search rendered panel text only. Closing during collection cancels without a partial document. --save-lookup writes the exact completed private JSON only after a normal browser close; it can contain normalised evidence omitted from panels and refuses an existing path.',
11
+ },
12
+ collection: {
13
+ scope: 'Accepts one target. Fast is the default; deep collection must be selected explicitly.',
14
+ },
15
+ summary: 'Collect one domain, IP, or ASN',
16
+ options: [
17
+ '--json',
18
+ '--junit',
19
+ '--markdown',
20
+ '--html',
21
+ '--no-attribution',
22
+ '--fast',
23
+ '--deep',
24
+ '--exact-url',
25
+ '--observer',
26
+ '--vantage',
27
+ '--plan',
28
+ '--summary',
29
+ '--verbose',
30
+ '--browse',
31
+ '--save-lookup',
32
+ '--strict-exit',
33
+ '--fail-on',
34
+ '--events',
35
+ '--quiet',
36
+ '--no-color',
37
+ ],
38
+ positionals: Object.freeze([
39
+ positional('target', 'text', 0, 1, [], 'argv_or_stdin', ['--browse']),
40
+ ]),
41
+ constraints: Object.freeze([
42
+ constraint({
43
+ kind: 'mutually_exclusive',
44
+ options: ['--json', '--junit', '--markdown', '--html'],
45
+ }),
46
+ constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
47
+ constraint({ kind: 'mutually_exclusive', options: ['--summary', '--verbose'] }),
48
+ constraint({ kind: 'requires_all', option: '--save-lookup', requiredOptions: ['--browse'] }),
49
+ constraint({ kind: 'requires_all', option: '--exact-url', requiredOptions: ['--deep'] }),
50
+ constraint({
51
+ kind: 'excludes_all',
52
+ option: '--summary',
53
+ excludedOptions: ['--json', '--junit', '--markdown', '--html'],
54
+ }),
55
+ constraint({
56
+ kind: 'excludes_all',
57
+ option: '--verbose',
58
+ excludedOptions: ['--json', '--junit', '--markdown', '--html'],
59
+ }),
60
+ constraint({
61
+ kind: 'excludes_all',
62
+ option: '--browse',
63
+ excludedOptions: [
64
+ '--json',
65
+ '--junit',
66
+ '--markdown',
67
+ '--html',
68
+ '--summary',
69
+ '--verbose',
70
+ '--events',
71
+ '--plan',
72
+ '--quiet',
73
+ ],
74
+ }),
75
+ constraint({
76
+ kind: 'requires_any',
77
+ option: '--no-attribution',
78
+ requiredOptions: ['--markdown', '--html'],
79
+ }),
80
+ constraint({
81
+ kind: 'excludes_all',
82
+ option: '--plan',
83
+ excludedOptions: [
84
+ '--junit',
85
+ '--markdown',
86
+ '--html',
87
+ '--summary',
88
+ '--verbose',
89
+ '--strict-exit',
90
+ '--events',
91
+ '--quiet',
92
+ '--fail-on',
93
+ ],
94
+ }),
95
+ ]),
96
+ handlerOwner: 'lookup',
97
+ networkEffect: 'conditional_network',
98
+ common: true,
99
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup', 'whoisleuth.cli.lookup-plan']),
100
+ primaryArtefacts: Object.freeze(['Source-qualified Lookup', 'Lookup request plan']),
101
+ planSupport: true,
102
+ additionalOutputFormats: Object.freeze([]),
103
+ bootstrapProfile: 'allowed',
104
+ }),
105
+ bulk: commandSeed({
106
+ reference: {
107
+ description: 'Triage newline-delimited domains, IPs, or ASNs with bounded concurrency.',
108
+ example: 'cat domains.txt | whoisleuth bulk --jsonl',
109
+ boundary: 'Fast and deep jobs use separate concurrency ceilings. Filters affect output only; collection failures and inconclusive authority states remain explicit in JSON, JSONL, and CSV. --csv-with-metadata adds source versions, separate observation and report times, collection origin and diagnostic states; --csv retains the compact columns.',
110
+ },
111
+ collection: {
112
+ scope: `Accepts at most ${COLLECTION_CEILINGS} targets, with concurrency capped at ${CONCURRENCY_CEILINGS}.`,
113
+ },
114
+ summary: 'Run bounded multi-target collection',
115
+ options: [
116
+ '--json',
117
+ '--jsonl',
118
+ '--junit',
119
+ '--csv',
120
+ '--csv-with-metadata',
121
+ '--domains',
122
+ '--queries',
123
+ '--registered-only',
124
+ '--inconclusive-only',
125
+ '--errors-only',
126
+ '--fast',
127
+ '--deep',
128
+ '--concurrency',
129
+ '--checkpoint',
130
+ '--resume',
131
+ '--events',
132
+ '--plan',
133
+ '--fail-on',
134
+ '--quiet',
135
+ '--no-color',
136
+ ],
137
+ positionals: OPTIONAL_FILE_POSITIONAL,
138
+ constraints: Object.freeze([
139
+ constraint({
140
+ kind: 'mutually_exclusive',
141
+ options: [
142
+ '--json',
143
+ '--jsonl',
144
+ '--junit',
145
+ '--csv',
146
+ '--csv-with-metadata',
147
+ '--domains',
148
+ '--queries',
149
+ ],
150
+ }),
151
+ constraint({
152
+ kind: 'mutually_exclusive',
153
+ options: ['--registered-only', '--inconclusive-only', '--errors-only'],
154
+ }),
155
+ constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
156
+ constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
157
+ constraint({
158
+ kind: 'excludes_all',
159
+ option: '--plan',
160
+ excludedOptions: [
161
+ '--jsonl',
162
+ '--junit',
163
+ '--csv',
164
+ '--csv-with-metadata',
165
+ '--domains',
166
+ '--queries',
167
+ '--events',
168
+ '--checkpoint',
169
+ '--resume',
170
+ '--quiet',
171
+ '--fail-on',
172
+ ],
173
+ }),
174
+ ]),
175
+ handlerOwner: 'bulk',
176
+ networkEffect: 'conditional_network',
177
+ common: true,
178
+ schemaIdentifiers: Object.freeze([
179
+ 'whoisleuth.cli.bulk',
180
+ 'whoisleuth.cli.bulk.item',
181
+ 'whoisleuth.cli.bulk-checkpoint',
182
+ ]),
183
+ primaryArtefacts: Object.freeze(['Bulk result', 'Bulk checkpoint']),
184
+ planSupport: true,
185
+ additionalOutputFormats: Object.freeze([]),
186
+ bootstrapProfile: 'allowed',
187
+ }),
188
+ discover: commandSeed({
189
+ reference: {
190
+ description: 'Generate bounded lookalike-domain candidates from a brand or registrable domain, including multi-part public suffixes.',
191
+ example: 'whoisleuth discover example.test --preset common --jsonl',
192
+ boundary: 'Generation and optional local snapshot comparison are offline. Candidates are leads only and are not resolved, registered, or classified as malicious.',
193
+ },
194
+ collection: {
195
+ scope: 'Generates a bounded candidate set from local rules, dictionaries, and optional saved snapshots.',
196
+ },
197
+ summary: 'Generate lookalike candidates offline',
198
+ options: [
199
+ '--tlds',
200
+ '--preset',
201
+ '--families',
202
+ '--keyboard',
203
+ '--dictionary',
204
+ '--snapshot',
205
+ '--json',
206
+ '--jsonl',
207
+ '--domains',
208
+ '--quiet',
209
+ '--no-color',
210
+ ],
211
+ positionals: OPTIONAL_TEXT_POSITIONAL,
212
+ constraints: Object.freeze([
213
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--domains'] }),
214
+ constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
215
+ constraint({
216
+ kind: 'value_excludes',
217
+ option: '--preset',
218
+ value: 'common',
219
+ excludedOptions: ['--dictionary'],
220
+ }),
221
+ ]),
222
+ handlerOwner: 'discovery',
223
+ networkEffect: 'offline',
224
+ common: true,
225
+ schemaIdentifiers: Object.freeze([
226
+ 'whoisleuth.cli.discover',
227
+ 'whoisleuth.cli.discover.item',
228
+ 'whoisleuth.cli.discovery-snapshot',
229
+ ]),
230
+ primaryArtefacts: Object.freeze(['Candidate set', 'Discovery snapshot']),
231
+ planSupport: false,
232
+ additionalOutputFormats: Object.freeze([]),
233
+ bootstrapProfile: 'allowed',
234
+ }),
235
+ 'discover-scan': commandSeed({
236
+ reference: {
237
+ description: 'Generate a bounded candidate set, collect a selected subset, and produce a supervised review queue.',
238
+ example: 'whoisleuth discover-scan example.test --scan-limit 50 --checkpoint scan.json --json',
239
+ boundary: `This command performs network collection. Fast compact lookup is the default; deep mode is capped at ${bulkQueryLimit('deep')} candidates. Allowlisting changes review priority only and shared infrastructure remains a lead, not attribution. --csv-with-metadata adds source versions, separate observation and report times, collection origin and diagnostic states; --csv retains the compact columns.`,
240
+ },
241
+ collection: {
242
+ scope: `Scans at most ${COLLECTION_CEILINGS} candidates, with concurrency capped at ${CONCURRENCY_CEILINGS}.`,
243
+ },
244
+ summary: 'Collect a supervised candidate review queue',
245
+ options: [
246
+ '--tlds',
247
+ '--preset',
248
+ '--families',
249
+ '--keyboard',
250
+ '--dictionary',
251
+ '--fast',
252
+ '--deep',
253
+ '--scan-limit',
254
+ '--chunk-size',
255
+ '--concurrency',
256
+ '--resolver',
257
+ '--allowlist',
258
+ '--checkpoint',
259
+ '--resume',
260
+ '--observation-snapshot',
261
+ '--registered-only',
262
+ '--inconclusive-only',
263
+ '--acquisition-only',
264
+ '--suppressed-only',
265
+ '--events',
266
+ '--plan',
267
+ '--fail-on',
268
+ '--json',
269
+ '--jsonl',
270
+ '--csv',
271
+ '--csv-with-metadata',
272
+ '--domains',
273
+ '--quiet',
274
+ '--no-color',
275
+ ],
276
+ positionals: OPTIONAL_TEXT_POSITIONAL,
277
+ constraints: Object.freeze([
278
+ constraint({
279
+ kind: 'mutually_exclusive',
280
+ options: ['--json', '--jsonl', '--csv', '--csv-with-metadata', '--domains'],
281
+ }),
282
+ constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
283
+ constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
284
+ constraint({
285
+ kind: 'mutually_exclusive',
286
+ options: [
287
+ '--registered-only',
288
+ '--inconclusive-only',
289
+ '--acquisition-only',
290
+ '--suppressed-only',
291
+ ],
292
+ }),
293
+ constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
294
+ constraint({
295
+ kind: 'value_excludes',
296
+ option: '--preset',
297
+ value: 'common',
298
+ excludedOptions: ['--dictionary'],
299
+ }),
300
+ constraint({
301
+ kind: 'excludes_all',
302
+ option: '--plan',
303
+ excludedOptions: [
304
+ '--jsonl',
305
+ '--csv',
306
+ '--csv-with-metadata',
307
+ '--domains',
308
+ '--events',
309
+ '--checkpoint',
310
+ '--resume',
311
+ '--observation-snapshot',
312
+ '--quiet',
313
+ '--fail-on',
314
+ ],
315
+ }),
316
+ ]),
317
+ handlerOwner: 'discovery_scan',
318
+ networkEffect: 'conditional_network',
319
+ common: true,
320
+ schemaIdentifiers: Object.freeze([
321
+ 'whoisleuth.cli.discovery-scan',
322
+ 'whoisleuth.cli.discovery-scan.item',
323
+ 'whoisleuth.cli.discovery-observation-snapshot',
324
+ ]),
325
+ primaryArtefacts: Object.freeze(['Reviewed candidate queue', 'Observation snapshot']),
326
+ planSupport: true,
327
+ additionalOutputFormats: Object.freeze([]),
328
+ bootstrapProfile: 'allowed',
329
+ }),
330
+ });
@@ -1,3 +1,4 @@
1
+ import { bulkQueryLimit } from "../lib/bulk-limits.mjs";
1
2
  export const CLI_COLLECTION_PREFLIGHT_SCHEMA = 'whoisleuth.cli.collection-preflight';
2
3
  export const CLI_COLLECTION_PREFLIGHT_VERSION = 1;
3
4
  export function buildCollectionPreflight(options) {
@@ -12,7 +13,7 @@ export function buildCollectionPreflight(options) {
12
13
  networkRequestsMade: false,
13
14
  scope: Object.freeze({
14
15
  selectedTargets: options.targetCount,
15
- commandTargetLimit: options.targetLimit,
16
+ commandTargetLimit: bulkQueryLimit(options.deep ? 'deep' : 'fast'),
16
17
  concurrency: options.concurrency,
17
18
  sourceFamilies: Object.freeze(sourceFamilies),
18
19
  exactRequestCountKnown: false,
@@ -53,6 +54,12 @@ export function formatCollectionPreflight(document) {
53
54
  ...document.scope.sourceFamilies.map((source) => ` ${source}`),
54
55
  '',
55
56
  ...document.disclosure.map((item) => `Disclosure: ${item}`),
57
+ '',
58
+ `Output format: ${document.persistence.output}`,
59
+ `Checkpoint: ${document.persistence.checkpoint ? 'enabled' : 'not written'}. ${document.persistence.note}`,
60
+ ...(document.review.allowlistAffectsPriorityOnly ? ['Allowlist: affects review priority only; it does not exclude targets from collection.'] : []),
61
+ 'Enforcement: none. This plan does not report, block or remove a target.',
62
+ '',
56
63
  ...document.limitations.map((item) => `Limitation: ${item}`),
57
64
  '',
58
65
  ].join('\n');