@slicedearth/whoisleuth-cli 2.3.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 (418) hide show
  1. package/NOTICE +8 -2
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/argument-values.mjs +55 -0
  7. package/cli/arguments.mjs +24 -408
  8. package/cli/artifact-validation/case-response.mjs +65 -44
  9. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  10. package/cli/artifact-validation/lookup-evidence.mjs +15 -9
  11. package/cli/artifact-validation/signed-review.mjs +136 -68
  12. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  13. package/cli/artifact-verify.mjs +92 -24
  14. package/cli/assurance-arguments.mjs +56 -0
  15. package/cli/assurance-command-definitions.mjs +179 -0
  16. package/cli/assurance-command-runner.mjs +40 -5
  17. package/cli/bagit-folder.mjs +57 -0
  18. package/cli/bagit-review.mjs +23 -0
  19. package/cli/bounded-json.mjs +1 -1
  20. package/cli/bulk-checkpoint.mjs +1 -1
  21. package/cli/bulk-command-runner.mjs +4 -2
  22. package/cli/bulk-output.mjs +29 -3
  23. package/cli/bulk.mjs +2 -3
  24. package/cli/case-command.mjs +212 -0
  25. package/cli/case-pack.mjs +50 -32
  26. package/cli/ci-report.mjs +6 -3
  27. package/cli/collection-arguments.mjs +123 -0
  28. package/cli/collection-command-definitions.mjs +330 -0
  29. package/cli/collection-preflight.mjs +8 -1
  30. package/cli/command-argument-grammar.mjs +8 -206
  31. package/cli/command-definition.mjs +329 -0
  32. package/cli/command-reference.mjs +94 -1357
  33. package/cli/command-seeds.mjs +18 -0
  34. package/cli/compare.mjs +1 -1
  35. package/cli/completion.mjs +298 -79
  36. package/cli/config-profile.mjs +8 -6
  37. package/cli/ct-event-intake.mjs +16 -16
  38. package/cli/discover.mjs +5 -4
  39. package/cli/discovery-observation-snapshot.mjs +46 -18
  40. package/cli/discovery-scan-command-runner.mjs +5 -3
  41. package/cli/discovery-scan.mjs +7 -4
  42. package/cli/discovery-snapshot.mjs +2 -2
  43. package/cli/discovery-workflow.mjs +5 -5
  44. package/cli/domain-control-monitor.mjs +50 -20
  45. package/cli/domain-control-observations.mjs +151 -103
  46. package/cli/errors.mjs +37 -6
  47. package/cli/evidence-arguments.mjs +46 -0
  48. package/cli/evidence-command-definitions.mjs +69 -0
  49. package/cli/evidence-command-runner.mjs +18 -8
  50. package/cli/evidence-signing.mjs +5 -4
  51. package/cli/external-observation-mapping.mjs +9 -10
  52. package/cli/formatters/terminal-command-formats.mjs +6 -0
  53. package/cli/formatters/terminal-lookup.mjs +24 -46
  54. package/cli/formatters/terminal-shared.mjs +5 -3
  55. package/cli/formatters/terminal-technology.mjs +114 -0
  56. package/cli/history-arguments.mjs +39 -0
  57. package/cli/history-command-definitions.mjs +88 -0
  58. package/cli/indicator-set-command.mjs +33 -0
  59. package/cli/inline-command-families.mjs +6 -46
  60. package/cli/inline-command-runner.mjs +4 -31
  61. package/cli/input.mjs +2 -2
  62. package/cli/intake-command.mjs +107 -0
  63. package/cli/interactive-launcher.mjs +2 -160
  64. package/cli/interchange-report.mjs +8 -10
  65. package/cli/investigation-artifacts.mjs +176 -0
  66. package/cli/investigation-checkpoint.mjs +10 -0
  67. package/cli/investigation-folder.mjs +114 -0
  68. package/cli/investigation-manifest.mjs +1 -112
  69. package/cli/investigation-package-review.mjs +103 -0
  70. package/cli/investigation-plan.mjs +38 -147
  71. package/cli/investigation-recipes.mjs +177 -0
  72. package/cli/investigation-run.mjs +256 -35
  73. package/cli/local-document-checkpoint.mjs +170 -0
  74. package/cli/local-mmdb-review.mjs +197 -17
  75. package/cli/lookalike-calibration.mjs +1 -1
  76. package/cli/lookup-brief.mjs +2 -2
  77. package/cli/lookup-browser.mjs +5 -12
  78. package/cli/lookup-command-runner.mjs +33 -39
  79. package/cli/lookup-diff.mjs +47 -12
  80. package/cli/lookup-plan.mjs +16 -14
  81. package/cli/lookup-reconcile.mjs +15 -12
  82. package/cli/lookup-timeline.mjs +7 -6
  83. package/cli/mail-header-review.mjs +24 -247
  84. package/cli/mail-review.mjs +75 -21
  85. package/cli/manual.mjs +1 -0
  86. package/cli/network-arguments.mjs +51 -0
  87. package/cli/network-command-definitions.mjs +130 -0
  88. package/cli/network-command-runner.mjs +45 -31
  89. package/cli/offline-evidence-review.mjs +39 -6
  90. package/cli/output-file.mjs +31 -5
  91. package/cli/posture.mjs +1 -1
  92. package/cli/registry-doctor.mjs +3 -1
  93. package/cli/registry-fixture-scaffold.mjs +1 -1
  94. package/cli/registry-support.mjs +1 -1
  95. package/cli/retained-artifact-diff.mjs +10 -9
  96. package/cli/review-arguments.mjs +131 -0
  97. package/cli/review-command-definitions.mjs +273 -0
  98. package/cli/review-command-runner.mjs +104 -149
  99. package/cli/risk-calibration.mjs +8 -8
  100. package/cli/runner.mjs +74 -37
  101. package/cli/saved-lookup.mjs +2 -2
  102. package/cli/selected-input-worker.mjs +63 -0
  103. package/cli/sharing-review.mjs +8 -80
  104. package/cli/signer-trust.mjs +104 -0
  105. package/cli/source-reliability.mjs +75 -84
  106. package/cli/support-arguments.mjs +49 -0
  107. package/cli/support-command-definitions.mjs +209 -0
  108. package/cli/support-command-runner.mjs +2 -2
  109. package/cli/terminal-input.mjs +168 -0
  110. package/cli/workflow-arguments.mjs +86 -0
  111. package/cli/workflow-command-definitions.mjs +78 -0
  112. package/cli/workflow-command-runner.mjs +75 -13
  113. package/docs/cli-reference.md +103 -30
  114. package/docs/cli.md +510 -25
  115. package/docs/contextual-reviews.md +155 -0
  116. package/lib/abort.mjs +4 -1
  117. package/lib/analyst-taxonomy.mjs +2 -40
  118. package/lib/application-version.mjs +1 -1
  119. package/lib/availability.mjs +27 -754
  120. package/lib/bounded-contract-normalizers.mjs +4 -0
  121. package/lib/bounded-file.mjs +3 -1
  122. package/lib/bounded-html-document.mjs +125 -0
  123. package/lib/bounded-json.mjs +2 -290
  124. package/lib/browser-library-profile.mjs +99 -130
  125. package/lib/browser-library-worker.mjs +112 -0
  126. package/lib/candidate-provenance-bounds.mjs +2 -0
  127. package/lib/classify.mjs +1 -1
  128. package/lib/client-behavior-profile.mjs +3 -3
  129. package/lib/ct-query.mjs +2 -27
  130. package/lib/ct-response-bounds.mjs +2 -9
  131. package/lib/ct-search.mjs +89 -46
  132. package/lib/dmarc-discovery.mjs +62 -0
  133. package/lib/dns-convergence-review.mjs +21 -8
  134. package/lib/dns-delegation-health.mjs +36 -22
  135. package/lib/dns-inheritance-review.mjs +207 -0
  136. package/lib/dns-intelligence.mjs +44 -29
  137. package/lib/dns-mx.mjs +1 -1
  138. package/lib/dns-query-coverage.mjs +40 -0
  139. package/lib/dnssec-evidence-validation.mjs +11 -3
  140. package/lib/domain-assurance.mjs +1 -1
  141. package/lib/domain-change-packet.mjs +18 -14
  142. package/lib/domain-change-review.mjs +40 -16
  143. package/lib/domain-control-flight-recorder.mjs +301 -124
  144. package/lib/domain-control-manifest.mjs +126 -58
  145. package/lib/domain-posture-analysis.mjs +41 -11
  146. package/lib/domain-posture-parsers.mjs +88 -6
  147. package/lib/domain-posture.mjs +120 -32
  148. package/lib/domain-registration.mjs +392 -0
  149. package/lib/evidence-export-privacy.mjs +568 -0
  150. package/lib/evidence-export.mjs +12 -593
  151. package/lib/evidence-report.mjs +7 -2
  152. package/lib/external-intelligence-risk.mjs +2 -124
  153. package/lib/favicon.mjs +117 -41
  154. package/lib/generated/cisa-kev-catalog.mjs +50 -4
  155. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  156. package/lib/generated/retire-browser-catalog.mjs +2742 -1034
  157. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  158. package/lib/html-signals.mjs +87 -85
  159. package/lib/http-evidence-bounds.mjs +2 -7
  160. package/lib/http-intelligence.mjs +52 -27
  161. package/lib/idn-confusable-policy.mjs +6 -4
  162. package/lib/idn-confusables.mjs +1 -1
  163. package/lib/interchange-fidelity-registry.mjs +10 -0
  164. package/lib/lookup-cache.mjs +34 -11
  165. package/lib/lookup-child-profile-contract.mjs +24 -15
  166. package/lib/lookup-contract-primitives.mjs +1 -1
  167. package/lib/lookup-diagnostics.mjs +2 -1
  168. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  169. package/lib/lookup-progress.mjs +79 -13
  170. package/lib/lookup-response-contract.mjs +1077 -0
  171. package/lib/lookup-response.mjs +11 -24
  172. package/lib/lookup-source-progress.mjs +24 -35
  173. package/lib/lookup.mjs +49 -28
  174. package/lib/observed-network-context.mjs +43 -27
  175. package/lib/opportunity-scoring.mjs +2 -107
  176. package/lib/outbound-identity.mjs +1 -1
  177. package/lib/outbound-request-bounds.mjs +4 -1
  178. package/lib/page-fingerprints.mjs +87 -134
  179. package/lib/page-role-profile.mjs +12 -8
  180. package/lib/perceptual-hash-comparison.mjs +2 -34
  181. package/lib/perceptual-hash.mjs +79 -24
  182. package/lib/portable-generator.mjs +2 -29
  183. package/lib/project-metadata.mjs +2 -8
  184. package/lib/rdap-bootstrap.mjs +72 -22
  185. package/lib/rdap-client.mjs +26 -14
  186. package/lib/rdap-normalization.mjs +33 -18
  187. package/lib/rdap-search-workbench.mjs +1 -1
  188. package/lib/rdap-transport.mjs +47 -20
  189. package/lib/rdap-types.mjs +2 -1
  190. package/lib/rdap.mjs +18 -3
  191. package/lib/registrable-domain.mjs +2 -36
  192. package/lib/registrar-standing-contract.mjs +2 -1
  193. package/lib/registrar-standing.mjs +9 -13
  194. package/lib/registry-capabilities.mjs +7 -3
  195. package/lib/registry-capability-catalogue.mjs +29 -20
  196. package/lib/registry-dates.mjs +2 -190
  197. package/lib/registry-insights.mjs +56 -18
  198. package/lib/response-policy.mjs +86 -48
  199. package/lib/risk-calibration-summary.mjs +2 -338
  200. package/lib/risk-scoring.mjs +2 -345
  201. package/lib/rpki-evidence.mjs +8 -5
  202. package/lib/safe-fetch.mjs +54 -47
  203. package/lib/scoring-evidence-quality.mjs +2 -107
  204. package/lib/security-txt.mjs +10 -7
  205. package/lib/semantic-version.mjs +2 -54
  206. package/lib/service-binding-dns.mjs +35 -0
  207. package/lib/smtp-transport-review.mjs +75 -34
  208. package/lib/sslbl-certificates.generated.mjs +141 -138
  209. package/lib/static-html-analysis.mjs +117 -146
  210. package/lib/structured-data-identity.mjs +2 -1
  211. package/lib/technology-markup.mjs +100 -0
  212. package/lib/threat-intelligence-manifests.mjs +11 -139
  213. package/lib/threat-intelligence-runtime.mjs +24 -52
  214. package/lib/threat-intelligence-targets.mjs +6 -34
  215. package/lib/threat-intelligence-types.mjs +2 -36
  216. package/lib/typosquat-generator.mjs +29 -16
  217. package/lib/urlhaus-intelligence.mjs +8 -3
  218. package/lib/urlscan-intelligence.mjs +1 -1
  219. package/lib/website-enrichment.mjs +388 -0
  220. package/lib/website-security-posture.mjs +30 -6
  221. package/lib/website-technology.mjs +111 -71
  222. package/lib/whois-authority.mjs +55 -5
  223. package/lib/whois-chain.mjs +9 -2
  224. package/lib/whois-common-formats.mjs +5 -5
  225. package/lib/whois-contracts.mjs +2 -1
  226. package/lib/whois-dialects.mjs +2 -2
  227. package/lib/whois-normalization.mjs +3 -1
  228. package/lib/whois-parser-support.mjs +8 -1
  229. package/lib/whois-parser.mjs +5 -3
  230. package/lib/whois-transport.mjs +23 -7
  231. package/package.json +14 -14
  232. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  233. package/packages/analysis/bounded-json.mjs +330 -0
  234. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  235. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  236. package/packages/analysis/ct-query.mjs +27 -0
  237. package/packages/analysis/ct-response-bounds.mjs +9 -0
  238. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  239. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  240. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  241. package/packages/analysis/opportunity-scoring.mjs +107 -0
  242. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  243. package/packages/analysis/portable-generator.mjs +29 -0
  244. package/packages/analysis/project-metadata.mjs +8 -0
  245. package/packages/analysis/registrable-domain.mjs +56 -0
  246. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  247. package/packages/analysis/risk-scoring.mjs +345 -0
  248. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  249. package/packages/analysis/semantic-version.mjs +54 -0
  250. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  251. package/packages/cases/case-attachment-model.mjs +83 -0
  252. package/packages/cases/case-evidence-links.mjs +135 -0
  253. package/packages/cases/case-evidence-model.mjs +229 -69
  254. package/packages/cases/case-evidence-quality.mjs +5 -0
  255. package/packages/cases/case-export-input.mjs +55 -0
  256. package/packages/cases/case-incident-context.mjs +52 -0
  257. package/packages/cases/case-markdown.mjs +11 -0
  258. package/packages/cases/case-migration-model.mjs +98 -48
  259. package/packages/cases/case-model.mjs +1 -0
  260. package/packages/cases/case-recheck-model.mjs +149 -0
  261. package/packages/cases/case-record-contracts.mjs +1 -1
  262. package/packages/cases/case-record-core.mjs +4 -3
  263. package/packages/cases/case-record-operations.mjs +123 -133
  264. package/packages/cases/case-record-projection.mjs +30 -20
  265. package/packages/cases/case-record-version-input.mjs +19 -0
  266. package/packages/cases/case-report.mjs +114 -67
  267. package/packages/cases/case-requested-evidence.mjs +146 -0
  268. package/packages/cases/case-response-actions.mjs +756 -0
  269. package/packages/cases/case-response-model.mjs +49 -1129
  270. package/packages/cases/case-response-outcomes.mjs +338 -0
  271. package/packages/cases/case-response-packet-render.mjs +157 -0
  272. package/packages/cases/case-response-packet-types.mjs +1 -0
  273. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  274. package/packages/cases/case-response-packet.mjs +58 -593
  275. package/packages/cases/case-response-records.mjs +93 -0
  276. package/packages/cases/case-response-review-inputs.mjs +382 -0
  277. package/packages/cases/case-response-values.mjs +85 -0
  278. package/packages/cases/case-review-package.mjs +45 -0
  279. package/packages/cases/case-selection.mjs +46 -0
  280. package/packages/cases/case-storage-model.mjs +26 -1
  281. package/packages/cases/case-workflow-metadata.mjs +225 -0
  282. package/packages/cases/case-workflow-migration.mjs +43 -0
  283. package/packages/cases/http-summary.mjs +6 -1
  284. package/packages/cases/response-route-freshness.mjs +16 -0
  285. package/packages/comparison/capture-context.mjs +61 -0
  286. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  287. package/packages/comparison/favicon-similarity.mjs +1 -1
  288. package/packages/comparison/page-similarity.mjs +3 -2
  289. package/packages/comparison/relationship-evidence.mjs +103 -14
  290. package/packages/comparison/relationship-provenance.mjs +99 -0
  291. package/packages/contracts/analyst-interchange.mjs +234 -0
  292. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  293. package/packages/contracts/analyst-review-state.mjs +5 -4
  294. package/packages/contracts/case-portability.mjs +370 -114
  295. package/packages/contracts/case-views-contract.mjs +12 -0
  296. package/packages/contracts/case-views.mjs +26 -0
  297. package/packages/contracts/cli-command-semantics.mjs +3 -0
  298. package/packages/contracts/cli-grammar.mjs +6 -0
  299. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  300. package/packages/contracts/cli-lookup.mjs +0 -1
  301. package/packages/contracts/context-review.mjs +24 -0
  302. package/packages/contracts/document-intake.mjs +11 -0
  303. package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
  304. package/packages/contracts/domain-control-manifest.mjs +128 -74
  305. package/packages/contracts/domain-control-monitor.mjs +59 -45
  306. package/packages/contracts/domain-control-review.mjs +200 -121
  307. package/packages/contracts/domain-name.mjs +21 -0
  308. package/packages/contracts/external-observation-interchange.mjs +2 -1
  309. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  310. package/packages/contracts/har-review.mjs +2 -0
  311. package/packages/contracts/http-url.mjs +2 -0
  312. package/packages/contracts/identity-events.mjs +4 -0
  313. package/packages/contracts/investigation-package-limits.mjs +24 -0
  314. package/packages/contracts/investigation-portability.mjs +56 -27
  315. package/packages/contracts/investigation-run.mjs +18 -0
  316. package/packages/contracts/ip-address.mjs +44 -0
  317. package/packages/contracts/lookup-evidence.mjs +35 -0
  318. package/packages/contracts/mail-authentication.mjs +5 -0
  319. package/packages/contracts/message-intake.mjs +31 -0
  320. package/packages/contracts/offline-comparison.mjs +4 -2
  321. package/packages/contracts/page-fingerprints.mjs +15 -0
  322. package/packages/contracts/passive-mail.mjs +10 -0
  323. package/packages/contracts/risk-calibration.mjs +0 -1
  324. package/packages/contracts/schema-compatibility.mjs +1 -1
  325. package/packages/contracts/schema-lifecycle.mjs +83 -153
  326. package/packages/contracts/selected-file-limits.mjs +6 -0
  327. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  328. package/packages/contracts/web-capture.mjs +10 -0
  329. package/packages/contracts/workspace-portability.mjs +219 -238
  330. package/packages/evidence/artifact-integrity.mjs +8 -2
  331. package/packages/evidence/artifact-structure.mjs +105 -0
  332. package/packages/evidence/collection-quality.mjs +67 -0
  333. package/packages/evidence/decision-fact.mjs +4 -1
  334. package/packages/evidence/dns-query-outcome.mjs +72 -0
  335. package/packages/evidence/domain-control-runtime.mjs +155 -27
  336. package/packages/evidence/domain-posture-context.mjs +79 -0
  337. package/packages/evidence/email-recipient.mjs +18 -0
  338. package/packages/evidence/image-regions.mjs +56 -0
  339. package/packages/evidence/latest-observations.mjs +28 -0
  340. package/packages/evidence/lookup-target.mjs +108 -0
  341. package/packages/evidence/observation.mjs +18 -1
  342. package/packages/evidence/passphrase-encryption.mjs +44 -0
  343. package/packages/evidence/png-pixels.mjs +117 -0
  344. package/packages/evidence/public-address-policy.mjs +83 -0
  345. package/packages/evidence/record-identity.mjs +72 -0
  346. package/packages/evidence/registry-dates.mjs +190 -0
  347. package/packages/evidence/retained-file.mjs +69 -0
  348. package/packages/evidence/review-age.mjs +14 -0
  349. package/packages/interchange/bagit.mjs +342 -0
  350. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  351. package/packages/interchange/crc32.mjs +17 -0
  352. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  353. package/packages/interchange/external-findings-import.mjs +93 -61
  354. package/packages/interchange/managed-indicator-export.mjs +35 -0
  355. package/packages/interchange/managed-indicator-set.mjs +210 -0
  356. package/packages/interchange/misp-indicator-export.mjs +93 -0
  357. package/packages/interchange/sharing-policy.mjs +56 -0
  358. package/packages/interchange/stix-indicator-export.mjs +129 -0
  359. package/packages/interchange/web-capture-import.mjs +389 -0
  360. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  361. package/packages/investigation/bulk-mail-exposure.mjs +9 -14
  362. package/packages/investigation/capture-coverage.mjs +57 -0
  363. package/packages/investigation/connector-provenance-review.mjs +100 -0
  364. package/packages/investigation/context-review.mjs +35 -0
  365. package/packages/investigation/document-intake.mjs +81 -0
  366. package/packages/investigation/docx-intake.mjs +133 -0
  367. package/packages/investigation/domain-history-review.mjs +100 -0
  368. package/packages/investigation/domain-transition-review.mjs +47 -0
  369. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  370. package/packages/investigation/har-intake.mjs +68 -0
  371. package/packages/investigation/identity-event-intake.mjs +131 -0
  372. package/packages/investigation/identity-incident-review.mjs +39 -0
  373. package/packages/investigation/incident-sequence-review.mjs +58 -0
  374. package/packages/investigation/intake-report.mjs +19 -0
  375. package/packages/investigation/investigation-bagit.mjs +14 -0
  376. package/packages/investigation/investigation-capsule.mjs +33 -5
  377. package/packages/investigation/investigation-manifest.mjs +200 -0
  378. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  379. package/packages/investigation/investigation-package.mjs +200 -0
  380. package/packages/investigation/link-intake.mjs +179 -0
  381. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  382. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  383. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  384. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  385. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  386. package/packages/investigation/mail-authentication-review.mjs +222 -0
  387. package/packages/investigation/mail-header-identity.mjs +209 -0
  388. package/packages/investigation/message-intake.mjs +195 -0
  389. package/packages/investigation/page-behaviour.mjs +113 -0
  390. package/packages/investigation/pdf-intake.mjs +155 -0
  391. package/packages/investigation/platform-continuity-review.mjs +60 -0
  392. package/packages/investigation/qr-intake.mjs +56 -0
  393. package/packages/investigation/requested-action-hints.mjs +11 -0
  394. package/packages/investigation/selected-input-review.mjs +17 -0
  395. package/packages/investigation/storefront-review.mjs +70 -0
  396. package/packages/monitoring/analyst-review-state.mjs +38 -90
  397. package/packages/workspace/brand-profile-model.mjs +160 -22
  398. package/packages/workspace/bulk-columns.mjs +17 -0
  399. package/packages/workspace/bulk-review-model.mjs +39 -31
  400. package/packages/workspace/bulk-session-model.mjs +112 -27
  401. package/packages/workspace/bulk-sort.mjs +1 -0
  402. package/packages/workspace/campaign-model.mjs +1 -1
  403. package/packages/workspace/case-views.mjs +102 -0
  404. package/packages/workspace/detection-rule-model.mjs +1 -1
  405. package/packages/workspace/hostile-input.mjs +29 -8
  406. package/packages/workspace/investigation-guide.mjs +2 -2
  407. package/packages/workspace/investigation-template-model.mjs +45 -6
  408. package/packages/workspace/page-baseline.mjs +20 -17
  409. package/packages/workspace/relationship-observation-model.mjs +54 -35
  410. package/packages/workspace/shortlist-model.mjs +14 -5
  411. package/packages/workspace/watchlist-history.mjs +30 -14
  412. package/packages/workspace/watchlist-store.mjs +24 -6
  413. package/packages/workspace/website-snapshot-model.mjs +98 -33
  414. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  415. package/packages/workspace/workspace-archive.mjs +90 -25
  416. package/third-party-notices.txt +577 -17
  417. package/lib/hostname.mjs +0 -38
  418. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,1354 +1,33 @@
1
- import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../lib/project-metadata.mjs";
2
- import { LOOKUP_EVIDENCE_SCHEMA_VERSION, PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../lib/evidence-export.mjs";
3
- import { RISK_CALIBRATION_DATASET_SCHEMA, RISK_CALIBRATION_REPORT_SCHEMA, } from "../packages/contracts/risk-calibration.mjs";
4
- import { CASE_SCHEMA_VERSION, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, WORKSPACE_ARCHIVE_VERSION, } from "../packages/contracts/case-portability.mjs";
5
- import { CLI_COMMAND_SEMANTICS, CLI_HELP_GROUP_ORDER, } from "../packages/contracts/cli-command-semantics.mjs";
6
- import { CLI_FAIL_POLICIES_BY_COMMAND } from "./fail-policy.mjs";
7
- const LEGACY_WORKSPACE_ARCHIVE_VERSIONS = SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS
8
- .filter((version) => version !== WORKSPACE_ARCHIVE_VERSION);
9
- const LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION = LEGACY_WORKSPACE_ARCHIVE_VERSIONS
10
- .map((version) => `version-${version}`)
11
- .join(' and ');
12
- const LEGACY_WORKSPACE_ARCHIVE_SCOPE = LEGACY_WORKSPACE_ARCHIVE_VERSIONS
13
- .map((version) => `v${version}`)
14
- .join(' and ');
15
- const INVESTIGATION_PLAN_RECIPES = Object.freeze([
16
- 'domain-triage',
17
- 'lookalike-review',
18
- 'owned-domain-review',
19
- 'historical-comparison',
20
- 'campaign-review',
21
- 'certificate-anomaly',
22
- 'registry-disagreement',
23
- 'evidence-handoff',
24
- 'planned-domain-change',
25
- 'post-change-verification',
26
- ]);
27
- const RUNNABLE_INVESTIGATION_PLAN_RECIPES = Object.freeze([
28
- 'domain-triage',
29
- 'lookalike-review',
30
- 'owned-domain-review',
31
- 'historical-comparison',
32
- ]);
33
- const CLI_META_ACTIONS = Object.freeze([
34
- Object.freeze({
35
- id: 'help',
36
- aliases: Object.freeze(['--help', '-h']),
37
- scope: 'root_or_command',
38
- precedence: 'before_command_grammar',
39
- bypassesOrdinaryRequirements: true,
40
- acceptsAdditionalArguments: false,
41
- }),
42
- Object.freeze({
43
- id: 'version',
44
- aliases: Object.freeze(['--version', '-V']),
45
- scope: 'root_only',
46
- precedence: 'before_command_grammar',
47
- bypassesOrdinaryRequirements: true,
48
- acceptsAdditionalArguments: false,
49
- }),
50
- ]);
51
- const CLI_META_ACTION_BY_ID = Object.freeze(Object.fromEntries(CLI_META_ACTIONS.map((action) => [action.id, action])));
52
- // Human-facing command reference kept separate from command execution so
53
- // additions do not expand the already broad runtime controller.
54
- const HELP_INTRO = `WHOISleuth CLI
55
- Domain investigation from your terminal.
56
-
57
- Quick start:
58
- whoisleuth
59
- whoisleuth example.test
60
- whoisleuth lookup example.test --deep
61
- cat domains.txt | whoisleuth bulk --jsonl
62
- `;
63
- const HELP_FOOTER = `
64
- Run "whoisleuth <command> --help" for focused usage and an example.
65
- With no arguments, an eligible interactive terminal opens a bounded launcher;
66
- redirected or unsupported terminals continue to print this help. No request
67
- starts until a Lookup plan is shown and the analyst confirms collection.
68
- Use --json or --jsonl where supported for machine-readable stdout.
69
- Use --output <file> for atomic private file output and --force to replace it.
70
- Use --palette auto, light, or dark after the command to select a fixed terminal
71
- colour palette; --no-color, NO_COLOR, and redirected output still suppress ANSI.
72
- Use --config <file> and --profile <name> for explicit versioned safe defaults.
73
- Registry scaffold is the exception: its --profile selects a fixture capability
74
- profile and --config is rejected, so bootstrap defaults cannot alter fixtures.
75
- An ICANN-recognised public domain, reserved documentation domain, IP, or ASN
76
- may replace "lookup"; URL-like or ambiguous input requires the explicit
77
- command. Both forms use the same Lookup options.
78
- Diagnostics are written to stderr. Fast lookup is the default; deep collection
79
- must be requested explicitly and can disclose a target to additional sources.
80
-
81
- Copyright 2026 slicedearth. Licensed under AGPL-3.0-only.
82
- Source and licence: ${WHOISLEUTH_SOURCE_REPOSITORY_URL}
83
- `;
84
- const COMMON_OPTIONS = Object.freeze([
85
- '--help', '--output', '--force', '--config', '--profile', '--palette',
86
- ]);
87
- const REGISTRY_SCAFFOLD_COMMON_OPTIONS = Object.freeze(COMMON_OPTIONS.filter((option) => option !== '--config' && option !== '--profile'));
88
- function commonOptionsSeedForCommand(command) {
89
- return command === 'registry-scaffold' ? REGISTRY_SCAFFOLD_COMMON_OPTIONS : COMMON_OPTIONS;
1
+ import { INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES } from "./investigation-recipes.mjs";
2
+ import { CLI_COMMAND_SEMANTICS, CLI_HELP_GROUP_ORDER } from "../packages/contracts/cli-command-semantics.mjs";
3
+ import { INLINE_COMMAND_FAMILIES, CLI_CASE_OPERATIONS, CLI_INDICATOR_OPERATIONS, CLI_META_ACTIONS, CLI_META_ACTION_BY_ID, HELP_INTRO, HELP_FOOTER, commonOptionsForSeed, CLI_OPTION_DEFINITIONS, PRESENTATION_OPTIONS, optionSpec, grammarConstraints, } from "./command-definition.mjs";
4
+ import { COMMAND_SEEDS } from "./command-seeds.mjs";
5
+ const COMMAND_ORDER = Object.freeze(Object.keys(CLI_COMMAND_SEMANTICS));
6
+ function commandOptionDefinition(command, option) {
7
+ return COMMAND_SEEDS[command].optionOverrides?.[option] ?? CLI_OPTION_DEFINITIONS[option];
90
8
  }
91
- function positional(name, valueKind, minimum, maximum, values = [], inputSource = 'argv', requiredWhenOptions = []) {
92
- return Object.freeze({
93
- name,
94
- valueKind,
95
- minimum,
96
- maximum,
97
- values: Object.freeze([...values]),
98
- inputSource,
99
- requiredWhenOptions: Object.freeze([...requiredWhenOptions]),
100
- });
101
- }
102
- const NO_POSITIONALS = Object.freeze([]);
103
- const OPTIONAL_FILE_POSITIONAL = Object.freeze([positional('source', 'file', 0, 1, [], 'argv_or_stdin')]);
104
- const OPTIONAL_TEXT_POSITIONAL = Object.freeze([positional('subject', 'text', 0, 1, [], 'argv_or_stdin')]);
105
- const NO_INTEGER_RANGES = () => Object.freeze([]);
106
- const BASE_INTEGER_RANGE = (minimum, maximum) => (Object.freeze({ minimum, maximum, whenOptionPresent: null }));
107
- const DEEP_INTEGER_RANGE = (minimum, maximum) => (Object.freeze({ minimum, maximum, whenOptionPresent: '--deep' }));
108
- function optionDefinition(valueKind, options = {}) {
109
- const configuredValues = options.values;
110
- return Object.freeze({
111
- valueKind: typeof valueKind === 'function' ? valueKind : () => valueKind,
112
- values: typeof configuredValues === 'function'
113
- ? configuredValues
114
- : () => Object.freeze([...(configuredValues ?? [])]),
115
- occurrence: options.occurrence ?? 'once',
116
- acceptsOptionLikeValue: options.acceptsOptionLikeValue ?? false,
117
- metaAction: options.metaAction ?? null,
118
- integerRanges: options.integerRanges ?? NO_INTEGER_RANGES,
119
- });
120
- }
121
- const flag = (occurrence = 'once') => optionDefinition('flag', { occurrence });
122
- const file = () => optionDefinition('file');
123
- const text = (acceptsOptionLikeValue = false) => optionDefinition('text', { acceptsOptionLikeValue });
124
- const enumeration = (values) => optionDefinition('enum', { values });
125
- const integer = (ranges) => (optionDefinition('integer', { integerRanges: ranges }));
126
- const CLI_OPTION_DEFINITIONS = Object.freeze({
127
- '--help': optionDefinition('flag', { metaAction: 'help' }),
128
- '--output': file(),
129
- '--force': flag(),
130
- '--config': file(),
131
- '--profile': text(true),
132
- '--palette': enumeration(['auto', 'light', 'dark']),
133
- '--network': flag(),
134
- '--json': flag(),
135
- '--quiet': flag('idempotent'),
136
- '--no-color': flag('idempotent'),
137
- '--common': flag(),
138
- '--group': enumeration(['investigate', 'respond', 'assure', 'utilities']),
139
- '--mode': enumeration(['offline', 'network']),
140
- '--workflow': text(true),
141
- '--configuration-digest': text(true),
142
- '--junit': flag(),
143
- '--markdown': flag(),
144
- '--html': flag(),
145
- '--no-attribution': flag(),
146
- '--fast': flag(),
147
- '--deep': flag(),
148
- '--observer': text(),
149
- '--vantage': text(),
150
- '--plan': flag(),
151
- '--summary': flag(),
152
- '--verbose': flag(),
153
- '--browse': flag(),
154
- '--save-lookup': file(),
155
- '--strict-exit': flag(),
156
- '--fail-on': optionDefinition('policy_list', {
157
- values: (command) => CLI_FAIL_POLICIES_BY_COMMAND[command] ?? [],
158
- }),
159
- '--events': flag(),
160
- '--jsonl': flag(),
161
- '--csv': flag(),
162
- '--domains': flag(),
163
- '--queries': flag(),
164
- '--registered-only': flag(),
165
- '--inconclusive-only': flag(),
166
- '--errors-only': flag(),
167
- '--concurrency': integer((command) => command === 'monitor-once'
168
- ? Object.freeze([BASE_INTEGER_RANGE(1, 3)])
169
- : Object.freeze([BASE_INTEGER_RANGE(1, 8), DEEP_INTEGER_RANGE(1, 3)])),
170
- '--checkpoint': file(),
171
- '--resume': optionDefinition((command) => command === 'workflow-run' ? 'file' : 'flag'),
172
- '--tlds': text(true),
173
- '--preset': enumeration(['common', 'impersonation', 'all']),
174
- '--families': text(),
175
- '--keyboard': enumeration(['qwerty', 'azerty', 'qwertz', 'all']),
176
- '--dictionary': file(),
177
- '--snapshot': file(),
178
- '--scan-limit': integer(() => Object.freeze([
179
- BASE_INTEGER_RANGE(1, 500),
180
- DEEP_INTEGER_RANGE(1, 50),
181
- ])),
182
- '--chunk-size': integer(() => Object.freeze([BASE_INTEGER_RANGE(1, 100)])),
183
- '--resolver': text(),
184
- '--allowlist': file(),
185
- '--observation-snapshot': file(),
186
- '--acquisition-only': flag(),
187
- '--suppressed-only': flag(),
188
- '--selectors': text(true),
189
- '--retired-selectors': text(true),
190
- '--mail-profile': enumeration(['standard', 'defensive-no-mail', 'parked']),
191
- '--sarif': flag(),
192
- '--owned-domain': flag(),
193
- '--trust-anchor': file(),
194
- '--owned-or-authorized': flag(),
195
- '--active-probe': flag(),
196
- '--suffix': text(true),
197
- '--scenario': enumeration(['registered', 'not_found', 'inconclusive']),
198
- '--summary-json': flag(),
199
- '--passphrase-file': file(),
200
- '--manifest': file(),
201
- '--manifest-entry': enumeration(Array.from({ length: 16 }, (_, index) => `artifact-${index + 1}`)),
202
- '--search': text(),
203
- '--require-match': flag(),
204
- '--reveal': flag(),
205
- '--expect-content-digest': text(true),
206
- '--private-key-file': file(),
207
- '--public-key-file': file(),
208
- '--mmdb': file(),
209
- '--audience': enumeration(['internal', 'trusted', 'public']),
210
- '--reviewed': flag(),
211
- '--previous': file(),
212
- '--limit': integer(() => Object.freeze([BASE_INTEGER_RANGE(1, 20)])),
213
- '--marking': enumeration(['clear', 'green', 'amber', 'amber-strict', 'red']),
214
- '--recipient-scope': enumeration(['public', 'community', 'organization', 'named-recipients']),
215
- '--purpose': text(),
216
- '--human-reviewed': flag('idempotent'),
217
- '--personal-data-reviewed': flag('idempotent'),
218
- '--redactions-confirmed': flag('idempotent'),
219
- '--list': flag(),
220
- '--explain': enumeration(INVESTIGATION_PLAN_RECIPES),
221
- '--select': optionDefinition('text', { occurrence: 'repeatable', acceptsOptionLikeValue: true }),
222
- '--approve-network': flag(),
223
- '--left-session': text(true),
224
- '--right-session': text(true),
225
- '--compact': flag(),
226
- });
227
- function constraint(value) {
228
- if (value.kind === 'mutually_exclusive') {
229
- return Object.freeze({ ...value, options: Object.freeze([...value.options]) });
230
- }
231
- if (value.kind === 'required') {
232
- return Object.freeze({ ...value, options: Object.freeze([...value.options]) });
233
- }
234
- if (value.kind === 'excludes_all' || value.kind === 'value_excludes') {
235
- return Object.freeze({ ...value, excludedOptions: Object.freeze([...value.excludedOptions]) });
236
- }
237
- return Object.freeze({ ...value, requiredOptions: Object.freeze([...value.requiredOptions]) });
238
- }
239
- const EMPTY_CONSTRAINTS = Object.freeze([]);
240
- const FILE_OUTPUT_CONSTRAINTS = Object.freeze([
241
- constraint({ kind: 'requires_all', option: '--force', requiredOptions: ['--output'] }),
242
- ]);
243
- const QUIET_OUTPUT_CONSTRAINT = constraint({
244
- kind: 'mutually_exclusive',
245
- options: ['--quiet', '--output'],
246
- });
247
- const MACHINE_OUTPUT_OPTIONS = Object.freeze([
248
- '--json', '--jsonl', '--junit', '--csv', '--domains', '--queries', '--markdown',
249
- '--html', '--sarif', '--summary-json',
250
- ]);
251
- function optionSpec(command, option, scope) {
252
- const definition = CLI_OPTION_DEFINITIONS[option];
253
- const valueKind = definition.valueKind(command);
254
- const values = definition.values(command);
255
- return Object.freeze({
256
- option,
257
- scope,
258
- arity: valueKind === 'flag' ? 0 : 1,
259
- valueKind,
260
- values: Object.freeze([...values]),
261
- integerRanges: Object.freeze([...definition.integerRanges(command)]),
262
- occurrence: definition.occurrence,
263
- acceptsOptionLikeValue: definition.acceptsOptionLikeValue,
264
- metaAction: definition.metaAction,
265
- });
9
+ export function commandHasOwner(command, owner) {
10
+ return isCliCommand(command) && COMMAND_SEEDS[command].handlerOwner === owner;
266
11
  }
267
- function grammarConstraints(commandOptions, commandConstraints) {
268
- const machineOutputOptions = commandOptions.filter((option) => MACHINE_OUTPUT_OPTIONS.includes(option));
269
- return Object.freeze([
270
- ...FILE_OUTPUT_CONSTRAINTS,
271
- ...(commandOptions.includes('--quiet') ? [QUIET_OUTPUT_CONSTRAINT] : []),
272
- ...(commandOptions.includes('--quiet') && machineOutputOptions.length > 0
273
- ? [constraint({ kind: 'excludes_all', option: '--quiet', excludedOptions: machineOutputOptions })]
274
- : []),
275
- ...(commandOptions.includes('--events')
276
- ? [constraint({ kind: 'mutually_exclusive', options: ['--events', '--output'] })]
277
- : []),
278
- ...(commandOptions.includes('--browse')
279
- ? [constraint({ kind: 'mutually_exclusive', options: ['--browse', '--output'] })]
280
- : []),
281
- ...commandConstraints,
282
- ]);
12
+ function inlineCommandFamily(command) {
13
+ const owner = COMMAND_SEEDS[command].handlerOwner;
14
+ return INLINE_COMMAND_FAMILIES.find(family => family === owner) ?? null;
283
15
  }
284
- function commandSeed(seed) {
285
- return Object.freeze({
286
- ...seed,
287
- reference: Object.freeze({ ...seed.reference }),
288
- collection: Object.freeze({ ...seed.collection }),
289
- options: Object.freeze([...seed.options]),
290
- positionals: Object.freeze([...seed.positionals]),
291
- constraints: Object.freeze([...seed.constraints]),
292
- schemaIdentifiers: Object.freeze([...seed.schemaIdentifiers]),
293
- primaryArtefacts: Object.freeze([...seed.primaryArtefacts]),
294
- additionalOutputFormats: Object.freeze([...seed.additionalOutputFormats]),
295
- });
16
+ function inlineCommandsFor(family) {
17
+ return Object.freeze(COMMAND_ORDER.filter((command) => COMMAND_SEEDS[command].handlerOwner === family));
296
18
  }
297
- const COMMAND_SEEDS = Object.freeze({
298
- completion: commandSeed({
299
- reference: {
300
- description: 'Print a static shell-completion script for the installed CLI.',
301
- example: 'whoisleuth completion zsh > ~/.zfunc/_whoisleuth',
302
- boundary: 'Generation is offline and writes only the script to stdout. The command never modifies shell configuration.',
303
- },
304
- collection: { mode: 'offline', scope: 'Prints one static script and changes no shell configuration.' },
305
- summary: 'Print shell completion',
306
- options: [],
307
- positionals: Object.freeze([positional('shell', 'enum', 1, 1, ['bash', 'zsh', 'fish', 'powershell'])]),
308
- constraints: EMPTY_CONSTRAINTS,
309
- handlerOwner: 'inline',
310
- networkEffect: 'offline',
311
- common: false,
312
- schemaIdentifiers: Object.freeze([]),
313
- primaryArtefacts: Object.freeze([]),
314
- planSupport: false,
315
- additionalOutputFormats: Object.freeze([]),
316
- bootstrapProfile: 'allowed',
317
- }),
318
- doctor: commandSeed({
319
- reference: {
320
- description: 'Check the supported runtime and local terminal capabilities.',
321
- example: 'whoisleuth doctor --json',
322
- boundary: 'The default check is offline. Public DNS and port 43 checks run only when --network is explicitly supplied.',
323
- },
324
- collection: { mode: 'network', scope: 'Network access is opt-in with --network and is limited to fixed public DNS, HTTPS, and WHOIS diagnostics.' },
325
- summary: 'Check the local CLI runtime',
326
- options: ['--network', '--json', '--quiet', '--no-color'],
327
- positionals: NO_POSITIONALS,
328
- constraints: EMPTY_CONSTRAINTS,
329
- handlerOwner: 'inline',
330
- networkEffect: 'conditional_network',
331
- common: true,
332
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.doctor']),
333
- primaryArtefacts: Object.freeze([]),
334
- planSupport: false,
335
- additionalOutputFormats: Object.freeze([]),
336
- bootstrapProfile: 'allowed',
337
- }),
338
- commands: commandSeed({
339
- reference: {
340
- description: 'List the installed command contracts in terminal or versioned JSON form.',
341
- example: 'whoisleuth commands --json',
342
- boundary: 'Catalogue generation is offline. It reports declared command modes and limits without executing collection or inspecting local evidence.',
343
- },
344
- collection: { mode: 'offline', scope: 'Reads the embedded command catalogue and performs no collection.' },
345
- summary: 'List installed command contracts',
346
- options: ['--common', '--group', '--mode', '--json', '--quiet', '--no-color'],
347
- positionals: NO_POSITIONALS,
348
- constraints: Object.freeze([]),
349
- handlerOwner: 'inline',
350
- networkEffect: 'offline',
351
- common: true,
352
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.command-catalogue']),
353
- primaryArtefacts: Object.freeze([]),
354
- planSupport: false,
355
- additionalOutputFormats: Object.freeze([]),
356
- bootstrapProfile: 'allowed',
357
- }),
358
- manual: commandSeed({
359
- reference: {
360
- description: 'Print a generated roff manual page for local installation.',
361
- example: 'whoisleuth manual | man -l -',
362
- boundary: 'Generation is offline and derives from the same command catalogue as focused help.',
363
- },
364
- collection: { mode: 'offline', scope: 'Builds documentation from the embedded command catalogue.' },
365
- summary: 'Print the generated manual page',
366
- options: [],
367
- positionals: NO_POSITIONALS,
368
- constraints: EMPTY_CONSTRAINTS,
369
- handlerOwner: 'inline',
370
- networkEffect: 'offline',
371
- common: false,
372
- schemaIdentifiers: Object.freeze([]),
373
- primaryArtefacts: Object.freeze([]),
374
- planSupport: false,
375
- additionalOutputFormats: Object.freeze([]),
376
- bootstrapProfile: 'allowed',
377
- }),
378
- manifest: commandSeed({
379
- reference: {
380
- description: 'Record an ordered, path-free manifest for up to 16 local JSON artefacts.',
381
- example: 'whoisleuth manifest lookup.json comparison.json --workflow "domain review" --json',
382
- boundary: 'The command records hashes and bounded schema metadata only. It omits source paths and artefact contents and performs no network collection.',
383
- },
384
- collection: { mode: 'offline', scope: 'Reads 1 to 16 local JSON artefacts capped at 32 MiB in total and retains no source paths.' },
385
- summary: 'Build an evidence manifest offline',
386
- options: ['--workflow', '--configuration-digest', '--json', '--quiet', '--no-color'],
387
- positionals: Object.freeze([positional('artefacts', 'file', 1, 16)]),
388
- constraints: Object.freeze([
389
- constraint({ kind: 'required', options: ['--workflow'] }),
390
- ]),
391
- handlerOwner: 'inline',
392
- networkEffect: 'offline',
393
- common: false,
394
- schemaIdentifiers: Object.freeze(['whoisleuth.investigation-manifest']),
395
- primaryArtefacts: Object.freeze([]),
396
- planSupport: false,
397
- additionalOutputFormats: Object.freeze([]),
398
- bootstrapProfile: 'allowed',
399
- }),
400
- "map-observations": commandSeed({
401
- reference: {
402
- description: 'Apply one bounded declarative field-mapping profile to local source observations.',
403
- example: 'whoisleuth map-observations mapping.json --json',
404
- boundary: 'Profiles select allowlisted dotted fields only. They execute no scripts, make no requests, and emit the browser-compatible external-findings contract.',
405
- },
406
- collection: { mode: 'offline', scope: 'Reads one mapping document capped at 4 MiB and executes no scripts or requests.' },
407
- summary: 'Apply a declarative observation map offline',
408
- options: ['--json', '--quiet', '--no-color'],
409
- positionals: OPTIONAL_FILE_POSITIONAL,
410
- constraints: EMPTY_CONSTRAINTS,
411
- handlerOwner: 'inline',
412
- networkEffect: 'offline',
413
- common: false,
414
- schemaIdentifiers: Object.freeze(['whoisleuth.external-observation-mapping']),
415
- primaryArtefacts: Object.freeze([]),
416
- planSupport: false,
417
- additionalOutputFormats: Object.freeze([]),
418
- bootstrapProfile: 'allowed',
419
- }),
420
- "oam-export": commandSeed({
421
- reference: {
422
- description: 'Project browser-compatible external findings into a bounded Open Asset Model bridge document.',
423
- example: 'whoisleuth oam-export external-findings.json --json',
424
- boundary: 'The projection is offline, preserves source completeness without inventing confidence, and covers only bounded FQDN, IP address, certificate, and related edge vocabulary.',
425
- },
426
- collection: { mode: 'offline', scope: 'Reads one browser-compatible external-findings document and projects bounded graph records locally.' },
427
- summary: 'Project external findings to Open Asset Model',
428
- options: ['--json', '--quiet', '--no-color'],
429
- positionals: OPTIONAL_FILE_POSITIONAL,
430
- constraints: EMPTY_CONSTRAINTS,
431
- handlerOwner: 'inline',
432
- networkEffect: 'offline',
433
- common: false,
434
- schemaIdentifiers: Object.freeze(['whoisleuth.open-asset-model-bridge']),
435
- primaryArtefacts: Object.freeze([]),
436
- planSupport: false,
437
- additionalOutputFormats: Object.freeze([]),
438
- bootstrapProfile: 'allowed',
439
- }),
440
- lookup: commandSeed({
441
- reference: {
442
- description: 'Collect registration evidence for one domain, IP, or ASN.',
443
- example: 'whoisleuth lookup example.test --deep --browse',
444
- 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. 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.',
445
- },
446
- collection: { mode: 'network', scope: 'Accepts one target. Fast is the default; deep collection must be selected explicitly.' },
447
- summary: 'Collect one domain, IP, or ASN',
448
- options: ['--json', '--junit', '--markdown', '--html', '--no-attribution', '--fast', '--deep', '--observer', '--vantage', '--plan', '--summary', '--verbose', '--browse', '--save-lookup', '--strict-exit', '--fail-on', '--events', '--quiet', '--no-color'],
449
- positionals: Object.freeze([positional('target', 'text', 0, 1, [], 'argv_or_stdin', ['--browse'])]),
450
- constraints: Object.freeze([
451
- constraint({ kind: 'mutually_exclusive', options: ['--json', '--junit', '--markdown', '--html'] }),
452
- constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
453
- constraint({ kind: 'mutually_exclusive', options: ['--summary', '--verbose'] }),
454
- constraint({ kind: 'requires_all', option: '--save-lookup', requiredOptions: ['--browse'] }),
455
- constraint({ kind: 'excludes_all', option: '--summary', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
456
- constraint({ kind: 'excludes_all', option: '--verbose', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
457
- constraint({ kind: 'excludes_all', option: '--browse', excludedOptions: ['--json', '--junit', '--markdown', '--html', '--summary', '--verbose', '--events', '--plan', '--quiet'] }),
458
- constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
459
- constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--junit', '--markdown', '--html', '--summary', '--verbose', '--strict-exit', '--events', '--quiet', '--fail-on'] }),
460
- ]),
461
- handlerOwner: 'lookup',
462
- networkEffect: 'conditional_network',
463
- common: true,
464
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup', 'whoisleuth.cli.lookup-plan']),
465
- primaryArtefacts: Object.freeze(['Source-qualified Lookup', 'Lookup request plan']),
466
- planSupport: true,
467
- additionalOutputFormats: Object.freeze([]),
468
- bootstrapProfile: 'allowed',
469
- }),
470
- bulk: commandSeed({
471
- reference: {
472
- description: 'Triage newline-delimited domains, IPs, or ASNs with bounded concurrency.',
473
- example: 'cat domains.txt | whoisleuth bulk --jsonl',
474
- 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.',
475
- },
476
- collection: { mode: 'network', scope: 'Accepts at most 500 fast or 50 deep targets, with concurrency capped at 8 fast or 3 deep.' },
477
- summary: 'Run bounded multi-target collection',
478
- options: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries', '--registered-only', '--inconclusive-only', '--errors-only', '--fast', '--deep', '--concurrency', '--checkpoint', '--resume', '--events', '--plan', '--fail-on', '--quiet', '--no-color'],
479
- positionals: OPTIONAL_FILE_POSITIONAL,
480
- constraints: Object.freeze([
481
- constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries'] }),
482
- constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--errors-only'] }),
483
- constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
484
- constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
485
- constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--junit', '--csv', '--domains', '--queries', '--events', '--checkpoint', '--resume', '--quiet', '--fail-on'] }),
486
- ]),
487
- handlerOwner: 'bulk',
488
- networkEffect: 'conditional_network',
489
- common: true,
490
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.bulk', 'whoisleuth.cli.bulk.item', 'whoisleuth.cli.bulk-checkpoint']),
491
- primaryArtefacts: Object.freeze(['Bulk result', 'Bulk checkpoint']),
492
- planSupport: true,
493
- additionalOutputFormats: Object.freeze([]),
494
- bootstrapProfile: 'allowed',
495
- }),
496
- "ct-search": commandSeed({
497
- reference: {
498
- description: 'Search certificate-transparency observations for one bounded keyword.',
499
- example: 'whoisleuth ct-search "example brand" --json',
500
- boundary: 'Certificate observations do not prove website activity, registration ownership, or malicious intent.',
501
- },
502
- collection: { mode: 'network', scope: 'Accepts one bounded search keyword and queries the fixed certificate-transparency source.' },
503
- summary: 'Search certificate observations',
504
- options: ['--json', '--quiet', '--no-color'],
505
- positionals: Object.freeze([positional('keyword', 'text', 0, 1, [], 'argv_or_stdin')]),
506
- constraints: EMPTY_CONSTRAINTS,
507
- handlerOwner: 'network',
508
- networkEffect: 'always_network',
509
- common: false,
510
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.ct-search']),
511
- primaryArtefacts: Object.freeze([]),
512
- planSupport: false,
513
- additionalOutputFormats: Object.freeze([]),
514
- bootstrapProfile: 'allowed',
515
- }),
516
- "ct-intake": commandSeed({
517
- reference: {
518
- description: 'Normalise source-qualified local certificate events into browser-compatible findings.',
519
- example: 'whoisleuth ct-intake certificate-events.json --json',
520
- boundary: 'The command is offline, caps output at 100 findings, and treats every event as a review lead rather than proof of serving or control.',
521
- },
522
- collection: { mode: 'offline', scope: 'Reads one source-qualified event batch capped at 4 MiB and makes no request.' },
523
- summary: 'Normalise certificate observations offline',
524
- options: ['--json', '--quiet', '--no-color'],
525
- positionals: OPTIONAL_FILE_POSITIONAL,
526
- constraints: EMPTY_CONSTRAINTS,
527
- handlerOwner: 'inline',
528
- networkEffect: 'offline',
529
- common: false,
530
- schemaIdentifiers: Object.freeze(['whoisleuth.ct-event-batch', 'whoisleuth.external-findings']),
531
- primaryArtefacts: Object.freeze([]),
532
- planSupport: false,
533
- additionalOutputFormats: Object.freeze([]),
534
- bootstrapProfile: 'allowed',
535
- }),
536
- discover: commandSeed({
537
- reference: {
538
- description: 'Generate bounded lookalike-domain candidates from local mutation rules.',
539
- example: 'whoisleuth discover example.test --preset common --jsonl',
540
- boundary: 'Generation and optional local snapshot comparison are offline. Candidates are leads only and are not resolved, registered, or classified as malicious.',
541
- },
542
- collection: { mode: 'offline', scope: 'Generates a bounded candidate set from local rules, dictionaries, and optional saved snapshots.' },
543
- summary: 'Generate lookalike candidates offline',
544
- options: ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--snapshot', '--json', '--jsonl', '--domains', '--quiet', '--no-color'],
545
- positionals: OPTIONAL_TEXT_POSITIONAL,
546
- constraints: Object.freeze([
547
- constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--domains'] }),
548
- constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
549
- constraint({ kind: 'value_excludes', option: '--preset', value: 'common', excludedOptions: ['--dictionary'] }),
550
- ]),
551
- handlerOwner: 'discovery',
552
- networkEffect: 'offline',
553
- common: true,
554
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.discover', 'whoisleuth.cli.discover.item', 'whoisleuth.cli.discovery-snapshot']),
555
- primaryArtefacts: Object.freeze(['Candidate set', 'Discovery snapshot']),
556
- planSupport: false,
557
- additionalOutputFormats: Object.freeze([]),
558
- bootstrapProfile: 'allowed',
559
- }),
560
- "discover-scan": commandSeed({
561
- reference: {
562
- description: 'Generate a bounded candidate set, collect a selected subset, and produce a supervised review queue.',
563
- example: 'whoisleuth discover-scan example.test --scan-limit 50 --checkpoint scan.json --json',
564
- boundary: 'This command performs network collection. Fast compact lookup is the default; deep mode is capped at 50 candidates. Allowlisting changes review priority only and shared infrastructure remains a lead, not attribution.',
565
- },
566
- collection: { mode: 'network', scope: 'Scans at most 500 fast or 50 deep candidates, with concurrency capped at 8 fast or 3 deep.' },
567
- summary: 'Collect a supervised candidate review queue',
568
- options: ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--fast', '--deep', '--scan-limit', '--chunk-size', '--concurrency', '--resolver', '--allowlist', '--checkpoint', '--resume', '--observation-snapshot', '--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only', '--events', '--plan', '--fail-on', '--json', '--jsonl', '--csv', '--domains', '--quiet', '--no-color'],
569
- positionals: OPTIONAL_TEXT_POSITIONAL,
570
- constraints: Object.freeze([
571
- constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--csv', '--domains'] }),
572
- constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
573
- constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
574
- constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only'] }),
575
- constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
576
- constraint({ kind: 'value_excludes', option: '--preset', value: 'common', excludedOptions: ['--dictionary'] }),
577
- constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--csv', '--domains', '--events', '--checkpoint', '--resume', '--observation-snapshot', '--quiet', '--fail-on'] }),
578
- ]),
579
- handlerOwner: 'discovery_scan',
580
- networkEffect: 'conditional_network',
581
- common: true,
582
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.discovery-scan', 'whoisleuth.cli.discovery-scan.item', 'whoisleuth.cli.discovery-observation-snapshot']),
583
- primaryArtefacts: Object.freeze(['Reviewed candidate queue', 'Observation snapshot']),
584
- planSupport: true,
585
- additionalOutputFormats: Object.freeze([]),
586
- bootstrapProfile: 'allowed',
587
- }),
588
- posture: commandSeed({
589
- reference: {
590
- description: 'Review bounded DNS mail, delegation, and domain-control posture.',
591
- example: 'whoisleuth posture example.test --mail-profile standard --json',
592
- boundary: 'Missing or failed DNS observations remain inconclusive and are not reported as absent controls.',
593
- },
594
- collection: { mode: 'network', scope: 'Accepts one domain and performs bounded RDAP, DNS, and conditional MTA-STS HTTPS requests.' },
595
- summary: 'Review DNS and mail posture',
596
- options: ['--selectors', '--retired-selectors', '--mail-profile', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
597
- positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
598
- constraints: Object.freeze([
599
- constraint({ kind: 'mutually_exclusive', options: ['--json', '--sarif'] }),
600
- constraint({ kind: 'requires_all', option: '--sarif', requiredOptions: ['--owned-domain'] }),
601
- ]),
602
- handlerOwner: 'network',
603
- networkEffect: 'always_network',
604
- common: false,
605
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.posture']),
606
- primaryArtefacts: Object.freeze([]),
607
- planSupport: false,
608
- additionalOutputFormats: Object.freeze([]),
609
- bootstrapProfile: 'allowed',
610
- }),
611
- http: commandSeed({
612
- reference: {
613
- description: 'Inspect one homepage request, redirects, and bounded response metadata.',
614
- example: 'whoisleuth http example.test --json',
615
- boundary: 'Requests use the shared public-address and redirect guards. Fixed content-coding and cache-policy metadata describes only the selected response, excludes raw header values, and does not prove caching, transfer savings, performance, privacy, or safety. This is not a rendered browser or vulnerability scan.',
616
- },
617
- collection: { mode: 'network', scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
618
- summary: 'Inspect one homepage request',
619
- options: ['--json', '--quiet', '--no-color'],
620
- positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
621
- constraints: EMPTY_CONSTRAINTS,
622
- handlerOwner: 'network',
623
- networkEffect: 'always_network',
624
- common: false,
625
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.http']),
626
- primaryArtefacts: Object.freeze([]),
627
- planSupport: false,
628
- additionalOutputFormats: Object.freeze([]),
629
- bootstrapProfile: 'allowed',
630
- }),
631
- tls: commandSeed({
632
- reference: {
633
- description: 'Inspect one hostname certificate through a bounded TLS connection.',
634
- example: 'whoisleuth tls example.test --json',
635
- boundary: 'One observed connection is point-in-time evidence and does not establish every address, edge, or historical certificate.',
636
- },
637
- collection: { mode: 'network', scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
638
- summary: 'Inspect one TLS connection',
639
- options: ['--json', '--quiet', '--no-color'],
640
- positionals: Object.freeze([positional('hostname', 'text', 0, 1, [], 'argv_or_stdin')]),
641
- constraints: EMPTY_CONSTRAINTS,
642
- handlerOwner: 'network',
643
- networkEffect: 'always_network',
644
- common: false,
645
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.tls']),
646
- primaryArtefacts: Object.freeze([]),
647
- planSupport: false,
648
- additionalOutputFormats: Object.freeze([]),
649
- bootstrapProfile: 'allowed',
650
- }),
651
- "dnssec-validate": commandSeed({
652
- reference: {
653
- description: 'Cryptographically validate one authorised DNSSEC chain from a supplied trust anchor through one selected public resolver.',
654
- example: 'whoisleuth dnssec-validate example.test --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --json',
655
- boundary: 'This isolated action is never invoked by Lookup, Bulk, monitoring, or recipes. It caps DNS queries, aliases, delegations, bytes, and duration; transport and validation failures remain separate, and secure is not a general safety verdict.',
656
- },
657
- collection: { mode: 'network', scope: 'Accepts one authorised domain, one public resolver IP, and one local trust-anchor file; DNS-over-TCP validation is capped at 32 queries and 15 seconds.' },
658
- summary: 'Validate an authorised DNSSEC chain',
659
- options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--json', '--quiet', '--no-color'],
660
- positionals: Object.freeze([positional('domain', 'text', 1, 1)]),
661
- constraints: Object.freeze([
662
- constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized'] }),
663
- ]),
664
- handlerOwner: 'network',
665
- networkEffect: 'always_network',
666
- common: false,
667
- schemaIdentifiers: Object.freeze(['whoisleuth.dnssec-chain-validation', 'whoisleuth.dnssec-trust-anchor']),
668
- primaryArtefacts: Object.freeze([]),
669
- planSupport: false,
670
- additionalOutputFormats: Object.freeze([]),
671
- bootstrapProfile: 'allowed',
672
- }),
673
- "mail-transport": commandSeed({
674
- reference: {
675
- description: 'Review selected authorised MX endpoints, DNSSEC-qualified TLSA evidence, SMTP capabilities, and optional STARTTLS certificates.',
676
- example: 'whoisleuth mail-transport selected-mx.json --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --active-probe --json',
677
- boundary: 'This isolated action probes at most three selected MX hosts sequentially, reports selection, public revalidation, connection, and address authentication separately, sends only EHLO and optional STARTTLS, never retries, and performs no authentication, relay, recipient, mailbox, catch-all, or message test. If a DANE-TA TLSA usage 2 association is published, active collection retains only the leaf certificate and leaves that comparison partial without certificate-path construction and trust-anchor path validation. SMTP relay PKIX-TA usage 0 and PKIX-EE usage 1 records remain unsupported and cannot complete SMTP DANE assurance; a separate usage 3 match remains eligible.',
678
- },
679
- collection: { mode: 'network', scope: 'Accepts at most three selected authorised MX hosts, uses one public resolver, and performs sequential bounded SMTP connections with no retries.' },
680
- summary: 'Review selected authorised SMTP transports',
681
- options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe', '--json', '--quiet', '--no-color'],
682
- positionals: OPTIONAL_FILE_POSITIONAL,
683
- constraints: Object.freeze([
684
- constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe'] }),
685
- ]),
686
- handlerOwner: 'network',
687
- networkEffect: 'always_network',
688
- common: false,
689
- schemaIdentifiers: Object.freeze(['whoisleuth.mail-transport.input', 'whoisleuth.cli.mail-transport-review']),
690
- primaryArtefacts: Object.freeze([]),
691
- planSupport: false,
692
- additionalOutputFormats: Object.freeze([]),
693
- bootstrapProfile: 'allowed',
694
- }),
695
- "registry-support": commandSeed({
696
- reference: {
697
- description: 'Explain the local registry capability profile for one domain or suffix.',
698
- example: 'whoisleuth registry-support example.test --json',
699
- boundary: 'This command is offline. Catalogue coverage does not test live reachability or decide registration or availability.',
700
- },
701
- collection: { mode: 'offline', scope: 'Reads the embedded registry capability catalogue for one domain or suffix.' },
702
- summary: 'Explain local registry coverage',
703
- options: ['--json', '--quiet', '--no-color'],
704
- positionals: Object.freeze([positional('domain-or-suffix', 'text', 0, 1, [], 'argv_or_stdin')]),
705
- constraints: EMPTY_CONSTRAINTS,
706
- handlerOwner: 'inline',
707
- networkEffect: 'offline',
708
- common: false,
709
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.registry-support', 'whoisleuth.registry-standards-coverage']),
710
- primaryArtefacts: Object.freeze([]),
711
- planSupport: false,
712
- additionalOutputFormats: Object.freeze([]),
713
- bootstrapProfile: 'allowed',
714
- }),
715
- "registry-doctor": commandSeed({
716
- reference: {
717
- description: 'Compare a saved Lookup registry result with the reviewed local capability profile.',
718
- example: 'whoisleuth registry-doctor lookup.json --json',
719
- boundary: 'The command is offline. It distinguishes expected access constraints from collection results and does not contact a live registry.',
720
- },
721
- collection: { mode: 'offline', scope: 'Reads one saved Lookup and the embedded registry capability catalogue.' },
722
- summary: 'Diagnose saved registry collection',
723
- options: ['--json', '--quiet', '--no-color'],
724
- positionals: OPTIONAL_FILE_POSITIONAL,
725
- constraints: EMPTY_CONSTRAINTS,
726
- handlerOwner: 'inline',
727
- networkEffect: 'offline',
728
- common: false,
729
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.registry-doctor']),
730
- primaryArtefacts: Object.freeze([]),
731
- planSupport: false,
732
- additionalOutputFormats: Object.freeze([]),
733
- bootstrapProfile: 'allowed',
734
- }),
735
- "registry-cohort": commandSeed({
736
- reference: {
737
- description: 'Build privacy-safe suffix and capability-profile timelines from saved observations or retained cohort reports.',
738
- example: 'whoisleuth registry-cohort saved-lookups.jsonl --json',
739
- boundary: 'This command is offline and omits domains, queries, and raw evidence. Input families cannot be mixed, and retained samples are never assumed independent.',
740
- },
741
- collection: { mode: 'offline', scope: 'Reads at most 500 saved Lookups or retained cohort reports from one unmixed family and emits bounded target-free timelines.' },
742
- summary: 'Build target-free registry quality timelines',
743
- options: ['--json', '--quiet', '--no-color'],
744
- positionals: OPTIONAL_FILE_POSITIONAL,
745
- constraints: EMPTY_CONSTRAINTS,
746
- handlerOwner: 'inline',
747
- networkEffect: 'offline',
748
- common: false,
749
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.registry-cohort']),
750
- primaryArtefacts: Object.freeze([]),
751
- planSupport: false,
752
- additionalOutputFormats: Object.freeze([]),
753
- bootstrapProfile: 'allowed',
754
- }),
755
- "registry-scaffold": commandSeed({
756
- reference: {
757
- description: 'Create a bounded synthetic WHOIS fixture scaffold for one existing capability profile.',
758
- example: 'whoisleuth registry-scaffold --profile example-profile --suffix test --scenario registered',
759
- boundary: 'The output is a sanitised template only. Its command-owned --profile selects fixture capability, --config is rejected, and contributors must not paste live responses or personal registration data into fixtures.',
760
- },
761
- collection: { mode: 'offline', scope: 'Reads the embedded registry capability catalogue and prints one synthetic fixture template.' },
762
- summary: 'Create a sanitised registry fixture scaffold',
763
- options: ['--profile', '--suffix', '--scenario'],
764
- positionals: NO_POSITIONALS,
765
- constraints: Object.freeze([
766
- constraint({ kind: 'required', options: ['--profile', '--suffix', '--scenario'] }),
767
- ]),
768
- handlerOwner: 'inline',
769
- networkEffect: 'offline',
770
- common: false,
771
- schemaIdentifiers: Object.freeze([]),
772
- primaryArtefacts: Object.freeze([]),
773
- planSupport: false,
774
- additionalOutputFormats: Object.freeze([]),
775
- bootstrapProfile: 'command_owned',
776
- }),
777
- "risk-calibrate": commandSeed({
778
- reference: {
779
- description: 'Replay reviewed labels against the current explainable Risk model.',
780
- example: 'whoisleuth risk-calibrate calibration.json --summary-json',
781
- boundary: 'Calibration is offline and diagnostic. The summary form omits record identifiers, domains, and evidence; neither form trains, tunes, or changes the scoring model automatically.',
782
- },
783
- collection: { mode: 'offline', scope: 'Reads one bounded reviewed-label dataset and changes no model or evidence.' },
784
- summary: 'Replay reviewed Risk labels offline',
785
- options: ['--json', '--summary-json', '--quiet', '--no-color'],
786
- positionals: OPTIONAL_FILE_POSITIONAL,
787
- constraints: Object.freeze([
788
- constraint({ kind: 'mutually_exclusive', options: ['--json', '--summary-json'] }),
789
- ]),
790
- handlerOwner: 'inline',
791
- networkEffect: 'offline',
792
- common: false,
793
- schemaIdentifiers: Object.freeze([RISK_CALIBRATION_DATASET_SCHEMA, RISK_CALIBRATION_REPORT_SCHEMA]),
794
- primaryArtefacts: Object.freeze([]),
795
- planSupport: false,
796
- additionalOutputFormats: Object.freeze([]),
797
- bootstrapProfile: 'allowed',
798
- }),
799
- "lookalike-calibrate": commandSeed({
800
- reference: {
801
- description: 'Summarise reviewed candidate dispositions by mutation family without retaining domains.',
802
- example: 'whoisleuth lookalike-calibrate reviewed-candidates.json --json',
803
- boundary: 'Calibration is offline and diagnostic. It omits candidate identifiers, domains, notes, and evidence and never tunes generation or filtering automatically.',
804
- },
805
- collection: { mode: 'offline', scope: 'Reads at most 5,000 reviewed candidate labels from one dataset capped at 2 MiB.' },
806
- summary: 'Summarise reviewed lookalike yield offline',
807
- options: ['--json', '--quiet', '--no-color'],
808
- positionals: OPTIONAL_FILE_POSITIONAL,
809
- constraints: EMPTY_CONSTRAINTS,
810
- handlerOwner: 'inline',
811
- networkEffect: 'offline',
812
- common: false,
813
- schemaIdentifiers: Object.freeze(['whoisleuth.lookalike-calibration-input', 'whoisleuth.lookalike-calibration']),
814
- primaryArtefacts: Object.freeze([]),
815
- planSupport: false,
816
- additionalOutputFormats: Object.freeze([]),
817
- bootstrapProfile: 'allowed',
818
- }),
819
- "verify-artifact": commandSeed({
820
- reference: {
821
- description: 'Validate a supported archive, claim passport, packet, manifest, saved Lookup, or supported Lookup-evidence export without printing evidence contents.',
822
- example: 'whoisleuth verify-artifact report.json --manifest manifest.json --manifest-entry artifact-2 --json --strict-exit',
823
- boundary: 'Verification is offline and redacted. Encrypted archives require an explicitly supplied passphrase file; --strict-exit returns 4 when only an envelope or legacy projection integrity was verified.',
824
- },
825
- collection: { mode: 'offline', scope: 'Reads one selected bounded artefact and, when explicitly supplied, one manifest whose selected entry is compared by exact bytes and canonical identity.' },
826
- summary: 'Validate saved evidence offline',
827
- options: ['--passphrase-file', '--manifest', '--manifest-entry', '--json', '--strict-exit', '--quiet', '--no-color'],
828
- positionals: OPTIONAL_FILE_POSITIONAL,
829
- constraints: Object.freeze([
830
- constraint({ kind: 'requires_all', option: '--manifest', requiredOptions: ['--manifest-entry'] }),
831
- constraint({ kind: 'requires_all', option: '--manifest-entry', requiredOptions: ['--manifest'] }),
832
- ]),
833
- handlerOwner: 'inline',
834
- networkEffect: 'offline',
835
- common: true,
836
- schemaIdentifiers: Object.freeze(['whoisleuth.offline-artifact-verification']),
837
- primaryArtefacts: Object.freeze(['Offline verification report']),
838
- planSupport: false,
839
- additionalOutputFormats: Object.freeze([]),
840
- bootstrapProfile: 'allowed',
841
- }),
842
- "interchange-report": commandSeed({
843
- reference: {
844
- description: 'Report what one recognised portable artefact preserves, excludes, and supports across browser and CLI workflows.',
845
- example: 'whoisleuth interchange-report workspace.json --json',
846
- boundary: 'The report is offline and metadata-only. It does not echo targets, contacts, notes, passphrases, evidence values, or an unrecognised schema string.',
847
- },
848
- collection: { mode: 'offline', scope: 'Reads one selected bounded portable artefact and emits fixed compatibility metadata only.' },
849
- summary: 'Report portable artefact fidelity offline',
850
- options: ['--passphrase-file', '--json', '--quiet', '--no-color'],
851
- positionals: OPTIONAL_FILE_POSITIONAL,
852
- constraints: EMPTY_CONSTRAINTS,
853
- handlerOwner: 'inline',
854
- networkEffect: 'offline',
855
- common: false,
856
- schemaIdentifiers: Object.freeze(['whoisleuth.interchange-fidelity-report']),
857
- primaryArtefacts: Object.freeze([]),
858
- planSupport: false,
859
- additionalOutputFormats: Object.freeze([]),
860
- bootstrapProfile: 'allowed',
861
- }),
862
- "inspect-archive": commandSeed({
863
- reference: {
864
- description: `Summarise or search one current version-${WORKSPACE_ARCHIVE_VERSION} workspace archive, with exact ${LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION} support and redacted output by default.`,
865
- example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
866
- boundary: 'Exact matches require --reveal. Retired and future archive versions are rejected without changing data. The archive is read locally and is never uploaded.',
867
- },
868
- collection: { mode: 'offline', scope: `Reads one selected bounded workspace archive v${WORKSPACE_ARCHIVE_VERSION}, retains exact ${LEGACY_WORKSPACE_ARCHIVE_SCOPE} compatibility, and redacts output by default.` },
869
- summary: 'Inspect an archive locally',
870
- options: ['--passphrase-file', '--search', '--require-match', '--reveal', '--expect-content-digest', '--json', '--quiet', '--no-color'],
871
- positionals: OPTIONAL_FILE_POSITIONAL,
872
- constraints: Object.freeze([
873
- constraint({ kind: 'requires_all', option: '--reveal', requiredOptions: ['--search'] }),
874
- constraint({ kind: 'requires_all', option: '--require-match', requiredOptions: ['--search'] }),
875
- ]),
876
- handlerOwner: 'evidence',
877
- networkEffect: 'offline',
878
- common: false,
879
- schemaIdentifiers: Object.freeze(['whoisleuth.workspace-archive-inspection']),
880
- primaryArtefacts: Object.freeze([]),
881
- planSupport: false,
882
- additionalOutputFormats: Object.freeze([]),
883
- bootstrapProfile: 'allowed',
884
- }),
885
- "sign-artifact": commandSeed({
886
- reference: {
887
- description: 'Sign one reviewed response packet or supported manifest with a local private key.',
888
- example: 'whoisleuth sign-artifact packet.json --private-key-file analyst-private.pem',
889
- boundary: 'The command never creates, stores, or transmits keys. Key custody and signer identity remain the operator\'s responsibility.',
890
- },
891
- collection: { mode: 'offline', scope: 'Reads one selected artefact and one local private key without transmitting either.' },
892
- summary: 'Sign a reviewed artefact locally',
893
- options: ['--private-key-file'],
894
- positionals: OPTIONAL_FILE_POSITIONAL,
895
- constraints: Object.freeze([
896
- constraint({ kind: 'required', options: ['--private-key-file'] }),
897
- ]),
898
- handlerOwner: 'evidence',
899
- networkEffect: 'offline',
900
- common: false,
901
- schemaIdentifiers: Object.freeze(['whoisleuth.signed-evidence-package']),
902
- primaryArtefacts: Object.freeze([]),
903
- planSupport: false,
904
- additionalOutputFormats: Object.freeze([]),
905
- bootstrapProfile: 'allowed',
906
- }),
907
- "verify-signature": commandSeed({
908
- reference: {
909
- description: 'Verify the cryptographic signature of one signed evidence package and report embedded-artefact assurance separately.',
910
- example: 'whoisleuth verify-signature packet.signed.json --json',
911
- boundary: 'A valid signature proves package consistency for the embedded key. It does not upgrade failed or unsupported embedded-artefact assurance or establish the holder\'s real-world identity or authority.',
912
- },
913
- collection: { mode: 'offline', scope: 'Reads one selected signed package and optional local public key.' },
914
- summary: 'Verify a signed evidence package',
915
- options: ['--public-key-file', '--json', '--quiet', '--no-color'],
916
- positionals: OPTIONAL_FILE_POSITIONAL,
917
- constraints: EMPTY_CONSTRAINTS,
918
- handlerOwner: 'evidence',
919
- networkEffect: 'offline',
920
- common: false,
921
- schemaIdentifiers: Object.freeze(['whoisleuth.evidence-signature-verification']),
922
- primaryArtefacts: Object.freeze([]),
923
- planSupport: false,
924
- additionalOutputFormats: Object.freeze([]),
925
- bootstrapProfile: 'allowed',
926
- }),
927
- "source-report": commandSeed({
928
- reference: {
929
- description: 'Create a target-free reliability summary from a saved lookup.',
930
- example: 'whoisleuth source-report lookup.json --json',
931
- boundary: 'The report retains source states and timings but excludes targets, queries, endpoints, and raw evidence.',
932
- },
933
- collection: { mode: 'offline', scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
934
- summary: 'Build a target-free source report',
935
- options: ['--json', '--quiet', '--no-color'],
936
- positionals: OPTIONAL_FILE_POSITIONAL,
937
- constraints: EMPTY_CONSTRAINTS,
938
- handlerOwner: 'inline',
939
- networkEffect: 'offline',
940
- common: false,
941
- schemaIdentifiers: Object.freeze(['whoisleuth.source-reliability-report']),
942
- primaryArtefacts: Object.freeze([]),
943
- planSupport: false,
944
- additionalOutputFormats: Object.freeze([]),
945
- bootstrapProfile: 'allowed',
946
- }),
947
- compare: commandSeed({
948
- reference: {
949
- description: 'Compare separately attributed registry publications in a saved lookup.',
950
- example: 'whoisleuth compare lookup.json --json',
951
- boundary: 'Comparison is offline. Differences are review context and do not by themselves prove which publication is current.',
952
- },
953
- collection: { mode: 'offline', scope: 'Reads one saved Lookup and compares its separately attributed registry publications.' },
954
- summary: 'Compare registry publications in one lookup',
955
- options: ['--json', '--quiet', '--no-color'],
956
- positionals: OPTIONAL_FILE_POSITIONAL,
957
- constraints: EMPTY_CONSTRAINTS,
958
- handlerOwner: 'inline',
959
- networkEffect: 'offline',
960
- common: false,
961
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.compare']),
962
- primaryArtefacts: Object.freeze([]),
963
- planSupport: false,
964
- additionalOutputFormats: Object.freeze([]),
965
- bootstrapProfile: 'allowed',
966
- }),
967
- "page-compare": commandSeed({
968
- reference: {
969
- description: 'Compare static page identity, favicon, technology, and TLS evidence in two saved deep lookups.',
970
- example: 'whoisleuth page-compare official.json candidate.json --json',
971
- boundary: 'Comparison is offline and component-based. It executes no page code and produces no aggregate similarity or maliciousness score.',
972
- },
973
- collection: { mode: 'offline', scope: 'Reads two saved Lookup documents and executes no page code.' },
974
- summary: 'Compare saved static page evidence',
975
- options: ['--json', '--quiet', '--no-color'],
976
- positionals: Object.freeze([positional('sources', 'file', 2, 2)]),
977
- constraints: EMPTY_CONSTRAINTS,
978
- handlerOwner: 'inline',
979
- networkEffect: 'offline',
980
- common: false,
981
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.page-compare']),
982
- primaryArtefacts: Object.freeze([]),
983
- planSupport: false,
984
- additionalOutputFormats: Object.freeze([]),
985
- bootstrapProfile: 'allowed',
986
- }),
987
- "mail-review": commandSeed({
988
- reference: {
989
- description: 'Review passive MX, null MX, SPF, DMARC, and shared mail-provider evidence from saved Bulk results.',
990
- example: 'whoisleuth mail-review candidates.json --json',
991
- boundary: 'Review is offline and sends no SMTP traffic. Missing or partial DNS evidence remains inconclusive.',
992
- },
993
- collection: { mode: 'offline', scope: 'Reads one saved Bulk result and sends no DNS or SMTP traffic.' },
994
- summary: 'Review saved passive mail evidence',
995
- options: ['--json', '--quiet', '--no-color'],
996
- positionals: OPTIONAL_FILE_POSITIONAL,
997
- constraints: EMPTY_CONSTRAINTS,
998
- handlerOwner: 'inline',
999
- networkEffect: 'offline',
1000
- common: false,
1001
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.mail-review']),
1002
- primaryArtefacts: Object.freeze([]),
1003
- planSupport: false,
1004
- additionalOutputFormats: Object.freeze([]),
1005
- bootstrapProfile: 'allowed',
1006
- }),
1007
- "mail-headers": commandSeed({
1008
- reference: {
1009
- description: 'Review identity, reported authentication, domain alignment, and Received routing from selected message headers.',
1010
- example: 'whoisleuth mail-headers message.eml --json',
1011
- boundary: 'Review is offline. It makes no DNS, SMTP, HTTP, registry, or provider request, and does not retain address local parts, display names, subjects, message bodies, attachments, or raw header values. Reported authentication is not independently validated.',
1012
- },
1013
- collection: { mode: 'offline', scope: 'Parses only the bounded header block from one selected message or standard input.' },
1014
- summary: 'Review message headers offline',
1015
- options: ['--json', '--quiet', '--no-color'],
1016
- positionals: OPTIONAL_FILE_POSITIONAL,
1017
- constraints: EMPTY_CONSTRAINTS,
1018
- handlerOwner: 'inline',
1019
- networkEffect: 'offline',
1020
- common: false,
1021
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.mail-header-review']),
1022
- primaryArtefacts: Object.freeze([]),
1023
- planSupport: false,
1024
- additionalOutputFormats: Object.freeze([]),
1025
- bootstrapProfile: 'allowed',
1026
- }),
1027
- "review-evidence": commandSeed({
1028
- reference: {
1029
- description: 'Review one versioned DNS, domain-change, routing, GeoIP, RDAP, or trust-store document offline.',
1030
- example: 'whoisleuth review-evidence domain-change.json --json --strict-exit',
1031
- boundary: 'The command reads only the supplied document. It performs no DNS, RDAP, BGP, GeoIP-provider, TLS, HTTP, certificate-authority, or SMTP request.',
1032
- },
1033
- collection: { mode: 'offline', scope: 'Reads one bounded versioned evidence or request-planning document and performs no collection.' },
1034
- summary: 'Review supplied evidence offline',
1035
- options: ['--mmdb', '--json', '--strict-exit', '--quiet', '--no-color'],
1036
- positionals: OPTIONAL_FILE_POSITIONAL,
1037
- constraints: EMPTY_CONSTRAINTS,
1038
- handlerOwner: 'inline',
1039
- networkEffect: 'offline',
1040
- common: true,
1041
- schemaIdentifiers: Object.freeze([
1042
- 'whoisleuth.cli.offline-evidence-review',
1043
- 'whoisleuth.rdap-search-input',
1044
- 'whoisleuth.dnssec-evidence-input',
1045
- 'whoisleuth.tlsa-evidence-input',
1046
- 'whoisleuth.rpki-route-input',
1047
- 'whoisleuth.local-geoip-query',
1048
- 'whoisleuth.encrypted-dns-plan-input',
1049
- ]),
1050
- primaryArtefacts: Object.freeze([]),
1051
- planSupport: false,
1052
- additionalOutputFormats: Object.freeze([]),
1053
- bootstrapProfile: 'allowed',
1054
- }),
1055
- brief: commandSeed({
1056
- reference: {
1057
- description: 'Turn one saved Lookup into a compact decision brief with facts, unknowns, contradictions, and next actions.',
1058
- example: 'whoisleuth brief lookup.json --json',
1059
- boundary: 'The command is offline, excludes raw upstream payloads, and does not create an analyst assertion or claim that the saved observation is current.',
1060
- },
1061
- collection: { mode: 'offline', scope: 'Reads one bounded saved Lookup and emits a compact source-attributed decision brief.' },
1062
- summary: 'Build a decision brief from a saved lookup',
1063
- options: ['--json', '--quiet', '--no-color'],
1064
- positionals: OPTIONAL_FILE_POSITIONAL,
1065
- constraints: EMPTY_CONSTRAINTS,
1066
- handlerOwner: 'inline',
1067
- networkEffect: 'offline',
1068
- common: false,
1069
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-brief']),
1070
- primaryArtefacts: Object.freeze([]),
1071
- planSupport: false,
1072
- additionalOutputFormats: Object.freeze([]),
1073
- bootstrapProfile: 'allowed',
1074
- }),
1075
- "case-pack": commandSeed({
1076
- reference: {
1077
- description: `Package browser-created Case-schema-${CASE_SCHEMA_VERSION} records as a reviewed, audience-specific Case-pack v2.`,
1078
- example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
1079
- boundary: 'The command is an offline handoff from the browser Case workflow: it creates a new package, never creates or mutates a durable Case, never mutates the source archive, and requires an explicit review acknowledgement.',
1080
- },
1081
- collection: { mode: 'offline', scope: `Reads one bounded Case-schema-${CASE_SCHEMA_VERSION} browser export and writes a separate audience-specific Case-pack v2.` },
1082
- summary: 'Build a reviewed case package',
1083
- options: ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
1084
- positionals: OPTIONAL_FILE_POSITIONAL,
1085
- constraints: Object.freeze([
1086
- constraint({ kind: 'required', options: ['--audience', '--reviewed'] }),
1087
- ]),
1088
- handlerOwner: 'inline',
1089
- networkEffect: 'offline',
1090
- common: true,
1091
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.case-pack', 'whoisleuth.case-report']),
1092
- primaryArtefacts: Object.freeze(['Reviewed Case-pack v2']),
1093
- planSupport: false,
1094
- additionalOutputFormats: Object.freeze([]),
1095
- bootstrapProfile: 'allowed',
1096
- }),
1097
- "domain-control": commandSeed({
1098
- reference: {
1099
- description: 'Build an integrity-protected desired-state manifest or compare one with supplied observations.',
1100
- example: 'whoisleuth domain-control domain-control-input.json --json',
1101
- boundary: 'The command is offline and changes no registrar, DNS, mail, or certificate configuration. Only complete supplied observations can produce drift.',
1102
- },
1103
- collection: { mode: 'offline', scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
1104
- summary: 'Build or review a domain control manifest',
1105
- options: ['--json', '--quiet', '--no-color'],
1106
- positionals: OPTIONAL_FILE_POSITIONAL,
1107
- constraints: EMPTY_CONSTRAINTS,
1108
- handlerOwner: 'inline',
1109
- networkEffect: 'offline',
1110
- common: false,
1111
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.domain-control-review-input', 'whoisleuth.cli.domain-control-review']),
1112
- primaryArtefacts: Object.freeze([]),
1113
- planSupport: false,
1114
- additionalOutputFormats: Object.freeze([]),
1115
- bootstrapProfile: 'allowed',
1116
- }),
1117
- "monitor-once": commandSeed({
1118
- reference: {
1119
- description: 'Collect one bounded owned-domain review and compare it with an optional prior checkpoint.',
1120
- example: 'whoisleuth monitor-once manifest.json --previous previous.json --json --output next.json',
1121
- boundary: 'This is an operator-scheduled one-shot collection, not a daemon. It caps targets and concurrency, retains normalised observations, and never changes domain configuration.',
1122
- },
1123
- collection: { mode: 'network', scope: 'Runs deep collection for at most 20 manifest domains with concurrency capped at 3.' },
1124
- summary: 'Run one bounded domain control review',
1125
- options: ['--previous', '--limit', '--concurrency', '--fail-on', '--json', '--junit', '--quiet', '--no-color'],
1126
- positionals: OPTIONAL_FILE_POSITIONAL,
1127
- constraints: Object.freeze([
1128
- constraint({ kind: 'mutually_exclusive', options: ['--json', '--junit'] }),
1129
- ]),
1130
- handlerOwner: 'inline',
1131
- networkEffect: 'always_network',
1132
- common: false,
1133
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.domain-control-monitor', 'whoisleuth.domain-control-flight-recorder.input']),
1134
- primaryArtefacts: Object.freeze([]),
1135
- planSupport: false,
1136
- additionalOutputFormats: Object.freeze([]),
1137
- bootstrapProfile: 'allowed',
1138
- }),
1139
- assurance: commandSeed({
1140
- reference: {
1141
- description: 'Review a versioned domain change, recovery-dependency, or retirement plan.',
1142
- example: 'whoisleuth assurance domain-assurance.json --json',
1143
- boundary: 'The command is offline and treats every provider label, readiness state, and evidence reference as analyst-authored input. It changes no configuration.',
1144
- },
1145
- collection: { mode: 'offline', scope: 'Reads one versioned plan capped at 2 MiB and makes no request or configuration change.' },
1146
- summary: 'Review domain change, recovery, or retirement plans',
1147
- options: ['--json', '--quiet', '--no-color'],
1148
- positionals: OPTIONAL_FILE_POSITIONAL,
1149
- constraints: EMPTY_CONSTRAINTS,
1150
- handlerOwner: 'inline',
1151
- networkEffect: 'offline',
1152
- common: false,
1153
- schemaIdentifiers: Object.freeze(['whoisleuth.domain-assurance.input', 'whoisleuth.domain-assurance']),
1154
- primaryArtefacts: Object.freeze([]),
1155
- planSupport: false,
1156
- additionalOutputFormats: Object.freeze([]),
1157
- bootstrapProfile: 'allowed',
1158
- }),
1159
- "change-packet": commandSeed({
1160
- reference: {
1161
- description: 'Assemble pre-change, post-change, and planning evidence into one integrity-protected packet.',
1162
- example: 'whoisleuth change-packet change-review.json --json',
1163
- boundary: 'Assembly is offline. Readiness reflects only the supplied bounded evidence and does not authorise or perform a domain change.',
1164
- },
1165
- collection: { mode: 'offline', scope: 'Reads one versioned packet input capped at 6 MiB and makes no request or configuration change.' },
1166
- summary: 'Build a reviewed change packet offline',
1167
- options: ['--json', '--quiet', '--no-color'],
1168
- positionals: OPTIONAL_FILE_POSITIONAL,
1169
- constraints: EMPTY_CONSTRAINTS,
1170
- handlerOwner: 'inline',
1171
- networkEffect: 'offline',
1172
- common: false,
1173
- schemaIdentifiers: Object.freeze(['whoisleuth.domain-change-packet.input', 'whoisleuth.domain-change-packet']),
1174
- primaryArtefacts: Object.freeze([]),
1175
- planSupport: false,
1176
- additionalOutputFormats: Object.freeze([]),
1177
- bootstrapProfile: 'allowed',
1178
- }),
1179
- "sharing-review": commandSeed({
1180
- reference: {
1181
- description: 'Lint one reviewed artefact against local integrity, marking, recipient, personal-data, and redaction controls.',
1182
- example: 'whoisleuth sharing-review packet.json --marking amber --recipient-scope organization --purpose "Reviewed incident handoff" --human-reviewed --personal-data-reviewed --redactions-confirmed --json',
1183
- boundary: 'The command is offline and emits only bounded schema/version metadata, no content values, and no raw evidence. Its result is a review aid, not legal advice or recipient authorisation.',
1184
- },
1185
- collection: { mode: 'offline', scope: 'Reads one artefact capped at 15 MiB, emits only bounded schema/version metadata and no content values, and performs no transmission.' },
1186
- summary: 'Lint an artefact before deliberate sharing',
1187
- options: ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
1188
- positionals: OPTIONAL_FILE_POSITIONAL,
1189
- constraints: Object.freeze([
1190
- constraint({ kind: 'required', options: ['--marking', '--recipient-scope', '--purpose'] }),
1191
- ]),
1192
- handlerOwner: 'inline',
1193
- networkEffect: 'offline',
1194
- common: false,
1195
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.sharing-review']),
1196
- primaryArtefacts: Object.freeze([]),
1197
- planSupport: false,
1198
- additionalOutputFormats: Object.freeze([]),
1199
- bootstrapProfile: 'allowed',
1200
- }),
1201
- "workflow-plan": commandSeed({
1202
- reference: {
1203
- description: 'Build a fixed domain-investigation plan from existing bounded CLI commands.',
1204
- example: 'whoisleuth workflow-plan domain-triage example.test --json',
1205
- boundary: 'Planning is offline and plan-only. It does not execute commands, expand placeholders, read files, make requests, or submit evidence.',
1206
- },
1207
- collection: { mode: 'offline', scope: 'Builds a fixed typed recipe and executes none of its network or file steps.' },
1208
- summary: 'Plan a fixed investigation recipe',
1209
- options: ['--list', '--explain', '--json', '--quiet', '--no-color'],
1210
- positionals: Object.freeze([
1211
- positional('recipe', 'enum', 0, 1, INVESTIGATION_PLAN_RECIPES),
1212
- positional('subject', 'text', 0, 1),
1213
- ]),
1214
- constraints: Object.freeze([
1215
- constraint({ kind: 'mutually_exclusive', options: ['--list', '--explain'] }),
1216
- ]),
1217
- handlerOwner: 'inline',
1218
- networkEffect: 'offline',
1219
- common: true,
1220
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.investigation-plan', 'whoisleuth.cli.workflow-recipe-catalogue']),
1221
- primaryArtefacts: Object.freeze(['Plan-only workflow document']),
1222
- planSupport: true,
1223
- additionalOutputFormats: Object.freeze([]),
1224
- bootstrapProfile: 'allowed',
1225
- }),
1226
- "workflow-run": commandSeed({
1227
- reference: {
1228
- description: 'Execute approved steps from a fixed investigation recipe and emit a resumable checkpoint.',
1229
- example: 'whoisleuth workflow-run domain-triage example.test --resume run.json --select export=saved-lookup.json --json --output run-next.json',
1230
- boundary: 'Only installed recipe commands can run. Network steps require explicit approval for each invocation. Repeat --select in placeholder order for one step; each bounded value replaces one exact placeholder and cannot start with a hyphen, become an option, or invoke a shell.',
1231
- },
1232
- collection: { mode: 'network', scope: 'Runs only fixed-recipe steps; network collection requires --approve-network and unresolved analyst selections pause.' },
1233
- summary: 'Execute approved fixed-recipe steps',
1234
- options: ['--select', '--approve-network', '--resume', '--json', '--quiet', '--no-color'],
1235
- positionals: Object.freeze([
1236
- positional('recipe', 'enum', 1, 1, RUNNABLE_INVESTIGATION_PLAN_RECIPES),
1237
- positional('subject', 'text', 1, 1),
1238
- ]),
1239
- constraints: EMPTY_CONSTRAINTS,
1240
- handlerOwner: 'inline',
1241
- networkEffect: 'conditional_network',
1242
- common: false,
1243
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.investigation-run']),
1244
- primaryArtefacts: Object.freeze(['Resumable workflow state']),
1245
- planSupport: false,
1246
- additionalOutputFormats: Object.freeze([]),
1247
- bootstrapProfile: 'allowed',
1248
- }),
1249
- diff: commandSeed({
1250
- reference: {
1251
- description: 'Compare an earlier and later artefact from the same retained Lookup, Bulk-session, or domain-portfolio family.',
1252
- example: 'whoisleuth diff earlier.json later.json --json',
1253
- boundary: 'Comparison is offline: the left input is earlier and the right input is later. Inputs must belong to the same supported family. For a multi-session Bulk export, --left-session selects a session from the left file and --right-session selects one from the right; missing, unavailable, equal, and different evidence remain separate states.',
1254
- },
1255
- collection: { mode: 'offline', scope: 'Reads two compatible retained artefacts capped at 8 MiB each and retains no source paths.' },
1256
- summary: 'Compare two compatible retained artefacts',
1257
- options: ['--left-session', '--right-session', '--json', '--quiet', '--no-color'],
1258
- positionals: Object.freeze([positional('sources', 'file', 2, 2)]),
1259
- constraints: EMPTY_CONSTRAINTS,
1260
- handlerOwner: 'inline',
1261
- networkEffect: 'offline',
1262
- common: true,
1263
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-diff']),
1264
- primaryArtefacts: Object.freeze(['Retained-evidence comparison']),
1265
- planSupport: false,
1266
- additionalOutputFormats: Object.freeze([]),
1267
- bootstrapProfile: 'allowed',
1268
- }),
1269
- reconcile: commandSeed({
1270
- reference: {
1271
- description: 'Reconcile bounded values across independently labelled observations of one domain.',
1272
- example: 'whoisleuth reconcile office.json mobile.json external.json --json',
1273
- boundary: 'The command is offline, accepts 2 to 5 saved observations for one domain, and never treats labels as proof of network independence or majority agreement as truth.',
1274
- },
1275
- collection: { mode: 'offline', scope: 'Reads 2 to 5 saved observations for one domain, capped at 32 MiB in total.' },
1276
- summary: 'Reconcile independently labelled observations',
1277
- options: ['--json', '--quiet', '--no-color'],
1278
- positionals: Object.freeze([positional('sources', 'file', 2, 5)]),
1279
- constraints: EMPTY_CONSTRAINTS,
1280
- handlerOwner: 'inline',
1281
- networkEffect: 'offline',
1282
- common: false,
1283
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-reconciliation']),
1284
- primaryArtefacts: Object.freeze([]),
1285
- planSupport: false,
1286
- additionalOutputFormats: Object.freeze([]),
1287
- bootstrapProfile: 'allowed',
1288
- }),
1289
- timeline: commandSeed({
1290
- reference: {
1291
- description: 'Build an ordered same-domain history from saved Lookup observations.',
1292
- example: 'whoisleuth timeline first.json second.json latest.json --json',
1293
- boundary: 'The command is offline, accepts 2 to 20 bounded inputs for one domain, retains no filenames or raw registry payloads, and does not treat changed collection conditions as a domain change.',
1294
- },
1295
- collection: { mode: 'offline', scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
1296
- summary: 'Build same-domain history from saved lookups',
1297
- options: ['--json', '--quiet', '--no-color'],
1298
- positionals: Object.freeze([positional('sources', 'file', 2, 20)]),
1299
- constraints: EMPTY_CONSTRAINTS,
1300
- handlerOwner: 'inline',
1301
- networkEffect: 'offline',
1302
- common: false,
1303
- schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-timeline']),
1304
- primaryArtefacts: Object.freeze(['Bounded retained-observation timeline']),
1305
- planSupport: false,
1306
- additionalOutputFormats: Object.freeze([]),
1307
- bootstrapProfile: 'allowed',
1308
- }),
1309
- export: commandSeed({
1310
- reference: {
1311
- description: 'Convert one saved lookup into a versioned evidence report.',
1312
- example: 'whoisleuth export lookup.json --markdown',
1313
- boundary: `Saved Lookup versions 1 and 2 are capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Current schema-${LOOKUP_EVIDENCE_SCHEMA_VERSION} exports preserve evidence-source attribution and limitations; published v2 schema ${PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION} and exact v1 schema ${V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION} remain readable, while other historical and unreleased shapes are unsupported. Markdown and HTML include a presentation-only generator footer unless --no-attribution is selected; JSON retains bounded generator provenance. Compact output intentionally omits raw registry payloads.`,
1314
- },
1315
- collection: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
1316
- summary: 'Convert a lookup to an evidence report',
1317
- options: ['--markdown', '--html', '--compact', '--no-attribution'],
1318
- positionals: OPTIONAL_FILE_POSITIONAL,
1319
- constraints: Object.freeze([
1320
- constraint({ kind: 'mutually_exclusive', options: ['--markdown', '--html'] }),
1321
- constraint({ kind: 'excludes_all', option: '--compact', excludedOptions: ['--markdown', '--html'] }),
1322
- constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
1323
- ]),
1324
- handlerOwner: 'inline',
1325
- networkEffect: 'offline',
1326
- common: true,
1327
- schemaIdentifiers: Object.freeze(['whoisleuth.lookup-evidence']),
1328
- primaryArtefacts: Object.freeze(['Portable evidence report']),
1329
- planSupport: false,
1330
- additionalOutputFormats: Object.freeze(['JSON']),
1331
- bootstrapProfile: 'allowed',
1332
- }),
1333
- });
1334
- const COMMAND_ORDER = Object.freeze(Object.keys(COMMAND_SEEDS));
1335
19
  const HELP_COMMANDS_BY_GROUP = Object.freeze(Object.fromEntries(CLI_HELP_GROUP_ORDER.map((group) => [
1336
20
  group,
1337
21
  Object.freeze(COMMAND_ORDER.filter((command) => CLI_COMMAND_SEMANTICS[command].group === group)),
1338
22
  ])));
1339
23
  function documentationMetadata(seed, commonOptions, commandOptions, positionals) {
1340
24
  const explicitAuthorisationRequired = commandOptions.some((option) => (option === '--owned-or-authorized' || option === '--active-probe' || option === '--approve-network'));
1341
- const outputOptionFormats = [
1342
- ['--json', 'JSON'], ['--jsonl', 'JSON Lines'], ['--junit', 'JUnit XML'],
1343
- ['--csv', 'CSV'], ['--domains', 'domain list'], ['--queries', 'query list'],
1344
- ['--markdown', 'Markdown'], ['--html', 'HTML'], ['--sarif', 'SARIF'],
1345
- ['--summary-json', 'summary JSON'],
1346
- ];
25
+ const presentationOptions = Object.freeze(PRESENTATION_OPTIONS
26
+ .filter(([option]) => commandOptions.includes(option))
27
+ .map(([option, format]) => Object.freeze({ option, format })));
1347
28
  const outputFormats = new Set(['terminal']);
1348
- for (const [option, label] of outputOptionFormats) {
1349
- if (commandOptions.includes(option))
1350
- outputFormats.add(label);
1351
- }
29
+ for (const { format } of presentationOptions)
30
+ outputFormats.add(format);
1352
31
  for (const format of seed.additionalOutputFormats)
1353
32
  outputFormats.add(format);
1354
33
  const positionalLimits = positionals.map((item) => (`${item.name}: ${item.minimum}-${item.maximum} ${item.valueKind} value${item.maximum === 1 ? '' : 's'}`));
@@ -1373,14 +52,19 @@ function documentationMetadata(seed, commonOptions, commandOptions, positionals)
1373
52
  : []),
1374
53
  ]),
1375
54
  outputFormats: Object.freeze([...outputFormats]),
55
+ presentationOptions,
56
+ fileOutput: commonOptions.includes('--output'),
1376
57
  primaryEvidenceArtefacts: seed.primaryArtefacts,
1377
58
  });
1378
59
  }
60
+ function expandedOptionValues(specification) {
61
+ return specification.valueKind === 'enum' && specification.values.join('|').length > 60;
62
+ }
1379
63
  function optionUsage(specification) {
1380
64
  if (specification.arity === 0)
1381
65
  return specification.option;
1382
66
  if (specification.valueKind === 'enum') {
1383
- return `${specification.option} <${specification.values.join('|')}>`;
67
+ return `${specification.option} <${expandedOptionValues(specification) ? specification.option.slice(2) : specification.values.join('|')}>`;
1384
68
  }
1385
69
  if (specification.valueKind === 'policy_list')
1386
70
  return `${specification.option} <policy[,policy...]>`;
@@ -1417,11 +101,11 @@ function generatedCommandUsage(command, options, positionals, constraints) {
1417
101
  }
1418
102
  const CLI_COMMAND_REGISTRY = Object.freeze(COMMAND_ORDER.map((command, order) => {
1419
103
  const seed = COMMAND_SEEDS[command];
1420
- const commonOptions = Object.freeze([...commonOptionsSeedForCommand(command)]);
104
+ const commonOptions = Object.freeze([...commonOptionsForSeed(seed)]);
1421
105
  const commandOptions = seed.options;
1422
106
  const grammarOptions = Object.freeze([
1423
- ...commonOptions.map((option) => optionSpec(command, option, 'common')),
1424
- ...commandOptions.map((option) => optionSpec(command, option, 'command')),
107
+ ...commonOptions.map((option) => optionSpec(command, option, 'common', commandOptionDefinition(command, option))),
108
+ ...commandOptions.map((option) => optionSpec(command, option, 'command', commandOptionDefinition(command, option))),
1425
109
  ]);
1426
110
  const constraints = grammarConstraints(commandOptions, seed.constraints);
1427
111
  return Object.freeze({
@@ -1446,7 +130,7 @@ const CLI_COMMAND_REGISTRY = Object.freeze(COMMAND_ORDER.map((command, order) =>
1446
130
  metaActions: Object.freeze(['help']),
1447
131
  }),
1448
132
  execution: Object.freeze({
1449
- handlerOwner: seed.handlerOwner,
133
+ handlerOwner: inlineCommandFamily(command) ? 'inline' : seed.handlerOwner,
1450
134
  networkEffect: seed.networkEffect,
1451
135
  }),
1452
136
  help: Object.freeze({
@@ -1470,11 +154,34 @@ function commandDefinition(command) {
1470
154
  function metaActionDefinition(id) {
1471
155
  return CLI_META_ACTION_BY_ID[id];
1472
156
  }
157
+ function cliInvocationOptionIndices(argv) {
158
+ const namedCommand = isCliCommand(argv[0]) ? argv[0] : null;
159
+ const options = new Map(commandDefinition(namedCommand ?? 'lookup').grammar.options
160
+ .map((option) => [option.option, option]));
161
+ const indices = new Set();
162
+ for (let index = namedCommand ? 1 : 0; index < argv.length; index += 1) {
163
+ const argument = argv[index];
164
+ if (argument === '--')
165
+ break;
166
+ if (!argument.startsWith('-'))
167
+ continue;
168
+ indices.add(index);
169
+ const option = options.get(argument);
170
+ const value = argv[index + 1];
171
+ if (option?.arity === 1 && value !== undefined) {
172
+ if (value === '--' && !option.acceptsOptionLikeValue)
173
+ break;
174
+ index += 1;
175
+ }
176
+ }
177
+ return indices;
178
+ }
1473
179
  function cliMetaActionForInvocation(argv) {
180
+ const optionIndices = cliInvocationOptionIndices(argv);
1474
181
  for (const action of CLI_META_ACTIONS) {
1475
182
  const matches = action.scope === 'root_only'
1476
183
  ? action.aliases.includes(argv[0] ?? '')
1477
- : argv.some((argument) => action.aliases.includes(argument));
184
+ : argv.some((argument, index) => optionIndices.has(index) && action.aliases.includes(argument));
1478
185
  if (matches)
1479
186
  return action;
1480
187
  }
@@ -1522,15 +229,8 @@ function commandOwnsOption(command, option) {
1522
229
  return commandDefinition(command).grammar.options.some((candidate) => candidate.option === option);
1523
230
  }
1524
231
  function invocationHasFlag(command, args, flag) {
1525
- for (let index = 0; index < args.length; index += 1) {
1526
- const argument = args[index];
1527
- if (argument === flag)
1528
- return true;
1529
- const specification = argument ? commandOptionSpec(command, argument) : null;
1530
- if (specification?.arity === 1)
1531
- index += 1;
1532
- }
1533
- return false;
232
+ const argv = [command, ...args];
233
+ return [...cliInvocationOptionIndices(argv)].some((index) => argv[index] === flag);
1534
234
  }
1535
235
  function cliInvocationNetworkEffect(command, args) {
1536
236
  const effect = commandDefinition(command).execution.networkEffect;
@@ -1551,6 +251,43 @@ function cliInvocationNetworkEffect(command, args) {
1551
251
  function commandHelp(command) {
1552
252
  const detail = COMMAND_DETAILS[command];
1553
253
  const collection = COMMAND_COLLECTION[command];
1554
- return `WHOISleuth ${command}\n${detail.description}\n\nUsage:\n ${COMMAND_USAGE[command]}\n\nExample:\n ${detail.example}\n\nCollection:\n ${collection.mode === 'offline' ? 'Offline' : 'Network'}: ${collection.scope}\n\nBoundary:\n ${detail.boundary}\n\nRun "whoisleuth --help" to see the grouped command list.\n`;
254
+ const values = commandDefinition(command).grammar.options.filter(expandedOptionValues)
255
+ .map(option => `\n${option.option} values:\n${option.values.map(value => ` ${value}`).join('\n')}\n`).join('');
256
+ const options = commandOptionHelp(command).map(option => {
257
+ const range = option.ranges.map(item => `${item.whenOptionPresent ? `With ${item.whenOptionPresent}: ` : 'Range: '}${item.minimum}–${item.maximum}.`).join(' ');
258
+ const policies = option.option === '--fail-on' ? ` Values: ${option.values.join(', ')}.` : '';
259
+ return ` ${option.usage}\n ${option.description}${option.defaultDescription ? ` Default: ${option.defaultDescription}.` : ''}${option.repeatable ? ' May be repeated.' : ''}${range ? ` ${range}` : ''}${policies}`;
260
+ }).join('\n');
261
+ return `WHOISleuth ${command}\n${detail.description}\n\nExample:\n ${detail.example}\n\nUsage:\n ${COMMAND_USAGE[command]}\n\nOptions:\n${options}\n\nCollection:\n ${collection.mode === 'offline' ? 'Offline' : 'Network'}: ${collection.scope}\n\nBoundary:\n ${detail.boundary}\n${values}\nRun "whoisleuth --help" to see the grouped command list.\n`;
262
+ }
263
+ export function commandDefaultNumber(command, option, deep = false) {
264
+ const value = commandOptionDefinition(command, option).defaultValue(command, deep);
265
+ if (!commandOwnsOption(command, option) || typeof value !== 'number')
266
+ throw new TypeError(`No numeric default for ${command} ${option}.`);
267
+ return value;
268
+ }
269
+ export function commandDefaultText(command, option) {
270
+ const value = commandOptionDefinition(command, option).defaultValue(command, false);
271
+ if (!commandOwnsOption(command, option) || typeof value !== 'string')
272
+ throw new TypeError(`No text default for ${command} ${option}.`);
273
+ return value;
274
+ }
275
+ // Help is a mechanical projection, not another parser or public grammar format.
276
+ export function commandOptionHelp(command) {
277
+ return commandDefinition(command).grammar.options.map(specification => {
278
+ const owner = commandOptionDefinition(command, specification.option);
279
+ const ordinary = owner.defaultValue(command, false);
280
+ const deep = owner.defaultValue(command, true);
281
+ return Object.freeze({
282
+ option: specification.option,
283
+ scope: specification.scope,
284
+ usage: optionUsage(specification),
285
+ description: owner.description(command),
286
+ values: specification.values,
287
+ repeatable: specification.occurrence === 'repeatable',
288
+ ranges: specification.integerRanges,
289
+ defaultDescription: ordinary === null ? null : ordinary === deep ? String(ordinary) : `${ordinary} in Fast mode; ${deep} in Deep mode`,
290
+ });
291
+ });
1555
292
  }
1556
- export { CLI_COMMAND_REGISTRY, CLI_COMMANDS, CLI_META_ACTIONS, COMMAND_COLLECTION, COMMAND_DESCRIPTIONS, COMMAND_DETAILS, COMMAND_USAGE, FILE_POSITIONAL_COMMANDS, HELP, HELP_COMMANDS_BY_GROUP, INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, OPTIONS_BY_COMMAND, cliMetaActionForInvocation, cliInvocationNetworkEffect, commandOptionSpec, commandOwnsOption, commandPositionalSpecs, commonOptionsForCommand, commandDefinition, commandHelp, isCliCommand, metaActionDefinition, };
293
+ export { INLINE_COMMAND_FAMILIES, inlineCommandFamily, inlineCommandsFor, CLI_COMMAND_REGISTRY, CLI_CASE_OPERATIONS, CLI_INDICATOR_OPERATIONS, CLI_COMMANDS, CLI_META_ACTIONS, COMMAND_COLLECTION, COMMAND_DESCRIPTIONS, COMMAND_DETAILS, COMMAND_USAGE, FILE_POSITIONAL_COMMANDS, HELP, HELP_COMMANDS_BY_GROUP, INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, OPTIONS_BY_COMMAND, cliMetaActionForInvocation, cliInvocationOptionIndices, cliInvocationNetworkEffect, commandOptionSpec, commandOwnsOption, commandPositionalSpecs, commonOptionsForCommand, commandDefinition, commandHelp, isCliCommand, metaActionDefinition, };