@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,4 +1,4 @@
1
- import { CASE_SCHEMA_VERSION, MAX_CASE_STORE_BYTES, } from "./case-record-model.mjs";
1
+ import { CASE_SCHEMA_VERSION, MAX_CASE_STORE_BYTES } from "../contracts/case-portability.mjs";
2
2
  import { normalizeCaseStore } from "./case-migration-model.mjs";
3
3
  import { projectCaseForDurableWrite } from "./case-record-projection.mjs";
4
4
  /** The exact versioned string persisted by the browser storage adapter. */
@@ -88,6 +88,31 @@ export function enforceStoreBudget(cases) {
88
88
  }
89
89
  return { cases: working, pruned };
90
90
  }
91
+ /** A read-only, ephemeral preview of the exact existing byte-budget policy. */
92
+ export function prepareCaseStoreSave(current, proposed) {
93
+ const result = enforceStoreBudget(proposed);
94
+ const retainedById = new Map(result.cases.map(record => [record.id, record]));
95
+ const removed = proposed.flatMap(record => {
96
+ const retained = retainedById.get(record.id);
97
+ const ids = new Set(retained?.evidenceHistory.map(snapshot => snapshot.id));
98
+ const snapshots = record.evidenceHistory.filter(snapshot => !ids.has(snapshot.id));
99
+ return snapshots.length ? [{
100
+ caseId: record.id, domain: record.domain,
101
+ snapshotIds: snapshots.map(snapshot => snapshot.id),
102
+ remainingSnapshots: retained?.evidenceHistory.length ?? 0,
103
+ }] : [];
104
+ });
105
+ return {
106
+ ...result,
107
+ current: normalizeCaseStore(current).cases,
108
+ removed,
109
+ removedBytes: Math.max(0, byteLength(serializeCaseStore(proposed)) - byteLength(serializeCaseStore(result.cases))),
110
+ };
111
+ }
112
+ /** Same-clock peer edits invalidate confirmation, including changes to other Cases. */
113
+ export function caseStoreSavePreviewIsCurrent(current, preview) {
114
+ return serializeCaseStore(current) === serializeCaseStore(preview.current);
115
+ }
91
116
  export function buildCaseExport(cases, nowIso) {
92
117
  return {
93
118
  version: CASE_SCHEMA_VERSION,
@@ -0,0 +1,225 @@
1
+ // Typed analyst metadata; ordinary tags and assertions are always literal.
2
+ import { MAX_CASE_ASSERTIONS, MAX_RESPONSE_RATIONALE_LENGTH, MAX_CASE_OBJECTIVE_LENGTH } from "../contracts/case-portability.mjs";
3
+ import { caseInvestigationContext, prepareCaseInvestigationContext, parseIncidentUrlContext, normalizeCaseObjective } from "./case-incident-context.mjs";
4
+ import { safeId, makeId } from "./case-record-core.mjs";
5
+ import { array, enumeration, exact, iso, text } from "../evidence/artifact-structure.mjs";
6
+ import { assertWorkspaceInputGraph } from "../workspace/hostile-input.mjs";
7
+ // Preserve the maximum URL previously admitted inside a 21-character label.
8
+ export const MAX_CASE_INCIDENT_TARGET_URL_LENGTH = MAX_RESPONSE_RATIONALE_LENGTH - 21;
9
+ export const MAX_CASE_INCIDENT_TARGETS = 20;
10
+ export const MAX_CASE_INCIDENT_TARGET_HISTORY = MAX_CASE_ASSERTIONS;
11
+ export const CASE_TYPES = Object.freeze([
12
+ Object.freeze({ id: 'phishing', label: 'Phishing', description: 'Deceptive content or messages intended to obtain credentials or other sensitive information.' }),
13
+ Object.freeze({ id: 'impersonation', label: 'Impersonation', description: 'A domain, account or page presenting itself as another person or organisation.' }),
14
+ Object.freeze({ id: 'lookalike_cybersquatting', label: 'Lookalike or cybersquatting', description: 'A domain or account selected for review because it resembles or may exploit another name, brand or identifier.' }),
15
+ Object.freeze({ id: 'trademark_infringement', label: 'Trademark infringement', description: 'Potential unauthorised trademark use that may cause confusion about source or affiliation.' }),
16
+ Object.freeze({ id: 'copyright_infringement', label: 'Copyright infringement', description: 'Potential unauthorised copying or distribution of protected material.' }),
17
+ Object.freeze({ id: 'counterfeit_goods', label: 'Counterfeit goods', description: 'Promotion or sale of goods presented as genuine branded products.' }),
18
+ Object.freeze({ id: 'scam_fraud', label: 'Scam or fraud', description: 'Deceptive activity intended to obtain money, property or another benefit.' }),
19
+ Object.freeze({ id: 'malware_distribution', label: 'Malware distribution', description: 'Delivery, hosting or promotion of malicious software.' }),
20
+ Object.freeze({ id: 'credential_theft', label: 'Credential theft', description: 'Collection, sale or misuse of account credentials.' }),
21
+ Object.freeze({ id: 'spam_platform_abuse', label: 'Spam or platform abuse', description: 'Unsolicited or abusive platform activity that does not fit a narrower type.' }),
22
+ Object.freeze({ id: 'privacy_personal_data', label: 'Privacy or personal data', description: 'Potential exposure or misuse of personal information.' }),
23
+ Object.freeze({ id: 'account_compromise', label: 'Account compromise', description: 'Suspected unauthorised control or use of an account.' }),
24
+ Object.freeze({ id: 'other', label: 'Other', description: 'A reviewed type not covered by the current taxonomy.' }),
25
+ ]);
26
+ const CASE_TYPE_IDS = new Set(CASE_TYPES.map((item) => item.id));
27
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/iu;
28
+ const CASE_REFERENCE_ALPHABET = '0123456789ABCDEFGHJKMNPQRSTVWXYZ';
29
+ export function readCaseTypes(value) {
30
+ assertWorkspaceInputGraph(value, 'Case types');
31
+ const selected = array(value, 'Case types', CASE_TYPES.length).map(id => {
32
+ if (typeof id !== 'string' || !CASE_TYPE_IDS.has(id))
33
+ throw new TypeError('Unknown Case type.');
34
+ return id;
35
+ });
36
+ if (new Set(selected).size !== selected.length)
37
+ throw new TypeError('Case types must be unique.');
38
+ return CASE_TYPES.map(item => item.id).filter(id => selected.includes(id));
39
+ }
40
+ export function caseTypeIds(record) {
41
+ return [...(record.workflowMetadata?.types ?? [])];
42
+ }
43
+ export function caseTypeRecords(record) {
44
+ const selected = new Set(caseTypeIds(record));
45
+ return CASE_TYPES.filter((item) => selected.has(item.id));
46
+ }
47
+ export function caseTypeSummary(record) {
48
+ const labels = caseTypeRecords(record).map((item) => item.label);
49
+ if (!labels.length)
50
+ return '';
51
+ if (labels.length <= 2)
52
+ return labels.join(' and ');
53
+ return `${labels.slice(0, 2).join(', ')} and ${labels.length - 2} more`;
54
+ }
55
+ function encodeCaseReference(value, minimumLength) {
56
+ let remainder = value;
57
+ let encoded = '';
58
+ do {
59
+ encoded = CASE_REFERENCE_ALPHABET[Number(remainder % 32n)] + encoded;
60
+ remainder /= 32n;
61
+ } while (remainder > 0n);
62
+ return encoded.padStart(minimumLength, '0');
63
+ }
64
+ /**
65
+ * Returns an unambiguous reference derived injectively from the immutable Case
66
+ * id. UUIDs use their complete 128-bit value. Legacy ids use every UTF-8 byte,
67
+ * its encoded length and a distinct namespace, so no shared counter or server
68
+ * is needed and imports retain the same reference.
69
+ */
70
+ export function caseNumber(caseId) {
71
+ const id = typeof caseId === 'string' && caseId ? caseId : 'unknown';
72
+ if (UUID_RE.test(id)) {
73
+ return `WS-${encodeCaseReference(BigInt(`0x${id.replaceAll('-', '')}`), 26)}`;
74
+ }
75
+ const bytes = new TextEncoder().encode(id);
76
+ let encoded = 0n;
77
+ for (const byte of bytes)
78
+ encoded = encoded * 256n + BigInt(byte);
79
+ return `WS-L${encodeCaseReference(BigInt(bytes.length), 2)}-${encodeCaseReference(encoded, Math.ceil(bytes.length * 8 / 5))}`;
80
+ }
81
+ export function formattedCaseNumber(caseId) {
82
+ const value = caseNumber(caseId);
83
+ if (value.startsWith('WS-L'))
84
+ return value;
85
+ const separator = value.indexOf('-');
86
+ const prefix = value.slice(0, separator + 1);
87
+ const body = value.slice(separator + 1);
88
+ return `${prefix}${body.match(/.{1,5}/gu)?.join('-') ?? body}`;
89
+ }
90
+ export function normalizeCaseIncidentTargetUrl(value) {
91
+ if (typeof value !== 'string' || value.length > MAX_CASE_INCIDENT_TARGET_URL_LENGTH || !value.trim())
92
+ return null;
93
+ let parsed;
94
+ try {
95
+ parsed = new URL(value.trim());
96
+ }
97
+ catch {
98
+ return null;
99
+ }
100
+ if (!['http:', 'https:'].includes(parsed.protocol) || !parsed.hostname || parsed.username || parsed.password)
101
+ return null;
102
+ const normalized = parsed.toString();
103
+ return normalized.length <= MAX_CASE_INCIDENT_TARGET_URL_LENGTH ? normalized : null;
104
+ }
105
+ /** Local review only: analyst link state is not independent observed coverage. */
106
+ export function buildCaseIncidentCoverage(record) {
107
+ return caseIncidentTargets(record, { includeResolved: true }).map(target => ({
108
+ target,
109
+ hostname: new URL(target.url).hostname,
110
+ actionCoverage: 'unknown',
111
+ observationCoverage: 'unknown',
112
+ }));
113
+ }
114
+ export function emptyCaseWorkflowMetadata() {
115
+ return { types: [], incidentTargets: [], investigationContext: null };
116
+ }
117
+ function identifier(value) {
118
+ const id = safeId(value);
119
+ if (!id || id !== value)
120
+ throw new TypeError('Case metadata identity is invalid.');
121
+ return id;
122
+ }
123
+ /** Strict admission before persistence; no inferred defaults for typed fields. */
124
+ export function readCaseWorkflowMetadata(value, domain) {
125
+ if (value === undefined)
126
+ return undefined;
127
+ assertWorkspaceInputGraph(value, 'Case workflow metadata');
128
+ const row = exact(value, ['types', 'incidentTargets', 'investigationContext'], 'Case workflow metadata');
129
+ const types = readCaseTypes(row.types);
130
+ const incidentTargets = array(row.incidentTargets, 'Incident target history', MAX_CASE_INCIDENT_TARGET_HISTORY).map(raw => {
131
+ const item = exact(raw, ['id', 'url', 'state', 'createdAt', 'updatedAt'], 'Incident target');
132
+ const url = normalizeCaseIncidentTargetUrl(item.url);
133
+ if (!url || url !== item.url)
134
+ throw new TypeError('Incident target URL is invalid.');
135
+ iso(item.createdAt, 'Incident target creation time');
136
+ iso(item.updatedAt, 'Incident target update time');
137
+ if (Date.parse(item.updatedAt) < Date.parse(item.createdAt))
138
+ throw new TypeError('Incident target update precedes creation.');
139
+ return { id: identifier(item.id), url, state: enumeration(item.state, ['open', 'resolved'], 'Incident target state'),
140
+ createdAt: item.createdAt, updatedAt: item.updatedAt };
141
+ });
142
+ if (new Set(incidentTargets.map(item => item.id)).size !== incidentTargets.length)
143
+ throw new TypeError('Incident target identities must be unique.');
144
+ let investigationContext = null;
145
+ if (row.investigationContext !== null) {
146
+ const item = exact(row.investigationContext, ['id', 'objective', 'incidentUrl', 'urlRetention', 'updatedAt'], 'Incident context');
147
+ const objective = text(item.objective, 'Investigation objective', MAX_CASE_OBJECTIVE_LENGTH);
148
+ if (!objective || normalizeCaseObjective(objective) !== objective)
149
+ throw new TypeError('Investigation objective is invalid.');
150
+ const parsed = parseIncidentUrlContext(item.incidentUrl);
151
+ const urlRetention = enumeration(item.urlRetention, ['exact', 'origin_only'], 'Incident URL retention');
152
+ if (!parsed || parsed.registrableDomain !== domain
153
+ || item.incidentUrl !== (urlRetention === 'exact' ? parsed.exactUrl : parsed.originUrl)) {
154
+ throw new TypeError('Incident context URL does not match its Case domain or retention choice.');
155
+ }
156
+ iso(item.updatedAt, 'Incident context update time');
157
+ investigationContext = { id: identifier(item.id), objective, incidentUrl: item.incidentUrl,
158
+ urlRetention, updatedAt: item.updatedAt };
159
+ }
160
+ return { types, incidentTargets, investigationContext };
161
+ }
162
+ /** Mutations use the existing Case save coordinator and compare-and-swap rules. */
163
+ export function updateCaseWorkflowMetadata(current, patch, domain, now) {
164
+ const next = structuredClone(current);
165
+ if (patch.caseTypes !== undefined) {
166
+ if (patch.expectedCaseTypes !== undefined && JSON.stringify(readCaseTypes(patch.expectedCaseTypes)) !== JSON.stringify(current.types)) {
167
+ throw new Error('The Case types changed after this edit was started. Reopen the Case before saving; your draft was not applied.');
168
+ }
169
+ next.types = readCaseTypes(patch.caseTypes);
170
+ }
171
+ if (patch.incidentTarget !== undefined) {
172
+ const url = normalizeCaseIncidentTargetUrl(patch.incidentTarget);
173
+ if (!url)
174
+ throw new TypeError('Enter an exact HTTP(S) incident URL without embedded credentials.');
175
+ if (next.incidentTargets.some(target => target.state === 'open' && target.url === url))
176
+ throw new Error('That exact incident URL is already active in this Case.');
177
+ if (next.incidentTargets.filter(target => target.state === 'open').length >= MAX_CASE_INCIDENT_TARGETS)
178
+ throw new Error(`A Case can retain at most ${MAX_CASE_INCIDENT_TARGETS} active incident links. Resolve one before adding another.`);
179
+ if (next.incidentTargets.length >= MAX_CASE_INCIDENT_TARGET_HISTORY)
180
+ throw new Error(`The retained incident link history is full (${MAX_CASE_INCIDENT_TARGET_HISTORY}). Export this Case and open another; no history was removed.`);
181
+ next.incidentTargets.push({ id: makeId(), url, state: 'open', createdAt: now, updatedAt: now });
182
+ }
183
+ if (patch.incidentTargetResolution !== undefined) {
184
+ const id = identifier(patch.incidentTargetResolution);
185
+ const target = next.incidentTargets.find(item => item.id === id);
186
+ if (!target || target.state !== 'open')
187
+ throw new Error('That incident target is missing or already resolved. Refresh the Case before continuing.');
188
+ next.incidentTargets = next.incidentTargets.map(item => item.id === id ? { ...item, state: 'resolved', updatedAt: now } : item);
189
+ }
190
+ if (patch.investigationContext !== undefined) {
191
+ assertWorkspaceInputGraph(patch.investigationContext, 'New incident context');
192
+ const raw = exact(patch.investigationContext, ['objective', 'incidentUrl', 'retainExactUrl'], 'New incident context');
193
+ if (typeof raw.retainExactUrl !== 'boolean')
194
+ throw new TypeError('Choose whether to retain the exact incident URL.');
195
+ next.investigationContext = { ...prepareCaseInvestigationContext({ objective: raw.objective, incidentUrl: raw.incidentUrl, retainExactUrl: raw.retainExactUrl }),
196
+ id: current.investigationContext?.id ?? makeId(), updatedAt: now };
197
+ }
198
+ return readCaseWorkflowMetadata(next, domain);
199
+ }
200
+ /** Add missing targets, retaining the newer state for an existing identity. */
201
+ export function mergeCaseWorkflowMetadata(local, incoming, importNewer, domain) {
202
+ const targets = new Map(local.incidentTargets.map(target => [target.id, target]));
203
+ for (const target of incoming.incidentTargets) {
204
+ const existing = targets.get(target.id);
205
+ if (existing && (existing.url !== target.url || existing.createdAt !== target.createdAt))
206
+ throw new TypeError('Imported incident target identity conflicts with retained history. No data was changed.');
207
+ if (!existing || Date.parse(target.updatedAt) > Date.parse(existing.updatedAt))
208
+ targets.set(target.id, target);
209
+ }
210
+ const left = local.investigationContext, right = incoming.investigationContext;
211
+ const investigationContext = right && (!left || Date.parse(right.updatedAt) > Date.parse(left.updatedAt)) ? right : left;
212
+ return readCaseWorkflowMetadata({ types: importNewer ? incoming.types : local.types,
213
+ incidentTargets: [...targets.values()], investigationContext }, domain);
214
+ }
215
+ export function caseIncidentTargets(record, options = {}) {
216
+ return (record.workflowMetadata?.incidentTargets ?? [])
217
+ .filter(target => options.includeResolved || target.state === 'open').map(target => ({ ...target }));
218
+ }
219
+ export function caseResponseIncidentUrls(record) {
220
+ const urls = new Set(caseIncidentTargets(record).map((target) => target.url));
221
+ const context = caseInvestigationContext(record);
222
+ if (context?.urlRetention === 'exact')
223
+ urls.add(context.incidentUrl);
224
+ return [...urls].slice(0, MAX_CASE_INCIDENT_TARGETS);
225
+ }
@@ -0,0 +1,43 @@
1
+ // Read legacy text conventions once at the versioned storage/import boundary.
2
+ // Keep the original assertions for references and historical evidence.
3
+ import { CASE_TYPES, normalizeCaseIncidentTargetUrl, readCaseWorkflowMetadata } from "./case-workflow-metadata.mjs";
4
+ import { normalizeTags } from "./case-record-core.mjs";
5
+ import { normalizeCaseObjective, parseIncidentUrlContext } from "./case-incident-context.mjs";
6
+ import { EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION } from "../contracts/case-portability.mjs";
7
+ const TYPE_PREFIX = 'case-type:';
8
+ const TARGET_PREFIX = 'Incident target URL: ';
9
+ const CONTEXT_PREFIX = 'Investigate incident URL: ';
10
+ const OBJECTIVE_PREFIX = 'Objective: ';
11
+ const RETENTION_SEPARATOR = ' | URL retained: ';
12
+ export function readCaseWorkflowFields(raw, domain, assertions, sourceVersion) {
13
+ const tags = normalizeTags(raw.tags);
14
+ if (raw.workflowMetadata !== undefined) {
15
+ if (sourceVersion != null && sourceVersion < EVIDENCE_FOLLOW_UP_CASE_SCHEMA_VERSION)
16
+ throw new TypeError('Typed workflow metadata requires the current Case schema.');
17
+ return { tags, workflowMetadata: readCaseWorkflowMetadata(raw.workflowMetadata, domain) };
18
+ }
19
+ const types = CASE_TYPES.filter(type => tags.some(tag => tag.toLowerCase() === `${TYPE_PREFIX}${type.id}`)).map(type => type.id);
20
+ const workflowMetadata = { types, incidentTargets: [], investigationContext: null };
21
+ for (const assertion of assertions) {
22
+ if (assertion.statement.startsWith(TARGET_PREFIX)) {
23
+ const url = normalizeCaseIncidentTargetUrl(assertion.statement.slice(TARGET_PREFIX.length));
24
+ if (url)
25
+ workflowMetadata.incidentTargets.push({ id: assertion.id, url, state: assertion.state, createdAt: assertion.createdAt, updatedAt: assertion.updatedAt });
26
+ }
27
+ if (assertion.kind !== 'next_step' || assertion.state !== 'open' || !assertion.statement.startsWith(CONTEXT_PREFIX))
28
+ continue;
29
+ const parsed = parseIncidentUrlContext(assertion.statement.slice(CONTEXT_PREFIX.length));
30
+ const rationale = assertion.rationale ?? '';
31
+ const index = rationale.lastIndexOf(RETENTION_SEPARATOR);
32
+ const objective = rationale.startsWith(OBJECTIVE_PREFIX) && index > OBJECTIVE_PREFIX.length
33
+ ? normalizeCaseObjective(rationale.slice(OBJECTIVE_PREFIX.length, index)) : '';
34
+ const retention = index >= 0 ? rationale.slice(index + RETENTION_SEPARATOR.length) : '';
35
+ if (!parsed || parsed.registrableDomain !== domain || !objective || (retention !== 'exact' && retention !== 'origin_only'))
36
+ continue;
37
+ workflowMetadata.investigationContext = { id: assertion.id, objective, urlRetention: retention,
38
+ incidentUrl: retention === 'exact' ? parsed.exactUrl : parsed.originUrl, updatedAt: assertion.updatedAt };
39
+ }
40
+ // Preserve historical text byte-for-byte where already normalised. Typed
41
+ // metadata becomes authoritative; subsequent tag edits never change types.
42
+ return { tags, workflowMetadata: readCaseWorkflowMetadata(workflowMetadata, domain) };
43
+ }
@@ -3,7 +3,7 @@
3
3
  // connection attempts. Bulk/watchlist/case records deliberately keep only this
4
4
  // smaller derived shape: no paths, queries, header values, attempt errors, or
5
5
  // redirect inventories.
6
- import { MAX_HTTP_EVIDENCE_REDIRECTS } from "../../lib/http-evidence-bounds.mjs";
6
+ import { MAX_HTTP_EVIDENCE_REDIRECTS } from "../analysis/http-evidence-bounds.mjs";
7
7
  export const HTTP_SUMMARY_VERSION = 1;
8
8
  export const MAX_HTTP_SUMMARY_ORIGIN_LENGTH = 300;
9
9
  export const MAX_HTTP_SUMMARY_CONTENT_TYPE_LENGTH = 100;
@@ -154,6 +154,11 @@ export function normalizeHttpSummary(value) {
154
154
  httpSecurityHeaders: securityHeaderTokens(record.httpSecurityHeaders),
155
155
  };
156
156
  }
157
+ /** A partial body still permits comparison of observed response metadata. */
158
+ export function httpSummaryFieldIsObserved(field, value) {
159
+ const summary = normalizeHttpSummary(value);
160
+ return summary !== null && Object.hasOwn(summary, field) && summary[field] !== null;
161
+ }
157
162
  export function httpSecurityHeaderLabel(token) {
158
163
  const labels = {
159
164
  'content-security-policy': 'Content Security Policy',
@@ -0,0 +1,16 @@
1
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
2
+ import { MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, RESPONSE_ROUTE_STALE_AFTER_DAYS } from "../contracts/case-portability.mjs";
3
+ /** Source freshness is independent of action deadlines and analyst follow-up. */
4
+ export function responseRouteFreshness(observedAt, reviewAfter, evaluatedAt) {
5
+ const observation = normalizeExplicitIsoTimestamp(observedAt);
6
+ const evaluation = normalizeExplicitIsoTimestamp(evaluatedAt);
7
+ const deadline = normalizeExplicitIsoTimestamp(reviewAfter);
8
+ if (!observation || !evaluation || (reviewAfter !== null && !deadline))
9
+ return 'unknown';
10
+ const age = Date.parse(evaluation) - Date.parse(observation);
11
+ return age < -MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS
12
+ || (deadline !== null && Date.parse(evaluation) >= Date.parse(deadline))
13
+ || (deadline === null && age > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000)
14
+ ? 'stale'
15
+ : 'current';
16
+ }
@@ -0,0 +1,61 @@
1
+ import { exact, text } from "../evidence/artifact-structure.mjs";
2
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
3
+ import { readEvidenceImageDimensions } from "../evidence/image-regions.mjs";
4
+ /** Optional declarations remain unknown for existing manifests; no inference from image dimensions. */
5
+ export function readCaptureConditions(raw) {
6
+ if (raw === undefined || raw === null)
7
+ return null;
8
+ const value = exact(raw, ['browser', 'browserVersion', 'viewport', 'deviceScaleFactor', 'locale', 'timezone', 'colourScheme'], 'Capture conditions');
9
+ const viewport = exact(value.viewport, ['width', 'height'], 'Capture viewport');
10
+ const dimensions = readEvidenceImageDimensions(viewport.width, viewport.height);
11
+ if (typeof value.deviceScaleFactor !== 'number' || !Number.isFinite(value.deviceScaleFactor)
12
+ || value.deviceScaleFactor < 0.25 || value.deviceScaleFactor > 8)
13
+ throw new TypeError('Capture scale must be between 0.25 and 8.');
14
+ if (value.colourScheme !== 'light' && value.colourScheme !== 'dark')
15
+ throw new TypeError('Capture colour scheme is unsupported.');
16
+ return Object.freeze({ browser: text(value.browser, 'Capture browser', 40), browserVersion: text(value.browserVersion, 'Capture browser version', 80),
17
+ viewport: dimensions, deviceScaleFactor: value.deviceScaleFactor,
18
+ locale: text(value.locale, 'Capture locale', 80), timezone: text(value.timezone, 'Capture timezone', 80), colourScheme: value.colourScheme });
19
+ }
20
+ export function readObservationLabel(raw) {
21
+ if (raw === undefined || raw === null || raw === '')
22
+ return null;
23
+ return text(raw, 'Observation label', 80);
24
+ }
25
+ export function compareObservationContexts(input) {
26
+ if (!Array.isArray(input) || input.length < 2 || input.length > 20)
27
+ throw new TypeError('Compare between 2 and 20 observation contexts.');
28
+ const observations = input.map(value => {
29
+ const observedAt = value.observedAt === null ? null : normalizeExplicitIsoTimestamp(value.observedAt);
30
+ if (value.observedAt !== null && !observedAt)
31
+ throw new TypeError('Observation time must include an explicit timezone.');
32
+ return { observedAt, observerLabel: readObservationLabel(value.observerLabel), vantageLabel: readObservationLabel(value.vantageLabel), conditions: readCaptureConditions(value.conditions) };
33
+ });
34
+ const timestamps = observations.map(value => value.observedAt === null ? null : Date.parse(value.observedAt));
35
+ const knownTimes = timestamps.filter((value) => value !== null);
36
+ const row = (id, label, values) => ({ id, label, values,
37
+ state: values.some(value => value === null) ? 'unknown' : new Set(values).size === 1 ? 'same' : 'different' });
38
+ const conditions = observations.map(value => value.conditions);
39
+ const rows = [
40
+ row('observer', 'Declared observer', observations.map(value => value.observerLabel)),
41
+ row('vantage', 'Declared vantage', observations.map(value => value.vantageLabel)),
42
+ row('browser', 'Declared browser', conditions.map(value => value ? `${value.browser} ${value.browserVersion}` : null)),
43
+ row('viewport', 'Declared viewport', conditions.map(value => value ? `${value.viewport.width} × ${value.viewport.height}` : null)),
44
+ row('scale', 'Declared scale', conditions.map(value => value ? String(value.deviceScaleFactor) : null)),
45
+ row('locale', 'Declared locale', conditions.map(value => value?.locale ?? null)),
46
+ row('timezone', 'Declared timezone', conditions.map(value => value?.timezone ?? null)),
47
+ row('colour', 'Declared colour scheme', conditions.map(value => value?.colourScheme ?? null)),
48
+ ];
49
+ const labelPairs = observations.map(value => value.observerLabel && value.vantageLabel ? JSON.stringify([value.observerLabel, value.vantageLabel]) : null);
50
+ return {
51
+ observations, rows, time: { state: knownTimes.length !== observations.length ? 'unknown' : new Set(knownTimes).size === 1 ? 'same_instant' : 'different_instants',
52
+ spanMilliseconds: knownTimes.length === observations.length ? Math.max(...knownTimes) - Math.min(...knownTimes) : null },
53
+ labels: labelPairs.some(value => value === null) ? 'incomplete' : new Set(labelPairs).size === labelPairs.length ? 'distinct' : 'repeated',
54
+ independence: 'not_verified',
55
+ limitations: [
56
+ 'Observer, vantage and capture conditions are declarations, not verified network location or independent collection.',
57
+ 'Different capture times, browser versions, viewports, scale, locale and shared upstream caches can explain differences. Equal declarations do not establish identical environments.',
58
+ 'These selected observations do not establish worldwide availability, removal or takedown.',
59
+ ],
60
+ };
61
+ }
@@ -295,6 +295,10 @@ export function comparisonLedgerBulkPairIndexId(earlierSessionId, laterSessionId
295
295
  return stableComparisonLedgerId('ledger-bulk_session_pair', [String(earlierSessionId ?? ''), String(laterSessionId ?? '')]);
296
296
  }
297
297
  export function buildBulkComparisonCandidates(sessionsRaw, pairsRaw, counters) {
298
+ const pairs = comparisonLedgerInputArray(pairsRaw);
299
+ // Saved sessions are comparison inputs only after a pair is requested.
300
+ if (!pairs.length)
301
+ return [];
298
302
  const input = comparisonLedgerInputArray(sessionsRaw);
299
303
  counters.inputRecords += Math.max(0, input.length - MAX_BULK_SESSIONS);
300
304
  const sessions = new Map();
@@ -310,7 +314,6 @@ export function buildBulkComparisonCandidates(sessionsRaw, pairsRaw, counters) {
310
314
  }
311
315
  sessions.set(item.id, item);
312
316
  }
313
- const pairs = comparisonLedgerInputArray(pairsRaw);
314
317
  counters.inputRecords += Math.max(0, pairs.length - MAX_COMPARISON_LEDGER_BULK_PAIRS);
315
318
  const candidates = [];
316
319
  const seen = new Set();
@@ -1,4 +1,4 @@
1
- import { hammingDistanceHex, isInformativePerceptualHash, isPerceptualHash, } from "../../lib/perceptual-hash-comparison.mjs";
1
+ import { hammingDistanceHex, isInformativePerceptualHash, isPerceptualHash, } from "../analysis/perceptual-hash-comparison.mjs";
2
2
  function plainRecord(value) {
3
3
  return value && typeof value === 'object' && !Array.isArray(value)
4
4
  ? value
@@ -3,7 +3,7 @@
3
3
  // produce an aggregate similarity score, risk factor, or maliciousness
4
4
  // verdict. Inputs pass through the strict baseline normalizer before use.
5
5
  import { normalizePageBaseline } from "../workspace/page-baseline.mjs";
6
- import { hammingDistanceHex } from "../../lib/perceptual-hash-comparison.mjs";
6
+ import { hammingDistanceHex } from "../analysis/perceptual-hash-comparison.mjs";
7
7
  import { PAGE_COMPARISON_VERSION } from "../contracts/offline-comparison.mjs";
8
8
  export { PAGE_COMPARISON_VERSION };
9
9
  function component(id, label, method, status, outcome, detail, partial) {
@@ -141,6 +141,7 @@ export function comparePageBaselines(rawReference, rawObserved) {
141
141
  const observed = normalizePageBaseline(rawObserved);
142
142
  if (!reference || !observed)
143
143
  return null;
144
+ const sameFingerprintVersion = reference.fingerprintVersion === observed.fingerprintVersion;
144
145
  const components = [
145
146
  digestComponent('normalized_html', 'Normalised HTML', 'Normalized-HTML', reference.normalizedHtml, observed.normalizedHtml),
146
147
  visibleTextComponent(reference.visibleText, observed.visibleText),
@@ -149,7 +150,7 @@ export function comparePageBaselines(rawReference, rawObserved) {
149
150
  faviconComponent(reference, observed),
150
151
  setComponent('resource_hosts', 'External resource hosts', 'host', reference.resourceHosts, observed.resourceHosts, (value) => value),
151
152
  setComponent('tracking_identifiers', 'Tracking identifiers', 'identifier', reference.trackingIdentifiers, observed.trackingIdentifiers, (value) => `${value.type}:${value.value}`),
152
- ];
153
+ ].map((item) => sameFingerprintVersion || item.id === 'favicon' ? item : component(item.id, item.label, item.method, 'unavailable', 'Different fingerprint versions', 'These captures use different HTML fingerprint algorithms. Retain the older baseline and capture a new baseline to compare current page evidence.', true));
153
154
  const counts = { same: 0, overlap: 0, different: 0, notObserved: 0, unavailable: 0 };
154
155
  for (const item of components) {
155
156
  if (item.status === 'not_observed')