@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,17 +1,19 @@
1
1
  // Lookup result shaping after the source plan has settled. This module starts no
2
2
  // collection and preserves compact, diagnostics and enrichment response shapes.
3
+ import { withoutHttpDeliveryMetadata } from "./http-intelligence.mjs";
3
4
  import { buildBulkComparisonEvidence } from "./bulk-comparison-evidence.mjs";
4
5
  import { failedReverseDnsIntelligence, skippedReverseDnsIntelligence, } from "./dns-intelligence.mjs";
5
- import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail, errorMessage, } from "./lookup-diagnostics.mjs";
6
+ import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail, } from "./lookup-diagnostics.mjs";
6
7
  import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
7
8
  import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
8
- import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "./threat-intelligence-types.mjs";
9
+ import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "../packages/analysis/threat-intelligence-types.mjs";
9
10
  import { registryAccessDiagnosticFor } from "./registry-capabilities.mjs";
10
11
  import { buildRegistryInsights } from "./registry-insights.mjs";
11
12
  import { buildRegistrarStanding } from "./registrar-standing.mjs";
12
13
  import { resolveRegistrarIanaId } from "./registrar-standing-contract.mjs";
13
14
  import { securityTxtUnavailable } from "./security-txt.mjs";
14
15
  import { parseWhoisChain } from "./whois.mjs";
16
+ import { whoisCollectionStatus } from "./whois-authority.mjs";
15
17
  function withoutNestedPublicationMetadata(value) {
16
18
  if (!value || typeof value !== 'object' || Array.isArray(value))
17
19
  return value;
@@ -19,17 +21,6 @@ function withoutNestedPublicationMetadata(value) {
19
21
  delete output.publicationMetadata;
20
22
  return output;
21
23
  }
22
- function withoutNestedDeliveryMetadata(value) {
23
- if (!value || typeof value !== 'object' || Array.isArray(value))
24
- return value;
25
- const output = { ...value };
26
- if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
27
- const response = { ...output.response };
28
- delete response.deliveryMetadata;
29
- output.response = response;
30
- }
31
- return output;
32
- }
33
24
  async function buildUnifiedLookupResponse(context) {
34
25
  const { availabilityEnabled, availabilityPromise, classified, compact, dnsIntelligenceEnabled, fast, inspectCertificateWarningData, networkContextPromise, options, rdapEnabled, rdapPromise, registrarRdapPromise, reverseDnsEligible, reverseDnsPromise, securityTxtPromise, skipWhois, threatfoxIntelligencePromise, timing, urlhausIntelligencePromise, urlscanIntelligencePromise, whoisEnabled, whoisPromise, } = context;
35
26
  const [rdapResult, whoisResult, availabilityResult, reverseDnsResult, registrarRdapResult, networkContextResult, securityTxtResult, urlscanIntelligenceResult, urlhausIntelligenceResult, threatfoxIntelligenceResult] = await Promise.allSettled([
@@ -99,7 +90,7 @@ async function buildUnifiedLookupResponse(context) {
99
90
  whois = { skipped: true, detail: 'WHOIS is omitted in fast RDAP-only mode.' };
100
91
  }
101
92
  else if (Array.isArray(whoisChain)) {
102
- whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain) };
93
+ whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined) };
103
94
  }
104
95
  else {
105
96
  whois = {
@@ -125,10 +116,8 @@ async function buildUnifiedLookupResponse(context) {
125
116
  availability = {
126
117
  applicable: true,
127
118
  ...result,
128
- // Every observation nested in the availability envelope is collected
129
- // against this registrable target. Request context remains on the
130
- // top-level response; exact-host sources such as security.txt retain
131
- // their own requested/final URL attribution.
119
+ // Registration authority retains its registrable target. Deep source
120
+ // observations declare their independently collected hostname.
132
121
  domain: classified.value,
133
122
  };
134
123
  }
@@ -138,7 +127,7 @@ async function buildUnifiedLookupResponse(context) {
138
127
  domain: classified.value,
139
128
  state: 'unknown',
140
129
  confidence: 'low',
141
- detail: errorMessage(availabilityResult.reason, 'Availability lookup failed'),
130
+ detail: boundedSourceDetail(availabilityResult.reason, 'Availability lookup failed'),
142
131
  };
143
132
  }
144
133
  }
@@ -153,11 +142,9 @@ async function buildUnifiedLookupResponse(context) {
153
142
  ? 'disabled'
154
143
  : skipWhois
155
144
  ? 'skipped'
156
- : whoisResult.status === 'rejected' || !Array.isArray(whoisChain)
145
+ : whoisResult.status === 'rejected'
157
146
  ? 'error'
158
- : whoisChain.length === 1
159
- ? 'unsupported'
160
- : whois.parsed && whois.parsed.chainStatus === 'complete' ? 'complete' : 'partial';
147
+ : whoisCollectionStatus(whoisChain, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined);
161
148
  const availabilityStatus = classified.type !== 'domain'
162
149
  ? 'not_applicable'
163
150
  : !availabilityEnabled ? 'disabled'
@@ -301,7 +288,7 @@ async function buildUnifiedLookupResponse(context) {
301
288
  availability: {
302
289
  ...compactAvailability,
303
290
  ...(richPageIdentity !== undefined ? { pageIdentity: withoutNestedPublicationMetadata(richPageIdentity) } : {}),
304
- ...(richHttp !== undefined ? { http: withoutNestedDeliveryMetadata(richHttp) } : {}),
291
+ ...(richHttp !== undefined ? { http: withoutHttpDeliveryMetadata(richHttp) } : {}),
305
292
  ...(bulkComparison ? { bulkComparison } : {}),
306
293
  },
307
294
  diagnostics,
@@ -2,7 +2,9 @@
2
2
  // Lookup. These deliberately exclude raw RDAP/WHOIS bodies, contacts, URLs,
3
3
  // provider payloads, and request errors. The ordinary final Lookup response
4
4
  // remains the only authoritative and persistable result.
5
- import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "./threat-intelligence-types.mjs";
5
+ import { whoisCollectionStatus } from "./whois-authority.mjs";
6
+ import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, } from "../packages/analysis/threat-intelligence-types.mjs";
7
+ import { plannedLookupSources } from "./lookup-progress.mjs";
6
8
  const RESULT_STATES = new Set([
7
9
  'available',
8
10
  'expiring',
@@ -31,44 +33,33 @@ function record(value) {
31
33
  ? value
32
34
  : {};
33
35
  }
36
+ function sourceTruncated(value) {
37
+ // RDAP's normalised *Truncated fields describe loss independently of HTTP success.
38
+ return [record(value), record(record(value).parsed)].some(part => Object.entries(part)
39
+ .some(([key, field]) => field === true && (key === 'truncated' || key.endsWith('Truncated'))));
40
+ }
34
41
  function plannedLookupProgressSources(classified, options = {}) {
35
- const sources = ['rdap', 'whois'];
36
- if (classified.type === 'domain') {
37
- sources.push('domain_evidence', 'registrar_rdap', 'network_context');
38
- if (options.securityTxt)
39
- sources.push('security_txt');
40
- if (options.externalIntelligence)
41
- sources.push('external_intelligence');
42
- if (options.malwareHostIntelligence)
43
- sources.push('malware_host_intelligence');
44
- if (options.malwareIocIntelligence)
45
- sources.push('malware_ioc_intelligence');
46
- }
47
- else if (classified.type === 'ipv4' || classified.type === 'ipv6') {
48
- sources.push('reverse_dns');
49
- }
50
- return Object.freeze(sources);
42
+ return plannedLookupSources(classified.type, 'deep', options);
51
43
  }
52
- function normalizedState(source, outcome, value) {
44
+ function normalizedState(source, outcome, value, requestedDomain, requested = true) {
53
45
  if (outcome === 'rejected')
54
46
  return 'error';
55
47
  if (value === null || value === undefined)
56
- return 'skipped';
48
+ return source === 'rdap' && requested ? 'unsupported' : 'skipped';
57
49
  if (source === 'rdap') {
58
50
  const rdap = record(value);
59
51
  if (rdap.upstreamStatus === 404)
60
52
  return 'not_found';
61
- return rdap.upstreamStatus === 200 ? 'success' : 'unsupported';
53
+ if (rdap.upstreamStatus !== 200 || !Object.keys(record(rdap.parsed)).length)
54
+ return 'error';
55
+ return sourceTruncated(rdap) ? 'partial' : 'success';
62
56
  }
63
57
  if (source === 'whois') {
64
- if (!Array.isArray(value))
65
- return 'error';
66
- if (value.length <= 1)
67
- return 'unsupported';
68
- return 'success';
58
+ const status = whoisCollectionStatus(value, requestedDomain);
59
+ return status === 'complete' ? 'success' : status;
69
60
  }
70
61
  if (source === 'domain_evidence') {
71
- return record(value).deepScanComplete === false ? 'partial' : 'success';
62
+ return record(value).deepScanComplete === true ? 'success' : 'partial';
72
63
  }
73
64
  if (THREAT_INTELLIGENCE_SOURCES.has(source)) {
74
65
  const intelligence = record(value);
@@ -81,16 +72,16 @@ function normalizedState(source, outcome, value) {
81
72
  }
82
73
  const status = record(value).status;
83
74
  if (typeof status === 'string' && DIRECT_STATES.has(status)) {
75
+ if (status === 'success' && (sourceTruncated(value) || record(value).complete === false))
76
+ return 'partial';
84
77
  return status;
85
78
  }
86
- if (status === 'complete' || status === 'observed' || status === 'found')
87
- return 'success';
88
79
  if (status === 'not_applicable' || status === 'disabled')
89
80
  return 'skipped';
90
- return 'success';
81
+ return 'error';
91
82
  }
92
- function normalizeLookupSourceSettlement(source, outcome, value) {
93
- const state = normalizedState(source, outcome, value);
83
+ function normalizeLookupSourceSettlement(source, outcome, value, requestedDomain, requested = true) {
84
+ const state = normalizedState(source, outcome, value, requestedDomain, requested);
94
85
  const sourceRecord = record(value);
95
86
  const threatObservation = THREAT_INTELLIGENCE_SOURCES.has(source)
96
87
  ? record(sourceRecord.observation)
@@ -102,12 +93,10 @@ function normalizeLookupSourceSettlement(source, outcome, value) {
102
93
  : null;
103
94
  const complete = threatObservation
104
95
  ? state !== 'error' && threatObservation.complete === true
105
- : !['partial', 'error', 'unavailable', 'rate_limited'].includes(state);
96
+ : ['success', 'not_found'].includes(state) && sourceRecord.complete !== false;
106
97
  const truncated = threatObservation
107
98
  ? threatObservation.truncated === true
108
- : sourceRecord.truncated === true
109
- || sourceRecord.recordsTruncated === true
110
- || sourceRecord.bodyTruncated === true;
99
+ : sourceTruncated(value);
111
100
  const limitation = outcome === 'rejected'
112
101
  ? 'This source did not complete. No absence or safety conclusion was inferred.'
113
102
  : state === 'skipped'
package/lib/lookup.mjs CHANGED
@@ -21,6 +21,7 @@ import { inspectSslblCertificate } from "./sslbl-intelligence.mjs";
21
21
  import { normalizeLookupSourceSettlement, plannedLookupProgressSources, } from "./lookup-source-progress.mjs";
22
22
  import { buildUnifiedLookupResponse } from "./lookup-response.mjs";
23
23
  import { createLookupTimingTracker } from "./lookup-diagnostics.mjs";
24
+ import { prepareSelectedLookupUrl } from "../packages/evidence/lookup-target.mjs";
24
25
  async function runUnifiedLookup(classified, options = {}) {
25
26
  const fetchRdap = options.fetchRdapRecord || fetchRdapRecord;
26
27
  const fetchRegistrarRdap = options.fetchRegistrarRdapRecord || fetchRegistrarRdapRecord;
@@ -44,19 +45,30 @@ async function runUnifiedLookup(classified, options = {}) {
44
45
  const securityTxtRequested = options.securityTxt === true;
45
46
  const featurePolicy = options.featurePolicy || networkFeaturePolicy();
46
47
  const timing = createLookupTimingTracker(!fast && !compact, options.now || Date.now);
47
- const measure = (source, operation) => (timing.measure(source, () => abortable(operation, options.signal)));
48
+ const startedCollectors = [];
49
+ const measure = (source, operation) => (timing.measure(source, () => abortable(() => {
50
+ const running = Promise.resolve(operation());
51
+ if (options.waitForStartedCollectors)
52
+ startedCollectors.push(running.catch(() => { }));
53
+ return running;
54
+ }, options.signal)));
48
55
  const rdapEnabled = featureDecision('rdap', featurePolicy).enabled;
49
56
  const whoisEnabled = featureDecision('whois', featurePolicy).enabled;
50
57
  const availabilityEnabled = featureDecision('availability', featurePolicy).enabled;
51
58
  const websiteProbeEnabled = featureDecision('website_probe', featurePolicy).enabled;
52
59
  const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
53
60
  const skipWhois = fast || !whoisEnabled;
61
+ const selectedUrl = options.selectedUrl === undefined ? undefined
62
+ : prepareSelectedLookupUrl(options.selectedUrl, classified.inputHostname ?? classified.value);
63
+ if (selectedUrl && (classified.type !== 'domain' || fast || compact || !availabilityEnabled || !websiteProbeEnabled)) {
64
+ throw new TypeError('Selected URL collection requires an enabled full Deep domain lookup.');
65
+ }
54
66
  const rdapPromise = rdapEnabled
55
- ? measure('rdap', () => fetchRdap(classified.type, classified.value))
67
+ ? measure('rdap', () => fetchRdap(classified.type, classified.value, options.signal ? { signal: options.signal } : {}))
56
68
  : Promise.resolve(null);
57
69
  const whoisPromise = skipWhois
58
70
  ? Promise.resolve(null)
59
- : measure('whois', () => fetchWhois(classified.value));
71
+ : measure('whois', () => fetchWhois(classified.value, options.signal ? { signal: options.signal } : {}));
60
72
  // Registrar RDAP is a separately attributed deep-lookup enrichment. It may
61
73
  // overlap the WHOIS chain, but it never joins the promises used to decide
62
74
  // availability and can add up to its own bounded timeout to a deep lookup.
@@ -76,7 +88,10 @@ async function runUnifiedLookup(classified, options = {}) {
76
88
  includeStructuredDataIdentity: !fast && !compact,
77
89
  includeTechnologyProfile: !fast,
78
90
  includeSecurityPosture: !compact,
91
+ ...(!fast && !compact ? { observationHostname: classified.inputHostname } : {}),
92
+ ...(selectedUrl ? { selectedUrl } : {}),
79
93
  featurePolicy,
94
+ ...(options.signal ? { signal: options.signal } : {}),
80
95
  rdapRecordPromise: rdapPromise,
81
96
  whoisChainPromise: whoisPromise,
82
97
  ...(selectedDnsResolvers ? {
@@ -155,7 +170,7 @@ async function runUnifiedLookup(classified, options = {}) {
155
170
  ]);
156
171
  const notify = (source, outcome, value) => {
157
172
  try {
158
- options.onSourceSettled?.(normalizeLookupSourceSettlement(source, outcome, value));
173
+ options.onSourceSettled?.(normalizeLookupSourceSettlement(source, outcome, value, classified.type === 'domain' ? classified.registrableDomain ?? classified.value : undefined, source !== 'rdap' || rdapEnabled));
159
174
  }
160
175
  catch {
161
176
  // Presentation callbacks must never change evidence collection.
@@ -171,30 +186,36 @@ async function runUnifiedLookup(classified, options = {}) {
171
186
  void Promise.resolve(sourcePromise).then((value) => notify(source, 'fulfilled', value), (error) => notify(source, 'rejected', error));
172
187
  }
173
188
  }
174
- return buildUnifiedLookupResponse({
175
- availabilityEnabled,
176
- availabilityPromise,
177
- classified,
178
- compact,
179
- dnsIntelligenceEnabled,
180
- fast,
181
- inspectCertificateWarningData,
182
- networkContextPromise,
183
- options,
184
- rdapEnabled,
185
- rdapPromise,
186
- registrarRdapPromise,
187
- reverseDnsEligible,
188
- reverseDnsPromise,
189
- securityTxtPromise,
190
- skipWhois,
191
- threatfoxIntelligencePromise,
192
- timing,
193
- urlhausIntelligencePromise,
194
- urlscanIntelligencePromise,
195
- whoisEnabled,
196
- whoisPromise,
197
- });
189
+ try {
190
+ return await buildUnifiedLookupResponse({
191
+ availabilityEnabled,
192
+ availabilityPromise,
193
+ classified,
194
+ compact,
195
+ dnsIntelligenceEnabled,
196
+ fast,
197
+ inspectCertificateWarningData,
198
+ networkContextPromise,
199
+ options,
200
+ rdapEnabled,
201
+ rdapPromise,
202
+ registrarRdapPromise,
203
+ reverseDnsEligible,
204
+ reverseDnsPromise,
205
+ securityTxtPromise,
206
+ skipWhois,
207
+ threatfoxIntelligencePromise,
208
+ timing,
209
+ urlhausIntelligencePromise,
210
+ urlscanIntelligencePromise,
211
+ whoisEnabled,
212
+ whoisPromise,
213
+ });
214
+ }
215
+ finally {
216
+ if (options.waitForStartedCollectors)
217
+ await Promise.all(startedCollectors);
218
+ }
198
219
  }
199
220
  export { runUnifiedLookup };
200
221
  export { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, LOOKUP_TIMING_VERSION, MAX_LOOKUP_TIMING_MS, LOOKUP_ERROR_CODES, } from "./lookup-diagnostics.mjs";
@@ -11,6 +11,7 @@ const OBSERVED_NETWORK_CONTEXT_VERSION = 1;
11
11
  const MAX_NETWORK_CIDRS = 16;
12
12
  const MAX_NETWORK_ATTEMPTS = 3;
13
13
  const MAX_NETWORK_ABUSE_ROUTES = 6;
14
+ const NETWORK_ABUSE_ROUTE_LIMITATION = 'The network abuse-route projection reached its local retention limit; additional published routes may exist.';
14
15
  const MAX_ENDPOINT_LENGTH = 2048;
15
16
  const MAX_DETAIL_LENGTH = 240;
16
17
  const MAX_NAME_LENGTH = 300;
@@ -144,20 +145,25 @@ function networkAbuseEntities(parsed) {
144
145
  const fallback = parsed.abuse && typeof parsed.abuse === 'object' && !Array.isArray(parsed.abuse)
145
146
  ? [parsed.abuse]
146
147
  : [];
147
- return [...roleEntities, ...fallback]
148
- .filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item))
149
- .slice(0, MAX_NETWORK_ABUSE_ROUTES);
148
+ return {
149
+ entities: [...roleEntities.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1), ...fallback]
150
+ .filter((item) => Boolean(item) && typeof item === 'object' && !Array.isArray(item)),
151
+ truncated: roleEntities.length > MAX_NETWORK_ABUSE_ROUTES + 1,
152
+ };
150
153
  }
151
154
  function projectNetworkAbuseRoutes(parsed, provenance) {
152
155
  const routes = new Map();
153
- for (const entity of networkAbuseEntities(parsed)) {
156
+ const inventory = networkAbuseEntities(parsed);
157
+ let truncated = inventory.truncated;
158
+ for (const entity of inventory.entities) {
154
159
  const emails = Array.isArray(entity.emails) ? entity.emails : [];
155
160
  const phones = Array.isArray(entity.phones) ? entity.phones : [];
161
+ truncated ||= emails.length > MAX_NETWORK_ABUSE_ROUTES + 1 || phones.length > MAX_NETWORK_ABUSE_ROUTES + 1;
156
162
  const candidates = [
157
163
  { channel: 'email', value: entity.email },
158
- ...emails.slice(0, MAX_NETWORK_ABUSE_ROUTES).map((value) => ({ channel: 'email', value })),
164
+ ...emails.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1).map((value) => ({ channel: 'email', value })),
159
165
  { channel: 'phone', value: entity.phone },
160
- ...phones.slice(0, MAX_NETWORK_ABUSE_ROUTES).map((value) => ({ channel: 'phone', value })),
166
+ ...phones.slice(0, MAX_NETWORK_ABUSE_ROUTES + 1).map((value) => ({ channel: 'phone', value })),
161
167
  ];
162
168
  for (const candidate of candidates) {
163
169
  const contact = normalizedAbuseContact(candidate.value, candidate.channel);
@@ -165,6 +171,10 @@ function projectNetworkAbuseRoutes(parsed, provenance) {
165
171
  continue;
166
172
  const key = `${candidate.channel}:${contact.toLowerCase()}`;
167
173
  if (!routes.has(key)) {
174
+ if (routes.size >= MAX_NETWORK_ABUSE_ROUTES) {
175
+ truncated = true;
176
+ continue;
177
+ }
168
178
  routes.set(key, {
169
179
  kind: 'network_hosting',
170
180
  channel: candidate.channel,
@@ -186,11 +196,14 @@ function projectNetworkAbuseRoutes(parsed, provenance) {
186
196
  ],
187
197
  });
188
198
  }
189
- if (routes.size >= MAX_NETWORK_ABUSE_ROUTES)
190
- return [...routes.values()];
191
199
  }
192
200
  }
193
- return [...routes.values()];
201
+ return {
202
+ routes: [...routes.values()].map((route) => truncated
203
+ ? { ...route, complete: false, truncated: true, limitations: [...route.limitations, NETWORK_ABUSE_ROUTE_LIMITATION] }
204
+ : route),
205
+ truncated,
206
+ };
194
207
  }
195
208
  function networkSummary(parsedValue, family) {
196
209
  const parsed = record(parsedValue);
@@ -204,10 +217,12 @@ function networkSummary(parsedValue, family) {
204
217
  const startAddress = publicAddress(parsed.startAddress, family);
205
218
  const endAddress = publicAddress(parsed.endAddress, family);
206
219
  const country = boundedString(parsed.country, 2);
207
- const truncated = parsed.serverTruncated === true
208
- || parsed.cidrsTruncated === true
209
- || parsed.entitiesTruncated === true
210
- || sourceCidrs.length > MAX_NETWORK_CIDRS;
220
+ const limitations = [
221
+ ...(parsed.serverTruncated === true ? ['The RDAP server declared that part of its response was truncated.'] : []),
222
+ ...(parsed.entitiesTruncated === true ? ['Some IP RDAP contact records or fields were omitted during bounded normalisation.'] : []),
223
+ ...(parsed.cidrsTruncated === true ? ['Some IP RDAP CIDR entries were invalid or exceeded the normalisation limit.'] : []),
224
+ ...(sourceCidrs.length > MAX_NETWORK_CIDRS ? ['The network CIDR summary reached its local retention limit.'] : []),
225
+ ];
211
226
  return {
212
227
  value: {
213
228
  handle: boundedString(parsed.handle, MAX_NAME_LENGTH),
@@ -220,8 +235,8 @@ function networkSummary(parsedValue, family) {
220
235
  networkType: boundedString(parsed.networkType, 160),
221
236
  databaseUpdatedAt: isoTimestamp(lifecycle.databaseUpdatedDateIso || lifecycle.databaseUpdatedDate),
222
237
  },
223
- truncated,
224
- serverTruncated: parsed.serverTruncated === true,
238
+ truncated: limitations.length > 0,
239
+ limitations,
225
240
  };
226
241
  }
227
242
  function baseContext(selection, input) {
@@ -299,28 +314,33 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
299
314
  if (rdap.httpStatus !== 200 || !Object.keys(parsed).length)
300
315
  throw new Error('IP RDAP returned no usable normalised object');
301
316
  const summary = networkSummary(parsed, selection.family);
302
- const partial = summary.truncated;
303
317
  const sourceObservedAt = rdap.fetchedAt || observedAt();
304
- const abuseRouting = projectNetworkAbuseRoutes(parsed, {
318
+ const routeProjection = projectNetworkAbuseRoutes(parsed, {
305
319
  endpoint: rdap.endpoint,
306
320
  observedAt: sourceObservedAt,
307
321
  selection,
308
- complete: !partial,
309
- truncated: partial,
322
+ complete: !summary.truncated,
323
+ truncated: summary.truncated,
310
324
  });
325
+ const incompleteReasons = [
326
+ ...summary.limitations,
327
+ ...(routeProjection.truncated ? [NETWORK_ABUSE_ROUTE_LIMITATION] : []),
328
+ ];
329
+ const partial = incompleteReasons.length > 0;
311
330
  return baseContext(selection, {
312
331
  status: partial ? 'partial' : 'success',
313
332
  observedAt: sourceObservedAt,
314
333
  durationMs,
315
334
  complete: !partial,
316
335
  truncated: partial,
317
- detail: 'The selected public endpoint address was mapped to its separately attributed IP RDAP registration.',
336
+ detail: partial
337
+ ? `Network registration was retained, but the source record is incomplete. ${incompleteReasons[0]}`
338
+ : 'The selected public endpoint address was mapped to its separately attributed IP RDAP registration.',
318
339
  limitations: [
340
+ ...incompleteReasons,
319
341
  'This identifies the registered network for one point-in-time endpoint address, not a definitive origin host or hosting provider.',
320
342
  'CDNs, reverse proxies, load balancers, shared hosting, and location-dependent DNS can present different networks.',
321
343
  'Network registration is an investigative lead and does not prove control, ownership, intent, or maliciousness.',
322
- ...(summary.serverTruncated ? ['The RDAP server declared that part of its response was truncated.'] : []),
323
- ...(sourceCidrsWereCapped(parsed) ? ['The network CIDR summary reached its local retention limit.'] : []),
324
344
  ],
325
345
  diagnostics: {
326
346
  requestCount: 1,
@@ -330,7 +350,7 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
330
350
  },
331
351
  rdap,
332
352
  network: summary.value,
333
- abuseRouting,
353
+ abuseRouting: routeProjection.routes,
334
354
  });
335
355
  }
336
356
  catch (error) {
@@ -348,8 +368,4 @@ async function collectObservedNetworkContext(availabilityValue, options = {}) {
348
368
  });
349
369
  }
350
370
  }
351
- function sourceCidrsWereCapped(parsed) {
352
- return parsed.cidrsTruncated === true
353
- || (Array.isArray(parsed.cidrs) && parsed.cidrs.length > MAX_NETWORK_CIDRS);
354
- }
355
371
  export { MAX_NETWORK_ATTEMPTS, MAX_NETWORK_ABUSE_ROUTES, MAX_NETWORK_CIDRS, OBSERVED_NETWORK_CONTEXT_VERSION, collectObservedNetworkContext, selectObservedNetworkAddress, };
@@ -1,107 +1,2 @@
1
- import { buildScoreEvidenceQuality, } from "./scoring-evidence-quality.mjs";
2
- export const OPPORTUNITY_MODEL_VERSION = 2;
3
- export const OPPORTUNITY_REVIEW_THRESHOLD = 70;
4
- const STATE_BASE = Object.freeze({
5
- available: 82,
6
- for_sale: 70,
7
- expiring: 52,
8
- registered: 22,
9
- });
10
- const STATE_LABELS = Object.freeze({
11
- available: 'available',
12
- for_sale: 'registered with a sale signal',
13
- expiring: 'registered in a lifecycle transition',
14
- registered: 'registered',
15
- });
16
- function finite(value) {
17
- return typeof value === 'number' && Number.isFinite(value) ? value : null;
18
- }
19
- function formatAge(days) {
20
- if (days < 60)
21
- return `${Math.max(0, Math.round(days))}d old`;
22
- const years = days / 365.25;
23
- return years < 1 ? `${Math.round(days / 30)}mo old` : `${years.toFixed(1)}y old`;
24
- }
25
- function addFactor(factors, dimensions, family, label, delta) {
26
- factors.push({ family, label, delta });
27
- dimensions.set(family, (dimensions.get(family) ?? 0) + delta);
28
- }
29
- export function normalizeOpportunityModelVersion(value) {
30
- return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= 1000 ? value : null;
31
- }
32
- // Opportunity is acquisition readiness, not price, value, eligibility, or a
33
- // prediction that a registration will lapse. It uses only bounded evidence
34
- // already collected for the current lookup.
35
- export function explainOpportunityScore(value) {
36
- if (!value || typeof value !== 'object' || Array.isArray(value))
37
- return null;
38
- const input = value;
39
- const state = input.availability ?? input.state;
40
- if (typeof state !== 'string' || !Object.hasOwn(STATE_BASE, state))
41
- return null;
42
- const factors = [];
43
- const dimensions = new Map();
44
- addFactor(factors, dimensions, 'registration', `Base readiness for “${STATE_LABELS[state] ?? state}”`, STATE_BASE[state] ?? 0);
45
- if (input.confidence === 'high')
46
- addFactor(factors, dimensions, 'registration', 'High-confidence registration decision', 8);
47
- else if (input.confidence === 'medium')
48
- addFactor(factors, dimensions, 'registration', 'Medium-confidence registration decision', 3);
49
- else if (input.confidence === 'low')
50
- addFactor(factors, dimensions, 'registration', 'Low-confidence registration decision', -8);
51
- if (state === 'for_sale')
52
- addFactor(factors, dimensions, 'listing', 'Bounded sale signal observed; listing remains unverified', 6);
53
- else if (input.activityStatus === 'parked')
54
- addFactor(factors, dimensions, 'listing', 'Parked page observed without a verified sale offer', 3);
55
- else if (state === 'registered' && input.activityStatus === 'active') {
56
- addFactor(factors, dimensions, 'listing', 'Active website suggests an in-use registration', -12);
57
- }
58
- if (input.hasPublicRegistrantContact === true) {
59
- addFactor(factors, dimensions, 'contactability', 'Public registrant contact route observed; acquisition suitability is not established', 10);
60
- }
61
- const expiresInDays = finite(input.expiresInDays);
62
- if (state === 'registered' && expiresInDays !== null && expiresInDays >= 0 && expiresInDays < 30) {
63
- addFactor(factors, dimensions, 'lifecycle', 'Published expiry is within 30 days; release is not implied', 4);
64
- }
65
- if (typeof input.privacyProtected === 'boolean') {
66
- addFactor(factors, dimensions, 'contactability', 'Registration privacy is neutral; it does not establish contactability', 0);
67
- }
68
- const ageDays = finite(input.domainAgeDays);
69
- if (ageDays !== null && ageDays >= 0) {
70
- addFactor(factors, dimensions, 'registration', `Domain age (${formatAge(ageDays)}) is context only`, 0);
71
- }
72
- const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
73
- const score = Math.max(0, Math.min(100, rawScore));
74
- const dimensionLabels = {
75
- registration: 'Registration certainty',
76
- listing: 'Listing readiness',
77
- contactability: 'Contactability',
78
- lifecycle: 'Lifecycle timing',
79
- };
80
- const observedFamilies = [...dimensions.keys()];
81
- const evidenceQuality = buildScoreEvidenceQuality(input, {
82
- expectedDepth: 'fast',
83
- observedFamilies,
84
- expectedFamilies: ['registration'],
85
- });
86
- return {
87
- modelVersion: OPPORTUNITY_MODEL_VERSION,
88
- score,
89
- rawScore,
90
- capped: score !== rawScore,
91
- factors,
92
- dimensions: [...dimensions.entries()].map(([id, contribution]) => ({ id, label: dimensionLabels[id], contribution })),
93
- evidenceQuality,
94
- };
95
- }
96
- export function computeOpportunityScore(value) {
97
- return explainOpportunityScore(value)?.score ?? null;
98
- }
99
- export function opportunityTone(score) {
100
- if (score === null)
101
- return 'neutral';
102
- if (score >= OPPORTUNITY_REVIEW_THRESHOLD)
103
- return 'good';
104
- if (score >= 40)
105
- return 'neutral';
106
- return 'warn';
107
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/opportunity-scoring.mjs";
@@ -1,5 +1,5 @@
1
1
  import { WHOISLEUTH_APPLICATION_VERSION } from "./application-version.mjs";
2
- import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "./project-metadata.mjs";
2
+ import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "../packages/analysis/project-metadata.mjs";
3
3
  export { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL };
4
4
  export const WHOISLEUTH_USER_AGENT_VERSION = WHOISLEUTH_APPLICATION_VERSION;
5
5
  export const WHOISLEUTH_USER_AGENT = `WHOISleuth/${WHOISLEUTH_USER_AGENT_VERSION} (+${WHOISLEUTH_REQUEST_POLICY_URL})`;
@@ -3,7 +3,10 @@
3
3
  // disclosures cannot drift from the enforced request boundaries.
4
4
  export const MAX_OUTBOUND_REDIRECTS = 5;
5
5
  export const HOMEPAGE_FETCH_TIMEOUT_MS = 6_000;
6
- export const MAX_HOMEPAGE_BYTES = 300_000;
6
+ // One MiB admits large static application pages while bounding bytes retained
7
+ // from each response. Parsing, derived outputs and request concurrency retain
8
+ // separate work limits; this is not a claim that larger pages lack evidence.
9
+ export const MAX_HOMEPAGE_BYTES = 1024 * 1024;
7
10
  export const FAVICON_FETCH_TIMEOUT_MS = 5_000;
8
11
  export const MAX_FAVICON_BYTES = 200_000;
9
12
  export const MAX_FAVICON_CANDIDATES = 4;