@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,79 @@
1
+ import { normalizeExplicitIsoTimestamp } from "./observation.mjs";
2
+ import { normalizeDomain } from "./domain-name.mjs";
3
+ export const DOMAIN_POSTURE_COMPARISON_VERSION = 1;
4
+ export const MAX_POSTURE_CHECKS = 32;
5
+ export const MAX_POSTURE_CHECK_RECORDS = 64;
6
+ export const MAX_POSTURE_RECORD_LENGTH = 4096;
7
+ function contextRecord(value, keys, label) {
8
+ if (!value || typeof value !== 'object' || Array.isArray(value)
9
+ || ![Object.prototype, null].includes(Object.getPrototypeOf(value))
10
+ || Reflect.ownKeys(value).length !== keys.length)
11
+ throw new TypeError(`${label} has an invalid structure.`);
12
+ const result = {};
13
+ for (const key of keys) {
14
+ const descriptor = Object.getOwnPropertyDescriptor(value, key);
15
+ if (!descriptor?.enumerable || !('value' in descriptor))
16
+ throw new TypeError(`${label} requires ordinary data fields.`);
17
+ result[key] = descriptor.value;
18
+ }
19
+ if (result.version !== DOMAIN_POSTURE_COMPARISON_VERSION)
20
+ throw new TypeError(`${label} has an unsupported comparison version.`);
21
+ return result;
22
+ }
23
+ /** Missing legacy provenance stays missing; declared malformed or future context is rejected. */
24
+ export function normalizeDomainPostureSourceContext(value) {
25
+ if (value === undefined)
26
+ return undefined;
27
+ const source = contextRecord(value, ['version', 'source', 'observedAt', 'state', 'omittedRecords'], 'Posture source context');
28
+ const observedAt = normalizeExplicitIsoTimestamp(source.observedAt);
29
+ if (typeof source.source !== 'string' || !['dns_ns', 'dns_mx', 'dns_caa', 'registry_rdap'].includes(source.source)
30
+ || typeof source.state !== 'string' || !['complete', 'partial', 'unavailable'].includes(source.state)
31
+ || (source.observedAt !== null && observedAt === null)
32
+ || (source.omittedRecords !== null && (!Number.isSafeInteger(source.omittedRecords) || Number(source.omittedRecords) < 0))
33
+ || (source.state === 'complete' && source.omittedRecords !== 0)) {
34
+ throw new TypeError('Posture source context has invalid provenance or omission counts.');
35
+ }
36
+ return Object.freeze({ ...source, observedAt });
37
+ }
38
+ export function normalizeDomainPostureProfileContext(value) {
39
+ if (value === undefined)
40
+ return undefined;
41
+ const source = contextRecord(value, ['version', 'domain', 'profileId', 'profileFingerprint'], 'Posture profile context');
42
+ const domain = typeof source.domain === 'string' ? normalizeDomain(source.domain) : '';
43
+ if (!domain || domain !== source.domain || typeof source.profileId !== 'string'
44
+ || !source.profileId.trim() || source.profileId.length > 200 || /[\u0000-\u001f\u007f]/u.test(source.profileId)
45
+ || typeof source.profileFingerprint !== 'string' || !/^[a-f0-9]{64}$/u.test(source.profileFingerprint)) {
46
+ throw new TypeError('Posture profile context has an invalid target or collection identity.');
47
+ }
48
+ return Object.freeze(source);
49
+ }
50
+ export const POSTURE_CHECK_SOURCES = Object.freeze({
51
+ nameservers: 'dns_ns', mx: 'dns_mx', caa: 'dns_caa', registration_lock: 'registry_rdap',
52
+ });
53
+ export const POSTURE_SOURCE_LABELS = Object.freeze({
54
+ dns_ns: 'DNS NS', dns_mx: 'DNS MX', dns_caa: 'DNS CAA', registry_rdap: 'Registry RDAP',
55
+ });
56
+ export function postureTransferRestriction(records) {
57
+ if (!records.length)
58
+ return null;
59
+ return records.some((value) => value === 'clienttransferprohibited' || value === 'servertransferprohibited')
60
+ ? 'required' : 'not_required';
61
+ }
62
+ export function postureSourceAdmission(check, capturedAt, now) {
63
+ if (!check?.sourceContext)
64
+ return 'Source comparison context is unavailable for this retained check.';
65
+ const context = check.sourceContext;
66
+ const expected = POSTURE_CHECK_SOURCES[check.id];
67
+ if (context.version !== DOMAIN_POSTURE_COMPARISON_VERSION || !expected || expected !== context.source)
68
+ return 'The source or comparison version does not match this field.';
69
+ const captured = normalizeExplicitIsoTimestamp(capturedAt);
70
+ const observed = normalizeExplicitIsoTimestamp(context.observedAt);
71
+ const reviewed = normalizeExplicitIsoTimestamp(now);
72
+ if (!captured || !observed || !reviewed)
73
+ return 'A source, capture or review time is unavailable.';
74
+ if (Date.parse(observed) > Date.parse(captured) || Date.parse(captured) > Date.parse(reviewed))
75
+ return 'The source or capture time is later than its review.';
76
+ if (context.state !== 'complete' || context.omittedRecords !== 0)
77
+ return 'The source observation is incomplete; retained values cannot establish alignment or drift.';
78
+ return null;
79
+ }
@@ -0,0 +1,18 @@
1
+ // Single plain recipient admission for manually opened drafts, not delivery validation.
2
+ const SINGLE_ADDRESS = /^[A-Za-z0-9._%+-]+@[A-Za-z0-9.-]+\.[A-Za-z]{2,}$/u;
3
+ export function emailRecipient(value) {
4
+ if (typeof value !== 'string' || value.length > 320 || /[\u0000-\u001f\u007f]/u.test(value))
5
+ return null;
6
+ const address = value.trim();
7
+ return SINGLE_ADDRESS.test(address) ? address : null;
8
+ }
9
+ /** Encode the recipient independently; upstream text never becomes a header. */
10
+ export function recipientMailto(value, headers) {
11
+ const address = emailRecipient(value);
12
+ if (!address)
13
+ return null;
14
+ const separator = address.indexOf('@');
15
+ const recipient = `${encodeURIComponent(address.slice(0, separator))}@${encodeURIComponent(address.slice(separator + 1))}`;
16
+ const query = headers ? `?subject=${encodeURIComponent(headers.subject)}&body=${encodeURIComponent(headers.body)}` : '';
17
+ return `mailto:${recipient}${query}`;
18
+ }
@@ -0,0 +1,56 @@
1
+ import { array, enumeration, exact, integer, text } from "./artifact-structure.mjs";
2
+ import { readRetainedFileReference } from "./retained-file.mjs";
3
+ // The decoded RGBA surface is bounded independently of compressed file bytes.
4
+ export const MAX_EVIDENCE_IMAGE_PIXELS = 16 * 1024 * 1024;
5
+ export const MAX_EVIDENCE_IMAGE_DIMENSION = 10_000;
6
+ // Each selected region emits at most four solid rectangles; no nested effects.
7
+ export const MAX_EVIDENCE_IMAGE_REGIONS = 64;
8
+ export function readEvidenceImageDimensions(width, height) {
9
+ const result = {
10
+ width: integer(width, 'Image width', 1, MAX_EVIDENCE_IMAGE_DIMENSION),
11
+ height: integer(height, 'Image height', 1, MAX_EVIDENCE_IMAGE_DIMENSION),
12
+ };
13
+ if (result.width * result.height > MAX_EVIDENCE_IMAGE_PIXELS)
14
+ throw new TypeError('This image exceeds the decoded-image pixel bound. Its original bytes are unchanged.');
15
+ return Object.freeze(result);
16
+ }
17
+ /** Pixel coordinates are integers, remain inside the source, and are never clipped silently. */
18
+ export function readImageRegionPlan(raw) {
19
+ const value = exact(raw, ['width', 'height', 'regions'], 'Image region plan');
20
+ const dimensions = readEvidenceImageDimensions(value.width, value.height);
21
+ const regions = Array.from(array(value.regions, 'Image regions', MAX_EVIDENCE_IMAGE_REGIONS, 1), candidate => {
22
+ const region = exact(candidate, ['kind', 'x', 'y', 'width', 'height'], 'Image region');
23
+ const kind = enumeration(region.kind, ['redact', 'outline'], 'Image region kind');
24
+ const x = integer(region.x, 'Region left coordinate', 0, dimensions.width - 1);
25
+ const y = integer(region.y, 'Region top coordinate', 0, dimensions.height - 1);
26
+ const width = integer(region.width, 'Region width', 1, dimensions.width - x);
27
+ const height = integer(region.height, 'Region height', 1, dimensions.height - y);
28
+ return Object.freeze({ kind, x, y, width, height });
29
+ });
30
+ return Object.freeze({ ...dimensions, regions: Object.freeze(regions) });
31
+ }
32
+ export function readImageDerivation(raw) {
33
+ const value = exact(raw, ['method', 'sourceAttachmentId', 'source', 'plan'], 'Image derivation');
34
+ if (value.method !== 'png-regions-v1')
35
+ throw new TypeError('This image derivation method is unsupported.');
36
+ return Object.freeze({
37
+ method: value.method,
38
+ sourceAttachmentId: text(value.sourceAttachmentId, 'Source attachment ID', 128),
39
+ source: readRetainedFileReference(value.source),
40
+ plan: readImageRegionPlan(value.plan),
41
+ });
42
+ }
43
+ /** Opaque redactions are always last, including where annotations overlap them. */
44
+ export function imageRegionPaintRectangles(raw) {
45
+ const plan = readImageRegionPlan(raw);
46
+ const result = [];
47
+ for (const region of plan.regions.filter(region => region.kind === 'outline')) {
48
+ const thickness = Math.min(3, region.width, region.height);
49
+ const { x, y, width, height } = region;
50
+ result.push({ x, y, width, height: thickness, colour: '#ffbf00' }, { x, y: y + height - thickness, width, height: thickness, colour: '#ffbf00' }, { x, y, width: thickness, height, colour: '#ffbf00' }, { x: x + width - thickness, y, width: thickness, height, colour: '#ffbf00' });
51
+ }
52
+ for (const region of plan.regions.filter(region => region.kind === 'redact')) {
53
+ result.push({ x: region.x, y: region.y, width: region.width, height: region.height, colour: '#000000' });
54
+ }
55
+ return result;
56
+ }
@@ -0,0 +1,28 @@
1
+ import { normalizeExplicitIsoTimestamp } from "./observation.mjs";
2
+ /** Select a time cohort from an already bounded source; never rank undated evidence as older. */
3
+ export function latestObservationCohort(values, observedAt) {
4
+ let latestTime = null;
5
+ let latestMillis = -Infinity;
6
+ let latest = [];
7
+ const undated = [];
8
+ let superseded = 0;
9
+ for (const value of values) {
10
+ const time = normalizeExplicitIsoTimestamp(observedAt(value));
11
+ if (time === null) {
12
+ undated.push(value);
13
+ continue;
14
+ }
15
+ const millis = Date.parse(time);
16
+ if (millis > latestMillis) {
17
+ superseded += latest.length;
18
+ latestTime = time;
19
+ latestMillis = millis;
20
+ latest = [value];
21
+ }
22
+ else if (millis === latestMillis)
23
+ latest.push(value);
24
+ else
25
+ superseded += 1;
26
+ }
27
+ return { observedAt: latestTime, latest, undated, superseded };
28
+ }
@@ -0,0 +1,108 @@
1
+ // Ordinary collection retains a hostname, not a pasted URL's private path or query.
2
+ // Registration classification and network-address safety remain separate.
3
+ import { isValidAsciiHostname } from "../contracts/domain-name.mjs";
4
+ import { MAX_OUTBOUND_HTTP_URL_CHARACTERS } from "../contracts/http-url.mjs";
5
+ export const MAX_LOOKUP_INPUT_CHARACTERS = 2 * 1024 * 1024;
6
+ const URL_SCHEME = /^[a-z][a-z\d+.-]*:\/\//iu;
7
+ const CONTROL = /[\u0000-\u001f\u007f]/u;
8
+ /** Lexical recognition only; protocol, credentials and target admission remain separate. */
9
+ export function hasLookupUrlScheme(value) {
10
+ return URL_SCHEME.test(value);
11
+ }
12
+ export function validWebObservationMode(value) {
13
+ return value === undefined || value === 'selected_url';
14
+ }
15
+ /** A separate deliberate action; ordinary URL input never selects its path. */
16
+ export function prepareSelectedLookupUrl(value, hostname) {
17
+ if (typeof value !== 'string' || value.includes('\\'))
18
+ throw new TypeError('Select a valid HTTP(S) URL.');
19
+ const parsed = parseCredentialFreeHttpUrl(value, MAX_OUTBOUND_HTTP_URL_CHARACTERS);
20
+ if (!parsed || parsed.port || !isValidAsciiHostname(parsed.hostname)
21
+ || /^\d+\.\d+\.\d+\.\d+$/u.test(parsed.hostname)
22
+ || hostname !== undefined && parsed.hostname !== hostname) {
23
+ throw new TypeError('Selected URL must use the submitted hostname and a default HTTP(S) port, without credentials.');
24
+ }
25
+ parsed.hash = '';
26
+ const url = parsed.toString();
27
+ if (url.length > MAX_OUTBOUND_HTTP_URL_CHARACTERS)
28
+ throw new TypeError('Selected URL exceeds the request bound.');
29
+ return url;
30
+ }
31
+ /** Older results collected supporting evidence at the registrable domain. */
32
+ export function lookupObservationHostname(availability) {
33
+ const value = availability.observationHostname === undefined ? availability.domain : availability.observationHostname;
34
+ return typeof value === 'string' && isValidAsciiHostname(value) && value === value.toLowerCase() ? value : null;
35
+ }
36
+ function validLookupObservationHostname(value, query) {
37
+ return typeof value === 'string' && isValidAsciiHostname(value) && value === value.toLowerCase()
38
+ && value === (query.inputHostname ?? query.submitted ?? query.registrableDomain)
39
+ && typeof query.registrableDomain === 'string'
40
+ && (value === query.registrableDomain || value.endsWith(`.${query.registrableDomain}`));
41
+ }
42
+ /** Bind collection identities without treating an omitted historical identity as new evidence. */
43
+ export function validLookupObservationScope(availability, query) {
44
+ if (availability.observationHostname !== undefined && !validLookupObservationHostname(availability.observationHostname, query))
45
+ return false;
46
+ if (!validWebObservationMode(availability.webObservationMode))
47
+ return false;
48
+ if (availability.webObservationMode === 'selected_url') {
49
+ if (!validLookupObservationHostname(availability.observationHostname, query))
50
+ return false;
51
+ const http = availability.http;
52
+ const requestUrl = http && typeof http === 'object' && !Array.isArray(http) ? Reflect.get(http, 'requestUrl') : null;
53
+ const parsed = parseCredentialFreeHttpUrl(requestUrl, MAX_OUTBOUND_HTTP_URL_CHARACTERS);
54
+ if (!parsed || parsed.hostname !== availability.observationHostname || parsed.port || parsed.search || parsed.hash)
55
+ return false;
56
+ }
57
+ const dns = availability.dns;
58
+ const delegation = dns && typeof dns === 'object' && !Array.isArray(dns) ? Reflect.get(dns, 'delegation') : null;
59
+ const registrationDomain = delegation && typeof delegation === 'object' && !Array.isArray(delegation) ? Reflect.get(delegation, 'domain') : null;
60
+ return registrationDomain == null || (typeof registrationDomain === 'string'
61
+ && isValidAsciiHostname(registrationDomain) && registrationDomain === query.registrableDomain);
62
+ }
63
+ export function parseCredentialFreeHttpUrl(value, maximumLength) {
64
+ if (typeof value !== 'string' || !value || value.length > maximumLength
65
+ || value.trim() !== value || CONTROL.test(value))
66
+ return null;
67
+ try {
68
+ const parsed = new URL(value);
69
+ return (parsed.protocol === 'https:' || parsed.protocol === 'http:')
70
+ && !parsed.username && !parsed.password && parsed.hostname ? parsed : null;
71
+ }
72
+ catch {
73
+ return null;
74
+ }
75
+ }
76
+ export function prepareLookupCollectionTarget(value) {
77
+ if (value.length > MAX_LOOKUP_INPUT_CHARACTERS || CONTROL.test(value) || value.includes('\\')) {
78
+ throw new TypeError('Lookup input exceeds its bound or contains control characters.');
79
+ }
80
+ const raw = value.trim();
81
+ if (!raw)
82
+ throw new TypeError('Enter a domain, IP address or ASN.');
83
+ const absolute = hasLookupUrlScheme(raw);
84
+ // An unbracketed IPv6 literal is not a URL with a port. Use the same URL
85
+ // parser to admit its syntax, then leave address safety and ASN bounds to
86
+ // the classifier; malformed colon-separated text must not become a request.
87
+ if (!absolute && !/[\s/@?#\\]/u.test(raw)) {
88
+ if (/^(?:AS)?\d+$/iu.test(raw))
89
+ return raw;
90
+ if ((raw.match(/:/gu)?.length ?? 0) > 1 && !raw.startsWith('[')) {
91
+ if (!parseCredentialFreeHttpUrl(`https://[${raw}]`, MAX_LOOKUP_INPUT_CHARACTERS + 10)) {
92
+ throw new TypeError('Enter a valid IPv6 address without a scope identifier.');
93
+ }
94
+ return raw;
95
+ }
96
+ }
97
+ const parsed = parseCredentialFreeHttpUrl(absolute ? raw : `https://${raw}`, MAX_LOOKUP_INPUT_CHARACTERS + 'https://'.length);
98
+ if (!parsed)
99
+ throw new TypeError('Enter a valid domain, IP, or ASN. URLs must use HTTP(S) without credentials.');
100
+ if (/^\d+\.\d+\.\d+\.\d+$/u.test(parsed.hostname)) {
101
+ const authority = (absolute ? raw.slice(raw.indexOf('://') + 3) : raw).split(/[/?#]/u)[0];
102
+ const originalHost = authority.split(':')[0].replace(/\.$/u, '');
103
+ if (originalHost !== parsed.hostname)
104
+ throw new TypeError('Use a complete canonical IP address, not an abbreviated or encoded address.');
105
+ }
106
+ // WHATWG URLs bracket IPv6; the existing classifier accepts the bare address.
107
+ return parsed.hostname.replace(/^\[|\]$/gu, '').toLowerCase();
108
+ }
@@ -68,6 +68,7 @@ const REGISTERED_DIAGNOSTIC_KEYS = [
68
68
  'discarded_relationships',
69
69
  'dmarc',
70
70
  'documentsParsed',
71
+ 'eligibleAuthorityCount',
71
72
  'entities',
72
73
  'error',
73
74
  'externalScriptsSkipped',
@@ -96,13 +97,17 @@ const REGISTERED_DIAGNOSTIC_KEYS = [
96
97
  'objectsExamined',
97
98
  'observed',
98
99
  'observedAbsence',
100
+ 'omittedAuthorityCount',
99
101
  'partialAuthorityCount',
100
102
  'passiveHeadersEvaluated',
101
103
  'potentialExposure',
102
104
  'ptr',
103
105
  'queriedAddressCount',
106
+ 'queriedAuthorityCount',
104
107
  'redirectCount',
105
108
  'referencesExamined',
109
+ 'referenceSignatureTimedOut',
110
+ 'referenceSignatureUnavailable',
106
111
  'rejectedRows',
107
112
  'relationshipTagsExamined',
108
113
  'relationshipUrlsDiscarded',
@@ -238,6 +243,18 @@ function canonicalTimestamp(value, expression, assignUtcWhenMissing) {
238
243
  function normalizeExplicitIsoTimestamp(value) {
239
244
  return canonicalTimestamp(value, ISO_DATE_TIME_RE, false);
240
245
  }
246
+ /** Retain a source's timestamp; missing, invalid or future times have no usable age. */
247
+ function readObservationTime(value, now) {
248
+ const observedAt = normalizeExplicitIsoTimestamp(value);
249
+ const reviewedAt = normalizeExplicitIsoTimestamp(now);
250
+ const elapsed = observedAt && reviewedAt
251
+ ? Date.parse(reviewedAt) - Date.parse(observedAt)
252
+ : Number.NaN;
253
+ return {
254
+ observedAt,
255
+ ageDays: Number.isFinite(elapsed) && elapsed >= 0 ? Math.floor(elapsed / 86_400_000) : null,
256
+ };
257
+ }
241
258
  // Frozen legacy schemas accepted zone-less ISO date-times. Preserve that
242
259
  // migration path deterministically by assigning UTC instead of consulting the
243
260
  // host timezone. Current schemas must use normalizeExplicitIsoTimestamp.
@@ -381,4 +398,4 @@ function readObservationEnvelope(value) {
381
398
  }
382
399
  return { state: 'supported', observation: createObservationFromSnapshot(record) };
383
400
  }
384
- export { MAX_OBSERVATION_DIAGNOSTICS, MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, OBSERVATION_VERSION, createObservation, normalizeCtTimestamp, normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp, readObservationEnvelope, };
401
+ export { MAX_OBSERVATION_DIAGNOSTICS, MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, OBSERVATION_VERSION, createObservation, normalizeCtTimestamp, normalizeExplicitIsoTimestamp, normalizeLegacyIsoTimestamp, readObservationEnvelope, readObservationTime, };
@@ -0,0 +1,44 @@
1
+ // Passphrase policy and native key derivation shared by portable encrypted files.
2
+ import { MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES, MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS, WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS } from "../contracts/case-portability.mjs";
3
+ export const AES_KEY_BITS = 256;
4
+ const encoder = new TextEncoder();
5
+ export function arrayBuffer(bytes) {
6
+ return new Uint8Array(bytes).buffer;
7
+ }
8
+ export function assertPassphrase(passphrase) {
9
+ if (typeof passphrase !== 'string') {
10
+ throw new Error(`Use a backup passphrase with at least ${MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS} characters.`);
11
+ }
12
+ if (passphrase.length > MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES) {
13
+ throw new Error('Backup passphrases are limited to 1024 UTF-8 bytes.');
14
+ }
15
+ if (Array.from(passphrase).length < MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS) {
16
+ throw new Error(`Use a backup passphrase with at least ${MIN_WORKSPACE_ARCHIVE_PASSPHRASE_CHARACTERS} characters.`);
17
+ }
18
+ const bytes = encoder.encode(passphrase);
19
+ if (bytes.byteLength > MAX_WORKSPACE_ARCHIVE_PASSPHRASE_BYTES) {
20
+ bytes.fill(0);
21
+ throw new Error('Backup passphrases are limited to 1024 UTF-8 bytes.');
22
+ }
23
+ return bytes;
24
+ }
25
+ export function cryptoProvider(provider = globalThis.crypto) {
26
+ if (!provider
27
+ || typeof provider.getRandomValues !== 'function'
28
+ || typeof provider.subtle?.importKey !== 'function'
29
+ || typeof provider.subtle?.deriveKey !== 'function'
30
+ || typeof provider.subtle?.encrypt !== 'function'
31
+ || typeof provider.subtle?.decrypt !== 'function') {
32
+ throw new Error('Encrypted workspace archives are unavailable in this browser.');
33
+ }
34
+ return provider;
35
+ }
36
+ export async function deriveArchiveKey(provider, passphraseBytes, salt, usages) {
37
+ const material = await provider.subtle.importKey('raw', arrayBuffer(passphraseBytes), 'PBKDF2', false, ['deriveKey']);
38
+ return provider.subtle.deriveKey({
39
+ name: 'PBKDF2',
40
+ hash: 'SHA-256',
41
+ salt: arrayBuffer(salt),
42
+ iterations: WORKSPACE_ARCHIVE_PBKDF2_ITERATIONS,
43
+ }, material, { name: 'AES-GCM', length: AES_KEY_BITS }, false, usages);
44
+ }
@@ -0,0 +1,117 @@
1
+ import { decode, convertIndexedToRgb, hasPngSignature } from 'fast-png';
2
+ import { Unzlib } from 'fflate';
3
+ import { updateCrc32 } from "../interchange/crc32.mjs";
4
+ import { readEvidenceImageDimensions } from "./image-regions.mjs";
5
+ /** Admit PNG structure and inflated work before the image library allocates it. */
6
+ export function decodeEvidencePng(bytes) {
7
+ if (!(bytes instanceof Uint8Array) || !(bytes.buffer instanceof ArrayBuffer) || !hasPngSignature(bytes) || bytes.byteLength < 33)
8
+ throw new TypeError('Select a valid PNG image.');
9
+ const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength);
10
+ if (view.getUint32(8) !== 13 || String.fromCharCode(...bytes.subarray(12, 16)) !== 'IHDR')
11
+ throw new TypeError('The PNG header is malformed.');
12
+ const { width, height } = readEvidenceImageDimensions(view.getUint32(16), view.getUint32(20));
13
+ if (bytes[24] < 8 && bytes[28] !== 0)
14
+ throw new TypeError('Convert this interlaced low-bit-depth PNG to a non-interlaced image before review.');
15
+ const maximumInflatedBytes = width * height * 8 + height * 8 + 64;
16
+ let inflatedBytes = 0;
17
+ const inflater = new Unzlib(chunk => {
18
+ inflatedBytes += chunk.byteLength;
19
+ if (inflatedBytes > maximumInflatedBytes)
20
+ throw new TypeError('The PNG exceeds its dimension-derived decompression bound.');
21
+ });
22
+ const selected = [bytes.subarray(0, 8)];
23
+ let offset = 8, total = 8, ended = false, imageData = false, dataEnded = false, header = false, palette = false, transparency = false;
24
+ while (offset < bytes.byteLength) {
25
+ if (bytes.byteLength - offset < 12)
26
+ throw new TypeError('The PNG contains a truncated chunk.');
27
+ const length = view.getUint32(offset), end = offset + length + 12;
28
+ if (end > bytes.byteLength)
29
+ throw new TypeError('The PNG contains a truncated chunk.');
30
+ const kind = String.fromCharCode(...bytes.subarray(offset + 4, offset + 8));
31
+ if (!/^[A-Za-z]{4}$/u.test(kind))
32
+ throw new TypeError('The PNG contains an invalid chunk type.');
33
+ const crc = (updateCrc32(0xffff_ffff, bytes.subarray(offset + 4, end - 4)) ^ 0xffff_ffff) >>> 0;
34
+ if (crc !== view.getUint32(end - 4))
35
+ throw new TypeError('The PNG chunk checksum does not match.');
36
+ if (kind === 'acTL' || kind === 'fcTL' || kind === 'fdAT')
37
+ throw new TypeError('Select a still PNG image; animated frames are not silently omitted.');
38
+ if (kind === 'IHDR') {
39
+ if (header || offset !== 8 || length !== 13)
40
+ throw new TypeError('The PNG has conflicting headers.');
41
+ header = true;
42
+ }
43
+ else if (kind === 'PLTE') {
44
+ if (palette || imageData || !length || length > 768 || length % 3)
45
+ throw new TypeError('The PNG palette is malformed.');
46
+ palette = true;
47
+ }
48
+ else if (kind === 'tRNS') {
49
+ if (transparency || imageData || length > 256)
50
+ throw new TypeError('The PNG transparency data is malformed.');
51
+ transparency = true;
52
+ }
53
+ else if (kind === 'IDAT') {
54
+ if (dataEnded)
55
+ throw new TypeError('The PNG image-data chunks are not consecutive.');
56
+ imageData = true;
57
+ // A small compressed slice bounds transient inflation before the callback.
58
+ for (let position = offset + 8; position < end - 4; position += 1_024)
59
+ inflater.push(bytes.subarray(position, Math.min(position + 1_024, end - 4)), false);
60
+ }
61
+ else if (kind === 'IEND') {
62
+ if (length || !imageData || end !== bytes.byteLength)
63
+ throw new TypeError('The PNG trailer is malformed.');
64
+ ended = true;
65
+ }
66
+ else if (kind[0] === kind[0].toUpperCase())
67
+ throw new TypeError('The PNG has an unsupported critical chunk.');
68
+ if (imageData && kind !== 'IDAT')
69
+ dataEnded = true;
70
+ // Colour-profile and text metadata are irrelevant to QR pixel analysis and
71
+ // may contain independent compressed payloads. Never decode those payloads.
72
+ if (['IHDR', 'PLTE', 'tRNS', 'IDAT', 'IEND'].includes(kind)) {
73
+ const part = bytes.subarray(offset, end);
74
+ selected.push(part);
75
+ total += part.byteLength;
76
+ }
77
+ offset = end;
78
+ }
79
+ if (!ended)
80
+ throw new TypeError('The PNG trailer is missing.');
81
+ inflater.push(new Uint8Array(), true);
82
+ const admitted = new Uint8Array(total);
83
+ let position = 0;
84
+ for (const part of selected) {
85
+ admitted.set(part, position);
86
+ position += part.byteLength;
87
+ }
88
+ let image = decode(admitted, { checkCrc: true });
89
+ if (image.width !== width || image.height !== height)
90
+ throw new TypeError('The decoded PNG dimensions changed.');
91
+ if (image.depth < 8 && !image.palette && image.channels === 1) {
92
+ const maximum = (1 << image.depth) - 1;
93
+ // Use the library's packed-sample conversion for grayscale as well as
94
+ // indexed images; a generated grayscale palette does not parse new bytes.
95
+ image = { ...image, palette: Array.from({ length: maximum + 1 }, (_, value) => {
96
+ const grey = Math.round(value * 255 / maximum);
97
+ return [grey, grey, grey, image.transparency?.[0] === value ? 0 : 255];
98
+ }) };
99
+ }
100
+ const indexed = Boolean(image.palette), samples = indexed ? convertIndexedToRgb(image) : image.data;
101
+ const channels = indexed ? image.palette[0].length : image.channels;
102
+ if (![1, 2, 3, 4].includes(channels) || samples.length !== width * height * channels)
103
+ throw new TypeError('The PNG pixel layout is unsupported.');
104
+ const pixels = new Uint8ClampedArray(width * height * 4);
105
+ const sample = (index) => image.depth === 16 && !indexed ? Math.round(samples[index] / 257) : samples[index];
106
+ for (let index = 0; index < width * height; index++) {
107
+ const source = index * channels, target = index * 4;
108
+ pixels[target] = sample(source);
109
+ pixels[target + 1] = channels <= 2 ? sample(source) : sample(source + 1);
110
+ pixels[target + 2] = channels <= 2 ? sample(source) : sample(source + 2);
111
+ pixels[target + 3] = channels === 2 || channels === 4 ? sample(source + channels - 1) : 255;
112
+ if (!indexed && image.transparency && (channels === 1 ? samples[source] === image.transparency[0]
113
+ : channels === 3 && [0, 1, 2].every(channel => samples[source + channel] === image.transparency[channel])))
114
+ pixels[target + 3] = 0;
115
+ }
116
+ return { width, height, pixels };
117
+ }
@@ -0,0 +1,83 @@
1
+ // Browser-safe canonical public-address policy for analyst-supplied address
2
+ // evidence. This performs no resolution or network work; transport owners must
3
+ // still revalidate and pin addresses before making a request.
4
+ import { ipv6Groups } from "../contracts/ip-address.mjs";
5
+ const IPV4_RE = /^\d{1,3}(?:\.\d{1,3}){3}$/u;
6
+ const IPV6_RE = /^[0-9a-f:]+$/iu;
7
+ function canonicalIpv4(value) {
8
+ if (typeof value !== 'string' || !IPV4_RE.test(value.trim()))
9
+ return null;
10
+ const octets = value.trim().split('.').map(Number);
11
+ if (octets.some((octet) => !Number.isInteger(octet) || octet < 0 || octet > 255))
12
+ return null;
13
+ return octets.join('.');
14
+ }
15
+ function isPublicIpv4(value) {
16
+ const octets = value.split('.').map(Number);
17
+ const [first = -1, second = -1, third = -1] = octets;
18
+ return octets.length === 4
19
+ && first !== 0
20
+ && first !== 10
21
+ && first !== 127
22
+ && first < 224
23
+ && !(first === 100 && second >= 64 && second <= 127)
24
+ && !(first === 169 && second === 254)
25
+ && !(first === 172 && second >= 16 && second <= 31)
26
+ && !(first === 192 && second === 168)
27
+ && !(first === 198 && (second === 18 || second === 19))
28
+ && !(first === 192 && second === 0 && third === 0)
29
+ && !(first === 192 && second === 0 && third === 2)
30
+ && !(first === 192 && second === 88 && third === 99)
31
+ && !(first === 198 && second === 51 && third === 100)
32
+ && !(first === 203 && second === 0 && third === 113);
33
+ }
34
+ function canonicalIpv6(value) {
35
+ if (typeof value !== 'string')
36
+ return null;
37
+ const candidate = value.trim().toLowerCase();
38
+ if (!candidate.includes(':') || candidate.includes('%') || candidate.includes('.') || !IPV6_RE.test(candidate)) {
39
+ return null;
40
+ }
41
+ try {
42
+ const host = new URL(`https://[${candidate}]/`).hostname;
43
+ return host.startsWith('[') && host.endsWith(']') ? host.slice(1, -1) : null;
44
+ }
45
+ catch {
46
+ return null;
47
+ }
48
+ }
49
+ function isPublicIpv6(value) {
50
+ const groups = ipv6Groups(value);
51
+ if (groups.length !== 8)
52
+ return false;
53
+ const [first = '', second = '', third = ''] = groups;
54
+ if (groups.every((group) => group === '0000'))
55
+ return false;
56
+ if (groups.slice(0, 7).every((group) => group === '0000') && groups[7] === '0001')
57
+ return false;
58
+ if (first.startsWith('ff') || /^fe[89ab]/u.test(first) || /^fe[cdef]/u.test(first) || /^f[cd]/u.test(first))
59
+ return false;
60
+ if (first === '0100' && groups.slice(1, 4).every((group) => group === '0000'))
61
+ return false;
62
+ if (first === '2001' && second === '0db8')
63
+ return false;
64
+ if (first === '3fff' && Number.parseInt(second, 16) <= 0x0fff)
65
+ return false;
66
+ if (first === '2001' && second === '0002' && third === '0000')
67
+ return false;
68
+ if (first === '2001' && /^00[1-2][0-9a-f]$/u.test(second))
69
+ return false;
70
+ if (first === '2001' && second === '0000')
71
+ return false;
72
+ if (first === '2002')
73
+ return false;
74
+ const firstValue = Number.parseInt(first, 16);
75
+ return Number.isInteger(firstValue) && firstValue >= 0x2000 && firstValue <= 0x3fff;
76
+ }
77
+ export function canonicalPublicIpAddress(value) {
78
+ const ipv4 = canonicalIpv4(value);
79
+ if (ipv4)
80
+ return isPublicIpv4(ipv4) ? ipv4 : null;
81
+ const ipv6 = canonicalIpv6(value);
82
+ return ipv6 && isPublicIpv6(ipv6) ? ipv6 : null;
83
+ }