@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
@@ -3,10 +3,12 @@
3
3
  // proof of common ownership, coordination, intent, or maliciousness. This
4
4
  // module deliberately produces no aggregate score and is never a persistence
5
5
  // boundary.
6
- import { normalizeDomain } from "../cases/case-model.mjs";
6
+ import { normalizeDomain } from "../evidence/domain-name.mjs";
7
+ import { canonicalPublicIpAddress } from "../evidence/public-address-policy.mjs";
7
8
  import { groupBySimilarFavicon } from "./favicon-similarity.mjs";
8
- import { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, } from "../contracts/offline-comparison.mjs";
9
- export { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, };
9
+ import { qualifyRelationshipSources, relationshipSourceEvidence, normalizeRelationshipSourceProjection, FAVICON_RELATIONSHIP_DESCRIPTION, unknownRelationshipSource, MAX_RELATIONSHIP_SOURCES_PER_DOMAIN, } from "./relationship-provenance.mjs";
10
+ import { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS, RELATIONSHIP_SOURCE_PROVENANCE_VERSION, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, } from "../contracts/offline-comparison.mjs";
11
+ export { RELATIONSHIP_EVIDENCE_SCHEMA, RELATIONSHIP_EVIDENCE_VERSION, SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS, SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS, TLS_RELATIONSHIP_PROFILE_VERSION, };
10
12
  export const MAX_RELATIONSHIP_ROWS = 2000;
11
13
  export const MAX_RELATIONSHIP_GROUPS = 100;
12
14
  export const MAX_RELATIONSHIP_DOMAINS = 50;
@@ -16,6 +18,12 @@ export const MAX_TRACKING_IDS_PER_ROW = 30;
16
18
  export const MAX_OFFICIAL_ASSET_HOSTS_PER_ROW = 30;
17
19
  export const MAX_OFFICIAL_DOMAINS = 200;
18
20
  export const MAX_FAVICON_ROWS = 250;
21
+ const ARRAY_RELATIONSHIP_FIELDS = {
22
+ nameserver_set: { field: 'nameservers', maximum: MAX_NAMESERVERS_PER_ROW },
23
+ ip_address: { field: 'ipAddresses', maximum: MAX_IPS_PER_ROW },
24
+ tracking_identifier: { field: 'trackingIdentifiers', maximum: MAX_TRACKING_IDS_PER_ROW },
25
+ official_asset: { field: 'officialAssetHosts', maximum: MAX_OFFICIAL_ASSET_HOSTS_PER_ROW },
26
+ };
19
27
  const CONTROL_RE = /[\x00-\x1f\x7f]/;
20
28
  const FAVICON_SHA_RE = /^[a-f0-9]{64}$/i;
21
29
  const FAVICON_PHASH_RE = /^[a-f0-9]{16}$/i;
@@ -92,7 +100,7 @@ function tlsCertificateFingerprint(availability) {
92
100
  const certificate = record(tls?.certificate);
93
101
  const fingerprint = certificate?.fingerprintSha256;
94
102
  const status = tls?.status;
95
- if (tls?.source !== 'tls' || !SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS.some((version) => version === Number(tls?.profileVersion))
103
+ if (tls?.source !== 'tls' || !SUPPORTED_TLS_RELATIONSHIP_PROFILE_VERSIONS.some((version) => version === tls?.profileVersion)
96
104
  || typeof status !== 'string' || !['success', 'partial'].includes(status) || typeof fingerprint !== 'string'
97
105
  || fingerprint.length !== 64 || !CERTIFICATE_SHA_RE.test(fingerprint))
98
106
  return null;
@@ -131,6 +139,36 @@ export function relationshipObservation(rawAvailability, rawOfficialDomains = []
131
139
  ? availability.faviconHash.toLowerCase() : null;
132
140
  const faviconPHash = typeof availability.faviconPHash === 'string' && FAVICON_PHASH_RE.test(availability.faviconPHash)
133
141
  ? availability.faviconPHash.toLowerCase() : null;
142
+ const certificateFingerprint = tlsCertificateFingerprint(availability);
143
+ const dnsSource = relationshipSourceEvidence('dns', availability.dns);
144
+ const httpSource = relationshipSourceEvidence('http', availability.http);
145
+ const projectedSource = (source, truncated) => truncated
146
+ ? { ...source, complete: false, truncated: true } : source;
147
+ const sourceEvidence = {};
148
+ if (nameservers.values.length)
149
+ sourceEvidence.nameserver_set = [
150
+ // The compact availability nameserver list can be registration data or a
151
+ // DNS fallback. Its source is not distinguishable from this projection.
152
+ ...(Array.isArray(availability.nameservers) && availability.nameservers.length
153
+ ? [availability.source === 'dns' ? projectedSource(dnsSource, nameservers.truncated)
154
+ : unknownRelationshipSource('registration_or_dns')] : []),
155
+ ...(Array.isArray(records.ns) && records.ns.length ? [projectedSource(dnsSource, nameservers.truncated)] : []),
156
+ ];
157
+ if (ipAddresses.values.length)
158
+ sourceEvidence.ip_address = [projectedSource(dnsSource, ipAddresses.truncated)];
159
+ if (certificateFingerprint)
160
+ sourceEvidence.certificate = [relationshipSourceEvidence('tls', availability.tls)];
161
+ if (trackingIdentifiers.values.length)
162
+ sourceEvidence.tracking_identifier = [
163
+ projectedSource(httpSource, trackingIdentifiers.truncated),
164
+ relationshipSourceEvidence('page_identity', availability.pageIdentity),
165
+ ];
166
+ if (officialAssetHosts.length)
167
+ sourceEvidence.official_asset = [projectedSource(httpSource, assets.truncated
168
+ || officialDomainSource.length > MAX_OFFICIAL_DOMAINS)];
169
+ // A homepage timestamp does not date the independently fetched favicon.
170
+ if (faviconHash || faviconPHash)
171
+ sourceEvidence.favicon = [unknownRelationshipSource('favicon')];
134
172
  return {
135
173
  version: RELATIONSHIP_EVIDENCE_VERSION,
136
174
  nameservers: nameservers.values,
@@ -139,7 +177,8 @@ export function relationshipObservation(rawAvailability, rawOfficialDomains = []
139
177
  officialAssetHosts,
140
178
  faviconHash,
141
179
  faviconPHash,
142
- certificateFingerprint: tlsCertificateFingerprint(availability),
180
+ certificateFingerprint,
181
+ sourceEvidence,
143
182
  truncated: nameservers.truncated || ipAddresses.truncated || trackingIdentifiers.truncated || assets.truncated
144
183
  || officialDomainSource.length > MAX_OFFICIAL_DOMAINS,
145
184
  };
@@ -152,7 +191,22 @@ function addBucket(buckets, value, domain) {
152
191
  buckets.get(value)?.add(domain);
153
192
  }
154
193
  function group(type, label, method, value, domains, description, normalizedValue = value) {
155
- return { type, label, method, value, normalizedValue, domains, description };
194
+ return { type, label, method, value, normalizedValue, domains, description, ...qualifyRelationshipSources([], domains) };
195
+ }
196
+ function contributesToGroup(observation, item, domain) {
197
+ if (item.type === 'certificate')
198
+ return typeof observation.certificateFingerprint === 'string'
199
+ && observation.certificateFingerprint.toLowerCase() === item.normalizedValue;
200
+ if (item.type === 'nameserver_set')
201
+ return Array.isArray(observation.nameservers)
202
+ && [...new Set(observation.nameservers.slice(0, MAX_NAMESERVERS_PER_ROW).map(hostname).filter(Boolean))].sort().join(' · ') === item.normalizedValue;
203
+ const field = ARRAY_RELATIONSHIP_FIELDS[item.type];
204
+ if (field)
205
+ return Array.isArray(observation[field.field]) && observation[field.field].slice(0, field.maximum).some((value) => (item.type === 'ip_address' ? ipAddress(value) : item.type === 'official_asset' ? hostname(value) : value) === item.normalizedValue);
206
+ return item.type === 'favicon' && item.normalizedValue.split('|').includes(`${domain}=${[
207
+ typeof observation.faviconHash === 'string' && FAVICON_SHA_RE.test(observation.faviconHash) ? `sha256:${observation.faviconHash.toLowerCase()}` : '',
208
+ typeof observation.faviconPHash === 'string' && FAVICON_PHASH_RE.test(observation.faviconPHash) ? `dhash:${observation.faviconPHash.toLowerCase()}` : '',
209
+ ].filter(Boolean).join(',')}`);
156
210
  }
157
211
  /**
158
212
  * @param {Array<{domain?:unknown,trusted?:unknown,relationship?:unknown}>} rawRows
@@ -164,7 +218,7 @@ export function buildScanRelationships(rawRows) {
164
218
  for (const raw of input.slice(0, MAX_RELATIONSHIP_ROWS)) {
165
219
  const domain = normalizeDomain(raw?.domain);
166
220
  const observation = record(raw?.relationship);
167
- if (!domain || raw?.trusted || !observation || observation.version !== RELATIONSHIP_EVIDENCE_VERSION)
221
+ if (!domain || raw?.trusted || !observation || !SUPPORTED_RELATIONSHIP_EVIDENCE_VERSIONS.some((version) => version === observation.version))
168
222
  continue;
169
223
  rows.push({ domain, observation });
170
224
  if (observation.truncated === true)
@@ -175,6 +229,7 @@ export function buildScanRelationships(rawRows) {
175
229
  const identifiers = new Map();
176
230
  const certificates = new Map();
177
231
  const officialAssets = new Map();
232
+ let excludedNonPublicAddresses = 0;
178
233
  for (const { domain, observation } of rows) {
179
234
  const nameserverSource = Array.isArray(observation.nameservers) ? observation.nameservers : [];
180
235
  const addressSource = Array.isArray(observation.ipAddresses) ? observation.ipAddresses : [];
@@ -186,8 +241,16 @@ export function buildScanRelationships(rawRows) {
186
241
  const nameservers = nameserverSource.slice(0, MAX_NAMESERVERS_PER_ROW).map(hostname).filter(Boolean);
187
242
  if (nameservers.length)
188
243
  addBucket(nameserverSets, [...new Set(nameservers)].sort().join(' · '), domain);
189
- for (const value of addressSource.slice(0, MAX_IPS_PER_ROW))
190
- addBucket(addresses, ipAddress(value), domain);
244
+ for (const value of addressSource.slice(0, MAX_IPS_PER_ROW)) {
245
+ const address = ipAddress(value);
246
+ if (!address)
247
+ continue;
248
+ const publicAddress = canonicalPublicIpAddress(address);
249
+ if (publicAddress)
250
+ addBucket(addresses, publicAddress, domain);
251
+ else
252
+ excludedNonPublicAddresses += 1;
253
+ }
191
254
  for (const value of identifierSource.slice(0, MAX_TRACKING_IDS_PER_ROW)) {
192
255
  if (typeof value === 'string' && /^[a-z-]{1,40}:[A-Z0-9-]{1,64}$/.test(value)) {
193
256
  addBucket(identifiers, value, domain);
@@ -235,7 +298,7 @@ export function buildScanRelationships(rawRows) {
235
298
  evidence?.faviconPHash ? `dhash:${evidence.faviconPHash}` : '',
236
299
  ].filter(Boolean).join(',')}`;
237
300
  }).join('|');
238
- output.push(group('favicon', 'Similar favicon', 'Exact SHA-256 or perceptual dHash distance ≤ 6', '', distinctDomains, 'These domains used an identical or perceptually similar favicon in this scan.', normalizedValue));
301
+ output.push(group('favicon', 'Connected favicon matches', 'Exact SHA-256 or perceptual dHash distance ≤ 6', '', distinctDomains, FAVICON_RELATIONSHIP_DESCRIPTION, normalizedValue));
239
302
  }
240
303
  }
241
304
  for (const [value, domains] of officialAssets)
@@ -245,17 +308,43 @@ export function buildScanRelationships(rawRows) {
245
308
  output.sort((left, right) => (Number(order.get(left.type)) - Number(order.get(right.type))) || left.value.localeCompare(right.value) || left.domains.join('|').localeCompare(right.domains.join('|')));
246
309
  if (output.length > MAX_RELATIONSHIP_GROUPS)
247
310
  truncated = true;
311
+ const rowsByDomain = new Map();
312
+ for (const row of rows) {
313
+ const members = rowsByDomain.get(row.domain) ?? [];
314
+ members.push(row);
315
+ rowsByDomain.set(row.domain, members);
316
+ }
248
317
  const groups = output.slice(0, MAX_RELATIONSHIP_GROUPS).map((item) => {
249
- if (item.domains.length <= MAX_RELATIONSHIP_DOMAINS)
250
- return item;
251
- truncated = true;
252
- return { ...item, domains: item.domains.slice(0, MAX_RELATIONSHIP_DOMAINS) };
318
+ const domains = item.domains.slice(0, MAX_RELATIONSHIP_DOMAINS);
319
+ let groupTruncated = item.domains.length > domains.length || input.length > MAX_RELATIONSHIP_ROWS;
320
+ if (groupTruncated)
321
+ truncated = true;
322
+ const contributors = [];
323
+ for (const domain of domains)
324
+ for (const { observation } of rowsByDomain.get(domain) ?? []) {
325
+ if (!contributesToGroup(observation, item, domain))
326
+ continue;
327
+ const field = ARRAY_RELATIONSHIP_FIELDS[item.type];
328
+ if (field && Array.isArray(observation[field.field]) && observation[field.field].length > field.maximum)
329
+ groupTruncated = true;
330
+ const projection = Number(observation.version) >= RELATIONSHIP_SOURCE_PROVENANCE_VERSION
331
+ ? normalizeRelationshipSourceProjection(observation.sourceEvidence) : {};
332
+ for (const source of projection[item.type] ?? [unknownRelationshipSource()]) {
333
+ if (contributors.length >= MAX_RELATIONSHIP_DOMAINS * MAX_RELATIONSHIP_SOURCES_PER_DOMAIN)
334
+ groupTruncated = true;
335
+ else
336
+ contributors.push({ domain, ...source });
337
+ }
338
+ }
339
+ return { ...item, domains, ...qualifyRelationshipSources(contributors, domains, { truncated: groupTruncated, type: item.type }) };
253
340
  });
254
341
  return {
255
342
  version: RELATIONSHIP_EVIDENCE_VERSION,
256
343
  groups,
344
+ excludedNonPublicAddresses,
257
345
  truncated,
258
346
  limitations: [
347
+ ...(excludedNonPublicAddresses ? [`${excludedNonPublicAddresses} non-public address observations were excluded from public-infrastructure grouping; their DNS answers remain in the scan evidence.`] : []),
259
348
  'Shared observations are investigation pivots, not proof of common ownership, coordination, intent, or maliciousness.',
260
349
  'Certificate relationships use exact native TLS leaf-certificate SHA-256 values only. Certificate Transparency counts and hostnames are never treated as certificate reuse.',
261
350
  'A shared certificate can reflect a multi-domain certificate, shared hosting, CDN, or managed platform and does not establish common control.',
@@ -0,0 +1,99 @@
1
+ // Only the source metadata needed to qualify an observed relationship crosses
2
+ // this boundary. Collection and retention times are never source-time fallbacks.
3
+ import { normalizeDomain } from "../evidence/domain-name.mjs";
4
+ import { canonicalPublicIpAddress } from "../evidence/public-address-policy.mjs";
5
+ import { normalizeExplicitIsoTimestamp, readObservationEnvelope } from "../evidence/observation.mjs";
6
+ export const RELATIONSHIP_TYPES = Object.freeze([
7
+ 'nameserver_set', 'ip_address', 'certificate', 'tracking_identifier', 'favicon', 'official_asset',
8
+ ]);
9
+ export const NON_PUBLIC_RELATIONSHIP_LIMITATION = 'This is an equal non-public DNS answer, not evidence of shared public hosting. Private, loopback, reserved and sinkhole-style answers can recur independently.';
10
+ export const FAVICON_RELATIONSHIP_DESCRIPTION = 'Each domain is connected by exact or bounded perceptual favicon matches, directly or through another member. Not every pair necessarily matches.';
11
+ export function isNonPublicAddressRelationship(type, value) {
12
+ return type === 'ip_address' && !canonicalPublicIpAddress(value);
13
+ }
14
+ const SOURCES = ['dns', 'tls', 'http', 'page_identity', 'favicon', 'registration_or_dns', 'unknown'];
15
+ const SOURCES_BY_TYPE = {
16
+ nameserver_set: ['dns', 'registration_or_dns'], ip_address: ['dns'], certificate: ['tls'],
17
+ tracking_identifier: ['http', 'page_identity'], favicon: ['favicon'], official_asset: ['http'],
18
+ };
19
+ const STATUSES = ['success', 'partial', 'not_found', 'skipped', 'error', 'unsupported', 'not_applicable', 'unknown'];
20
+ export const MAX_RELATIONSHIP_SOURCES_PER_DOMAIN = 3;
21
+ export const MAX_RELATIONSHIP_SOURCE_DOMAINS = 50;
22
+ function record(value) {
23
+ return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
24
+ }
25
+ export function unknownRelationshipSource(source = 'unknown') {
26
+ return { source, status: 'unknown', observedAt: null, complete: false, truncated: null };
27
+ }
28
+ export function relationshipSourceEvidence(source, raw) {
29
+ const result = readObservationEnvelope(raw);
30
+ if (result.state !== 'supported' || result.observation.source !== source) {
31
+ return { ...unknownRelationshipSource(source), status: result.state === 'unsupported' ? 'unsupported' : 'unknown' };
32
+ }
33
+ return normalizeSource({ ...result.observation, truncated: record(raw).truncated });
34
+ }
35
+ function normalizeSource(raw, type) {
36
+ const value = record(raw);
37
+ const source = SOURCES.find((candidate) => candidate === value.source) ?? 'unknown';
38
+ if (type && source !== 'unknown' && !SOURCES_BY_TYPE[type].includes(source))
39
+ return unknownRelationshipSource();
40
+ const status = STATUSES.find((candidate) => candidate === value.status) ?? 'unknown';
41
+ const observedAt = normalizeExplicitIsoTimestamp(value.observedAt);
42
+ const truncated = typeof value.truncated === 'boolean' ? value.truncated : null;
43
+ return {
44
+ source, status, observedAt, truncated,
45
+ complete: source !== 'unknown' && source !== 'registration_or_dns' && status === 'success'
46
+ && value.complete === true && truncated === false && observedAt !== null,
47
+ };
48
+ }
49
+ export function normalizeRelationshipSourceProjection(raw) {
50
+ const value = record(raw);
51
+ const output = {};
52
+ for (const type of RELATIONSHIP_TYPES) {
53
+ if (!Array.isArray(value[type]))
54
+ continue;
55
+ const candidates = value[type];
56
+ output[type] = candidates.slice(0, MAX_RELATIONSHIP_SOURCES_PER_DOMAIN).map((candidate) => {
57
+ const source = normalizeSource(candidate, type);
58
+ return candidates.length > MAX_RELATIONSHIP_SOURCES_PER_DOMAIN ? { ...source, complete: false, truncated: true } : source;
59
+ });
60
+ }
61
+ return output;
62
+ }
63
+ /** Missing, malformed or omitted contributors cannot make a group complete. */
64
+ export function qualifyRelationshipSources(raw, rawDomains, options = {}) {
65
+ const domains = [...new Set(rawDomains.slice(0, MAX_RELATIONSHIP_SOURCE_DOMAINS * 2).map(normalizeDomain).filter(Boolean))].slice(0, MAX_RELATIONSHIP_SOURCE_DOMAINS);
66
+ const allowed = new Set(domains);
67
+ const candidates = Array.isArray(raw) ? raw : [];
68
+ const maximum = MAX_RELATIONSHIP_SOURCE_DOMAINS * MAX_RELATIONSHIP_SOURCES_PER_DOMAIN;
69
+ const byDomain = new Map();
70
+ let omitted = options.truncated === true || rawDomains.length > MAX_RELATIONSHIP_SOURCE_DOMAINS || candidates.length > maximum;
71
+ for (const candidate of candidates.slice(0, maximum)) {
72
+ const value = record(candidate);
73
+ const domain = normalizeDomain(value.domain);
74
+ if (!domain || !allowed.has(domain)) {
75
+ omitted = true;
76
+ continue;
77
+ }
78
+ const contribution = { domain, ...normalizeSource(value, options.type) };
79
+ const entries = byDomain.get(domain) ?? new Map();
80
+ const key = JSON.stringify(contribution);
81
+ if (!entries.has(key) && entries.size >= MAX_RELATIONSHIP_SOURCES_PER_DOMAIN)
82
+ omitted = true;
83
+ else
84
+ entries.set(key, contribution);
85
+ byDomain.set(domain, entries);
86
+ }
87
+ const sourceEvidence = domains.sort().flatMap((domain) => {
88
+ const entries = byDomain.get(domain);
89
+ return entries?.size ? [...entries.values()].sort((a, b) => JSON.stringify(a).localeCompare(JSON.stringify(b)))
90
+ : [{ domain, ...unknownRelationshipSource() }];
91
+ });
92
+ const observedAt = sourceEvidence.length && sourceEvidence.every((entry) => entry.observedAt !== null)
93
+ ? sourceEvidence.map((entry) => entry.observedAt).sort().at(-1) ?? null : null;
94
+ return {
95
+ sourceEvidence, observedAt,
96
+ complete: sourceEvidence.length > 0 && !omitted && sourceEvidence.every((entry) => entry.complete),
97
+ truncated: omitted || sourceEvidence.some((entry) => entry.truncated === true),
98
+ };
99
+ }
@@ -0,0 +1,234 @@
1
+ import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
2
+ import { buildExtractedLifecycleFamily } from "./extracted-domain-lifecycle.mjs";
3
+ import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
4
+ export const ANALYST_INTERCHANGE_CONTRACT_OWNER = 'packages/contracts/analyst-interchange.mts';
5
+ export const INVESTIGATION_CACAO_SPEC_VERSION = 'cacao-2.0';
6
+ export const INVESTIGATION_CACAO_PROFILE_VERSION = 3;
7
+ export const INVESTIGATION_CACAO_PROFILE_SEMVER = '3.0.0';
8
+ export const INVESTIGATION_CACAO_SUPPORTED_PROFILE_VERSIONS = [2, INVESTIGATION_CACAO_PROFILE_VERSION];
9
+ export const MAX_INVESTIGATION_CACAO_IMPORT_BYTES = 384 * 1024;
10
+ export const BRAND_PROTECTION_OPERATIONS_REPORT_SCHEMA = 'whoisleuth.brand-protection-operations-report';
11
+ export const BRAND_PROTECTION_OPERATIONS_REPORT_VERSION = 2;
12
+ export const MAX_OPERATIONS_REPORT_BYTES = 64 * 1024;
13
+ export const DEFENSIVE_INDICATOR_LIFECYCLE_SCHEMA = 'whoisleuth.internal.defensive-indicators';
14
+ export const DEFENSIVE_INDICATOR_EXPORT_VERSION = 2;
15
+ export const DEFENSIVE_INDICATOR_MANIFEST_SCHEMA = 'whoisleuth.defensive-indicator-manifest';
16
+ export const DEFENSIVE_INDICATOR_ROLLBACK_SCHEMA = 'whoisleuth.defensive-indicator-rollback';
17
+ export const STIX_INDICATOR_LIFECYCLE_SCHEMA = 'whoisleuth.internal.stix-indicators';
18
+ export const STIX_INDICATOR_EXPORT_VERSION = 2;
19
+ export const MISP_INDICATOR_LIFECYCLE_SCHEMA = 'whoisleuth.internal.misp-indicators';
20
+ export const MISP_INDICATOR_EXPORT_VERSION = 2;
21
+ export const MANAGED_INDICATOR_SET_SCHEMA = 'whoisleuth.managed-indicator-set';
22
+ export const MANAGED_INDICATOR_SET_VERSION = 1;
23
+ export const MAX_MANAGED_INDICATOR_SET_BYTES = 4 * 1024 * 1024;
24
+ export const MAX_MANAGED_INDICATOR_PLAN_BYTES = 16 * 1024 * 1024;
25
+ export const DNS_CHANGE_REHEARSAL_VERSION = 2;
26
+ export const DNS_CHANGE_REHEARSAL_EXPORT_SCHEMA = 'whoisleuth.dns-change-rehearsal';
27
+ export const MAIL_REPORT_SCHEMA = 'whoisleuth.mail-report-review';
28
+ export const MAIL_REPORT_VERSION = 3;
29
+ export const MAIL_REPORT_CANONICALIZATION_ROUTES = Object.freeze([
30
+ Object.freeze({ version: 2, canonicalization: 'sorted-json-v1', explicit: false }),
31
+ Object.freeze({ version: MAIL_REPORT_VERSION, canonicalization: 'sorted-json-v2', explicit: true }),
32
+ ]);
33
+ export const REGISTRATION_DISCLOSURE_PLAN_SCHEMA = 'whoisleuth.registration-disclosure-plan';
34
+ export const REGISTRATION_DISCLOSURE_PLAN_VERSION = 2;
35
+ export const STATIC_PAGE_PATTERN_PACK_SCHEMA = 'whoisleuth.static-page-pattern-pack';
36
+ export const STATIC_PAGE_PATTERN_PACK_VERSION = 2;
37
+ export const MAX_STATIC_PAGE_PATTERN_PACK_BYTES = 256 * 1024;
38
+ export const WEB_CAPTURE_SUMMARY_SCHEMA = 'whoisleuth.web-capture-summary';
39
+ export const WEB_CAPTURE_SUMMARY_VERSION = 1;
40
+ export const WEB_CAPTURE_MANIFEST_SCHEMA = 'whoisleuth.web-capture-manifest';
41
+ export const WEB_CAPTURE_MANIFEST_VERSION = 3;
42
+ export const WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS = [2, WEB_CAPTURE_MANIFEST_VERSION];
43
+ export const MAX_WEB_CAPTURE_MANIFEST_BYTES = 1024 * 1024;
44
+ const CACAO_COMPATIBILITY = defineSchemaCompatibility({
45
+ id: 'export.investigation-cacao-profile', kind: 'export', schema: INVESTIGATION_CACAO_SPEC_VERSION,
46
+ currentVersion: INVESTIGATION_CACAO_PROFILE_VERSION, supportedVersions: INVESTIGATION_CACAO_SUPPORTED_PROFILE_VERSIONS,
47
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'normalize_to_current',
48
+ writeSemantics: 'non_destructive_merge', byteBudget: MAX_INVESTIGATION_CACAO_IMPORT_BYTES,
49
+ owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
50
+ note: 'Restricted CACAO 2.0 manual profile. Version 3 retains optional lesson-revision provenance; public profile 2 remains readable. Executable commands, branches, targets and credentials remain rejected.',
51
+ });
52
+ const OPERATIONS_REPORT_COMPATIBILITY = defineSchemaCompatibility({
53
+ id: 'export.brand-protection-operations-report', kind: 'export', schema: BRAND_PROTECTION_OPERATIONS_REPORT_SCHEMA,
54
+ currentVersion: BRAND_PROTECTION_OPERATIONS_REPORT_VERSION, supportedVersions: [BRAND_PROTECTION_OPERATIONS_REPORT_VERSION],
55
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'not_applicable', migration: 'read_only',
56
+ writeSemantics: 'read_only', byteBudget: MAX_OPERATIONS_REPORT_BYTES, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
57
+ note: 'Version 2 aggregates typed action and independent-effect events with explicit denominators, omissions, time-window basis, and limitations. It excludes case identities and is not a service level or provider ranking.',
58
+ });
59
+ const DEFENSIVE_INDICATORS_COMPATIBILITY = defineSchemaCompatibility({
60
+ id: 'export.defensive-indicators', kind: 'export', schema: null,
61
+ currentVersion: DEFENSIVE_INDICATOR_EXPORT_VERSION, supportedVersions: [DEFENSIVE_INDICATOR_EXPORT_VERSION],
62
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'not_applicable', migration: 'read_only',
63
+ writeSemantics: 'read_only', byteBudget: null, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
64
+ note: 'Review-only indicator, provenance-manifest, and rollback formats; never submitted or applied automatically.',
65
+ });
66
+ const STIX_INDICATORS_COMPATIBILITY = defineSchemaCompatibility({
67
+ id: 'export.stix-indicators', kind: 'export', schema: null,
68
+ currentVersion: STIX_INDICATOR_EXPORT_VERSION, supportedVersions: [1, STIX_INDICATOR_EXPORT_VERSION],
69
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'not_applicable', migration: 'read_only',
70
+ writeSemantics: 'read_only', byteBudget: null, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
71
+ note: 'STIX 2.1 bundle with observations separated from heuristic indicators. Version 2 uses a Note when observation time is unknown; Indicator validity starts at creation, not at a substituted sighting time.',
72
+ });
73
+ const MISP_INDICATORS_COMPATIBILITY = defineSchemaCompatibility({
74
+ id: 'export.misp-indicators', kind: 'export', schema: null,
75
+ currentVersion: MISP_INDICATOR_EXPORT_VERSION, supportedVersions: [1, MISP_INDICATOR_EXPORT_VERSION],
76
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'not_applicable', migration: 'read_only',
77
+ writeSemantics: 'read_only', byteBudget: null, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
78
+ note: 'Unpublished, non-IDS, non-correlating event for reviewed import. Version 2 omits optional seen times when the source observation time is unknown.',
79
+ });
80
+ export const MANAGED_INDICATOR_SET_COMPATIBILITY = defineSchemaCompatibility({
81
+ id: 'export.managed-indicator-set', kind: 'export', schema: MANAGED_INDICATOR_SET_SCHEMA,
82
+ currentVersion: MANAGED_INDICATOR_SET_VERSION, supportedVersions: [MANAGED_INDICATOR_SET_VERSION],
83
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'exact_current_only',
84
+ writeSemantics: 'read_only', byteBudget: MAX_MANAGED_INDICATOR_SET_BYTES, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
85
+ note: 'File-backed managed indicator revisions retain original observations, stable identities, explicit renewals and permanent withdrawals. Known predecessors prevent history replacement; content digests do not authenticate authorship or an unavailable earlier chain.',
86
+ });
87
+ const WEB_CAPTURE_SUMMARY_COMPATIBILITY = defineSchemaCompatibility({
88
+ id: 'export.web-capture-summary', kind: 'export', schema: WEB_CAPTURE_SUMMARY_SCHEMA,
89
+ currentVersion: WEB_CAPTURE_SUMMARY_VERSION, supportedVersions: [WEB_CAPTURE_SUMMARY_VERSION],
90
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'exact_current_only',
91
+ writeSemantics: 'read_only', byteBudget: null, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
92
+ note: 'Sanitised metadata-only capture summary accepted through the bounded external-finding preview.',
93
+ });
94
+ const WEB_CAPTURE_MANIFEST_COMPATIBILITY = defineSchemaCompatibility({
95
+ id: 'export.web-capture-manifest', kind: 'export', schema: WEB_CAPTURE_MANIFEST_SCHEMA,
96
+ currentVersion: WEB_CAPTURE_MANIFEST_VERSION, supportedVersions: WEB_CAPTURE_MANIFEST_SUPPORTED_VERSIONS,
97
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'read_only',
98
+ writeSemantics: 'read_only', byteBudget: MAX_WEB_CAPTURE_MANIFEST_BYTES,
99
+ owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
100
+ note: 'Metadata-only capture manifest. Version 3 adds bounded page and dependency observations; version 2 remains readable with those observations unavailable.',
101
+ });
102
+ const DEFENSIVE_MANIFEST_COMPATIBILITY = defineSchemaCompatibility({
103
+ id: 'export.defensive-indicator-manifest', kind: 'export', schema: DEFENSIVE_INDICATOR_MANIFEST_SCHEMA,
104
+ currentVersion: DEFENSIVE_INDICATOR_EXPORT_VERSION, supportedVersions: [DEFENSIVE_INDICATOR_EXPORT_VERSION],
105
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'not_applicable', migration: 'read_only',
106
+ writeSemantics: 'read_only', byteBudget: null, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
107
+ note: 'Output-only manifest for explicitly selected defensive indicators; historical version 1 is not claimed without a retained reader.',
108
+ });
109
+ const DEFENSIVE_ROLLBACK_COMPATIBILITY = defineSchemaCompatibility({
110
+ id: 'export.defensive-indicator-rollback', kind: 'export', schema: DEFENSIVE_INDICATOR_ROLLBACK_SCHEMA,
111
+ currentVersion: DEFENSIVE_INDICATOR_EXPORT_VERSION, supportedVersions: [DEFENSIVE_INDICATOR_EXPORT_VERSION],
112
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'not_applicable', migration: 'read_only',
113
+ writeSemantics: 'read_only', byteBudget: null, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
114
+ note: 'Output-only rollback companion for the same bounded defensive-indicator selection.',
115
+ });
116
+ const DNS_REHEARSAL_COMPATIBILITY = defineSchemaCompatibility({
117
+ id: 'export.dns-change-rehearsal', kind: 'export', schema: DNS_CHANGE_REHEARSAL_EXPORT_SCHEMA,
118
+ currentVersion: DNS_CHANGE_REHEARSAL_VERSION, supportedVersions: [DNS_CHANGE_REHEARSAL_VERSION],
119
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'not_applicable', migration: 'read_only',
120
+ writeSemantics: 'read_only', byteBudget: null, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
121
+ note: 'Output-only DNS change rehearsal document; version 2 is the only producer contract represented by this tree.',
122
+ });
123
+ const MAIL_REPORT_COMPATIBILITY = defineSchemaCompatibility({
124
+ id: 'export.mail-report-review', kind: 'export', schema: MAIL_REPORT_SCHEMA,
125
+ currentVersion: MAIL_REPORT_VERSION, supportedVersions: [1, 2, MAIL_REPORT_VERSION], acceptsUnversionedLegacy: false,
126
+ futureVersionBehavior: 'not_applicable', migration: 'read_only', writeSemantics: 'read_only', byteBudget: null,
127
+ owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
128
+ note: 'Output-only aggregate mail review: v3 declares sorted-json-v2 integrity; v2 retains its undeclared v1 digest. Admission coverage is separate from pagination. Historical v1 has no reader; input and output byte bounds differ.',
129
+ });
130
+ const REGISTRATION_DISCLOSURE_COMPATIBILITY = defineSchemaCompatibility({
131
+ id: 'export.registration-disclosure-plan', kind: 'export', schema: REGISTRATION_DISCLOSURE_PLAN_SCHEMA,
132
+ currentVersion: REGISTRATION_DISCLOSURE_PLAN_VERSION, supportedVersions: [REGISTRATION_DISCLOSURE_PLAN_VERSION],
133
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'not_applicable', migration: 'read_only',
134
+ writeSemantics: 'read_only', byteBudget: null, owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
135
+ note: 'Output-only bounded registration-disclosure planning document; version 2 is the only current producer contract.',
136
+ });
137
+ const STATIC_PATTERN_PACK_COMPATIBILITY = defineSchemaCompatibility({
138
+ id: 'export.static-page-pattern-pack', kind: 'export', schema: STATIC_PAGE_PATTERN_PACK_SCHEMA,
139
+ currentVersion: STATIC_PAGE_PATTERN_PACK_VERSION, supportedVersions: [STATIC_PAGE_PATTERN_PACK_VERSION],
140
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'exact_current_only',
141
+ writeSemantics: 'non_destructive_merge', byteBudget: MAX_STATIC_PAGE_PATTERN_PACK_BYTES,
142
+ owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
143
+ note: 'Exact-current bounded page-pattern pack imported by explicit analyst action and merged without executing code or starting collection.',
144
+ });
145
+ export function serialiseAnalystInterchangeJson(value) {
146
+ return `${JSON.stringify(value, null, 2)}\n`;
147
+ }
148
+ const F = 'test/fixtures/extracted-domain-lifecycle/';
149
+ export const ANALYST_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(buildExtractedLifecycleFamily({
150
+ id: 'analyst-interchange', owner: ANALYST_INTERCHANGE_CONTRACT_OWNER,
151
+ serializerExportName: 'serialiseAnalystInterchangeJson', plane: 'shared', projection: 'browser_export',
152
+ retention: 'operator_controlled_output',
153
+ includedCategories: ['reviewed-indicators', 'source-identity', 'provenance', 'completeness', 'omitted-counts', 'limitations'],
154
+ excludedCategories: ['credentials', 'cookies', 'message-content', 'raw-upstream-responses', 'expanded-contacts'],
155
+ formats: [
156
+ { descriptor: MANAGED_INDICATOR_SET_COMPATIBILITY, lifecycleSchema: MANAGED_INDICATOR_SET_SCHEMA,
157
+ requiredKeys: ['schema', 'version', 'id', 'producerId', 'revisionId', 'name', 'revision', 'createdAt', 'modifiedAt', 'previous', 'entries', 'integrity'], optionalKeys: [],
158
+ hook: { module: 'packages/interchange/managed-indicator-set.mts', exportName: 'validateManagedIndicatorSet', role: 'structure_validator', runtime: 'shared' },
159
+ fixtures: [{ id: 'managed-indicator-set-v1', path: `${F}managed-indicator-set-v1.json`, bytes: 1_193, sha256: '3a4ffb7256e696d1f1b3bae14743bb8c03ddeb0922186868e935f5a489ba3985', version: MANAGED_INDICATOR_SET_VERSION }] },
160
+ { descriptor: CACAO_COMPATIBILITY, lifecycleSchema: INVESTIGATION_CACAO_SPEC_VERSION,
161
+ requiredKeys: ['type', 'spec_version', 'id', 'name', 'created', 'modified', 'workflow_start', 'workflow', 'extension_definitions'],
162
+ optionalKeys: ['description', 'playbook_types', 'playbook_activities', 'created_by', 'playbook_extensions', 'agent_definitions', 'x_whoisleuth_profile_version'],
163
+ hook: { module: 'packages/interchange/investigation-playbook-interchange.mts', exportName: 'parseCacaoInvestigationPlaybook', role: 'normaliser', runtime: 'shared' },
164
+ fixtures: [
165
+ { id: 'investigation-cacao-profile-v2', path: `${F}investigation-cacao-profile-v2.json`, bytes: 4_091, sha256: '7e9a3de6e12b83477690c0c58e03e6efbbb8b0c11fd98dc8470bdb6784d42358', version: 2 },
166
+ { id: 'investigation-cacao-profile-v3', path: `${F}investigation-cacao-profile-v3.json`, bytes: 4_067, sha256: '608cbb7875c3448ca11f6f8cb2f96ae098e18316532a8f651bbe540ca7f25a2d', version: INVESTIGATION_CACAO_PROFILE_VERSION },
167
+ ] },
168
+ { descriptor: OPERATIONS_REPORT_COMPATIBILITY, lifecycleSchema: BRAND_PROTECTION_OPERATIONS_REPORT_SCHEMA,
169
+ requiredKeys: ['schema', 'version', 'generatedAt', 'window', 'limitations'], optionalKeys: ['sourceState', 'summary', 'counts', 'states', 'actionTypes', 'durations', 'denominators', 'omissions'],
170
+ hook: { module: 'packages/interchange/brand-protection-operations-report.mts', exportName: 'serializeBrandProtectionOperationsReport', role: 'builder', runtime: 'shared' },
171
+ fixtures: [
172
+ { id: 'brand-protection-operations-report-v2', path: `${F}brand-protection-operations-report-v2.json`, bytes: 187, sha256: 'd38bf9b2c5cfc9c83df267ff2303ea2c3cdb398dd12c6607fbedb265b880a324', version: BRAND_PROTECTION_OPERATIONS_REPORT_VERSION },
173
+ ] },
174
+ { descriptor: DEFENSIVE_INDICATORS_COMPATIBILITY, lifecycleSchema: DEFENSIVE_INDICATOR_LIFECYCLE_SCHEMA,
175
+ requiredKeys: ['version', 'format'], optionalKeys: ['generatedAt', 'expiresAt', 'domains', 'entries', 'indicators', 'exclusions', 'explicitSelection', 'includeWildcards', 'truncated', 'filename', 'manifestFilename', 'rollbackFilename', 'mimeType', 'content', 'manifestContent', 'rollbackContent', 'limitations'],
176
+ hook: { module: 'packages/interchange/defensive-indicator-export.mts', exportName: 'buildDefensiveIndicatorExport', role: 'builder', runtime: 'shared' },
177
+ fixtures: [
178
+ { id: 'defensive-indicators-v2', path: `${F}defensive-indicators-v2.json`, bytes: 66, sha256: '597794782fbb2291acbcd854ced48bb2b8142f175fd9dd8ab99d53dd5532701e', version: DEFENSIVE_INDICATOR_EXPORT_VERSION },
179
+ ] },
180
+ { descriptor: STIX_INDICATORS_COMPATIBILITY, lifecycleSchema: STIX_INDICATOR_LIFECYCLE_SCHEMA,
181
+ requiredKeys: ['type', 'id', 'objects'], optionalKeys: ['x_whoisleuth_export_version'],
182
+ hook: { module: 'packages/interchange/stix-indicator-export.mts', exportName: 'buildStixIndicatorExport', role: 'builder', runtime: 'shared' },
183
+ fixtures: [
184
+ { id: 'stix-indicators-v1', path: `${F}stix-indicators-v1.json`, bytes: 115, sha256: '517fe4aec65bbdb5252945d5f0f0a393c5c72a2eb8f48a1c8147ae5c6e681f8e', version: 1 },
185
+ { id: 'stix-indicators-v2', path: `${F}stix-indicators-v2.json`, bytes: 5302, sha256: '09b3e53413944efbad81458b0042cc76e4d64f0895d5c1c2dbace3d52af0a6a4', version: STIX_INDICATOR_EXPORT_VERSION },
186
+ ] },
187
+ { descriptor: MISP_INDICATORS_COMPATIBILITY, lifecycleSchema: MISP_INDICATOR_LIFECYCLE_SCHEMA,
188
+ requiredKeys: ['Event'], optionalKeys: [],
189
+ hook: { module: 'packages/interchange/misp-indicator-export.mts', exportName: 'buildMispIndicatorExport', role: 'builder', runtime: 'shared' },
190
+ fixtures: [
191
+ { id: 'misp-indicators-v1', path: `${F}misp-indicators-v1.json`, bytes: 167, sha256: '043102cfb145ead16f6eba9b4ebe723aed12cc02f5899c918a0d172674e74997', version: 1 },
192
+ { id: 'misp-indicators-v2', path: `${F}misp-indicators-v2.json`, bytes: 1611, sha256: '9bcf624c2bcc6da9e1299ef470b288ca2f2da51c65665f0729f332c513a3d558', version: MISP_INDICATOR_EXPORT_VERSION },
193
+ ] },
194
+ { descriptor: WEB_CAPTURE_SUMMARY_COMPATIBILITY, lifecycleSchema: WEB_CAPTURE_SUMMARY_SCHEMA,
195
+ requiredKeys: ['schema', 'schemaVersion', 'source', 'captures'], optionalKeys: [],
196
+ hook: { module: 'packages/interchange/web-capture-import.mts', exportName: 'parseWebCaptureSummary', role: 'normaliser', runtime: 'shared' },
197
+ fixtures: [{ id: 'web-capture-summary-v1', path: `${F}web-capture-summary-v1.json`, bytes: 341, sha256: '70fba8a784f61f473ba6cde71a7d9dd915cfca46fbb3469e3bb57767adf973d9', version: WEB_CAPTURE_SUMMARY_VERSION }] },
198
+ { descriptor: WEB_CAPTURE_MANIFEST_COMPATIBILITY, lifecycleSchema: WEB_CAPTURE_MANIFEST_SCHEMA,
199
+ requiredKeys: ['schema', 'schemaVersion', 'source', 'captures'], optionalKeys: [],
200
+ hook: { module: 'packages/interchange/web-capture-import.mts', exportName: 'parseWebCaptureManifest', role: 'normaliser', runtime: 'shared' },
201
+ fixtures: [
202
+ { id: 'web-capture-manifest-v2', path: `${F}web-capture-manifest-v2.json`, bytes: 494, sha256: 'a2208dc956a832d70e37cd9a5929db81d29148856c485e4fd6ce5b123de5b8e6', version: 2 },
203
+ { id: 'web-capture-manifest-v3', path: `${F}web-capture-manifest-v3.json`, bytes: 868, sha256: 'bca4e8cb539ac6035dd44586fa7e27774766a14dadbc4fc73cad2b71dace2ac8', version: WEB_CAPTURE_MANIFEST_VERSION },
204
+ ] },
205
+ { descriptor: DEFENSIVE_MANIFEST_COMPATIBILITY, lifecycleSchema: DEFENSIVE_INDICATOR_MANIFEST_SCHEMA,
206
+ requiredKeys: ['schema', 'version', 'generatedAt'], optionalKeys: ['expiresAt', 'reviewRequired', 'explicitSelection', 'includeWildcards', 'entries', 'indicators', 'exclusions', 'limitations'],
207
+ hook: { module: 'packages/interchange/defensive-indicator-export.mts', exportName: 'buildDefensiveIndicatorExport', role: 'builder', runtime: 'shared' },
208
+ fixtures: [{ id: 'defensive-indicator-manifest-v2', path: `${F}defensive-indicator-manifest-v2.json`, bytes: 139, sha256: '8a2475b1be1810bcc10b6285630e7056986f961320c884fd8cfc72951eb79db7', version: DEFENSIVE_INDICATOR_EXPORT_VERSION }] },
209
+ { descriptor: DEFENSIVE_ROLLBACK_COMPATIBILITY, lifecycleSchema: DEFENSIVE_INDICATOR_ROLLBACK_SCHEMA,
210
+ requiredKeys: ['schema', 'version', 'generatedAt'], optionalKeys: ['removes', 'indicators', 'limitations'],
211
+ hook: { module: 'packages/interchange/defensive-indicator-export.mts', exportName: 'buildDefensiveIndicatorExport', role: 'builder', runtime: 'shared' },
212
+ fixtures: [{ id: 'defensive-indicator-rollback-v2', path: `${F}defensive-indicator-rollback-v2.json`, bytes: 139, sha256: '1af3d32d3e808327fadc39066256d441abb5957d1ae696b834a61a37658103e6', version: DEFENSIVE_INDICATOR_EXPORT_VERSION }] },
213
+ { descriptor: DNS_REHEARSAL_COMPATIBILITY, lifecycleSchema: DNS_CHANGE_REHEARSAL_EXPORT_SCHEMA,
214
+ requiredKeys: ['schema', 'version', 'generatedAt'], optionalKeys: ['domain', 'changes', 'findings', 'limitations'],
215
+ hook: { module: 'packages/interchange/dns-change-rehearsal.mts', exportName: 'buildDnsChangeRehearsalExport', role: 'builder', runtime: 'shared' },
216
+ fixtures: [{ id: 'dns-change-rehearsal-v2', path: `${F}dns-change-rehearsal-v2.json`, bytes: 169, sha256: '31b2e5d8d3774356362c4f5fc1741506ce7060953b49bb3cd3588adbd3bb96d3', version: DNS_CHANGE_REHEARSAL_VERSION }] },
217
+ { descriptor: MAIL_REPORT_COMPATIBILITY, lifecycleSchema: MAIL_REPORT_SCHEMA,
218
+ requiredKeys: ['schema', 'version', 'generatedAt'], optionalKeys: ['source', 'summary', 'findings', 'limitations', 'reports', 'profileScope', 'integrity'],
219
+ hook: { module: 'packages/interchange/mail-report-workbench.mts', exportName: 'buildMailReportReview', role: 'builder', runtime: 'shared' },
220
+ fixtures: [
221
+ { id: 'mail-report-review-v1', path: `${F}mail-report-review-v1.json`, bytes: 152, sha256: '804ffda72784ee2f0b34985f47f78075238841b4fbcc484b971efcd17f2b0c17', version: 1 },
222
+ { id: 'mail-report-review-v2', path: `${F}mail-report-review-v2.json`, bytes: 2188, sha256: '42bb3b3a05605b0ffdda3f369a52e0490dcf1541405f2db9cee900ddf0477d97', version: 2 },
223
+ { id: 'mail-report-review-v3', path: `${F}mail-report-review-v3.json`, bytes: 1627, sha256: '32d71697a08eae70cc070b981953cb18829efee139283f6163edecd126614048', version: MAIL_REPORT_VERSION },
224
+ ] },
225
+ { descriptor: REGISTRATION_DISCLOSURE_COMPATIBILITY, lifecycleSchema: REGISTRATION_DISCLOSURE_PLAN_SCHEMA,
226
+ requiredKeys: ['schema', 'version', 'generatedAt'], optionalKeys: ['domain', 'requests', 'limitations'],
227
+ hook: { module: 'packages/interchange/registration-disclosure-plan.mts', exportName: 'buildRegistrationDisclosurePlan', role: 'builder', runtime: 'shared' },
228
+ fixtures: [{ id: 'registration-disclosure-plan-v2', path: `${F}registration-disclosure-plan-v2.json`, bytes: 164, sha256: '74e516dc369a2c62fc5794134512bff8ec251faa85b30e78172f670b58db9037', version: REGISTRATION_DISCLOSURE_PLAN_VERSION }] },
229
+ { descriptor: STATIC_PATTERN_PACK_COMPATIBILITY, lifecycleSchema: STATIC_PAGE_PATTERN_PACK_SCHEMA,
230
+ requiredKeys: ['schema', 'version', 'id', 'label', 'description', 'evidenceBoundary', 'relationship', 'confidence', 'rules'], optionalKeys: [],
231
+ hook: { module: 'packages/interchange/static-page-pattern-packs.mts', exportName: 'validateStaticPagePatternPack', role: 'structure_validator', runtime: 'shared' },
232
+ fixtures: [{ id: 'static-page-pattern-pack-v2', path: `${F}static-page-pattern-pack-v2.json`, bytes: 1_094, sha256: 'c409e527850bcb924f5be0708a58c5e2c32d98947c42150f6a79765991c0eb6f', version: STATIC_PAGE_PATTERN_PACK_VERSION }] },
233
+ ],
234
+ }));
@@ -53,6 +53,17 @@ export const ANALYST_REVIEW_EVIDENCE_FAMILIES = Object.freeze([
53
53
  'packet',
54
54
  ]);
55
55
  export const ANALYST_REVIEW_DISPOSITIONS = Object.freeze(['open', 'expected', 'suppressed', 'resolved']);
56
+ export const ANALYST_REVIEW_PRIORITIES = ['urgent', 'high', 'normal'];
57
+ export const ANALYST_REVIEW_AGES = ['current', 'aging', 'stale', 'unknown'];
58
+ export const ANALYST_REVIEW_NEXT_ACTIONS = ['review', 'refresh', 'follow_up', 'resume'];
59
+ export const ANALYST_REVIEW_LIFECYCLE_STATES = ['open', 'expected', 'suppressed', 'resolved', 'expired', 'invalidated', 'recurred', 'orphaned'];
60
+ export const ANALYST_REVIEW_QUEUE_OPTIONS = [
61
+ { value: 'needs_action', label: 'Needs action' },
62
+ { value: 'waiting', label: 'Waiting / follow-up' },
63
+ { value: 'changed', label: 'Changed since review' },
64
+ { value: 'reviewed', label: 'Reviewed' },
65
+ { value: 'all', label: 'Everything' },
66
+ ];
56
67
  export const ANALYST_REVIEW_DISPOSITION_OPTIONS = Object.freeze([
57
68
  { value: 'open', label: 'Open' },
58
69
  { value: 'expected', label: 'Expected' },
@@ -1,5 +1,5 @@
1
1
  import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
2
- import { buildExtractedLifecycleFamilyV2 } from "./extracted-domain-lifecycle.mjs";
2
+ import { buildExtractedLifecycleFamily } from "./extracted-domain-lifecycle.mjs";
3
3
  import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
4
4
  import { ANALYST_REVIEW_STATE_SCHEMA, ANALYST_REVIEW_STATE_SCHEMA_VERSION, ANALYST_REVIEW_STATE_SUPPORTED_VERSIONS, MAX_ANALYST_REVIEW_STATE_BYTES, } from "./analyst-review-state-contract.mjs";
5
5
  export * from "./analyst-review-state-contract.mjs";
@@ -18,14 +18,15 @@ export const ANALYST_REVIEW_STATE_COMPATIBILITY = defineSchemaCompatibility({
18
18
  owner: ANALYST_REVIEW_STATE_CONTRACT_OWNER,
19
19
  note: 'Bounded analyst-authored lifecycle overlay. Material evidence changes, expiry, and incomplete evidence reopen review without rewriting source evidence or losing earlier rationale.',
20
20
  });
21
- export const ANALYST_REVIEW_STATE_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(buildExtractedLifecycleFamilyV2({
21
+ export const ANALYST_REVIEW_STATE_LIFECYCLE_FAMILY = defineSchemaLifecycleFamily(buildExtractedLifecycleFamily({
22
22
  id: 'analyst-review-state',
23
23
  owner: ANALYST_REVIEW_STATE_CONTRACT_OWNER,
24
24
  serializerModule: 'packages/monitoring/analyst-review-state.mts',
25
25
  serializerExportName: 'serialiseAnalystReviewStateJson',
26
26
  plane: 'browser',
27
- projection: 'browser_export',
28
- retention: 'operator_controlled_output',
27
+ projection: 'browser_import',
28
+ retention: 'browser_indexeddb',
29
+ notePolicy: 'allowed_bounded',
29
30
  includedCategories: ['subject-identity', 'material-fingerprint', 'analyst-disposition', 'rationale', 'review-times', 'case-and-campaign-references'],
30
31
  excludedCategories: ['raw-upstream-responses', 'expanded-contacts', 'complete-query-urls', 'credentials', 'cookies', 'certificate-transparency-history'],
31
32
  formats: [{