@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
@@ -3,6 +3,7 @@ import { unlinkSync } from 'node:fs';
3
3
  import { link, open, rename, unlink } from 'node:fs/promises';
4
4
  import { basename, dirname, join, resolve } from 'node:path';
5
5
  import { CliUsageError } from "./errors.mjs";
6
+ import { MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES } from "../packages/contracts/investigation-package-limits.mjs";
6
7
  export const MAX_CLI_OUTPUT_BYTES = 32 * 1024 * 1024;
7
8
  const MAX_CLI_OUTPUT_PATH_LENGTH = 4096;
8
9
  const pendingOutputFiles = new Map();
@@ -22,14 +23,26 @@ function safeOutputPath(value) {
22
23
  }
23
24
  return resolve(value);
24
25
  }
25
- function createBufferedOutput() {
26
+ function createBufferedOutput(options = { binary: false }) {
26
27
  const chunks = [];
28
+ const binaryChunks = [];
27
29
  let bytes = 0;
30
+ const writeBinary = (chunk) => {
31
+ if (!options.binary || !(chunk instanceof Uint8Array))
32
+ throw new CliUsageError('Binary output requires an explicit package destination.');
33
+ if (chunk.byteLength > MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES - bytes)
34
+ throw new CliUsageError('Binary package output exceeds its byte limit.');
35
+ bytes += chunk.byteLength;
36
+ binaryChunks.push(new Uint8Array(chunk));
37
+ };
28
38
  return Object.freeze({
39
+ writeBinary,
29
40
  stream: {
30
41
  isTTY: false,
31
42
  columns: 80,
32
43
  write(chunk) {
44
+ if (options.binary)
45
+ throw new CliUsageError('Binary package output cannot contain terminal text.');
33
46
  const value = typeof chunk === 'string' ? chunk : Buffer.from(chunk).toString('utf8');
34
47
  bytes += Buffer.byteLength(value, 'utf8');
35
48
  if (bytes > MAX_CLI_OUTPUT_BYTES) {
@@ -39,14 +52,26 @@ function createBufferedOutput() {
39
52
  return true;
40
53
  },
41
54
  },
42
- value: () => chunks.join(''),
55
+ value: () => {
56
+ if (!options.binary)
57
+ return chunks.join('');
58
+ const result = new Uint8Array(bytes);
59
+ let offset = 0;
60
+ for (const chunk of binaryChunks) {
61
+ result.set(chunk, offset);
62
+ offset += chunk.byteLength;
63
+ }
64
+ return result;
65
+ },
43
66
  });
44
67
  }
45
68
  async function writePrivateFile(pathValue, content, options = {}, operations = OUTPUT_FILE_OPERATIONS) {
46
69
  const target = safeOutputPath(pathValue);
47
- if (Buffer.byteLength(content, 'utf8') > MAX_CLI_OUTPUT_BYTES) {
48
- throw new CliUsageError(`Generated output is limited to ${MAX_CLI_OUTPUT_BYTES} bytes.`);
70
+ const maximumBytes = typeof content === 'string' ? MAX_CLI_OUTPUT_BYTES : MAX_ENCRYPTED_INVESTIGATION_PACKAGE_BYTES;
71
+ if ((typeof content === 'string' ? Buffer.byteLength(content, 'utf8') : content.byteLength) > maximumBytes) {
72
+ throw new CliUsageError(`Generated output is limited to ${maximumBytes} bytes.`);
49
73
  }
74
+ const captured = typeof content === 'string' ? content : new Uint8Array(content);
50
75
  const directory = dirname(target);
51
76
  const temporary = join(directory, `.${basename(target)}.${process.pid}.${operations.randomUUID()}.tmp`);
52
77
  let temporaryCreated = false;
@@ -59,12 +84,13 @@ async function writePrivateFile(pathValue, content, options = {}, operations = O
59
84
  settled: false,
60
85
  });
61
86
  try {
62
- await handle.writeFile(content, { encoding: 'utf8' });
87
+ await handle.writeFile(captured, { encoding: 'utf8' });
63
88
  await handle.sync();
64
89
  }
65
90
  finally {
66
91
  await handle.close();
67
92
  }
93
+ await options.beforePublish?.();
68
94
  if (options.force) {
69
95
  await operations.rename(temporary, target);
70
96
  temporaryCreated = false;
package/cli/posture.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { CliUsageError } from "./arguments.mjs";
1
+ import { CliUsageError } from "./errors.mjs";
2
2
  const MAX_POSTURE_SELECTORS = 10;
3
3
  function normalizePostureSelectors(raw, normalizeSelectors) {
4
4
  if (raw === null || raw === undefined || raw === '')
@@ -189,7 +189,9 @@ function buildRegistryDoctorReport(raw, generatedAt = new Date().toISOString())
189
189
  ...sources.filter((source) => source.alignment === 'investigate').map((source) => (source.expected === 'allowed'
190
190
  ? `Review ${source.source.toUpperCase()} source health, transport attempts, and fixture coverage; the catalogue expected collection to be available.`
191
191
  : `Review the ${source.source.toUpperCase()} result and capability profile; the observed state did not match the declared access constraint.`)),
192
- ...sources.filter((source) => source.alignment === 'unexpected_observation').map((source) => (`Review why ${source.source.toUpperCase()} produced normalized evidence despite the declared ${source.expected.replaceAll('_', ' ')} access profile.`)),
192
+ ...sources.filter((source) => source.alignment === 'unexpected_observation').map((source) => (source.source === 'rdap' && source.expected === 'unsupported'
193
+ ? 'Review whether the retained no-service RDAP profile needs refreshing; current bootstrap discovery may identify a newly published service.'
194
+ : `Review why ${source.source.toUpperCase()} produced normalized evidence despite the declared ${source.expected.replaceAll('_', ' ')} access profile.`)),
193
195
  ...sources.filter((source) => source.alignment === 'observed' && source.objectIdentifier === 'not_observed').map((source) => (`${source.source.toUpperCase()} succeeded without a retained registry object identifier; this can be a valid publication omission and should remain unknown rather than an error.`)),
194
196
  ].slice(0, 12);
195
197
  return {
@@ -14,7 +14,7 @@ export function buildRegistryFixtureScaffold(profileIdInput, suffixInput, scenar
14
14
  throw new TypeError('Scenario must be registered, not_found, or inconclusive.');
15
15
  const matchingProfiles = registryCompatibilityMatrix().filter((item) => item.id === profileId);
16
16
  if (!matchingProfiles.length)
17
- throw new TypeError('The capability profile is not present in the registry catalogue.');
17
+ throw new TypeError('The capability profile is not present in the registry catalogue. Run registry-support <suffix> --json to find its profile ID.');
18
18
  const profile = matchingProfiles.find((item) => item.suffixes.includes(suffix));
19
19
  if (!profile)
20
20
  throw new TypeError(`The capability profile does not cover .${suffix}.`);
@@ -1,7 +1,7 @@
1
1
  import { registryStandardsCoverageSnapshot, } from "../lib/registry-capabilities.mjs";
2
+ import { REGISTRY_STANDARDS_COVERAGE_SCHEMA } from "../lib/registry-capability-catalogue.mjs";
2
3
  const REGISTRY_SUPPORT_SCHEMA = 'whoisleuth.cli.registry-support';
3
4
  const REGISTRY_SUPPORT_SCHEMA_VERSION = 4;
4
- const REGISTRY_STANDARDS_COVERAGE_SCHEMA = 'whoisleuth.registry-standards-coverage';
5
5
  const MAX_REGISTRY_SUPPORT_TEXT_LENGTH = 2048;
6
6
  const MAX_REGISTRY_SUPPORT_REFERENCES = 20;
7
7
  const MAX_REGISTRY_SUPPORT_REFERENCE_LENGTH = 2048;
@@ -9,6 +9,7 @@ import { parseBoundedJsonObject } from "./bounded-json.mjs";
9
9
  import { CliUsageError } from "./errors.mjs";
10
10
  import { buildCliLookupDiff, formatCliLookupDiff } from "./lookup-diff.mjs";
11
11
  import { SAVED_LOOKUP_SCHEMA } from "./saved-lookup.mjs";
12
+ import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
12
13
  import { CLI_COMPARISON_LEDGER_SCHEMA, CLI_COMPARISON_LEDGER_VERSION, MAX_RETAINED_ARTIFACT_DIFF_BYTES, } from "../packages/contracts/offline-comparison.mjs";
13
14
  export { CLI_COMPARISON_LEDGER_SCHEMA, CLI_COMPARISON_LEDGER_VERSION, MAX_RETAINED_ARTIFACT_DIFF_BYTES };
14
15
  const BULK_ROOT_KEYS = new Set(['schema', 'version', 'generatedAt', 'sessions', 'limitations']);
@@ -62,7 +63,7 @@ function parseBulkExport(root) {
62
63
  const rawSession = record(candidate);
63
64
  const rawResults = Array.isArray(rawSession?.results) ? rawSession.results : null;
64
65
  const rawDomains = Array.isArray(rawSession?.domains) ? rawSession.domains : null;
65
- const session = normalizeBulkSession(candidate);
66
+ const session = normalizeBulkSession(candidate, version);
66
67
  if (!session || !rawResults || !rawDomains
67
68
  || rawResults.length > MAX_BULK_SESSION_ROWS
68
69
  || rawResults.length !== session.results.length
@@ -338,7 +339,7 @@ export function buildCliRetainedArtifactDiff(leftRaw, rightRaw, options = {}, ge
338
339
  if (left.schema === SAVED_LOOKUP_SCHEMA) {
339
340
  if (options.leftSessionId || options.rightSessionId)
340
341
  throw new CliUsageError('Saved-session selectors apply only to Bulk session exports.');
341
- return buildCliLookupDiff(leftRaw, rightRaw, generatedAt);
342
+ return buildCliLookupDiff(leftRaw, rightRaw, generatedAt, { domainMode: 'auto' });
342
343
  }
343
344
  if (left.schema === BULK_SESSION_SCHEMA)
344
345
  return buildBulkLedger(leftRaw, rightRaw, left, right, options, generatedAt);
@@ -354,22 +355,22 @@ export function formatCliRetainedArtifactDiff(document) {
354
355
  return formatCliLookupDiff(document);
355
356
  const lines = [
356
357
  'Retained artifact comparison ledger',
357
- `Family ${document.artifactFamily.replaceAll('_', ' ')}`,
358
- `Left ${document.left.label} · ${document.left.id}`,
359
- `Right ${document.right.label} · ${document.right.id}`,
358
+ `Family ${safeTerminalValue(document.artifactFamily).replaceAll('_', ' ')}`,
359
+ `Left ${safeTerminalValue(document.left.label)} · ${safeTerminalValue(document.left.id)}`,
360
+ `Right ${safeTerminalValue(document.right.label)} · ${safeTerminalValue(document.right.id)}`,
360
361
  `Candidates ${document.index.counts.retained}`,
361
362
  `Exact rows ${document.details.rows.length} of ${document.details.totalRows}`,
362
363
  '',
363
364
  ];
364
365
  for (const row of document.details.rows) {
365
- lines.push(`${row.entityId} · ${row.field} [${row.state.replaceAll('_', ' ')}]`);
366
- lines.push(` Earlier: ${row.earlier.value ?? 'not retained'} · ${row.earlier.sourceState}`);
367
- lines.push(` Later: ${row.later.value ?? 'not retained'} · ${row.later.sourceState}`);
366
+ lines.push(`${safeTerminalValue(row.entityId)} · ${safeTerminalValue(row.field)} [${safeTerminalValue(row.state).replaceAll('_', ' ')}]`);
367
+ lines.push(` Earlier: ${safeTerminalValue(row.earlier.value, 'not retained')} · ${safeTerminalValue(row.earlier.sourceState)}`);
368
+ lines.push(` Later: ${safeTerminalValue(row.later.value, 'not retained')} · ${safeTerminalValue(row.later.sourceState)}`);
368
369
  }
369
370
  if (document.details.omissions.detailRows)
370
371
  lines.push(`Omitted exact rows: ${document.details.omissions.detailRows}`);
371
372
  lines.push('', 'Limitations:');
372
373
  for (const limitation of document.limitations)
373
- lines.push(` - ${limitation}`);
374
+ lines.push(` - ${safeTerminalValue(limitation)}`);
374
375
  return `${lines.join('\n')}\n`;
375
376
  }
@@ -0,0 +1,131 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import { CliUsageError } from "./errors.mjs";
3
+ import { CLI_CASE_OPERATIONS, CLI_INDICATOR_OPERATIONS } from "./command-reference.mjs";
4
+ import { terminalOptions, jsonOutput, parseTwoFileComparisonArguments, singleInputAction, } from "./argument-values.mjs";
5
+ function parseVerifyArtifactArguments(parsed) {
6
+ if (parsed.hasOption('--folder') && parsed.positionalValue('source'))
7
+ throw new CliUsageError('--folder selects its own input; do not also supply a file or stdin marker.');
8
+ return {
9
+ action: 'verify-artifact',
10
+ source: parsed.positionalValue('source'),
11
+ passphraseSource: parsed.optionValue('--passphrase-file'),
12
+ manifestSource: parsed.optionValue('--manifest'),
13
+ manifestEntryId: parsed.optionValue('--manifest-entry'),
14
+ ...(parsed.hasOption('--package') ? { package: true } : {}),
15
+ ...(parsed.hasOption('--bagit') ? { bagit: true } : {}),
16
+ ...(parsed.optionValue('--folder') ? { folder: parsed.optionValue('--folder') } : {}),
17
+ output: jsonOutput(parsed),
18
+ strictExit: parsed.hasOption('--strict-exit'),
19
+ ...terminalOptions(parsed),
20
+ };
21
+ }
22
+ function parseCaseArguments(parsed) {
23
+ const operation = parsed.positionalValue('operation');
24
+ const source = parsed.positionalValue('source');
25
+ const domain = parsed.optionValue('--domain');
26
+ const caseId = parsed.optionValue('--case-id');
27
+ const title = parsed.optionValue('--title');
28
+ const newIncident = parsed.hasOption('--new-incident');
29
+ const text = parsed.optionValue('--text');
30
+ const noteSource = parsed.optionValue('--note-file');
31
+ const inputSource = parsed.optionValue('--input');
32
+ const expectedFileDigest = parsed.optionValue('--expect-file-digest');
33
+ if (source === '-' || inputSource === '-' || noteSource === '-')
34
+ throw new CliUsageError('Case operations require selected files, not stdin.');
35
+ if (operation !== 'open' && !source)
36
+ throw new CliUsageError(`${operation} requires a Case file.`);
37
+ if (operation !== 'show' && !parsed.hasOption('--output'))
38
+ throw new CliUsageError('Case mutations require --output; nothing was changed.');
39
+ if (operation === 'open' && !domain)
40
+ throw new CliUsageError('case open requires --domain.');
41
+ if (operation !== 'open' && (title || newIncident))
42
+ throw new CliUsageError('--title and --new-incident belong to case open.');
43
+ if (newIncident && (!title || caseId))
44
+ throw new CliUsageError('--new-incident requires a distinguishing --title and cannot select an existing --case-id.');
45
+ if (operation === 'note' ? text === null && !noteSource : text !== null || noteSource !== null) {
46
+ throw new CliUsageError('Only case note accepts and requires --text or --note-file.');
47
+ }
48
+ if (['pin', 'link', 'withdraw-link', 'assess', 'recheck'].includes(operation)
49
+ ? !inputSource
50
+ : inputSource !== null) {
51
+ throw new CliUsageError('Only case pin, link, withdraw-link, assess and recheck accept and require --input.');
52
+ }
53
+ if (expectedFileDigest !== null && (!source || !/^sha256:[a-f0-9]{64}$/u.test(expectedFileDigest))) {
54
+ throw new CliUsageError('--expect-file-digest requires a source file and sha256:<64 lowercase hexadecimal characters>.');
55
+ }
56
+ return {
57
+ action: 'case',
58
+ operation,
59
+ source,
60
+ caseId,
61
+ domain,
62
+ title,
63
+ newIncident,
64
+ text,
65
+ noteSource,
66
+ inputSource,
67
+ expectedFileDigest,
68
+ output: jsonOutput(parsed),
69
+ ...terminalOptions(parsed),
70
+ };
71
+ }
72
+ export const REVIEW_ARGUMENT_PARSERS = Object.freeze({
73
+ 'verify-artifact': parseVerifyArtifactArguments,
74
+ 'interchange-report': (parsed) => ({
75
+ action: 'interchange-report',
76
+ source: parsed.positionalValue('source'),
77
+ passphraseSource: parsed.optionValue('--passphrase-file'),
78
+ output: jsonOutput(parsed),
79
+ ...terminalOptions(parsed),
80
+ }),
81
+ 'source-report': (parsed) => singleInputAction('source-report', parsed),
82
+ compare: (parsed) => singleInputAction('compare', parsed),
83
+ 'page-compare': (parsed) => ({
84
+ action: 'page-compare',
85
+ ...parseTwoFileComparisonArguments(parsed, 'page-compare'),
86
+ }),
87
+ 'mail-review': (parsed) => singleInputAction('mail-review', parsed),
88
+ 'mail-headers': (parsed) => ({
89
+ ...singleInputAction('mail-headers', parsed),
90
+ ...(parsed.hasOption('--trusted-auth-header')
91
+ ? { trustedAuthHeaders: parsed.optionValues('--trusted-auth-header') }
92
+ : {}),
93
+ }),
94
+ intake: (parsed) => ({
95
+ action: 'intake',
96
+ kind: parsed.positionalValue('kind'),
97
+ reportedActions: parsed.optionValues('--reported-action'),
98
+ ...(parsed.hasOption('--trusted-auth-header')
99
+ ? { trustedAuthHeaders: parsed.optionValues('--trusted-auth-header') }
100
+ : {}),
101
+ source: parsed.positionalValue('source'),
102
+ output: jsonOutput(parsed),
103
+ strictExit: parsed.hasOption('--strict-exit'),
104
+ ...terminalOptions(parsed),
105
+ }),
106
+ 'review-evidence': (parsed) => ({
107
+ action: 'review-evidence',
108
+ source: parsed.positionalValue('source'),
109
+ mmdbSource: parsed.optionValue('--mmdb'),
110
+ output: jsonOutput(parsed),
111
+ strictExit: parsed.hasOption('--strict-exit'),
112
+ ...terminalOptions(parsed),
113
+ }),
114
+ brief: (parsed) => singleInputAction('brief', parsed),
115
+ case: parseCaseArguments,
116
+ 'indicator-set': (parsed) => ({
117
+ action: 'indicator-set',
118
+ operation: parsed.positionalValue('operation'),
119
+ source: parsed.positionalValue('source'),
120
+ output: jsonOutput(parsed),
121
+ ...terminalOptions(parsed),
122
+ }),
123
+ 'case-pack': (parsed) => ({
124
+ action: 'case-pack',
125
+ source: parsed.positionalValue('source'),
126
+ output: jsonOutput(parsed),
127
+ audience: parsed.optionValue('--audience'),
128
+ reviewed: true,
129
+ ...terminalOptions(parsed),
130
+ }),
131
+ });
@@ -0,0 +1,273 @@
1
+ import { CASE_SCHEMA_VERSION, CLI_CASE_PACK_INPUT_CASE_VERSIONS, MAX_CASE_STORE_BYTES, MAX_EDITABLE_CASE_INPUT_BYTES } from "../packages/contracts/case-portability.mjs";
2
+ import { MESSAGE_INTAKE_KINDS, MESSAGE_INTAKE_SCHEMA } from "../packages/contracts/message-intake.mjs";
3
+ import { IDENTITY_EVENTS_INPUT_SCHEMA } from "../packages/contracts/identity-events.mjs";
4
+ import { CONTEXT_INPUT_SCHEMAS, CONTEXT_REVIEW_SCHEMA } from "../packages/contracts/context-review.mjs";
5
+ import { MANAGED_INDICATOR_SET_SCHEMA, MAX_MANAGED_INDICATOR_SET_BYTES, MAX_MANAGED_INDICATOR_PLAN_BYTES } from "../packages/contracts/analyst-interchange.mjs";
6
+ import { CLI_CASE_OPERATIONS, CLI_INDICATOR_OPERATIONS, positional, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
7
+ export const REVIEW_COMMAND_DEFINITIONS = Object.freeze({
8
+ "verify-artifact": commandSeed({
9
+ reference: {
10
+ description: 'Validate a supported archive, ordinary Case export, claim passport, packet, manifest, saved Lookup or Lookup-evidence export without printing evidence contents. Use --package for an evidence ZIP or encrypted package, with --passphrase-file to unlock it; use --folder ./evidence for an unencrypted evidence folder. Add --bagit with --package or --folder to verify BagIt 1.0.',
11
+ example: 'whoisleuth verify-artifact report.json --manifest manifest.json --manifest-entry artifact-2 --json --strict-exit',
12
+ boundary: 'Verification is offline and redacted. ZIP and folder entries are reported separately without importing them. Case exports are checked without repairing content; ordinary package review also counts original references with matching bytes. Ordinary folders allow only the declared layout; BagIt allows bounded nested payloads and checks SHA-256/SHA-512 manifests without interpreting payloads. Symbolic links are refused. BagIt fetch.txt is never fetched; missing files, mismatches and unsupported algorithms remain explicit. Package digests describe bytes, not filesystem metadata or authenticity. In scripts, use --strict-exit: incomplete verification returns 4. Unencrypted workspace archives always return 4 under --strict-exit because section digests do not cover mutable root metadata; structure-only formats can pass their narrower contract. Default exit 0 means the report was produced, not that its checks passed.',
13
+ },
14
+ collection: { scope: 'Reads one selected bounded artefact, ZIP or explicit evidence folder and, when explicitly supplied, one manifest whose selected entry is compared by exact bytes and canonical identity.' },
15
+ summary: 'Validate saved evidence offline',
16
+ options: ['--passphrase-file', '--manifest', '--manifest-entry', '--package', '--bagit', '--folder', '--json', '--strict-exit', '--quiet', '--no-color'],
17
+ positionals: Object.freeze([positional('source', 'file', 0, 1, [], 'argv_or_stdin', ['--package'])]),
18
+ constraints: Object.freeze([
19
+ constraint({ kind: 'requires_any', option: '--bagit', requiredOptions: ['--package', '--folder'] }),
20
+ constraint({ kind: 'excludes_all', option: '--bagit', excludedOptions: ['--passphrase-file', '--manifest', '--manifest-entry'] }),
21
+ constraint({ kind: 'requires_all', option: '--manifest', requiredOptions: ['--manifest-entry'] }),
22
+ constraint({ kind: 'requires_all', option: '--manifest-entry', requiredOptions: ['--manifest'] }),
23
+ constraint({ kind: 'excludes_all', option: '--package', excludedOptions: ['--manifest', '--manifest-entry'] }),
24
+ constraint({ kind: 'excludes_all', option: '--folder', excludedOptions: ['--package', '--manifest', '--manifest-entry', '--passphrase-file'] }),
25
+ ]),
26
+ handlerOwner: 'review',
27
+ networkEffect: 'offline',
28
+ common: true,
29
+ schemaIdentifiers: Object.freeze(['whoisleuth.offline-artifact-verification']),
30
+ primaryArtefacts: Object.freeze(['Offline verification report']),
31
+ planSupport: false,
32
+ additionalOutputFormats: Object.freeze([]),
33
+ bootstrapProfile: 'allowed',
34
+ }),
35
+ "interchange-report": commandSeed({
36
+ reference: {
37
+ description: 'Report what one recognised portable artefact preserves, excludes, and supports across browser and CLI workflows.',
38
+ example: 'whoisleuth interchange-report workspace.json --json',
39
+ boundary: 'The report is offline and metadata-only. It does not echo targets, contacts, notes, passphrases, evidence values, or an unrecognised schema string.',
40
+ },
41
+ collection: { scope: 'Reads one selected bounded portable artefact and emits fixed compatibility metadata only.' },
42
+ summary: 'Report portable artefact fidelity offline',
43
+ options: ['--passphrase-file', '--json', '--quiet', '--no-color'],
44
+ positionals: OPTIONAL_FILE_POSITIONAL,
45
+ constraints: EMPTY_CONSTRAINTS,
46
+ handlerOwner: 'review',
47
+ networkEffect: 'offline',
48
+ common: false,
49
+ schemaIdentifiers: Object.freeze(['whoisleuth.interchange-fidelity-report']),
50
+ primaryArtefacts: Object.freeze([]),
51
+ planSupport: false,
52
+ additionalOutputFormats: Object.freeze([]),
53
+ bootstrapProfile: 'allowed',
54
+ }),
55
+ "source-report": commandSeed({
56
+ reference: {
57
+ description: 'Create a target-free reliability summary from a saved lookup.',
58
+ example: 'whoisleuth source-report lookup.json --json',
59
+ boundary: 'The report retains source states and timings but excludes targets, queries, endpoints, and raw evidence.',
60
+ },
61
+ collection: { scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
62
+ summary: 'Build a target-free source report',
63
+ options: ['--json', '--quiet', '--no-color'],
64
+ positionals: OPTIONAL_FILE_POSITIONAL,
65
+ constraints: EMPTY_CONSTRAINTS,
66
+ handlerOwner: 'review',
67
+ networkEffect: 'offline',
68
+ common: false,
69
+ schemaIdentifiers: Object.freeze(['whoisleuth.source-reliability-report']),
70
+ primaryArtefacts: Object.freeze([]),
71
+ planSupport: false,
72
+ additionalOutputFormats: Object.freeze([]),
73
+ bootstrapProfile: 'allowed',
74
+ }),
75
+ compare: commandSeed({
76
+ reference: {
77
+ description: 'Compare separately attributed registry publications in a saved lookup.',
78
+ example: 'whoisleuth compare lookup.json --json',
79
+ boundary: 'Comparison is offline. Differences are review context and do not by themselves prove which publication is current.',
80
+ },
81
+ collection: { scope: 'Reads one saved Lookup and compares its separately attributed registry publications.' },
82
+ summary: 'Compare registry publications in one lookup',
83
+ options: ['--json', '--quiet', '--no-color'],
84
+ positionals: OPTIONAL_FILE_POSITIONAL,
85
+ constraints: EMPTY_CONSTRAINTS,
86
+ handlerOwner: 'review',
87
+ networkEffect: 'offline',
88
+ common: false,
89
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.compare']),
90
+ primaryArtefacts: Object.freeze([]),
91
+ planSupport: false,
92
+ additionalOutputFormats: Object.freeze([]),
93
+ bootstrapProfile: 'allowed',
94
+ }),
95
+ "page-compare": commandSeed({
96
+ reference: {
97
+ description: 'Compare static page identity, favicon, technology, and TLS evidence in two saved deep lookups.',
98
+ example: 'whoisleuth page-compare official.json candidate.json --json',
99
+ boundary: 'Comparison is offline and component-based. It executes no page code and produces no aggregate similarity or maliciousness score.',
100
+ },
101
+ collection: { scope: 'Reads two saved Lookup documents and executes no page code.' },
102
+ summary: 'Compare saved static page evidence',
103
+ options: ['--json', '--quiet', '--no-color'],
104
+ positionals: Object.freeze([positional('sources', 'file', 2, 2)]),
105
+ constraints: EMPTY_CONSTRAINTS,
106
+ handlerOwner: 'review',
107
+ networkEffect: 'offline',
108
+ common: false,
109
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.page-compare']),
110
+ primaryArtefacts: Object.freeze([]),
111
+ planSupport: false,
112
+ additionalOutputFormats: Object.freeze([]),
113
+ bootstrapProfile: 'allowed',
114
+ }),
115
+ "mail-review": commandSeed({
116
+ reference: {
117
+ description: 'Review passive MX, null MX, SPF, DMARC, and shared mail-provider evidence from saved Bulk results.',
118
+ example: 'whoisleuth mail-review candidates.json --json',
119
+ boundary: 'Review is offline and sends no SMTP traffic. Missing or partial DNS evidence remains inconclusive.',
120
+ },
121
+ collection: { scope: 'Reads one saved Bulk result and sends no DNS or SMTP traffic.' },
122
+ summary: 'Review saved passive mail evidence',
123
+ options: ['--json', '--quiet', '--no-color'],
124
+ positionals: OPTIONAL_FILE_POSITIONAL,
125
+ constraints: EMPTY_CONSTRAINTS,
126
+ handlerOwner: 'review',
127
+ networkEffect: 'offline',
128
+ common: false,
129
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.mail-review']),
130
+ primaryArtefacts: Object.freeze([]),
131
+ planSupport: false,
132
+ additionalOutputFormats: Object.freeze([]),
133
+ bootstrapProfile: 'allowed',
134
+ }),
135
+ "mail-headers": commandSeed({
136
+ reference: {
137
+ description: 'Review identity, reported authentication, domain alignment, and Received routing from selected message headers.',
138
+ example: 'whoisleuth mail-headers message.eml --json',
139
+ 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.',
140
+ },
141
+ collection: { scope: 'Parses only the bounded header block from one selected message or standard input.' },
142
+ summary: 'Review message headers offline',
143
+ options: ['--trusted-auth-header', '--json', '--quiet', '--no-color'],
144
+ positionals: OPTIONAL_FILE_POSITIONAL,
145
+ constraints: EMPTY_CONSTRAINTS,
146
+ handlerOwner: 'review',
147
+ networkEffect: 'offline',
148
+ common: false,
149
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.mail-header-review']),
150
+ primaryArtefacts: Object.freeze([]),
151
+ planSupport: false,
152
+ additionalOutputFormats: Object.freeze([]),
153
+ bootstrapProfile: 'allowed',
154
+ }),
155
+ intake: commandSeed({
156
+ reference: {
157
+ description: 'Review message identities, document links, QR destinations, HTTP archives or selected identity events locally.',
158
+ example: 'whoisleuth intake email message.eml --json',
159
+ boundary: 'Offline only: no destination, embedded command or QR payload is opened or executed. Output omits original bodies, subjects, address local parts, URL paths, queries and fragments. Authentication headers remain attributed claims. Selected PDF and DOCX review retains part identities and extraction coverage; embedded raster QR support does not imply complete rendered-page review. HAR retains minimised request order and timings, never replay authority, headers, cookies or bodies. Use --strict-exit to return 4 for partial coverage.',
160
+ },
161
+ collection: { scope: 'Reads one selected text, MIME email, calendar, PNG, PDF, DOCX, HAR or identity-event JSON file. No collection or automatic Case write.' },
162
+ summary: 'Review selected files and identity events offline',
163
+ options: ['--json', '--reported-action', '--trusted-auth-header', '--strict-exit', '--quiet', '--no-color'],
164
+ positionals: Object.freeze([positional('kind', 'enum', 1, 1, MESSAGE_INTAKE_KINDS), positional('source', 'file', 0, 1, [], 'argv_or_stdin')]),
165
+ constraints: EMPTY_CONSTRAINTS, handlerOwner: 'review', networkEffect: 'offline', common: true,
166
+ schemaIdentifiers: Object.freeze([MESSAGE_INTAKE_SCHEMA, IDENTITY_EVENTS_INPUT_SCHEMA]), primaryArtefacts: Object.freeze([]),
167
+ planSupport: false, additionalOutputFormats: Object.freeze([]), bootstrapProfile: 'allowed',
168
+ }),
169
+ "review-evidence": commandSeed({
170
+ reference: {
171
+ description: 'Review versioned protocol evidence, incident sequences, domain history, platform objects, storefronts or connector configuration offline.',
172
+ example: 'whoisleuth review-evidence domain-change.json --json --strict-exit',
173
+ boundary: 'The command reads only the supplied document and, with --mmdb, one explicitly selected local database up to 512 MiB. MMDB parsing and lookup run in one disposable worker with a ten-second processing deadline and an 8 KiB result limit. Version-2 MMDB queries require a justified age policy; unavailable or partial current reviews return 4 under --strict-exit. Historical version-1 MMDB queries preserve their output without freshness admission. No database is bundled, downloaded, updated or transmitted. It performs no DNS, RDAP, BGP, GeoIP-provider, TLS, HTTP, certificate-authority, or SMTP request.',
174
+ },
175
+ collection: { scope: 'Reads one bounded versioned evidence or request-planning document and, only with --mmdb, one selected bounded local database. Performs no collection.' },
176
+ summary: 'Review supplied evidence offline',
177
+ options: ['--mmdb', '--json', '--strict-exit', '--quiet', '--no-color'],
178
+ positionals: OPTIONAL_FILE_POSITIONAL,
179
+ constraints: EMPTY_CONSTRAINTS,
180
+ handlerOwner: 'review',
181
+ networkEffect: 'offline',
182
+ common: true,
183
+ schemaIdentifiers: Object.freeze([
184
+ CONTEXT_REVIEW_SCHEMA,
185
+ ...CONTEXT_INPUT_SCHEMAS,
186
+ 'whoisleuth.cli.offline-evidence-review',
187
+ 'whoisleuth.rdap-search-input',
188
+ 'whoisleuth.dnssec-evidence-input',
189
+ 'whoisleuth.tlsa-evidence-input',
190
+ 'whoisleuth.rpki-route-input',
191
+ 'whoisleuth.local-geoip-query',
192
+ 'whoisleuth.local-mmdb-query',
193
+ 'whoisleuth.local-mmdb-review',
194
+ 'whoisleuth.encrypted-dns-plan-input',
195
+ ]),
196
+ primaryArtefacts: Object.freeze([]),
197
+ planSupport: false,
198
+ additionalOutputFormats: Object.freeze([]),
199
+ bootstrapProfile: 'allowed',
200
+ }),
201
+ brief: commandSeed({
202
+ reference: {
203
+ description: 'Turn one saved Lookup into a compact decision brief with facts, unknowns, contradictions, and next actions.',
204
+ example: 'whoisleuth brief lookup.json --json',
205
+ boundary: 'The command is offline, excludes raw upstream payloads, and does not create an analyst assertion or claim that the saved observation is current.',
206
+ },
207
+ collection: { scope: 'Reads one bounded saved Lookup and emits a compact source-attributed decision brief.' },
208
+ summary: 'Build a decision brief from a saved lookup',
209
+ options: ['--json', '--quiet', '--no-color'],
210
+ positionals: OPTIONAL_FILE_POSITIONAL,
211
+ constraints: EMPTY_CONSTRAINTS,
212
+ handlerOwner: 'review',
213
+ networkEffect: 'offline',
214
+ common: false,
215
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.lookup-brief']),
216
+ primaryArtefacts: Object.freeze([]),
217
+ planSupport: false,
218
+ additionalOutputFormats: Object.freeze([]),
219
+ bootstrapProfile: 'allowed',
220
+ }),
221
+ case: commandSeed({
222
+ reference: {
223
+ description: 'Show or open a local Case, append a note or evidence pin, link evidence, withdraw a link, record an assessment, or retain an offline recheck. Use --input for pin, link, withdraw-link, assessment and recheck JSON; --text or --note-file for a note. Link input contains fromPinId, toPinId, kind (derived_from or shared_source), and basis. Withdrawal input contains id and reason. Mutations require --output and always write the complete current Case export.',
224
+ example: 'whoisleuth case open --domain example.test --output cases.json\n whoisleuth case show cases.json\n whoisleuth case note cases.json --text "Review the retained observation" --output cases.json --force',
225
+ boundary: 'No database, browser launch, request or external report is created. Select --case-id when a file contains multiple Cases. Existing files require --force; --expect-file-digest sha256:<digest> additionally checks the exact file reviewed earlier. Source and output leases reject concurrent changes. Interrupted .workflow.lock files require deliberate inspection. Recheck records supplied observations; it does not collect them. Not reproduced requires an existing saved question, a complete observation and comparable conditions. Working exports include private analyst content and file references, not attached file bytes.',
226
+ },
227
+ collection: { scope: `Reads exact Case schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')}. Input is bounded to ${MAX_EDITABLE_CASE_INPUT_BYTES / 1024 / 1024} MiB including formatting; the complete canonical Case store must fit ${MAX_CASE_STORE_BYTES / 1024 / 1024} MiB without pruning. Writes current schema ${CASE_SCHEMA_VERSION}.` },
228
+ summary: 'Review and update ordinary local Case files',
229
+ options: ['--case-id', '--domain', '--title', '--new-incident', '--text', '--note-file', '--input', '--expect-file-digest', '--json', '--no-color'],
230
+ positionals: Object.freeze([positional('operation', 'enum', 1, 1, CLI_CASE_OPERATIONS), positional('source', 'file', 0, 1)]),
231
+ constraints: Object.freeze([constraint({ kind: 'mutually_exclusive', options: ['--text', '--note-file'] })]),
232
+ handlerOwner: 'review', networkEffect: 'offline', common: true,
233
+ schemaIdentifiers: Object.freeze(['whoisleuth.case-export']),
234
+ primaryArtefacts: Object.freeze(['Case export']), planSupport: false,
235
+ additionalOutputFormats: Object.freeze([]), bootstrapProfile: 'allowed',
236
+ }),
237
+ 'indicator-set': commandSeed({
238
+ reference: {
239
+ description: 'Create or revise a file-based indicator set, inspect its identities and review windows, or export the exact revision as STIX or MISP JSON. Revise takes a plan containing basis, name, expiresAt, reviewed rows and selectedDomains; later plans embed previous and explicitly select renewIds, withdrawIds or reintroduceDomains. Use --json with revise to retain the manifest. STIX and MISP operations always output JSON.',
240
+ example: 'whoisleuth indicator-set revise indicator-plan.json --json --output indicators.json\n whoisleuth indicator-set inspect indicators.json\n whoisleuth indicator-set stix indicators.json --output indicators.stix.json',
241
+ boundary: 'Offline files only; no submission, feed synchronisation or automatic deletion. Missing candidates are unchanged. Renewal changes the review window, not the original observation. Withdrawn identities stay withdrawn; reintroduction requires a new identity. Keep prior manifests: a digest verifies content, not authorship or the complete revision chain. MISP expiry is a review deadline, not automatic enforcement.',
242
+ },
243
+ collection: { scope: `Reads a selected ${MAX_MANAGED_INDICATOR_PLAN_BYTES / 1024 / 1024} MiB revision plan or ${MAX_MANAGED_INDICATOR_SET_BYTES / 1024 / 1024} MiB manifest. Makes no requests and does not alter the source file.` },
244
+ summary: 'Manage reviewed indicator revisions offline',
245
+ options: ['--json', '--quiet', '--no-color'],
246
+ positionals: Object.freeze([positional('operation', 'enum', 1, 1, CLI_INDICATOR_OPERATIONS), positional('source', 'file', 0, 1)]),
247
+ constraints: EMPTY_CONSTRAINTS, handlerOwner: 'review', networkEffect: 'offline', common: false,
248
+ schemaIdentifiers: Object.freeze([MANAGED_INDICATOR_SET_SCHEMA]), primaryArtefacts: Object.freeze(['Managed indicator revision']),
249
+ planSupport: false, additionalOutputFormats: Object.freeze([]), bootstrapProfile: 'allowed',
250
+ }),
251
+ "case-pack": commandSeed({
252
+ reference: {
253
+ description: `Package browser-created Case records from schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')} as a reviewed, audience-specific Case-pack v2 with current schema ${CASE_SCHEMA_VERSION}.`,
254
+ example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
255
+ boundary: 'The command creates a separate offline package without changing the source or durable Cases and requires an explicit review acknowledgement. Every audience, including public, retains domain evidence, Case, pin and decision identifiers, tags, decision summaries and rationale. Review that analyst-authored content as well as the exclusions before sharing.',
256
+ },
257
+ collection: { scope: `Reads one bounded Case export from schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')} and writes a separate audience-specific Case-pack v2.` },
258
+ summary: 'Build a reviewed case package',
259
+ options: ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
260
+ positionals: OPTIONAL_FILE_POSITIONAL,
261
+ constraints: Object.freeze([
262
+ constraint({ kind: 'required', options: ['--audience', '--reviewed'] }),
263
+ ]),
264
+ handlerOwner: 'review',
265
+ networkEffect: 'offline',
266
+ common: true,
267
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.case-pack', 'whoisleuth.case-report']),
268
+ primaryArtefacts: Object.freeze(['Reviewed Case-pack v2']),
269
+ planSupport: false,
270
+ additionalOutputFormats: Object.freeze([]),
271
+ bootstrapProfile: 'allowed',
272
+ }),
273
+ });