@slicedearth/whoisleuth-cli 2.2.0 → 2.3.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.
- package/README.md +2 -1
- package/cli/arguments.mjs +389 -1745
- package/cli/artifact-validation/case-response.mjs +55 -30
- package/cli/artifact-validation/signed-review.mjs +30 -12
- package/cli/bulk-checkpoint.mjs +34 -3
- package/cli/case-pack.mjs +39 -92
- package/cli/command-argument-grammar.mjs +211 -0
- package/cli/command-reference.mjs +200 -213
- package/cli/completion.mjs +5 -1
- package/cli/discovery-observation-snapshot.mjs +16 -3
- package/cli/discovery-scan-command-runner.mjs +19 -0
- package/cli/discovery-scan.mjs +6 -0
- package/cli/domain-control-observations.mjs +1 -1
- package/cli/inline-command-families.mjs +1 -0
- package/cli/input.mjs +116 -0
- package/cli/interchange-report.mjs +14 -0
- package/cli/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- package/cli/mail-header-review.mjs +467 -0
- package/cli/review-command-runner.mjs +25 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +7 -1
- package/docs/cli-reference.md +8 -1
- package/docs/cli.md +12 -1
- package/lib/bounded-json.mjs +9 -6
- package/lib/dns-convergence-review.mjs +1 -0
- package/lib/dns-intelligence.mjs +37 -3
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/domain-change-packet.mjs +42 -14
- package/lib/domain-control-flight-recorder.mjs +4 -1
- package/lib/domain-control-manifest.mjs +16 -16
- package/lib/domain-posture-parsers.mjs +114 -3
- package/lib/domain-posture.mjs +34 -3
- package/lib/evidence-export.mjs +60 -11
- package/lib/evidence-report.mjs +2 -2
- package/lib/html-signals.mjs +39 -33
- package/lib/local-geoip-evidence.mjs +19 -3
- package/lib/lookup-response.mjs +5 -4
- package/lib/lookup-source-progress.mjs +27 -4
- package/lib/nameserver-preflight-review.mjs +1 -0
- package/lib/rdap-capabilities.mjs +1 -1
- package/lib/registry-insights.mjs +35 -22
- package/lib/risk-calibration-summary.mjs +1 -3
- package/lib/security-txt.mjs +33 -1
- package/lib/static-html-analysis.mjs +69 -13
- package/lib/website-technology.mjs +79 -11
- package/lib/zone-intent-review.mjs +16 -6
- package/package.json +1 -1
- package/packages/cases/case-migration-model.mjs +71 -22
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +10 -21
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +6 -4
- package/packages/cases/case-record-projection.mjs +218 -0
- package/packages/cases/case-report.mjs +11 -6
- package/packages/cases/case-response-model.mjs +43 -6
- package/packages/cases/case-response-packet.mjs +78 -27
- package/packages/cases/case-storage-model.mjs +6 -2
- package/packages/comparison/comparison-ledger-bulk.mjs +9 -10
- package/packages/comparison/page-similarity.mjs +3 -1
- package/packages/contracts/case-portability.mjs +171 -51
- package/packages/contracts/cli-command-semantics.mjs +93 -0
- package/packages/contracts/risk-calibration.mjs +15 -10
- package/packages/contracts/workspace-portability.mjs +44 -7
- package/packages/evidence/domain-control-runtime.mjs +46 -6
- package/packages/interchange/external-findings-import.mjs +3 -2
- package/packages/investigation/lookup-asset-graph.mjs +35 -27
- package/packages/workspace/brand-profile-model.mjs +118 -2
- package/packages/workspace/bulk-review-model.mjs +13 -7
- package/packages/workspace/bulk-session-model.mjs +5 -4
- package/packages/workspace/detection-rule-model.mjs +70 -22
- package/packages/workspace/relationship-observation-model.mjs +4 -1
- package/packages/workspace/website-snapshot-model.mjs +106 -8
- package/packages/workspace/workspace-archive.mjs +2 -0
- package/cli/evidence-command-arguments.mjs +0 -156
package/cli/arguments.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import {
|
|
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";
|
|
4
3
|
import { parseCliFailPolicies } from "./fail-policy.mjs";
|
|
5
4
|
import { isDirectLookupTarget } from "../lib/classify.mjs";
|
|
6
5
|
import { CLI_COMMANDS, cliMetaActionForInvocation, isCliCommand, } from "./command-reference.mjs";
|
|
@@ -12,1812 +11,457 @@ function boundedArgument(value) {
|
|
|
12
11
|
}
|
|
13
12
|
return value;
|
|
14
13
|
}
|
|
15
|
-
function
|
|
16
|
-
|
|
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 };
|
|
14
|
+
function terminalOptions(parsed) {
|
|
15
|
+
return { quiet: parsed.hasOption('--quiet'), color: !parsed.hasOption('--no-color') };
|
|
53
16
|
}
|
|
54
|
-
function
|
|
55
|
-
|
|
56
|
-
|
|
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.');
|
|
69
|
-
}
|
|
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.');
|
|
17
|
+
function parseInspectArchiveArguments(parsed) {
|
|
18
|
+
const expectedContentDigest = parsed.optionValue('--expect-content-digest');
|
|
19
|
+
if (expectedContentDigest !== null && !/^sha256:[a-f0-9]{64}$/u.test(expectedContentDigest)) {
|
|
20
|
+
throw new CliUsageError('--expect-content-digest requires sha256 followed by 64 lowercase hexadecimal characters.');
|
|
72
21
|
}
|
|
73
22
|
return {
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
23
|
+
action: 'inspect-archive',
|
|
24
|
+
source: parsed.positionalValue('source'),
|
|
25
|
+
passphraseSource: parsed.optionValue('--passphrase-file'),
|
|
26
|
+
search: parsed.optionValue('--search'),
|
|
27
|
+
reveal: parsed.hasOption('--reveal'),
|
|
28
|
+
requireMatch: parsed.hasOption('--require-match'),
|
|
29
|
+
expectedContentDigest,
|
|
30
|
+
output: parsed.hasOption('--json') ? 'json' : 'terminal',
|
|
31
|
+
...terminalOptions(parsed),
|
|
78
32
|
};
|
|
79
33
|
}
|
|
80
|
-
function
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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 };
|
|
34
|
+
function parseSignArtifactArguments(parsed) {
|
|
35
|
+
return {
|
|
36
|
+
action: 'sign-artifact',
|
|
37
|
+
source: parsed.positionalValue('source'),
|
|
38
|
+
privateKeySource: parsed.optionValue('--private-key-file'),
|
|
39
|
+
};
|
|
319
40
|
}
|
|
320
|
-
function
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
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 };
|
|
41
|
+
function parseVerifySignatureArguments(parsed) {
|
|
42
|
+
return {
|
|
43
|
+
action: 'verify-signature',
|
|
44
|
+
source: parsed.positionalValue('source'),
|
|
45
|
+
publicKeySource: parsed.optionValue('--public-key-file'),
|
|
46
|
+
output: parsed.hasOption('--json') ? 'json' : 'terminal',
|
|
47
|
+
...terminalOptions(parsed),
|
|
48
|
+
};
|
|
345
49
|
}
|
|
346
|
-
function
|
|
347
|
-
|
|
348
|
-
return { action, ...parsed };
|
|
50
|
+
function jsonOutput(parsed) {
|
|
51
|
+
return parsed.hasOption('--json') ? 'json' : 'terminal';
|
|
349
52
|
}
|
|
350
|
-
function
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
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}".`);
|
|
53
|
+
function normalizedLabel(parsed, option, maximum) {
|
|
54
|
+
const value = parsed.optionValue(option);
|
|
55
|
+
if (value === null)
|
|
56
|
+
return null;
|
|
57
|
+
const normalized = value.replace(/\s+/gu, ' ').trim();
|
|
58
|
+
if (!normalized || normalized.length > maximum) {
|
|
59
|
+
throw new CliUsageError(`${option} requires a label of at most ${maximum} characters.`);
|
|
393
60
|
}
|
|
394
|
-
|
|
395
|
-
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
396
|
-
return { action: 'commands', output, common, group, mode, quiet, color };
|
|
61
|
+
return normalized;
|
|
397
62
|
}
|
|
398
|
-
function
|
|
399
|
-
|
|
400
|
-
|
|
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}".`);
|
|
420
|
-
}
|
|
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 };
|
|
63
|
+
function failPolicies(parsed, command) {
|
|
64
|
+
const value = parsed.optionValue('--fail-on');
|
|
65
|
+
return value === null ? null : parseCliFailPolicies(value, command);
|
|
424
66
|
}
|
|
425
|
-
function
|
|
426
|
-
|
|
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 } : {}) };
|
|
534
|
-
}
|
|
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 };
|
|
67
|
+
function parseOutput(parsed, formats) {
|
|
68
|
+
return formats.find(([option]) => parsed.hasOption(option))?.[1] ?? 'terminal';
|
|
538
69
|
}
|
|
539
|
-
function
|
|
540
|
-
|
|
541
|
-
|
|
70
|
+
function singleInputAction(action, parsed, positionalName = 'source') {
|
|
71
|
+
return {
|
|
72
|
+
action,
|
|
73
|
+
[positionalName === 'source' ? 'source' : positionalName]: parsed.positionalValue(positionalName),
|
|
74
|
+
output: jsonOutput(parsed),
|
|
75
|
+
...terminalOptions(parsed),
|
|
76
|
+
};
|
|
542
77
|
}
|
|
543
|
-
function
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
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 };
|
|
78
|
+
function parseLookupArguments(parsed) {
|
|
79
|
+
const output = parseOutput(parsed, [
|
|
80
|
+
['--json', 'json'], ['--junit', 'junit'], ['--markdown', 'markdown'], ['--html', 'html'],
|
|
81
|
+
]);
|
|
82
|
+
const selectedFailPolicies = failPolicies(parsed, 'lookup');
|
|
83
|
+
return {
|
|
84
|
+
action: 'lookup',
|
|
85
|
+
query: parsed.positionalValue('target'),
|
|
86
|
+
output,
|
|
87
|
+
deep: parsed.hasOption('--deep'),
|
|
88
|
+
detail: parsed.hasOption('--summary') ? 'summary' : parsed.hasOption('--verbose') ? 'verbose' : 'standard',
|
|
89
|
+
strictExit: parsed.hasOption('--strict-exit'),
|
|
90
|
+
events: parsed.hasOption('--events'),
|
|
91
|
+
plan: parsed.hasOption('--plan'),
|
|
92
|
+
includeAttribution: !parsed.hasOption('--no-attribution'),
|
|
93
|
+
observerLabel: normalizedLabel(parsed, '--observer', 80),
|
|
94
|
+
vantageLabel: normalizedLabel(parsed, '--vantage', 80),
|
|
95
|
+
...terminalOptions(parsed),
|
|
96
|
+
...(parsed.hasOption('--browse') ? { browse: true } : {}),
|
|
97
|
+
...(parsed.optionValue('--save-lookup') ? { saveLookup: parsed.optionValue('--save-lookup') } : {}),
|
|
98
|
+
...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
|
|
99
|
+
};
|
|
639
100
|
}
|
|
640
|
-
function
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
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.');
|
|
101
|
+
function parseManifestArguments(parsed) {
|
|
102
|
+
const sources = parsed.positionalValues('artefacts');
|
|
103
|
+
if (sources.some((source) => !source))
|
|
104
|
+
throw new CliUsageError('manifest requires from 1 to 16 JSON artefact files.');
|
|
105
|
+
if (new Set(sources).size !== sources.length)
|
|
106
|
+
throw new CliUsageError('manifest artefact files must be different.');
|
|
107
|
+
const configurationDigestSha256 = parsed.optionValue('--configuration-digest');
|
|
108
|
+
if (configurationDigestSha256 !== null && !/^sha256:[a-f0-9]{64}$/u.test(configurationDigestSha256)) {
|
|
109
|
+
throw new CliUsageError('--configuration-digest requires a sha256: hexadecimal digest.');
|
|
829
110
|
}
|
|
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
111
|
return {
|
|
835
|
-
action: '
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
112
|
+
action: 'manifest',
|
|
113
|
+
sources,
|
|
114
|
+
workflow: parsed.optionValue('--workflow'),
|
|
115
|
+
configurationDigestSha256,
|
|
116
|
+
output: jsonOutput(parsed),
|
|
117
|
+
...terminalOptions(parsed),
|
|
839
118
|
};
|
|
840
119
|
}
|
|
841
|
-
function
|
|
842
|
-
|
|
843
|
-
|
|
844
|
-
const
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
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 };
|
|
120
|
+
function parseBulkArguments(parsed) {
|
|
121
|
+
const deep = parsed.hasOption('--deep');
|
|
122
|
+
const selectedFailPolicies = failPolicies(parsed, 'bulk');
|
|
123
|
+
const output = parseOutput(parsed, [
|
|
124
|
+
['--json', 'json'], ['--jsonl', 'jsonl'], ['--csv', 'csv'], ['--domains', 'domains'],
|
|
125
|
+
['--queries', 'queries'], ['--junit', 'junit'],
|
|
126
|
+
]);
|
|
127
|
+
const filter = parsed.hasOption('--registered-only') ? 'registered'
|
|
128
|
+
: parsed.hasOption('--inconclusive-only') ? 'inconclusive'
|
|
129
|
+
: parsed.hasOption('--errors-only') ? 'errors' : 'all';
|
|
130
|
+
return {
|
|
131
|
+
action: 'bulk',
|
|
132
|
+
source: parsed.positionalValue('source'),
|
|
133
|
+
output,
|
|
134
|
+
deep,
|
|
135
|
+
concurrency: parsed.integerOption('--concurrency') ?? (deep ? 2 : 4),
|
|
136
|
+
checkpoint: parsed.optionValue('--checkpoint'),
|
|
137
|
+
resume: parsed.hasOption('--resume'),
|
|
138
|
+
events: parsed.hasOption('--events'),
|
|
139
|
+
plan: parsed.hasOption('--plan'),
|
|
140
|
+
filter,
|
|
141
|
+
...terminalOptions(parsed),
|
|
142
|
+
...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
|
|
143
|
+
};
|
|
920
144
|
}
|
|
921
|
-
function
|
|
922
|
-
const
|
|
923
|
-
return {
|
|
145
|
+
function discoveryValues(parsed) {
|
|
146
|
+
const familyText = parsed.optionValue('--families');
|
|
147
|
+
return {
|
|
148
|
+
preset: (familyText ? 'custom' : parsed.optionValue('--preset') ?? 'all'),
|
|
149
|
+
keyboardLayout: (parsed.optionValue('--keyboard') ?? 'qwerty'),
|
|
150
|
+
tldText: parsed.optionValue('--tlds'),
|
|
151
|
+
dictionarySource: parsed.optionValue('--dictionary'),
|
|
152
|
+
familyText,
|
|
153
|
+
};
|
|
924
154
|
}
|
|
925
|
-
function
|
|
926
|
-
|
|
927
|
-
|
|
155
|
+
function parseDiscoverArguments(parsed) {
|
|
156
|
+
return {
|
|
157
|
+
action: 'discover',
|
|
158
|
+
seed: parsed.positionalValue('subject'),
|
|
159
|
+
output: parseOutput(parsed, [['--json', 'json'], ['--jsonl', 'jsonl'], ['--domains', 'domains']]),
|
|
160
|
+
...discoveryValues(parsed),
|
|
161
|
+
snapshotSource: parsed.optionValue('--snapshot'),
|
|
162
|
+
...terminalOptions(parsed),
|
|
163
|
+
};
|
|
928
164
|
}
|
|
929
|
-
function
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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 };
|
|
165
|
+
function parseDiscoverScanArguments(parsed) {
|
|
166
|
+
const deep = parsed.hasOption('--deep');
|
|
167
|
+
const selectedFailPolicies = failPolicies(parsed, 'discover-scan');
|
|
168
|
+
const filter = parsed.hasOption('--registered-only') ? 'registered'
|
|
169
|
+
: parsed.hasOption('--inconclusive-only') ? 'inconclusive'
|
|
170
|
+
: parsed.hasOption('--acquisition-only') ? 'acquisition'
|
|
171
|
+
: parsed.hasOption('--suppressed-only') ? 'suppressed' : 'all';
|
|
172
|
+
return {
|
|
173
|
+
action: 'discover-scan',
|
|
174
|
+
seed: parsed.positionalValue('subject'),
|
|
175
|
+
output: parseOutput(parsed, [['--json', 'json'], ['--jsonl', 'jsonl'], ['--csv', 'csv'], ['--domains', 'domains']]),
|
|
176
|
+
...discoveryValues(parsed),
|
|
177
|
+
deep,
|
|
178
|
+
scanLimit: parsed.integerOption('--scan-limit') ?? Math.min(100, deep ? 50 : 500),
|
|
179
|
+
chunkSize: parsed.integerOption('--chunk-size') ?? 25,
|
|
180
|
+
concurrency: parsed.integerOption('--concurrency') ?? (deep ? 2 : 4),
|
|
181
|
+
checkpoint: parsed.optionValue('--checkpoint'),
|
|
182
|
+
resume: parsed.hasOption('--resume'),
|
|
183
|
+
resolverText: parsed.optionValue('--resolver'),
|
|
184
|
+
observationSnapshot: parsed.optionValue('--observation-snapshot'),
|
|
185
|
+
allowlistSource: parsed.optionValue('--allowlist'),
|
|
186
|
+
filter,
|
|
187
|
+
events: parsed.hasOption('--events'),
|
|
188
|
+
plan: parsed.hasOption('--plan'),
|
|
189
|
+
...terminalOptions(parsed),
|
|
190
|
+
...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
|
|
191
|
+
};
|
|
983
192
|
}
|
|
984
|
-
function
|
|
985
|
-
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
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 };
|
|
193
|
+
function parsePostureArguments(parsed) {
|
|
194
|
+
const mailProfile = parsed.optionValue('--mail-profile') ?? 'standard';
|
|
195
|
+
return {
|
|
196
|
+
action: 'posture',
|
|
197
|
+
domain: parsed.positionalValue('domain'),
|
|
198
|
+
output: parseOutput(parsed, [['--json', 'json'], ['--sarif', 'sarif']]),
|
|
199
|
+
selectorText: parsed.optionValue('--selectors'),
|
|
200
|
+
retiredSelectorText: parsed.optionValue('--retired-selectors'),
|
|
201
|
+
mailProfile: mailProfile === 'defensive-no-mail' ? 'defensive_no_mail' : mailProfile,
|
|
202
|
+
ownedDomain: parsed.hasOption('--owned-domain'),
|
|
203
|
+
...terminalOptions(parsed),
|
|
204
|
+
};
|
|
1044
205
|
}
|
|
1045
|
-
function
|
|
1046
|
-
|
|
1047
|
-
|
|
206
|
+
function parseDnssecValidateArguments(parsed) {
|
|
207
|
+
return {
|
|
208
|
+
action: 'dnssec-validate',
|
|
209
|
+
target: parsed.positionalValue('domain'),
|
|
210
|
+
resolver: parsed.optionValue('--resolver'),
|
|
211
|
+
trustAnchorSource: parsed.optionValue('--trust-anchor'),
|
|
212
|
+
ownedOrAuthorized: true,
|
|
213
|
+
output: jsonOutput(parsed),
|
|
214
|
+
...terminalOptions(parsed),
|
|
215
|
+
};
|
|
1048
216
|
}
|
|
1049
|
-
function
|
|
1050
|
-
|
|
1051
|
-
|
|
217
|
+
function parseMailTransportArguments(parsed) {
|
|
218
|
+
return {
|
|
219
|
+
action: 'mail-transport',
|
|
220
|
+
source: parsed.positionalValue('source'),
|
|
221
|
+
resolver: parsed.optionValue('--resolver'),
|
|
222
|
+
trustAnchorSource: parsed.optionValue('--trust-anchor'),
|
|
223
|
+
ownedOrAuthorized: true,
|
|
224
|
+
activeProbeAcknowledged: true,
|
|
225
|
+
output: jsonOutput(parsed),
|
|
226
|
+
...terminalOptions(parsed),
|
|
227
|
+
};
|
|
1052
228
|
}
|
|
1053
|
-
function parseTwoFileComparisonArguments(
|
|
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.`);
|
|
229
|
+
function parseTwoFileComparisonArguments(parsed, command) {
|
|
230
|
+
const sources = parsed.positionalValues('sources');
|
|
1075
231
|
if (sources[0] === sources[1])
|
|
1076
232
|
throw new CliUsageError(`${command} requires two different input files.`);
|
|
1077
|
-
|
|
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 };
|
|
1130
|
-
}
|
|
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 };
|
|
233
|
+
return { leftSource: sources[0], rightSource: sources[1], output: jsonOutput(parsed), ...terminalOptions(parsed) };
|
|
1178
234
|
}
|
|
1179
|
-
function
|
|
1180
|
-
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
|
|
1186
|
-
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
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 };
|
|
235
|
+
function parseVerifyArtifactArguments(parsed) {
|
|
236
|
+
return {
|
|
237
|
+
action: 'verify-artifact',
|
|
238
|
+
source: parsed.positionalValue('source'),
|
|
239
|
+
passphraseSource: parsed.optionValue('--passphrase-file'),
|
|
240
|
+
manifestSource: parsed.optionValue('--manifest'),
|
|
241
|
+
manifestEntryId: parsed.optionValue('--manifest-entry'),
|
|
242
|
+
output: jsonOutput(parsed),
|
|
243
|
+
strictExit: parsed.hasOption('--strict-exit'),
|
|
244
|
+
...terminalOptions(parsed),
|
|
245
|
+
};
|
|
1256
246
|
}
|
|
1257
|
-
function
|
|
1258
|
-
const
|
|
1259
|
-
return {
|
|
247
|
+
function parseMonitorOnceArguments(parsed) {
|
|
248
|
+
const selectedFailPolicies = failPolicies(parsed, 'monitor-once');
|
|
249
|
+
return {
|
|
250
|
+
action: 'monitor-once',
|
|
251
|
+
source: parsed.positionalValue('source'),
|
|
252
|
+
previousSource: parsed.optionValue('--previous'),
|
|
253
|
+
output: parseOutput(parsed, [['--json', 'json'], ['--junit', 'junit']]),
|
|
254
|
+
limit: parsed.integerOption('--limit') ?? 20,
|
|
255
|
+
concurrency: parsed.integerOption('--concurrency') ?? 2,
|
|
256
|
+
...terminalOptions(parsed),
|
|
257
|
+
...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
|
|
258
|
+
};
|
|
1260
259
|
}
|
|
1261
|
-
function parseSharingReviewArguments(
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
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.');
|
|
260
|
+
function parseSharingReviewArguments(parsed) {
|
|
261
|
+
const purpose = parsed.optionValue('--purpose').replace(/\s+/gu, ' ').trim();
|
|
262
|
+
if (!purpose || purpose.length > 200)
|
|
263
|
+
throw new CliUsageError('--purpose is limited to 200 characters.');
|
|
1332
264
|
return {
|
|
1333
|
-
action: 'sharing-review',
|
|
1334
|
-
|
|
265
|
+
action: 'sharing-review',
|
|
266
|
+
source: parsed.positionalValue('source'),
|
|
267
|
+
output: jsonOutput(parsed),
|
|
268
|
+
marking: parsed.optionValue('--marking'),
|
|
269
|
+
recipientScope: parsed.optionValue('--recipient-scope'),
|
|
270
|
+
purpose,
|
|
271
|
+
humanReviewed: parsed.hasOption('--human-reviewed'),
|
|
272
|
+
personalDataReviewed: parsed.hasOption('--personal-data-reviewed'),
|
|
273
|
+
redactionsConfirmed: parsed.hasOption('--redactions-confirmed'),
|
|
274
|
+
...terminalOptions(parsed),
|
|
1335
275
|
};
|
|
1336
276
|
}
|
|
1337
|
-
function parseWorkflowPlanArguments(
|
|
1338
|
-
const
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
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);
|
|
277
|
+
function parseWorkflowPlanArguments(parsed) {
|
|
278
|
+
const options = { output: jsonOutput(parsed), ...terminalOptions(parsed) };
|
|
279
|
+
if (parsed.hasOption('--list')) {
|
|
280
|
+
if (parsed.allPositionals.length > 0)
|
|
281
|
+
throw new CliUsageError('workflow-plan discovery options do not accept a subject.');
|
|
282
|
+
return { action: 'workflow-plan', discovery: 'list', ...options };
|
|
1374
283
|
}
|
|
1375
|
-
if (
|
|
1376
|
-
|
|
1377
|
-
if (discovery !== null) {
|
|
1378
|
-
if (positional.length !== 0)
|
|
284
|
+
if (parsed.hasOption('--explain')) {
|
|
285
|
+
if (parsed.allPositionals.length > 0)
|
|
1379
286
|
throw new CliUsageError('workflow-plan discovery options do not accept a subject.');
|
|
1380
|
-
|
|
1381
|
-
return { action: 'workflow-plan', discovery, output, quiet, color };
|
|
1382
|
-
return { action: 'workflow-plan', discovery, recipe: explainedRecipe, output, quiet, color };
|
|
287
|
+
return { action: 'workflow-plan', discovery: 'explain', recipe: parsed.optionValue('--explain'), ...options };
|
|
1383
288
|
}
|
|
1384
|
-
if (
|
|
289
|
+
if (parsed.allPositionals.length !== 2) {
|
|
1385
290
|
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
291
|
}
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
}
|
|
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 };
|
|
292
|
+
return {
|
|
293
|
+
action: 'workflow-plan',
|
|
294
|
+
recipe: parsed.positionalValue('recipe'),
|
|
295
|
+
subject: parsed.positionalValue('subject'),
|
|
296
|
+
...options,
|
|
297
|
+
};
|
|
1630
298
|
}
|
|
1631
|
-
function
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
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';
|
|
299
|
+
function parseWorkflowRunArguments(parsed) {
|
|
300
|
+
const selections = parsed.optionValues('--select').map((value) => {
|
|
301
|
+
const separator = value.indexOf('=');
|
|
302
|
+
const stepId = separator === -1 ? '' : value.slice(0, separator);
|
|
303
|
+
const selection = separator === -1 ? '' : value.slice(separator + 1);
|
|
304
|
+
if (!/^[a-z0-9]+(?:-[a-z0-9]+)*$/u.test(stepId) || !selection.trim()) {
|
|
305
|
+
throw new CliUsageError('--select requires <step-id>=<path-or-value>.');
|
|
1648
306
|
}
|
|
1649
|
-
|
|
1650
|
-
|
|
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;
|
|
307
|
+
if (selection.startsWith('-')) {
|
|
308
|
+
throw new CliUsageError('--select values cannot start with a hyphen; prefix a file path with ./ when needed.');
|
|
1657
309
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
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 };
|
|
310
|
+
return Object.freeze({ stepId, value: selection });
|
|
311
|
+
});
|
|
312
|
+
return {
|
|
313
|
+
action: 'workflow-run',
|
|
314
|
+
recipe: parsed.positionalValue('recipe'),
|
|
315
|
+
subject: parsed.positionalValue('subject'),
|
|
316
|
+
resumeSource: parsed.optionValue('--resume'),
|
|
317
|
+
selections: Object.freeze(selections),
|
|
318
|
+
approveNetwork: parsed.hasOption('--approve-network'),
|
|
319
|
+
output: jsonOutput(parsed),
|
|
320
|
+
...terminalOptions(parsed),
|
|
321
|
+
};
|
|
1697
322
|
}
|
|
1698
|
-
function
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
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';
|
|
323
|
+
function parseDiffArguments(parsed) {
|
|
324
|
+
const compared = parseTwoFileComparisonArguments(parsed, 'diff');
|
|
325
|
+
const leftSessionId = parsed.optionValue('--left-session');
|
|
326
|
+
const rightSessionId = parsed.optionValue('--right-session');
|
|
327
|
+
for (const [option, value] of [['--left-session', leftSessionId], ['--right-session', rightSessionId]]) {
|
|
328
|
+
if (value !== null && !/^[A-Za-z0-9_-]{1,128}$/u.test(value)) {
|
|
329
|
+
throw new CliUsageError(`${option} requires one bounded saved-session ID.`);
|
|
1712
330
|
}
|
|
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
331
|
}
|
|
1732
|
-
|
|
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 };
|
|
332
|
+
return { action: 'diff', ...compared, leftSessionId, rightSessionId };
|
|
1739
333
|
}
|
|
1740
|
-
function
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
1744
|
-
|
|
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 };
|
|
334
|
+
function uniqueSources(parsed, command) {
|
|
335
|
+
const sources = parsed.positionalValues('sources');
|
|
336
|
+
if (new Set(sources).size !== sources.length)
|
|
337
|
+
throw new CliUsageError(`${command} input files must be different.`);
|
|
338
|
+
return sources;
|
|
1773
339
|
}
|
|
1774
340
|
const CLI_PARSERS = Object.freeze({
|
|
1775
|
-
completion:
|
|
1776
|
-
doctor:
|
|
1777
|
-
commands:
|
|
1778
|
-
|
|
341
|
+
completion: (parsed) => ({ action: 'completion', shell: parsed.positionalValue('shell') }),
|
|
342
|
+
doctor: (parsed) => ({ action: 'doctor', network: parsed.hasOption('--network'), output: jsonOutput(parsed), ...terminalOptions(parsed) }),
|
|
343
|
+
commands: (parsed) => ({
|
|
344
|
+
action: 'commands', output: jsonOutput(parsed), common: parsed.hasOption('--common'),
|
|
345
|
+
group: parsed.optionValue('--group'),
|
|
346
|
+
mode: parsed.optionValue('--mode'),
|
|
347
|
+
...terminalOptions(parsed),
|
|
348
|
+
}),
|
|
349
|
+
manual: () => ({ action: 'manual' }),
|
|
1779
350
|
manifest: parseManifestArguments,
|
|
1780
|
-
'map-observations': (
|
|
1781
|
-
'oam-export': (
|
|
351
|
+
'map-observations': (parsed) => singleInputAction('map-observations', parsed),
|
|
352
|
+
'oam-export': (parsed) => singleInputAction('oam-export', parsed),
|
|
1782
353
|
lookup: parseLookupArguments,
|
|
1783
354
|
bulk: parseBulkArguments,
|
|
1784
|
-
'ct-search':
|
|
1785
|
-
'ct-intake':
|
|
355
|
+
'ct-search': (parsed) => singleInputAction('ct-search', parsed, 'keyword'),
|
|
356
|
+
'ct-intake': (parsed) => singleInputAction('ct-intake', parsed),
|
|
1786
357
|
discover: parseDiscoverArguments,
|
|
1787
358
|
'discover-scan': parseDiscoverScanArguments,
|
|
1788
359
|
posture: parsePostureArguments,
|
|
1789
|
-
http:
|
|
1790
|
-
tls:
|
|
360
|
+
http: (parsed) => singleInputAction('http', parsed, 'domain'),
|
|
361
|
+
tls: (parsed) => singleInputAction('tls', parsed, 'hostname'),
|
|
1791
362
|
'dnssec-validate': parseDnssecValidateArguments,
|
|
1792
363
|
'mail-transport': parseMailTransportArguments,
|
|
1793
|
-
'registry-support':
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
'
|
|
1798
|
-
'
|
|
364
|
+
'registry-support': (parsed) => ({
|
|
365
|
+
action: 'registry-support', target: parsed.positionalValue('domain-or-suffix'),
|
|
366
|
+
output: jsonOutput(parsed), ...terminalOptions(parsed),
|
|
367
|
+
}),
|
|
368
|
+
'registry-doctor': (parsed) => singleInputAction('registry-doctor', parsed),
|
|
369
|
+
'registry-cohort': (parsed) => singleInputAction('registry-cohort', parsed),
|
|
370
|
+
'registry-scaffold': (parsed) => ({
|
|
371
|
+
action: 'registry-scaffold', profile: parsed.optionValue('--profile'), suffix: parsed.optionValue('--suffix'),
|
|
372
|
+
scenario: parsed.optionValue('--scenario'),
|
|
373
|
+
}),
|
|
374
|
+
'risk-calibrate': (parsed) => ({
|
|
375
|
+
action: 'risk-calibrate', source: parsed.positionalValue('source'),
|
|
376
|
+
output: parsed.hasOption('--json') ? 'json' : parsed.hasOption('--summary-json') ? 'summary_json' : 'terminal',
|
|
377
|
+
...terminalOptions(parsed),
|
|
378
|
+
}),
|
|
379
|
+
'lookalike-calibrate': (parsed) => singleInputAction('lookalike-calibrate', parsed),
|
|
1799
380
|
'verify-artifact': parseVerifyArtifactArguments,
|
|
1800
|
-
'interchange-report':
|
|
381
|
+
'interchange-report': (parsed) => ({
|
|
382
|
+
action: 'interchange-report', source: parsed.positionalValue('source'),
|
|
383
|
+
passphraseSource: parsed.optionValue('--passphrase-file'), output: jsonOutput(parsed), ...terminalOptions(parsed),
|
|
384
|
+
}),
|
|
1801
385
|
'inspect-archive': parseInspectArchiveArguments,
|
|
1802
386
|
'sign-artifact': parseSignArtifactArguments,
|
|
1803
387
|
'verify-signature': parseVerifySignatureArguments,
|
|
1804
|
-
'source-report':
|
|
1805
|
-
compare:
|
|
1806
|
-
'page-compare':
|
|
1807
|
-
'mail-review':
|
|
1808
|
-
'
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
388
|
+
'source-report': (parsed) => singleInputAction('source-report', parsed),
|
|
389
|
+
compare: (parsed) => singleInputAction('compare', parsed),
|
|
390
|
+
'page-compare': (parsed) => ({ action: 'page-compare', ...parseTwoFileComparisonArguments(parsed, 'page-compare') }),
|
|
391
|
+
'mail-review': (parsed) => singleInputAction('mail-review', parsed),
|
|
392
|
+
'mail-headers': (parsed) => singleInputAction('mail-headers', parsed),
|
|
393
|
+
'review-evidence': (parsed) => ({
|
|
394
|
+
action: 'review-evidence', source: parsed.positionalValue('source'), mmdbSource: parsed.optionValue('--mmdb'),
|
|
395
|
+
output: jsonOutput(parsed), strictExit: parsed.hasOption('--strict-exit'), ...terminalOptions(parsed),
|
|
396
|
+
}),
|
|
397
|
+
brief: (parsed) => singleInputAction('brief', parsed),
|
|
398
|
+
'case-pack': (parsed) => ({
|
|
399
|
+
action: 'case-pack', source: parsed.positionalValue('source'), output: jsonOutput(parsed),
|
|
400
|
+
audience: parsed.optionValue('--audience'), reviewed: true, ...terminalOptions(parsed),
|
|
401
|
+
}),
|
|
402
|
+
'domain-control': (parsed) => singleInputAction('domain-control', parsed),
|
|
1812
403
|
'monitor-once': parseMonitorOnceArguments,
|
|
1813
|
-
assurance:
|
|
1814
|
-
'change-packet':
|
|
404
|
+
assurance: (parsed) => singleInputAction('assurance', parsed),
|
|
405
|
+
'change-packet': (parsed) => singleInputAction('change-packet', parsed),
|
|
1815
406
|
'sharing-review': parseSharingReviewArguments,
|
|
1816
407
|
'workflow-plan': parseWorkflowPlanArguments,
|
|
1817
408
|
'workflow-run': parseWorkflowRunArguments,
|
|
1818
409
|
diff: parseDiffArguments,
|
|
1819
|
-
reconcile:
|
|
1820
|
-
timeline:
|
|
1821
|
-
export:
|
|
410
|
+
reconcile: (parsed) => ({ action: 'reconcile', sources: uniqueSources(parsed, 'reconcile'), output: jsonOutput(parsed), ...terminalOptions(parsed) }),
|
|
411
|
+
timeline: (parsed) => ({ action: 'timeline', sources: uniqueSources(parsed, 'timeline'), output: jsonOutput(parsed), ...terminalOptions(parsed) }),
|
|
412
|
+
export: (parsed) => ({
|
|
413
|
+
action: 'export', source: parsed.positionalValue('source'),
|
|
414
|
+
format: parsed.hasOption('--markdown') ? 'markdown' : parsed.hasOption('--html') ? 'html' : 'json',
|
|
415
|
+
compact: parsed.hasOption('--compact'), includeAttribution: !parsed.hasOption('--no-attribution'),
|
|
416
|
+
}),
|
|
1822
417
|
});
|
|
418
|
+
function parseCliArguments(rawArgv) {
|
|
419
|
+
if (!Array.isArray(rawArgv) || rawArgv.length > MAX_CLI_ARGUMENTS) {
|
|
420
|
+
throw new CliUsageError(`At most ${MAX_CLI_ARGUMENTS} command arguments are supported.`);
|
|
421
|
+
}
|
|
422
|
+
return parseCliArgumentsCore(rawArgv.map(boundedArgument));
|
|
423
|
+
}
|
|
424
|
+
function parseCliArgumentsCore(argv) {
|
|
425
|
+
if (!argv.length)
|
|
426
|
+
return { action: 'help' };
|
|
427
|
+
const firstArgument = argv[0] ?? '';
|
|
428
|
+
const metaAction = cliMetaActionForInvocation(argv);
|
|
429
|
+
if (metaAction?.id === 'help') {
|
|
430
|
+
if (argv.length === 1)
|
|
431
|
+
return { action: 'help' };
|
|
432
|
+
if (argv.length === 2 && (isCliCommand(firstArgument) || isDirectLookupTarget(firstArgument))) {
|
|
433
|
+
return { action: 'help', command: isCliCommand(firstArgument) ? firstArgument : 'lookup' };
|
|
434
|
+
}
|
|
435
|
+
throw new CliUsageError('Help accepts only an optional command name.');
|
|
436
|
+
}
|
|
437
|
+
if (metaAction?.id === 'version') {
|
|
438
|
+
if (argv.length !== 1)
|
|
439
|
+
throw new CliUsageError('--version does not accept other arguments.');
|
|
440
|
+
return { action: 'version' };
|
|
441
|
+
}
|
|
442
|
+
let command;
|
|
443
|
+
let directLookup = false;
|
|
444
|
+
if (isCliCommand(firstArgument))
|
|
445
|
+
command = firstArgument;
|
|
446
|
+
else if (isDirectLookupTarget(firstArgument)) {
|
|
447
|
+
command = 'lookup';
|
|
448
|
+
directLookup = true;
|
|
449
|
+
}
|
|
450
|
+
else {
|
|
451
|
+
const displayedCommand = firstArgument.length > 80 ? `${firstArgument.slice(0, 79)}…` : firstArgument;
|
|
452
|
+
throw new CliUsageError(`Unknown command "${displayedCommand}". Run "whoisleuth commands" to list supported commands.`);
|
|
453
|
+
}
|
|
454
|
+
const parsedGrammar = parseCommandArguments(command, directLookup ? argv : argv.slice(1));
|
|
455
|
+
const parsed = CLI_PARSERS[command](parsedGrammar);
|
|
456
|
+
if (parsed.action !== command)
|
|
457
|
+
throw new Error(`CLI parser contract mismatch for ${command}.`);
|
|
458
|
+
const destination = parsedGrammar.optionValue('--output');
|
|
459
|
+
const palette = parsedGrammar.optionValue('--palette');
|
|
460
|
+
return {
|
|
461
|
+
...parsed,
|
|
462
|
+
...(destination !== null ? { destination } : {}),
|
|
463
|
+
...(parsedGrammar.hasOption('--force') ? { force: true } : {}),
|
|
464
|
+
...(palette !== null ? { palette } : {}),
|
|
465
|
+
};
|
|
466
|
+
}
|
|
1823
467
|
export { CLI_COMMANDS, CLI_PARSERS, CliUsageError, MAX_CLI_ARGUMENTS, MAX_CLI_ARGUMENT_LENGTH, parseCliArguments };
|