@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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 (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -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/arguments.mjs +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +112 -72
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,8 +1,8 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { canonicalArtifactJsonV2 } from "../../packages/evidence/artifact-integrity.mjs";
3
- import { CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
3
+ import { CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
4
4
  import { CASE_ACTION_EVENT_SOURCE_CLASSES, CASE_ACTION_STATES, CASE_ACTION_TYPES, CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, CASE_PROVIDER_OUTCOMES, isLegalCaseActionTransition, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RECIPIENT_LENGTH, } from "../../packages/cases/case-response-model.mjs";
5
- import { HEX_DIGEST_RE, array, boolean, domain, enumeration, exact, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
5
+ import { HEX_DIGEST_RE, array, boolean, domain, enumeration, exact, exactOptional, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
6
6
  const CASE_RESPONSE_PREFLIGHT_IDS_V6 = [
7
7
  'required_incident_fields',
8
8
  'evidence_pins',
@@ -21,6 +21,9 @@ const CASE_RESPONSE_PREFLIGHT_IDS_V8 = [
21
21
  'action_tracking',
22
22
  ];
23
23
  const PUBLIC_CASE_ACTION_STATES = ['planned', 'ready_for_review', 'submitted', 'acknowledged', 'resolved', 'closed'];
24
+ const PRE_PLATFORM_PROFILE_IDS = RESPONSE_PACKET_PROFILE_IDS.filter((id) => id !== 'application_platform');
25
+ const PRE_PLATFORM_CONTACT_KINDS = RESPONSE_CONTACT_KINDS.filter((kind) => kind !== 'application_platform');
26
+ const PRE_PLATFORM_ACTION_TYPES = CASE_ACTION_TYPES.filter((type) => type !== 'platform_report');
24
27
  function legacyProfileRedactions(profileId) {
25
28
  if (profileId === 'registrar' || profileId === 'registry') {
26
29
  return ['Remove unnecessary personal registration data', 'Remove credentials, tokens, and URL fragments'];
@@ -68,7 +71,7 @@ function validateCaseResponsePacketV6(value) {
68
71
  if (root.reviewRequired !== true || root.submissionPerformed !== false)
69
72
  fail('Case-response packet review state');
70
73
  const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
71
- const profileId = enumeration(profile.id, RESPONSE_PACKET_PROFILE_IDS, 'Case-response profile id');
74
+ const profileId = enumeration(profile.id, PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
72
75
  text(profile.label, 'Case-response profile label', 200);
73
76
  text(profile.audience, 'Case-response profile audience', 300);
74
77
  text(profile.subject, 'Case-response profile subject', 500);
@@ -124,7 +127,7 @@ function validateCaseResponsePacketV6(value) {
124
127
  const contactKeys = new Set();
125
128
  for (const candidate of contacts) {
126
129
  const contact = exact(candidate, ['kind', 'contact', 'source', 'limitations'], 'Case-response contact');
127
- const kind = enumeration(contact.kind, RESPONSE_CONTACT_KINDS, 'Case-response contact kind');
130
+ const kind = enumeration(contact.kind, PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
128
131
  const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
129
132
  const contactKey = `${kind}\u0000${contactValue.toLowerCase()}`;
130
133
  if (contactKeys.has(contactKey))
@@ -160,7 +163,7 @@ function validateCaseResponsePacketV6(value) {
160
163
  const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
161
164
  for (const candidate of history) {
162
165
  const action = exact(candidate, ['type', 'recipient', 'contactSource', 'state', 'reference', 'outcome', 'createdAt', 'updatedAt'], 'Case-response escalation action');
163
- enumeration(action.type, CASE_ACTION_TYPES, 'Case-response action type');
166
+ enumeration(action.type, PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
164
167
  text(action.recipient, 'Case-response action recipient', 320, true);
165
168
  text(action.contactSource, 'Case-response action source', 120, true);
166
169
  enumeration(action.state, PUBLIC_CASE_ACTION_STATES, 'Case-response action state');
@@ -221,14 +224,15 @@ function validateCurrentActionSummary(value, label) {
221
224
  }
222
225
  return total;
223
226
  }
224
- function validateCaseResponsePacketV7OrV8(value, version) {
225
- const current = version === CASE_RESPONSE_PACKET_VERSION;
227
+ function validateVersionedCaseResponsePacket(value, version) {
228
+ const hasPlatformRoutes = version >= PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION;
229
+ const hasActionBinding = version >= PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION;
226
230
  const root = exact(value, [
227
231
  'schema', 'schemaVersion', 'generatedAt', 'reviewRequired', 'submissionPerformed', 'profile', 'case',
228
232
  'incident', 'contacts', 'selectedEvidence', 'contradictions', 'readiness', 'artefactReferences',
229
233
  'authorisation', 'preflight', 'escalationHistory', 'escalationHistoryOmitted',
230
234
  'escalationHistoryLimitations', 'responseLifecycle', 'provenance', 'integrity',
231
- ...(current ? ['recipientRoute', 'actionBinding'] : []),
235
+ ...(hasActionBinding ? ['recipientRoute', 'actionBinding'] : []),
232
236
  ], `Case-response packet v${version}`);
233
237
  if (root.schemaVersion !== version)
234
238
  fail(`Case-response packet v${version}`);
@@ -236,14 +240,14 @@ function validateCaseResponsePacketV7OrV8(value, version) {
236
240
  if (root.reviewRequired !== true || root.submissionPerformed !== false)
237
241
  fail('Case-response packet review state');
238
242
  const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
239
- const profileId = enumeration(profile.id, RESPONSE_PACKET_PROFILE_IDS, 'Case-response profile id');
243
+ const profileId = enumeration(profile.id, hasPlatformRoutes ? RESPONSE_PACKET_PROFILE_IDS : PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
240
244
  text(profile.label, 'Case-response profile label', 200);
241
245
  text(profile.audience, 'Case-response profile audience', 300);
242
246
  text(profile.subject, 'Case-response profile subject', 500);
243
247
  for (const key of ['checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'])
244
248
  strings(profile[key], `Case-response profile ${key}`, 24, 500);
245
249
  const selectedProfile = RESPONSE_PACKET_PROFILES.find((candidate) => candidate.id === profileId);
246
- const expectedRedactions = current ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
250
+ const expectedRedactions = hasActionBinding ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
247
251
  if (!selectedProfile
248
252
  || profile.label !== selectedProfile.label
249
253
  || profile.audience !== selectedProfile.audience
@@ -282,8 +286,10 @@ function validateCaseResponsePacketV7OrV8(value, version) {
282
286
  const contacts = array(root.contacts, 'Case-response contacts', MAX_RESPONSE_CONTACTS);
283
287
  const contactKeys = new Set();
284
288
  for (const candidate of contacts) {
285
- const contact = exact(candidate, ['kind', 'contact', 'source', 'observedAt', 'freshness', 'limitations'], 'Case-response contact');
286
- const kind = enumeration(contact.kind, RESPONSE_CONTACT_KINDS, 'Case-response contact kind');
289
+ const contact = exact(candidate, [
290
+ 'kind', 'contact', 'source', 'observedAt', ...(hasPlatformRoutes ? ['reviewAfter'] : []), 'freshness', 'limitations',
291
+ ], 'Case-response contact');
292
+ const kind = enumeration(contact.kind, hasPlatformRoutes ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
287
293
  const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
288
294
  const key = `${kind}\u0000${contactValue.toLowerCase()}`;
289
295
  if (contactKeys.has(key))
@@ -291,13 +297,18 @@ function validateCaseResponsePacketV7OrV8(value, version) {
291
297
  contactKeys.add(key);
292
298
  text(contact.source, 'Case-response contact source', 120);
293
299
  iso(contact.observedAt, 'Case-response contact observedAt', true);
300
+ if (hasPlatformRoutes)
301
+ iso(contact.reviewAfter, 'Case-response contact reviewAfter', true);
294
302
  const freshness = enumeration(contact.freshness, ['current', 'stale', 'unknown'], 'Case-response contact freshness');
295
303
  const routeAge = contact.observedAt === null
296
304
  ? null
297
305
  : Date.parse(root.generatedAt) - Date.parse(contact.observedAt);
306
+ const reviewExpired = hasPlatformRoutes && contact.reviewAfter !== null
307
+ && Date.parse(root.generatedAt) >= Date.parse(contact.reviewAfter);
298
308
  const expectedFreshness = routeAge === null
299
309
  ? 'unknown'
300
- : routeAge < -300_000 || routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000
310
+ : routeAge < -300_000 || reviewExpired
311
+ || (contact.reviewAfter == null && routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000)
301
312
  ? 'stale'
302
313
  : 'current';
303
314
  if (freshness !== expectedFreshness)
@@ -308,24 +319,29 @@ function validateCaseResponsePacketV7OrV8(value, version) {
308
319
  let selectedActionId = null;
309
320
  let lineageActionIds = [];
310
321
  let lineageComplete = true;
311
- if (current) {
322
+ if (hasActionBinding) {
312
323
  if (root.recipientRoute !== null) {
313
324
  recipientRoute = exact(root.recipientRoute, [
314
- 'actionId', 'kind', 'contact', 'source', 'observedAt', 'freshness', 'limitations',
325
+ 'actionId', 'kind', 'contact', 'source', 'observedAt', ...(hasPlatformRoutes ? ['reviewAfter'] : []), 'freshness', 'limitations',
315
326
  ], 'Case-response recipient route');
316
327
  text(recipientRoute.actionId, 'Case-response recipient action id', 64);
317
- enumeration(recipientRoute.kind, [...RESPONSE_CONTACT_KINDS, 'manual'], 'Case-response recipient kind');
328
+ enumeration(recipientRoute.kind, [...(hasPlatformRoutes ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS), 'manual'], 'Case-response recipient kind');
318
329
  text(recipientRoute.contact, 'Case-response recipient value', MAX_RESPONSE_RECIPIENT_LENGTH);
319
330
  text(recipientRoute.source, 'Case-response recipient source', 120);
320
331
  iso(recipientRoute.observedAt, 'Case-response recipient observedAt', true);
332
+ if (hasPlatformRoutes)
333
+ iso(recipientRoute.reviewAfter, 'Case-response recipient reviewAfter', true);
321
334
  const freshness = enumeration(recipientRoute.freshness, ['current', 'stale', 'unknown'], 'Case-response recipient freshness');
322
335
  const routeAge = recipientRoute.observedAt === null
323
336
  ? null
324
337
  : Date.parse(root.generatedAt) - Date.parse(recipientRoute.observedAt);
338
+ const reviewExpired = hasPlatformRoutes && recipientRoute.reviewAfter !== null
339
+ && Date.parse(root.generatedAt) >= Date.parse(recipientRoute.reviewAfter);
325
340
  const expectedFreshness = routeAge === null
326
341
  ? 'unknown'
327
342
  : routeAge < -MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS
328
- || routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000 ? 'stale' : 'current';
343
+ || reviewExpired
344
+ || (recipientRoute.reviewAfter == null && routeAge > RESPONSE_ROUTE_STALE_AFTER_DAYS * 86_400_000) ? 'stale' : 'current';
329
345
  if (freshness !== expectedFreshness)
330
346
  fail('Case-response recipient freshness');
331
347
  strings(recipientRoute.limitations, 'Case-response recipient limitations', MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_LIMITATION_LENGTH);
@@ -354,6 +370,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
354
370
  || selectedContact.contact !== recipientRoute.contact
355
371
  || selectedContact.source !== recipientRoute.source
356
372
  || selectedContact.observedAt !== recipientRoute.observedAt
373
+ || (hasPlatformRoutes && selectedContact.reviewAfter !== recipientRoute.reviewAfter)
357
374
  || selectedContact.freshness !== recipientRoute.freshness)
358
375
  fail('Case-response selected contact projection');
359
376
  }
@@ -361,14 +378,16 @@ function validateCaseResponsePacketV7OrV8(value, version) {
361
378
  const evidence = array(root.selectedEvidence, 'Case-response selected evidence', MAX_RESPONSE_SELECTED_EVIDENCE);
362
379
  const evidenceIds = new Set();
363
380
  for (const candidate of evidence) {
364
- const item = exact(candidate, ['id', 'label', 'source', 'observedAt', 'completeness', 'limitations'], 'Case-response selected evidence');
381
+ const item = exactOptional(candidate, ['id', 'label', 'source', 'observedAt', 'completeness', 'limitations'], version > PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION ? ['observationHostname'] : [], 'Case-response selected evidence');
365
382
  const id = text(item.id, 'Case-response evidence id', 64);
366
383
  if (evidenceIds.has(id))
367
384
  fail('Case-response selected evidence');
368
385
  evidenceIds.add(id);
369
386
  text(item.label, 'Case-response evidence label', 80);
370
387
  text(item.source, 'Case-response evidence source', 120);
371
- iso(item.observedAt, 'Case-response evidence observedAt');
388
+ if (item.observationHostname !== undefined)
389
+ domain(item.observationHostname, 'Case-response evidence hostname');
390
+ iso(item.observedAt, 'Case-response evidence observedAt', version > PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION);
372
391
  enumeration(item.completeness, ['complete', 'partial', 'inconclusive', 'unknown'], 'Case-response evidence completeness');
373
392
  strings(item.limitations, 'Case-response evidence limitations', 8, 240);
374
393
  }
@@ -395,7 +414,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
395
414
  ...(profileId === 'internal_soc' ? [] : ['recipient_route']),
396
415
  ...(['registrar', 'registry', 'network_hosting'].includes(profileId) ? ['infrastructure_responsibility'] : []),
397
416
  ]);
398
- const requiredContact = current
417
+ const requiredContact = hasActionBinding
399
418
  ? recipientRoute ?? undefined
400
419
  : selectedProfile?.requiredContactKind
401
420
  ? contacts.find((candidate) => candidate.kind === selectedProfile.requiredContactKind)
@@ -404,7 +423,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
404
423
  observed_behaviour: 'complete',
405
424
  exact_url: 'complete',
406
425
  observation_time: expectedObservationAge(incident.observedAt, root.generatedAt).refreshRecommended ? 'stale' : 'complete',
407
- capture_provenance: evidence.length ? 'complete' : 'unavailable',
426
+ capture_provenance: !evidence.length ? 'unavailable' : evidence.every((candidate) => candidate.observedAt !== null) ? 'complete' : 'partial',
408
427
  recipient_route: !requiredContact
409
428
  ? 'not_provided'
410
429
  : requiredContact.freshness === 'stale'
@@ -490,8 +509,8 @@ function validateCaseResponsePacketV7OrV8(value, version) {
490
509
  || (authorisationStatus === 'draft' && authorisation.confirmedAt !== null))
491
510
  fail('Case-response authorisation');
492
511
  const preflight = exact(root.preflight, ['version', 'status', 'canExport', 'counts', 'checks', 'actionSummary'], 'Case-response preflight');
493
- const preflightIds = current ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
494
- if (preflight.version !== (current ? 3 : 2))
512
+ const preflightIds = hasActionBinding ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
513
+ if (preflight.version !== (hasActionBinding ? 3 : 2))
495
514
  fail('Case-response preflight');
496
515
  const checks = array(preflight.checks, 'Case-response preflight checks', preflightIds.length, preflightIds.length);
497
516
  const actualCounts = { block: 0, caution: 0, pass: 0 };
@@ -515,8 +534,8 @@ function validateCaseResponsePacketV7OrV8(value, version) {
515
534
  const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
516
535
  const escalationHistoryOmitted = integer(root.escalationHistoryOmitted, 'Case-response omitted actions', 0, MAX_CASE_ACTIONS);
517
536
  const escalationHistoryLimitations = strings(root.escalationHistoryLimitations, 'Case-response history limitations', 4, 600);
518
- if ((!current && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
519
- || (current && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
537
+ if ((!hasActionBinding && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
538
+ || (hasActionBinding && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
520
539
  || escalationHistoryOmitted !== actionTotal - history.length
521
540
  || (escalationHistoryOmitted > 0) !== (escalationHistoryLimitations.length > 0))
522
541
  fail('Case-response action-history bounds');
@@ -528,18 +547,21 @@ function validateCaseResponsePacketV7OrV8(value, version) {
528
547
  'actionId', 'type', 'recipient', 'contactSource', 'state', 'reference',
529
548
  'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
530
549
  'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
531
- ...(current ? ['routeObservedAt'] : []),
550
+ ...(hasActionBinding ? ['routeObservedAt'] : []),
551
+ ...(hasPlatformRoutes ? ['routeReviewAfter'] : []),
532
552
  ], 'Case-response escalation action');
533
553
  const actionId = text(action.actionId, 'Case-response action id', 64);
534
554
  if (actionIds.has(actionId))
535
555
  fail('Case-response action identity');
536
556
  actionIds.add(actionId);
537
557
  actionIdOrder.push(actionId);
538
- enumeration(action.type, CASE_ACTION_TYPES, 'Case-response action type');
558
+ enumeration(action.type, hasPlatformRoutes ? CASE_ACTION_TYPES : PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
539
559
  text(action.recipient, 'Case-response action recipient', 320);
540
560
  text(action.contactSource, 'Case-response action source', 120);
541
- if (current)
561
+ if (hasActionBinding)
542
562
  iso(action.routeObservedAt, 'Case-response action route observedAt', true);
563
+ if (hasPlatformRoutes)
564
+ iso(action.routeReviewAfter, 'Case-response action route reviewAfter', true);
543
565
  enumeration(action.state, CASE_ACTION_STATES, 'Case-response action state');
544
566
  optionalText(action.reference, 'Case-response action reference', 500);
545
567
  if (action.providerOutcome !== null)
@@ -638,7 +660,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
638
660
  iso(action.createdAt, 'Case-response action createdAt');
639
661
  iso(action.updatedAt, 'Case-response action updatedAt');
640
662
  }
641
- if (current) {
663
+ if (hasActionBinding) {
642
664
  if (actionIds.size !== lineageActionIds.length
643
665
  || actionIdOrder.some((id, index) => lineageActionIds[index] !== id))
644
666
  fail('Case-response action lineage');
@@ -670,11 +692,11 @@ function validateCaseResponsePacketV7OrV8(value, version) {
670
692
  const latestProviderEvents = latestProviderTime === null
671
693
  ? []
672
694
  : providerEvents.filter((event) => event.occurredAt === latestProviderTime);
673
- const expectedLatestProvider = (current ? lineageComplete : escalationHistoryOmitted === 0)
695
+ const expectedLatestProvider = (hasActionBinding ? lineageComplete : escalationHistoryOmitted === 0)
674
696
  && latestProviderEvents.length === 1 && latestProviderEvents[0].applied
675
697
  ? latestProviderEvents[0]
676
698
  : null;
677
- const expectedProviderState = (current ? !lineageComplete : escalationHistoryOmitted > 0)
699
+ const expectedProviderState = (hasActionBinding ? !lineageComplete : escalationHistoryOmitted > 0)
678
700
  ? 'ambiguous'
679
701
  : providerEvents.length === 0 ? 'missing' : expectedLatestProvider ? 'available' : 'ambiguous';
680
702
  if (providerOutcomeState !== expectedProviderState
@@ -707,7 +729,10 @@ function validateCaseResponsePacketV7OrV8(value, version) {
707
729
  strings(lifecycle.limitations, 'Case-response lifecycle limitations', 8, 600);
708
730
  const reviewedInputs = {
709
731
  contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
710
- version: current ? CASE_RESPONSE_REVIEW_INPUTS_VERSION : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION,
732
+ version: version === CASE_RESPONSE_PACKET_VERSION
733
+ ? CASE_RESPONSE_REVIEW_INPUTS_VERSION
734
+ : hasPlatformRoutes ? PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION
735
+ : hasActionBinding ? PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION,
711
736
  profile: {
712
737
  id: profile.id,
713
738
  label: profile.label,
@@ -721,7 +746,7 @@ function validateCaseResponsePacketV7OrV8(value, version) {
721
746
  case: root.case,
722
747
  incident: root.incident,
723
748
  contacts: root.contacts,
724
- ...(current ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
749
+ ...(hasActionBinding ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
725
750
  selectedEvidence: root.selectedEvidence,
726
751
  contradictions: root.contradictions,
727
752
  readiness: root.readiness,
@@ -759,7 +784,13 @@ export function validateCaseResponsePacket(value) {
759
784
  if (value.schemaVersion === PUBLIC_CASE_RESPONSE_PACKET_VERSION)
760
785
  return validateCaseResponsePacketV6(value);
761
786
  if (value.schemaVersion === PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION) {
762
- return validateCaseResponsePacketV7OrV8(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
787
+ return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
788
+ }
789
+ if (value.schemaVersion === PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION) {
790
+ return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION);
791
+ }
792
+ if (value.schemaVersion === PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION) {
793
+ return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION);
763
794
  }
764
795
  if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion < CASE_RESPONSE_PACKET_VERSION) {
765
796
  throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is not part of the public compatibility boundary; no data was changed.`);
@@ -767,5 +798,5 @@ export function validateCaseResponsePacket(value) {
767
798
  if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion > CASE_RESPONSE_PACKET_VERSION) {
768
799
  throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is newer than the supported version ${CASE_RESPONSE_PACKET_VERSION}; no data was changed.`);
769
800
  }
770
- validateCaseResponsePacketV7OrV8(value, CASE_RESPONSE_PACKET_VERSION);
801
+ validateVersionedCaseResponsePacket(value, CASE_RESPONSE_PACKET_VERSION);
771
802
  }
@@ -1,6 +1,7 @@
1
- import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, INVESTIGATION_CAPSULE_VERSION, PUBLIC_INVESTIGATION_CAPSULE_VERSION, } from "../../packages/investigation/investigation-capsule.mjs";
1
+ import { investigationCapsuleContracts, MAX_LOOKUP_ASSET_NODES, MAX_LOOKUP_ASSET_EDGES, MAX_LOOKUP_ASSET_LIMITATIONS, MAX_LOOKUP_ASSET_INPUT_ROWS } from "../../packages/contracts/investigation-portability.mjs";
2
+ import { INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA, INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION, investigationCapsuleProjectionsMatch, } from "../../packages/investigation/investigation-capsule.mjs";
2
3
  import { LOOKUP_ASSET_GRAPH_SCHEMA, LOOKUP_ASSET_GRAPH_VERSION, } from "../../packages/investigation/lookup-asset-graph.mjs";
3
- import { PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION, LOOKUP_INVESTIGATION_BRIEF_SCHEMA, LOOKUP_INVESTIGATION_BRIEF_VERSION, MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES, } from "../../packages/investigation/lookup-investigation-brief.mjs";
4
+ import { PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION, LOOKUP_INVESTIGATION_BRIEF_SCHEMA, MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES, } from "../../packages/investigation/lookup-investigation-brief.mjs";
4
5
  import { DECISION_FACT_CONSISTENCY_STATES, DECISION_FACT_EVIDENCE_STATES, DECISION_FACT_FRESHNESS_STATES, DECISION_FACT_PROVENANCE_STATES, DECISION_FACT_PROJECTION_VERSION, DECISION_FACT_VERSION, MAX_DECISION_FACT_CONTRIBUTORS, MAX_DECISION_FACT_CONTRADICTIONS, MAX_DECISION_FACT_LIMITATIONS, MAX_DECISION_FACT_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_CONTRADICTIONS, MAX_DECISION_FACT_PROJECTION_BYTES, MAX_DECISION_FACT_PROJECTION_FACTS, MAX_DECISION_FACT_PROJECTION_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_NEXT_ACTIONS, MAX_DECISION_FACT_PROJECTION_REFERENCES, MAX_DECISION_FACT_PROJECTION_SOURCES, MAX_DECISION_FACT_PROJECTION_SOURCE_LIMITATIONS, MAX_DECISION_FACT_PROJECTION_SOURCE_REFERENCES, MAX_DECISION_FACT_REFERENCES, MAX_DECISION_FACTS, decisionFactCompleteness, } from "../../packages/evidence/decision-fact.mjs";
5
6
  import { MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_DECISION_PIN_REFERENCES, } from "../../packages/cases/case-response-model.mjs";
6
7
  import { array, boolean, digest, enumeration, exact, exactOptional, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
@@ -221,10 +222,10 @@ function validateDecisionFactProjection(value) {
221
222
  fail('Investigation capsule Decision Fact projection byte limit');
222
223
  }
223
224
  }
224
- function validateCurrentBrief(value) {
225
+ function validateCurrentBrief(value, expectedVersion) {
225
226
  const brief = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'target', 'targetType', 'task', 'taskLabel', 'question', 'summary', 'observation', 'decisionFacts', 'relationships', 'limitations'], 'Investigation capsule brief');
226
227
  if (brief.schema !== LOOKUP_INVESTIGATION_BRIEF_SCHEMA
227
- || brief.schemaVersion !== LOOKUP_INVESTIGATION_BRIEF_VERSION)
228
+ || brief.schemaVersion !== expectedVersion)
228
229
  fail('Investigation capsule brief');
229
230
  iso(brief.generatedAt, 'Investigation capsule brief generatedAt');
230
231
  text(brief.target, 'Investigation capsule brief target', 253);
@@ -250,21 +251,22 @@ function validateCurrentBrief(value) {
250
251
  integer(thresholds[key], 'Investigation capsule freshness threshold', 0, 3650);
251
252
  validateDecisionFactProjection(brief.decisionFacts);
252
253
  const relationships = exact(brief.relationships, ['nodes', 'edges', 'truncated', 'kinds'], 'Investigation capsule relationship summary');
253
- integer(relationships.nodes, 'Investigation capsule relationship node count', 0, 72);
254
- integer(relationships.edges, 'Investigation capsule relationship edge count', 0, 120);
254
+ integer(relationships.nodes, 'Investigation capsule relationship node count', 0, expectedVersion === 2 ? 72 : MAX_LOOKUP_ASSET_NODES);
255
+ integer(relationships.edges, 'Investigation capsule relationship edge count', 0, expectedVersion === 2 ? 120 : MAX_LOOKUP_ASSET_EDGES);
255
256
  boolean(relationships.truncated, 'Investigation capsule relationship truncation');
256
- strings(relationships.kinds, 'Investigation capsule relationship kinds', 12, 320);
257
+ strings(relationships.kinds, 'Investigation capsule relationship kinds', expectedVersion === 2 ? 12 : 64, 320);
257
258
  strings(brief.limitations, 'Investigation capsule brief limitations', 20, 320);
258
259
  if (new TextEncoder().encode(JSON.stringify(brief)).byteLength > MAX_LOOKUP_INVESTIGATION_BRIEF_BYTES) {
259
260
  fail('Investigation capsule brief byte limit');
260
261
  }
261
262
  }
262
- function validateGraph(value) {
263
- const graph = exact(value, ['version', 'targetId', 'nodes', 'edges', 'sources', 'truncated', 'limitations'], 'Investigation capsule graph');
264
- if (graph.version !== LOOKUP_ASSET_GRAPH_VERSION)
263
+ function validateGraph(value, expectedVersion) {
264
+ const current = expectedVersion === LOOKUP_ASSET_GRAPH_VERSION;
265
+ const graph = exact(value, ['version', 'targetId', 'nodes', 'edges', 'sources', 'truncated', 'limitations', ...(current ? ['coverage'] : [])], 'Investigation capsule graph');
266
+ if (graph.version !== expectedVersion)
265
267
  fail('Investigation capsule graph');
266
268
  text(graph.targetId, 'Investigation capsule graph target', 160);
267
- const nodes = array(graph.nodes, 'Investigation capsule graph nodes', 72, 1);
269
+ const nodes = array(graph.nodes, 'Investigation capsule graph nodes', current ? MAX_LOOKUP_ASSET_NODES : 72, 1);
268
270
  const nodeIds = new Set();
269
271
  for (const candidate of nodes) {
270
272
  const node = exact(candidate, ['id', 'label', 'kind', 'detail'], 'Investigation capsule graph node');
@@ -279,7 +281,7 @@ function validateGraph(value) {
279
281
  if (!nodeIds.has(graph.targetId))
280
282
  fail('Investigation capsule graph target');
281
283
  const sourceIds = new Set();
282
- for (const candidate of array(graph.sources, 'Investigation capsule graph sources', 32)) {
284
+ for (const candidate of array(graph.sources, 'Investigation capsule graph sources', current ? 128 : 32)) {
283
285
  const source = exact(candidate, ['id', 'label', 'href', 'observedAt', 'completeness', 'limitations'], 'Investigation capsule graph source');
284
286
  const id = text(source.id, 'Investigation capsule graph source id', 160);
285
287
  if (sourceIds.has(id))
@@ -290,9 +292,9 @@ function validateGraph(value) {
290
292
  fail('Investigation capsule graph source href');
291
293
  iso(source.observedAt, 'Investigation capsule graph source observedAt', true);
292
294
  enumeration(source.completeness, ['complete', 'partial', 'unknown'], 'Investigation capsule graph source completeness');
293
- strings(source.limitations, 'Investigation capsule graph source limitations', 5, 320);
295
+ strings(source.limitations, 'Investigation capsule graph source limitations', current ? MAX_LOOKUP_ASSET_LIMITATIONS : 5, 320);
294
296
  }
295
- const edges = array(graph.edges, 'Investigation capsule graph edges', 120);
297
+ const edges = array(graph.edges, 'Investigation capsule graph edges', current ? MAX_LOOKUP_ASSET_EDGES : 120);
296
298
  const edgeIds = new Set();
297
299
  for (const candidate of edges) {
298
300
  const edge = exactOptional(candidate, ['id', 'sourceId', 'source', 'target', 'kind', 'label', 'sourceLabel', 'observedAt', 'completeness', 'limitations', 'lenses', 'href'], ['boundary'], 'Investigation capsule graph edge');
@@ -310,13 +312,36 @@ function validateGraph(value) {
310
312
  text(edge.sourceLabel, 'Investigation capsule graph edge source label', 320);
311
313
  iso(edge.observedAt, 'Investigation capsule graph edge observedAt', true);
312
314
  enumeration(edge.completeness, ['complete', 'partial', 'unknown'], 'Investigation capsule graph edge completeness');
313
- strings(edge.limitations, 'Investigation capsule graph edge limitations', 5, 320);
315
+ strings(edge.limitations, 'Investigation capsule graph edge limitations', current ? MAX_LOOKUP_ASSET_LIMITATIONS : 5, 320);
314
316
  strings(edge.lenses, 'Investigation capsule graph edge lenses', 4, 40).forEach((lens) => enumeration(lens, ['all', 'identity', 'delegation', 'certificate'], 'Investigation capsule graph edge lens'));
315
317
  if (typeof edge.href !== 'string' || !/^#[^\u0000-\u001f\u007f]{1,160}$/u.test(edge.href))
316
318
  fail('Investigation capsule graph edge href');
317
319
  if (edge.boundary !== undefined)
318
320
  enumeration(edge.boundary, ['external', 'reviewed_profile', 'same_origin', 'same_registrable_domain', 'unresolved'], 'Investigation capsule graph edge boundary');
319
321
  }
322
+ if (current) {
323
+ const coverage = exact(graph.coverage, ['inputs'], 'Investigation capsule graph coverage');
324
+ const ids = new Set();
325
+ let incomplete = false;
326
+ for (const candidate of array(coverage.inputs, 'Investigation capsule graph input coverage', MAX_LOOKUP_ASSET_INPUT_ROWS)) {
327
+ const row = exact(candidate, ['id', 'supplied', 'inspected', 'admitted', 'invalid', 'duplicates', 'omitted'], 'Investigation capsule graph input coverage row');
328
+ const id = text(row.id, 'Investigation capsule graph input id', 160);
329
+ if (ids.has(id))
330
+ fail('Investigation capsule graph input ids');
331
+ ids.add(id);
332
+ const supplied = integer(row.supplied, 'Investigation capsule graph supplied input', 0, 0xffff_ffff);
333
+ const inspected = integer(row.inspected, 'Investigation capsule graph inspected input', 0, 800);
334
+ const admitted = integer(row.admitted, 'Investigation capsule graph admitted input', 0, inspected);
335
+ const invalid = integer(row.invalid, 'Investigation capsule graph invalid input', 0, inspected);
336
+ const duplicates = integer(row.duplicates, 'Investigation capsule graph duplicate input', 0, inspected);
337
+ const omitted = integer(row.omitted, 'Investigation capsule graph omitted input', 0, inspected);
338
+ if (inspected > supplied || admitted + invalid + duplicates + omitted !== inspected)
339
+ fail('Investigation capsule graph input accounting');
340
+ incomplete ||= supplied > inspected || invalid > 0 || omitted > 0;
341
+ }
342
+ if (graph.truncated !== incomplete)
343
+ fail('Investigation capsule graph coverage linkage');
344
+ }
320
345
  boolean(graph.truncated, 'Investigation capsule graph truncation');
321
346
  strings(graph.limitations, 'Investigation capsule graph limitations', 20, 320);
322
347
  }
@@ -349,8 +374,8 @@ function validateAnalystRecords(value) {
349
374
  }
350
375
  export function validateInvestigationCapsuleStructure(value) {
351
376
  const root = exact(value, ['schema', 'schemaVersion', 'generatedAt', 'application', 'target', 'sourceContracts', 'investigationBrief', 'graphSnapshot', 'analystRecords', 'integrity', 'limitations'], 'Investigation capsule');
352
- if (root.schemaVersion !== PUBLIC_INVESTIGATION_CAPSULE_VERSION
353
- && root.schemaVersion !== INVESTIGATION_CAPSULE_VERSION)
377
+ const expected = investigationCapsuleContracts(root.schemaVersion);
378
+ if (!expected)
354
379
  fail('Investigation capsule');
355
380
  iso(root.generatedAt, 'Investigation capsule generatedAt');
356
381
  const application = exact(root.application, ['name', 'version'], 'Investigation capsule application');
@@ -376,9 +401,7 @@ export function validateInvestigationCapsuleStructure(value) {
376
401
  const expectedContractIds = root.analystRecords === null
377
402
  ? ['lookup-evidence', 'investigation-brief', 'asset-graph']
378
403
  : ['lookup-evidence', 'investigation-brief', 'asset-graph', 'analyst-records'];
379
- const expectedBriefVersion = root.schemaVersion === INVESTIGATION_CAPSULE_VERSION
380
- ? LOOKUP_INVESTIGATION_BRIEF_VERSION
381
- : PUBLIC_LOOKUP_INVESTIGATION_BRIEF_VERSION;
404
+ const expectedBriefVersion = expected.brief;
382
405
  if (!sameValues(contracts.map((candidate) => candidate.id), expectedContractIds))
383
406
  fail('Investigation capsule source contracts');
384
407
  if (!byId.has('lookup-evidence') || !byId.has('investigation-brief') || !byId.has('asset-graph')
@@ -387,25 +410,23 @@ export function validateInvestigationCapsuleStructure(value) {
387
410
  || byId.get('investigation-brief')?.version !== expectedBriefVersion
388
411
  || byId.get('investigation-brief')?.embedded !== true
389
412
  || byId.get('asset-graph')?.schema !== LOOKUP_ASSET_GRAPH_SCHEMA
390
- || byId.get('asset-graph')?.version !== LOOKUP_ASSET_GRAPH_VERSION
413
+ || byId.get('asset-graph')?.version !== expected.graph
391
414
  || byId.get('asset-graph')?.embedded !== true
392
415
  || (byId.has('analyst-records') && (byId.get('analyst-records')?.schema !== INVESTIGATION_CAPSULE_ANALYST_RECORDS_SCHEMA
393
416
  || byId.get('analyst-records')?.version !== INVESTIGATION_CAPSULE_ANALYST_RECORDS_VERSION
394
417
  || byId.get('analyst-records')?.embedded !== true)))
395
418
  fail('Investigation capsule source contracts');
396
- if (root.schemaVersion === INVESTIGATION_CAPSULE_VERSION)
397
- validateCurrentBrief(root.investigationBrief);
398
- else
419
+ if (expectedBriefVersion === 1)
399
420
  validatePublicBrief(root.investigationBrief);
400
- validateGraph(root.graphSnapshot);
421
+ else
422
+ validateCurrentBrief(root.investigationBrief, expectedBriefVersion);
423
+ validateGraph(root.graphSnapshot, expected.graph);
401
424
  validateAnalystRecords(root.analystRecords);
402
- const brief = root.investigationBrief;
403
- const graph = root.graphSnapshot;
404
- if (target.value !== brief.target || target.type !== brief.targetType
405
- || graph.targetId === undefined
406
- || brief.relationships.nodes !== graph.nodes.length
407
- || brief.relationships.edges !== graph.edges.length)
425
+ // The version-specific validators above independently establish these common fields.
426
+ const projections = root;
427
+ if (!investigationCapsuleProjectionsMatch(projections.target, projections.investigationBrief, projections.graphSnapshot)) {
408
428
  fail('Investigation capsule projection linkage');
429
+ }
409
430
  const integrity = exact(root.integrity, ['algorithm', 'canonicalization', 'scope', 'briefDigest', 'graphDigest', 'analystRecordsDigest', 'digestSha256'], 'Investigation capsule integrity');
410
431
  if (integrity.algorithm !== 'SHA-256')
411
432
  fail('Investigation capsule integrity');
@@ -1,5 +1,6 @@
1
1
  import { isDeepStrictEqual } from 'node:util';
2
- import { assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, projectLookupEvidenceAvailability, projectLookupEvidenceAvailabilityPublic, projectLookupEvidenceQuery, projectLookupEvidenceRdapPublication, projectLookupEvidenceRegistryInsights, projectLookupEvidenceWhoisPublication, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../../lib/evidence-export.mjs";
2
+ import { assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION, PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION, REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, projectLookupEvidenceAvailability, projectLookupEvidenceAvailabilityPublic, projectLookupEvidenceQuery, projectLookupEvidenceRdapPublication, projectLookupEvidenceRegistryInsights, projectLookupEvidenceWhoisPublication, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../../lib/evidence-export.mjs";
3
+ import { validLookupObservationScope } from "../../packages/evidence/lookup-target.mjs";
3
4
  import { registrarStandingObservedBy, resolveRegistrarIanaId, validRegistrarStanding, } from "../../lib/registrar-standing-contract.mjs";
4
5
  import { compareRegistrySources } from "../../lib/registry-comparison.mjs";
5
6
  import { buildRegistryInsights } from "../../lib/registry-insights.mjs";
@@ -649,24 +650,29 @@ export function validateLookupEvidenceArtifactStructure(value) {
649
650
  }
650
651
  validateLookupEvidenceSecurityTxt(sources.securityTxt);
651
652
  validateLookupEvidenceSslbl(sources.sslbl);
652
- const analysis = exact(root.analysis, version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
653
+ const analysis = exact(root.analysis, version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
653
654
  ? ['availability', 'idn', 'registryInsights', 'registryComparison', 'registrarPublicationComparison', 'registrarStanding']
654
655
  : ['availability', 'idn', 'registryInsights', 'registryComparison', 'registrarPublicationComparison'], 'Lookup evidence analysis');
655
- if (version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
656
+ if (version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
656
657
  && analysis.registrarStanding !== null
657
658
  && !validRegistrarStanding(analysis.registrarStanding))
658
659
  fail('Lookup evidence registrar standing');
659
- if (version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
660
+ if (version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
660
661
  && analysis.registrarStanding !== null
661
662
  && !registrarStandingObservedBy(analysis.registrarStanding, root.generatedAt)) {
662
663
  fail('Lookup evidence registrar standing observation time');
663
664
  }
664
665
  if (analysis.availability !== null) {
665
666
  const currentAvailability = version >= PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION;
666
- const availability = exactOptional(analysis.availability, currentAvailability ? ['registryContactsExcluded'] : [], currentAvailability ? LOOKUP_AVAILABILITY_ANALYSIS_KEYS : PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS, 'Lookup evidence availability analysis');
667
+ const availability = exactOptional(analysis.availability, currentAvailability ? ['registryContactsExcluded'] : [], currentAvailability
668
+ ? [...LOOKUP_AVAILABILITY_ANALYSIS_KEYS, ...(version >= HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION ? ['observationHostname', 'webObservationMode'] : [])]
669
+ : PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS, 'Lookup evidence availability analysis');
667
670
  if (currentAvailability && availability.registryContactsExcluded !== true) {
668
671
  fail('Lookup evidence availability contact exclusion');
669
672
  }
673
+ if (!validLookupObservationScope(availability, query)) {
674
+ fail('Lookup evidence observation hostname binding');
675
+ }
670
676
  validateLookupEvidenceHomepageMetadata(availability, version);
671
677
  if (!isDeepStrictEqual(analysis.availability, currentAvailability
672
678
  ? projectLookupEvidenceAvailability(analysis.availability)
@@ -684,7 +690,7 @@ export function validateLookupEvidenceArtifactStructure(value) {
684
690
  const whois = record(sources.whois, 'Lookup evidence WHOIS source');
685
691
  const rdapParsed = ['success', 'partial'].includes(rdapDiagnosticStatus) ? rdap.parsed : null;
686
692
  const whoisParsed = ['complete', 'partial'].includes(whoisDiagnosticStatus) ? whois.parsed : null;
687
- if (version >= LOOKUP_EVIDENCE_SCHEMA_VERSION
693
+ if (version >= REGISTRAR_STANDING_LOOKUP_EVIDENCE_SCHEMA_VERSION
688
694
  && validRegistrarStanding(analysis.registrarStanding)
689
695
  && analysis.registrarStanding.ianaId !== resolveRegistrarIanaId(rdapParsed, whoisParsed)) {
690
696
  fail('Lookup evidence registrar standing source binding');