@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/NOTICE +8 -2
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/argument-values.mjs +55 -0
  7. package/cli/arguments.mjs +24 -408
  8. package/cli/artifact-validation/case-response.mjs +65 -44
  9. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  10. package/cli/artifact-validation/lookup-evidence.mjs +15 -9
  11. package/cli/artifact-validation/signed-review.mjs +136 -68
  12. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  13. package/cli/artifact-verify.mjs +92 -24
  14. package/cli/assurance-arguments.mjs +56 -0
  15. package/cli/assurance-command-definitions.mjs +179 -0
  16. package/cli/assurance-command-runner.mjs +40 -5
  17. package/cli/bagit-folder.mjs +57 -0
  18. package/cli/bagit-review.mjs +23 -0
  19. package/cli/bounded-json.mjs +1 -1
  20. package/cli/bulk-checkpoint.mjs +1 -1
  21. package/cli/bulk-command-runner.mjs +4 -2
  22. package/cli/bulk-output.mjs +29 -3
  23. package/cli/bulk.mjs +2 -3
  24. package/cli/case-command.mjs +212 -0
  25. package/cli/case-pack.mjs +50 -32
  26. package/cli/ci-report.mjs +6 -3
  27. package/cli/collection-arguments.mjs +123 -0
  28. package/cli/collection-command-definitions.mjs +330 -0
  29. package/cli/collection-preflight.mjs +8 -1
  30. package/cli/command-argument-grammar.mjs +8 -206
  31. package/cli/command-definition.mjs +329 -0
  32. package/cli/command-reference.mjs +94 -1357
  33. package/cli/command-seeds.mjs +18 -0
  34. package/cli/compare.mjs +1 -1
  35. package/cli/completion.mjs +298 -79
  36. package/cli/config-profile.mjs +8 -6
  37. package/cli/ct-event-intake.mjs +16 -16
  38. package/cli/discover.mjs +5 -4
  39. package/cli/discovery-observation-snapshot.mjs +46 -18
  40. package/cli/discovery-scan-command-runner.mjs +5 -3
  41. package/cli/discovery-scan.mjs +7 -4
  42. package/cli/discovery-snapshot.mjs +2 -2
  43. package/cli/discovery-workflow.mjs +5 -5
  44. package/cli/domain-control-monitor.mjs +50 -20
  45. package/cli/domain-control-observations.mjs +151 -103
  46. package/cli/errors.mjs +37 -6
  47. package/cli/evidence-arguments.mjs +46 -0
  48. package/cli/evidence-command-definitions.mjs +69 -0
  49. package/cli/evidence-command-runner.mjs +18 -8
  50. package/cli/evidence-signing.mjs +5 -4
  51. package/cli/external-observation-mapping.mjs +9 -10
  52. package/cli/formatters/terminal-command-formats.mjs +6 -0
  53. package/cli/formatters/terminal-lookup.mjs +24 -46
  54. package/cli/formatters/terminal-shared.mjs +5 -3
  55. package/cli/formatters/terminal-technology.mjs +114 -0
  56. package/cli/history-arguments.mjs +39 -0
  57. package/cli/history-command-definitions.mjs +88 -0
  58. package/cli/indicator-set-command.mjs +33 -0
  59. package/cli/inline-command-families.mjs +6 -46
  60. package/cli/inline-command-runner.mjs +4 -31
  61. package/cli/input.mjs +2 -2
  62. package/cli/intake-command.mjs +107 -0
  63. package/cli/interactive-launcher.mjs +2 -160
  64. package/cli/interchange-report.mjs +8 -10
  65. package/cli/investigation-artifacts.mjs +176 -0
  66. package/cli/investigation-checkpoint.mjs +10 -0
  67. package/cli/investigation-folder.mjs +114 -0
  68. package/cli/investigation-manifest.mjs +1 -112
  69. package/cli/investigation-package-review.mjs +103 -0
  70. package/cli/investigation-plan.mjs +38 -147
  71. package/cli/investigation-recipes.mjs +177 -0
  72. package/cli/investigation-run.mjs +256 -35
  73. package/cli/local-document-checkpoint.mjs +170 -0
  74. package/cli/local-mmdb-review.mjs +197 -17
  75. package/cli/lookalike-calibration.mjs +1 -1
  76. package/cli/lookup-brief.mjs +2 -2
  77. package/cli/lookup-browser.mjs +5 -12
  78. package/cli/lookup-command-runner.mjs +33 -39
  79. package/cli/lookup-diff.mjs +47 -12
  80. package/cli/lookup-plan.mjs +16 -14
  81. package/cli/lookup-reconcile.mjs +15 -12
  82. package/cli/lookup-timeline.mjs +7 -6
  83. package/cli/mail-header-review.mjs +24 -247
  84. package/cli/mail-review.mjs +75 -21
  85. package/cli/manual.mjs +1 -0
  86. package/cli/network-arguments.mjs +51 -0
  87. package/cli/network-command-definitions.mjs +130 -0
  88. package/cli/network-command-runner.mjs +45 -31
  89. package/cli/offline-evidence-review.mjs +39 -6
  90. package/cli/output-file.mjs +31 -5
  91. package/cli/posture.mjs +1 -1
  92. package/cli/registry-doctor.mjs +3 -1
  93. package/cli/registry-fixture-scaffold.mjs +1 -1
  94. package/cli/registry-support.mjs +1 -1
  95. package/cli/retained-artifact-diff.mjs +10 -9
  96. package/cli/review-arguments.mjs +131 -0
  97. package/cli/review-command-definitions.mjs +273 -0
  98. package/cli/review-command-runner.mjs +104 -149
  99. package/cli/risk-calibration.mjs +8 -8
  100. package/cli/runner.mjs +74 -37
  101. package/cli/saved-lookup.mjs +2 -2
  102. package/cli/selected-input-worker.mjs +63 -0
  103. package/cli/sharing-review.mjs +8 -80
  104. package/cli/signer-trust.mjs +104 -0
  105. package/cli/source-reliability.mjs +75 -84
  106. package/cli/support-arguments.mjs +49 -0
  107. package/cli/support-command-definitions.mjs +209 -0
  108. package/cli/support-command-runner.mjs +2 -2
  109. package/cli/terminal-input.mjs +168 -0
  110. package/cli/workflow-arguments.mjs +86 -0
  111. package/cli/workflow-command-definitions.mjs +78 -0
  112. package/cli/workflow-command-runner.mjs +75 -13
  113. package/docs/cli-reference.md +103 -30
  114. package/docs/cli.md +510 -25
  115. package/docs/contextual-reviews.md +155 -0
  116. package/lib/abort.mjs +4 -1
  117. package/lib/analyst-taxonomy.mjs +2 -40
  118. package/lib/application-version.mjs +1 -1
  119. package/lib/availability.mjs +27 -754
  120. package/lib/bounded-contract-normalizers.mjs +4 -0
  121. package/lib/bounded-file.mjs +3 -1
  122. package/lib/bounded-html-document.mjs +125 -0
  123. package/lib/bounded-json.mjs +2 -290
  124. package/lib/browser-library-profile.mjs +99 -130
  125. package/lib/browser-library-worker.mjs +112 -0
  126. package/lib/candidate-provenance-bounds.mjs +2 -0
  127. package/lib/classify.mjs +1 -1
  128. package/lib/client-behavior-profile.mjs +3 -3
  129. package/lib/ct-query.mjs +2 -27
  130. package/lib/ct-response-bounds.mjs +2 -9
  131. package/lib/ct-search.mjs +89 -46
  132. package/lib/dmarc-discovery.mjs +62 -0
  133. package/lib/dns-convergence-review.mjs +21 -8
  134. package/lib/dns-delegation-health.mjs +36 -22
  135. package/lib/dns-inheritance-review.mjs +207 -0
  136. package/lib/dns-intelligence.mjs +44 -29
  137. package/lib/dns-mx.mjs +1 -1
  138. package/lib/dns-query-coverage.mjs +40 -0
  139. package/lib/dnssec-evidence-validation.mjs +11 -3
  140. package/lib/domain-assurance.mjs +1 -1
  141. package/lib/domain-change-packet.mjs +18 -14
  142. package/lib/domain-change-review.mjs +40 -16
  143. package/lib/domain-control-flight-recorder.mjs +301 -124
  144. package/lib/domain-control-manifest.mjs +126 -58
  145. package/lib/domain-posture-analysis.mjs +41 -11
  146. package/lib/domain-posture-parsers.mjs +88 -6
  147. package/lib/domain-posture.mjs +120 -32
  148. package/lib/domain-registration.mjs +392 -0
  149. package/lib/evidence-export-privacy.mjs +568 -0
  150. package/lib/evidence-export.mjs +12 -593
  151. package/lib/evidence-report.mjs +7 -2
  152. package/lib/external-intelligence-risk.mjs +2 -124
  153. package/lib/favicon.mjs +117 -41
  154. package/lib/generated/cisa-kev-catalog.mjs +50 -4
  155. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  156. package/lib/generated/retire-browser-catalog.mjs +2742 -1034
  157. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  158. package/lib/html-signals.mjs +87 -85
  159. package/lib/http-evidence-bounds.mjs +2 -7
  160. package/lib/http-intelligence.mjs +52 -27
  161. package/lib/idn-confusable-policy.mjs +6 -4
  162. package/lib/idn-confusables.mjs +1 -1
  163. package/lib/interchange-fidelity-registry.mjs +10 -0
  164. package/lib/lookup-cache.mjs +34 -11
  165. package/lib/lookup-child-profile-contract.mjs +24 -15
  166. package/lib/lookup-contract-primitives.mjs +1 -1
  167. package/lib/lookup-diagnostics.mjs +2 -1
  168. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  169. package/lib/lookup-progress.mjs +79 -13
  170. package/lib/lookup-response-contract.mjs +1077 -0
  171. package/lib/lookup-response.mjs +11 -24
  172. package/lib/lookup-source-progress.mjs +24 -35
  173. package/lib/lookup.mjs +49 -28
  174. package/lib/observed-network-context.mjs +43 -27
  175. package/lib/opportunity-scoring.mjs +2 -107
  176. package/lib/outbound-identity.mjs +1 -1
  177. package/lib/outbound-request-bounds.mjs +4 -1
  178. package/lib/page-fingerprints.mjs +87 -134
  179. package/lib/page-role-profile.mjs +12 -8
  180. package/lib/perceptual-hash-comparison.mjs +2 -34
  181. package/lib/perceptual-hash.mjs +79 -24
  182. package/lib/portable-generator.mjs +2 -29
  183. package/lib/project-metadata.mjs +2 -8
  184. package/lib/rdap-bootstrap.mjs +72 -22
  185. package/lib/rdap-client.mjs +26 -14
  186. package/lib/rdap-normalization.mjs +33 -18
  187. package/lib/rdap-search-workbench.mjs +1 -1
  188. package/lib/rdap-transport.mjs +47 -20
  189. package/lib/rdap-types.mjs +2 -1
  190. package/lib/rdap.mjs +18 -3
  191. package/lib/registrable-domain.mjs +2 -36
  192. package/lib/registrar-standing-contract.mjs +2 -1
  193. package/lib/registrar-standing.mjs +9 -13
  194. package/lib/registry-capabilities.mjs +7 -3
  195. package/lib/registry-capability-catalogue.mjs +29 -20
  196. package/lib/registry-dates.mjs +2 -190
  197. package/lib/registry-insights.mjs +56 -18
  198. package/lib/response-policy.mjs +86 -48
  199. package/lib/risk-calibration-summary.mjs +2 -338
  200. package/lib/risk-scoring.mjs +2 -345
  201. package/lib/rpki-evidence.mjs +8 -5
  202. package/lib/safe-fetch.mjs +54 -47
  203. package/lib/scoring-evidence-quality.mjs +2 -107
  204. package/lib/security-txt.mjs +10 -7
  205. package/lib/semantic-version.mjs +2 -54
  206. package/lib/service-binding-dns.mjs +35 -0
  207. package/lib/smtp-transport-review.mjs +75 -34
  208. package/lib/sslbl-certificates.generated.mjs +141 -138
  209. package/lib/static-html-analysis.mjs +117 -146
  210. package/lib/structured-data-identity.mjs +2 -1
  211. package/lib/technology-markup.mjs +100 -0
  212. package/lib/threat-intelligence-manifests.mjs +11 -139
  213. package/lib/threat-intelligence-runtime.mjs +24 -52
  214. package/lib/threat-intelligence-targets.mjs +6 -34
  215. package/lib/threat-intelligence-types.mjs +2 -36
  216. package/lib/typosquat-generator.mjs +29 -16
  217. package/lib/urlhaus-intelligence.mjs +8 -3
  218. package/lib/urlscan-intelligence.mjs +1 -1
  219. package/lib/website-enrichment.mjs +388 -0
  220. package/lib/website-security-posture.mjs +30 -6
  221. package/lib/website-technology.mjs +111 -71
  222. package/lib/whois-authority.mjs +55 -5
  223. package/lib/whois-chain.mjs +9 -2
  224. package/lib/whois-common-formats.mjs +5 -5
  225. package/lib/whois-contracts.mjs +2 -1
  226. package/lib/whois-dialects.mjs +2 -2
  227. package/lib/whois-normalization.mjs +3 -1
  228. package/lib/whois-parser-support.mjs +8 -1
  229. package/lib/whois-parser.mjs +5 -3
  230. package/lib/whois-transport.mjs +23 -7
  231. package/package.json +14 -14
  232. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  233. package/packages/analysis/bounded-json.mjs +330 -0
  234. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  235. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  236. package/packages/analysis/ct-query.mjs +27 -0
  237. package/packages/analysis/ct-response-bounds.mjs +9 -0
  238. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  239. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  240. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  241. package/packages/analysis/opportunity-scoring.mjs +107 -0
  242. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  243. package/packages/analysis/portable-generator.mjs +29 -0
  244. package/packages/analysis/project-metadata.mjs +8 -0
  245. package/packages/analysis/registrable-domain.mjs +56 -0
  246. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  247. package/packages/analysis/risk-scoring.mjs +345 -0
  248. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  249. package/packages/analysis/semantic-version.mjs +54 -0
  250. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  251. package/packages/cases/case-attachment-model.mjs +83 -0
  252. package/packages/cases/case-evidence-links.mjs +135 -0
  253. package/packages/cases/case-evidence-model.mjs +229 -69
  254. package/packages/cases/case-evidence-quality.mjs +5 -0
  255. package/packages/cases/case-export-input.mjs +55 -0
  256. package/packages/cases/case-incident-context.mjs +52 -0
  257. package/packages/cases/case-markdown.mjs +11 -0
  258. package/packages/cases/case-migration-model.mjs +98 -48
  259. package/packages/cases/case-model.mjs +1 -0
  260. package/packages/cases/case-recheck-model.mjs +149 -0
  261. package/packages/cases/case-record-contracts.mjs +1 -1
  262. package/packages/cases/case-record-core.mjs +4 -3
  263. package/packages/cases/case-record-operations.mjs +123 -133
  264. package/packages/cases/case-record-projection.mjs +30 -20
  265. package/packages/cases/case-record-version-input.mjs +19 -0
  266. package/packages/cases/case-report.mjs +114 -67
  267. package/packages/cases/case-requested-evidence.mjs +146 -0
  268. package/packages/cases/case-response-actions.mjs +756 -0
  269. package/packages/cases/case-response-model.mjs +49 -1129
  270. package/packages/cases/case-response-outcomes.mjs +338 -0
  271. package/packages/cases/case-response-packet-render.mjs +157 -0
  272. package/packages/cases/case-response-packet-types.mjs +1 -0
  273. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  274. package/packages/cases/case-response-packet.mjs +58 -593
  275. package/packages/cases/case-response-records.mjs +93 -0
  276. package/packages/cases/case-response-review-inputs.mjs +382 -0
  277. package/packages/cases/case-response-values.mjs +85 -0
  278. package/packages/cases/case-review-package.mjs +45 -0
  279. package/packages/cases/case-selection.mjs +46 -0
  280. package/packages/cases/case-storage-model.mjs +26 -1
  281. package/packages/cases/case-workflow-metadata.mjs +225 -0
  282. package/packages/cases/case-workflow-migration.mjs +43 -0
  283. package/packages/cases/http-summary.mjs +6 -1
  284. package/packages/cases/response-route-freshness.mjs +16 -0
  285. package/packages/comparison/capture-context.mjs +61 -0
  286. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  287. package/packages/comparison/favicon-similarity.mjs +1 -1
  288. package/packages/comparison/page-similarity.mjs +3 -2
  289. package/packages/comparison/relationship-evidence.mjs +103 -14
  290. package/packages/comparison/relationship-provenance.mjs +99 -0
  291. package/packages/contracts/analyst-interchange.mjs +234 -0
  292. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  293. package/packages/contracts/analyst-review-state.mjs +5 -4
  294. package/packages/contracts/case-portability.mjs +370 -114
  295. package/packages/contracts/case-views-contract.mjs +12 -0
  296. package/packages/contracts/case-views.mjs +26 -0
  297. package/packages/contracts/cli-command-semantics.mjs +3 -0
  298. package/packages/contracts/cli-grammar.mjs +6 -0
  299. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  300. package/packages/contracts/cli-lookup.mjs +0 -1
  301. package/packages/contracts/context-review.mjs +24 -0
  302. package/packages/contracts/document-intake.mjs +11 -0
  303. package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
  304. package/packages/contracts/domain-control-manifest.mjs +128 -74
  305. package/packages/contracts/domain-control-monitor.mjs +59 -45
  306. package/packages/contracts/domain-control-review.mjs +200 -121
  307. package/packages/contracts/domain-name.mjs +21 -0
  308. package/packages/contracts/external-observation-interchange.mjs +2 -1
  309. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  310. package/packages/contracts/har-review.mjs +2 -0
  311. package/packages/contracts/http-url.mjs +2 -0
  312. package/packages/contracts/identity-events.mjs +4 -0
  313. package/packages/contracts/investigation-package-limits.mjs +24 -0
  314. package/packages/contracts/investigation-portability.mjs +56 -27
  315. package/packages/contracts/investigation-run.mjs +18 -0
  316. package/packages/contracts/ip-address.mjs +44 -0
  317. package/packages/contracts/lookup-evidence.mjs +35 -0
  318. package/packages/contracts/mail-authentication.mjs +5 -0
  319. package/packages/contracts/message-intake.mjs +31 -0
  320. package/packages/contracts/offline-comparison.mjs +4 -2
  321. package/packages/contracts/page-fingerprints.mjs +15 -0
  322. package/packages/contracts/passive-mail.mjs +10 -0
  323. package/packages/contracts/risk-calibration.mjs +0 -1
  324. package/packages/contracts/schema-compatibility.mjs +1 -1
  325. package/packages/contracts/schema-lifecycle.mjs +83 -153
  326. package/packages/contracts/selected-file-limits.mjs +6 -0
  327. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  328. package/packages/contracts/web-capture.mjs +10 -0
  329. package/packages/contracts/workspace-portability.mjs +219 -238
  330. package/packages/evidence/artifact-integrity.mjs +8 -2
  331. package/packages/evidence/artifact-structure.mjs +105 -0
  332. package/packages/evidence/collection-quality.mjs +67 -0
  333. package/packages/evidence/decision-fact.mjs +4 -1
  334. package/packages/evidence/dns-query-outcome.mjs +72 -0
  335. package/packages/evidence/domain-control-runtime.mjs +155 -27
  336. package/packages/evidence/domain-posture-context.mjs +79 -0
  337. package/packages/evidence/email-recipient.mjs +18 -0
  338. package/packages/evidence/image-regions.mjs +56 -0
  339. package/packages/evidence/latest-observations.mjs +28 -0
  340. package/packages/evidence/lookup-target.mjs +108 -0
  341. package/packages/evidence/observation.mjs +18 -1
  342. package/packages/evidence/passphrase-encryption.mjs +44 -0
  343. package/packages/evidence/png-pixels.mjs +117 -0
  344. package/packages/evidence/public-address-policy.mjs +83 -0
  345. package/packages/evidence/record-identity.mjs +72 -0
  346. package/packages/evidence/registry-dates.mjs +190 -0
  347. package/packages/evidence/retained-file.mjs +69 -0
  348. package/packages/evidence/review-age.mjs +14 -0
  349. package/packages/interchange/bagit.mjs +342 -0
  350. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  351. package/packages/interchange/crc32.mjs +17 -0
  352. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  353. package/packages/interchange/external-findings-import.mjs +93 -61
  354. package/packages/interchange/managed-indicator-export.mjs +35 -0
  355. package/packages/interchange/managed-indicator-set.mjs +210 -0
  356. package/packages/interchange/misp-indicator-export.mjs +93 -0
  357. package/packages/interchange/sharing-policy.mjs +56 -0
  358. package/packages/interchange/stix-indicator-export.mjs +129 -0
  359. package/packages/interchange/web-capture-import.mjs +389 -0
  360. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  361. package/packages/investigation/bulk-mail-exposure.mjs +9 -14
  362. package/packages/investigation/capture-coverage.mjs +57 -0
  363. package/packages/investigation/connector-provenance-review.mjs +100 -0
  364. package/packages/investigation/context-review.mjs +35 -0
  365. package/packages/investigation/document-intake.mjs +81 -0
  366. package/packages/investigation/docx-intake.mjs +133 -0
  367. package/packages/investigation/domain-history-review.mjs +100 -0
  368. package/packages/investigation/domain-transition-review.mjs +47 -0
  369. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  370. package/packages/investigation/har-intake.mjs +68 -0
  371. package/packages/investigation/identity-event-intake.mjs +131 -0
  372. package/packages/investigation/identity-incident-review.mjs +39 -0
  373. package/packages/investigation/incident-sequence-review.mjs +58 -0
  374. package/packages/investigation/intake-report.mjs +19 -0
  375. package/packages/investigation/investigation-bagit.mjs +14 -0
  376. package/packages/investigation/investigation-capsule.mjs +33 -5
  377. package/packages/investigation/investigation-manifest.mjs +200 -0
  378. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  379. package/packages/investigation/investigation-package.mjs +200 -0
  380. package/packages/investigation/link-intake.mjs +179 -0
  381. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  382. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  383. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  384. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  385. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  386. package/packages/investigation/mail-authentication-review.mjs +222 -0
  387. package/packages/investigation/mail-header-identity.mjs +209 -0
  388. package/packages/investigation/message-intake.mjs +195 -0
  389. package/packages/investigation/page-behaviour.mjs +113 -0
  390. package/packages/investigation/pdf-intake.mjs +155 -0
  391. package/packages/investigation/platform-continuity-review.mjs +60 -0
  392. package/packages/investigation/qr-intake.mjs +56 -0
  393. package/packages/investigation/requested-action-hints.mjs +11 -0
  394. package/packages/investigation/selected-input-review.mjs +17 -0
  395. package/packages/investigation/storefront-review.mjs +70 -0
  396. package/packages/monitoring/analyst-review-state.mjs +38 -90
  397. package/packages/workspace/brand-profile-model.mjs +160 -22
  398. package/packages/workspace/bulk-columns.mjs +17 -0
  399. package/packages/workspace/bulk-review-model.mjs +39 -31
  400. package/packages/workspace/bulk-session-model.mjs +112 -27
  401. package/packages/workspace/bulk-sort.mjs +1 -0
  402. package/packages/workspace/campaign-model.mjs +1 -1
  403. package/packages/workspace/case-views.mjs +102 -0
  404. package/packages/workspace/detection-rule-model.mjs +1 -1
  405. package/packages/workspace/hostile-input.mjs +29 -8
  406. package/packages/workspace/investigation-guide.mjs +2 -2
  407. package/packages/workspace/investigation-template-model.mjs +45 -6
  408. package/packages/workspace/page-baseline.mjs +20 -17
  409. package/packages/workspace/relationship-observation-model.mjs +54 -35
  410. package/packages/workspace/shortlist-model.mjs +14 -5
  411. package/packages/workspace/watchlist-history.mjs +30 -14
  412. package/packages/workspace/watchlist-store.mjs +24 -6
  413. package/packages/workspace/website-snapshot-model.mjs +98 -33
  414. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  415. package/packages/workspace/workspace-archive.mjs +90 -25
  416. package/third-party-notices.txt +577 -17
  417. package/lib/hostname.mjs +0 -38
  418. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,345 +1,2 @@
1
- import { calibrateExternalIntelligenceRisk } from "./external-intelligence-risk.mjs";
2
- import { buildScoreEvidenceQuality, } from "./scoring-evidence-quality.mjs";
3
- const RISK_STATES = new Set(['registered', 'for_sale', 'expiring']);
4
- const STATE_LABELS = Object.freeze({
5
- for_sale: 'for sale',
6
- expiring: 'expiring/pending delete',
7
- registered: 'registered',
8
- });
9
- // Version 8 keeps unreviewed whole-page similarity as visible review context
10
- // without assigning it Risk points. Reviewed favicon and official-asset
11
- // observations remain bounded within the brand-presentation family, generic
12
- // operational properties remain supporting context, and missing evidence never
13
- // contributes points.
14
- export const RISK_MODEL_VERSION = 8;
15
- export const RISK_REVIEW_THRESHOLD = 70;
16
- const CURRENT_RISK_MODEL = Object.freeze({
17
- modelVersion: RISK_MODEL_VERSION,
18
- includePageBaselineMatch: false,
19
- });
20
- const RISK_MODEL_V7 = Object.freeze({
21
- modelVersion: 7,
22
- includePageBaselineMatch: true,
23
- });
24
- const RISK_STATE_BASE = Object.freeze({
25
- registered: 6,
26
- for_sale: 4,
27
- expiring: 5,
28
- });
29
- const FAMILY_CAPS = Object.freeze({
30
- 'domain-resemblance': 20,
31
- 'brand-presentation': 24,
32
- 'credential-lure': 18,
33
- 'external-intelligence': 18,
34
- 'operational-support': 12,
35
- });
36
- const REMOVABLE_RISK_FAMILY_MAP = Object.freeze({
37
- 'brand-presentation': true,
38
- 'credential-lure': true,
39
- 'domain-resemblance': true,
40
- 'external-intelligence': true,
41
- 'operational-support': true,
42
- });
43
- const REMOVABLE_RISK_FAMILIES = Object.freeze(Object.keys(REMOVABLE_RISK_FAMILY_MAP));
44
- const HIGH_CONTEXT_MUTATIONS = new Set([
45
- 'unicode_homoglyph',
46
- 'unicode_homoglyph_depth_2',
47
- 'dictionary',
48
- 'dictionary_token_replacement',
49
- ]);
50
- const MEDIUM_CONTEXT_MUTATIONS = new Set(['ascii_homoglyph', 'bitsquatting', 'tld_embedding', 'tld_typo', 'tld_substitution']);
51
- const LOW_CONTEXT_MUTATIONS = new Set([
52
- 'character_addition',
53
- 'character_omission',
54
- 'character_duplication',
55
- 'character_transposition',
56
- 'pluralization',
57
- 'www_prefix',
58
- 'hyphenation',
59
- 'separator_omission',
60
- 'word_reordering',
61
- 'keyboard_substitution',
62
- 'keyboard_insertion',
63
- 'vowel_swap',
64
- ]);
65
- export const RISK_MUTATION_TYPES = Object.freeze([
66
- ...HIGH_CONTEXT_MUTATIONS,
67
- ...MEDIUM_CONTEXT_MUTATIONS,
68
- ...LOW_CONTEXT_MUTATIONS,
69
- ]);
70
- export function normalizeRiskModelVersion(value) {
71
- return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= 1000 ? value : null;
72
- }
73
- function formatAge(days) {
74
- if (days < 60)
75
- return `${Math.round(days)}d old`;
76
- const years = days / 365.25;
77
- return years < 1 ? `${Math.round(days / 30)}mo old` : `${years.toFixed(1)}y old`;
78
- }
79
- function mutationContext(mutationTypes) {
80
- if (!Array.isArray(mutationTypes))
81
- return null;
82
- const bounded = mutationTypes.slice(0, 30);
83
- if (bounded.some((value) => typeof value === 'string' && HIGH_CONTEXT_MUTATIONS.has(value))) {
84
- return { label: 'High-similarity or phishing-term candidate context', delta: 18 };
85
- }
86
- if (bounded.some((value) => typeof value === 'string' && MEDIUM_CONTEXT_MUTATIONS.has(value))) {
87
- return { label: 'Lookalike candidate context', delta: 12 };
88
- }
89
- if (bounded.some((value) => typeof value === 'string' && LOW_CONTEXT_MUTATIONS.has(value))) {
90
- return { label: 'Generated variation candidate context', delta: 8 };
91
- }
92
- return null;
93
- }
94
- function hasBoolean(value) {
95
- return typeof value === 'boolean';
96
- }
97
- function scoreQuality(input) {
98
- const observedFamilies = ['registration'];
99
- const expectedFamilies = ['registration', 'operational-support'];
100
- if (Array.isArray(input.mutationTypes) || hasBoolean(input.idnReferenceMatch))
101
- observedFamilies.push('domain-resemblance');
102
- if (input.hasActiveBrandProfile === true)
103
- expectedFamilies.push('domain-resemblance', 'brand-presentation');
104
- if ([input.faviconMatch, input.faviconNearMatch, input.reusesOfficialAssets, input.pageBaselineMatch].some(hasBoolean)) {
105
- observedFamilies.push('brand-presentation');
106
- }
107
- if ([input.hasPasswordField, input.hasExternalFormAction].some(hasBoolean) || typeof input.phishingLanguageMatch === 'string') {
108
- observedFamilies.push('credential-lure');
109
- }
110
- if (calibrateExternalIntelligenceRisk(input.threatIntelligence, input.domain).eligibleProviderCount > 0) {
111
- observedFamilies.push('external-intelligence');
112
- }
113
- if ([input.hasMx, input.hasSpf, input.hasDmarc].some(hasBoolean)
114
- || typeof input.activityStatus === 'string'
115
- || typeof input.domainAgeDays === 'number')
116
- observedFamilies.push('operational-support');
117
- if (input.scanDepth === 'deep')
118
- expectedFamilies.push('credential-lure');
119
- return buildScoreEvidenceQuality(input, {
120
- expectedDepth: 'deep',
121
- observedFamilies,
122
- expectedFamilies,
123
- });
124
- }
125
- // Risk prioritizes a registered lookalike/typosquat domain for analyst review.
126
- // It is a heuristic indicator, never a maliciousness or safety verdict.
127
- function explainRiskScoreInternal(input, excludedFamily, model) {
128
- const state = input.availability ?? input.state;
129
- if (typeof state !== 'string' || !RISK_STATES.has(state))
130
- return null;
131
- const factors = [];
132
- const familyTotals = new Map();
133
- const add = (family, label, requestedDelta) => {
134
- if (family === excludedFamily)
135
- return 0;
136
- const current = familyTotals.get(family) ?? 0;
137
- const cap = FAMILY_CAPS[family] ?? Number.POSITIVE_INFINITY;
138
- const delta = Math.max(0, Math.min(requestedDelta, Math.max(0, cap - current)));
139
- const capped = delta !== requestedDelta;
140
- factors.push({ family, label: capped ? `${label} (family cap reached)` : label, delta });
141
- familyTotals.set(family, current + delta);
142
- return delta;
143
- };
144
- add('registration', `Base context for “${STATE_LABELS[state] ?? state}”`, RISK_STATE_BASE[state] ?? 0);
145
- const mutation = mutationContext(input.mutationTypes);
146
- if (mutation)
147
- add('domain-resemblance', mutation.label, mutation.delta);
148
- if (input.idnReferenceMatch === true)
149
- add('domain-resemblance', 'IDN skeleton matches an official Brand Profile domain', 20);
150
- if (input.faviconMatch === true)
151
- add('brand-presentation', 'Favicon matches an official Brand Profile site', 18);
152
- else if (input.faviconNearMatch === true)
153
- add('brand-presentation', 'Favicon resembles an official Brand Profile site', 14);
154
- if (input.reusesOfficialAssets === true)
155
- add('brand-presentation', 'Official asset-host relationship observed', 6);
156
- if (model.includePageBaselineMatch && input.pageBaselineMatch === true) {
157
- add('brand-presentation', 'Multiple independently compared page-identity components match the official baseline', 14);
158
- }
159
- if (typeof input.phishingLanguageMatch === 'string' && input.phishingLanguageMatch.trim()) {
160
- add('credential-lure', 'Suspicious urgency language observed', 8);
161
- }
162
- if (input.hasPasswordField === true && input.hasExternalFormAction === true) {
163
- add('credential-lure', 'Password form submits to an external origin', 10);
164
- }
165
- else if (input.hasPasswordField === true) {
166
- add('credential-lure', 'Login/password form present', 5);
167
- }
168
- else if (input.hasExternalFormAction === true) {
169
- add('credential-lure', 'External form destination observed without a password field', 0);
170
- }
171
- const external = calibrateExternalIntelligenceRisk(input.threatIntelligence, input.domain);
172
- if (external.factor)
173
- add('external-intelligence', external.factor.label, external.contribution);
174
- const primaryFamilies = ['domain-resemblance', 'brand-presentation', 'credential-lure', 'external-intelligence']
175
- .filter((family) => (familyTotals.get(family) ?? 0) > 0);
176
- if (primaryFamilies.length >= 2) {
177
- add('corroboration', `Corroborating context across ${primaryFamilies.length} independent evidence families`, primaryFamilies.length >= 3 ? 18 : 10);
178
- }
179
- const hasIndependentContext = primaryFamilies.length > 0;
180
- const supporting = (label, delta) => {
181
- add('operational-support', hasIndependentContext ? label : `${label}; neutral without independent suspicious context`, hasIndependentContext ? delta : 0);
182
- };
183
- if (input.activityStatus === 'active')
184
- supporting('Active website observed', 4);
185
- if (input.hasMx === true)
186
- supporting('Mail server configured', 4);
187
- if (input.hasSpf === true && input.hasDmarc === true)
188
- supporting('SPF and DMARC records present', 1);
189
- else if (input.hasSpf === true || input.hasDmarc === true)
190
- supporting('SPF or DMARC record present', 0);
191
- if (typeof input.domainAgeDays === 'number' && Number.isFinite(input.domainAgeDays) && input.domainAgeDays >= 0) {
192
- if (input.domainAgeDays < 90)
193
- supporting(`Recently registered (${formatAge(input.domainAgeDays)})`, 6);
194
- else if (input.domainAgeDays < 365)
195
- supporting(`Registered under a year ago (${formatAge(input.domainAgeDays)})`, 2);
196
- }
197
- if (typeof input.privacyProtected === 'boolean') {
198
- add('registration', 'Registration privacy is neutral context', 0);
199
- }
200
- const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
201
- const score = Math.max(0, Math.min(100, rawScore));
202
- const familyOrder = [
203
- 'registration', 'domain-resemblance', 'brand-presentation', 'credential-lure',
204
- 'external-intelligence', 'corroboration', 'operational-support',
205
- ];
206
- return {
207
- modelVersion: model.modelVersion,
208
- score,
209
- rawScore,
210
- capped: score !== rawScore,
211
- factors,
212
- families: familyOrder
213
- .filter((id) => familyTotals.has(id))
214
- .map((id) => ({ id, contribution: familyTotals.get(id) ?? 0, cap: FAMILY_CAPS[id] ?? null })),
215
- evidenceQuality: scoreQuality(input),
216
- };
217
- }
218
- export function explainRiskScore(input) {
219
- return explainRiskScoreInternal(input, null, CURRENT_RISK_MODEL);
220
- }
221
- export function buildRiskScoreSensitivity(input) {
222
- const baseline = explainRiskScoreInternal(input, null, CURRENT_RISK_MODEL);
223
- if (!baseline)
224
- return null;
225
- const removableFamilies = REMOVABLE_RISK_FAMILIES
226
- .filter((family) => baseline.families.some((item) => item.id === family && item.contribution > 0));
227
- const scenarios = removableFamilies.map((excludedFamily) => {
228
- const result = explainRiskScoreInternal(input, excludedFamily, CURRENT_RISK_MODEL);
229
- const score = result?.score ?? baseline.score;
230
- return Object.freeze({ excludedFamily, score, difference: score - baseline.score });
231
- }).sort((left, right) => left.score - right.score || left.excludedFamily.localeCompare(right.excludedFamily));
232
- const minimumScenarioScore = scenarios[0]?.score ?? baseline.score;
233
- const thresholdState = baseline.score < RISK_REVIEW_THRESHOLD
234
- ? 'below'
235
- : minimumScenarioScore < RISK_REVIEW_THRESHOLD ? 'crosses' : 'stable_above';
236
- return Object.freeze({
237
- version: 1,
238
- baselineScore: baseline.score,
239
- reviewThreshold: RISK_REVIEW_THRESHOLD,
240
- minimumScenarioScore,
241
- thresholdState,
242
- scenarios: Object.freeze(scenarios),
243
- limitations: Object.freeze([
244
- 'Each scenario removes one observed evidence family and recalculates family caps and corroboration; it does not predict missing evidence.',
245
- 'The sensitivity review qualifies prioritisation only and does not determine maliciousness, safety, ownership, or intent.',
246
- ]),
247
- });
248
- }
249
- export function computeRiskScore(input) {
250
- return explainRiskScore(input)?.score ?? null;
251
- }
252
- // Retained only for deterministic offline comparison of reviewed calibration
253
- // datasets. Runtime lookups always use the current model above.
254
- export function explainRiskScoreV7(input) {
255
- return explainRiskScoreInternal(input, null, RISK_MODEL_V7);
256
- }
257
- // Retained only for deterministic offline comparison of reviewed calibration
258
- // datasets. Runtime lookups always use the current model above.
259
- export function explainRiskScoreV6(input) {
260
- const state = input.availability ?? input.state;
261
- if (typeof state !== 'string' || !RISK_STATES.has(state))
262
- return null;
263
- const legacyBase = { registered: 10, for_sale: 5, expiring: 8 };
264
- const factors = [];
265
- const totals = new Map();
266
- const add = (family, label, delta) => {
267
- factors.push({ family, label, delta });
268
- totals.set(family, (totals.get(family) ?? 0) + delta);
269
- };
270
- add('registration', `Base score for “${STATE_LABELS[state] ?? state}”`, legacyBase[state] ?? 0);
271
- const contextual = new Set();
272
- const context = mutationContext(input.mutationTypes);
273
- if (context) {
274
- add('domain-resemblance', context.label, context.delta);
275
- contextual.add('domain-resemblance');
276
- }
277
- if (input.faviconMatch === true) {
278
- add('brand-presentation', 'Favicon matches an official Brand Profile site', 18);
279
- contextual.add('brand-presentation');
280
- }
281
- else if (input.faviconNearMatch === true) {
282
- add('brand-presentation', 'Favicon resembles an official Brand Profile site', 14);
283
- contextual.add('brand-presentation');
284
- }
285
- if (input.reusesOfficialAssets === true) {
286
- add('brand-presentation', 'Official asset-host relationship observed', 6);
287
- contextual.add('brand-presentation');
288
- }
289
- if (typeof input.phishingLanguageMatch === 'string' && input.phishingLanguageMatch.trim()) {
290
- add('credential-lure', 'Suspicious urgency language observed', 8);
291
- contextual.add('credential-lure');
292
- }
293
- if (input.hasPasswordField === true) {
294
- add('credential-lure', 'Login/password form present', 5);
295
- contextual.add('credential-lure');
296
- }
297
- if (contextual.size >= 2)
298
- add('corroboration', `Corroborating context across ${contextual.size} distinct evidence families`, contextual.size >= 3 ? 20 : 10);
299
- const external = calibrateExternalIntelligenceRisk(input.threatIntelligence, input.domain);
300
- if (external.factor)
301
- add('external-intelligence', external.factor.label, external.contribution);
302
- if (input.activityStatus === 'active')
303
- add('operational-support', 'Active site in use', 8);
304
- if (input.hasMx === true)
305
- add('operational-support', 'Mail server configured', 8);
306
- if (input.hasSpf === true && input.hasDmarc === true)
307
- add('operational-support', 'SPF and DMARC records present', 3);
308
- else if (input.hasSpf === true || input.hasDmarc === true)
309
- add('operational-support', 'SPF or DMARC record present', 1);
310
- if (input.privacyProtected === true)
311
- add('registration', 'WHOIS privacy protected', 3);
312
- if (typeof input.domainAgeDays === 'number' && Number.isFinite(input.domainAgeDays) && input.domainAgeDays >= 0) {
313
- if (input.domainAgeDays < 90)
314
- add('operational-support', `Recently registered (${formatAge(input.domainAgeDays)})`, 10);
315
- else if (input.domainAgeDays < 365)
316
- add('operational-support', `Registered under a year ago (${formatAge(input.domainAgeDays)})`, 4);
317
- }
318
- const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
319
- const score = Math.max(0, Math.min(100, rawScore));
320
- return {
321
- modelVersion: 6,
322
- score,
323
- rawScore,
324
- capped: score !== rawScore,
325
- factors,
326
- families: [...totals].map(([id, contribution]) => ({ id, contribution, cap: null })),
327
- evidenceQuality: scoreQuality(input),
328
- };
329
- }
330
- export function riskTone(score) {
331
- if (score === null)
332
- return 'neutral';
333
- if (score >= RISK_REVIEW_THRESHOLD)
334
- return 'danger';
335
- if (score >= 40)
336
- return 'warn';
337
- return 'neutral';
338
- }
339
- export function formatScoreBreakdown(explained, separator = '\n') {
340
- if (!explained)
341
- return '';
342
- const parts = explained.factors.map((factor) => `${factor.label} ${factor.delta >= 0 ? '+' : ''}${Math.round(factor.delta)}`);
343
- parts.push(`Total ${explained.score} · Risk model v${explained.modelVersion} · evidence ${explained.evidenceQuality.state}`);
344
- return parts.join(separator);
345
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/risk-scoring.mjs";
@@ -9,9 +9,10 @@ function record(value) {
9
9
  : null;
10
10
  }
11
11
  function normalizeAsn(value) {
12
- const text = typeof value === 'string' ? value.trim().replace(/^AS/iu, '') : value;
13
- const number = Number(text);
14
- return Number.isInteger(number) && number >= 0 && number <= 4_294_967_295 ? number : null;
12
+ const number = typeof value === 'number' ? value
13
+ : typeof value === 'string' && value.length <= 16 && /^(?:AS)?\d{1,10}$/iu.test(value.trim())
14
+ ? Number(value.trim().replace(/^AS/iu, '')) : NaN;
15
+ return Number.isSafeInteger(number) && number >= 0 && number <= 4_294_967_295 ? number : null;
15
16
  }
16
17
  function normalizeAuthorization(value) {
17
18
  const source = record(value);
@@ -21,7 +22,8 @@ function normalizeAuthorization(value) {
21
22
  const parsedPrefix = parseIpPrefix(prefix);
22
23
  const asn = normalizeAsn(source.asn ?? source.asID ?? source.origin);
23
24
  const maxLengthValue = source.maxLength ?? source.max_length ?? parsedPrefix?.length;
24
- const maxLength = Number(maxLengthValue);
25
+ const maxLength = typeof maxLengthValue === 'number' ? maxLengthValue
26
+ : typeof maxLengthValue === 'string' && /^\d{1,3}$/u.test(maxLengthValue) ? Number(maxLengthValue) : NaN;
25
27
  const maximum = parsedPrefix?.family === 4 ? 32 : 128;
26
28
  if (!parsedPrefix || asn === null || !Number.isInteger(maxLength)
27
29
  || maxLength < parsedPrefix.length || maxLength > maximum)
@@ -49,8 +51,9 @@ function reviewRpkiRoute(input) {
49
51
  const baseLimitations = Object.freeze([
50
52
  'This offline review evaluates an explicitly supplied route prefix and origin ASN against an analyst-supplied VRP snapshot.',
51
53
  'It does not collect BGP announcements, establish route ownership, or prove that the snapshot was current or complete.',
54
+ 'AS0 authorisations do not authorise an announcement. An AS0 route origin is invalid input; snapshot validation and signature verification remain external to this review.',
52
55
  ]);
53
- if (!route || originAsn === null || !rows) {
56
+ if (!route || originAsn === null || originAsn === 0 || !rows) {
54
57
  return Object.freeze({
55
58
  schema: RPKI_EVIDENCE_SCHEMA,
56
59
  version: RPKI_EVIDENCE_VERSION,
@@ -23,8 +23,10 @@ import * as net from 'node:net';
23
23
  import * as crypto from 'node:crypto';
24
24
  import { Agent, fetch as undiciFetch } from 'undici';
25
25
  import { MAX_OUTBOUND_REDIRECTS } from "./outbound-request-bounds.mjs";
26
+ import { MAX_OUTBOUND_HTTP_URL_CHARACTERS } from "../packages/contracts/http-url.mjs";
27
+ import { ipv6Groups } from "../packages/contracts/ip-address.mjs";
26
28
  const MAX_REDIRECTS = MAX_OUTBOUND_REDIRECTS;
27
- const MAX_SAFE_FETCH_URL_LENGTH = 4096;
29
+ const MAX_SAFE_FETCH_URL_LENGTH = MAX_OUTBOUND_HTTP_URL_CHARACTERS;
28
30
  const MAX_SAFE_FETCH_ADDRESS_CANDIDATES = 64;
29
31
  const REDIRECT_STATUSES = new Set([301, 302, 303, 307, 308]);
30
32
  async function defaultSafeFetchRequest(url, options) {
@@ -70,47 +72,6 @@ function isPrivateIpv4(ip) {
70
72
  return true; // multicast/reserved
71
73
  return false;
72
74
  }
73
- // Expands a valid IPv6 address string to its full 8-group, zero-padded hex
74
- // form - e.g. "::1" -> all-zero groups ending in "0001". Needed because the
75
- // same address can be written multiple ways (a "::" shorthand for a run of
76
- // zero groups, an embedded IPv4 tail as either dotted-decimal or two hex
77
- // groups), and a hostile authoritative DNS server - the exact threat this
78
- // module exists to guard against - is free to choose whichever string form
79
- // it wants. Checking string prefixes on the as-written form (the previous
80
- // approach) only catches whichever form happened to be anticipated; e.g.
81
- // "::ffff:127.0.0.1" and "::ffff:7f00:1" are the same address, but only the
82
- // first was recognized as IPv4-mapped loopback.
83
- function expandIpv6Groups(ip) {
84
- const hasDoubleColon = ip.includes('::');
85
- const [headStr, tailStr] = hasDoubleColon ? ip.split('::') : [ip, undefined];
86
- // A dotted-decimal segment (the tail of "::ffff:127.0.0.1") is two
87
- // 16-bit groups' worth of bits - convert it to hex groups first so the
88
- // rest of this function only ever deals with plain hex.
89
- const toHexGroups = (part) => {
90
- if (!part)
91
- return [];
92
- return part.split(':').flatMap((group) => {
93
- if (!group.includes('.'))
94
- return [group];
95
- const bytes = group.split('.').map(Number);
96
- const [first = -1, second = -1, third = -1, fourth = -1] = bytes;
97
- if (bytes.length !== 4
98
- || [first, second, third, fourth].some((byte) => !Number.isInteger(byte)))
99
- return [];
100
- return [
101
- (((first << 8) | second) >>> 0).toString(16),
102
- (((third << 8) | fourth) >>> 0).toString(16),
103
- ];
104
- });
105
- };
106
- const headGroups = toHexGroups(headStr);
107
- const tailGroups = hasDoubleColon ? toHexGroups(tailStr) : [];
108
- if (!hasDoubleColon)
109
- return headGroups.map((g) => g.padStart(4, '0').toLowerCase());
110
- const missing = 8 - headGroups.length - tailGroups.length;
111
- const middleGroups = new Array(Math.max(missing, 0)).fill('0');
112
- return [...headGroups, ...middleGroups, ...tailGroups].map((g) => g.padStart(4, '0').toLowerCase());
113
- }
114
75
  function groupsToIpv4(hiGroup, loGroup) {
115
76
  const hi = parseInt(hiGroup, 16);
116
77
  const lo = parseInt(loGroup, 16);
@@ -119,7 +80,7 @@ function groupsToIpv4(hiGroup, loGroup) {
119
80
  return [(hi >> 8) & 0xff, hi & 0xff, (lo >> 8) & 0xff, lo & 0xff].join('.');
120
81
  }
121
82
  function isPrivateIpv6(ip) {
122
- const groups = expandIpv6Groups(ip.toLowerCase());
83
+ const groups = ipv6Groups(ip);
123
84
  if (groups.length !== 8)
124
85
  return true; // couldn't parse cleanly - fail closed
125
86
  const [g0 = '', g1 = '', g2 = '', g3 = '', g4 = '', g5 = '', g6 = '', g7 = '',] = groups;
@@ -319,6 +280,47 @@ async function raceWithSignal(promise, signal) {
319
280
  void promise.then((value) => { signal.removeEventListener('abort', abort); resolve(value); }, (error) => { signal.removeEventListener('abort', abort); reject(error); });
320
281
  });
321
282
  }
283
+ // Request bodies that can be replayed must be owned before the first await.
284
+ // Streams and multipart encoders remain valid for one request, never a replay.
285
+ function ownedRequestBody(body) {
286
+ if (body instanceof URLSearchParams)
287
+ return new URLSearchParams(body);
288
+ if (body instanceof ArrayBuffer)
289
+ return body.slice(0);
290
+ if (ArrayBuffer.isView(body))
291
+ return Uint8Array.from(new Uint8Array(body.buffer, body.byteOffset, body.byteLength));
292
+ return body;
293
+ }
294
+ function redirectedRequest(options, status, source, destination) {
295
+ const next = { ...options, headers: new Headers(options.headers) };
296
+ const method = (options.method ?? 'GET').toUpperCase();
297
+ if ((status === 301 || status === 302) && method === 'POST'
298
+ || status === 303 && method !== 'GET' && method !== 'HEAD') {
299
+ next.method = 'GET';
300
+ next.body = null;
301
+ for (const name of ['content-encoding', 'content-language', 'content-location', 'content-type', 'content-length', 'transfer-encoding'])
302
+ next.headers.delete(name);
303
+ }
304
+ else if (next.body != null && !(typeof next.body === 'string' || next.body instanceof Blob
305
+ || next.body instanceof URLSearchParams || next.body instanceof ArrayBuffer || ArrayBuffer.isView(next.body))) {
306
+ throw new Error('Refusing to replay a streaming or multipart request body after a redirect');
307
+ }
308
+ if (source.origin !== destination.origin) {
309
+ // This collector is not a general authenticated browser. Do not forward a
310
+ // caller's body or custom credentials to another authority or a downgrade.
311
+ if (next.body != null || !['GET', 'HEAD'].includes((next.method ?? 'GET').toUpperCase())) {
312
+ throw new Error('Refusing to forward a request body or mutation across origins');
313
+ }
314
+ const publicHeaders = new Set(['accept', 'accept-encoding', 'accept-language', 'user-agent', 'range']);
315
+ for (const name of [...next.headers.keys()])
316
+ if (!publicHeaders.has(name))
317
+ next.headers.delete(name);
318
+ next.credentials = 'omit';
319
+ next.referrer = '';
320
+ next.referrerPolicy = 'no-referrer';
321
+ }
322
+ return next;
323
+ }
322
324
  // Detailed form of the shared safe request engine. It follows redirects
323
325
  // manually under the same DNS validation and connection-pinning policy as
324
326
  // safeFetch(), while retaining a bounded hop trace for consumers that need
@@ -343,10 +345,14 @@ async function safeFetchDetailed(url, options = {}, dependencies = {}) {
343
345
  const hops = [];
344
346
  let currentUrl = requestedUrl;
345
347
  let redirectCount = 0;
348
+ let requestOptions = { ...options, headers: new Headers(options.headers),
349
+ ...(options.body !== undefined ? { body: ownedRequestBody(options.body) } : {}) };
346
350
  while (true) {
347
351
  const parsed = new URL(currentUrl);
348
352
  const hostname = resolutionHostname(parsed);
349
- const signal = options.signal ?? null;
353
+ const signal = requestOptions.signal ?? null;
354
+ if (signal?.aborted)
355
+ throw abortError(signal);
350
356
  const records = normalizePublicAddressCandidates(await raceWithSignal(resolveAddresses(hostname), signal), hostname);
351
357
  if (signal?.aborted)
352
358
  throw abortError(signal);
@@ -354,7 +360,7 @@ async function safeFetchDetailed(url, options = {}, dependencies = {}) {
354
360
  const hopStartedAt = now();
355
361
  // `dispatcher` is a supported undici extension used to pin the connection.
356
362
  // Keep the non-standard property local to this request boundary.
357
- const fetchOptions = { ...options, redirect: 'manual', dispatcher };
363
+ const fetchOptions = { ...requestOptions, redirect: 'manual', dispatcher };
358
364
  let response;
359
365
  try {
360
366
  response = await request(currentUrl, fetchOptions);
@@ -384,6 +390,7 @@ async function safeFetchDetailed(url, options = {}, dependencies = {}) {
384
390
  if (nextUrl && redirectCount < maxRedirects) {
385
391
  await response.body?.cancel().catch(() => { });
386
392
  await closeDispatcher(dispatcher);
393
+ requestOptions = redirectedRequest(requestOptions, response.status, parsed, new URL(nextUrl));
387
394
  redirectCount += 1;
388
395
  currentUrl = nextUrl;
389
396
  continue;
@@ -418,8 +425,8 @@ async function safeFetch(url, options = {}, redirectsLeft = MAX_REDIRECTS) {
418
425
  // them can be made to hold an arbitrarily large body in memory. `truncated`
419
426
  // tells the caller whether the real body was actually cut short - callers
420
427
  // that need well-formed content (e.g. JSON) should treat that as a hard
421
- // failure rather than parsing a partial blob; callers that only need to
422
- // scan for a substring (e.g. plain-text/HTML matching) can safely ignore it.
428
+ // failure rather than parsing a partial blob. Partial-evidence consumers may
429
+ // use the prefix, but must preserve its incomplete scope.
423
430
  async function readTextCapped(res, maxBytes, options = {}) {
424
431
  const hasher = options.includeSha256 === true ? crypto.createHash('sha256') : null;
425
432
  const result = (text, truncated, bytesRead) => ({
@@ -1,107 +1,2 @@
1
- export const SCORE_EVIDENCE_QUALITY_VERSION = 1;
2
- const MAX_SOURCES = 24;
3
- const MAX_FAMILIES = 12;
4
- const SOURCE_ID_RE = /^[a-z][a-z0-9_-]{0,63}$/u;
5
- const COMPLETE_STATES = new Set(['complete', 'not_found', 'success']);
6
- const LIMITED_STATES = new Set(['partial', 'unknown']);
7
- const UNAVAILABLE_STATES = new Set(['error', 'rate_limited', 'unavailable']);
8
- const SKIPPED_STATES = new Set(['skipped', 'unsupported']);
9
- const ACCEPTED_SOURCE_STATES = new Set([
10
- ...COMPLETE_STATES,
11
- ...LIMITED_STATES,
12
- ...UNAVAILABLE_STATES,
13
- ...SKIPPED_STATES,
14
- ]);
15
- function boundedFamilyList(value) {
16
- const output = new Set();
17
- for (const item of value.slice(0, MAX_FAMILIES * 2)) {
18
- const normalized = String(item).trim().toLowerCase().replace(/[^a-z0-9_-]+/gu, '-').slice(0, 64);
19
- if (SOURCE_ID_RE.test(normalized))
20
- output.add(normalized);
21
- if (output.size >= MAX_FAMILIES)
22
- break;
23
- }
24
- return [...output].sort();
25
- }
26
- function sourceCoverage(value) {
27
- if (!Array.isArray(value))
28
- return [];
29
- const output = [];
30
- const seen = new Set();
31
- for (const candidate of value.slice(0, MAX_SOURCES * 4)) {
32
- if (!candidate || typeof candidate !== 'object' || Array.isArray(candidate))
33
- continue;
34
- const item = candidate;
35
- const source = typeof item.source === 'string' ? item.source.trim().toLowerCase() : '';
36
- const state = typeof item.state === 'string' ? item.state.trim().toLowerCase() : '';
37
- if (!SOURCE_ID_RE.test(source) || seen.has(source))
38
- continue;
39
- if (!ACCEPTED_SOURCE_STATES.has(state))
40
- continue;
41
- seen.add(source);
42
- output.push({ source, state });
43
- if (output.length >= MAX_SOURCES)
44
- break;
45
- }
46
- return output;
47
- }
48
- function scanDepth(value) {
49
- return value === 'deep' || value === 'fast' ? value : 'unknown';
50
- }
51
- function observedFreshness(value) {
52
- if (typeof value !== 'string' || value.length > 64)
53
- return 'unknown';
54
- return Number.isFinite(Date.parse(value)) ? 'observed' : 'unknown';
55
- }
56
- export function buildScoreEvidenceQuality(input, options) {
57
- const depth = scanDepth(input.scanDepth);
58
- const coverage = sourceCoverage(input.sourceCoverage);
59
- const completeSources = coverage.filter((item) => COMPLETE_STATES.has(item.state)).length;
60
- const limitedSources = coverage.filter((item) => LIMITED_STATES.has(item.state)).length;
61
- const unavailableSources = coverage.filter((item) => UNAVAILABLE_STATES.has(item.state)).length;
62
- const skippedSources = coverage.filter((item) => SKIPPED_STATES.has(item.state)).length;
63
- const observedFamilies = boundedFamilyList(options.observedFamilies ?? []);
64
- const expectedFamilies = boundedFamilyList(options.expectedFamilies ?? []);
65
- const observedSet = new Set(observedFamilies);
66
- const missingFamilies = expectedFamilies.filter((family) => !observedSet.has(family));
67
- const limitations = [];
68
- let state = 'complete';
69
- if (!coverage.length || depth === 'unknown')
70
- state = 'unknown';
71
- else if (unavailableSources || limitedSources)
72
- state = 'partial';
73
- else if (options.expectedDepth === 'deep' && depth !== 'deep')
74
- state = 'limited';
75
- if (missingFamilies.length && state === 'complete')
76
- state = 'limited';
77
- if (options.expectedDepth === 'deep' && depth !== 'deep') {
78
- limitations.push('A fast or unknown-depth observation does not evaluate every website and mail scoring family.');
79
- }
80
- if (limitedSources || unavailableSources) {
81
- limitations.push('One or more score-relevant sources were partial, unavailable, or indeterminate.');
82
- }
83
- if (skippedSources) {
84
- limitations.push('One or more reported sources were skipped or unsupported and did not contribute to the score.');
85
- }
86
- if (!coverage.length)
87
- limitations.push('Source-level coverage was not supplied with this score.');
88
- if (missingFamilies.length)
89
- limitations.push(`No usable observation was supplied for: ${missingFamilies.join(', ')}.`);
90
- const boundedLimitations = [
91
- ...limitations.slice(0, 5),
92
- 'Evidence coverage qualifies the score; it never adds points or turns missing evidence into a negative finding.',
93
- ];
94
- return Object.freeze({
95
- version: SCORE_EVIDENCE_QUALITY_VERSION,
96
- state,
97
- scanDepth: depth,
98
- freshness: observedFreshness(input.observedAt),
99
- completeSources,
100
- limitedSources,
101
- unavailableSources,
102
- skippedSources,
103
- observedFamilies: Object.freeze(observedFamilies),
104
- missingFamilies: Object.freeze(missingFamilies),
105
- limitations: Object.freeze(boundedLimitations),
106
- });
107
- }
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/scoring-evidence-quality.mjs";