@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/NOTICE +2 -2
  2. package/cli/argument-values.mjs +55 -0
  3. package/cli/arguments.mjs +24 -470
  4. package/cli/artifact-validation/case-response.mjs +21 -6
  5. package/cli/artifact-validation/lookup-evidence.mjs +3 -3
  6. package/cli/artifact-validation/signed-review.mjs +59 -13
  7. package/cli/artifact-verify.mjs +2 -0
  8. package/cli/assurance-arguments.mjs +56 -0
  9. package/cli/assurance-command-definitions.mjs +179 -0
  10. package/cli/assurance-command-runner.mjs +1 -1
  11. package/cli/bounded-json.mjs +1 -1
  12. package/cli/bulk-checkpoint.mjs +1 -1
  13. package/cli/bulk-command-runner.mjs +2 -2
  14. package/cli/bulk.mjs +2 -3
  15. package/cli/case-command.mjs +20 -3
  16. package/cli/case-pack.mjs +12 -2
  17. package/cli/ci-report.mjs +6 -3
  18. package/cli/collection-arguments.mjs +123 -0
  19. package/cli/collection-command-definitions.mjs +330 -0
  20. package/cli/collection-preflight.mjs +8 -1
  21. package/cli/command-argument-grammar.mjs +8 -210
  22. package/cli/command-definition.mjs +329 -0
  23. package/cli/command-reference.mjs +55 -1388
  24. package/cli/command-seeds.mjs +18 -0
  25. package/cli/compare.mjs +1 -1
  26. package/cli/config-profile.mjs +1 -1
  27. package/cli/ct-event-intake.mjs +4 -4
  28. package/cli/discover.mjs +2 -2
  29. package/cli/discovery-observation-snapshot.mjs +2 -2
  30. package/cli/discovery-scan-command-runner.mjs +3 -3
  31. package/cli/discovery-scan.mjs +1 -1
  32. package/cli/discovery-snapshot.mjs +2 -2
  33. package/cli/discovery-workflow.mjs +4 -4
  34. package/cli/domain-control-monitor.mjs +6 -6
  35. package/cli/domain-control-observations.mjs +2 -1
  36. package/cli/errors.mjs +5 -6
  37. package/cli/evidence-arguments.mjs +46 -0
  38. package/cli/evidence-command-definitions.mjs +69 -0
  39. package/cli/evidence-command-runner.mjs +2 -3
  40. package/cli/evidence-signing.mjs +3 -2
  41. package/cli/external-observation-mapping.mjs +2 -2
  42. package/cli/formatters/terminal-command-formats.mjs +6 -0
  43. package/cli/formatters/terminal-lookup.mjs +6 -0
  44. package/cli/history-arguments.mjs +39 -0
  45. package/cli/history-command-definitions.mjs +88 -0
  46. package/cli/indicator-set-command.mjs +33 -0
  47. package/cli/inline-command-families.mjs +6 -47
  48. package/cli/inline-command-runner.mjs +4 -31
  49. package/cli/input.mjs +2 -2
  50. package/cli/intake-command.mjs +107 -0
  51. package/cli/interchange-report.mjs +4 -2
  52. package/cli/investigation-artifacts.mjs +1 -1
  53. package/cli/investigation-plan.mjs +32 -154
  54. package/cli/investigation-recipes.mjs +177 -0
  55. package/cli/investigation-run.mjs +78 -12
  56. package/cli/local-mmdb-review.mjs +197 -17
  57. package/cli/lookalike-calibration.mjs +1 -1
  58. package/cli/lookup-brief.mjs +2 -2
  59. package/cli/lookup-command-runner.mjs +18 -35
  60. package/cli/lookup-diff.mjs +10 -9
  61. package/cli/lookup-plan.mjs +10 -14
  62. package/cli/lookup-reconcile.mjs +6 -5
  63. package/cli/lookup-timeline.mjs +7 -6
  64. package/cli/mail-header-review.mjs +24 -247
  65. package/cli/mail-review.mjs +9 -7
  66. package/cli/network-arguments.mjs +51 -0
  67. package/cli/network-command-definitions.mjs +130 -0
  68. package/cli/network-command-runner.mjs +43 -31
  69. package/cli/offline-evidence-review.mjs +39 -6
  70. package/cli/posture.mjs +1 -1
  71. package/cli/registry-fixture-scaffold.mjs +1 -1
  72. package/cli/retained-artifact-diff.mjs +8 -7
  73. package/cli/review-arguments.mjs +131 -0
  74. package/cli/review-command-definitions.mjs +273 -0
  75. package/cli/review-command-runner.mjs +68 -158
  76. package/cli/risk-calibration.mjs +8 -8
  77. package/cli/runner.mjs +20 -32
  78. package/cli/saved-lookup.mjs +2 -2
  79. package/cli/selected-input-worker.mjs +63 -0
  80. package/cli/sharing-review.mjs +5 -64
  81. package/cli/source-reliability.mjs +2 -2
  82. package/cli/support-arguments.mjs +49 -0
  83. package/cli/support-command-definitions.mjs +209 -0
  84. package/cli/support-command-runner.mjs +2 -2
  85. package/cli/workflow-arguments.mjs +86 -0
  86. package/cli/workflow-command-definitions.mjs +78 -0
  87. package/cli/workflow-command-runner.mjs +8 -1
  88. package/docs/cli-reference.md +27 -0
  89. package/docs/cli.md +223 -2
  90. package/docs/contextual-reviews.md +155 -0
  91. package/lib/analyst-taxonomy.mjs +2 -40
  92. package/lib/application-version.mjs +1 -1
  93. package/lib/availability.mjs +13 -789
  94. package/lib/bounded-contract-normalizers.mjs +4 -0
  95. package/lib/bounded-json.mjs +2 -330
  96. package/lib/browser-library-profile.mjs +62 -84
  97. package/lib/candidate-provenance-bounds.mjs +2 -0
  98. package/lib/classify.mjs +1 -1
  99. package/lib/ct-query.mjs +2 -27
  100. package/lib/ct-response-bounds.mjs +2 -9
  101. package/lib/ct-search.mjs +83 -47
  102. package/lib/dmarc-discovery.mjs +62 -0
  103. package/lib/dns-convergence-review.mjs +21 -8
  104. package/lib/dns-delegation-health.mjs +12 -8
  105. package/lib/dns-inheritance-review.mjs +53 -63
  106. package/lib/dns-intelligence.mjs +9 -7
  107. package/lib/dns-query-coverage.mjs +40 -0
  108. package/lib/domain-assurance.mjs +1 -1
  109. package/lib/domain-change-packet.mjs +12 -13
  110. package/lib/domain-change-review.mjs +32 -14
  111. package/lib/domain-control-flight-recorder.mjs +103 -27
  112. package/lib/domain-control-manifest.mjs +25 -17
  113. package/lib/domain-posture-analysis.mjs +22 -5
  114. package/lib/domain-posture.mjs +16 -15
  115. package/lib/domain-registration.mjs +392 -0
  116. package/lib/evidence-export-privacy.mjs +22 -15
  117. package/lib/evidence-export.mjs +3 -3
  118. package/lib/evidence-report.mjs +4 -1
  119. package/lib/external-intelligence-risk.mjs +2 -124
  120. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  121. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  122. package/lib/generated/retire-browser-catalog.mjs +332 -19
  123. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  124. package/lib/html-signals.mjs +7 -2
  125. package/lib/http-evidence-bounds.mjs +2 -8
  126. package/lib/http-intelligence.mjs +13 -1
  127. package/lib/idn-confusable-policy.mjs +6 -4
  128. package/lib/idn-confusables.mjs +1 -1
  129. package/lib/interchange-fidelity-registry.mjs +10 -0
  130. package/lib/lookup-contract-primitives.mjs +1 -1
  131. package/lib/lookup-diagnostics.mjs +1 -23
  132. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  133. package/lib/lookup-progress.mjs +50 -1
  134. package/lib/lookup-response-contract.mjs +69 -40
  135. package/lib/lookup-response.mjs +5 -15
  136. package/lib/lookup-source-progress.mjs +8 -22
  137. package/lib/lookup.mjs +1 -1
  138. package/lib/opportunity-scoring.mjs +2 -107
  139. package/lib/outbound-identity.mjs +1 -1
  140. package/lib/page-fingerprints.mjs +18 -1
  141. package/lib/perceptual-hash-comparison.mjs +2 -34
  142. package/lib/perceptual-hash.mjs +36 -10
  143. package/lib/portable-generator.mjs +2 -29
  144. package/lib/project-metadata.mjs +2 -8
  145. package/lib/rdap-client.mjs +11 -7
  146. package/lib/registrable-domain.mjs +2 -56
  147. package/lib/registrar-standing-contract.mjs +1 -1
  148. package/lib/registrar-standing.mjs +9 -13
  149. package/lib/registry-capabilities.mjs +2 -2
  150. package/lib/registry-capability-catalogue.mjs +27 -19
  151. package/lib/risk-calibration-summary.mjs +2 -352
  152. package/lib/risk-scoring.mjs +2 -345
  153. package/lib/scoring-evidence-quality.mjs +2 -107
  154. package/lib/semantic-version.mjs +2 -54
  155. package/lib/service-binding-dns.mjs +27 -0
  156. package/lib/smtp-transport-review.mjs +48 -19
  157. package/lib/sslbl-certificates.generated.mjs +141 -139
  158. package/lib/static-html-analysis.mjs +1 -1
  159. package/lib/structured-data-identity.mjs +1 -1
  160. package/lib/threat-intelligence-manifests.mjs +1 -1
  161. package/lib/threat-intelligence-runtime.mjs +1 -1
  162. package/lib/threat-intelligence-targets.mjs +1 -1
  163. package/lib/threat-intelligence-types.mjs +2 -75
  164. package/lib/typosquat-generator.mjs +11 -2
  165. package/lib/website-enrichment.mjs +388 -0
  166. package/lib/whois-authority.mjs +43 -5
  167. package/lib/whois-normalization.mjs +3 -1
  168. package/lib/whois-parser.mjs +5 -3
  169. package/package.json +13 -14
  170. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  171. package/packages/analysis/bounded-json.mjs +330 -0
  172. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  173. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  174. package/packages/analysis/ct-query.mjs +27 -0
  175. package/packages/analysis/ct-response-bounds.mjs +9 -0
  176. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  177. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  178. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  179. package/packages/analysis/opportunity-scoring.mjs +107 -0
  180. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  181. package/packages/analysis/portable-generator.mjs +29 -0
  182. package/packages/analysis/project-metadata.mjs +8 -0
  183. package/packages/analysis/registrable-domain.mjs +56 -0
  184. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  185. package/packages/analysis/risk-scoring.mjs +345 -0
  186. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  187. package/packages/analysis/semantic-version.mjs +54 -0
  188. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  189. package/packages/cases/case-evidence-links.mjs +135 -0
  190. package/packages/cases/case-evidence-model.mjs +78 -12
  191. package/packages/cases/case-evidence-quality.mjs +5 -0
  192. package/packages/cases/case-export-input.mjs +7 -5
  193. package/packages/cases/case-incident-context.mjs +52 -0
  194. package/packages/cases/case-markdown.mjs +11 -0
  195. package/packages/cases/case-migration-model.mjs +40 -24
  196. package/packages/cases/case-recheck-model.mjs +90 -32
  197. package/packages/cases/case-record-contracts.mjs +1 -1
  198. package/packages/cases/case-record-core.mjs +1 -1
  199. package/packages/cases/case-record-operations.mjs +40 -112
  200. package/packages/cases/case-record-projection.mjs +8 -1
  201. package/packages/cases/case-record-version-input.mjs +19 -0
  202. package/packages/cases/case-report.mjs +63 -27
  203. package/packages/cases/case-requested-evidence.mjs +146 -0
  204. package/packages/cases/case-response-actions.mjs +103 -1
  205. package/packages/cases/case-response-model.mjs +9 -1
  206. package/packages/cases/case-response-outcomes.mjs +29 -8
  207. package/packages/cases/case-response-packet-render.mjs +13 -9
  208. package/packages/cases/case-response-packet.mjs +25 -11
  209. package/packages/cases/case-response-records.mjs +0 -1
  210. package/packages/cases/case-response-review-inputs.mjs +14 -3
  211. package/packages/cases/case-storage-model.mjs +1 -1
  212. package/packages/cases/case-workflow-metadata.mjs +225 -0
  213. package/packages/cases/case-workflow-migration.mjs +43 -0
  214. package/packages/cases/http-summary.mjs +6 -1
  215. package/packages/comparison/favicon-similarity.mjs +1 -1
  216. package/packages/comparison/page-similarity.mjs +1 -1
  217. package/packages/comparison/relationship-evidence.mjs +21 -9
  218. package/packages/comparison/relationship-provenance.mjs +7 -1
  219. package/packages/contracts/analyst-interchange.mjs +28 -10
  220. package/packages/contracts/case-portability.mjs +118 -25
  221. package/packages/contracts/cli-command-semantics.mjs +2 -0
  222. package/packages/contracts/cli-grammar.mjs +6 -0
  223. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  224. package/packages/contracts/context-review.mjs +24 -0
  225. package/packages/contracts/document-intake.mjs +11 -0
  226. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  227. package/packages/contracts/domain-control-manifest.mjs +1 -1
  228. package/packages/contracts/domain-control-monitor.mjs +13 -2
  229. package/packages/contracts/domain-control-review.mjs +36 -15
  230. package/packages/contracts/har-review.mjs +2 -0
  231. package/packages/contracts/identity-events.mjs +4 -0
  232. package/packages/contracts/investigation-run.mjs +2 -0
  233. package/packages/contracts/ip-address.mjs +10 -0
  234. package/packages/contracts/mail-authentication.mjs +5 -0
  235. package/packages/contracts/message-intake.mjs +31 -0
  236. package/packages/contracts/offline-comparison.mjs +4 -1
  237. package/packages/contracts/passive-mail.mjs +10 -0
  238. package/packages/contracts/web-capture.mjs +2 -2
  239. package/packages/contracts/workspace-portability.mjs +63 -17
  240. package/packages/evidence/collection-quality.mjs +67 -0
  241. package/packages/evidence/dns-query-outcome.mjs +72 -0
  242. package/packages/evidence/email-recipient.mjs +18 -0
  243. package/packages/evidence/lookup-target.mjs +5 -1
  244. package/packages/evidence/observation.mjs +2 -0
  245. package/packages/evidence/png-pixels.mjs +117 -0
  246. package/packages/evidence/public-address-policy.mjs +83 -0
  247. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  248. package/packages/interchange/external-findings-import.mjs +4 -12
  249. package/packages/interchange/managed-indicator-export.mjs +35 -0
  250. package/packages/interchange/managed-indicator-set.mjs +210 -0
  251. package/packages/interchange/misp-indicator-export.mjs +93 -0
  252. package/packages/interchange/sharing-policy.mjs +56 -0
  253. package/packages/interchange/stix-indicator-export.mjs +129 -0
  254. package/packages/interchange/web-capture-import.mjs +22 -15
  255. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  256. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  257. package/packages/investigation/capture-coverage.mjs +57 -0
  258. package/packages/investigation/connector-provenance-review.mjs +100 -0
  259. package/packages/investigation/context-review.mjs +35 -0
  260. package/packages/investigation/document-intake.mjs +81 -0
  261. package/packages/investigation/docx-intake.mjs +133 -0
  262. package/packages/investigation/domain-history-review.mjs +100 -0
  263. package/packages/investigation/domain-transition-review.mjs +47 -0
  264. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  265. package/packages/investigation/har-intake.mjs +68 -0
  266. package/packages/investigation/identity-event-intake.mjs +131 -0
  267. package/packages/investigation/identity-incident-review.mjs +39 -0
  268. package/packages/investigation/incident-sequence-review.mjs +58 -0
  269. package/packages/investigation/intake-report.mjs +19 -0
  270. package/packages/investigation/investigation-manifest.mjs +2 -2
  271. package/packages/investigation/link-intake.mjs +179 -0
  272. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  273. package/packages/investigation/mail-authentication-review.mjs +222 -0
  274. package/packages/investigation/mail-header-identity.mjs +209 -0
  275. package/packages/investigation/message-intake.mjs +195 -0
  276. package/packages/investigation/page-behaviour.mjs +113 -0
  277. package/packages/investigation/pdf-intake.mjs +155 -0
  278. package/packages/investigation/platform-continuity-review.mjs +60 -0
  279. package/packages/investigation/qr-intake.mjs +56 -0
  280. package/packages/investigation/requested-action-hints.mjs +11 -0
  281. package/packages/investigation/selected-input-review.mjs +17 -0
  282. package/packages/investigation/storefront-review.mjs +70 -0
  283. package/packages/workspace/brand-profile-model.mjs +2 -2
  284. package/packages/workspace/bulk-review-model.mjs +28 -26
  285. package/packages/workspace/bulk-session-model.mjs +23 -13
  286. package/packages/workspace/campaign-model.mjs +1 -1
  287. package/packages/workspace/detection-rule-model.mjs +1 -1
  288. package/packages/workspace/hostile-input.mjs +1 -1
  289. package/packages/workspace/investigation-guide.mjs +2 -2
  290. package/packages/workspace/investigation-template-model.mjs +26 -2
  291. package/packages/workspace/page-baseline.mjs +2 -2
  292. package/packages/workspace/relationship-observation-model.mjs +17 -13
  293. package/packages/workspace/shortlist-model.mjs +14 -5
  294. package/packages/workspace/watchlist-history.mjs +28 -12
  295. package/packages/workspace/watchlist-store.mjs +17 -5
  296. package/packages/workspace/website-snapshot-model.mjs +28 -3
  297. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  298. package/packages/workspace/workspace-archive.mjs +5 -4
  299. package/third-party-notices.txt +549 -16
  300. package/lib/hostname.mjs +0 -2
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'node:crypto';
2
+ import { readCaseEvidenceRequest, readCasePacketAmendment, assertEvidenceRequestEvent, assertEvidenceRequestHistory, assertPacketAmendmentSelection } from "../../packages/cases/case-requested-evidence.mjs";
2
3
  import { canonicalArtifactJsonV2 } from "../../packages/evidence/artifact-integrity.mjs";
3
- import { CASE_RESPONSE_PACKET_VERSION, 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
+ import { CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_PACKET_VERSION, PUBLIC_CASE_RESPONSE_PACKET_VERSION, PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_REVIEW_INPUTS_SCHEMA, CASE_RESPONSE_REVIEW_INPUTS_VERSION, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION, LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION, PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION, MAX_ABUSIVE_URLS, MAX_RESPONSE_ARTEFACT_REFERENCES, MAX_RESPONSE_AUTHORISATION_CLOCK_SKEW_MS, MAX_RESPONSE_ACTION_HISTORY, MAX_RESPONSE_CONTACTS, MAX_RESPONSE_CONTRADICTIONS, MAX_RESPONSE_SELECTED_EVIDENCE, RESPONSE_AUTHORISATION_CONFIRMATION_IDS, RESPONSE_PACKET_PROFILES, RESPONSE_CONTACT_KINDS, RESPONSE_PACKET_PROFILE_IDS, RESPONSE_READINESS_ROW_IDS, RESPONSE_READINESS_STATES, RESPONSE_ROUTE_STALE_AFTER_DAYS, } from "../../packages/cases/case-response-packet.mjs";
4
5
  import { CASE_ACTION_EVENT_SOURCE_CLASSES, CASE_ACTION_STATES, CASE_ACTION_TYPES, CASE_CLOSURE_REASONS, CASE_OBSERVED_EFFECT_SOURCE_CLASSES, CASE_OBSERVED_EFFECT_STATES, CASE_PROVIDER_OUTCOMES, isLegalCaseActionTransition, MAX_CASE_ACTION_EVENTS_PER_ACTION, MAX_CASE_ACTIONS, MAX_CASE_ASSERTIONS, MAX_CASE_DECISIONS, MAX_CASE_EVIDENCE_PINS, MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, MAX_RESPONSE_LIMITATIONS, MAX_RESPONSE_RECIPIENT_LENGTH, } from "../../packages/cases/case-response-model.mjs";
5
6
  import { HEX_DIGEST_RE, array, boolean, domain, enumeration, exact, exactOptional, fail, integer, iso, optionalText, sameValues, strings, text, } from "./structure-primitives.mjs";
6
7
  const CASE_RESPONSE_PREFLIGHT_IDS_V6 = [
@@ -543,13 +544,14 @@ function validateVersionedCaseResponsePacket(value, version) {
543
544
  const actionIdOrder = [];
544
545
  const providerEvents = [];
545
546
  for (const candidate of history) {
546
- const action = exact(candidate, [
547
+ const action = exactOptional(candidate, [
547
548
  'actionId', 'type', 'recipient', 'contactSource', 'state', 'reference',
548
549
  'providerOutcome', 'outcomeDetail', 'originActionId', 'historyOmitted',
549
550
  'historyLimitations', 'transitions', 'createdAt', 'updatedAt',
550
551
  ...(hasActionBinding ? ['routeObservedAt'] : []),
551
552
  ...(hasPlatformRoutes ? ['routeReviewAfter'] : []),
552
- ], 'Case-response escalation action');
553
+ ], Number(root.schemaVersion) >= 11 ? ['amendment'] : [], 'Case-response escalation action');
554
+ readCasePacketAmendment(action.amendment);
553
555
  const actionId = text(action.actionId, 'Case-response action id', 64);
554
556
  if (actionIds.has(actionId))
555
557
  fail('Case-response action identity');
@@ -571,6 +573,7 @@ function validateVersionedCaseResponsePacket(value, version) {
571
573
  integer(action.historyOmitted, 'Case-response action omitted history', 0, 1_000_000);
572
574
  strings(action.historyLimitations, 'Case-response action history limitations', 8, 240);
573
575
  const transitions = array(action.transitions, 'Case-response action transitions', MAX_CASE_ACTION_EVENTS_PER_ACTION, 1);
576
+ const requestEvents = [];
574
577
  const eventIds = new Set();
575
578
  let projectedState = null;
576
579
  let latestReference = null;
@@ -580,8 +583,11 @@ function validateVersionedCaseResponsePacket(value, version) {
580
583
  let previousTime = Number.NEGATIVE_INFINITY;
581
584
  let previousEventId = '';
582
585
  for (const eventCandidate of transitions) {
583
- const event = exact(eventCandidate, ['id', 'previousState', 'nextState', 'occurredAt', 'sourceClass', 'provenance', 'reference', 'evidencePinId', 'limitations', 'providerOutcome', 'outcomeDetail', 'originActionId', 'applied'], 'Case-response action transition');
586
+ const event = exactOptional(eventCandidate, ['id', 'previousState', 'nextState', 'occurredAt', 'sourceClass', 'provenance', 'reference', 'evidencePinId', 'limitations', 'providerOutcome', 'outcomeDetail', 'originActionId', 'applied'], Number(root.schemaVersion) >= 11 ? ['evidenceRequest'] : [], 'Case-response action transition');
587
+ const evidenceRequest = readCaseEvidenceRequest(event.evidenceRequest);
588
+ assertEvidenceRequestEvent(evidenceRequest, event);
584
589
  const eventId = text(event.id, 'Case-response action event id', 64);
590
+ requestEvents.push({ id: eventId, ...(evidenceRequest ? { evidenceRequest } : {}) });
585
591
  if (eventIds.has(eventId))
586
592
  fail('Case-response action event identity');
587
593
  eventIds.add(eventId);
@@ -650,6 +656,7 @@ function validateVersionedCaseResponsePacket(value, version) {
650
656
  }
651
657
  }
652
658
  }
659
+ assertEvidenceRequestHistory(requestEvents, Number(action.historyOmitted) > 0);
653
660
  if ((projectedState ?? 'drafting') !== action.state
654
661
  || latestReference !== action.reference
655
662
  || latestProviderOutcome !== action.providerOutcome
@@ -667,6 +674,11 @@ function validateVersionedCaseResponsePacket(value, version) {
667
674
  if (recipientRoute && !actionIds.has(recipientRoute.actionId))
668
675
  fail('Case-response recipient action');
669
676
  }
677
+ if (version >= 11)
678
+ assertPacketAmendmentSelection(history.map(candidate => {
679
+ const action = candidate;
680
+ return { id: action.actionId, history: action.transitions, originActionId: action.originActionId, amendment: action.amendment };
681
+ }), selectedActionId, evidenceIds);
670
682
  const lifecycle = exact(root.responseLifecycle, ['providerOutcomeState', 'latestProviderOutcome', 'observedChangeState', 'latestObservedEffect', 'latestObservedChangeAt', 'closure', 'limitations'], 'Case-response lifecycle');
671
683
  const providerOutcomeState = enumeration(lifecycle.providerOutcomeState, ['available', 'missing', 'ambiguous'], 'Case-response lifecycle provider state');
672
684
  let actualLatestProvider = null;
@@ -731,8 +743,9 @@ function validateVersionedCaseResponsePacket(value, version) {
731
743
  contract: CASE_RESPONSE_REVIEW_INPUTS_SCHEMA,
732
744
  version: version === CASE_RESPONSE_PACKET_VERSION
733
745
  ? 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,
746
+ : version === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION ? LATEST_PUBLIC_CASE_RESPONSE_REVIEW_INPUTS_VERSION
747
+ : hasPlatformRoutes ? PUBLISHED_V2_3_CASE_RESPONSE_REVIEW_INPUTS_VERSION
748
+ : hasActionBinding ? PUBLISHED_V2_2_CASE_RESPONSE_REVIEW_INPUTS_VERSION : PUBLISHED_V2_CASE_RESPONSE_REVIEW_INPUTS_VERSION,
736
749
  profile: {
737
750
  id: profile.id,
738
751
  label: profile.label,
@@ -781,6 +794,8 @@ function validateVersionedCaseResponsePacket(value, version) {
781
794
  fail('Case-response integrity');
782
795
  }
783
796
  export function validateCaseResponsePacket(value) {
797
+ if (value.schemaVersion === LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION)
798
+ return validateVersionedCaseResponsePacket(value, LATEST_PUBLIC_CASE_RESPONSE_PACKET_VERSION);
784
799
  if (value.schemaVersion === PUBLIC_CASE_RESPONSE_PACKET_VERSION)
785
800
  return validateCaseResponsePacketV6(value);
786
801
  if (value.schemaVersion === PUBLISHED_V2_CASE_RESPONSE_PACKET_VERSION) {
@@ -4,7 +4,7 @@ import { validLookupObservationScope } from "../../packages/evidence/lookup-targ
4
4
  import { registrarStandingObservedBy, resolveRegistrarIanaId, validRegistrarStanding, } from "../../lib/registrar-standing-contract.mjs";
5
5
  import { compareRegistrySources } from "../../lib/registry-comparison.mjs";
6
6
  import { buildRegistryInsights } from "../../lib/registry-insights.mjs";
7
- import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../../lib/project-metadata.mjs";
7
+ import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../../packages/analysis/project-metadata.mjs";
8
8
  import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "../../lib/homepage-metadata-contract.mjs";
9
9
  import { HEX_DIGEST_RE, SEMANTIC_VERSION_RE, absoluteUrl, array, boolean, domain, enumeration, exact, exactOptional, fail, integer, iso, nullableRecord, optionalText, record, strings, text, } from "./structure-primitives.mjs";
10
10
  const LOOKUP_RDAP_ATTEMPT_OUTCOMES = [
@@ -675,7 +675,7 @@ export function validateLookupEvidenceArtifactStructure(value) {
675
675
  }
676
676
  validateLookupEvidenceHomepageMetadata(availability, version);
677
677
  if (!isDeepStrictEqual(analysis.availability, currentAvailability
678
- ? projectLookupEvidenceAvailability(analysis.availability)
678
+ ? projectLookupEvidenceAvailability(analysis.availability, { legacyUris: version < HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION })
679
679
  : projectLookupEvidenceAvailabilityPublic(analysis.availability)))
680
680
  fail('Lookup evidence availability analysis');
681
681
  }
@@ -715,7 +715,7 @@ export function validateLookupEvidenceArtifactStructure(value) {
715
715
  if (!isDeepStrictEqual(registryInsights, expectedInsights))
716
716
  fail('Lookup evidence registry insight derivation');
717
717
  try {
718
- assertLookupEvidencePrivacySafeTree(root);
718
+ assertLookupEvidencePrivacySafeTree(root, { legacyUris: version < HOSTNAME_SCOPED_LOOKUP_EVIDENCE_SCHEMA_VERSION });
719
719
  }
720
720
  catch {
721
721
  fail('Lookup evidence privacy boundary');
@@ -3,6 +3,8 @@ import { BULK_DOMAIN_COMPARISON_EXPORT_VERSION, BULK_DOMAIN_COMPARISON_SCHEMA, }
3
3
  import { BULK_MAIL_EXPOSURE_EXPORT_VERSION, BULK_MAIL_EXPOSURE_SCHEMA, MAX_BULK_MAIL_EXPOSURE_ROWS, } from "../../packages/investigation/bulk-mail-exposure.mjs";
4
4
  import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS, } from "../../packages/contracts/investigation-portability.mjs";
5
5
  import { MAX_BULK_SESSION_ROWS, MAX_BULK_SESSION_SOURCES } from "../../packages/contracts/workspace-portability.mjs";
6
+ import { MANAGED_INDICATOR_SET_SCHEMA } from "../../packages/contracts/analyst-interchange.mjs";
7
+ import { validateManagedIndicatorSet } from "../../packages/interchange/managed-indicator-set.mjs";
6
8
  import { BULK_SORT_KEYS } from "../../packages/workspace/bulk-sort.mjs";
7
9
  import { normalizeExplicitIsoTimestamp } from "../../packages/evidence/observation.mjs";
8
10
  import { LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_TARGET_TYPES, LOOKUP_CLAIM_PASSPORT_VERSION, MAX_LOOKUP_CLAIM_PASSPORT_LIMITATIONS, MAX_LOOKUP_CLAIM_PASSPORT_REQUIREMENTS, } from "../../packages/investigation/lookup-claim-passport.mjs";
@@ -334,10 +336,11 @@ function validateBulkReviewManifest(value) {
334
336
  strings(root.limitations, 'Bulk review manifest limitations', 8, 600);
335
337
  validateIntegrity(root.integrity, 'Bulk review manifest integrity', root.version, version);
336
338
  }
337
- function validateSourceObservations(value, label) {
339
+ function validateSourceObservations(value, label, queryCoverage) {
338
340
  const names = new Set();
341
+ let queryCount = 0;
339
342
  return array(value, label, 16).map((candidate) => {
340
- const source = exact(candidate, ['label', 'source', 'state', 'observedAt'], label);
343
+ const source = exact(candidate, ['label', 'source', 'state', 'observedAt', ...(queryCoverage ? ['queries'] : [])], label);
341
344
  const name = text(source.label, `${label} label`, 120).toLowerCase();
342
345
  if (names.has(name))
343
346
  fail(`${label} duplicate label`);
@@ -345,14 +348,37 @@ function validateSourceObservations(value, label) {
345
348
  text(source.source, `${label} source`, 240);
346
349
  enumeration(source.state, ['observed', 'partial', 'unavailable'], `${label} state`);
347
350
  iso(source.observedAt, `${label} observedAt`);
351
+ if (queryCoverage) {
352
+ const keys = new Set();
353
+ const queries = array(source.queries, `${label} queries`, 500 - queryCount);
354
+ queryCount += queries.length;
355
+ for (const candidate of queries) {
356
+ const query = exact(candidate, ['owner', 'type', 'state'], `${label} query`);
357
+ text(query.owner, `${label} query owner`, 253);
358
+ enumeration(query.type, DNS_TYPES, `${label} query type`);
359
+ enumeration(query.state, ['observed', 'partial', 'unavailable'], `${label} query state`);
360
+ const key = `${query.owner}\u0000${query.type}`;
361
+ if (keys.has(key))
362
+ fail(`${label} duplicate query`);
363
+ keys.add(key);
364
+ if ((source.state === 'observed' && query.state !== 'observed')
365
+ || (source.state === 'unavailable' && query.state !== 'unavailable'))
366
+ fail(`${label} query state`);
367
+ }
368
+ }
348
369
  return source;
349
370
  });
350
371
  }
351
- function validateReviewMatrix(value, label, sources) {
372
+ function validateReviewMatrix(value, label, sources, queryCoverage = false) {
373
+ const rowKeys = new Set();
352
374
  for (const [rowIndex, candidate] of array(value, label, 500).entries()) {
353
375
  const row = exact(candidate, ['owner', 'type', 'state', 'observations'], `${label} row ${rowIndex + 1}`);
354
376
  text(row.owner, `${label} owner`, 253);
355
377
  enumeration(row.type, DNS_TYPES, `${label} type`);
378
+ const key = `${row.owner}\u0000${row.type}`;
379
+ if (queryCoverage && rowKeys.has(key))
380
+ fail(`${label} duplicate row`);
381
+ rowKeys.add(key);
356
382
  const observations = array(row.observations, `${label} observations`, 16);
357
383
  if (sources && observations.length !== sources.length)
358
384
  fail(`${label} source coverage`);
@@ -363,7 +389,7 @@ function validateReviewMatrix(value, label, sources) {
363
389
  const observation = exact(observationCandidate, ['label', 'source', 'state', 'values', 'ttlRange', ...(sources ? ['observedAt'] : [])], `${label} observation`);
364
390
  text(observation.label, `${label} observation label`, 120);
365
391
  text(observation.source, `${label} observation source`, 240);
366
- const state = enumeration(observation.state, ['observed', 'partial', 'unavailable'], `${label} observation state`);
392
+ const state = enumeration(observation.state, ['observed', 'partial', 'unavailable', ...(queryCoverage ? ['not_queried'] : [])], `${label} observation state`);
367
393
  const values = strings(observation.values, `${label} observation values`, 500, 16_384);
368
394
  if (sources) {
369
395
  const name = String(observation.label).toLowerCase();
@@ -371,8 +397,16 @@ function validateReviewMatrix(value, label, sources) {
371
397
  fail(`${label} duplicate observation`);
372
398
  names.add(name);
373
399
  const source = sources.find((item) => item.label === observation.label);
374
- if (!source || ['label', 'source', 'state', 'observedAt'].some((key) => observation[key] !== source[key]))
400
+ const provenance = queryCoverage ? ['label', 'source', 'observedAt'] : ['label', 'source', 'state', 'observedAt'];
401
+ if (!source || provenance.some((key) => observation[key] !== source[key]))
375
402
  fail(`${label} source provenance`);
403
+ if (queryCoverage) {
404
+ const query = source.queries.find((item) => item.owner === row.owner && item.type === row.type);
405
+ if (state !== (query?.state ?? 'not_queried'))
406
+ fail(`${label} query provenance`);
407
+ if (['not_queried', 'unavailable'].includes(state) && (values.length || observation.ttlRange !== null))
408
+ fail(`${label} unavailable values`);
409
+ }
376
410
  iso(observation.observedAt, `${label} observation time`);
377
411
  }
378
412
  if (state === 'observed') {
@@ -387,13 +421,19 @@ function validateReviewMatrix(value, label, sources) {
387
421
  fail(`${label} TTL range`);
388
422
  }
389
423
  }
390
- const expected = complete < 2 ? 'insufficient' : signatures.size === 1 ? 'aligned' : 'different';
424
+ const expected = complete < 2 || (queryCoverage && complete !== observations.length) ? 'insufficient' : signatures.size === 1 ? 'aligned' : 'different';
391
425
  if (row.state !== expected)
392
426
  fail(`${label} state`);
393
427
  }
428
+ if (queryCoverage && sources) {
429
+ const expectedKeys = new Set(sources.flatMap((source) => source.queries.map((item) => `${item.owner}\u0000${item.type}`)));
430
+ if (rowKeys.size !== expectedKeys.size || [...rowKeys].some((key) => !expectedKeys.has(key)))
431
+ fail(`${label} query coverage`);
432
+ }
394
433
  }
395
434
  function validateDomainChangeReview(value, label, version) {
396
435
  const retainedTimes = version >= 2;
436
+ const queryCoverage = version >= 3;
397
437
  const review = exact(value, ['schema', 'version', 'generatedAt', 'domain', 'state', 'authoritativeRecordMatrix', 'resolverDivergenceMatrix', 'dnssecAutomation', 'acmeDependencies', 'certificate', 'services', 'hsts', 'gate', 'limitations', ...(retainedTimes ? ['sourceObservations'] : [])], label);
398
438
  if (review.schema !== DOMAIN_CHANGE_REVIEW_SCHEMA || review.version !== version)
399
439
  fail(label);
@@ -401,8 +441,8 @@ function validateDomainChangeReview(value, label, version) {
401
441
  domain(review.domain, `${label} domain`);
402
442
  enumeration(review.state, ['ready', 'review'], `${label} state`);
403
443
  const sources = retainedTimes ? exact(review.sourceObservations, ['authorities', 'resolvers'], `${label} source observations`) : null;
404
- validateReviewMatrix(review.authoritativeRecordMatrix, `${label} authority matrix`, sources ? validateSourceObservations(sources.authorities, `${label} authority sources`) : undefined);
405
- validateReviewMatrix(review.resolverDivergenceMatrix, `${label} resolver matrix`, sources ? validateSourceObservations(sources.resolvers, `${label} resolver sources`) : undefined);
444
+ validateReviewMatrix(review.authoritativeRecordMatrix, `${label} authority matrix`, sources ? validateSourceObservations(sources.authorities, `${label} authority sources`, queryCoverage) : undefined, queryCoverage);
445
+ validateReviewMatrix(review.resolverDivergenceMatrix, `${label} resolver matrix`, sources ? validateSourceObservations(sources.resolvers, `${label} resolver sources`, queryCoverage) : undefined, queryCoverage);
406
446
  const automation = exact(review.dnssecAutomation, ['state', 'cdsObserved', 'cdnskeyObserved', 'csyncObserved', 'conflictingTypes', 'detail'], `${label} DNSSEC automation`);
407
447
  enumeration(automation.state, ['not_observed', 'conflict', 'partial', 'review_ready'], `${label} DNSSEC automation`);
408
448
  boolean(automation.cdsObserved, `${label} CDS observed`);
@@ -514,7 +554,7 @@ function validatePlannedAssurance(value, label) {
514
554
  strings(assurance.limitations, `${label} limitations`, 8, 600);
515
555
  return assurance;
516
556
  }
517
- function expectedDomainChangeSummary(before, after) {
557
+ function expectedDomainChangeSummary(before, after, queryCoverage) {
518
558
  const rows = (review) => new Map(review.authoritativeRecordMatrix.map((row) => [`${row.owner}\u0000${row.type}`, row]));
519
559
  const beforeRows = rows(before);
520
560
  const afterRows = rows(after);
@@ -526,7 +566,7 @@ function expectedDomainChangeSummary(before, after) {
526
566
  };
527
567
  const rowEvidence = (review, row) => {
528
568
  if (!row)
529
- return { state: collectionComplete(review) ? 'complete' : 'unavailable', values: [] };
569
+ return { state: !queryCoverage && collectionComplete(review) ? 'complete' : 'unavailable', values: [] };
530
570
  const observations = row.observations;
531
571
  if (observations.some((item) => item.state !== 'observed'))
532
572
  return { state: 'partial', values: [] };
@@ -537,18 +577,21 @@ function expectedDomainChangeSummary(before, after) {
537
577
  return { state: 'complete', values: [...new Set(observations.flatMap((item) => item.values))].sort() };
538
578
  };
539
579
  const changed = [];
580
+ const reasons = queryCoverage && keys.length > 500 ? ['The comparison exceeds 500 owner and record-type pairs.'] : [];
540
581
  for (const key of keys.slice(0, 500)) {
541
582
  const left = rowEvidence(before, beforeRows.get(key));
542
583
  const right = rowEvidence(after, afterRows.get(key));
543
584
  const [owner, type] = key.split('\u0000');
544
585
  if (left.state !== 'complete' || right.state !== 'complete') {
586
+ if (queryCoverage)
587
+ reasons.push(`Comparison evidence is incomplete for ${owner} ${type}.`);
545
588
  continue;
546
589
  }
547
590
  else if (!sameValues(left.values, right.values)) {
548
591
  changed.push({ owner: owner ?? '', type: type ?? '', beforeValues: left.values, afterValues: right.values });
549
592
  }
550
593
  }
551
- return changed;
594
+ return { changed, reasons };
552
595
  }
553
596
  function validateDomainChangePacket(value) {
554
597
  const root = exact(value, ['schema', 'version', 'generatedAt', 'domain', 'reference', 'state', 'gate', 'summary', 'evidence', 'limitations', 'integrity'], 'Domain change packet');
@@ -590,13 +633,14 @@ function validateDomainChangePacket(value) {
590
633
  const expectedReasons = [
591
634
  ...pre.gate.reasons,
592
635
  ].map((reason) => `Pre-change evidence: ${reason}`);
593
- expectedReasons.push(...post.gate.reasons.map((reason) => `Post-change evidence: ${reason}`), ...(assuranceResult.review.reasons.map((reason) => `Change plan: ${reason}`)));
636
+ const comparison = expectedDomainChangeSummary(pre, post, reviewVersion >= 3);
637
+ expectedReasons.push(...post.gate.reasons.map((reason) => `Post-change evidence: ${reason}`), ...(assuranceResult.review.reasons.map((reason) => `Change plan: ${reason}`)), ...comparison.reasons);
594
638
  const boundedReasons = expectedReasons.slice(0, 100);
595
639
  if (!sameValues(gateReasons, boundedReasons)
596
640
  || gate.pass !== (boundedReasons.length === 0)
597
641
  || root.state !== (boundedReasons.length === 0 ? 'ready' : 'review'))
598
642
  fail('Domain change packet gate');
599
- const expectedChanges = expectedDomainChangeSummary(pre, post);
643
+ const expectedChanges = comparison.changed;
600
644
  if (changed.length !== expectedChanges.length || changed.some((candidate, index) => {
601
645
  const item = candidate;
602
646
  const expected = expectedChanges[index];
@@ -613,6 +657,8 @@ export function validateSignedDigestArtifactStructure(schema, value) {
613
657
  fail('Signed review artefact');
614
658
  if (schema === ACQUISITION_DECISION_PACKET_SCHEMA)
615
659
  validateAcquisition(value);
660
+ else if (schema === MANAGED_INDICATOR_SET_SCHEMA)
661
+ validateManagedIndicatorSet(value);
616
662
  else if (schema === LOOKUP_CLAIM_PASSPORT_SCHEMA)
617
663
  validateLookupClaimPassport(value);
618
664
  else if (schema === BULK_DOMAIN_COMPARISON_SCHEMA)
@@ -1,4 +1,5 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import { MANAGED_INDICATOR_SET_SCHEMA, MANAGED_INDICATOR_SET_VERSION } from "../packages/contracts/analyst-interchange.mjs";
2
3
  import { createHash } from 'node:crypto';
3
4
  import { readEditableCaseExport } from "../packages/cases/case-export-input.mjs";
4
5
  import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "./bounded-json.mjs";
@@ -36,6 +37,7 @@ function currentCanonicalizationRoutes(versions) {
36
37
  })));
37
38
  }
38
39
  const SIGNED_ARTIFACT_ROUTES = Object.freeze({
40
+ [MANAGED_INDICATOR_SET_SCHEMA]: currentCanonicalizationRoutes([MANAGED_INDICATOR_SET_VERSION]),
39
41
  [ACQUISITION_DECISION_PACKET_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_ACQUISITION_DECISION_PACKET_VERSIONS),
40
42
  [LOOKUP_CLAIM_PASSPORT_SCHEMA]: currentCanonicalizationRoutes([LOOKUP_CLAIM_PASSPORT_VERSION]),
41
43
  [BULK_DOMAIN_COMPARISON_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_DOMAIN_COMPARISON_EXPORT_VERSIONS),
@@ -0,0 +1,56 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import { CliUsageError } from "./errors.mjs";
3
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS } from "../packages/contracts/investigation-package-limits.mjs";
4
+ import { terminalOptions, jsonOutput, singleInputAction } from "./argument-values.mjs";
5
+ function parseManifestArguments(parsed) {
6
+ const sources = parsed.positionalValues('artefacts');
7
+ if (sources.some((source) => !source))
8
+ throw new CliUsageError(`manifest requires from 1 to ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} artefact files.`);
9
+ if (new Set(sources).size !== sources.length)
10
+ throw new CliUsageError('manifest artefact files must be different.');
11
+ const configurationDigestSha256 = parsed.optionValue('--configuration-digest');
12
+ if (configurationDigestSha256 !== null && !/^sha256:[a-f0-9]{64}$/u.test(configurationDigestSha256)) {
13
+ throw new CliUsageError('--configuration-digest requires a sha256: hexadecimal digest.');
14
+ }
15
+ return {
16
+ action: 'manifest',
17
+ sources,
18
+ workflow: parsed.optionValue('--workflow'),
19
+ configurationDigestSha256,
20
+ ...(parsed.optionValue('--passphrase-file')
21
+ ? { passphraseSource: parsed.optionValue('--passphrase-file') }
22
+ : {}),
23
+ ...(parsed.hasOption('--package') ? { package: true } : {}),
24
+ ...(parsed.hasOption('--bagit') ? { bagit: true } : {}),
25
+ ...(parsed.optionValue('--folder') ? { folder: parsed.optionValue('--folder') } : {}),
26
+ output: jsonOutput(parsed),
27
+ ...terminalOptions(parsed),
28
+ };
29
+ }
30
+ function parseSharingReviewArguments(parsed) {
31
+ const purpose = parsed.optionValue('--purpose').replace(/\s+/gu, ' ').trim();
32
+ if (!purpose || purpose.length > 200)
33
+ throw new CliUsageError('--purpose is limited to 200 characters.');
34
+ return {
35
+ action: 'sharing-review',
36
+ source: parsed.positionalValue('source'),
37
+ output: jsonOutput(parsed),
38
+ marking: parsed.optionValue('--marking'),
39
+ recipientScope: parsed.optionValue('--recipient-scope'),
40
+ purpose,
41
+ humanReviewed: parsed.hasOption('--human-reviewed'),
42
+ personalDataReviewed: parsed.hasOption('--personal-data-reviewed'),
43
+ redactionsConfirmed: parsed.hasOption('--redactions-confirmed'),
44
+ ...terminalOptions(parsed),
45
+ };
46
+ }
47
+ export const ASSURANCE_ARGUMENT_PARSERS = Object.freeze({
48
+ manifest: parseManifestArguments,
49
+ 'map-observations': (parsed) => singleInputAction('map-observations', parsed),
50
+ 'oam-export': (parsed) => singleInputAction('oam-export', parsed),
51
+ 'ct-intake': (parsed) => singleInputAction('ct-intake', parsed),
52
+ 'domain-control': (parsed) => singleInputAction('domain-control', parsed),
53
+ assurance: (parsed) => singleInputAction('assurance', parsed),
54
+ 'change-packet': (parsed) => singleInputAction('change-packet', parsed),
55
+ 'sharing-review': parseSharingReviewArguments,
56
+ });
@@ -0,0 +1,179 @@
1
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES } from "../packages/investigation/investigation-manifest.mjs";
2
+ import { positional, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed, flag, file } from "./command-definition.mjs";
3
+ export const ASSURANCE_COMMAND_DEFINITIONS = Object.freeze({
4
+ manifest: commandSeed({
5
+ reference: {
6
+ description: `Record an ordered, path-free manifest for up to ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} local files. Use --package --output evidence.zip for a ZIP, add --passphrase-file to encrypt the ordinary package, or use --folder ./evidence for a new unencrypted folder. Add --bagit to ZIP or folder output for BagIt 1.0 with SHA-512 checksums.`,
7
+ example: 'whoisleuth manifest lookup.json comparison.json --workflow "domain review" --json',
8
+ boundary: 'Ordinary output contains metadata only. ZIP and folder output include unchanged selected bytes and are private until reviewed for sharing. Folders must be new; existing destinations are never replaced and a failed write may leave explicit partial output. BagIt output is unencrypted; its checksums do not authenticate evidence. Filenames ending in .json are parsed as JSON; other files are opaque and never executed. Original paths are omitted. No network request is made.',
9
+ },
10
+ collection: { scope: `Reads 1 to ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} local files, at most ${MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES / 1024 / 1024} MiB each and ${MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES.toLocaleString('en-AU')} bytes combined; retains no source paths.` },
11
+ summary: 'Build an evidence manifest offline',
12
+ options: ['--workflow', '--configuration-digest', '--package', '--bagit', '--passphrase-file', '--folder', '--json', '--quiet', '--no-color'],
13
+ optionOverrides: {
14
+ '--package': flag('Create a portable evidence ZIP containing the selected files.'),
15
+ '--folder': file('Create a new evidence folder containing the selected files.'),
16
+ '--bagit': flag('Create a BagIt 1.0 package with SHA-512 checksums.'),
17
+ },
18
+ positionals: Object.freeze([positional('artefacts', 'file', 1, MAX_INVESTIGATION_MANIFEST_ARTIFACTS)]),
19
+ constraints: Object.freeze([
20
+ constraint({ kind: 'required', options: ['--workflow'] }),
21
+ constraint({ kind: 'requires_any', option: '--bagit', requiredOptions: ['--package', '--folder'] }),
22
+ constraint({ kind: 'excludes_all', option: '--bagit', excludedOptions: ['--passphrase-file'] }),
23
+ constraint({ kind: 'requires_all', option: '--package', requiredOptions: ['--output'] }),
24
+ constraint({ kind: 'requires_all', option: '--passphrase-file', requiredOptions: ['--package'] }),
25
+ constraint({ kind: 'mutually_exclusive', options: ['--package', '--json'] }),
26
+ constraint({ kind: 'excludes_all', option: '--folder', excludedOptions: ['--package', '--output', '--force'] }),
27
+ ]),
28
+ handlerOwner: 'assurance',
29
+ networkEffect: 'offline',
30
+ common: false,
31
+ schemaIdentifiers: Object.freeze(['whoisleuth.investigation-manifest']),
32
+ primaryArtefacts: Object.freeze([]),
33
+ planSupport: false,
34
+ additionalOutputFormats: Object.freeze([]),
35
+ bootstrapProfile: 'allowed',
36
+ }),
37
+ "map-observations": commandSeed({
38
+ reference: {
39
+ description: 'Apply one bounded declarative field-mapping profile to local source observations.',
40
+ example: 'whoisleuth map-observations mapping.json --json',
41
+ boundary: 'Profiles select allowlisted dotted fields only. They execute no scripts, make no requests, and emit the browser-compatible external-findings contract.',
42
+ },
43
+ collection: { scope: 'Reads one mapping document capped at 4 MiB and executes no scripts or requests.' },
44
+ summary: 'Apply a declarative observation map offline',
45
+ options: ['--json', '--quiet', '--no-color'],
46
+ positionals: OPTIONAL_FILE_POSITIONAL,
47
+ constraints: EMPTY_CONSTRAINTS,
48
+ handlerOwner: 'assurance',
49
+ networkEffect: 'offline',
50
+ common: false,
51
+ schemaIdentifiers: Object.freeze(['whoisleuth.external-observation-mapping']),
52
+ primaryArtefacts: Object.freeze([]),
53
+ planSupport: false,
54
+ additionalOutputFormats: Object.freeze([]),
55
+ bootstrapProfile: 'allowed',
56
+ }),
57
+ "oam-export": commandSeed({
58
+ reference: {
59
+ description: 'Project browser-compatible external findings into a bounded Open Asset Model bridge document.',
60
+ example: 'whoisleuth oam-export external-findings.json --json',
61
+ boundary: 'The projection is offline, preserves source completeness without inventing confidence, and covers only bounded FQDN, IP address, certificate, and related edge vocabulary.',
62
+ },
63
+ collection: { scope: 'Reads one browser-compatible external-findings document and projects bounded graph records locally.' },
64
+ summary: 'Project external findings to Open Asset Model',
65
+ options: ['--json', '--quiet', '--no-color'],
66
+ positionals: OPTIONAL_FILE_POSITIONAL,
67
+ constraints: EMPTY_CONSTRAINTS,
68
+ handlerOwner: 'assurance',
69
+ networkEffect: 'offline',
70
+ common: false,
71
+ schemaIdentifiers: Object.freeze(['whoisleuth.open-asset-model-bridge']),
72
+ primaryArtefacts: Object.freeze([]),
73
+ planSupport: false,
74
+ additionalOutputFormats: Object.freeze([]),
75
+ bootstrapProfile: 'allowed',
76
+ }),
77
+ "ct-intake": commandSeed({
78
+ reference: {
79
+ description: 'Normalise source-qualified local certificate events into browser-compatible findings.',
80
+ example: 'whoisleuth ct-intake certificate-events.json --json',
81
+ boundary: 'The command is offline, caps output at 100 findings, and treats every event as a review lead rather than proof of serving or control.',
82
+ },
83
+ collection: { scope: 'Reads one source-qualified event batch capped at 4 MiB and makes no request.' },
84
+ summary: 'Normalise certificate observations offline',
85
+ options: ['--json', '--quiet', '--no-color'],
86
+ positionals: OPTIONAL_FILE_POSITIONAL,
87
+ constraints: EMPTY_CONSTRAINTS,
88
+ handlerOwner: 'assurance',
89
+ networkEffect: 'offline',
90
+ common: false,
91
+ schemaIdentifiers: Object.freeze(['whoisleuth.ct-event-batch', 'whoisleuth.external-findings']),
92
+ primaryArtefacts: Object.freeze([]),
93
+ planSupport: false,
94
+ additionalOutputFormats: Object.freeze([]),
95
+ bootstrapProfile: 'allowed',
96
+ }),
97
+ "domain-control": commandSeed({
98
+ reference: {
99
+ description: 'Build an integrity-protected desired-state manifest or compare one with supplied observations.',
100
+ example: 'whoisleuth domain-control domain-control-input.json --json',
101
+ boundary: 'The command is offline and changes no registrar, DNS, mail, or certificate configuration. Only complete, recent source observations can establish drift or expected absence.',
102
+ },
103
+ collection: { scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
104
+ summary: 'Build or review a domain control manifest',
105
+ options: ['--json', '--quiet', '--no-color'],
106
+ positionals: OPTIONAL_FILE_POSITIONAL,
107
+ constraints: EMPTY_CONSTRAINTS,
108
+ handlerOwner: 'assurance',
109
+ networkEffect: 'offline',
110
+ common: false,
111
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.domain-control-review-input', 'whoisleuth.cli.domain-control-review']),
112
+ primaryArtefacts: Object.freeze([]),
113
+ planSupport: false,
114
+ additionalOutputFormats: Object.freeze([]),
115
+ bootstrapProfile: 'allowed',
116
+ }),
117
+ assurance: commandSeed({
118
+ reference: {
119
+ description: 'Review a versioned domain change, recovery-dependency, or retirement plan.',
120
+ example: 'whoisleuth assurance domain-assurance.json --json',
121
+ boundary: 'The command is offline and treats every provider label, readiness state, and evidence reference as analyst-authored input. It changes no configuration.',
122
+ },
123
+ collection: { scope: 'Reads one versioned plan capped at 2 MiB and makes no request or configuration change.' },
124
+ summary: 'Review domain change, recovery, or retirement plans',
125
+ options: ['--json', '--quiet', '--no-color'],
126
+ positionals: OPTIONAL_FILE_POSITIONAL,
127
+ constraints: EMPTY_CONSTRAINTS,
128
+ handlerOwner: 'assurance',
129
+ networkEffect: 'offline',
130
+ common: false,
131
+ schemaIdentifiers: Object.freeze(['whoisleuth.domain-assurance.input', 'whoisleuth.domain-assurance']),
132
+ primaryArtefacts: Object.freeze([]),
133
+ planSupport: false,
134
+ additionalOutputFormats: Object.freeze([]),
135
+ bootstrapProfile: 'allowed',
136
+ }),
137
+ "change-packet": commandSeed({
138
+ reference: {
139
+ description: 'Assemble pre-change, post-change, and planning evidence into one integrity-protected packet.',
140
+ example: 'whoisleuth change-packet change-review.json --json',
141
+ boundary: 'Assembly is offline. Readiness reflects only the supplied bounded evidence and does not authorise or perform a domain change.',
142
+ },
143
+ collection: { scope: 'Reads one versioned packet input capped at 6 MiB and makes no request or configuration change.' },
144
+ summary: 'Build a reviewed change packet offline',
145
+ options: ['--json', '--quiet', '--no-color'],
146
+ positionals: OPTIONAL_FILE_POSITIONAL,
147
+ constraints: EMPTY_CONSTRAINTS,
148
+ handlerOwner: 'assurance',
149
+ networkEffect: 'offline',
150
+ common: false,
151
+ schemaIdentifiers: Object.freeze(['whoisleuth.domain-change-packet.input', 'whoisleuth.domain-change-packet']),
152
+ primaryArtefacts: Object.freeze([]),
153
+ planSupport: false,
154
+ additionalOutputFormats: Object.freeze([]),
155
+ bootstrapProfile: 'allowed',
156
+ }),
157
+ "sharing-review": commandSeed({
158
+ reference: {
159
+ description: 'Lint one reviewed artefact against local integrity, marking, recipient, personal-data, and redaction controls.',
160
+ example: 'whoisleuth sharing-review packet.json --marking amber --recipient-scope organization --purpose "Reviewed incident handoff" --human-reviewed --personal-data-reviewed --redactions-confirmed --json',
161
+ boundary: 'The command is offline and emits only bounded schema/version metadata, no content values, and no raw evidence. Its result is a review aid, not legal advice or recipient authorisation.',
162
+ },
163
+ collection: { scope: 'Reads one bounded artefact, emits only schema/version metadata and no content values, and performs no transmission.' },
164
+ summary: 'Lint an artefact before deliberate sharing',
165
+ options: ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
166
+ positionals: OPTIONAL_FILE_POSITIONAL,
167
+ constraints: Object.freeze([
168
+ constraint({ kind: 'required', options: ['--marking', '--recipient-scope', '--purpose'] }),
169
+ ]),
170
+ handlerOwner: 'assurance',
171
+ networkEffect: 'offline',
172
+ common: false,
173
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.sharing-review']),
174
+ primaryArtefacts: Object.freeze([]),
175
+ planSupport: false,
176
+ additionalOutputFormats: Object.freeze([]),
177
+ bootstrapProfile: 'allowed',
178
+ }),
179
+ });
@@ -4,7 +4,7 @@ import { buildInvestigationPackage } from "../packages/investigation/investigati
4
4
  import { encryptInvestigationPackage } from "../packages/investigation/investigation-package-crypto.mjs";
5
5
  import { writeInvestigationFolder, writePreparedEvidenceFolder } from "./investigation-folder.mjs";
6
6
  import { buildInvestigationBagIt, prepareInvestigationBagIt } from "../packages/investigation/investigation-bagit.mjs";
7
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
7
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
8
8
  import { MAX_ASSURANCE_INPUT_BYTES, buildDomainAssurance, formatDomainAssurance, } from "../lib/domain-assurance.mjs";
9
9
  import { MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, buildDomainChangePacket, formatDomainChangePacket, } from "../lib/domain-change-packet.mjs";
10
10
  import { DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, buildDomainControlFlightRecorder, formatDomainControlFlightRecorder, serializeDomainControlFlightRecorder, } from "../lib/domain-control-flight-recorder.mjs";
@@ -1 +1 @@
1
- export { boundedJsonLimitsForBytes, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../lib/bounded-json.mjs";
1
+ export { boundedJsonLimitsForBytes, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../packages/analysis/bounded-json.mjs";
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { lstat } from 'node:fs/promises';
3
3
  import { recordOrNull } from "../lib/bounded-contract-normalizers.mjs";
4
4
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
5
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
5
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
6
6
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
7
7
  import { boundedCliInputError, CliUsageError } from "./errors.mjs";
8
8
  import { writePrivateFile } from "./output-file.mjs";
@@ -28,12 +28,12 @@ async function runBulkCommand(args, dependencies, context) {
28
28
  catch (error) {
29
29
  if (error instanceof CliUsageError)
30
30
  throw error;
31
- throw new CliUsageError(`Could not read bulk input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
31
+ throw new CliUsageError(`Could not read bulk input: ${boundedCliErrorMessage(error, 'Input could not be read')}`, 'input_unavailable');
32
32
  }
33
33
  const parsed = parseBulkQueries(input, { deep: args.deep });
34
34
  if (args.plan) {
35
35
  const document = buildCollectionPreflight({
36
- command: 'bulk', targetCount: parsed.queries.length, targetLimit: args.deep ? 50 : 500,
36
+ command: 'bulk', targetCount: parsed.queries.length,
37
37
  deep: args.deep, concurrency: args.concurrency, output: args.output, checkpoint: false,
38
38
  });
39
39
  context.writeStdout(args.output === 'json' ? formatJsonDocument(document) : context.terminal(formatCollectionPreflight(document), args.color));
package/cli/bulk.mjs CHANGED
@@ -1,8 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { abortable } from "../lib/abort.mjs";
3
3
  import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
4
- import { CliUsageError } from "./arguments.mjs";
5
- import { hasUnsafeCliText } from "./errors.mjs";
4
+ import { CliUsageError, hasUnsafeCliText } from "./errors.mjs";
6
5
  import { MAX_DEEP_BULK_QUERIES, MAX_FAST_BULK_QUERIES, } from "../lib/bulk-limits.mjs";
7
6
  const MAX_BULK_INPUT_BYTES = 1024 * 1024;
8
7
  async function readTextStreamBounded(stream, limit = MAX_BULK_INPUT_BYTES, signal) {
@@ -65,7 +64,7 @@ function parseBulkQueries(text, { deep = false } = {}) {
65
64
  }
66
65
  }
67
66
  if (!queries.length)
68
- throw new CliUsageError('Bulk input did not contain any queries.');
67
+ throw new CliUsageError('Bulk input did not contain any queries.', 'missing_input');
69
68
  return { queries, duplicates, limit };
70
69
  }
71
70
  function boundedLookupError(error) {