@slicedearth/whoisleuth-cli 2.3.0 → 2.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (264) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +19 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +68 -6
  7. package/cli/artifact-validation/case-response.mjs +47 -41
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +83 -61
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-command-runner.mjs +2 -0
  18. package/cli/bulk-output.mjs +29 -3
  19. package/cli/case-command.mjs +195 -0
  20. package/cli/case-pack.mjs +39 -31
  21. package/cli/command-argument-grammar.mjs +4 -0
  22. package/cli/command-reference.mjs +145 -75
  23. package/cli/completion.mjs +298 -79
  24. package/cli/config-profile.mjs +7 -5
  25. package/cli/ct-event-intake.mjs +12 -12
  26. package/cli/discover.mjs +4 -3
  27. package/cli/discovery-observation-snapshot.mjs +44 -16
  28. package/cli/discovery-scan-command-runner.mjs +2 -0
  29. package/cli/discovery-scan.mjs +7 -4
  30. package/cli/discovery-workflow.mjs +1 -1
  31. package/cli/domain-control-monitor.mjs +50 -20
  32. package/cli/domain-control-observations.mjs +149 -102
  33. package/cli/errors.mjs +33 -1
  34. package/cli/evidence-command-runner.mjs +16 -5
  35. package/cli/evidence-signing.mjs +2 -2
  36. package/cli/external-observation-mapping.mjs +7 -8
  37. package/cli/formatters/terminal-lookup.mjs +18 -46
  38. package/cli/formatters/terminal-shared.mjs +5 -3
  39. package/cli/formatters/terminal-technology.mjs +114 -0
  40. package/cli/inline-command-families.mjs +1 -0
  41. package/cli/interactive-launcher.mjs +2 -160
  42. package/cli/interchange-report.mjs +4 -8
  43. package/cli/investigation-artifacts.mjs +176 -0
  44. package/cli/investigation-checkpoint.mjs +10 -0
  45. package/cli/investigation-folder.mjs +114 -0
  46. package/cli/investigation-manifest.mjs +1 -112
  47. package/cli/investigation-package-review.mjs +103 -0
  48. package/cli/investigation-plan.mjs +32 -19
  49. package/cli/investigation-run.mjs +188 -33
  50. package/cli/local-document-checkpoint.mjs +170 -0
  51. package/cli/lookup-browser.mjs +5 -12
  52. package/cli/lookup-command-runner.mjs +17 -6
  53. package/cli/lookup-diff.mjs +37 -3
  54. package/cli/lookup-plan.mjs +11 -5
  55. package/cli/lookup-reconcile.mjs +10 -8
  56. package/cli/mail-review.mjs +66 -14
  57. package/cli/manual.mjs +1 -0
  58. package/cli/network-command-runner.mjs +2 -0
  59. package/cli/output-file.mjs +31 -5
  60. package/cli/registry-doctor.mjs +3 -1
  61. package/cli/registry-support.mjs +1 -1
  62. package/cli/retained-artifact-diff.mjs +2 -2
  63. package/cli/review-command-runner.mjs +45 -0
  64. package/cli/runner.mjs +55 -6
  65. package/cli/sharing-review.mjs +21 -34
  66. package/cli/signer-trust.mjs +104 -0
  67. package/cli/source-reliability.mjs +74 -83
  68. package/cli/terminal-input.mjs +168 -0
  69. package/cli/workflow-command-runner.mjs +68 -13
  70. package/docs/cli-reference.md +76 -30
  71. package/docs/cli.md +289 -25
  72. package/lib/abort.mjs +4 -1
  73. package/lib/availability.mjs +161 -112
  74. package/lib/bounded-file.mjs +3 -1
  75. package/lib/bounded-html-document.mjs +125 -0
  76. package/lib/bounded-json.mjs +43 -3
  77. package/lib/browser-library-profile.mjs +42 -51
  78. package/lib/browser-library-worker.mjs +112 -0
  79. package/lib/client-behavior-profile.mjs +3 -3
  80. package/lib/ct-search.mjs +11 -4
  81. package/lib/dns-delegation-health.mjs +24 -14
  82. package/lib/dns-inheritance-review.mjs +217 -0
  83. package/lib/dns-intelligence.mjs +36 -23
  84. package/lib/dns-mx.mjs +1 -1
  85. package/lib/dnssec-evidence-validation.mjs +11 -3
  86. package/lib/domain-change-packet.mjs +7 -2
  87. package/lib/domain-change-review.mjs +14 -8
  88. package/lib/domain-control-flight-recorder.mjs +219 -118
  89. package/lib/domain-control-manifest.mjs +117 -57
  90. package/lib/domain-posture-analysis.mjs +20 -7
  91. package/lib/domain-posture-parsers.mjs +88 -6
  92. package/lib/domain-posture.mjs +113 -26
  93. package/lib/evidence-export-privacy.mjs +561 -0
  94. package/lib/evidence-export.mjs +11 -592
  95. package/lib/evidence-report.mjs +3 -1
  96. package/lib/favicon.mjs +117 -41
  97. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  98. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  99. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  100. package/lib/hostname.mjs +2 -38
  101. package/lib/html-signals.mjs +82 -85
  102. package/lib/http-evidence-bounds.mjs +2 -1
  103. package/lib/http-intelligence.mjs +39 -26
  104. package/lib/lookup-cache.mjs +34 -11
  105. package/lib/lookup-child-profile-contract.mjs +24 -15
  106. package/lib/lookup-diagnostics.mjs +24 -1
  107. package/lib/lookup-progress.mjs +30 -13
  108. package/lib/lookup-response-contract.mjs +1048 -0
  109. package/lib/lookup-response.mjs +7 -10
  110. package/lib/lookup-source-progress.mjs +17 -14
  111. package/lib/lookup.mjs +48 -27
  112. package/lib/observed-network-context.mjs +43 -27
  113. package/lib/outbound-request-bounds.mjs +4 -1
  114. package/lib/page-fingerprints.mjs +69 -133
  115. package/lib/page-role-profile.mjs +12 -8
  116. package/lib/perceptual-hash.mjs +43 -14
  117. package/lib/rdap-bootstrap.mjs +72 -22
  118. package/lib/rdap-client.mjs +15 -7
  119. package/lib/rdap-normalization.mjs +33 -18
  120. package/lib/rdap-search-workbench.mjs +1 -1
  121. package/lib/rdap-transport.mjs +47 -20
  122. package/lib/rdap-types.mjs +2 -1
  123. package/lib/rdap.mjs +18 -3
  124. package/lib/registrable-domain.mjs +21 -1
  125. package/lib/registrar-standing-contract.mjs +1 -0
  126. package/lib/registry-capabilities.mjs +5 -1
  127. package/lib/registry-capability-catalogue.mjs +2 -1
  128. package/lib/registry-dates.mjs +2 -190
  129. package/lib/registry-insights.mjs +56 -18
  130. package/lib/response-policy.mjs +86 -48
  131. package/lib/risk-calibration-summary.mjs +17 -3
  132. package/lib/rpki-evidence.mjs +8 -5
  133. package/lib/safe-fetch.mjs +54 -47
  134. package/lib/security-txt.mjs +10 -7
  135. package/lib/service-binding-dns.mjs +8 -0
  136. package/lib/smtp-transport-review.mjs +27 -15
  137. package/lib/sslbl-certificates.generated.mjs +136 -135
  138. package/lib/static-html-analysis.mjs +117 -146
  139. package/lib/structured-data-identity.mjs +1 -0
  140. package/lib/technology-markup.mjs +100 -0
  141. package/lib/threat-intelligence-manifests.mjs +11 -139
  142. package/lib/threat-intelligence-runtime.mjs +24 -52
  143. package/lib/threat-intelligence-targets.mjs +6 -34
  144. package/lib/threat-intelligence-types.mjs +39 -0
  145. package/lib/typosquat-generator.mjs +19 -15
  146. package/lib/urlhaus-intelligence.mjs +8 -3
  147. package/lib/urlscan-intelligence.mjs +1 -1
  148. package/lib/website-security-posture.mjs +30 -6
  149. package/lib/website-technology.mjs +111 -71
  150. package/lib/whois-authority.mjs +14 -2
  151. package/lib/whois-chain.mjs +9 -2
  152. package/lib/whois-common-formats.mjs +5 -5
  153. package/lib/whois-contracts.mjs +2 -1
  154. package/lib/whois-dialects.mjs +2 -2
  155. package/lib/whois-parser-support.mjs +8 -1
  156. package/lib/whois-transport.mjs +23 -7
  157. package/package.json +2 -1
  158. package/packages/cases/case-attachment-model.mjs +83 -0
  159. package/packages/cases/case-evidence-model.mjs +156 -62
  160. package/packages/cases/case-export-input.mjs +53 -0
  161. package/packages/cases/case-migration-model.mjs +59 -25
  162. package/packages/cases/case-model.mjs +1 -0
  163. package/packages/cases/case-recheck-model.mjs +91 -0
  164. package/packages/cases/case-record-core.mjs +3 -2
  165. package/packages/cases/case-record-operations.mjs +89 -27
  166. package/packages/cases/case-record-projection.mjs +23 -20
  167. package/packages/cases/case-report.mjs +63 -52
  168. package/packages/cases/case-response-actions.mjs +654 -0
  169. package/packages/cases/case-response-model.mjs +41 -1129
  170. package/packages/cases/case-response-outcomes.mjs +317 -0
  171. package/packages/cases/case-response-packet-render.mjs +153 -0
  172. package/packages/cases/case-response-packet-types.mjs +1 -0
  173. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  174. package/packages/cases/case-response-packet.mjs +38 -587
  175. package/packages/cases/case-response-records.mjs +94 -0
  176. package/packages/cases/case-response-review-inputs.mjs +371 -0
  177. package/packages/cases/case-response-values.mjs +85 -0
  178. package/packages/cases/case-review-package.mjs +45 -0
  179. package/packages/cases/case-selection.mjs +46 -0
  180. package/packages/cases/case-storage-model.mjs +25 -0
  181. package/packages/cases/response-route-freshness.mjs +16 -0
  182. package/packages/comparison/capture-context.mjs +61 -0
  183. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  184. package/packages/comparison/page-similarity.mjs +2 -1
  185. package/packages/comparison/relationship-evidence.mjs +85 -8
  186. package/packages/comparison/relationship-provenance.mjs +93 -0
  187. package/packages/contracts/analyst-interchange.mjs +216 -0
  188. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  189. package/packages/contracts/analyst-review-state.mjs +5 -4
  190. package/packages/contracts/case-portability.mjs +279 -116
  191. package/packages/contracts/case-views-contract.mjs +12 -0
  192. package/packages/contracts/case-views.mjs +26 -0
  193. package/packages/contracts/cli-command-semantics.mjs +1 -0
  194. package/packages/contracts/cli-lookup.mjs +0 -1
  195. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  196. package/packages/contracts/domain-control-manifest.mjs +127 -73
  197. package/packages/contracts/domain-control-monitor.mjs +48 -45
  198. package/packages/contracts/domain-control-review.mjs +179 -121
  199. package/packages/contracts/domain-name.mjs +21 -0
  200. package/packages/contracts/external-observation-interchange.mjs +2 -1
  201. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  202. package/packages/contracts/http-url.mjs +2 -0
  203. package/packages/contracts/investigation-package-limits.mjs +24 -0
  204. package/packages/contracts/investigation-portability.mjs +56 -27
  205. package/packages/contracts/investigation-run.mjs +16 -0
  206. package/packages/contracts/ip-address.mjs +34 -0
  207. package/packages/contracts/lookup-evidence.mjs +35 -0
  208. package/packages/contracts/offline-comparison.mjs +1 -2
  209. package/packages/contracts/page-fingerprints.mjs +15 -0
  210. package/packages/contracts/risk-calibration.mjs +0 -1
  211. package/packages/contracts/schema-compatibility.mjs +1 -1
  212. package/packages/contracts/schema-lifecycle.mjs +83 -153
  213. package/packages/contracts/selected-file-limits.mjs +6 -0
  214. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  215. package/packages/contracts/web-capture.mjs +10 -0
  216. package/packages/contracts/workspace-portability.mjs +160 -225
  217. package/packages/evidence/artifact-integrity.mjs +8 -2
  218. package/packages/evidence/artifact-structure.mjs +105 -0
  219. package/packages/evidence/decision-fact.mjs +4 -1
  220. package/packages/evidence/domain-control-runtime.mjs +155 -27
  221. package/packages/evidence/domain-posture-context.mjs +79 -0
  222. package/packages/evidence/image-regions.mjs +56 -0
  223. package/packages/evidence/latest-observations.mjs +28 -0
  224. package/packages/evidence/lookup-target.mjs +104 -0
  225. package/packages/evidence/observation.mjs +16 -1
  226. package/packages/evidence/passphrase-encryption.mjs +44 -0
  227. package/packages/evidence/record-identity.mjs +72 -0
  228. package/packages/evidence/registry-dates.mjs +190 -0
  229. package/packages/evidence/retained-file.mjs +69 -0
  230. package/packages/evidence/review-age.mjs +14 -0
  231. package/packages/interchange/bagit.mjs +342 -0
  232. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  233. package/packages/interchange/crc32.mjs +17 -0
  234. package/packages/interchange/external-findings-import.mjs +89 -49
  235. package/packages/interchange/web-capture-import.mjs +382 -0
  236. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  237. package/packages/investigation/investigation-bagit.mjs +14 -0
  238. package/packages/investigation/investigation-capsule.mjs +33 -5
  239. package/packages/investigation/investigation-manifest.mjs +200 -0
  240. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  241. package/packages/investigation/investigation-package.mjs +200 -0
  242. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  243. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  244. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  245. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  246. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  247. package/packages/monitoring/analyst-review-state.mjs +38 -90
  248. package/packages/workspace/brand-profile-model.mjs +158 -20
  249. package/packages/workspace/bulk-columns.mjs +17 -0
  250. package/packages/workspace/bulk-review-model.mjs +11 -5
  251. package/packages/workspace/bulk-session-model.mjs +95 -20
  252. package/packages/workspace/bulk-sort.mjs +1 -0
  253. package/packages/workspace/case-views.mjs +102 -0
  254. package/packages/workspace/hostile-input.mjs +29 -8
  255. package/packages/workspace/investigation-template-model.mjs +19 -4
  256. package/packages/workspace/page-baseline.mjs +18 -15
  257. package/packages/workspace/relationship-observation-model.mjs +41 -26
  258. package/packages/workspace/watchlist-history.mjs +2 -2
  259. package/packages/workspace/watchlist-store.mjs +8 -2
  260. package/packages/workspace/website-snapshot-model.mjs +70 -30
  261. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  262. package/packages/workspace/workspace-archive.mjs +85 -21
  263. package/third-party-notices.txt +29 -2
  264. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,14 +1,18 @@
1
1
  import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../lib/project-metadata.mjs";
2
- import { LOOKUP_EVIDENCE_SCHEMA_VERSION, PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../lib/evidence-export.mjs";
2
+ import { LOOKUP_EVIDENCE_SCHEMA_VERSION, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../packages/contracts/lookup-evidence.mjs";
3
3
  import { RISK_CALIBRATION_DATASET_SCHEMA, RISK_CALIBRATION_REPORT_SCHEMA, } from "../packages/contracts/risk-calibration.mjs";
4
- import { CASE_SCHEMA_VERSION, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, WORKSPACE_ARCHIVE_VERSION, } from "../packages/contracts/case-portability.mjs";
4
+ import { CASE_SCHEMA_VERSION, CLI_CASE_PACK_INPUT_CASE_VERSIONS, MAX_CASE_STORE_BYTES, MAX_EDITABLE_CASE_INPUT_BYTES, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, WORKSPACE_ARCHIVE_VERSION, } from "../packages/contracts/case-portability.mjs";
5
5
  import { CLI_COMMAND_SEMANTICS, CLI_HELP_GROUP_ORDER, } from "../packages/contracts/cli-command-semantics.mjs";
6
6
  import { CLI_FAIL_POLICIES_BY_COMMAND } from "./fail-policy.mjs";
7
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES } from "../packages/investigation/investigation-manifest.mjs";
7
8
  const LEGACY_WORKSPACE_ARCHIVE_VERSIONS = SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS
8
9
  .filter((version) => version !== WORKSPACE_ARCHIVE_VERSION);
9
10
  const LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION = LEGACY_WORKSPACE_ARCHIVE_VERSIONS
10
11
  .map((version) => `version-${version}`)
11
12
  .join(' and ');
13
+ const PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS = SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS
14
+ .filter((version) => version > V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION && version !== LOOKUP_EVIDENCE_SCHEMA_VERSION)
15
+ .join(', ');
12
16
  const LEGACY_WORKSPACE_ARCHIVE_SCOPE = LEGACY_WORKSPACE_ARCHIVE_VERSIONS
13
17
  .map((version) => `v${version}`)
14
18
  .join(' and ');
@@ -24,12 +28,8 @@ const INVESTIGATION_PLAN_RECIPES = Object.freeze([
24
28
  'planned-domain-change',
25
29
  'post-change-verification',
26
30
  ]);
27
- const RUNNABLE_INVESTIGATION_PLAN_RECIPES = Object.freeze([
28
- 'domain-triage',
29
- 'lookalike-review',
30
- 'owned-domain-review',
31
- 'historical-comparison',
32
- ]);
31
+ const RUNNABLE_INVESTIGATION_PLAN_RECIPES = INVESTIGATION_PLAN_RECIPES;
32
+ const CLI_CASE_OPERATIONS = ['show', 'open', 'note', 'pin', 'assess', 'recheck'];
33
33
  const CLI_META_ACTIONS = Object.freeze([
34
34
  Object.freeze({
35
35
  id: 'help',
@@ -67,6 +67,8 @@ redirected or unsupported terminals continue to print this help. No request
67
67
  starts until a Lookup plan is shown and the analyst confirms collection.
68
68
  Use --json or --jsonl where supported for machine-readable stdout.
69
69
  Use --output <file> for atomic private file output and --force to replace it.
70
+ Use -- before positional filenames that begin with a hyphen; ./ also makes a
71
+ filename unambiguous, for example: whoisleuth verify-artifact -- ./-evidence.json.
70
72
  Use --palette auto, light, or dark after the command to select a fixed terminal
71
73
  colour palette; --no-color, NO_COLOR, and redirected output still suppress ANSI.
72
74
  Use --config <file> and --profile <name> for explicit versioned safe defaults.
@@ -132,6 +134,8 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
132
134
  '--palette': enumeration(['auto', 'light', 'dark']),
133
135
  '--network': flag(),
134
136
  '--json': flag(),
137
+ '--package': flag(),
138
+ '--folder': file(),
135
139
  '--quiet': flag('idempotent'),
136
140
  '--no-color': flag('idempotent'),
137
141
  '--common': flag(),
@@ -145,12 +149,14 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
145
149
  '--no-attribution': flag(),
146
150
  '--fast': flag(),
147
151
  '--deep': flag(),
152
+ '--exact-url': flag(),
148
153
  '--observer': text(),
149
154
  '--vantage': text(),
150
155
  '--plan': flag(),
151
156
  '--summary': flag(),
152
157
  '--verbose': flag(),
153
158
  '--browse': flag(),
159
+ '--interactive': flag(),
154
160
  '--save-lookup': file(),
155
161
  '--strict-exit': flag(),
156
162
  '--fail-on': optionDefinition('policy_list', {
@@ -159,6 +165,7 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
159
165
  '--events': flag(),
160
166
  '--jsonl': flag(),
161
167
  '--csv': flag(),
168
+ '--csv-with-metadata': flag(),
162
169
  '--domains': flag(),
163
170
  '--queries': flag(),
164
171
  '--registered-only': flag(),
@@ -190,6 +197,7 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
190
197
  '--mail-profile': enumeration(['standard', 'defensive-no-mail', 'parked']),
191
198
  '--sarif': flag(),
192
199
  '--owned-domain': flag(),
200
+ '--include-inherited-dns': flag(),
193
201
  '--trust-anchor': file(),
194
202
  '--owned-or-authorized': flag(),
195
203
  '--active-probe': flag(),
@@ -198,13 +206,15 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
198
206
  '--summary-json': flag(),
199
207
  '--passphrase-file': file(),
200
208
  '--manifest': file(),
201
- '--manifest-entry': enumeration(Array.from({ length: 16 }, (_, index) => `artifact-${index + 1}`)),
209
+ '--bagit': flag(),
210
+ '--manifest-entry': enumeration(Array.from({ length: MAX_INVESTIGATION_MANIFEST_ARTIFACTS }, (_, index) => `artifact-${index + 1}`)),
202
211
  '--search': text(),
203
212
  '--require-match': flag(),
204
213
  '--reveal': flag(),
205
214
  '--expect-content-digest': text(true),
206
215
  '--private-key-file': file(),
207
216
  '--public-key-file': file(),
217
+ '--trust-store-file': file(),
208
218
  '--mmdb': file(),
209
219
  '--audience': enumeration(['internal', 'trusted', 'public']),
210
220
  '--reviewed': flag(),
@@ -219,10 +229,20 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
219
229
  '--list': flag(),
220
230
  '--explain': enumeration(INVESTIGATION_PLAN_RECIPES),
221
231
  '--select': optionDefinition('text', { occurrence: 'repeatable', acceptsOptionLikeValue: true }),
232
+ '--use-artifact': optionDefinition('text', { occurrence: 'repeatable' }),
233
+ '--confirm-review': optionDefinition('text', { occurrence: 'repeatable' }),
222
234
  '--approve-network': flag(),
223
235
  '--left-session': text(true),
224
236
  '--right-session': text(true),
225
237
  '--compact': flag(),
238
+ '--case-id': text(),
239
+ '--domain': text(),
240
+ '--title': text(),
241
+ '--new-incident': flag(),
242
+ '--text': text(),
243
+ '--note-file': file(),
244
+ '--input': file(),
245
+ '--expect-file-digest': text(),
226
246
  });
227
247
  function constraint(value) {
228
248
  if (value.kind === 'mutually_exclusive') {
@@ -244,10 +264,15 @@ const QUIET_OUTPUT_CONSTRAINT = constraint({
244
264
  kind: 'mutually_exclusive',
245
265
  options: ['--quiet', '--output'],
246
266
  });
247
- const MACHINE_OUTPUT_OPTIONS = Object.freeze([
248
- '--json', '--jsonl', '--junit', '--csv', '--domains', '--queries', '--markdown',
249
- '--html', '--sarif', '--summary-json',
267
+ const PRESENTATION_OPTIONS = Object.freeze([
268
+ ['--json', 'JSON'], ['--jsonl', 'JSON Lines'], ['--junit', 'JUnit XML'],
269
+ ['--csv', 'CSV'],
270
+ ['--csv-with-metadata', 'CSV with evidence metadata'],
271
+ ['--domains', 'domain list'], ['--queries', 'query list'],
272
+ ['--markdown', 'Markdown'], ['--html', 'HTML'], ['--sarif', 'SARIF'],
273
+ ['--summary-json', 'summary JSON'],
250
274
  ]);
275
+ const MACHINE_OUTPUT_OPTIONS = Object.freeze(PRESENTATION_OPTIONS.map(([option]) => option));
251
276
  function optionSpec(command, option, scope) {
252
277
  const definition = CLI_OPTION_DEFINITIONS[option];
253
278
  const valueKind = definition.valueKind(command);
@@ -377,16 +402,22 @@ const COMMAND_SEEDS = Object.freeze({
377
402
  }),
378
403
  manifest: commandSeed({
379
404
  reference: {
380
- description: 'Record an ordered, path-free manifest for up to 16 local JSON artefacts.',
405
+ 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.`,
381
406
  example: 'whoisleuth manifest lookup.json comparison.json --workflow "domain review" --json',
382
- boundary: 'The command records hashes and bounded schema metadata only. It omits source paths and artefact contents and performs no network collection.',
407
+ 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.',
383
408
  },
384
- collection: { mode: 'offline', scope: 'Reads 1 to 16 local JSON artefacts capped at 32 MiB in total and retains no source paths.' },
409
+ collection: { mode: 'offline', 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.` },
385
410
  summary: 'Build an evidence manifest offline',
386
- options: ['--workflow', '--configuration-digest', '--json', '--quiet', '--no-color'],
387
- positionals: Object.freeze([positional('artefacts', 'file', 1, 16)]),
411
+ options: ['--workflow', '--configuration-digest', '--package', '--bagit', '--passphrase-file', '--folder', '--json', '--quiet', '--no-color'],
412
+ positionals: Object.freeze([positional('artefacts', 'file', 1, MAX_INVESTIGATION_MANIFEST_ARTIFACTS)]),
388
413
  constraints: Object.freeze([
389
414
  constraint({ kind: 'required', options: ['--workflow'] }),
415
+ constraint({ kind: 'requires_any', option: '--bagit', requiredOptions: ['--package', '--folder'] }),
416
+ constraint({ kind: 'excludes_all', option: '--bagit', excludedOptions: ['--passphrase-file'] }),
417
+ constraint({ kind: 'requires_all', option: '--package', requiredOptions: ['--output'] }),
418
+ constraint({ kind: 'requires_all', option: '--passphrase-file', requiredOptions: ['--package'] }),
419
+ constraint({ kind: 'mutually_exclusive', options: ['--package', '--json'] }),
420
+ constraint({ kind: 'excludes_all', option: '--folder', excludedOptions: ['--package', '--output', '--force'] }),
390
421
  ]),
391
422
  handlerOwner: 'inline',
392
423
  networkEffect: 'offline',
@@ -441,17 +472,18 @@ const COMMAND_SEEDS = Object.freeze({
441
472
  reference: {
442
473
  description: 'Collect registration evidence for one domain, IP, or ASN.',
443
474
  example: 'whoisleuth lookup example.test --deep --browse',
444
- boundary: 'Fast is the default. An ICANN-recognised public domain, reserved documentation domain, IP, or ASN may occupy command position as shorthand; it delegates to this same parser and URL-like input requires the explicit lookup command. Deep mode adds bounded WHOIS, DNS, HTTP, TLS, technology, posture, and network context where applicable. A full Deep homepage observation can derive fixed publication and delivery/cache summaries from the same response without retaining raw metadata values or making another request. --browse opens before collection, shows aggregate Fast progress or independently settled planned Deep sources, and then navigates allowlisted retained fields in the completed document. Press ? for help and / to search rendered panel text only. Closing during collection cancels without a partial document. --save-lookup writes the exact completed private JSON only after a normal browser close; it can contain normalised evidence omitted from panels and refuses an existing path.',
475
+ boundary: 'Fast is the default. An ICANN-recognised public domain, reserved documentation domain, IP, or ASN may occupy command position as shorthand; it delegates to this same parser and URL-like input requires the explicit lookup command. Deep mode adds bounded WHOIS, DNS, HTTP, TLS, technology, posture, and network context where applicable. --deep --exact-url explicitly sends the input URL path and query to the website, without its fragment; ordinary URL input sends only the hostname. --plan remains offline and omits the selected URL. Retained paths and page-derived text require review before sharing. A full Deep homepage observation can derive fixed publication and delivery/cache summaries from the same response without retaining raw metadata values or making another request. --browse opens before collection, shows aggregate Fast progress or independently settled planned Deep sources, and then navigates allowlisted retained fields in the completed document. Press ? for help and / to search rendered panel text only. Closing during collection cancels without a partial document. --save-lookup writes the exact completed private JSON only after a normal browser close; it can contain normalised evidence omitted from panels and refuses an existing path.',
445
476
  },
446
477
  collection: { mode: 'network', scope: 'Accepts one target. Fast is the default; deep collection must be selected explicitly.' },
447
478
  summary: 'Collect one domain, IP, or ASN',
448
- options: ['--json', '--junit', '--markdown', '--html', '--no-attribution', '--fast', '--deep', '--observer', '--vantage', '--plan', '--summary', '--verbose', '--browse', '--save-lookup', '--strict-exit', '--fail-on', '--events', '--quiet', '--no-color'],
479
+ options: ['--json', '--junit', '--markdown', '--html', '--no-attribution', '--fast', '--deep', '--exact-url', '--observer', '--vantage', '--plan', '--summary', '--verbose', '--browse', '--save-lookup', '--strict-exit', '--fail-on', '--events', '--quiet', '--no-color'],
449
480
  positionals: Object.freeze([positional('target', 'text', 0, 1, [], 'argv_or_stdin', ['--browse'])]),
450
481
  constraints: Object.freeze([
451
482
  constraint({ kind: 'mutually_exclusive', options: ['--json', '--junit', '--markdown', '--html'] }),
452
483
  constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
453
484
  constraint({ kind: 'mutually_exclusive', options: ['--summary', '--verbose'] }),
454
485
  constraint({ kind: 'requires_all', option: '--save-lookup', requiredOptions: ['--browse'] }),
486
+ constraint({ kind: 'requires_all', option: '--exact-url', requiredOptions: ['--deep'] }),
455
487
  constraint({ kind: 'excludes_all', option: '--summary', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
456
488
  constraint({ kind: 'excludes_all', option: '--verbose', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
457
489
  constraint({ kind: 'excludes_all', option: '--browse', excludedOptions: ['--json', '--junit', '--markdown', '--html', '--summary', '--verbose', '--events', '--plan', '--quiet'] }),
@@ -471,18 +503,18 @@ const COMMAND_SEEDS = Object.freeze({
471
503
  reference: {
472
504
  description: 'Triage newline-delimited domains, IPs, or ASNs with bounded concurrency.',
473
505
  example: 'cat domains.txt | whoisleuth bulk --jsonl',
474
- boundary: 'Fast and deep jobs use separate concurrency ceilings. Filters affect output only; collection failures and inconclusive authority states remain explicit in JSON, JSONL, and CSV.',
506
+ boundary: 'Fast and deep jobs use separate concurrency ceilings. Filters affect output only; collection failures and inconclusive authority states remain explicit in JSON, JSONL, and CSV. --csv-with-metadata adds source versions, separate observation and report times, collection origin and diagnostic states; --csv retains the compact columns.',
475
507
  },
476
508
  collection: { mode: 'network', scope: 'Accepts at most 500 fast or 50 deep targets, with concurrency capped at 8 fast or 3 deep.' },
477
509
  summary: 'Run bounded multi-target collection',
478
- options: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries', '--registered-only', '--inconclusive-only', '--errors-only', '--fast', '--deep', '--concurrency', '--checkpoint', '--resume', '--events', '--plan', '--fail-on', '--quiet', '--no-color'],
510
+ options: ['--json', '--jsonl', '--junit', '--csv', '--csv-with-metadata', '--domains', '--queries', '--registered-only', '--inconclusive-only', '--errors-only', '--fast', '--deep', '--concurrency', '--checkpoint', '--resume', '--events', '--plan', '--fail-on', '--quiet', '--no-color'],
479
511
  positionals: OPTIONAL_FILE_POSITIONAL,
480
512
  constraints: Object.freeze([
481
- constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries'] }),
513
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--junit', '--csv', '--csv-with-metadata', '--domains', '--queries'] }),
482
514
  constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--errors-only'] }),
483
515
  constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
484
516
  constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
485
- constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--junit', '--csv', '--domains', '--queries', '--events', '--checkpoint', '--resume', '--quiet', '--fail-on'] }),
517
+ constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--junit', '--csv', '--csv-with-metadata', '--domains', '--queries', '--events', '--checkpoint', '--resume', '--quiet', '--fail-on'] }),
486
518
  ]),
487
519
  handlerOwner: 'bulk',
488
520
  networkEffect: 'conditional_network',
@@ -535,7 +567,7 @@ const COMMAND_SEEDS = Object.freeze({
535
567
  }),
536
568
  discover: commandSeed({
537
569
  reference: {
538
- description: 'Generate bounded lookalike-domain candidates from local mutation rules.',
570
+ description: 'Generate bounded lookalike-domain candidates from a brand or registrable domain, including multi-part public suffixes.',
539
571
  example: 'whoisleuth discover example.test --preset common --jsonl',
540
572
  boundary: 'Generation and optional local snapshot comparison are offline. Candidates are leads only and are not resolved, registered, or classified as malicious.',
541
573
  },
@@ -561,20 +593,20 @@ const COMMAND_SEEDS = Object.freeze({
561
593
  reference: {
562
594
  description: 'Generate a bounded candidate set, collect a selected subset, and produce a supervised review queue.',
563
595
  example: 'whoisleuth discover-scan example.test --scan-limit 50 --checkpoint scan.json --json',
564
- boundary: 'This command performs network collection. Fast compact lookup is the default; deep mode is capped at 50 candidates. Allowlisting changes review priority only and shared infrastructure remains a lead, not attribution.',
596
+ boundary: 'This command performs network collection. Fast compact lookup is the default; deep mode is capped at 50 candidates. Allowlisting changes review priority only and shared infrastructure remains a lead, not attribution. --csv-with-metadata adds source versions, separate observation and report times, collection origin and diagnostic states; --csv retains the compact columns.',
565
597
  },
566
598
  collection: { mode: 'network', scope: 'Scans at most 500 fast or 50 deep candidates, with concurrency capped at 8 fast or 3 deep.' },
567
599
  summary: 'Collect a supervised candidate review queue',
568
- options: ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--fast', '--deep', '--scan-limit', '--chunk-size', '--concurrency', '--resolver', '--allowlist', '--checkpoint', '--resume', '--observation-snapshot', '--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only', '--events', '--plan', '--fail-on', '--json', '--jsonl', '--csv', '--domains', '--quiet', '--no-color'],
600
+ options: ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--fast', '--deep', '--scan-limit', '--chunk-size', '--concurrency', '--resolver', '--allowlist', '--checkpoint', '--resume', '--observation-snapshot', '--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only', '--events', '--plan', '--fail-on', '--json', '--jsonl', '--csv', '--csv-with-metadata', '--domains', '--quiet', '--no-color'],
569
601
  positionals: OPTIONAL_TEXT_POSITIONAL,
570
602
  constraints: Object.freeze([
571
- constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--csv', '--domains'] }),
603
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--csv', '--csv-with-metadata', '--domains'] }),
572
604
  constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
573
605
  constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
574
606
  constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only'] }),
575
607
  constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
576
608
  constraint({ kind: 'value_excludes', option: '--preset', value: 'common', excludedOptions: ['--dictionary'] }),
577
- constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--csv', '--domains', '--events', '--checkpoint', '--resume', '--observation-snapshot', '--quiet', '--fail-on'] }),
609
+ constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--csv', '--csv-with-metadata', '--domains', '--events', '--checkpoint', '--resume', '--observation-snapshot', '--quiet', '--fail-on'] }),
578
610
  ]),
579
611
  handlerOwner: 'discovery_scan',
580
612
  networkEffect: 'conditional_network',
@@ -589,11 +621,11 @@ const COMMAND_SEEDS = Object.freeze({
589
621
  reference: {
590
622
  description: 'Review bounded DNS mail, delegation, and domain-control posture.',
591
623
  example: 'whoisleuth posture example.test --mail-profile standard --json',
592
- boundary: 'Missing or failed DNS observations remain inconclusive and are not reported as absent controls.',
624
+ boundary: 'Missing or failed DNS observations remain inconclusive. --include-inherited-dns explicitly adds a bounded DMARC tree walk and direct parent-delegation sample; records retain their queried owner and source. No message is sent and receiver enforcement is not inferred.',
593
625
  },
594
- collection: { mode: 'network', scope: 'Accepts one domain and performs bounded RDAP, DNS, and conditional MTA-STS HTTPS requests.' },
626
+ collection: { mode: 'network', scope: 'Accepts one domain and performs bounded RDAP, DNS, and conditional MTA-STS HTTPS requests. --include-inherited-dns separately adds ancestor DMARC questions and direct DNS/TCP to sampled parent servers.' },
595
627
  summary: 'Review DNS and mail posture',
596
- options: ['--selectors', '--retired-selectors', '--mail-profile', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
628
+ options: ['--selectors', '--retired-selectors', '--mail-profile', '--include-inherited-dns', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
597
629
  positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
598
630
  constraints: Object.freeze([
599
631
  constraint({ kind: 'mutually_exclusive', options: ['--json', '--sarif'] }),
@@ -818,17 +850,21 @@ const COMMAND_SEEDS = Object.freeze({
818
850
  }),
819
851
  "verify-artifact": commandSeed({
820
852
  reference: {
821
- description: 'Validate a supported archive, claim passport, packet, manifest, saved Lookup, or supported Lookup-evidence export without printing evidence contents.',
853
+ description: 'Validate a supported archive, ordinary Case export, claim passport, packet, manifest, saved Lookup or Lookup-evidence export without printing evidence contents. Use --package for an evidence ZIP or encrypted package, with --passphrase-file to unlock it; use --folder ./evidence for an unencrypted evidence folder. Add --bagit with --package or --folder to verify BagIt 1.0.',
822
854
  example: 'whoisleuth verify-artifact report.json --manifest manifest.json --manifest-entry artifact-2 --json --strict-exit',
823
- boundary: 'Verification is offline and redacted. Encrypted archives require an explicitly supplied passphrase file; --strict-exit returns 4 when only an envelope or legacy projection integrity was verified.',
855
+ boundary: 'Verification is offline and redacted. ZIP and folder entries are reported separately without importing them. Case exports are checked without repairing content; ordinary package review also counts original references with matching bytes. Ordinary folders allow only the declared layout; BagIt allows bounded nested payloads and checks SHA-256/SHA-512 manifests without interpreting payloads. Symbolic links are refused. BagIt fetch.txt is never fetched; missing files, mismatches and unsupported algorithms remain explicit. Package digests describe bytes, not filesystem metadata or authenticity. In scripts, use --strict-exit: incomplete verification returns 4. Default exit 0 means the report was produced, not that its checks passed.',
824
856
  },
825
- collection: { mode: 'offline', scope: 'Reads one selected bounded artefact and, when explicitly supplied, one manifest whose selected entry is compared by exact bytes and canonical identity.' },
857
+ collection: { mode: 'offline', scope: 'Reads one selected bounded artefact, ZIP or explicit evidence folder and, when explicitly supplied, one manifest whose selected entry is compared by exact bytes and canonical identity.' },
826
858
  summary: 'Validate saved evidence offline',
827
- options: ['--passphrase-file', '--manifest', '--manifest-entry', '--json', '--strict-exit', '--quiet', '--no-color'],
828
- positionals: OPTIONAL_FILE_POSITIONAL,
859
+ options: ['--passphrase-file', '--manifest', '--manifest-entry', '--package', '--bagit', '--folder', '--json', '--strict-exit', '--quiet', '--no-color'],
860
+ positionals: Object.freeze([positional('source', 'file', 0, 1, [], 'argv_or_stdin', ['--package'])]),
829
861
  constraints: Object.freeze([
862
+ constraint({ kind: 'requires_any', option: '--bagit', requiredOptions: ['--package', '--folder'] }),
863
+ constraint({ kind: 'excludes_all', option: '--bagit', excludedOptions: ['--passphrase-file', '--manifest', '--manifest-entry'] }),
830
864
  constraint({ kind: 'requires_all', option: '--manifest', requiredOptions: ['--manifest-entry'] }),
831
865
  constraint({ kind: 'requires_all', option: '--manifest-entry', requiredOptions: ['--manifest'] }),
866
+ constraint({ kind: 'excludes_all', option: '--package', excludedOptions: ['--manifest', '--manifest-entry'] }),
867
+ constraint({ kind: 'excludes_all', option: '--folder', excludedOptions: ['--package', '--manifest', '--manifest-entry', '--passphrase-file'] }),
832
868
  ]),
833
869
  handlerOwner: 'inline',
834
870
  networkEffect: 'offline',
@@ -863,7 +899,7 @@ const COMMAND_SEEDS = Object.freeze({
863
899
  reference: {
864
900
  description: `Summarise or search one current version-${WORKSPACE_ARCHIVE_VERSION} workspace archive, with exact ${LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION} support and redacted output by default.`,
865
901
  example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
866
- boundary: 'Exact matches require --reveal. Retired and future archive versions are rejected without changing data. The archive is read locally and is never uploaded.',
902
+ boundary: 'Exact values require --reveal. New content comparisons use the reported sorted-json-v2:sha256 identity with --expect-content-digest; bare sha256 hashes retain their legacy locale-sensitive meaning. Retired and future archives are rejected. The archive is read locally and is never uploaded.',
867
903
  },
868
904
  collection: { mode: 'offline', scope: `Reads one selected bounded workspace archive v${WORKSPACE_ARCHIVE_VERSION}, retains exact ${LEGACY_WORKSPACE_ARCHIVE_SCOPE} compatibility, and redacts output by default.` },
869
905
  summary: 'Inspect an archive locally',
@@ -906,19 +942,19 @@ const COMMAND_SEEDS = Object.freeze({
906
942
  }),
907
943
  "verify-signature": commandSeed({
908
944
  reference: {
909
- description: 'Verify the cryptographic signature of one signed evidence package and report embedded-artefact assurance separately.',
910
- example: 'whoisleuth verify-signature packet.signed.json --json',
911
- boundary: 'A valid signature proves package consistency for the embedded key. It does not upgrade failed or unsupported embedded-artefact assurance or establish the holder\'s real-world identity or authority.',
945
+ description: 'Verify one signed package and report embedded-artefact assurance separately. --trust-store-file also checks an explicit local fingerprint policy and emits a signer-trust report; unknown, retired, revoked or future-reviewed entries exit 4.',
946
+ example: 'whoisleuth verify-signature packet.signed.json --trust-store-file trust.json --json',
947
+ boundary: 'A valid signature proves package consistency for the embedded key, not identity, authority or evidence accuracy. With --trust-store-file, unknown, retired, revoked or future-reviewed entries exit 4 even if --public-key-file matches. Replacement fingerprints need their own trusted entry; no signing date overrides current revocation.',
912
948
  },
913
- collection: { mode: 'offline', scope: 'Reads one selected signed package and optional local public key.' },
949
+ collection: { mode: 'offline', scope: 'Reads one selected signed package, optional local public key and explicit fingerprint trust file. No automatic trust discovery or network requests.' },
914
950
  summary: 'Verify a signed evidence package',
915
- options: ['--public-key-file', '--json', '--quiet', '--no-color'],
951
+ options: ['--public-key-file', '--trust-store-file', '--json', '--quiet', '--no-color'],
916
952
  positionals: OPTIONAL_FILE_POSITIONAL,
917
953
  constraints: EMPTY_CONSTRAINTS,
918
954
  handlerOwner: 'evidence',
919
955
  networkEffect: 'offline',
920
956
  common: false,
921
- schemaIdentifiers: Object.freeze(['whoisleuth.evidence-signature-verification']),
957
+ schemaIdentifiers: Object.freeze(['whoisleuth.evidence-signature-verification', 'whoisleuth.evidence-signer-trust-store', 'whoisleuth.evidence-signer-trust-report']),
922
958
  primaryArtefacts: Object.freeze([]),
923
959
  planSupport: false,
924
960
  additionalOutputFormats: Object.freeze([]),
@@ -1072,13 +1108,29 @@ const COMMAND_SEEDS = Object.freeze({
1072
1108
  additionalOutputFormats: Object.freeze([]),
1073
1109
  bootstrapProfile: 'allowed',
1074
1110
  }),
1111
+ case: commandSeed({
1112
+ reference: {
1113
+ description: 'Show or open a local Case, append a note or evidence pin, record an assessment, or retain an offline recheck. Use --input for pin, assessment and recheck JSON; --text or --note-file for a note. Mutations require --output and always write the complete current Case export.',
1114
+ example: 'whoisleuth case open --domain example.test --output cases.json\n whoisleuth case show cases.json\n whoisleuth case note cases.json --text "Review the retained observation" --output cases.json --force',
1115
+ boundary: 'No database, browser launch, request or external report is created. Select --case-id when a file contains multiple Cases. Existing files require --force; --expect-file-digest sha256:<digest> additionally checks the exact file reviewed earlier. Source and output leases reject concurrent changes. Interrupted .workflow.lock files require deliberate inspection. Recheck records supplied observations; it does not collect them. Not reproduced requires an existing saved question, a complete observation and comparable conditions. Working exports include private analyst content and file references, not attached file bytes.',
1116
+ },
1117
+ collection: { mode: 'offline', scope: `Reads exact Case schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')}. Input is bounded to ${MAX_EDITABLE_CASE_INPUT_BYTES / 1024 / 1024} MiB including formatting; the complete canonical Case store must fit ${MAX_CASE_STORE_BYTES / 1024 / 1024} MiB without pruning. Writes current schema ${CASE_SCHEMA_VERSION}.` },
1118
+ summary: 'Review and update ordinary local Case files',
1119
+ options: ['--case-id', '--domain', '--title', '--new-incident', '--text', '--note-file', '--input', '--expect-file-digest', '--json', '--no-color'],
1120
+ positionals: Object.freeze([positional('operation', 'enum', 1, 1, CLI_CASE_OPERATIONS), positional('source', 'file', 0, 1)]),
1121
+ constraints: Object.freeze([constraint({ kind: 'mutually_exclusive', options: ['--text', '--note-file'] })]),
1122
+ handlerOwner: 'inline', networkEffect: 'offline', common: true,
1123
+ schemaIdentifiers: Object.freeze(['whoisleuth.case-export']),
1124
+ primaryArtefacts: Object.freeze(['Case export']), planSupport: false,
1125
+ additionalOutputFormats: Object.freeze([]), bootstrapProfile: 'allowed',
1126
+ }),
1075
1127
  "case-pack": commandSeed({
1076
1128
  reference: {
1077
- description: `Package browser-created Case-schema-${CASE_SCHEMA_VERSION} records as a reviewed, audience-specific Case-pack v2.`,
1129
+ description: `Package browser-created Case records from schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')} as a reviewed, audience-specific Case-pack v2 with current schema ${CASE_SCHEMA_VERSION}.`,
1078
1130
  example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
1079
1131
  boundary: 'The command is an offline handoff from the browser Case workflow: it creates a new package, never creates or mutates a durable Case, never mutates the source archive, and requires an explicit review acknowledgement.',
1080
1132
  },
1081
- collection: { mode: 'offline', scope: `Reads one bounded Case-schema-${CASE_SCHEMA_VERSION} browser export and writes a separate audience-specific Case-pack v2.` },
1133
+ collection: { mode: 'offline', scope: `Reads one bounded Case export from schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')} and writes a separate audience-specific Case-pack v2.` },
1082
1134
  summary: 'Build a reviewed case package',
1083
1135
  options: ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
1084
1136
  positionals: OPTIONAL_FILE_POSITIONAL,
@@ -1098,7 +1150,7 @@ const COMMAND_SEEDS = Object.freeze({
1098
1150
  reference: {
1099
1151
  description: 'Build an integrity-protected desired-state manifest or compare one with supplied observations.',
1100
1152
  example: 'whoisleuth domain-control domain-control-input.json --json',
1101
- boundary: 'The command is offline and changes no registrar, DNS, mail, or certificate configuration. Only complete supplied observations can produce drift.',
1153
+ 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.',
1102
1154
  },
1103
1155
  collection: { mode: 'offline', scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
1104
1156
  summary: 'Build or review a domain control manifest',
@@ -1182,7 +1234,7 @@ const COMMAND_SEEDS = Object.freeze({
1182
1234
  example: 'whoisleuth sharing-review packet.json --marking amber --recipient-scope organization --purpose "Reviewed incident handoff" --human-reviewed --personal-data-reviewed --redactions-confirmed --json',
1183
1235
  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.',
1184
1236
  },
1185
- collection: { mode: 'offline', scope: 'Reads one artefact capped at 15 MiB, emits only bounded schema/version metadata and no content values, and performs no transmission.' },
1237
+ collection: { mode: 'offline', scope: 'Reads one bounded artefact, emits only schema/version metadata and no content values, and performs no transmission.' },
1186
1238
  summary: 'Lint an artefact before deliberate sharing',
1187
1239
  options: ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
1188
1240
  positionals: OPTIONAL_FILE_POSITIONAL,
@@ -1226,12 +1278,12 @@ const COMMAND_SEEDS = Object.freeze({
1226
1278
  "workflow-run": commandSeed({
1227
1279
  reference: {
1228
1280
  description: 'Execute approved steps from a fixed investigation recipe and emit a resumable checkpoint.',
1229
- example: 'whoisleuth workflow-run domain-triage example.test --resume run.json --select export=saved-lookup.json --json --output run-next.json',
1230
- boundary: 'Only installed recipe commands can run. Network steps require explicit approval for each invocation. Repeat --select in placeholder order for one step; each bounded value replaces one exact placeholder and cannot start with a hyphen, become an option, or invoke a shell.',
1281
+ example: 'whoisleuth workflow-run domain-triage example.test --approve-network --json --output run.json',
1282
+ boundary: 'Only installed recipe commands can run. Network steps require explicit approval for each invocation. New runs connect compatible earlier outputs using the recipe defaults. Use --use-artifact <step-id>:<input-number>=<earlier-step-id> to override a connection; input numbers start at 1. Repeat --select for remaining placeholders in order, or supply every input for a step to replace its connections with files. Values stay literal and cannot start with a hyphen or invoke a shell. Optional --interactive prompts on terminal stderr for missing inputs; a blank answer pauses. It grants neither network approval nor human-review confirmation. A step declaring human review still requires --confirm-review <step-id> for that invocation. Checkpoints do not grant later approvals. Resumes preserve recorded connections. Content digests identify retained output, not authenticity or freshness. Partial collections pause for review and are not recollected on resume; failed validation or export steps remain retryable. Diagnostics go to stderr. File output holds exclusive adjacent locks and refuses concurrently changed state files.',
1231
1283
  },
1232
1284
  collection: { mode: 'network', scope: 'Runs only fixed-recipe steps; network collection requires --approve-network and unresolved analyst selections pause.' },
1233
1285
  summary: 'Execute approved fixed-recipe steps',
1234
- options: ['--select', '--approve-network', '--resume', '--json', '--quiet', '--no-color'],
1286
+ options: ['--select', '--use-artifact', '--confirm-review', '--approve-network', '--resume', '--interactive', '--json', '--quiet', '--no-color'],
1235
1287
  positionals: Object.freeze([
1236
1288
  positional('recipe', 'enum', 1, 1, RUNNABLE_INVESTIGATION_PLAN_RECIPES),
1237
1289
  positional('subject', 'text', 1, 1),
@@ -1250,7 +1302,7 @@ const COMMAND_SEEDS = Object.freeze({
1250
1302
  reference: {
1251
1303
  description: 'Compare an earlier and later artefact from the same retained Lookup, Bulk-session, or domain-portfolio family.',
1252
1304
  example: 'whoisleuth diff earlier.json later.json --json',
1253
- boundary: 'Comparison is offline: the left input is earlier and the right input is later. Inputs must belong to the same supported family. For a multi-session Bulk export, --left-session selects a session from the left file and --right-session selects one from the right; missing, unavailable, equal, and different evidence remain separate states.',
1305
+ boundary: 'Comparison is offline: the left input is earlier and the right input is later. Inputs must belong to the same supported family. Saved Lookups can describe the same or different domains; same-domain comparisons preserve observation times and collection uncertainty. For a multi-session Bulk export, --left-session selects a session from the left file and --right-session selects one from the right; missing, unavailable, equal, and different evidence remain separate states.',
1254
1306
  },
1255
1307
  collection: { mode: 'offline', scope: 'Reads two compatible retained artefacts capped at 8 MiB each and retains no source paths.' },
1256
1308
  summary: 'Compare two compatible retained artefacts',
@@ -1310,7 +1362,7 @@ const COMMAND_SEEDS = Object.freeze({
1310
1362
  reference: {
1311
1363
  description: 'Convert one saved lookup into a versioned evidence report.',
1312
1364
  example: 'whoisleuth export lookup.json --markdown',
1313
- boundary: `Saved Lookup versions 1 and 2 are capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Current schema-${LOOKUP_EVIDENCE_SCHEMA_VERSION} exports preserve evidence-source attribution and limitations; published v2 schema ${PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION} and exact v1 schema ${V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION} remain readable, while other historical and unreleased shapes are unsupported. Markdown and HTML include a presentation-only generator footer unless --no-attribution is selected; JSON retains bounded generator provenance. Compact output intentionally omits raw registry payloads.`,
1365
+ boundary: `Saved Lookup versions 1 and 2 are capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Current schema-${LOOKUP_EVIDENCE_SCHEMA_VERSION} exports preserve evidence-source attribution and limitations; published v2 schemas ${PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS} and exact v1 schema ${V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION} remain readable, while other historical and unreleased shapes are unsupported. Markdown and HTML include a presentation-only generator footer unless --no-attribution is selected; JSON retains bounded generator provenance. Compact output intentionally omits raw registry payloads.`,
1314
1366
  },
1315
1367
  collection: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
1316
1368
  summary: 'Convert a lookup to an evidence report',
@@ -1338,17 +1390,12 @@ const HELP_COMMANDS_BY_GROUP = Object.freeze(Object.fromEntries(CLI_HELP_GROUP_O
1338
1390
  ])));
1339
1391
  function documentationMetadata(seed, commonOptions, commandOptions, positionals) {
1340
1392
  const explicitAuthorisationRequired = commandOptions.some((option) => (option === '--owned-or-authorized' || option === '--active-probe' || option === '--approve-network'));
1341
- const outputOptionFormats = [
1342
- ['--json', 'JSON'], ['--jsonl', 'JSON Lines'], ['--junit', 'JUnit XML'],
1343
- ['--csv', 'CSV'], ['--domains', 'domain list'], ['--queries', 'query list'],
1344
- ['--markdown', 'Markdown'], ['--html', 'HTML'], ['--sarif', 'SARIF'],
1345
- ['--summary-json', 'summary JSON'],
1346
- ];
1393
+ const presentationOptions = Object.freeze(PRESENTATION_OPTIONS
1394
+ .filter(([option]) => commandOptions.includes(option))
1395
+ .map(([option, format]) => Object.freeze({ option, format })));
1347
1396
  const outputFormats = new Set(['terminal']);
1348
- for (const [option, label] of outputOptionFormats) {
1349
- if (commandOptions.includes(option))
1350
- outputFormats.add(label);
1351
- }
1397
+ for (const { format } of presentationOptions)
1398
+ outputFormats.add(format);
1352
1399
  for (const format of seed.additionalOutputFormats)
1353
1400
  outputFormats.add(format);
1354
1401
  const positionalLimits = positionals.map((item) => (`${item.name}: ${item.minimum}-${item.maximum} ${item.valueKind} value${item.maximum === 1 ? '' : 's'}`));
@@ -1373,14 +1420,19 @@ function documentationMetadata(seed, commonOptions, commandOptions, positionals)
1373
1420
  : []),
1374
1421
  ]),
1375
1422
  outputFormats: Object.freeze([...outputFormats]),
1423
+ presentationOptions,
1424
+ fileOutput: commonOptions.includes('--output'),
1376
1425
  primaryEvidenceArtefacts: seed.primaryArtefacts,
1377
1426
  });
1378
1427
  }
1428
+ function expandedOptionValues(specification) {
1429
+ return specification.valueKind === 'enum' && specification.values.join('|').length > 60;
1430
+ }
1379
1431
  function optionUsage(specification) {
1380
1432
  if (specification.arity === 0)
1381
1433
  return specification.option;
1382
1434
  if (specification.valueKind === 'enum') {
1383
- return `${specification.option} <${specification.values.join('|')}>`;
1435
+ return `${specification.option} <${expandedOptionValues(specification) ? specification.option.slice(2) : specification.values.join('|')}>`;
1384
1436
  }
1385
1437
  if (specification.valueKind === 'policy_list')
1386
1438
  return `${specification.option} <policy[,policy...]>`;
@@ -1470,11 +1522,34 @@ function commandDefinition(command) {
1470
1522
  function metaActionDefinition(id) {
1471
1523
  return CLI_META_ACTION_BY_ID[id];
1472
1524
  }
1525
+ function cliInvocationOptionIndices(argv) {
1526
+ const namedCommand = isCliCommand(argv[0]) ? argv[0] : null;
1527
+ const options = new Map(commandDefinition(namedCommand ?? 'lookup').grammar.options
1528
+ .map((option) => [option.option, option]));
1529
+ const indices = new Set();
1530
+ for (let index = namedCommand ? 1 : 0; index < argv.length; index += 1) {
1531
+ const argument = argv[index];
1532
+ if (argument === '--')
1533
+ break;
1534
+ if (!argument.startsWith('-'))
1535
+ continue;
1536
+ indices.add(index);
1537
+ const option = options.get(argument);
1538
+ const value = argv[index + 1];
1539
+ if (option?.arity === 1 && value !== undefined) {
1540
+ if (value === '--' && !option.acceptsOptionLikeValue)
1541
+ break;
1542
+ index += 1;
1543
+ }
1544
+ }
1545
+ return indices;
1546
+ }
1473
1547
  function cliMetaActionForInvocation(argv) {
1548
+ const optionIndices = cliInvocationOptionIndices(argv);
1474
1549
  for (const action of CLI_META_ACTIONS) {
1475
1550
  const matches = action.scope === 'root_only'
1476
1551
  ? action.aliases.includes(argv[0] ?? '')
1477
- : argv.some((argument) => action.aliases.includes(argument));
1552
+ : argv.some((argument, index) => optionIndices.has(index) && action.aliases.includes(argument));
1478
1553
  if (matches)
1479
1554
  return action;
1480
1555
  }
@@ -1522,15 +1597,8 @@ function commandOwnsOption(command, option) {
1522
1597
  return commandDefinition(command).grammar.options.some((candidate) => candidate.option === option);
1523
1598
  }
1524
1599
  function invocationHasFlag(command, args, flag) {
1525
- for (let index = 0; index < args.length; index += 1) {
1526
- const argument = args[index];
1527
- if (argument === flag)
1528
- return true;
1529
- const specification = argument ? commandOptionSpec(command, argument) : null;
1530
- if (specification?.arity === 1)
1531
- index += 1;
1532
- }
1533
- return false;
1600
+ const argv = [command, ...args];
1601
+ return [...cliInvocationOptionIndices(argv)].some((index) => argv[index] === flag);
1534
1602
  }
1535
1603
  function cliInvocationNetworkEffect(command, args) {
1536
1604
  const effect = commandDefinition(command).execution.networkEffect;
@@ -1551,6 +1619,8 @@ function cliInvocationNetworkEffect(command, args) {
1551
1619
  function commandHelp(command) {
1552
1620
  const detail = COMMAND_DETAILS[command];
1553
1621
  const collection = COMMAND_COLLECTION[command];
1554
- return `WHOISleuth ${command}\n${detail.description}\n\nUsage:\n ${COMMAND_USAGE[command]}\n\nExample:\n ${detail.example}\n\nCollection:\n ${collection.mode === 'offline' ? 'Offline' : 'Network'}: ${collection.scope}\n\nBoundary:\n ${detail.boundary}\n\nRun "whoisleuth --help" to see the grouped command list.\n`;
1622
+ const values = commandDefinition(command).grammar.options.filter(expandedOptionValues)
1623
+ .map(option => `\n${option.option} values:\n${option.values.map(value => ` ${value}`).join('\n')}\n`).join('');
1624
+ return `WHOISleuth ${command}\n${detail.description}\n\nUsage:\n ${COMMAND_USAGE[command]}\n\nExample:\n ${detail.example}\n\nCollection:\n ${collection.mode === 'offline' ? 'Offline' : 'Network'}: ${collection.scope}\n\nBoundary:\n ${detail.boundary}\n${values}\nRun "whoisleuth --help" to see the grouped command list.\n`;
1555
1625
  }
1556
- export { CLI_COMMAND_REGISTRY, CLI_COMMANDS, CLI_META_ACTIONS, COMMAND_COLLECTION, COMMAND_DESCRIPTIONS, COMMAND_DETAILS, COMMAND_USAGE, FILE_POSITIONAL_COMMANDS, HELP, HELP_COMMANDS_BY_GROUP, INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, OPTIONS_BY_COMMAND, cliMetaActionForInvocation, cliInvocationNetworkEffect, commandOptionSpec, commandOwnsOption, commandPositionalSpecs, commonOptionsForCommand, commandDefinition, commandHelp, isCliCommand, metaActionDefinition, };
1626
+ export { CLI_COMMAND_REGISTRY, CLI_CASE_OPERATIONS, CLI_COMMANDS, CLI_META_ACTIONS, COMMAND_COLLECTION, COMMAND_DESCRIPTIONS, COMMAND_DETAILS, COMMAND_USAGE, FILE_POSITIONAL_COMMANDS, HELP, HELP_COMMANDS_BY_GROUP, INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, OPTIONS_BY_COMMAND, cliMetaActionForInvocation, cliInvocationOptionIndices, cliInvocationNetworkEffect, commandOptionSpec, commandOwnsOption, commandPositionalSpecs, commonOptionsForCommand, commandDefinition, commandHelp, isCliCommand, metaActionDefinition, };