@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/NOTICE +8 -2
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/argument-values.mjs +55 -0
  7. package/cli/arguments.mjs +24 -408
  8. package/cli/artifact-validation/case-response.mjs +65 -44
  9. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  10. package/cli/artifact-validation/lookup-evidence.mjs +15 -9
  11. package/cli/artifact-validation/signed-review.mjs +136 -68
  12. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  13. package/cli/artifact-verify.mjs +92 -24
  14. package/cli/assurance-arguments.mjs +56 -0
  15. package/cli/assurance-command-definitions.mjs +179 -0
  16. package/cli/assurance-command-runner.mjs +40 -5
  17. package/cli/bagit-folder.mjs +57 -0
  18. package/cli/bagit-review.mjs +23 -0
  19. package/cli/bounded-json.mjs +1 -1
  20. package/cli/bulk-checkpoint.mjs +1 -1
  21. package/cli/bulk-command-runner.mjs +4 -2
  22. package/cli/bulk-output.mjs +29 -3
  23. package/cli/bulk.mjs +2 -3
  24. package/cli/case-command.mjs +212 -0
  25. package/cli/case-pack.mjs +50 -32
  26. package/cli/ci-report.mjs +6 -3
  27. package/cli/collection-arguments.mjs +123 -0
  28. package/cli/collection-command-definitions.mjs +330 -0
  29. package/cli/collection-preflight.mjs +8 -1
  30. package/cli/command-argument-grammar.mjs +8 -206
  31. package/cli/command-definition.mjs +329 -0
  32. package/cli/command-reference.mjs +94 -1357
  33. package/cli/command-seeds.mjs +18 -0
  34. package/cli/compare.mjs +1 -1
  35. package/cli/completion.mjs +298 -79
  36. package/cli/config-profile.mjs +8 -6
  37. package/cli/ct-event-intake.mjs +16 -16
  38. package/cli/discover.mjs +5 -4
  39. package/cli/discovery-observation-snapshot.mjs +46 -18
  40. package/cli/discovery-scan-command-runner.mjs +5 -3
  41. package/cli/discovery-scan.mjs +7 -4
  42. package/cli/discovery-snapshot.mjs +2 -2
  43. package/cli/discovery-workflow.mjs +5 -5
  44. package/cli/domain-control-monitor.mjs +50 -20
  45. package/cli/domain-control-observations.mjs +151 -103
  46. package/cli/errors.mjs +37 -6
  47. package/cli/evidence-arguments.mjs +46 -0
  48. package/cli/evidence-command-definitions.mjs +69 -0
  49. package/cli/evidence-command-runner.mjs +18 -8
  50. package/cli/evidence-signing.mjs +5 -4
  51. package/cli/external-observation-mapping.mjs +9 -10
  52. package/cli/formatters/terminal-command-formats.mjs +6 -0
  53. package/cli/formatters/terminal-lookup.mjs +24 -46
  54. package/cli/formatters/terminal-shared.mjs +5 -3
  55. package/cli/formatters/terminal-technology.mjs +114 -0
  56. package/cli/history-arguments.mjs +39 -0
  57. package/cli/history-command-definitions.mjs +88 -0
  58. package/cli/indicator-set-command.mjs +33 -0
  59. package/cli/inline-command-families.mjs +6 -46
  60. package/cli/inline-command-runner.mjs +4 -31
  61. package/cli/input.mjs +2 -2
  62. package/cli/intake-command.mjs +107 -0
  63. package/cli/interactive-launcher.mjs +2 -160
  64. package/cli/interchange-report.mjs +8 -10
  65. package/cli/investigation-artifacts.mjs +176 -0
  66. package/cli/investigation-checkpoint.mjs +10 -0
  67. package/cli/investigation-folder.mjs +114 -0
  68. package/cli/investigation-manifest.mjs +1 -112
  69. package/cli/investigation-package-review.mjs +103 -0
  70. package/cli/investigation-plan.mjs +38 -147
  71. package/cli/investigation-recipes.mjs +177 -0
  72. package/cli/investigation-run.mjs +256 -35
  73. package/cli/local-document-checkpoint.mjs +170 -0
  74. package/cli/local-mmdb-review.mjs +197 -17
  75. package/cli/lookalike-calibration.mjs +1 -1
  76. package/cli/lookup-brief.mjs +2 -2
  77. package/cli/lookup-browser.mjs +5 -12
  78. package/cli/lookup-command-runner.mjs +33 -39
  79. package/cli/lookup-diff.mjs +47 -12
  80. package/cli/lookup-plan.mjs +16 -14
  81. package/cli/lookup-reconcile.mjs +15 -12
  82. package/cli/lookup-timeline.mjs +7 -6
  83. package/cli/mail-header-review.mjs +24 -247
  84. package/cli/mail-review.mjs +75 -21
  85. package/cli/manual.mjs +1 -0
  86. package/cli/network-arguments.mjs +51 -0
  87. package/cli/network-command-definitions.mjs +130 -0
  88. package/cli/network-command-runner.mjs +45 -31
  89. package/cli/offline-evidence-review.mjs +39 -6
  90. package/cli/output-file.mjs +31 -5
  91. package/cli/posture.mjs +1 -1
  92. package/cli/registry-doctor.mjs +3 -1
  93. package/cli/registry-fixture-scaffold.mjs +1 -1
  94. package/cli/registry-support.mjs +1 -1
  95. package/cli/retained-artifact-diff.mjs +10 -9
  96. package/cli/review-arguments.mjs +131 -0
  97. package/cli/review-command-definitions.mjs +273 -0
  98. package/cli/review-command-runner.mjs +104 -149
  99. package/cli/risk-calibration.mjs +8 -8
  100. package/cli/runner.mjs +74 -37
  101. package/cli/saved-lookup.mjs +2 -2
  102. package/cli/selected-input-worker.mjs +63 -0
  103. package/cli/sharing-review.mjs +8 -80
  104. package/cli/signer-trust.mjs +104 -0
  105. package/cli/source-reliability.mjs +75 -84
  106. package/cli/support-arguments.mjs +49 -0
  107. package/cli/support-command-definitions.mjs +209 -0
  108. package/cli/support-command-runner.mjs +2 -2
  109. package/cli/terminal-input.mjs +168 -0
  110. package/cli/workflow-arguments.mjs +86 -0
  111. package/cli/workflow-command-definitions.mjs +78 -0
  112. package/cli/workflow-command-runner.mjs +75 -13
  113. package/docs/cli-reference.md +103 -30
  114. package/docs/cli.md +510 -25
  115. package/docs/contextual-reviews.md +155 -0
  116. package/lib/abort.mjs +4 -1
  117. package/lib/analyst-taxonomy.mjs +2 -40
  118. package/lib/application-version.mjs +1 -1
  119. package/lib/availability.mjs +27 -754
  120. package/lib/bounded-contract-normalizers.mjs +4 -0
  121. package/lib/bounded-file.mjs +3 -1
  122. package/lib/bounded-html-document.mjs +125 -0
  123. package/lib/bounded-json.mjs +2 -290
  124. package/lib/browser-library-profile.mjs +99 -130
  125. package/lib/browser-library-worker.mjs +112 -0
  126. package/lib/candidate-provenance-bounds.mjs +2 -0
  127. package/lib/classify.mjs +1 -1
  128. package/lib/client-behavior-profile.mjs +3 -3
  129. package/lib/ct-query.mjs +2 -27
  130. package/lib/ct-response-bounds.mjs +2 -9
  131. package/lib/ct-search.mjs +89 -46
  132. package/lib/dmarc-discovery.mjs +62 -0
  133. package/lib/dns-convergence-review.mjs +21 -8
  134. package/lib/dns-delegation-health.mjs +36 -22
  135. package/lib/dns-inheritance-review.mjs +207 -0
  136. package/lib/dns-intelligence.mjs +44 -29
  137. package/lib/dns-mx.mjs +1 -1
  138. package/lib/dns-query-coverage.mjs +40 -0
  139. package/lib/dnssec-evidence-validation.mjs +11 -3
  140. package/lib/domain-assurance.mjs +1 -1
  141. package/lib/domain-change-packet.mjs +18 -14
  142. package/lib/domain-change-review.mjs +40 -16
  143. package/lib/domain-control-flight-recorder.mjs +301 -124
  144. package/lib/domain-control-manifest.mjs +126 -58
  145. package/lib/domain-posture-analysis.mjs +41 -11
  146. package/lib/domain-posture-parsers.mjs +88 -6
  147. package/lib/domain-posture.mjs +120 -32
  148. package/lib/domain-registration.mjs +392 -0
  149. package/lib/evidence-export-privacy.mjs +568 -0
  150. package/lib/evidence-export.mjs +12 -593
  151. package/lib/evidence-report.mjs +7 -2
  152. package/lib/external-intelligence-risk.mjs +2 -124
  153. package/lib/favicon.mjs +117 -41
  154. package/lib/generated/cisa-kev-catalog.mjs +50 -4
  155. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  156. package/lib/generated/retire-browser-catalog.mjs +2742 -1034
  157. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  158. package/lib/html-signals.mjs +87 -85
  159. package/lib/http-evidence-bounds.mjs +2 -7
  160. package/lib/http-intelligence.mjs +52 -27
  161. package/lib/idn-confusable-policy.mjs +6 -4
  162. package/lib/idn-confusables.mjs +1 -1
  163. package/lib/interchange-fidelity-registry.mjs +10 -0
  164. package/lib/lookup-cache.mjs +34 -11
  165. package/lib/lookup-child-profile-contract.mjs +24 -15
  166. package/lib/lookup-contract-primitives.mjs +1 -1
  167. package/lib/lookup-diagnostics.mjs +2 -1
  168. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  169. package/lib/lookup-progress.mjs +79 -13
  170. package/lib/lookup-response-contract.mjs +1077 -0
  171. package/lib/lookup-response.mjs +11 -24
  172. package/lib/lookup-source-progress.mjs +24 -35
  173. package/lib/lookup.mjs +49 -28
  174. package/lib/observed-network-context.mjs +43 -27
  175. package/lib/opportunity-scoring.mjs +2 -107
  176. package/lib/outbound-identity.mjs +1 -1
  177. package/lib/outbound-request-bounds.mjs +4 -1
  178. package/lib/page-fingerprints.mjs +87 -134
  179. package/lib/page-role-profile.mjs +12 -8
  180. package/lib/perceptual-hash-comparison.mjs +2 -34
  181. package/lib/perceptual-hash.mjs +79 -24
  182. package/lib/portable-generator.mjs +2 -29
  183. package/lib/project-metadata.mjs +2 -8
  184. package/lib/rdap-bootstrap.mjs +72 -22
  185. package/lib/rdap-client.mjs +26 -14
  186. package/lib/rdap-normalization.mjs +33 -18
  187. package/lib/rdap-search-workbench.mjs +1 -1
  188. package/lib/rdap-transport.mjs +47 -20
  189. package/lib/rdap-types.mjs +2 -1
  190. package/lib/rdap.mjs +18 -3
  191. package/lib/registrable-domain.mjs +2 -36
  192. package/lib/registrar-standing-contract.mjs +2 -1
  193. package/lib/registrar-standing.mjs +9 -13
  194. package/lib/registry-capabilities.mjs +7 -3
  195. package/lib/registry-capability-catalogue.mjs +29 -20
  196. package/lib/registry-dates.mjs +2 -190
  197. package/lib/registry-insights.mjs +56 -18
  198. package/lib/response-policy.mjs +86 -48
  199. package/lib/risk-calibration-summary.mjs +2 -338
  200. package/lib/risk-scoring.mjs +2 -345
  201. package/lib/rpki-evidence.mjs +8 -5
  202. package/lib/safe-fetch.mjs +54 -47
  203. package/lib/scoring-evidence-quality.mjs +2 -107
  204. package/lib/security-txt.mjs +10 -7
  205. package/lib/semantic-version.mjs +2 -54
  206. package/lib/service-binding-dns.mjs +35 -0
  207. package/lib/smtp-transport-review.mjs +75 -34
  208. package/lib/sslbl-certificates.generated.mjs +141 -138
  209. package/lib/static-html-analysis.mjs +117 -146
  210. package/lib/structured-data-identity.mjs +2 -1
  211. package/lib/technology-markup.mjs +100 -0
  212. package/lib/threat-intelligence-manifests.mjs +11 -139
  213. package/lib/threat-intelligence-runtime.mjs +24 -52
  214. package/lib/threat-intelligence-targets.mjs +6 -34
  215. package/lib/threat-intelligence-types.mjs +2 -36
  216. package/lib/typosquat-generator.mjs +29 -16
  217. package/lib/urlhaus-intelligence.mjs +8 -3
  218. package/lib/urlscan-intelligence.mjs +1 -1
  219. package/lib/website-enrichment.mjs +388 -0
  220. package/lib/website-security-posture.mjs +30 -6
  221. package/lib/website-technology.mjs +111 -71
  222. package/lib/whois-authority.mjs +55 -5
  223. package/lib/whois-chain.mjs +9 -2
  224. package/lib/whois-common-formats.mjs +5 -5
  225. package/lib/whois-contracts.mjs +2 -1
  226. package/lib/whois-dialects.mjs +2 -2
  227. package/lib/whois-normalization.mjs +3 -1
  228. package/lib/whois-parser-support.mjs +8 -1
  229. package/lib/whois-parser.mjs +5 -3
  230. package/lib/whois-transport.mjs +23 -7
  231. package/package.json +14 -14
  232. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  233. package/packages/analysis/bounded-json.mjs +330 -0
  234. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  235. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  236. package/packages/analysis/ct-query.mjs +27 -0
  237. package/packages/analysis/ct-response-bounds.mjs +9 -0
  238. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  239. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  240. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  241. package/packages/analysis/opportunity-scoring.mjs +107 -0
  242. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  243. package/packages/analysis/portable-generator.mjs +29 -0
  244. package/packages/analysis/project-metadata.mjs +8 -0
  245. package/packages/analysis/registrable-domain.mjs +56 -0
  246. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  247. package/packages/analysis/risk-scoring.mjs +345 -0
  248. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  249. package/packages/analysis/semantic-version.mjs +54 -0
  250. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  251. package/packages/cases/case-attachment-model.mjs +83 -0
  252. package/packages/cases/case-evidence-links.mjs +135 -0
  253. package/packages/cases/case-evidence-model.mjs +229 -69
  254. package/packages/cases/case-evidence-quality.mjs +5 -0
  255. package/packages/cases/case-export-input.mjs +55 -0
  256. package/packages/cases/case-incident-context.mjs +52 -0
  257. package/packages/cases/case-markdown.mjs +11 -0
  258. package/packages/cases/case-migration-model.mjs +98 -48
  259. package/packages/cases/case-model.mjs +1 -0
  260. package/packages/cases/case-recheck-model.mjs +149 -0
  261. package/packages/cases/case-record-contracts.mjs +1 -1
  262. package/packages/cases/case-record-core.mjs +4 -3
  263. package/packages/cases/case-record-operations.mjs +123 -133
  264. package/packages/cases/case-record-projection.mjs +30 -20
  265. package/packages/cases/case-record-version-input.mjs +19 -0
  266. package/packages/cases/case-report.mjs +114 -67
  267. package/packages/cases/case-requested-evidence.mjs +146 -0
  268. package/packages/cases/case-response-actions.mjs +756 -0
  269. package/packages/cases/case-response-model.mjs +49 -1129
  270. package/packages/cases/case-response-outcomes.mjs +338 -0
  271. package/packages/cases/case-response-packet-render.mjs +157 -0
  272. package/packages/cases/case-response-packet-types.mjs +1 -0
  273. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  274. package/packages/cases/case-response-packet.mjs +58 -593
  275. package/packages/cases/case-response-records.mjs +93 -0
  276. package/packages/cases/case-response-review-inputs.mjs +382 -0
  277. package/packages/cases/case-response-values.mjs +85 -0
  278. package/packages/cases/case-review-package.mjs +45 -0
  279. package/packages/cases/case-selection.mjs +46 -0
  280. package/packages/cases/case-storage-model.mjs +26 -1
  281. package/packages/cases/case-workflow-metadata.mjs +225 -0
  282. package/packages/cases/case-workflow-migration.mjs +43 -0
  283. package/packages/cases/http-summary.mjs +6 -1
  284. package/packages/cases/response-route-freshness.mjs +16 -0
  285. package/packages/comparison/capture-context.mjs +61 -0
  286. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  287. package/packages/comparison/favicon-similarity.mjs +1 -1
  288. package/packages/comparison/page-similarity.mjs +3 -2
  289. package/packages/comparison/relationship-evidence.mjs +103 -14
  290. package/packages/comparison/relationship-provenance.mjs +99 -0
  291. package/packages/contracts/analyst-interchange.mjs +234 -0
  292. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  293. package/packages/contracts/analyst-review-state.mjs +5 -4
  294. package/packages/contracts/case-portability.mjs +370 -114
  295. package/packages/contracts/case-views-contract.mjs +12 -0
  296. package/packages/contracts/case-views.mjs +26 -0
  297. package/packages/contracts/cli-command-semantics.mjs +3 -0
  298. package/packages/contracts/cli-grammar.mjs +6 -0
  299. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  300. package/packages/contracts/cli-lookup.mjs +0 -1
  301. package/packages/contracts/context-review.mjs +24 -0
  302. package/packages/contracts/document-intake.mjs +11 -0
  303. package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
  304. package/packages/contracts/domain-control-manifest.mjs +128 -74
  305. package/packages/contracts/domain-control-monitor.mjs +59 -45
  306. package/packages/contracts/domain-control-review.mjs +200 -121
  307. package/packages/contracts/domain-name.mjs +21 -0
  308. package/packages/contracts/external-observation-interchange.mjs +2 -1
  309. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  310. package/packages/contracts/har-review.mjs +2 -0
  311. package/packages/contracts/http-url.mjs +2 -0
  312. package/packages/contracts/identity-events.mjs +4 -0
  313. package/packages/contracts/investigation-package-limits.mjs +24 -0
  314. package/packages/contracts/investigation-portability.mjs +56 -27
  315. package/packages/contracts/investigation-run.mjs +18 -0
  316. package/packages/contracts/ip-address.mjs +44 -0
  317. package/packages/contracts/lookup-evidence.mjs +35 -0
  318. package/packages/contracts/mail-authentication.mjs +5 -0
  319. package/packages/contracts/message-intake.mjs +31 -0
  320. package/packages/contracts/offline-comparison.mjs +4 -2
  321. package/packages/contracts/page-fingerprints.mjs +15 -0
  322. package/packages/contracts/passive-mail.mjs +10 -0
  323. package/packages/contracts/risk-calibration.mjs +0 -1
  324. package/packages/contracts/schema-compatibility.mjs +1 -1
  325. package/packages/contracts/schema-lifecycle.mjs +83 -153
  326. package/packages/contracts/selected-file-limits.mjs +6 -0
  327. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  328. package/packages/contracts/web-capture.mjs +10 -0
  329. package/packages/contracts/workspace-portability.mjs +219 -238
  330. package/packages/evidence/artifact-integrity.mjs +8 -2
  331. package/packages/evidence/artifact-structure.mjs +105 -0
  332. package/packages/evidence/collection-quality.mjs +67 -0
  333. package/packages/evidence/decision-fact.mjs +4 -1
  334. package/packages/evidence/dns-query-outcome.mjs +72 -0
  335. package/packages/evidence/domain-control-runtime.mjs +155 -27
  336. package/packages/evidence/domain-posture-context.mjs +79 -0
  337. package/packages/evidence/email-recipient.mjs +18 -0
  338. package/packages/evidence/image-regions.mjs +56 -0
  339. package/packages/evidence/latest-observations.mjs +28 -0
  340. package/packages/evidence/lookup-target.mjs +108 -0
  341. package/packages/evidence/observation.mjs +18 -1
  342. package/packages/evidence/passphrase-encryption.mjs +44 -0
  343. package/packages/evidence/png-pixels.mjs +117 -0
  344. package/packages/evidence/public-address-policy.mjs +83 -0
  345. package/packages/evidence/record-identity.mjs +72 -0
  346. package/packages/evidence/registry-dates.mjs +190 -0
  347. package/packages/evidence/retained-file.mjs +69 -0
  348. package/packages/evidence/review-age.mjs +14 -0
  349. package/packages/interchange/bagit.mjs +342 -0
  350. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  351. package/packages/interchange/crc32.mjs +17 -0
  352. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  353. package/packages/interchange/external-findings-import.mjs +93 -61
  354. package/packages/interchange/managed-indicator-export.mjs +35 -0
  355. package/packages/interchange/managed-indicator-set.mjs +210 -0
  356. package/packages/interchange/misp-indicator-export.mjs +93 -0
  357. package/packages/interchange/sharing-policy.mjs +56 -0
  358. package/packages/interchange/stix-indicator-export.mjs +129 -0
  359. package/packages/interchange/web-capture-import.mjs +389 -0
  360. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  361. package/packages/investigation/bulk-mail-exposure.mjs +9 -14
  362. package/packages/investigation/capture-coverage.mjs +57 -0
  363. package/packages/investigation/connector-provenance-review.mjs +100 -0
  364. package/packages/investigation/context-review.mjs +35 -0
  365. package/packages/investigation/document-intake.mjs +81 -0
  366. package/packages/investigation/docx-intake.mjs +133 -0
  367. package/packages/investigation/domain-history-review.mjs +100 -0
  368. package/packages/investigation/domain-transition-review.mjs +47 -0
  369. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  370. package/packages/investigation/har-intake.mjs +68 -0
  371. package/packages/investigation/identity-event-intake.mjs +131 -0
  372. package/packages/investigation/identity-incident-review.mjs +39 -0
  373. package/packages/investigation/incident-sequence-review.mjs +58 -0
  374. package/packages/investigation/intake-report.mjs +19 -0
  375. package/packages/investigation/investigation-bagit.mjs +14 -0
  376. package/packages/investigation/investigation-capsule.mjs +33 -5
  377. package/packages/investigation/investigation-manifest.mjs +200 -0
  378. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  379. package/packages/investigation/investigation-package.mjs +200 -0
  380. package/packages/investigation/link-intake.mjs +179 -0
  381. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  382. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  383. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  384. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  385. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  386. package/packages/investigation/mail-authentication-review.mjs +222 -0
  387. package/packages/investigation/mail-header-identity.mjs +209 -0
  388. package/packages/investigation/message-intake.mjs +195 -0
  389. package/packages/investigation/page-behaviour.mjs +113 -0
  390. package/packages/investigation/pdf-intake.mjs +155 -0
  391. package/packages/investigation/platform-continuity-review.mjs +60 -0
  392. package/packages/investigation/qr-intake.mjs +56 -0
  393. package/packages/investigation/requested-action-hints.mjs +11 -0
  394. package/packages/investigation/selected-input-review.mjs +17 -0
  395. package/packages/investigation/storefront-review.mjs +70 -0
  396. package/packages/monitoring/analyst-review-state.mjs +38 -90
  397. package/packages/workspace/brand-profile-model.mjs +160 -22
  398. package/packages/workspace/bulk-columns.mjs +17 -0
  399. package/packages/workspace/bulk-review-model.mjs +39 -31
  400. package/packages/workspace/bulk-session-model.mjs +112 -27
  401. package/packages/workspace/bulk-sort.mjs +1 -0
  402. package/packages/workspace/campaign-model.mjs +1 -1
  403. package/packages/workspace/case-views.mjs +102 -0
  404. package/packages/workspace/detection-rule-model.mjs +1 -1
  405. package/packages/workspace/hostile-input.mjs +29 -8
  406. package/packages/workspace/investigation-guide.mjs +2 -2
  407. package/packages/workspace/investigation-template-model.mjs +45 -6
  408. package/packages/workspace/page-baseline.mjs +20 -17
  409. package/packages/workspace/relationship-observation-model.mjs +54 -35
  410. package/packages/workspace/shortlist-model.mjs +14 -5
  411. package/packages/workspace/watchlist-history.mjs +30 -14
  412. package/packages/workspace/watchlist-store.mjs +24 -6
  413. package/packages/workspace/website-snapshot-model.mjs +98 -33
  414. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  415. package/packages/workspace/workspace-archive.mjs +90 -25
  416. package/third-party-notices.txt +577 -17
  417. package/lib/hostname.mjs +0 -38
  418. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,11 +1,11 @@
1
1
  // Generated by tools/unicode-confusable-audit.mts from the pinned Unicode
2
2
  // UTS #39 data file. Do not edit this projection by hand.
3
- export const GENERATED_CONFUSABLE_MAPPING_VERSION = "tr39-17.0.0-bounded-ascii-v3";
3
+ export const GENERATED_CONFUSABLE_MAPPING_VERSION = "tr39-18.0.0-bounded-ascii-v3";
4
4
  export const GENERATED_CONFUSABLE_SOURCE = Object.freeze({
5
5
  "standard": "Unicode UTS #39",
6
- "unicodeVersion": "17.0.0",
7
- "url": "https://www.unicode.org/Public/17.0.0/security/confusables.txt",
8
- "sha256": "091c7f82fc39ef208faf8f94d29c244de99254675e09de163160c810d13ef22a",
6
+ "unicodeVersion": "18.0.0",
7
+ "url": "https://www.unicode.org/Public/18.0.0/security/confusables.txt",
8
+ "sha256": "6ed3ee967c9dfdf6677d563c9985182fbc50a2efb7d6059cd57b2e2ce18f5b92",
9
9
  "license": "Unicode-3.0"
10
10
  });
11
11
  export const GENERATED_CONFUSABLE_POLICY = Object.freeze({
@@ -24,33 +24,33 @@ export const GENERATED_CONFUSABLE_POLICY = Object.freeze({
24
24
  "totalMappings": 312
25
25
  });
26
26
  export const GENERATED_CONFUSABLE_STATS = Object.freeze({
27
- "sourceBytes": 745683,
28
- "sourceLines": 9995,
29
- "parsedMappings": 6565,
30
- "eligibleMappings": 232,
31
- "projectedMappings": 111,
32
- "generationMappings": 101,
27
+ "sourceBytes": 763128,
28
+ "sourceLines": 9863,
29
+ "parsedMappings": 6712,
30
+ "eligibleMappings": 255,
31
+ "projectedMappings": 126,
32
+ "generationMappings": 115,
33
33
  "rejectedMalformedLines": 0,
34
- "rejectedOutsidePolicy": 5695,
34
+ "rejectedOutsidePolicy": 5780,
35
35
  "skeletonCapHits": 0,
36
- "generationCapHits": 3
36
+ "generationCapHits": 4
37
37
  });
38
38
  export const GENERATED_CONFUSABLE_GROUPS = Object.freeze({
39
- a: "аαɑ",
40
- b: "ьƄ",
41
- c: "сϲⅽᴄⲥ𐐽",
42
- d: "ԁⅾꓒ",
39
+ a: "аαɑꭤ",
40
+ b: "ьƄ𝈇",
41
+ c: "сϲⅽᴄⲥ𐐽ᲃ",
42
+ d: "ԁⅾꓒ𐆋",
43
43
  e: "еεҽꬲ",
44
- f: "ϝꞙƒẝꬵք",
44
+ f: "ϝꞙƒʄẝꬵ𐅾ք",
45
45
  g: "ɡƍᶃց",
46
46
  h: "һհ",
47
47
  i: "іιıɪɩⲓꙇւ",
48
- j: "јϳ",
48
+ j: "јϳȷյ",
49
49
  k: "κк",
50
- l: "ӏⅼǀꓲ",
51
- m: "мⅿ",
50
+ l: "ӏⅼǀꓲꞁꟾ",
51
+ m: "мⅿՠ",
52
52
  n: "ոռ",
53
- o: "оοօᴏⲟᴑꬽσϭ𐐬",
53
+ o: "оοօᴏⲟᴑꬽᲂσϭ𐐬",
54
54
  p: "рρϱⲣþƿϸⳏ",
55
55
  q: "ԛգզ",
56
56
  r: "гꭇꭈᴦⲅ",
@@ -58,25 +58,25 @@ export const GENERATED_CONFUSABLE_GROUPS = Object.freeze({
58
58
  t: "тτ",
59
59
  u: "υսᴜꭎʋꞟꭒ",
60
60
  v: "ѵνᴠ",
61
- w: "ԝաᴡшɯѡⲽ",
61
+ w: "ԝաᴡшɯꟺѡⲽ",
62
62
  x: "хχ",
63
63
  y: "уүʏγⲩɣᶌỿꭚ",
64
- z: "ᴢ",
64
+ z: "ᴢⱬⲍ",
65
65
  });
66
66
  export const GENERATED_GENERATION_CONFUSABLE_GROUPS = Object.freeze({
67
- a: "аαɑ",
68
- b: "ь",
69
- c: "сᴄⲥ𐐽",
70
- d: "ԁꓒ",
67
+ a: "аαɑꭤ",
68
+ b: "ь𝈇",
69
+ c: "сᴄⲥ𐐽ᲃ",
70
+ d: "ԁꓒ𐆋",
71
71
  e: "еεҽꬲ",
72
- f: "ϝꞙƒẝꬵք",
72
+ f: "ϝꞙƒʄẝꬵ𐅾ք",
73
73
  g: "ɡƍᶃց",
74
74
  h: "һհ",
75
75
  i: "іιıɪɩⲓꙇւ",
76
- j: "јϳ",
76
+ j: "јϳȷյ",
77
77
  k: "κк",
78
- l: "ӏǀꓲ",
79
- m: "м",
78
+ l: "ӏǀꓲꞁꟾ",
79
+ m: "мՠ",
80
80
  n: "ոռ",
81
81
  o: "оοօᴏⲟᴑꬽσ",
82
82
  p: "рρⲣþƿϸⳏ",
@@ -86,8 +86,8 @@ export const GENERATED_GENERATION_CONFUSABLE_GROUPS = Object.freeze({
86
86
  t: "тτ",
87
87
  u: "υսᴜꭎʋꞟꭒ",
88
88
  v: "ѵνᴠ",
89
- w: "ԝաᴡшɯѡⲽ",
89
+ w: "ԝաᴡшɯꟺѡⲽ",
90
90
  x: "хχ",
91
91
  y: "уүʏγⲩɣᶌỿ",
92
- z: "ᴢ",
92
+ z: "ᴢⱬⲍ",
93
93
  });
@@ -43,13 +43,8 @@ const HAS_CONTROL_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignora
43
43
  // scoped to resource tags, not every <a href> - an outbound link to the
44
44
  // real site is normal on all sorts of pages; a resource pulled live from it
45
45
  // during page load is not.
46
- const ASSET_TAG_RE = /<(?:img|script|link)\b[^>]*?\b(?:src|href)\s*=\s*["']([^"']+)["'][^>]*>/gi;
47
- const ABSOLUTE_URL_HOST_RE = /^(?:https?:)?\/\/([^/]+)/i;
48
- const IDENTITY_TAG_RE = /<(html|link|meta|form)\b[^>]{0,4096}>/gi;
49
- const OVERSIZED_IDENTITY_TAG_RE = /<(?:html|link|meta|form)\b[^>]{4097}/i;
50
- const RELATIONSHIP_TAG_RE = /<(a|img|script|link|iframe|frame|source|video|audio|object|embed)\b[^>]{0,4096}>/gi;
51
- const OVERSIZED_RELATIONSHIP_TAG_RE = /<(?:a|img|script|link|iframe|frame|source|video|audio|object|embed)\b[^>]{4097}/i;
52
- const ATTRIBUTE_RE = /([^\s"'<>\/=]+)(?:\s*=\s*(?:"([^"]*)"|'([^']*)'|([^\s"'=<>`]+)))?/g;
46
+ const IDENTITY_TAGS = new Set(['html', 'link', 'meta', 'form']);
47
+ const RELATIONSHIP_TAGS = new Set(['a', 'img', 'script', 'link', 'iframe', 'frame', 'source', 'video', 'audio', 'object', 'embed']);
53
48
  const LANGUAGE_TAG_RE = /^[a-z]{2,8}(?:-[a-z0-9]{1,8})*$/i;
54
49
  const HOSTNAME_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])?$/i;
55
50
  const RESOURCE_LINK_RELS = new Set(['stylesheet', 'icon', 'preload', 'prefetch', 'modulepreload', 'manifest']);
@@ -57,9 +52,6 @@ const RISKY_DOWNLOAD_EXTENSIONS = new Set([
57
52
  '7z', 'apk', 'bat', 'cmd', 'dmg', 'docm', 'exe', 'img', 'iso', 'jar', 'js',
58
53
  'msi', 'pkg', 'ps1', 'rar', 'scr', 'vbs', 'xlsm', 'zip',
59
54
  ]);
60
- const HTML_COMMENT_RE = /<!--[\s\S]*?(?:-->|$)/g;
61
- const RAW_TEXT_BLOCK_RE = /<(script|style|textarea|template)\b([^>]*)>[\s\S]*?<\/\1\s*>/gi;
62
- const NON_EXECUTING_RAW_TEXT_BLOCK_RE = /<(style|textarea|template)\b[^>]*>[\s\S]*?<\/\1\s*>/gi;
63
55
  function stripWwwPrefix(host) {
64
56
  return host.toLowerCase().replace(/^www\./, '');
65
57
  }
@@ -72,24 +64,19 @@ function boundedHtmlText(value, maxLength, ellipsis = false) {
72
64
  return null;
73
65
  if (text.length <= maxLength)
74
66
  return text;
75
- return ellipsis ? `${text.slice(0, maxLength)}…` : text.slice(0, maxLength);
67
+ return ellipsis ? `${text.slice(0, Math.max(0, maxLength - 1))}…` : text.slice(0, maxLength);
76
68
  }
77
- function extractExternalAssetHosts(html, ownDomain) {
69
+ function extractExternalAssetHosts(analysis, ownDomain, documentUrl) {
78
70
  const ownHost = stripWwwPrefix(ownDomain);
79
71
  const hosts = new Set();
80
- let match;
81
- ASSET_TAG_RE.lastIndex = 0;
82
- while ((match = ASSET_TAG_RE.exec(html))) {
83
- const resourceUrl = match[1];
84
- if (!resourceUrl)
72
+ for (const element of analysis.elements) {
73
+ if (!element.html || !['img', 'script', 'link'].includes(element.name))
85
74
  continue;
86
- const hostMatch = resourceUrl.match(ABSOLUTE_URL_HOST_RE);
87
- if (!hostMatch)
75
+ const attribute = element.attributes.find((attribute) => attribute.name === (element.name === 'link' ? 'href' : 'src'));
76
+ const resource = normalizeIdentityUrl(attribute?.value, analysis.effectiveBaseUrl ?? documentUrl);
77
+ if (!resource)
88
78
  continue;
89
- const matchedHost = hostMatch[1];
90
- if (!matchedHost || HAS_CONTROL_CHARACTER_RE.test(matchedHost))
91
- continue;
92
- const host = stripWwwPrefix(matchedHost);
79
+ const host = stripWwwPrefix(new URL(resource.url).hostname);
93
80
  if (host && host !== ownHost)
94
81
  hosts.add(host);
95
82
  if (hosts.size >= MAX_EXTERNAL_ASSET_HOSTS)
@@ -97,32 +84,6 @@ function extractExternalAssetHosts(html, ownDomain) {
97
84
  }
98
85
  return [...hosts];
99
86
  }
100
- function parseAttributes(tag) {
101
- const start = tag.search(/\s/);
102
- if (start === -1)
103
- return new Map();
104
- const attributes = new Map();
105
- ATTRIBUTE_RE.lastIndex = 0;
106
- let match;
107
- while ((match = ATTRIBUTE_RE.exec(tag.slice(start)))) {
108
- const name = (match[1] ?? '').toLowerCase();
109
- if (!name)
110
- continue;
111
- if (!attributes.has(name))
112
- attributes.set(name, match[2] ?? match[3] ?? match[4] ?? '');
113
- }
114
- return attributes;
115
- }
116
- function markupForTagParsing(html) {
117
- return html
118
- .replace(HTML_COMMENT_RE, ' ')
119
- .replace(RAW_TEXT_BLOCK_RE, '<$1$2>');
120
- }
121
- function markupForTrackingIdentifiers(html) {
122
- return html
123
- .replace(HTML_COMMENT_RE, ' ')
124
- .replace(NON_EXECUTING_RAW_TEXT_BLOCK_RE, ' ');
125
- }
126
87
  function normalizeLanguage(value) {
127
88
  if (typeof value !== 'string' || value.length > 35 || HAS_CONTROL_CHARACTER_RE.test(value))
128
89
  return null;
@@ -295,10 +256,9 @@ function trackingIdentifiers(html) {
295
256
  truncated,
296
257
  };
297
258
  }
298
- function extractPageRelationships(html, domain, options = {}) {
259
+ function extractPageRelationships(analysis, domain, options = {}) {
299
260
  const documentUrl = resolvedBaseUrl(domain, options.baseUrl);
300
261
  const baseUrl = resolvedBaseUrl(domain, options.effectiveBaseUrl ?? documentUrl);
301
- const markup = typeof options.tagMarkup === 'string' ? options.tagMarkup : markupForTagParsing(html);
302
262
  const baseOrigin = options.documentOrigin ?? new URL(documentUrl).origin;
303
263
  const resourceKeys = new Set();
304
264
  const resourceOrigins = new Set();
@@ -312,25 +272,27 @@ function extractPageRelationships(html, domain, options = {}) {
312
272
  let riskyDownloadCount = 0;
313
273
  let tagsExamined = 0;
314
274
  let discardedUrls = 0;
315
- let tagLimitReached = OVERSIZED_RELATIONSHIP_TAG_RE.test(markup);
275
+ let tagLimitReached = analysis.inputLimitReached || analysis.tagLimitReached;
316
276
  let resourceOriginLimitReached = false;
317
277
  let embeddedOriginLimitReached = false;
318
278
  let contactDomainLimitReached = false;
319
279
  let downloadOriginLimitReached = false;
320
280
  let downloadFileTypeLimitReached = false;
321
281
  let perTagLimitReached = false;
322
- let match;
323
- RELATIONSHIP_TAG_RE.lastIndex = 0;
324
- while ((match = RELATIONSHIP_TAG_RE.exec(markup))) {
282
+ let resourceCountLimitReached = false;
283
+ let resourceIdentityTruncated = false;
284
+ for (const element of analysis.elements) {
285
+ if (!element.html || !RELATIONSHIP_TAGS.has(element.name))
286
+ continue;
325
287
  if (tagsExamined >= MAX_RESOURCE_TAGS) {
326
288
  tagLimitReached = true;
327
289
  break;
328
290
  }
329
291
  tagsExamined += 1;
330
- const tagName = (match[1] ?? '').toLowerCase();
331
- if (!tagName)
332
- continue;
333
- const attributes = parseAttributes(match[0]);
292
+ const tagName = element.name;
293
+ const attributes = new Map(element.attributes.map(({ name, value }) => [name, value]));
294
+ if (element.attributesTruncated)
295
+ tagLimitReached = true;
334
296
  if (tagName === 'a') {
335
297
  const href = attributes.get('href');
336
298
  const contacts = mailtoDomains(href);
@@ -376,8 +338,14 @@ function extractPageRelationships(html, domain, options = {}) {
376
338
  discardedUrls += 1;
377
339
  continue;
378
340
  }
341
+ if (normalized.pathTruncated)
342
+ resourceIdentityTruncated = true;
379
343
  const key = `${reference.type}:${normalized.url}`;
380
344
  if (!resourceKeys.has(key)) {
345
+ if (resourceKeys.size >= MAX_RESOURCE_TAGS) {
346
+ resourceCountLimitReached = true;
347
+ continue;
348
+ }
381
349
  resourceKeys.add(key);
382
350
  byType[reference.type] += 1;
383
351
  }
@@ -389,10 +357,15 @@ function extractPageRelationships(html, domain, options = {}) {
389
357
  embeddedOriginLimitReached = true;
390
358
  }
391
359
  }
392
- const tracking = trackingIdentifiers(markupForTrackingIdentifiers(html));
360
+ const tracking = trackingIdentifiers([
361
+ ...analysis.elements.filter((element) => element.html).flatMap((element) => element.attributes.map((attribute) => attribute.value)),
362
+ ...analysis.scripts.map((script) => script.inlineContent),
363
+ ].join('\n'));
364
+ if (analysis.scriptLimitReached || analysis.inlineLimitReached)
365
+ tracking.truncated = true;
393
366
  const truncated = tagLimitReached || resourceOriginLimitReached || embeddedOriginLimitReached
394
367
  || contactDomainLimitReached || downloadOriginLimitReached || downloadFileTypeLimitReached
395
- || perTagLimitReached || tracking.truncated;
368
+ || perTagLimitReached || resourceCountLimitReached || resourceIdentityTruncated || tracking.truncated;
396
369
  const limitations = [];
397
370
  if (tagLimitReached)
398
371
  limitations.push(`Resource parsing reached the ${MAX_RESOURCE_TAGS}-tag or ${MAX_IDENTITY_TAG_LENGTH}-character tag limit.`);
@@ -408,6 +381,10 @@ function extractPageRelationships(html, domain, options = {}) {
408
381
  limitations.push(`Only the first ${MAX_DOWNLOAD_FILE_TYPES} risky download file types were retained.`);
409
382
  if (perTagLimitReached)
410
383
  limitations.push(`Some srcset URL candidates could not be safely enumerated within the ${MAX_URLS_PER_TAG}-candidate per-tag boundary.`);
384
+ if (resourceCountLimitReached)
385
+ limitations.push(`Only the first ${MAX_RESOURCE_TAGS} distinct resource references were examined.`);
386
+ if (resourceIdentityTruncated)
387
+ limitations.push('Overlong resource paths were reduced to origins; distinct resource counts may be incomplete.');
411
388
  if (tracking.truncated)
412
389
  limitations.push(`Only the first ${MAX_TRACKING_IDENTIFIERS} tracking identifiers were retained.`);
413
390
  return {
@@ -415,7 +392,7 @@ function extractPageRelationships(html, domain, options = {}) {
415
392
  count: resourceKeys.size,
416
393
  byType,
417
394
  externalOrigins: [...resourceOrigins].sort(),
418
- truncated: tagLimitReached || resourceOriginLimitReached || perTagLimitReached,
395
+ truncated: tagLimitReached || resourceOriginLimitReached || perTagLimitReached || resourceCountLimitReached || resourceIdentityTruncated,
419
396
  },
420
397
  embeddedOrigins: [...embeddedOrigins].sort(),
421
398
  contactDomains: [...contactDomains].sort(),
@@ -445,12 +422,9 @@ function metaRefreshTarget(content) {
445
422
  }
446
423
  function extractPageIdentity(html, domain, options = {}) {
447
424
  const documentUrl = resolvedBaseUrl(domain, options.baseUrl);
448
- const baseAnalysis = options.effectiveBaseUrl === undefined
449
- ? analyzeStaticHtml(html, { baseUrl: documentUrl })
450
- : null;
451
- const baseUrl = resolvedBaseUrl(domain, options.effectiveBaseUrl ?? baseAnalysis?.effectiveBaseUrl ?? documentUrl);
452
- const baseHrefState = options.baseHrefState ?? baseAnalysis?.baseHrefState ?? 'absent';
453
- const tagMarkup = markupForTagParsing(html);
425
+ const analysis = options.htmlAnalysis ?? analyzeStaticHtml(html, { baseUrl: documentUrl, includeVisibleText: true });
426
+ const baseUrl = resolvedBaseUrl(domain, options.effectiveBaseUrl ?? analysis.effectiveBaseUrl ?? documentUrl);
427
+ const baseHrefState = options.baseHrefState ?? analysis.baseHrefState;
454
428
  const parsedDocument = new URL(documentUrl);
455
429
  const baseOrigin = options.documentOrigin ?? parsedDocument.origin;
456
430
  const baseUsesHttps = parsedDocument.protocol === 'https:';
@@ -469,19 +443,19 @@ function extractPageIdentity(html, domain, options = {}) {
469
443
  let discardedUrls = 0;
470
444
  let formLimitReached = false;
471
445
  let originLimitReached = false;
472
- let tagLimitReached = OVERSIZED_IDENTITY_TAG_RE.test(tagMarkup);
473
- let match;
474
- IDENTITY_TAG_RE.lastIndex = 0;
475
- while ((match = IDENTITY_TAG_RE.exec(tagMarkup))) {
446
+ let tagLimitReached = analysis.inputLimitReached || analysis.tagLimitReached;
447
+ for (const element of analysis.elements) {
448
+ if (!element.html || !IDENTITY_TAGS.has(element.name))
449
+ continue;
476
450
  if (tagsExamined >= MAX_IDENTITY_TAGS) {
477
451
  tagLimitReached = true;
478
452
  break;
479
453
  }
480
454
  tagsExamined += 1;
481
- const tagName = (match[1] ?? '').toLowerCase();
482
- if (!tagName)
483
- continue;
484
- const attributes = parseAttributes(match[0]);
455
+ const tagName = element.name;
456
+ const attributes = new Map(element.attributes.map(({ name, value }) => [name, value]));
457
+ if (element.attributesTruncated)
458
+ tagLimitReached = true;
485
459
  if (tagName === 'html' && documentLanguage === null) {
486
460
  documentLanguage = normalizeLanguage(attributes.get('lang'));
487
461
  continue;
@@ -551,12 +525,10 @@ function extractPageIdentity(html, domain, options = {}) {
551
525
  const queryOmitted = [canonical, metaRefresh, openGraphUrl].some((item) => item?.queryOmitted);
552
526
  const pathTruncated = [canonical, metaRefresh, openGraphUrl].some((item) => item?.pathTruncated);
553
527
  const sourceTruncated = options.sourceTruncated === true;
554
- const relationships = extractPageRelationships(html, domain, { baseUrl: documentUrl, effectiveBaseUrl: baseUrl, documentOrigin: baseOrigin, tagMarkup });
528
+ const relationships = extractPageRelationships(analysis, domain, { baseUrl: documentUrl, effectiveBaseUrl: baseUrl, documentOrigin: baseOrigin });
555
529
  const fingerprints = createPageFingerprints(html, {
556
- // Retained fingerprint algorithms pre-date document-base handling. Keep
557
- // their response-URL normalization stable until a versioned fingerprint
558
- // migration is intentionally introduced.
559
530
  baseUrl: documentUrl,
531
+ htmlAnalysis: analysis,
560
532
  exactBodyHash: options.exactBodyHash,
561
533
  sourceTruncated,
562
534
  resources: relationships.resources,
@@ -696,9 +668,34 @@ function buildPagePublicationMetadata(analysis, sourceTruncated) {
696
668
  },
697
669
  };
698
670
  }
699
- function extractHtmlSignals(html, domain, options = {}) {
671
+ function domainSaleLandingPage(analysis, domain) {
672
+ if (analysis.inputLimitReached || analysis.tagLimitReached || analysis.visibleTextLimitReached)
673
+ return false;
674
+ const title = analysis.title?.trim().toLowerCase() ?? '';
675
+ const content = analysis.visibleText.trim().toLowerCase();
676
+ const saleSuffix = /^\s+(?:(?:is|may be)\s+)?(?:for sale|available for purchase|available for lease)(?:[.!?:\s]|$)/u;
677
+ const explicitSale = (value) => {
678
+ const genericSubject = /^(?:(?:this|the) domain(?: name)?|domain(?: name)?)/u.exec(value)?.[0];
679
+ return (genericSubject !== undefined && saleSuffix.test(value.slice(genericSubject.length)))
680
+ || (value.startsWith(domain) && saleSuffix.test(value.slice(domain.length)));
681
+ };
682
+ const explicitPurchase = /^(?:buy|purchase|own|bid on|inquire about) (?:this|the) domain(?: name)?(?:[.!?:\s]|$)/u;
683
+ // A short, directly worded landing page is sufficient. Long articles need
684
+ // corroborating page-title and transaction-control evidence; a generic
685
+ // commerce CTA or quoted example cannot classify the domain for sale.
686
+ if (content.length <= 240 && (explicitSale(content) || explicitPurchase.test(content)))
687
+ return true;
688
+ if (!explicitSale(title) && !explicitPurchase.test(title))
689
+ return false;
690
+ if (!/(?:this domain(?: name)? (?:is )?for sale|buy this domain|purchase this domain|domain name for sale)/u.test(content))
691
+ return false;
692
+ return analysis.elements.some((element) => element.html
693
+ && ['form', 'button', 'a'].includes(element.name));
694
+ }
695
+ async function extractHtmlSignals(html, domain, options = {}) {
696
+ options.signal?.throwIfAborted();
700
697
  const documentUrl = resolvedBaseUrl(domain, options.baseUrl);
701
- const htmlAnalysis = analyzeStaticHtml(html, { baseUrl: documentUrl, includeVisibleText: true });
698
+ const htmlAnalysis = options.htmlAnalysis ?? analyzeStaticHtml(html, { baseUrl: documentUrl, includeVisibleText: true });
702
699
  const effectiveBaseUrl = htmlAnalysis.effectiveBaseUrl ?? documentUrl;
703
700
  const documentOrigin = new URL(documentUrl).origin;
704
701
  const pageIdentity = options.includePageIdentity === false ? null : extractPageIdentity(html, domain, {
@@ -707,6 +704,7 @@ function extractHtmlSignals(html, domain, options = {}) {
707
704
  effectiveBaseUrl,
708
705
  documentOrigin,
709
706
  baseHrefState: htmlAnalysis.baseHrefState,
707
+ htmlAnalysis,
710
708
  });
711
709
  const includeCredentialSurfaceProfile = pageIdentity && options.includeCredentialSurfaceProfile === true;
712
710
  const includeStructuredDataIdentity = pageIdentity && options.includeStructuredDataIdentity !== false;
@@ -719,14 +717,17 @@ function extractHtmlSignals(html, domain, options = {}) {
719
717
  }
720
718
  : pageIdentity;
721
719
  const pageLanguageSignal = detectPageLanguageSignal(html, pageIdentity?.documentLanguage, htmlAnalysis);
720
+ const domainSaleSignal = options.sourceTruncated !== true && domainSaleLandingPage(htmlAnalysis, domain)
721
+ ? 'explicit domain-sale landing-page content' : null;
722
722
  return {
723
+ domainSaleSignal,
723
724
  pageTitle: htmlAnalysis.title,
724
725
  hasPasswordField: htmlAnalysis.forms.categories.password > 0,
725
726
  phishingLanguageMatch: pageLanguageSignal?.label ?? null,
726
727
  hasExternalFormAction: pageIdentity
727
728
  ? pageIdentity.forms.externalActionOrigins.length > 0
728
729
  : null,
729
- externalAssetHosts: extractExternalAssetHosts(html, domain),
730
+ externalAssetHosts: extractExternalAssetHosts(htmlAnalysis, domain, documentUrl),
730
731
  cspMetaPolicy: analyzeCspMetaPolicies(htmlAnalysis.cspMetaPolicies, htmlAnalysis.cspMetaLimitReached),
731
732
  pageIdentity: pageIdentityOutput,
732
733
  credentialSurfaceProfile: includeCredentialSurfaceProfile && htmlAnalysis ? analyzeCredentialSurfaceProfile({
@@ -740,9 +741,10 @@ function extractHtmlSignals(html, domain, options = {}) {
740
741
  observedAt: options.observedAt,
741
742
  sourceTruncated: options.sourceTruncated,
742
743
  }) : null,
743
- technologyProfile: includeTechnologyProfile && htmlAnalysis ? analyzeWebsiteTechnology({
744
+ technologyProfile: includeTechnologyProfile && htmlAnalysis ? await analyzeWebsiteTechnology({
744
745
  htmlAnalysis,
745
746
  generator: pageIdentity.generator,
747
+ ...(options.signal ? { signal: options.signal } : {}),
746
748
  httpServer: options.httpServer,
747
749
  responseHeaders: options.responseHeaders,
748
750
  resourceOrigins: pageIdentity.resources.externalOrigins,
@@ -754,7 +756,7 @@ function extractHtmlSignals(html, domain, options = {}) {
754
756
  pageRoleProfile: includeDerivedPageProfiles && htmlAnalysis ? analyzePageRole({
755
757
  htmlAnalysis,
756
758
  pageTitle: htmlAnalysis.title,
757
- activityStatus: options.activityStatus,
759
+ activityStatus: domainSaleSignal ? 'parked' : options.activityStatus,
758
760
  observedAt: options.observedAt,
759
761
  sourceTruncated: options.sourceTruncated,
760
762
  }) : null,
@@ -1,7 +1,2 @@
1
- // Browser-safe bounds for the rich HTTP evidence contract and its compact
2
- // browser-local projection. Both producers and consumers import these values
3
- // so valid retained evidence is not rejected by a stale projection.
4
- export const MAX_HTTP_EVIDENCE_REDIRECTS = 5;
5
- export const MAX_HTTP_ATTEMPTS = 2;
6
- export const MAX_HTTP_PROVENANCE_URL = 2_048;
7
- export const MAX_HTTP_ERROR_LENGTH = 180;
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/http-evidence-bounds.mjs";
@@ -3,8 +3,21 @@
3
3
  // converts the shared safe-fetch trace and selected response metadata into an
4
4
  // additive evidence object suitable for API responses and exports.
5
5
  import { createObservation } from "../packages/evidence/observation.mjs";
6
- import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, MAX_HTTP_PROVENANCE_URL, } from "./http-evidence-bounds.mjs";
6
+ import { MAX_OUTBOUND_HTTP_URL_CHARACTERS } from "../packages/contracts/http-url.mjs";
7
+ import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, MAX_HTTP_PROVENANCE_URL, } from "../packages/analysis/http-evidence-bounds.mjs";
7
8
  import { HTTP_DELIVERY_LIMITATIONS, HTTP_DELIVERY_METADATA_VERSION, MAX_HTTP_CACHE_SECONDS, MAX_HTTP_DELIVERY_HEADER_BYTES, MAX_HTTP_DELIVERY_TOKENS, } from "./homepage-metadata-contract.mjs";
9
+ /** Publish delivery metadata only at its owning response boundary. */
10
+ export function withoutHttpDeliveryMetadata(value) {
11
+ if (!value || typeof value !== 'object' || Array.isArray(value))
12
+ return value;
13
+ const output = { ...value };
14
+ if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
15
+ const response = { ...output.response };
16
+ delete response.deliveryMetadata;
17
+ output.response = response;
18
+ }
19
+ return output;
20
+ }
8
21
  const MAX_HTTP_REDIRECTS = MAX_HTTP_EVIDENCE_REDIRECTS;
9
22
  const MAX_HTTP_HEADER_LENGTH = 1024;
10
23
  const MAX_HTTP_DELIVERY_HEADER_LENGTH = MAX_HTTP_DELIVERY_HEADER_BYTES;
@@ -25,7 +38,7 @@ function boundedString(value, maxLength) {
25
38
  return normalized ? normalized.slice(0, maxLength) : null;
26
39
  }
27
40
  function normalizeProvenanceUrl(value) {
28
- if (typeof value !== 'string' || value.length === 0 || value.length > 4096)
41
+ if (typeof value !== 'string' || value.length === 0 || value.length > MAX_OUTBOUND_HTTP_URL_CHARACTERS)
29
42
  return null;
30
43
  try {
31
44
  const parsed = new URL(value);
@@ -165,14 +178,14 @@ function headerValidity(header, validator) {
165
178
  return { present: true, valid: null };
166
179
  return { present: true, valid: header.state === 'observed' && Boolean(header.value && validator(header.value)) };
167
180
  }
168
- function validHttpDate(value) {
181
+ function validHttpDate(value, observedAt) {
169
182
  const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
170
183
  const weekdays = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
171
184
  const longWeekdays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
172
- const time = '([01]\\d|2[0-3]):([0-5]\\d):([0-5]\\d)';
185
+ const time = '([01]\\d|2[0-3]):([0-5]\\d):([0-5]\\d|60)';
173
186
  const imfFixdate = value.match(new RegExp(`^(${weekdays.join('|')}), (0[1-9]|[12]\\d|3[01]) (${months.join('|')}) (\\d{4}) ${time} GMT$`, 'u'));
174
187
  const rfc850 = value.match(new RegExp(`^(${longWeekdays.join('|')}), (0[1-9]|[12]\\d|3[01])-(${months.join('|')})-(\\d{2}) ${time} GMT$`, 'u'));
175
- const asctime = value.match(new RegExp(`^(${weekdays.join('|')}) (${months.join('|')}) ( {2}[1-9]| [12]\\d| 3[01]) ${time} (\\d{4})$`, 'u'));
188
+ const asctime = value.match(new RegExp(`^(${weekdays.join('|')}) (${months.join('|')}) ( [1-9]|0[1-9]|[12]\\d|3[01]) ${time} (\\d{4})$`, 'u'));
176
189
  let weekday;
177
190
  let day;
178
191
  let month;
@@ -196,10 +209,19 @@ function validHttpDate(value) {
196
209
  day = Number(rfc850[2]);
197
210
  month = months.indexOf(rfc850[3]);
198
211
  const shortYear = Number(rfc850[4]);
199
- year = shortYear >= 70 ? 1900 + shortYear : 2000 + shortYear;
212
+ // RFC 9110's rolling window is relative to receipt, not a later review
213
+ // or the host's timezone. The observation supplies that stable reference.
214
+ const futureBoundary = new Date(observedAt);
215
+ futureBoundary.setUTCFullYear(futureBoundary.getUTCFullYear() + 50);
216
+ year = Math.floor(futureBoundary.getUTCFullYear() / 100) * 100 + shortYear;
200
217
  hour = Number(rfc850[5]);
201
218
  minute = Number(rfc850[6]);
202
219
  second = Number(rfc850[7]);
220
+ const candidate = new Date(0);
221
+ candidate.setUTCFullYear(year, month, day);
222
+ candidate.setUTCHours(hour, minute, Math.min(second, 59), 0);
223
+ if (candidate.getTime() > futureBoundary.getTime())
224
+ year -= 100;
203
225
  expectedWeekdays = longWeekdays;
204
226
  }
205
227
  else if (asctime) {
@@ -218,13 +240,15 @@ function validHttpDate(value) {
218
240
  return false;
219
241
  const parsed = new Date(0);
220
242
  parsed.setUTCFullYear(year, month, day);
221
- parsed.setUTCHours(hour, minute, second, 0);
243
+ // A leap-second spelling must not roll into the following calendar day
244
+ // while validating the declared date and weekday.
245
+ parsed.setUTCHours(hour, minute, Math.min(second, 59), 0);
222
246
  return parsed.getUTCFullYear() === year
223
247
  && parsed.getUTCMonth() === month
224
248
  && parsed.getUTCDate() === day
225
249
  && expectedWeekdays[parsed.getUTCDay()] === weekday;
226
250
  }
227
- function parseCachePolicy(headers) {
251
+ function parseCachePolicy(headers, observedAt) {
228
252
  const cacheControl = deliveryHeader(headers, 'cache-control');
229
253
  const age = deliveryHeader(headers, 'age', 64);
230
254
  const etagHeader = deliveryHeader(headers, 'etag');
@@ -329,8 +353,8 @@ function parseCachePolicy(headers) {
329
353
  if (age.state === 'observed' && ageSeconds === null)
330
354
  malformed = true;
331
355
  const etag = headerValidity(etagHeader, (value) => /^(?:W\/)?"[\x21\x23-\x7e]*"$/u.test(value));
332
- const lastModified = headerValidity(lastModifiedHeader, validHttpDate);
333
- const expires = headerValidity(expiresHeader, validHttpDate);
356
+ const lastModified = headerValidity(lastModifiedHeader, (value) => validHttpDate(value, observedAt));
357
+ const expires = headerValidity(expiresHeader, (value) => validHttpDate(value, observedAt));
334
358
  malformed = malformed || [etag, lastModified, expires].some((item) => item.present && item.valid === false);
335
359
  const headersObserved = [cacheControl, age, etagHeader, lastModifiedHeader, expiresHeader]
336
360
  .some((header) => header.state !== 'not_observed');
@@ -362,9 +386,9 @@ function parseCachePolicy(headers) {
362
386
  truncated: partial,
363
387
  };
364
388
  }
365
- function buildDeliveryMetadata(headers) {
389
+ function buildDeliveryMetadata(headers, observedAt) {
366
390
  const contentEncoding = parseContentEncoding(headers);
367
- const cacheResult = parseCachePolicy(headers);
391
+ const cacheResult = parseCachePolicy(headers, observedAt);
368
392
  const cachePolicy = cacheResult.value;
369
393
  const incomplete = [contentEncoding.status, cachePolicy.status].some((status) => ['partial', 'malformed'].includes(status));
370
394
  const truncated = contentEncoding.status === 'partial' || cacheResult.truncated;
@@ -466,21 +490,22 @@ function buildHttpObservation(detail, options = {}) {
466
490
  ...(Array.isArray(options.previousAttempts) ? options.previousAttempts : []),
467
491
  { url: detail && detail.requestedUrl, httpStatus: status },
468
492
  ]);
493
+ const observation = createObservation({
494
+ status: incomplete ? 'partial' : 'success',
495
+ observedAt: options.observedAt,
496
+ scanMode: 'deep',
497
+ source: 'http',
498
+ durationMs: detail && detail.durationMs,
499
+ complete: !incomplete,
500
+ truncated: incomplete,
501
+ limitations,
502
+ diagnostics: {
503
+ redirectCount: redirects.length,
504
+ httpStatus: status,
505
+ },
506
+ });
469
507
  return {
470
- ...createObservation({
471
- status: incomplete ? 'partial' : 'success',
472
- observedAt: options.observedAt,
473
- scanMode: 'deep',
474
- source: 'http',
475
- durationMs: detail && detail.durationMs,
476
- complete: !incomplete,
477
- truncated: incomplete,
478
- limitations,
479
- diagnostics: {
480
- redirectCount: redirects.length,
481
- httpStatus: status,
482
- },
483
- }),
508
+ ...observation,
484
509
  requestUrl: requested ? requested.url : null,
485
510
  finalUrl: final ? final.url : null,
486
511
  transportSecurity: final ? final.url.split(':', 1)[0] : null,
@@ -506,7 +531,7 @@ function buildHttpObservation(detail, options = {}) {
506
531
  bytes: capturedBodyBytes,
507
532
  } : null,
508
533
  securityHeaders: securityHeaders(response && response.headers),
509
- deliveryMetadata: buildDeliveryMetadata(response && response.headers),
534
+ deliveryMetadata: buildDeliveryMetadata(response && response.headers, observation.observedAt),
510
535
  },
511
536
  };
512
537
  }
@@ -1,9 +1,9 @@
1
1
  // Versioned policy for projecting the Unicode UTS #39 confusables data into
2
2
  // WHOISleuth's intentionally small domain-label mapping. The full upstream
3
3
  // table is never shipped to browsers or consulted at runtime.
4
- export const UNICODE_CONFUSABLE_DATA_VERSION = '17.0.0';
5
- export const UNICODE_CONFUSABLE_SOURCE_URL = 'https://www.unicode.org/Public/17.0.0/security/confusables.txt';
6
- export const UNICODE_CONFUSABLE_SOURCE_SHA256 = '091c7f82fc39ef208faf8f94d29c244de99254675e09de163160c810d13ef22a';
4
+ export const UNICODE_CONFUSABLE_DATA_VERSION = '18.0.0';
5
+ export const UNICODE_CONFUSABLE_SOURCE_URL = `https://www.unicode.org/Public/${UNICODE_CONFUSABLE_DATA_VERSION}/security/confusables.txt`;
6
+ export const UNICODE_CONFUSABLE_SOURCE_SHA256 = '6ed3ee967c9dfdf6677d563c9985182fbc50a2efb7d6059cd57b2e2ce18f5b92';
7
7
  export const UNICODE_CONFUSABLE_LICENSE = 'Unicode-3.0';
8
8
  export const CONFUSABLE_PROJECTION_SCHEMA = 'whoisleuth.unicode-confusable-projection';
9
9
  export const CONFUSABLE_PROJECTION_VERSION = 1;
@@ -71,7 +71,9 @@ export const REVIEWED_GENERATION_CONFUSABLES = Object.freeze({
71
71
  l: 'ӏǀꓲ',
72
72
  m: 'м',
73
73
  n: 'ոռ',
74
- o: 'оοօᴏⲟ',
74
+ // Keep the complete previously generated o-set when newer source mappings
75
+ // compete for the same bounded generation slots.
76
+ o: 'оοօᴏⲟᴑꬽσ',
75
77
  p: 'рρⲣ',
76
78
  q: 'ԛգզ',
77
79
  r: 'гꭇ',
@@ -4,7 +4,7 @@
4
4
  // bounded projection of Unicode Technical Standard #39 data plus the existing
5
5
  // reviewed compatibility set. It focuses on characters that visually collapse
6
6
  // to common ASCII domain-label characters.
7
- import { GENERATED_CONFUSABLE_GROUPS, GENERATED_CONFUSABLE_MAPPING_VERSION, GENERATED_GENERATION_CONFUSABLE_GROUPS, } from "./generated/unicode-confusables-17.mjs";
7
+ import { GENERATED_CONFUSABLE_GROUPS, GENERATED_CONFUSABLE_MAPPING_VERSION, GENERATED_GENERATION_CONFUSABLE_GROUPS, } from "./generated/unicode-confusables.mjs";
8
8
  import { MAX_GENERATION_CONFUSABLES_PER_ASCII, REVIEWED_GENERATION_CONFUSABLES, } from "./idn-confusable-policy.mjs";
9
9
  export const IDN_ANALYSIS_VERSION = 1;
10
10
  export const CONFUSABLE_MAPPING_VERSION = GENERATED_CONFUSABLE_MAPPING_VERSION;