@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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 (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/cli/arguments.mjs CHANGED
@@ -1,68 +1,22 @@
1
1
  import { parseInspectArchiveArguments, parseSignArtifactArguments, parseVerifySignatureArguments, } from "./evidence-command-arguments.mjs";
2
- import { CliUsageError } from "./errors.mjs";
3
- import { INVESTIGATION_PLAN_RECIPES, } from "./investigation-plan.mjs";
2
+ import { CliUsageError, hasUnsafeCliText } from "./errors.mjs";
3
+ import { INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, } from "./investigation-plan.mjs";
4
4
  import { parseCliFailPolicies } from "./fail-policy.mjs";
5
+ import { isDirectLookupTarget } from "../lib/classify.mjs";
6
+ import { CLI_COMMANDS, cliMetaActionForInvocation, isCliCommand, } from "./command-reference.mjs";
5
7
  const MAX_CLI_ARGUMENTS = 32;
6
8
  const MAX_CLI_ARGUMENT_LENGTH = 1024;
7
- const CLI_COMMANDS = [
8
- 'completion',
9
- 'doctor',
10
- 'commands',
11
- 'manual',
12
- 'manifest',
13
- 'map-observations',
14
- 'oam-export',
15
- 'lookup',
16
- 'bulk',
17
- 'ct-search',
18
- 'ct-intake',
19
- 'discover',
20
- 'discover-scan',
21
- 'posture',
22
- 'http',
23
- 'tls',
24
- 'registry-support',
25
- 'registry-doctor',
26
- 'registry-cohort',
27
- 'registry-scaffold',
28
- 'risk-calibrate',
29
- 'lookalike-calibrate',
30
- 'verify-artifact',
31
- 'inspect-archive',
32
- 'sign-artifact',
33
- 'verify-signature',
34
- 'source-report',
35
- 'compare',
36
- 'page-compare',
37
- 'mail-review',
38
- 'review-evidence',
39
- 'brief',
40
- 'case-pack',
41
- 'domain-control',
42
- 'monitor-once',
43
- 'assurance',
44
- 'change-packet',
45
- 'sharing-review',
46
- 'workflow-plan',
47
- 'workflow-run',
48
- 'diff',
49
- 'reconcile',
50
- 'timeline',
51
- 'export',
52
- ];
53
9
  function boundedArgument(value) {
54
- if (typeof value !== 'string' || value.length > MAX_CLI_ARGUMENT_LENGTH || /[\x00-\x08\x0b\x0c\x0e-\x1f\x7f]/.test(value)) {
10
+ if (typeof value !== 'string' || value.length > MAX_CLI_ARGUMENT_LENGTH || hasUnsafeCliText(value)) {
55
11
  throw new CliUsageError('Arguments must be bounded text without control characters.');
56
12
  }
57
13
  return value;
58
14
  }
59
- function isCliCommand(value) {
60
- return CLI_COMMANDS.includes(value);
61
- }
62
15
  function extractFileOutputArguments(argv) {
63
16
  const retained = [];
64
17
  let destination = null;
65
18
  let force = false;
19
+ let palette = null;
66
20
  for (let index = 0; index < argv.length; index++) {
67
21
  const argument = argv[index];
68
22
  if (argument === undefined)
@@ -80,19 +34,32 @@ function extractFileOutputArguments(argv) {
80
34
  throw new CliUsageError('--force may be supplied only once.');
81
35
  force = true;
82
36
  }
37
+ else if (index > 0 && argument === '--palette') {
38
+ if (palette !== null)
39
+ throw new CliUsageError('--palette may be supplied only once.');
40
+ const value = argv[++index];
41
+ if (value !== 'auto' && value !== 'light' && value !== 'dark') {
42
+ throw new CliUsageError('--palette requires auto, light, or dark.');
43
+ }
44
+ palette = value;
45
+ }
83
46
  else {
84
47
  retained.push(argument);
85
48
  }
86
49
  }
87
50
  if (force && destination === null)
88
51
  throw new CliUsageError('--force requires --output.');
89
- return { argv: retained, destination, force };
52
+ return { argv: retained, destination, force, palette };
90
53
  }
91
54
  function parseCliArguments(rawArgv) {
92
55
  if (!Array.isArray(rawArgv) || rawArgv.length > MAX_CLI_ARGUMENTS) {
93
56
  throw new CliUsageError(`At most ${MAX_CLI_ARGUMENTS} command arguments are supported.`);
94
57
  }
95
- const extracted = extractFileOutputArguments(rawArgv.map(boundedArgument));
58
+ const boundedArguments = rawArgv.map(boundedArgument);
59
+ const metaAction = cliMetaActionForInvocation(boundedArguments);
60
+ if (metaAction)
61
+ return parseCliArgumentsCore(boundedArguments);
62
+ const extracted = extractFileOutputArguments(boundedArguments);
96
63
  const parsed = parseCliArgumentsCore(extracted.argv);
97
64
  if ('quiet' in parsed && parsed.quiet && extracted.destination !== null) {
98
65
  throw new CliUsageError('--quiet cannot be combined with --output.');
@@ -100,118 +67,53 @@ function parseCliArguments(rawArgv) {
100
67
  if ('events' in parsed && parsed.events && extracted.destination !== null) {
101
68
  throw new CliUsageError('--events cannot be combined with --output because completion must describe the final output delivery.');
102
69
  }
103
- return extracted.destination === null
104
- ? parsed
105
- : { ...parsed, destination: extracted.destination, ...(extracted.force ? { force: true } : {}) };
70
+ if ('browse' in parsed && parsed.browse === true && extracted.destination !== null) {
71
+ throw new CliUsageError('--browse cannot be combined with --output because it requires an interactive terminal.');
72
+ }
73
+ return {
74
+ ...parsed,
75
+ ...(extracted.destination !== null ? { destination: extracted.destination } : {}),
76
+ ...(extracted.force ? { force: true } : {}),
77
+ ...(extracted.palette ? { palette: extracted.palette } : {}),
78
+ };
106
79
  }
107
80
  function parseCliArgumentsCore(argv) {
108
81
  if (!argv.length)
109
82
  return { action: 'help' };
110
83
  const firstArgument = argv[0] ?? '';
111
- const helpRequested = argv.includes('--help') || argv.includes('-h');
112
- if (helpRequested) {
84
+ const metaAction = cliMetaActionForInvocation(argv);
85
+ if (metaAction?.id === 'help') {
113
86
  if (argv.length === 1)
114
87
  return { action: 'help' };
115
- if (argv.length === 2 && isCliCommand(firstArgument))
116
- return { action: 'help', command: firstArgument };
88
+ if (argv.length === 2 && (isCliCommand(firstArgument) || isDirectLookupTarget(firstArgument))) {
89
+ return { action: 'help', command: isCliCommand(firstArgument) ? firstArgument : 'lookup' };
90
+ }
117
91
  throw new CliUsageError('Help accepts only an optional command name.');
118
92
  }
119
- if (firstArgument === '--version' || firstArgument === '-V') {
93
+ if (metaAction?.id === 'version') {
120
94
  if (argv.length !== 1)
121
95
  throw new CliUsageError('--version does not accept other arguments.');
122
96
  return { action: 'version' };
123
97
  }
124
- const command = firstArgument;
125
- if (!isCliCommand(command)) {
126
- const displayedCommand = command.length > 80 ? `${command.slice(0, 79)}…` : command;
98
+ let command;
99
+ let directLookup = false;
100
+ if (isCliCommand(firstArgument))
101
+ command = firstArgument;
102
+ else if (isDirectLookupTarget(firstArgument)) {
103
+ command = 'lookup';
104
+ directLookup = true;
105
+ }
106
+ else {
107
+ const displayedCommand = firstArgument.length > 80 ? `${firstArgument.slice(0, 79)}…` : firstArgument;
127
108
  throw new CliUsageError(`Unknown command "${displayedCommand}". Run "whoisleuth commands" to list supported commands.`);
128
109
  }
129
- if (command === 'bulk')
130
- return parseBulkArguments(argv.slice(1));
131
- if (command === 'completion')
132
- return parseCompletionArguments(argv.slice(1));
133
- if (command === 'doctor')
134
- return parseDoctorArguments(argv.slice(1));
135
- if (command === 'commands')
136
- return parseCommandsArguments(argv.slice(1));
137
- if (command === 'manual')
138
- return parseManualArguments(argv.slice(1));
139
- if (command === 'manifest')
140
- return parseManifestArguments(argv.slice(1));
141
- if (command === 'map-observations')
142
- return parseOfflineImportArguments('map-observations', argv.slice(1));
143
- if (command === 'oam-export')
144
- return parseOfflineImportArguments('oam-export', argv.slice(1));
145
- if (command === 'ct-search')
146
- return parseCtSearchArguments(argv.slice(1));
147
- if (command === 'ct-intake')
148
- return parseCtIntakeArguments(argv.slice(1));
149
- if (command === 'discover')
150
- return parseDiscoverArguments(argv.slice(1));
151
- if (command === 'discover-scan')
152
- return parseDiscoverScanArguments(argv.slice(1));
153
- if (command === 'posture')
154
- return parsePostureArguments(argv.slice(1));
155
- if (command === 'http')
156
- return parseHttpArguments(argv.slice(1));
157
- if (command === 'tls')
158
- return parseTlsArguments(argv.slice(1));
159
- if (command === 'registry-support')
160
- return parseRegistrySupportArguments(argv.slice(1));
161
- if (command === 'registry-doctor')
162
- return parseRegistryDoctorArguments(argv.slice(1));
163
- if (command === 'registry-cohort')
164
- return parseRegistryCohortArguments(argv.slice(1));
165
- if (command === 'registry-scaffold')
166
- return parseRegistryScaffoldArguments(argv.slice(1));
167
- if (command === 'risk-calibrate')
168
- return parseRiskCalibrateArguments(argv.slice(1));
169
- if (command === 'lookalike-calibrate')
170
- return parseLookalikeCalibrateArguments(argv.slice(1));
171
- if (command === 'verify-artifact')
172
- return parseVerifyArtifactArguments(argv.slice(1));
173
- if (command === 'inspect-archive')
174
- return parseInspectArchiveArguments(argv.slice(1));
175
- if (command === 'sign-artifact')
176
- return parseSignArtifactArguments(argv.slice(1));
177
- if (command === 'verify-signature')
178
- return parseVerifySignatureArguments(argv.slice(1));
179
- if (command === 'source-report')
180
- return parseSourceReportArguments(argv.slice(1));
181
- if (command === 'compare')
182
- return parseCompareArguments(argv.slice(1));
183
- if (command === 'page-compare')
184
- return parsePageCompareArguments(argv.slice(1));
185
- if (command === 'mail-review')
186
- return parseMailReviewArguments(argv.slice(1));
187
- if (command === 'review-evidence')
188
- return parseReviewEvidenceArguments(argv.slice(1));
189
- if (command === 'brief')
190
- return parseBriefArguments(argv.slice(1));
191
- if (command === 'case-pack')
192
- return parseCasePackArguments(argv.slice(1));
193
- if (command === 'domain-control')
194
- return parseDomainControlArguments(argv.slice(1));
195
- if (command === 'monitor-once')
196
- return parseMonitorOnceArguments(argv.slice(1));
197
- if (command === 'assurance')
198
- return parseAssuranceArguments(argv.slice(1));
199
- if (command === 'change-packet')
200
- return parseChangePacketArguments(argv.slice(1));
201
- if (command === 'sharing-review')
202
- return parseSharingReviewArguments(argv.slice(1));
203
- if (command === 'workflow-plan')
204
- return parseWorkflowPlanArguments(argv.slice(1));
205
- if (command === 'workflow-run')
206
- return parseWorkflowRunArguments(argv.slice(1));
207
- if (command === 'diff')
208
- return parseDiffArguments(argv.slice(1));
209
- if (command === 'reconcile')
210
- return parseReconcileArguments(argv.slice(1));
211
- if (command === 'timeline')
212
- return parseTimelineArguments(argv.slice(1));
213
- if (command === 'export')
214
- return parseExportArguments(argv.slice(1));
110
+ const parsed = CLI_PARSERS[command](directLookup ? argv : argv.slice(1));
111
+ if (parsed.action !== command) {
112
+ throw new Error(`CLI parser contract mismatch for ${command}.`);
113
+ }
114
+ return parsed;
115
+ }
116
+ function parseLookupArguments(lookupArguments) {
215
117
  let query = null;
216
118
  let output = 'terminal';
217
119
  let deep = false;
@@ -223,10 +125,12 @@ function parseCliArgumentsCore(argv) {
223
125
  let strictExit = false;
224
126
  let events = false;
225
127
  let plan = false;
128
+ let browse = false;
129
+ let includeAttribution = true;
226
130
  let failOn = null;
227
131
  let observerLabel = null;
228
132
  let vantageLabel = null;
229
- const lookupArguments = argv.slice(1);
133
+ let saveLookup = null;
230
134
  for (let index = 0; index < lookupArguments.length; index++) {
231
135
  const argument = lookupArguments[index];
232
136
  if (argument === undefined)
@@ -279,6 +183,24 @@ function parseCliArgumentsCore(argv) {
279
183
  throw new CliUsageError('--plan may be supplied only once.');
280
184
  plan = true;
281
185
  }
186
+ else if (argument === '--browse') {
187
+ if (browse)
188
+ throw new CliUsageError('--browse may be supplied only once.');
189
+ browse = true;
190
+ }
191
+ else if (argument === '--save-lookup') {
192
+ if (saveLookup !== null)
193
+ throw new CliUsageError('--save-lookup may be supplied only once.');
194
+ const value = lookupArguments[++index];
195
+ if (!value || value.startsWith('-'))
196
+ throw new CliUsageError('--save-lookup requires one bounded file path.');
197
+ saveLookup = value;
198
+ }
199
+ else if (argument === '--no-attribution') {
200
+ if (!includeAttribution)
201
+ throw new CliUsageError('--no-attribution may be supplied only once.');
202
+ includeAttribution = false;
203
+ }
282
204
  else if (argument === '--fail-on') {
283
205
  if (failOn !== null)
284
206
  throw new CliUsageError('--fail-on may be supplied only once.');
@@ -315,12 +237,26 @@ function parseCliArgumentsCore(argv) {
315
237
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
316
238
  if (detailSet && output !== 'terminal')
317
239
  throw new CliUsageError('--summary and --verbose apply only to terminal output.');
240
+ if (browse && output !== 'terminal')
241
+ throw new CliUsageError('--browse applies only to terminal output.');
242
+ if (browse && (detailSet || events || plan || quiet)) {
243
+ throw new CliUsageError('--browse cannot be combined with detail, event, plan, or quiet options.');
244
+ }
245
+ if (browse && query === null) {
246
+ throw new CliUsageError('--browse requires a positional target because interactive stdin is reserved for navigation.');
247
+ }
248
+ if (saveLookup !== null && !browse) {
249
+ throw new CliUsageError('--save-lookup requires --browse so the exact completed Lookup document is saved after the browser closes.');
250
+ }
251
+ if (!includeAttribution && output !== 'markdown' && output !== 'html') {
252
+ throw new CliUsageError('--no-attribution applies only to Markdown or HTML reports.');
253
+ }
318
254
  if (plan && !['terminal', 'json'].includes(output))
319
255
  throw new CliUsageError('--plan supports terminal or JSON output only.');
320
256
  if (plan && (detailSet || strictExit || events || quiet || failOn !== null)) {
321
257
  throw new CliUsageError('--plan cannot be combined with detail, strict-exit, event, or quiet options.');
322
258
  }
323
- return { action: 'lookup', query, output, deep, detail, strictExit, events, plan, observerLabel, vantageLabel, quiet, color, ...(failOn ? { failOn } : {}) };
259
+ return { action: 'lookup', query, output, deep, detail, strictExit, events, plan, includeAttribution, observerLabel, vantageLabel, quiet, color, ...(browse ? { browse: true } : {}), ...(saveLookup ? { saveLookup } : {}), ...(failOn ? { failOn } : {}) };
324
260
  }
325
261
  function parseCompletionArguments(argv) {
326
262
  if (argv.length !== 1 || !['bash', 'zsh', 'fish', 'powershell'].includes(argv[0] || '')) {
@@ -413,14 +349,41 @@ function parseOfflineImportArguments(action, argv) {
413
349
  }
414
350
  function parseCommandsArguments(argv) {
415
351
  let output = 'terminal';
352
+ let common = false;
353
+ let group = null;
354
+ let mode = null;
416
355
  let quiet = false;
417
356
  let color = true;
418
- for (const argument of argv) {
357
+ for (let index = 0; index < argv.length; index += 1) {
358
+ const argument = argv[index];
419
359
  if (argument === '--json') {
420
360
  if (output !== 'terminal')
421
361
  throw new CliUsageError('--json may be supplied only once.');
422
362
  output = 'json';
423
363
  }
364
+ else if (argument === '--common') {
365
+ if (common)
366
+ throw new CliUsageError('--common may be supplied only once.');
367
+ common = true;
368
+ }
369
+ else if (argument === '--group') {
370
+ if (group !== null)
371
+ throw new CliUsageError('--group may be supplied only once.');
372
+ const value = argv[++index];
373
+ if (!['investigate', 'respond', 'assure', 'utilities'].includes(value ?? '')) {
374
+ throw new CliUsageError('--group requires investigate, respond, assure, or utilities.');
375
+ }
376
+ group = value;
377
+ }
378
+ else if (argument === '--mode') {
379
+ if (mode !== null)
380
+ throw new CliUsageError('--mode may be supplied only once.');
381
+ const value = argv[++index];
382
+ if (value !== 'offline' && value !== 'network') {
383
+ throw new CliUsageError('--mode requires offline or network.');
384
+ }
385
+ mode = value;
386
+ }
424
387
  else if (argument === '--quiet')
425
388
  quiet = true;
426
389
  else if (argument === '--no-color')
@@ -430,7 +393,7 @@ function parseCommandsArguments(argv) {
430
393
  }
431
394
  if (quiet && output !== 'terminal')
432
395
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
433
- return { action: 'commands', output, quiet, color };
396
+ return { action: 'commands', output, common, group, mode, quiet, color };
434
397
  }
435
398
  function parseDoctorArguments(argv) {
436
399
  let network = false;
@@ -963,6 +926,122 @@ function parseTlsArguments(argv) {
963
926
  const { source: hostname, ...options } = parseSingleJsonInput(argv, 'tls accepts one hostname.');
964
927
  return { action: 'tls', hostname, ...options };
965
928
  }
929
+ function parseDnssecValidateArguments(argv) {
930
+ let target = null;
931
+ let resolver = null;
932
+ let trustAnchorSource = null;
933
+ let ownedOrAuthorized = false;
934
+ let output = 'terminal';
935
+ let quiet = false;
936
+ let color = true;
937
+ for (let index = 0; index < argv.length; index += 1) {
938
+ const argument = argv[index];
939
+ if (argument === '--resolver' || argument === '--trust-anchor') {
940
+ const selected = argument === '--resolver' ? resolver : trustAnchorSource;
941
+ if (selected !== null)
942
+ throw new CliUsageError(`${argument} may be supplied only once.`);
943
+ const value = argv[++index];
944
+ if (!value || value.startsWith('-'))
945
+ throw new CliUsageError(`${argument} requires one bounded value.`);
946
+ if (argument === '--resolver')
947
+ resolver = value;
948
+ else
949
+ trustAnchorSource = value;
950
+ }
951
+ else if (argument === '--owned-or-authorized') {
952
+ if (ownedOrAuthorized)
953
+ throw new CliUsageError('--owned-or-authorized may be supplied only once.');
954
+ ownedOrAuthorized = true;
955
+ }
956
+ else if (argument === '--json') {
957
+ if (output !== 'terminal')
958
+ throw new CliUsageError('--json may be supplied only once.');
959
+ output = 'json';
960
+ }
961
+ else if (argument === '--quiet')
962
+ quiet = true;
963
+ else if (argument === '--no-color')
964
+ color = false;
965
+ else if (argument?.startsWith('-'))
966
+ throw new CliUsageError(`Unknown option "${argument}".`);
967
+ else if (argument && target === null)
968
+ target = argument;
969
+ else
970
+ throw new CliUsageError('dnssec-validate accepts one domain target.');
971
+ }
972
+ if (!target)
973
+ throw new CliUsageError('dnssec-validate requires one domain target.');
974
+ if (!resolver)
975
+ throw new CliUsageError('dnssec-validate requires --resolver <public-IP>.');
976
+ if (!trustAnchorSource)
977
+ throw new CliUsageError('dnssec-validate requires --trust-anchor <anchor.json>.');
978
+ if (!ownedOrAuthorized)
979
+ throw new CliUsageError('dnssec-validate requires --owned-or-authorized for every run.');
980
+ if (quiet && output !== 'terminal')
981
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
982
+ return { action: 'dnssec-validate', target, resolver, trustAnchorSource, ownedOrAuthorized: true, output, quiet, color };
983
+ }
984
+ function parseMailTransportArguments(argv) {
985
+ let source = null;
986
+ let resolver = null;
987
+ let trustAnchorSource = null;
988
+ let ownedOrAuthorized = false;
989
+ let activeProbeAcknowledged = false;
990
+ let output = 'terminal';
991
+ let quiet = false;
992
+ let color = true;
993
+ for (let index = 0; index < argv.length; index += 1) {
994
+ const argument = argv[index];
995
+ if (argument === '--resolver' || argument === '--trust-anchor') {
996
+ const selected = argument === '--resolver' ? resolver : trustAnchorSource;
997
+ if (selected !== null)
998
+ throw new CliUsageError(`${argument} may be supplied only once.`);
999
+ const value = argv[++index];
1000
+ if (!value || value.startsWith('-'))
1001
+ throw new CliUsageError(`${argument} requires one bounded value.`);
1002
+ if (argument === '--resolver')
1003
+ resolver = value;
1004
+ else
1005
+ trustAnchorSource = value;
1006
+ }
1007
+ else if (argument === '--owned-or-authorized') {
1008
+ if (ownedOrAuthorized)
1009
+ throw new CliUsageError('--owned-or-authorized may be supplied only once.');
1010
+ ownedOrAuthorized = true;
1011
+ }
1012
+ else if (argument === '--active-probe') {
1013
+ if (activeProbeAcknowledged)
1014
+ throw new CliUsageError('--active-probe may be supplied only once.');
1015
+ activeProbeAcknowledged = true;
1016
+ }
1017
+ else if (argument === '--json') {
1018
+ if (output !== 'terminal')
1019
+ throw new CliUsageError('--json may be supplied only once.');
1020
+ output = 'json';
1021
+ }
1022
+ else if (argument === '--quiet')
1023
+ quiet = true;
1024
+ else if (argument === '--no-color')
1025
+ color = false;
1026
+ else if (argument?.startsWith('-'))
1027
+ throw new CliUsageError(`Unknown option "${argument}".`);
1028
+ else if (argument && source === null)
1029
+ source = argument;
1030
+ else
1031
+ throw new CliUsageError('mail-transport accepts one optional versioned JSON input file.');
1032
+ }
1033
+ if (!resolver)
1034
+ throw new CliUsageError('mail-transport requires --resolver <public-IP>.');
1035
+ if (!trustAnchorSource)
1036
+ throw new CliUsageError('mail-transport requires --trust-anchor <anchor.json>.');
1037
+ if (!ownedOrAuthorized)
1038
+ throw new CliUsageError('mail-transport requires --owned-or-authorized for every run.');
1039
+ if (!activeProbeAcknowledged)
1040
+ throw new CliUsageError('mail-transport requires --active-probe for every run.');
1041
+ if (quiet && output !== 'terminal')
1042
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1043
+ return { action: 'mail-transport', source, resolver, trustAnchorSource, ownedOrAuthorized: true, activeProbeAcknowledged: true, output, quiet, color };
1044
+ }
966
1045
  function parseCompareArguments(argv) {
967
1046
  const parsed = parseSingleJsonInput(argv, 'compare accepts one optional lookup JSON file. Otherwise pipe one lookup document on stdin.');
968
1047
  return { action: 'compare', ...parsed };
@@ -1091,9 +1170,11 @@ function parseCasePackArguments(argv) {
1091
1170
  }
1092
1171
  if (audience === null)
1093
1172
  throw new CliUsageError('case-pack requires --audience internal, trusted, or public.');
1173
+ if (!reviewed)
1174
+ throw new CliUsageError('case-pack requires --reviewed to confirm deliberate analyst review.');
1094
1175
  if (quiet && output !== 'terminal')
1095
1176
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1096
- return { action: 'case-pack', source, output, audience, reviewed, quiet, color };
1177
+ return { action: 'case-pack', source, output, audience, reviewed: true, quiet, color };
1097
1178
  }
1098
1179
  function parseMonitorOnceArguments(argv) {
1099
1180
  let source = null;
@@ -1101,6 +1182,8 @@ function parseMonitorOnceArguments(argv) {
1101
1182
  let output = 'terminal';
1102
1183
  let limit = 20;
1103
1184
  let concurrency = 2;
1185
+ let limitSupplied = false;
1186
+ let concurrencySupplied = false;
1104
1187
  let failOn = null;
1105
1188
  let quiet = false;
1106
1189
  let color = true;
@@ -1127,14 +1210,21 @@ function parseMonitorOnceArguments(argv) {
1127
1210
  previousSource = value;
1128
1211
  }
1129
1212
  else if (argument === '--limit' || argument === '--concurrency') {
1213
+ if (argument === '--limit' ? limitSupplied : concurrencySupplied) {
1214
+ throw new CliUsageError(`${argument} may be supplied only once.`);
1215
+ }
1130
1216
  const value = Number(argv[++index]);
1131
1217
  const maximum = argument === '--limit' ? 20 : 3;
1132
1218
  if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
1133
1219
  throw new CliUsageError(`${argument} requires an integer from 1 to ${maximum}.`);
1134
- if (argument === '--limit')
1220
+ if (argument === '--limit') {
1135
1221
  limit = value;
1136
- else
1222
+ limitSupplied = true;
1223
+ }
1224
+ else {
1137
1225
  concurrency = value;
1226
+ concurrencySupplied = true;
1227
+ }
1138
1228
  }
1139
1229
  else if (argument === '--fail-on') {
1140
1230
  if (failOn !== null)
@@ -1246,15 +1336,33 @@ function parseSharingReviewArguments(argv) {
1246
1336
  }
1247
1337
  function parseWorkflowPlanArguments(argv) {
1248
1338
  const positional = [];
1339
+ let discovery = null;
1340
+ let explainedRecipe = null;
1249
1341
  let output = 'terminal';
1250
1342
  let quiet = false;
1251
1343
  let color = true;
1252
- for (const argument of argv) {
1344
+ for (let index = 0; index < argv.length; index += 1) {
1345
+ const argument = argv[index];
1253
1346
  if (argument === '--json') {
1254
1347
  if (output !== 'terminal')
1255
1348
  throw new CliUsageError('--json may be supplied only once.');
1256
1349
  output = 'json';
1257
1350
  }
1351
+ else if (argument === '--list') {
1352
+ if (discovery !== null)
1353
+ throw new CliUsageError('--list and --explain are mutually exclusive and may be supplied only once.');
1354
+ discovery = 'list';
1355
+ }
1356
+ else if (argument === '--explain') {
1357
+ if (discovery !== null)
1358
+ throw new CliUsageError('--list and --explain are mutually exclusive and may be supplied only once.');
1359
+ const value = argv[++index];
1360
+ if (!INVESTIGATION_PLAN_RECIPES.includes(value)) {
1361
+ throw new CliUsageError(`--explain recipe must be one of: ${INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
1362
+ }
1363
+ discovery = 'explain';
1364
+ explainedRecipe = value;
1365
+ }
1258
1366
  else if (argument === '--quiet')
1259
1367
  quiet = true;
1260
1368
  else if (argument === '--no-color')
@@ -1264,14 +1372,21 @@ function parseWorkflowPlanArguments(argv) {
1264
1372
  else
1265
1373
  positional.push(argument);
1266
1374
  }
1375
+ if (quiet && output !== 'terminal')
1376
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1377
+ if (discovery !== null) {
1378
+ if (positional.length !== 0)
1379
+ throw new CliUsageError('workflow-plan discovery options do not accept a subject.');
1380
+ if (discovery === 'list')
1381
+ return { action: 'workflow-plan', discovery, output, quiet, color };
1382
+ return { action: 'workflow-plan', discovery, recipe: explainedRecipe, output, quiet, color };
1383
+ }
1267
1384
  if (positional.length !== 2)
1268
- throw new CliUsageError('workflow-plan requires one fixed recipe and one subject.');
1385
+ throw new CliUsageError('workflow-plan requires one fixed recipe and one subject, --list, or --explain <recipe>.');
1269
1386
  const recipe = positional[0];
1270
1387
  if (!INVESTIGATION_PLAN_RECIPES.includes(recipe)) {
1271
1388
  throw new CliUsageError(`workflow-plan recipe must be one of: ${INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
1272
1389
  }
1273
- if (quiet && output !== 'terminal')
1274
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1275
1390
  return { action: 'workflow-plan', recipe: recipe, subject: positional[1], output, quiet, color };
1276
1391
  }
1277
1392
  function parseWorkflowRunArguments(argv) {
@@ -1315,8 +1430,8 @@ function parseWorkflowRunArguments(argv) {
1315
1430
  if (positional.length !== 2)
1316
1431
  throw new CliUsageError('workflow-run requires one fixed recipe and one subject.');
1317
1432
  const recipe = positional[0];
1318
- if (!INVESTIGATION_PLAN_RECIPES.includes(recipe)) {
1319
- throw new CliUsageError(`workflow-run recipe must be one of: ${INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
1433
+ if (!RUNNABLE_INVESTIGATION_PLAN_RECIPES.includes(recipe)) {
1434
+ throw new CliUsageError(`workflow-run recipe must be one of: ${RUNNABLE_INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
1320
1435
  }
1321
1436
  if (quiet && output !== 'terminal')
1322
1437
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
@@ -1325,15 +1440,32 @@ function parseWorkflowRunArguments(argv) {
1325
1440
  function parseDiffArguments(argv) {
1326
1441
  let leftSource = null;
1327
1442
  let rightSource = null;
1443
+ let leftSessionId = null;
1444
+ let rightSessionId = null;
1328
1445
  let output = 'terminal';
1329
1446
  let quiet = false;
1330
1447
  let color = true;
1331
- for (const argument of argv) {
1448
+ for (let index = 0; index < argv.length; index++) {
1449
+ const argument = argv[index];
1450
+ if (argument === undefined)
1451
+ break;
1332
1452
  if (argument === '--json') {
1333
1453
  if (output !== 'terminal')
1334
1454
  throw new CliUsageError('--json may be supplied only once.');
1335
1455
  output = 'json';
1336
1456
  }
1457
+ else if (argument === '--left-session' || argument === '--right-session') {
1458
+ const current = argument === '--left-session' ? leftSessionId : rightSessionId;
1459
+ if (current !== null)
1460
+ throw new CliUsageError(`${argument} may be supplied only once.`);
1461
+ const value = argv[++index];
1462
+ if (!value || !/^[A-Za-z0-9_-]{1,128}$/u.test(value))
1463
+ throw new CliUsageError(`${argument} requires one bounded saved-session ID.`);
1464
+ if (argument === '--left-session')
1465
+ leftSessionId = value;
1466
+ else
1467
+ rightSessionId = value;
1468
+ }
1337
1469
  else if (argument === '--quiet')
1338
1470
  quiet = true;
1339
1471
  else if (argument === '--no-color')
@@ -1345,15 +1477,15 @@ function parseDiffArguments(argv) {
1345
1477
  else if (rightSource === null)
1346
1478
  rightSource = argument;
1347
1479
  else
1348
- throw new CliUsageError('diff accepts exactly two saved lookup JSON files.');
1480
+ throw new CliUsageError('diff accepts exactly two compatible retained JSON files.');
1349
1481
  }
1350
1482
  if (!leftSource || !rightSource)
1351
- throw new CliUsageError('diff requires exactly two saved lookup JSON files.');
1483
+ throw new CliUsageError('diff requires exactly two compatible retained JSON files.');
1352
1484
  if (leftSource === rightSource)
1353
1485
  throw new CliUsageError('diff requires two different input files.');
1354
1486
  if (quiet && output !== 'terminal')
1355
1487
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1356
- return { action: 'diff', leftSource, rightSource, output, quiet, color };
1488
+ return { action: 'diff', leftSource, rightSource, leftSessionId, rightSessionId, output, quiet, color };
1357
1489
  }
1358
1490
  function parseReconcileArguments(argv) {
1359
1491
  const sources = [];
@@ -1451,8 +1583,30 @@ function parseRegistryScaffoldArguments(argv) {
1451
1583
  return { action: 'registry-scaffold', profile, suffix, scenario: scenario };
1452
1584
  }
1453
1585
  function parseRiskCalibrateArguments(argv) {
1454
- const parsed = parseSingleJsonInput(argv, 'risk-calibrate accepts one optional dataset file. Otherwise pipe one dataset on stdin.');
1455
- return { action: 'risk-calibrate', ...parsed };
1586
+ let source = null;
1587
+ let output = 'terminal';
1588
+ let quiet = false;
1589
+ let color = true;
1590
+ for (const argument of argv) {
1591
+ if (argument === '--json' || argument === '--summary-json') {
1592
+ if (output !== 'terminal')
1593
+ throw new CliUsageError('--json and --summary-json are mutually exclusive and may be supplied only once.');
1594
+ output = argument === '--json' ? 'json' : 'summary_json';
1595
+ }
1596
+ else if (argument === '--quiet')
1597
+ quiet = true;
1598
+ else if (argument === '--no-color')
1599
+ color = false;
1600
+ else if (argument.startsWith('-'))
1601
+ throw new CliUsageError(`Unknown option "${argument}".`);
1602
+ else if (source === null)
1603
+ source = argument;
1604
+ else
1605
+ throw new CliUsageError('risk-calibrate accepts one optional dataset file. Otherwise pipe one dataset on stdin.');
1606
+ }
1607
+ if (quiet && output !== 'terminal')
1608
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1609
+ return { action: 'risk-calibrate', source, output, quiet, color };
1456
1610
  }
1457
1611
  function parseLookalikeCalibrateArguments(argv) {
1458
1612
  const parsed = parseSingleJsonInput(argv, 'lookalike-calibrate accepts one optional reviewed dataset JSON file.');
@@ -1461,7 +1615,10 @@ function parseLookalikeCalibrateArguments(argv) {
1461
1615
  function parseVerifyArtifactArguments(argv) {
1462
1616
  let source = null;
1463
1617
  let passphraseSource = null;
1618
+ let manifestSource = null;
1619
+ let manifestEntryId = null;
1464
1620
  let output = 'terminal';
1621
+ let strictExit = false;
1465
1622
  let quiet = false;
1466
1623
  let color = true;
1467
1624
  for (let index = 0; index < argv.length; index++) {
@@ -1482,6 +1639,28 @@ function parseVerifyArtifactArguments(argv) {
1482
1639
  }
1483
1640
  passphraseSource = value;
1484
1641
  }
1642
+ else if (argument === '--manifest') {
1643
+ if (manifestSource !== null)
1644
+ throw new CliUsageError('--manifest may be supplied only once.');
1645
+ const value = argv[++index];
1646
+ if (!value || value.startsWith('-'))
1647
+ throw new CliUsageError('--manifest requires one bounded investigation manifest file.');
1648
+ manifestSource = value;
1649
+ }
1650
+ else if (argument === '--manifest-entry') {
1651
+ if (manifestEntryId !== null)
1652
+ throw new CliUsageError('--manifest-entry may be supplied only once.');
1653
+ const value = argv[++index];
1654
+ if (!value || !/^artifact-(?:[1-9]|1[0-6])$/u.test(value)) {
1655
+ throw new CliUsageError('--manifest-entry requires an ID from artifact-1 through artifact-16.');
1656
+ }
1657
+ manifestEntryId = value;
1658
+ }
1659
+ else if (argument === '--strict-exit') {
1660
+ if (strictExit)
1661
+ throw new CliUsageError('--strict-exit may be supplied only once.');
1662
+ strictExit = true;
1663
+ }
1485
1664
  else if (argument === '--quiet')
1486
1665
  quiet = true;
1487
1666
  else if (argument === '--no-color')
@@ -1493,9 +1672,50 @@ function parseVerifyArtifactArguments(argv) {
1493
1672
  else
1494
1673
  throw new CliUsageError('verify-artifact accepts one optional JSON file. Otherwise pipe one artefact on stdin.');
1495
1674
  }
1675
+ if ((manifestSource === null) !== (manifestEntryId === null)) {
1676
+ throw new CliUsageError('--manifest and --manifest-entry must be supplied together.');
1677
+ }
1496
1678
  if (quiet && output !== 'terminal')
1497
1679
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1498
- return { action: 'verify-artifact', source, passphraseSource, output, quiet, color };
1680
+ return { action: 'verify-artifact', source, passphraseSource, manifestSource, manifestEntryId, output, strictExit, quiet, color };
1681
+ }
1682
+ function parseInterchangeReportArguments(argv) {
1683
+ let source = null;
1684
+ let passphraseSource = null;
1685
+ let output = 'terminal';
1686
+ let quiet = false;
1687
+ let color = true;
1688
+ for (let index = 0; index < argv.length; index++) {
1689
+ const argument = argv[index];
1690
+ if (argument === undefined)
1691
+ break;
1692
+ if (argument === '--json') {
1693
+ if (output !== 'terminal')
1694
+ throw new CliUsageError('--json may be supplied only once.');
1695
+ output = 'json';
1696
+ }
1697
+ else if (argument === '--passphrase-file') {
1698
+ if (passphraseSource !== null)
1699
+ throw new CliUsageError('--passphrase-file may be supplied only once.');
1700
+ const value = argv[++index];
1701
+ if (!value || value.startsWith('-'))
1702
+ throw new CliUsageError('--passphrase-file requires one bounded UTF-8 file.');
1703
+ passphraseSource = value;
1704
+ }
1705
+ else if (argument === '--quiet')
1706
+ quiet = true;
1707
+ else if (argument === '--no-color')
1708
+ color = false;
1709
+ else if (argument.startsWith('-'))
1710
+ throw new CliUsageError(`Unknown option "${argument}".`);
1711
+ else if (source === null)
1712
+ source = argument;
1713
+ else
1714
+ throw new CliUsageError('interchange-report accepts one optional JSON file. Otherwise pipe one artefact on stdin.');
1715
+ }
1716
+ if (quiet && output !== 'terminal')
1717
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1718
+ return { action: 'interchange-report', source, passphraseSource, output, quiet, color };
1499
1719
  }
1500
1720
  function parseSourceReportArguments(argv) {
1501
1721
  const parsed = parseSingleJsonInput(argv, 'source-report accepts one optional JSON file. Otherwise pipe lookup documents on stdin.');
@@ -1504,6 +1724,7 @@ function parseSourceReportArguments(argv) {
1504
1724
  function parseExportArguments(argv) {
1505
1725
  let source = null;
1506
1726
  let compact = false;
1727
+ let includeAttribution = true;
1507
1728
  let format = 'json';
1508
1729
  for (const argument of argv) {
1509
1730
  if (argument === '--compact') {
@@ -1511,6 +1732,11 @@ function parseExportArguments(argv) {
1511
1732
  throw new CliUsageError('--compact may be supplied only once.');
1512
1733
  compact = true;
1513
1734
  }
1735
+ else if (argument === '--no-attribution') {
1736
+ if (!includeAttribution)
1737
+ throw new CliUsageError('--no-attribution may be supplied only once.');
1738
+ includeAttribution = false;
1739
+ }
1514
1740
  else if (argument === '--markdown' || argument === '--html') {
1515
1741
  if (format !== 'json')
1516
1742
  throw new CliUsageError('Choose only one evidence export format.');
@@ -1525,6 +1751,57 @@ function parseExportArguments(argv) {
1525
1751
  }
1526
1752
  if (compact && format !== 'json')
1527
1753
  throw new CliUsageError('--compact applies to JSON export and cannot be combined with --markdown or --html.');
1528
- return { action: 'export', source, format, compact };
1754
+ if (!includeAttribution && format === 'json')
1755
+ throw new CliUsageError('--no-attribution applies only to Markdown or HTML reports.');
1756
+ return { action: 'export', source, format, compact, includeAttribution };
1529
1757
  }
1530
- export { CLI_COMMANDS, CliUsageError, MAX_CLI_ARGUMENTS, MAX_CLI_ARGUMENT_LENGTH, parseCliArguments };
1758
+ const CLI_PARSERS = Object.freeze({
1759
+ completion: parseCompletionArguments,
1760
+ doctor: parseDoctorArguments,
1761
+ commands: parseCommandsArguments,
1762
+ manual: parseManualArguments,
1763
+ manifest: parseManifestArguments,
1764
+ 'map-observations': (argv) => parseOfflineImportArguments('map-observations', argv),
1765
+ 'oam-export': (argv) => parseOfflineImportArguments('oam-export', argv),
1766
+ lookup: parseLookupArguments,
1767
+ bulk: parseBulkArguments,
1768
+ 'ct-search': parseCtSearchArguments,
1769
+ 'ct-intake': parseCtIntakeArguments,
1770
+ discover: parseDiscoverArguments,
1771
+ 'discover-scan': parseDiscoverScanArguments,
1772
+ posture: parsePostureArguments,
1773
+ http: parseHttpArguments,
1774
+ tls: parseTlsArguments,
1775
+ 'dnssec-validate': parseDnssecValidateArguments,
1776
+ 'mail-transport': parseMailTransportArguments,
1777
+ 'registry-support': parseRegistrySupportArguments,
1778
+ 'registry-doctor': parseRegistryDoctorArguments,
1779
+ 'registry-cohort': parseRegistryCohortArguments,
1780
+ 'registry-scaffold': parseRegistryScaffoldArguments,
1781
+ 'risk-calibrate': parseRiskCalibrateArguments,
1782
+ 'lookalike-calibrate': parseLookalikeCalibrateArguments,
1783
+ 'verify-artifact': parseVerifyArtifactArguments,
1784
+ 'interchange-report': parseInterchangeReportArguments,
1785
+ 'inspect-archive': parseInspectArchiveArguments,
1786
+ 'sign-artifact': parseSignArtifactArguments,
1787
+ 'verify-signature': parseVerifySignatureArguments,
1788
+ 'source-report': parseSourceReportArguments,
1789
+ compare: parseCompareArguments,
1790
+ 'page-compare': parsePageCompareArguments,
1791
+ 'mail-review': parseMailReviewArguments,
1792
+ 'review-evidence': parseReviewEvidenceArguments,
1793
+ brief: parseBriefArguments,
1794
+ 'case-pack': parseCasePackArguments,
1795
+ 'domain-control': parseDomainControlArguments,
1796
+ 'monitor-once': parseMonitorOnceArguments,
1797
+ assurance: parseAssuranceArguments,
1798
+ 'change-packet': parseChangePacketArguments,
1799
+ 'sharing-review': parseSharingReviewArguments,
1800
+ 'workflow-plan': parseWorkflowPlanArguments,
1801
+ 'workflow-run': parseWorkflowRunArguments,
1802
+ diff: parseDiffArguments,
1803
+ reconcile: parseReconcileArguments,
1804
+ timeline: parseTimelineArguments,
1805
+ export: parseExportArguments,
1806
+ });
1807
+ export { CLI_COMMANDS, CLI_PARSERS, CliUsageError, MAX_CLI_ARGUMENTS, MAX_CLI_ARGUMENT_LENGTH, parseCliArguments };