@slicedearth/whoisleuth-cli 2.3.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 (264) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +68 -6
  7. package/cli/artifact-validation/case-response.mjs +47 -41
  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 +83 -61
  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-command-runner.mjs +2 -0
  18. package/cli/bulk-output.mjs +29 -3
  19. package/cli/case-command.mjs +195 -0
  20. package/cli/case-pack.mjs +39 -31
  21. package/cli/command-argument-grammar.mjs +4 -0
  22. package/cli/command-reference.mjs +145 -75
  23. package/cli/completion.mjs +298 -79
  24. package/cli/config-profile.mjs +7 -5
  25. package/cli/ct-event-intake.mjs +12 -12
  26. package/cli/discover.mjs +4 -3
  27. package/cli/discovery-observation-snapshot.mjs +44 -16
  28. package/cli/discovery-scan-command-runner.mjs +2 -0
  29. package/cli/discovery-scan.mjs +7 -4
  30. package/cli/discovery-workflow.mjs +1 -1
  31. package/cli/domain-control-monitor.mjs +50 -20
  32. package/cli/domain-control-observations.mjs +149 -102
  33. package/cli/errors.mjs +33 -1
  34. package/cli/evidence-command-runner.mjs +16 -5
  35. package/cli/evidence-signing.mjs +2 -2
  36. package/cli/external-observation-mapping.mjs +7 -8
  37. package/cli/formatters/terminal-lookup.mjs +18 -46
  38. package/cli/formatters/terminal-shared.mjs +5 -3
  39. package/cli/formatters/terminal-technology.mjs +114 -0
  40. package/cli/inline-command-families.mjs +1 -0
  41. package/cli/interactive-launcher.mjs +2 -160
  42. package/cli/interchange-report.mjs +4 -8
  43. package/cli/investigation-artifacts.mjs +176 -0
  44. package/cli/investigation-checkpoint.mjs +10 -0
  45. package/cli/investigation-folder.mjs +114 -0
  46. package/cli/investigation-manifest.mjs +1 -112
  47. package/cli/investigation-package-review.mjs +103 -0
  48. package/cli/investigation-plan.mjs +32 -19
  49. package/cli/investigation-run.mjs +188 -33
  50. package/cli/local-document-checkpoint.mjs +170 -0
  51. package/cli/lookup-browser.mjs +5 -12
  52. package/cli/lookup-command-runner.mjs +17 -6
  53. package/cli/lookup-diff.mjs +37 -3
  54. package/cli/lookup-plan.mjs +11 -5
  55. package/cli/lookup-reconcile.mjs +10 -8
  56. package/cli/mail-review.mjs +66 -14
  57. package/cli/manual.mjs +1 -0
  58. package/cli/network-command-runner.mjs +2 -0
  59. package/cli/output-file.mjs +31 -5
  60. package/cli/registry-doctor.mjs +3 -1
  61. package/cli/registry-support.mjs +1 -1
  62. package/cli/retained-artifact-diff.mjs +2 -2
  63. package/cli/review-command-runner.mjs +45 -0
  64. package/cli/runner.mjs +55 -6
  65. package/cli/sharing-review.mjs +21 -34
  66. package/cli/signer-trust.mjs +104 -0
  67. package/cli/source-reliability.mjs +74 -83
  68. package/cli/terminal-input.mjs +168 -0
  69. package/cli/workflow-command-runner.mjs +68 -13
  70. package/docs/cli-reference.md +76 -30
  71. package/docs/cli.md +289 -25
  72. package/lib/abort.mjs +4 -1
  73. package/lib/availability.mjs +161 -112
  74. package/lib/bounded-file.mjs +3 -1
  75. package/lib/bounded-html-document.mjs +125 -0
  76. package/lib/bounded-json.mjs +43 -3
  77. package/lib/browser-library-profile.mjs +42 -51
  78. package/lib/browser-library-worker.mjs +112 -0
  79. package/lib/client-behavior-profile.mjs +3 -3
  80. package/lib/ct-search.mjs +11 -4
  81. package/lib/dns-delegation-health.mjs +24 -14
  82. package/lib/dns-inheritance-review.mjs +217 -0
  83. package/lib/dns-intelligence.mjs +36 -23
  84. package/lib/dns-mx.mjs +1 -1
  85. package/lib/dnssec-evidence-validation.mjs +11 -3
  86. package/lib/domain-change-packet.mjs +7 -2
  87. package/lib/domain-change-review.mjs +14 -8
  88. package/lib/domain-control-flight-recorder.mjs +219 -118
  89. package/lib/domain-control-manifest.mjs +117 -57
  90. package/lib/domain-posture-analysis.mjs +20 -7
  91. package/lib/domain-posture-parsers.mjs +88 -6
  92. package/lib/domain-posture.mjs +113 -26
  93. package/lib/evidence-export-privacy.mjs +561 -0
  94. package/lib/evidence-export.mjs +11 -592
  95. package/lib/evidence-report.mjs +3 -1
  96. package/lib/favicon.mjs +117 -41
  97. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  98. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  99. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  100. package/lib/hostname.mjs +2 -38
  101. package/lib/html-signals.mjs +82 -85
  102. package/lib/http-evidence-bounds.mjs +2 -1
  103. package/lib/http-intelligence.mjs +39 -26
  104. package/lib/lookup-cache.mjs +34 -11
  105. package/lib/lookup-child-profile-contract.mjs +24 -15
  106. package/lib/lookup-diagnostics.mjs +24 -1
  107. package/lib/lookup-progress.mjs +30 -13
  108. package/lib/lookup-response-contract.mjs +1048 -0
  109. package/lib/lookup-response.mjs +7 -10
  110. package/lib/lookup-source-progress.mjs +17 -14
  111. package/lib/lookup.mjs +48 -27
  112. package/lib/observed-network-context.mjs +43 -27
  113. package/lib/outbound-request-bounds.mjs +4 -1
  114. package/lib/page-fingerprints.mjs +69 -133
  115. package/lib/page-role-profile.mjs +12 -8
  116. package/lib/perceptual-hash.mjs +43 -14
  117. package/lib/rdap-bootstrap.mjs +72 -22
  118. package/lib/rdap-client.mjs +15 -7
  119. package/lib/rdap-normalization.mjs +33 -18
  120. package/lib/rdap-search-workbench.mjs +1 -1
  121. package/lib/rdap-transport.mjs +47 -20
  122. package/lib/rdap-types.mjs +2 -1
  123. package/lib/rdap.mjs +18 -3
  124. package/lib/registrable-domain.mjs +21 -1
  125. package/lib/registrar-standing-contract.mjs +1 -0
  126. package/lib/registry-capabilities.mjs +5 -1
  127. package/lib/registry-capability-catalogue.mjs +2 -1
  128. package/lib/registry-dates.mjs +2 -190
  129. package/lib/registry-insights.mjs +56 -18
  130. package/lib/response-policy.mjs +86 -48
  131. package/lib/risk-calibration-summary.mjs +17 -3
  132. package/lib/rpki-evidence.mjs +8 -5
  133. package/lib/safe-fetch.mjs +54 -47
  134. package/lib/security-txt.mjs +10 -7
  135. package/lib/service-binding-dns.mjs +8 -0
  136. package/lib/smtp-transport-review.mjs +27 -15
  137. package/lib/sslbl-certificates.generated.mjs +136 -135
  138. package/lib/static-html-analysis.mjs +117 -146
  139. package/lib/structured-data-identity.mjs +1 -0
  140. package/lib/technology-markup.mjs +100 -0
  141. package/lib/threat-intelligence-manifests.mjs +11 -139
  142. package/lib/threat-intelligence-runtime.mjs +24 -52
  143. package/lib/threat-intelligence-targets.mjs +6 -34
  144. package/lib/threat-intelligence-types.mjs +39 -0
  145. package/lib/typosquat-generator.mjs +19 -15
  146. package/lib/urlhaus-intelligence.mjs +8 -3
  147. package/lib/urlscan-intelligence.mjs +1 -1
  148. package/lib/website-security-posture.mjs +30 -6
  149. package/lib/website-technology.mjs +111 -71
  150. package/lib/whois-authority.mjs +14 -2
  151. package/lib/whois-chain.mjs +9 -2
  152. package/lib/whois-common-formats.mjs +5 -5
  153. package/lib/whois-contracts.mjs +2 -1
  154. package/lib/whois-dialects.mjs +2 -2
  155. package/lib/whois-parser-support.mjs +8 -1
  156. package/lib/whois-transport.mjs +23 -7
  157. package/package.json +2 -1
  158. package/packages/cases/case-attachment-model.mjs +83 -0
  159. package/packages/cases/case-evidence-model.mjs +156 -62
  160. package/packages/cases/case-export-input.mjs +53 -0
  161. package/packages/cases/case-migration-model.mjs +59 -25
  162. package/packages/cases/case-model.mjs +1 -0
  163. package/packages/cases/case-recheck-model.mjs +91 -0
  164. package/packages/cases/case-record-core.mjs +3 -2
  165. package/packages/cases/case-record-operations.mjs +89 -27
  166. package/packages/cases/case-record-projection.mjs +23 -20
  167. package/packages/cases/case-report.mjs +63 -52
  168. package/packages/cases/case-response-actions.mjs +654 -0
  169. package/packages/cases/case-response-model.mjs +41 -1129
  170. package/packages/cases/case-response-outcomes.mjs +317 -0
  171. package/packages/cases/case-response-packet-render.mjs +153 -0
  172. package/packages/cases/case-response-packet-types.mjs +1 -0
  173. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  174. package/packages/cases/case-response-packet.mjs +38 -587
  175. package/packages/cases/case-response-records.mjs +94 -0
  176. package/packages/cases/case-response-review-inputs.mjs +371 -0
  177. package/packages/cases/case-response-values.mjs +85 -0
  178. package/packages/cases/case-review-package.mjs +45 -0
  179. package/packages/cases/case-selection.mjs +46 -0
  180. package/packages/cases/case-storage-model.mjs +25 -0
  181. package/packages/cases/response-route-freshness.mjs +16 -0
  182. package/packages/comparison/capture-context.mjs +61 -0
  183. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  184. package/packages/comparison/page-similarity.mjs +2 -1
  185. package/packages/comparison/relationship-evidence.mjs +85 -8
  186. package/packages/comparison/relationship-provenance.mjs +93 -0
  187. package/packages/contracts/analyst-interchange.mjs +216 -0
  188. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  189. package/packages/contracts/analyst-review-state.mjs +5 -4
  190. package/packages/contracts/case-portability.mjs +279 -116
  191. package/packages/contracts/case-views-contract.mjs +12 -0
  192. package/packages/contracts/case-views.mjs +26 -0
  193. package/packages/contracts/cli-command-semantics.mjs +1 -0
  194. package/packages/contracts/cli-lookup.mjs +0 -1
  195. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  196. package/packages/contracts/domain-control-manifest.mjs +127 -73
  197. package/packages/contracts/domain-control-monitor.mjs +48 -45
  198. package/packages/contracts/domain-control-review.mjs +179 -121
  199. package/packages/contracts/domain-name.mjs +21 -0
  200. package/packages/contracts/external-observation-interchange.mjs +2 -1
  201. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  202. package/packages/contracts/http-url.mjs +2 -0
  203. package/packages/contracts/investigation-package-limits.mjs +24 -0
  204. package/packages/contracts/investigation-portability.mjs +56 -27
  205. package/packages/contracts/investigation-run.mjs +16 -0
  206. package/packages/contracts/ip-address.mjs +34 -0
  207. package/packages/contracts/lookup-evidence.mjs +35 -0
  208. package/packages/contracts/offline-comparison.mjs +1 -2
  209. package/packages/contracts/page-fingerprints.mjs +15 -0
  210. package/packages/contracts/risk-calibration.mjs +0 -1
  211. package/packages/contracts/schema-compatibility.mjs +1 -1
  212. package/packages/contracts/schema-lifecycle.mjs +83 -153
  213. package/packages/contracts/selected-file-limits.mjs +6 -0
  214. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  215. package/packages/contracts/web-capture.mjs +10 -0
  216. package/packages/contracts/workspace-portability.mjs +160 -225
  217. package/packages/evidence/artifact-integrity.mjs +8 -2
  218. package/packages/evidence/artifact-structure.mjs +105 -0
  219. package/packages/evidence/decision-fact.mjs +4 -1
  220. package/packages/evidence/domain-control-runtime.mjs +155 -27
  221. package/packages/evidence/domain-posture-context.mjs +79 -0
  222. package/packages/evidence/image-regions.mjs +56 -0
  223. package/packages/evidence/latest-observations.mjs +28 -0
  224. package/packages/evidence/lookup-target.mjs +104 -0
  225. package/packages/evidence/observation.mjs +16 -1
  226. package/packages/evidence/passphrase-encryption.mjs +44 -0
  227. package/packages/evidence/record-identity.mjs +72 -0
  228. package/packages/evidence/registry-dates.mjs +190 -0
  229. package/packages/evidence/retained-file.mjs +69 -0
  230. package/packages/evidence/review-age.mjs +14 -0
  231. package/packages/interchange/bagit.mjs +342 -0
  232. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  233. package/packages/interchange/crc32.mjs +17 -0
  234. package/packages/interchange/external-findings-import.mjs +89 -49
  235. package/packages/interchange/web-capture-import.mjs +382 -0
  236. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  237. package/packages/investigation/investigation-bagit.mjs +14 -0
  238. package/packages/investigation/investigation-capsule.mjs +33 -5
  239. package/packages/investigation/investigation-manifest.mjs +200 -0
  240. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  241. package/packages/investigation/investigation-package.mjs +200 -0
  242. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  243. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  244. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  245. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  246. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  247. package/packages/monitoring/analyst-review-state.mjs +38 -90
  248. package/packages/workspace/brand-profile-model.mjs +158 -20
  249. package/packages/workspace/bulk-columns.mjs +17 -0
  250. package/packages/workspace/bulk-review-model.mjs +11 -5
  251. package/packages/workspace/bulk-session-model.mjs +95 -20
  252. package/packages/workspace/bulk-sort.mjs +1 -0
  253. package/packages/workspace/case-views.mjs +102 -0
  254. package/packages/workspace/hostile-input.mjs +29 -8
  255. package/packages/workspace/investigation-template-model.mjs +19 -4
  256. package/packages/workspace/page-baseline.mjs +18 -15
  257. package/packages/workspace/relationship-observation-model.mjs +41 -26
  258. package/packages/workspace/watchlist-history.mjs +2 -2
  259. package/packages/workspace/watchlist-store.mjs +8 -2
  260. package/packages/workspace/website-snapshot-model.mjs +70 -30
  261. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  262. package/packages/workspace/workspace-archive.mjs +85 -21
  263. package/third-party-notices.txt +29 -2
  264. 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, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
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',
@@ -224,15 +224,15 @@ function validateCurrentActionSummary(value, label) {
224
224
  }
225
225
  return total;
226
226
  }
227
- function validateCaseResponsePacketV7ToV9(value, version) {
228
- const current = version === CASE_RESPONSE_PACKET_VERSION;
229
- const modern = version === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION || current;
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;
230
230
  const root = exact(value, [
231
231
  'schema', 'schemaVersion', 'generatedAt', 'reviewRequired', 'submissionPerformed', 'profile', 'case',
232
232
  'incident', 'contacts', 'selectedEvidence', 'contradictions', 'readiness', 'artefactReferences',
233
233
  'authorisation', 'preflight', 'escalationHistory', 'escalationHistoryOmitted',
234
234
  'escalationHistoryLimitations', 'responseLifecycle', 'provenance', 'integrity',
235
- ...(modern ? ['recipientRoute', 'actionBinding'] : []),
235
+ ...(hasActionBinding ? ['recipientRoute', 'actionBinding'] : []),
236
236
  ], `Case-response packet v${version}`);
237
237
  if (root.schemaVersion !== version)
238
238
  fail(`Case-response packet v${version}`);
@@ -240,14 +240,14 @@ function validateCaseResponsePacketV7ToV9(value, version) {
240
240
  if (root.reviewRequired !== true || root.submissionPerformed !== false)
241
241
  fail('Case-response packet review state');
242
242
  const profile = exact(root.profile, ['id', 'label', 'audience', 'subject', 'checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'], 'Case-response profile');
243
- const profileId = enumeration(profile.id, current ? RESPONSE_PACKET_PROFILE_IDS : PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
243
+ const profileId = enumeration(profile.id, hasPlatformRoutes ? RESPONSE_PACKET_PROFILE_IDS : PRE_PLATFORM_PROFILE_IDS, 'Case-response profile id');
244
244
  text(profile.label, 'Case-response profile label', 200);
245
245
  text(profile.audience, 'Case-response profile audience', 300);
246
246
  text(profile.subject, 'Case-response profile subject', 500);
247
247
  for (const key of ['checklist', 'evidenceOrder', 'includedEvidence', 'excludedEvidence', 'redactions', 'attachments', 'followUpFields'])
248
248
  strings(profile[key], `Case-response profile ${key}`, 24, 500);
249
249
  const selectedProfile = RESPONSE_PACKET_PROFILES.find((candidate) => candidate.id === profileId);
250
- const expectedRedactions = modern ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
250
+ const expectedRedactions = hasActionBinding ? selectedProfile?.redactions : legacyProfileRedactions(profileId);
251
251
  if (!selectedProfile
252
252
  || profile.label !== selectedProfile.label
253
253
  || profile.audience !== selectedProfile.audience
@@ -287,9 +287,9 @@ function validateCaseResponsePacketV7ToV9(value, version) {
287
287
  const contactKeys = new Set();
288
288
  for (const candidate of contacts) {
289
289
  const contact = exact(candidate, [
290
- 'kind', 'contact', 'source', 'observedAt', ...(current ? ['reviewAfter'] : []), 'freshness', 'limitations',
290
+ 'kind', 'contact', 'source', 'observedAt', ...(hasPlatformRoutes ? ['reviewAfter'] : []), 'freshness', 'limitations',
291
291
  ], 'Case-response contact');
292
- const kind = enumeration(contact.kind, current ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
292
+ const kind = enumeration(contact.kind, hasPlatformRoutes ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS, 'Case-response contact kind');
293
293
  const contactValue = text(contact.contact, 'Case-response contact value', MAX_RESPONSE_RECIPIENT_LENGTH);
294
294
  const key = `${kind}\u0000${contactValue.toLowerCase()}`;
295
295
  if (contactKeys.has(key))
@@ -297,13 +297,13 @@ function validateCaseResponsePacketV7ToV9(value, version) {
297
297
  contactKeys.add(key);
298
298
  text(contact.source, 'Case-response contact source', 120);
299
299
  iso(contact.observedAt, 'Case-response contact observedAt', true);
300
- if (current)
300
+ if (hasPlatformRoutes)
301
301
  iso(contact.reviewAfter, 'Case-response contact reviewAfter', true);
302
302
  const freshness = enumeration(contact.freshness, ['current', 'stale', 'unknown'], 'Case-response contact freshness');
303
303
  const routeAge = contact.observedAt === null
304
304
  ? null
305
305
  : Date.parse(root.generatedAt) - Date.parse(contact.observedAt);
306
- const reviewExpired = current && contact.reviewAfter !== null
306
+ const reviewExpired = hasPlatformRoutes && contact.reviewAfter !== null
307
307
  && Date.parse(root.generatedAt) >= Date.parse(contact.reviewAfter);
308
308
  const expectedFreshness = routeAge === null
309
309
  ? 'unknown'
@@ -319,23 +319,23 @@ function validateCaseResponsePacketV7ToV9(value, version) {
319
319
  let selectedActionId = null;
320
320
  let lineageActionIds = [];
321
321
  let lineageComplete = true;
322
- if (modern) {
322
+ if (hasActionBinding) {
323
323
  if (root.recipientRoute !== null) {
324
324
  recipientRoute = exact(root.recipientRoute, [
325
- 'actionId', 'kind', 'contact', 'source', 'observedAt', ...(current ? ['reviewAfter'] : []), 'freshness', 'limitations',
325
+ 'actionId', 'kind', 'contact', 'source', 'observedAt', ...(hasPlatformRoutes ? ['reviewAfter'] : []), 'freshness', 'limitations',
326
326
  ], 'Case-response recipient route');
327
327
  text(recipientRoute.actionId, 'Case-response recipient action id', 64);
328
- enumeration(recipientRoute.kind, [...(current ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS), 'manual'], 'Case-response recipient kind');
328
+ enumeration(recipientRoute.kind, [...(hasPlatformRoutes ? RESPONSE_CONTACT_KINDS : PRE_PLATFORM_CONTACT_KINDS), 'manual'], 'Case-response recipient kind');
329
329
  text(recipientRoute.contact, 'Case-response recipient value', MAX_RESPONSE_RECIPIENT_LENGTH);
330
330
  text(recipientRoute.source, 'Case-response recipient source', 120);
331
331
  iso(recipientRoute.observedAt, 'Case-response recipient observedAt', true);
332
- if (current)
332
+ if (hasPlatformRoutes)
333
333
  iso(recipientRoute.reviewAfter, 'Case-response recipient reviewAfter', true);
334
334
  const freshness = enumeration(recipientRoute.freshness, ['current', 'stale', 'unknown'], 'Case-response recipient freshness');
335
335
  const routeAge = recipientRoute.observedAt === null
336
336
  ? null
337
337
  : Date.parse(root.generatedAt) - Date.parse(recipientRoute.observedAt);
338
- const reviewExpired = current && recipientRoute.reviewAfter !== null
338
+ const reviewExpired = hasPlatformRoutes && recipientRoute.reviewAfter !== null
339
339
  && Date.parse(root.generatedAt) >= Date.parse(recipientRoute.reviewAfter);
340
340
  const expectedFreshness = routeAge === null
341
341
  ? 'unknown'
@@ -370,7 +370,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
370
370
  || selectedContact.contact !== recipientRoute.contact
371
371
  || selectedContact.source !== recipientRoute.source
372
372
  || selectedContact.observedAt !== recipientRoute.observedAt
373
- || (current && selectedContact.reviewAfter !== recipientRoute.reviewAfter)
373
+ || (hasPlatformRoutes && selectedContact.reviewAfter !== recipientRoute.reviewAfter)
374
374
  || selectedContact.freshness !== recipientRoute.freshness)
375
375
  fail('Case-response selected contact projection');
376
376
  }
@@ -378,14 +378,16 @@ function validateCaseResponsePacketV7ToV9(value, version) {
378
378
  const evidence = array(root.selectedEvidence, 'Case-response selected evidence', MAX_RESPONSE_SELECTED_EVIDENCE);
379
379
  const evidenceIds = new Set();
380
380
  for (const candidate of evidence) {
381
- 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');
382
382
  const id = text(item.id, 'Case-response evidence id', 64);
383
383
  if (evidenceIds.has(id))
384
384
  fail('Case-response selected evidence');
385
385
  evidenceIds.add(id);
386
386
  text(item.label, 'Case-response evidence label', 80);
387
387
  text(item.source, 'Case-response evidence source', 120);
388
- 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);
389
391
  enumeration(item.completeness, ['complete', 'partial', 'inconclusive', 'unknown'], 'Case-response evidence completeness');
390
392
  strings(item.limitations, 'Case-response evidence limitations', 8, 240);
391
393
  }
@@ -412,7 +414,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
412
414
  ...(profileId === 'internal_soc' ? [] : ['recipient_route']),
413
415
  ...(['registrar', 'registry', 'network_hosting'].includes(profileId) ? ['infrastructure_responsibility'] : []),
414
416
  ]);
415
- const requiredContact = modern
417
+ const requiredContact = hasActionBinding
416
418
  ? recipientRoute ?? undefined
417
419
  : selectedProfile?.requiredContactKind
418
420
  ? contacts.find((candidate) => candidate.kind === selectedProfile.requiredContactKind)
@@ -421,7 +423,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
421
423
  observed_behaviour: 'complete',
422
424
  exact_url: 'complete',
423
425
  observation_time: expectedObservationAge(incident.observedAt, root.generatedAt).refreshRecommended ? 'stale' : 'complete',
424
- capture_provenance: evidence.length ? 'complete' : 'unavailable',
426
+ capture_provenance: !evidence.length ? 'unavailable' : evidence.every((candidate) => candidate.observedAt !== null) ? 'complete' : 'partial',
425
427
  recipient_route: !requiredContact
426
428
  ? 'not_provided'
427
429
  : requiredContact.freshness === 'stale'
@@ -507,8 +509,8 @@ function validateCaseResponsePacketV7ToV9(value, version) {
507
509
  || (authorisationStatus === 'draft' && authorisation.confirmedAt !== null))
508
510
  fail('Case-response authorisation');
509
511
  const preflight = exact(root.preflight, ['version', 'status', 'canExport', 'counts', 'checks', 'actionSummary'], 'Case-response preflight');
510
- const preflightIds = modern ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
511
- if (preflight.version !== (modern ? 3 : 2))
512
+ const preflightIds = hasActionBinding ? CASE_RESPONSE_PREFLIGHT_IDS_V8 : CASE_RESPONSE_PREFLIGHT_IDS_V7;
513
+ if (preflight.version !== (hasActionBinding ? 3 : 2))
512
514
  fail('Case-response preflight');
513
515
  const checks = array(preflight.checks, 'Case-response preflight checks', preflightIds.length, preflightIds.length);
514
516
  const actualCounts = { block: 0, caution: 0, pass: 0 };
@@ -532,8 +534,8 @@ function validateCaseResponsePacketV7ToV9(value, version) {
532
534
  const history = array(root.escalationHistory, 'Case-response escalation history', MAX_RESPONSE_ACTION_HISTORY);
533
535
  const escalationHistoryOmitted = integer(root.escalationHistoryOmitted, 'Case-response omitted actions', 0, MAX_CASE_ACTIONS);
534
536
  const escalationHistoryLimitations = strings(root.escalationHistoryLimitations, 'Case-response history limitations', 4, 600);
535
- if ((!modern && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
536
- || (modern && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
537
+ if ((!hasActionBinding && history.length !== Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
538
+ || (hasActionBinding && history.length > Math.min(actionTotal, MAX_RESPONSE_ACTION_HISTORY))
537
539
  || escalationHistoryOmitted !== actionTotal - history.length
538
540
  || (escalationHistoryOmitted > 0) !== (escalationHistoryLimitations.length > 0))
539
541
  fail('Case-response action-history bounds');
@@ -545,20 +547,20 @@ function validateCaseResponsePacketV7ToV9(value, version) {
545
547
  'actionId', 'type', 'recipient', 'contactSource', 'state', 'reference',
546
548
  'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
547
549
  'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
548
- ...(modern ? ['routeObservedAt'] : []),
549
- ...(current ? ['routeReviewAfter'] : []),
550
+ ...(hasActionBinding ? ['routeObservedAt'] : []),
551
+ ...(hasPlatformRoutes ? ['routeReviewAfter'] : []),
550
552
  ], 'Case-response escalation action');
551
553
  const actionId = text(action.actionId, 'Case-response action id', 64);
552
554
  if (actionIds.has(actionId))
553
555
  fail('Case-response action identity');
554
556
  actionIds.add(actionId);
555
557
  actionIdOrder.push(actionId);
556
- enumeration(action.type, current ? CASE_ACTION_TYPES : PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
558
+ enumeration(action.type, hasPlatformRoutes ? CASE_ACTION_TYPES : PRE_PLATFORM_ACTION_TYPES, 'Case-response action type');
557
559
  text(action.recipient, 'Case-response action recipient', 320);
558
560
  text(action.contactSource, 'Case-response action source', 120);
559
- if (modern)
561
+ if (hasActionBinding)
560
562
  iso(action.routeObservedAt, 'Case-response action route observedAt', true);
561
- if (current)
563
+ if (hasPlatformRoutes)
562
564
  iso(action.routeReviewAfter, 'Case-response action route reviewAfter', true);
563
565
  enumeration(action.state, CASE_ACTION_STATES, 'Case-response action state');
564
566
  optionalText(action.reference, 'Case-response action reference', 500);
@@ -658,7 +660,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
658
660
  iso(action.createdAt, 'Case-response action createdAt');
659
661
  iso(action.updatedAt, 'Case-response action updatedAt');
660
662
  }
661
- if (modern) {
663
+ if (hasActionBinding) {
662
664
  if (actionIds.size !== lineageActionIds.length
663
665
  || actionIdOrder.some((id, index) => lineageActionIds[index] !== id))
664
666
  fail('Case-response action lineage');
@@ -690,11 +692,11 @@ function validateCaseResponsePacketV7ToV9(value, version) {
690
692
  const latestProviderEvents = latestProviderTime === null
691
693
  ? []
692
694
  : providerEvents.filter((event) => event.occurredAt === latestProviderTime);
693
- const expectedLatestProvider = (modern ? lineageComplete : escalationHistoryOmitted === 0)
695
+ const expectedLatestProvider = (hasActionBinding ? lineageComplete : escalationHistoryOmitted === 0)
694
696
  && latestProviderEvents.length === 1 && latestProviderEvents[0].applied
695
697
  ? latestProviderEvents[0]
696
698
  : null;
697
- const expectedProviderState = (modern ? !lineageComplete : escalationHistoryOmitted > 0)
699
+ const expectedProviderState = (hasActionBinding ? !lineageComplete : escalationHistoryOmitted > 0)
698
700
  ? 'ambiguous'
699
701
  : providerEvents.length === 0 ? 'missing' : expectedLatestProvider ? 'available' : 'ambiguous';
700
702
  if (providerOutcomeState !== expectedProviderState
@@ -727,9 +729,10 @@ function validateCaseResponsePacketV7ToV9(value, version) {
727
729
  strings(lifecycle.limitations, 'Case-response lifecycle limitations', 8, 600);
728
730
  const reviewedInputs = {
729
731
  contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
730
- version: current
732
+ version: version === CASE_RESPONSE_PACKET_VERSION
731
733
  ? CASE_RESPONSE_REVIEW_INPUTS_VERSION
732
- : modern ? LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION : PUBLISHED_V2_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,
733
736
  profile: {
734
737
  id: profile.id,
735
738
  label: profile.label,
@@ -743,7 +746,7 @@ function validateCaseResponsePacketV7ToV9(value, version) {
743
746
  case: root.case,
744
747
  incident: root.incident,
745
748
  contacts: root.contacts,
746
- ...(modern ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
749
+ ...(hasActionBinding ? { recipientRoute: root.recipientRoute, actionBinding: root.actionBinding } : {}),
747
750
  selectedEvidence: root.selectedEvidence,
748
751
  contradictions: root.contradictions,
749
752
  readiness: root.readiness,
@@ -781,10 +784,13 @@ export function validateCaseResponsePacket(value) {
781
784
  if (value.schemaVersion === PUBLIC_CASE_RESPONSE_PACKET_VERSION)
782
785
  return validateCaseResponsePacketV6(value);
783
786
  if (value.schemaVersion === PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION) {
784
- return validateCaseResponsePacketV7ToV9(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
787
+ return validateVersionedCaseResponsePacket(value, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION);
785
788
  }
786
- if (value.schemaVersion === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION) {
787
- return validateCaseResponsePacketV7ToV9(value, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION);
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);
788
794
  }
789
795
  if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion < CASE_RESPONSE_PACKET_VERSION) {
790
796
  throw new TypeError(`Case-response packet version ${String(value.schemaVersion)} is not part of the public compatibility boundary; no data was changed.`);
@@ -792,5 +798,5 @@ export function validateCaseResponsePacket(value) {
792
798
  if (Number.isSafeInteger(value.schemaVersion) && value.schemaVersion > CASE_RESPONSE_PACKET_VERSION) {
793
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.`);
794
800
  }
795
- validateCaseResponsePacketV7ToV9(value, CASE_RESPONSE_PACKET_VERSION);
801
+ validateVersionedCaseResponsePacket(value, CASE_RESPONSE_PACKET_VERSION);
796
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');