@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/NOTICE +8 -2
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/argument-values.mjs +55 -0
  7. package/cli/arguments.mjs +24 -408
  8. package/cli/artifact-validation/case-response.mjs +65 -44
  9. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  10. package/cli/artifact-validation/lookup-evidence.mjs +15 -9
  11. package/cli/artifact-validation/signed-review.mjs +136 -68
  12. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  13. package/cli/artifact-verify.mjs +92 -24
  14. package/cli/assurance-arguments.mjs +56 -0
  15. package/cli/assurance-command-definitions.mjs +179 -0
  16. package/cli/assurance-command-runner.mjs +40 -5
  17. package/cli/bagit-folder.mjs +57 -0
  18. package/cli/bagit-review.mjs +23 -0
  19. package/cli/bounded-json.mjs +1 -1
  20. package/cli/bulk-checkpoint.mjs +1 -1
  21. package/cli/bulk-command-runner.mjs +4 -2
  22. package/cli/bulk-output.mjs +29 -3
  23. package/cli/bulk.mjs +2 -3
  24. package/cli/case-command.mjs +212 -0
  25. package/cli/case-pack.mjs +50 -32
  26. package/cli/ci-report.mjs +6 -3
  27. package/cli/collection-arguments.mjs +123 -0
  28. package/cli/collection-command-definitions.mjs +330 -0
  29. package/cli/collection-preflight.mjs +8 -1
  30. package/cli/command-argument-grammar.mjs +8 -206
  31. package/cli/command-definition.mjs +329 -0
  32. package/cli/command-reference.mjs +94 -1357
  33. package/cli/command-seeds.mjs +18 -0
  34. package/cli/compare.mjs +1 -1
  35. package/cli/completion.mjs +298 -79
  36. package/cli/config-profile.mjs +8 -6
  37. package/cli/ct-event-intake.mjs +16 -16
  38. package/cli/discover.mjs +5 -4
  39. package/cli/discovery-observation-snapshot.mjs +46 -18
  40. package/cli/discovery-scan-command-runner.mjs +5 -3
  41. package/cli/discovery-scan.mjs +7 -4
  42. package/cli/discovery-snapshot.mjs +2 -2
  43. package/cli/discovery-workflow.mjs +5 -5
  44. package/cli/domain-control-monitor.mjs +50 -20
  45. package/cli/domain-control-observations.mjs +151 -103
  46. package/cli/errors.mjs +37 -6
  47. package/cli/evidence-arguments.mjs +46 -0
  48. package/cli/evidence-command-definitions.mjs +69 -0
  49. package/cli/evidence-command-runner.mjs +18 -8
  50. package/cli/evidence-signing.mjs +5 -4
  51. package/cli/external-observation-mapping.mjs +9 -10
  52. package/cli/formatters/terminal-command-formats.mjs +6 -0
  53. package/cli/formatters/terminal-lookup.mjs +24 -46
  54. package/cli/formatters/terminal-shared.mjs +5 -3
  55. package/cli/formatters/terminal-technology.mjs +114 -0
  56. package/cli/history-arguments.mjs +39 -0
  57. package/cli/history-command-definitions.mjs +88 -0
  58. package/cli/indicator-set-command.mjs +33 -0
  59. package/cli/inline-command-families.mjs +6 -46
  60. package/cli/inline-command-runner.mjs +4 -31
  61. package/cli/input.mjs +2 -2
  62. package/cli/intake-command.mjs +107 -0
  63. package/cli/interactive-launcher.mjs +2 -160
  64. package/cli/interchange-report.mjs +8 -10
  65. package/cli/investigation-artifacts.mjs +176 -0
  66. package/cli/investigation-checkpoint.mjs +10 -0
  67. package/cli/investigation-folder.mjs +114 -0
  68. package/cli/investigation-manifest.mjs +1 -112
  69. package/cli/investigation-package-review.mjs +103 -0
  70. package/cli/investigation-plan.mjs +38 -147
  71. package/cli/investigation-recipes.mjs +177 -0
  72. package/cli/investigation-run.mjs +256 -35
  73. package/cli/local-document-checkpoint.mjs +170 -0
  74. package/cli/local-mmdb-review.mjs +197 -17
  75. package/cli/lookalike-calibration.mjs +1 -1
  76. package/cli/lookup-brief.mjs +2 -2
  77. package/cli/lookup-browser.mjs +5 -12
  78. package/cli/lookup-command-runner.mjs +33 -39
  79. package/cli/lookup-diff.mjs +47 -12
  80. package/cli/lookup-plan.mjs +16 -14
  81. package/cli/lookup-reconcile.mjs +15 -12
  82. package/cli/lookup-timeline.mjs +7 -6
  83. package/cli/mail-header-review.mjs +24 -247
  84. package/cli/mail-review.mjs +75 -21
  85. package/cli/manual.mjs +1 -0
  86. package/cli/network-arguments.mjs +51 -0
  87. package/cli/network-command-definitions.mjs +130 -0
  88. package/cli/network-command-runner.mjs +45 -31
  89. package/cli/offline-evidence-review.mjs +39 -6
  90. package/cli/output-file.mjs +31 -5
  91. package/cli/posture.mjs +1 -1
  92. package/cli/registry-doctor.mjs +3 -1
  93. package/cli/registry-fixture-scaffold.mjs +1 -1
  94. package/cli/registry-support.mjs +1 -1
  95. package/cli/retained-artifact-diff.mjs +10 -9
  96. package/cli/review-arguments.mjs +131 -0
  97. package/cli/review-command-definitions.mjs +273 -0
  98. package/cli/review-command-runner.mjs +104 -149
  99. package/cli/risk-calibration.mjs +8 -8
  100. package/cli/runner.mjs +74 -37
  101. package/cli/saved-lookup.mjs +2 -2
  102. package/cli/selected-input-worker.mjs +63 -0
  103. package/cli/sharing-review.mjs +8 -80
  104. package/cli/signer-trust.mjs +104 -0
  105. package/cli/source-reliability.mjs +75 -84
  106. package/cli/support-arguments.mjs +49 -0
  107. package/cli/support-command-definitions.mjs +209 -0
  108. package/cli/support-command-runner.mjs +2 -2
  109. package/cli/terminal-input.mjs +168 -0
  110. package/cli/workflow-arguments.mjs +86 -0
  111. package/cli/workflow-command-definitions.mjs +78 -0
  112. package/cli/workflow-command-runner.mjs +75 -13
  113. package/docs/cli-reference.md +103 -30
  114. package/docs/cli.md +510 -25
  115. package/docs/contextual-reviews.md +155 -0
  116. package/lib/abort.mjs +4 -1
  117. package/lib/analyst-taxonomy.mjs +2 -40
  118. package/lib/application-version.mjs +1 -1
  119. package/lib/availability.mjs +27 -754
  120. package/lib/bounded-contract-normalizers.mjs +4 -0
  121. package/lib/bounded-file.mjs +3 -1
  122. package/lib/bounded-html-document.mjs +125 -0
  123. package/lib/bounded-json.mjs +2 -290
  124. package/lib/browser-library-profile.mjs +99 -130
  125. package/lib/browser-library-worker.mjs +112 -0
  126. package/lib/candidate-provenance-bounds.mjs +2 -0
  127. package/lib/classify.mjs +1 -1
  128. package/lib/client-behavior-profile.mjs +3 -3
  129. package/lib/ct-query.mjs +2 -27
  130. package/lib/ct-response-bounds.mjs +2 -9
  131. package/lib/ct-search.mjs +89 -46
  132. package/lib/dmarc-discovery.mjs +62 -0
  133. package/lib/dns-convergence-review.mjs +21 -8
  134. package/lib/dns-delegation-health.mjs +36 -22
  135. package/lib/dns-inheritance-review.mjs +207 -0
  136. package/lib/dns-intelligence.mjs +44 -29
  137. package/lib/dns-mx.mjs +1 -1
  138. package/lib/dns-query-coverage.mjs +40 -0
  139. package/lib/dnssec-evidence-validation.mjs +11 -3
  140. package/lib/domain-assurance.mjs +1 -1
  141. package/lib/domain-change-packet.mjs +18 -14
  142. package/lib/domain-change-review.mjs +40 -16
  143. package/lib/domain-control-flight-recorder.mjs +301 -124
  144. package/lib/domain-control-manifest.mjs +126 -58
  145. package/lib/domain-posture-analysis.mjs +41 -11
  146. package/lib/domain-posture-parsers.mjs +88 -6
  147. package/lib/domain-posture.mjs +120 -32
  148. package/lib/domain-registration.mjs +392 -0
  149. package/lib/evidence-export-privacy.mjs +568 -0
  150. package/lib/evidence-export.mjs +12 -593
  151. package/lib/evidence-report.mjs +7 -2
  152. package/lib/external-intelligence-risk.mjs +2 -124
  153. package/lib/favicon.mjs +117 -41
  154. package/lib/generated/cisa-kev-catalog.mjs +50 -4
  155. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  156. package/lib/generated/retire-browser-catalog.mjs +2742 -1034
  157. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  158. package/lib/html-signals.mjs +87 -85
  159. package/lib/http-evidence-bounds.mjs +2 -7
  160. package/lib/http-intelligence.mjs +52 -27
  161. package/lib/idn-confusable-policy.mjs +6 -4
  162. package/lib/idn-confusables.mjs +1 -1
  163. package/lib/interchange-fidelity-registry.mjs +10 -0
  164. package/lib/lookup-cache.mjs +34 -11
  165. package/lib/lookup-child-profile-contract.mjs +24 -15
  166. package/lib/lookup-contract-primitives.mjs +1 -1
  167. package/lib/lookup-diagnostics.mjs +2 -1
  168. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  169. package/lib/lookup-progress.mjs +79 -13
  170. package/lib/lookup-response-contract.mjs +1077 -0
  171. package/lib/lookup-response.mjs +11 -24
  172. package/lib/lookup-source-progress.mjs +24 -35
  173. package/lib/lookup.mjs +49 -28
  174. package/lib/observed-network-context.mjs +43 -27
  175. package/lib/opportunity-scoring.mjs +2 -107
  176. package/lib/outbound-identity.mjs +1 -1
  177. package/lib/outbound-request-bounds.mjs +4 -1
  178. package/lib/page-fingerprints.mjs +87 -134
  179. package/lib/page-role-profile.mjs +12 -8
  180. package/lib/perceptual-hash-comparison.mjs +2 -34
  181. package/lib/perceptual-hash.mjs +79 -24
  182. package/lib/portable-generator.mjs +2 -29
  183. package/lib/project-metadata.mjs +2 -8
  184. package/lib/rdap-bootstrap.mjs +72 -22
  185. package/lib/rdap-client.mjs +26 -14
  186. package/lib/rdap-normalization.mjs +33 -18
  187. package/lib/rdap-search-workbench.mjs +1 -1
  188. package/lib/rdap-transport.mjs +47 -20
  189. package/lib/rdap-types.mjs +2 -1
  190. package/lib/rdap.mjs +18 -3
  191. package/lib/registrable-domain.mjs +2 -36
  192. package/lib/registrar-standing-contract.mjs +2 -1
  193. package/lib/registrar-standing.mjs +9 -13
  194. package/lib/registry-capabilities.mjs +7 -3
  195. package/lib/registry-capability-catalogue.mjs +29 -20
  196. package/lib/registry-dates.mjs +2 -190
  197. package/lib/registry-insights.mjs +56 -18
  198. package/lib/response-policy.mjs +86 -48
  199. package/lib/risk-calibration-summary.mjs +2 -338
  200. package/lib/risk-scoring.mjs +2 -345
  201. package/lib/rpki-evidence.mjs +8 -5
  202. package/lib/safe-fetch.mjs +54 -47
  203. package/lib/scoring-evidence-quality.mjs +2 -107
  204. package/lib/security-txt.mjs +10 -7
  205. package/lib/semantic-version.mjs +2 -54
  206. package/lib/service-binding-dns.mjs +35 -0
  207. package/lib/smtp-transport-review.mjs +75 -34
  208. package/lib/sslbl-certificates.generated.mjs +141 -138
  209. package/lib/static-html-analysis.mjs +117 -146
  210. package/lib/structured-data-identity.mjs +2 -1
  211. package/lib/technology-markup.mjs +100 -0
  212. package/lib/threat-intelligence-manifests.mjs +11 -139
  213. package/lib/threat-intelligence-runtime.mjs +24 -52
  214. package/lib/threat-intelligence-targets.mjs +6 -34
  215. package/lib/threat-intelligence-types.mjs +2 -36
  216. package/lib/typosquat-generator.mjs +29 -16
  217. package/lib/urlhaus-intelligence.mjs +8 -3
  218. package/lib/urlscan-intelligence.mjs +1 -1
  219. package/lib/website-enrichment.mjs +388 -0
  220. package/lib/website-security-posture.mjs +30 -6
  221. package/lib/website-technology.mjs +111 -71
  222. package/lib/whois-authority.mjs +55 -5
  223. package/lib/whois-chain.mjs +9 -2
  224. package/lib/whois-common-formats.mjs +5 -5
  225. package/lib/whois-contracts.mjs +2 -1
  226. package/lib/whois-dialects.mjs +2 -2
  227. package/lib/whois-normalization.mjs +3 -1
  228. package/lib/whois-parser-support.mjs +8 -1
  229. package/lib/whois-parser.mjs +5 -3
  230. package/lib/whois-transport.mjs +23 -7
  231. package/package.json +14 -14
  232. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  233. package/packages/analysis/bounded-json.mjs +330 -0
  234. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  235. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  236. package/packages/analysis/ct-query.mjs +27 -0
  237. package/packages/analysis/ct-response-bounds.mjs +9 -0
  238. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  239. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  240. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  241. package/packages/analysis/opportunity-scoring.mjs +107 -0
  242. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  243. package/packages/analysis/portable-generator.mjs +29 -0
  244. package/packages/analysis/project-metadata.mjs +8 -0
  245. package/packages/analysis/registrable-domain.mjs +56 -0
  246. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  247. package/packages/analysis/risk-scoring.mjs +345 -0
  248. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  249. package/packages/analysis/semantic-version.mjs +54 -0
  250. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  251. package/packages/cases/case-attachment-model.mjs +83 -0
  252. package/packages/cases/case-evidence-links.mjs +135 -0
  253. package/packages/cases/case-evidence-model.mjs +229 -69
  254. package/packages/cases/case-evidence-quality.mjs +5 -0
  255. package/packages/cases/case-export-input.mjs +55 -0
  256. package/packages/cases/case-incident-context.mjs +52 -0
  257. package/packages/cases/case-markdown.mjs +11 -0
  258. package/packages/cases/case-migration-model.mjs +98 -48
  259. package/packages/cases/case-model.mjs +1 -0
  260. package/packages/cases/case-recheck-model.mjs +149 -0
  261. package/packages/cases/case-record-contracts.mjs +1 -1
  262. package/packages/cases/case-record-core.mjs +4 -3
  263. package/packages/cases/case-record-operations.mjs +123 -133
  264. package/packages/cases/case-record-projection.mjs +30 -20
  265. package/packages/cases/case-record-version-input.mjs +19 -0
  266. package/packages/cases/case-report.mjs +114 -67
  267. package/packages/cases/case-requested-evidence.mjs +146 -0
  268. package/packages/cases/case-response-actions.mjs +756 -0
  269. package/packages/cases/case-response-model.mjs +49 -1129
  270. package/packages/cases/case-response-outcomes.mjs +338 -0
  271. package/packages/cases/case-response-packet-render.mjs +157 -0
  272. package/packages/cases/case-response-packet-types.mjs +1 -0
  273. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  274. package/packages/cases/case-response-packet.mjs +58 -593
  275. package/packages/cases/case-response-records.mjs +93 -0
  276. package/packages/cases/case-response-review-inputs.mjs +382 -0
  277. package/packages/cases/case-response-values.mjs +85 -0
  278. package/packages/cases/case-review-package.mjs +45 -0
  279. package/packages/cases/case-selection.mjs +46 -0
  280. package/packages/cases/case-storage-model.mjs +26 -1
  281. package/packages/cases/case-workflow-metadata.mjs +225 -0
  282. package/packages/cases/case-workflow-migration.mjs +43 -0
  283. package/packages/cases/http-summary.mjs +6 -1
  284. package/packages/cases/response-route-freshness.mjs +16 -0
  285. package/packages/comparison/capture-context.mjs +61 -0
  286. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  287. package/packages/comparison/favicon-similarity.mjs +1 -1
  288. package/packages/comparison/page-similarity.mjs +3 -2
  289. package/packages/comparison/relationship-evidence.mjs +103 -14
  290. package/packages/comparison/relationship-provenance.mjs +99 -0
  291. package/packages/contracts/analyst-interchange.mjs +234 -0
  292. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  293. package/packages/contracts/analyst-review-state.mjs +5 -4
  294. package/packages/contracts/case-portability.mjs +370 -114
  295. package/packages/contracts/case-views-contract.mjs +12 -0
  296. package/packages/contracts/case-views.mjs +26 -0
  297. package/packages/contracts/cli-command-semantics.mjs +3 -0
  298. package/packages/contracts/cli-grammar.mjs +6 -0
  299. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  300. package/packages/contracts/cli-lookup.mjs +0 -1
  301. package/packages/contracts/context-review.mjs +24 -0
  302. package/packages/contracts/document-intake.mjs +11 -0
  303. package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
  304. package/packages/contracts/domain-control-manifest.mjs +128 -74
  305. package/packages/contracts/domain-control-monitor.mjs +59 -45
  306. package/packages/contracts/domain-control-review.mjs +200 -121
  307. package/packages/contracts/domain-name.mjs +21 -0
  308. package/packages/contracts/external-observation-interchange.mjs +2 -1
  309. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  310. package/packages/contracts/har-review.mjs +2 -0
  311. package/packages/contracts/http-url.mjs +2 -0
  312. package/packages/contracts/identity-events.mjs +4 -0
  313. package/packages/contracts/investigation-package-limits.mjs +24 -0
  314. package/packages/contracts/investigation-portability.mjs +56 -27
  315. package/packages/contracts/investigation-run.mjs +18 -0
  316. package/packages/contracts/ip-address.mjs +44 -0
  317. package/packages/contracts/lookup-evidence.mjs +35 -0
  318. package/packages/contracts/mail-authentication.mjs +5 -0
  319. package/packages/contracts/message-intake.mjs +31 -0
  320. package/packages/contracts/offline-comparison.mjs +4 -2
  321. package/packages/contracts/page-fingerprints.mjs +15 -0
  322. package/packages/contracts/passive-mail.mjs +10 -0
  323. package/packages/contracts/risk-calibration.mjs +0 -1
  324. package/packages/contracts/schema-compatibility.mjs +1 -1
  325. package/packages/contracts/schema-lifecycle.mjs +83 -153
  326. package/packages/contracts/selected-file-limits.mjs +6 -0
  327. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  328. package/packages/contracts/web-capture.mjs +10 -0
  329. package/packages/contracts/workspace-portability.mjs +219 -238
  330. package/packages/evidence/artifact-integrity.mjs +8 -2
  331. package/packages/evidence/artifact-structure.mjs +105 -0
  332. package/packages/evidence/collection-quality.mjs +67 -0
  333. package/packages/evidence/decision-fact.mjs +4 -1
  334. package/packages/evidence/dns-query-outcome.mjs +72 -0
  335. package/packages/evidence/domain-control-runtime.mjs +155 -27
  336. package/packages/evidence/domain-posture-context.mjs +79 -0
  337. package/packages/evidence/email-recipient.mjs +18 -0
  338. package/packages/evidence/image-regions.mjs +56 -0
  339. package/packages/evidence/latest-observations.mjs +28 -0
  340. package/packages/evidence/lookup-target.mjs +108 -0
  341. package/packages/evidence/observation.mjs +18 -1
  342. package/packages/evidence/passphrase-encryption.mjs +44 -0
  343. package/packages/evidence/png-pixels.mjs +117 -0
  344. package/packages/evidence/public-address-policy.mjs +83 -0
  345. package/packages/evidence/record-identity.mjs +72 -0
  346. package/packages/evidence/registry-dates.mjs +190 -0
  347. package/packages/evidence/retained-file.mjs +69 -0
  348. package/packages/evidence/review-age.mjs +14 -0
  349. package/packages/interchange/bagit.mjs +342 -0
  350. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  351. package/packages/interchange/crc32.mjs +17 -0
  352. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  353. package/packages/interchange/external-findings-import.mjs +93 -61
  354. package/packages/interchange/managed-indicator-export.mjs +35 -0
  355. package/packages/interchange/managed-indicator-set.mjs +210 -0
  356. package/packages/interchange/misp-indicator-export.mjs +93 -0
  357. package/packages/interchange/sharing-policy.mjs +56 -0
  358. package/packages/interchange/stix-indicator-export.mjs +129 -0
  359. package/packages/interchange/web-capture-import.mjs +389 -0
  360. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  361. package/packages/investigation/bulk-mail-exposure.mjs +9 -14
  362. package/packages/investigation/capture-coverage.mjs +57 -0
  363. package/packages/investigation/connector-provenance-review.mjs +100 -0
  364. package/packages/investigation/context-review.mjs +35 -0
  365. package/packages/investigation/document-intake.mjs +81 -0
  366. package/packages/investigation/docx-intake.mjs +133 -0
  367. package/packages/investigation/domain-history-review.mjs +100 -0
  368. package/packages/investigation/domain-transition-review.mjs +47 -0
  369. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  370. package/packages/investigation/har-intake.mjs +68 -0
  371. package/packages/investigation/identity-event-intake.mjs +131 -0
  372. package/packages/investigation/identity-incident-review.mjs +39 -0
  373. package/packages/investigation/incident-sequence-review.mjs +58 -0
  374. package/packages/investigation/intake-report.mjs +19 -0
  375. package/packages/investigation/investigation-bagit.mjs +14 -0
  376. package/packages/investigation/investigation-capsule.mjs +33 -5
  377. package/packages/investigation/investigation-manifest.mjs +200 -0
  378. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  379. package/packages/investigation/investigation-package.mjs +200 -0
  380. package/packages/investigation/link-intake.mjs +179 -0
  381. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  382. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  383. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  384. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  385. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  386. package/packages/investigation/mail-authentication-review.mjs +222 -0
  387. package/packages/investigation/mail-header-identity.mjs +209 -0
  388. package/packages/investigation/message-intake.mjs +195 -0
  389. package/packages/investigation/page-behaviour.mjs +113 -0
  390. package/packages/investigation/pdf-intake.mjs +155 -0
  391. package/packages/investigation/platform-continuity-review.mjs +60 -0
  392. package/packages/investigation/qr-intake.mjs +56 -0
  393. package/packages/investigation/requested-action-hints.mjs +11 -0
  394. package/packages/investigation/selected-input-review.mjs +17 -0
  395. package/packages/investigation/storefront-review.mjs +70 -0
  396. package/packages/monitoring/analyst-review-state.mjs +38 -90
  397. package/packages/workspace/brand-profile-model.mjs +160 -22
  398. package/packages/workspace/bulk-columns.mjs +17 -0
  399. package/packages/workspace/bulk-review-model.mjs +39 -31
  400. package/packages/workspace/bulk-session-model.mjs +112 -27
  401. package/packages/workspace/bulk-sort.mjs +1 -0
  402. package/packages/workspace/campaign-model.mjs +1 -1
  403. package/packages/workspace/case-views.mjs +102 -0
  404. package/packages/workspace/detection-rule-model.mjs +1 -1
  405. package/packages/workspace/hostile-input.mjs +29 -8
  406. package/packages/workspace/investigation-guide.mjs +2 -2
  407. package/packages/workspace/investigation-template-model.mjs +45 -6
  408. package/packages/workspace/page-baseline.mjs +20 -17
  409. package/packages/workspace/relationship-observation-model.mjs +54 -35
  410. package/packages/workspace/shortlist-model.mjs +14 -5
  411. package/packages/workspace/watchlist-history.mjs +30 -14
  412. package/packages/workspace/watchlist-store.mjs +24 -6
  413. package/packages/workspace/website-snapshot-model.mjs +98 -33
  414. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  415. package/packages/workspace/workspace-archive.mjs +90 -25
  416. package/third-party-notices.txt +577 -17
  417. package/lib/hostname.mjs +0 -38
  418. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,759 +1,32 @@
1
- // Domain availability check.
2
- //
3
- // Determines registered vs. available vs. expiring via RDAP/WHOIS, and flags
4
- // likely for-sale listings by checking for known parking/marketplace
5
- // nameservers and "for sale" listing language on the domain's own homepage.
6
- // This does not attempt to price the domain - registries don't publish sale
7
- // prices, and that's proprietary marketplace data.
8
- //
9
- // Shared by the Express server and the Netlify Functions.
10
- import { promises as dns } from 'node:dns';
11
- import { fetchRdapRecord } from "./rdap.mjs";
12
- import { buildWhoisChain, parseWhoisChain } from "./whois.mjs";
13
- import { safeFetchDetailed, readTextCapped } from "./safe-fetch.mjs";
14
- import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
15
- import { collectDnsIntelligence, skippedDnsIntelligence } from "./dns-intelligence.mjs";
16
- import { fetchFaviconHash } from "./favicon.mjs";
17
- import { extractHtmlSignals } from "./html-signals.mjs";
1
+ // Composition boundary shared by the server and serverless handlers.
2
+ // Registration authority and optional website enrichment retain separate owners.
18
3
  import { featureDecision, networkFeaturePolicy } from "./feature-policy.mjs";
19
- import { buildHttpObservation, failedHttpObservation, skippedHttpObservation } from "./http-intelligence.mjs";
20
- import { collectTlsIntelligence, skippedTlsObservation } from "./tls-intelligence.mjs";
21
- import { parseRegistryDate, registryDateIso } from "./registry-dates.mjs";
22
- import { analyzeWebsiteSecurityPosture } from "./website-security-posture.mjs";
23
- import { analyzeWebsiteTechnology, captureTechnologyResponseHeaders, } from "./website-technology.mjs";
24
- import { analyzeResponsePolicyHeaders, qualifyResponsePolicyWithCspMeta, } from "./response-policy.mjs";
25
- import { nonEmptyErrorMessage } from "./error-detail.mjs";
26
- import { registryServiceAdmissionFor } from "./registry-capabilities.mjs";
27
- import { HOMEPAGE_FETCH_TIMEOUT_MS, MAX_HOMEPAGE_BYTES, } from "./outbound-request-bounds.mjs";
28
- const DNS_DELEGATION_TIMEOUT_MS = 4000;
29
- const MAX_DELEGATION_NAMESERVERS = 50;
30
- const MISSING_DNS_CODES = new Set(['ENODATA', 'ENOTFOUND', 'ENONAME', 'NXDOMAIN']);
31
- function errorRecord(value) {
32
- return value && typeof value === 'object' && !Array.isArray(value)
33
- ? value
34
- : {};
35
- }
36
- function registryStatusToken(value) {
37
- return value.toLowerCase().replace(/[^a-z0-9]+/gu, '');
38
- }
39
- function rdapEventDate(events, action) {
40
- const date = events.find((event) => event.action === action)?.date;
41
- return typeof date === 'string' ? date : null;
42
- }
43
- function withoutHttpDeliveryMetadata(value) {
44
- if (!value || typeof value !== 'object' || Array.isArray(value))
45
- return value;
46
- const output = { ...value };
47
- if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
48
- const response = { ...output.response };
49
- delete response.deliveryMetadata;
50
- output.response = response;
51
- }
52
- return output;
53
- }
54
- // No marketplace (Afternic/Sedo/Dan.com/GoDaddy Auctions/etc.) offers a
55
- // free, no-auth API to check "is this specific domain listed for sale" -
56
- // what's checkable without credentials is the parking/landing-page
57
- // nameservers and homepage copy these services actually use, which is
58
- // broader coverage of the same signal, not a live cross-marketplace lookup.
59
- const PARKING_NS_PATTERNS = [
60
- /sedoparking\.com$/i,
61
- /sedo\.com$/i,
62
- /above\.com$/i,
63
- /bodis\.com$/i,
64
- /parkingcrew\.net$/i,
65
- /dan\.com$/i,
66
- /hugedomains\.com$/i,
67
- /uniregistry/i,
68
- /squadhelp/i,
69
- /afternic/i,
70
- /voodoo\.com$/i,
71
- /fabulous\.com$/i,
72
- /namedrive/i,
73
- /smartname\.com$/i,
74
- /domainsponsor\.com$/i,
75
- /undeveloped\.com$/i,
76
- /trafficz\.com$/i,
77
- /dsredirection\.com$/i,
78
- ];
79
- const FOR_SALE_TEXT_RE = /(this domain (?:name )?(?:may be|is) for sale|buy this domain|domain(?: name)? for sale|make (?:an|your) offer|inquire about (?:this|the) domain|purchase this domain|this domain is available for purchase|backorder this domain|bid on this domain|premium domain for sale|own this domain|click here to buy this domain|would you like to buy this domain|this domain is (?:available for lease|listed for sale))/i;
80
- const FOR_SALE_PATH_RE = /\/(?:premium-)?domains?-for-sale(?:\/|$)/i;
81
- function forSaleRedirectSignal(httpObservation) {
82
- if (!httpObservation || typeof httpObservation !== 'object')
83
- return null;
84
- const observation = httpObservation;
85
- const urls = [
86
- observation.finalUrl,
87
- ...(Array.isArray(observation.redirects)
88
- ? observation.redirects.map((redirect) => redirect && typeof redirect === 'object' ? redirect.to : null)
89
- : []),
90
- ];
91
- for (const value of urls) {
92
- if (typeof value !== 'string' || value.length > 2048)
93
- continue;
94
- try {
95
- const url = new URL(value);
96
- if (['http:', 'https:'].includes(url.protocol) && FOR_SALE_PATH_RE.test(url.pathname)) {
97
- return `for-sale landing-page redirect (${url.origin}${url.pathname})`;
98
- }
99
- }
100
- catch {
101
- // Malformed retained provenance cannot establish a sale signal.
102
- }
103
- }
104
- return null;
105
- }
106
- // ---------------------------------------------------------------------------
107
- // Acquisition/sourcing signals: domain age, privacy-redaction, expiry
108
- // proximity, and site-activity status. These read data already fetched by
109
- // checkDomainAvailability (RDAP/WHOIS dates and registrant info) - none of
110
- // this triggers extra registry queries.
111
- // ---------------------------------------------------------------------------
112
- function computeAgeDays(dateStr) {
113
- const d = parseRegistryDate(dateStr);
114
- return d ? Math.floor((Date.now() - d.getTime()) / 86400000) : null;
115
- }
116
- function computeDaysUntil(dateStr) {
117
- const d = parseRegistryDate(dateStr);
118
- return d ? Math.ceil((d.getTime() - Date.now()) / 86400000) : null;
119
- }
120
- const PRIVACY_MARKERS = [
121
- /redacted for privacy/i,
122
- /data protected/i,
123
- /privacy\s*protect/i,
124
- /whoisguard/i,
125
- /domains by proxy/i,
126
- /perfect privacy/i,
127
- /contact privacy/i,
128
- /private registration/i,
129
- /identity protect/i,
130
- /not disclosed/i,
131
- /withheld for privacy/i,
132
- ];
133
- // Explicit privacy/redaction markers establish a positive privacy signal.
134
- // Usable public contact data establishes a negative one. A missing or blank
135
- // contact can instead reflect registry policy, access tier, parser coverage,
136
- // or ordinary omission, so it remains inconclusive rather than being promoted
137
- // to an affirmative privacy claim.
138
- function isPrivacyProtected(registrant) {
139
- if (!registrant)
140
- return null;
141
- const blob = [registrant.name, registrant.org, registrant.email].filter(Boolean).join(' ');
142
- if (!blob)
143
- return null;
144
- return PRIVACY_MARKERS.some((re) => re.test(blob));
145
- }
146
- // RDAP Lookup retains a bounded multi-value contact inventory, but Bulk and
147
- // watchlist availability records deliberately keep the historical compact
148
- // shape. This prevents repeated contact arrays and registry links from
149
- // expanding browser-local stores or flowing into case evidence implicitly.
150
- function compactContact(contact) {
151
- if (!contact || typeof contact !== 'object' || Array.isArray(contact))
152
- return null;
153
- const value = contact;
154
- const compact = {
155
- handle: value.handle || null,
156
- name: value.name || null,
157
- org: value.org || null,
158
- email: value.email || null,
159
- phone: value.phone || null,
160
- address: value.address || null,
161
- };
162
- return Object.values(compact).some(Boolean) ? compact : null;
163
- }
164
- // A number of ccTLD registries publish neither an RDAP bootstrap entry nor a
165
- // generally reachable port-43 WHOIS service. A positive NS answer cannot
166
- // replace registry registration data, but it does prove that the registrable
167
- // domain has an active DNS delegation. Use that as a bounded, positive-only
168
- // fallback: no answer is never interpreted as availability because registered
169
- // domains can legitimately be undelegated.
170
- async function checkDnsDelegation(domain, { resolver = dns.resolveNs } = {}) {
171
- let timer;
172
- try {
173
- const records = await Promise.race([
174
- Promise.resolve().then(() => resolver(domain)),
175
- new Promise((_, reject) => {
176
- timer = setTimeout(() => reject(new Error('DNS delegation lookup timed out')), DNS_DELEGATION_TIMEOUT_MS);
177
- }),
178
- ]);
179
- const validNameservers = [...new Set((Array.isArray(records) ? records : [])
180
- .filter((value) => typeof value === 'string')
181
- .map((value) => value.trim().replace(/\.+$/, '').toLowerCase())
182
- .filter((value) => value.length > 0 && value.length <= 253)
183
- .filter((value) => value.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label))))]
184
- .sort();
185
- const normalized = validNameservers.slice(0, MAX_DELEGATION_NAMESERVERS);
186
- return {
187
- delegated: normalized.length > 0,
188
- nameservers: normalized,
189
- nameserversTruncated: validNameservers.length > MAX_DELEGATION_NAMESERVERS,
190
- error: null,
191
- };
192
- }
193
- catch (err) {
194
- const error = errorRecord(err);
195
- if (typeof error.code === 'string' && MISSING_DNS_CODES.has(error.code)) {
196
- return { delegated: false, nameservers: [], nameserversTruncated: false, error: null };
197
- }
198
- return {
199
- delegated: false,
200
- nameservers: [],
201
- nameserversTruncated: false,
202
- error: nonEmptyErrorMessage(err, String(err)).slice(0, 180),
203
- };
204
- }
205
- finally {
206
- clearTimeout(timer);
207
- }
208
- }
209
- // Fetches enough of the homepage for the lightweight HTML signals below and
210
- // preserves why the probe failed. A failed request is not evidence that a
211
- // domain has "no site": transient DNS/TLS/network failures, a slow origin, or
212
- // an HTTP error can all produce the same null body. Keeping that distinction
213
- // avoids turning an inconclusive probe into a false inactivity claim.
214
- async function fetchHomepage(domain, { fetcher = safeFetchDetailed, timeoutMs = HOMEPAGE_FETCH_TIMEOUT_MS } = {}) {
215
- const requestTimeoutMs = Number.isInteger(timeoutMs) && timeoutMs >= 10 && timeoutMs <= HOMEPAGE_FETCH_TIMEOUT_MS
216
- ? timeoutMs
217
- : HOMEPAGE_FETCH_TIMEOUT_MS;
218
- const headers = whoisleuthRequestHeaders();
219
- const failures = [];
220
- const probeStartedAt = Date.now();
221
- for (const scheme of ['https', 'http']) {
222
- const requestUrl = `${scheme}://${domain}`;
223
- const attemptStartedAt = Date.now();
224
- const controller = new AbortController();
225
- const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
226
- try {
227
- const fetched = await fetcher(requestUrl, { signal: controller.signal, headers });
228
- const fetchedResponse = fetched instanceof Response ? fetched : fetched.response;
229
- const detail = fetched && typeof fetched === 'object' && 'response' in fetched
230
- ? fetched
231
- : {
232
- response: fetchedResponse,
233
- requestedUrl: requestUrl,
234
- finalUrl: requestUrl,
235
- redirectCount: 0,
236
- redirectLimitReached: false,
237
- hops: [{ url: requestUrl, status: fetchedResponse.status, location: null, durationMs: Date.now() - attemptStartedAt }],
238
- durationMs: Date.now() - attemptStartedAt,
239
- };
240
- const res = detail.response;
241
- // A truncated homepage is still fine here - only used to scan for a
242
- // for-sale text match, not parsed as well-formed content. The
243
- // timeout stays armed through this read (cleared in `finally` below,
244
- // not here) - a malicious site could otherwise send headers
245
- // immediately and then trickle or stall the body forever, hanging
246
- // this worker with no deadline once the timer above is disarmed.
247
- if (res.ok) {
248
- const body = await readTextCapped(res, MAX_HOMEPAGE_BYTES, { includeSha256: true });
249
- return {
250
- text: body.text,
251
- status: 'fetched',
252
- detail: `Homepage responded over ${scheme.toUpperCase()} (HTTP ${res.status}).`,
253
- responsePolicy: analyzeResponsePolicyHeaders(res.headers),
254
- technologyHeaders: captureTechnologyResponseHeaders(res.headers),
255
- http: buildHttpObservation({ ...detail, durationMs: Date.now() - attemptStartedAt }, {
256
- previousAttempts: failures,
257
- capturedBodyBytes: body.bytesRead,
258
- bodyInspected: true,
259
- bodyTruncated: body.truncated,
260
- bodySha256: body.sha256,
261
- }),
262
- };
263
- }
264
- // A non-2xx HTTP response still conclusively proves that a web service
265
- // answered on this domain. It may block this probe (403), require auth
266
- // (401), have a broken homepage route (404), or be unhealthy (5xx), but
267
- // none of those are equivalent to "no website". We cannot inspect its
268
- // HTML signals, so preserve a separate responded status and release the
269
- // unused body.
270
- await res.body?.cancel().catch(() => { });
271
- return {
272
- text: null,
273
- status: 'responded',
274
- detail: `Web server responded over ${scheme.toUpperCase()} (HTTP ${res.status}); homepage content was not available for inspection.`,
275
- responsePolicy: analyzeResponsePolicyHeaders(res.headers),
276
- technologyHeaders: captureTechnologyResponseHeaders(res.headers),
277
- http: buildHttpObservation({ ...detail, durationMs: Date.now() - attemptStartedAt }, {
278
- previousAttempts: failures,
279
- capturedBodyBytes: 0,
280
- bodyInspected: false,
281
- }),
282
- };
283
- }
284
- catch (err) {
285
- const error = errorRecord(err);
286
- const reason = error.name === 'AbortError'
287
- ? `timed out after ${requestTimeoutMs} milliseconds`
288
- : nonEmptyErrorMessage(err, 'request failed')
289
- .replace(/[\u0000-\u001f\u007f]+/g, ' ')
290
- .slice(0, 180);
291
- failures.push({ url: requestUrl, error: `${scheme.toUpperCase()} ${reason}` });
292
- }
293
- finally {
294
- clearTimeout(timeout);
295
- }
296
- }
297
- return {
298
- text: null,
299
- status: 'inconclusive',
300
- detail: failures.length
301
- ? `Could not confirm homepage activity: ${failures.map((attempt) => attempt.error).join('; ')}.`
302
- : 'Could not confirm homepage activity.',
303
- http: failedHttpObservation(failures, { durationMs: Date.now() - probeStartedAt }),
304
- };
305
- }
306
- function deriveWebsiteActivity(homepageStatus, hasFavicon, alreadyParked = false) {
307
- if (alreadyParked)
308
- return 'parked';
309
- if (homepageStatus === 'fetched' || homepageStatus === 'responded' || hasFavicon)
310
- return 'active';
311
- return 'unreachable';
312
- }
313
- function registryPolicyDetail(domain, fast) {
314
- const details = [];
315
- const rdapAdmission = registryServiceAdmissionFor(domain, 'rdap');
316
- if (rdapAdmission && !rdapAdmission.allowed) {
317
- details.push('RDAP was not queried because the registry capability profile records no IANA-published RDAP service for this suffix.');
318
- }
319
- const whoisAdmission = fast ? null : registryServiceAdmissionFor(domain, 'whois');
320
- if (whoisAdmission && !whoisAdmission.allowed) {
321
- details.push(whoisAdmission.state === 'permission_required'
322
- ? 'WHOIS was not queried because registry permission or source authorisation is required.'
323
- : 'WHOIS was not queried because IANA publishes no domain WHOIS service for this suffix.');
324
- }
325
- return details.length ? ` ${details.join(' ')}` : '';
326
- }
327
- // fast: true skips the WHOIS fallback (no TCP:43 chain) and the homepage
328
- // fetch (no for-sale/parking detection) - just RDAP plus the signals
329
- // derivable from it (age, expiry proximity, privacy). Meant for scanning
330
- // large sourcing candidate lists quickly and gently on registry rate
331
- // limits; anything it can't resolve (state "unknown") is meant to get a
332
- // follow-up deep check (fast: false, the default) on the shortlist only.
4
+ import { canonicalRegistrableDomain } from "../packages/analysis/registrable-domain.mjs";
5
+ import { isValidAsciiHostname } from "../packages/contracts/domain-name.mjs";
6
+ import { prepareSelectedLookupUrl } from "../packages/evidence/lookup-target.mjs";
7
+ import { resolveDomainRegistration } from "./domain-registration.mjs";
8
+ import { enrichWebsite } from "./website-enrichment.mjs";
333
9
  async function checkDomainAvailability(domain, options = {}) {
10
+ options.signal?.throwIfAborted();
334
11
  const fast = options.fast === true;
335
- const collectDns = options.collectDnsIntelligence || collectDnsIntelligence;
336
- const collectTls = options.collectTlsIntelligence || collectTlsIntelligence;
337
- const fetchHomepageForDomain = options.fetchHomepage || fetchHomepage;
338
- const fetchFaviconForDomain = options.fetchFaviconHash || fetchFaviconHash;
12
+ const observationHostname = fast ? domain : options.observationHostname ?? domain;
13
+ if (options.observationHostname !== undefined && (!isValidAsciiHostname(options.observationHostname)
14
+ || options.observationHostname !== options.observationHostname.toLowerCase()
15
+ || canonicalRegistrableDomain(options.observationHostname) !== domain)) {
16
+ throw new TypeError('The observation hostname must belong to the registration target.');
17
+ }
18
+ const selectedUrl = options.selectedUrl === undefined ? undefined : prepareSelectedLookupUrl(options.selectedUrl, observationHostname);
339
19
  const featurePolicy = options.featurePolicy || networkFeaturePolicy();
340
- const rdapEnabled = featureDecision('rdap', featurePolicy).enabled;
341
- const whoisEnabled = featureDecision('whois', featurePolicy).enabled;
342
- const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
343
20
  const websiteProbeEnabled = featureDecision('website_probe', featurePolicy).enabled;
344
- const tlsIntelligenceEnabled = featureDecision('tls_intelligence', featurePolicy).enabled;
345
- const deepScanComplete = rdapEnabled
346
- && whoisEnabled
347
- && dnsIntelligenceEnabled
348
- && websiteProbeEnabled
349
- && tlsIntelligenceEnabled;
350
- const hasPreloadedRdap = Object.prototype.hasOwnProperty.call(options, 'rdapRecord');
351
- const hasPreloadedWhois = Object.prototype.hasOwnProperty.call(options, 'whoisChain');
352
- const hasPreloadedRdapPromise = Object.prototype.hasOwnProperty.call(options, 'rdapRecordPromise');
353
- const hasPreloadedWhoisPromise = Object.prototype.hasOwnProperty.call(options, 'whoisChainPromise');
354
- const hasPreloadedDnsDelegation = Object.prototype.hasOwnProperty.call(options, 'dnsDelegation');
355
- let nameservers = [];
356
- let statuses = [];
357
- let rdapServer = null;
358
- let rdapFound = false;
359
- let registrar = null;
360
- let registrant = null;
361
- let abuse = null;
362
- let createdDate = null;
363
- let expiryDate = null;
364
- let createdDateIso = null;
365
- let expiryDateIso = null;
366
- let registrationSource = null;
367
- let registrationConfidence = 'high';
368
- let dnssec = null;
369
- let registryDnsEvidence = null;
370
- if (rdapEnabled) {
371
- try {
372
- // Shared with /api/rdap (lib/rdap.mts's fetchRdapRecord) rather than a
373
- // separate fetch+parse here - same registry data either way, and this
374
- // also picks up that function's short-TTL cache (lib/lookup-cache.mts)
375
- // and upstream timeout for free.
376
- const recordValue = hasPreloadedRdapPromise
377
- ? await options.rdapRecordPromise
378
- : hasPreloadedRdap
379
- ? options.rdapRecord
380
- : await fetchRdapRecord('domain', domain);
381
- const record = errorRecord(recordValue);
382
- const recordRdapServer = typeof record.rdapServer === 'string' ? record.rdapServer : null;
383
- const upstreamStatus = typeof record.upstreamStatus === 'number' ? record.upstreamStatus : null;
384
- if (Object.keys(record).length) {
385
- rdapServer = recordRdapServer;
386
- if (upstreamStatus === 404) {
387
- return {
388
- state: 'available',
389
- confidence: 'high',
390
- detail: 'The registry\'s RDAP service has no record for this domain.',
391
- source: 'rdap',
392
- rdapServer: recordRdapServer,
393
- };
394
- }
395
- const parsed = errorRecord(record.parsed);
396
- if (Object.keys(parsed).length) {
397
- registryDnsEvidence = parsed;
398
- statuses = Array.isArray(parsed.statuses)
399
- ? parsed.statuses.filter((status) => typeof status === 'string').map((status) => status.toLowerCase())
400
- : [];
401
- nameservers = Array.isArray(parsed.nameservers)
402
- ? parsed.nameservers.filter((nameserver) => typeof nameserver === 'string')
403
- : [];
404
- registrar = compactContact(parsed.registrar);
405
- registrant = compactContact(parsed.registrant);
406
- abuse = compactContact(parsed.abuse);
407
- const events = Array.isArray(parsed.events)
408
- ? parsed.events.map(errorRecord)
409
- : [];
410
- const lifecycle = errorRecord(parsed.lifecycle);
411
- createdDate = typeof lifecycle.createdDate === 'string'
412
- ? lifecycle.createdDate
413
- : rdapEventDate(events, 'registration');
414
- expiryDate = typeof lifecycle.expiryDate === 'string'
415
- ? lifecycle.expiryDate
416
- : rdapEventDate(events, 'expiration');
417
- createdDateIso = typeof lifecycle.createdDateIso === 'string'
418
- ? lifecycle.createdDateIso
419
- : registryDateIso(createdDate);
420
- expiryDateIso = typeof lifecycle.expiryDateIso === 'string'
421
- ? lifecycle.expiryDateIso
422
- : registryDateIso(expiryDate);
423
- dnssec = typeof parsed.dnssec === 'string' ? parsed.dnssec : null;
424
- rdapFound = true;
425
- registrationSource = 'rdap';
426
- }
427
- }
428
- }
429
- catch {
430
- /* fall through to WHOIS-based detection (deep mode only) */
431
- }
432
- }
433
- const dnsDelegationPromise = !rdapFound && dnsIntelligenceEnabled
434
- ? hasPreloadedDnsDelegation
435
- ? Promise.resolve(options.dnsDelegation)
436
- : checkDnsDelegation(domain, { resolver: options.resolveNs || dns.resolveNs })
437
- : null;
438
- let whoisChain = null;
439
- let whoisParsed = null;
440
- if (!rdapFound && !fast && whoisEnabled) {
441
- try {
442
- whoisChain = (hasPreloadedWhoisPromise
443
- ? await options.whoisChainPromise
444
- : hasPreloadedWhois
445
- ? options.whoisChain
446
- : await buildWhoisChain(domain)) ?? null;
447
- if (!Array.isArray(whoisChain))
448
- throw new Error('WHOIS chain unavailable');
449
- const parsed = parseWhoisChain(whoisChain);
450
- whoisParsed = parsed;
451
- if (parsed.notFound) {
452
- return {
453
- state: 'available',
454
- confidence: 'medium',
455
- detail: `WHOIS reports no matching record for this domain${parsed.notFoundSource ? ` (per ${parsed.notFoundSource})` : ''}.`,
456
- source: 'whois',
457
- };
458
- }
459
- if (parsed.registrationStatus === 'registered') {
460
- if (parsed.nameservers.length)
461
- nameservers = parsed.nameservers;
462
- if (parsed.statuses.length)
463
- statuses = parsed.statuses.map((status) => status.toLowerCase());
464
- if (parsed.registrar) {
465
- registrar = {
466
- handle: null,
467
- ianaId: parsed.registrarIanaId || null,
468
- name: parsed.registrar,
469
- org: null,
470
- email: parsed.abuseEmail || null,
471
- phone: parsed.abusePhone || null,
472
- };
473
- }
474
- if (parsed.registrantName || parsed.registrantOrg || parsed.registrantEmail || parsed.registrantPhone) {
475
- registrant = {
476
- handle: null,
477
- name: parsed.registrantName || null,
478
- org: parsed.registrantOrg || null,
479
- email: parsed.registrantEmail || null,
480
- phone: parsed.registrantPhone || null,
481
- };
482
- }
483
- if (parsed.abuseEmail || parsed.abusePhone) {
484
- abuse = { handle: null, name: null, org: parsed.registrar || null, email: parsed.abuseEmail || null, phone: parsed.abusePhone || null };
485
- }
486
- createdDate = parsed.createdDate || null;
487
- expiryDate = parsed.expiryDate || null;
488
- createdDateIso = parsed.createdDateIso || parsed.lifecycle?.createdDateIso || registryDateIso(createdDate);
489
- expiryDateIso = parsed.expiryDateIso || parsed.lifecycle?.expiryDateIso || registryDateIso(expiryDate);
490
- if (!dnssec)
491
- dnssec = parsed.dnssec || null;
492
- registrationSource = 'whois';
493
- }
494
- }
495
- catch {
496
- /* if both RDAP and WHOIS fail, we simply can't determine availability */
497
- }
498
- }
499
- // WHOIS ran and was not a confirmed not-found, but produced no positive
500
- // registration evidence either - e.g. the registry answered inconclusively
501
- // or every referral hop failed/rate-limited. Report "unknown" rather than
502
- // fabricating "registered" from an empty record.
503
- const hasWhoisRegistrationData = whoisParsed?.registrationStatus === 'registered';
504
- let dnsDelegated = false;
505
- if (!rdapFound && !hasWhoisRegistrationData && dnsDelegationPromise) {
506
- const delegation = await dnsDelegationPromise;
507
- if (delegation && delegation.delegated === true && Array.isArray(delegation.nameservers) && delegation.nameservers.length) {
508
- dnsDelegated = true;
509
- nameservers = delegation.nameservers;
510
- registrationSource = 'dns';
511
- registrationConfidence = 'medium';
512
- }
513
- }
514
- if (!rdapFound && !hasWhoisRegistrationData && !dnsDelegated) {
515
- const disabledSources = [
516
- !rdapEnabled ? 'RDAP' : null,
517
- !fast && !whoisEnabled ? 'WHOIS' : null,
518
- !dnsIntelligenceEnabled ? 'DNS intelligence' : null,
519
- ].filter(Boolean);
520
- const disabledDetail = disabledSources.length
521
- ? ` ${disabledSources.join(', ')} ${disabledSources.length === 1 ? 'is' : 'are'} disabled by deployment policy.`
522
- : '';
523
- const policyDetail = registryPolicyDetail(domain, fast);
524
- return {
525
- state: 'unknown',
526
- confidence: 'low',
527
- detail: fast
528
- ? `No enabled registration source produced a record or authoritative delegation. A fast scan cannot determine registration status.${disabledDetail}${policyDetail}`
529
- : whoisParsed && whoisParsed.failedHop
530
- ? `WHOIS was inconclusive - a referral hop did not answer conclusively (${whoisParsed.failedHop}).${policyDetail}`
531
- : `No enabled registration source returned conclusive data or an authoritative DNS delegation.${disabledDetail}${policyDetail}`,
532
- ...(!fast && whoisEnabled ? { source: 'whois' } : {}),
533
- };
534
- }
535
- const domainAgeDays = computeAgeDays(createdDateIso || createdDate);
536
- const expiresInDays = computeDaysUntil(expiryDateIso || expiryDate);
537
- // DNS proves delegation only; it says nothing about whether registry
538
- // contact data is privacy-protected or merely unavailable.
539
- const privacyProtected = registrationSource === 'dns' ? null : isPrivacyProtected(registrant);
540
- const baseInfo = {
541
- nameservers,
542
- statuses,
543
- registrar,
544
- registrant,
545
- abuse,
546
- createdDate,
547
- expiryDate,
548
- createdDateIso,
549
- expiryDateIso,
550
- rdapServer,
551
- domainAgeDays,
552
- expiresInDays,
553
- privacyProtected,
554
- dnssec,
555
- source: registrationSource,
556
- };
557
- if (statuses.some((status) => {
558
- const normalizedStatus = registryStatusToken(status);
559
- return normalizedStatus.includes('pendingdelete') || normalizedStatus.includes('redemptionperiod');
560
- })) {
561
- return {
562
- state: 'expiring',
563
- confidence: 'medium',
564
- detail: 'Domain is in redemption/pending-delete status and may become available soon.',
565
- ...baseInfo,
566
- };
567
- }
568
- if (fast) {
569
- return {
570
- state: 'registered',
571
- confidence: registrationConfidence,
572
- detail: registrationSource === 'dns'
573
- ? 'Authoritative DNS delegation confirms the domain is registered, but RDAP/WHOIS registration details were unavailable.'
574
- : 'Domain is registered. Run a deep check for parking/for-sale detection.',
575
- ...baseInfo,
576
- };
577
- }
578
- // Registered - look for for-sale/parked/website signals (homepage fetch,
579
- // deep mode only), and check for a configured mail exchanger as a
580
- // phishing-risk signal (a lookalike domain that can receive/send mail is
581
- // capable of running credential-harvesting or BEC campaigns).
582
- const nsSignal = nameservers.find((ns) => PARKING_NS_PATTERNS.some((re) => re.test(ns)));
583
- let forSaleSignal = nsSignal ? `parking nameserver (${nsSignal})` : null;
584
- let activityStatus = nsSignal ? 'parked' : 'unknown';
585
- // Homepage + bounded DNS evidence resolve in parallel; the favicon fetch is sequenced after
586
- // the homepage so it can use any <link rel="icon"> the page declares (many
587
- // sites serve no /favicon.ico and only point to a CDN PNG this way). One
588
- // extra round-trip on the already-slow deep path, in exchange for finding
589
- // favicons the bare /favicon.ico probe would miss.
590
- const [homepage, dnsIntelligence, tlsIntelligence] = await Promise.all([
591
- websiteProbeEnabled ? fetchHomepageForDomain(domain).catch((err) => ({
592
- text: null,
593
- status: 'inconclusive',
594
- detail: `Could not confirm homepage activity: ${String(err && err.message ? err.message : 'request failed').slice(0, 180)}.`,
595
- http: failedHttpObservation([
596
- { url: `https://${domain}`, error: String(err && err.message ? err.message : 'request failed') },
597
- ]),
598
- })) : Promise.resolve({
599
- text: null,
600
- status: 'skipped',
601
- detail: 'Website probing is disabled by deployment policy.',
602
- http: skippedHttpObservation(),
603
- }),
604
- dnsIntelligenceEnabled
605
- ? collectDns(domain, {
606
- ...(options.dnsResolvers ? { resolvers: options.dnsResolvers } : {}),
607
- includeExtendedContext: options.includeExtendedDnsContext === true,
608
- includeInheritedCaa: options.includeInheritedCaa === true,
609
- registryEvidence: registryDnsEvidence,
610
- })
611
- : Promise.resolve(skippedDnsIntelligence('DNS intelligence is disabled by deployment policy.', {
612
- includeExtendedContext: options.includeExtendedDnsContext === true,
613
- includeInheritedCaa: options.includeInheritedCaa === true,
614
- })),
615
- tlsIntelligenceEnabled
616
- ? collectTls(domain)
617
- : Promise.resolve(skippedTlsObservation()),
618
- ]);
619
- const page = homepage.text;
620
- const favicon = websiteProbeEnabled
621
- ? await fetchFaviconForDomain(domain, { html: page || '' }).catch(() => null)
622
- : null;
623
- const faviconHash = favicon ? favicon.hash : null;
624
- const faviconPHash = favicon ? favicon.phash : null;
625
- let htmlSignals = {
626
- pageTitle: null,
627
- hasPasswordField: false,
628
- phishingLanguageMatch: null,
629
- hasExternalFormAction: null,
630
- externalAssetHosts: [],
631
- cspMetaPolicy: null,
632
- pageIdentity: null,
633
- credentialSurfaceProfile: null,
634
- structuredDataIdentity: null,
635
- technologyProfile: null,
636
- pageRoleProfile: null,
637
- clientBehaviorProfile: null,
638
- };
639
- if (homepage.status === 'fetched') {
640
- if (page) {
641
- const saleMatch = page.match(FOR_SALE_TEXT_RE);
642
- if (saleMatch) {
643
- forSaleSignal = forSaleSignal || `homepage text ("${saleMatch[0]}")`;
644
- activityStatus = 'parked';
645
- }
646
- const responseContentType = homepage.http?.response?.contentType;
647
- const pageIdentityEligible = typeof responseContentType !== 'string'
648
- || responseContentType.trim() === ''
649
- || /^(?:text\/html|application\/xhtml\+xml)(?:\s*;|$)/i.test(responseContentType.trim());
650
- htmlSignals = extractHtmlSignals(page, domain, {
651
- ...(typeof homepage.http?.finalUrl === 'string' ? { baseUrl: homepage.http.finalUrl } : {}),
652
- ...(typeof homepage.http?.observedAt === 'string' ? { observedAt: homepage.http.observedAt } : {}),
653
- sourceTruncated: homepage.http?.response?.bodyTruncated === true,
654
- exactBodyHash: homepage.http?.response?.bodyHash,
655
- httpServer: homepage.http?.response?.server,
656
- responseHeaders: homepage.technologyHeaders,
657
- activityStatus,
658
- includePageIdentity: pageIdentityEligible,
659
- includePublicationMetadata: options.includePublicationMetadata !== false,
660
- ...(options.includeCredentialSurfaceProfile !== undefined
661
- ? { includeCredentialSurfaceProfile: options.includeCredentialSurfaceProfile }
662
- : {}),
663
- ...(options.includeStructuredDataIdentity !== undefined
664
- ? { includeStructuredDataIdentity: options.includeStructuredDataIdentity }
665
- : {}),
666
- ...(options.includeTechnologyProfile !== undefined
667
- ? { includeTechnologyProfile: options.includeTechnologyProfile }
668
- : {}),
669
- });
670
- }
671
- }
672
- if (options.includeTechnologyProfile !== false && htmlSignals.technologyProfile === null
673
- && ['fetched', 'responded'].includes(homepage.status)) {
674
- htmlSignals.technologyProfile = analyzeWebsiteTechnology({
675
- htmlAvailable: false,
676
- httpServer: homepage.http?.response?.server,
677
- responseHeaders: homepage.technologyHeaders,
678
- observedAt: homepage.http?.observedAt,
679
- });
680
- }
681
- const responsePolicy = qualifyResponsePolicyWithCspMeta(homepage.responsePolicy, htmlSignals.cspMetaPolicy);
682
- const retainedHttp = options.includeDeliveryMetadata === false
683
- ? withoutHttpDeliveryMetadata(homepage.http)
684
- : homepage.http;
685
- const { cspMetaPolicy: _cspMetaPolicy, ...retainedHtmlSignals } = htmlSignals;
686
- const securityPosture = options.includeSecurityPosture === false ? null : analyzeWebsiteSecurityPosture({
687
- http: homepage.http,
688
- responsePolicy,
689
- pageIdentity: htmlSignals.pageIdentity,
690
- tls: tlsIntelligence,
691
- dns: dnsIntelligence,
692
- dnssec,
693
- observedAt: homepage.http?.observedAt,
694
- });
695
- const redirectSaleSignal = forSaleRedirectSignal(homepage.http);
696
- if (!forSaleSignal && redirectSaleSignal) {
697
- forSaleSignal = redirectSaleSignal;
698
- activityStatus = 'parked';
699
- }
700
- const faviconProvedActive = homepage.status === 'inconclusive' && Boolean(favicon);
701
- const websiteProbeStatus = faviconProvedActive ? 'responded' : homepage.status;
702
- const websiteProbeDetail = faviconProvedActive
703
- ? `${homepage.detail} A favicon responded successfully, confirming an active web service.`
704
- : homepage.detail;
705
- if (websiteProbeEnabled) {
706
- activityStatus = deriveWebsiteActivity(homepage.status, Boolean(favicon), activityStatus === 'parked');
707
- }
708
- if (!forSaleSignal) {
709
- return {
710
- state: 'registered',
711
- confidence: registrationConfidence,
712
- detail: registrationSource === 'dns'
713
- ? 'Authoritative DNS delegation confirms the domain is registered, but RDAP/WHOIS registration details were unavailable. No for-sale signals were observed.'
714
- : 'Domain is registered and shows no for-sale signals.',
715
- activityStatus,
716
- websiteProbeStatus,
717
- websiteProbeDetail,
718
- http: retainedHttp,
719
- deepScanComplete,
720
- faviconHash,
721
- faviconPHash,
722
- ...retainedHtmlSignals,
723
- securityPosture,
724
- ...baseInfo,
725
- nameservers: nameservers.length ? nameservers : dnsIntelligence.records.ns,
726
- dns: dnsIntelligence,
727
- tls: tlsIntelligence,
728
- hasMx: dnsIntelligence.hasMx,
729
- hasNullMx: dnsIntelligence.hasNullMx,
730
- mxHosts: dnsIntelligence.mxHosts,
731
- hasSpf: dnsIntelligence.hasSpf,
732
- hasDmarc: dnsIntelligence.hasDmarc,
733
- };
734
- }
735
- return {
736
- state: 'for_sale',
737
- confidence: 'medium',
738
- detail: `Detected a for-sale listing (${forSaleSignal}).`,
739
- activityStatus,
740
- websiteProbeStatus,
741
- websiteProbeDetail,
742
- http: retainedHttp,
743
- deepScanComplete,
744
- faviconHash,
745
- faviconPHash,
746
- ...retainedHtmlSignals,
747
- securityPosture,
748
- ...baseInfo,
749
- nameservers: nameservers.length ? nameservers : dnsIntelligence.records.ns,
750
- dns: dnsIntelligence,
751
- tls: tlsIntelligence,
752
- hasMx: dnsIntelligence.hasMx,
753
- hasNullMx: dnsIntelligence.hasNullMx,
754
- mxHosts: dnsIntelligence.mxHosts,
755
- hasSpf: dnsIntelligence.hasSpf,
756
- hasDmarc: dnsIntelligence.hasDmarc,
757
- };
758
- }
759
- export { checkDomainAvailability, checkDnsDelegation, fetchHomepage, isPrivacyProtected, deriveWebsiteActivity, forSaleRedirectSignal, parseRegistryDate as parseWhoisDate, };
21
+ if (selectedUrl && (fast || !websiteProbeEnabled))
22
+ throw new TypeError('Selected URL collection requires an enabled Deep website observation.');
23
+ const assessment = await resolveDomainRegistration(domain, options, featurePolicy);
24
+ const { registration } = assessment;
25
+ if (fast || registration.state !== 'registered' && !selectedUrl)
26
+ return registration;
27
+ return enrichWebsite({ domain, observationHostname, selectedUrl, ...assessment }, options, featurePolicy);
28
+ }
29
+ export { checkDomainAvailability };
30
+ export { checkDnsDelegation, isPrivacyProtected } from "./domain-registration.mjs";
31
+ export { fetchHomepage, deriveWebsiteActivity, forSaleRedirectSignal } from "./website-enrichment.mjs";
32
+ export { parseRegistryDate as parseWhoisDate } from "./registry-dates.mjs";