@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,170 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { lstat, open, realpath, unlink } from 'node:fs/promises';
3
+ import { basename, dirname, join, resolve } from 'node:path';
4
+ import { decodeBoundedUtf8, readBoundedRegularFile } from "../lib/bounded-file.mjs";
5
+ import { CliUsageError } from "./errors.mjs";
6
+ import { writePrivateFile } from "./output-file.mjs";
7
+ function missing(error) {
8
+ return Boolean(error && typeof error === 'object' && 'code' in error && error.code === 'ENOENT');
9
+ }
10
+ async function canonicalPath(value, allowMissing, label) {
11
+ const absolute = resolve(value);
12
+ if (basename(absolute).endsWith('.workflow.lock'))
13
+ throw new CliUsageError('The .workflow.lock suffix is reserved for workflow file ownership.');
14
+ try {
15
+ const metadata = await lstat(absolute);
16
+ if (!metadata.isFile() || metadata.nlink !== 1) {
17
+ throw new CliUsageError(`${label} files must be regular files without symbolic or additional hard links.`);
18
+ }
19
+ return await realpath(absolute);
20
+ }
21
+ catch (error) {
22
+ if (!allowMissing || !missing(error))
23
+ throw error;
24
+ return join(await realpath(dirname(absolute)), basename(absolute));
25
+ }
26
+ }
27
+ async function snapshot(path, maximumBytes, label, signal) {
28
+ let before;
29
+ try {
30
+ before = await lstat(path);
31
+ }
32
+ catch (error) {
33
+ if (missing(error))
34
+ return null;
35
+ throw error;
36
+ }
37
+ if (!before.isFile() || before.nlink !== 1)
38
+ throw new CliUsageError(`${label} files must be regular files without links.`);
39
+ const bytes = await readBoundedRegularFile(path, {
40
+ maximumBytes, label: `${label} file`, ...(signal ? { signal } : {}),
41
+ });
42
+ const after = await lstat(path);
43
+ if (!after.isFile() || after.nlink !== 1 || before.dev !== after.dev || before.ino !== after.ino
44
+ || before.mtimeMs !== after.mtimeMs || before.ctimeMs !== after.ctimeMs || before.size !== after.size) {
45
+ throw new CliUsageError(`${label} file changed while it was being read.`);
46
+ }
47
+ return {
48
+ identity: { device: after.dev, inode: after.ino, modified: after.mtimeMs, changed: after.ctimeMs,
49
+ bytes: bytes.length, sha256: createHash('sha256').update(bytes).digest('hex') },
50
+ text: decodeBoundedUtf8(bytes, `${label} file`),
51
+ };
52
+ }
53
+ async function ownsLease(lease) {
54
+ try {
55
+ const current = await lstat(lease.path);
56
+ return current.isFile() && current.nlink === 1 && current.dev === lease.device && current.ino === lease.inode;
57
+ }
58
+ catch (error) {
59
+ if (missing(error))
60
+ return false;
61
+ throw error;
62
+ }
63
+ }
64
+ /** One selected document write, holding source and destination ownership until publication. */
65
+ export async function prepareLocalDocumentWrite(options) {
66
+ if (![options.maximumInputBytes, options.maximumOutputBytes].every(value => Number.isSafeInteger(value) && value > 0)) {
67
+ throw new TypeError('Local document byte limits must be positive safe integers.');
68
+ }
69
+ const label = options.label;
70
+ const leases = [];
71
+ let released = false;
72
+ let published = false;
73
+ async function release() {
74
+ released = true;
75
+ let unresolved = 0;
76
+ for (const lease of leases.splice(0).reverse()) {
77
+ try {
78
+ if (await ownsLease(lease))
79
+ await unlink(lease.path);
80
+ else
81
+ unresolved += 1;
82
+ }
83
+ catch {
84
+ unresolved += 1;
85
+ }
86
+ }
87
+ return unresolved;
88
+ }
89
+ try {
90
+ options.signal?.throwIfAborted();
91
+ const destination = await canonicalPath(options.destination, true, label);
92
+ const source = options.source ? await canonicalPath(options.source, false, label) : null;
93
+ if (source === destination && options.allowSourceReplacement === false) {
94
+ throw new CliUsageError(`${label} review output must use a different path from its source file.`);
95
+ }
96
+ const paths = [...new Set([destination, ...(source ? [source] : [])])].sort();
97
+ for (const path of paths) {
98
+ options.signal?.throwIfAborted();
99
+ const leasePath = join(dirname(path), `.${basename(path)}.workflow.lock`);
100
+ let handle;
101
+ try {
102
+ handle = await open(leasePath, 'wx', 0o600);
103
+ }
104
+ catch (error) {
105
+ if (error && typeof error === 'object' && 'code' in error && error.code === 'EEXIST') {
106
+ throw new CliUsageError(`${label} is already locked. Wait for its owner; after an interrupted process, inspect and remove only its abandoned .workflow.lock file.`);
107
+ }
108
+ throw error;
109
+ }
110
+ try {
111
+ const identity = await handle.stat();
112
+ leases.push({ path: leasePath, device: identity.dev, inode: identity.ino });
113
+ await handle.writeFile(`${JSON.stringify({ pid: process.pid })}\n`, 'utf8');
114
+ await handle.sync();
115
+ }
116
+ finally {
117
+ await handle.close();
118
+ }
119
+ }
120
+ const snapshots = new Map();
121
+ let sourceInput = null;
122
+ for (const path of paths) {
123
+ const value = await snapshot(path, options.maximumInputBytes, label, options.signal);
124
+ if (path === source) {
125
+ if (!value)
126
+ throw new CliUsageError(`${label} source file disappeared before it could be read.`);
127
+ sourceInput = value.text;
128
+ }
129
+ if (path === destination && value && !options.force) {
130
+ throw new CliUsageError(`${label} output already exists; use --force to replace it or choose another path.`);
131
+ }
132
+ snapshots.set(path, value?.identity ?? null);
133
+ }
134
+ async function assertUnchanged() {
135
+ if (released)
136
+ throw new CliUsageError(`${label} file ownership has already been released.`);
137
+ options.signal?.throwIfAborted();
138
+ for (const lease of leases) {
139
+ if (!await ownsLease(lease))
140
+ throw new CliUsageError(`${label} file ownership changed; output was not published.`);
141
+ }
142
+ for (const [path, expected] of snapshots) {
143
+ const actual = (await snapshot(path, options.maximumInputBytes, label, options.signal))?.identity ?? null;
144
+ if (JSON.stringify(actual) !== JSON.stringify(expected)) {
145
+ throw new CliUsageError(`${label} changed during execution; output was not published. Review the current files before resuming.`);
146
+ }
147
+ }
148
+ }
149
+ return Object.freeze({
150
+ sourceInput,
151
+ async publish(content) {
152
+ if (published || released)
153
+ throw new CliUsageError(`${label} file publication has already settled.`);
154
+ if (Buffer.byteLength(content, 'utf8') > options.maximumOutputBytes) {
155
+ throw new CliUsageError(`${label} output is limited to ${options.maximumOutputBytes} bytes.`);
156
+ }
157
+ await writePrivateFile(destination, content, { force: options.force, beforePublish: assertUnchanged });
158
+ published = true;
159
+ },
160
+ release,
161
+ });
162
+ }
163
+ catch (error) {
164
+ const unresolved = await release();
165
+ if (unresolved) {
166
+ throw new CliUsageError(`${label} file preparation failed and lease cleanup could not be verified. Inspect the selected directory before resuming.`);
167
+ }
168
+ throw error;
169
+ }
170
+ }
@@ -1,12 +1,24 @@
1
1
  import { constants as fsConstants } from 'node:fs';
2
2
  import { open, realpath } from 'node:fs/promises';
3
3
  import { isIP } from 'node:net';
4
+ import { createHash } from 'node:crypto';
5
+ import { Worker, isMainThread, parentPort, workerData } from 'node:worker_threads';
4
6
  import { Reader } from 'maxmind';
5
7
  import { formatIpPrefix, parseIpPrefix } from "../lib/ip-prefix.mjs";
6
8
  import { CliUsageError } from "./errors.mjs";
9
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
10
+ import { canonicalPublicIpAddress } from "../packages/evidence/public-address-policy.mjs";
7
11
  export const LOCAL_MMDB_QUERY_SCHEMA = 'whoisleuth.local-mmdb-query';
8
- export const LOCAL_MMDB_QUERY_VERSION = 1;
12
+ export const LOCAL_MMDB_QUERY_VERSION = 2;
13
+ export const LOCAL_MMDB_REVIEW_SCHEMA = 'whoisleuth.local-mmdb-review';
14
+ export const LOCAL_MMDB_REVIEW_VERSION = 1;
9
15
  export const MAX_LOCAL_MMDB_BYTES = 512 * 1024 * 1024;
16
+ export const LOCAL_MMDB_REVIEW_DEADLINE_MS = 10_000;
17
+ export const MAX_LOCAL_MMDB_REVIEW_BYTES = 8 * 1024;
18
+ const DAY_MS = 86_400_000;
19
+ const LOCAL_MMDB_WORKER_KIND = 'local-mmdb-review';
20
+ const SUPPORTED_DATABASE_TYPES = new Set(['GeoIP2-City', 'GeoLite2-City', 'GeoIP2-Country', 'GeoLite2-Country',
21
+ 'GeoIP2-ASN', 'GeoLite2-ASN', 'GeoIP2-ISP', 'GeoIP2-Enterprise']);
10
22
  function record(value) {
11
23
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
12
24
  }
@@ -19,16 +31,18 @@ function names(value) {
19
31
  const source = record(value);
20
32
  return boundedText(source.en, 120);
21
33
  }
22
- async function readStableMmdb(databasePath) {
34
+ async function readStableMmdb(databasePath, openFile = open) {
23
35
  let handle = null;
24
36
  try {
25
37
  const resolvedPath = await realpath(databasePath);
26
- handle = await open(resolvedPath, fsConstants.O_RDONLY | fsConstants.O_NONBLOCK | fsConstants.O_NOFOLLOW);
38
+ handle = await openFile(resolvedPath, fsConstants.O_RDONLY | fsConstants.O_NONBLOCK | fsConstants.O_NOFOLLOW);
27
39
  const before = await handle.stat();
28
40
  if (!before.isFile() || before.size <= 0 || before.size > MAX_LOCAL_MMDB_BYTES) {
29
41
  throw new CliUsageError(`The supplied MMDB must be a file no larger than ${MAX_LOCAL_MMDB_BYTES} bytes.`);
30
42
  }
31
- const database = Buffer.allocUnsafe(before.size);
43
+ // Dedicated backing storage can be transferred without copying a pooled
44
+ // buffer or exposing bytes outside the stable file's exact length.
45
+ const database = Buffer.allocUnsafeSlow(before.size);
32
46
  let offset = 0;
33
47
  while (offset < database.length) {
34
48
  const { bytesRead } = await handle.read(database, offset, database.length - offset, offset);
@@ -56,7 +70,7 @@ async function readStableMmdb(databasePath) {
56
70
  await handle?.close().catch(() => { });
57
71
  }
58
72
  }
59
- export async function reviewLocalMmdb(input, databasePath) {
73
+ function prepareQuery(input, checkedAt) {
60
74
  const address = boundedText(input.address, 64)?.toLowerCase() ?? '';
61
75
  const sourceLabel = boundedText(input.sourceLabel, 120);
62
76
  const databaseVersion = boundedText(input.databaseVersion, 80);
@@ -66,15 +80,79 @@ export async function reviewLocalMmdb(input, databasePath) {
66
80
  if (!sourceLabel || !databaseVersion || !license) {
67
81
  throw new CliUsageError('Local MMDB review requires bounded sourceLabel, databaseVersion, and licence metadata.');
68
82
  }
83
+ if (input.version !== undefined && input.version !== 1 && input.version !== LOCAL_MMDB_QUERY_VERSION) {
84
+ throw new CliUsageError('Local MMDB review requires a supported query version.');
85
+ }
86
+ if (input.version !== LOCAL_MMDB_QUERY_VERSION)
87
+ return { version: 1, address, sourceLabel, databaseVersion, license, checkedAt: null, freshnessPolicy: null };
88
+ const policy = record(input.freshnessPolicy);
89
+ const rationale = boundedText(policy.rationale, 240);
90
+ const reviewedAt = normalizeExplicitIsoTimestamp(checkedAt);
91
+ if (!reviewedAt || !rationale || /[\u0080-\u009f]/u.test(String(policy.rationale)) || !Number.isSafeInteger(policy.maxAgeDays) || Number(policy.maxAgeDays) <= 0
92
+ || Number(policy.maxAgeDays) > Math.floor(Number.MAX_SAFE_INTEGER / DAY_MS)) {
93
+ throw new CliUsageError('Current MMDB review requires an explicit review time and freshnessPolicy with positive whole maxAgeDays and a bounded plain-text rationale.');
94
+ }
95
+ return { version: 2, address, sourceLabel, databaseVersion, license, checkedAt: reviewedAt,
96
+ freshnessPolicy: { maxAgeDays: Number(policy.maxAgeDays), rationale } };
97
+ }
98
+ /** Intrinsic metadata is separate from the analyst's declared version/licence.
99
+ * The policy admits age for this review, not accuracy or an update guarantee. */
100
+ export function inspectLocalMmdbMetadata(value, byteLength, checkedAt, policy) {
101
+ const metadata = record(value);
102
+ const databaseType = boundedText(metadata.databaseType, 120);
103
+ const buildMs = metadata.buildEpoch instanceof Date ? metadata.buildEpoch.getTime() : NaN;
104
+ const reviewMs = Date.parse(normalizeExplicitIsoTimestamp(checkedAt) ?? '');
105
+ const integer = (candidate) => typeof candidate === 'number' && Number.isSafeInteger(candidate) && candidate >= 0;
106
+ if (!databaseType || metadata.databaseType !== databaseType || !Number.isSafeInteger(buildMs) || buildMs < 0 || buildMs % 1000 !== 0 || !Number.isFinite(reviewMs)
107
+ || !integer(metadata.binaryFormatMajorVersion) || !integer(metadata.binaryFormatMinorVersion)
108
+ || ![4, 6].includes(Number(metadata.ipVersion)) || typeof metadata.ipVersion !== 'number'
109
+ || !integer(metadata.nodeCount) || metadata.nodeCount < 1 || ![24, 28, 32].includes(Number(metadata.recordSize))
110
+ || typeof metadata.recordSize !== 'number' || !integer(metadata.searchTreeSize)
111
+ || metadata.searchTreeSize !== metadata.nodeCount * metadata.recordSize / 4 || metadata.searchTreeSize + 16 >= byteLength) {
112
+ return { metadata: null, ageDays: null, reason: 'invalid_metadata' };
113
+ }
114
+ const intrinsic = { databaseType, builtAt: new Date(buildMs).toISOString(),
115
+ binaryFormat: { major: metadata.binaryFormatMajorVersion, minor: metadata.binaryFormatMinorVersion }, ipVersion: metadata.ipVersion };
116
+ const ageDays = (reviewMs - buildMs) / DAY_MS;
117
+ const reason = metadata.binaryFormatMajorVersion !== 2 || metadata.binaryFormatMinorVersion !== 0 ? 'unsupported_format'
118
+ : !SUPPORTED_DATABASE_TYPES.has(databaseType) ? 'unsupported_database'
119
+ : ageDays < 0 ? 'future_database' : ageDays > policy.maxAgeDays ? 'stale_database' : 'admitted';
120
+ return { metadata: intrinsic, ageDays, reason };
121
+ }
122
+ const LIMITATIONS = Object.freeze([
123
+ 'This CLI-only result comes from an analyst-supplied local MMDB file. WHOISleuth does not bundle, download, update, license, or transmit the database.',
124
+ 'Geolocation and network attribution are provider estimates and may be stale, coarse, shared, or incorrect.',
125
+ 'A location or ASN association does not establish the operator, owner, user, intent, safety, or maliciousness of an address.',
126
+ ]);
127
+ function currentResult(query, identity, admission, reason, match = null) {
128
+ const state = match ? reason === 'matched' ? 'matched' : 'partial' : 'unavailable';
129
+ return { schema: LOCAL_MMDB_REVIEW_SCHEMA, version: LOCAL_MMDB_REVIEW_VERSION, state, reason,
130
+ completeness: state === 'matched' ? 'complete' : state === 'partial' ? 'partial' : 'unavailable',
131
+ address: query.address, match, source: { label: query.sourceLabel, version: query.databaseVersion, license: query.license },
132
+ database: { ...identity, metadata: admission.metadata },
133
+ freshness: { state: admission.reason === 'stale_database' ? 'stale' : admission.reason === 'admitted' ? 'current' : 'unknown',
134
+ checkedAt: query.checkedAt, ageDays: admission.ageDays, policy: query.freshnessPolicy },
135
+ limitations: [...LIMITATIONS, 'Declared source, version and licence labels are analyst claims; intrinsic metadata and digest do not authenticate the database or verify licensing.',
136
+ 'Freshness is checked against the explicitly justified age policy. A miss, rejected or stale database is unavailable context, not evidence of location or absence.'] };
137
+ }
138
+ function reviewBytes(query, database, identity) {
69
139
  let reader;
70
140
  try {
71
- reader = new Reader(await readStableMmdb(databasePath));
141
+ reader = new Reader(database);
72
142
  }
73
- catch (cause) {
74
- if (cause instanceof CliUsageError)
75
- throw cause;
143
+ catch {
144
+ if (query.version === 2)
145
+ return currentResult(query, identity, { metadata: null, ageDays: null, reason: 'invalid_metadata' }, 'invalid_database');
76
146
  throw new CliUsageError('The supplied file is not a readable MaxMind DB database.');
77
147
  }
148
+ const admission = query.version === 2 ? inspectLocalMmdbMetadata(reader.metadata, database.length, query.checkedAt, query.freshnessPolicy) : null;
149
+ if (admission && admission.reason !== 'admitted')
150
+ return currentResult(query, identity, admission, admission.reason);
151
+ if (admission && !canonicalPublicIpAddress(query.address))
152
+ return currentResult(query, identity, admission, 'unsupported_address');
153
+ if (admission && isIP(query.address) === 6 && admission.metadata?.ipVersion === 4)
154
+ return currentResult(query, identity, admission, 'unsupported_address_family');
155
+ const address = query.address;
78
156
  const [rawMatch, prefixLength] = reader.getWithPrefixLength(address);
79
157
  const match = record(rawMatch);
80
158
  const traits = record(match.traits);
@@ -84,9 +162,10 @@ export async function reviewLocalMmdb(input, databasePath) {
84
162
  const subdivision = record(subdivisions[0]);
85
163
  const city = record(match.city);
86
164
  const asnValue = match.autonomous_system_number ?? traits.autonomous_system_number;
87
- const asn = typeof asnValue === 'number' && Number.isSafeInteger(asnValue) && asnValue >= 0 ? asnValue : null;
165
+ const asn = typeof asnValue === 'number' && Number.isSafeInteger(asnValue) && asnValue >= 0
166
+ && (query.version === 1 || asnValue <= 4_294_967_295) ? asnValue : null;
88
167
  const prefix = parseIpPrefix(`${address}/${prefixLength}`);
89
- return Object.freeze({
168
+ const result = Object.freeze({
90
169
  state: rawMatch ? 'matched' : 'not_found',
91
170
  address,
92
171
  match: rawMatch ? Object.freeze({
@@ -97,11 +176,112 @@ export async function reviewLocalMmdb(input, databasePath) {
97
176
  asn,
98
177
  asName: boundedText(match.autonomous_system_organization ?? traits.autonomous_system_organization, 240),
99
178
  }) : null,
100
- source: Object.freeze({ label: sourceLabel, version: databaseVersion, license }),
101
- limitations: Object.freeze([
102
- 'This CLI-only result comes from an analyst-supplied local MMDB file. WHOISleuth does not bundle, download, update, license, or transmit the database.',
103
- 'Geolocation and network attribution are provider estimates and may be stale, coarse, shared, or incorrect.',
104
- 'A location or ASN association does not establish the operator, owner, user, intent, safety, or maliciousness of an address.',
105
- ]),
179
+ source: Object.freeze({ label: query.sourceLabel, version: query.databaseVersion, license: query.license }),
180
+ limitations: LIMITATIONS,
106
181
  });
182
+ if (!admission)
183
+ return result;
184
+ if (!rawMatch)
185
+ return currentResult(query, identity, admission, 'no_match');
186
+ if (!result.match || !prefix || !rawMatch || typeof rawMatch !== 'object' || Array.isArray(rawMatch))
187
+ return currentResult(query, identity, admission, 'invalid_record');
188
+ const rawCountry = country.iso_code;
189
+ const countryCode = boundedText(rawCountry, 2)?.toUpperCase() ?? null;
190
+ const projected = { ...result.match, countryCode: countryCode && /^[A-Z]{2}$/u.test(countryCode) ? countryCode : null };
191
+ if (![projected.countryCode, projected.region, projected.city, projected.asn, projected.asName].some(value => value !== null)) {
192
+ return currentResult(query, identity, admission, 'no_useful_record');
193
+ }
194
+ const invalid = rawCountry !== undefined && projected.countryCode === null
195
+ || asnValue !== undefined && asn === null
196
+ || match.country !== undefined && (!match.country || typeof match.country !== 'object' || Array.isArray(match.country))
197
+ || match.city !== undefined && (!match.city || typeof match.city !== 'object' || Array.isArray(match.city))
198
+ || match.subdivisions !== undefined && !Array.isArray(match.subdivisions)
199
+ || subdivision.names !== undefined && projected.region === null
200
+ || subdivision.iso_code !== undefined && projected.region === null
201
+ || city.names !== undefined && projected.city === null
202
+ || (match.autonomous_system_organization ?? traits.autonomous_system_organization) !== undefined && projected.asName === null;
203
+ return currentResult(query, identity, admission, invalid ? 'incomplete_record' : 'matched', projected);
204
+ }
205
+ function freezeResult(result) {
206
+ if (result.match)
207
+ Object.freeze(result.match);
208
+ Object.freeze(result.source);
209
+ Object.freeze(result.limitations);
210
+ if ('database' in result) {
211
+ if (result.database.metadata) {
212
+ Object.freeze(result.database.metadata.binaryFormat);
213
+ Object.freeze(result.database.metadata);
214
+ }
215
+ Object.freeze(result.database);
216
+ Object.freeze(result.freshness.policy);
217
+ Object.freeze(result.freshness);
218
+ }
219
+ return Object.freeze(result);
220
+ }
221
+ if (!isMainThread && parentPort && record(workerData).kind === LOCAL_MMDB_WORKER_KIND) {
222
+ const port = parentPort, request = workerData;
223
+ try {
224
+ const bytes = Buffer.from(request.bytes.buffer, request.bytes.byteOffset, request.bytes.byteLength);
225
+ const result = reviewBytes(request.query, bytes, request.identity);
226
+ if (Buffer.byteLength(JSON.stringify(result), 'utf8') > MAX_LOCAL_MMDB_REVIEW_BYTES)
227
+ throw new TypeError('Local result exceeds its output bound.');
228
+ port.postMessage({ result });
229
+ }
230
+ catch {
231
+ port.postMessage({ error: true });
232
+ }
233
+ finally {
234
+ request.bytes.fill(0);
235
+ port.close();
236
+ }
237
+ }
238
+ /** One disposable worker pre-empts the existing synchronous reader. It is not
239
+ * a sandbox and never receives a database path or inherited environment. */
240
+ export async function reviewLocalMmdb(input, databasePath, checkedAt = new Date().toISOString(), dependencies = {}) {
241
+ const query = prepareQuery(input, checkedAt);
242
+ const deadlineMs = dependencies.deadlineMs ?? LOCAL_MMDB_REVIEW_DEADLINE_MS;
243
+ if (!Number.isSafeInteger(deadlineMs) || deadlineMs <= 0 || deadlineMs > LOCAL_MMDB_REVIEW_DEADLINE_MS)
244
+ throw new CliUsageError('Invalid local database processing deadline.');
245
+ const database = await readStableMmdb(databasePath, dependencies.openFile);
246
+ const identity = { sha256: createHash('sha256').update(database).digest('hex'), byteLength: database.length };
247
+ const options = { workerData: { kind: LOCAL_MMDB_WORKER_KIND, query, bytes: database, identity },
248
+ transferList: [database.buffer], env: {}, execArgv: [], stdout: true, stderr: true,
249
+ resourceLimits: { maxOldGenerationSizeMb: 64, maxYoungGenerationSizeMb: 16, stackSizeMb: 4 } };
250
+ let worker;
251
+ try {
252
+ worker = dependencies.createWorker ? dependencies.createWorker(options) : new Worker(new URL(import.meta.url), options);
253
+ }
254
+ catch {
255
+ if (database.byteLength)
256
+ database.fill(0);
257
+ if (query.version === 2)
258
+ return freezeResult(currentResult(query, identity, { metadata: null, ageDays: null, reason: 'invalid_metadata' }, 'processing_unavailable'));
259
+ throw new CliUsageError('The supplied database could not be reviewed within its format and processing bounds.');
260
+ }
261
+ worker.stdout?.resume();
262
+ worker.stderr?.resume();
263
+ try {
264
+ return await new Promise((resolve, reject) => {
265
+ let settled = false;
266
+ const finish = (result, reason = 'processing_unavailable') => {
267
+ if (settled)
268
+ return;
269
+ settled = true;
270
+ clearTimeout(timer);
271
+ if (result)
272
+ resolve(freezeResult(result));
273
+ else if (query.version === 2)
274
+ resolve(freezeResult(currentResult(query, identity, { metadata: null, ageDays: null, reason: 'invalid_metadata' }, reason)));
275
+ else
276
+ reject(new CliUsageError('The supplied database could not be reviewed within its format and processing bounds.'));
277
+ };
278
+ const timer = setTimeout(() => finish(undefined, 'processing_deadline'), deadlineMs);
279
+ worker.once('message', (reply) => reply && 'result' in reply ? finish(reply.result) : finish());
280
+ worker.once('error', () => finish());
281
+ worker.once('exit', () => finish());
282
+ });
283
+ }
284
+ finally {
285
+ await worker.terminate();
286
+ }
107
287
  }
@@ -1,5 +1,5 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
2
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
3
3
  const LOOKALIKE_CALIBRATION_INPUT_SCHEMA = 'whoisleuth.lookalike-calibration-input';
4
4
  const LOOKALIKE_CALIBRATION_SCHEMA = 'whoisleuth.lookalike-calibration';
5
5
  const LOOKALIKE_CALIBRATION_VERSION = 1;
@@ -1,8 +1,8 @@
1
1
  import { parseSavedLookupDocument } from "./saved-lookup.mjs";
2
2
  import { safeTerminalValue } from "./formatters/terminal.mjs";
3
3
  import { compareRdapPublications, compareRegistrySources } from "../lib/registry-comparison.mjs";
4
- export const CLI_LOOKUP_BRIEF_SCHEMA = 'whoisleuth.cli.lookup-brief';
5
- export const CLI_LOOKUP_BRIEF_VERSION = 3;
4
+ import { CLI_LOOKUP_BRIEF_SCHEMA, CLI_LOOKUP_BRIEF_VERSION } from "../packages/contracts/cli-lookup-brief.mjs";
5
+ export { CLI_LOOKUP_BRIEF_SCHEMA, CLI_LOOKUP_BRIEF_VERSION };
6
6
  function record(value) {
7
7
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
8
8
  }
@@ -1,4 +1,5 @@
1
1
  import { StringDecoder } from 'node:string_decoder';
2
+ import { canReadInteractiveLine } from "./terminal-input.mjs";
2
3
  import { safeTerminalValue } from "./formatters/terminal.mjs";
3
4
  import { terminalPresentation, } from "./terminal-presentation.mjs";
4
5
  import { MIN_LOOKUP_BROWSER_HEIGHT, MAX_LOOKUP_BROWSER_PANELS, MAX_LOOKUP_BROWSER_SEARCH_BYTES, MAX_LOOKUP_BROWSER_SEARCH_MATCHES, MAX_LOOKUP_BROWSER_SEARCH_SCALARS, boundedBrowserDimensions, boundedSearchText, buildLookupBrowserHelpFrame, buildLookupBrowserFrame, buildLookupBrowserPanels, buildLookupBrowserSearchFrame, buildLookupCollectionFrame, findLookupBrowserMatches, renderedPanelLineOffset, renderLookupBrowser, terminalDisplayWidth, } from "./lookup-browser-view.mjs";
@@ -10,18 +11,10 @@ const MAX_PENDING_INPUT_LENGTH = 512;
10
11
  const MAX_LOOKUP_BROWSER_INPUT_CHUNK_BYTES = 2_048;
11
12
  const COMPLETE_CSI_RE = /^\u001b\[[0-?]*[ -/]*[@-~]/u;
12
13
  function canBrowseLookup(input, output, environment = process.env) {
13
- return input?.isTTY === true
14
- && output?.isTTY === true
15
- && typeof input.setRawMode === 'function'
16
- && typeof input.resume === 'function'
17
- && typeof input.pause === 'function'
18
- && typeof input.on === 'function'
19
- && typeof input.off === 'function'
20
- && typeof output.write === 'function'
21
- && !(Number.isFinite(output.columns) && Number(output.columns) < 40)
22
- && !(Number.isFinite(output.rows) && Number(output.rows) < MIN_LOOKUP_BROWSER_HEIGHT)
23
- && environment.TERM !== 'dumb'
24
- && !environment.CI;
14
+ return canReadInteractiveLine(input, output, environment)
15
+ && !(Number.isFinite(output?.columns) && Number(output?.columns) < 40)
16
+ && !(Number.isFinite(output?.rows) && Number(output?.rows) < MIN_LOOKUP_BROWSER_HEIGHT)
17
+ && environment.TERM !== 'dumb';
25
18
  }
26
19
  function runLookupBrowserSession(options) {
27
20
  const environment = options.environment || process.env;
@@ -2,7 +2,7 @@ import { abortable } from "../lib/abort.mjs";
2
2
  import { classifyQuery } from "../lib/classify.mjs";
3
3
  import { runUnifiedLookup } from "../lib/lookup.mjs";
4
4
  import { plannedLookupProgressSources } from "../lib/lookup-source-progress.mjs";
5
- import { CliUsageError, boundedCliErrorMessage } from "./errors.mjs";
5
+ import { CliUsageError } from "./errors.mjs";
6
6
  import EXIT_CODES from "./exit-codes.mjs";
7
7
  import { buildCliEvidenceExport } from "./export-evidence.mjs";
8
8
  import { formatLookupEvidenceHtml } from "./formatters/html.mjs";
@@ -17,6 +17,7 @@ import { evaluateCliFailPolicies, formatFailPolicyNotice } from "./fail-policy.m
17
17
  import { formatCliJunit } from "./ci-report.mjs";
18
18
  import { browseLookupOperation, canBrowseLookup } from "./lookup-browser.mjs";
19
19
  import { writePrivateFile } from "./output-file.mjs";
20
+ import { prepareLookupCollectionTarget, prepareSelectedLookupUrl } from "../packages/evidence/lookup-target.mjs";
20
21
  async function runLookupCommand(args, dependencies, context) {
21
22
  const browserInput = dependencies.stdin || process.stdin;
22
23
  const browserEnvironment = dependencies.environment || process.env;
@@ -35,23 +36,31 @@ async function runLookupCommand(args, dependencies, context) {
35
36
  });
36
37
  context.setEventProgress(eventProgress);
37
38
  eventProgress.emit({ event: 'started' });
38
- const query = args.query || await context.readSingleInput();
39
- if (!query)
40
- throw new CliUsageError('lookup requires one domain, IP address, or ASN as an argument or on stdin.');
39
+ const input = args.query || await context.readSingleInput();
40
+ if (!input)
41
+ throw new CliUsageError('lookup requires one domain, IP address, or ASN as an argument or on stdin.', 'missing_input');
41
42
  const classify = dependencies.classifyQuery || classifyQuery;
42
43
  const executeLookup = dependencies.runUnifiedLookup || runUnifiedLookup;
43
44
  let classified;
45
+ let query;
46
+ let selectedUrl;
44
47
  try {
48
+ query = prepareLookupCollectionTarget(input);
45
49
  classified = classify(query);
50
+ if (args.exactUrl) {
51
+ if (!args.deep || classified.type !== 'domain')
52
+ throw new TypeError();
53
+ selectedUrl = prepareSelectedLookupUrl(input, classified.inputHostname);
54
+ }
46
55
  }
47
- catch (error) {
48
- throw new CliUsageError(boundedCliErrorMessage(error, 'Invalid query'));
56
+ catch {
57
+ throw new CliUsageError(args.exactUrl ? '--exact-url requires --deep and an HTTP(S) domain URL without credentials or a custom port.' : 'Invalid domain, IP address, ASN or credential-free HTTP(S) URL.');
49
58
  }
50
59
  if ((args.output === 'markdown' || args.output === 'html') && classified.type !== 'domain') {
51
60
  throw new CliUsageError('Markdown and HTML reports support domain lookups only.');
52
61
  }
53
62
  if (args.plan) {
54
- const plan = buildCliLookupPlan(query, classified, args.deep);
63
+ const plan = buildCliLookupPlan(query, classified, args.deep, Boolean(selectedUrl));
55
64
  context.writeStdout(args.output === 'json'
56
65
  ? formatJsonDocument(plan)
57
66
  : context.terminal(formatCliLookupPlan(plan), args.color));
@@ -65,6 +74,18 @@ async function runLookupCommand(args, dependencies, context) {
65
74
  ...(args.vantageLabel ? { vantageLabel: args.vantageLabel } : {}),
66
75
  });
67
76
  };
77
+ const collect = (signal, onSourceSettled) => abortable(() => executeLookup(classified, {
78
+ fast: !args.deep,
79
+ compact: false,
80
+ ...(signal ? { signal } : {}),
81
+ ...(args.deep ? {
82
+ ...(selectedUrl ? { selectedUrl } : {}),
83
+ onSourceSettled: settlement => {
84
+ onSourceSettled(settlement);
85
+ eventProgress.emit({ event: 'source_settled', source: settlement.source, state: settlement.state });
86
+ },
87
+ } : {}),
88
+ }), signal);
68
89
  let document;
69
90
  if (args.browse === true) {
70
91
  const browse = dependencies.browseLookupOperation || browseLookupOperation;
@@ -79,22 +100,7 @@ async function runLookupCommand(args, dependencies, context) {
79
100
  plannedSources: args.deep ? plannedLookupProgressSources(classified) : [],
80
101
  ...(dependencies.signal ? { signal: dependencies.signal } : {}),
81
102
  collect: async ({ signal, onSourceSettled }) => {
82
- const result = await abortable(() => executeLookup(classified, args.deep
83
- ? {
84
- fast: false,
85
- compact: false,
86
- signal,
87
- onSourceSettled: (settlement) => {
88
- onSourceSettled(settlement);
89
- eventProgress.emit({ event: 'source_settled', source: settlement.source, state: settlement.state });
90
- },
91
- }
92
- : {
93
- fast: true,
94
- compact: false,
95
- signal,
96
- }), signal);
97
- return buildDocument(result);
103
+ return buildDocument(await collect(signal, onSourceSettled));
98
104
  },
99
105
  });
100
106
  if (args.saveLookup) {
@@ -115,22 +121,10 @@ async function runLookupCommand(args, dependencies, context) {
115
121
  let settledSources = 0;
116
122
  let result;
117
123
  try {
118
- result = await abortable(() => executeLookup(classified, args.deep
119
- ? {
120
- fast: false,
121
- compact: false,
122
- ...(dependencies.signal ? { signal: dependencies.signal } : {}),
123
- onSourceSettled: (settlement) => {
124
- settledSources += 1;
125
- indicator.update(`Collected ${settledSources} source${settledSources === 1 ? '' : 's'} · ${settlement.source.replaceAll('_', ' ')} ${settlement.state}`);
126
- eventProgress.emit({ event: 'source_settled', source: settlement.source, state: settlement.state });
127
- },
128
- }
129
- : {
130
- fast: true,
131
- compact: false,
132
- ...(dependencies.signal ? { signal: dependencies.signal } : {}),
133
- }), dependencies.signal);
124
+ result = await collect(dependencies.signal, settlement => {
125
+ settledSources += 1;
126
+ indicator.update(`Collected ${settledSources} source${settledSources === 1 ? '' : 's'} · ${settlement.source.replaceAll('_', ' ')} ${settlement.state}`);
127
+ });
134
128
  }
135
129
  finally {
136
130
  context.endProgress();