@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
@@ -39,7 +39,13 @@ function canonicalArtifactJsonV2(value) {
39
39
  }
40
40
  async function sha256ArtifactDigestV2(value) {
41
41
  const bytes = new TextEncoder().encode(canonicalArtifactJsonV2(value));
42
- const digest = await globalThis.crypto.subtle.digest('SHA-256', bytes);
42
+ return sha256ArtifactBytes(bytes);
43
+ }
44
+ async function sha256ArtifactBytes(bytes) {
45
+ const buffer = bytes.buffer;
46
+ if (!(buffer instanceof ArrayBuffer))
47
+ throw new TypeError('Artefact digests require non-shared bytes.');
48
+ const digest = await globalThis.crypto.subtle.digest('SHA-256', new Uint8Array(buffer, bytes.byteOffset, bytes.byteLength));
43
49
  return `sha256:${[...new Uint8Array(digest)]
44
50
  .map((byte) => byte.toString(16).padStart(2, '0'))
45
51
  .join('')}`;
@@ -62,4 +68,4 @@ function sha256ArtifactDigestFor(value, canonicalization) {
62
68
  ? sha256ArtifactDigest(value)
63
69
  : sha256ArtifactDigestV2(value);
64
70
  }
65
- export { SORTED_JSON_V1, SORTED_JSON_V2, canonicalArtifactJson, canonicalArtifactJsonFor, canonicalArtifactJsonV2, resolveArtifactCanonicalization, sha256ArtifactDigest, sha256ArtifactDigestFor, sha256ArtifactDigestV2, };
71
+ export { SORTED_JSON_V1, SORTED_JSON_V2, canonicalArtifactJson, canonicalArtifactJsonFor, canonicalArtifactJsonV2, resolveArtifactCanonicalization, sha256ArtifactDigest, sha256ArtifactDigestFor, sha256ArtifactDigestV2, sha256ArtifactBytes, };
@@ -0,0 +1,105 @@
1
+ const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
2
+ const DIGEST_RE = /^sha256:[a-f0-9]{64}$/u;
3
+ const DOMAIN_RE = /^(?=.{1,253}$)(?:[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u;
4
+ export const HEX_DIGEST_RE = /^[a-f0-9]{64}$/u;
5
+ export const SEMANTIC_VERSION_RE = /^(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)\.(?:0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/u;
6
+ export function fail(label) {
7
+ throw new TypeError(`${label} has an unsupported or malformed structure.`);
8
+ }
9
+ export function record(value, label) {
10
+ if (!value || typeof value !== 'object' || Array.isArray(value))
11
+ fail(label);
12
+ return value;
13
+ }
14
+ export function exact(value, keys, label) {
15
+ const source = record(value, label);
16
+ const actual = Object.keys(source);
17
+ if (actual.length !== keys.length || actual.some((key) => !keys.includes(key)))
18
+ fail(label);
19
+ return source;
20
+ }
21
+ export function exactOptional(value, required, optional, label) {
22
+ const source = record(value, label);
23
+ const actual = Object.keys(source);
24
+ if (required.some((key) => !actual.includes(key))
25
+ || actual.some((key) => !required.includes(key) && !optional.includes(key)))
26
+ fail(label);
27
+ return source;
28
+ }
29
+ export function text(value, label, maximum = 2_000, allowEmpty = false) {
30
+ if (typeof value !== 'string' || value.length > maximum || CONTROL_RE.test(value) || (!allowEmpty && !value))
31
+ fail(label);
32
+ return value;
33
+ }
34
+ export function optionalText(value, label, maximum = 2_000) {
35
+ if (value !== null)
36
+ text(value, label, maximum);
37
+ }
38
+ export function iso(value, label, nullable = false) {
39
+ if (nullable && value === null)
40
+ return;
41
+ const candidate = text(value, label, 64);
42
+ const parsed = Date.parse(candidate);
43
+ if (!Number.isFinite(parsed) || new Date(parsed).toISOString() !== candidate)
44
+ fail(label);
45
+ }
46
+ export function integer(value, label, minimum = 0, maximum = Number.MAX_SAFE_INTEGER) {
47
+ if (!Number.isSafeInteger(value) || Number(value) < minimum || Number(value) > maximum)
48
+ fail(label);
49
+ return Number(value);
50
+ }
51
+ export function boolean(value, label) {
52
+ if (typeof value !== 'boolean')
53
+ fail(label);
54
+ }
55
+ export function enumeration(value, allowed, label) {
56
+ if (typeof value !== 'string' || !allowed.includes(value))
57
+ fail(label);
58
+ return value;
59
+ }
60
+ export function array(value, label, maximum, minimum = 0) {
61
+ if (!Array.isArray(value) || value.length < minimum || value.length > maximum)
62
+ fail(label);
63
+ return value;
64
+ }
65
+ export function strings(value, label, maximum, textMaximum = 2_000) {
66
+ const values = array(value, label, maximum);
67
+ for (const item of values)
68
+ text(item, label, textMaximum);
69
+ return values;
70
+ }
71
+ export function digest(value, label) {
72
+ if (typeof value !== 'string' || !DIGEST_RE.test(value))
73
+ fail(label);
74
+ }
75
+ export function sameValues(left, right) {
76
+ return left.length === right.length && left.every((value, index) => value === right[index]);
77
+ }
78
+ export function domain(value, label) {
79
+ if (typeof value !== 'string' || !DOMAIN_RE.test(value))
80
+ fail(label);
81
+ }
82
+ export function absoluteUrl(value, label, protocols) {
83
+ const candidate = text(value, label, 2_048);
84
+ try {
85
+ const parsed = new URL(candidate);
86
+ if (!protocols.includes(parsed.protocol) || parsed.username || parsed.password || parsed.search || parsed.hash)
87
+ fail(label);
88
+ return parsed;
89
+ }
90
+ catch {
91
+ return fail(label);
92
+ }
93
+ }
94
+ export function nullableRecord(value, label) {
95
+ if (value !== null)
96
+ record(value, label);
97
+ }
98
+ export function validateIntegrity(value, label, version, currentVersion) {
99
+ if (version !== currentVersion)
100
+ fail(label);
101
+ const integrity = exact(value, ['algorithm', 'canonicalization', 'digestSha256'], label);
102
+ if (integrity.algorithm !== 'SHA-256' || integrity.canonicalization !== 'sorted-json-v2')
103
+ fail(label);
104
+ digest(integrity.digestSha256, label);
105
+ }
@@ -0,0 +1,67 @@
1
+ const STATES = new Set(['complete', 'partial', 'unavailable', 'not_collected', 'unknown']);
2
+ export function isWebCollectionQuality(value) {
3
+ if (!value || typeof value !== 'object' || Array.isArray(value))
4
+ return false;
5
+ const prototype = Object.getPrototypeOf(value);
6
+ if (prototype !== Object.prototype && prototype !== null)
7
+ return false;
8
+ const record = value;
9
+ const keys = Object.keys(record);
10
+ if (keys.length !== 4 || keys.some(key => !['version', 'page', 'favicon', 'combined'].includes(key)
11
+ || !Object.hasOwn(Object.getOwnPropertyDescriptor(record, key) ?? {}, 'value')))
12
+ return false;
13
+ return record.version === 1
14
+ && typeof record.page === 'string' && STATES.has(record.page)
15
+ && typeof record.favicon === 'string' && STATES.has(record.favicon)
16
+ && typeof record.combined === 'string' && STATES.has(record.combined)
17
+ && (record.combined !== 'complete' || (record.page === 'complete' && record.favicon === 'complete'));
18
+ }
19
+ export function normalizeWebCollectionQuality(value, depth) {
20
+ if (value === undefined)
21
+ return undefined;
22
+ if (!isWebCollectionQuality(value))
23
+ throw new TypeError('Unsupported or invalid web collection quality.');
24
+ if (depth === 'fast')
25
+ return capturedWebCollectionQuality('not_collected', 'not_collected');
26
+ return { version: 1, page: value.page, favicon: value.favicon, combined: value.combined };
27
+ }
28
+ export function capturedWebCollectionQuality(page, favicon) {
29
+ const combined = page === 'complete' && favicon === 'complete' ? 'complete'
30
+ : page === favicon ? page : 'partial';
31
+ return { version: 1, page, favicon, combined };
32
+ }
33
+ export function webCollectionQualityForCapture(value, depth) {
34
+ if (depth !== 'deep')
35
+ return capturedWebCollectionQuality('not_collected', 'not_collected');
36
+ return normalizeWebCollectionQuality(value) ?? capturedWebCollectionQuality('unknown', 'unknown');
37
+ }
38
+ const PAGE_FIELDS = new Set(['activityStatus', 'pageTitle', 'hasPasswordField', 'hasExternalFormAction', 'phishingLanguageMatch', 'pageBaselineMatch']);
39
+ const FAVICON_FIELDS = new Set(['faviconHash', 'faviconPHash', 'faviconMatch', 'faviconNearMatch']);
40
+ const COMBINED_FIELDS = new Set(['riskScore', 'riskFactors', 'riskModelVersion', 'opportunityScore', 'opportunityFactors', 'reusesOfficialAssets']);
41
+ /** Missing historical quality never proves that a false/null value was observed. */
42
+ export function webCollectionAllowsComparison(field, quality, depth) {
43
+ if (PAGE_FIELDS.has(field))
44
+ return quality?.page === 'complete';
45
+ if (FAVICON_FIELDS.has(field))
46
+ return quality?.favicon === 'complete';
47
+ if (COMBINED_FIELDS.has(field) && depth === 'deep')
48
+ return quality?.combined === 'complete';
49
+ return true;
50
+ }
51
+ /** Retain the measured score while qualifying incomplete inputs at presentation. */
52
+ export function webCollectionScoreLimitation(quality, depth) {
53
+ if (depth === 'unknown')
54
+ return 'Collection depth is unknown; score is not comparable.';
55
+ return webCollectionAllowsComparison('riskScore', quality, depth)
56
+ ? null : 'Incomplete or unknown web collection; score is not comparable.';
57
+ }
58
+ /** An incomplete current attempt cannot erase the last comparable baseline. */
59
+ export function mergeWebCollectionQuality(previous, current) {
60
+ if (!previous && !current)
61
+ return undefined;
62
+ return { version: 1,
63
+ page: current?.page === 'complete' ? 'complete' : previous?.page ?? current?.page ?? 'unknown',
64
+ favicon: current?.favicon === 'complete' ? 'complete' : previous?.favicon ?? current?.favicon ?? 'unknown',
65
+ combined: current?.combined === 'complete' ? 'complete' : previous?.combined ?? current?.combined ?? 'unknown',
66
+ };
67
+ }
@@ -352,7 +352,10 @@ function boundedDataArray(value, maximum, label) {
352
352
  || Number(length) < 0) {
353
353
  throw new TypeError(`${label} must have a stable non-negative length.`);
354
354
  }
355
- const retainedLength = Math.min(Number(length), maximum);
355
+ if (Number(length) > maximum) {
356
+ throw new TypeError(`${label} exceeds its ${maximum}-item admission bound; no partial fact was retained.`);
357
+ }
358
+ const retainedLength = Number(length);
356
359
  const output = [];
357
360
  for (let index = 0; index < retainedLength; index += 1) {
358
361
  const descriptor = stableOwnDescriptor(value, String(index), label);
@@ -0,0 +1,72 @@
1
+ import { MAX_LOOKUP_DNS_RECORDS_PER_TYPE } from "../analysis/lookup-network-evidence-bounds.mjs";
2
+ import { addressValue, canonicalIpAddress } from "../contracts/ip-address.mjs";
3
+ /** Fixed values retained in Observation v1's existing diagnostic detail slot. */
4
+ export const DNS_QUERY_OUTCOMES = Object.freeze({
5
+ records: { status: 'success', label: 'Records returned' },
6
+ empty_answer: { status: 'not_found', label: 'No records returned' },
7
+ no_data: { status: 'not_found', label: 'No data for this record type (NODATA)' },
8
+ name_not_found: { status: 'not_found', label: 'Name not found by resolver' },
9
+ timeout: { status: 'error', label: 'DNS query timed out' },
10
+ server_failure: { status: 'error', label: 'DNS server failed to answer (SERVFAIL)' },
11
+ refused: { status: 'error', label: 'DNS query refused' },
12
+ invalid_response: { status: 'error', label: 'DNS answer could not be read completely' },
13
+ error: { status: 'error', label: 'DNS query failed' },
14
+ skipped: { status: 'skipped', label: 'Not queried' },
15
+ });
16
+ function record(value) {
17
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
18
+ ? value : {};
19
+ }
20
+ /** Legacy not_found diagnostics do not identify which negative answer occurred. */
21
+ export function readDnsQueryOutcome(value) {
22
+ const diagnostic = record(value);
23
+ const detail = diagnostic.detail;
24
+ return typeof detail === 'string' && Object.hasOwn(DNS_QUERY_OUTCOMES, detail)
25
+ && DNS_QUERY_OUTCOMES[detail].status === diagnostic.status
26
+ ? detail : null;
27
+ }
28
+ export function dnsQueryFailureOutcome(code) {
29
+ switch (code) {
30
+ case 'ENODATA': return 'no_data';
31
+ case 'ENOTFOUND': return 'name_not_found';
32
+ case 'ETIMEOUT': return 'timeout';
33
+ case 'ESERVFAIL': return 'server_failure';
34
+ case 'EREFUSED': return 'refused';
35
+ case 'EBADRESP': return 'invalid_response';
36
+ default: return 'error';
37
+ }
38
+ }
39
+ /** One family, one source state. Unrelated mail/delegation failures cannot erase it. */
40
+ export function dnsAddressFamilyEvidence(dnsValue, family) {
41
+ const dns = record(dnsValue);
42
+ const diagnostic = record(record(dns.diagnostics)[family]);
43
+ const raw = record(dns.records)[family];
44
+ const candidates = Array.isArray(raw) ? raw.slice(0, MAX_LOOKUP_DNS_RECORDS_PER_TYPE) : [];
45
+ const addresses = candidates.flatMap(value => {
46
+ const address = canonicalIpAddress(value);
47
+ return address && addressValue(address)?.family === (family === 'a' ? 4 : 6) ? [address] : [];
48
+ });
49
+ const malformed = raw !== undefined && !Array.isArray(raw)
50
+ || candidates.length !== addresses.length;
51
+ const truncated = diagnostic.truncated === true
52
+ || typeof diagnostic.discarded === 'number' && diagnostic.discarded > 0
53
+ || Array.isArray(raw) && raw.length > candidates.length;
54
+ const outcome = readDnsQueryOutcome(diagnostic);
55
+ const inconsistent = outcome === 'records' && !addresses.length
56
+ || outcome !== null && outcome !== 'records' && addresses.length > 0;
57
+ const complete = !malformed && !truncated && !inconsistent && Array.isArray(raw)
58
+ && outcome !== null && DNS_QUERY_OUTCOMES[outcome].status !== 'error' && outcome !== 'skipped';
59
+ const state = malformed || inconsistent ? 'error'
60
+ : truncated ? 'partial'
61
+ : outcome ? DNS_QUERY_OUTCOMES[outcome].status
62
+ : diagnostic.status === 'error' ? 'error'
63
+ : diagnostic.status === 'skipped' || dns.status === 'skipped' ? 'skipped' : 'unknown';
64
+ const value = addresses.length ? [...new Set(addresses)].sort().join(' · ')
65
+ : malformed || inconsistent ? DNS_QUERY_OUTCOMES.invalid_response.label
66
+ : truncated ? 'DNS answer incomplete'
67
+ : outcome ? DNS_QUERY_OUTCOMES[outcome].label
68
+ : state === 'error' ? DNS_QUERY_OUTCOMES.error.label
69
+ : state === 'skipped' ? DNS_QUERY_OUTCOMES.skipped.label
70
+ : diagnostic.status === 'not_found' ? 'Negative DNS outcome not recorded' : null;
71
+ return { outcome, value, state, complete, truncated };
72
+ }
@@ -1,7 +1,9 @@
1
+ import { analystReviewAgeAt, ANALYST_REVIEW_STALE_AFTER_DAYS } from "./review-age.mjs";
1
2
  import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, } from "./artifact-integrity.mjs";
2
3
  import { normalizeDomain } from "./domain-name.mjs";
3
4
  import { normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp } from "./observation.mjs";
4
- import { DOMAIN_CONTROL_CAA_RECORD_KEYS, DOMAIN_CONTROL_DIGEST_SHA256_HEX_LENGTH, DOMAIN_CONTROL_DS_RECORD_KEYS, DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS, DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA as DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA, DOMAIN_CONTROL_MANIFEST_INPUT_KEYS, DOMAIN_CONTROL_MANIFEST_INPUT_VERSION, DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, DOMAIN_CONTROL_MANIFEST_INTEGRITY_KEYS, DOMAIN_CONTROL_MANIFEST_LIMITATIONS, DOMAIN_CONTROL_MANIFEST_ROOT_KEYS, DOMAIN_CONTROL_MANIFEST_SCHEMA as DOMAIN_CONTROL_PASSPORT_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, DOMAIN_CONTROL_MX_RECORD_KEYS, DOMAIN_CONTROL_RECORD_LIST_FIELDS, DOMAIN_CONTROL_SPKI_SHA256_HEX_LENGTH, DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_CAA_FLAGS, MAX_DOMAIN_CONTROL_CAA_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_CAA_TAG_LENGTH, MAX_DOMAIN_CONTROL_CAA_VALUE_LENGTH, MAX_DOMAIN_CONTROL_DOMAIN_LENGTH, MAX_DOMAIN_CONTROL_DS_ALGORITHM, MAX_DOMAIN_CONTROL_DS_DIGEST_LENGTH, MAX_DOMAIN_CONTROL_DS_DIGEST_TYPE, MAX_DOMAIN_CONTROL_DS_KEY_TAG, MAX_DOMAIN_CONTROL_DS_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_INPUT_RECORDS, MAX_DOMAIN_CONTROL_JSON_DEPTH, MAX_DOMAIN_CONTROL_JSON_VALUES, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES, MAX_DOMAIN_CONTROL_MX_PRIORITY, MAX_DOMAIN_CONTROL_MX_TEXT_LENGTH, MAX_DOMAIN_CONTROL_NAME_INPUT_LENGTH, MAX_DOMAIN_CONTROL_NOTE_LENGTH, MAX_DOMAIN_CONTROL_RECORD_INTEGER_TEXT_LENGTH, MAX_DOMAIN_CONTROL_TEXT_LENGTH, MAX_DOMAIN_CONTROL_TIMESTAMP_LENGTH, MIN_DOMAIN_CONTROL_DS_DIGEST_LENGTH, MIN_DOMAIN_CONTROL_MANIFEST_ENTRIES, MIN_DOMAIN_CONTROL_RECORDS, } from "../contracts/domain-control-manifest.mjs";
5
+ import { MAX_POSTURE_CHECK_RECORDS } from "./domain-posture-context.mjs";
6
+ import { DOMAIN_CONTROL_CAA_RECORD_KEYS, DOMAIN_CONTROL_DIGEST_SHA256_HEX_LENGTH, DOMAIN_CONTROL_DS_RECORD_KEYS, DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS, DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA as DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA, DOMAIN_CONTROL_MANIFEST_INPUT_KEYS, DOMAIN_CONTROL_MANIFEST_INPUT_VERSION, DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, DOMAIN_CONTROL_MANIFEST_INTEGRITY_KEYS, DOMAIN_CONTROL_MANIFEST_LIMITATIONS, DOMAIN_CONTROL_MANIFEST_ROOT_KEYS, DOMAIN_CONTROL_MANIFEST_SCHEMA as DOMAIN_CONTROL_PASSPORT_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, DOMAIN_CONTROL_MX_RECORD_KEYS, DOMAIN_CONTROL_RECORD_LIST_FIELDS, DOMAIN_CONTROL_RECORD_MODE_OPTIONS, DOMAIN_CONTROL_SPKI_SHA256_HEX_LENGTH, DOMAIN_CONTROL_TEXT_INPUT_BOUND_FACTOR, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS, MAX_CURRENT_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_CAA_FLAGS, MAX_DOMAIN_CONTROL_CAA_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_CAA_TAG_LENGTH, MAX_DOMAIN_CONTROL_CAA_VALUE_LENGTH, MAX_DOMAIN_CONTROL_DOMAIN_LENGTH, MAX_DOMAIN_CONTROL_DS_ALGORITHM, MAX_DOMAIN_CONTROL_DS_DIGEST_LENGTH, MAX_DOMAIN_CONTROL_DS_DIGEST_TYPE, MAX_DOMAIN_CONTROL_DS_KEY_TAG, MAX_DOMAIN_CONTROL_DS_PRESENTATION_LENGTH, MAX_DOMAIN_CONTROL_INPUT_RECORDS, MAX_DOMAIN_CONTROL_JSON_DEPTH, MAX_DOMAIN_CONTROL_JSON_VALUES, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES, MAX_DOMAIN_CONTROL_MX_PRIORITY, MAX_DOMAIN_CONTROL_MX_TEXT_LENGTH, MAX_DOMAIN_CONTROL_NAME_INPUT_LENGTH, MAX_DOMAIN_CONTROL_NOTE_LENGTH, MAX_DOMAIN_CONTROL_RECORD_INTEGER_TEXT_LENGTH, MAX_DOMAIN_CONTROL_TEXT_LENGTH, MAX_DOMAIN_CONTROL_TIMESTAMP_LENGTH, MIN_DOMAIN_CONTROL_DS_DIGEST_LENGTH, MIN_DOMAIN_CONTROL_MANIFEST_ENTRIES, MIN_DOMAIN_CONTROL_RECORDS, PUBLIC_DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS, PUBLIC_DOMAIN_CONTROL_MANIFEST_INPUT_VERSION, PUBLIC_DOMAIN_CONTROL_MANIFEST_LIMITATIONS, PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION, SUPPORTED_DOMAIN_CONTROL_MANIFEST_INPUT_VERSIONS, } from "../contracts/domain-control-manifest.mjs";
5
7
  const DOMAIN_CONTROL_PASSPORT_VERSION = DOMAIN_CONTROL_MANIFEST_INPUT_VERSION;
6
8
  export { DOMAIN_CONTROL_MANIFEST_VERSION, DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA, DOMAIN_CONTROL_PASSPORT_SCHEMA, DOMAIN_CONTROL_PASSPORT_VERSION, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, };
7
9
  export const MAX_DOMAIN_CONTROL_PASSPORT_BYTES = MAX_DOMAIN_CONTROL_MANIFEST_BYTES;
@@ -10,6 +12,8 @@ export const DOMAIN_CONTROL_PASSPORT_LIMITATIONS = DOMAIN_CONTROL_MANIFEST_LIMIT
10
12
  const INPUT_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_INPUT_KEYS);
11
13
  const MANIFEST_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_ROOT_KEYS);
12
14
  const ENTRY_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS);
15
+ const PUBLIC_ENTRY_KEYS = new Set(PUBLIC_DOMAIN_CONTROL_MANIFEST_ENTRY_KEYS);
16
+ const RECORD_MODE_KEYS = new Set(DOMAIN_CONTROL_RECORD_LIST_FIELDS);
13
17
  const INTEGRITY_KEYS = new Set(DOMAIN_CONTROL_MANIFEST_INTEGRITY_KEYS);
14
18
  const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
15
19
  const RECORD_LIST_FIELDS = DOMAIN_CONTROL_RECORD_LIST_FIELDS;
@@ -82,6 +86,7 @@ function boundedDataArray(value, label, minimum, maximum, maximumMessage) {
82
86
  return result;
83
87
  }
84
88
  const MX_RECORD_KEYS = new Set(DOMAIN_CONTROL_MX_RECORD_KEYS);
89
+ const RESOLVER_MX_RECORD_KEYS = new Set([...MX_RECORD_KEYS, 'type']);
85
90
  const CAA_RECORD_KEYS = new Set(DOMAIN_CONTROL_CAA_RECORD_KEYS);
86
91
  const DS_RECORD_KEYS = new Set(DOMAIN_CONTROL_DS_RECORD_KEYS);
87
92
  const DS_DIGEST_PATTERN = new RegExp(`^[a-f0-9]{${MIN_DOMAIN_CONTROL_DS_DIGEST_LENGTH},${MAX_DOMAIN_CONTROL_DS_DIGEST_LENGTH}}$`, 'u');
@@ -155,21 +160,48 @@ function canonicalIntegerAlias(source, keys, maximum, label, fallback = null) {
155
160
  }
156
161
  return normalised[0] ?? null;
157
162
  }
158
- export function canonicalMxRecord(value) {
159
- const item = structuredRecord(value, MX_RECORD_KEYS, 'Domain control MX record');
163
+ function mxRecordParts(value, resolverRecord = false) {
164
+ const item = structuredRecord(value, resolverRecord ? RESOLVER_MX_RECORD_KEYS : MX_RECORD_KEYS, 'Domain control MX record');
160
165
  if (item) {
161
- const host = canonicalStringAlias(item, ['exchange', 'host', 'value'], exchange, 'Domain control MX host');
166
+ if (resolverRecord && Object.hasOwn(item, 'type') && item.type !== 'MX')
167
+ return null;
168
+ const host = canonicalStringAlias(item, ['exchange', 'host', 'value'], (value) => value === '' ? '.' : exchange(value), 'Domain control MX host');
162
169
  const priority = canonicalIntegerAlias(item, ['priority', 'preference'], MAX_DOMAIN_CONTROL_MX_PRIORITY, 'Domain control MX priority');
163
- return host && priority !== null ? `${priority} ${host}` : '';
170
+ return host && priority !== null ? { priority, exchange: host } : null;
164
171
  }
165
172
  const candidate = recordText(value, MAX_DOMAIN_CONTROL_MX_TEXT_LENGTH).toLowerCase();
166
173
  const match = /^(\d{1,5})\s+(.+)$/u.exec(candidate);
167
174
  if (match) {
168
175
  const priority = recordInteger(match[1], MAX_DOMAIN_CONTROL_MX_PRIORITY);
169
176
  const host = exchange(match[2]);
170
- return priority !== null && host ? `${priority} ${host}` : '';
177
+ return priority !== null && host ? { priority, exchange: host } : null;
178
+ }
179
+ const host = exchange(candidate);
180
+ return host ? { priority: null, exchange: host } : null;
181
+ }
182
+ export function canonicalMxRecord(value) {
183
+ const parts = mxRecordParts(value);
184
+ return !parts ? '' : parts.priority === null ? parts.exchange : `${parts.priority} ${parts.exchange}`;
185
+ }
186
+ function completeMxRecord(value, resolverRecord) {
187
+ try {
188
+ const parts = mxRecordParts(value, resolverRecord);
189
+ return parts && parts.priority !== null
190
+ ? { priority: parts.priority, exchange: parts.exchange } : null;
191
+ }
192
+ catch (error) {
193
+ if (!(error instanceof TypeError))
194
+ throw error;
195
+ return null;
171
196
  }
172
- return exchange(candidate);
197
+ }
198
+ /** Complete canonical records require a preference and retain the root exchange as '.'. */
199
+ export function normalizeMxRecord(value) {
200
+ return completeMxRecord(value, false);
201
+ }
202
+ /** Accept the native resolver's optional record type without broadening import shapes. */
203
+ export function normalizeResolverMxRecord(value) {
204
+ return completeMxRecord(value, true);
173
205
  }
174
206
  function caaValue(value, tag) {
175
207
  const candidate = recordText(value, MAX_DOMAIN_CONTROL_CAA_VALUE_LENGTH)
@@ -265,6 +297,88 @@ export function canonicalDomainControlRecordList(value, kind) {
265
297
  .sort(compareCodeUnits)
266
298
  .slice(0, MAX_CANONICAL_DOMAIN_CONTROL_RECORDS);
267
299
  }
300
+ /** Complete field values for posture review; an invalid value never becomes an absent record. */
301
+ export function canonicalPostureRecords(checkId, values) {
302
+ if (values.length > MAX_POSTURE_CHECK_RECORDS)
303
+ return null;
304
+ const normalise = checkId === 'nameservers' ? domainControlName
305
+ : checkId === 'mx' ? canonicalMxRecord : checkId === 'caa' ? canonicalCaaRecord
306
+ : checkId === 'registration_lock'
307
+ ? (value) => typeof value === 'string' && value.length <= 120 && !/[\u0000-\u001f\u007f]/u.test(value)
308
+ ? value.trim().toLowerCase().replace(/[\s_-]+/gu, '') : ''
309
+ : null;
310
+ if (!normalise)
311
+ return null;
312
+ try {
313
+ const records = values.map(normalise);
314
+ if (records.some((value) => !value))
315
+ return null;
316
+ return [...new Set(records)].sort(compareCodeUnits);
317
+ }
318
+ catch {
319
+ return null;
320
+ }
321
+ }
322
+ /** Complete current record inputs are rejected, not shortened, when invalid or over capacity. */
323
+ export function canonicalDomainControlRecords(value, field) {
324
+ if (value === undefined)
325
+ return [];
326
+ const rows = boundedDataArray(value, `Domain control ${field}`, 0, MAX_DOMAIN_CONTROL_INPUT_RECORDS);
327
+ const normalise = field === 'nameservers' ? domainControlName : field === 'ds' ? canonicalDsRecord
328
+ : field === 'mx' ? canonicalMxRecord : canonicalCaaRecord;
329
+ const values = rows.map(normalise);
330
+ if (values.some((item) => !item))
331
+ throw new TypeError(`Domain control ${field} contains an invalid record.`);
332
+ const records = [...new Set(values)].sort(compareCodeUnits);
333
+ if (records.length > MAX_CURRENT_DOMAIN_CONTROL_RECORDS)
334
+ throw new TypeError(`Domain control ${field} exceeds ${MAX_CURRENT_DOMAIN_CONTROL_RECORDS} records.`);
335
+ return records;
336
+ }
337
+ export function domainControlRecordMode(entry, field) {
338
+ return entry.recordModes?.[field] ?? (entry[field].length ? 'expect_records' : 'unconfigured');
339
+ }
340
+ export function normalizeDomainControlRecordModes(value, records) {
341
+ const source = normalizeDeclaredDomainControlRecordModes(value);
342
+ return Object.freeze(Object.fromEntries(DOMAIN_CONTROL_RECORD_LIST_FIELDS.map((field) => {
343
+ const mode = Object.hasOwn(source, field) ? source[field] : (records[field].length ? 'expect_records' : 'unconfigured');
344
+ if ((mode === 'expect_records') !== (records[field].length > 0)) {
345
+ throw new TypeError(`Domain control ${field} requires records only in the specified-records mode.`);
346
+ }
347
+ return [field, mode];
348
+ })));
349
+ }
350
+ export function normalizeDeclaredDomainControlRecordModes(value) {
351
+ const source = value === undefined ? {} : boundedDataRecord(value, RECORD_MODE_KEYS, 'Domain control record modes');
352
+ if (!source)
353
+ throw new TypeError('Domain control record modes must be an ordinary object.');
354
+ const modes = {};
355
+ for (const field of DOMAIN_CONTROL_RECORD_LIST_FIELDS) {
356
+ if (!Object.hasOwn(source, field))
357
+ continue;
358
+ const mode = source[field];
359
+ if (!DOMAIN_CONTROL_RECORD_MODE_OPTIONS.some((option) => option.value === mode))
360
+ throw new TypeError(`Domain control ${field} has an unsupported record mode.`);
361
+ modes[field] = mode;
362
+ }
363
+ return Object.freeze(modes);
364
+ }
365
+ export function normalizeDomainControlRecordSettings(value) {
366
+ const records = {
367
+ nameservers: canonicalDomainControlRecords(value.nameservers, 'nameservers'),
368
+ ds: canonicalDomainControlRecords(value.ds, 'ds'),
369
+ mx: canonicalDomainControlRecords(value.mx, 'mx'),
370
+ caa: canonicalDomainControlRecords(value.caa, 'caa'),
371
+ };
372
+ return { ...records, recordModes: normalizeDomainControlRecordModes(value.recordModes, records) };
373
+ }
374
+ export function domainControlEvidenceAdmission(observedAt, now) {
375
+ const age = analystReviewAgeAt(observedAt, now);
376
+ if (age === 'unknown')
377
+ return 'A valid source observation time at or before the review is required.';
378
+ if (age === 'stale')
379
+ return `The source observation is more than ${ANALYST_REVIEW_STALE_AFTER_DAYS} days old; collect a current observation before comparing expected settings.`;
380
+ return null;
381
+ }
268
382
  class DomainControlJsonBudgetError extends Error {
269
383
  }
270
384
  class DomainControlJsonStructureError extends Error {
@@ -397,10 +511,10 @@ export function domainControlPassportSerialisedBytes(value, label = 'Domain cont
397
511
  export function assertDomainControlPassportByteBudget(value, label = 'Domain control manifest') {
398
512
  domainControlPassportSerialisedBytes(value, label);
399
513
  }
400
- function boundedDomainControlPassportEntries(value, label) {
514
+ function boundedDomainControlPassportEntries(value, label, legacy = false) {
401
515
  const entries = boundedDataArray(value, label, MIN_DOMAIN_CONTROL_MANIFEST_ENTRIES, MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES);
402
516
  return entries.map((entry) => {
403
- const source = boundedDataRecord(entry, ENTRY_KEYS, `${label} entry`);
517
+ const source = boundedDataRecord(entry, legacy ? PUBLIC_ENTRY_KEYS : ENTRY_KEYS, `${label} entry`);
404
518
  if (!source)
405
519
  return entry;
406
520
  for (const field of RECORD_LIST_FIELDS) {
@@ -408,6 +522,11 @@ function boundedDomainControlPassportEntries(value, label) {
408
522
  continue;
409
523
  source[field] = boundedDataArray(source[field], `${label} entry ${field}`, MIN_DOMAIN_CONTROL_RECORDS, MAX_DOMAIN_CONTROL_INPUT_RECORDS, `${label} entry ${field} exceeds ${MAX_DOMAIN_CONTROL_INPUT_RECORDS} input records.`);
410
524
  }
525
+ if (Object.hasOwn(source, 'recordModes')) {
526
+ source.recordModes = boundedDataRecord(source.recordModes, RECORD_MODE_KEYS, `${label} entry record modes`);
527
+ if (!source.recordModes)
528
+ throw new TypeError(`${label} entry record modes must be an ordinary object.`);
529
+ }
411
530
  return source;
412
531
  });
413
532
  }
@@ -442,7 +561,7 @@ function digest(value) {
442
561
  const candidate = value.trim().toLowerCase();
443
562
  return SPKI_DIGEST_PATTERN.test(candidate) ? candidate : null;
444
563
  }
445
- function normalizeEntry(value) {
564
+ function normalizeEntry(value, legacyRecords, includeModes) {
446
565
  if (!value || typeof value !== 'object' || Array.isArray(value))
447
566
  return null;
448
567
  const source = value;
@@ -455,38 +574,44 @@ function normalizeEntry(value) {
455
574
  && !tlsSpkiSha256) {
456
575
  throw new TypeError('Domain control TLS public-key fingerprint must be exactly 64 hexadecimal characters.');
457
576
  }
577
+ const records = {
578
+ nameservers: Object.freeze(legacyRecords ? hostnames(source.nameservers) : canonicalDomainControlRecords(source.nameservers, 'nameservers')),
579
+ ds: Object.freeze(legacyRecords ? canonicalDomainControlRecordList(source.ds, 'ds') : canonicalDomainControlRecords(source.ds, 'ds')),
580
+ mx: Object.freeze(legacyRecords ? canonicalDomainControlRecordList(source.mx, 'mx') : canonicalDomainControlRecords(source.mx, 'mx')),
581
+ caa: Object.freeze(legacyRecords ? canonicalDomainControlRecordList(source.caa, 'caa') : canonicalDomainControlRecords(source.caa, 'caa')),
582
+ };
458
583
  return Object.freeze({
459
584
  domain,
460
- nameservers: Object.freeze(hostnames(source.nameservers)),
461
- ds: Object.freeze(canonicalDomainControlRecordList(source.ds, 'ds')),
462
- mx: Object.freeze(canonicalDomainControlRecordList(source.mx, 'mx')),
463
- caa: Object.freeze(canonicalDomainControlRecordList(source.caa, 'caa')),
585
+ ...records,
586
+ ...(includeModes ? { recordModes: normalizeDomainControlRecordModes(source.recordModes, records) } : {}),
464
587
  tlsIssuer: text(source.tlsIssuer, MAX_DOMAIN_CONTROL_TEXT_LENGTH)?.toLowerCase() ?? null,
465
588
  tlsSpkiSha256,
466
589
  registrarLock: source.registrarLock === 'required' || source.registrarLock === 'not_required'
467
590
  ? source.registrarLock
468
591
  : null,
469
- renewalReviewAt: timestamp(source.renewalReviewAt, true),
592
+ renewalReviewAt: timestamp(source.renewalReviewAt, legacyRecords),
470
593
  note: text(source.note, MAX_DOMAIN_CONTROL_NOTE_LENGTH),
471
594
  });
472
595
  }
473
596
  function buildUnsignedDomainControlPassportInternal(input, generatedAtValue, options = {}) {
474
597
  const source = boundedDataRecord(input, INPUT_KEYS, 'Domain control manifest input');
475
- if (!source || source.schema !== DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA || source.version !== DOMAIN_CONTROL_MANIFEST_INPUT_VERSION) {
476
- throw new TypeError(`Domain control manifest input must use ${DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA} version ${DOMAIN_CONTROL_MANIFEST_INPUT_VERSION}.`);
598
+ if (!source || source.schema !== DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA || !SUPPORTED_DOMAIN_CONTROL_MANIFEST_INPUT_VERSIONS.some((version) => version === source.version)) {
599
+ throw new TypeError(`Domain control manifest input must use ${DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA} version ${SUPPORTED_DOMAIN_CONTROL_MANIFEST_INPUT_VERSIONS.join(' or ')}.`);
477
600
  }
478
- const inputEntries = boundedDomainControlPassportEntries(source.entries, 'Domain control manifest input');
601
+ const legacy = source.version === PUBLIC_DOMAIN_CONTROL_MANIFEST_INPUT_VERSION;
602
+ const version = options.manifestVersion ?? DOMAIN_CONTROL_MANIFEST_VERSION;
603
+ const inputEntries = boundedDomainControlPassportEntries(source.entries, 'Domain control manifest input', legacy);
479
604
  source.entries = inputEntries;
480
605
  assertDomainControlPassportByteBudget(source, 'Domain control manifest input');
481
606
  const generatedAt = timestamp(generatedAtValue, options.legacyGeneratedAt === true);
482
- const expiresAt = timestamp(source.expiresAt, true);
607
+ const expiresAt = timestamp(source.expiresAt, legacy);
483
608
  if (!generatedAt) {
484
609
  throw new TypeError('Domain control manifest generation time must be valid and include an explicit timezone.');
485
610
  }
486
611
  if (!expiresAt || Date.parse(expiresAt) <= Date.parse(generatedAt)) {
487
612
  throw new TypeError('Domain control manifest expiry must be a valid time after generation.');
488
613
  }
489
- const entries = inputEntries.map(normalizeEntry);
614
+ const entries = inputEntries.map((entry) => normalizeEntry(entry, legacy, version === DOMAIN_CONTROL_MANIFEST_VERSION));
490
615
  if (entries.some((entry) => entry === null))
491
616
  throw new TypeError('Domain control manifest contains an invalid entry.');
492
617
  const normalizedEntries = entries;
@@ -498,11 +623,11 @@ function buildUnsignedDomainControlPassportInternal(input, generatedAtValue, opt
498
623
  : [...normalizedEntries].sort((left, right) => compareCodeUnits(left.domain, right.domain));
499
624
  return Object.freeze({
500
625
  schema: DOMAIN_CONTROL_PASSPORT_SCHEMA,
501
- version: DOMAIN_CONTROL_MANIFEST_VERSION,
626
+ version,
502
627
  generatedAt,
503
628
  expiresAt,
504
629
  entries: Object.freeze(orderedEntries),
505
- limitations: DOMAIN_CONTROL_PASSPORT_LIMITATIONS,
630
+ limitations: version === PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION ? PUBLIC_DOMAIN_CONTROL_MANIFEST_LIMITATIONS : DOMAIN_CONTROL_PASSPORT_LIMITATIONS,
506
631
  });
507
632
  }
508
633
  export function buildUnsignedDomainControlPassport(input, generatedAtValue, options = {}) {
@@ -519,29 +644,32 @@ function normalizeDomainControlPassportDocumentInternal(value) {
519
644
  || !MANIFEST_DIGEST_PATTERN.test(integrity.digestSha256)) {
520
645
  throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
521
646
  }
647
+ const canonicalization = resolveArtifactCanonicalization(source.version, integrity.canonicalization, DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, 'Domain control manifest');
648
+ const legacy = source.version === PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION;
649
+ const expectedLimitations = legacy ? PUBLIC_DOMAIN_CONTROL_MANIFEST_LIMITATIONS : DOMAIN_CONTROL_PASSPORT_LIMITATIONS;
522
650
  let limitations;
523
651
  try {
524
- limitations = boundedDataArray(source.limitations, 'Domain control manifest limitations', DOMAIN_CONTROL_PASSPORT_LIMITATIONS.length, DOMAIN_CONTROL_PASSPORT_LIMITATIONS.length);
652
+ limitations = boundedDataArray(source.limitations, 'Domain control manifest limitations', expectedLimitations.length, expectedLimitations.length);
525
653
  }
526
654
  catch {
527
655
  throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
528
656
  }
529
- if (limitations.some((item, index) => item !== DOMAIN_CONTROL_PASSPORT_LIMITATIONS[index])) {
657
+ if (limitations.some((item, index) => item !== expectedLimitations[index])) {
530
658
  throw new TypeError('Domain control manifest has an unsupported or malformed structure.');
531
659
  }
532
660
  source.limitations = limitations;
533
- const manifestEntries = boundedDomainControlPassportEntries(source.entries, 'Domain control manifest');
661
+ const manifestEntries = boundedDomainControlPassportEntries(source.entries, 'Domain control manifest', legacy);
534
662
  source.entries = manifestEntries;
535
663
  source.integrity = integrity;
536
664
  assertDomainControlPassportByteBudget(source);
537
- const canonicalization = resolveArtifactCanonicalization(source.version, integrity.canonicalization, DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, 'Domain control manifest');
538
665
  const baseUnsigned = buildUnsignedDomainControlPassportInternal({
539
666
  schema: DOMAIN_CONTROL_PASSPORT_INPUT_SCHEMA,
540
- version: DOMAIN_CONTROL_MANIFEST_INPUT_VERSION,
667
+ version: legacy ? PUBLIC_DOMAIN_CONTROL_MANIFEST_INPUT_VERSION : DOMAIN_CONTROL_MANIFEST_INPUT_VERSION,
541
668
  expiresAt: source.expiresAt,
542
669
  entries: manifestEntries,
543
670
  }, source.generatedAt, {
544
671
  preserveEntryOrder: true,
672
+ manifestVersion: legacy ? PUBLIC_DOMAIN_CONTROL_MANIFEST_VERSION : DOMAIN_CONTROL_MANIFEST_VERSION,
545
673
  });
546
674
  const unsigned = baseUnsigned;
547
675
  const { integrity: _integrity, ...suppliedUnsigned } = source;