@slicedearth/whoisleuth-cli 1.32.2 → 1.36.1
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 +43 -5
- package/SECURITY.md +35 -0
- package/cli/arguments.mjs +180 -8
- package/cli/artifact-verify.mjs +32 -0
- package/cli/bulk-command-runner.mjs +3 -1
- package/cli/bulk-output.mjs +6 -1
- package/cli/bulk.mjs +1 -2
- package/cli/command-catalogue.mjs +33 -0
- package/cli/completion.mjs +70 -7
- package/cli/doctor.mjs +78 -43
- package/cli/export-evidence.mjs +9 -2
- package/cli/formatters/html.mjs +1 -1
- package/cli/formatters/terminal.mjs +13 -7
- package/cli/investigation-plan.mjs +112 -0
- package/cli/lookup-command-runner.mjs +8 -0
- package/cli/lookup-diff.mjs +16 -5
- package/cli/lookup-plan.mjs +99 -0
- package/cli/lookup-timeline.mjs +123 -0
- package/cli/mail-review.mjs +47 -2
- package/cli/manual.mjs +2 -1
- package/cli/offline-evidence-review.mjs +141 -0
- package/cli/registry-support.mjs +1 -4
- package/cli/runner.mjs +214 -11
- package/docs/cli.md +200 -45
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -6
- package/frontend/src/lib/analysis/bulk-retry-plan.js +3 -10
- package/frontend/src/lib/analysis/bulk-source-coverage.js +76 -0
- package/frontend/src/lib/analysis/http-summary.js +2 -1
- package/frontend/src/lib/analysis/utils.js +7 -40
- package/frontend/src/lib/analysis/website-snapshot-model.js +57 -3
- package/lib/availability.mjs +30 -10
- package/lib/bulk-limits.mjs +13 -0
- package/lib/csv.mjs +1 -1
- package/lib/ct-response-bounds.mjs +9 -0
- package/lib/ct-search.mjs +66 -12
- package/lib/dns-delegation-health.mjs +56 -5
- package/lib/dnssec-evidence-validation.mjs +235 -0
- package/lib/domain-control-manifest.mjs +339 -0
- package/lib/domain-posture.mjs +2 -1
- package/lib/encrypted-dns-contract.mjs +81 -0
- package/lib/evidence-export.mjs +17 -3
- package/lib/evidence-report-markdown.mjs +3 -3
- package/lib/evidence-report.mjs +3 -0
- package/lib/favicon.mjs +3 -8
- package/lib/html-signals.mjs +4 -0
- package/lib/http-evidence-bounds.mjs +4 -0
- package/lib/http-intelligence.mjs +2 -1
- package/lib/ip-prefix.mjs +54 -0
- package/lib/local-geoip-evidence.mjs +89 -0
- package/lib/outbound-identity.mjs +9 -0
- package/lib/outbound-request-bounds.mjs +9 -0
- package/lib/perceptual-hash-comparison.mjs +34 -0
- package/lib/perceptual-hash.mjs +3 -31
- package/lib/project-metadata.mjs +8 -0
- package/lib/provider-policy-admission.mjs +49 -0
- package/lib/rdap-search-workbench.mjs +157 -0
- package/lib/rdap.mjs +7 -2
- package/lib/registry-capabilities.mjs +31 -1
- package/lib/registry-capability-catalogue.mjs +11 -10
- package/lib/response-policy.mjs +81 -6
- package/lib/rpki-evidence.mjs +105 -0
- package/lib/safe-fetch.mjs +2 -1
- package/lib/semantic-version.mjs +54 -0
- package/lib/static-html-analysis.mjs +27 -0
- package/lib/threatfox-intelligence.mjs +10 -5
- package/lib/tlsa-evidence.mjs +194 -0
- package/lib/urlhaus-intelligence.mjs +10 -5
- package/lib/urlscan-intelligence.mjs +10 -5
- package/lib/website-security-posture.mjs +11 -4
- package/lib/whois-chain.mjs +14 -152
- package/lib/whois.mjs +2 -2
- package/package.json +7 -5
- package/third-party-notices.txt +2 -2
package/cli/runner.mjs
CHANGED
|
@@ -2,12 +2,13 @@ import { Buffer } from 'node:buffer';
|
|
|
2
2
|
import { createReadStream } from 'node:fs';
|
|
3
3
|
import { createRequire } from 'node:module';
|
|
4
4
|
import { abortable } from "../lib/abort.mjs";
|
|
5
|
-
import { resolvePublicAddresses } from "../lib/safe-fetch.mjs";
|
|
5
|
+
import { resolvePublicAddresses, safeFetch } from "../lib/safe-fetch.mjs";
|
|
6
6
|
import { whoisQuery } from "../lib/whois-transport.mjs";
|
|
7
7
|
import { REGISTRY_CAPABILITIES_VERSION, registryCapabilityFor } from "../lib/registry-capabilities.mjs";
|
|
8
8
|
import { explainRiskScore, RISK_MODEL_VERSION, RISK_REVIEW_THRESHOLD } from "../lib/risk-scoring.mjs";
|
|
9
9
|
import { CLI_COMMANDS, parseCliArguments } from "./arguments.mjs";
|
|
10
10
|
import { runBulkCommand } from "./bulk-command-runner.mjs";
|
|
11
|
+
import { buildCliCommandCatalogue, formatCliCommandCatalogue } from "./command-catalogue.mjs";
|
|
11
12
|
import { buildShellCompletion } from "./completion.mjs";
|
|
12
13
|
import { buildDoctorReport, formatDoctorReport } from "./doctor.mjs";
|
|
13
14
|
import { MAX_COMPARE_INPUT_BYTES, compareLookupDocument, parseCliLookupDocument, readCompareInputBounded, } from "./compare.mjs";
|
|
@@ -22,10 +23,15 @@ import { buildCliCompareDocument, formatJsonDocument, } from "./formatters/json.
|
|
|
22
23
|
import { formatLookupEvidenceMarkdown } from "./formatters/markdown.mjs";
|
|
23
24
|
import { formatTerminalCompare, formatTerminalRegistrySupport, formatTerminalRiskCalibration, } from "./formatters/terminal.mjs";
|
|
24
25
|
import { buildCliLookupDiff, formatCliLookupDiff } from "./lookup-diff.mjs";
|
|
26
|
+
import { MAX_LOOKUP_TIMELINE_INPUT_BYTES, buildCliLookupTimeline, formatCliLookupTimeline, } from "./lookup-timeline.mjs";
|
|
25
27
|
import { buildCliPageComparison, formatCliPageComparison } from "./page-compare.mjs";
|
|
26
28
|
import { MAX_MAIL_REVIEW_INPUT_BYTES, buildCliMailReview, formatCliMailReview, } from "./mail-review.mjs";
|
|
29
|
+
import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES, buildOfflineEvidenceReview, formatOfflineEvidenceReview, } from "./offline-evidence-review.mjs";
|
|
30
|
+
import { DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA, DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, buildDomainControlManifest, formatDomainControlResult, reviewDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
27
31
|
import { runLookupCommand } from "./lookup-command-runner.mjs";
|
|
28
32
|
import { buildCliManual } from "./manual.mjs";
|
|
33
|
+
import { buildInvestigationPlan, formatInvestigationPlan } from "./investigation-plan.mjs";
|
|
34
|
+
import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../lib/project-metadata.mjs";
|
|
29
35
|
import { createBufferedOutput, writePrivateFile } from "./output-file.mjs";
|
|
30
36
|
import { createTerminalProgress } from "./progress.mjs";
|
|
31
37
|
import { buildRegistrySupportDocument } from "./registry-support.mjs";
|
|
@@ -64,10 +70,14 @@ Review saved evidence:
|
|
|
64
70
|
compare Compare saved registry publications.
|
|
65
71
|
page-compare Compare saved static page and TLS evidence.
|
|
66
72
|
mail-review Review saved passive mail exposure evidence.
|
|
73
|
+
review-evidence Review supplied DNS, routing, GeoIP, or RDAP evidence offline.
|
|
74
|
+
domain-control Build or review an integrity-protected desired-state manifest.
|
|
75
|
+
workflow-plan Plan a fixed investigation recipe without executing it.
|
|
67
76
|
diff Compare two saved domain observations.
|
|
77
|
+
timeline Compare a sequence of observations for one domain.
|
|
68
78
|
export Convert a saved lookup into an evidence report.
|
|
69
79
|
inspect-archive Inspect a workspace archive, redacted by default.
|
|
70
|
-
verify-artifact Validate
|
|
80
|
+
verify-artifact Validate saved evidence or an integrity envelope offline.
|
|
71
81
|
|
|
72
82
|
Integrity and calibration:
|
|
73
83
|
sign-artifact Sign one reviewed packet or manifest locally.
|
|
@@ -76,7 +86,8 @@ Integrity and calibration:
|
|
|
76
86
|
|
|
77
87
|
Terminal:
|
|
78
88
|
doctor Check the local runtime; network tests require --network.
|
|
79
|
-
|
|
89
|
+
commands List command contracts for people or local tooling.
|
|
90
|
+
completion Print completion for bash, zsh, fish, or PowerShell.
|
|
80
91
|
manual Print the generated manual page.
|
|
81
92
|
|
|
82
93
|
Run "whoisleuth <command> --help" for focused usage and an example.
|
|
@@ -86,14 +97,15 @@ Diagnostics are written to stderr. Fast lookup is the default; deep collection
|
|
|
86
97
|
must be requested explicitly and can disclose a target to additional sources.
|
|
87
98
|
|
|
88
99
|
Copyright 2026 slicedearth. Licensed under AGPL-3.0-only.
|
|
89
|
-
Source and licence:
|
|
100
|
+
Source and licence: ${WHOISLEUTH_SOURCE_REPOSITORY_URL}
|
|
90
101
|
`;
|
|
91
102
|
const COMMAND_USAGE = Object.freeze({
|
|
92
|
-
completion: 'whoisleuth completion <bash|zsh|fish>',
|
|
103
|
+
completion: 'whoisleuth completion <bash|zsh|fish|powershell>',
|
|
93
104
|
doctor: 'whoisleuth doctor [--network] [--json] [--quiet] [--no-color]',
|
|
105
|
+
commands: 'whoisleuth commands [--json] [--quiet] [--no-color]',
|
|
94
106
|
manual: 'whoisleuth manual',
|
|
95
|
-
lookup: 'whoisleuth lookup <domain|IP|ASN> [--json|--markdown|--html] [--fast|--deep] [--summary|--verbose] [--strict-exit] [--events] [--quiet] [--no-color]',
|
|
96
|
-
bulk: 'whoisleuth bulk [file] [--json|--jsonl|--csv|--domains] [--registered-only|--inconclusive-only] [--fast|--deep] [--concurrency <1-8>] [--checkpoint <file> [--resume]] [--events]',
|
|
107
|
+
lookup: 'whoisleuth lookup <domain|IP|ASN> [--json|--markdown|--html] [--fast|--deep] [--plan] [--summary|--verbose] [--strict-exit] [--events] [--quiet] [--no-color]',
|
|
108
|
+
bulk: 'whoisleuth bulk [file] [--json|--jsonl|--csv|--domains|--queries] [--registered-only|--inconclusive-only|--errors-only] [--fast|--deep] [--concurrency <1-8>] [--checkpoint <file> [--resume]] [--events]',
|
|
97
109
|
'ct-search': 'whoisleuth ct-search <keyword> [--json] [--quiet] [--no-color]',
|
|
98
110
|
discover: 'whoisleuth discover <brand|domain> [--tlds <list>] [--preset <name>|--families <ids>] [--keyboard <layout>] [--dictionary <file>] [--snapshot <file>] [--json|--jsonl|--domains]',
|
|
99
111
|
'discover-scan': 'whoisleuth discover-scan <brand|domain> [--fast|--deep] [--scan-limit <n>] [--chunk-size <n>] [--concurrency <n>] [--resolver <IPs>] [--allowlist <file>] [--checkpoint <file> [--resume]] [--observation-snapshot <file>] [--json|--jsonl|--csv|--domains]',
|
|
@@ -110,7 +122,11 @@ const COMMAND_USAGE = Object.freeze({
|
|
|
110
122
|
compare: 'whoisleuth compare [lookup.json] [--json] [--quiet] [--no-color]',
|
|
111
123
|
'page-compare': 'whoisleuth page-compare <left.json> <right.json> [--json] [--quiet] [--no-color]',
|
|
112
124
|
'mail-review': 'whoisleuth mail-review [bulk.json|bulk.jsonl] [--json] [--quiet] [--no-color]',
|
|
125
|
+
'review-evidence': 'whoisleuth review-evidence [evidence.json] [--json] [--quiet] [--no-color]',
|
|
126
|
+
'domain-control': 'whoisleuth domain-control [manifest-input.json|review-input.json] [--json] [--quiet] [--no-color]',
|
|
127
|
+
'workflow-plan': 'whoisleuth workflow-plan <recipe> <domain|brand> [--json] [--quiet] [--no-color]',
|
|
113
128
|
diff: 'whoisleuth diff <left.json> <right.json> [--json] [--quiet] [--no-color]',
|
|
129
|
+
timeline: 'whoisleuth timeline <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
|
|
114
130
|
export: 'whoisleuth export [lookup.json] [--markdown|--html|--compact]',
|
|
115
131
|
});
|
|
116
132
|
const COMMAND_DETAILS = Object.freeze({
|
|
@@ -124,6 +140,11 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
124
140
|
example: 'whoisleuth doctor --json',
|
|
125
141
|
boundary: 'The default check is offline. Public DNS and port 43 checks run only when --network is explicitly supplied.',
|
|
126
142
|
},
|
|
143
|
+
commands: {
|
|
144
|
+
description: 'List the installed command contracts in terminal or versioned JSON form.',
|
|
145
|
+
example: 'whoisleuth commands --json',
|
|
146
|
+
boundary: 'Catalogue generation is offline. It reports declared command modes and limits without executing collection or inspecting local evidence.',
|
|
147
|
+
},
|
|
127
148
|
manual: {
|
|
128
149
|
description: 'Print a generated roff manual page for local installation.',
|
|
129
150
|
example: 'whoisleuth manual | man -l -',
|
|
@@ -180,7 +201,7 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
180
201
|
boundary: 'Calibration is offline and diagnostic. It never trains, tunes, or changes the scoring model automatically.',
|
|
181
202
|
},
|
|
182
203
|
'verify-artifact': {
|
|
183
|
-
description: 'Validate a supported archive, packet, or
|
|
204
|
+
description: 'Validate a supported archive, packet, manifest, or saved Lookup without printing evidence contents.',
|
|
184
205
|
example: 'whoisleuth verify-artifact workspace.json --json',
|
|
185
206
|
boundary: 'Verification is offline and redacted. Encrypted archives require an explicitly supplied passphrase file.',
|
|
186
207
|
},
|
|
@@ -219,20 +240,71 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
219
240
|
example: 'whoisleuth mail-review candidates.json --json',
|
|
220
241
|
boundary: 'Review is offline and sends no SMTP traffic. Missing or partial DNS evidence remains inconclusive.',
|
|
221
242
|
},
|
|
243
|
+
'review-evidence': {
|
|
244
|
+
description: 'Review one versioned DNSSEC, TLSA, RPKI, GeoIP, encrypted-DNS, or RDAP planning document offline.',
|
|
245
|
+
example: 'whoisleuth review-evidence dnssec-evidence.json --json',
|
|
246
|
+
boundary: 'The command reads only the supplied document. It performs no DNS, RDAP, BGP, GeoIP-provider, TLS, HTTP, or SMTP request.',
|
|
247
|
+
},
|
|
248
|
+
'domain-control': {
|
|
249
|
+
description: 'Build an integrity-protected desired-state manifest or compare one with supplied observations.',
|
|
250
|
+
example: 'whoisleuth domain-control domain-control-input.json --json',
|
|
251
|
+
boundary: 'The command is offline and changes no registrar, DNS, mail, or certificate configuration. Only complete supplied observations can produce drift.',
|
|
252
|
+
},
|
|
253
|
+
'workflow-plan': {
|
|
254
|
+
description: 'Build a fixed domain-investigation plan from existing bounded CLI commands.',
|
|
255
|
+
example: 'whoisleuth workflow-plan domain-triage example.test --json',
|
|
256
|
+
boundary: 'Planning is offline and plan-only. It does not execute commands, expand placeholders, read files, make requests, or submit evidence.',
|
|
257
|
+
},
|
|
222
258
|
diff: {
|
|
223
259
|
description: 'Compare bounded evidence retained in two saved domain lookups.',
|
|
224
260
|
example: 'whoisleuth diff first.json second.json --json',
|
|
225
261
|
boundary: 'Comparison is offline. Missing, unavailable, equal, and different evidence remain separate states.',
|
|
226
262
|
},
|
|
263
|
+
timeline: {
|
|
264
|
+
description: 'Build an ordered same-domain history from saved Lookup observations.',
|
|
265
|
+
example: 'whoisleuth timeline first.json second.json latest.json --json',
|
|
266
|
+
boundary: 'The command is offline, accepts 2 to 20 bounded inputs for one domain, retains no filenames or raw registry payloads, and does not treat changed collection conditions as a domain change.',
|
|
267
|
+
},
|
|
227
268
|
export: {
|
|
228
269
|
description: 'Convert one saved lookup into a versioned evidence report.',
|
|
229
270
|
example: 'whoisleuth export lookup.json --markdown',
|
|
230
271
|
boundary: 'Exports preserve source attribution and limitations. Compact output intentionally omits raw registry payloads.',
|
|
231
272
|
},
|
|
232
273
|
});
|
|
274
|
+
const COMMAND_COLLECTION = Object.freeze({
|
|
275
|
+
completion: { mode: 'offline', scope: 'Prints one static script and changes no shell configuration.' },
|
|
276
|
+
doctor: { mode: 'network', scope: 'Network access is opt-in with --network and is limited to fixed public DNS, HTTPS, and WHOIS diagnostics.' },
|
|
277
|
+
commands: { mode: 'offline', scope: 'Reads the embedded command catalogue and performs no collection.' },
|
|
278
|
+
manual: { mode: 'offline', scope: 'Builds documentation from the embedded command catalogue.' },
|
|
279
|
+
lookup: { mode: 'network', scope: 'Accepts one target. Fast is the default; deep collection must be selected explicitly.' },
|
|
280
|
+
bulk: { mode: 'network', scope: 'Accepts at most 500 fast or 50 deep targets, with concurrency capped at 8 fast or 3 deep.' },
|
|
281
|
+
'ct-search': { mode: 'network', scope: 'Accepts one bounded search keyword and queries the fixed certificate-transparency source.' },
|
|
282
|
+
discover: { mode: 'offline', scope: 'Generates a bounded candidate set from local rules, dictionaries, and optional saved snapshots.' },
|
|
283
|
+
'discover-scan': { mode: 'network', scope: 'Scans at most 500 fast or 50 deep candidates, with concurrency capped at 8 fast or 3 deep.' },
|
|
284
|
+
posture: { mode: 'network', scope: 'Accepts one domain and performs bounded DNS queries only.' },
|
|
285
|
+
http: { mode: 'network', scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
|
|
286
|
+
tls: { mode: 'network', scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
|
|
287
|
+
'registry-support': { mode: 'offline', scope: 'Reads the embedded registry capability catalogue for one domain or suffix.' },
|
|
288
|
+
'risk-calibrate': { mode: 'offline', scope: 'Reads one bounded reviewed-label dataset and changes no model or evidence.' },
|
|
289
|
+
'verify-artifact': { mode: 'offline', scope: 'Reads one selected bounded archive, packet, manifest, or saved Lookup document.' },
|
|
290
|
+
'inspect-archive': { mode: 'offline', scope: 'Reads one selected bounded workspace archive with redacted output by default.' },
|
|
291
|
+
'sign-artifact': { mode: 'offline', scope: 'Reads one selected artifact and one local private key without transmitting either.' },
|
|
292
|
+
'verify-signature': { mode: 'offline', scope: 'Reads one selected signed package and optional local public key.' },
|
|
293
|
+
'source-report': { mode: 'offline', scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
|
|
294
|
+
compare: { mode: 'offline', scope: 'Reads one saved Lookup and compares its separately attributed registry publications.' },
|
|
295
|
+
'page-compare': { mode: 'offline', scope: 'Reads two saved Lookup documents and executes no page code.' },
|
|
296
|
+
'mail-review': { mode: 'offline', scope: 'Reads one saved Bulk result and sends no DNS or SMTP traffic.' },
|
|
297
|
+
'review-evidence': { mode: 'offline', scope: 'Reads one bounded versioned evidence or request-planning document and performs no collection.' },
|
|
298
|
+
'domain-control': { mode: 'offline', scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
|
|
299
|
+
'workflow-plan': { mode: 'offline', scope: 'Builds a fixed typed recipe and executes none of its network or file steps.' },
|
|
300
|
+
diff: { mode: 'offline', scope: 'Reads two saved Lookup documents for different domains.' },
|
|
301
|
+
timeline: { mode: 'offline', scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
|
|
302
|
+
export: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
|
|
303
|
+
});
|
|
233
304
|
function commandHelp(command) {
|
|
234
305
|
const detail = COMMAND_DETAILS[command];
|
|
235
|
-
|
|
306
|
+
const collection = COMMAND_COLLECTION[command];
|
|
307
|
+
return `WHOISleuth ${command}\n${detail.description}\n\nUsage:\n ${COMMAND_USAGE[command]}\n\nExample:\n ${detail.example}\n\nCollection:\n ${collection.mode === 'offline' ? 'Offline' : 'Network'}: ${collection.scope}\n\nBoundary:\n ${detail.boundary}\n\nRun "whoisleuth --help" to see the grouped command list.\n`;
|
|
236
308
|
}
|
|
237
309
|
async function readStdinBounded(stream, limit = MAX_STDIN_BYTES) {
|
|
238
310
|
if (!stream || stream.isTTY)
|
|
@@ -341,8 +413,28 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
341
413
|
write(stdout, buildShellCompletion(args.shell));
|
|
342
414
|
return EXIT_CODES.SUCCESS;
|
|
343
415
|
}
|
|
416
|
+
if (args.action === 'commands') {
|
|
417
|
+
const catalogue = buildCliCommandCatalogue({
|
|
418
|
+
commands: CLI_COMMANDS,
|
|
419
|
+
collections: COMMAND_COLLECTION,
|
|
420
|
+
details: COMMAND_DETAILS,
|
|
421
|
+
usage: COMMAND_USAGE,
|
|
422
|
+
packageVersion: VERSION,
|
|
423
|
+
});
|
|
424
|
+
if (!args.quiet)
|
|
425
|
+
write(stdout, args.output === 'json'
|
|
426
|
+
? formatJsonDocument(catalogue)
|
|
427
|
+
: terminal(formatCliCommandCatalogue(catalogue), args.color));
|
|
428
|
+
return EXIT_CODES.SUCCESS;
|
|
429
|
+
}
|
|
344
430
|
if (args.action === 'manual') {
|
|
345
|
-
write(stdout, buildCliManual({
|
|
431
|
+
write(stdout, buildCliManual({
|
|
432
|
+
commands: CLI_COMMANDS,
|
|
433
|
+
collections: COMMAND_COLLECTION,
|
|
434
|
+
details: COMMAND_DETAILS,
|
|
435
|
+
usage: COMMAND_USAGE,
|
|
436
|
+
version: VERSION,
|
|
437
|
+
}));
|
|
346
438
|
return EXIT_CODES.SUCCESS;
|
|
347
439
|
}
|
|
348
440
|
if (args.action === 'doctor') {
|
|
@@ -353,10 +445,11 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
353
445
|
network: args.network,
|
|
354
446
|
presentation: terminalPresentation(stdout, args.color, environment),
|
|
355
447
|
...(dependencies.resolvePublicAddresses ? { resolveAddresses: dependencies.resolvePublicAddresses } : {}),
|
|
448
|
+
...(dependencies.safeFetch ? { fetchHttps: dependencies.safeFetch } : { fetchHttps: safeFetch }),
|
|
356
449
|
...(dependencies.whoisQuery ? { queryWhois: dependencies.whoisQuery } : {}),
|
|
357
450
|
});
|
|
358
451
|
const report = args.network
|
|
359
|
-
? await withProgress('Checking public DNS and WHOIS connectivity', buildReport)
|
|
452
|
+
? await withProgress('Checking public DNS, HTTPS, and WHOIS connectivity', buildReport)
|
|
360
453
|
: await buildReport();
|
|
361
454
|
if (!args.quiet) {
|
|
362
455
|
write(stdout, args.output === 'json'
|
|
@@ -579,6 +672,86 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
579
672
|
: terminal(formatCliMailReview(document), args.color));
|
|
580
673
|
return EXIT_CODES.SUCCESS;
|
|
581
674
|
}
|
|
675
|
+
if (args.action === 'review-evidence') {
|
|
676
|
+
failureLabel = 'Offline evidence review';
|
|
677
|
+
let input;
|
|
678
|
+
try {
|
|
679
|
+
input = dependencies.readArtifactInput
|
|
680
|
+
? await dependencies.readArtifactInput(args.source)
|
|
681
|
+
: await readSavedLookupInputBounded(args.source
|
|
682
|
+
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
683
|
+
: dependencies.stdin || process.stdin, {
|
|
684
|
+
limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
|
|
685
|
+
label: 'Offline evidence input',
|
|
686
|
+
});
|
|
687
|
+
}
|
|
688
|
+
catch (error) {
|
|
689
|
+
if (error instanceof CliUsageError)
|
|
690
|
+
throw error;
|
|
691
|
+
throw new CliUsageError(`Could not read offline evidence input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
692
|
+
}
|
|
693
|
+
if (!input.trim())
|
|
694
|
+
throw new CliUsageError('review-evidence requires one JSON file or a document on stdin.');
|
|
695
|
+
const document = buildOfflineEvidenceReview(input, commandContext.now());
|
|
696
|
+
if (!args.quiet)
|
|
697
|
+
write(stdout, args.output === 'json'
|
|
698
|
+
? formatJsonDocument(document)
|
|
699
|
+
: terminal(formatOfflineEvidenceReview(document), args.color));
|
|
700
|
+
return EXIT_CODES.SUCCESS;
|
|
701
|
+
}
|
|
702
|
+
if (args.action === 'domain-control') {
|
|
703
|
+
failureLabel = 'Domain control review';
|
|
704
|
+
let input;
|
|
705
|
+
try {
|
|
706
|
+
input = dependencies.readArtifactInput
|
|
707
|
+
? await dependencies.readArtifactInput(args.source)
|
|
708
|
+
: await readSavedLookupInputBounded(args.source
|
|
709
|
+
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
710
|
+
: dependencies.stdin || process.stdin, {
|
|
711
|
+
limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
|
|
712
|
+
label: 'Domain control input',
|
|
713
|
+
});
|
|
714
|
+
}
|
|
715
|
+
catch (error) {
|
|
716
|
+
if (error instanceof CliUsageError)
|
|
717
|
+
throw error;
|
|
718
|
+
throw new CliUsageError(`Could not read domain control input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
719
|
+
}
|
|
720
|
+
if (!input.trim())
|
|
721
|
+
throw new CliUsageError('domain-control requires one JSON file or a document on stdin.');
|
|
722
|
+
let parsed;
|
|
723
|
+
try {
|
|
724
|
+
parsed = JSON.parse(input);
|
|
725
|
+
}
|
|
726
|
+
catch {
|
|
727
|
+
throw new CliUsageError('Domain control input is not valid JSON.');
|
|
728
|
+
}
|
|
729
|
+
const schema = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
730
|
+
? parsed.schema
|
|
731
|
+
: null;
|
|
732
|
+
const document = schema === DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA
|
|
733
|
+
? buildDomainControlManifest(parsed, commandContext.now())
|
|
734
|
+
: schema === DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA
|
|
735
|
+
? reviewDomainControlManifest(parsed, commandContext.now())
|
|
736
|
+
: null;
|
|
737
|
+
if (!document) {
|
|
738
|
+
throw new CliUsageError(`Domain control input must use ${DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA} or ${DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA}.`);
|
|
739
|
+
}
|
|
740
|
+
if (!args.quiet)
|
|
741
|
+
write(stdout, args.output === 'json'
|
|
742
|
+
? formatJsonDocument(document)
|
|
743
|
+
: terminal(formatDomainControlResult(document), args.color));
|
|
744
|
+
return EXIT_CODES.SUCCESS;
|
|
745
|
+
}
|
|
746
|
+
if (args.action === 'workflow-plan') {
|
|
747
|
+
failureLabel = 'Investigation plan';
|
|
748
|
+
const document = buildInvestigationPlan(args.recipe, args.subject, commandContext.now());
|
|
749
|
+
if (!args.quiet)
|
|
750
|
+
write(stdout, args.output === 'json'
|
|
751
|
+
? formatJsonDocument(document)
|
|
752
|
+
: terminal(formatInvestigationPlan(document), args.color));
|
|
753
|
+
return EXIT_CODES.SUCCESS;
|
|
754
|
+
}
|
|
582
755
|
if (args.action === 'diff') {
|
|
583
756
|
failureLabel = 'Lookup evidence diff';
|
|
584
757
|
const readDiffInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
|
|
@@ -605,6 +778,36 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
605
778
|
: terminal(formatCliLookupDiff(document), args.color));
|
|
606
779
|
return EXIT_CODES.SUCCESS;
|
|
607
780
|
}
|
|
781
|
+
if (args.action === 'timeline') {
|
|
782
|
+
failureLabel = 'Lookup observation timeline';
|
|
783
|
+
const readTimelineInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
|
|
784
|
+
limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
|
|
785
|
+
label: 'Lookup timeline input',
|
|
786
|
+
})));
|
|
787
|
+
const inputs = [];
|
|
788
|
+
let totalBytes = 0;
|
|
789
|
+
try {
|
|
790
|
+
for (const source of args.sources) {
|
|
791
|
+
const input = await readTimelineInput(source);
|
|
792
|
+
totalBytes += Buffer.byteLength(input, 'utf8');
|
|
793
|
+
if (totalBytes > MAX_LOOKUP_TIMELINE_INPUT_BYTES) {
|
|
794
|
+
throw new CliUsageError(`Lookup timeline input is limited to ${MAX_LOOKUP_TIMELINE_INPUT_BYTES} bytes in total.`);
|
|
795
|
+
}
|
|
796
|
+
inputs.push(input);
|
|
797
|
+
}
|
|
798
|
+
}
|
|
799
|
+
catch (error) {
|
|
800
|
+
if (error instanceof CliUsageError)
|
|
801
|
+
throw error;
|
|
802
|
+
throw new CliUsageError(`Could not read Lookup timeline input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
803
|
+
}
|
|
804
|
+
const document = buildCliLookupTimeline(inputs, commandContext.now());
|
|
805
|
+
if (!args.quiet)
|
|
806
|
+
write(stdout, args.output === 'json'
|
|
807
|
+
? formatJsonDocument(document)
|
|
808
|
+
: terminal(formatCliLookupTimeline(document), args.color));
|
|
809
|
+
return EXIT_CODES.SUCCESS;
|
|
810
|
+
}
|
|
608
811
|
if (args.action === 'export') {
|
|
609
812
|
failureLabel = 'Evidence export';
|
|
610
813
|
let input;
|