@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
@@ -8,7 +8,7 @@
8
8
  //
9
9
  // Pure JS, no external dependency and no native bindings, so it runs the same
10
10
  // on the serverless (Netlify) and self-hosted (Express) paths. That means
11
- // decoding the image ourselves: Node has no built-in image decoder, only
11
+ // decoding the image ourselves: the runtime has no built-in image decoder, only
12
12
  // zlib (used here for PNG inflate). We support the favicon formats that
13
13
  // actually occur in practice - PNG, and PNG- or BMP-encoded ICO - and return
14
14
  // null for anything else (GIF/JPEG/SVG), which simply falls back to
@@ -21,11 +21,23 @@
21
21
  // case, while staying cheap (no DCT). Output is 16 lowercase hex chars;
22
22
  // similarity is the Hamming distance between two such strings.
23
23
  import * as zlib from 'node:zlib';
24
- import { hammingDistanceHex, isInformativePerceptualHash, } from "./perceptual-hash-comparison.mjs";
24
+ import { hammingDistanceHex, isInformativePerceptualHash, } from "../packages/analysis/perceptual-hash-comparison.mjs";
25
25
  // Bound the decode work on attacker-controlled image bytes. Real favicons top
26
- // out around 256x256; anything claiming to be larger is refused rather than
27
- // decoded (it would also never be a legitimate favicon to compare against).
26
+ // out around 256x256; larger inputs above this ceiling remain exact-only
27
+ // evidence rather than causing unbounded pixel allocation.
28
28
  const MAX_DIM = 1024;
29
+ // Permit two maximum-sized decode attempts (more than a normal resolution
30
+ // pyramid), while bounding aggregate inflation/filtering across a container.
31
+ // Encoded payloads may consume at most the container's byte length in total;
32
+ // ordinary non-overlapping entries never exceed that bound.
33
+ const MAX_DECODE_PIXELS = 2 * MAX_DIM * MAX_DIM;
34
+ function admitPixels(work, width, height) {
35
+ const count = width * height;
36
+ if (count > work.pixels)
37
+ return false;
38
+ work.pixels -= count;
39
+ return true;
40
+ }
29
41
  const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
30
42
  function byteAt(bytes, index) {
31
43
  return bytes[index] ?? 0;
@@ -53,7 +65,7 @@ function paethPredictor(a, b, c) {
53
65
  // { width, height, pixels } with pixels as RGBA bytes, or null for anything
54
66
  // outside that subset (or malformed input). CRCs are intentionally not
55
67
  // verified - we're computing a fuzzy hash, not validating integrity.
56
- function decodePng(buf) {
68
+ function decodePng(buf, work) {
57
69
  const CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
58
70
  let width = 0;
59
71
  let height = 0;
@@ -70,14 +82,18 @@ function decodePng(buf) {
70
82
  const dataStart = offset + 8;
71
83
  const dataEnd = dataStart + length;
72
84
  if (dataEnd + 4 > buf.length)
73
- break; // truncated chunk
85
+ return null;
74
86
  const data = buf.subarray(dataStart, dataEnd);
75
87
  if (type === 'IHDR') {
88
+ if (length !== 13 || width !== 0 || offset !== 8)
89
+ return null;
76
90
  width = buf.readUInt32BE(dataStart);
77
91
  height = buf.readUInt32BE(dataStart + 4);
78
92
  bitDepth = byteAt(data, 8);
79
93
  colorType = byteAt(data, 9);
80
94
  interlace = byteAt(data, 12);
95
+ if (byteAt(data, 10) !== 0 || byteAt(data, 11) !== 0)
96
+ return null;
81
97
  }
82
98
  else if (type === 'PLTE') {
83
99
  palette = data;
@@ -86,7 +102,7 @@ function decodePng(buf) {
86
102
  transparency = data;
87
103
  }
88
104
  else if (type === 'IDAT') {
89
- idatParts.push(Buffer.from(data));
105
+ idatParts.push(data);
90
106
  }
91
107
  else if (type === 'IEND') {
92
108
  break;
@@ -100,8 +116,17 @@ function decodePng(buf) {
100
116
  return null;
101
117
  if (colorType === 3 && !palette)
102
118
  return null;
119
+ if (palette && (palette.length < 3 || palette.length > 768 || palette.length % 3 !== 0))
120
+ return null;
121
+ if (transparency && ((colorType === 0 && transparency.length !== 2)
122
+ || (colorType === 2 && transparency.length !== 6)
123
+ || (colorType === 3 && (!palette || transparency.length > palette.length / 3))
124
+ || colorType === 4 || colorType === 6))
125
+ return null;
103
126
  if (idatParts.length === 0)
104
127
  return null;
128
+ if (!admitPixels(work, width, height))
129
+ return null;
105
130
  // A valid non-interlaced 8-bit PNG decompresses to exactly one filter byte
106
131
  // plus one scanline per row. Cap the inflate at that size so a malicious
107
132
  // IDAT can't expand a sub-200KB favicon into hundreds of MB (a zip bomb)
@@ -162,6 +187,8 @@ function decodePng(buf) {
162
187
  let alpha = 255;
163
188
  if (colorType === 0) {
164
189
  r = g = b = byteAt(recon, s);
190
+ if (transparency && r === transparency.readUInt16BE(0))
191
+ alpha = 0;
165
192
  }
166
193
  else if (colorType === 4) {
167
194
  r = g = b = byteAt(recon, s);
@@ -171,6 +198,9 @@ function decodePng(buf) {
171
198
  r = byteAt(recon, s);
172
199
  g = byteAt(recon, s + 1);
173
200
  b = byteAt(recon, s + 2);
201
+ if (transparency && r === transparency.readUInt16BE(0)
202
+ && g === transparency.readUInt16BE(2) && b === transparency.readUInt16BE(4))
203
+ alpha = 0;
174
204
  }
175
205
  else if (colorType === 6) {
176
206
  r = byteAt(recon, s);
@@ -200,13 +230,13 @@ function decodePng(buf) {
200
230
  // BITMAPINFOHEADER, an optional palette (for <=8-bit depths), then bottom-up
201
231
  // pixel rows. Supports the depths favicons actually ship: 1/4/8-bit
202
232
  // palettized and 24/32-bit truecolour. The stored height is doubled (it
203
- // includes the 1-bpp AND transparency mask); we use the real image height
204
- // and ignore the mask (opaque is fine for a brightness-based hash).
205
- function decodeDib(buf) {
233
+ // includes the 1-bpp AND transparency mask). Non-alpha and legacy all-zero
234
+ // alpha images use that mask; meaningful 32-bit alpha remains authoritative.
235
+ function decodeDib(buf, work) {
206
236
  if (buf.length < 40)
207
237
  return null;
208
238
  const headerSize = buf.readUInt32LE(0);
209
- if (headerSize < 40)
239
+ if (headerSize < 40 || headerSize > buf.length)
210
240
  return null;
211
241
  const width = buf.readInt32LE(4);
212
242
  const rawHeight = buf.readInt32LE(8);
@@ -216,7 +246,9 @@ function decodeDib(buf) {
216
246
  return null; // BI_RGB only
217
247
  if (![1, 4, 8, 24, 32].includes(bitCount))
218
248
  return null;
219
- const height = Math.floor(Math.abs(rawHeight) / 2) || Math.abs(rawHeight);
249
+ if (rawHeight <= 0 || rawHeight % 2 !== 0 || buf.readUInt16LE(12) !== 1)
250
+ return null;
251
+ const height = rawHeight / 2;
220
252
  if (width < 1 || height < 1 || width > MAX_DIM || height > MAX_DIM)
221
253
  return null;
222
254
  // Palette (BGRA quads) for indexed depths, sitting between the header and
@@ -235,6 +267,13 @@ function decodeDib(buf) {
235
267
  const rowSize = Math.floor((bitCount * width + 31) / 32) * 4; // padded to 4 bytes
236
268
  if (pixelStart + rowSize * height > buf.length)
237
269
  return null;
270
+ const maskStart = pixelStart + rowSize * height;
271
+ const maskRowSize = Math.ceil(width / 32) * 4;
272
+ const maskPresent = maskStart + maskRowSize * height <= buf.length;
273
+ if (bitCount !== 32 && !maskPresent)
274
+ return null;
275
+ if (!admitPixels(work, width, height))
276
+ return null;
238
277
  const paletteColor = (index) => {
239
278
  if (!palette || index >= palette.count)
240
279
  return [0, 0, 0];
@@ -282,11 +321,16 @@ function decodeDib(buf) {
282
321
  }
283
322
  // Many legacy 32-bit BMP-in-ICO payloads leave the alpha plane all-zero and
284
323
  // rely on the separate 1-bit AND mask for transparency. Trusting those alpha
285
- // bytes literally would composite the whole icon to white and hash to a
286
- // meaningless all-zero value, so treat a fully-zero alpha plane as opaque.
287
- if (bitCount === 32 && !sawNonzeroAlpha) {
288
- for (let i = 3; i < pixels.length; i += 4)
289
- pixels[i] = 255;
324
+ // bytes literally would composite the whole icon to white. Restore opacity
325
+ // only where the legacy mask permits it. No mask plus meaningful 32-bit
326
+ // alpha is valid; an omitted legacy mask retains the opaque fallback.
327
+ if (bitCount !== 32 || !sawNonzeroAlpha) {
328
+ for (let y = 0; y < height; y += 1) {
329
+ for (let x = 0; x < width; x += 1) {
330
+ const mask = maskPresent ? byteAt(buf, maskStart + (height - 1 - y) * maskRowSize + (x >> 3)) : 0;
331
+ pixels[(y * width + x) * 4 + 3] = mask & (0x80 >> (x & 7)) ? 0 : 255;
332
+ }
333
+ }
290
334
  }
291
335
  return { width, height, pixels };
292
336
  }
@@ -295,9 +339,10 @@ function decodeDib(buf) {
295
339
  // one is in a variant we don't decode (e.g. a 4-bit-palette PNG sub-entry
296
340
  // alongside a plain BMP one). A multi-size favicon.ico only needs one usable
297
341
  // entry, and any size hashes to nearly the same dHash since it downsamples.
298
- function decodeIco(buf) {
342
+ function decodeIco(buf, work) {
299
343
  const count = buf.readUInt16LE(4);
300
- if (count < 1 || 6 + count * 16 > buf.length)
344
+ const directoryEnd = 6 + count * 16;
345
+ if (count < 1 || directoryEnd > buf.length)
301
346
  return null;
302
347
  const entries = [];
303
348
  for (let i = 0; i < count; i += 1) {
@@ -306,14 +351,22 @@ function decodeIco(buf) {
306
351
  const h = buf[entry + 1] || 256;
307
352
  const size = buf.readUInt32LE(entry + 8);
308
353
  const dataOffset = buf.readUInt32LE(entry + 12);
309
- if (dataOffset + size > buf.length || size < 1)
354
+ if (dataOffset < directoryEnd || dataOffset + size > buf.length || size < 1)
310
355
  continue;
311
356
  entries.push({ area: w * h, dataOffset, size });
312
357
  }
313
358
  entries.sort((a, b) => b.area - a.area);
359
+ const visited = new Set();
314
360
  for (const { dataOffset, size } of entries) {
361
+ const identity = `${dataOffset}:${size}`;
362
+ if (visited.has(identity))
363
+ continue;
364
+ visited.add(identity);
365
+ if (size > work.encodedBytes)
366
+ return null;
367
+ work.encodedBytes -= size;
315
368
  const payload = buf.subarray(dataOffset, dataOffset + size);
316
- const image = isPng(payload) ? decodePng(payload) : decodeDib(payload);
369
+ const image = isPng(payload) ? decodePng(payload, work) : decodeDib(payload, work);
317
370
  if (image)
318
371
  return image;
319
372
  }
@@ -322,10 +375,11 @@ function decodeIco(buf) {
322
375
  function decodeImage(buf) {
323
376
  if (!Buffer.isBuffer(buf))
324
377
  return null;
378
+ const work = { encodedBytes: buf.length, pixels: MAX_DECODE_PIXELS };
325
379
  if (isPng(buf))
326
- return decodePng(buf);
380
+ return decodePng(buf, work);
327
381
  if (isIco(buf))
328
- return decodeIco(buf);
382
+ return decodeIco(buf, work);
329
383
  return null;
330
384
  }
331
385
  // Downscales to cols x rows of grayscale luma. Transparency is composited
@@ -428,4 +482,5 @@ function inspectDecodedImage(buf) {
428
482
  // produced by the optional local capture package. Keep the generic name as an
429
483
  // alias so the evidence contract does not imply that a screenshot is a favicon.
430
484
  const imagePerceptualHash = faviconPerceptualHash;
431
- export { faviconPerceptualHash, hammingDistanceHex, imagePerceptualHash, inspectDecodedImage };
485
+ // Pixel consumers share the same strict byte/dimension admission as hashing.
486
+ export { faviconPerceptualHash, hammingDistanceHex, imagePerceptualHash, inspectDecodedImage, decodeImage as decodeBoundedImagePixels, dHash as imagePixelsPerceptualHash };
@@ -1,29 +1,2 @@
1
- import { WHOISLEUTH_SOURCE_REPOSITORY_URL, } from "./project-metadata.mjs";
2
- import { normalizeBoundedSemanticVersion } from "./semantic-version.mjs";
3
- const URI_LABEL_UNSAFE_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
4
- const URI_LABEL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/|[^\s])/u;
5
- export function isUriShapedLabel(value) {
6
- if (typeof value !== 'string')
7
- return false;
8
- const candidate = value.replace(URI_LABEL_UNSAFE_GLOBAL_RE, '').trim();
9
- return URI_LABEL_SCHEME_RE.test(candidate);
10
- }
11
- export function buildPortableGeneratorMetadata(applicationVersion) {
12
- let version = null;
13
- if (applicationVersion !== null && applicationVersion !== undefined && applicationVersion !== '') {
14
- try {
15
- version = normalizeBoundedSemanticVersion(applicationVersion, 'Portable report application');
16
- }
17
- catch {
18
- version = null;
19
- }
20
- }
21
- return Object.freeze({
22
- name: 'WHOISleuth',
23
- version,
24
- projectUrl: WHOISLEUTH_SOURCE_REPOSITORY_URL,
25
- });
26
- }
27
- export function portableGeneratorAttribution(generator) {
28
- return `Generated with ${generator.name}${generator.version ? ` ${generator.version}` : ''} · Source: ${generator.projectUrl}`;
29
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/portable-generator.mjs";
@@ -1,8 +1,2 @@
1
- // Browser-safe public project metadata shared by request identity, package
2
- // verification, canonical URLs, structured data, and public policy pages.
3
- export const WHOISLEUTH_SITE_ORIGIN = 'https://www.whoisleuth.com';
4
- export const WHOISLEUTH_PROJECT_URL = `${WHOISLEUTH_SITE_ORIGIN}/`;
5
- export const WHOISLEUTH_REQUEST_POLICY_URL = `${WHOISLEUTH_SITE_ORIGIN}/request-policy`;
6
- export const WHOISLEUTH_SOURCE_REPOSITORY_URL = 'https://github.com/slicedearth/whoisleuth';
7
- export const WHOISLEUTH_SOURCE_REPOSITORY_GIT_URL = `git+${WHOISLEUTH_SOURCE_REPOSITORY_URL}.git`;
8
- export const WHOISLEUTH_SOURCE_ISSUES_URL = `${WHOISLEUTH_SOURCE_REPOSITORY_URL}/issues`;
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/project-metadata.mjs";
@@ -1,6 +1,7 @@
1
1
  // IANA RDAP bootstrap retrieval, bounded stale caching, and authority
2
2
  // selection for domain, IP, and ASN queries.
3
3
  import net from 'node:net';
4
+ import { abortable } from "./abort.mjs";
4
5
  import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
5
6
  const BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
6
7
  const BOOTSTRAP_STALE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
@@ -116,21 +117,17 @@ function validBootstrap(data) {
116
117
  && Array.isArray(service[1]) && service[1].length > 0
117
118
  && service[1].some((url) => typeof url === 'string' && /^https?:\/\//i.test(url))));
118
119
  }
119
- async function fetchBootstrap(kind, options = {}) {
120
- if (!BOOTSTRAP_KINDS.has(kind))
121
- throw new Error(`Unsupported RDAP bootstrap kind: ${kind}`);
122
- const now = typeof options.now === 'function' ? options.now : Date.now;
123
- const fetchUpstream = options.fetchUpstream || fetchRdapDetailedWithTimeout;
124
- const cached = bootstrapCache.get(kind);
125
- if (cached && now() - cached.fetchedAt < BOOTSTRAP_TTL_MS)
126
- return cached.data;
127
- const inflight = bootstrapInflight.get(kind);
128
- if (inflight)
129
- return inflight;
130
- const request = (async () => {
120
+ function startBootstrap(kind, options) {
121
+ const now = options.now ?? Date.now;
122
+ const fetchUpstream = options.fetchUpstream ?? fetchRdapDetailedWithTimeout;
123
+ const controller = new AbortController();
124
+ const { signal } = controller;
125
+ const request = abortable(async () => {
131
126
  try {
127
+ signal.throwIfAborted();
132
128
  const requestedEndpoint = `https://data.iana.org/rdap/${kind}.json`;
133
- const response = await fetchUpstream(requestedEndpoint, {}, BOOTSTRAP_FETCH_TIMEOUT_MS);
129
+ const response = await fetchUpstream(requestedEndpoint, { signal }, BOOTSTRAP_FETCH_TIMEOUT_MS);
130
+ signal.throwIfAborted();
134
131
  const finalEndpoint = admitRdapEndpoint(response.finalUrl ?? requestedEndpoint);
135
132
  if (finalEndpoint !== requestedEndpoint) {
136
133
  throw new Error(`IANA bootstrap redirected outside its fixed source endpoint for ${kind}`);
@@ -152,21 +149,74 @@ async function fetchBootstrap(kind, options = {}) {
152
149
  return data;
153
150
  }
154
151
  catch (cause) {
152
+ signal.throwIfAborted();
155
153
  const fallback = bootstrapCache.get(kind);
156
154
  if (fallback && now() - fallback.fetchedAt <= BOOTSTRAP_STALE_TTL_MS) {
157
155
  return fallback.data;
158
156
  }
159
157
  throw cause;
160
158
  }
161
- finally {
162
- bootstrapInflight.delete(kind);
159
+ }, signal);
160
+ const flight = {
161
+ controller, readers: 0, settled: false,
162
+ request: request.finally(() => {
163
+ flight.settled = true;
164
+ if (bootstrapInflight.get(kind) === flight)
165
+ bootstrapInflight.delete(kind);
166
+ }),
167
+ };
168
+ bootstrapInflight.set(kind, flight);
169
+ return flight;
170
+ }
171
+ async function fetchBootstrap(kind, options = {}) {
172
+ if (!BOOTSTRAP_KINDS.has(kind))
173
+ throw new Error(`Unsupported RDAP bootstrap kind: ${kind}`);
174
+ options.signal?.throwIfAborted();
175
+ const now = options.now ?? Date.now;
176
+ const cached = bootstrapCache.get(kind);
177
+ if (cached && now() - cached.fetchedAt < BOOTSTRAP_TTL_MS)
178
+ return cached.data;
179
+ const flight = bootstrapInflight.get(kind) ?? startBootstrap(kind, options);
180
+ flight.readers++;
181
+ let released = false;
182
+ const release = () => {
183
+ if (released)
184
+ return;
185
+ released = true;
186
+ // One deadline releases only its reader. The fixed-source transport lives
187
+ // until the last reader leaves, and never caches a cancelled late response.
188
+ flight.readers--;
189
+ if (!flight.readers && !flight.settled) {
190
+ flight.controller.abort();
191
+ if (bootstrapInflight.get(kind) === flight)
192
+ bootstrapInflight.delete(kind);
163
193
  }
164
- })();
165
- bootstrapInflight.set(kind, request);
166
- return request;
194
+ };
195
+ const { signal } = options;
196
+ let cancel = () => { };
197
+ const result = new Promise((resolve, reject) => {
198
+ // Attach before observing cancellation so even an abandoned queued flight
199
+ // retains a rejection handler. Release synchronously to avoid starting it.
200
+ flight.request.then(resolve, reject);
201
+ cancel = () => { release(); reject(signal?.reason); };
202
+ signal?.addEventListener('abort', cancel, { once: true });
203
+ if (signal?.aborted)
204
+ cancel();
205
+ });
206
+ try {
207
+ return await result;
208
+ }
209
+ finally {
210
+ signal?.removeEventListener('abort', cancel);
211
+ release();
212
+ if (flight.controller.signal.aborted)
213
+ await flight.request.catch(() => { });
214
+ }
167
215
  }
168
216
  function clearRdapBootstrapCache() {
169
217
  bootstrapCache.clear();
218
+ for (const flight of bootstrapInflight.values())
219
+ flight.controller.abort();
170
220
  bootstrapInflight.clear();
171
221
  }
172
222
  function uniqueRdapBases(urls) {
@@ -185,9 +235,9 @@ function uniqueRdapBases(urls) {
185
235
  return true;
186
236
  });
187
237
  }
188
- async function findRdapBases(type, value) {
238
+ async function findRdapBases(type, value, options = {}) {
189
239
  if (type === 'domain') {
190
- const bootstrap = await fetchBootstrap('dns');
240
+ const bootstrap = await fetchBootstrap('dns', options);
191
241
  const tld = value.split('.').pop()?.toLowerCase() || '';
192
242
  for (const [tlds, urls] of bootstrap.services) {
193
243
  if (tlds.some((entry) => entry.toLowerCase() === tld))
@@ -196,7 +246,7 @@ async function findRdapBases(type, value) {
196
246
  return [];
197
247
  }
198
248
  if (type === 'ipv4' || type === 'ipv6') {
199
- const bootstrap = await fetchBootstrap(type);
249
+ const bootstrap = await fetchBootstrap(type, options);
200
250
  const matcher = type === 'ipv4' ? ipInCidrV4 : ipInCidrV6;
201
251
  let best = null;
202
252
  let bestPrefix = -1;
@@ -215,7 +265,7 @@ async function findRdapBases(type, value) {
215
265
  return best || [];
216
266
  }
217
267
  if (type === 'asn') {
218
- const bootstrap = await fetchBootstrap('asn');
268
+ const bootstrap = await fetchBootstrap('asn', options);
219
269
  const number = parseInt(value.replace(/^AS/i, ''), 10);
220
270
  for (const [ranges, urls] of bootstrap.services) {
221
271
  for (const range of ranges) {
@@ -1,7 +1,7 @@
1
1
  import { cached } from "./lookup-cache.mjs";
2
2
  import { findRdapBases, uniqueRdapBases, } from "./rdap-bootstrap.mjs";
3
3
  import { rdapAttempt, rdapFailure } from "./rdap-attempts.mjs";
4
- import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
4
+ import { fetchRdapDetailedWithTimeout, RdapBodyAdmissionError, } from "./rdap-transport.mjs";
5
5
  import { admitRdapObjectEndpoint, validateRdapResponse, } from "./rdap-validation.mjs";
6
6
  const UPSTREAM_TIMEOUT_MS = 7_000;
7
7
  const UPSTREAM_TOTAL_DEADLINE_MS = 12_000;
@@ -24,20 +24,23 @@ function rdapPathFor(type, value) {
24
24
  * Query the bounded bootstrap candidate set while retaining every attempted
25
25
  * endpoint as separately attributed transport evidence.
26
26
  */
27
- export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapDetailedWithTimeout) {
27
+ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapDetailedWithTimeout, signal) {
28
+ signal?.throwIfAborted();
28
29
  const candidates = uniqueRdapBases(bases).slice(0, MAX_RDAP_ENDPOINTS);
29
30
  if (candidates.length === 0)
30
31
  return null;
31
32
  const startedAt = Date.now();
32
33
  const attempts = [];
33
34
  for (const base of candidates) {
35
+ signal?.throwIfAborted();
34
36
  const elapsed = Date.now() - startedAt;
35
37
  const remaining = UPSTREAM_TOTAL_DEADLINE_MS - elapsed;
36
38
  if (remaining <= 0)
37
39
  break;
38
40
  const url = `${base.replace(/\/$/, '')}/${rdapPathFor(type, value)}`;
39
41
  try {
40
- const upstream = await fetchUpstream(url, { headers: { Accept: 'application/rdap+json' } }, Math.min(UPSTREAM_TIMEOUT_MS, remaining));
42
+ const upstream = await fetchUpstream(url, { headers: { Accept: 'application/rdap+json' }, ...(signal ? { signal } : {}) }, Math.min(UPSTREAM_TIMEOUT_MS, remaining));
43
+ signal?.throwIfAborted();
41
44
  const selectedEndpoint = admitRdapObjectEndpoint(type, value, upstream.finalUrl ?? url);
42
45
  if (!selectedEndpoint) {
43
46
  attempts.push(rdapAttempt(url, 'invalid_response', {
@@ -58,21 +61,25 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
58
61
  }));
59
62
  continue;
60
63
  }
61
- let data;
64
+ let data = null;
62
65
  try {
63
66
  data = JSON.parse(upstream.text);
64
67
  }
65
68
  catch {
66
- attempts.push(rdapAttempt(selectedEndpoint, 'invalid_json', {
67
- status: upstream.status,
68
- detail: 'The endpoint returned invalid JSON.',
69
- }));
70
- continue;
69
+ // RFC 7480 section 5.3 makes the explanatory body optional for a
70
+ // negative answer. URL binding and transport/body bounds still apply.
71
+ if (upstream.status !== 404) {
72
+ attempts.push(rdapAttempt(selectedEndpoint, 'invalid_json', {
73
+ status: upstream.status,
74
+ detail: 'The endpoint returned invalid JSON.',
75
+ }));
76
+ continue;
77
+ }
71
78
  }
72
79
  if (upstream.status === 404) {
73
80
  attempts.push(rdapAttempt(selectedEndpoint, 'not_found', {
74
81
  status: upstream.status,
75
- detail: 'The authoritative endpoint reported no matching object.',
82
+ detail: `The authoritative endpoint reported no matching object.${data === null ? ' No usable JSON explanation was supplied.' : ''}`,
76
83
  selected: true,
77
84
  }));
78
85
  return {
@@ -110,6 +117,11 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
110
117
  };
111
118
  }
112
119
  catch (cause) {
120
+ signal?.throwIfAborted();
121
+ if (cause instanceof RdapBodyAdmissionError) {
122
+ attempts.push(rdapAttempt(url, 'invalid_response', { status: cause.status, detail: cause.message }));
123
+ continue;
124
+ }
113
125
  const detail = String(errorProperty(cause, 'message') || 'request failed');
114
126
  const outcome = errorProperty(cause, 'name') === 'AbortError' ||
115
127
  /timed? out|time limit/i.test(detail)
@@ -125,9 +137,9 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
125
137
  : 'the total upstream deadline expired';
126
138
  throw Object.assign(new Error(`RDAP lookup failed across ${candidates.length} endpoint(s): ${detail}`), { attempts });
127
139
  }
128
- export async function fetchRdapRecordWithParser(type, value, parseRdap) {
140
+ export async function fetchRdapRecordWithParser(type, value, parseRdap, options = {}) {
129
141
  return cached(`rdap:${type}:${value}`, async () => {
130
- const bases = await findRdapBases(type, value);
131
- return fetchRdapFromBasesWithParser(type, value, bases, parseRdap);
132
- });
142
+ const bases = await findRdapBases(type, value, options);
143
+ return fetchRdapFromBasesWithParser(type, value, bases, parseRdap, options.fetchUpstream, options.signal);
144
+ }, options.signal);
133
145
  }
@@ -1,8 +1,9 @@
1
1
  // Bounded normalization of untrusted RDAP response objects.
2
2
  import net from 'node:net';
3
3
  import { registryDateIso } from "./registry-dates.mjs";
4
- import {} from "./rdap-types.mjs";
4
+ import { MAX_RDAP_STATUSES, MAX_RDAP_REDACTIONS, } from "./rdap-types.mjs";
5
5
  const MAX_RDAP_ENTITIES = 100;
6
+ const MAX_RDAP_EVENTS = 100;
6
7
  const MAX_RDAP_ENTITY_DEPTH = 6;
7
8
  const MAX_ENTITIES_PER_ROLE = 5;
8
9
  const MAX_VCARD_ENTRIES = 100;
@@ -10,7 +11,6 @@ const MAX_ENTITY_ROLES = 12;
10
11
  const MAX_CONTACT_VALUES = 8;
11
12
  const MAX_ENTITY_LINKS = 10;
12
13
  const MAX_RDAP_LINKS = 20;
13
- const MAX_RDAP_REDACTIONS = 100;
14
14
  const MAX_RDAP_VARIANT_GROUPS = 20;
15
15
  const MAX_RDAP_VARIANT_NAMES = 50;
16
16
  const MAX_RDAP_SERVER_TRUNCATION_REASONS = 8;
@@ -166,8 +166,10 @@ function contactValuesTruncated(vcardArray) {
166
166
  return rawPart.length > 300 || /[\u0000-\u001f\u007f]/u.test(rawPart) || rawPart.trim() !== '';
167
167
  });
168
168
  const retainedParts = parts.filter((part) => part !== null);
169
+ // Empty structured components publish no address; they are not omitted
170
+ // evidence. Supplied non-blank values still pass every validation bound.
169
171
  if (rejectedNonBlankPart
170
- || boundedString(retainedParts.join(', '), 1000) === null)
172
+ || (retainedParts.length > 0 && boundedString(retainedParts.join(', '), 1000) === null))
171
173
  return true;
172
174
  }
173
175
  return false;
@@ -506,18 +508,33 @@ function boundedEventString(value, maxLength) {
506
508
  return null;
507
509
  return value.trim() || null;
508
510
  }
509
- function normalizeRdapEvents(value) {
511
+ function normalizedEventInventory(value) {
510
512
  if (!Array.isArray(value))
511
- return [];
512
- return value.slice(0, 100).map((event) => {
513
- if (!event || typeof event !== 'object' || Array.isArray(event))
514
- return null;
513
+ return { items: [], truncated: value !== undefined };
514
+ let truncated = value.length > MAX_RDAP_EVENTS;
515
+ const items = [];
516
+ for (const event of value.slice(0, MAX_RDAP_EVENTS)) {
517
+ if (!event || typeof event !== 'object' || Array.isArray(event)) {
518
+ truncated = true;
519
+ continue;
520
+ }
515
521
  const record = event;
516
522
  const action = boundedEventString(record.eventAction, 100)?.toLowerCase().replace(/\s+/g, ' ') || null;
517
523
  const date = boundedEventString(record.eventDate, 64);
518
524
  const actor = boundedEventString(record.eventActor, 160);
519
- return action || date ? { action, date, actor } : null;
520
- }).filter((event) => event !== null);
525
+ if (record.eventAction !== undefined && action === null
526
+ || record.eventDate !== undefined && date === null
527
+ || record.eventActor !== undefined && actor === null)
528
+ truncated = true;
529
+ if (action || date)
530
+ items.push({ action, date, actor });
531
+ else
532
+ truncated = true;
533
+ }
534
+ return { items, truncated };
535
+ }
536
+ function normalizeRdapEvents(value) {
537
+ return normalizedEventInventory(value).items;
521
538
  }
522
539
  function lifecycleDate(events, action, newest) {
523
540
  let selected = null;
@@ -697,7 +714,8 @@ function entityInventory(entities) {
697
714
  function parseRdapObject(type, data) {
698
715
  if (!data || typeof data !== 'object' || Array.isArray(data))
699
716
  return null;
700
- const events = normalizeRdapEvents(data.events);
717
+ const eventInfo = normalizedEventInventory(data.events);
718
+ const events = eventInfo.items;
701
719
  const redactionInfo = normalizeRedactions(data.redacted);
702
720
  const noticesInfo = summarizeTextBlocks(data.notices);
703
721
  const remarksInfo = summarizeTextBlocks(data.remarks);
@@ -708,12 +726,11 @@ function parseRdapObject(type, data) {
708
726
  // to every RDAP object type. Unlike set-like fields, status order and
709
727
  // repetition remain as published for backwards compatibility.
710
728
  const statuses = Array.isArray(data.status)
711
- ? data.status.slice(0, 100)
729
+ ? data.status.slice(0, MAX_RDAP_STATUSES)
712
730
  .map((status) => boundedString(status, 160))
713
731
  .filter((status) => status !== null)
714
732
  : [];
715
- const inspectedStatusCount = Array.isArray(data.status) ? Math.min(data.status.length, 100) : 0;
716
- const inspectedEventCount = Array.isArray(data.events) ? Math.min(data.events.length, 100) : 0;
733
+ const inspectedStatusCount = Array.isArray(data.status) ? Math.min(data.status.length, MAX_RDAP_STATUSES) : 0;
717
734
  const common = {
718
735
  objectClassName: boundedString(data.objectClassName, 80, { lower: true }),
719
736
  language: boundedString(data.lang, 35, { lower: true }),
@@ -732,12 +749,10 @@ function parseRdapObject(type, data) {
732
749
  serverTruncationReasons,
733
750
  statuses,
734
751
  statusesTruncated: data.status !== undefined && (!Array.isArray(data.status)
735
- || data.status.length > 100
752
+ || data.status.length > MAX_RDAP_STATUSES
736
753
  || statuses.length < inspectedStatusCount),
737
754
  events,
738
- eventsTruncated: data.events !== undefined && (!Array.isArray(data.events)
739
- || data.events.length > 100
740
- || events.length < inspectedEventCount),
755
+ eventsTruncated: eventInfo.truncated,
741
756
  lifecycle: summarizeLifecycle(events),
742
757
  };
743
758
  if (type === 'domain') {
@@ -213,7 +213,7 @@ function inspectRdapReverseSearchResponse(value, requestedProperties = []) {
213
213
  mappings.push(Object.freeze({
214
214
  property,
215
215
  propertyPath,
216
- state: REGISTERED_PROPERTY_PATHS[property]?.includes(propertyPath)
216
+ state: Object.hasOwn(REGISTERED_PROPERTY_PATHS, property) && REGISTERED_PROPERTY_PATHS[property]?.includes(propertyPath)
217
217
  ? 'registered'
218
218
  : 'unrecognized',
219
219
  }));