@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/README.md
CHANGED
|
@@ -11,19 +11,35 @@ WHOISleuth requires Node.js 24 or later. Run the reviewed public package without
|
|
|
11
11
|
a global installation:
|
|
12
12
|
|
|
13
13
|
```bash
|
|
14
|
-
npm exec --yes --package=@slicedearth/whoisleuth-cli -- whoisleuth --help
|
|
14
|
+
npm exec --yes --ignore-scripts --package=@slicedearth/whoisleuth-cli -- whoisleuth --help
|
|
15
15
|
```
|
|
16
16
|
|
|
17
17
|
Or install the `whoisleuth` command globally:
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npm install --global @slicedearth/whoisleuth-cli
|
|
20
|
+
npm install --global --ignore-scripts @slicedearth/whoisleuth-cli
|
|
21
21
|
whoisleuth doctor
|
|
22
22
|
```
|
|
23
23
|
|
|
24
|
+
Update an existing global installation and confirm the installed release with:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
npm install --global --ignore-scripts @slicedearth/whoisleuth-cli@latest
|
|
28
|
+
whoisleuth --version
|
|
29
|
+
```
|
|
30
|
+
|
|
24
31
|
The package version follows the application version. Each release is assembled
|
|
25
32
|
from the executable's bounded dependency closure and retains the project
|
|
26
33
|
licence, notices, trademark terms, source location, and provenance metadata.
|
|
34
|
+
The generated package manifest pins each direct runtime dependency to the exact
|
|
35
|
+
version exercised by the reviewed repository lockfile.
|
|
36
|
+
|
|
37
|
+
The package has no install, postinstall, or other lifecycle scripts. Network
|
|
38
|
+
access begins only when an analyst runs a networked command. Offline commands
|
|
39
|
+
may read deliberately selected local evidence or configuration files, but do
|
|
40
|
+
not upload them or invoke a hosted WHOISleuth deployment. The installation
|
|
41
|
+
examples disable lifecycle scripts for the package and its dependency tree;
|
|
42
|
+
WHOISleuth does not require them.
|
|
27
43
|
|
|
28
44
|
## Repository checkout
|
|
29
45
|
|
|
@@ -33,17 +49,24 @@ Contributors can run the same source command without installing the package:
|
|
|
33
49
|
node bin/whoisleuth.mts --help
|
|
34
50
|
node bin/whoisleuth.mts lookup example.test
|
|
35
51
|
node bin/whoisleuth.mts lookup example.test --deep --summary
|
|
52
|
+
node bin/whoisleuth.mts lookup example.test --deep --plan --json
|
|
36
53
|
node bin/whoisleuth.mts lookup example.test --deep --markdown --output lookup.md
|
|
37
54
|
node bin/whoisleuth.mts bulk domains.txt --deep --checkpoint bulk-checkpoint.json
|
|
38
55
|
node bin/whoisleuth.mts bulk domains.txt --csv --registered-only
|
|
56
|
+
node bin/whoisleuth.mts bulk domains.txt --queries --errors-only
|
|
39
57
|
node bin/whoisleuth.mts discover example.test --dictionary private-terms.txt --snapshot discovery-state.json --json
|
|
40
58
|
node bin/whoisleuth.mts discover-scan example.test --scan-limit 50 --checkpoint candidate-scan.json --json
|
|
41
59
|
node bin/whoisleuth.mts page-compare official.json candidate.json --json
|
|
42
60
|
node bin/whoisleuth.mts mail-review bulk.json --json
|
|
61
|
+
node bin/whoisleuth.mts domain-control domain-control-input.json --json
|
|
62
|
+
node bin/whoisleuth.mts workflow-plan domain-triage example.test --json
|
|
43
63
|
node bin/whoisleuth.mts diff first-lookup.json second-lookup.json --json
|
|
64
|
+
node bin/whoisleuth.mts timeline first-observation.json second-observation.json latest-observation.json --json
|
|
44
65
|
node bin/whoisleuth.mts registry-support example.test --json
|
|
45
66
|
node bin/whoisleuth.mts doctor
|
|
67
|
+
node bin/whoisleuth.mts commands --json
|
|
46
68
|
node bin/whoisleuth.mts completion zsh
|
|
69
|
+
node bin/whoisleuth.mts completion powershell
|
|
47
70
|
node bin/whoisleuth.mts manual | man -l -
|
|
48
71
|
```
|
|
49
72
|
|
|
@@ -51,6 +74,13 @@ Fast lookup is the default. Deep collection must be requested explicitly and
|
|
|
51
74
|
can disclose the target to additional authoritative or first-party network
|
|
52
75
|
sources. Missing, partial, rate-limited, and unsupported sources remain
|
|
53
76
|
explicit and are never interpreted as evidence of safety or absence.
|
|
77
|
+
`lookup --plan` classifies a target and explains the selected source families
|
|
78
|
+
and disclosures without making a network request. `commands --json` exposes the
|
|
79
|
+
installed command contracts for local wrappers without running collection.
|
|
80
|
+
Focused command help and the generated manual label every operation as offline
|
|
81
|
+
or networked and state its target, input, and concurrency boundaries. The
|
|
82
|
+
packaged [dual-use disclosure](DISCLOSURE) defines the supported defensive use
|
|
83
|
+
of those capabilities.
|
|
54
84
|
|
|
55
85
|
Interactive output uses restrained semantic colour, width-aware wrapping, and
|
|
56
86
|
stderr-only progress for slower collection. Redirected and machine-readable
|
|
@@ -65,8 +95,15 @@ the partial-result exit code. Ctrl-C suppresses partial final output and
|
|
|
65
95
|
returns exit code 130; a second interrupt performs best-effort temporary-file
|
|
66
96
|
cleanup before exiting immediately.
|
|
67
97
|
|
|
68
|
-
|
|
69
|
-
|
|
98
|
+
The offline `timeline` command orders 2 to 20 saved Lookup documents for one
|
|
99
|
+
domain and compares each adjacent observation. It retains observation times and
|
|
100
|
+
normalized field states but no input filenames or raw registry payloads.
|
|
101
|
+
Observed differences remain separate from missing, unavailable, and changed
|
|
102
|
+
collection conditions.
|
|
103
|
+
|
|
104
|
+
Bulk also supports fixed-column CSV, domain-only or exact-query output,
|
|
105
|
+
registered, inconclusive, and hard-failure output filters, and bounded A, AAAA,
|
|
106
|
+
NS, and MX summaries. Discover
|
|
70
107
|
supports local custom dictionaries and private snapshot comparison suitable
|
|
71
108
|
for an operator-managed scheduler. The separate networked `discover-scan`
|
|
72
109
|
command can collect a deterministic bounded subset in chunks, retain mutation
|
|
@@ -79,6 +116,7 @@ favicon, technology, and TLS evidence, while saved Bulk output can be reviewed
|
|
|
79
116
|
for passive MX, null MX, SPF, DMARC, and shared-provider context.
|
|
80
117
|
|
|
81
118
|
See `docs/cli.md` in the package for the complete command, privacy, output, and
|
|
82
|
-
evidence-contract reference.
|
|
119
|
+
evidence-contract reference. Report suspected vulnerabilities through the
|
|
120
|
+
private channel described in `SECURITY.md`.
|
|
83
121
|
|
|
84
122
|
Copyright 2026 slicedearth. Licensed under AGPL-3.0-only.
|
package/SECURITY.md
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Security policy
|
|
2
|
+
|
|
3
|
+
## Supported versions
|
|
4
|
+
|
|
5
|
+
Security fixes are applied to the latest released WHOISleuth application and
|
|
6
|
+
CLI version. Older releases are not maintained as separate security branches.
|
|
7
|
+
Upgrade to the latest release before reporting a problem that may already have
|
|
8
|
+
been corrected.
|
|
9
|
+
|
|
10
|
+
## Report a vulnerability privately
|
|
11
|
+
|
|
12
|
+
Use [GitHub private vulnerability reporting](https://github.com/slicedearth/whoisleuth/security/advisories/new)
|
|
13
|
+
for suspected vulnerabilities in the application, CLI, release process, or
|
|
14
|
+
deployment defaults. Do not open a public issue for an undisclosed security
|
|
15
|
+
problem.
|
|
16
|
+
|
|
17
|
+
Include the affected version, the smallest reproducible example, expected and
|
|
18
|
+
observed behaviour, and the likely impact. Use reserved or synthetic domains
|
|
19
|
+
and remove credentials, session material, personal information, private case
|
|
20
|
+
data, and raw registration records before submitting evidence.
|
|
21
|
+
|
|
22
|
+
Ordinary defects, feature requests, and questions that do not expose a security
|
|
23
|
+
weakness can use the public issue tracker.
|
|
24
|
+
|
|
25
|
+
## Scope and response
|
|
26
|
+
|
|
27
|
+
Reports are assessed against WHOISleuth's documented trust boundaries,
|
|
28
|
+
including its local-first workspace, bounded network collection, shared
|
|
29
|
+
deployment password, and optional hosted monitoring. A source outage, partial
|
|
30
|
+
registry response, or heuristic disagreement is not by itself a security
|
|
31
|
+
vulnerability, but unsafe handling of those states may be.
|
|
32
|
+
|
|
33
|
+
Confirmed reports will be handled through a private advisory until a fix and
|
|
34
|
+
coordinated disclosure are ready. No bounty or guaranteed response time is
|
|
35
|
+
offered.
|
package/cli/arguments.mjs
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
import { parseInspectArchiveArguments, parseSignArtifactArguments, parseVerifySignatureArguments, } from "./evidence-command-arguments.mjs";
|
|
2
2
|
import { CliUsageError } from "./errors.mjs";
|
|
3
|
+
import { INVESTIGATION_PLAN_RECIPES, } from "./investigation-plan.mjs";
|
|
3
4
|
const MAX_CLI_ARGUMENTS = 32;
|
|
4
5
|
const MAX_CLI_ARGUMENT_LENGTH = 1024;
|
|
5
6
|
const CLI_COMMANDS = [
|
|
6
7
|
'completion',
|
|
7
8
|
'doctor',
|
|
9
|
+
'commands',
|
|
8
10
|
'manual',
|
|
9
11
|
'lookup',
|
|
10
12
|
'bulk',
|
|
@@ -24,7 +26,11 @@ const CLI_COMMANDS = [
|
|
|
24
26
|
'compare',
|
|
25
27
|
'page-compare',
|
|
26
28
|
'mail-review',
|
|
29
|
+
'review-evidence',
|
|
30
|
+
'domain-control',
|
|
31
|
+
'workflow-plan',
|
|
27
32
|
'diff',
|
|
33
|
+
'timeline',
|
|
28
34
|
'export',
|
|
29
35
|
];
|
|
30
36
|
function boundedArgument(value) {
|
|
@@ -108,6 +114,8 @@ function parseCliArgumentsCore(argv) {
|
|
|
108
114
|
return parseCompletionArguments(argv.slice(1));
|
|
109
115
|
if (command === 'doctor')
|
|
110
116
|
return parseDoctorArguments(argv.slice(1));
|
|
117
|
+
if (command === 'commands')
|
|
118
|
+
return parseCommandsArguments(argv.slice(1));
|
|
111
119
|
if (command === 'manual')
|
|
112
120
|
return parseManualArguments(argv.slice(1));
|
|
113
121
|
if (command === 'ct-search')
|
|
@@ -142,8 +150,16 @@ function parseCliArgumentsCore(argv) {
|
|
|
142
150
|
return parsePageCompareArguments(argv.slice(1));
|
|
143
151
|
if (command === 'mail-review')
|
|
144
152
|
return parseMailReviewArguments(argv.slice(1));
|
|
153
|
+
if (command === 'review-evidence')
|
|
154
|
+
return parseReviewEvidenceArguments(argv.slice(1));
|
|
155
|
+
if (command === 'domain-control')
|
|
156
|
+
return parseDomainControlArguments(argv.slice(1));
|
|
157
|
+
if (command === 'workflow-plan')
|
|
158
|
+
return parseWorkflowPlanArguments(argv.slice(1));
|
|
145
159
|
if (command === 'diff')
|
|
146
160
|
return parseDiffArguments(argv.slice(1));
|
|
161
|
+
if (command === 'timeline')
|
|
162
|
+
return parseTimelineArguments(argv.slice(1));
|
|
147
163
|
if (command === 'export')
|
|
148
164
|
return parseExportArguments(argv.slice(1));
|
|
149
165
|
let query = null;
|
|
@@ -156,6 +172,7 @@ function parseCliArgumentsCore(argv) {
|
|
|
156
172
|
let detailSet = false;
|
|
157
173
|
let strictExit = false;
|
|
158
174
|
let events = false;
|
|
175
|
+
let plan = false;
|
|
159
176
|
for (const argument of argv.slice(1)) {
|
|
160
177
|
if (argument === '--json') {
|
|
161
178
|
if (output !== 'terminal')
|
|
@@ -195,6 +212,11 @@ function parseCliArgumentsCore(argv) {
|
|
|
195
212
|
throw new CliUsageError('--events may be supplied only once.');
|
|
196
213
|
events = true;
|
|
197
214
|
}
|
|
215
|
+
else if (argument === '--plan') {
|
|
216
|
+
if (plan)
|
|
217
|
+
throw new CliUsageError('--plan may be supplied only once.');
|
|
218
|
+
plan = true;
|
|
219
|
+
}
|
|
198
220
|
else if (argument === '--quiet')
|
|
199
221
|
quiet = true;
|
|
200
222
|
else if (argument === '--no-color')
|
|
@@ -210,11 +232,16 @@ function parseCliArgumentsCore(argv) {
|
|
|
210
232
|
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
211
233
|
if (detailSet && output !== 'terminal')
|
|
212
234
|
throw new CliUsageError('--summary and --verbose apply only to terminal output.');
|
|
213
|
-
|
|
235
|
+
if (plan && (output === 'markdown' || output === 'html'))
|
|
236
|
+
throw new CliUsageError('--plan supports terminal or JSON output only.');
|
|
237
|
+
if (plan && (detailSet || strictExit || events || quiet)) {
|
|
238
|
+
throw new CliUsageError('--plan cannot be combined with detail, strict-exit, event, or quiet options.');
|
|
239
|
+
}
|
|
240
|
+
return { action: 'lookup', query, output, deep, detail, strictExit, events, plan, quiet, color };
|
|
214
241
|
}
|
|
215
242
|
function parseCompletionArguments(argv) {
|
|
216
|
-
if (argv.length !== 1 || !['bash', 'zsh', 'fish'].includes(argv[0] || '')) {
|
|
217
|
-
throw new CliUsageError('completion requires exactly one shell: bash, zsh, or
|
|
243
|
+
if (argv.length !== 1 || !['bash', 'zsh', 'fish', 'powershell'].includes(argv[0] || '')) {
|
|
244
|
+
throw new CliUsageError('completion requires exactly one shell: bash, zsh, fish, or powershell.');
|
|
218
245
|
}
|
|
219
246
|
return { action: 'completion', shell: argv[0] };
|
|
220
247
|
}
|
|
@@ -223,6 +250,27 @@ function parseManualArguments(argv) {
|
|
|
223
250
|
throw new CliUsageError('manual does not accept command arguments.');
|
|
224
251
|
return { action: 'manual' };
|
|
225
252
|
}
|
|
253
|
+
function parseCommandsArguments(argv) {
|
|
254
|
+
let output = 'terminal';
|
|
255
|
+
let quiet = false;
|
|
256
|
+
let color = true;
|
|
257
|
+
for (const argument of argv) {
|
|
258
|
+
if (argument === '--json') {
|
|
259
|
+
if (output !== 'terminal')
|
|
260
|
+
throw new CliUsageError('--json may be supplied only once.');
|
|
261
|
+
output = 'json';
|
|
262
|
+
}
|
|
263
|
+
else if (argument === '--quiet')
|
|
264
|
+
quiet = true;
|
|
265
|
+
else if (argument === '--no-color')
|
|
266
|
+
color = false;
|
|
267
|
+
else
|
|
268
|
+
throw new CliUsageError(`Unknown option "${argument}".`);
|
|
269
|
+
}
|
|
270
|
+
if (quiet && output !== 'terminal')
|
|
271
|
+
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
272
|
+
return { action: 'commands', output, quiet, color };
|
|
273
|
+
}
|
|
226
274
|
function parseDoctorArguments(argv) {
|
|
227
275
|
let network = false;
|
|
228
276
|
let output = 'terminal';
|
|
@@ -266,10 +314,18 @@ function parseBulkArguments(argv) {
|
|
|
266
314
|
const argument = argv[index];
|
|
267
315
|
if (argument === undefined)
|
|
268
316
|
break;
|
|
269
|
-
if (argument === '--json' || argument === '--jsonl' || argument === '--csv' || argument === '--domains') {
|
|
317
|
+
if (argument === '--json' || argument === '--jsonl' || argument === '--csv' || argument === '--domains' || argument === '--queries') {
|
|
270
318
|
if (output !== 'terminal')
|
|
271
319
|
throw new CliUsageError('Choose only one output format.');
|
|
272
|
-
output = argument === '--json'
|
|
320
|
+
output = argument === '--json'
|
|
321
|
+
? 'json'
|
|
322
|
+
: argument === '--jsonl'
|
|
323
|
+
? 'jsonl'
|
|
324
|
+
: argument === '--csv'
|
|
325
|
+
? 'csv'
|
|
326
|
+
: argument === '--domains'
|
|
327
|
+
? 'domains'
|
|
328
|
+
: 'queries';
|
|
273
329
|
}
|
|
274
330
|
else if (argument === '--deep' || argument === '--fast') {
|
|
275
331
|
if (scanMode)
|
|
@@ -305,10 +361,14 @@ function parseBulkArguments(argv) {
|
|
|
305
361
|
throw new CliUsageError('--events may be supplied only once.');
|
|
306
362
|
events = true;
|
|
307
363
|
}
|
|
308
|
-
else if (argument === '--registered-only' || argument === '--inconclusive-only') {
|
|
364
|
+
else if (argument === '--registered-only' || argument === '--inconclusive-only' || argument === '--errors-only') {
|
|
309
365
|
if (filter !== 'all')
|
|
310
|
-
throw new CliUsageError('
|
|
311
|
-
filter = argument === '--registered-only'
|
|
366
|
+
throw new CliUsageError('Bulk output filters are mutually exclusive and may be supplied only once.');
|
|
367
|
+
filter = argument === '--registered-only'
|
|
368
|
+
? 'registered'
|
|
369
|
+
: argument === '--inconclusive-only'
|
|
370
|
+
? 'inconclusive'
|
|
371
|
+
: 'errors';
|
|
312
372
|
}
|
|
313
373
|
else if (argument === '--quiet')
|
|
314
374
|
quiet = true;
|
|
@@ -843,6 +903,88 @@ function parseMailReviewArguments(argv) {
|
|
|
843
903
|
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
844
904
|
return { action: 'mail-review', source, output, quiet, color };
|
|
845
905
|
}
|
|
906
|
+
function parseReviewEvidenceArguments(argv) {
|
|
907
|
+
let source = null;
|
|
908
|
+
let output = 'terminal';
|
|
909
|
+
let quiet = false;
|
|
910
|
+
let color = true;
|
|
911
|
+
for (const argument of argv) {
|
|
912
|
+
if (argument === '--json') {
|
|
913
|
+
if (output !== 'terminal')
|
|
914
|
+
throw new CliUsageError('--json may be supplied only once.');
|
|
915
|
+
output = 'json';
|
|
916
|
+
}
|
|
917
|
+
else if (argument === '--quiet')
|
|
918
|
+
quiet = true;
|
|
919
|
+
else if (argument === '--no-color')
|
|
920
|
+
color = false;
|
|
921
|
+
else if (argument.startsWith('-'))
|
|
922
|
+
throw new CliUsageError(`Unknown option "${argument}".`);
|
|
923
|
+
else if (source === null)
|
|
924
|
+
source = argument;
|
|
925
|
+
else
|
|
926
|
+
throw new CliUsageError('review-evidence accepts one optional versioned JSON input file.');
|
|
927
|
+
}
|
|
928
|
+
if (quiet && output !== 'terminal')
|
|
929
|
+
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
930
|
+
return { action: 'review-evidence', source, output, quiet, color };
|
|
931
|
+
}
|
|
932
|
+
function parseDomainControlArguments(argv) {
|
|
933
|
+
let source = null;
|
|
934
|
+
let output = 'terminal';
|
|
935
|
+
let quiet = false;
|
|
936
|
+
let color = true;
|
|
937
|
+
for (const argument of argv) {
|
|
938
|
+
if (argument === '--json') {
|
|
939
|
+
if (output !== 'terminal')
|
|
940
|
+
throw new CliUsageError('--json may be supplied only once.');
|
|
941
|
+
output = 'json';
|
|
942
|
+
}
|
|
943
|
+
else if (argument === '--quiet')
|
|
944
|
+
quiet = true;
|
|
945
|
+
else if (argument === '--no-color')
|
|
946
|
+
color = false;
|
|
947
|
+
else if (argument.startsWith('-'))
|
|
948
|
+
throw new CliUsageError(`Unknown option "${argument}".`);
|
|
949
|
+
else if (source === null)
|
|
950
|
+
source = argument;
|
|
951
|
+
else
|
|
952
|
+
throw new CliUsageError('domain-control accepts one optional versioned JSON input file.');
|
|
953
|
+
}
|
|
954
|
+
if (quiet && output !== 'terminal')
|
|
955
|
+
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
956
|
+
return { action: 'domain-control', source, output, quiet, color };
|
|
957
|
+
}
|
|
958
|
+
function parseWorkflowPlanArguments(argv) {
|
|
959
|
+
const positional = [];
|
|
960
|
+
let output = 'terminal';
|
|
961
|
+
let quiet = false;
|
|
962
|
+
let color = true;
|
|
963
|
+
for (const argument of argv) {
|
|
964
|
+
if (argument === '--json') {
|
|
965
|
+
if (output !== 'terminal')
|
|
966
|
+
throw new CliUsageError('--json may be supplied only once.');
|
|
967
|
+
output = 'json';
|
|
968
|
+
}
|
|
969
|
+
else if (argument === '--quiet')
|
|
970
|
+
quiet = true;
|
|
971
|
+
else if (argument === '--no-color')
|
|
972
|
+
color = false;
|
|
973
|
+
else if (argument.startsWith('-'))
|
|
974
|
+
throw new CliUsageError(`Unknown option "${argument}".`);
|
|
975
|
+
else
|
|
976
|
+
positional.push(argument);
|
|
977
|
+
}
|
|
978
|
+
if (positional.length !== 2)
|
|
979
|
+
throw new CliUsageError('workflow-plan requires one fixed recipe and one subject.');
|
|
980
|
+
const recipe = positional[0];
|
|
981
|
+
if (!INVESTIGATION_PLAN_RECIPES.includes(recipe)) {
|
|
982
|
+
throw new CliUsageError(`workflow-plan recipe must be one of: ${INVESTIGATION_PLAN_RECIPES.join(', ')}.`);
|
|
983
|
+
}
|
|
984
|
+
if (quiet && output !== 'terminal')
|
|
985
|
+
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
986
|
+
return { action: 'workflow-plan', recipe: recipe, subject: positional[1], output, quiet, color };
|
|
987
|
+
}
|
|
846
988
|
function parseDiffArguments(argv) {
|
|
847
989
|
let leftSource = null;
|
|
848
990
|
let rightSource = null;
|
|
@@ -876,6 +1018,36 @@ function parseDiffArguments(argv) {
|
|
|
876
1018
|
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
877
1019
|
return { action: 'diff', leftSource, rightSource, output, quiet, color };
|
|
878
1020
|
}
|
|
1021
|
+
function parseTimelineArguments(argv) {
|
|
1022
|
+
const sources = [];
|
|
1023
|
+
let output = 'terminal';
|
|
1024
|
+
let quiet = false;
|
|
1025
|
+
let color = true;
|
|
1026
|
+
for (const argument of argv) {
|
|
1027
|
+
if (argument === '--json') {
|
|
1028
|
+
if (output !== 'terminal')
|
|
1029
|
+
throw new CliUsageError('--json may be supplied only once.');
|
|
1030
|
+
output = 'json';
|
|
1031
|
+
}
|
|
1032
|
+
else if (argument === '--quiet')
|
|
1033
|
+
quiet = true;
|
|
1034
|
+
else if (argument === '--no-color')
|
|
1035
|
+
color = false;
|
|
1036
|
+
else if (argument.startsWith('-'))
|
|
1037
|
+
throw new CliUsageError(`Unknown option "${argument}".`);
|
|
1038
|
+
else
|
|
1039
|
+
sources.push(argument);
|
|
1040
|
+
}
|
|
1041
|
+
if (sources.length < 2 || sources.length > 20) {
|
|
1042
|
+
throw new CliUsageError('timeline requires from 2 to 20 saved lookup JSON files.');
|
|
1043
|
+
}
|
|
1044
|
+
if (new Set(sources).size !== sources.length) {
|
|
1045
|
+
throw new CliUsageError('timeline input files must be different.');
|
|
1046
|
+
}
|
|
1047
|
+
if (quiet && output !== 'terminal')
|
|
1048
|
+
throw new CliUsageError('--quiet cannot be combined with machine-readable output.');
|
|
1049
|
+
return { action: 'timeline', sources, output, quiet, color };
|
|
1050
|
+
}
|
|
879
1051
|
function parseRegistrySupportArguments(argv) {
|
|
880
1052
|
let target = null;
|
|
881
1053
|
let output = 'terminal';
|
package/cli/artifact-verify.mjs
CHANGED
|
@@ -7,6 +7,8 @@ import { BULK_REVIEW_MANIFEST_SCHEMA, BULK_REVIEW_MANIFEST_VERSION, } from "../f
|
|
|
7
7
|
import { decryptWorkspaceArchive, inspectEncryptedWorkspaceArchive, isEncryptedWorkspaceArchive, } from "../frontend/src/lib/analysis/workspace-archive-crypto.js";
|
|
8
8
|
import { WORKSPACE_ARCHIVE_SCHEMA, readWorkspaceArchive, } from "../frontend/src/lib/analysis/workspace-archive.js";
|
|
9
9
|
import { sha256ArtifactDigest } from "../frontend/src/lib/analysis/artifact-integrity.js";
|
|
10
|
+
import { SAVED_LOOKUP_SCHEMA, SAVED_LOOKUP_SCHEMA_VERSION, parseSavedLookupDocument, } from "./saved-lookup.mjs";
|
|
11
|
+
import { DOMAIN_CONTROL_MANIFEST_SCHEMA, DOMAIN_CONTROL_MANIFEST_VERSION, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
10
12
|
export const OFFLINE_ARTIFACT_VERIFICATION_SCHEMA = 'whoisleuth.offline-artifact-verification';
|
|
11
13
|
export const OFFLINE_ARTIFACT_VERIFICATION_VERSION = 1;
|
|
12
14
|
export const MAX_OFFLINE_ARTIFACT_BYTES = 15 * 1024 * 1024;
|
|
@@ -16,6 +18,7 @@ const SIGNED_ARTIFACT_VERSIONS = Object.freeze({
|
|
|
16
18
|
[BULK_DOMAIN_COMPARISON_SCHEMA]: new Set([BULK_DOMAIN_COMPARISON_VERSION]),
|
|
17
19
|
[BULK_MAIL_EXPOSURE_SCHEMA]: new Set([BULK_MAIL_EXPOSURE_VERSION]),
|
|
18
20
|
[BULK_REVIEW_MANIFEST_SCHEMA]: new Set([BULK_REVIEW_MANIFEST_VERSION]),
|
|
21
|
+
[DOMAIN_CONTROL_MANIFEST_SCHEMA]: new Set([DOMAIN_CONTROL_MANIFEST_VERSION]),
|
|
19
22
|
});
|
|
20
23
|
function record(value) {
|
|
21
24
|
return value && typeof value === 'object' && !Array.isArray(value)
|
|
@@ -93,6 +96,8 @@ async function verifySignedArtifact(raw, value, schema, version) {
|
|
|
93
96
|
if (await sha256ArtifactDigest(unsigned) !== integrity.digestSha256) {
|
|
94
97
|
throw new TypeError('The signed review artifact failed its SHA-256 integrity check.');
|
|
95
98
|
}
|
|
99
|
+
if (schema === DOMAIN_CONTROL_MANIFEST_SCHEMA)
|
|
100
|
+
verifyDomainControlManifest(value);
|
|
96
101
|
return Object.freeze({
|
|
97
102
|
schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
|
|
98
103
|
version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
|
|
@@ -186,6 +191,33 @@ export async function verifyOfflineArtifact(raw, options = {}) {
|
|
|
186
191
|
]),
|
|
187
192
|
});
|
|
188
193
|
}
|
|
194
|
+
if (schema === SAVED_LOOKUP_SCHEMA) {
|
|
195
|
+
if (version !== SAVED_LOOKUP_SCHEMA_VERSION) {
|
|
196
|
+
throw new TypeError('This saved Lookup document version is not supported.');
|
|
197
|
+
}
|
|
198
|
+
const document = parseSavedLookupDocument(raw, { label: 'Saved Lookup artifact' });
|
|
199
|
+
return Object.freeze({
|
|
200
|
+
schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
|
|
201
|
+
version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
|
|
202
|
+
artifact: Object.freeze({ kind: 'saved_lookup', schema, version }),
|
|
203
|
+
state: 'structure_valid',
|
|
204
|
+
valid: true,
|
|
205
|
+
checks: Object.freeze({
|
|
206
|
+
structure: 'verified',
|
|
207
|
+
contentIntegrity: 'not_checked',
|
|
208
|
+
authenticatedEncryption: 'not_applicable',
|
|
209
|
+
}),
|
|
210
|
+
summary: Object.freeze({
|
|
211
|
+
inputBytes: inputBytes(raw),
|
|
212
|
+
sectionCount: null,
|
|
213
|
+
recordCount: null,
|
|
214
|
+
ciphertextBytes: null,
|
|
215
|
+
}),
|
|
216
|
+
limitations: Object.freeze([
|
|
217
|
+
`The saved ${document.mode} Lookup matches its versioned structural contract, but this document format has no embedded checksum or signature. Structural validity does not prove that its evidence is accurate, current, or unchanged since collection.`,
|
|
218
|
+
]),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
189
221
|
return verifySignedArtifact(raw, value, schema, version);
|
|
190
222
|
}
|
|
191
223
|
export function formatOfflineArtifactVerification(report) {
|
|
@@ -3,7 +3,7 @@ import { classifyQuery } from "../lib/classify.mjs";
|
|
|
3
3
|
import { runUnifiedLookup } from "../lib/lookup.mjs";
|
|
4
4
|
import { createBulkCheckpointWriter } from "./bulk-checkpoint.mjs";
|
|
5
5
|
import { MAX_BULK_INPUT_BYTES, parseBulkQueries, readTextStreamBounded, runBulkLookups, } from "./bulk.mjs";
|
|
6
|
-
import { formatBulkCsv, formatBulkDomainList, selectBulkItems } from "./bulk-output.mjs";
|
|
6
|
+
import { formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems } from "./bulk-output.mjs";
|
|
7
7
|
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
8
8
|
import EXIT_CODES from "./exit-codes.mjs";
|
|
9
9
|
import { buildCliBulkDocument, formatJsonDocument, formatJsonLines } from "./formatters/json.mjs";
|
|
@@ -91,6 +91,8 @@ async function runBulkCommand(args, dependencies, context) {
|
|
|
91
91
|
context.writeStdout(formatBulkCsv(selectedItems));
|
|
92
92
|
else if (args.output === 'domains')
|
|
93
93
|
context.writeStdout(formatBulkDomainList(selectedItems));
|
|
94
|
+
else if (args.output === 'queries')
|
|
95
|
+
context.writeStdout(formatBulkQueryList(selectedItems));
|
|
94
96
|
else
|
|
95
97
|
context.writeStdout(context.terminal(formatTerminalBulk(selectedItems, metadata), args.color));
|
|
96
98
|
}
|
package/cli/bulk-output.mjs
CHANGED
|
@@ -18,6 +18,8 @@ function selectBulkItems(items, filter) {
|
|
|
18
18
|
if (filter === 'all')
|
|
19
19
|
return [...items];
|
|
20
20
|
return items.filter((item) => {
|
|
21
|
+
if (filter === 'errors')
|
|
22
|
+
return !item.ok;
|
|
21
23
|
if (!item.ok)
|
|
22
24
|
return filter === 'inconclusive';
|
|
23
25
|
const state = availabilityState(item);
|
|
@@ -109,4 +111,7 @@ function formatBulkDomainList(items) {
|
|
|
109
111
|
: []);
|
|
110
112
|
return values.length ? `${[...new Set(values)].join('\n')}\n` : '';
|
|
111
113
|
}
|
|
112
|
-
|
|
114
|
+
function formatBulkQueryList(items) {
|
|
115
|
+
return items.length ? `${items.map((item) => item.query).join('\n')}\n` : '';
|
|
116
|
+
}
|
|
117
|
+
export { REGISTERED_STATES, availabilityState, bulkDnsSummary, formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems, };
|
package/cli/bulk.mjs
CHANGED
|
@@ -1,9 +1,8 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
2
|
import { abortable } from "../lib/abort.mjs";
|
|
3
3
|
import { CliUsageError } from "./arguments.mjs";
|
|
4
|
+
import { MAX_DEEP_BULK_QUERIES, MAX_FAST_BULK_QUERIES, } from "../lib/bulk-limits.mjs";
|
|
4
5
|
const MAX_BULK_INPUT_BYTES = 1024 * 1024;
|
|
5
|
-
const MAX_FAST_BULK_QUERIES = 500;
|
|
6
|
-
const MAX_DEEP_BULK_QUERIES = 50;
|
|
7
6
|
async function readTextStreamBounded(stream, limit = MAX_BULK_INPUT_BYTES) {
|
|
8
7
|
if (!stream || stream.isTTY)
|
|
9
8
|
return '';
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const CLI_COMMAND_CATALOGUE_SCHEMA = 'whoisleuth.cli.command-catalogue';
|
|
2
|
+
export const CLI_COMMAND_CATALOGUE_VERSION = 1;
|
|
3
|
+
function buildCliCommandCatalogue(options) {
|
|
4
|
+
return Object.freeze({
|
|
5
|
+
schema: CLI_COMMAND_CATALOGUE_SCHEMA,
|
|
6
|
+
version: CLI_COMMAND_CATALOGUE_VERSION,
|
|
7
|
+
packageVersion: options.packageVersion,
|
|
8
|
+
commands: Object.freeze(options.commands.map((command) => {
|
|
9
|
+
const detail = options.details[command];
|
|
10
|
+
return Object.freeze({
|
|
11
|
+
command,
|
|
12
|
+
description: detail.description,
|
|
13
|
+
usage: options.usage[command],
|
|
14
|
+
example: detail.example,
|
|
15
|
+
collection: options.collections[command],
|
|
16
|
+
boundary: detail.boundary,
|
|
17
|
+
});
|
|
18
|
+
})),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
function formatCliCommandCatalogue(catalogue) {
|
|
22
|
+
const lines = [
|
|
23
|
+
`WHOISleuth CLI ${catalogue.packageVersion}`,
|
|
24
|
+
'Command catalogue',
|
|
25
|
+
'',
|
|
26
|
+
];
|
|
27
|
+
for (const entry of catalogue.commands) {
|
|
28
|
+
lines.push(`${entry.command} [${entry.collection.mode}]`, ` ${entry.description}`, ` ${entry.usage}`);
|
|
29
|
+
}
|
|
30
|
+
lines.push('', 'Run "whoisleuth <command> --help" for examples and collection boundaries.');
|
|
31
|
+
return `${lines.join('\n')}\n`;
|
|
32
|
+
}
|
|
33
|
+
export { buildCliCommandCatalogue, formatCliCommandCatalogue };
|