@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
@@ -4,6 +4,8 @@ import { buildBulkDomainComparison, } from "../packages/investigation/bulk-domai
4
4
  import { relationshipObservation } from "../packages/comparison/relationship-evidence.mjs";
5
5
  import { CliUsageError } from "./errors.mjs";
6
6
  import { parseSavedLookupDocument } from "./saved-lookup.mjs";
7
+ import { lookupObservationHostname } from "../packages/evidence/lookup-target.mjs";
8
+ import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
7
9
  export const CLI_LOOKUP_DIFF_SCHEMA = 'whoisleuth.cli.lookup-diff';
8
10
  export const CLI_LOOKUP_DIFF_VERSION = 1;
9
11
  function text(value, maximum = 300) {
@@ -129,19 +131,22 @@ function lookupComparisonInput(document) {
129
131
  function buildCliLookupDiff(leftText, rightText, generatedAt = new Date().toISOString(), options = {}) {
130
132
  const left = parseSavedLookupDocument(leftText, { label: 'Left lookup input' });
131
133
  const right = parseSavedLookupDocument(rightText, { label: 'Right lookup input' });
132
- const domainMode = options.domainMode ?? 'different';
134
+ const domainMode = options.domainMode === 'auto'
135
+ ? left.registrableDomain === right.registrableDomain ? 'same' : 'different'
136
+ : options.domainMode ?? 'different';
133
137
  if (domainMode === 'different' && left.registrableDomain === right.registrableDomain) {
134
138
  throw new CliUsageError('Lookup diff requires documents for two different domains.');
135
139
  }
136
140
  if (domainMode === 'same' && left.registrableDomain !== right.registrableDomain) {
137
141
  throw new CliUsageError('Lookup history requires observations for the same domain.');
138
142
  }
139
- const comparison = buildBulkDomainComparison(lookupComparisonInput(left), lookupComparisonInput(right), domainMode === 'same' ? right.generatedAt : null, {
143
+ const measuredComparison = buildBulkDomainComparison(lookupComparisonInput(left), lookupComparisonInput(right), domainMode === 'same' ? right.generatedAt : null, {
140
144
  allowSameDomain: domainMode === 'same',
141
145
  ...(domainMode === 'same' ? { now: Date.parse(right.generatedAt) } : {}),
142
146
  });
143
- if (!comparison)
147
+ if (!measuredComparison)
144
148
  throw new CliUsageError('Lookup documents could not be compared.');
149
+ const comparison = qualifyLookupComparison(measuredComparison, left, right, domainMode === 'same');
145
150
  return {
146
151
  schema: CLI_LOOKUP_DIFF_SCHEMA,
147
152
  version: CLI_LOOKUP_DIFF_VERSION,
@@ -160,14 +165,44 @@ function buildCliLookupDiff(leftText, rightText, generatedAt = new Date().toISOS
160
165
  ],
161
166
  };
162
167
  }
168
+ function qualifyLookupComparison(comparison, left, right, temporal) {
169
+ const leftScope = recordOrEmpty(left.availability);
170
+ const rightScope = recordOrEmpty(right.availability);
171
+ const hostnameChanged = temporal && lookupObservationHostname(leftScope) !== lookupObservationHostname(rightScope);
172
+ const selectedPage = leftScope.webObservationMode === 'selected_url' || rightScope.webObservationMode === 'selected_url';
173
+ if (!hostnameChanged && !selectedPage)
174
+ return comparison;
175
+ const limitation = 'The retained collection hostname differs or a selected URL has no retained complete path/query identity. Values remain visible, but those fields cannot establish change at the same target.';
176
+ const rows = comparison.rows.map((row) => {
177
+ const web = ['web', 'technology', 'identity'].includes(row.category);
178
+ const incomparable = hostnameChanged && !['registration', 'lifecycle', 'source'].includes(row.category)
179
+ || temporal && selectedPage && web;
180
+ return {
181
+ ...row,
182
+ ...(selectedPage && web ? { method: 'Bounded website observation; one or both captures concern a selected URL rather than the homepage' } : {}),
183
+ ...(incomparable ? {
184
+ state: 'not_recorded',
185
+ source: `${row.source}; comparable target identity not recorded`,
186
+ leftSourceState: 'not_recorded',
187
+ rightSourceState: 'not_recorded',
188
+ limitations: [...row.limitations, limitation],
189
+ } : {}),
190
+ };
191
+ });
192
+ return {
193
+ ...comparison, rows,
194
+ counts: Object.fromEntries(Object.keys(comparison.counts).map((state) => [state, rows.filter((row) => row.state === state).length])),
195
+ limitations: [...comparison.limitations, limitation],
196
+ };
197
+ }
163
198
  function formatCliLookupDiff(document) {
164
199
  const rows = document.comparison.rows.filter((row) => row.state !== 'equal');
165
200
  const output = [
166
201
  'Domain evidence diff',
167
- `Left ${document.left.domain}`,
168
- `Left observed ${document.left.generatedAt}`,
169
- `Right ${document.right.domain}`,
170
- `Right observed ${document.right.generatedAt}`,
202
+ `Left ${safeTerminalValue(document.left.domain)}`,
203
+ `Left observed ${safeTerminalValue(document.left.generatedAt)}`,
204
+ `Right ${safeTerminalValue(document.right.domain)}`,
205
+ `Right observed ${safeTerminalValue(document.right.generatedAt)}`,
171
206
  `Changed ${document.comparison.counts.different + document.comparison.counts.conflicting}`,
172
207
  `One-sided ${document.comparison.counts.missing}`,
173
208
  `Unavailable ${document.comparison.counts.unavailable + document.comparison.counts.not_recorded}`,
@@ -176,14 +211,14 @@ function formatCliLookupDiff(document) {
176
211
  if (!rows.length)
177
212
  output.push('No bounded differences were observed in the comparable fields.');
178
213
  for (const row of rows) {
179
- output.push(`${row.label} [${row.state.replaceAll('_', ' ')}]`);
180
- output.push(` Left: ${row.left}`);
181
- output.push(` Right: ${row.right}`);
182
- output.push(` Source: ${row.source}`);
214
+ output.push(`${safeTerminalValue(row.label)} [${safeTerminalValue(row.state).replaceAll('_', ' ')}]`);
215
+ output.push(` Left: ${safeTerminalValue(row.left)}`);
216
+ output.push(` Right: ${safeTerminalValue(row.right)}`);
217
+ output.push(` Source: ${safeTerminalValue(row.source)}`);
183
218
  }
184
219
  output.push('', 'Limitations:');
185
220
  for (const limitation of document.limitations)
186
- output.push(` - ${limitation}`);
221
+ output.push(` - ${safeTerminalValue(limitation)}`);
187
222
  return `${output.join('\n')}\n`;
188
223
  }
189
224
  export { buildCliLookupDiff, formatCliLookupDiff };
@@ -1,3 +1,4 @@
1
+ import { plannedLookupSources } from "../lib/lookup-progress.mjs";
1
2
  export const CLI_LOOKUP_PLAN_SCHEMA = 'whoisleuth.cli.lookup-plan';
2
3
  export const CLI_LOOKUP_PLAN_VERSION = 1;
3
4
  const RDAP = Object.freeze({
@@ -21,7 +22,7 @@ const FAST_DOMAIN_EVIDENCE = Object.freeze({
21
22
  const DEEP_DOMAIN_EVIDENCE = Object.freeze({
22
23
  source: 'domain_evidence',
23
24
  purpose: 'Collect bounded DNS, HTTP, TLS, page-identity, technology, and security-posture evidence.',
24
- disclosure: 'DNS resolvers and the target website infrastructure receive the hostname through bounded probes.',
25
+ disclosure: 'DNS, TLS and website probes use the submitted hostname. Registration-delegation queries use the registrable domain.',
25
26
  conditional: false,
26
27
  });
27
28
  const REGISTRAR_RDAP = Object.freeze({
@@ -42,19 +43,18 @@ const REVERSE_DNS = Object.freeze({
42
43
  disclosure: 'A DNS resolver receives the reverse lookup for the normalised IP address.',
43
44
  conditional: false,
44
45
  });
45
- function plannedSources(classified, deep) {
46
- if (!deep) {
47
- return Object.freeze(classified.type === 'domain' ? [RDAP, FAST_DOMAIN_EVIDENCE] : [RDAP]);
48
- }
49
- if (classified.type === 'domain') {
50
- return Object.freeze([RDAP, WHOIS, DEEP_DOMAIN_EVIDENCE, REGISTRAR_RDAP, NETWORK_CONTEXT]);
51
- }
52
- if (classified.type === 'ipv4' || classified.type === 'ipv6') {
53
- return Object.freeze([RDAP, WHOIS, REVERSE_DNS]);
54
- }
55
- return Object.freeze([RDAP, WHOIS]);
46
+ function plannedSources(classified, deep, selectedUrl) {
47
+ const domainEvidence = selectedUrl && deep ? Object.freeze({
48
+ ...DEEP_DOMAIN_EVIDENCE,
49
+ disclosure: 'The selected URL path and query are sent to the website and followed redirects. The fragment is not sent. DNS and TLS use its hostname; registration uses the registrable domain. Saved HTTP provenance omits queries, but page-derived text may contain sensitive information.',
50
+ }) : deep ? DEEP_DOMAIN_EVIDENCE : FAST_DOMAIN_EVIDENCE;
51
+ const sources = {
52
+ rdap: RDAP, whois: WHOIS, domain_evidence: domainEvidence,
53
+ registrar_rdap: REGISTRAR_RDAP, network_context: NETWORK_CONTEXT, reverse_dns: REVERSE_DNS,
54
+ };
55
+ return Object.freeze(plannedLookupSources(classified.type, deep ? 'deep' : 'fast').map(id => sources[id]));
56
56
  }
57
- function buildCliLookupPlan(query, classified, deep) {
57
+ function buildCliLookupPlan(query, classified, deep, selectedUrl = false) {
58
58
  return Object.freeze({
59
59
  schema: CLI_LOOKUP_PLAN_SCHEMA,
60
60
  version: CLI_LOOKUP_PLAN_VERSION,
@@ -69,7 +69,7 @@ function buildCliLookupPlan(query, classified, deep) {
69
69
  planning: Object.freeze({
70
70
  networkRequestsMade: false,
71
71
  collectionRequiresNetwork: true,
72
- sources: plannedSources(classified, deep),
72
+ sources: plannedSources(classified, deep, selectedUrl),
73
73
  }),
74
74
  limitations: Object.freeze([
75
75
  'This is a local preflight. It does not test source availability, feature configuration, cache state, redirects, referrals, or the exact number of requests a completed lookup may require.',
@@ -81,6 +81,8 @@ function formatCliLookupPlan(plan) {
81
81
  const lines = [
82
82
  'WHOISleuth lookup preflight',
83
83
  `Target: ${plan.target.normalized}`,
84
+ ...(plan.target.inputHostname && plan.target.inputHostname !== plan.target.normalized
85
+ ? [`Submitted hostname: ${plan.target.inputHostname}`] : []),
84
86
  `Type: ${plan.target.type}`,
85
87
  `Mode: ${plan.mode}`,
86
88
  'Network requests made: no',
@@ -1,6 +1,8 @@
1
1
  import { CliUsageError } from "./errors.mjs";
2
2
  import { buildCliLookupDiff } from "./lookup-diff.mjs";
3
3
  import { parseSavedLookupDocument } from "./saved-lookup.mjs";
4
+ import { compareObservationContexts } from "../packages/comparison/capture-context.mjs";
5
+ import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
4
6
  const CLI_LOOKUP_RECONCILIATION_SCHEMA = 'whoisleuth.cli.lookup-reconciliation';
5
7
  const CLI_LOOKUP_RECONCILIATION_VERSION = 1;
6
8
  const MAX_LOOKUP_RECONCILIATION_INPUT_BYTES = 32 * 1024 * 1024;
@@ -79,15 +81,11 @@ function buildCliLookupReconciliation(inputs, generatedAt = new Date().toISOStri
79
81
  };
80
82
  });
81
83
  const observations = documents.map(observationContext);
82
- const identities = observations.map((observation) => (observation.observerLabel && observation.vantageLabel
83
- ? `${observation.observerLabel}\u0000${observation.vantageLabel}`
84
- : null));
85
- const labelsComplete = identities.every((identity) => identity !== null);
86
- const labelsDistinct = labelsComplete && new Set(identities).size === identities.length;
87
- const independence = labelsDistinct
84
+ const context = compareObservationContexts(observations.map(observation => ({ ...observation, observedAt: observation.generatedAt })));
85
+ const independence = context.labels === 'distinct'
88
86
  ? {
89
87
  state: 'verified_distinct_labels',
90
- reason: 'Every observation carries a different analyst-supplied observer and vantage label.',
88
+ reason: 'Every observation carries a distinct analyst-supplied observer/vantage label pair. Actual collection independence is not verified.',
91
89
  }
92
90
  : {
93
91
  state: 'unverified',
@@ -117,25 +115,30 @@ function buildCliLookupReconciliation(inputs, generatedAt = new Date().toISOStri
117
115
  };
118
116
  }
119
117
  function formatCliLookupReconciliation(document) {
118
+ const context = compareObservationContexts(document.observations.map(observation => ({ ...observation, observedAt: observation.generatedAt })));
120
119
  const output = [
121
120
  'Lookup observation reconciliation',
122
- `Domain ${document.domain}`,
121
+ `Domain ${safeTerminalValue(document.domain)}`,
123
122
  `Observations ${document.summary.observationCount}`,
124
- `Independence ${document.independence.state.replaceAll('_', ' ')}`,
123
+ `Declared labels ${context.labels}; collection independence not verified`,
124
+ `Capture time span ${context.time.spanMilliseconds === null ? 'unknown' : `${context.time.spanMilliseconds / 1000} seconds`}`,
125
125
  `Agreement ${document.summary.agreement}`,
126
126
  `Disagreement ${document.summary.disagreement}`,
127
127
  `Non-comparable ${document.summary.nonComparable}`,
128
128
  '',
129
+ ...document.observations.map((observation, index) => `Observation ${index + 1}: ${safeTerminalValue(observation.generatedAt)} · ${safeTerminalValue(observation.mode)} · observer ${safeTerminalValue(observation.observerLabel, 'not declared')} · vantage ${safeTerminalValue(observation.vantageLabel, 'not declared')}`),
130
+ '',
129
131
  ];
130
132
  for (const field of document.fields.filter((candidate) => candidate.state !== 'agreement')) {
131
- output.push(`${field.label} [${field.state.replaceAll('_', ' ')}]`);
133
+ output.push(`${safeTerminalValue(field.label)} [${safeTerminalValue(field.state).replaceAll('_', ' ')}]`);
132
134
  for (const value of field.values) {
133
- output.push(` Observation ${value.observation + 1}: ${value.value} (${value.sourceState.replaceAll('_', ' ')})`);
135
+ output.push(` Observation ${value.observation + 1}: ${safeTerminalValue(value.value)} (${safeTerminalValue(value.sourceState).replaceAll('_', ' ')})`);
134
136
  }
135
137
  }
136
138
  output.push('', 'Limitations:');
137
139
  for (const limitation of document.limitations)
138
- output.push(` - ${limitation}`);
140
+ output.push(` - ${safeTerminalValue(limitation)}`);
141
+ output.push(' - These selected observations do not establish worldwide availability, removal or takedown.');
139
142
  return `${output.join('\n')}\n`;
140
143
  }
141
144
  export { CLI_LOOKUP_RECONCILIATION_SCHEMA, CLI_LOOKUP_RECONCILIATION_VERSION, MAX_LOOKUP_RECONCILIATION_INPUT_BYTES, buildCliLookupReconciliation, formatCliLookupReconciliation, };
@@ -2,6 +2,7 @@ import { Buffer } from 'node:buffer';
2
2
  import { CliUsageError } from "./errors.mjs";
3
3
  import { buildCliLookupDiff } from "./lookup-diff.mjs";
4
4
  import { parseSavedLookupDocument } from "./saved-lookup.mjs";
5
+ import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
5
6
  export const CLI_LOOKUP_TIMELINE_SCHEMA = 'whoisleuth.cli.lookup-timeline';
6
7
  export const CLI_LOOKUP_TIMELINE_VERSION = 1;
7
8
  export const MAX_LOOKUP_TIMELINE_OBSERVATIONS = 20;
@@ -95,10 +96,10 @@ function buildCliLookupTimeline(inputs, generatedAt = new Date().toISOString())
95
96
  function formatCliLookupTimeline(document) {
96
97
  const output = [
97
98
  'Same-domain observation timeline',
98
- `Domain ${document.domain}`,
99
+ `Domain ${safeTerminalValue(document.domain)}`,
99
100
  `Observations ${document.summary.observationCount}`,
100
- `First observed ${document.observations[0]?.generatedAt || 'Unavailable'}`,
101
- `Last observed ${document.observations.at(-1)?.generatedAt || 'Unavailable'}`,
101
+ `First observed ${safeTerminalValue(document.observations[0]?.generatedAt, 'Unavailable')}`,
102
+ `Last observed ${safeTerminalValue(document.observations.at(-1)?.generatedAt, 'Unavailable')}`,
102
103
  `Changed transitions ${document.summary.transitionsWithObservedChanges}`,
103
104
  ];
104
105
  for (const transition of document.transitions) {
@@ -106,18 +107,18 @@ function formatCliLookupTimeline(document) {
106
107
  const to = document.observations[transition.toObservation];
107
108
  const counts = transition.comparison.counts;
108
109
  const changedRows = transition.comparison.rows.filter((row) => (row.state === 'conflicting' || row.state === 'different' || row.state === 'missing'));
109
- output.push('', `${from?.generatedAt || 'Unavailable'} to ${to?.generatedAt || 'Unavailable'}`, ` Observed changes ${counts.conflicting + counts.different}`, ` One-sided fields ${counts.missing}`, ` Unavailable ${counts.unavailable + counts.not_recorded}`);
110
+ output.push('', `${safeTerminalValue(from?.generatedAt, 'Unavailable')} to ${safeTerminalValue(to?.generatedAt, 'Unavailable')}`, ` Observed changes ${counts.conflicting + counts.different}`, ` One-sided fields ${counts.missing}`, ` Unavailable ${counts.unavailable + counts.not_recorded}`);
110
111
  if (!changedRows.length)
111
112
  output.push(' No observed field changes in comparable evidence.');
112
113
  for (const row of changedRows.slice(0, 10)) {
113
- output.push(` ${row.label} [${row.state.replaceAll('_', ' ')}]: ${row.left} -> ${row.right}`);
114
+ output.push(` ${safeTerminalValue(row.label)} [${safeTerminalValue(row.state).replaceAll('_', ' ')}]: ${safeTerminalValue(row.left)} -> ${safeTerminalValue(row.right)}`);
114
115
  }
115
116
  if (changedRows.length > 10)
116
117
  output.push(` ${changedRows.length - 10} additional changed fields omitted; use --json for the complete bounded comparison.`);
117
118
  }
118
119
  output.push('', 'Limitations:');
119
120
  for (const limitation of document.limitations)
120
- output.push(` - ${limitation}`);
121
+ output.push(` - ${safeTerminalValue(limitation)}`);
121
122
  return `${output.join('\n')}\n`;
122
123
  }
123
124
  export { buildCliLookupTimeline, formatCliLookupTimeline };
@@ -1,20 +1,19 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { Buffer } from 'node:buffer';
3
- import { isValidAsciiDomainName } from "../lib/hostname.mjs";
3
+ import { MAX_MAIL_REVIEW_DOMAINS, addressDomains, messageIdDomains, authenticationServiceDomains, dkimSigningDomains, normalizeHeaderDomain } from "../packages/investigation/mail-header-identity.mjs";
4
+ export { MAX_MAIL_REVIEW_DOMAINS } from "../packages/investigation/mail-header-identity.mjs";
4
5
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
5
6
  import { CliUsageError } from "./errors.mjs";
7
+ import { reviewMailAuthentication, selectReceiverTrust, authenticationHeaderLabel, aggregateMailAuthentication } from "../packages/investigation/mail-authentication-review.mjs";
6
8
  export const CLI_MAIL_HEADER_REVIEW_SCHEMA = 'whoisleuth.cli.mail-header-review';
7
- export const CLI_MAIL_HEADER_REVIEW_VERSION = 1;
9
+ export const CLI_MAIL_HEADER_REVIEW_VERSION = 2;
8
10
  export const MAX_MAIL_HEADER_INPUT_BYTES = 256 * 1024;
9
11
  export const MAX_MAIL_HEADER_FIELDS = 512;
10
12
  export const MAX_MAIL_HEADER_LINE_BYTES = 4 * 1024;
11
13
  export const MAX_MAIL_HEADER_VALUE_LENGTH = 16 * 1024;
12
14
  export const MAX_MAIL_RECEIVED_HOPS = 64;
13
- export const MAX_MAIL_REVIEW_DOMAINS = 128;
14
15
  const UNSAFE_HEADER_CONTROL_RE = /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f-\u009f\u200b-\u200f\u202a-\u202e\u2060-\u206f]/u;
15
16
  const HEADER_NAME_RE = /^[A-Za-z0-9-]{1,78}$/u;
16
- const AUTHENTICATION_METHODS = Object.freeze(['spf', 'dkim', 'dmarc', 'arc']);
17
- const AUTHENTICATION_STATES = new Set(['fail', 'neutral', 'none', 'pass', 'permerror', 'softfail', 'temperror']);
18
17
  function boundedHeaderSection(value) {
19
18
  if (typeof value !== 'string' || !value)
20
19
  throw new CliUsageError('Mail-header review requires one UTF-8 message or header block.');
@@ -75,240 +74,6 @@ function parseHeaderFields(value) {
75
74
  function fieldsNamed(fields, name) {
76
75
  return fields.filter((field) => field.name === name).map((field) => field.value);
77
76
  }
78
- function normalizeHeaderDomain(value) {
79
- const normalized = value.trim().toLowerCase().replace(/^\[|\]$/gu, '').replace(/\.$/u, '');
80
- return isValidAsciiDomainName(normalized, { requireLowercase: true }) ? normalized : null;
81
- }
82
- function lexicalSegments(value, delimiter, trackAngles = false) {
83
- const segments = [];
84
- let start = 0;
85
- let quoted = false;
86
- let escaped = false;
87
- let commentDepth = 0;
88
- let angleDepth = 0;
89
- for (let index = 0; index < value.length; index += 1) {
90
- const character = value[index] ?? '';
91
- if (escaped) {
92
- escaped = false;
93
- continue;
94
- }
95
- if (character === '\\' && (quoted || commentDepth > 0)) {
96
- escaped = true;
97
- continue;
98
- }
99
- if (commentDepth > 0) {
100
- if (character === '(')
101
- commentDepth += 1;
102
- else if (character === ')')
103
- commentDepth -= 1;
104
- continue;
105
- }
106
- if (quoted) {
107
- if (character === '"')
108
- quoted = false;
109
- continue;
110
- }
111
- if (character === '"')
112
- quoted = true;
113
- else if (character === '(')
114
- commentDepth = 1;
115
- else if (character === ')')
116
- return null;
117
- else if (trackAngles && character === '<') {
118
- if (angleDepth !== 0)
119
- return null;
120
- angleDepth = 1;
121
- }
122
- else if (trackAngles && character === '>') {
123
- if (angleDepth !== 1)
124
- return null;
125
- angleDepth = 0;
126
- }
127
- else if (character === delimiter && angleDepth === 0) {
128
- segments.push(value.slice(start, index));
129
- start = index + 1;
130
- }
131
- }
132
- if (escaped || quoted || commentDepth !== 0 || angleDepth !== 0)
133
- return null;
134
- segments.push(value.slice(start));
135
- return segments;
136
- }
137
- function angleAddress(value) {
138
- let quoted = false;
139
- let escaped = false;
140
- let commentDepth = 0;
141
- let start = -1;
142
- let end = -1;
143
- for (let index = 0; index < value.length; index += 1) {
144
- const character = value[index] ?? '';
145
- if (escaped) {
146
- escaped = false;
147
- continue;
148
- }
149
- if (character === '\\' && (quoted || commentDepth > 0)) {
150
- escaped = true;
151
- continue;
152
- }
153
- if (commentDepth > 0) {
154
- if (character === '(')
155
- commentDepth += 1;
156
- else if (character === ')')
157
- commentDepth -= 1;
158
- continue;
159
- }
160
- if (quoted) {
161
- if (character === '"')
162
- quoted = false;
163
- continue;
164
- }
165
- if (character === '"')
166
- quoted = true;
167
- else if (character === '(')
168
- commentDepth = 1;
169
- else if (character === ')')
170
- return null;
171
- else if (character === '<') {
172
- if (start >= 0 || end >= 0)
173
- return null;
174
- start = index;
175
- }
176
- else if (character === '>') {
177
- if (start < 0 || end >= 0)
178
- return null;
179
- end = index;
180
- }
181
- }
182
- if (escaped || quoted || commentDepth !== 0 || (start >= 0) !== (end >= 0))
183
- return null;
184
- return start >= 0 ? value.slice(start + 1, end) : value;
185
- }
186
- function addressDomain(value) {
187
- let candidate = '';
188
- let quoted = false;
189
- let escaped = false;
190
- let commentDepth = 0;
191
- const separators = [];
192
- for (const character of value) {
193
- if (escaped) {
194
- candidate += character;
195
- escaped = false;
196
- continue;
197
- }
198
- if (character === '\\' && (quoted || commentDepth > 0)) {
199
- if (quoted)
200
- candidate += character;
201
- escaped = true;
202
- continue;
203
- }
204
- if (commentDepth > 0) {
205
- if (character === '(')
206
- commentDepth += 1;
207
- else if (character === ')')
208
- commentDepth -= 1;
209
- continue;
210
- }
211
- if (quoted) {
212
- candidate += character;
213
- if (character === '"')
214
- quoted = false;
215
- continue;
216
- }
217
- if (character === '"') {
218
- quoted = true;
219
- candidate += character;
220
- }
221
- else if (character === '(')
222
- commentDepth = 1;
223
- else if (character === ')')
224
- return null;
225
- else {
226
- if (character === '@')
227
- separators.push(candidate.length);
228
- candidate += character;
229
- }
230
- }
231
- if (escaped || quoted || commentDepth !== 0 || separators.length !== 1)
232
- return null;
233
- const separator = separators[0] ?? -1;
234
- const local = candidate.slice(0, separator).trim();
235
- const domain = candidate.slice(separator + 1).trim();
236
- const validLocal = /^(?:[^\s"<>(),:;@]+|"(?:[^"\\]|\\.)+")$/u.test(local);
237
- if (!validLocal || !domain || /\s/u.test(domain))
238
- return null;
239
- return normalizeHeaderDomain(domain);
240
- }
241
- function addressDomains(values) {
242
- const domains = new Set();
243
- for (const value of values) {
244
- const mailboxes = lexicalSegments(value, ',', true);
245
- if (!mailboxes)
246
- continue;
247
- for (const mailbox of mailboxes) {
248
- const selected = angleAddress(mailbox);
249
- const domain = selected === null ? null : addressDomain(selected);
250
- if (domain)
251
- domains.add(domain);
252
- if (domains.size >= MAX_MAIL_REVIEW_DOMAINS)
253
- return [...domains].sort();
254
- }
255
- }
256
- return [...domains].sort();
257
- }
258
- function messageIdDomains(values) {
259
- return addressDomains(values.map((value) => value.replace(/[<>]/gu, '')));
260
- }
261
- function authenticationState(values, method, directValues = []) {
262
- const states = new Set();
263
- let observations = 0;
264
- for (const value of values) {
265
- const clauses = lexicalSegments(value, ';');
266
- if (!clauses)
267
- continue;
268
- for (const clause of clauses) {
269
- const match = /^\s*([a-z][a-z0-9_-]*)\s*=\s*([a-z0-9_-]+)(?=\s|$)/iu.exec(clause);
270
- if (match?.[1]?.toLowerCase() === method) {
271
- observations += 1;
272
- const raw = (match[2] ?? '').toLowerCase();
273
- states.add(AUTHENTICATION_STATES.has(raw) ? raw : 'unknown');
274
- }
275
- }
276
- }
277
- for (const value of directValues) {
278
- const match = /^\s*([a-z0-9_-]+)(?=\s|\(|$)/iu.exec(value);
279
- const raw = (match?.[1] ?? '').toLowerCase();
280
- if (!raw)
281
- continue;
282
- observations += 1;
283
- states.add(AUTHENTICATION_STATES.has(raw) ? raw : 'unknown');
284
- }
285
- return Object.freeze({
286
- state: observations === 0 ? 'unknown' : states.size === 1 ? [...states][0] : 'mixed',
287
- observations,
288
- });
289
- }
290
- function authenticationServiceDomains(values) {
291
- const output = new Set();
292
- for (const value of values) {
293
- const clauses = lexicalSegments(value, ';');
294
- const service = normalizeHeaderDomain((clauses?.[0] ?? '').trim().split(/\s/u, 1)[0] ?? '');
295
- if (service)
296
- output.add(service);
297
- }
298
- return [...output].sort().slice(0, MAX_MAIL_REVIEW_DOMAINS);
299
- }
300
- function dkimSigningDomains(values) {
301
- const output = new Set();
302
- for (const value of values) {
303
- for (const clause of lexicalSegments(value, ';') ?? []) {
304
- const match = /^\s*d\s*=\s*([^\s;]+)/iu.exec(clause);
305
- const domain = normalizeHeaderDomain(match?.[1] ?? '');
306
- if (domain)
307
- output.add(domain);
308
- }
309
- }
310
- return [...output].sort().slice(0, MAX_MAIL_REVIEW_DOMAINS);
311
- }
312
77
  function reportedDate(values) {
313
78
  if (values.length !== 1 || values[0].length > 240)
314
79
  return null;
@@ -348,21 +113,26 @@ function addDomainRoles(store, domains, role) {
348
113
  store.set(domain, roles);
349
114
  }
350
115
  }
351
- export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOString()) {
116
+ export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOString(), trustedHeaders = []) {
352
117
  const normalizedGeneratedAt = normalizeExplicitIsoTimestamp(generatedAt);
353
118
  if (!normalizedGeneratedAt)
354
119
  throw new CliUsageError('Mail-header review generation time must be a valid ISO 8601 timestamp.');
355
120
  const parsed = parseHeaderFields(value);
121
+ let authenticationReview;
122
+ try {
123
+ authenticationReview = selectReceiverTrust(reviewMailAuthentication(parsed.fields), trustedHeaders);
124
+ }
125
+ catch (cause) {
126
+ if (cause instanceof TypeError)
127
+ throw new CliUsageError(cause.message);
128
+ throw cause;
129
+ }
356
130
  const fromDomains = addressDomains(fieldsNamed(parsed.fields, 'from'));
357
131
  const replyToDomains = addressDomains(fieldsNamed(parsed.fields, 'reply-to'));
358
132
  const returnPathDomains = addressDomains(fieldsNamed(parsed.fields, 'return-path'));
359
133
  const messageIdentifierDomains = messageIdDomains(fieldsNamed(parsed.fields, 'message-id'));
360
134
  const authenticationResults = fieldsNamed(parsed.fields, 'authentication-results');
361
- const receivedSpf = fieldsNamed(parsed.fields, 'received-spf');
362
- const authentication = Object.fromEntries(AUTHENTICATION_METHODS.map((method) => [
363
- method,
364
- authenticationState(authenticationResults, method, method === 'spf' ? receivedSpf : []),
365
- ]));
135
+ const authentication = aggregateMailAuthentication(authenticationReview);
366
136
  const authServiceDomains = authenticationServiceDomains(authenticationResults);
367
137
  const signerDomains = dkimSigningDomains(fieldsNamed(parsed.fields, 'dkim-signature'));
368
138
  const receivedValues = fieldsNamed(parsed.fields, 'received');
@@ -414,6 +184,7 @@ export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOSt
414
184
  messageIdentifierDomains: Object.freeze(messageIdentifierDomains),
415
185
  }),
416
186
  authentication: Object.freeze(authentication),
187
+ authenticationReview,
417
188
  routing: Object.freeze({
418
189
  receivedHops: Object.freeze(receivedHops),
419
190
  omittedReceivedHops,
@@ -428,6 +199,7 @@ export function buildCliMailHeaderReview(value, generatedAt = new Date().toISOSt
428
199
  limitations: Object.freeze([
429
200
  'This review parses analyst-selected message headers offline. It makes no DNS, SMTP, HTTP, registry or provider request.',
430
201
  'Authentication results are claims reported in the supplied headers; they are not independently validated here.',
202
+ 'Receiver trust is an explicit analyst selection for an exact header, not a conclusion from a matching service name. Aggregate counts combine all reported sources, not a trusted verdict.',
431
203
  'Exact-domain alignment is descriptive. Divergence can be legitimate and does not establish spoofing, abuse or maliciousness.',
432
204
  'Display names, subjects, address local parts, message bodies, attachments and raw header values are not retained in the output.',
433
205
  ...(omittedReceivedHops ? [`${omittedReceivedHops} older Received header${omittedReceivedHops === 1 ? ' was' : 's were'} omitted by the ${MAX_MAIL_RECEIVED_HOPS}-hop bound.`] : []),
@@ -444,8 +216,13 @@ export function formatCliMailHeaderReview(document) {
444
216
  `Reply-To domains ${document.identity.replyToDomains.join(', ') || 'Unavailable'}`,
445
217
  `Return-Path ${document.identity.returnPathDomains.join(', ') || 'Unavailable'}`,
446
218
  '',
447
- 'Reported authentication',
448
- ...AUTHENTICATION_METHODS.map((method) => ` ${method.toUpperCase().padEnd(6)} ${document.authentication[method].state} (${document.authentication[method].observations} observation${document.authentication[method].observations === 1 ? '' : 's'})`),
219
+ 'Authentication by source header',
220
+ ...document.authenticationReview.headers.flatMap(header => [
221
+ ` ${authenticationHeaderLabel(header)} · ${header.state} · receiver trust: ${header.receiverTrust.replaceAll('_', ' ')}`,
222
+ ...header.claims.map(claim => ` ${claim.method.toUpperCase()}/${claim.methodVersion}: ${claim.result}${claim.state === 'unsupported' ? ' (unsupported)' : ''}${claim.domains.map(value => ` · ${value.property}=${value.domain}`).join('')}${claim.duplicateProperties.length ? ` · ambiguous: ${claim.duplicateProperties.join(', ')}` : ''}`),
223
+ ...(header.duplicateOf ? [` Duplicate of header ${header.duplicateOf} in this part.`] : []),
224
+ ...header.issues.map(issue => ` ${issue}`),
225
+ ]),
449
226
  '',
450
227
  'Exact-domain alignment',
451
228
  ` From / Reply-To ${document.alignment.fromToReplyTo}`,