@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
@@ -0,0 +1,216 @@
1
+ export class CliGrammarError extends Error {
2
+ constructor(message) { super(message); this.name = 'CliGrammarError'; }
3
+ }
4
+ function humanList(values, conjunction = 'or') {
5
+ if (values.length < 2)
6
+ return values[0] ?? '';
7
+ if (values.length === 2)
8
+ return `${values[0]} ${conjunction} ${values[1]}`;
9
+ return `${values.slice(0, -1).join(', ')}, ${conjunction} ${values.at(-1)}`;
10
+ }
11
+ function valueError(specification) {
12
+ if (specification.valueKind === 'file') {
13
+ return new CliGrammarError(`${specification.option} requires one bounded file path.`);
14
+ }
15
+ if (specification.valueKind === 'integer') {
16
+ const baseRange = specification.integerRanges[0];
17
+ return new CliGrammarError(baseRange
18
+ ? `${specification.option} requires an integer from ${baseRange.minimum} to ${baseRange.maximum}.`
19
+ : `${specification.option} requires one bounded integer.`);
20
+ }
21
+ if (specification.values.length > 0) {
22
+ return new CliGrammarError(`${specification.option} requires ${humanList(specification.values)}.`);
23
+ }
24
+ return new CliGrammarError(`${specification.option} requires one bounded value.`);
25
+ }
26
+ function parseTokens(grammar, argv) {
27
+ const specificationByOption = new Map(grammar.options.map((specification) => [specification.option, specification]));
28
+ const options = new Map();
29
+ const positionals = [];
30
+ for (let index = 0; index < argv.length; index += 1) {
31
+ const argument = argv[index];
32
+ if (argument === '--') {
33
+ positionals.push(...argv.slice(index + 1));
34
+ break;
35
+ }
36
+ if (!argument.startsWith('-')) {
37
+ positionals.push(argument);
38
+ continue;
39
+ }
40
+ const specification = specificationByOption.get(argument);
41
+ if (!specification || specification.metaAction !== null) {
42
+ throw new CliGrammarError(`Unknown option "${argument}".`);
43
+ }
44
+ const existing = options.get(argument);
45
+ if (existing && specification.occurrence === 'once') {
46
+ throw new CliGrammarError(`${argument} may be supplied only once.`);
47
+ }
48
+ if (existing && specification.occurrence === 'idempotent')
49
+ continue;
50
+ let value = '';
51
+ if (specification.arity === 1) {
52
+ const candidate = argv[++index];
53
+ if (candidate === undefined || candidate === ''
54
+ || (!specification.acceptsOptionLikeValue && candidate.startsWith('-'))) {
55
+ throw valueError(specification);
56
+ }
57
+ value = candidate;
58
+ }
59
+ if (existing)
60
+ existing.push(value);
61
+ else
62
+ options.set(argument, [value]);
63
+ }
64
+ return { options, positionals };
65
+ }
66
+ function selectedIntegerRange(specification, options) {
67
+ return specification.integerRanges
68
+ .filter((range) => range.whenOptionPresent === null || options.has(range.whenOptionPresent))
69
+ .at(-1) ?? null;
70
+ }
71
+ function validateOptionValues(command, grammar, options) {
72
+ for (const specification of grammar.options) {
73
+ const values = options.get(specification.option);
74
+ if (!values || specification.arity === 0)
75
+ continue;
76
+ if (specification.valueKind === 'enum') {
77
+ if (values.some((value) => !specification.values.includes(value)))
78
+ throw valueError(specification);
79
+ continue;
80
+ }
81
+ if (specification.valueKind === 'integer') {
82
+ const range = selectedIntegerRange(specification, options);
83
+ if (!range)
84
+ throw new Error(`Integer option ${command} ${specification.option} has no applicable range.`);
85
+ for (const value of values) {
86
+ if (!/^\d+$/u.test(value))
87
+ throw valueError(specification);
88
+ const parsed = Number(value);
89
+ if (!Number.isSafeInteger(parsed) || parsed < range.minimum || parsed > range.maximum) {
90
+ throw new CliGrammarError(`${specification.option} must be from ${range.minimum} to ${range.maximum}.`);
91
+ }
92
+ }
93
+ continue;
94
+ }
95
+ if (specification.valueKind === 'policy_list') {
96
+ for (const value of values) {
97
+ const policies = [...new Set(value.split(',').map((item) => item.trim().toLowerCase()).filter(Boolean))];
98
+ if (!policies.length || policies.some((policy) => !specification.values.includes(policy))) {
99
+ throw new CliGrammarError(`${specification.option} for ${command} supports: ${specification.values.join(', ')}.`);
100
+ }
101
+ }
102
+ }
103
+ }
104
+ }
105
+ function allocatePositionals(command, grammar, rawPositionals) {
106
+ const specifications = grammar.positionals;
107
+ const maximum = specifications.reduce((sum, specification) => sum + specification.maximum, 0);
108
+ if (rawPositionals.length > maximum) {
109
+ throw new CliGrammarError(`${command} accepts at most ${maximum} positional value${maximum === 1 ? '' : 's'}.`);
110
+ }
111
+ const allocated = {};
112
+ let offset = 0;
113
+ for (const [specificationIndex, specification] of specifications.entries()) {
114
+ const remainingMinimum = specifications.slice(specificationIndex + 1)
115
+ .reduce((sum, later) => sum + later.minimum, 0);
116
+ const available = Math.max(0, rawPositionals.length - offset - remainingMinimum);
117
+ const count = Math.min(specification.maximum, available);
118
+ const values = Object.freeze(rawPositionals.slice(offset, offset + count));
119
+ offset += count;
120
+ if (values.length < specification.minimum) {
121
+ throw new CliGrammarError(`${command} requires ${specification.minimum} ${specification.name} value${specification.minimum === 1 ? '' : 's'}.`);
122
+ }
123
+ if (specification.valueKind === 'enum'
124
+ && values.some((value) => !specification.values.includes(value))) {
125
+ throw new CliGrammarError(`${command} ${specification.name} must be ${humanList(specification.values)}.`);
126
+ }
127
+ allocated[specification.name] = values;
128
+ }
129
+ return Object.freeze(allocated);
130
+ }
131
+ function present(options, option) {
132
+ return options.has(option);
133
+ }
134
+ function validateConstraint(command, constraint, options) {
135
+ if (constraint.kind === 'required') {
136
+ const missing = constraint.options.filter((option) => !present(options, option));
137
+ if (missing.length > 0)
138
+ throw new CliGrammarError(`${command} requires ${missing.join(', ')}.`);
139
+ return;
140
+ }
141
+ if (constraint.kind === 'mutually_exclusive') {
142
+ const selected = constraint.options.filter((option) => present(options, option));
143
+ if (selected.length > 1) {
144
+ throw new CliGrammarError(`${selected.join(' and ')} are mutually exclusive and may be supplied only once.`);
145
+ }
146
+ return;
147
+ }
148
+ if (constraint.kind === 'excludes_all') {
149
+ if (!present(options, constraint.option))
150
+ return;
151
+ const selected = constraint.excludedOptions.filter((option) => present(options, option));
152
+ if (selected.length > 0) {
153
+ throw new CliGrammarError(`${constraint.option} cannot be combined with ${selected.join(', ')}.`);
154
+ }
155
+ return;
156
+ }
157
+ if (constraint.kind === 'value_excludes') {
158
+ if (!options.get(constraint.option)?.includes(constraint.value))
159
+ return;
160
+ const selected = constraint.excludedOptions.filter((option) => present(options, option));
161
+ if (selected.length > 0) {
162
+ throw new CliGrammarError(`${constraint.option} ${constraint.value} cannot be combined with ${selected.join(', ')}.`);
163
+ }
164
+ return;
165
+ }
166
+ if (!present(options, constraint.option))
167
+ return;
168
+ const selected = constraint.requiredOptions.filter((option) => present(options, option));
169
+ if (constraint.kind === 'requires_all' && selected.length !== constraint.requiredOptions.length) {
170
+ throw new CliGrammarError(`${constraint.option} requires ${constraint.requiredOptions.join(' and ')}.`);
171
+ }
172
+ if (constraint.kind === 'requires_any' && selected.length === 0) {
173
+ throw new CliGrammarError(`${constraint.option} requires ${constraint.requiredOptions.join(' or ')}.`);
174
+ }
175
+ }
176
+ function validatePositionalRequirements(grammar, allocated, options) {
177
+ for (const specification of grammar.positionals) {
178
+ if (specification.requiredWhenOptions.some((option) => options.has(option))
179
+ && (allocated[specification.name]?.length ?? 0) === 0) {
180
+ throw new CliGrammarError(`${specification.requiredWhenOptions.join(' or ')} requires a positional ${specification.name}.`);
181
+ }
182
+ }
183
+ }
184
+ function freezeOptions(options) {
185
+ return new Map([...options].map(([option, values]) => [option, Object.freeze([...values])]));
186
+ }
187
+ export function parseCliGrammar(command, grammar, argv) {
188
+ const parsed = parseTokens(grammar, argv);
189
+ const options = freezeOptions(parsed.options);
190
+ validateOptionValues(command, grammar, options);
191
+ const allocated = allocatePositionals(command, grammar, parsed.positionals);
192
+ validatePositionalRequirements(grammar, allocated, options);
193
+ for (const constraint of grammar.constraints) {
194
+ validateConstraint(command, constraint, options);
195
+ }
196
+ for (const option of ['--config', '--profile']) {
197
+ const specification = grammar.options.find((candidate) => candidate.option === option);
198
+ if (specification?.scope === 'common' && options.has(option)) {
199
+ throw new CliGrammarError(`${option} must be resolved before command parsing.`);
200
+ }
201
+ }
202
+ const allPositionals = Object.freeze([...parsed.positionals]);
203
+ return Object.freeze({
204
+ command,
205
+ allPositionals,
206
+ hasOption: (option) => options.has(option),
207
+ optionValue: (option) => options.get(option)?.at(-1) ?? null,
208
+ optionValues: (option) => options.get(option) ?? Object.freeze([]),
209
+ integerOption: (option) => {
210
+ const value = options.get(option)?.at(-1);
211
+ return value === undefined ? null : Number(value);
212
+ },
213
+ positionalValue: (name) => allocated[name]?.[0] ?? null,
214
+ positionalValues: (name) => allocated[name] ?? Object.freeze([]),
215
+ });
216
+ }
@@ -0,0 +1,27 @@
1
+ const MAX_CT_QUERY_LENGTH = 200;
2
+ const CT_QUERY_ERROR_CODE = 'INVALID_CT_QUERY';
3
+ const CT_QUERY_ERROR_MESSAGE = `Certificate Transparency query must be at most ${MAX_CT_QUERY_LENGTH} characters and contain no control or invisible formatting characters.`;
4
+ const UNSAFE_QUERY_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
5
+ class CtQueryError extends Error {
6
+ code = CT_QUERY_ERROR_CODE;
7
+ constructor() {
8
+ super(CT_QUERY_ERROR_MESSAGE);
9
+ this.name = 'CtQueryError';
10
+ }
11
+ }
12
+ function normalizeCtQuery(value) {
13
+ if (value === undefined || value === null)
14
+ return '';
15
+ if (typeof value !== 'string'
16
+ || value.length > MAX_CT_QUERY_LENGTH
17
+ || UNSAFE_QUERY_CHARACTER_RE.test(value)) {
18
+ throw new CtQueryError();
19
+ }
20
+ return value.trim();
21
+ }
22
+ function isCtQueryError(value) {
23
+ return value instanceof Error
24
+ && 'code' in value
25
+ && value.code === CT_QUERY_ERROR_CODE;
26
+ }
27
+ export { CT_QUERY_ERROR_CODE, CT_QUERY_ERROR_MESSAGE, CtQueryError, isCtQueryError, MAX_CT_QUERY_LENGTH, normalizeCtQuery, };
@@ -0,0 +1,9 @@
1
+ // Shared response-contract bounds for certificate-log search. The collector
2
+ // and browser normalizer import these values so a valid backend response is
3
+ // not accidentally clipped by a stale client-side mirror.
4
+ export const MAX_CT_RESPONSE_RESULTS = 500;
5
+ export const MAX_CT_RESPONSE_HOSTNAMES_PER_MATCH = 50;
6
+ export const MAX_CT_RESPONSE_TIMESTAMP_LENGTH = 64;
7
+ export const MAX_CT_RESPONSE_CERTIFICATE_GROUPS = 100;
8
+ export const MAX_CT_RESPONSE_DOMAINS_PER_GROUP = 20;
9
+ export const MAX_CT_RESPONSE_HOSTNAMES_PER_GROUP = 30;
@@ -0,0 +1,124 @@
1
+ // Converts already-returned, separately attributed external observations into
2
+ // a conservative Risk contribution. Unknown providers and lone publisher
3
+ // families never contribute. Two adapters operated by the same publisher are
4
+ // deliberately one corroboration source, even when their datasets differ.
5
+ export const EXTERNAL_INTELLIGENCE_CALIBRATION_VERSION = 1;
6
+ export const EXTERNAL_INTELLIGENCE_RECENT_DAYS = 90;
7
+ const DAY_MS = 24 * 60 * 60 * 1000;
8
+ const MAX_CLOCK_SKEW_MS = DAY_MS;
9
+ const MAX_PROVIDERS = 10;
10
+ const MAX_FINDINGS_PER_PROVIDER = 100;
11
+ const POSITIVE_STATES = new Set(['success', 'partial']);
12
+ const QUALIFYING_CATEGORIES = new Set(['phishing', 'malware']);
13
+ // Only built-in provider IDs can affect the built-in score. The two community
14
+ // malware datasets share one publisher family and therefore cannot corroborate
15
+ // one another by themselves.
16
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
17
+ import { canonicalRegistrableDomain } from "./registrable-domain.mjs";
18
+ import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_SCHEMA, } from "./threat-intelligence-types.mjs";
19
+ const PUBLISHER_FAMILIES = Object.freeze({
20
+ urlscan_search: 'archived-scan-publisher',
21
+ urlhaus_host: 'community-malware-publisher',
22
+ threatfox_domain_ioc: 'community-malware-publisher',
23
+ });
24
+ function record(value) {
25
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
26
+ }
27
+ function timestamp(value) {
28
+ const normalized = normalizeExplicitIsoTimestamp(value);
29
+ return normalized ? Date.parse(normalized) : null;
30
+ }
31
+ function providerEvidence(value, expectedDomain) {
32
+ const provider = record(value);
33
+ const identity = record(provider?.provider);
34
+ const observation = record(provider?.observation);
35
+ const target = record(provider?.target);
36
+ const providerId = typeof identity?.id === 'string' ? identity.id : '';
37
+ const publisherFamily = Object.hasOwn(PUBLISHER_FAMILIES, providerId)
38
+ ? PUBLISHER_FAMILIES[providerId]
39
+ : null;
40
+ if (provider?.schema !== THREAT_INTELLIGENCE_SCHEMA
41
+ || provider.version !== THREAT_INTELLIGENCE_CONTRACT_VERSION
42
+ || target?.type !== 'domain'
43
+ || target.exposure !== 'registrable_domain'
44
+ || target.value !== expectedDomain
45
+ || !publisherFamily
46
+ || typeof provider?.state !== 'string'
47
+ || !POSITIVE_STATES.has(provider.state)
48
+ || !Array.isArray(provider.findings))
49
+ return null;
50
+ const observedAt = timestamp(observation?.observedAt);
51
+ let latestFindingAt = null;
52
+ let qualifyingFindings = 0;
53
+ for (const item of provider.findings.slice(0, MAX_FINDINGS_PER_PROVIDER)) {
54
+ const finding = record(item);
55
+ if (!finding || typeof finding.category !== 'string' || !QUALIFYING_CATEGORIES.has(finding.category))
56
+ continue;
57
+ const firstObservedAt = timestamp(finding.firstObservedAt);
58
+ const lastObservedAt = timestamp(finding.lastObservedAt);
59
+ if (firstObservedAt !== null && lastObservedAt !== null && firstObservedAt > lastObservedAt)
60
+ continue;
61
+ qualifyingFindings += 1;
62
+ const candidate = lastObservedAt ?? firstObservedAt;
63
+ if (candidate !== null && (latestFindingAt === null || candidate > latestFindingAt))
64
+ latestFindingAt = candidate;
65
+ }
66
+ if (!qualifyingFindings)
67
+ return null;
68
+ let ageDays = null;
69
+ if (observedAt !== null && latestFindingAt !== null && latestFindingAt <= observedAt + MAX_CLOCK_SKEW_MS) {
70
+ ageDays = Math.max(0, Math.floor((observedAt - latestFindingAt) / DAY_MS));
71
+ }
72
+ return {
73
+ providerId,
74
+ publisherFamily,
75
+ qualifyingFindings,
76
+ lastObservedAt: latestFindingAt === null ? null : new Date(latestFindingAt).toISOString(),
77
+ ageDays,
78
+ recent: ageDays !== null && ageDays <= EXTERNAL_INTELLIGENCE_RECENT_DAYS,
79
+ };
80
+ }
81
+ export function calibrateExternalIntelligenceRisk(value, expectedDomainValue) {
82
+ const envelope = record(value);
83
+ const expectedDomain = canonicalRegistrableDomain(expectedDomainValue);
84
+ const providers = expectedDomain
85
+ && envelope?.version === THREAT_INTELLIGENCE_ENVELOPE_VERSION
86
+ && Array.isArray(envelope.providers)
87
+ ? envelope.providers.slice(0, MAX_PROVIDERS)
88
+ : [];
89
+ const byProvider = new Map();
90
+ for (const item of providers) {
91
+ const evidence = providerEvidence(item, expectedDomain ?? '');
92
+ if (!evidence || byProvider.has(evidence.providerId))
93
+ continue;
94
+ byProvider.set(evidence.providerId, evidence);
95
+ }
96
+ const sources = [...byProvider.values()].sort((a, b) => a.providerId.localeCompare(b.providerId));
97
+ const publisherFamilies = new Set(sources.map((source) => source.publisherFamily));
98
+ const recentPublisherFamilies = new Set(sources.filter((source) => source.recent).map((source) => source.publisherFamily));
99
+ const knownAges = sources.map((source) => source.ageDays).filter((age) => age !== null);
100
+ const independentPublisherCount = publisherFamilies.size;
101
+ const recentPublisherCount = recentPublisherFamilies.size;
102
+ let contribution = 0;
103
+ let factor = null;
104
+ if (independentPublisherCount >= 2) {
105
+ contribution = recentPublisherCount >= 2 ? 18 : 10;
106
+ factor = {
107
+ label: recentPublisherCount >= 2
108
+ ? 'Corroborated recent external phishing/malware records'
109
+ : 'Corroborated external phishing/malware records',
110
+ delta: contribution,
111
+ };
112
+ }
113
+ return {
114
+ version: EXTERNAL_INTELLIGENCE_CALIBRATION_VERSION,
115
+ contribution,
116
+ factor,
117
+ eligibleProviderCount: sources.length,
118
+ independentPublisherCount,
119
+ recentPublisherCount,
120
+ freshestAgeDays: knownAges.length ? Math.min(...knownAges) : null,
121
+ unknownAgeProviderCount: sources.filter((source) => source.ageDays === null).length,
122
+ sources,
123
+ };
124
+ }
@@ -0,0 +1,8 @@
1
+ // Browser-safe bounds for the rich HTTP evidence contract and its compact
2
+ // browser-local projection. Both producers and consumers import these values
3
+ // so valid retained evidence is not rejected by a stale projection.
4
+ import { MAX_OUTBOUND_HTTP_URL_CHARACTERS } from "../contracts/http-url.mjs";
5
+ export const MAX_HTTP_EVIDENCE_REDIRECTS = 5;
6
+ export const MAX_HTTP_ATTEMPTS = 2;
7
+ export const MAX_HTTP_PROVENANCE_URL = MAX_OUTBOUND_HTTP_URL_CHARACTERS;
8
+ export const MAX_HTTP_ERROR_LENGTH = 180;
@@ -0,0 +1,10 @@
1
+ // Browser-safe collection bounds shared by the network evidence producers,
2
+ // the Lookup response trust boundary, and the display projection.
3
+ export const MAX_LOOKUP_DNS_RECORDS_PER_TYPE = 16;
4
+ export const MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS = 8;
5
+ export const TLS_PROFILE_VERSION = 3;
6
+ export const MAX_LOOKUP_TLS_CHAIN_CERTIFICATES = 8;
7
+ export const MAX_LOOKUP_TLS_ALT_NAMES = 50;
8
+ export const MAX_LOOKUP_TLS_NAME_VALUES = 4;
9
+ export const MAX_LOOKUP_TLS_FINDINGS = 5;
10
+ export const MAX_LOOKUP_TLS_CERTIFICATE_POLICIES = 16;
@@ -0,0 +1,107 @@
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
+ }
@@ -0,0 +1,34 @@
1
+ // Browser-safe comparison contract for the 64-bit perceptual hashes produced
2
+ // by the bounded image decoder. Keeping comparison and informativeness here
3
+ // prevents Node and browser consumers from drifting into different matches.
4
+ export const PERCEPTUAL_HASH_HEX_RE = /^[0-9a-f]{16}$/u;
5
+ export const MIN_INFORMATIVE_HASH_BITS = 10;
6
+ const POPCOUNT = new Uint8Array(16);
7
+ for (let index = 0; index < POPCOUNT.length; index += 1) {
8
+ POPCOUNT[index] = (index & 1) + ((index >> 1) & 1) + ((index >> 2) & 1) + ((index >> 3) & 1);
9
+ }
10
+ export function isPerceptualHash(value) {
11
+ return typeof value === 'string' && PERCEPTUAL_HASH_HEX_RE.test(value);
12
+ }
13
+ function popcountHex(hex) {
14
+ let bits = 0;
15
+ for (let index = 0; index < hex.length; index += 1) {
16
+ bits += POPCOUNT[parseInt(hex.charAt(index), 16)] ?? 0;
17
+ }
18
+ return bits;
19
+ }
20
+ export function isInformativePerceptualHash(value) {
21
+ if (!isPerceptualHash(value))
22
+ return false;
23
+ const bits = popcountHex(value);
24
+ return bits >= MIN_INFORMATIVE_HASH_BITS && bits <= 64 - MIN_INFORMATIVE_HASH_BITS;
25
+ }
26
+ export function hammingDistanceHex(left, right) {
27
+ if (!isPerceptualHash(left) || !isPerceptualHash(right))
28
+ return null;
29
+ let distance = 0;
30
+ for (let index = 0; index < 16; index += 1) {
31
+ distance += POPCOUNT[parseInt(left.charAt(index), 16) ^ parseInt(right.charAt(index), 16)] ?? 0;
32
+ }
33
+ return distance;
34
+ }
@@ -0,0 +1,29 @@
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
+ }
@@ -0,0 +1,8 @@
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`;
@@ -0,0 +1,56 @@
1
+ import { getDomain, getPublicSuffix } from 'tldts';
2
+ import { isValidAsciiDomainName } from "../contracts/domain-name.mjs";
3
+ import { MAX_DOMAIN_NAME_LENGTH, MAX_DOMAIN_LABEL_LENGTH } from "../contracts/domain-name.mjs";
4
+ // Reserve the shortest generated name and its separating dot.
5
+ export const MAX_DISCOVERY_SUFFIX_LENGTH = MAX_DOMAIN_NAME_LENGTH - 2;
6
+ /** ICANN suffix rules for a plain ASCII hostname; private suffixes do not change identity. */
7
+ export function publicSuffixForAsciiHostname(value) {
8
+ if (!isValidAsciiDomainName(value, { requireDot: false, requireLowercase: true }))
9
+ return null;
10
+ return getPublicSuffix(value);
11
+ }
12
+ /** Preserve reserved single labels; compound suffixes must match the local ICANN suffix rules. */
13
+ export function normalizeDiscoverySuffix(raw) {
14
+ if (typeof raw !== 'string' || raw.length > MAX_DISCOVERY_SUFFIX_LENGTH + 1 || /[\u0000-\u001f\u007f]/u.test(raw))
15
+ return null;
16
+ const value = raw.trim().toLowerCase().replace(/^\./u, '');
17
+ if (!value || value.length > MAX_DISCOVERY_SUFFIX_LENGTH)
18
+ return null;
19
+ if (value.length <= MAX_DOMAIN_LABEL_LENGTH && /^[a-z]{2,}$/u.test(value))
20
+ return value;
21
+ return value.includes('.') && publicSuffixForAsciiHostname(value) === value ? value : null;
22
+ }
23
+ // Returns one canonical registrable-domain identity for an already public DNS
24
+ // hostname. Callers retain the original hostname separately when subdomain
25
+ // context matters; authority, evidence binding and scoring use this value.
26
+ export function canonicalRegistrableDomain(value) {
27
+ if (typeof value !== 'string')
28
+ return null;
29
+ const trimmed = value.trim();
30
+ if (!trimmed || /[\u0000-\u001f\u007f]/u.test(trimmed))
31
+ return null;
32
+ const withoutScheme = trimmed.replace(/^[a-z]+:\/\//iu, '').split(/[/?#]/u)[0] ?? '';
33
+ if (withoutScheme.endsWith('..'))
34
+ return null;
35
+ const candidate = withoutScheme.replace(/\.$/u, '');
36
+ if (!candidate)
37
+ return null;
38
+ let hostname;
39
+ try {
40
+ hostname = new URL(`https://${candidate}/`).hostname.toLowerCase().replace(/\.$/u, '');
41
+ }
42
+ catch {
43
+ return null;
44
+ }
45
+ if (!isValidAsciiDomainName(hostname, { requireDot: true, requireLowercase: true }))
46
+ return null;
47
+ // Keep this boundary identical to classifyQuery() and the collection
48
+ // runtime: registrable identity follows the ICANN Public Suffix List, while
49
+ // privately operated suffixes remain ordinary host context.
50
+ const domain = getDomain(hostname);
51
+ return domain
52
+ && isValidAsciiDomainName(domain, { requireDot: true, requireLowercase: true })
53
+ && (hostname === domain || hostname.endsWith(`.${domain}`))
54
+ ? domain
55
+ : null;
56
+ }