@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,8 +1,9 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { readCaseEvidenceRequest, readCasePacketAmendment, assertEvidenceRequestEvent, assertEvidenceRequestHistory, assertPacketAmendmentSelection } from "../../packages/cases/case-requested-evidence.mjs";
2
3
  import { canonicalArtifactJsonV2 } from "../../packages/evidence/artifact-integrity.mjs";
3
- import { CASE_RESPONSE_PACKET_VERSION, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
4
+ import { CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION, LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
4
5
  import { CASE_ACTION_EVENT_SOURCE_CLASSES, CASE_ACTION_STATES, CASE_ACTION_TYPES, CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, CASE_PROVIDER_OUTCOMES, isLegalCaseActionTransition, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RECIPIENT_LENGTH, } from "../../packages/cases/case-response-model.mjs";
5
- import { HEX_DIGEST_RE, array, boolean, domain, enumeration, exact, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
6
+ import { HEX_DIGEST_RE, array, boolean, domain, enumeration, exact, exactOptional, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
6
7
  const CASE_RESPONSE_PREFLIGHT_IDS_V6 = [
7
8
  'required_incident_fields',
8
9
  'evidence_pins',
@@ -224,15 +225,15 @@ function validateCurrentActionSummary(value, label) {
224
225
  }
225
226
  return total;
226
227
  }
227
- function validateCaseResponsePacketV7ToV9(value, version) {
228
- const current = version === CASE_RESPONSE_PACKET_VERSION;
229
- const modern = version === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION || current;
228
+ function validateVersionedCaseResponsePacket(value, version) {
229
+ const hasPlatformRoutes = version >= PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION;
230
+ const hasActionBinding = version >= PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION;
230
231
  const root = exact(value, [
231
232
  'schema', 'schemaVersion', 'generatedAt', 'reviewRequired', 'submissionPerformed', 'profile', 'case',
232
233
  'incident', 'contacts', 'selectedEvidence', 'contradictions', 'readiness', 'artefactReferences',
233
234
  'authorisation', 'preflight', 'escalationHistory', 'escalationHistoryOmitted',
234
235
  'escalationHistoryLimitations', 'responseLifecycle', 'provenance', 'integrity',
235
- ...(modern ? ['recipientRoute', 'actionBinding'] : []),
236
+ ...(hasActionBinding ? ['recipientRoute', 'actionBinding'] : []),
236
237
  ], `Case-response packet v${version}`);
237
238
  if (root.schemaVersion !== version)
238
239
  fail(`Case-response packet v${version}`);
@@ -240,14 +241,14 @@ function validateCaseResponsePacketV7ToV9(value, version) {
240
241
  if (root.reviewRequired !== true || root.submissionPerformed !== false)
241
242
  fail('Case-response packet review state');
242
243
  const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
243
- const profileId = enumeration(profile.id, current ? RESPONSE_PACKET_PROFILE_IDS : PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
244
+ const profileId = enumeration(profile.id, hasPlatformRoutes ? RESPONSE_PACKET_PROFILE_IDS : PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
244
245
  text(profile.label, 'Case-response profile label', 200);
245
246
  text(profile.audience, 'Case-response profile audience', 300);
246
247
  text(profile.subject, 'Case-response profile subject', 500);
247
248
  for (const key of ['checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'])
248
249
  strings(profile[key], `Case-response profile ${key}`, 24, 500);
249
250
  const selectedProfile = RESPONSE_PACKET_PROFILES.find((candidate) => candidate.id === profileId);
250
- const expectedRedactions = modern ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
251
+ const expectedRedactions = hasActionBinding ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
251
252
  if (!selectedProfile
252
253
  || profile.label !== selectedProfile.label
253
254
  || profile.audience !== selectedProfile.audience
@@ -287,9 +288,9 @@ function validateCaseResponsePacketV7ToV9(value, version) {
287
288
  const contactKeys = new Set();
288
289
  for (const candidate of contacts) {
289
290
  const contact = exact(candidate, [
290
- 'kind', 'contact', 'source', 'observedAt', ...(current ? ['reviewAfter'] : []), 'freshness', 'limitations',
291
+ 'kind', 'contact', 'source', 'observedAt', ...(hasPlatformRoutes ? ['reviewAfter'] : []), 'freshness', 'limitations',
291
292
  ], 'Case-response contact');
292
- const kind = enumeration(contact.kind, current ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
293
+ const kind = enumeration(contact.kind, hasPlatformRoutes ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
293
294
  const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
294
295
  const key = `${kind}\u0000${contactValue.toLowerCase()}`;
295
296
  if (contactKeys.has(key))
@@ -297,13 +298,13 @@ function validateCaseResponsePacketV7ToV9(value, version) {
297
298
  contactKeys.add(key);
298
299
  text(contact.source, 'Case-response contact source', 120);
299
300
  iso(contact.observedAt, 'Case-response contact observedAt', true);
300
- if (current)
301
+ if (hasPlatformRoutes)
301
302
  iso(contact.reviewAfter, 'Case-response contact reviewAfter', true);
302
303
  const freshness = enumeration(contact.freshness, ['current', 'stale', 'unknown'], 'Case-response contact freshness');
303
304
  const routeAge = contact.observedAt === null
304
305
  ? null
305
306
  : Date.parse(root.generatedAt) - Date.parse(contact.observedAt);
306
- const reviewExpired = current && contact.reviewAfter !== null
307
+ const reviewExpired = hasPlatformRoutes && contact.reviewAfter !== null
307
308
  && Date.parse(root.generatedAt) >= Date.parse(contact.reviewAfter);
308
309
  const expectedFreshness = routeAge === null
309
310
  ? 'unknown'
@@ -319,23 +320,23 @@ function validateCaseResponsePacketV7ToV9(value, version) {
319
320
  let selectedActionId = null;
320
321
  let lineageActionIds = [];
321
322
  let lineageComplete = true;
322
- if (modern) {
323
+ if (hasActionBinding) {
323
324
  if (root.recipientRoute !== null) {
324
325
  recipientRoute = exact(root.recipientRoute, [
325
- 'actionId', 'kind', 'contact', 'source', 'observedAt', ...(current ? ['reviewAfter'] : []), 'freshness', 'limitations',
326
+ 'actionId', 'kind', 'contact', 'source', 'observedAt', ...(hasPlatformRoutes ? ['reviewAfter'] : []), 'freshness', 'limitations',
326
327
  ], 'Case-response recipient route');
327
328
  text(recipientRoute.actionId, 'Case-response recipient action id', 64);
328
- enumeration(recipientRoute.kind, [...(current ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS), 'manual'], 'Case-response recipient kind');
329
+ enumeration(recipientRoute.kind, [...(hasPlatformRoutes ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS), 'manual'], 'Case-response recipient kind');
329
330
  text(recipientRoute.contact, 'Case-response recipient value', MAX_RESPONSE_RECIPIENT_LENGTH);
330
331
  text(recipientRoute.source, 'Case-response recipient source', 120);
331
332
  iso(recipientRoute.observedAt, 'Case-response recipient observedAt', true);
332
- if (current)
333
+ if (hasPlatformRoutes)
333
334
  iso(recipientRoute.reviewAfter, 'Case-response recipient reviewAfter', true);
334
335
  const freshness = enumeration(recipientRoute.freshness, ['current', 'stale', 'unknown'], 'Case-response recipient freshness');
335
336
  const routeAge = recipientRoute.observedAt === null
336
337
  ? null
337
338
  : Date.parse(root.generatedAt) - Date.parse(recipientRoute.observedAt);
338
- const reviewExpired = current && recipientRoute.reviewAfter !== null
339
+ const reviewExpired = hasPlatformRoutes && recipientRoute.reviewAfter !== null
339
340
  && Date.parse(root.generatedAt) >= Date.parse(recipientRoute.reviewAfter);
340
341
  const expectedFreshness = routeAge === null
341
342
  ? 'unknown'
@@ -370,7 +371,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
370
371
  || selectedContact.contact !== recipientRoute.contact
371
372
  || selectedContact.source !== recipientRoute.source
372
373
  || selectedContact.observedAt !== recipientRoute.observedAt
373
- || (current && selectedContact.reviewAfter !== recipientRoute.reviewAfter)
374
+ || (hasPlatformRoutes && selectedContact.reviewAfter !== recipientRoute.reviewAfter)
374
375
  || selectedContact.freshness !== recipientRoute.freshness)
375
376
  fail('Case-response selected contact projection');
376
377
  }
@@ -378,14 +379,16 @@ function validateCaseResponsePacketV7ToV9(value, version) {
378
379
  const evidence = array(root.selectedEvidence, 'Case-response selected evidence', MAX_RESPONSE_SELECTED_EVIDENCE);
379
380
  const evidenceIds = new Set();
380
381
  for (const candidate of evidence) {
381
- const item = exact(candidate, ['id', 'label', 'source', 'observedAt', 'completeness', 'limitations'], 'Case-response selected evidence');
382
+ const item = exactOptional(candidate, ['id', 'label', 'source', 'observedAt', 'completeness', 'limitations'], version > PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION ? ['observationHostname'] : [], 'Case-response selected evidence');
382
383
  const id = text(item.id, 'Case-response evidence id', 64);
383
384
  if (evidenceIds.has(id))
384
385
  fail('Case-response selected evidence');
385
386
  evidenceIds.add(id);
386
387
  text(item.label, 'Case-response evidence label', 80);
387
388
  text(item.source, 'Case-response evidence source', 120);
388
- iso(item.observedAt, 'Case-response evidence observedAt');
389
+ if (item.observationHostname !== undefined)
390
+ domain(item.observationHostname, 'Case-response evidence hostname');
391
+ iso(item.observedAt, 'Case-response evidence observedAt', version > PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION);
389
392
  enumeration(item.completeness, ['complete', 'partial', 'inconclusive', 'unknown'], 'Case-response evidence completeness');
390
393
  strings(item.limitations, 'Case-response evidence limitations', 8, 240);
391
394
  }
@@ -412,7 +415,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
412
415
  ...(profileId === 'internal_soc' ? [] : ['recipient_route']),
413
416
  ...(['registrar', 'registry', 'network_hosting'].includes(profileId) ? ['infrastructure_responsibility'] : []),
414
417
  ]);
415
- const requiredContact = modern
418
+ const requiredContact = hasActionBinding
416
419
  ? recipientRoute ?? undefined
417
420
  : selectedProfile?.requiredContactKind
418
421
  ? contacts.find((candidate) => candidate.kind === selectedProfile.requiredContactKind)
@@ -421,7 +424,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
421
424
  observed_behaviour: 'complete',
422
425
  exact_url: 'complete',
423
426
  observation_time: expectedObservationAge(incident.observedAt, root.generatedAt).refreshRecommended ? 'stale' : 'complete',
424
- capture_provenance: evidence.length ? 'complete' : 'unavailable',
427
+ capture_provenance: !evidence.length ? 'unavailable' : evidence.every((candidate) => candidate.observedAt !== null) ? 'complete' : 'partial',
425
428
  recipient_route: !requiredContact
426
429
  ? 'not_provided'
427
430
  : requiredContact.freshness === 'stale'
@@ -507,8 +510,8 @@ function validateCaseResponsePacketV7ToV9(value, version) {
507
510
  || (authorisationStatus === 'draft' && authorisation.confirmedAt !== null))
508
511
  fail('Case-response authorisation');
509
512
  const preflight = exact(root.preflight, ['version', 'status', 'canExport', 'counts', 'checks', 'actionSummary'], 'Case-response preflight');
510
- const preflightIds = modern ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
511
- if (preflight.version !== (modern ? 3 : 2))
513
+ const preflightIds = hasActionBinding ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
514
+ if (preflight.version !== (hasActionBinding ? 3 : 2))
512
515
  fail('Case-response preflight');
513
516
  const checks = array(preflight.checks, 'Case-response preflight checks', preflightIds.length, preflightIds.length);
514
517
  const actualCounts = { block: 0, caution: 0, pass: 0 };
@@ -532,8 +535,8 @@ function validateCaseResponsePacketV7ToV9(value, version) {
532
535
  const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
533
536
  const escalationHistoryOmitted = integer(root.escalationHistoryOmitted, 'Case-response omitted actions', 0, MAX_CASE_ACTIONS);
534
537
  const escalationHistoryLimitations = strings(root.escalationHistoryLimitations, 'Case-response history limitations', 4, 600);
535
- if ((!modern && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
536
- || (modern && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
538
+ if ((!hasActionBinding && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
539
+ || (hasActionBinding && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
537
540
  || escalationHistoryOmitted !== actionTotal - history.length
538
541
  || (escalationHistoryOmitted > 0) !== (escalationHistoryLimitations.length > 0))
539
542
  fail('Case-response action-history bounds');
@@ -541,24 +544,25 @@ function validateCaseResponsePacketV7ToV9(value, version) {
541
544
  const actionIdOrder = [];
542
545
  const providerEvents = [];
543
546
  for (const candidate of history) {
544
- const action = exact(candidate, [
547
+ const action = exactOptional(candidate, [
545
548
  'actionId', 'type', 'recipient', 'contactSource', 'state', 'reference',
546
549
  'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
547
550
  'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
548
- ...(modern ? ['routeObservedAt'] : []),
549
- ...(current ? ['routeReviewAfter'] : []),
550
- ], 'Case-response escalation action');
551
+ ...(hasActionBinding ? ['routeObservedAt'] : []),
552
+ ...(hasPlatformRoutes ? ['routeReviewAfter'] : []),
553
+ ], Number(root.schemaVersion) >= 11 ? ['amendment'] : [], 'Case-response escalation action');
554
+ readCasePacketAmendment(action.amendment);
551
555
  const actionId = text(action.actionId, 'Case-response action id', 64);
552
556
  if (actionIds.has(actionId))
553
557
  fail('Case-response action identity');
554
558
  actionIds.add(actionId);
555
559
  actionIdOrder.push(actionId);
556
- enumeration(action.type, current ? CASE_ACTION_TYPES : PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
560
+ enumeration(action.type, hasPlatformRoutes ? CASE_ACTION_TYPES : PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
557
561
  text(action.recipient, 'Case-response action recipient', 320);
558
562
  text(action.contactSource, 'Case-response action source', 120);
559
- if (modern)
563
+ if (hasActionBinding)
560
564
  iso(action.routeObservedAt, 'Case-response action route observedAt', true);
561
- if (current)
565
+ if (hasPlatformRoutes)
562
566
  iso(action.routeReviewAfter, 'Case-response action route reviewAfter', true);
563
567
  enumeration(action.state, CASE_ACTION_STATES, 'Case-response action state');
564
568
  optionalText(action.reference, 'Case-response action reference', 500);
@@ -569,6 +573,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
569
573
  integer(action.historyOmitted, 'Case-response action omitted history', 0, 1_000_000);
570
574
  strings(action.historyLimitations, 'Case-response action history limitations', 8, 240);
571
575
  const transitions = array(action.transitions, 'Case-response action transitions', MAX_CASE_ACTION_EVENTS_PER_ACTION, 1);
576
+ const requestEvents = [];
572
577
  const eventIds = new Set();
573
578
  let projectedState = null;
574
579
  let latestReference = null;
@@ -578,8 +583,11 @@ function validateCaseResponsePacketV7ToV9(value, version) {
578
583
  let previousTime = Number.NEGATIVE_INFINITY;
579
584
  let previousEventId = '';
580
585
  for (const eventCandidate of transitions) {
581
- const event = exact(eventCandidate, ['id', 'previousState', 'nextState', 'occurredAt', 'sourceClass', 'provenance', 'reference', 'evidencePinId', 'limitations', 'providerOutcome', 'outcomeDetail', 'originActionId', 'applied'], 'Case-response action transition');
586
+ const event = exactOptional(eventCandidate, ['id', 'previousState', 'nextState', 'occurredAt', 'sourceClass', 'provenance', 'reference', 'evidencePinId', 'limitations', 'providerOutcome', 'outcomeDetail', 'originActionId', 'applied'], Number(root.schemaVersion) >= 11 ? ['evidenceRequest'] : [], 'Case-response action transition');
587
+ const evidenceRequest = readCaseEvidenceRequest(event.evidenceRequest);
588
+ assertEvidenceRequestEvent(evidenceRequest, event);
582
589
  const eventId = text(event.id, 'Case-response action event id', 64);
590
+ requestEvents.push({ id: eventId, ...(evidenceRequest ? { evidenceRequest } : {}) });
583
591
  if (eventIds.has(eventId))
584
592
  fail('Case-response action event identity');
585
593
  eventIds.add(eventId);
@@ -648,6 +656,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
648
656
  }
649
657
  }
650
658
  }
659
+ assertEvidenceRequestHistory(requestEvents, Number(action.historyOmitted) > 0);
651
660
  if ((projectedState ?? 'drafting') !== action.state
652
661
  || latestReference !== action.reference
653
662
  || latestProviderOutcome !== action.providerOutcome
@@ -658,13 +667,18 @@ function validateCaseResponsePacketV7ToV9(value, version) {
658
667
  iso(action.createdAt, 'Case-response action createdAt');
659
668
  iso(action.updatedAt, 'Case-response action updatedAt');
660
669
  }
661
- if (modern) {
670
+ if (hasActionBinding) {
662
671
  if (actionIds.size !== lineageActionIds.length
663
672
  || actionIdOrder.some((id, index) => lineageActionIds[index] !== id))
664
673
  fail('Case-response action lineage');
665
674
  if (recipientRoute && !actionIds.has(recipientRoute.actionId))
666
675
  fail('Case-response recipient action');
667
676
  }
677
+ if (version >= 11)
678
+ assertPacketAmendmentSelection(history.map(candidate => {
679
+ const action = candidate;
680
+ return { id: action.actionId, history: action.transitions, originActionId: action.originActionId, amendment: action.amendment };
681
+ }), selectedActionId, evidenceIds);
668
682
  const lifecycle = exact(root.responseLifecycle, ['providerOutcomeState', 'latestProviderOutcome', 'observedChangeState', 'latestObservedEffect', 'latestObservedChangeAt', 'closure', 'limitations'], 'Case-response lifecycle');
669
683
  const providerOutcomeState = enumeration(lifecycle.providerOutcomeState, ['available', 'missing', 'ambiguous'], 'Case-response lifecycle provider state');
670
684
  let actualLatestProvider = null;
@@ -690,11 +704,11 @@ function validateCaseResponsePacketV7ToV9(value, version) {
690
704
  const latestProviderEvents = latestProviderTime === null
691
705
  ? []
692
706
  : providerEvents.filter((event) => event.occurredAt === latestProviderTime);
693
- const expectedLatestProvider = (modern ? lineageComplete : escalationHistoryOmitted === 0)
707
+ const expectedLatestProvider = (hasActionBinding ? lineageComplete : escalationHistoryOmitted === 0)
694
708
  && latestProviderEvents.length === 1 && latestProviderEvents[0].applied
695
709
  ? latestProviderEvents[0]
696
710
  : null;
697
- const expectedProviderState = (modern ? !lineageComplete : escalationHistoryOmitted > 0)
711
+ const expectedProviderState = (hasActionBinding ? !lineageComplete : escalationHistoryOmitted > 0)
698
712
  ? 'ambiguous'
699
713
  : providerEvents.length === 0 ? 'missing' : expectedLatestProvider ? 'available' : 'ambiguous';
700
714
  if (providerOutcomeState !== expectedProviderState
@@ -727,9 +741,11 @@ function validateCaseResponsePacketV7ToV9(value, version) {
727
741
  strings(lifecycle.limitations, 'Case-response lifecycle limitations', 8, 600);
728
742
  const reviewedInputs = {
729
743
  contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
730
- version: current
744
+ version: version === CASE_RESPONSE_PACKET_VERSION
731
745
  ? CASE_RESPONSE_REVIEW_INPUTS_VERSION
732
- : modern ? LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION,
746
+ : version === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION ? LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION
747
+ : hasPlatformRoutes ? PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION
748
+ : hasActionBinding ? PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION,
733
749
  profile: {
734
750
  id: profile.id,
735
751
  label: profile.label,
@@ -743,7 +759,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
743
759
  case: root.case,
744
760
  incident: root.incident,
745
761
  contacts: root.contacts,
746
- ...(modern ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
762
+ ...(hasActionBinding ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
747
763
  selectedEvidence: root.selectedEvidence,
748
764
  contradictions: root.contradictions,
749
765
  readiness: root.readiness,
@@ -778,13 +794,18 @@ function validateCaseResponsePacketV7ToV9(value, version) {
778
794
  fail('Case-response integrity');
779
795
  }
780
796
  export function validateCaseResponsePacket(value) {
797
+ if (value.schemaVersion === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION)
798
+ return validateVersionedCaseResponsePacket(value, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION);
781
799
  if (value.schemaVersion === PUBLIC_CASE_RESPONSE_PACKET_VERSION)
782
800
  return validateCaseResponsePacketV6(value);
783
801
  if (value.schemaVersion === PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION) {
784
- return validateCaseResponsePacketV7ToV9(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
802
+ return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
785
803
  }
786
- if (value.schemaVersion === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION) {
787
- return validateCaseResponsePacketV7ToV9(value, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION);
804
+ if (value.schemaVersion === PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION) {
805
+ return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION);
806
+ }
807
+ if (value.schemaVersion === PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION) {
808
+ return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION);
788
809
  }
789
810
  if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion < CASE_RESPONSE_PACKET_VERSION) {
790
811
  throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is not part of the public compatibility boundary; no data was changed.`);
@@ -792,5 +813,5 @@ export function validateCaseResponsePacket(value) {
792
813
  if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion > CASE_RESPONSE_PACKET_VERSION) {
793
814
  throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is newer than the supported version ${CASE_RESPONSE_PACKET_VERSION}; no data was changed.`);
794
815
  }
795
- validateCaseResponsePacketV7ToV9(value, CASE_RESPONSE_PACKET_VERSION);
816
+ validateVersionedCaseResponsePacket(value, CASE_RESPONSE_PACKET_VERSION);
796
817
  }
@@ -1,6 +1,7 @@
1
- import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, } from "../../packages/investigation/investigation-capsule.mjs";
1
+ import { investigationCapsuleContracts, MAX_LOOKUP_ASSET_NODES, MAX_LOOKUP_ASSET_EDGES, MAX_LOOKUP_ASSET_LIMITATIONS, MAX_LOOKUP_ASSET_INPUT_ROWS } from "../../packages/contracts/investigation-portability.mjs";
2
+ import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, investigationCapsuleProjectionsMatch, } from "../../packages/investigation/investigation-capsule.mjs";
2
3
  import { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION, } from "../../packages/investigation/lookup-asset-graph.mjs";
3
- import { PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION, LOOKUP_INVESTIGATION_BRIEF_SCHEMA, LOOKUP_INVESTIGATION_BRIEF_VERSION, MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES, } from "../../packages/investigation/lookup-investigation-brief.mjs";
4
+ import { PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION, LOOKUP_INVESTIGATION_BRIEF_SCHEMA, MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES, } from "../../packages/investigation/lookup-investigation-brief.mjs";
4
5
  import { DECISION_FACT_CONSISTENCY_STATES, DECISION_FACT_EVIDENCE_STATES, DECISION_FACT_FRESHNESS_STATES, DECISION_FACT_PROVENANCE_STATES, DECISION_FACT_PROJECTION_VERSION, DECISION_FACT_VERSION, MAX_DECISION_FACT_CONTRIBUTORS, MAX_DECISION_FACT_CONTRADICTIONS, MAX_DECISION_FACT_LIMITATIONS, MAX_DECISION_FACT_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_CONTRADICTIONS, MAX_DECISION_FACT_PROJECTION_BYTES, MAX_DECISION_FACT_PROJECTION_FACTS, MAX_DECISION_FACT_PROJECTION_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_REFERENCES, MAX_DECISION_FACT_PROJECTION_SOURCES, MAX_DECISION_FACT_PROJECTION_SOURCE_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_SOURCE_REFERENCES, MAX_DECISION_FACT_REFERENCES, MAX_DECISION_FACTS, decisionFactCompleteness, } from "../../packages/evidence/decision-fact.mjs";
5
6
  import { MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_DECISION_PIN_REFERENCES, } from "../../packages/cases/case-response-model.mjs";
6
7
  import { array, boolean, digest, enumeration, exact, exactOptional, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
@@ -221,10 +222,10 @@ function validateDecisionFactProjection(value) {
221
222
  fail('Investigation capsule Decision Fact projection byte limit');
222
223
  }
223
224
  }
224
- function validateCurrentBrief(value) {
225
+ function validateCurrentBrief(value, expectedVersion) {
225
226
  const brief = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'target', 'targetType', 'task', 'taskLabel', 'question', 'summary', 'observation', 'decisionFacts', 'relationships', 'limitations'], 'Investigation capsule brief');
226
227
  if (brief.schema !== LOOKUP_INVESTIGATION_BRIEF_SCHEMA
227
- || brief.schemaVersion !== LOOKUP_INVESTIGATION_BRIEF_VERSION)
228
+ || brief.schemaVersion !== expectedVersion)
228
229
  fail('Investigation capsule brief');
229
230
  iso(brief.generatedAt, 'Investigation capsule brief generatedAt');
230
231
  text(brief.target, 'Investigation capsule brief target', 253);
@@ -250,21 +251,22 @@ function validateCurrentBrief(value) {
250
251
  integer(thresholds[key], 'Investigation capsule freshness threshold', 0, 3650);
251
252
  validateDecisionFactProjection(brief.decisionFacts);
252
253
  const relationships = exact(brief.relationships, ['nodes', 'edges', 'truncated', 'kinds'], 'Investigation capsule relationship summary');
253
- integer(relationships.nodes, 'Investigation capsule relationship node count', 0, 72);
254
- integer(relationships.edges, 'Investigation capsule relationship edge count', 0, 120);
254
+ integer(relationships.nodes, 'Investigation capsule relationship node count', 0, expectedVersion === 2 ? 72 : MAX_LOOKUP_ASSET_NODES);
255
+ integer(relationships.edges, 'Investigation capsule relationship edge count', 0, expectedVersion === 2 ? 120 : MAX_LOOKUP_ASSET_EDGES);
255
256
  boolean(relationships.truncated, 'Investigation capsule relationship truncation');
256
- strings(relationships.kinds, 'Investigation capsule relationship kinds', 12, 320);
257
+ strings(relationships.kinds, 'Investigation capsule relationship kinds', expectedVersion === 2 ? 12 : 64, 320);
257
258
  strings(brief.limitations, 'Investigation capsule brief limitations', 20, 320);
258
259
  if (new TextEncoder().encode(JSON.stringify(brief)).byteLength > MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES) {
259
260
  fail('Investigation capsule brief byte limit');
260
261
  }
261
262
  }
262
- function validateGraph(value) {
263
- const graph = exact(value, ['version', 'targetId', 'nodes', 'edges', 'sources', 'truncated', 'limitations'], 'Investigation capsule graph');
264
- if (graph.version !== LOOKUP_ASSET_GRAPH_VERSION)
263
+ function validateGraph(value, expectedVersion) {
264
+ const current = expectedVersion === LOOKUP_ASSET_GRAPH_VERSION;
265
+ const graph = exact(value, ['version', 'targetId', 'nodes', 'edges', 'sources', 'truncated', 'limitations', ...(current ? ['coverage'] : [])], 'Investigation capsule graph');
266
+ if (graph.version !== expectedVersion)
265
267
  fail('Investigation capsule graph');
266
268
  text(graph.targetId, 'Investigation capsule graph target', 160);
267
- const nodes = array(graph.nodes, 'Investigation capsule graph nodes', 72, 1);
269
+ const nodes = array(graph.nodes, 'Investigation capsule graph nodes', current ? MAX_LOOKUP_ASSET_NODES : 72, 1);
268
270
  const nodeIds = new Set();
269
271
  for (const candidate of nodes) {
270
272
  const node = exact(candidate, ['id', 'label', 'kind', 'detail'], 'Investigation capsule graph node');
@@ -279,7 +281,7 @@ function validateGraph(value) {
279
281
  if (!nodeIds.has(graph.targetId))
280
282
  fail('Investigation capsule graph target');
281
283
  const sourceIds = new Set();
282
- for (const candidate of array(graph.sources, 'Investigation capsule graph sources', 32)) {
284
+ for (const candidate of array(graph.sources, 'Investigation capsule graph sources', current ? 128 : 32)) {
283
285
  const source = exact(candidate, ['id', 'label', 'href', 'observedAt', 'completeness', 'limitations'], 'Investigation capsule graph source');
284
286
  const id = text(source.id, 'Investigation capsule graph source id', 160);
285
287
  if (sourceIds.has(id))
@@ -290,9 +292,9 @@ function validateGraph(value) {
290
292
  fail('Investigation capsule graph source href');
291
293
  iso(source.observedAt, 'Investigation capsule graph source observedAt', true);
292
294
  enumeration(source.completeness, ['complete', 'partial', 'unknown'], 'Investigation capsule graph source completeness');
293
- strings(source.limitations, 'Investigation capsule graph source limitations', 5, 320);
295
+ strings(source.limitations, 'Investigation capsule graph source limitations', current ? MAX_LOOKUP_ASSET_LIMITATIONS : 5, 320);
294
296
  }
295
- const edges = array(graph.edges, 'Investigation capsule graph edges', 120);
297
+ const edges = array(graph.edges, 'Investigation capsule graph edges', current ? MAX_LOOKUP_ASSET_EDGES : 120);
296
298
  const edgeIds = new Set();
297
299
  for (const candidate of edges) {
298
300
  const edge = exactOptional(candidate, ['id', 'sourceId', 'source', 'target', 'kind', 'label', 'sourceLabel', 'observedAt', 'completeness', 'limitations', 'lenses', 'href'], ['boundary'], 'Investigation capsule graph edge');
@@ -310,13 +312,36 @@ function validateGraph(value) {
310
312
  text(edge.sourceLabel, 'Investigation capsule graph edge source label', 320);
311
313
  iso(edge.observedAt, 'Investigation capsule graph edge observedAt', true);
312
314
  enumeration(edge.completeness, ['complete', 'partial', 'unknown'], 'Investigation capsule graph edge completeness');
313
- strings(edge.limitations, 'Investigation capsule graph edge limitations', 5, 320);
315
+ strings(edge.limitations, 'Investigation capsule graph edge limitations', current ? MAX_LOOKUP_ASSET_LIMITATIONS : 5, 320);
314
316
  strings(edge.lenses, 'Investigation capsule graph edge lenses', 4, 40).forEach((lens) => enumeration(lens, ['all', 'identity', 'delegation', 'certificate'], 'Investigation capsule graph edge lens'));
315
317
  if (typeof edge.href !== 'string' || !/^#[^\u0000-\u001f\u007f]{1,160}$/u.test(edge.href))
316
318
  fail('Investigation capsule graph edge href');
317
319
  if (edge.boundary !== undefined)
318
320
  enumeration(edge.boundary, ['external', 'reviewed_profile', 'same_origin', 'same_registrable_domain', 'unresolved'], 'Investigation capsule graph edge boundary');
319
321
  }
322
+ if (current) {
323
+ const coverage = exact(graph.coverage, ['inputs'], 'Investigation capsule graph coverage');
324
+ const ids = new Set();
325
+ let incomplete = false;
326
+ for (const candidate of array(coverage.inputs, 'Investigation capsule graph input coverage', MAX_LOOKUP_ASSET_INPUT_ROWS)) {
327
+ const row = exact(candidate, ['id', 'supplied', 'inspected', 'admitted', 'invalid', 'duplicates', 'omitted'], 'Investigation capsule graph input coverage row');
328
+ const id = text(row.id, 'Investigation capsule graph input id', 160);
329
+ if (ids.has(id))
330
+ fail('Investigation capsule graph input ids');
331
+ ids.add(id);
332
+ const supplied = integer(row.supplied, 'Investigation capsule graph supplied input', 0, 0xffff_ffff);
333
+ const inspected = integer(row.inspected, 'Investigation capsule graph inspected input', 0, 800);
334
+ const admitted = integer(row.admitted, 'Investigation capsule graph admitted input', 0, inspected);
335
+ const invalid = integer(row.invalid, 'Investigation capsule graph invalid input', 0, inspected);
336
+ const duplicates = integer(row.duplicates, 'Investigation capsule graph duplicate input', 0, inspected);
337
+ const omitted = integer(row.omitted, 'Investigation capsule graph omitted input', 0, inspected);
338
+ if (inspected > supplied || admitted + invalid + duplicates + omitted !== inspected)
339
+ fail('Investigation capsule graph input accounting');
340
+ incomplete ||= supplied > inspected || invalid > 0 || omitted > 0;
341
+ }
342
+ if (graph.truncated !== incomplete)
343
+ fail('Investigation capsule graph coverage linkage');
344
+ }
320
345
  boolean(graph.truncated, 'Investigation capsule graph truncation');
321
346
  strings(graph.limitations, 'Investigation capsule graph limitations', 20, 320);
322
347
  }
@@ -349,8 +374,8 @@ function validateAnalystRecords(value) {
349
374
  }
350
375
  export function validateInvestigationCapsuleStructure(value) {
351
376
  const root = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'application', 'target', 'sourceContracts', 'investigationBrief', 'graphSnapshot', 'analystRecords', 'integrity', 'limitations'], 'Investigation capsule');
352
- if (root.schemaVersion !== PUBLIC_INVESTIGATION_CAPSULE_VERSION
353
- && root.schemaVersion !== INVESTIGATION_CAPSULE_VERSION)
377
+ const expected = investigationCapsuleContracts(root.schemaVersion);
378
+ if (!expected)
354
379
  fail('Investigation capsule');
355
380
  iso(root.generatedAt, 'Investigation capsule generatedAt');
356
381
  const application = exact(root.application, ['name', 'version'], 'Investigation capsule application');
@@ -376,9 +401,7 @@ export function validateInvestigationCapsuleStructure(value) {
376
401
  const expectedContractIds = root.analystRecords === null
377
402
  ? ['lookup-evidence', 'investigation-brief', 'asset-graph']
378
403
  : ['lookup-evidence', 'investigation-brief', 'asset-graph', 'analyst-records'];
379
- const expectedBriefVersion = root.schemaVersion === INVESTIGATION_CAPSULE_VERSION
380
- ? LOOKUP_INVESTIGATION_BRIEF_VERSION
381
- : PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION;
404
+ const expectedBriefVersion = expected.brief;
382
405
  if (!sameValues(contracts.map((candidate) => candidate.id), expectedContractIds))
383
406
  fail('Investigation capsule source contracts');
384
407
  if (!byId.has('lookup-evidence') || !byId.has('investigation-brief') || !byId.has('asset-graph')
@@ -387,25 +410,23 @@ export function validateInvestigationCapsuleStructure(value) {
387
410
  || byId.get('investigation-brief')?.version !== expectedBriefVersion
388
411
  || byId.get('investigation-brief')?.embedded !== true
389
412
  || byId.get('asset-graph')?.schema !== LOOKUP_ASSET_GRAPH_SCHEMA
390
- || byId.get('asset-graph')?.version !== LOOKUP_ASSET_GRAPH_VERSION
413
+ || byId.get('asset-graph')?.version !== expected.graph
391
414
  || byId.get('asset-graph')?.embedded !== true
392
415
  || (byId.has('analyst-records') && (byId.get('analyst-records')?.schema !== INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA
393
416
  || byId.get('analyst-records')?.version !== INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION
394
417
  || byId.get('analyst-records')?.embedded !== true)))
395
418
  fail('Investigation capsule source contracts');
396
- if (root.schemaVersion === INVESTIGATION_CAPSULE_VERSION)
397
- validateCurrentBrief(root.investigationBrief);
398
- else
419
+ if (expectedBriefVersion === 1)
399
420
  validatePublicBrief(root.investigationBrief);
400
- validateGraph(root.graphSnapshot);
421
+ else
422
+ validateCurrentBrief(root.investigationBrief, expectedBriefVersion);
423
+ validateGraph(root.graphSnapshot, expected.graph);
401
424
  validateAnalystRecords(root.analystRecords);
402
- const brief = root.investigationBrief;
403
- const graph = root.graphSnapshot;
404
- if (target.value !== brief.target || target.type !== brief.targetType
405
- || graph.targetId === undefined
406
- || brief.relationships.nodes !== graph.nodes.length
407
- || brief.relationships.edges !== graph.edges.length)
425
+ // The version-specific validators above independently establish these common fields.
426
+ const projections = root;
427
+ if (!investigationCapsuleProjectionsMatch(projections.target, projections.investigationBrief, projections.graphSnapshot)) {
408
428
  fail('Investigation capsule projection linkage');
429
+ }
409
430
  const integrity = exact(root.integrity, ['algorithm', 'canonicalization', 'scope', 'briefDigest', 'graphDigest', 'analystRecordsDigest', 'digestSha256'], 'Investigation capsule integrity');
410
431
  if (integrity.algorithm !== 'SHA-256')
411
432
  fail('Investigation capsule integrity');
@@ -1,9 +1,10 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
- import { assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, projectLookupEvidenceAvailability, projectLookupEvidenceAvailabilityPublic, projectLookupEvidenceQuery, projectLookupEvidenceRdapPublication, projectLookupEvidenceRegistryInsights, projectLookupEvidenceWhoisPublication, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../../lib/evidence-export.mjs";
2
+ import { assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION, PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION, REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, projectLookupEvidenceAvailability, projectLookupEvidenceAvailabilityPublic, projectLookupEvidenceQuery, projectLookupEvidenceRdapPublication, projectLookupEvidenceRegistryInsights, projectLookupEvidenceWhoisPublication, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../../lib/evidence-export.mjs";
3
+ import { validLookupObservationScope } from "../../packages/evidence/lookup-target.mjs";
3
4
  import { registrarStandingObservedBy, resolveRegistrarIanaId, validRegistrarStanding, } from "../../lib/registrar-standing-contract.mjs";
4
5
  import { compareRegistrySources } from "../../lib/registry-comparison.mjs";
5
6
  import { buildRegistryInsights } from "../../lib/registry-insights.mjs";
6
- import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../../lib/project-metadata.mjs";
7
+ import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../../packages/analysis/project-metadata.mjs";
7
8
  import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "../../lib/homepage-metadata-contract.mjs";
8
9
  import { HEX_DIGEST_RE, SEMANTIC_VERSION_RE, absoluteUrl, array, boolean, domain, enumeration, exact, exactOptional, fail, integer, iso, nullableRecord, optionalText, record, strings, text, } from "./structure-primitives.mjs";
9
10
  const LOOKUP_RDAP_ATTEMPT_OUTCOMES = [
@@ -649,27 +650,32 @@ export function validateLookupEvidenceArtifactStructure(value) {
649
650
  }
650
651
  validateLookupEvidenceSecurityTxt(sources.securityTxt);
651
652
  validateLookupEvidenceSslbl(sources.sslbl);
652
- const analysis = exact(root.analysis, version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
653
+ const analysis = exact(root.analysis, version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
653
654
  ? ['availability', 'idn', 'registryInsights', 'registryComparison', 'registrarPublicationComparison', 'registrarStanding']
654
655
  : ['availability', 'idn', 'registryInsights', 'registryComparison', 'registrarPublicationComparison'], 'Lookup evidence analysis');
655
- if (version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
656
+ if (version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
656
657
  && analysis.registrarStanding !== null
657
658
  && !validRegistrarStanding(analysis.registrarStanding))
658
659
  fail('Lookup evidence registrar standing');
659
- if (version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
660
+ if (version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
660
661
  && analysis.registrarStanding !== null
661
662
  && !registrarStandingObservedBy(analysis.registrarStanding, root.generatedAt)) {
662
663
  fail('Lookup evidence registrar standing observation time');
663
664
  }
664
665
  if (analysis.availability !== null) {
665
666
  const currentAvailability = version >= PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION;
666
- const availability = exactOptional(analysis.availability, currentAvailability ? ['registryContactsExcluded'] : [], currentAvailability ? LOOKUP_AVAILABILITY_ANALYSIS_KEYS : PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS, 'Lookup evidence availability analysis');
667
+ const availability = exactOptional(analysis.availability, currentAvailability ? ['registryContactsExcluded'] : [], currentAvailability
668
+ ? [...LOOKUP_AVAILABILITY_ANALYSIS_KEYS, ...(version >= HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION ? ['observationHostname', 'webObservationMode'] : [])]
669
+ : PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS, 'Lookup evidence availability analysis');
667
670
  if (currentAvailability && availability.registryContactsExcluded !== true) {
668
671
  fail('Lookup evidence availability contact exclusion');
669
672
  }
673
+ if (!validLookupObservationScope(availability, query)) {
674
+ fail('Lookup evidence observation hostname binding');
675
+ }
670
676
  validateLookupEvidenceHomepageMetadata(availability, version);
671
677
  if (!isDeepStrictEqual(analysis.availability, currentAvailability
672
- ? projectLookupEvidenceAvailability(analysis.availability)
678
+ ? projectLookupEvidenceAvailability(analysis.availability, { legacyUris: version < HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION })
673
679
  : projectLookupEvidenceAvailabilityPublic(analysis.availability)))
674
680
  fail('Lookup evidence availability analysis');
675
681
  }
@@ -684,7 +690,7 @@ export function validateLookupEvidenceArtifactStructure(value) {
684
690
  const whois = record(sources.whois, 'Lookup evidence WHOIS source');
685
691
  const rdapParsed = ['success', 'partial'].includes(rdapDiagnosticStatus) ? rdap.parsed : null;
686
692
  const whoisParsed = ['complete', 'partial'].includes(whoisDiagnosticStatus) ? whois.parsed : null;
687
- if (version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
693
+ if (version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
688
694
  && validRegistrarStanding(analysis.registrarStanding)
689
695
  && analysis.registrarStanding.ianaId !== resolveRegistrarIanaId(rdapParsed, whoisParsed)) {
690
696
  fail('Lookup evidence registrar standing source binding');
@@ -709,7 +715,7 @@ export function validateLookupEvidenceArtifactStructure(value) {
709
715
  if (!isDeepStrictEqual(registryInsights, expectedInsights))
710
716
  fail('Lookup evidence registry insight derivation');
711
717
  try {
712
- assertLookupEvidencePrivacySafeTree(root);
718
+ assertLookupEvidencePrivacySafeTree(root, { legacyUris: version < HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION });
713
719
  }
714
720
  catch {
715
721
  fail('Lookup evidence privacy boundary');