@slicedearth/whoisleuth-cli 1.36.1 → 1.43.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 (139) hide show
  1. package/README.md +32 -2
  2. package/bin/whoisleuth.mjs +52 -24
  3. package/cli/archive-inspect.mjs +37 -2
  4. package/cli/arguments.mjs +493 -157
  5. package/cli/artifact-verify.mjs +91 -15
  6. package/cli/bulk-checkpoint.mjs +7 -11
  7. package/cli/bulk-command-runner.mjs +19 -2
  8. package/cli/case-pack.mjs +136 -0
  9. package/cli/ci-report.mjs +73 -0
  10. package/cli/collection-preflight.mjs +59 -0
  11. package/cli/command-reference.mjs +395 -0
  12. package/cli/compare.mjs +1 -1
  13. package/cli/completion.mjs +49 -11
  14. package/cli/config-profile.mjs +180 -0
  15. package/cli/ct-event-intake.mjs +159 -0
  16. package/cli/discovery-scan-command-runner.mjs +15 -1
  17. package/cli/doctor.mjs +11 -5
  18. package/cli/domain-control-monitor.mjs +104 -0
  19. package/cli/domain-control-observations.mjs +235 -0
  20. package/cli/evidence-command-arguments.mjs +12 -1
  21. package/cli/evidence-command-runner.mjs +3 -2
  22. package/cli/evidence-signing.mjs +4 -4
  23. package/cli/external-observation-mapping.mjs +162 -0
  24. package/cli/fail-policy.mjs +64 -0
  25. package/cli/formatters/html.mjs +2 -2
  26. package/cli/formatters/json.mjs +13 -1
  27. package/cli/formatters/terminal.mjs +7 -3
  28. package/cli/investigation-manifest.mjs +113 -0
  29. package/cli/investigation-plan.mjs +2 -2
  30. package/cli/investigation-run.mjs +112 -0
  31. package/cli/local-mmdb-review.mjs +76 -0
  32. package/cli/lookalike-calibration.mjs +121 -0
  33. package/cli/lookup-brief.mjs +116 -0
  34. package/cli/lookup-command-runner.mjs +12 -2
  35. package/cli/lookup-diff.mjs +17 -19
  36. package/cli/lookup-plan.mjs +3 -3
  37. package/cli/lookup-reconcile.mjs +141 -0
  38. package/cli/lookup-timeline.mjs +1 -1
  39. package/cli/mail-review.mjs +1 -1
  40. package/cli/network-command-runner.mjs +4 -1
  41. package/cli/offline-evidence-review.mjs +125 -10
  42. package/cli/open-asset-model-bridge.mjs +169 -0
  43. package/cli/registry-cohort.mjs +121 -0
  44. package/cli/registry-doctor.mjs +220 -0
  45. package/cli/registry-fixture-scaffold.mjs +73 -0
  46. package/cli/risk-calibration.mjs +117 -9
  47. package/cli/runner.mjs +561 -280
  48. package/cli/saved-lookup.mjs +2 -2
  49. package/cli/sharing-review.mjs +202 -0
  50. package/cli/source-reliability.mjs +1 -1
  51. package/docs/cli-reference.md +1336 -0
  52. package/docs/cli.md +259 -940
  53. package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
  54. package/frontend/src/lib/analysis/acquisition-due-diligence.js +7 -7
  55. package/frontend/src/lib/analysis/brand-profile-model.js +46 -4
  56. package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
  57. package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
  58. package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
  59. package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
  60. package/frontend/src/lib/analysis/case-migration-model.js +6 -1
  61. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  62. package/frontend/src/lib/analysis/case-record-core.js +5 -1
  63. package/frontend/src/lib/analysis/case-record-operations.js +6 -1
  64. package/frontend/src/lib/analysis/case-report.js +573 -0
  65. package/frontend/src/lib/analysis/case-response-model.js +30 -1
  66. package/frontend/src/lib/analysis/case-response-packet.js +7 -7
  67. package/frontend/src/lib/analysis/detection-rule-model.js +36 -0
  68. package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
  69. package/frontend/src/lib/analysis/external-findings-import.js +320 -0
  70. package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
  71. package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
  72. package/frontend/src/lib/analysis/lookup-decision-support.js +487 -0
  73. package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
  74. package/frontend/src/lib/analysis/lookup-presentation.js +64 -0
  75. package/frontend/src/lib/analysis/lookup-response.js +2 -0
  76. package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
  77. package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
  78. package/frontend/src/lib/analysis/page-similarity.js +1 -1
  79. package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
  80. package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
  81. package/frontend/src/lib/analysis/scoring.js +9 -89
  82. package/frontend/src/lib/analysis/shortlist-model.js +8 -4
  83. package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
  84. package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
  85. package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
  86. package/frontend/src/lib/analysis/visualization-models.js +369 -0
  87. package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
  88. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
  89. package/frontend/src/lib/analysis/workspace-archive.js +4 -2
  90. package/lib/abusech-auth.mjs +1 -1
  91. package/lib/analyst-taxonomy.mjs +40 -0
  92. package/lib/availability.mjs +2 -2
  93. package/lib/bounded-contract-normalizers.mjs +33 -0
  94. package/lib/browser-library-profile.mjs +15 -2
  95. package/lib/certificate-extension-profile.mjs +71 -0
  96. package/lib/credential-surface-profile.mjs +1 -1
  97. package/lib/dns-convergence-review.mjs +183 -0
  98. package/lib/dns-delegation-health.mjs +196 -3
  99. package/lib/domain-assurance.mjs +368 -0
  100. package/lib/domain-change-packet.mjs +133 -0
  101. package/lib/domain-change-review.mjs +307 -0
  102. package/lib/domain-control-flight-recorder.mjs +268 -0
  103. package/lib/domain-control-manifest.mjs +50 -7
  104. package/lib/domain-portfolio-review.mjs +151 -0
  105. package/lib/domain-posture.mjs +9 -9
  106. package/lib/encrypted-dns-contract.mjs +1 -1
  107. package/lib/evidence-export.mjs +1 -1
  108. package/lib/evidence-report-markdown.mjs +1 -1
  109. package/lib/evidence-report.mjs +1 -1
  110. package/lib/generated/cisa-kev-catalog.mjs +1668 -0
  111. package/lib/html-signals.mjs +5 -11
  112. package/lib/ip-prefix.mjs +34 -1
  113. package/lib/local-geoip-evidence.mjs +2 -2
  114. package/lib/lookup-response-contract.mjs +346 -0
  115. package/lib/nameserver-preflight-review.mjs +159 -0
  116. package/lib/observed-network-context.mjs +1 -1
  117. package/lib/opportunity-scoring.mjs +107 -0
  118. package/lib/page-fingerprints.mjs +1 -1
  119. package/lib/page-language-signals.mjs +124 -0
  120. package/lib/rdap-capabilities.mjs +6 -6
  121. package/lib/rdap-search-workbench.mjs +96 -6
  122. package/lib/registry-access.mjs +1 -1
  123. package/lib/registry-capability-catalogue.mjs +4 -4
  124. package/lib/registry-insights.mjs +6 -6
  125. package/lib/risk-scoring.mjs +247 -83
  126. package/lib/scoring-evidence-quality.mjs +107 -0
  127. package/lib/security-txt.mjs +3 -3
  128. package/lib/static-html-analysis.mjs +29 -1
  129. package/lib/threat-intelligence-manifests.mjs +1 -1
  130. package/lib/threatfox-intelligence.mjs +1 -1
  131. package/lib/tls-intelligence.mjs +13 -3
  132. package/lib/trust-store-comparison.mjs +146 -0
  133. package/lib/urlhaus-intelligence.mjs +1 -1
  134. package/lib/website-security-posture.mjs +5 -5
  135. package/lib/website-technology.mjs +25 -11
  136. package/lib/whois-chain.mjs +1 -1
  137. package/lib/zone-intent-review.mjs +534 -0
  138. package/package.json +5 -1
  139. package/third-party-notices.txt +911 -19
package/cli/arguments.mjs CHANGED
@@ -1,6 +1,7 @@
1
1
  import { parseInspectArchiveArguments, parseSignArtifactArguments, parseVerifySignatureArguments, } from "./evidence-command-arguments.mjs";
2
2
  import { CliUsageError } from "./errors.mjs";
3
3
  import { INVESTIGATION_PLAN_RECIPES, } from "./investigation-plan.mjs";
4
+ import { parseCliFailPolicies } from "./fail-policy.mjs";
4
5
  const MAX_CLI_ARGUMENTS = 32;
5
6
  const MAX_CLI_ARGUMENT_LENGTH = 1024;
6
7
  const CLI_COMMANDS = [
@@ -8,16 +9,24 @@ const CLI_COMMANDS = [
8
9
  'doctor',
9
10
  'commands',
10
11
  'manual',
12
+ 'manifest',
13
+ 'map-observations',
14
+ 'oam-export',
11
15
  'lookup',
12
16
  'bulk',
13
17
  'ct-search',
18
+ 'ct-intake',
14
19
  'discover',
15
20
  'discover-scan',
16
21
  'posture',
17
22
  'http',
18
23
  'tls',
19
24
  'registry-support',
25
+ 'registry-doctor',
26
+ 'registry-cohort',
27
+ 'registry-scaffold',
20
28
  'risk-calibrate',
29
+ 'lookalike-calibrate',
21
30
  'verify-artifact',
22
31
  'inspect-archive',
23
32
  'sign-artifact',
@@ -27,9 +36,17 @@ const CLI_COMMANDS = [
27
36
  'page-compare',
28
37
  'mail-review',
29
38
  'review-evidence',
39
+ 'brief',
40
+ 'case-pack',
30
41
  'domain-control',
42
+ 'monitor-once',
43
+ 'assurance',
44
+ 'change-packet',
45
+ 'sharing-review',
31
46
  'workflow-plan',
47
+ 'workflow-run',
32
48
  'diff',
49
+ 'reconcile',
33
50
  'timeline',
34
51
  'export',
35
52
  ];
@@ -106,7 +123,8 @@ function parseCliArgumentsCore(argv) {
106
123
  }
107
124
  const command = firstArgument;
108
125
  if (!isCliCommand(command)) {
109
- throw new CliUsageError(`Unknown command "${command}". This release supports: ${CLI_COMMANDS.join(', ')}.`);
126
+ const displayedCommand = command.length > 80 ? `${command.slice(0, 79)}…` : command;
127
+ throw new CliUsageError(`Unknown command "${displayedCommand}". Run "whoisleuth commands" to list supported commands.`);
110
128
  }
111
129
  if (command === 'bulk')
112
130
  return parseBulkArguments(argv.slice(1));
@@ -118,8 +136,16 @@ function parseCliArgumentsCore(argv) {
118
136
  return parseCommandsArguments(argv.slice(1));
119
137
  if (command === 'manual')
120
138
  return parseManualArguments(argv.slice(1));
139
+ if (command === 'manifest')
140
+ return parseManifestArguments(argv.slice(1));
141
+ if (command === 'map-observations')
142
+ return parseOfflineImportArguments('map-observations', argv.slice(1));
143
+ if (command === 'oam-export')
144
+ return parseOfflineImportArguments('oam-export', argv.slice(1));
121
145
  if (command === 'ct-search')
122
146
  return parseCtSearchArguments(argv.slice(1));
147
+ if (command === 'ct-intake')
148
+ return parseCtIntakeArguments(argv.slice(1));
123
149
  if (command === 'discover')
124
150
  return parseDiscoverArguments(argv.slice(1));
125
151
  if (command === 'discover-scan')
@@ -132,8 +158,16 @@ function parseCliArgumentsCore(argv) {
132
158
  return parseTlsArguments(argv.slice(1));
133
159
  if (command === 'registry-support')
134
160
  return parseRegistrySupportArguments(argv.slice(1));
161
+ if (command === 'registry-doctor')
162
+ return parseRegistryDoctorArguments(argv.slice(1));
163
+ if (command === 'registry-cohort')
164
+ return parseRegistryCohortArguments(argv.slice(1));
165
+ if (command === 'registry-scaffold')
166
+ return parseRegistryScaffoldArguments(argv.slice(1));
135
167
  if (command === 'risk-calibrate')
136
168
  return parseRiskCalibrateArguments(argv.slice(1));
169
+ if (command === 'lookalike-calibrate')
170
+ return parseLookalikeCalibrateArguments(argv.slice(1));
137
171
  if (command === 'verify-artifact')
138
172
  return parseVerifyArtifactArguments(argv.slice(1));
139
173
  if (command === 'inspect-archive')
@@ -152,12 +186,28 @@ function parseCliArgumentsCore(argv) {
152
186
  return parseMailReviewArguments(argv.slice(1));
153
187
  if (command === 'review-evidence')
154
188
  return parseReviewEvidenceArguments(argv.slice(1));
189
+ if (command === 'brief')
190
+ return parseBriefArguments(argv.slice(1));
191
+ if (command === 'case-pack')
192
+ return parseCasePackArguments(argv.slice(1));
155
193
  if (command === 'domain-control')
156
194
  return parseDomainControlArguments(argv.slice(1));
195
+ if (command === 'monitor-once')
196
+ return parseMonitorOnceArguments(argv.slice(1));
197
+ if (command === 'assurance')
198
+ return parseAssuranceArguments(argv.slice(1));
199
+ if (command === 'change-packet')
200
+ return parseChangePacketArguments(argv.slice(1));
201
+ if (command === 'sharing-review')
202
+ return parseSharingReviewArguments(argv.slice(1));
157
203
  if (command === 'workflow-plan')
158
204
  return parseWorkflowPlanArguments(argv.slice(1));
205
+ if (command === 'workflow-run')
206
+ return parseWorkflowRunArguments(argv.slice(1));
159
207
  if (command === 'diff')
160
208
  return parseDiffArguments(argv.slice(1));
209
+ if (command === 'reconcile')
210
+ return parseReconcileArguments(argv.slice(1));
161
211
  if (command === 'timeline')
162
212
  return parseTimelineArguments(argv.slice(1));
163
213
  if (command === 'export')
@@ -173,12 +223,24 @@ function parseCliArgumentsCore(argv) {
173
223
  let strictExit = false;
174
224
  let events = false;
175
225
  let plan = false;
176
- for (const argument of argv.slice(1)) {
226
+ let failOn = null;
227
+ let observerLabel = null;
228
+ let vantageLabel = null;
229
+ const lookupArguments = argv.slice(1);
230
+ for (let index = 0; index < lookupArguments.length; index++) {
231
+ const argument = lookupArguments[index];
232
+ if (argument === undefined)
233
+ break;
177
234
  if (argument === '--json') {
178
235
  if (output !== 'terminal')
179
236
  throw new CliUsageError('Choose only one output format.');
180
237
  output = 'json';
181
238
  }
239
+ else if (argument === '--junit') {
240
+ if (output !== 'terminal')
241
+ throw new CliUsageError('Choose only one output format.');
242
+ output = 'junit';
243
+ }
182
244
  else if (argument === '--markdown' || argument === '--html') {
183
245
  if (output !== 'terminal')
184
246
  throw new CliUsageError('Choose only one output format.');
@@ -217,6 +279,27 @@ function parseCliArgumentsCore(argv) {
217
279
  throw new CliUsageError('--plan may be supplied only once.');
218
280
  plan = true;
219
281
  }
282
+ else if (argument === '--fail-on') {
283
+ if (failOn !== null)
284
+ throw new CliUsageError('--fail-on may be supplied only once.');
285
+ failOn = parseCliFailPolicies(lookupArguments[++index]);
286
+ }
287
+ else if (argument === '--observer' || argument === '--vantage') {
288
+ const isObserver = argument === '--observer';
289
+ if (isObserver ? observerLabel !== null : vantageLabel !== null) {
290
+ throw new CliUsageError(`${argument} may be supplied only once.`);
291
+ }
292
+ const value = lookupArguments[++index];
293
+ if (!value || value.startsWith('-'))
294
+ throw new CliUsageError(`${argument} requires one bounded label.`);
295
+ const normalized = value.replace(/\s+/gu, ' ').trim();
296
+ if (!normalized || normalized.length > 80)
297
+ throw new CliUsageError(`${argument} requires a label of at most 80 characters.`);
298
+ if (isObserver)
299
+ observerLabel = normalized;
300
+ else
301
+ vantageLabel = normalized;
302
+ }
220
303
  else if (argument === '--quiet')
221
304
  quiet = true;
222
305
  else if (argument === '--no-color')
@@ -232,12 +315,12 @@ function parseCliArgumentsCore(argv) {
232
315
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
233
316
  if (detailSet && output !== 'terminal')
234
317
  throw new CliUsageError('--summary and --verbose apply only to terminal output.');
235
- if (plan && (output === 'markdown' || output === 'html'))
318
+ if (plan && !['terminal', 'json'].includes(output))
236
319
  throw new CliUsageError('--plan supports terminal or JSON output only.');
237
- if (plan && (detailSet || strictExit || events || quiet)) {
320
+ if (plan && (detailSet || strictExit || events || quiet || failOn !== null)) {
238
321
  throw new CliUsageError('--plan cannot be combined with detail, strict-exit, event, or quiet options.');
239
322
  }
240
- return { action: 'lookup', query, output, deep, detail, strictExit, events, plan, quiet, color };
323
+ return { action: 'lookup', query, output, deep, detail, strictExit, events, plan, observerLabel, vantageLabel, quiet, color, ...(failOn ? { failOn } : {}) };
241
324
  }
242
325
  function parseCompletionArguments(argv) {
243
326
  if (argv.length !== 1 || !['bash', 'zsh', 'fish', 'powershell'].includes(argv[0] || '')) {
@@ -250,6 +333,84 @@ function parseManualArguments(argv) {
250
333
  throw new CliUsageError('manual does not accept command arguments.');
251
334
  return { action: 'manual' };
252
335
  }
336
+ function parseManifestArguments(argv) {
337
+ const sources = [];
338
+ let workflow = null;
339
+ let configurationDigestSha256 = null;
340
+ let output = 'terminal';
341
+ let quiet = false;
342
+ let color = true;
343
+ for (let index = 0; index < argv.length; index += 1) {
344
+ const argument = argv[index];
345
+ if (argument === '--json') {
346
+ if (output !== 'terminal')
347
+ throw new CliUsageError('--json may be supplied only once.');
348
+ output = 'json';
349
+ }
350
+ else if (argument === '--workflow') {
351
+ if (workflow !== null)
352
+ throw new CliUsageError('--workflow may be supplied only once.');
353
+ workflow = argv[++index] ?? null;
354
+ if (!workflow)
355
+ throw new CliUsageError('--workflow requires a bounded label.');
356
+ }
357
+ else if (argument === '--configuration-digest') {
358
+ if (configurationDigestSha256 !== null)
359
+ throw new CliUsageError('--configuration-digest may be supplied only once.');
360
+ configurationDigestSha256 = argv[++index] ?? null;
361
+ if (!configurationDigestSha256 || !/^sha256:[a-f0-9]{64}$/u.test(configurationDigestSha256)) {
362
+ throw new CliUsageError('--configuration-digest requires a sha256: hexadecimal digest.');
363
+ }
364
+ }
365
+ else if (argument === '--quiet')
366
+ quiet = true;
367
+ else if (argument === '--no-color')
368
+ color = false;
369
+ else if (argument?.startsWith('-'))
370
+ throw new CliUsageError(`Unknown option "${argument}".`);
371
+ else if (argument)
372
+ sources.push(argument);
373
+ }
374
+ if (!workflow)
375
+ throw new CliUsageError('manifest requires --workflow <label>.');
376
+ if (sources.length < 1 || sources.length > 16)
377
+ throw new CliUsageError('manifest requires from 1 to 16 JSON artefact files.');
378
+ if (new Set(sources).size !== sources.length)
379
+ throw new CliUsageError('manifest artefact files must be different.');
380
+ if (quiet && output !== 'terminal')
381
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
382
+ return { action: 'manifest', sources, workflow, configurationDigestSha256, output, quiet, color };
383
+ }
384
+ function parseSingleJsonInput(argv, overflowMessage) {
385
+ let source = null;
386
+ let output = 'terminal';
387
+ let quiet = false;
388
+ let color = true;
389
+ for (const argument of argv) {
390
+ if (argument === '--json') {
391
+ if (output !== 'terminal')
392
+ throw new CliUsageError('--json may be supplied only once.');
393
+ output = 'json';
394
+ }
395
+ else if (argument === '--quiet')
396
+ quiet = true;
397
+ else if (argument === '--no-color')
398
+ color = false;
399
+ else if (argument.startsWith('-'))
400
+ throw new CliUsageError(`Unknown option "${argument}".`);
401
+ else if (source === null)
402
+ source = argument;
403
+ else
404
+ throw new CliUsageError(overflowMessage);
405
+ }
406
+ if (quiet && output !== 'terminal')
407
+ throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
408
+ return { source, output, quiet, color };
409
+ }
410
+ function parseOfflineImportArguments(action, argv) {
411
+ const parsed = parseSingleJsonInput(argv, `${action} accepts one optional versioned JSON input file.`);
412
+ return { action, ...parsed };
413
+ }
253
414
  function parseCommandsArguments(argv) {
254
415
  let output = 'terminal';
255
416
  let quiet = false;
@@ -309,12 +470,14 @@ function parseBulkArguments(argv) {
309
470
  let checkpoint = null;
310
471
  let resume = false;
311
472
  let events = false;
473
+ let plan = false;
474
+ let failOn = null;
312
475
  let filter = 'all';
313
476
  for (let index = 0; index < argv.length; index++) {
314
477
  const argument = argv[index];
315
478
  if (argument === undefined)
316
479
  break;
317
- if (argument === '--json' || argument === '--jsonl' || argument === '--csv' || argument === '--domains' || argument === '--queries') {
480
+ if (argument === '--json' || argument === '--jsonl' || argument === '--csv' || argument === '--domains' || argument === '--queries' || argument === '--junit') {
318
481
  if (output !== 'terminal')
319
482
  throw new CliUsageError('Choose only one output format.');
320
483
  output = argument === '--json'
@@ -325,7 +488,9 @@ function parseBulkArguments(argv) {
325
488
  ? 'csv'
326
489
  : argument === '--domains'
327
490
  ? 'domains'
328
- : 'queries';
491
+ : argument === '--queries'
492
+ ? 'queries'
493
+ : 'junit';
329
494
  }
330
495
  else if (argument === '--deep' || argument === '--fast') {
331
496
  if (scanMode)
@@ -361,6 +526,16 @@ function parseBulkArguments(argv) {
361
526
  throw new CliUsageError('--events may be supplied only once.');
362
527
  events = true;
363
528
  }
529
+ else if (argument === '--plan') {
530
+ if (plan)
531
+ throw new CliUsageError('--plan may be supplied only once.');
532
+ plan = true;
533
+ }
534
+ else if (argument === '--fail-on') {
535
+ if (failOn !== null)
536
+ throw new CliUsageError('--fail-on may be supplied only once.');
537
+ failOn = parseCliFailPolicies(argv[++index]);
538
+ }
364
539
  else if (argument === '--registered-only' || argument === '--inconclusive-only' || argument === '--errors-only') {
365
540
  if (filter !== 'all')
366
541
  throw new CliUsageError('Bulk output filters are mutually exclusive and may be supplied only once.');
@@ -385,37 +560,22 @@ function parseBulkArguments(argv) {
385
560
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
386
561
  if (resume && checkpoint === null)
387
562
  throw new CliUsageError('--resume requires --checkpoint.');
563
+ if (plan && (events || resume || checkpoint !== null || quiet || failOn !== null || !['terminal', 'json'].includes(output))) {
564
+ throw new CliUsageError('--plan supports terminal or JSON output and cannot be combined with events, checkpoint, resume, or quiet options.');
565
+ }
388
566
  const maximum = deep ? 3 : 8;
389
567
  if (concurrency !== null && concurrency > maximum) {
390
568
  throw new CliUsageError(`--concurrency is capped at ${maximum} in ${deep ? 'deep' : 'fast'} bulk mode.`);
391
569
  }
392
- return { action: 'bulk', source, output, deep, quiet, color, concurrency: concurrency ?? (deep ? 2 : 4), checkpoint, resume, events, filter };
570
+ return { action: 'bulk', source, output, deep, quiet, color, concurrency: concurrency ?? (deep ? 2 : 4), checkpoint, resume, events, plan, filter, ...(failOn ? { failOn } : {}) };
393
571
  }
394
572
  function parseCtSearchArguments(argv) {
395
- let keyword = null;
396
- let output = 'terminal';
397
- let quiet = false;
398
- let color = true;
399
- for (const argument of argv) {
400
- if (argument === '--json') {
401
- if (output !== 'terminal')
402
- throw new CliUsageError('--json may be supplied only once.');
403
- output = 'json';
404
- }
405
- else if (argument === '--quiet')
406
- quiet = true;
407
- else if (argument === '--no-color')
408
- color = false;
409
- else if (argument.startsWith('-'))
410
- throw new CliUsageError(`Unknown option "${argument}".`);
411
- else if (keyword === null)
412
- keyword = argument;
413
- else
414
- throw new CliUsageError('ct-search accepts one keyword. Quote multi-word keywords as one argument.');
415
- }
416
- if (quiet && output !== 'terminal')
417
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
418
- return { action: 'ct-search', keyword, output, quiet, color };
573
+ const { source: keyword, ...options } = parseSingleJsonInput(argv, 'ct-search accepts one keyword. Quote multi-word keywords as one argument.');
574
+ return { action: 'ct-search', keyword, ...options };
575
+ }
576
+ function parseCtIntakeArguments(argv) {
577
+ const parsed = parseSingleJsonInput(argv, 'ct-intake accepts one optional versioned JSON event file.');
578
+ return { action: 'ct-intake', ...parsed };
419
579
  }
420
580
  function parseDiscoverArguments(argv) {
421
581
  let seed = null;
@@ -539,6 +699,8 @@ function parseDiscoverScanArguments(argv) {
539
699
  let filter = 'all';
540
700
  let filterSet = false;
541
701
  let events = false;
702
+ let plan = false;
703
+ let failOn = null;
542
704
  for (let index = 0; index < argv.length; index++) {
543
705
  const argument = argv[index];
544
706
  if (argument === undefined)
@@ -669,6 +831,16 @@ function parseDiscoverScanArguments(argv) {
669
831
  throw new CliUsageError('--events may be supplied only once.');
670
832
  events = true;
671
833
  }
834
+ else if (argument === '--plan') {
835
+ if (plan)
836
+ throw new CliUsageError('--plan may be supplied only once.');
837
+ plan = true;
838
+ }
839
+ else if (argument === '--fail-on') {
840
+ if (failOn !== null)
841
+ throw new CliUsageError('--fail-on may be supplied only once.');
842
+ failOn = parseCliFailPolicies(argv[++index]);
843
+ }
672
844
  else if (argument === '--quiet')
673
845
  quiet = true;
674
846
  else if (argument === '--no-color')
@@ -689,6 +861,9 @@ function parseDiscoverScanArguments(argv) {
689
861
  }
690
862
  if (resume && checkpoint === null)
691
863
  throw new CliUsageError('--resume requires --checkpoint.');
864
+ if (plan && (events || resume || checkpoint !== null || observationSnapshot !== null || quiet || failOn !== null || !['terminal', 'json'].includes(output))) {
865
+ throw new CliUsageError('--plan supports terminal or JSON output and cannot be combined with events, checkpoint, resume, observation snapshots, or quiet options.');
866
+ }
692
867
  if (quiet && output !== 'terminal')
693
868
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
694
869
  if (dictionarySource && preset === 'common')
@@ -697,7 +872,7 @@ function parseDiscoverScanArguments(argv) {
697
872
  action: 'discover-scan', seed, output, quiet, color, preset, keyboardLayout, tldText,
698
873
  dictionarySource, familyText, deep, scanLimit: scanLimit ?? Math.min(100, maximum),
699
874
  chunkSize: chunkSize ?? 25, concurrency: concurrency ?? (deep ? 2 : 4), checkpoint,
700
- resume, resolverText, observationSnapshot, allowlistSource, filter, events,
875
+ resume, resolverText, observationSnapshot, allowlistSource, filter, events, plan, ...(failOn ? { failOn } : {}),
701
876
  };
702
877
  }
703
878
  function positiveIntegerOption(value, option, maximum) {
@@ -717,6 +892,7 @@ function parsePostureArguments(argv) {
717
892
  let retiredSelectorText = null;
718
893
  let mailProfile = 'standard';
719
894
  let mailProfileSeen = false;
895
+ let ownedDomain = false;
720
896
  for (let index = 0; index < argv.length; index++) {
721
897
  const argument = argv[index];
722
898
  if (argument === undefined)
@@ -726,6 +902,16 @@ function parsePostureArguments(argv) {
726
902
  throw new CliUsageError('--json may be supplied only once.');
727
903
  output = 'json';
728
904
  }
905
+ else if (argument === '--sarif') {
906
+ if (output !== 'terminal')
907
+ throw new CliUsageError('Choose only one output format.');
908
+ output = 'sarif';
909
+ }
910
+ else if (argument === '--owned-domain') {
911
+ if (ownedDomain)
912
+ throw new CliUsageError('--owned-domain may be supplied only once.');
913
+ ownedDomain = true;
914
+ }
729
915
  else if (argument === '--selectors') {
730
916
  if (selectorText !== null)
731
917
  throw new CliUsageError('--selectors may be supplied only once.');
@@ -765,36 +951,28 @@ function parsePostureArguments(argv) {
765
951
  }
766
952
  if (quiet && output !== 'terminal')
767
953
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
768
- return { action: 'posture', domain, output, quiet, color, selectorText, retiredSelectorText, mailProfile };
954
+ if (output === 'sarif' && !ownedDomain)
955
+ throw new CliUsageError('--sarif requires --owned-domain to confirm the passive posture target is controlled by the operator.');
956
+ return { action: 'posture', domain, output, quiet, color, selectorText, retiredSelectorText, mailProfile, ownedDomain };
769
957
  }
770
958
  function parseHttpArguments(argv) {
771
- let domain = null;
772
- let output = 'terminal';
773
- let quiet = false;
774
- let color = true;
775
- for (const argument of argv) {
776
- if (argument === '--json') {
777
- if (output !== 'terminal')
778
- throw new CliUsageError('--json may be supplied only once.');
779
- output = 'json';
780
- }
781
- else if (argument === '--quiet')
782
- quiet = true;
783
- else if (argument === '--no-color')
784
- color = false;
785
- else if (argument.startsWith('-'))
786
- throw new CliUsageError(`Unknown option "${argument}".`);
787
- else if (domain === null)
788
- domain = argument;
789
- else
790
- throw new CliUsageError('http accepts one domain.');
791
- }
792
- if (quiet && output !== 'terminal')
793
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
794
- return { action: 'http', domain, output, quiet, color };
959
+ const { source: domain, ...options } = parseSingleJsonInput(argv, 'http accepts one domain.');
960
+ return { action: 'http', domain, ...options };
795
961
  }
796
962
  function parseTlsArguments(argv) {
797
- let hostname = null;
963
+ const { source: hostname, ...options } = parseSingleJsonInput(argv, 'tls accepts one hostname.');
964
+ return { action: 'tls', hostname, ...options };
965
+ }
966
+ function parseCompareArguments(argv) {
967
+ const parsed = parseSingleJsonInput(argv, 'compare accepts one optional lookup JSON file. Otherwise pipe one lookup document on stdin.');
968
+ return { action: 'compare', ...parsed };
969
+ }
970
+ function parsePageCompareArguments(argv) {
971
+ const parsed = parseTwoFileComparisonArguments(argv, 'page-compare');
972
+ return { action: 'page-compare', ...parsed };
973
+ }
974
+ function parseTwoFileComparisonArguments(argv, command) {
975
+ const sources = [];
798
976
  let output = 'terminal';
799
977
  let quiet = false;
800
978
  let color = true;
@@ -810,26 +988,48 @@ function parseTlsArguments(argv) {
810
988
  color = false;
811
989
  else if (argument.startsWith('-'))
812
990
  throw new CliUsageError(`Unknown option "${argument}".`);
813
- else if (hostname === null)
814
- hostname = argument;
815
991
  else
816
- throw new CliUsageError('tls accepts one hostname.');
992
+ sources.push(argument);
817
993
  }
994
+ if (sources.length !== 2 || !sources[0] || !sources[1])
995
+ throw new CliUsageError(`${command} requires two input JSON files.`);
996
+ if (sources[0] === sources[1])
997
+ throw new CliUsageError(`${command} requires two different input files.`);
818
998
  if (quiet && output !== 'terminal')
819
999
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
820
- return { action: 'tls', hostname, output, quiet, color };
1000
+ return { leftSource: sources[0], rightSource: sources[1], output, quiet, color };
821
1001
  }
822
- function parseCompareArguments(argv) {
1002
+ function parseMailReviewArguments(argv) {
1003
+ const parsed = parseSingleJsonInput(argv, 'mail-review accepts one optional Bulk JSON or JSONL input file.');
1004
+ return { action: 'mail-review', ...parsed };
1005
+ }
1006
+ function parseReviewEvidenceArguments(argv) {
823
1007
  let source = null;
1008
+ let mmdbSource = null;
824
1009
  let output = 'terminal';
1010
+ let strictExit = false;
825
1011
  let quiet = false;
826
1012
  let color = true;
827
- for (const argument of argv) {
1013
+ for (let index = 0; index < argv.length; index += 1) {
1014
+ const argument = argv[index];
1015
+ if (argument === undefined)
1016
+ break;
828
1017
  if (argument === '--json') {
829
1018
  if (output !== 'terminal')
830
1019
  throw new CliUsageError('--json may be supplied only once.');
831
1020
  output = 'json';
832
1021
  }
1022
+ else if (argument === '--mmdb') {
1023
+ const value = argv[++index];
1024
+ if (!value || value.startsWith('-') || mmdbSource !== null)
1025
+ throw new CliUsageError('--mmdb requires one database file and may be supplied only once.');
1026
+ mmdbSource = value;
1027
+ }
1028
+ else if (argument === '--strict-exit') {
1029
+ if (strictExit)
1030
+ throw new CliUsageError('--strict-exit may be supplied only once.');
1031
+ strictExit = true;
1032
+ }
833
1033
  else if (argument === '--quiet')
834
1034
  quiet = true;
835
1035
  else if (argument === '--no-color')
@@ -839,55 +1039,108 @@ function parseCompareArguments(argv) {
839
1039
  else if (source === null)
840
1040
  source = argument;
841
1041
  else
842
- throw new CliUsageError('compare accepts one optional lookup JSON file. Otherwise pipe one lookup document on stdin.');
1042
+ throw new CliUsageError('review-evidence accepts one optional versioned JSON input file.');
843
1043
  }
844
1044
  if (quiet && output !== 'terminal')
845
1045
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
846
- return { action: 'compare', source, output, quiet, color };
1046
+ return { action: 'review-evidence', source, mmdbSource, output, strictExit, quiet, color };
847
1047
  }
848
- function parsePageCompareArguments(argv) {
849
- const parsed = parseTwoFileComparisonArguments(argv, 'page-compare');
850
- return { action: 'page-compare', ...parsed };
1048
+ function parseBriefArguments(argv) {
1049
+ const parsed = parseSingleJsonInput(argv, 'brief accepts one optional saved Lookup file.');
1050
+ return { action: 'brief', ...parsed };
851
1051
  }
852
- function parseTwoFileComparisonArguments(argv, command) {
853
- const sources = [];
1052
+ function parseCasePackArguments(argv) {
1053
+ let source = null;
854
1054
  let output = 'terminal';
1055
+ let audience = null;
1056
+ let reviewed = false;
855
1057
  let quiet = false;
856
1058
  let color = true;
857
- for (const argument of argv) {
1059
+ for (let index = 0; index < argv.length; index += 1) {
1060
+ const argument = argv[index];
1061
+ if (argument === undefined)
1062
+ break;
858
1063
  if (argument === '--json') {
859
1064
  if (output !== 'terminal')
860
1065
  throw new CliUsageError('--json may be supplied only once.');
861
1066
  output = 'json';
862
1067
  }
1068
+ else if (argument === '--audience') {
1069
+ if (audience !== null)
1070
+ throw new CliUsageError('--audience may be supplied only once.');
1071
+ const value = argv[++index];
1072
+ if (!['internal', 'trusted', 'public'].includes(value || ''))
1073
+ throw new CliUsageError('--audience requires internal, trusted, or public.');
1074
+ audience = value;
1075
+ }
1076
+ else if (argument === '--reviewed') {
1077
+ if (reviewed)
1078
+ throw new CliUsageError('--reviewed may be supplied only once.');
1079
+ reviewed = true;
1080
+ }
863
1081
  else if (argument === '--quiet')
864
1082
  quiet = true;
865
1083
  else if (argument === '--no-color')
866
1084
  color = false;
867
1085
  else if (argument.startsWith('-'))
868
1086
  throw new CliUsageError(`Unknown option "${argument}".`);
1087
+ else if (source === null)
1088
+ source = argument;
869
1089
  else
870
- sources.push(argument);
1090
+ throw new CliUsageError('case-pack accepts one optional case-export file.');
871
1091
  }
872
- if (sources.length !== 2 || !sources[0] || !sources[1])
873
- throw new CliUsageError(`${command} requires two input JSON files.`);
874
- if (sources[0] === sources[1])
875
- throw new CliUsageError(`${command} requires two different input files.`);
1092
+ if (audience === null)
1093
+ throw new CliUsageError('case-pack requires --audience internal, trusted, or public.');
876
1094
  if (quiet && output !== 'terminal')
877
1095
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
878
- return { leftSource: sources[0], rightSource: sources[1], output, quiet, color };
1096
+ return { action: 'case-pack', source, output, audience, reviewed, quiet, color };
879
1097
  }
880
- function parseMailReviewArguments(argv) {
1098
+ function parseMonitorOnceArguments(argv) {
881
1099
  let source = null;
1100
+ let previousSource = null;
882
1101
  let output = 'terminal';
1102
+ let limit = 20;
1103
+ let concurrency = 2;
1104
+ let failOn = null;
883
1105
  let quiet = false;
884
1106
  let color = true;
885
- for (const argument of argv) {
1107
+ for (let index = 0; index < argv.length; index += 1) {
1108
+ const argument = argv[index];
1109
+ if (argument === undefined)
1110
+ break;
886
1111
  if (argument === '--json') {
887
1112
  if (output !== 'terminal')
888
1113
  throw new CliUsageError('--json may be supplied only once.');
889
1114
  output = 'json';
890
1115
  }
1116
+ else if (argument === '--junit') {
1117
+ if (output !== 'terminal')
1118
+ throw new CliUsageError('Choose only one output format.');
1119
+ output = 'junit';
1120
+ }
1121
+ else if (argument === '--previous') {
1122
+ if (previousSource !== null)
1123
+ throw new CliUsageError('--previous may be supplied only once.');
1124
+ const value = argv[++index];
1125
+ if (!value || value.startsWith('-'))
1126
+ throw new CliUsageError('--previous requires one prior monitor snapshot file.');
1127
+ previousSource = value;
1128
+ }
1129
+ else if (argument === '--limit' || argument === '--concurrency') {
1130
+ const value = Number(argv[++index]);
1131
+ const maximum = argument === '--limit' ? 20 : 3;
1132
+ if (!Number.isSafeInteger(value) || value < 1 || value > maximum)
1133
+ throw new CliUsageError(`${argument} requires an integer from 1 to ${maximum}.`);
1134
+ if (argument === '--limit')
1135
+ limit = value;
1136
+ else
1137
+ concurrency = value;
1138
+ }
1139
+ else if (argument === '--fail-on') {
1140
+ if (failOn !== null)
1141
+ throw new CliUsageError('--fail-on may be supplied only once.');
1142
+ failOn = parseCliFailPolicies(argv[++index]);
1143
+ }
891
1144
  else if (argument === '--quiet')
892
1145
  quiet = true;
893
1146
  else if (argument === '--no-color')
@@ -897,23 +1150,79 @@ function parseMailReviewArguments(argv) {
897
1150
  else if (source === null)
898
1151
  source = argument;
899
1152
  else
900
- throw new CliUsageError('mail-review accepts one optional Bulk JSON or JSONL input file.');
1153
+ throw new CliUsageError('monitor-once accepts one optional domain-control manifest file.');
901
1154
  }
902
1155
  if (quiet && output !== 'terminal')
903
1156
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
904
- return { action: 'mail-review', source, output, quiet, color };
1157
+ return { action: 'monitor-once', source, previousSource, output, limit, concurrency, quiet, color, ...(failOn ? { failOn } : {}) };
905
1158
  }
906
- function parseReviewEvidenceArguments(argv) {
1159
+ function parseDomainControlArguments(argv) {
1160
+ const parsed = parseSingleJsonInput(argv, 'domain-control accepts one optional versioned JSON input file.');
1161
+ return { action: 'domain-control', ...parsed };
1162
+ }
1163
+ function parseAssuranceArguments(argv) {
1164
+ const parsed = parseSingleJsonInput(argv, 'assurance accepts one optional versioned JSON input file.');
1165
+ return { action: 'assurance', ...parsed };
1166
+ }
1167
+ function parseChangePacketArguments(argv) {
1168
+ const parsed = parseSingleJsonInput(argv, 'change-packet accepts one optional versioned JSON input file.');
1169
+ return { action: 'change-packet', ...parsed };
1170
+ }
1171
+ function parseSharingReviewArguments(argv) {
907
1172
  let source = null;
908
1173
  let output = 'terminal';
1174
+ let marking = null;
1175
+ let recipientScope = null;
1176
+ let purpose = null;
1177
+ let humanReviewed = false;
1178
+ let personalDataReviewed = false;
1179
+ let redactionsConfirmed = false;
909
1180
  let quiet = false;
910
1181
  let color = true;
911
- for (const argument of argv) {
1182
+ for (let index = 0; index < argv.length; index++) {
1183
+ const argument = argv[index];
1184
+ if (argument === undefined)
1185
+ break;
912
1186
  if (argument === '--json') {
913
1187
  if (output !== 'terminal')
914
1188
  throw new CliUsageError('--json may be supplied only once.');
915
1189
  output = 'json';
916
1190
  }
1191
+ else if (argument === '--marking') {
1192
+ if (marking !== null)
1193
+ throw new CliUsageError('--marking may be supplied only once.');
1194
+ const value = argv[++index];
1195
+ if (!['clear', 'green', 'amber', 'amber-strict', 'red'].includes(value || '')) {
1196
+ throw new CliUsageError('--marking requires clear, green, amber, amber-strict, or red.');
1197
+ }
1198
+ marking = value;
1199
+ }
1200
+ else if (argument === '--recipient-scope') {
1201
+ if (recipientScope !== null)
1202
+ throw new CliUsageError('--recipient-scope may be supplied only once.');
1203
+ const value = argv[++index];
1204
+ if (!['public', 'community', 'organization', 'named-recipients'].includes(value || '')) {
1205
+ throw new CliUsageError('--recipient-scope requires public, community, organization, or named-recipients.');
1206
+ }
1207
+ recipientScope = value;
1208
+ }
1209
+ else if (argument === '--purpose') {
1210
+ if (purpose !== null)
1211
+ throw new CliUsageError('--purpose may be supplied only once.');
1212
+ const value = argv[++index];
1213
+ if (!value || value.startsWith('-'))
1214
+ throw new CliUsageError('--purpose requires one bounded description.');
1215
+ const normalized = value.replace(/\s+/gu, ' ').trim();
1216
+ if (!normalized || normalized.length > 200)
1217
+ throw new CliUsageError('--purpose is limited to 200 characters.');
1218
+ purpose = normalized;
1219
+ }
1220
+ else if (argument === '--human-reviewed')
1221
+ humanReviewed = true;
1222
+ else if (argument === '--personal-data-reviewed')
1223
+ personalDataReviewed = true;
1224
+ else if (argument === '--redactions-confirmed')
1225
+ redactionsConfirmed = true;
917
1226
  else if (argument === '--quiet')
918
1227
  quiet = true;
919
1228
  else if (argument === '--no-color')
@@ -923,14 +1232,20 @@ function parseReviewEvidenceArguments(argv) {
923
1232
  else if (source === null)
924
1233
  source = argument;
925
1234
  else
926
- throw new CliUsageError('review-evidence accepts one optional versioned JSON input file.');
1235
+ throw new CliUsageError('sharing-review accepts one optional artefact JSON file.');
1236
+ }
1237
+ if (!marking || !recipientScope || !purpose) {
1238
+ throw new CliUsageError('sharing-review requires --marking, --recipient-scope, and --purpose.');
927
1239
  }
928
1240
  if (quiet && output !== 'terminal')
929
1241
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
930
- return { action: 'review-evidence', source, output, quiet, color };
1242
+ return {
1243
+ action: 'sharing-review', source, output, marking, recipientScope, purpose,
1244
+ humanReviewed, personalDataReviewed, redactionsConfirmed, quiet, color,
1245
+ };
931
1246
  }
932
- function parseDomainControlArguments(argv) {
933
- let source = null;
1247
+ function parseWorkflowPlanArguments(argv) {
1248
+ const positional = [];
934
1249
  let output = 'terminal';
935
1250
  let quiet = false;
936
1251
  let color = true;
@@ -946,26 +1261,48 @@ function parseDomainControlArguments(argv) {
946
1261
  color = false;
947
1262
  else if (argument.startsWith('-'))
948
1263
  throw new CliUsageError(`Unknown option "${argument}".`);
949
- else if (source === null)
950
- source = argument;
951
1264
  else
952
- throw new CliUsageError('domain-control accepts one optional versioned JSON input file.');
1265
+ positional.push(argument);
1266
+ }
1267
+ if (positional.length !== 2)
1268
+ throw new CliUsageError('workflow-plan requires one fixed recipe and one subject.');
1269
+ const recipe = positional[0];
1270
+ if (!INVESTIGATION_PLAN_RECIPES.includes(recipe)) {
1271
+ throw new CliUsageError(`workflow-plan recipe must be one of: ${INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
953
1272
  }
954
1273
  if (quiet && output !== 'terminal')
955
1274
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
956
- return { action: 'domain-control', source, output, quiet, color };
1275
+ return { action: 'workflow-plan', recipe: recipe, subject: positional[1], output, quiet, color };
957
1276
  }
958
- function parseWorkflowPlanArguments(argv) {
1277
+ function parseWorkflowRunArguments(argv) {
959
1278
  const positional = [];
1279
+ let resumeSource = null;
1280
+ let approveNetwork = false;
960
1281
  let output = 'terminal';
961
1282
  let quiet = false;
962
1283
  let color = true;
963
- for (const argument of argv) {
1284
+ for (let index = 0; index < argv.length; index += 1) {
1285
+ const argument = argv[index];
1286
+ if (argument === undefined)
1287
+ break;
964
1288
  if (argument === '--json') {
965
1289
  if (output !== 'terminal')
966
1290
  throw new CliUsageError('--json may be supplied only once.');
967
1291
  output = 'json';
968
1292
  }
1293
+ else if (argument === '--resume') {
1294
+ if (resumeSource !== null)
1295
+ throw new CliUsageError('--resume may be supplied only once.');
1296
+ const value = argv[++index];
1297
+ if (!value || value.startsWith('-'))
1298
+ throw new CliUsageError('--resume requires one prior workflow-run state file.');
1299
+ resumeSource = value;
1300
+ }
1301
+ else if (argument === '--approve-network') {
1302
+ if (approveNetwork)
1303
+ throw new CliUsageError('--approve-network may be supplied only once.');
1304
+ approveNetwork = true;
1305
+ }
969
1306
  else if (argument === '--quiet')
970
1307
  quiet = true;
971
1308
  else if (argument === '--no-color')
@@ -976,14 +1313,14 @@ function parseWorkflowPlanArguments(argv) {
976
1313
  positional.push(argument);
977
1314
  }
978
1315
  if (positional.length !== 2)
979
- throw new CliUsageError('workflow-plan requires one fixed recipe and one subject.');
1316
+ throw new CliUsageError('workflow-run requires one fixed recipe and one subject.');
980
1317
  const recipe = positional[0];
981
1318
  if (!INVESTIGATION_PLAN_RECIPES.includes(recipe)) {
982
- throw new CliUsageError(`workflow-plan recipe must be one of: ${INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
1319
+ throw new CliUsageError(`workflow-run recipe must be one of: ${INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
983
1320
  }
984
1321
  if (quiet && output !== 'terminal')
985
1322
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
986
- return { action: 'workflow-plan', recipe: recipe, subject: positional[1], output, quiet, color };
1323
+ return { action: 'workflow-run', recipe: recipe, subject: positional[1], resumeSource, approveNetwork, output, quiet, color };
987
1324
  }
988
1325
  function parseDiffArguments(argv) {
989
1326
  let leftSource = null;
@@ -1018,7 +1355,7 @@ function parseDiffArguments(argv) {
1018
1355
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1019
1356
  return { action: 'diff', leftSource, rightSource, output, quiet, color };
1020
1357
  }
1021
- function parseTimelineArguments(argv) {
1358
+ function parseReconcileArguments(argv) {
1022
1359
  const sources = [];
1023
1360
  let output = 'terminal';
1024
1361
  let quiet = false;
@@ -1038,18 +1375,18 @@ function parseTimelineArguments(argv) {
1038
1375
  else
1039
1376
  sources.push(argument);
1040
1377
  }
1041
- if (sources.length < 2 || sources.length > 20) {
1042
- throw new CliUsageError('timeline requires from 2 to 20 saved lookup JSON files.');
1378
+ if (sources.length < 2 || sources.length > 5) {
1379
+ throw new CliUsageError('reconcile requires from 2 to 5 saved lookup JSON files.');
1043
1380
  }
1044
1381
  if (new Set(sources).size !== sources.length) {
1045
- throw new CliUsageError('timeline input files must be different.');
1382
+ throw new CliUsageError('reconcile input files must be different.');
1046
1383
  }
1047
1384
  if (quiet && output !== 'terminal')
1048
1385
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1049
- return { action: 'timeline', sources, output, quiet, color };
1386
+ return { action: 'reconcile', sources, output, quiet, color };
1050
1387
  }
1051
- function parseRegistrySupportArguments(argv) {
1052
- let target = null;
1388
+ function parseTimelineArguments(argv) {
1389
+ const sources = [];
1053
1390
  let output = 'terminal';
1054
1391
  let quiet = false;
1055
1392
  let color = true;
@@ -1065,40 +1402,61 @@ function parseRegistrySupportArguments(argv) {
1065
1402
  color = false;
1066
1403
  else if (argument.startsWith('-'))
1067
1404
  throw new CliUsageError(`Unknown option "${argument}".`);
1068
- else if (target === null)
1069
- target = argument;
1070
1405
  else
1071
- throw new CliUsageError('registry-support accepts one domain or suffix.');
1406
+ sources.push(argument);
1407
+ }
1408
+ if (sources.length < 2 || sources.length > 20) {
1409
+ throw new CliUsageError('timeline requires from 2 to 20 saved lookup JSON files.');
1410
+ }
1411
+ if (new Set(sources).size !== sources.length) {
1412
+ throw new CliUsageError('timeline input files must be different.');
1072
1413
  }
1073
1414
  if (quiet && output !== 'terminal')
1074
1415
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1075
- return { action: 'registry-support', target, output, quiet, color };
1416
+ return { action: 'timeline', sources, output, quiet, color };
1076
1417
  }
1077
- function parseRiskCalibrateArguments(argv) {
1078
- let source = null;
1079
- let output = 'terminal';
1080
- let quiet = false;
1081
- let color = true;
1082
- for (const argument of argv) {
1083
- if (argument === '--json') {
1084
- if (output !== 'terminal')
1085
- throw new CliUsageError('--json may be supplied only once.');
1086
- output = 'json';
1087
- }
1088
- else if (argument === '--quiet')
1089
- quiet = true;
1090
- else if (argument === '--no-color')
1091
- color = false;
1092
- else if (argument.startsWith('-'))
1093
- throw new CliUsageError(`Unknown option "${argument}".`);
1094
- else if (source === null)
1095
- source = argument;
1418
+ function parseRegistrySupportArguments(argv) {
1419
+ const { source: target, ...options } = parseSingleJsonInput(argv, 'registry-support accepts one domain or suffix.');
1420
+ return { action: 'registry-support', target, ...options };
1421
+ }
1422
+ function parseRegistryDoctorArguments(argv) {
1423
+ const parsed = parseSingleJsonInput(argv, 'registry-doctor accepts one optional saved Lookup JSON file.');
1424
+ return { action: 'registry-doctor', ...parsed };
1425
+ }
1426
+ function parseRegistryCohortArguments(argv) {
1427
+ const parsed = parseSingleJsonInput(argv, 'registry-cohort accepts one optional JSON or JSONL input file.');
1428
+ return { action: 'registry-cohort', ...parsed };
1429
+ }
1430
+ function parseRegistryScaffoldArguments(argv) {
1431
+ let profile = '';
1432
+ let suffix = '';
1433
+ let scenario = '';
1434
+ for (let index = 0; index < argv.length; index += 2) {
1435
+ const option = argv[index];
1436
+ const value = argv[index + 1];
1437
+ if (!value)
1438
+ throw new CliUsageError(`${option || 'Option'} requires a value.`);
1439
+ if (option === '--profile' && !profile)
1440
+ profile = value;
1441
+ else if (option === '--suffix' && !suffix)
1442
+ suffix = value;
1443
+ else if (option === '--scenario' && !scenario)
1444
+ scenario = value;
1096
1445
  else
1097
- throw new CliUsageError('risk-calibrate accepts one optional dataset file. Otherwise pipe one dataset on stdin.');
1446
+ throw new CliUsageError(`Unknown or repeated option "${option || ''}".`);
1098
1447
  }
1099
- if (quiet && output !== 'terminal')
1100
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1101
- return { action: 'risk-calibrate', source, output, quiet, color };
1448
+ if (!profile || !suffix || !['registered', 'not_found', 'inconclusive'].includes(scenario)) {
1449
+ throw new CliUsageError('registry-scaffold requires --profile, --suffix, and --scenario <registered|not_found|inconclusive>.');
1450
+ }
1451
+ return { action: 'registry-scaffold', profile, suffix, scenario: scenario };
1452
+ }
1453
+ function parseRiskCalibrateArguments(argv) {
1454
+ const parsed = parseSingleJsonInput(argv, 'risk-calibrate accepts one optional dataset file. Otherwise pipe one dataset on stdin.');
1455
+ return { action: 'risk-calibrate', ...parsed };
1456
+ }
1457
+ function parseLookalikeCalibrateArguments(argv) {
1458
+ const parsed = parseSingleJsonInput(argv, 'lookalike-calibrate accepts one optional reviewed dataset JSON file.');
1459
+ return { action: 'lookalike-calibrate', ...parsed };
1102
1460
  }
1103
1461
  function parseVerifyArtifactArguments(argv) {
1104
1462
  let source = null;
@@ -1133,37 +1491,15 @@ function parseVerifyArtifactArguments(argv) {
1133
1491
  else if (source === null)
1134
1492
  source = argument;
1135
1493
  else
1136
- throw new CliUsageError('verify-artifact accepts one optional JSON file. Otherwise pipe one artifact on stdin.');
1494
+ throw new CliUsageError('verify-artifact accepts one optional JSON file. Otherwise pipe one artefact on stdin.');
1137
1495
  }
1138
1496
  if (quiet && output !== 'terminal')
1139
1497
  throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1140
1498
  return { action: 'verify-artifact', source, passphraseSource, output, quiet, color };
1141
1499
  }
1142
1500
  function parseSourceReportArguments(argv) {
1143
- let source = null;
1144
- let output = 'terminal';
1145
- let quiet = false;
1146
- let color = true;
1147
- for (const argument of argv) {
1148
- if (argument === '--json') {
1149
- if (output !== 'terminal')
1150
- throw new CliUsageError('--json may be supplied only once.');
1151
- output = 'json';
1152
- }
1153
- else if (argument === '--quiet')
1154
- quiet = true;
1155
- else if (argument === '--no-color')
1156
- color = false;
1157
- else if (argument.startsWith('-'))
1158
- throw new CliUsageError(`Unknown option "${argument}".`);
1159
- else if (source === null)
1160
- source = argument;
1161
- else
1162
- throw new CliUsageError('source-report accepts one optional JSON file. Otherwise pipe lookup documents on stdin.');
1163
- }
1164
- if (quiet && output !== 'terminal')
1165
- throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
1166
- return { action: 'source-report', source, output, quiet, color };
1501
+ const parsed = parseSingleJsonInput(argv, 'source-report accepts one optional JSON file. Otherwise pipe lookup documents on stdin.');
1502
+ return { action: 'source-report', ...parsed };
1167
1503
  }
1168
1504
  function parseExportArguments(argv) {
1169
1505
  let source = null;