@slicedearth/whoisleuth-cli 2.4.0 → 2.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -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));
@@ -93,6 +93,10 @@ async function runBulkCommand(args, dependencies, context) {
93
93
  }
94
94
  catch (error) {
95
95
  checkpointFailure = error;
96
+ eventProgress.emit({ event: 'warning', state: 'checkpoint_unavailable' });
97
+ if (!eventProgress.enabled) {
98
+ context.writeStderr(`Checkpoint warning: ${boundedCliErrorMessage(error, 'Checkpoint could not be written')}. The latest progress may not be saved.\n`);
99
+ }
96
100
  }
97
101
  }
98
102
  const metadata = {
@@ -122,12 +126,6 @@ async function runBulkCommand(args, dependencies, context) {
122
126
  else
123
127
  context.writeStdout(context.terminal(formatTerminalBulk(selectedItems, metadata), args.color));
124
128
  }
125
- if (checkpointFailure) {
126
- eventProgress.emit({ event: 'warning', state: 'checkpoint_unavailable' });
127
- if (!eventProgress.enabled) {
128
- context.writeStderr(`Checkpoint warning: ${boundedCliErrorMessage(checkpointFailure, 'Checkpoint could not be written')}. Completed output is still available.\n`);
129
- }
130
- }
131
129
  const policyFindings = evaluateCliFailPolicies(buildCliBulkDocument(items, { ...metadata, filter: 'all' }), args.failOn || []);
132
130
  if (policyFindings.length && !eventProgress.enabled)
133
131
  context.writeStderr(formatFailPolicyNotice(policyFindings));
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) {
@@ -1,5 +1,5 @@
1
1
  import { createHash } from 'node:crypto';
2
- import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "../lib/bounded-json.mjs";
2
+ import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "../packages/analysis/bounded-json.mjs";
3
3
  import { canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
4
4
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
5
5
  import { MAX_CASE_CHECKPOINT_FACTS, MAX_CASE_OBJECTIVE_LENGTH, MAX_CASE_STORE_BYTES, MAX_EDITABLE_CASE_INPUT_BYTES, MAX_NOTE_LENGTH, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../packages/contracts/case-portability.mjs";
@@ -8,6 +8,8 @@ import { createCaseIncident, openOrCreateCase, recordCaseConclusion, recordCaseR
8
8
  import { buildCaseExport, serializeCaseStore } from "../packages/cases/case-storage-model.mjs";
9
9
  import { appendCaseEvidencePin } from "../packages/cases/case-response-model.mjs";
10
10
  import { readCaseRecheckAnswerContext } from "../packages/cases/case-recheck-model.mjs";
11
+ import { readCaseResponseObjects } from "../packages/cases/case-response-object.mjs";
12
+ import { caseEvidenceLinkIssues, mergeCaseEvidenceLinks } from "../packages/cases/case-evidence-links.mjs";
11
13
  import { dispositionLabel, statusLabel } from "../packages/cases/case-record-decisions.mjs";
12
14
  import { normalizeDomain } from "../packages/cases/case-record-core.mjs";
13
15
  import { safeTerminalValue } from "./formatters/terminal.mjs";
@@ -52,9 +54,13 @@ function pin(input, now) {
52
54
  return raw;
53
55
  }
54
56
  function retainExistingEntries(before, after) {
57
+ if (canonicalArtifactJsonV2(mergeCaseEvidenceLinks(before.evidenceLinks, after.evidenceLinks) ?? [])
58
+ !== canonicalArtifactJsonV2(mergeCaseEvidenceLinks(undefined, after.evidenceLinks) ?? [])) {
59
+ throw new CliUsageError('The operation would remove retained evidence relationships. Nothing was written.');
60
+ }
55
61
  const pairs = [
56
62
  [before.notes, after.notes], [before.evidenceHistory, after.evidenceHistory], [before.evidencePins, after.evidencePins],
57
- [before.decisions, after.decisions], [before.actions, after.actions], [before.assertions, after.assertions],
63
+ [before.decisions, after.decisions], [before.assertions, after.assertions],
58
64
  [before.manualTrail, after.manualTrail], [before.sightings, after.sightings],
59
65
  [before.observedEffects.reviews, after.observedEffects.reviews], [before.closures.records, after.closures.records],
60
66
  [before.branches ?? [], after.branches ?? []], [before.attachments ?? [], after.attachments ?? []],
@@ -65,8 +71,15 @@ function retainExistingEntries(before, after) {
65
71
  throw new CliUsageError('The operation would remove or alter earlier retained evidence. Export a smaller working set or review it in the console; nothing was written.');
66
72
  }
67
73
  }
74
+ for (const action of before.actions) {
75
+ const next = after.actions.find(candidate => candidate.id === action.id);
76
+ const events = new Set(next?.history.map(event => canonicalArtifactJsonV2(event)) ?? []);
77
+ if (!next || action.history.some(event => !events.has(canonicalArtifactJsonV2(event)))) {
78
+ throw new CliUsageError('The operation would alter retained response events. Nothing was written.');
79
+ }
80
+ }
68
81
  }
69
- function mutate(cases, args, input, note, now) {
82
+ export function applyCliCaseOperation(cases, args, input, note, now) {
70
83
  if (args.operation === 'open') {
71
84
  const title = args.title === null ? undefined : prose(args.title, MAX_CASE_OBJECTIVE_LENGTH, 'Incident title');
72
85
  const result = args.newIncident
@@ -84,6 +97,70 @@ function mutate(cases, args, input, note, now) {
84
97
  else if (args.operation === 'pin') {
85
98
  result = updateCase(cases, current.id, { evidencePin: pin(input, now) }, now);
86
99
  }
100
+ else if (args.operation === 'link' || args.operation === 'withdraw-link') {
101
+ result = updateCase(cases, current.id, args.operation === 'link' ? { evidenceLink: input } : { evidenceLinkWithdrawal: input }, now);
102
+ }
103
+ else if (args.operation === 'incident-link') {
104
+ if (!input)
105
+ throw new CliUsageError('An incident link requires JSON input.');
106
+ fields(input, ['url'], 'Incident link');
107
+ result = updateCase(cases, current.id, { incidentTarget: input.url }, now);
108
+ }
109
+ else if (args.operation === 'action') {
110
+ if (!input)
111
+ throw new CliUsageError('A response action requires JSON input.');
112
+ fields(input, ['type', 'recipient', 'contactSource', 'routeObservedAt', 'routeReviewAfter', 'contactLimitations', 'dueAt', 'followUpAt', 'responseObjects', 'originActionId', 'amendment', 'correction'], 'Response action');
113
+ result = updateCase(cases, current.id, { action: input }, now);
114
+ const action = result.record.actions.find(candidate => !current.actions.some(prior => prior.id === candidate.id));
115
+ if (!action)
116
+ throw new CliUsageError('No new action was retained within the Case bounds; nothing was written.');
117
+ for (const [key, value] of Object.entries(input))
118
+ if (canonicalArtifactJsonV2(key === 'responseObjects' ? readCaseResponseObjects(value) : value) !== canonicalArtifactJsonV2(action[key])) {
119
+ throw new CliUsageError(`Action field ${safeTerminalValue(key)} is not canonical; nothing was written.`);
120
+ }
121
+ }
122
+ else if (args.operation === 'action-event') {
123
+ if (!input || !input.transition || typeof input.transition !== 'object' || Array.isArray(input.transition))
124
+ throw new CliUsageError('An action event requires id and transition JSON.');
125
+ fields(input, ['id', 'transition'], 'Response action event');
126
+ const transition = input.transition;
127
+ fields(transition, ['nextState', 'occurredAt', 'sourceClass', 'provenance', 'reference', 'evidencePinId', 'limitations', 'providerOutcome', 'outcomeDetail', 'responseObjects', 'objectOutcome', 'evidenceRequest', 'packetReceipt'], 'Response transition');
128
+ result = updateCase(cases, current.id, { actionUpdate: input }, now);
129
+ const previous = current.actions.find(action => action.id === input.id);
130
+ const event = result.record.actions.find(action => action.id === input.id)?.history.find(candidate => !previous?.history.some(prior => prior.id === candidate.id));
131
+ if (!event)
132
+ throw new CliUsageError('The selected action is not retained. Nothing was written.');
133
+ for (const [key, value] of Object.entries(transition))
134
+ if (canonicalArtifactJsonV2(key === 'responseObjects' ? readCaseResponseObjects(value) : value) !== canonicalArtifactJsonV2(event[key])) {
135
+ throw new CliUsageError(`Transition field ${safeTerminalValue(key)} is not canonical; nothing was written.`);
136
+ }
137
+ }
138
+ else if (args.operation === 'recheck-question') {
139
+ if (!input)
140
+ throw new CliUsageError('A recheck question requires JSON input.');
141
+ fields(input, ['statement', 'rationale', 'recheck', 'evidencePinIds'], 'Recheck question');
142
+ result = updateCase(cases, current.id, { assertion: { ...input, kind: 'next_step' } }, now);
143
+ const question = result.record.assertions.find(candidate => !current.assertions.some(prior => prior.id === candidate.id));
144
+ if (!question)
145
+ throw new CliUsageError('No new question was retained within the Case bounds; nothing was written.');
146
+ for (const [key, value] of Object.entries(input))
147
+ if (canonicalArtifactJsonV2(value) !== canonicalArtifactJsonV2(question[key])) {
148
+ throw new CliUsageError(`Question field ${safeTerminalValue(key)} is not canonical; nothing was written.`);
149
+ }
150
+ }
151
+ else if (args.operation === 'close-object') {
152
+ if (!input?.responseObject)
153
+ throw new CliUsageError('Object closure requires one explicit responseObject; it cannot close the whole Case.');
154
+ fields(input, ['responseObject', 'reason', 'summary', 'observedEffectReviewId', 'actionId', 'limitations'], 'Object closure');
155
+ result = updateCase(cases, current.id, { closure: input }, now);
156
+ const closure = result.record.closures.records.find(candidate => !current.closures.records.some(prior => prior.id === candidate.id));
157
+ if (!closure)
158
+ throw new CliUsageError('No new closure was retained within the Case bounds; nothing was written.');
159
+ for (const [key, value] of Object.entries(input))
160
+ if (canonicalArtifactJsonV2(value) !== canonicalArtifactJsonV2(closure[key])) {
161
+ throw new CliUsageError(`Closure field ${safeTerminalValue(key)} is not canonical; nothing was written.`);
162
+ }
163
+ }
87
164
  else if (args.operation === 'assess') {
88
165
  if (!input)
89
166
  throw new CliUsageError('An assessment requires JSON input.');
@@ -111,7 +188,7 @@ function mutate(cases, args, input, note, now) {
111
188
  else if (args.operation === 'recheck') {
112
189
  if (!input)
113
190
  throw new CliUsageError('A recheck requires JSON input.');
114
- fields(input, ['state', 'observedAt', 'completeness', 'comparisonSummary', 'source', 'followUpAt', 'limitations', 'collectionDepth', 'recheck', 'observationHostname'], 'Recheck');
191
+ fields(input, ['state', 'observedAt', 'completeness', 'comparisonSummary', 'source', 'followUpAt', 'limitations', 'collectionDepth', 'recheck', 'observationHostname', 'responseObject', 'objectOutcome'], 'Recheck');
115
192
  const recheck = readCaseRecheckAnswerContext(input.recheck);
116
193
  if (input.state === 'not_reproduced' && !recheck) {
117
194
  throw new CliUsageError('Not reproduced requires a saved recheck question and confirmed comparable conditions. Import the planned Case or record unavailable for a limited observation.');
@@ -127,6 +204,8 @@ function mutate(cases, args, input, note, now) {
127
204
  comparisonSummary: comparison.value, source: review.source, followUpAt: review.followUpAt,
128
205
  limitations: review.limitations, collectionDepth: comparison.collectionDepth,
129
206
  ...(review.recheck ? { recheck: review.recheck } : {}),
207
+ ...(review.responseObject ? { responseObject: review.responseObject } : {}),
208
+ ...(review.objectOutcome ? { objectOutcome: review.objectOutcome } : {}),
130
209
  ...(comparison.observationHostname ? { observationHostname: comparison.observationHostname } : {}),
131
210
  };
132
211
  for (const [key, value] of Object.entries(input)) {
@@ -149,8 +228,22 @@ function formatCases(cases, digest) {
149
228
  lines.push(`Pin ${safeTerminalValue(pin.id)} · ${safeTerminalValue(pin.label)}`, safeTerminalValue(pin.value, '—', MAX_RESPONSE_VALUE_LENGTH), ` Source ${safeTerminalValue(pin.source)} · ${safeTerminalValue(pin.observedAt, 'time unavailable')} · ${pin.completeness}`);
150
229
  for (const decision of record.decisions)
151
230
  lines.push(`Assessment ${decision.createdAt}`, safeTerminalValue(decision.summary, '—', MAX_RESPONSE_VALUE_LENGTH), safeTerminalValue(decision.rationale, '—', MAX_RESPONSE_RATIONALE_LENGTH));
231
+ for (const { link, missingPinIds, cyclic } of caseEvidenceLinkIssues(record.evidenceLinks ?? [], record.evidencePins)) {
232
+ lines.push(`Evidence relationship ${safeTerminalValue(link.id)} · analyst declaration`, ` ${safeTerminalValue(link.fromPinId)} ${link.kind.replaceAll('_', ' ')} ${safeTerminalValue(link.toPinId)}`, ` ${safeTerminalValue(link.basis, '—', MAX_RESPONSE_RATIONALE_LENGTH)}`);
233
+ if (link.withdrawal)
234
+ lines.push(` Withdrawn ${link.withdrawal.at}: ${safeTerminalValue(link.withdrawal.reason, '—', MAX_RESPONSE_RATIONALE_LENGTH)}`);
235
+ if (missingPinIds.length)
236
+ lines.push(` Referenced pins not retained: ${missingPinIds.map(id => safeTerminalValue(id)).join(', ')}`);
237
+ if (cyclic)
238
+ lines.push(' Conflicting imported derivation cycle; no order is inferred.');
239
+ }
240
+ for (const action of record.actions) {
241
+ lines.push(`Action ${safeTerminalValue(action.id)} · ${action.type} · ${action.state}`, ` Objects: ${action.responseObjects?.map(object => `${object.kind}: ${safeTerminalValue(object.identifier)}`).join('; ') || 'unknown binding'}`);
242
+ for (const event of action.history)
243
+ lines.push(` Event ${event.occurredAt} · ${event.sourceClass} · ${event.nextState}${event.objectOutcome ? ` · reported ${event.objectOutcome}` : ''}`, ` Scope: ${event.responseObjects?.map(object => `${object.kind}: ${safeTerminalValue(object.identifier)}`).join('; ') || 'unknown binding'}`);
244
+ }
152
245
  for (const review of record.observedEffects.reviews)
153
- lines.push(`Recheck ${safeTerminalValue(review.observedAt, 'time unavailable')} · ${review.state} · ${review.completeness} · ${safeTerminalValue(review.source)}`);
246
+ lines.push(`Recheck ${safeTerminalValue(review.observedAt, 'time unavailable')} · ${review.state} · ${review.completeness} · ${safeTerminalValue(review.source)}`, ` Object: ${review.responseObject ? `${review.responseObject.kind}: ${safeTerminalValue(review.responseObject.identifier)}` : 'unknown binding'}${review.objectOutcome ? ` · observed ${review.objectOutcome}` : ''}`);
154
247
  lines.push(`Other retained records: ${record.evidenceHistory.length} snapshots, ${record.actions.length} actions, ${record.assertions.length} assertions, ${record.attachments?.length ?? 0} file references.`, '');
155
248
  }
156
249
  lines.push('Use --json for the full Case export. Attached file bytes remain separate. This command makes no request.', '');
@@ -176,7 +269,7 @@ export async function runCaseCommand(args, dependencies, context) {
176
269
  maximumBytes: MAX_EDITABLE_CASE_INPUT_BYTES, limits: boundedJsonLimitsForBytes(MAX_CASE_STORE_BYTES), label: 'Case operation input',
177
270
  }) : null;
178
271
  const note = args.noteSource ? await context.readInput(args.noteSource, MAX_NOTE_LENGTH * 4 + 4, 'Case note') : args.text;
179
- const next = mutate(cases, args, input, note, now);
272
+ const next = applyCliCaseOperation(cases, args, input, note, now);
180
273
  if (Buffer.byteLength(serializeCaseStore(next)) > MAX_CASE_STORE_BYTES)
181
274
  throw new CliUsageError('The updated Case store exceeds its byte budget. No evidence was pruned and nothing was written.');
182
275
  const output = `${JSON.stringify(buildCaseExport(next, now))}\n`;
package/cli/case-pack.mjs CHANGED
@@ -3,8 +3,12 @@ import { Buffer } from 'node:buffer';
3
3
  import { canonicalArtifactJson, canonicalArtifactJsonV2, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
4
4
  import { buildCaseReport, buildCaseReportVerificationProjection } from "../packages/cases/case-report.mjs";
5
5
  import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
6
- import { assertBoundedJsonStructure, scanBoundedJson } from "../lib/bounded-json.mjs";
7
- import { assertCaseBrandProfileIds, CASE_AUDIENCE_SENSITIVE_FIELD_NAMES, caseAudienceExclusions, normalizeDomain, normalizeCaseStore, projectCaseForAudience, safeId, } from "../packages/cases/case-model.mjs";
6
+ import { assertBoundedJsonStructure, scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
7
+ import { assertCaseBrandProfileIds } from "../packages/cases/case-brand-profile-references.mjs";
8
+ import { CASE_AUDIENCE_SENSITIVE_FIELD_NAMES, caseAudienceExclusions, projectCaseForAudience, } from "../packages/cases/case-record-projection.mjs";
9
+ import { normalizeDomain } from "../packages/evidence/domain-name.mjs";
10
+ import { normalizeCaseStore } from "../packages/cases/case-migration-model.mjs";
11
+ import { safeId } from "../packages/cases/case-record-core.mjs";
8
12
  import { CASE_REPORT_SCHEMA, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_INPUT_CASE_VERSIONS, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS as INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS as PACKET_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS as PACKET_KEYS, CLI_CASE_PACK_REPORT_KEYS as REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS as ROOT_KEYS, MAX_CASE_IMPORT_BYTES, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, PUBLIC_CASE_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, } from "../packages/contracts/case-portability.mjs";
9
13
  import { CliUsageError } from "./errors.mjs";
10
14
  import { canonicalCaseExportProjection as canonicalPackProjection } from "../packages/cases/case-export-input.mjs";
@@ -58,17 +62,27 @@ function listItemPath(path, list) {
58
62
  && path[3] === 'analystResponse' && path[4] === list && typeof path[5] === 'number');
59
63
  }
60
64
  function allowedSensitiveFieldPath(key, path) {
65
+ const deliveryReceipt = path.at(-1) === 'packetReceipt' && typeof path.at(-2) === 'number' && path.at(-3) === 'history' && listItemPath(path.slice(0, -3), 'actions');
61
66
  if (key === 'brandProfileIds')
62
67
  return topCasePath(path) || reportCasePath(path);
63
68
  if (key === 'notes')
64
69
  return topCasePath(path) || reportCasePath(path);
70
+ if (key === 'workflowMetadata')
71
+ return topCasePath(path) || analystResponsePath(path);
72
+ if (key === 'incidentTargets' || key === 'investigationContext') {
73
+ return path.at(-1) === 'workflowMetadata' && (topCasePath(path.slice(0, -1)) || analystResponsePath(path.slice(0, -1)));
74
+ }
65
75
  if (key === 'actions' || key === 'assertions' || key === 'manualTrail' || key === 'observedEffects' || key === 'closures' || key === 'branches') {
66
76
  return topCasePath(path) || analystResponsePath(path);
67
77
  }
68
78
  if (key === 'recipient')
69
- return listItemPath(path, 'actions');
79
+ return listItemPath(path, 'actions') || deliveryReceipt;
70
80
  if (key === 'target')
71
- return listItemPath(path, 'manualTrail');
81
+ return listItemPath(path, 'manualTrail') || deliveryReceipt;
82
+ if (key === 'correction')
83
+ return listItemPath(path, 'actions');
84
+ if (key === 'packetReceipt')
85
+ return typeof path.at(-1) === 'number' && path.at(-2) === 'history' && listItemPath(path.slice(0, -2), 'actions');
72
86
  return true;
73
87
  }
74
88
  /** Rejects hidden audience-sensitive copies with a budget proven by the serialized byte bound. */
@@ -473,6 +487,7 @@ export function formatCliCasePack(document) {
473
487
  `Cases ${document.cases.length}`,
474
488
  `Reports ${document.packet.reports.length}`,
475
489
  `Digest ${document.integrity.digestSha256}`,
490
+ 'Included for every audience: domain evidence, Case, pin and decision identifiers, tags, decision summaries and rationale. Review these before sharing.',
476
491
  '',
477
492
  ...document.packet.redactionManifest.excluded.map((item) => `Excluded: ${item}`),
478
493
  '',
package/cli/ci-report.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { WHOISLEUTH_REQUEST_POLICY_URL } from "../lib/project-metadata.mjs";
1
+ import { WHOISLEUTH_REQUEST_POLICY_URL } from "../packages/analysis/project-metadata.mjs";
2
2
  import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA } from "../packages/contracts/domain-control-monitor.mjs";
3
3
  function record(value) {
4
4
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
@@ -45,9 +45,12 @@ function junitCases(document) {
45
45
  return [{ name: 'WHOISleuth result', failure: 'Unsupported document for JUnit conversion.' }];
46
46
  }
47
47
  export function formatCliJunit(document) {
48
- const cases = junitCases(record(document));
48
+ const root = record(document);
49
+ const cases = junitCases(root);
49
50
  const failures = cases.filter((item) => item.failure).length;
50
- return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="WHOISleuth" tests="${cases.length}" failures="${failures}">\n${cases.map((item) => ` <testcase classname="whoisleuth" name="${xml(item.name)}">${item.failure ? `<failure message="${xml(item.failure)}" />` : ''}</testcase>`).join('\n')}\n</testsuite>\n`;
51
+ const reminders = root.schema === CLI_DOMAIN_CONTROL_MONITOR_SCHEMA
52
+ ? `\n <system-out>${xml(`Renewal reviews due: ${Number(record(record(root.review).counts).due ?? 0)}`)}</system-out>` : '';
53
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="WHOISleuth" tests="${cases.length}" failures="${failures}">\n${cases.map((item) => ` <testcase classname="whoisleuth" name="${xml(item.name)}">${item.failure ? `<failure message="${xml(item.failure)}" />` : ''}</testcase>`).join('\n')}${reminders}\n</testsuite>\n`;
51
54
  }
52
55
  export function buildPostureSarif(document) {
53
56
  const root = record(document);
@@ -0,0 +1,123 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import {} from "./fail-policy.mjs";
3
+ import { commandDefaultNumber, commandDefaultText } from "./command-reference.mjs";
4
+ import { COLLECTION_COMMAND_DEFINITIONS } from "./collection-command-definitions.mjs";
5
+ import { terminalOptions, normalizedLabel, failPolicies, parseOutput, } from "./argument-values.mjs";
6
+ function parseLookupArguments(parsed) {
7
+ const output = parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS.lookup);
8
+ const selectedFailPolicies = failPolicies(parsed, 'lookup');
9
+ return {
10
+ action: 'lookup',
11
+ query: parsed.positionalValue('target'),
12
+ output,
13
+ deep: parsed.hasOption('--deep'),
14
+ detail: parsed.hasOption('--summary')
15
+ ? 'summary'
16
+ : parsed.hasOption('--verbose')
17
+ ? 'verbose'
18
+ : 'standard',
19
+ strictExit: parsed.hasOption('--strict-exit'),
20
+ events: parsed.hasOption('--events'),
21
+ plan: parsed.hasOption('--plan'),
22
+ includeAttribution: !parsed.hasOption('--no-attribution'),
23
+ observerLabel: normalizedLabel(parsed, '--observer', 80),
24
+ vantageLabel: normalizedLabel(parsed, '--vantage', 80),
25
+ ...terminalOptions(parsed),
26
+ ...(parsed.hasOption('--browse') ? { browse: true } : {}),
27
+ ...(parsed.hasOption('--exact-url') ? { exactUrl: true } : {}),
28
+ ...(parsed.optionValue('--save-lookup')
29
+ ? { saveLookup: parsed.optionValue('--save-lookup') }
30
+ : {}),
31
+ ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
32
+ };
33
+ }
34
+ function parseBulkArguments(parsed) {
35
+ const deep = parsed.hasOption('--deep');
36
+ const selectedFailPolicies = failPolicies(parsed, 'bulk');
37
+ const output = parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS.bulk);
38
+ const filter = parsed.hasOption('--registered-only')
39
+ ? 'registered'
40
+ : parsed.hasOption('--inconclusive-only')
41
+ ? 'inconclusive'
42
+ : parsed.hasOption('--errors-only')
43
+ ? 'errors'
44
+ : 'all';
45
+ return {
46
+ action: 'bulk',
47
+ source: parsed.positionalValue('source'),
48
+ output,
49
+ deep,
50
+ concurrency: parsed.integerOption('--concurrency') ?? commandDefaultNumber('bulk', '--concurrency', deep),
51
+ checkpoint: parsed.optionValue('--checkpoint'),
52
+ resume: parsed.hasOption('--resume'),
53
+ events: parsed.hasOption('--events'),
54
+ plan: parsed.hasOption('--plan'),
55
+ filter,
56
+ ...terminalOptions(parsed),
57
+ ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
58
+ };
59
+ }
60
+ function parseDiscoverArguments(parsed) {
61
+ return {
62
+ action: 'discover',
63
+ seed: parsed.positionalValue('subject'),
64
+ output: parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS.discover),
65
+ ...discoveryValues(parsed),
66
+ snapshotSource: parsed.optionValue('--snapshot'),
67
+ ...terminalOptions(parsed),
68
+ };
69
+ }
70
+ function discoveryValues(parsed) {
71
+ const familyText = parsed.optionValue('--families');
72
+ return {
73
+ preset: (familyText
74
+ ? 'custom'
75
+ : (parsed.optionValue('--preset') ?? commandDefaultText('discover', '--preset'))),
76
+ keyboardLayout: (parsed.optionValue('--keyboard') ??
77
+ commandDefaultText('discover', '--keyboard')),
78
+ tldText: parsed.optionValue('--tlds'),
79
+ dictionarySource: parsed.optionValue('--dictionary'),
80
+ familyText,
81
+ };
82
+ }
83
+ function parseDiscoverScanArguments(parsed) {
84
+ const deep = parsed.hasOption('--deep');
85
+ const selectedFailPolicies = failPolicies(parsed, 'discover-scan');
86
+ const filter = parsed.hasOption('--registered-only')
87
+ ? 'registered'
88
+ : parsed.hasOption('--inconclusive-only')
89
+ ? 'inconclusive'
90
+ : parsed.hasOption('--acquisition-only')
91
+ ? 'acquisition'
92
+ : parsed.hasOption('--suppressed-only')
93
+ ? 'suppressed'
94
+ : 'all';
95
+ return {
96
+ action: 'discover-scan',
97
+ seed: parsed.positionalValue('subject'),
98
+ output: parseOutput(parsed, COLLECTION_COMMAND_DEFINITIONS['discover-scan']),
99
+ ...discoveryValues(parsed),
100
+ deep,
101
+ scanLimit: parsed.integerOption('--scan-limit') ??
102
+ commandDefaultNumber('discover-scan', '--scan-limit', deep),
103
+ chunkSize: parsed.integerOption('--chunk-size') ?? commandDefaultNumber('discover-scan', '--chunk-size'),
104
+ concurrency: parsed.integerOption('--concurrency') ??
105
+ commandDefaultNumber('discover-scan', '--concurrency', deep),
106
+ checkpoint: parsed.optionValue('--checkpoint'),
107
+ resume: parsed.hasOption('--resume'),
108
+ resolverText: parsed.optionValue('--resolver'),
109
+ observationSnapshot: parsed.optionValue('--observation-snapshot'),
110
+ allowlistSource: parsed.optionValue('--allowlist'),
111
+ filter,
112
+ events: parsed.hasOption('--events'),
113
+ plan: parsed.hasOption('--plan'),
114
+ ...terminalOptions(parsed),
115
+ ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
116
+ };
117
+ }
118
+ export const COLLECTION_ARGUMENT_PARSERS = Object.freeze({
119
+ lookup: parseLookupArguments,
120
+ bulk: parseBulkArguments,
121
+ discover: parseDiscoverArguments,
122
+ 'discover-scan': parseDiscoverScanArguments,
123
+ });