@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0

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 (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -14,6 +14,7 @@ export const CAPABILITY_IDS = Object.freeze({
14
14
  URLSCAN_SEARCH: 'urlscan_search',
15
15
  URLHAUS_HOST: 'urlhaus_host',
16
16
  THREATFOX_DOMAIN_IOC: 'threatfox_domain_ioc',
17
+ DOMAIN_FEED_SEARCH: 'domain_feed_search',
17
18
  REGISTRAR_RDAP: 'registrar_rdap',
18
19
  NETWORK_CONTEXT: 'network_context',
19
20
  REVERSE_DNS: 'reverse_dns',
@@ -76,9 +77,11 @@ export const CLI_COMMAND_SEMANTICS = Object.freeze({
76
77
  'page-compare': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
77
78
  'mail-review': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
78
79
  'mail-headers': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
80
+ intake: semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
79
81
  'review-evidence': semantic('investigate', CAPABILITY_IDS.PORTABLE_EVIDENCE),
80
82
  brief: semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
81
83
  case: semantic('respond', CAPABILITY_IDS.ANALYST_CASES),
84
+ 'indicator-set': semantic('respond', CAPABILITY_IDS.PORTABLE_EVIDENCE),
82
85
  'case-pack': semantic('respond', CAPABILITY_IDS.PORTABLE_EVIDENCE),
83
86
  'domain-control': semantic('assure', CAPABILITY_IDS.PORTABLE_EVIDENCE),
84
87
  'monitor-once': semantic('assure', CAPABILITY_IDS.LOOKUP),
@@ -86,6 +89,8 @@ export const CLI_COMMAND_SEMANTICS = Object.freeze({
86
89
  'change-packet': semantic('respond', CAPABILITY_IDS.PORTABLE_EVIDENCE),
87
90
  'sharing-review': semantic('respond', CAPABILITY_IDS.PORTABLE_EVIDENCE),
88
91
  'workflow-plan': semantic('assure', CAPABILITY_IDS.OFFLINE_REVIEW),
92
+ 'watchlist-review': semantic('assure', CAPABILITY_IDS.OFFLINE_REVIEW),
93
+ 'domain-feed': semantic('investigate', CAPABILITY_IDS.OFFLINE_REVIEW),
89
94
  'workflow-run': semantic('assure', CAPABILITY_IDS.WORKFLOW_EXECUTION),
90
95
  diff: semantic('assure', CAPABILITY_IDS.OFFLINE_REVIEW),
91
96
  reconcile: semantic('assure', CAPABILITY_IDS.OFFLINE_REVIEW),
@@ -0,0 +1,6 @@
1
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS } from "./investigation-package-limits.mjs";
2
+ export const MAX_CLI_ARGUMENTS = MAX_INVESTIGATION_MANIFEST_ARTIFACTS + 32;
3
+ export const MAX_CLI_ARGUMENT_LENGTH = 1024;
4
+ export function hasUnsafeCliText(value) {
5
+ return /[\x00-\x1f\x7f-\x9f]|\p{Default_Ignorable_Code_Point}/u.test(value);
6
+ }
@@ -0,0 +1,2 @@
1
+ export const CLI_LOOKUP_BRIEF_SCHEMA = 'whoisleuth.cli.lookup-brief';
2
+ export const CLI_LOOKUP_BRIEF_VERSION = 3;
@@ -0,0 +1,25 @@
1
+ /** Common presentation of bounded, analyst-selected contextual evidence. */
2
+ export const CONTEXT_REVIEW_SCHEMA = 'whoisleuth.context-review';
3
+ export const CONTEXT_REVIEW_VERSION = 1;
4
+ export const DOMAIN_HISTORY_INPUT_SCHEMA = 'whoisleuth.domain-history.input';
5
+ export const DOMAIN_HISTORY_INPUT_VERSION = 2;
6
+ export const DOMAIN_HISTORY_LEGACY_INPUT_VERSION = 1;
7
+ export const PLATFORM_CONTINUITY_INPUT_SCHEMA = 'whoisleuth.platform-continuity.input';
8
+ export const PLATFORM_CONTINUITY_INPUT_VERSION = 1;
9
+ export const STOREFRONT_INPUT_SCHEMA = 'whoisleuth.storefront-review.input';
10
+ export const STOREFRONT_INPUT_VERSION = 1;
11
+ export const CONNECTOR_INPUT_SCHEMA = 'whoisleuth.connector-review.input';
12
+ export const CONNECTOR_INPUT_VERSION = 1;
13
+ export const INCIDENT_SEQUENCE_INPUT_SCHEMA = 'whoisleuth.incident-sequence.input';
14
+ export const INCIDENT_SEQUENCE_INPUT_VERSION = 1;
15
+ export const CONTEXT_INPUT_CONTRACTS = [
16
+ { schema: DOMAIN_HISTORY_INPUT_SCHEMA, version: DOMAIN_HISTORY_INPUT_VERSION },
17
+ { schema: PLATFORM_CONTINUITY_INPUT_SCHEMA, version: PLATFORM_CONTINUITY_INPUT_VERSION },
18
+ { schema: STOREFRONT_INPUT_SCHEMA, version: STOREFRONT_INPUT_VERSION },
19
+ { schema: CONNECTOR_INPUT_SCHEMA, version: CONNECTOR_INPUT_VERSION },
20
+ { schema: INCIDENT_SEQUENCE_INPUT_SCHEMA, version: INCIDENT_SEQUENCE_INPUT_VERSION },
21
+ ];
22
+ export const CONTEXT_INPUT_SCHEMAS = CONTEXT_INPUT_CONTRACTS.map(input => input.schema);
23
+ export const MAX_CONTEXT_INPUT_BYTES = 16 * 1024 * 1024;
24
+ export const MAX_CONTEXT_RECORDS = 200;
25
+ export const CONTEXT_REVIEW_KINDS = ['domain_history', 'platform_continuity', 'storefront', 'connector', 'incident_sequence'];
@@ -0,0 +1,11 @@
1
+ /** Input admission, decoded XML, page work and pixels are independently bounded. */
2
+ export const MAX_DOCUMENT_PARTS = 512;
3
+ export const MAX_DOCUMENT_PAGES = 256;
4
+ export const MAX_DOCUMENT_DECODED_BYTES = 32 * 1024 * 1024;
5
+ export const MAX_DOCUMENT_TEXT_BYTES = 8 * 1024 * 1024;
6
+ export const MAX_DOCUMENT_XML_NODES = 200_000;
7
+ export const MAX_DOCUMENT_PAGE_OPERATIONS = 200_000;
8
+ export const MAX_DOCUMENT_XML_DEPTH = 64;
9
+ export const MAX_DOCUMENT_IMAGE_PIXELS = 16 * 1024 * 1024;
10
+ export const MAX_DOCUMENT_TOTAL_IMAGE_PIXELS = 64 * 1024 * 1024;
11
+ export const DOCUMENT_REVIEW_DEADLINE_MS = 60_000;
@@ -5,8 +5,8 @@ import { MAX_DOMAIN_CONTROL_MONITOR_CONCURRENCY, MAX_DOMAIN_CONTROL_MONITOR_DOMA
5
5
  export const DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA = 'whoisleuth.domain-control-flight-recorder.input';
6
6
  export const DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA = 'whoisleuth.domain-control-flight-recorder';
7
7
  export const PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION = 1;
8
- export const DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION = 2;
9
- export const SUPPORTED_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSIONS = [PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION];
8
+ export const DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION = 3;
9
+ export const SUPPORTED_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSIONS = [PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION, 2, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION];
10
10
  export const MIN_FLIGHT_RECORDER_OBSERVATIONS = 1;
11
11
  export const MAX_FLIGHT_RECORDER_OBSERVATIONS = 200;
12
12
  export const MAX_FLIGHT_RECORDER_WINDOWS = 40;
@@ -95,11 +95,14 @@ export const PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS = Object.freeze([
95
95
  'approvedWindow',
96
96
  'explanation',
97
97
  ]);
98
- export const DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS = Object.freeze([...PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS, 'capturedAt']);
99
- export const DOMAIN_CONTROL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS = Object.freeze([
98
+ export const SOURCE_TIMED_FLIGHT_RECORDER_EVENT_KEYS = Object.freeze([...PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS, 'capturedAt']);
99
+ export const DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS = Object.freeze([...SOURCE_TIMED_FLIGHT_RECORDER_EVENT_KEYS, 'collectionDepth', 'changeInterval']);
100
+ export const FLIGHT_RECORDER_CHANGE_INTERVAL_KEYS = Object.freeze(['after', 'by', 'incompleteObservations']);
101
+ export const HISTORICAL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS = Object.freeze([
100
102
  'id',
101
103
  'reason',
102
104
  ]);
105
+ export const DOMAIN_CONTROL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS = Object.freeze([...HISTORICAL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS, 'startsAt', 'endsAt']);
103
106
  export const PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS = Object.freeze([
104
107
  'firstObservations',
105
108
  'observedChanges',
@@ -115,10 +118,14 @@ export const PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS = Object.freeze([
115
118
  'Approved windows label expected timing but do not delete evidence or establish that a change was authorised, successful, safe, or complete.',
116
119
  'Observed changes can reflect publication lag or different collection conditions and require analyst review.',
117
120
  ]);
118
- export const DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS = Object.freeze([
121
+ export const SOURCE_TIMED_FLIGHT_RECORDER_LIMITATIONS = Object.freeze([
119
122
  ...PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS,
120
123
  'Capture time and source observation time remain separate. Unknown, conflicting or non-increasing source times cannot establish an observed change.',
121
124
  ]);
125
+ export const DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS = Object.freeze([
126
+ ...SOURCE_TIMED_FLIGHT_RECORDER_LIMITATIONS,
127
+ 'A change interval is bounded by comparable complete source observations, not an exact change time. Incomplete observations inside it remain explicit. An approved window must contain the whole interval.',
128
+ ]);
122
129
  export const DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_COMPATIBILITY = defineSchemaCompatibility({
123
130
  id: 'cli.domain-control-flight-recorder-input',
124
131
  kind: 'cli_document',
@@ -224,14 +231,22 @@ export const DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA_LIFECYCLE = defineSchemaLifec
224
231
  scope: 'repository',
225
232
  },
226
233
  ].flatMap((fixture) => {
227
- const version = DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION;
228
- const id = fixture.id.replace(/v\d+$/u, `v${version}`);
229
- const content = fixture.role === 'input'
230
- ? { bytes: 2108, sha256: '6998ea864355dd0802b3b6f2c4905f8c84136ad0d4ac9cad1a0fd63dca523b60' }
231
- : { bytes: 4649, sha256: '8f3168d101291166f34cede351bc176a934e7997920cd8c22ec00d642ec000df' };
232
234
  return [
233
235
  { ...fixture, role: fixture.role === 'input' ? 'input' : 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
234
- { ...fixture, ...content, id, path: `test/fixtures/${id}.json`, version, expectedOutputFixtureId: fixture.expectedOutputFixtureId?.replace(/v\d+$/u, `v${version}`) ?? null },
236
+ ...[2, DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION].map(version => {
237
+ const id = fixture.id.replace(/v\d+$/u, `v${version}`);
238
+ const content = version === 2
239
+ ? fixture.role === 'input'
240
+ ? { bytes: 2108, sha256: '6998ea864355dd0802b3b6f2c4905f8c84136ad0d4ac9cad1a0fd63dca523b60' }
241
+ : { bytes: 4649, sha256: '8f3168d101291166f34cede351bc176a934e7997920cd8c22ec00d642ec000df' }
242
+ : fixture.role === 'input'
243
+ ? { bytes: 2108, sha256: '3d9b91dfd34754482047cde64d6343676ec4747267677fcbcc989a65fbd9a6c6' }
244
+ : { bytes: 5597, sha256: 'ab6a82cfecfb8238a72fbe2dcd4a68a1fd888990e5070eff9d98a337796624d6' };
245
+ return { ...fixture, ...content, id, path: `test/fixtures/${id}.json`, version,
246
+ role: fixture.role === 'input' ? 'input' : version === DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION ? 'current' : 'historical',
247
+ expectation: version === DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION ? fixture.expectation : 'accepted_exact',
248
+ expectedOutputFixtureId: version === DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION ? fixture.expectedOutputFixtureId?.replace(/v\d+$/u, `v${version}`) ?? null : null };
249
+ }),
235
250
  ];
236
251
  }),
237
252
  metadata: {
@@ -257,11 +272,12 @@ export const DOMAIN_CONTROL_FLIGHT_RECORDER_SCHEMA_LIFECYCLE = defineSchemaLifec
257
272
  versions: [version],
258
273
  objects: [
259
274
  { path: '$', requiredKeys: DOMAIN_CONTROL_FLIGHT_RECORDER_ROOT_KEYS, optionalKeys: [], unknownKeys: 'reject' },
260
- { path: '$.events[]', requiredKeys: version === PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION ? PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS : DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS, optionalKeys: [], unknownKeys: 'reject' },
261
- { path: '$.events[].approvedWindow', requiredKeys: DOMAIN_CONTROL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS, optionalKeys: [], unknownKeys: 'reject' },
275
+ { path: '$.events[]', requiredKeys: version === 1 ? PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS : version === 2 ? SOURCE_TIMED_FLIGHT_RECORDER_EVENT_KEYS : DOMAIN_CONTROL_FLIGHT_RECORDER_EVENT_KEYS, optionalKeys: [], unknownKeys: 'reject' },
276
+ { path: '$.events[].approvedWindow', requiredKeys: version >= 3 ? DOMAIN_CONTROL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS : HISTORICAL_FLIGHT_RECORDER_APPROVED_WINDOW_KEYS, optionalKeys: [], unknownKeys: 'reject' },
277
+ ...(version >= 3 ? [{ path: '$.events[].changeInterval', requiredKeys: FLIGHT_RECORDER_CHANGE_INTERVAL_KEYS, optionalKeys: [], unknownKeys: 'reject' }] : []),
262
278
  { path: '$.summary', requiredKeys: version === PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION ? PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS : DOMAIN_CONTROL_FLIGHT_RECORDER_SUMMARY_KEYS, optionalKeys: [], unknownKeys: 'reject' },
263
279
  ],
264
- fixedArrays: [{ path: '$.limitations', values: version === PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_VERSION ? PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS : DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS }],
280
+ fixedArrays: [{ path: '$.limitations', values: version === 1 ? PUBLIC_DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS : version === 2 ? SOURCE_TIMED_FLIGHT_RECORDER_LIMITATIONS : DOMAIN_CONTROL_FLIGHT_RECORDER_LIMITATIONS }],
265
281
  normalisation: 'preserve_document',
266
282
  target: null,
267
283
  },
@@ -410,7 +410,7 @@ export const DOMAIN_CONTROL_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamily({
410
410
  bounds: [
411
411
  { id: 'exported-entries', path: '$.selectedDomains', phase: 'action', unit: 'entries', minimum: 1, maximum: MAX_DOMAIN_CONTROL_BROWSER_PROFILE_ENTRIES, handling: 'cap_operation' },
412
412
  { id: 'previewed-entries', path: '$.verifiedManifest.entries', phase: 'action', unit: 'entries', minimum: 1, maximum: MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES, handling: 'reject' },
413
- { id: 'applied-baselines', path: '$.choices', phase: 'action', unit: 'entries', minimum: 0, maximum: MAX_DOMAIN_CONTROL_BROWSER_PROFILE_ENTRIES, handling: 'cap_operation' },
413
+ { id: 'applied-baselines', path: '$.choices', phase: 'action', unit: 'entries', minimum: 0, maximum: MAX_DOMAIN_CONTROL_BROWSER_PROFILE_ENTRIES, handling: 'reject' },
414
414
  ],
415
415
  },
416
416
  {
@@ -2,8 +2,13 @@ import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
2
2
  import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
3
3
  export const CLI_DOMAIN_CONTROL_MONITOR_SCHEMA = 'whoisleuth.cli.domain-control-monitor';
4
4
  export const PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION = 1;
5
- export const CLI_DOMAIN_CONTROL_MONITOR_VERSION = 2;
6
- export const SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, CLI_DOMAIN_CONTROL_MONITOR_VERSION];
5
+ export const CLI_DOMAIN_CONTROL_MONITOR_VERSION = 3;
6
+ export const SUPPORTED_CLI_DOMAIN_CONTROL_MONITOR_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_MONITOR_VERSION, 2, CLI_DOMAIN_CONTROL_MONITOR_VERSION];
7
+ export const DOMAIN_CONTROL_MONITOR_EMBEDDED_VERSIONS = Object.freeze({
8
+ 1: Object.freeze({ review: 1, flightRecorder: 1 }),
9
+ 2: Object.freeze({ review: 2, flightRecorder: 2 }),
10
+ 3: Object.freeze({ review: 3, flightRecorder: 3 }),
11
+ });
7
12
  export const MAX_DOMAIN_CONTROL_MONITOR_INPUT_BYTES = 16 * 1024 * 1024;
8
13
  export const MAX_DOMAIN_CONTROL_MONITOR_JSON_DEPTH = 48;
9
14
  export const MAX_DOMAIN_CONTROL_MONITOR_JSON_KEYS = 50_000;
@@ -100,6 +105,12 @@ export const DOMAIN_CONTROL_MONITOR_SCHEMA_LIFECYCLE = defineSchemaLifecycleFami
100
105
  {
101
106
  id: 'domain-control-monitor-v2', path: 'test/fixtures/domain-control-monitor-v2.json',
102
107
  bytes: 18_023, sha256: 'f4017f3dc84b3adb6b6114ea49c37ff05939779daabbbb56c868010145cac55c',
108
+ contentDigestSha256: null, schema: CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, version: 2,
109
+ role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null, scope: 'repository',
110
+ },
111
+ {
112
+ id: 'domain-control-monitor-v3', path: 'test/fixtures/domain-control-monitor-v3.json',
113
+ bytes: 19_215, sha256: 'a7233a0b52b282e3ad8097b9bd71bf15125b743e60a421cfec765cb8b9456563',
103
114
  contentDigestSha256: null, schema: CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, version: CLI_DOMAIN_CONTROL_MONITOR_VERSION,
104
115
  role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null, scope: 'repository',
105
116
  },
@@ -10,10 +10,10 @@ export const CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA = 'whoisleuth.cli.domain-con
10
10
  export const CLI_DOMAIN_CONTROL_REVIEW_SCHEMA = 'whoisleuth.cli.domain-control-review';
11
11
  export const PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION = 1;
12
12
  export const PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION = 1;
13
- export const DOMAIN_CONTROL_REVIEW_VERSION = 2;
14
- export const CLI_DOMAIN_CONTROL_REVIEW_VERSION = 2;
15
- export const SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION, DOMAIN_CONTROL_REVIEW_VERSION];
16
- export const SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, CLI_DOMAIN_CONTROL_REVIEW_VERSION];
13
+ export const DOMAIN_CONTROL_REVIEW_VERSION = 3;
14
+ export const CLI_DOMAIN_CONTROL_REVIEW_VERSION = 3;
15
+ export const SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION, 2, DOMAIN_CONTROL_REVIEW_VERSION];
16
+ export const SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS = [PUBLIC_CLI_DOMAIN_CONTROL_REVIEW_VERSION, 2, CLI_DOMAIN_CONTROL_REVIEW_VERSION];
17
17
  export const DOMAIN_CONTROL_REVIEW_OBSERVATION_FACTOR = 2;
18
18
  export const MAX_DOMAIN_CONTROL_REVIEW_OBSERVATIONS = MAX_DOMAIN_CONTROL_MANIFEST_ENTRIES
19
19
  * DOMAIN_CONTROL_REVIEW_OBSERVATION_FACTOR;
@@ -87,7 +87,8 @@ export const PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS = Object.freeze([
87
87
  'not_configured',
88
88
  'due',
89
89
  ]);
90
- export const DOMAIN_CONTROL_REVIEW_COUNT_KEYS = Object.freeze([...PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS, 'observed']);
90
+ export const DOMAIN_CONTROL_REVIEW_V2_COUNT_KEYS = Object.freeze([...PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS, 'observed']);
91
+ export const DOMAIN_CONTROL_REVIEW_COUNT_KEYS = Object.freeze([...DOMAIN_CONTROL_REVIEW_V2_COUNT_KEYS, 'not_due']);
91
92
  export const DOMAIN_CONTROL_REVIEW_DOMAIN_KEYS = Object.freeze([
92
93
  'domain',
93
94
  'state',
@@ -108,11 +109,21 @@ export const PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS = Object.freeze([
108
109
  'Only complete observations can produce drift. Partial, unavailable, unsupported, or missing evidence remains inconclusive.',
109
110
  'A valid manifest digest or signature establishes file integrity, not the correctness of the desired state or supplied observations.',
110
111
  ]);
111
- export const DOMAIN_CONTROL_REVIEW_LIMITATIONS = Object.freeze([
112
+ export const DOMAIN_CONTROL_REVIEW_V2_LIMITATIONS = Object.freeze([
112
113
  PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS[0],
113
114
  'Only complete observations with valid source times within the current review-age window can establish alignment, drift or expected absence. Observation-only fields do not establish alignment.',
114
115
  PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS[2],
115
116
  ]);
117
+ export const DOMAIN_CONTROL_REVIEW_LIMITATIONS = Object.freeze([
118
+ ...DOMAIN_CONTROL_REVIEW_V2_LIMITATIONS,
119
+ 'Renewal dates are analyst-authored reminders, not observations of configuration.',
120
+ ]);
121
+ export function domainControlReviewCountKeys(version) {
122
+ return version === 1 ? PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS : version === 2 ? DOMAIN_CONTROL_REVIEW_V2_COUNT_KEYS : DOMAIN_CONTROL_REVIEW_COUNT_KEYS;
123
+ }
124
+ export function domainControlReviewLimitations(version) {
125
+ return version === 1 ? PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS : version === 2 ? DOMAIN_CONTROL_REVIEW_V2_LIMITATIONS : DOMAIN_CONTROL_REVIEW_LIMITATIONS;
126
+ }
116
127
  export const CLI_DOMAIN_CONTROL_REVIEW_INPUT_KEYS = Object.freeze([
117
128
  'schema',
118
129
  'version',
@@ -233,11 +244,11 @@ const CORE_REVIEW_SHAPES = SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS.flatMap((ver
233
244
  objects: [
234
245
  { path: '$', requiredKeys: DOMAIN_CONTROL_REVIEW_ROOT_KEYS, optionalKeys: [], unknownKeys: 'reject' },
235
246
  { path: '$.manifest', requiredKeys: DOMAIN_CONTROL_REVIEW_MANIFEST_SUMMARY_KEYS, optionalKeys: [], unknownKeys: 'reject' },
236
- { path: '$.counts', requiredKeys: version === PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION ? PUBLIC_DOMAIN_CONTROL_REVIEW_COUNT_KEYS : DOMAIN_CONTROL_REVIEW_COUNT_KEYS, optionalKeys: [], unknownKeys: 'reject' },
247
+ { path: '$.counts', requiredKeys: domainControlReviewCountKeys(version), optionalKeys: [], unknownKeys: 'reject' },
237
248
  { path: '$.domains[]', requiredKeys: DOMAIN_CONTROL_REVIEW_DOMAIN_KEYS, optionalKeys: [], unknownKeys: 'reject' },
238
249
  { path: '$.domains[].comparisons[]', requiredKeys: version === PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION ? PUBLIC_DOMAIN_CONTROL_REVIEW_COMPARISON_KEYS : DOMAIN_CONTROL_REVIEW_COMPARISON_KEYS, optionalKeys: [], unknownKeys: 'reject' },
239
250
  ],
240
- fixedArrays: [{ path: '$.limitations', values: version === PUBLIC_DOMAIN_CONTROL_REVIEW_VERSION ? PUBLIC_DOMAIN_CONTROL_REVIEW_LIMITATIONS : DOMAIN_CONTROL_REVIEW_LIMITATIONS }],
251
+ fixedArrays: [{ path: '$.limitations', values: domainControlReviewLimitations(version) }],
241
252
  normalisation: 'preserve_document',
242
253
  target: null,
243
254
  },
@@ -253,6 +264,10 @@ function reviewFixtureShapeId(schema, version) {
253
264
  return `domain-control-review.${cli ? 'cli-' : ''}${schema.endsWith('-input') ? 'input' : 'document'}.v${version}`;
254
265
  }
255
266
  const CURRENT_REVIEW_FIXTURE_CONTENT = {
267
+ "domain-control-review-input-v3": { "bytes": 1879, "sha256": "747cccb9a0610dd5b77ebd2a01f44dc7bd6a929b09b6e52037596427d0c2275b" },
268
+ "domain-control-review-v3": { "bytes": 4496, "sha256": "736926cf402ca868ddc832c229b88d3c91b44a4c73563c356633a69c1ec8d06c" },
269
+ "cli-domain-control-review-input-v3": { "bytes": 4123, "sha256": "0283efe6f78551a8aa5a9449a608fb4f3b46b39a9f11f6ffe93244979b16d30b" },
270
+ "cli-domain-control-review-v3": { "bytes": 9194, "sha256": "31c263f17cd663be74ab9801970a8800894da4023842137e41f602fde633452e" },
256
271
  "domain-control-review-input-v2": {
257
272
  "bytes": 1879,
258
273
  "sha256": "ac3b5913a44c47bd179cd69e4a8bf4268f10e9f01dd7f59c7d722a8c0d241ad1"
@@ -408,13 +423,19 @@ export const DOMAIN_CONTROL_REVIEW_SCHEMA_LIFECYCLE = defineSchemaLifecycleFamil
408
423
  scope: 'repository',
409
424
  },
410
425
  ].flatMap((fixture) => {
411
- const version = fixture.schema.startsWith('whoisleuth.cli.') ? CLI_DOMAIN_CONTROL_REVIEW_VERSION : DOMAIN_CONTROL_REVIEW_VERSION;
412
- const id = fixture.id.replace(/v\d+$/u, `v${version}`);
413
- const content = CURRENT_REVIEW_FIXTURE_CONTENT[id];
414
- return [
415
- { ...fixture, shapeId: reviewFixtureShapeId(fixture.schema, fixture.version), role: fixture.role === 'input' ? 'input' : 'historical', expectation: fixture.schema === CLI_DOMAIN_CONTROL_REVIEW_SCHEMA ? 'historical_output_exact' : 'accepted_exact', expectedOutputFixtureId: null },
416
- { ...fixture, ...content, id, path: `test/fixtures/${id}.json`, version, shapeId: reviewFixtureShapeId(fixture.schema, version), role: fixture.role, expectation: fixture.expectation, expectedOutputFixtureId: fixture.expectedOutputFixtureId?.replace(/v\d+$/u, `v${version}`) ?? null },
417
- ];
426
+ const currentVersion = fixture.schema.startsWith('whoisleuth.cli.') ? CLI_DOMAIN_CONTROL_REVIEW_VERSION : DOMAIN_CONTROL_REVIEW_VERSION;
427
+ const versions = fixture.schema.startsWith('whoisleuth.cli.') ? SUPPORTED_CLI_DOMAIN_CONTROL_REVIEW_VERSIONS : SUPPORTED_DOMAIN_CONTROL_REVIEW_VERSIONS;
428
+ return versions.map((version) => {
429
+ const id = fixture.id.replace(/v\d+$/u, `v${version}`);
430
+ const current = version === currentVersion;
431
+ return {
432
+ ...fixture, ...(version === 1 ? {} : CURRENT_REVIEW_FIXTURE_CONTENT[id]), id,
433
+ path: `test/fixtures/${id}.json`, version, shapeId: reviewFixtureShapeId(fixture.schema, version),
434
+ role: fixture.role === 'input' ? 'input' : current ? 'current' : 'historical',
435
+ expectation: current ? fixture.expectation : fixture.schema === CLI_DOMAIN_CONTROL_REVIEW_SCHEMA ? 'historical_output_exact' : 'accepted_exact',
436
+ expectedOutputFixtureId: current ? fixture.expectedOutputFixtureId?.replace(/v\d+$/u, `v${version}`) ?? null : null,
437
+ };
438
+ });
418
439
  }),
419
440
  metadata: {
420
441
  enforcement: 'declarative_only',
@@ -2,8 +2,8 @@ import { defineSchemaCompatibility } from "./schema-compatibility.mjs";
2
2
  import { defineSchemaLifecycleFamily } from "./schema-lifecycle.mjs";
3
3
  export const EXTERNAL_OBSERVATION_INTERCHANGE_CONTRACT_OWNER = 'packages/contracts/external-observation-interchange.mts';
4
4
  export const EXTERNAL_FINDINGS_SCHEMA = 'whoisleuth.external-findings';
5
- export const EXTERNAL_FINDINGS_VERSION = 4;
6
- export const SUPPORTED_EXTERNAL_FINDINGS_VERSIONS = Object.freeze([EXTERNAL_FINDINGS_VERSION]);
5
+ export const EXTERNAL_FINDINGS_VERSION = 5;
6
+ export const SUPPORTED_EXTERNAL_FINDINGS_VERSIONS = Object.freeze([4, 5]);
7
7
  export const MAX_EXTERNAL_FINDINGS_IMPORT_BYTES = 384 * 1024;
8
8
  export const MAX_EXTERNAL_FINDINGS = 100;
9
9
  export const MAX_EXTERNAL_FINDINGS_PER_DOMAIN = 20;
@@ -17,6 +17,19 @@ export const DNS_OBSERVATION_ROWS_SCHEMA = 'whoisleuth.dns-observation-rows';
17
17
  export const CERTIFICATE_OBSERVATION_ROWS_SCHEMA = 'whoisleuth.certificate-observation-rows';
18
18
  export const SUPPORTED_OBSERVATION_ROWS_VERSION = 1;
19
19
  export const MAX_CONVERSION_INPUT_ROWS = MAX_EXTERNAL_FINDINGS * 4;
20
+ export const INFRASTRUCTURE_OBSERVATION_SCHEMA = 'whoisleuth.infrastructure-observation';
21
+ export const INFRASTRUCTURE_OBSERVATION_VERSION = 1;
22
+ export const MAX_INFRASTRUCTURE_OBSERVATION_BYTES = 128 * 1024;
23
+ export const MAX_INFRASTRUCTURE_HOSTS = 128;
24
+ export const MAX_INFRASTRUCTURE_DNS_ROWS = 512;
25
+ export const MAX_INFRASTRUCTURE_CERTIFICATES = 32;
26
+ export const MAX_INFRASTRUCTURE_ROLES = 128;
27
+ export const MAX_INFRASTRUCTURE_SOURCES = 16;
28
+ export const MAX_INFRASTRUCTURE_LIMITATIONS = 12;
29
+ export const INFRASTRUCTURE_DNS_TYPES = ['A', 'AAAA', 'CNAME', 'MX', 'NS', 'PTR'];
30
+ export const INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA = 'whoisleuth.infrastructure-comparison.input';
31
+ export const INFRASTRUCTURE_COMPARISON_INPUT_VERSION = 1;
32
+ export const MAX_INFRASTRUCTURE_COMPARISON_INPUT_BYTES = 2 * MAX_INFRASTRUCTURE_OBSERVATION_BYTES + 1_024;
20
33
  export const EXTERNAL_OBSERVATION_INTERCHANGE_COMPATIBILITY_FACADES = Object.freeze([
21
34
  ['frontend/src/lib/analysis/external-findings-import.ts', 'packages/interchange/external-findings-import.mts'],
22
35
  ['frontend/src/lib/analysis/external-findings-converters.ts', 'packages/interchange/external-findings-converters.mts'],
@@ -24,9 +37,9 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_COMPATIBILITY_FACADES = Object.fre
24
37
  export const EXTERNAL_FINDINGS_COMPATIBILITY = defineSchemaCompatibility({
25
38
  id: 'export.external-findings', kind: 'export', schema: EXTERNAL_FINDINGS_SCHEMA,
26
39
  currentVersion: EXTERNAL_FINDINGS_VERSION, supportedVersions: SUPPORTED_EXTERNAL_FINDINGS_VERSIONS,
27
- acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'exact_current_only', writeSemantics: 'non_destructive_merge',
40
+ acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'none', writeSemantics: 'non_destructive_merge',
28
41
  byteBudget: MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, owner: EXTERNAL_OBSERVATION_INTERCHANGE_CONTRACT_OWNER,
29
- note: 'Strict local findings import. Version 4 can retain bounded certificate event identity and name-completeness metadata while analyst assertions remain a separate case workflow.',
42
+ note: 'Strict local findings import. Historical version 4 is preserved exactly; version 5 adds an optional exact source-qualified infrastructure snapshot. No collection is initiated.',
30
43
  });
31
44
  export const EXTERNAL_FINDING_ROWS_COMPATIBILITY = defineSchemaCompatibility({
32
45
  id: 'import.external-finding-rows', kind: 'export', schema: EXTERNAL_FINDING_ROWS_SCHEMA,
@@ -62,17 +75,26 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_COMPATIBILITY = Object.freeze([
62
75
  DOMAIN_OBSERVATION_ROWS_COMPATIBILITY,
63
76
  DNS_OBSERVATION_ROWS_COMPATIBILITY,
64
77
  CERTIFICATE_OBSERVATION_ROWS_COMPATIBILITY,
78
+ defineSchemaCompatibility({ id: 'export.infrastructure-observation', kind: 'export', schema: INFRASTRUCTURE_OBSERVATION_SCHEMA, currentVersion: 1, supportedVersions: [1], acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'exact_current_only', writeSemantics: 'read_only', byteBudget: MAX_INFRASTRUCTURE_OBSERVATION_BYTES, owner: EXTERNAL_OBSERVATION_INTERCHANGE_CONTRACT_OWNER, note: 'Exact source-qualified selected infrastructure evidence retained through existing Case pins. Wildcards, DNS outcomes and independent provider roles remain distinct.' }),
79
+ defineSchemaCompatibility({ id: 'cli.infrastructure-comparison-input', kind: 'cli_document', schema: INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA, currentVersion: 1, supportedVersions: [1], acceptsUnversionedLegacy: false, futureVersionBehavior: 'reject', migration: 'read_only', writeSemantics: 'read_only', byteBudget: MAX_INFRASTRUCTURE_COMPARISON_INPUT_BYTES, owner: EXTERNAL_OBSERVATION_INTERCHANGE_CONTRACT_OWNER, note: 'An explicit earlier/later snapshot pair reviewed offline without collection, persistence or current absence claims.' }),
65
80
  ]);
66
81
  const EXTERNAL_INTERCHANGE_FIXTURES = Object.freeze([
67
- { id: 'external-findings-v4', path: 'test/fixtures/external-observation-interchange/external-findings-v4.json', bytes: 634, sha256: 'ceab916e1dcf99704c94e1b0cd82f5a061f138ae6a07ce8335273080c73b7624', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
68
- { id: 'external-finding-rows-v1', path: 'test/fixtures/external-observation-interchange/external-finding-rows-v1.json', bytes: 435, sha256: '09048325d801fdc837134580fd887cacde28744c9f87a43afc388c96117a4659', schema: EXTERNAL_FINDING_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'external-finding-rows-v1-output' },
69
- { id: 'external-finding-rows-v1-output', path: 'test/fixtures/external-observation-interchange/external-finding-rows-v1-output.json', bytes: 581, sha256: '7f84a9f6177d3245acd6fc21275276694fbe9e8cd94922b1e6997a73afa3149a', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
70
- { id: 'domain-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/domain-observation-rows-v1.json', bytes: 390, sha256: '2f95ab21e57e1a6aaab03cea75a4d58fd9e1234ba87f5268a899b23103359474', schema: DOMAIN_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'domain-observation-rows-v1-output' },
71
- { id: 'domain-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/domain-observation-rows-v1-output.json', bytes: 1_027, sha256: 'bba949308e9bb5b58bf5611d6dec23f074979522627631948cffc2f0056ac817', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
72
- { id: 'dns-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/dns-observation-rows-v1.json', bytes: 369, sha256: 'afb1808a8a81339a1801d337259c19c77ed957583b4ae06579b38ea6a8f68ed1', schema: DNS_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'dns-observation-rows-v1-output' },
73
- { id: 'dns-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/dns-observation-rows-v1-output.json', bytes: 988, sha256: '4c9a49b96181d2907eb95319d15975758823994fe029c051486f1cb6869221a0', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
74
- { id: 'certificate-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/certificate-observation-rows-v1.json', bytes: 515, sha256: '68363a5fe074b08a386dbc95a0993a2933ebcf05ba2f96148df632842d1c5869', schema: CERTIFICATE_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'certificate-observation-rows-v1-output' },
75
- { id: 'certificate-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/certificate-observation-rows-v1-output.json', bytes: 1_244, sha256: '6c484601b872f10bfdb7d9519d4b216f14ef277e7a429e4cc131154ad0e07652', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
82
+ { id: 'external-findings-v4', path: 'test/fixtures/external-observation-interchange/external-findings-v4.json', bytes: 634, sha256: 'ceab916e1dcf99704c94e1b0cd82f5a061f138ae6a07ce8335273080c73b7624', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
83
+ { id: 'external-finding-rows-v1', path: 'test/fixtures/external-observation-interchange/external-finding-rows-v1.json', bytes: 435, sha256: '09048325d801fdc837134580fd887cacde28744c9f87a43afc388c96117a4659', schema: EXTERNAL_FINDING_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'external-finding-rows-v1-output-v5' },
84
+ { id: 'external-finding-rows-v1-output', path: 'test/fixtures/external-observation-interchange/external-finding-rows-v1-output.json', bytes: 581, sha256: '7f84a9f6177d3245acd6fc21275276694fbe9e8cd94922b1e6997a73afa3149a', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
85
+ { id: 'domain-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/domain-observation-rows-v1.json', bytes: 390, sha256: '2f95ab21e57e1a6aaab03cea75a4d58fd9e1234ba87f5268a899b23103359474', schema: DOMAIN_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'domain-observation-rows-v1-output-v5' },
86
+ { id: 'domain-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/domain-observation-rows-v1-output.json', bytes: 1_027, sha256: 'bba949308e9bb5b58bf5611d6dec23f074979522627631948cffc2f0056ac817', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
87
+ { id: 'dns-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/dns-observation-rows-v1.json', bytes: 369, sha256: 'afb1808a8a81339a1801d337259c19c77ed957583b4ae06579b38ea6a8f68ed1', schema: DNS_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'dns-observation-rows-v1-output-v5' },
88
+ { id: 'dns-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/dns-observation-rows-v1-output.json', bytes: 988, sha256: '4c9a49b96181d2907eb95319d15975758823994fe029c051486f1cb6869221a0', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
89
+ { id: 'certificate-observation-rows-v1', path: 'test/fixtures/external-observation-interchange/certificate-observation-rows-v1.json', bytes: 515, sha256: '68363a5fe074b08a386dbc95a0993a2933ebcf05ba2f96148df632842d1c5869', schema: CERTIFICATE_OBSERVATION_ROWS_SCHEMA, version: 1, role: 'input', expectation: 'normalises_to_current_output', expectedOutputFixtureId: 'certificate-observation-rows-v1-output-v5' },
90
+ { id: 'certificate-observation-rows-v1-output', path: 'test/fixtures/external-observation-interchange/certificate-observation-rows-v1-output.json', bytes: 1_244, sha256: '6c484601b872f10bfdb7d9519d4b216f14ef277e7a429e4cc131154ad0e07652', schema: EXTERNAL_FINDINGS_SCHEMA, version: 4, role: 'historical', expectation: 'accepted_exact', expectedOutputFixtureId: null },
91
+ { id: 'external-finding-rows-v1-output-v5', path: 'test/fixtures/external-observation-interchange/external-finding-rows-v1-output-v5.json', bytes: 442, sha256: '9996be2b9d832fca9aa523a1a424148ef762483ab3923d35ca73e83cd16a8195', schema: EXTERNAL_FINDINGS_SCHEMA, version: 5, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
92
+ { id: 'domain-observation-rows-v1-output-v5', path: 'test/fixtures/external-observation-interchange/domain-observation-rows-v1-output-v5.json', bytes: 771, sha256: 'd375353576b5990e835d53c14bf7fa5adb5205191d3969478a69ec68b5e1291b', schema: EXTERNAL_FINDINGS_SCHEMA, version: 5, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
93
+ { id: 'dns-observation-rows-v1-output-v5', path: 'test/fixtures/external-observation-interchange/dns-observation-rows-v1-output-v5.json', bytes: 732, sha256: '532ff6ac9e9255f14105eb0a0286434fadb94f6d7397f0fb96007ebb6b7d1522', schema: EXTERNAL_FINDINGS_SCHEMA, version: 5, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
94
+ { id: 'certificate-observation-rows-v1-output-v5', path: 'test/fixtures/external-observation-interchange/certificate-observation-rows-v1-output-v5.json', bytes: 988, sha256: '09904df103f83f64616556e653fa14c6f2f1dc392b1f75a85fd3099680840e37', schema: EXTERNAL_FINDINGS_SCHEMA, version: 5, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
95
+ { id: 'external-findings-v5', path: 'test/fixtures/external-observation-interchange/external-findings-v5.json', bytes: 3871, sha256: 'debd062c2d951d22c06c29bb076529fa69a2769b8bae7269ce3884550fb55ba3', schema: EXTERNAL_FINDINGS_SCHEMA, version: 5, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
96
+ { id: 'infrastructure-comparison-input-v1', path: 'test/fixtures/infrastructure-observations/infrastructure-comparison-input-v1.json', bytes: 6284, sha256: 'ef0022faf29586774bd1b2de1f95eb3839d382e424c7b3d446e4dc1058437104', schema: INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA, version: 1, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
97
+ { id: 'infrastructure-observation-v1', path: 'test/fixtures/infrastructure-observations/infrastructure-observation-v1.json', bytes: 3430, sha256: '1b76e43e5d5c889654faf12114217994c5cbd3a378685e7f9b9d18ebdde801f7', schema: INFRASTRUCTURE_OBSERVATION_SCHEMA, version: 1, role: 'current', expectation: 'accepted_exact', expectedOutputFixtureId: null },
76
98
  ].map((fixture) => Object.freeze({
77
99
  ...fixture,
78
100
  contentDigestSha256: null,
@@ -84,6 +106,7 @@ function externalFixtureIds(schema, version) {
84
106
  .map((fixture) => fixture.id));
85
107
  }
86
108
  const EXTERNAL_INTERCHANGE_CONTRACTS = Object.freeze([
109
+ ...[['export.infrastructure-observation', INFRASTRUCTURE_OBSERVATION_SCHEMA, true, MAX_INFRASTRUCTURE_OBSERVATION_BYTES], ['cli.infrastructure-comparison-input', INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA, false, MAX_INFRASTRUCTURE_COMPARISON_INPUT_BYTES]].map(([compatibilityId, schema, emitted, byteBudget]) => Object.freeze({ compatibilityId, schema, version: 1, role: 'document', lifecycle: 'current', readable: true, emitted, exactKeys: true, extensionPolicy: 'reject', futureVersionBehaviour: 'reject', migrationTarget: null, canonicalisation: null, byteBudget, fixtureIds: externalFixtureIds(schema, 1) })),
87
110
  ...SUPPORTED_EXTERNAL_FINDINGS_VERSIONS.map((version) => Object.freeze({
88
111
  compatibilityId: EXTERNAL_FINDINGS_COMPATIBILITY.id,
89
112
  schema: EXTERNAL_FINDINGS_SCHEMA,
@@ -95,7 +118,7 @@ const EXTERNAL_INTERCHANGE_CONTRACTS = Object.freeze([
95
118
  exactKeys: true,
96
119
  extensionPolicy: 'reject',
97
120
  futureVersionBehaviour: 'reject',
98
- migrationTarget: version === EXTERNAL_FINDINGS_VERSION ? null : { schema: EXTERNAL_FINDINGS_SCHEMA, version: EXTERNAL_FINDINGS_VERSION },
121
+ migrationTarget: null,
99
122
  canonicalisation: null,
100
123
  byteBudget: MAX_EXTERNAL_FINDINGS_IMPORT_BYTES,
101
124
  fixtureIds: externalFixtureIds(EXTERNAL_FINDINGS_SCHEMA, version),
@@ -151,18 +174,38 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLif
151
174
  metadata: {
152
175
  enforcement: 'declarative_only',
153
176
  shapes: [
154
- externalShape('external-interchange.findings.v4', EXTERNAL_FINDINGS_SCHEMA, [...SUPPORTED_EXTERNAL_FINDINGS_VERSIONS], ['schema', 'schemaVersion', 'source', 'findings'], 'preserve_document'),
177
+ externalShape('external-interchange.findings.v4', EXTERNAL_FINDINGS_SCHEMA, [4], ['schema', 'schemaVersion', 'source', 'findings'], 'preserve_document'),
178
+ externalShape('external-interchange.findings.v5', EXTERNAL_FINDINGS_SCHEMA, [5], ['schema', 'schemaVersion', 'source', 'findings'], 'preserve_document'),
179
+ externalShape('external-interchange.infrastructure.v1', INFRASTRUCTURE_OBSERVATION_SCHEMA, [1], ['schema', 'version', 'id', 'target', 'observedAt', 'mode', 'scope', 'coverage', 'sources', 'dns', 'certificates', 'roles', 'limitations'], 'preserve_document'),
180
+ externalShape('external-interchange.infrastructure-comparison.v1', INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA, [1], ['schema', 'version', 'earlier', 'later'], 'preserve_document'),
155
181
  externalShape('external-interchange.finding-rows.v1', EXTERNAL_FINDING_ROWS_SCHEMA, [1], ['schema', 'schemaVersion', 'source', 'rows'], 'input_to_current'),
156
182
  externalShape('external-interchange.domain-rows.v1', DOMAIN_OBSERVATION_ROWS_SCHEMA, [1], ['schema', 'schemaVersion', 'source', 'observations'], 'input_to_current'),
157
183
  externalShape('external-interchange.dns-rows.v1', DNS_OBSERVATION_ROWS_SCHEMA, [1], ['schema', 'schemaVersion', 'source', 'observations'], 'input_to_current'),
158
184
  externalShape('external-interchange.certificate-rows.v1', CERTIFICATE_OBSERVATION_ROWS_SCHEMA, [1], ['schema', 'schemaVersion', 'source', 'observations'], 'input_to_current'),
159
185
  ],
160
- boundProfiles: [{ id: 'external-interchange.document.bounds', bounds: [
186
+ boundProfiles: [{ id: 'external-interchange.infrastructure.bounds', bounds: [
187
+ { id: 'raw-bytes', path: '$', phase: 'raw_intake', unit: 'bytes', minimum: 1, maximum: MAX_INFRASTRUCTURE_OBSERVATION_BYTES, handling: 'reject' },
188
+ { id: 'serialised-bytes', path: '$', phase: 'serialised', unit: 'bytes', minimum: 1, maximum: MAX_INFRASTRUCTURE_OBSERVATION_BYTES, handling: 'reject' },
189
+ { id: 'hostnames', path: 'scope.hostnames', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: MAX_INFRASTRUCTURE_HOSTS, handling: 'reject' },
190
+ { id: 'dns-rows', path: 'dns', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: MAX_INFRASTRUCTURE_DNS_ROWS, handling: 'reject' },
191
+ { id: 'certificate-rows', path: 'certificates', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: MAX_INFRASTRUCTURE_CERTIFICATES, handling: 'reject' },
192
+ { id: 'role-rows', path: 'roles', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: MAX_INFRASTRUCTURE_ROLES, handling: 'reject' },
193
+ { id: 'sources', path: 'sources', phase: 'pre_accumulation', unit: 'items', minimum: 1, maximum: MAX_INFRASTRUCTURE_SOURCES, handling: 'reject' },
194
+ { id: 'dns-types', path: 'scope.dnsTypes', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: INFRASTRUCTURE_DNS_TYPES.length, handling: 'reject' },
195
+ { id: 'limitations', path: 'limitations', phase: 'pre_accumulation', unit: 'items', minimum: 0, maximum: MAX_INFRASTRUCTURE_LIMITATIONS, handling: 'reject' },
196
+ ] }, { id: 'external-interchange.infrastructure-comparison.bounds', bounds: [
197
+ { id: 'raw-bytes', path: '$', phase: 'raw_intake', unit: 'bytes', minimum: 1, maximum: MAX_INFRASTRUCTURE_COMPARISON_INPUT_BYTES, handling: 'reject' },
198
+ { id: 'serialised-bytes', path: '$', phase: 'serialised', unit: 'bytes', minimum: 1, maximum: MAX_INFRASTRUCTURE_COMPARISON_INPUT_BYTES, handling: 'reject' },
199
+ ] }, { id: 'external-interchange.document.bounds', bounds: [
161
200
  { id: 'raw-bytes', path: '$', phase: 'raw_intake', unit: 'bytes', minimum: 1, maximum: MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, handling: 'reject' },
162
201
  { id: 'serialised-bytes', path: '$', phase: 'serialised', unit: 'bytes', minimum: 1, maximum: MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, handling: 'reject' },
163
202
  { id: 'rows', path: '$', phase: 'pre_accumulation', unit: 'items', minimum: 1, maximum: MAX_CONVERSION_INPUT_ROWS, handling: 'reject' },
164
203
  ] }],
165
204
  hooks: [
205
+ { id: 'external-interchange.infrastructure.read', role: 'normaliser', runtime: 'shared', module: 'packages/investigation/infrastructure-observation.mts', exportName: 'readInfrastructureObservation' },
206
+ { id: 'external-interchange.infrastructure.serialise', role: 'serialiser', runtime: 'shared', module: 'packages/investigation/infrastructure-observation.mts', exportName: 'serialiseInfrastructureObservation' },
207
+ { id: 'external-interchange.infrastructure.convert', role: 'normaliser', runtime: 'shared', module: 'packages/interchange/external-findings-converters.mts', exportName: 'convertInfrastructureObservation' },
208
+ { id: 'external-interchange.infrastructure.compare', role: 'normaliser', runtime: 'cli', module: 'cli/offline-evidence-review.mts', exportName: 'buildOfflineEvidenceReview' },
166
209
  { id: 'external-interchange.findings.normalise', role: 'normaliser', runtime: 'shared', module: 'packages/interchange/external-findings-import.mts', exportName: 'parseExternalFindingsDocument' },
167
210
  { id: 'external-interchange.findings.serialise', role: 'serialiser', runtime: 'shared', module: 'packages/interchange/external-findings-import.mts', exportName: 'serializeExternalFindingsDocument' },
168
211
  { id: 'external-interchange.finding-rows.convert', role: 'normaliser', runtime: 'shared', module: 'packages/interchange/external-findings-converters.mts', exportName: 'convertExternalFindingRows' },
@@ -170,10 +213,15 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLif
170
213
  { id: 'external-interchange.findings.merge', role: 'merger', runtime: 'shared', module: 'packages/interchange/external-findings-import.mts', exportName: 'mergeExternalFindingsIntoCases' },
171
214
  ],
172
215
  serialisationProfiles: [{
173
- id: 'external-interchange.findings.json.v4', schema: EXTERNAL_FINDINGS_SCHEMA,
174
- versions: [...SUPPORTED_EXTERNAL_FINDINGS_VERSIONS], mediaType: 'application/json', encoding: 'utf-8', bom: false,
175
- indentSpaces: 2, terminalLf: true, propertyOrder: 'normalised_fixed', canonicalisation: null, integrity: 'none',
216
+ id: 'external-interchange.findings.json.v5', schema: EXTERNAL_FINDINGS_SCHEMA,
217
+ versions: [5], mediaType: 'application/json', encoding: 'utf-8', bom: false,
218
+ indentSpaces: 0, terminalLf: true, propertyOrder: 'normalised_fixed', canonicalisation: null, integrity: 'none',
176
219
  serializerHookId: 'external-interchange.findings.serialise', verifierHookIds: [],
220
+ }, {
221
+ id: 'external-interchange.infrastructure.json.v1', schema: INFRASTRUCTURE_OBSERVATION_SCHEMA,
222
+ versions: [1], mediaType: 'application/json', encoding: 'utf-8', bom: false,
223
+ indentSpaces: 0, terminalLf: true, propertyOrder: 'normalised_fixed', canonicalisation: null, integrity: 'none',
224
+ serializerHookId: 'external-interchange.infrastructure.serialise', verifierHookIds: [],
177
225
  }],
178
226
  privacyProfiles: [
179
227
  { id: 'external-interchange.privacy.transient', classification: 'analyst_authored_sensitive', projection: 'browser_import', includedCategories: ['source-attribution', 'bounded-observations', 'completeness', 'limitations'], excludedCategories: ['raw-upstream-payloads', 'expanded-contacts', 'credentials', 'cookies', 'query-bearing-urls'], notePolicy: 'discarded', retention: 'transient_report', network: 'none', sharingReview: 'required' },
@@ -181,22 +229,42 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLif
181
229
  ],
182
230
  expiryProfiles: [{ id: 'external-interchange.expiry.not-applicable', field: null, anchor: null, handling: 'not_applicable', phase: 'not_applicable', maximumLifetimeDays: null }],
183
231
  consumerEdges: [
232
+ {
233
+ id: 'external-interchange.consumer.infrastructure-read', plane: 'shared', operation: 'read-and-export-exact-infrastructure-snapshot',
234
+ acceptedContracts: [{ schema: INFRASTRUCTURE_OBSERVATION_SCHEMA, versions: [1], mode: 'direct' }], emittedContract: { schema: INFRASTRUCTURE_OBSERVATION_SCHEMA, version: 1 },
235
+ shapeIds: ['external-interchange.infrastructure.v1'], boundProfileIds: ['external-interchange.infrastructure.bounds'], hookIds: ['external-interchange.infrastructure.read', 'external-interchange.infrastructure.serialise'], serialisationProfileId: 'external-interchange.infrastructure.json.v1', privacyProfileId: 'external-interchange.privacy.transient', retentionEffect: 'transient_report', ...externalConsumerCommon,
236
+ },
237
+ {
238
+ id: 'external-interchange.consumer.infrastructure-convert', plane: 'shared', operation: 'convert-exact-snapshot-to-one-finding',
239
+ acceptedContracts: [{ schema: INFRASTRUCTURE_OBSERVATION_SCHEMA, versions: [1], mode: 'direct' }], emittedContract: { schema: EXTERNAL_FINDINGS_SCHEMA, version: 5 },
240
+ shapeIds: ['external-interchange.infrastructure.v1', 'external-interchange.findings.v5'], boundProfileIds: ['external-interchange.infrastructure.bounds', 'external-interchange.document.bounds'], hookIds: ['external-interchange.infrastructure.convert', 'external-interchange.findings.serialise'], serialisationProfileId: 'external-interchange.findings.json.v5', privacyProfileId: 'external-interchange.privacy.transient', retentionEffect: 'transient_report', ...externalConsumerCommon,
241
+ },
242
+ {
243
+ id: 'external-interchange.consumer.infrastructure-compare', plane: 'cli', operation: 'offline-infrastructure-comparison',
244
+ acceptedContracts: [{ schema: INFRASTRUCTURE_COMPARISON_INPUT_SCHEMA, versions: [1], mode: 'direct' }], emittedContract: null,
245
+ shapeIds: ['external-interchange.infrastructure-comparison.v1'], boundProfileIds: ['external-interchange.infrastructure-comparison.bounds'], hookIds: ['external-interchange.infrastructure.compare'], serialisationProfileId: null, privacyProfileId: 'external-interchange.privacy.transient', retentionEffect: 'transient_report', ...externalConsumerCommon,
246
+ },
247
+ {
248
+ id: 'external-interchange.consumer.findings-v4-read', plane: 'shared', operation: 'preserve-historical-findings',
249
+ acceptedContracts: [{ schema: EXTERNAL_FINDINGS_SCHEMA, versions: [4], mode: 'direct' }], emittedContract: null,
250
+ shapeIds: ['external-interchange.findings.v4'], boundProfileIds: ['external-interchange.document.bounds'], hookIds: ['external-interchange.findings.normalise'], serialisationProfileId: null, privacyProfileId: 'external-interchange.privacy.transient', retentionEffect: 'transient_report', ...externalConsumerCommon,
251
+ },
184
252
  {
185
253
  id: 'external-interchange.consumer.findings-normalise', plane: 'shared', operation: 'normalise-strict-findings',
186
- acceptedContracts: [{ schema: EXTERNAL_FINDINGS_SCHEMA, versions: [...SUPPORTED_EXTERNAL_FINDINGS_VERSIONS], mode: 'direct' }],
254
+ acceptedContracts: [{ schema: EXTERNAL_FINDINGS_SCHEMA, versions: [5], mode: 'direct' }],
187
255
  emittedContract: { schema: EXTERNAL_FINDINGS_SCHEMA, version: EXTERNAL_FINDINGS_VERSION },
188
- shapeIds: ['external-interchange.findings.v4'], boundProfileIds: ['external-interchange.document.bounds'],
256
+ shapeIds: ['external-interchange.findings.v5'], boundProfileIds: ['external-interchange.document.bounds'],
189
257
  hookIds: ['external-interchange.findings.normalise', 'external-interchange.findings.serialise'],
190
- serialisationProfileId: 'external-interchange.findings.json.v4', privacyProfileId: 'external-interchange.privacy.transient',
258
+ serialisationProfileId: 'external-interchange.findings.json.v5', privacyProfileId: 'external-interchange.privacy.transient',
191
259
  retentionEffect: 'transient_report', ...externalConsumerCommon,
192
260
  },
193
261
  {
194
262
  id: 'external-interchange.consumer.finding-rows', plane: 'shared', operation: 'convert-finding-rows',
195
263
  acceptedContracts: [{ schema: EXTERNAL_FINDING_ROWS_SCHEMA, versions: [1], mode: 'direct' }],
196
264
  emittedContract: { schema: EXTERNAL_FINDINGS_SCHEMA, version: EXTERNAL_FINDINGS_VERSION },
197
- shapeIds: ['external-interchange.finding-rows.v1', 'external-interchange.findings.v4'], boundProfileIds: ['external-interchange.document.bounds'],
265
+ shapeIds: ['external-interchange.finding-rows.v1', 'external-interchange.findings.v5'], boundProfileIds: ['external-interchange.document.bounds'],
198
266
  hookIds: ['external-interchange.finding-rows.convert', 'external-interchange.findings.serialise'],
199
- serialisationProfileId: 'external-interchange.findings.json.v4', privacyProfileId: 'external-interchange.privacy.transient',
267
+ serialisationProfileId: 'external-interchange.findings.json.v5', privacyProfileId: 'external-interchange.privacy.transient',
200
268
  retentionEffect: 'transient_report', ...externalConsumerCommon,
201
269
  },
202
270
  {
@@ -207,15 +275,15 @@ export const EXTERNAL_OBSERVATION_INTERCHANGE_LIFECYCLE_FAMILY = defineSchemaLif
207
275
  { schema: CERTIFICATE_OBSERVATION_ROWS_SCHEMA, versions: [1], mode: 'direct' },
208
276
  ],
209
277
  emittedContract: { schema: EXTERNAL_FINDINGS_SCHEMA, version: EXTERNAL_FINDINGS_VERSION },
210
- shapeIds: ['external-interchange.domain-rows.v1', 'external-interchange.dns-rows.v1', 'external-interchange.certificate-rows.v1', 'external-interchange.findings.v4'],
278
+ shapeIds: ['external-interchange.domain-rows.v1', 'external-interchange.dns-rows.v1', 'external-interchange.certificate-rows.v1', 'external-interchange.findings.v5'],
211
279
  boundProfileIds: ['external-interchange.document.bounds'], hookIds: ['external-interchange.observation-rows.convert', 'external-interchange.findings.serialise'],
212
- serialisationProfileId: 'external-interchange.findings.json.v4', privacyProfileId: 'external-interchange.privacy.transient',
280
+ serialisationProfileId: 'external-interchange.findings.json.v5', privacyProfileId: 'external-interchange.privacy.transient',
213
281
  retentionEffect: 'transient_report', ...externalConsumerCommon,
214
282
  },
215
283
  {
216
284
  id: 'external-interchange.consumer.case-merge', plane: 'shared', operation: 'non-destructive-case-merge',
217
- acceptedContracts: [{ schema: EXTERNAL_FINDINGS_SCHEMA, versions: [EXTERNAL_FINDINGS_VERSION], mode: 'direct' }], emittedContract: null,
218
- shapeIds: ['external-interchange.findings.v4'], boundProfileIds: ['external-interchange.document.bounds'],
285
+ acceptedContracts: [{ schema: EXTERNAL_FINDINGS_SCHEMA, versions: [...SUPPORTED_EXTERNAL_FINDINGS_VERSIONS], mode: 'direct' }], emittedContract: null,
286
+ shapeIds: ['external-interchange.findings.v4', 'external-interchange.findings.v5'], boundProfileIds: ['external-interchange.document.bounds'],
219
287
  hookIds: ['external-interchange.findings.merge'], serialisationProfileId: null,
220
288
  privacyProfileId: 'external-interchange.privacy.case-merge', retentionEffect: 'browser_indexeddb', ...externalConsumerCommon,
221
289
  },
@@ -0,0 +1,2 @@
1
+ export const MAX_HAR_ENTRIES = 10_000;
2
+ export const HAR_TIMING_PHASES = ['blocked', 'dns', 'connect', 'ssl', 'send', 'wait', 'receive'];
@@ -0,0 +1,4 @@
1
+ /** Selected account telemetry, never a provider connection or an identity verdict. */
2
+ export const IDENTITY_EVENTS_INPUT_SCHEMA = 'whoisleuth.identity-events.input';
3
+ export const IDENTITY_EVENTS_INPUT_VERSION = 1;
4
+ export const MAX_IDENTITY_EVENTS = 10_000;
@@ -1,5 +1,7 @@
1
1
  export const CLI_INVESTIGATION_RUN_SCHEMA = 'whoisleuth.cli.investigation-run';
2
2
  export const CLI_INVESTIGATION_RUN_VERSION = 3;
3
+ export const CLI_INVESTIGATION_PREVIEW_SCHEMA = 'whoisleuth.cli.investigation-preview';
4
+ export const CLI_INVESTIGATION_PREVIEW_VERSION = 1;
3
5
  export const SUPPORTED_CLI_INVESTIGATION_RUN_VERSIONS = Object.freeze([1, 2, 3]);
4
6
  export const MAX_INVESTIGATION_RUN_BYTES = 24 * 1024 * 1024;
5
7
  export const MAX_INVESTIGATION_RUN_SELECTIONS = 16;
@@ -1,3 +1,13 @@
1
+ /** Syntactic identity only: private and reserved addresses remain valid evidence. */
2
+ export function canonicalIpAddress(value) {
3
+ const candidate = typeof value === 'string' ? value.trim() : value;
4
+ const parsed = addressValue(candidate);
5
+ if (!parsed)
6
+ return null;
7
+ if (parsed.family === 4)
8
+ return [24, 16, 8, 0].map(shift => (parsed.value >>> shift) & 255).join('.');
9
+ return new URL(`https://[${candidate}]/`).hostname.slice(1, -1);
10
+ }
1
11
  export function ipv6Groups(value) {
2
12
  const address = addressValue(value);
3
13
  if (address?.family !== 6)