@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,11 +1,15 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { getDomain } from 'tldts';
3
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
4
- import { isValidAsciiDomainName } from "../lib/hostname.mjs";
3
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
4
+ import { compareCodeUnits } from "../lib/bounded-contract-normalizers.mjs";
5
+ import { isValidAsciiDomainName } from "../packages/contracts/domain-name.mjs";
5
6
  import { isRecord, recordOrEmpty } from "../lib/json-record.mjs";
7
+ import { classifyQuery } from "../lib/classify.mjs";
8
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
6
9
  import { CliUsageError } from "./errors.mjs";
10
+ import { PASSIVE_MAIL_INTERPRETATION, PASSIVE_MAIL_LABELS } from "../packages/contracts/passive-mail.mjs";
7
11
  export const CLI_MAIL_REVIEW_SCHEMA = 'whoisleuth.cli.mail-review';
8
- export const CLI_MAIL_REVIEW_VERSION = 3;
12
+ export const CLI_MAIL_REVIEW_VERSION = 4;
9
13
  export const MAX_MAIL_REVIEW_INPUT_BYTES = 16 * 1024 * 1024;
10
14
  export const MAX_MAIL_REVIEW_ROWS = 500;
11
15
  const UNSAFE_TEXT_RE = /[\u0000-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f]/u;
@@ -20,10 +24,10 @@ function boolean(value) {
20
24
  return typeof value === 'boolean' ? value : null;
21
25
  }
22
26
  function timestamp(value, label) {
23
- if (typeof value !== 'string' || value.length > 64 || !Number.isFinite(Date.parse(value))) {
24
- throw new CliUsageError(`${label} must be a valid bounded timestamp.`);
25
- }
26
- return new Date(value).toISOString();
27
+ const result = normalizeExplicitIsoTimestamp(value);
28
+ if (!result)
29
+ throw new CliUsageError(`${label} must be a valid bounded timestamp with an explicit timezone.`);
30
+ return result;
27
31
  }
28
32
  function booleanOrNull(value, label) {
29
33
  if (value === null || typeof value === 'boolean')
@@ -87,8 +91,8 @@ function validateMxEvidence(availability, summary, label, hasMx, hasNullMx) {
87
91
  function validateMailRow(value, index, expectedVersion) {
88
92
  const item = record(value);
89
93
  const label = `Mail review row ${index + 1}`;
90
- const version = Number(item.version);
91
- if (item.schema !== 'whoisleuth.cli.bulk.item' || ![2, 3].includes(version)
94
+ const version = item.version;
95
+ if (item.schema !== 'whoisleuth.cli.bulk.item' || typeof version !== 'number' || ![2, 3].includes(version)
92
96
  || (expectedVersion !== null && version !== expectedVersion)) {
93
97
  throw new CliUsageError(`${label} must use the matching WHOISleuth Bulk item schema version 2 or 3.`);
94
98
  }
@@ -193,6 +197,7 @@ function normalizeMailRow(value) {
193
197
  const providers = providerDomains(hosts);
194
198
  return {
195
199
  domain,
200
+ provenance: mailRowProvenance(item),
196
201
  state,
197
202
  dnsStatus,
198
203
  hasMx,
@@ -210,6 +215,29 @@ function normalizeMailRow(value) {
210
215
  ],
211
216
  };
212
217
  }
218
+ function mailRowProvenance(item) {
219
+ return Object.freeze({
220
+ sourceGeneratedAt: timestamp(item.generatedAt, 'Bulk row generation time'),
221
+ observedAt: item.version === 3 ? normalizeExplicitIsoTimestamp(item.observedAt) : null,
222
+ dnsObservedAt: item.ok === true ? normalizeExplicitIsoTimestamp(record(record(item.availability).dns).observedAt) : null,
223
+ collectionOrigin: item.version === 3
224
+ ? item.collectionOrigin
225
+ : 'unknown',
226
+ });
227
+ }
228
+ function failedMailRow(item, index) {
229
+ let target = null;
230
+ try {
231
+ target = classifyQuery(String(item.query)).value;
232
+ }
233
+ catch { /* Invalid targets remain identifiable by input position. */ }
234
+ return Object.freeze({
235
+ rowNumber: index + 1,
236
+ target,
237
+ state: 'collection_failed',
238
+ provenance: mailRowProvenance(item),
239
+ });
240
+ }
213
241
  function parseInput(textValue) {
214
242
  if (typeof textValue !== 'string' || Buffer.byteLength(textValue, 'utf8') > MAX_MAIL_REVIEW_INPUT_BYTES) {
215
243
  throw new CliUsageError(`Mail review input is limited to ${MAX_MAIL_REVIEW_INPUT_BYTES} bytes.`);
@@ -223,12 +251,12 @@ function parseInput(textValue) {
223
251
  scanBoundedJson(textInput);
224
252
  const parsed = JSON.parse(textInput);
225
253
  const document = record(parsed);
226
- if (document.schema === 'whoisleuth.cli.bulk' && [2, 3].includes(Number(document.version)) && Array.isArray(document.results)) {
254
+ if (document.schema === 'whoisleuth.cli.bulk' && typeof document.version === 'number' && [2, 3].includes(document.version) && Array.isArray(document.results)) {
227
255
  values = document.results;
228
256
  containerVersion = Number(document.version);
229
257
  timestamp(document.generatedAt, 'Mail review Bulk generation time');
230
258
  }
231
- else if (document.schema === 'whoisleuth.cli.bulk.item' && [2, 3].includes(Number(document.version))) {
259
+ else if (document.schema === 'whoisleuth.cli.bulk.item' && typeof document.version === 'number' && [2, 3].includes(document.version)) {
232
260
  values = [document];
233
261
  }
234
262
  else {
@@ -258,6 +286,13 @@ function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
258
286
  const rows = inputRows.map(normalizeMailRow).filter((row) => Boolean(row));
259
287
  if (!rows.length)
260
288
  throw new CliUsageError('Mail review input contains no completed domain results.');
289
+ const failedRows = inputRows.flatMap((row, index) => row.ok === false ? [failedMailRow(row, index)] : []);
290
+ const inputCoverage = Object.freeze({
291
+ inputRows: inputRows.length,
292
+ reviewedRows: rows.length,
293
+ failedRows: failedRows.length,
294
+ unmeasuredObservationRows: [...rows, ...failedRows].filter((row) => row.provenance.observedAt === null).length,
295
+ });
261
296
  const counts = {
262
297
  authenticated_mail: 0,
263
298
  evidence_incomplete: 0,
@@ -293,12 +328,16 @@ function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
293
328
  limitation: 'Shared mail providers are common and do not establish common ownership, control, intent, safety, or maliciousness.',
294
329
  };
295
330
  })
296
- .sort((left, right) => left.providerDomain.localeCompare(right.providerDomain));
331
+ .sort((left, right) => compareCodeUnits(left.providerDomain, right.providerDomain));
297
332
  const providerRelationships = allProviderRelationships.slice(0, 100);
333
+ const rowsWithIncompleteDns = rows.filter((row) => row.dnsStatus !== 'success' || row.hasMx === null || row.hasNullMx === null).length;
298
334
  const providerCoverage = {
299
- complete: rows.every((row) => !row.providerDomainsTruncated)
335
+ complete: failedRows.length === 0 && rowsWithIncompleteDns === 0
336
+ && rows.every((row) => !row.providerDomainsTruncated)
300
337
  && allProviderRelationships.length <= 100
301
338
  && allProviderRelationships.every((relationship) => !relationship.domainsTruncated),
339
+ failedRows: failedRows.length,
340
+ rowsWithIncompleteDns,
302
341
  rowsWithOmittedProviders: rows.filter((row) => row.providerDomainsTruncated).length,
303
342
  omittedProviderDomains: rows.reduce((sum, row) => sum + row.providerDomainsOmitted, 0),
304
343
  relationshipCount: allProviderRelationships.length,
@@ -309,16 +348,20 @@ function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
309
348
  return {
310
349
  schema: CLI_MAIL_REVIEW_SCHEMA,
311
350
  version: CLI_MAIL_REVIEW_VERSION,
312
- generatedAt,
351
+ generatedAt: timestamp(generatedAt, 'Mail review generation time'),
313
352
  counts,
314
353
  rows,
354
+ failedRows,
355
+ inputCoverage,
315
356
  providerRelationships,
316
357
  providerCoverage,
317
358
  limitations: [
318
359
  'This review is passive and uses DNS evidence already retained in a WHOISleuth Bulk result; it makes no network request.',
319
- 'Null MX, no explicit MX, receiving mail, authentication gaps, and incomplete evidence remain separate states.',
360
+ PASSIVE_MAIL_INTERPRETATION,
320
361
  'SMTP delivery, mailbox existence, catch-all behaviour, banner collection, and message acceptance were not tested.',
321
- ...(!providerCoverage.complete
362
+ ...(failedRows.length ? [`${failedRows.length} input target${failedRows.length === 1 ? '' : 's'} failed collection and cannot contribute mail evidence. Input positions and normalised targets are retained; consult the original input for failure diagnostics.`] : []),
363
+ ...(rowsWithIncompleteDns ? [`${rowsWithIncompleteDns} reviewed domain${rowsWithIncompleteDns === 1 ? ' has' : 's have'} incomplete DNS evidence; unobserved provider relationships remain unknown.`] : []),
364
+ ...(providerCoverage.rowsWithOmittedProviders || providerCoverage.omittedRelationships || providerCoverage.omittedRelationshipDomains
322
365
  ? ['Mail-provider relationship coverage reached a configured row, relationship, or domain bound; omitted relationships remain unknown.']
323
366
  : []),
324
367
  ],
@@ -327,19 +370,30 @@ function buildCliMailReview(textValue, generatedAt = new Date().toISOString()) {
327
370
  function formatCliMailReview(document) {
328
371
  const lines = [
329
372
  'Passive mail exposure review',
373
+ `Input targets ${document.inputCoverage.inputRows}`,
330
374
  `Domains ${document.rows.length}`,
331
- `Authenticated ${document.counts.authenticated_mail}`,
332
- `Auth gaps ${document.counts.mail_auth_gap}`,
375
+ `Failed targets ${document.inputCoverage.failedRows}`,
376
+ `MX + SPF + DMARC ${document.counts.authenticated_mail}`,
377
+ `DNS policy gaps ${document.counts.mail_auth_gap}`,
333
378
  `Null MX ${document.counts.null_mx}`,
334
379
  `No explicit MX ${document.counts.no_explicit_mx}`,
335
- `Incomplete ${document.counts.evidence_incomplete + document.counts.mail_auth_incomplete}`,
380
+ `Incomplete ${document.counts.evidence_incomplete + document.counts.mail_auth_incomplete + document.inputCoverage.failedRows}`,
336
381
  '',
337
382
  ];
338
383
  for (const row of document.rows) {
339
- lines.push(`${row.domain} ${row.state.replaceAll('_', ' ')}`);
384
+ lines.push(`${row.domain} ${PASSIVE_MAIL_LABELS[row.state]}`);
385
+ lines.push(` Observed ${row.provenance.observedAt ?? 'unknown'} · ${row.provenance.collectionOrigin.replaceAll('_', ' ')}`);
386
+ lines.push(` DNS observed ${row.provenance.dnsObservedAt ?? 'unknown'} · Source output ${row.provenance.sourceGeneratedAt}`);
340
387
  lines.push(` MX providers ${row.providerDomains.join(', ') || 'None observed'}${row.providerDomainsOmitted ? ` · +${row.providerDomainsOmitted} omitted` : ''}`);
341
388
  lines.push(` SPF / DMARC ${row.hasSpf === null ? 'unknown' : row.hasSpf ? 'observed' : 'not observed'} / ${row.hasDmarc === null ? 'unknown' : row.hasDmarc ? 'observed' : 'not observed'}`);
342
389
  }
390
+ if (document.failedRows.length) {
391
+ lines.push('', 'Failed collection inputs');
392
+ for (const row of document.failedRows) {
393
+ lines.push(`Input ${row.rowNumber} ${row.target ?? 'unrecognised target'} · collection failed`);
394
+ lines.push(` Observed ${row.provenance.observedAt ?? 'unknown'} · ${row.provenance.collectionOrigin.replaceAll('_', ' ')} · Source output ${row.provenance.sourceGeneratedAt}`);
395
+ }
396
+ }
343
397
  if (document.providerRelationships.length) {
344
398
  lines.push('', 'Shared mail-provider relationships');
345
399
  for (const relationship of document.providerRelationships) {
@@ -347,7 +401,7 @@ function formatCliMailReview(document) {
347
401
  }
348
402
  }
349
403
  if (!document.providerCoverage.complete) {
350
- lines.push('', `Provider coverage partial ${document.providerCoverage.omittedProviderDomains} row providers · ${document.providerCoverage.omittedRelationships} relationships · ${document.providerCoverage.omittedRelationshipDomains} relationship domains omitted`);
404
+ lines.push('', `Provider coverage partial ${document.providerCoverage.failedRows} failed targets · ${document.providerCoverage.rowsWithIncompleteDns} incomplete DNS rows · ${document.providerCoverage.omittedProviderDomains} row providers · ${document.providerCoverage.omittedRelationships} relationships · ${document.providerCoverage.omittedRelationshipDomains} relationship domains omitted`);
351
405
  }
352
406
  lines.push('', 'Limitations:');
353
407
  for (const limitation of document.limitations)
package/cli/manual.mjs CHANGED
@@ -24,6 +24,7 @@ ${commands}
24
24
  Human-readable output is the default. Versioned JSON and JSONL are available where documented. Diagnostics and optional progress events use standard error. Use --output and optional --force for atomic private file output. Use --palette auto, light, or dark for a fixed terminal palette. Lookup --browse provides an interactive terminal view.
25
25
  .SH EXIT STATUS
26
26
  0 indicates command completion, 2 invalid usage, 3 a collection or comparison failure, 4 an explicitly detected partial result, 70 an internal bootstrap failure, 130 analyst cancellation, and 143 service termination.
27
+ For verify-artifact, use --strict-exit in scripts: incomplete verification then returns 4. Without it, producing a verification report can return 0 even when its checks fail. Read the report state and checks.
27
28
  .SH PRIVACY
28
29
  Network commands disclose the target to the sources named in focused help. Offline commands read local input only. Output files use private permissions and remain on the operator's machine.
29
30
  .SH LICENSE
@@ -0,0 +1,51 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import { commandDefaultText } from "./command-reference.mjs";
3
+ import { NETWORK_COMMAND_DEFINITIONS } from "./network-command-definitions.mjs";
4
+ import { terminalOptions, jsonOutput, parseOutput, singleInputAction, } from "./argument-values.mjs";
5
+ function parsePostureArguments(parsed) {
6
+ const mailProfile = parsed.optionValue('--mail-profile') ?? commandDefaultText('posture', '--mail-profile');
7
+ return {
8
+ action: 'posture',
9
+ domain: parsed.positionalValue('domain'),
10
+ output: parseOutput(parsed, NETWORK_COMMAND_DEFINITIONS.posture),
11
+ selectorText: parsed.optionValue('--selectors'),
12
+ retiredSelectorText: parsed.optionValue('--retired-selectors'),
13
+ mailProfile: mailProfile === 'defensive-no-mail'
14
+ ? 'defensive_no_mail'
15
+ : mailProfile,
16
+ ownedDomain: parsed.hasOption('--owned-domain'),
17
+ ...(parsed.hasOption('--include-inherited-dns') ? { includeInheritedDns: true } : {}),
18
+ ...terminalOptions(parsed),
19
+ };
20
+ }
21
+ function parseDnssecValidateArguments(parsed) {
22
+ return {
23
+ action: 'dnssec-validate',
24
+ target: parsed.positionalValue('domain'),
25
+ resolver: parsed.optionValue('--resolver'),
26
+ trustAnchorSource: parsed.optionValue('--trust-anchor'),
27
+ ownedOrAuthorized: true,
28
+ output: jsonOutput(parsed),
29
+ ...terminalOptions(parsed),
30
+ };
31
+ }
32
+ function parseMailTransportArguments(parsed) {
33
+ return {
34
+ action: 'mail-transport',
35
+ source: parsed.positionalValue('source'),
36
+ resolver: parsed.optionValue('--resolver'),
37
+ trustAnchorSource: parsed.optionValue('--trust-anchor'),
38
+ ownedOrAuthorized: true,
39
+ activeProbeAcknowledged: true,
40
+ output: jsonOutput(parsed),
41
+ ...terminalOptions(parsed),
42
+ };
43
+ }
44
+ export const NETWORK_ARGUMENT_PARSERS = Object.freeze({
45
+ 'ct-search': (parsed) => singleInputAction('ct-search', parsed, 'keyword'),
46
+ posture: parsePostureArguments,
47
+ http: (parsed) => singleInputAction('http', parsed, 'domain'),
48
+ tls: (parsed) => singleInputAction('tls', parsed, 'hostname'),
49
+ 'dnssec-validate': parseDnssecValidateArguments,
50
+ 'mail-transport': parseMailTransportArguments,
51
+ });
@@ -0,0 +1,130 @@
1
+ import { positional, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
2
+ export const NETWORK_COMMAND_DEFINITIONS = Object.freeze({
3
+ "ct-search": commandSeed({
4
+ reference: {
5
+ description: 'Search certificate-transparency observations for one bounded keyword.',
6
+ example: 'whoisleuth ct-search "example brand" --json',
7
+ boundary: 'Certificate observations do not prove website activity, registration ownership, or malicious intent.',
8
+ },
9
+ collection: { scope: 'Accepts one bounded search keyword and queries the fixed certificate-transparency source.' },
10
+ summary: 'Search certificate observations',
11
+ options: ['--json', '--quiet', '--no-color'],
12
+ positionals: Object.freeze([positional('keyword', 'text', 0, 1, [], 'argv_or_stdin')]),
13
+ constraints: EMPTY_CONSTRAINTS,
14
+ handlerOwner: 'network',
15
+ networkEffect: 'always_network',
16
+ common: false,
17
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.ct-search']),
18
+ primaryArtefacts: Object.freeze([]),
19
+ planSupport: false,
20
+ additionalOutputFormats: Object.freeze([]),
21
+ bootstrapProfile: 'allowed',
22
+ }),
23
+ posture: commandSeed({
24
+ reference: {
25
+ description: 'Review bounded DNS mail, delegation, and domain-control posture.',
26
+ example: 'whoisleuth posture example.test --mail-profile standard --json',
27
+ boundary: 'Missing or failed DNS observations remain inconclusive. --include-inherited-dns adds inherited policy and reporting-boundary reviews within 32 additional TXT queries and ten seconds, plus a direct parent-delegation sample. No message is sent and receiver enforcement is not inferred.',
28
+ },
29
+ collection: { scope: 'Accepts one domain and performs bounded RDAP, DNS, and conditional MTA-STS HTTPS requests. --include-inherited-dns separately adds ancestor and reporting-destination DMARC questions and direct DNS/TCP to sampled parent servers.' },
30
+ summary: 'Review DNS and mail posture',
31
+ options: ['--selectors', '--retired-selectors', '--mail-profile', '--include-inherited-dns', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
32
+ positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
33
+ constraints: Object.freeze([
34
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--sarif'] }),
35
+ constraint({ kind: 'requires_all', option: '--sarif', requiredOptions: ['--owned-domain'] }),
36
+ ]),
37
+ handlerOwner: 'network',
38
+ networkEffect: 'always_network',
39
+ common: false,
40
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.posture']),
41
+ primaryArtefacts: Object.freeze([]),
42
+ planSupport: false,
43
+ additionalOutputFormats: Object.freeze([]),
44
+ bootstrapProfile: 'allowed',
45
+ }),
46
+ http: commandSeed({
47
+ reference: {
48
+ description: 'Inspect one homepage request, redirects, and bounded response metadata.',
49
+ example: 'whoisleuth http example.test --json',
50
+ 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.',
51
+ },
52
+ collection: { scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
53
+ summary: 'Inspect one homepage request',
54
+ options: ['--json', '--quiet', '--no-color'],
55
+ positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
56
+ constraints: EMPTY_CONSTRAINTS,
57
+ handlerOwner: 'network',
58
+ networkEffect: 'always_network',
59
+ common: false,
60
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.http']),
61
+ primaryArtefacts: Object.freeze([]),
62
+ planSupport: false,
63
+ additionalOutputFormats: Object.freeze([]),
64
+ bootstrapProfile: 'allowed',
65
+ }),
66
+ tls: commandSeed({
67
+ reference: {
68
+ description: 'Inspect one hostname certificate through a bounded TLS connection.',
69
+ example: 'whoisleuth tls example.test --json',
70
+ boundary: 'One observed connection is point-in-time evidence and does not establish every address, edge, or historical certificate.',
71
+ },
72
+ collection: { scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
73
+ summary: 'Inspect one TLS connection',
74
+ options: ['--json', '--quiet', '--no-color'],
75
+ positionals: Object.freeze([positional('hostname', 'text', 0, 1, [], 'argv_or_stdin')]),
76
+ constraints: EMPTY_CONSTRAINTS,
77
+ handlerOwner: 'network',
78
+ networkEffect: 'always_network',
79
+ common: false,
80
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.tls']),
81
+ primaryArtefacts: Object.freeze([]),
82
+ planSupport: false,
83
+ additionalOutputFormats: Object.freeze([]),
84
+ bootstrapProfile: 'allowed',
85
+ }),
86
+ "dnssec-validate": commandSeed({
87
+ reference: {
88
+ description: 'Cryptographically validate one authorised DNSSEC chain from a supplied trust anchor through one selected public resolver.',
89
+ example: 'whoisleuth dnssec-validate example.test --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --json',
90
+ 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.',
91
+ },
92
+ collection: { 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.' },
93
+ summary: 'Validate an authorised DNSSEC chain',
94
+ options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--json', '--quiet', '--no-color'],
95
+ positionals: Object.freeze([positional('domain', 'text', 1, 1)]),
96
+ constraints: Object.freeze([
97
+ constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized'] }),
98
+ ]),
99
+ handlerOwner: 'network',
100
+ networkEffect: 'always_network',
101
+ common: false,
102
+ schemaIdentifiers: Object.freeze(['whoisleuth.dnssec-chain-validation', 'whoisleuth.dnssec-trust-anchor']),
103
+ primaryArtefacts: Object.freeze([]),
104
+ planSupport: false,
105
+ additionalOutputFormats: Object.freeze([]),
106
+ bootstrapProfile: 'allowed',
107
+ }),
108
+ "mail-transport": commandSeed({
109
+ reference: {
110
+ description: 'Review selected authorised MX endpoints, DNSSEC-qualified TLSA evidence, SMTP capabilities, and optional STARTTLS certificates.',
111
+ example: 'whoisleuth mail-transport selected-mx.json --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --active-probe --json',
112
+ 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.',
113
+ },
114
+ collection: { scope: 'Accepts at most three selected authorised MX hosts, uses one public resolver, and performs sequential bounded SMTP connections with no retries.' },
115
+ summary: 'Review selected authorised SMTP transports',
116
+ options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe', '--json', '--quiet', '--no-color'],
117
+ positionals: OPTIONAL_FILE_POSITIONAL,
118
+ constraints: Object.freeze([
119
+ constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe'] }),
120
+ ]),
121
+ handlerOwner: 'network',
122
+ networkEffect: 'always_network',
123
+ common: false,
124
+ schemaIdentifiers: Object.freeze(['whoisleuth.mail-transport.input', 'whoisleuth.cli.mail-transport-review']),
125
+ primaryArtefacts: Object.freeze([]),
126
+ planSupport: false,
127
+ additionalOutputFormats: Object.freeze([]),
128
+ bootstrapProfile: 'allowed',
129
+ }),
130
+ });
@@ -1,6 +1,7 @@
1
+ import { runDiscriminatedCommandHandler, } from "./discriminated-command-handlers.mjs";
1
2
  import { fetchHomepage } from "../lib/availability.mjs";
2
3
  import { searchCertificateTransparency } from "../lib/ct-search.mjs";
3
- import { checkDomainPosture, normalizeAuditDomain, normalizeDkimSelectors } from "../lib/domain-posture.mjs";
4
+ import { checkDomainPosture, normalizeAuditDomain, normalizeDkimSelectors, } from "../lib/domain-posture.mjs";
4
5
  import { collectTlsIntelligence, normalizeTlsHostname } from "../lib/tls-intelligence.mjs";
5
6
  import { MAX_DNSSEC_TRUST_ANCHOR_BYTES, formatDnssecChainReport, validateDnssecChain, } from "../lib/dnssec-chain-validation.mjs";
6
7
  import { MAX_MAIL_TRANSPORT_INPUT_BYTES, collectMailTransportReview, formatMailTransportReview, } from "../lib/smtp-transport-review.mjs";
@@ -11,9 +12,10 @@ import { buildHttpProbeResult } from "./http.mjs";
11
12
  import { normalizePostureSelectors } from "./posture.mjs";
12
13
  import EXIT_CODES from "./exit-codes.mjs";
13
14
  import { buildPostureSarif } from "./ci-report.mjs";
14
- async function runNetworkCommand(args, dependencies, context) {
15
- if (args.action === 'ct-search') {
16
- const keyword = args.keyword || await context.readSingleInput();
15
+ const networkCommandHandlers = {
16
+ 'ct-search': async (args, dependencies, context) => {
17
+ context.setFailureLabel('Certificate Transparency search');
18
+ const keyword = args.keyword || (await context.readSingleInput());
17
19
  if (!keyword)
18
20
  throw new CliUsageError('ct-search requires one keyword as an argument or on stdin.');
19
21
  const search = dependencies.searchCertificateTransparency || searchCertificateTransparency;
@@ -25,9 +27,10 @@ async function runNetworkCommand(args, dependencies, context) {
25
27
  : context.terminal(formatTerminalCtSearch(document), args.color));
26
28
  }
27
29
  return EXIT_CODES.SUCCESS;
28
- }
29
- if (args.action === 'posture') {
30
- const requestedDomain = args.domain || await context.readSingleInput();
30
+ },
31
+ posture: async (args, dependencies, context) => {
32
+ context.setFailureLabel('Domain posture audit');
33
+ const requestedDomain = args.domain || (await context.readSingleInput());
31
34
  if (!requestedDomain)
32
35
  throw new CliUsageError('posture requires one domain as an argument or on stdin.');
33
36
  const normalizeDomain = dependencies.normalizeAuditDomain || normalizeAuditDomain;
@@ -44,6 +47,8 @@ async function runNetworkCommand(args, dependencies, context) {
44
47
  dkimSelectors,
45
48
  retiredDkimSelectors,
46
49
  mailProtectionProfile: args.mailProfile,
50
+ ...(args.includeInheritedDns ? { includeInheritedDns: true } : {}),
51
+ ...(dependencies.signal ? { signal: dependencies.signal } : {}),
47
52
  }));
48
53
  const document = buildCliPostureDocument(requestedDomain, report, context.now());
49
54
  if (!args.quiet) {
@@ -54,9 +59,10 @@ async function runNetworkCommand(args, dependencies, context) {
54
59
  : context.terminal(formatTerminalPosture(document), args.color));
55
60
  }
56
61
  return EXIT_CODES.SUCCESS;
57
- }
58
- if (args.action === 'http') {
59
- const requestedDomain = args.domain || await context.readSingleInput();
62
+ },
63
+ http: async (args, dependencies, context) => {
64
+ context.setFailureLabel('HTTP probe');
65
+ const requestedDomain = args.domain || (await context.readSingleInput());
60
66
  if (!requestedDomain)
61
67
  throw new CliUsageError('http requires one domain as an argument or on stdin.');
62
68
  const normalizeDomain = dependencies.normalizeAuditDomain || normalizeAuditDomain;
@@ -72,8 +78,9 @@ async function runNetworkCommand(args, dependencies, context) {
72
78
  : context.terminal(formatTerminalHttp(document), args.color));
73
79
  }
74
80
  return EXIT_CODES.SUCCESS;
75
- }
76
- if (args.action === 'dnssec-validate') {
81
+ },
82
+ 'dnssec-validate': async (args, dependencies, context) => {
83
+ context.setFailureLabel('DNSSEC chain validation');
77
84
  let anchorInput;
78
85
  try {
79
86
  anchorInput = dependencies.readTrustAnchorInput
@@ -101,8 +108,9 @@ async function runNetworkCommand(args, dependencies, context) {
101
108
  return report.state === 'secure' || report.state === 'insecure'
102
109
  ? EXIT_CODES.SUCCESS
103
110
  : EXIT_CODES.PARTIAL_FAILURE;
104
- }
105
- if (args.action === 'mail-transport') {
111
+ },
112
+ 'mail-transport': async (args, dependencies, context) => {
113
+ context.setFailureLabel('Mail transport review');
106
114
  let input;
107
115
  let anchorInput;
108
116
  try {
@@ -148,22 +156,28 @@ async function runNetworkCommand(args, dependencies, context) {
148
156
  : context.terminal(formatMailTransportReview(review), args.color));
149
157
  }
150
158
  return review.runState === 'complete' ? EXIT_CODES.SUCCESS : EXIT_CODES.PARTIAL_FAILURE;
151
- }
152
- const requestedHostname = args.hostname || await context.readSingleInput();
153
- if (!requestedHostname)
154
- throw new CliUsageError('tls requires one hostname as an argument or on stdin.');
155
- const normalizeHostname = dependencies.normalizeTlsHostname || normalizeTlsHostname;
156
- const hostname = normalizeHostname(requestedHostname);
157
- if (!hostname)
158
- throw new CliUsageError('tls requires a valid DNS hostname, not an IP address.');
159
- const collect = dependencies.collectTlsIntelligence || collectTlsIntelligence;
160
- const result = await context.withProgress('Inspecting the current TLS connection', () => collect(hostname));
161
- const document = buildCliTlsDocument(requestedHostname, result, context.now());
162
- if (!args.quiet) {
163
- context.writeStdout(args.output === 'json'
164
- ? formatJsonDocument(document)
165
- : context.terminal(formatTerminalTls(document), args.color));
166
- }
167
- return EXIT_CODES.SUCCESS;
159
+ },
160
+ tls: async (args, dependencies, context) => {
161
+ context.setFailureLabel('TLS evidence collection');
162
+ const requestedHostname = args.hostname || (await context.readSingleInput());
163
+ if (!requestedHostname)
164
+ throw new CliUsageError('tls requires one hostname as an argument or on stdin.');
165
+ const normalizeHostname = dependencies.normalizeTlsHostname || normalizeTlsHostname;
166
+ const hostname = normalizeHostname(requestedHostname);
167
+ if (!hostname)
168
+ throw new CliUsageError('tls requires a valid DNS hostname, not an IP address.');
169
+ const collect = dependencies.collectTlsIntelligence || collectTlsIntelligence;
170
+ const result = await context.withProgress('Inspecting the current TLS connection', () => collect(hostname));
171
+ const document = buildCliTlsDocument(requestedHostname, result, context.now());
172
+ if (!args.quiet) {
173
+ context.writeStdout(args.output === 'json'
174
+ ? formatJsonDocument(document)
175
+ : context.terminal(formatTerminalTls(document), args.color));
176
+ }
177
+ return EXIT_CODES.SUCCESS;
178
+ },
179
+ };
180
+ async function runNetworkCommand(args, dependencies, context) {
181
+ return runDiscriminatedCommandHandler(networkCommandHandlers, args, dependencies, context);
168
182
  }
169
183
  export { runNetworkCommand };
@@ -1,5 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
2
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
3
+ import { CONTEXT_INPUT_SCHEMAS, reviewContextInput } from "../packages/investigation/context-review.mjs";
4
+ import { CONTEXT_REVIEW_KINDS } from "../packages/contracts/context-review.mjs";
3
5
  import { normalizeEncryptedDnsAdapter, planEncryptedDnsQuery, } from "../lib/encrypted-dns-contract.mjs";
4
6
  import { validateDnssecEvidence } from "../lib/dnssec-evidence-validation.mjs";
5
7
  import { reviewDomainChange } from "../lib/domain-change-review.mjs";
@@ -15,7 +17,7 @@ import { reviewDnsConvergence } from "../lib/dns-convergence-review.mjs";
15
17
  import { compareTrustStoreEvidence } from "../lib/trust-store-comparison.mjs";
16
18
  import { CliUsageError } from "./errors.mjs";
17
19
  import { safeTerminalValue } from "./formatters/terminal.mjs";
18
- import { LOCAL_MMDB_QUERY_SCHEMA, reviewLocalMmdb } from "./local-mmdb-review.mjs";
20
+ import { LOCAL_MMDB_QUERY_SCHEMA, LOCAL_MMDB_QUERY_VERSION, LOCAL_MMDB_REVIEW_SCHEMA, LOCAL_MMDB_REVIEW_VERSION, reviewLocalMmdb } from "./local-mmdb-review.mjs";
19
21
  const OFFLINE_EVIDENCE_REVIEW_SCHEMA = 'whoisleuth.cli.offline-evidence-review';
20
22
  const OFFLINE_EVIDENCE_REVIEW_VERSION = 1;
21
23
  const MAX_OFFLINE_EVIDENCE_INPUT_BYTES = 16 * 1024 * 1024;
@@ -43,16 +45,24 @@ function parseInput(value) {
43
45
  throw new CliUsageError('Offline evidence review requires one valid bounded JSON document without duplicate keys.');
44
46
  }
45
47
  const document = record(parsed);
46
- if (document.version !== 1 || typeof document.schema !== 'string') {
48
+ if (typeof document.schema !== 'string' || document.version !== 1
49
+ && !(document.schema === LOCAL_MMDB_QUERY_SCHEMA && document.version === LOCAL_MMDB_QUERY_VERSION)) {
47
50
  throw new CliUsageError('Offline evidence review requires a supported versioned input schema.');
48
51
  }
49
52
  return document;
50
53
  }
51
54
  function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString()) {
52
55
  const input = parseInput(value);
56
+ if (input.schema === LOCAL_MMDB_QUERY_SCHEMA)
57
+ throw new CliUsageError('Local MMDB review requires --mmdb <database-file>.');
53
58
  let kind;
54
59
  let result;
55
- if (input.schema === RDAP_SEARCH_INPUT_SCHEMA) {
60
+ if (CONTEXT_INPUT_SCHEMAS.includes(input.schema)) {
61
+ const review = reviewContextInput(input, generatedAt);
62
+ kind = review.kind;
63
+ result = review;
64
+ }
65
+ else if (input.schema === RDAP_SEARCH_INPUT_SCHEMA) {
56
66
  kind = 'rdap_search';
57
67
  const request = record(input.request);
58
68
  const help = normalizeRdapSearchHelp(input.help);
@@ -163,7 +173,7 @@ async function buildOfflineEvidenceReviewWithLocalResources(value, generatedAt =
163
173
  version: OFFLINE_EVIDENCE_REVIEW_VERSION,
164
174
  generatedAt,
165
175
  kind: 'geoip',
166
- result: await reviewLocalMmdb(input, options.mmdbPath),
176
+ result: await reviewLocalMmdb(input, options.mmdbPath, generatedAt),
167
177
  limitations: Object.freeze([
168
178
  'The review is local and uses only the supplied query metadata and analyst-supplied database. It does not transmit the address or refresh the database.',
169
179
  ]),
@@ -171,6 +181,9 @@ async function buildOfflineEvidenceReviewWithLocalResources(value, generatedAt =
171
181
  }
172
182
  function formatOfflineEvidenceReview(document) {
173
183
  const result = record(document.result);
184
+ if (document.kind === 'geoip' && result.schema === LOCAL_MMDB_REVIEW_SCHEMA && result.version !== LOCAL_MMDB_REVIEW_VERSION) {
185
+ throw new CliUsageError('Offline evidence review does not support this MMDB result version.');
186
+ }
174
187
  const gate = record(result.gate);
175
188
  const counts = record(result.counts);
176
189
  const count = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
@@ -191,7 +204,16 @@ function formatOfflineEvidenceReview(document) {
191
204
  `Kind ${document.kind.replaceAll('_', ' ')}`,
192
205
  `State ${state.replaceAll('_', ' ')}`,
193
206
  ];
194
- if (document.kind === 'rdap_search') {
207
+ if (CONTEXT_REVIEW_KINDS.includes(document.kind)) {
208
+ lines.push(safeTerminalValue(result.summary, ''));
209
+ for (const item of Array.isArray(result.observations) ? result.observations : []) {
210
+ const row = record(item);
211
+ lines.push('', `${safeTerminalValue(row.label, 'Observation')} [${safeTerminalValue(row.state, 'unknown')}]`, ` ${safeTerminalValue(row.detail, '')}`, ` Source: ${safeTerminalValue(row.source, '')} · ${safeTerminalValue(row.observedAt, 'time unknown')}`);
212
+ }
213
+ for (const step of Array.isArray(result.nextSteps) ? result.nextSteps : [])
214
+ lines.push(`Next: ${safeTerminalValue(step, '')}`);
215
+ }
216
+ else if (document.kind === 'rdap_search') {
195
217
  const help = record(result.help);
196
218
  const plan = record(result.plan);
197
219
  const responseInspection = record(result.responseInspection);
@@ -200,6 +222,17 @@ function formatOfflineEvidenceReview(document) {
200
222
  if (result.responseInspection !== null)
201
223
  lines.push(`Result ${String(responseInspection.state ?? 'invalid').replaceAll('_', ' ')}`);
202
224
  }
225
+ else if (document.kind === 'geoip' && result.schema === LOCAL_MMDB_REVIEW_SCHEMA && result.version === LOCAL_MMDB_REVIEW_VERSION) {
226
+ const database = record(result.database), metadata = record(database.metadata), binaryFormat = record(metadata.binaryFormat);
227
+ const freshness = record(result.freshness), policy = record(freshness.policy), source = record(result.source), match = record(result.match);
228
+ lines.push(`Outcome ${safeTerminalValue(result.reason, 'unknown').replaceAll('_', ' ')}`, `Coverage ${safeTerminalValue(result.completeness, 'unavailable')}`, `Database SHA-256 ${safeTerminalValue(database.sha256, 'unavailable')}`, `Database bytes ${safeTerminalValue(database.byteLength, 'unavailable')}`, `Intrinsic type ${safeTerminalValue(metadata.databaseType, 'unavailable')}`, `Built ${safeTerminalValue(metadata.builtAt, 'unavailable')}`, `Binary format ${safeTerminalValue(binaryFormat.major, '?')}.${safeTerminalValue(binaryFormat.minor, '?')} · IP version ${safeTerminalValue(metadata.ipVersion, '?')}`, `Freshness ${safeTerminalValue(freshness.state, 'unknown')} · checked ${safeTerminalValue(freshness.checkedAt, 'unknown')}`, `Age policy ${safeTerminalValue(policy.maxAgeDays, '?')} days · ${safeTerminalValue(policy.rationale, '')}`, `Declared source ${safeTerminalValue(source.label, 'unknown')} · version ${safeTerminalValue(source.version, 'unknown')} · licence ${safeTerminalValue(source.license, 'unknown')}`);
229
+ if (result.match !== null) {
230
+ lines.push(`Matched network ${safeTerminalValue(match.network, 'unknown')}`, `Country ${safeTerminalValue(match.countryCode, 'unknown')} · region ${safeTerminalValue(match.region, 'unknown')} · city ${safeTerminalValue(match.city, 'unknown')}`, `ASN ${safeTerminalValue(match.asn, 'unknown')} · network label ${safeTerminalValue(match.asName, 'unknown')}`);
231
+ }
232
+ }
233
+ else if (document.kind === 'geoip') {
234
+ lines.push('Intrinsic MMDB freshness is not assessed by this result. A version-2 MMDB query can assess a selected local database against an explicit age policy.');
235
+ }
203
236
  else if (document.kind === 'zone_intent') {
204
237
  const desired = record(result.desired);
205
238
  lines.push(`Desired ${listLength(desired.records)}`);