@slicedearth/whoisleuth-cli 2.2.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 (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -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 +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  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 +112 -72
  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-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. package/packages/investigation/bulk-review-export.mjs +0 -63
package/cli/arguments.mjs CHANGED
@@ -1,10 +1,11 @@
1
- import { parseInspectArchiveArguments, parseSignArtifactArguments, parseVerifySignatureArguments, } from "./evidence-command-arguments.mjs";
1
+ import { parseCommandArguments } from "./command-argument-grammar.mjs";
2
2
  import { CliUsageError, hasUnsafeCliText } from "./errors.mjs";
3
- import { INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, } from "./investigation-plan.mjs";
3
+ import { parseArchiveContentDigest } from "./archive-content-digest.mjs";
4
4
  import { parseCliFailPolicies } from "./fail-policy.mjs";
5
5
  import { isDirectLookupTarget } from "../lib/classify.mjs";
6
- import { CLI_COMMANDS, cliMetaActionForInvocation, isCliCommand, } from "./command-reference.mjs";
7
- const MAX_CLI_ARGUMENTS = 32;
6
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS } from "../packages/investigation/investigation-manifest.mjs";
7
+ import { CLI_COMMANDS, CLI_CASE_OPERATIONS, cliMetaActionForInvocation, isCliCommand, } from "./command-reference.mjs";
8
+ const MAX_CLI_ARGUMENTS = MAX_INVESTIGATION_MANIFEST_ARTIFACTS + 32;
8
9
  const MAX_CLI_ARGUMENT_LENGTH = 1024;
9
10
  function boundedArgument(value) {
10
11
  if (typeof value !== 'string' || value.length > MAX_CLI_ARGUMENT_LENGTH || hasUnsafeCliText(value)) {
@@ -12,1812 +13,517 @@ function boundedArgument(value) {
12
13
  }
13
14
  return value;
14
15
  }
15
- function extractFileOutputArguments(argv) {
16
- const retained = [];
17
- let destination = null;
18
- let force = false;
19
- let palette = null;
20
- for (let index = 0; index < argv.length; index++) {
21
- const argument = argv[index];
22
- if (argument === undefined)
23
- break;
24
- if (index > 0 && argument === '--output') {
25
- if (destination !== null)
26
- throw new CliUsageError('--output may be supplied only once.');
27
- const value = argv[++index];
28
- if (!value || value.startsWith('-'))
29
- throw new CliUsageError('--output requires one bounded file path.');
30
- destination = value;
31
- }
32
- else if (index > 0 && argument === '--force') {
33
- if (force)
34
- throw new CliUsageError('--force may be supplied only once.');
35
- force = true;
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
- }
46
- else {
47
- retained.push(argument);
48
- }
49
- }
50
- if (force && destination === null)
51
- throw new CliUsageError('--force requires --output.');
52
- return { argv: retained, destination, force, palette };
16
+ function terminalOptions(parsed) {
17
+ return { quiet: parsed.hasOption('--quiet'), color: !parsed.hasOption('--no-color') };
53
18
  }
54
- function parseCliArguments(rawArgv) {
55
- if (!Array.isArray(rawArgv) || rawArgv.length > MAX_CLI_ARGUMENTS) {
56
- throw new CliUsageError(`At most ${MAX_CLI_ARGUMENTS} command arguments are supported.`);
57
- }
58
- const boundedArguments = rawArgv.map(boundedArgument);
59
- const metaAction = cliMetaActionForInvocation(boundedArguments);
60
- if (metaAction)
61
- return parseCliArgumentsCore(boundedArguments);
62
- const extracted = extractFileOutputArguments(boundedArguments);
63
- const parsed = parseCliArgumentsCore(extracted.argv);
64
- if ('quiet' in parsed && parsed.quiet && extracted.destination !== null) {
65
- throw new CliUsageError('--quiet cannot be combined with --output.');
66
- }
67
- if ('events' in parsed && parsed.events && extracted.destination !== null) {
68
- throw new CliUsageError('--events cannot be combined with --output because completion must describe the final output delivery.');
19
+ function parseInspectArchiveArguments(parsed) {
20
+ const expectedContentDigest = parsed.optionValue('--expect-content-digest');
21
+ try {
22
+ parseArchiveContentDigest(expectedContentDigest);
69
23
  }
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.');
24
+ catch {
25
+ throw new CliUsageError('--expect-content-digest requires sha256:<64 lowercase hex> or sorted-json-v2:sha256:<64 lowercase hex>.');
72
26
  }
73
27
  return {
74
- ...parsed,
75
- ...(extracted.destination !== null ? { destination: extracted.destination } : {}),
76
- ...(extracted.force ? { force: true } : {}),
77
- ...(extracted.palette ? { palette: extracted.palette } : {}),
28
+ action: 'inspect-archive',
29
+ source: parsed.positionalValue('source'),
30
+ passphraseSource: parsed.optionValue('--passphrase-file'),
31
+ search: parsed.optionValue('--search'),
32
+ reveal: parsed.hasOption('--reveal'),
33
+ requireMatch: parsed.hasOption('--require-match'),
34
+ expectedContentDigest,
35
+ output: parsed.hasOption('--json') ? 'json' : 'terminal',
36
+ ...terminalOptions(parsed),
78
37
  };
79
38
  }
80
- function parseCliArgumentsCore(argv) {
81
- if (!argv.length)
82
- return { action: 'help' };
83
- const firstArgument = argv[0] ?? '';
84
- const metaAction = cliMetaActionForInvocation(argv);
85
- if (metaAction?.id === 'help') {
86
- if (argv.length === 1)
87
- return { action: 'help' };
88
- if (argv.length === 2 && (isCliCommand(firstArgument) || isDirectLookupTarget(firstArgument))) {
89
- return { action: 'help', command: isCliCommand(firstArgument) ? firstArgument : 'lookup' };
90
- }
91
- throw new CliUsageError('Help accepts only an optional command name.');
92
- }
93
- if (metaAction?.id === 'version') {
94
- if (argv.length !== 1)
95
- throw new CliUsageError('--version does not accept other arguments.');
96
- return { action: 'version' };
97
- }
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;
108
- throw new CliUsageError(`Unknown command "${displayedCommand}". Run "whoisleuth commands" to list supported commands.`);
109
- }
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) {
117
- let query = null;
118
- let output = 'terminal';
119
- let deep = false;
120
- let scanMode = null;
121
- let quiet = false;
122
- let color = true;
123
- let detail = 'standard';
124
- let detailSet = false;
125
- let strictExit = false;
126
- let events = false;
127
- let plan = false;
128
- let browse = false;
129
- let includeAttribution = true;
130
- let failOn = null;
131
- let observerLabel = null;
132
- let vantageLabel = null;
133
- let saveLookup = null;
134
- for (let index = 0; index < lookupArguments.length; index++) {
135
- const argument = lookupArguments[index];
136
- if (argument === undefined)
137
- break;
138
- if (argument === '--json') {
139
- if (output !== 'terminal')
140
- throw new CliUsageError('Choose only one output format.');
141
- output = 'json';
142
- }
143
- else if (argument === '--junit') {
144
- if (output !== 'terminal')
145
- throw new CliUsageError('Choose only one output format.');
146
- output = 'junit';
147
- }
148
- else if (argument === '--markdown' || argument === '--html') {
149
- if (output !== 'terminal')
150
- throw new CliUsageError('Choose only one output format.');
151
- output = argument === '--markdown' ? 'markdown' : 'html';
152
- }
153
- else if (argument === '--deep') {
154
- if (scanMode)
155
- throw new CliUsageError('--fast and --deep are mutually exclusive and may be supplied only once.');
156
- deep = true;
157
- scanMode = 'deep';
158
- }
159
- else if (argument === '--fast') {
160
- if (scanMode)
161
- throw new CliUsageError('--fast and --deep are mutually exclusive and may be supplied only once.');
162
- deep = false;
163
- scanMode = 'fast';
164
- }
165
- else if (argument === '--summary' || argument === '--verbose') {
166
- if (detailSet)
167
- throw new CliUsageError('--summary and --verbose are mutually exclusive and may be supplied only once.');
168
- detail = argument === '--summary' ? 'summary' : 'verbose';
169
- detailSet = true;
170
- }
171
- else if (argument === '--strict-exit') {
172
- if (strictExit)
173
- throw new CliUsageError('--strict-exit may be supplied only once.');
174
- strictExit = true;
175
- }
176
- else if (argument === '--events') {
177
- if (events)
178
- throw new CliUsageError('--events may be supplied only once.');
179
- events = true;
180
- }
181
- else if (argument === '--plan') {
182
- if (plan)
183
- throw new CliUsageError('--plan may be supplied only once.');
184
- plan = true;
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
- }
204
- else if (argument === '--fail-on') {
205
- if (failOn !== null)
206
- throw new CliUsageError('--fail-on may be supplied only once.');
207
- failOn = parseCliFailPolicies(lookupArguments[++index], 'lookup');
208
- }
209
- else if (argument === '--observer' || argument === '--vantage') {
210
- const isObserver = argument === '--observer';
211
- if (isObserver ? observerLabel !== null : vantageLabel !== null) {
212
- throw new CliUsageError(`${argument} may be supplied only once.`);
213
- }
214
- const value = lookupArguments[++index];
215
- if (!value || value.startsWith('-'))
216
- throw new CliUsageError(`${argument} requires one bounded label.`);
217
- const normalized = value.replace(/\s+/gu, ' ').trim();
218
- if (!normalized || normalized.length > 80)
219
- throw new CliUsageError(`${argument} requires a label of at most 80 characters.`);
220
- if (isObserver)
221
- observerLabel = normalized;
222
- else
223
- vantageLabel = normalized;
224
- }
225
- else if (argument === '--quiet')
226
- quiet = true;
227
- else if (argument === '--no-color')
228
- color = false;
229
- else if (argument.startsWith('-'))
230
- throw new CliUsageError(`Unknown option "${argument}".`);
231
- else if (query === null)
232
- query = argument;
233
- else
234
- throw new CliUsageError('lookup accepts one query. Use the bulk command for multiple inputs.');
235
- }
236
- if (quiet && output !== 'terminal')
237
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
238
- if (detailSet && output !== 'terminal')
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
- }
254
- if (plan && !['terminal', 'json'].includes(output))
255
- throw new CliUsageError('--plan supports terminal or JSON output only.');
256
- if (plan && (detailSet || strictExit || events || quiet || failOn !== null)) {
257
- throw new CliUsageError('--plan cannot be combined with detail, strict-exit, event, or quiet options.');
258
- }
259
- return { action: 'lookup', query, output, deep, detail, strictExit, events, plan, includeAttribution, observerLabel, vantageLabel, quiet, color, ...(browse ? { browse: true } : {}), ...(saveLookup ? { saveLookup } : {}), ...(failOn ? { failOn } : {}) };
260
- }
261
- function parseCompletionArguments(argv) {
262
- if (argv.length !== 1 || !['bash', 'zsh', 'fish', 'powershell'].includes(argv[0] || '')) {
263
- throw new CliUsageError('completion requires exactly one shell: bash, zsh, fish, or powershell.');
264
- }
265
- return { action: 'completion', shell: argv[0] };
266
- }
267
- function parseManualArguments(argv) {
268
- if (argv.length !== 0)
269
- throw new CliUsageError('manual does not accept command arguments.');
270
- return { action: 'manual' };
271
- }
272
- function parseManifestArguments(argv) {
273
- const sources = [];
274
- let workflow = null;
275
- let configurationDigestSha256 = null;
276
- let output = 'terminal';
277
- let quiet = false;
278
- let color = true;
279
- for (let index = 0; index < argv.length; index += 1) {
280
- const argument = argv[index];
281
- if (argument === '--json') {
282
- if (output !== 'terminal')
283
- throw new CliUsageError('--json may be supplied only once.');
284
- output = 'json';
285
- }
286
- else if (argument === '--workflow') {
287
- if (workflow !== null)
288
- throw new CliUsageError('--workflow may be supplied only once.');
289
- workflow = argv[++index] ?? null;
290
- if (!workflow)
291
- throw new CliUsageError('--workflow requires a bounded label.');
292
- }
293
- else if (argument === '--configuration-digest') {
294
- if (configurationDigestSha256 !== null)
295
- throw new CliUsageError('--configuration-digest may be supplied only once.');
296
- configurationDigestSha256 = argv[++index] ?? null;
297
- if (!configurationDigestSha256 || !/^sha256:[a-f0-9]{64}$/u.test(configurationDigestSha256)) {
298
- throw new CliUsageError('--configuration-digest requires a sha256: hexadecimal digest.');
299
- }
300
- }
301
- else if (argument === '--quiet')
302
- quiet = true;
303
- else if (argument === '--no-color')
304
- color = false;
305
- else if (argument?.startsWith('-'))
306
- throw new CliUsageError(`Unknown option "${argument}".`);
307
- else if (argument)
308
- sources.push(argument);
309
- }
310
- if (!workflow)
311
- throw new CliUsageError('manifest requires --workflow <label>.');
312
- if (sources.length < 1 || sources.length > 16)
313
- throw new CliUsageError('manifest requires from 1 to 16 JSON artefact files.');
314
- if (new Set(sources).size !== sources.length)
315
- throw new CliUsageError('manifest artefact files must be different.');
316
- if (quiet && output !== 'terminal')
317
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
318
- return { action: 'manifest', sources, workflow, configurationDigestSha256, output, quiet, color };
319
- }
320
- function parseSingleJsonInput(argv, overflowMessage) {
321
- let source = null;
322
- let output = 'terminal';
323
- let quiet = false;
324
- let color = true;
325
- for (const argument of argv) {
326
- if (argument === '--json') {
327
- if (output !== 'terminal')
328
- throw new CliUsageError('--json may be supplied only once.');
329
- output = 'json';
330
- }
331
- else if (argument === '--quiet')
332
- quiet = true;
333
- else if (argument === '--no-color')
334
- color = false;
335
- else if (argument.startsWith('-'))
336
- throw new CliUsageError(`Unknown option "${argument}".`);
337
- else if (source === null)
338
- source = argument;
339
- else
340
- throw new CliUsageError(overflowMessage);
341
- }
342
- if (quiet && output !== 'terminal')
343
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
344
- return { source, output, quiet, color };
39
+ function parseSignArtifactArguments(parsed) {
40
+ return {
41
+ action: 'sign-artifact',
42
+ source: parsed.positionalValue('source'),
43
+ privateKeySource: parsed.optionValue('--private-key-file'),
44
+ };
345
45
  }
346
- function parseOfflineImportArguments(action, argv) {
347
- const parsed = parseSingleJsonInput(argv, `${action} accepts one optional versioned JSON input file.`);
348
- return { action, ...parsed };
46
+ function parseVerifySignatureArguments(parsed) {
47
+ return {
48
+ action: 'verify-signature',
49
+ source: parsed.positionalValue('source'),
50
+ publicKeySource: parsed.optionValue('--public-key-file'),
51
+ trustStoreSource: parsed.optionValue('--trust-store-file'),
52
+ output: parsed.hasOption('--json') ? 'json' : 'terminal',
53
+ ...terminalOptions(parsed),
54
+ };
349
55
  }
350
- function parseCommandsArguments(argv) {
351
- let output = 'terminal';
352
- let common = false;
353
- let group = null;
354
- let mode = null;
355
- let quiet = false;
356
- let color = true;
357
- for (let index = 0; index < argv.length; index += 1) {
358
- const argument = argv[index];
359
- if (argument === '--json') {
360
- if (output !== 'terminal')
361
- throw new CliUsageError('--json may be supplied only once.');
362
- output = 'json';
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
- }
387
- else if (argument === '--quiet')
388
- quiet = true;
389
- else if (argument === '--no-color')
390
- color = false;
391
- else
392
- throw new CliUsageError(`Unknown option "${argument}".`);
393
- }
394
- if (quiet && output !== 'terminal')
395
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
396
- return { action: 'commands', output, common, group, mode, quiet, color };
56
+ function jsonOutput(parsed) {
57
+ return parsed.hasOption('--json') ? 'json' : 'terminal';
397
58
  }
398
- function parseDoctorArguments(argv) {
399
- let network = false;
400
- let output = 'terminal';
401
- let quiet = false;
402
- let color = true;
403
- for (const argument of argv) {
404
- if (argument === '--network') {
405
- if (network)
406
- throw new CliUsageError('--network may be supplied only once.');
407
- network = true;
408
- }
409
- else if (argument === '--json') {
410
- if (output !== 'terminal')
411
- throw new CliUsageError('--json may be supplied only once.');
412
- output = 'json';
413
- }
414
- else if (argument === '--quiet')
415
- quiet = true;
416
- else if (argument === '--no-color')
417
- color = false;
418
- else
419
- throw new CliUsageError(`Unknown option "${argument}".`);
59
+ function normalizedLabel(parsed, option, maximum) {
60
+ const value = parsed.optionValue(option);
61
+ if (value === null)
62
+ return null;
63
+ const normalized = value.replace(/\s+/gu, ' ').trim();
64
+ if (!normalized || normalized.length > maximum) {
65
+ throw new CliUsageError(`${option} requires a label of at most ${maximum} characters.`);
420
66
  }
421
- if (quiet && output !== 'terminal')
422
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
423
- return { action: 'doctor', network, output, quiet, color };
67
+ return normalized;
424
68
  }
425
- function parseBulkArguments(argv) {
426
- let source = null;
427
- let output = 'terminal';
428
- let deep = false;
429
- let scanMode = null;
430
- let quiet = false;
431
- let color = true;
432
- let concurrency = null;
433
- let checkpoint = null;
434
- let resume = false;
435
- let events = false;
436
- let plan = false;
437
- let failOn = null;
438
- let filter = 'all';
439
- for (let index = 0; index < argv.length; index++) {
440
- const argument = argv[index];
441
- if (argument === undefined)
442
- break;
443
- if (argument === '--json' || argument === '--jsonl' || argument === '--csv' || argument === '--domains' || argument === '--queries' || argument === '--junit') {
444
- if (output !== 'terminal')
445
- throw new CliUsageError('Choose only one output format.');
446
- output = argument === '--json'
447
- ? 'json'
448
- : argument === '--jsonl'
449
- ? 'jsonl'
450
- : argument === '--csv'
451
- ? 'csv'
452
- : argument === '--domains'
453
- ? 'domains'
454
- : argument === '--queries'
455
- ? 'queries'
456
- : 'junit';
457
- }
458
- else if (argument === '--deep' || argument === '--fast') {
459
- if (scanMode)
460
- throw new CliUsageError('--fast and --deep are mutually exclusive and may be supplied only once.');
461
- scanMode = argument === '--deep' ? 'deep' : 'fast';
462
- deep = scanMode === 'deep';
463
- }
464
- else if (argument === '--concurrency') {
465
- if (concurrency !== null)
466
- throw new CliUsageError('--concurrency may be supplied only once.');
467
- const raw = argv[++index];
468
- if (!raw || !/^\d+$/.test(raw))
469
- throw new CliUsageError('--concurrency requires an integer from 1 to 8.');
470
- concurrency = Number(raw);
471
- if (concurrency < 1 || concurrency > 8)
472
- throw new CliUsageError('--concurrency must be from 1 to 8.');
473
- }
474
- else if (argument === '--checkpoint') {
475
- if (checkpoint !== null)
476
- throw new CliUsageError('--checkpoint may be supplied only once.');
477
- const value = argv[++index];
478
- if (!value || value.startsWith('-'))
479
- throw new CliUsageError('--checkpoint requires one bounded file path.');
480
- checkpoint = value;
481
- }
482
- else if (argument === '--resume') {
483
- if (resume)
484
- throw new CliUsageError('--resume may be supplied only once.');
485
- resume = true;
486
- }
487
- else if (argument === '--events') {
488
- if (events)
489
- throw new CliUsageError('--events may be supplied only once.');
490
- events = true;
491
- }
492
- else if (argument === '--plan') {
493
- if (plan)
494
- throw new CliUsageError('--plan may be supplied only once.');
495
- plan = true;
496
- }
497
- else if (argument === '--fail-on') {
498
- if (failOn !== null)
499
- throw new CliUsageError('--fail-on may be supplied only once.');
500
- failOn = parseCliFailPolicies(argv[++index], 'bulk');
501
- }
502
- else if (argument === '--registered-only' || argument === '--inconclusive-only' || argument === '--errors-only') {
503
- if (filter !== 'all')
504
- throw new CliUsageError('Bulk output filters are mutually exclusive and may be supplied only once.');
505
- filter = argument === '--registered-only'
506
- ? 'registered'
507
- : argument === '--inconclusive-only'
508
- ? 'inconclusive'
509
- : 'errors';
510
- }
511
- else if (argument === '--quiet')
512
- quiet = true;
513
- else if (argument === '--no-color')
514
- color = false;
515
- else if (argument.startsWith('-'))
516
- throw new CliUsageError(`Unknown option "${argument}".`);
517
- else if (source === null)
518
- source = argument;
519
- else
520
- throw new CliUsageError('bulk accepts one optional input file. Otherwise pipe newline-delimited queries on stdin.');
521
- }
522
- if (quiet && output !== 'terminal')
523
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
524
- if (resume && checkpoint === null)
525
- throw new CliUsageError('--resume requires --checkpoint.');
526
- if (plan && (events || resume || checkpoint !== null || quiet || failOn !== null || !['terminal', 'json'].includes(output))) {
527
- throw new CliUsageError('--plan supports terminal or JSON output and cannot be combined with events, checkpoint, resume, or quiet options.');
528
- }
529
- const maximum = deep ? 3 : 8;
530
- if (concurrency !== null && concurrency > maximum) {
531
- throw new CliUsageError(`--concurrency is capped at ${maximum} in ${deep ? 'deep' : 'fast'} bulk mode.`);
532
- }
533
- return { action: 'bulk', source, output, deep, quiet, color, concurrency: concurrency ?? (deep ? 2 : 4), checkpoint, resume, events, plan, filter, ...(failOn ? { failOn } : {}) };
69
+ function failPolicies(parsed, command) {
70
+ const value = parsed.optionValue('--fail-on');
71
+ return value === null ? null : parseCliFailPolicies(value, command);
534
72
  }
535
- function parseCtSearchArguments(argv) {
536
- const { source: keyword, ...options } = parseSingleJsonInput(argv, 'ct-search accepts one keyword. Quote multi-word keywords as one argument.');
537
- return { action: 'ct-search', keyword, ...options };
73
+ function parseOutput(parsed, formats) {
74
+ return formats.find(([option]) => parsed.hasOption(option))?.[1] ?? 'terminal';
538
75
  }
539
- function parseCtIntakeArguments(argv) {
540
- const parsed = parseSingleJsonInput(argv, 'ct-intake accepts one optional versioned JSON event file.');
541
- return { action: 'ct-intake', ...parsed };
76
+ function singleInputAction(action, parsed, positionalName = 'source') {
77
+ return {
78
+ action,
79
+ [positionalName === 'source' ? 'source' : positionalName]: parsed.positionalValue(positionalName),
80
+ output: jsonOutput(parsed),
81
+ ...terminalOptions(parsed),
82
+ };
542
83
  }
543
- function parseDiscoverArguments(argv) {
544
- let seed = null;
545
- let output = 'terminal';
546
- let quiet = false;
547
- let color = true;
548
- let preset = 'all';
549
- let keyboardLayout = 'qwerty';
550
- let tldText = null;
551
- let dictionarySource = null;
552
- let familyText = null;
553
- let snapshotSource = null;
554
- let presetSet = false;
555
- let keyboardSet = false;
556
- for (let index = 0; index < argv.length; index++) {
557
- const argument = argv[index];
558
- if (argument === undefined)
559
- break;
560
- if (argument === '--json' || argument === '--jsonl' || argument === '--domains') {
561
- if (output !== 'terminal')
562
- throw new CliUsageError('Choose only one output format.');
563
- output = argument === '--json' ? 'json' : argument === '--jsonl' ? 'jsonl' : 'domains';
564
- }
565
- else if (argument === '--preset') {
566
- if (presetSet)
567
- throw new CliUsageError('--preset may be supplied only once.');
568
- if (familyText !== null)
569
- throw new CliUsageError('--preset cannot be combined with --families.');
570
- const value = argv[++index];
571
- if (value !== 'common' && value !== 'impersonation' && value !== 'all') {
572
- throw new CliUsageError('--preset requires common, impersonation, or all.');
573
- }
574
- preset = value;
575
- presetSet = true;
576
- }
577
- else if (argument === '--keyboard') {
578
- if (keyboardSet)
579
- throw new CliUsageError('--keyboard may be supplied only once.');
580
- const value = argv[++index];
581
- if (value !== 'qwerty' && value !== 'azerty' && value !== 'qwertz' && value !== 'all') {
582
- throw new CliUsageError('--keyboard requires qwerty, azerty, qwertz, or all.');
583
- }
584
- keyboardLayout = value;
585
- keyboardSet = true;
586
- }
587
- else if (argument === '--tlds') {
588
- if (tldText !== null)
589
- throw new CliUsageError('--tlds may be supplied only once.');
590
- const value = argv[++index];
591
- if (!value)
592
- throw new CliUsageError('--tlds requires a comma-separated list.');
593
- tldText = value;
594
- }
595
- else if (argument === '--dictionary') {
596
- if (dictionarySource !== null)
597
- throw new CliUsageError('--dictionary may be supplied only once.');
598
- const value = argv[++index];
599
- if (!value || value.startsWith('-'))
600
- throw new CliUsageError('--dictionary requires one UTF-8 text file.');
601
- dictionarySource = value;
602
- }
603
- else if (argument === '--families') {
604
- if (familyText !== null)
605
- throw new CliUsageError('--families may be supplied only once.');
606
- if (presetSet)
607
- throw new CliUsageError('--families cannot be combined with --preset.');
608
- const value = argv[++index];
609
- if (!value || value.startsWith('-'))
610
- throw new CliUsageError('--families requires a comma-separated list of mutation family IDs.');
611
- familyText = value;
612
- preset = 'custom';
613
- }
614
- else if (argument === '--snapshot') {
615
- if (snapshotSource !== null)
616
- throw new CliUsageError('--snapshot may be supplied only once.');
617
- const value = argv[++index];
618
- if (!value || value.startsWith('-'))
619
- throw new CliUsageError('--snapshot requires one bounded local state file path.');
620
- snapshotSource = value;
621
- }
622
- else if (argument === '--quiet')
623
- quiet = true;
624
- else if (argument === '--no-color')
625
- color = false;
626
- else if (argument.startsWith('-'))
627
- throw new CliUsageError(`Unknown option "${argument}".`);
628
- else if (seed === null)
629
- seed = argument;
630
- else
631
- throw new CliUsageError('discover accepts one brand label or domain. Quote multi-word labels as one argument.');
632
- }
633
- if (quiet && output !== 'terminal')
634
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
635
- if (dictionarySource && preset === 'common') {
636
- throw new CliUsageError('--dictionary requires the impersonation or all preset.');
637
- }
638
- return { action: 'discover', seed, output, quiet, color, preset, keyboardLayout, tldText, dictionarySource, familyText, snapshotSource };
84
+ function parseLookupArguments(parsed) {
85
+ const output = parseOutput(parsed, [
86
+ ['--json', 'json'], ['--junit', 'junit'], ['--markdown', 'markdown'], ['--html', 'html'],
87
+ ]);
88
+ const selectedFailPolicies = failPolicies(parsed, 'lookup');
89
+ return {
90
+ action: 'lookup',
91
+ query: parsed.positionalValue('target'),
92
+ output,
93
+ deep: parsed.hasOption('--deep'),
94
+ detail: parsed.hasOption('--summary') ? 'summary' : parsed.hasOption('--verbose') ? 'verbose' : 'standard',
95
+ strictExit: parsed.hasOption('--strict-exit'),
96
+ events: parsed.hasOption('--events'),
97
+ plan: parsed.hasOption('--plan'),
98
+ includeAttribution: !parsed.hasOption('--no-attribution'),
99
+ observerLabel: normalizedLabel(parsed, '--observer', 80),
100
+ vantageLabel: normalizedLabel(parsed, '--vantage', 80),
101
+ ...terminalOptions(parsed),
102
+ ...(parsed.hasOption('--browse') ? { browse: true } : {}),
103
+ ...(parsed.hasOption('--exact-url') ? { exactUrl: true } : {}),
104
+ ...(parsed.optionValue('--save-lookup') ? { saveLookup: parsed.optionValue('--save-lookup') } : {}),
105
+ ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
106
+ };
639
107
  }
640
- function parseDiscoverScanArguments(argv) {
641
- let seed = null;
642
- let output = 'terminal';
643
- let quiet = false;
644
- let color = true;
645
- let preset = 'all';
646
- let keyboardLayout = 'qwerty';
647
- let tldText = null;
648
- let dictionarySource = null;
649
- let familyText = null;
650
- let presetSet = false;
651
- let keyboardSet = false;
652
- let deep = false;
653
- let scanModeSet = false;
654
- let scanLimit = null;
655
- let chunkSize = null;
656
- let concurrency = null;
657
- let checkpoint = null;
658
- let resume = false;
659
- let resolverText = null;
660
- let observationSnapshot = null;
661
- let allowlistSource = null;
662
- let filter = 'all';
663
- let filterSet = false;
664
- let events = false;
665
- let plan = false;
666
- let failOn = null;
667
- for (let index = 0; index < argv.length; index++) {
668
- const argument = argv[index];
669
- if (argument === undefined)
670
- break;
671
- if (argument === '--json' || argument === '--jsonl' || argument === '--csv' || argument === '--domains') {
672
- if (output !== 'terminal')
673
- throw new CliUsageError('Choose only one output format.');
674
- output = argument === '--json' ? 'json' : argument === '--jsonl' ? 'jsonl' : argument === '--csv' ? 'csv' : 'domains';
675
- }
676
- else if (argument === '--preset') {
677
- if (presetSet)
678
- throw new CliUsageError('--preset may be supplied only once.');
679
- if (familyText !== null)
680
- throw new CliUsageError('--preset cannot be combined with --families.');
681
- const value = argv[++index];
682
- if (value !== 'common' && value !== 'impersonation' && value !== 'all') {
683
- throw new CliUsageError('--preset requires common, impersonation, or all.');
684
- }
685
- preset = value;
686
- presetSet = true;
687
- }
688
- else if (argument === '--keyboard') {
689
- if (keyboardSet)
690
- throw new CliUsageError('--keyboard may be supplied only once.');
691
- const value = argv[++index];
692
- if (value !== 'qwerty' && value !== 'azerty' && value !== 'qwertz' && value !== 'all') {
693
- throw new CliUsageError('--keyboard requires qwerty, azerty, qwertz, or all.');
694
- }
695
- keyboardLayout = value;
696
- keyboardSet = true;
697
- }
698
- else if (argument === '--tlds') {
699
- if (tldText !== null)
700
- throw new CliUsageError('--tlds may be supplied only once.');
701
- const value = argv[++index];
702
- if (!value)
703
- throw new CliUsageError('--tlds requires a comma-separated list.');
704
- tldText = value;
705
- }
706
- else if (argument === '--dictionary') {
707
- if (dictionarySource !== null)
708
- throw new CliUsageError('--dictionary may be supplied only once.');
709
- const value = argv[++index];
710
- if (!value || value.startsWith('-'))
711
- throw new CliUsageError('--dictionary requires one UTF-8 text file.');
712
- dictionarySource = value;
713
- }
714
- else if (argument === '--families') {
715
- if (familyText !== null)
716
- throw new CliUsageError('--families may be supplied only once.');
717
- if (presetSet)
718
- throw new CliUsageError('--families cannot be combined with --preset.');
719
- const value = argv[++index];
720
- if (!value || value.startsWith('-'))
721
- throw new CliUsageError('--families requires a comma-separated list of mutation family IDs.');
722
- familyText = value;
723
- preset = 'custom';
724
- }
725
- else if (argument === '--deep' || argument === '--fast') {
726
- if (scanModeSet)
727
- throw new CliUsageError('--fast and --deep are mutually exclusive and may be supplied only once.');
728
- deep = argument === '--deep';
729
- scanModeSet = true;
730
- }
731
- else if (argument === '--scan-limit') {
732
- if (scanLimit !== null)
733
- throw new CliUsageError('--scan-limit may be supplied only once.');
734
- scanLimit = positiveIntegerOption(argv[++index], '--scan-limit', 500);
735
- }
736
- else if (argument === '--chunk-size') {
737
- if (chunkSize !== null)
738
- throw new CliUsageError('--chunk-size may be supplied only once.');
739
- chunkSize = positiveIntegerOption(argv[++index], '--chunk-size', 100);
740
- }
741
- else if (argument === '--concurrency') {
742
- if (concurrency !== null)
743
- throw new CliUsageError('--concurrency may be supplied only once.');
744
- concurrency = positiveIntegerOption(argv[++index], '--concurrency', 8);
745
- }
746
- else if (argument === '--checkpoint') {
747
- if (checkpoint !== null)
748
- throw new CliUsageError('--checkpoint may be supplied only once.');
749
- const value = argv[++index];
750
- if (!value || value.startsWith('-'))
751
- throw new CliUsageError('--checkpoint requires one bounded file path.');
752
- checkpoint = value;
753
- }
754
- else if (argument === '--resume') {
755
- if (resume)
756
- throw new CliUsageError('--resume may be supplied only once.');
757
- resume = true;
758
- }
759
- else if (argument === '--resolver') {
760
- if (resolverText !== null)
761
- throw new CliUsageError('--resolver may be supplied only once.');
762
- const value = argv[++index];
763
- if (!value || value.startsWith('-'))
764
- throw new CliUsageError('--resolver requires a comma-separated list of IP addresses.');
765
- resolverText = value;
766
- }
767
- else if (argument === '--observation-snapshot') {
768
- if (observationSnapshot !== null)
769
- throw new CliUsageError('--observation-snapshot may be supplied only once.');
770
- const value = argv[++index];
771
- if (!value || value.startsWith('-'))
772
- throw new CliUsageError('--observation-snapshot requires one bounded local state file path.');
773
- observationSnapshot = value;
774
- }
775
- else if (argument === '--allowlist') {
776
- if (allowlistSource !== null)
777
- throw new CliUsageError('--allowlist may be supplied only once.');
778
- const value = argv[++index];
779
- if (!value || value.startsWith('-'))
780
- throw new CliUsageError('--allowlist requires one newline-delimited local file.');
781
- allowlistSource = value;
782
- }
783
- else if (argument === '--registered-only' || argument === '--inconclusive-only'
784
- || argument === '--acquisition-only' || argument === '--suppressed-only') {
785
- if (filterSet)
786
- throw new CliUsageError('Discovery scan filters are mutually exclusive and may be supplied only once.');
787
- filter = argument === '--registered-only' ? 'registered'
788
- : argument === '--inconclusive-only' ? 'inconclusive'
789
- : argument === '--acquisition-only' ? 'acquisition' : 'suppressed';
790
- filterSet = true;
791
- }
792
- else if (argument === '--events') {
793
- if (events)
794
- throw new CliUsageError('--events may be supplied only once.');
795
- events = true;
796
- }
797
- else if (argument === '--plan') {
798
- if (plan)
799
- throw new CliUsageError('--plan may be supplied only once.');
800
- plan = true;
801
- }
802
- else if (argument === '--fail-on') {
803
- if (failOn !== null)
804
- throw new CliUsageError('--fail-on may be supplied only once.');
805
- failOn = parseCliFailPolicies(argv[++index], 'discover-scan');
806
- }
807
- else if (argument === '--quiet')
808
- quiet = true;
809
- else if (argument === '--no-color')
810
- color = false;
811
- else if (argument.startsWith('-'))
812
- throw new CliUsageError(`Unknown option "${argument}".`);
813
- else if (seed === null)
814
- seed = argument;
815
- else
816
- throw new CliUsageError('discover-scan accepts one brand label or domain. Quote multi-word labels as one argument.');
817
- }
818
- const maximum = deep ? 50 : 500;
819
- if (scanLimit !== null && scanLimit > maximum)
820
- throw new CliUsageError(`--scan-limit is capped at ${maximum} in ${deep ? 'deep' : 'fast'} mode.`);
821
- const maximumConcurrency = deep ? 3 : 8;
822
- if ((concurrency ?? (deep ? 2 : 4)) > maximumConcurrency) {
823
- throw new CliUsageError(`--concurrency is capped at ${maximumConcurrency} in ${deep ? 'deep' : 'fast'} mode.`);
824
- }
825
- if (resume && checkpoint === null)
826
- throw new CliUsageError('--resume requires --checkpoint.');
827
- if (plan && (events || resume || checkpoint !== null || observationSnapshot !== null || quiet || failOn !== null || !['terminal', 'json'].includes(output))) {
828
- throw new CliUsageError('--plan supports terminal or JSON output and cannot be combined with events, checkpoint, resume, observation snapshots, or quiet options.');
108
+ function parseManifestArguments(parsed) {
109
+ const sources = parsed.positionalValues('artefacts');
110
+ if (sources.some((source) => !source))
111
+ throw new CliUsageError(`manifest requires from 1 to ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} artefact files.`);
112
+ if (new Set(sources).size !== sources.length)
113
+ throw new CliUsageError('manifest artefact files must be different.');
114
+ const configurationDigestSha256 = parsed.optionValue('--configuration-digest');
115
+ if (configurationDigestSha256 !== null && !/^sha256:[a-f0-9]{64}$/u.test(configurationDigestSha256)) {
116
+ throw new CliUsageError('--configuration-digest requires a sha256: hexadecimal digest.');
829
117
  }
830
- if (quiet && output !== 'terminal')
831
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
832
- if (dictionarySource && preset === 'common')
833
- throw new CliUsageError('--dictionary requires the impersonation or all preset.');
834
118
  return {
835
- action: 'discover-scan', seed, output, quiet, color, preset, keyboardLayout, tldText,
836
- dictionarySource, familyText, deep, scanLimit: scanLimit ?? Math.min(100, maximum),
837
- chunkSize: chunkSize ?? 25, concurrency: concurrency ?? (deep ? 2 : 4), checkpoint,
838
- resume, resolverText, observationSnapshot, allowlistSource, filter, events, plan, ...(failOn ? { failOn } : {}),
119
+ action: 'manifest',
120
+ sources,
121
+ workflow: parsed.optionValue('--workflow'),
122
+ configurationDigestSha256,
123
+ ...(parsed.optionValue('--passphrase-file') ? { passphraseSource: parsed.optionValue('--passphrase-file') } : {}),
124
+ ...(parsed.hasOption('--package') ? { package: true } : {}),
125
+ ...(parsed.hasOption('--bagit') ? { bagit: true } : {}),
126
+ ...(parsed.optionValue('--folder') ? { folder: parsed.optionValue('--folder') } : {}),
127
+ output: jsonOutput(parsed),
128
+ ...terminalOptions(parsed),
839
129
  };
840
130
  }
841
- function positiveIntegerOption(value, option, maximum) {
842
- if (!value || !/^\d+$/u.test(value))
843
- throw new CliUsageError(`${option} requires an integer from 1 to ${maximum}.`);
844
- const parsed = Number(value);
845
- if (parsed < 1 || parsed > maximum)
846
- throw new CliUsageError(`${option} must be from 1 to ${maximum}.`);
847
- return parsed;
848
- }
849
- function parsePostureArguments(argv) {
850
- let domain = null;
851
- let output = 'terminal';
852
- let quiet = false;
853
- let color = true;
854
- let selectorText = null;
855
- let retiredSelectorText = null;
856
- let mailProfile = 'standard';
857
- let mailProfileSeen = false;
858
- let ownedDomain = false;
859
- for (let index = 0; index < argv.length; index++) {
860
- const argument = argv[index];
861
- if (argument === undefined)
862
- break;
863
- if (argument === '--json') {
864
- if (output !== 'terminal')
865
- throw new CliUsageError('--json may be supplied only once.');
866
- output = 'json';
867
- }
868
- else if (argument === '--sarif') {
869
- if (output !== 'terminal')
870
- throw new CliUsageError('Choose only one output format.');
871
- output = 'sarif';
872
- }
873
- else if (argument === '--owned-domain') {
874
- if (ownedDomain)
875
- throw new CliUsageError('--owned-domain may be supplied only once.');
876
- ownedDomain = true;
877
- }
878
- else if (argument === '--selectors') {
879
- if (selectorText !== null)
880
- throw new CliUsageError('--selectors may be supplied only once.');
881
- const value = argv[++index];
882
- if (!value)
883
- throw new CliUsageError('--selectors requires a comma-separated list.');
884
- selectorText = value;
885
- }
886
- else if (argument === '--retired-selectors') {
887
- if (retiredSelectorText !== null)
888
- throw new CliUsageError('--retired-selectors may be supplied only once.');
889
- const value = argv[++index];
890
- if (!value)
891
- throw new CliUsageError('--retired-selectors requires a comma-separated list.');
892
- retiredSelectorText = value;
893
- }
894
- else if (argument === '--mail-profile') {
895
- if (mailProfileSeen)
896
- throw new CliUsageError('--mail-profile may be supplied only once.');
897
- const value = argv[++index];
898
- if (!value || !['standard', 'defensive-no-mail', 'parked'].includes(value)) {
899
- throw new CliUsageError('--mail-profile must be standard, defensive-no-mail, or parked.');
900
- }
901
- mailProfile = value === 'defensive-no-mail' ? 'defensive_no_mail' : value;
902
- mailProfileSeen = true;
903
- }
904
- else if (argument === '--quiet')
905
- quiet = true;
906
- else if (argument === '--no-color')
907
- color = false;
908
- else if (argument.startsWith('-'))
909
- throw new CliUsageError(`Unknown option "${argument}".`);
910
- else if (domain === null)
911
- domain = argument;
912
- else
913
- throw new CliUsageError('posture accepts one domain.');
914
- }
915
- if (quiet && output !== 'terminal')
916
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
917
- if (output === 'sarif' && !ownedDomain)
918
- throw new CliUsageError('--sarif requires --owned-domain to confirm the passive posture target is controlled by the operator.');
919
- return { action: 'posture', domain, output, quiet, color, selectorText, retiredSelectorText, mailProfile, ownedDomain };
131
+ function parseBulkArguments(parsed) {
132
+ const deep = parsed.hasOption('--deep');
133
+ const selectedFailPolicies = failPolicies(parsed, 'bulk');
134
+ const output = parseOutput(parsed, [
135
+ ['--json', 'json'], ['--jsonl', 'jsonl'], ['--csv', 'csv'], ['--domains', 'domains'],
136
+ ['--csv-with-metadata', 'csv_metadata'],
137
+ ['--queries', 'queries'], ['--junit', 'junit'],
138
+ ]);
139
+ const filter = parsed.hasOption('--registered-only') ? 'registered'
140
+ : parsed.hasOption('--inconclusive-only') ? 'inconclusive'
141
+ : parsed.hasOption('--errors-only') ? 'errors' : 'all';
142
+ return {
143
+ action: 'bulk',
144
+ source: parsed.positionalValue('source'),
145
+ output,
146
+ deep,
147
+ concurrency: parsed.integerOption('--concurrency') ?? (deep ? 2 : 4),
148
+ checkpoint: parsed.optionValue('--checkpoint'),
149
+ resume: parsed.hasOption('--resume'),
150
+ events: parsed.hasOption('--events'),
151
+ plan: parsed.hasOption('--plan'),
152
+ filter,
153
+ ...terminalOptions(parsed),
154
+ ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
155
+ };
920
156
  }
921
- function parseHttpArguments(argv) {
922
- const { source: domain, ...options } = parseSingleJsonInput(argv, 'http accepts one domain.');
923
- return { action: 'http', domain, ...options };
157
+ function discoveryValues(parsed) {
158
+ const familyText = parsed.optionValue('--families');
159
+ return {
160
+ preset: (familyText ? 'custom' : parsed.optionValue('--preset') ?? 'all'),
161
+ keyboardLayout: (parsed.optionValue('--keyboard') ?? 'qwerty'),
162
+ tldText: parsed.optionValue('--tlds'),
163
+ dictionarySource: parsed.optionValue('--dictionary'),
164
+ familyText,
165
+ };
924
166
  }
925
- function parseTlsArguments(argv) {
926
- const { source: hostname, ...options } = parseSingleJsonInput(argv, 'tls accepts one hostname.');
927
- return { action: 'tls', hostname, ...options };
167
+ function parseDiscoverArguments(parsed) {
168
+ return {
169
+ action: 'discover',
170
+ seed: parsed.positionalValue('subject'),
171
+ output: parseOutput(parsed, [['--json', 'json'], ['--jsonl', 'jsonl'], ['--domains', 'domains']]),
172
+ ...discoveryValues(parsed),
173
+ snapshotSource: parsed.optionValue('--snapshot'),
174
+ ...terminalOptions(parsed),
175
+ };
928
176
  }
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 };
177
+ function parseDiscoverScanArguments(parsed) {
178
+ const deep = parsed.hasOption('--deep');
179
+ const selectedFailPolicies = failPolicies(parsed, 'discover-scan');
180
+ const filter = parsed.hasOption('--registered-only') ? 'registered'
181
+ : parsed.hasOption('--inconclusive-only') ? 'inconclusive'
182
+ : parsed.hasOption('--acquisition-only') ? 'acquisition'
183
+ : parsed.hasOption('--suppressed-only') ? 'suppressed' : 'all';
184
+ return {
185
+ action: 'discover-scan',
186
+ seed: parsed.positionalValue('subject'),
187
+ output: parseOutput(parsed, [['--json', 'json'], ['--jsonl', 'jsonl'], ['--csv', 'csv'], ['--csv-with-metadata', 'csv_metadata'], ['--domains', 'domains']]),
188
+ ...discoveryValues(parsed),
189
+ deep,
190
+ scanLimit: parsed.integerOption('--scan-limit') ?? Math.min(100, deep ? 50 : 500),
191
+ chunkSize: parsed.integerOption('--chunk-size') ?? 25,
192
+ concurrency: parsed.integerOption('--concurrency') ?? (deep ? 2 : 4),
193
+ checkpoint: parsed.optionValue('--checkpoint'),
194
+ resume: parsed.hasOption('--resume'),
195
+ resolverText: parsed.optionValue('--resolver'),
196
+ observationSnapshot: parsed.optionValue('--observation-snapshot'),
197
+ allowlistSource: parsed.optionValue('--allowlist'),
198
+ filter,
199
+ events: parsed.hasOption('--events'),
200
+ plan: parsed.hasOption('--plan'),
201
+ ...terminalOptions(parsed),
202
+ ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
203
+ };
983
204
  }
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 };
205
+ function parsePostureArguments(parsed) {
206
+ const mailProfile = parsed.optionValue('--mail-profile') ?? 'standard';
207
+ return {
208
+ action: 'posture',
209
+ domain: parsed.positionalValue('domain'),
210
+ output: parseOutput(parsed, [['--json', 'json'], ['--sarif', 'sarif']]),
211
+ selectorText: parsed.optionValue('--selectors'),
212
+ retiredSelectorText: parsed.optionValue('--retired-selectors'),
213
+ mailProfile: mailProfile === 'defensive-no-mail' ? 'defensive_no_mail' : mailProfile,
214
+ ownedDomain: parsed.hasOption('--owned-domain'),
215
+ ...(parsed.hasOption('--include-inherited-dns') ? { includeInheritedDns: true } : {}),
216
+ ...terminalOptions(parsed),
217
+ };
1044
218
  }
1045
- function parseCompareArguments(argv) {
1046
- const parsed = parseSingleJsonInput(argv, 'compare accepts one optional lookup JSON file. Otherwise pipe one lookup document on stdin.');
1047
- return { action: 'compare', ...parsed };
219
+ function parseDnssecValidateArguments(parsed) {
220
+ return {
221
+ action: 'dnssec-validate',
222
+ target: parsed.positionalValue('domain'),
223
+ resolver: parsed.optionValue('--resolver'),
224
+ trustAnchorSource: parsed.optionValue('--trust-anchor'),
225
+ ownedOrAuthorized: true,
226
+ output: jsonOutput(parsed),
227
+ ...terminalOptions(parsed),
228
+ };
1048
229
  }
1049
- function parsePageCompareArguments(argv) {
1050
- const parsed = parseTwoFileComparisonArguments(argv, 'page-compare');
1051
- return { action: 'page-compare', ...parsed };
230
+ function parseMailTransportArguments(parsed) {
231
+ return {
232
+ action: 'mail-transport',
233
+ source: parsed.positionalValue('source'),
234
+ resolver: parsed.optionValue('--resolver'),
235
+ trustAnchorSource: parsed.optionValue('--trust-anchor'),
236
+ ownedOrAuthorized: true,
237
+ activeProbeAcknowledged: true,
238
+ output: jsonOutput(parsed),
239
+ ...terminalOptions(parsed),
240
+ };
1052
241
  }
1053
- function parseTwoFileComparisonArguments(argv, command) {
1054
- const sources = [];
1055
- let output = 'terminal';
1056
- let quiet = false;
1057
- let color = true;
1058
- for (const argument of argv) {
1059
- if (argument === '--json') {
1060
- if (output !== 'terminal')
1061
- throw new CliUsageError('--json may be supplied only once.');
1062
- output = 'json';
1063
- }
1064
- else if (argument === '--quiet')
1065
- quiet = true;
1066
- else if (argument === '--no-color')
1067
- color = false;
1068
- else if (argument.startsWith('-'))
1069
- throw new CliUsageError(`Unknown option "${argument}".`);
1070
- else
1071
- sources.push(argument);
1072
- }
1073
- if (sources.length !== 2 || !sources[0] || !sources[1])
1074
- throw new CliUsageError(`${command} requires two input JSON files.`);
242
+ function parseTwoFileComparisonArguments(parsed, command) {
243
+ const sources = parsed.positionalValues('sources');
1075
244
  if (sources[0] === sources[1])
1076
245
  throw new CliUsageError(`${command} requires two different input files.`);
1077
- if (quiet && output !== 'terminal')
1078
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1079
- return { leftSource: sources[0], rightSource: sources[1], output, quiet, color };
1080
- }
1081
- function parseMailReviewArguments(argv) {
1082
- const parsed = parseSingleJsonInput(argv, 'mail-review accepts one optional Bulk JSON or JSONL input file.');
1083
- return { action: 'mail-review', ...parsed };
1084
- }
1085
- function parseReviewEvidenceArguments(argv) {
1086
- let source = null;
1087
- let mmdbSource = null;
1088
- let output = 'terminal';
1089
- let strictExit = false;
1090
- let quiet = false;
1091
- let color = true;
1092
- for (let index = 0; index < argv.length; index += 1) {
1093
- const argument = argv[index];
1094
- if (argument === undefined)
1095
- break;
1096
- if (argument === '--json') {
1097
- if (output !== 'terminal')
1098
- throw new CliUsageError('--json may be supplied only once.');
1099
- output = 'json';
1100
- }
1101
- else if (argument === '--mmdb') {
1102
- const value = argv[++index];
1103
- if (!value || value.startsWith('-') || mmdbSource !== null)
1104
- throw new CliUsageError('--mmdb requires one database file and may be supplied only once.');
1105
- mmdbSource = value;
1106
- }
1107
- else if (argument === '--strict-exit') {
1108
- if (strictExit)
1109
- throw new CliUsageError('--strict-exit may be supplied only once.');
1110
- strictExit = true;
1111
- }
1112
- else if (argument === '--quiet')
1113
- quiet = true;
1114
- else if (argument === '--no-color')
1115
- color = false;
1116
- else if (argument.startsWith('-'))
1117
- throw new CliUsageError(`Unknown option "${argument}".`);
1118
- else if (source === null)
1119
- source = argument;
1120
- else
1121
- throw new CliUsageError('review-evidence accepts one optional versioned JSON input file.');
1122
- }
1123
- if (quiet && output !== 'terminal')
1124
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1125
- return { action: 'review-evidence', source, mmdbSource, output, strictExit, quiet, color };
1126
- }
1127
- function parseBriefArguments(argv) {
1128
- const parsed = parseSingleJsonInput(argv, 'brief accepts one optional saved Lookup file.');
1129
- return { action: 'brief', ...parsed };
246
+ return { leftSource: sources[0], rightSource: sources[1], output: jsonOutput(parsed), ...terminalOptions(parsed) };
1130
247
  }
1131
- function parseCasePackArguments(argv) {
1132
- let source = null;
1133
- let output = 'terminal';
1134
- let audience = null;
1135
- let reviewed = false;
1136
- let quiet = false;
1137
- let color = true;
1138
- for (let index = 0; index < argv.length; index += 1) {
1139
- const argument = argv[index];
1140
- if (argument === undefined)
1141
- break;
1142
- if (argument === '--json') {
1143
- if (output !== 'terminal')
1144
- throw new CliUsageError('--json may be supplied only once.');
1145
- output = 'json';
1146
- }
1147
- else if (argument === '--audience') {
1148
- if (audience !== null)
1149
- throw new CliUsageError('--audience may be supplied only once.');
1150
- const value = argv[++index];
1151
- if (!['internal', 'trusted', 'public'].includes(value || ''))
1152
- throw new CliUsageError('--audience requires internal, trusted, or public.');
1153
- audience = value;
1154
- }
1155
- else if (argument === '--reviewed') {
1156
- if (reviewed)
1157
- throw new CliUsageError('--reviewed may be supplied only once.');
1158
- reviewed = true;
1159
- }
1160
- else if (argument === '--quiet')
1161
- quiet = true;
1162
- else if (argument === '--no-color')
1163
- color = false;
1164
- else if (argument.startsWith('-'))
1165
- throw new CliUsageError(`Unknown option "${argument}".`);
1166
- else if (source === null)
1167
- source = argument;
1168
- else
1169
- throw new CliUsageError('case-pack accepts one optional case-export file.');
1170
- }
1171
- if (audience === null)
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.');
1175
- if (quiet && output !== 'terminal')
1176
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1177
- return { action: 'case-pack', source, output, audience, reviewed: true, quiet, color };
1178
- }
1179
- function parseMonitorOnceArguments(argv) {
1180
- let source = null;
1181
- let previousSource = null;
1182
- let output = 'terminal';
1183
- let limit = 20;
1184
- let concurrency = 2;
1185
- let limitSupplied = false;
1186
- let concurrencySupplied = false;
1187
- let failOn = null;
1188
- let quiet = false;
1189
- let color = true;
1190
- for (let index = 0; index < argv.length; index += 1) {
1191
- const argument = argv[index];
1192
- if (argument === undefined)
1193
- break;
1194
- if (argument === '--json') {
1195
- if (output !== 'terminal')
1196
- throw new CliUsageError('--json may be supplied only once.');
1197
- output = 'json';
1198
- }
1199
- else if (argument === '--junit') {
1200
- if (output !== 'terminal')
1201
- throw new CliUsageError('Choose only one output format.');
1202
- output = 'junit';
1203
- }
1204
- else if (argument === '--previous') {
1205
- if (previousSource !== null)
1206
- throw new CliUsageError('--previous may be supplied only once.');
1207
- const value = argv[++index];
1208
- if (!value || value.startsWith('-'))
1209
- throw new CliUsageError('--previous requires one prior monitor snapshot file.');
1210
- previousSource = value;
1211
- }
1212
- else if (argument === '--limit' || argument === '--concurrency') {
1213
- if (argument === '--limit' ? limitSupplied : concurrencySupplied) {
1214
- throw new CliUsageError(`${argument} may be supplied only once.`);
1215
- }
1216
- const value = Number(argv[++index]);
1217
- const maximum = argument === '--limit' ? 20 : 3;
1218
- if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
1219
- throw new CliUsageError(`${argument} requires an integer from 1 to ${maximum}.`);
1220
- if (argument === '--limit') {
1221
- limit = value;
1222
- limitSupplied = true;
1223
- }
1224
- else {
1225
- concurrency = value;
1226
- concurrencySupplied = true;
1227
- }
1228
- }
1229
- else if (argument === '--fail-on') {
1230
- if (failOn !== null)
1231
- throw new CliUsageError('--fail-on may be supplied only once.');
1232
- failOn = parseCliFailPolicies(argv[++index], 'monitor-once');
1233
- }
1234
- else if (argument === '--quiet')
1235
- quiet = true;
1236
- else if (argument === '--no-color')
1237
- color = false;
1238
- else if (argument.startsWith('-'))
1239
- throw new CliUsageError(`Unknown option "${argument}".`);
1240
- else if (source === null)
1241
- source = argument;
1242
- else
1243
- throw new CliUsageError('monitor-once accepts one optional domain-control manifest file.');
1244
- }
1245
- if (quiet && output !== 'terminal')
1246
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1247
- return { action: 'monitor-once', source, previousSource, output, limit, concurrency, quiet, color, ...(failOn ? { failOn } : {}) };
1248
- }
1249
- function parseDomainControlArguments(argv) {
1250
- const parsed = parseSingleJsonInput(argv, 'domain-control accepts one optional versioned JSON input file.');
1251
- return { action: 'domain-control', ...parsed };
1252
- }
1253
- function parseAssuranceArguments(argv) {
1254
- const parsed = parseSingleJsonInput(argv, 'assurance accepts one optional versioned JSON input file.');
1255
- return { action: 'assurance', ...parsed };
248
+ function parseVerifyArtifactArguments(parsed) {
249
+ if (parsed.hasOption('--folder') && parsed.positionalValue('source'))
250
+ throw new CliUsageError('--folder selects its own input; do not also supply a file or stdin marker.');
251
+ return {
252
+ action: 'verify-artifact',
253
+ source: parsed.positionalValue('source'),
254
+ passphraseSource: parsed.optionValue('--passphrase-file'),
255
+ manifestSource: parsed.optionValue('--manifest'),
256
+ manifestEntryId: parsed.optionValue('--manifest-entry'),
257
+ ...(parsed.hasOption('--package') ? { package: true } : {}),
258
+ ...(parsed.hasOption('--bagit') ? { bagit: true } : {}),
259
+ ...(parsed.optionValue('--folder') ? { folder: parsed.optionValue('--folder') } : {}),
260
+ output: jsonOutput(parsed),
261
+ strictExit: parsed.hasOption('--strict-exit'),
262
+ ...terminalOptions(parsed),
263
+ };
1256
264
  }
1257
- function parseChangePacketArguments(argv) {
1258
- const parsed = parseSingleJsonInput(argv, 'change-packet accepts one optional versioned JSON input file.');
1259
- return { action: 'change-packet', ...parsed };
265
+ function parseMonitorOnceArguments(parsed) {
266
+ const selectedFailPolicies = failPolicies(parsed, 'monitor-once');
267
+ return {
268
+ action: 'monitor-once',
269
+ source: parsed.positionalValue('source'),
270
+ previousSource: parsed.optionValue('--previous'),
271
+ output: parseOutput(parsed, [['--json', 'json'], ['--junit', 'junit']]),
272
+ limit: parsed.integerOption('--limit') ?? 20,
273
+ concurrency: parsed.integerOption('--concurrency') ?? 2,
274
+ ...terminalOptions(parsed),
275
+ ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
276
+ };
1260
277
  }
1261
- function parseSharingReviewArguments(argv) {
1262
- let source = null;
1263
- let output = 'terminal';
1264
- let marking = null;
1265
- let recipientScope = null;
1266
- let purpose = null;
1267
- let humanReviewed = false;
1268
- let personalDataReviewed = false;
1269
- let redactionsConfirmed = false;
1270
- let quiet = false;
1271
- let color = true;
1272
- for (let index = 0; index < argv.length; index++) {
1273
- const argument = argv[index];
1274
- if (argument === undefined)
1275
- break;
1276
- if (argument === '--json') {
1277
- if (output !== 'terminal')
1278
- throw new CliUsageError('--json may be supplied only once.');
1279
- output = 'json';
1280
- }
1281
- else if (argument === '--marking') {
1282
- if (marking !== null)
1283
- throw new CliUsageError('--marking may be supplied only once.');
1284
- const value = argv[++index];
1285
- if (!['clear', 'green', 'amber', 'amber-strict', 'red'].includes(value || '')) {
1286
- throw new CliUsageError('--marking requires clear, green, amber, amber-strict, or red.');
1287
- }
1288
- marking = value;
1289
- }
1290
- else if (argument === '--recipient-scope') {
1291
- if (recipientScope !== null)
1292
- throw new CliUsageError('--recipient-scope may be supplied only once.');
1293
- const value = argv[++index];
1294
- if (!['public', 'community', 'organization', 'named-recipients'].includes(value || '')) {
1295
- throw new CliUsageError('--recipient-scope requires public, community, organization, or named-recipients.');
1296
- }
1297
- recipientScope = value;
1298
- }
1299
- else if (argument === '--purpose') {
1300
- if (purpose !== null)
1301
- throw new CliUsageError('--purpose may be supplied only once.');
1302
- const value = argv[++index];
1303
- if (!value || value.startsWith('-'))
1304
- throw new CliUsageError('--purpose requires one bounded description.');
1305
- const normalized = value.replace(/\s+/gu, ' ').trim();
1306
- if (!normalized || normalized.length > 200)
1307
- throw new CliUsageError('--purpose is limited to 200 characters.');
1308
- purpose = normalized;
1309
- }
1310
- else if (argument === '--human-reviewed')
1311
- humanReviewed = true;
1312
- else if (argument === '--personal-data-reviewed')
1313
- personalDataReviewed = true;
1314
- else if (argument === '--redactions-confirmed')
1315
- redactionsConfirmed = true;
1316
- else if (argument === '--quiet')
1317
- quiet = true;
1318
- else if (argument === '--no-color')
1319
- color = false;
1320
- else if (argument.startsWith('-'))
1321
- throw new CliUsageError(`Unknown option "${argument}".`);
1322
- else if (source === null)
1323
- source = argument;
1324
- else
1325
- throw new CliUsageError('sharing-review accepts one optional artefact JSON file.');
1326
- }
1327
- if (!marking || !recipientScope || !purpose) {
1328
- throw new CliUsageError('sharing-review requires --marking, --recipient-scope, and --purpose.');
1329
- }
1330
- if (quiet && output !== 'terminal')
1331
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
278
+ function parseSharingReviewArguments(parsed) {
279
+ const purpose = parsed.optionValue('--purpose').replace(/\s+/gu, ' ').trim();
280
+ if (!purpose || purpose.length > 200)
281
+ throw new CliUsageError('--purpose is limited to 200 characters.');
1332
282
  return {
1333
- action: 'sharing-review', source, output, marking, recipientScope, purpose,
1334
- humanReviewed, personalDataReviewed, redactionsConfirmed, quiet, color,
283
+ action: 'sharing-review',
284
+ source: parsed.positionalValue('source'),
285
+ output: jsonOutput(parsed),
286
+ marking: parsed.optionValue('--marking'),
287
+ recipientScope: parsed.optionValue('--recipient-scope'),
288
+ purpose,
289
+ humanReviewed: parsed.hasOption('--human-reviewed'),
290
+ personalDataReviewed: parsed.hasOption('--personal-data-reviewed'),
291
+ redactionsConfirmed: parsed.hasOption('--redactions-confirmed'),
292
+ ...terminalOptions(parsed),
1335
293
  };
1336
294
  }
1337
- function parseWorkflowPlanArguments(argv) {
1338
- const positional = [];
1339
- let discovery = null;
1340
- let explainedRecipe = null;
1341
- let output = 'terminal';
1342
- let quiet = false;
1343
- let color = true;
1344
- for (let index = 0; index < argv.length; index += 1) {
1345
- const argument = argv[index];
1346
- if (argument === '--json') {
1347
- if (output !== 'terminal')
1348
- throw new CliUsageError('--json may be supplied only once.');
1349
- output = 'json';
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
- }
1366
- else if (argument === '--quiet')
1367
- quiet = true;
1368
- else if (argument === '--no-color')
1369
- color = false;
1370
- else if (argument.startsWith('-'))
1371
- throw new CliUsageError(`Unknown option "${argument}".`);
1372
- else
1373
- positional.push(argument);
295
+ function parseWorkflowPlanArguments(parsed) {
296
+ const options = { output: jsonOutput(parsed), ...terminalOptions(parsed) };
297
+ if (parsed.hasOption('--list')) {
298
+ if (parsed.allPositionals.length > 0)
299
+ throw new CliUsageError('workflow-plan discovery options do not accept a subject.');
300
+ return { action: 'workflow-plan', discovery: 'list', ...options };
1374
301
  }
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)
302
+ if (parsed.hasOption('--explain')) {
303
+ if (parsed.allPositionals.length > 0)
1379
304
  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 };
305
+ return { action: 'workflow-plan', discovery: 'explain', recipe: parsed.optionValue('--explain'), ...options };
1383
306
  }
1384
- if (positional.length !== 2)
307
+ if (parsed.allPositionals.length !== 2) {
1385
308
  throw new CliUsageError('workflow-plan requires one fixed recipe and one subject, --list, or --explain <recipe>.');
1386
- const recipe = positional[0];
1387
- if (!INVESTIGATION_PLAN_RECIPES.includes(recipe)) {
1388
- throw new CliUsageError(`workflow-plan recipe must be one of: ${INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
1389
- }
1390
- return { action: 'workflow-plan', recipe: recipe, subject: positional[1], output, quiet, color };
1391
- }
1392
- function parseWorkflowRunArguments(argv) {
1393
- const positional = [];
1394
- const selections = [];
1395
- let resumeSource = null;
1396
- let approveNetwork = false;
1397
- let output = 'terminal';
1398
- let quiet = false;
1399
- let color = true;
1400
- for (let index = 0; index < argv.length; index += 1) {
1401
- const argument = argv[index];
1402
- if (argument === undefined)
1403
- break;
1404
- if (argument === '--json') {
1405
- if (output !== 'terminal')
1406
- throw new CliUsageError('--json may be supplied only once.');
1407
- output = 'json';
1408
- }
1409
- else if (argument === '--select') {
1410
- const value = argv[++index];
1411
- if (!value)
1412
- throw new CliUsageError('--select requires <step-id>=<path-or-value>.');
1413
- const separator = value.indexOf('=');
1414
- const stepId = separator === -1 ? '' : value.slice(0, separator);
1415
- const selection = separator === -1 ? '' : value.slice(separator + 1);
1416
- if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(stepId) || !selection.trim()) {
1417
- throw new CliUsageError('--select requires <step-id>=<path-or-value>.');
1418
- }
1419
- if (selection.startsWith('-')) {
1420
- throw new CliUsageError('--select values cannot start with a hyphen; prefix a file path with ./ when needed.');
1421
- }
1422
- selections.push(Object.freeze({ stepId, value: selection }));
1423
- }
1424
- else if (argument === '--resume') {
1425
- if (resumeSource !== null)
1426
- throw new CliUsageError('--resume may be supplied only once.');
1427
- const value = argv[++index];
1428
- if (!value || value.startsWith('-'))
1429
- throw new CliUsageError('--resume requires one prior workflow-run state file.');
1430
- resumeSource = value;
1431
- }
1432
- else if (argument === '--approve-network') {
1433
- if (approveNetwork)
1434
- throw new CliUsageError('--approve-network may be supplied only once.');
1435
- approveNetwork = true;
1436
- }
1437
- else if (argument === '--quiet')
1438
- quiet = true;
1439
- else if (argument === '--no-color')
1440
- color = false;
1441
- else if (argument.startsWith('-'))
1442
- throw new CliUsageError(`Unknown option "${argument}".`);
1443
- else
1444
- positional.push(argument);
1445
- }
1446
- if (positional.length !== 2)
1447
- throw new CliUsageError('workflow-run requires one fixed recipe and one subject.');
1448
- const recipe = positional[0];
1449
- if (!RUNNABLE_INVESTIGATION_PLAN_RECIPES.includes(recipe)) {
1450
- throw new CliUsageError(`workflow-run recipe must be one of: ${RUNNABLE_INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
1451
- }
1452
- if (quiet && output !== 'terminal')
1453
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1454
- return { action: 'workflow-run', recipe: recipe, subject: positional[1], resumeSource, selections: Object.freeze(selections), approveNetwork, output, quiet, color };
1455
- }
1456
- function parseDiffArguments(argv) {
1457
- let leftSource = null;
1458
- let rightSource = null;
1459
- let leftSessionId = null;
1460
- let rightSessionId = null;
1461
- let output = 'terminal';
1462
- let quiet = false;
1463
- let color = true;
1464
- for (let index = 0; index < argv.length; index++) {
1465
- const argument = argv[index];
1466
- if (argument === undefined)
1467
- break;
1468
- if (argument === '--json') {
1469
- if (output !== 'terminal')
1470
- throw new CliUsageError('--json may be supplied only once.');
1471
- output = 'json';
1472
- }
1473
- else if (argument === '--left-session' || argument === '--right-session') {
1474
- const current = argument === '--left-session' ? leftSessionId : rightSessionId;
1475
- if (current !== null)
1476
- throw new CliUsageError(`${argument} may be supplied only once.`);
1477
- const value = argv[++index];
1478
- if (!value || !/^[A-Za-z0-9_-]{1,128}$/u.test(value))
1479
- throw new CliUsageError(`${argument} requires one bounded saved-session ID.`);
1480
- if (argument === '--left-session')
1481
- leftSessionId = value;
1482
- else
1483
- rightSessionId = value;
1484
- }
1485
- else if (argument === '--quiet')
1486
- quiet = true;
1487
- else if (argument === '--no-color')
1488
- color = false;
1489
- else if (argument.startsWith('-'))
1490
- throw new CliUsageError(`Unknown option "${argument}".`);
1491
- else if (leftSource === null)
1492
- leftSource = argument;
1493
- else if (rightSource === null)
1494
- rightSource = argument;
1495
- else
1496
- throw new CliUsageError('diff accepts exactly two compatible retained JSON files.');
1497
- }
1498
- if (!leftSource || !rightSource)
1499
- throw new CliUsageError('diff requires exactly two compatible retained JSON files.');
1500
- if (leftSource === rightSource)
1501
- throw new CliUsageError('diff requires two different input files.');
1502
- if (quiet && output !== 'terminal')
1503
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1504
- return { action: 'diff', leftSource, rightSource, leftSessionId, rightSessionId, output, quiet, color };
1505
- }
1506
- function parseReconcileArguments(argv) {
1507
- const sources = [];
1508
- let output = 'terminal';
1509
- let quiet = false;
1510
- let color = true;
1511
- for (const argument of argv) {
1512
- if (argument === '--json') {
1513
- if (output !== 'terminal')
1514
- throw new CliUsageError('--json may be supplied only once.');
1515
- output = 'json';
1516
- }
1517
- else if (argument === '--quiet')
1518
- quiet = true;
1519
- else if (argument === '--no-color')
1520
- color = false;
1521
- else if (argument.startsWith('-'))
1522
- throw new CliUsageError(`Unknown option "${argument}".`);
1523
- else
1524
- sources.push(argument);
1525
- }
1526
- if (sources.length < 2 || sources.length > 5) {
1527
- throw new CliUsageError('reconcile requires from 2 to 5 saved lookup JSON files.');
1528
- }
1529
- if (new Set(sources).size !== sources.length) {
1530
- throw new CliUsageError('reconcile input files must be different.');
1531
309
  }
1532
- if (quiet && output !== 'terminal')
1533
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1534
- return { action: 'reconcile', sources, output, quiet, color };
1535
- }
1536
- function parseTimelineArguments(argv) {
1537
- const sources = [];
1538
- let output = 'terminal';
1539
- let quiet = false;
1540
- let color = true;
1541
- for (const argument of argv) {
1542
- if (argument === '--json') {
1543
- if (output !== 'terminal')
1544
- throw new CliUsageError('--json may be supplied only once.');
1545
- output = 'json';
1546
- }
1547
- else if (argument === '--quiet')
1548
- quiet = true;
1549
- else if (argument === '--no-color')
1550
- color = false;
1551
- else if (argument.startsWith('-'))
1552
- throw new CliUsageError(`Unknown option "${argument}".`);
1553
- else
1554
- sources.push(argument);
1555
- }
1556
- if (sources.length < 2 || sources.length > 20) {
1557
- throw new CliUsageError('timeline requires from 2 to 20 saved lookup JSON files.');
1558
- }
1559
- if (new Set(sources).size !== sources.length) {
1560
- throw new CliUsageError('timeline input files must be different.');
1561
- }
1562
- if (quiet && output !== 'terminal')
1563
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1564
- return { action: 'timeline', sources, output, quiet, color };
1565
- }
1566
- function parseRegistrySupportArguments(argv) {
1567
- const { source: target, ...options } = parseSingleJsonInput(argv, 'registry-support accepts one domain or suffix.');
1568
- return { action: 'registry-support', target, ...options };
1569
- }
1570
- function parseRegistryDoctorArguments(argv) {
1571
- const parsed = parseSingleJsonInput(argv, 'registry-doctor accepts one optional saved Lookup JSON file.');
1572
- return { action: 'registry-doctor', ...parsed };
1573
- }
1574
- function parseRegistryCohortArguments(argv) {
1575
- const parsed = parseSingleJsonInput(argv, 'registry-cohort accepts one optional JSON or JSONL input file.');
1576
- return { action: 'registry-cohort', ...parsed };
1577
- }
1578
- function parseRegistryScaffoldArguments(argv) {
1579
- let profile = '';
1580
- let suffix = '';
1581
- let scenario = '';
1582
- for (let index = 0; index < argv.length; index += 2) {
1583
- const option = argv[index];
1584
- const value = argv[index + 1];
1585
- if (!value)
1586
- throw new CliUsageError(`${option || 'Option'} requires a value.`);
1587
- if (option === '--profile' && !profile)
1588
- profile = value;
1589
- else if (option === '--suffix' && !suffix)
1590
- suffix = value;
1591
- else if (option === '--scenario' && !scenario)
1592
- scenario = value;
1593
- else
1594
- throw new CliUsageError(`Unknown or repeated option "${option || ''}".`);
1595
- }
1596
- if (!profile || !suffix || !['registered', 'not_found', 'inconclusive'].includes(scenario)) {
1597
- throw new CliUsageError('registry-scaffold requires --profile, --suffix, and --scenario <registered|not_found|inconclusive>.');
1598
- }
1599
- return { action: 'registry-scaffold', profile, suffix, scenario: scenario };
1600
- }
1601
- function parseRiskCalibrateArguments(argv) {
1602
- let source = null;
1603
- let output = 'terminal';
1604
- let quiet = false;
1605
- let color = true;
1606
- for (const argument of argv) {
1607
- if (argument === '--json' || argument === '--summary-json') {
1608
- if (output !== 'terminal')
1609
- throw new CliUsageError('--json and --summary-json are mutually exclusive and may be supplied only once.');
1610
- output = argument === '--json' ? 'json' : 'summary_json';
1611
- }
1612
- else if (argument === '--quiet')
1613
- quiet = true;
1614
- else if (argument === '--no-color')
1615
- color = false;
1616
- else if (argument.startsWith('-'))
1617
- throw new CliUsageError(`Unknown option "${argument}".`);
1618
- else if (source === null)
1619
- source = argument;
1620
- else
1621
- throw new CliUsageError('risk-calibrate accepts one optional dataset file. Otherwise pipe one dataset on stdin.');
1622
- }
1623
- if (quiet && output !== 'terminal')
1624
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1625
- return { action: 'risk-calibrate', source, output, quiet, color };
1626
- }
1627
- function parseLookalikeCalibrateArguments(argv) {
1628
- const parsed = parseSingleJsonInput(argv, 'lookalike-calibrate accepts one optional reviewed dataset JSON file.');
1629
- return { action: 'lookalike-calibrate', ...parsed };
310
+ return {
311
+ action: 'workflow-plan',
312
+ recipe: parsed.positionalValue('recipe'),
313
+ subject: parsed.positionalValue('subject'),
314
+ ...options,
315
+ };
1630
316
  }
1631
- function parseVerifyArtifactArguments(argv) {
1632
- let source = null;
1633
- let passphraseSource = null;
1634
- let manifestSource = null;
1635
- let manifestEntryId = null;
1636
- let output = 'terminal';
1637
- let strictExit = false;
1638
- let quiet = false;
1639
- let color = true;
1640
- for (let index = 0; index < argv.length; index++) {
1641
- const argument = argv[index];
1642
- if (argument === undefined)
1643
- break;
1644
- if (argument === '--json') {
1645
- if (output !== 'terminal')
1646
- throw new CliUsageError('--json may be supplied only once.');
1647
- output = 'json';
1648
- }
1649
- else if (argument === '--passphrase-file') {
1650
- if (passphraseSource !== null)
1651
- throw new CliUsageError('--passphrase-file may be supplied only once.');
1652
- const value = argv[++index];
1653
- if (!value || value.startsWith('-')) {
1654
- throw new CliUsageError('--passphrase-file requires one bounded UTF-8 file.');
1655
- }
1656
- passphraseSource = value;
1657
- }
1658
- else if (argument === '--manifest') {
1659
- if (manifestSource !== null)
1660
- throw new CliUsageError('--manifest may be supplied only once.');
1661
- const value = argv[++index];
1662
- if (!value || value.startsWith('-'))
1663
- throw new CliUsageError('--manifest requires one bounded investigation manifest file.');
1664
- manifestSource = value;
1665
- }
1666
- else if (argument === '--manifest-entry') {
1667
- if (manifestEntryId !== null)
1668
- throw new CliUsageError('--manifest-entry may be supplied only once.');
1669
- const value = argv[++index];
1670
- if (!value || !/^artifact-(?:[1-9]|1[0-6])$/u.test(value)) {
1671
- throw new CliUsageError('--manifest-entry requires an ID from artifact-1 through artifact-16.');
1672
- }
1673
- manifestEntryId = value;
1674
- }
1675
- else if (argument === '--strict-exit') {
1676
- if (strictExit)
1677
- throw new CliUsageError('--strict-exit may be supplied only once.');
1678
- strictExit = true;
1679
- }
1680
- else if (argument === '--quiet')
1681
- quiet = true;
1682
- else if (argument === '--no-color')
1683
- color = false;
1684
- else if (argument.startsWith('-'))
1685
- throw new CliUsageError(`Unknown option "${argument}".`);
1686
- else if (source === null)
1687
- source = argument;
1688
- else
1689
- throw new CliUsageError('verify-artifact accepts one optional JSON file. Otherwise pipe one artefact on stdin.');
1690
- }
1691
- if ((manifestSource === null) !== (manifestEntryId === null)) {
1692
- throw new CliUsageError('--manifest and --manifest-entry must be supplied together.');
1693
- }
1694
- if (quiet && output !== 'terminal')
1695
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1696
- return { action: 'verify-artifact', source, passphraseSource, manifestSource, manifestEntryId, output, strictExit, quiet, color };
317
+ function parseWorkflowRunArguments(parsed) {
318
+ const artifactBindings = parsed.optionValues('--use-artifact').map((value) => {
319
+ const match = /^([a-z0-9]+(?:-[a-z0-9]+)*):([1-9][0-9]?)=([a-z0-9]+(?:-[a-z0-9]+)*)$/u.exec(value);
320
+ if (!match)
321
+ throw new CliUsageError('--use-artifact requires <step-id>:<input-number>=<earlier-step-id>.');
322
+ return Object.freeze({ stepId: match[1], input: Number(match[2]), sourceStepId: match[3] });
323
+ });
324
+ const selections = parsed.optionValues('--select').map((value) => {
325
+ const separator = value.indexOf('=');
326
+ const stepId = separator === -1 ? '' : value.slice(0, separator);
327
+ const selection = separator === -1 ? '' : value.slice(separator + 1);
328
+ if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(stepId) || !selection.trim()) {
329
+ throw new CliUsageError('--select requires <step-id>=<path-or-value>.');
330
+ }
331
+ if (selection.startsWith('-')) {
332
+ throw new CliUsageError('--select values cannot start with a hyphen; prefix a file path with ./ when needed.');
333
+ }
334
+ return Object.freeze({ stepId, value: selection });
335
+ });
336
+ return {
337
+ action: 'workflow-run',
338
+ recipe: parsed.positionalValue('recipe'),
339
+ subject: parsed.positionalValue('subject'),
340
+ resumeSource: parsed.optionValue('--resume'),
341
+ selections: Object.freeze(selections),
342
+ artifactBindings: Object.freeze(artifactBindings),
343
+ confirmedReviews: Object.freeze(parsed.optionValues('--confirm-review')),
344
+ approveNetwork: parsed.hasOption('--approve-network'),
345
+ interactive: parsed.hasOption('--interactive'),
346
+ output: jsonOutput(parsed),
347
+ ...terminalOptions(parsed),
348
+ };
1697
349
  }
1698
- function parseInterchangeReportArguments(argv) {
1699
- let source = null;
1700
- let passphraseSource = null;
1701
- let output = 'terminal';
1702
- let quiet = false;
1703
- let color = true;
1704
- for (let index = 0; index < argv.length; index++) {
1705
- const argument = argv[index];
1706
- if (argument === undefined)
1707
- break;
1708
- if (argument === '--json') {
1709
- if (output !== 'terminal')
1710
- throw new CliUsageError('--json may be supplied only once.');
1711
- output = 'json';
350
+ function parseDiffArguments(parsed) {
351
+ const compared = parseTwoFileComparisonArguments(parsed, 'diff');
352
+ const leftSessionId = parsed.optionValue('--left-session');
353
+ const rightSessionId = parsed.optionValue('--right-session');
354
+ for (const [option, value] of [['--left-session', leftSessionId], ['--right-session', rightSessionId]]) {
355
+ if (value !== null && !/^[A-Za-z0-9_-]{1,128}$/u.test(value)) {
356
+ throw new CliUsageError(`${option} requires one bounded saved-session ID.`);
1712
357
  }
1713
- else if (argument === '--passphrase-file') {
1714
- if (passphraseSource !== null)
1715
- throw new CliUsageError('--passphrase-file may be supplied only once.');
1716
- const value = argv[++index];
1717
- if (!value || value.startsWith('-'))
1718
- throw new CliUsageError('--passphrase-file requires one bounded UTF-8 file.');
1719
- passphraseSource = value;
1720
- }
1721
- else if (argument === '--quiet')
1722
- quiet = true;
1723
- else if (argument === '--no-color')
1724
- color = false;
1725
- else if (argument.startsWith('-'))
1726
- throw new CliUsageError(`Unknown option "${argument}".`);
1727
- else if (source === null)
1728
- source = argument;
1729
- else
1730
- throw new CliUsageError('interchange-report accepts one optional JSON file. Otherwise pipe one artefact on stdin.');
1731
358
  }
1732
- if (quiet && output !== 'terminal')
1733
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1734
- return { action: 'interchange-report', source, passphraseSource, output, quiet, color };
1735
- }
1736
- function parseSourceReportArguments(argv) {
1737
- const parsed = parseSingleJsonInput(argv, 'source-report accepts one optional JSON file. Otherwise pipe lookup documents on stdin.');
1738
- return { action: 'source-report', ...parsed };
359
+ return { action: 'diff', ...compared, leftSessionId, rightSessionId };
1739
360
  }
1740
- function parseExportArguments(argv) {
1741
- let source = null;
1742
- let compact = false;
1743
- let includeAttribution = true;
1744
- let format = 'json';
1745
- for (const argument of argv) {
1746
- if (argument === '--compact') {
1747
- if (compact)
1748
- throw new CliUsageError('--compact may be supplied only once.');
1749
- compact = true;
1750
- }
1751
- else if (argument === '--no-attribution') {
1752
- if (!includeAttribution)
1753
- throw new CliUsageError('--no-attribution may be supplied only once.');
1754
- includeAttribution = false;
1755
- }
1756
- else if (argument === '--markdown' || argument === '--html') {
1757
- if (format !== 'json')
1758
- throw new CliUsageError('Choose only one evidence export format.');
1759
- format = argument === '--markdown' ? 'markdown' : 'html';
1760
- }
1761
- else if (argument.startsWith('-'))
1762
- throw new CliUsageError(`Unknown option "${argument}".`);
1763
- else if (source === null)
1764
- source = argument;
1765
- else
1766
- throw new CliUsageError('export accepts one optional lookup JSON file. Otherwise pipe one lookup document on stdin.');
1767
- }
1768
- if (compact && format !== 'json')
1769
- throw new CliUsageError('--compact applies to JSON export and cannot be combined with --markdown or --html.');
1770
- if (!includeAttribution && format === 'json')
1771
- throw new CliUsageError('--no-attribution applies only to Markdown or HTML reports.');
1772
- return { action: 'export', source, format, compact, includeAttribution };
361
+ function uniqueSources(parsed, command) {
362
+ const sources = parsed.positionalValues('sources');
363
+ if (new Set(sources).size !== sources.length)
364
+ throw new CliUsageError(`${command} input files must be different.`);
365
+ return sources;
366
+ }
367
+ function parseCaseArguments(parsed) {
368
+ const operation = parsed.positionalValue('operation');
369
+ const source = parsed.positionalValue('source');
370
+ const domain = parsed.optionValue('--domain');
371
+ const caseId = parsed.optionValue('--case-id');
372
+ const title = parsed.optionValue('--title');
373
+ const newIncident = parsed.hasOption('--new-incident');
374
+ const text = parsed.optionValue('--text');
375
+ const noteSource = parsed.optionValue('--note-file');
376
+ const inputSource = parsed.optionValue('--input');
377
+ const expectedFileDigest = parsed.optionValue('--expect-file-digest');
378
+ if (source === '-' || inputSource === '-' || noteSource === '-')
379
+ throw new CliUsageError('Case operations require selected files, not stdin.');
380
+ if (operation !== 'open' && !source)
381
+ throw new CliUsageError(`${operation} requires a Case file.`);
382
+ if (operation !== 'show' && !parsed.hasOption('--output'))
383
+ throw new CliUsageError('Case mutations require --output; nothing was changed.');
384
+ if (operation === 'open' && !domain)
385
+ throw new CliUsageError('case open requires --domain.');
386
+ if (operation !== 'open' && (title || newIncident))
387
+ throw new CliUsageError('--title and --new-incident belong to case open.');
388
+ if (newIncident && (!title || caseId))
389
+ throw new CliUsageError('--new-incident requires a distinguishing --title and cannot select an existing --case-id.');
390
+ if (operation === 'note' ? (text === null && !noteSource) : (text !== null || noteSource !== null)) {
391
+ throw new CliUsageError('Only case note accepts and requires --text or --note-file.');
392
+ }
393
+ if (['pin', 'assess', 'recheck'].includes(operation) ? !inputSource : inputSource !== null) {
394
+ throw new CliUsageError('Only case pin, assess and recheck accept and require --input.');
395
+ }
396
+ if (expectedFileDigest !== null && (!source || !/^sha256:[a-f0-9]{64}$/u.test(expectedFileDigest))) {
397
+ throw new CliUsageError('--expect-file-digest requires a source file and sha256:<64 lowercase hexadecimal characters>.');
398
+ }
399
+ return { action: 'case', operation, source, caseId, domain, title, newIncident, text, noteSource, inputSource, expectedFileDigest, output: jsonOutput(parsed), ...terminalOptions(parsed) };
1773
400
  }
1774
401
  const CLI_PARSERS = Object.freeze({
1775
- completion: parseCompletionArguments,
1776
- doctor: parseDoctorArguments,
1777
- commands: parseCommandsArguments,
1778
- manual: parseManualArguments,
402
+ completion: (parsed) => ({ action: 'completion', shell: parsed.positionalValue('shell') }),
403
+ doctor: (parsed) => ({ action: 'doctor', network: parsed.hasOption('--network'), output: jsonOutput(parsed), ...terminalOptions(parsed) }),
404
+ commands: (parsed) => ({
405
+ action: 'commands', output: jsonOutput(parsed), common: parsed.hasOption('--common'),
406
+ group: parsed.optionValue('--group'),
407
+ mode: parsed.optionValue('--mode'),
408
+ ...terminalOptions(parsed),
409
+ }),
410
+ manual: () => ({ action: 'manual' }),
1779
411
  manifest: parseManifestArguments,
1780
- 'map-observations': (argv) => parseOfflineImportArguments('map-observations', argv),
1781
- 'oam-export': (argv) => parseOfflineImportArguments('oam-export', argv),
412
+ 'map-observations': (parsed) => singleInputAction('map-observations', parsed),
413
+ 'oam-export': (parsed) => singleInputAction('oam-export', parsed),
1782
414
  lookup: parseLookupArguments,
1783
415
  bulk: parseBulkArguments,
1784
- 'ct-search': parseCtSearchArguments,
1785
- 'ct-intake': parseCtIntakeArguments,
416
+ 'ct-search': (parsed) => singleInputAction('ct-search', parsed, 'keyword'),
417
+ 'ct-intake': (parsed) => singleInputAction('ct-intake', parsed),
1786
418
  discover: parseDiscoverArguments,
1787
419
  'discover-scan': parseDiscoverScanArguments,
1788
420
  posture: parsePostureArguments,
1789
- http: parseHttpArguments,
1790
- tls: parseTlsArguments,
421
+ http: (parsed) => singleInputAction('http', parsed, 'domain'),
422
+ tls: (parsed) => singleInputAction('tls', parsed, 'hostname'),
1791
423
  'dnssec-validate': parseDnssecValidateArguments,
1792
424
  'mail-transport': parseMailTransportArguments,
1793
- 'registry-support': parseRegistrySupportArguments,
1794
- 'registry-doctor': parseRegistryDoctorArguments,
1795
- 'registry-cohort': parseRegistryCohortArguments,
1796
- 'registry-scaffold': parseRegistryScaffoldArguments,
1797
- 'risk-calibrate': parseRiskCalibrateArguments,
1798
- 'lookalike-calibrate': parseLookalikeCalibrateArguments,
425
+ 'registry-support': (parsed) => ({
426
+ action: 'registry-support', target: parsed.positionalValue('domain-or-suffix'),
427
+ output: jsonOutput(parsed), ...terminalOptions(parsed),
428
+ }),
429
+ 'registry-doctor': (parsed) => singleInputAction('registry-doctor', parsed),
430
+ 'registry-cohort': (parsed) => singleInputAction('registry-cohort', parsed),
431
+ 'registry-scaffold': (parsed) => ({
432
+ action: 'registry-scaffold', profile: parsed.optionValue('--profile'), suffix: parsed.optionValue('--suffix'),
433
+ scenario: parsed.optionValue('--scenario'),
434
+ }),
435
+ 'risk-calibrate': (parsed) => ({
436
+ action: 'risk-calibrate', source: parsed.positionalValue('source'),
437
+ output: parsed.hasOption('--json') ? 'json' : parsed.hasOption('--summary-json') ? 'summary_json' : 'terminal',
438
+ ...terminalOptions(parsed),
439
+ }),
440
+ 'lookalike-calibrate': (parsed) => singleInputAction('lookalike-calibrate', parsed),
1799
441
  'verify-artifact': parseVerifyArtifactArguments,
1800
- 'interchange-report': parseInterchangeReportArguments,
442
+ 'interchange-report': (parsed) => ({
443
+ action: 'interchange-report', source: parsed.positionalValue('source'),
444
+ passphraseSource: parsed.optionValue('--passphrase-file'), output: jsonOutput(parsed), ...terminalOptions(parsed),
445
+ }),
1801
446
  'inspect-archive': parseInspectArchiveArguments,
1802
447
  'sign-artifact': parseSignArtifactArguments,
1803
448
  'verify-signature': parseVerifySignatureArguments,
1804
- 'source-report': parseSourceReportArguments,
1805
- compare: parseCompareArguments,
1806
- 'page-compare': parsePageCompareArguments,
1807
- 'mail-review': parseMailReviewArguments,
1808
- 'review-evidence': parseReviewEvidenceArguments,
1809
- brief: parseBriefArguments,
1810
- 'case-pack': parseCasePackArguments,
1811
- 'domain-control': parseDomainControlArguments,
449
+ 'source-report': (parsed) => singleInputAction('source-report', parsed),
450
+ compare: (parsed) => singleInputAction('compare', parsed),
451
+ 'page-compare': (parsed) => ({ action: 'page-compare', ...parseTwoFileComparisonArguments(parsed, 'page-compare') }),
452
+ 'mail-review': (parsed) => singleInputAction('mail-review', parsed),
453
+ 'mail-headers': (parsed) => singleInputAction('mail-headers', parsed),
454
+ 'review-evidence': (parsed) => ({
455
+ action: 'review-evidence', source: parsed.positionalValue('source'), mmdbSource: parsed.optionValue('--mmdb'),
456
+ output: jsonOutput(parsed), strictExit: parsed.hasOption('--strict-exit'), ...terminalOptions(parsed),
457
+ }),
458
+ brief: (parsed) => singleInputAction('brief', parsed),
459
+ case: parseCaseArguments,
460
+ 'case-pack': (parsed) => ({
461
+ action: 'case-pack', source: parsed.positionalValue('source'), output: jsonOutput(parsed),
462
+ audience: parsed.optionValue('--audience'), reviewed: true, ...terminalOptions(parsed),
463
+ }),
464
+ 'domain-control': (parsed) => singleInputAction('domain-control', parsed),
1812
465
  'monitor-once': parseMonitorOnceArguments,
1813
- assurance: parseAssuranceArguments,
1814
- 'change-packet': parseChangePacketArguments,
466
+ assurance: (parsed) => singleInputAction('assurance', parsed),
467
+ 'change-packet': (parsed) => singleInputAction('change-packet', parsed),
1815
468
  'sharing-review': parseSharingReviewArguments,
1816
469
  'workflow-plan': parseWorkflowPlanArguments,
1817
470
  'workflow-run': parseWorkflowRunArguments,
1818
471
  diff: parseDiffArguments,
1819
- reconcile: parseReconcileArguments,
1820
- timeline: parseTimelineArguments,
1821
- export: parseExportArguments,
472
+ reconcile: (parsed) => ({ action: 'reconcile', sources: uniqueSources(parsed, 'reconcile'), output: jsonOutput(parsed), ...terminalOptions(parsed) }),
473
+ timeline: (parsed) => ({ action: 'timeline', sources: uniqueSources(parsed, 'timeline'), output: jsonOutput(parsed), ...terminalOptions(parsed) }),
474
+ export: (parsed) => ({
475
+ action: 'export', source: parsed.positionalValue('source'),
476
+ format: parsed.hasOption('--markdown') ? 'markdown' : parsed.hasOption('--html') ? 'html' : 'json',
477
+ compact: parsed.hasOption('--compact'), includeAttribution: !parsed.hasOption('--no-attribution'),
478
+ }),
1822
479
  });
480
+ function parseCliArguments(rawArgv) {
481
+ if (!Array.isArray(rawArgv) || rawArgv.length > MAX_CLI_ARGUMENTS) {
482
+ throw new CliUsageError(`At most ${MAX_CLI_ARGUMENTS} command arguments are supported.`);
483
+ }
484
+ return parseCliArgumentsCore(rawArgv.map(boundedArgument));
485
+ }
486
+ function parseCliArgumentsCore(argv) {
487
+ if (!argv.length)
488
+ return { action: 'help' };
489
+ const firstArgument = argv[0] ?? '';
490
+ const metaAction = cliMetaActionForInvocation(argv);
491
+ if (metaAction?.id === 'help') {
492
+ if (argv.length === 1)
493
+ return { action: 'help' };
494
+ if (argv.length === 2 && (isCliCommand(firstArgument) || isDirectLookupTarget(firstArgument))) {
495
+ return { action: 'help', command: isCliCommand(firstArgument) ? firstArgument : 'lookup' };
496
+ }
497
+ throw new CliUsageError('Help accepts only an optional command name.');
498
+ }
499
+ if (metaAction?.id === 'version') {
500
+ if (argv.length !== 1)
501
+ throw new CliUsageError('--version does not accept other arguments.');
502
+ return { action: 'version' };
503
+ }
504
+ let command;
505
+ let directLookup = false;
506
+ if (isCliCommand(firstArgument))
507
+ command = firstArgument;
508
+ else if (isDirectLookupTarget(firstArgument)) {
509
+ command = 'lookup';
510
+ directLookup = true;
511
+ }
512
+ else {
513
+ const displayedCommand = firstArgument.length > 80 ? `${firstArgument.slice(0, 79)}…` : firstArgument;
514
+ throw new CliUsageError(`Unknown command "${displayedCommand}". Run "whoisleuth commands" to list supported commands.`);
515
+ }
516
+ const parsedGrammar = parseCommandArguments(command, directLookup ? argv : argv.slice(1));
517
+ const parsed = CLI_PARSERS[command](parsedGrammar);
518
+ if (parsed.action !== command)
519
+ throw new Error(`CLI parser contract mismatch for ${command}.`);
520
+ const destination = parsedGrammar.optionValue('--output');
521
+ const palette = parsedGrammar.optionValue('--palette');
522
+ return {
523
+ ...parsed,
524
+ ...(destination !== null ? { destination } : {}),
525
+ ...(parsedGrammar.hasOption('--force') ? { force: true } : {}),
526
+ ...(palette !== null ? { palette } : {}),
527
+ };
528
+ }
1823
529
  export { CLI_COMMANDS, CLI_PARSERS, CliUsageError, MAX_CLI_ARGUMENTS, MAX_CLI_ARGUMENT_LENGTH, parseCliArguments };