@slicedearth/whoisleuth-cli 1.32.2 → 1.34.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 +41 -5
- package/SECURITY.md +35 -0
- package/cli/arguments.mjs +88 -8
- package/cli/artifact-verify.mjs +28 -0
- package/cli/bulk-command-runner.mjs +3 -1
- package/cli/bulk-output.mjs +6 -1
- package/cli/command-catalogue.mjs +33 -0
- package/cli/completion.mjs +51 -7
- package/cli/doctor.mjs +78 -43
- package/cli/formatters/html.mjs +1 -1
- package/cli/formatters/terminal.mjs +6 -6
- 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/manual.mjs +2 -1
- package/cli/runner.mjs +105 -10
- package/docs/cli.md +100 -38
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -6
- package/lib/csv.mjs +1 -1
- package/lib/evidence-report-markdown.mjs +3 -3
- package/package.json +6 -4
- 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,6 +23,7 @@ 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";
|
|
27
29
|
import { runLookupCommand } from "./lookup-command-runner.mjs";
|
|
@@ -65,9 +67,10 @@ Review saved evidence:
|
|
|
65
67
|
page-compare Compare saved static page and TLS evidence.
|
|
66
68
|
mail-review Review saved passive mail exposure evidence.
|
|
67
69
|
diff Compare two saved domain observations.
|
|
70
|
+
timeline Compare a sequence of observations for one domain.
|
|
68
71
|
export Convert a saved lookup into an evidence report.
|
|
69
72
|
inspect-archive Inspect a workspace archive, redacted by default.
|
|
70
|
-
verify-artifact Validate
|
|
73
|
+
verify-artifact Validate saved evidence or an integrity envelope offline.
|
|
71
74
|
|
|
72
75
|
Integrity and calibration:
|
|
73
76
|
sign-artifact Sign one reviewed packet or manifest locally.
|
|
@@ -76,7 +79,8 @@ Integrity and calibration:
|
|
|
76
79
|
|
|
77
80
|
Terminal:
|
|
78
81
|
doctor Check the local runtime; network tests require --network.
|
|
79
|
-
|
|
82
|
+
commands List command contracts for people or local tooling.
|
|
83
|
+
completion Print completion for bash, zsh, fish, or PowerShell.
|
|
80
84
|
manual Print the generated manual page.
|
|
81
85
|
|
|
82
86
|
Run "whoisleuth <command> --help" for focused usage and an example.
|
|
@@ -89,11 +93,12 @@ Copyright 2026 slicedearth. Licensed under AGPL-3.0-only.
|
|
|
89
93
|
Source and licence: https://github.com/slicedearth/whoisleuth
|
|
90
94
|
`;
|
|
91
95
|
const COMMAND_USAGE = Object.freeze({
|
|
92
|
-
completion: 'whoisleuth completion <bash|zsh|fish>',
|
|
96
|
+
completion: 'whoisleuth completion <bash|zsh|fish|powershell>',
|
|
93
97
|
doctor: 'whoisleuth doctor [--network] [--json] [--quiet] [--no-color]',
|
|
98
|
+
commands: 'whoisleuth commands [--json] [--quiet] [--no-color]',
|
|
94
99
|
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]',
|
|
100
|
+
lookup: 'whoisleuth lookup <domain|IP|ASN> [--json|--markdown|--html] [--fast|--deep] [--plan] [--summary|--verbose] [--strict-exit] [--events] [--quiet] [--no-color]',
|
|
101
|
+
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
102
|
'ct-search': 'whoisleuth ct-search <keyword> [--json] [--quiet] [--no-color]',
|
|
98
103
|
discover: 'whoisleuth discover <brand|domain> [--tlds <list>] [--preset <name>|--families <ids>] [--keyboard <layout>] [--dictionary <file>] [--snapshot <file>] [--json|--jsonl|--domains]',
|
|
99
104
|
'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]',
|
|
@@ -111,6 +116,7 @@ const COMMAND_USAGE = Object.freeze({
|
|
|
111
116
|
'page-compare': 'whoisleuth page-compare <left.json> <right.json> [--json] [--quiet] [--no-color]',
|
|
112
117
|
'mail-review': 'whoisleuth mail-review [bulk.json|bulk.jsonl] [--json] [--quiet] [--no-color]',
|
|
113
118
|
diff: 'whoisleuth diff <left.json> <right.json> [--json] [--quiet] [--no-color]',
|
|
119
|
+
timeline: 'whoisleuth timeline <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
|
|
114
120
|
export: 'whoisleuth export [lookup.json] [--markdown|--html|--compact]',
|
|
115
121
|
});
|
|
116
122
|
const COMMAND_DETAILS = Object.freeze({
|
|
@@ -124,6 +130,11 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
124
130
|
example: 'whoisleuth doctor --json',
|
|
125
131
|
boundary: 'The default check is offline. Public DNS and port 43 checks run only when --network is explicitly supplied.',
|
|
126
132
|
},
|
|
133
|
+
commands: {
|
|
134
|
+
description: 'List the installed command contracts in terminal or versioned JSON form.',
|
|
135
|
+
example: 'whoisleuth commands --json',
|
|
136
|
+
boundary: 'Catalogue generation is offline. It reports declared command modes and limits without executing collection or inspecting local evidence.',
|
|
137
|
+
},
|
|
127
138
|
manual: {
|
|
128
139
|
description: 'Print a generated roff manual page for local installation.',
|
|
129
140
|
example: 'whoisleuth manual | man -l -',
|
|
@@ -180,7 +191,7 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
180
191
|
boundary: 'Calibration is offline and diagnostic. It never trains, tunes, or changes the scoring model automatically.',
|
|
181
192
|
},
|
|
182
193
|
'verify-artifact': {
|
|
183
|
-
description: 'Validate a supported archive, packet, or
|
|
194
|
+
description: 'Validate a supported archive, packet, manifest, or saved Lookup without printing evidence contents.',
|
|
184
195
|
example: 'whoisleuth verify-artifact workspace.json --json',
|
|
185
196
|
boundary: 'Verification is offline and redacted. Encrypted archives require an explicitly supplied passphrase file.',
|
|
186
197
|
},
|
|
@@ -224,15 +235,48 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
224
235
|
example: 'whoisleuth diff first.json second.json --json',
|
|
225
236
|
boundary: 'Comparison is offline. Missing, unavailable, equal, and different evidence remain separate states.',
|
|
226
237
|
},
|
|
238
|
+
timeline: {
|
|
239
|
+
description: 'Build an ordered same-domain history from saved Lookup observations.',
|
|
240
|
+
example: 'whoisleuth timeline first.json second.json latest.json --json',
|
|
241
|
+
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.',
|
|
242
|
+
},
|
|
227
243
|
export: {
|
|
228
244
|
description: 'Convert one saved lookup into a versioned evidence report.',
|
|
229
245
|
example: 'whoisleuth export lookup.json --markdown',
|
|
230
246
|
boundary: 'Exports preserve source attribution and limitations. Compact output intentionally omits raw registry payloads.',
|
|
231
247
|
},
|
|
232
248
|
});
|
|
249
|
+
const COMMAND_COLLECTION = Object.freeze({
|
|
250
|
+
completion: { mode: 'offline', scope: 'Prints one static script and changes no shell configuration.' },
|
|
251
|
+
doctor: { mode: 'network', scope: 'Network access is opt-in with --network and is limited to fixed public DNS, HTTPS, and WHOIS diagnostics.' },
|
|
252
|
+
commands: { mode: 'offline', scope: 'Reads the embedded command catalogue and performs no collection.' },
|
|
253
|
+
manual: { mode: 'offline', scope: 'Builds documentation from the embedded command catalogue.' },
|
|
254
|
+
lookup: { mode: 'network', scope: 'Accepts one target. Fast is the default; deep collection must be selected explicitly.' },
|
|
255
|
+
bulk: { mode: 'network', scope: 'Accepts at most 500 fast or 50 deep targets, with concurrency capped at 8 fast or 3 deep.' },
|
|
256
|
+
'ct-search': { mode: 'network', scope: 'Accepts one bounded search keyword and queries the fixed certificate-transparency source.' },
|
|
257
|
+
discover: { mode: 'offline', scope: 'Generates a bounded candidate set from local rules, dictionaries, and optional saved snapshots.' },
|
|
258
|
+
'discover-scan': { mode: 'network', scope: 'Scans at most 500 fast or 50 deep candidates, with concurrency capped at 8 fast or 3 deep.' },
|
|
259
|
+
posture: { mode: 'network', scope: 'Accepts one domain and performs bounded DNS queries only.' },
|
|
260
|
+
http: { mode: 'network', scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
|
|
261
|
+
tls: { mode: 'network', scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
|
|
262
|
+
'registry-support': { mode: 'offline', scope: 'Reads the embedded registry capability catalogue for one domain or suffix.' },
|
|
263
|
+
'risk-calibrate': { mode: 'offline', scope: 'Reads one bounded reviewed-label dataset and changes no model or evidence.' },
|
|
264
|
+
'verify-artifact': { mode: 'offline', scope: 'Reads one selected bounded archive, packet, manifest, or saved Lookup document.' },
|
|
265
|
+
'inspect-archive': { mode: 'offline', scope: 'Reads one selected bounded workspace archive with redacted output by default.' },
|
|
266
|
+
'sign-artifact': { mode: 'offline', scope: 'Reads one selected artifact and one local private key without transmitting either.' },
|
|
267
|
+
'verify-signature': { mode: 'offline', scope: 'Reads one selected signed package and optional local public key.' },
|
|
268
|
+
'source-report': { mode: 'offline', scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
|
|
269
|
+
compare: { mode: 'offline', scope: 'Reads one saved Lookup and compares its separately attributed registry publications.' },
|
|
270
|
+
'page-compare': { mode: 'offline', scope: 'Reads two saved Lookup documents and executes no page code.' },
|
|
271
|
+
'mail-review': { mode: 'offline', scope: 'Reads one saved Bulk result and sends no DNS or SMTP traffic.' },
|
|
272
|
+
diff: { mode: 'offline', scope: 'Reads two saved Lookup documents for different domains.' },
|
|
273
|
+
timeline: { mode: 'offline', scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
|
|
274
|
+
export: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
|
|
275
|
+
});
|
|
233
276
|
function commandHelp(command) {
|
|
234
277
|
const detail = COMMAND_DETAILS[command];
|
|
235
|
-
|
|
278
|
+
const collection = COMMAND_COLLECTION[command];
|
|
279
|
+
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
280
|
}
|
|
237
281
|
async function readStdinBounded(stream, limit = MAX_STDIN_BYTES) {
|
|
238
282
|
if (!stream || stream.isTTY)
|
|
@@ -341,8 +385,28 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
341
385
|
write(stdout, buildShellCompletion(args.shell));
|
|
342
386
|
return EXIT_CODES.SUCCESS;
|
|
343
387
|
}
|
|
388
|
+
if (args.action === 'commands') {
|
|
389
|
+
const catalogue = buildCliCommandCatalogue({
|
|
390
|
+
commands: CLI_COMMANDS,
|
|
391
|
+
collections: COMMAND_COLLECTION,
|
|
392
|
+
details: COMMAND_DETAILS,
|
|
393
|
+
usage: COMMAND_USAGE,
|
|
394
|
+
packageVersion: VERSION,
|
|
395
|
+
});
|
|
396
|
+
if (!args.quiet)
|
|
397
|
+
write(stdout, args.output === 'json'
|
|
398
|
+
? formatJsonDocument(catalogue)
|
|
399
|
+
: terminal(formatCliCommandCatalogue(catalogue), args.color));
|
|
400
|
+
return EXIT_CODES.SUCCESS;
|
|
401
|
+
}
|
|
344
402
|
if (args.action === 'manual') {
|
|
345
|
-
write(stdout, buildCliManual({
|
|
403
|
+
write(stdout, buildCliManual({
|
|
404
|
+
commands: CLI_COMMANDS,
|
|
405
|
+
collections: COMMAND_COLLECTION,
|
|
406
|
+
details: COMMAND_DETAILS,
|
|
407
|
+
usage: COMMAND_USAGE,
|
|
408
|
+
version: VERSION,
|
|
409
|
+
}));
|
|
346
410
|
return EXIT_CODES.SUCCESS;
|
|
347
411
|
}
|
|
348
412
|
if (args.action === 'doctor') {
|
|
@@ -353,10 +417,11 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
353
417
|
network: args.network,
|
|
354
418
|
presentation: terminalPresentation(stdout, args.color, environment),
|
|
355
419
|
...(dependencies.resolvePublicAddresses ? { resolveAddresses: dependencies.resolvePublicAddresses } : {}),
|
|
420
|
+
...(dependencies.safeFetch ? { fetchHttps: dependencies.safeFetch } : { fetchHttps: safeFetch }),
|
|
356
421
|
...(dependencies.whoisQuery ? { queryWhois: dependencies.whoisQuery } : {}),
|
|
357
422
|
});
|
|
358
423
|
const report = args.network
|
|
359
|
-
? await withProgress('Checking public DNS and WHOIS connectivity', buildReport)
|
|
424
|
+
? await withProgress('Checking public DNS, HTTPS, and WHOIS connectivity', buildReport)
|
|
360
425
|
: await buildReport();
|
|
361
426
|
if (!args.quiet) {
|
|
362
427
|
write(stdout, args.output === 'json'
|
|
@@ -605,6 +670,36 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
605
670
|
: terminal(formatCliLookupDiff(document), args.color));
|
|
606
671
|
return EXIT_CODES.SUCCESS;
|
|
607
672
|
}
|
|
673
|
+
if (args.action === 'timeline') {
|
|
674
|
+
failureLabel = 'Lookup observation timeline';
|
|
675
|
+
const readTimelineInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
|
|
676
|
+
limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
|
|
677
|
+
label: 'Lookup timeline input',
|
|
678
|
+
})));
|
|
679
|
+
const inputs = [];
|
|
680
|
+
let totalBytes = 0;
|
|
681
|
+
try {
|
|
682
|
+
for (const source of args.sources) {
|
|
683
|
+
const input = await readTimelineInput(source);
|
|
684
|
+
totalBytes += Buffer.byteLength(input, 'utf8');
|
|
685
|
+
if (totalBytes > MAX_LOOKUP_TIMELINE_INPUT_BYTES) {
|
|
686
|
+
throw new CliUsageError(`Lookup timeline input is limited to ${MAX_LOOKUP_TIMELINE_INPUT_BYTES} bytes in total.`);
|
|
687
|
+
}
|
|
688
|
+
inputs.push(input);
|
|
689
|
+
}
|
|
690
|
+
}
|
|
691
|
+
catch (error) {
|
|
692
|
+
if (error instanceof CliUsageError)
|
|
693
|
+
throw error;
|
|
694
|
+
throw new CliUsageError(`Could not read Lookup timeline input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
695
|
+
}
|
|
696
|
+
const document = buildCliLookupTimeline(inputs, commandContext.now());
|
|
697
|
+
if (!args.quiet)
|
|
698
|
+
write(stdout, args.output === 'json'
|
|
699
|
+
? formatJsonDocument(document)
|
|
700
|
+
: terminal(formatCliLookupTimeline(document), args.color));
|
|
701
|
+
return EXIT_CODES.SUCCESS;
|
|
702
|
+
}
|
|
608
703
|
if (args.action === 'export') {
|
|
609
704
|
failureLabel = 'Evidence export';
|
|
610
705
|
let input;
|
package/docs/cli.md
CHANGED
|
@@ -8,7 +8,9 @@ hosted WHOISleuth deployment.
|
|
|
8
8
|
source location. It groups commands by investigation, discovery, saved-evidence,
|
|
9
9
|
integrity, and calibration workflows. `whoisleuth <command> --help` displays the
|
|
10
10
|
command's purpose, focused invocation, example, and collection boundary without
|
|
11
|
-
printing the full command list.
|
|
11
|
+
printing the full command list. `whoisleuth commands --json` exposes those same
|
|
12
|
+
contracts as a stable local catalogue for wrappers and terminal tooling.
|
|
13
|
+
Packaged copies include `LICENSE`, `NOTICE`, and
|
|
12
14
|
`TRADEMARKS.md` alongside the CLI documentation.
|
|
13
15
|
|
|
14
16
|
## Installation
|
|
@@ -17,11 +19,18 @@ Public releases require Node.js 24 or later and install the `whoisleuth`
|
|
|
17
19
|
command:
|
|
18
20
|
|
|
19
21
|
```bash
|
|
20
|
-
npm exec --yes --package=@slicedearth/whoisleuth-cli -- whoisleuth --help
|
|
21
|
-
npm install --global @slicedearth/whoisleuth-cli
|
|
22
|
+
npm exec --yes --ignore-scripts --package=@slicedearth/whoisleuth-cli -- whoisleuth --help
|
|
23
|
+
npm install --global --ignore-scripts @slicedearth/whoisleuth-cli
|
|
22
24
|
whoisleuth doctor
|
|
23
25
|
```
|
|
24
26
|
|
|
27
|
+
Update and verify an existing global installation with:
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
npm install --global --ignore-scripts @slicedearth/whoisleuth-cli@latest
|
|
31
|
+
whoisleuth --version
|
|
32
|
+
```
|
|
33
|
+
|
|
25
34
|
The scoped package and application share one semantic version. The package runs
|
|
26
35
|
locally and does not call the hosted WHOISleuth deployment. Networked commands
|
|
27
36
|
contact only the sources described by their collection profile; offline review,
|
|
@@ -34,6 +43,7 @@ node bin/whoisleuth.mts lookup example.com
|
|
|
34
43
|
node bin/whoisleuth.mts lookup AS13335 --json
|
|
35
44
|
printf 'example.com\n' | node bin/whoisleuth.mts lookup --json
|
|
36
45
|
node bin/whoisleuth.mts lookup example.com --deep
|
|
46
|
+
node bin/whoisleuth.mts lookup example.com --deep --plan --json
|
|
37
47
|
node bin/whoisleuth.mts lookup example.com --deep --summary
|
|
38
48
|
node bin/whoisleuth.mts lookup example.com --deep --verbose
|
|
39
49
|
node bin/whoisleuth.mts lookup example.com --deep --markdown --output lookup.md
|
|
@@ -41,6 +51,7 @@ node bin/whoisleuth.mts lookup example.com --deep --json --strict-exit --events
|
|
|
41
51
|
cat domains.txt | node bin/whoisleuth.mts bulk --jsonl
|
|
42
52
|
node bin/whoisleuth.mts bulk domains.txt --csv --registered-only
|
|
43
53
|
node bin/whoisleuth.mts bulk domains.txt --domains --inconclusive-only
|
|
54
|
+
node bin/whoisleuth.mts bulk domains.txt --queries --errors-only
|
|
44
55
|
node bin/whoisleuth.mts bulk domains.txt --concurrency 4
|
|
45
56
|
node bin/whoisleuth.mts bulk domains.txt --deep --checkpoint bulk-checkpoint.json
|
|
46
57
|
node bin/whoisleuth.mts bulk domains.txt --deep --checkpoint bulk-checkpoint.json --resume
|
|
@@ -55,6 +66,7 @@ node bin/whoisleuth.mts tls example.com --json
|
|
|
55
66
|
node bin/whoisleuth.mts registry-support example.uk --json
|
|
56
67
|
node bin/whoisleuth.mts risk-calibrate calibration.json --json
|
|
57
68
|
node bin/whoisleuth.mts verify-artifact workspace.json --json
|
|
69
|
+
node bin/whoisleuth.mts verify-artifact lookup.json --json
|
|
58
70
|
node bin/whoisleuth.mts verify-artifact workspace-encrypted.json --passphrase-file passphrase.txt --json
|
|
59
71
|
node bin/whoisleuth.mts source-report lookup.json --json
|
|
60
72
|
node bin/whoisleuth.mts inspect-archive workspace.json --json
|
|
@@ -66,12 +78,15 @@ node bin/whoisleuth.mts compare lookup.json --json
|
|
|
66
78
|
node bin/whoisleuth.mts page-compare official.json candidate.json --json
|
|
67
79
|
node bin/whoisleuth.mts mail-review bulk.json --json
|
|
68
80
|
node bin/whoisleuth.mts diff first-lookup.json second-lookup.json --json
|
|
81
|
+
node bin/whoisleuth.mts timeline first-observation.json second-observation.json latest-observation.json --json
|
|
69
82
|
node bin/whoisleuth.mts export lookup.json > evidence.json
|
|
70
83
|
node bin/whoisleuth.mts export lookup.json --markdown > evidence.md
|
|
71
84
|
node bin/whoisleuth.mts export lookup.json --html > evidence.html
|
|
72
85
|
node bin/whoisleuth.mts doctor
|
|
73
86
|
node bin/whoisleuth.mts doctor --network --json
|
|
87
|
+
node bin/whoisleuth.mts commands --json
|
|
74
88
|
node bin/whoisleuth.mts completion zsh
|
|
89
|
+
node bin/whoisleuth.mts completion powershell
|
|
75
90
|
node bin/whoisleuth.mts manual | man -l -
|
|
76
91
|
```
|
|
77
92
|
|
|
@@ -87,10 +102,13 @@ npm run cli:package:check
|
|
|
87
102
|
|
|
88
103
|
The check includes only the executable, reachable TypeScript modules, CLI
|
|
89
104
|
guide, licence, notices, and trademark terms. It excludes application routes,
|
|
90
|
-
deployment adapters, tests, and development tools.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
105
|
+
deployment adapters, tests, and development tools. Direct runtime dependencies
|
|
106
|
+
are pinned to the exact versions exercised by the reviewed repository lockfile.
|
|
107
|
+
The package and its dependencies require no lifecycle scripts, so the install
|
|
108
|
+
examples disable them explicitly.
|
|
109
|
+
Its generated manifest stays private. Release-candidate assembly is a separate
|
|
110
|
+
explicit operation and never publishes from a developer checkout. Use the
|
|
111
|
+
scoped package name; an unqualified `npx whoisleuth` is not this project.
|
|
94
112
|
|
|
95
113
|
Lookup defaults to the conservative fast profile. `--deep` must be requested
|
|
96
114
|
explicitly and can add bounded WHOIS, DNS, website, TLS, registrar RDAP, and
|
|
@@ -104,6 +122,14 @@ up to five normalized PTR names; JSON retains up to eight. PTR names are
|
|
|
104
122
|
operator-published routing context, not proof of hosting control or ownership.
|
|
105
123
|
Fast lookups do not run this query.
|
|
106
124
|
|
|
125
|
+
`lookup --plan` performs classification and prints a versioned preflight without
|
|
126
|
+
starting collection. It lists the normalized target, selected fast or deep
|
|
127
|
+
mode, planned source families, conditional operations, and the data each family
|
|
128
|
+
may receive. The plan cannot predict live feature configuration, cache state,
|
|
129
|
+
redirects, referrals, source availability, or an exact request count. It can be
|
|
130
|
+
combined with `--json`, but not with report, detail, strict-exit, progress-event,
|
|
131
|
+
or quiet options.
|
|
132
|
+
|
|
107
133
|
Only one query is accepted by `lookup`. Multiple-input processing belongs to
|
|
108
134
|
the explicit `bulk` command rather than being silently inferred by `lookup`.
|
|
109
135
|
Standard input is capped at 4 KiB and must contain one non-empty line.
|
|
@@ -114,32 +140,49 @@ request. Its output is described below.
|
|
|
114
140
|
|
|
115
141
|
`doctor` is offline by default. It checks the Node runtime, platform, terminal
|
|
116
142
|
capabilities, and availability of offline command families. `doctor --network`
|
|
117
|
-
adds bounded public-DNS and WHOIS port 43
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
143
|
+
adds independent bounded public-DNS, HTTPS/RDAP bootstrap, and WHOIS port 43
|
|
144
|
+
checks against fixed IANA diagnostic infrastructure. The three probes run
|
|
145
|
+
within the same per-probe ceiling so one stalled transport does not block the
|
|
146
|
+
others. The report retains only state, HTTP status, and a bounded explanation,
|
|
147
|
+
not resolved addresses or response content. A failed optional network check
|
|
148
|
+
returns the partial-failure exit code rather than claiming that all CLI
|
|
149
|
+
collection is unavailable.
|
|
150
|
+
|
|
151
|
+
`completion bash`, `completion zsh`, `completion fish`, and `completion
|
|
152
|
+
powershell` print static shell completion scripts to stdout. The command does
|
|
153
|
+
not edit a shell profile or make a network request. Review the generated script
|
|
154
|
+
before sourcing it or placing it in the relevant shell completion directory.
|
|
155
|
+
`manual` prints a generated roff manual page and likewise changes no local
|
|
156
|
+
configuration.
|
|
157
|
+
|
|
158
|
+
`commands` prints the installed command catalogue without running collection or
|
|
159
|
+
reading evidence. `commands --json` emits version 1 of the
|
|
160
|
+
`whoisleuth.cli.command-catalogue` schema, including each command's usage,
|
|
161
|
+
example, declared collection mode, collection scope, and boundary. This is the
|
|
162
|
+
supported discovery contract for local wrappers; it is not a permission grant
|
|
163
|
+
or a substitute for focused help.
|
|
164
|
+
|
|
165
|
+
Focused command help and the generated manual classify every operation as
|
|
166
|
+
offline or networked and state the relevant target, input, and concurrency
|
|
167
|
+
ceiling. These labels describe observable behavior rather than granting
|
|
168
|
+
authorization. The packaged `DISCLOSURE` defines the intended defensive scope;
|
|
169
|
+
operators remain responsible for law, provider terms, and authorization.
|
|
128
170
|
|
|
129
171
|
## Deployment boundary
|
|
130
172
|
|
|
131
|
-
The CLI
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
173
|
+
The CLI runs locally. The serverless deployment publishes only `frontend/build`
|
|
174
|
+
and packages functions only from `netlify/functions`, so `bin/` and `cli/` are
|
|
175
|
+
not part of the hosted static site or function bundle. The application root and
|
|
176
|
+
ordinary package-check candidate remain private. Explicitly reviewed CLI
|
|
177
|
+
release candidates can be published under the scoped public package described
|
|
178
|
+
in the installation section.
|
|
136
179
|
|
|
137
180
|
Commands that query RDAP, WHOIS, DNS, HTTP, TLS, or Certificate Transparency do
|
|
138
181
|
so directly from the machine running the CLI. They do not use the hosted login,
|
|
139
182
|
hosted session, or deployment usage controls; upstream providers can see and
|
|
140
183
|
rate-limit the local machine's network address. Offline `discover`, `compare`,
|
|
141
|
-
`page-compare`, `mail-review`, `diff`, `risk-calibrate`, `verify-artifact`, `source-report`, `export`,
|
|
142
|
-
`completion`, and `manual` operations make no network requests. Commands write
|
|
184
|
+
`page-compare`, `mail-review`, `diff`, `timeline`, `risk-calibrate`, `verify-artifact`, `source-report`, `export`,
|
|
185
|
+
`commands`, `completion`, and `manual` operations make no network requests. Commands write
|
|
143
186
|
to stdout unless the analyst deliberately selects a local output file.
|
|
144
187
|
|
|
145
188
|
## Output
|
|
@@ -266,6 +309,15 @@ registration, DNS, page identity, mail, certificate, and relationship fields,
|
|
|
266
309
|
keeps missing and unavailable evidence distinct, and makes no ownership or
|
|
267
310
|
maliciousness inference. The output records both original observation times.
|
|
268
311
|
|
|
312
|
+
`timeline <observation.json> <observation.json> [...]` accepts from 2 to 20
|
|
313
|
+
saved Lookup documents for one domain, orders them by their validated
|
|
314
|
+
observation times, and compares each adjacent pair offline. The 32 MiB aggregate
|
|
315
|
+
input ceiling is enforced while files are read and again by the timeline
|
|
316
|
+
builder. Output retains normalized field states and observation times but no
|
|
317
|
+
input filenames or raw registry payloads. A difference can reflect a domain
|
|
318
|
+
change or changed collection conditions, so the command makes no current-state,
|
|
319
|
+
ownership, intent, safety, or maliciousness conclusion.
|
|
320
|
+
|
|
269
321
|
When diagnostics version 5, 6, 7, or 8 reports a documented registry collection
|
|
270
322
|
constraint, terminal output also shows the suffix, WHOIS and RDAP access
|
|
271
323
|
profiles, and the bounded limitation. This is static access-policy context: it
|
|
@@ -286,7 +338,7 @@ machine access is not evidence that a domain is unregistered or safe.
|
|
|
286
338
|
This release supports `lookup`, `bulk`, `ct-search`, `discover`, `discover-scan`, `posture`,
|
|
287
339
|
`http`, `tls`, `registry-support`, `risk-calibrate`, `verify-artifact`,
|
|
288
340
|
`inspect-archive`, `sign-artifact`, `verify-signature`, `source-report`,
|
|
289
|
-
`compare`, `page-compare`, `mail-review`, `diff`, `export`, `doctor`, `completion`, and `manual`. Additional command families
|
|
341
|
+
`compare`, `page-compare`, `mail-review`, `diff`, `timeline`, `export`, `doctor`, `commands`, `completion`, and `manual`. Additional command families
|
|
290
342
|
are added as separate bounded increments rather than exposing incomplete
|
|
291
343
|
aliases.
|
|
292
344
|
|
|
@@ -385,7 +437,11 @@ Risk model.
|
|
|
385
437
|
evidence contents. Input is capped at 15 MiB. It currently recognises ordinary
|
|
386
438
|
and encrypted workspace archives, case-response packets, acquisition-decision
|
|
387
439
|
exports, domain-comparison exports, Bulk mail-exposure exports, and Bulk review
|
|
388
|
-
manifests.
|
|
440
|
+
manifests. It also validates the bounded versioned structure of saved CLI
|
|
441
|
+
Lookup JSON. Because saved Lookup documents do not embed a checksum or
|
|
442
|
+
signature, that result is reported as `structure_valid` with content integrity
|
|
443
|
+
explicitly unchecked. Saved Lookup validation retains its stricter 8 MiB
|
|
444
|
+
document ceiling.
|
|
389
445
|
|
|
390
446
|
For an ordinary workspace archive, the command validates the versioned
|
|
391
447
|
structure, section byte counts, record counts, and per-section checksums. For a
|
|
@@ -469,19 +525,25 @@ deep mode.
|
|
|
469
525
|
|
|
470
526
|
Bulk uses the shared compact lookup response, so it does not retain raw RDAP
|
|
471
527
|
objects or WHOIS response bodies. `--json` returns one bounded collection;
|
|
472
|
-
`--jsonl` emits one self-contained versioned item per line.
|
|
528
|
+
`--jsonl` emits one self-contained versioned item per line. `--domains` emits
|
|
529
|
+
only successfully normalized domain names, while `--queries` preserves the
|
|
530
|
+
selected bounded input queries and is suitable for an exact retry queue.
|
|
531
|
+
Version 2 adds a
|
|
473
532
|
bounded `dnsSummary` projection for observed A, AAAA, NS, and MX records plus
|
|
474
533
|
null MX, SPF, and DMARC state. `--csv` writes fixed columns for automation,
|
|
475
|
-
including the DNS summaries and explicit outcome
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
534
|
+
including the DNS summaries and explicit outcome.
|
|
535
|
+
|
|
536
|
+
`--registered-only`, `--inconclusive-only`, and `--errors-only` are mutually
|
|
537
|
+
exclusive output filters. The first retains registered, for-sale, and expiring
|
|
538
|
+
authority-aware states. The second retains unknown authority states and failed
|
|
539
|
+
rows. The third retains only rows whose lookup operation failed, which is useful
|
|
540
|
+
for producing an exact retry queue without treating an inconclusive successful
|
|
541
|
+
lookup as an operational error. Filtering does not change collection and never
|
|
542
|
+
converts an unavailable or failed source into an unregistered result. Machine
|
|
543
|
+
documents record collected and emitted counts. A mixture of successful and
|
|
544
|
+
failed queries exits with code 4 while preserving every collected result before
|
|
545
|
+
output filtering. For example, `bulk domains.txt --queries --errors-only`
|
|
546
|
+
produces only the exact failed inputs while retaining the partial-failure exit.
|
|
485
547
|
|
|
486
548
|
## Certificate Transparency search
|
|
487
549
|
|
|
@@ -151,7 +151,7 @@ function freshness(observedAt, now) {
|
|
|
151
151
|
export function buildBulkDomainComparison(leftRaw, rightRaw, observedAtRaw = null, options = {}) {
|
|
152
152
|
const left = normalizeBulkSessionResult(leftRaw);
|
|
153
153
|
const right = normalizeBulkSessionResult(rightRaw);
|
|
154
|
-
if (!left || !right || left.domain === right.domain)
|
|
154
|
+
if (!left || !right || (left.domain === right.domain && !options.allowSameDomain))
|
|
155
155
|
return null;
|
|
156
156
|
const observedAt = timestamp(observedAtRaw);
|
|
157
157
|
const registry = context(left, right, 'Registry and registrar evidence', ['rdap', 'availability'], observedAt, options);
|
|
@@ -214,11 +214,17 @@ export function buildBulkDomainComparison(leftRaw, rightRaw, observedAtRaw = nul
|
|
|
214
214
|
freshness: freshness(observedAt, options.now ?? Date.now()),
|
|
215
215
|
rows,
|
|
216
216
|
counts,
|
|
217
|
-
limitations:
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
217
|
+
limitations: options.allowSameDomain
|
|
218
|
+
? [
|
|
219
|
+
'This compares normalized evidence already present in two saved observations for the same domain and makes no new request.',
|
|
220
|
+
'Missing, unavailable, and differently collected evidence remain distinct from an observed difference.',
|
|
221
|
+
'An observed difference can reflect changed collection conditions and does not by itself establish current state, ownership, intent, safety, or maliciousness.',
|
|
222
|
+
]
|
|
223
|
+
: [
|
|
224
|
+
'This compares compact settled evidence already present in Bulk and makes no new request.',
|
|
225
|
+
'Missing, unavailable, and differently collected evidence remain distinct from an observed difference.',
|
|
226
|
+
'Equality does not establish common ownership, infrastructure control, intent, safety, or maliciousness.',
|
|
227
|
+
],
|
|
222
228
|
};
|
|
223
229
|
}
|
|
224
230
|
export async function buildBulkDomainComparisonExport(comparisonRaw, generatedAtRaw = new Date().toISOString()) {
|
package/lib/csv.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// Shared bounded CSV serialization for browser and CLI exports. Values can
|
|
2
2
|
// contain upstream evidence, so normalize controls and prevent spreadsheet
|
|
3
3
|
// formula execution before applying ordinary CSV quoting.
|
|
4
|
-
const CSV_FORMULA_TRIGGER_RE = /^
|
|
4
|
+
const CSV_FORMULA_TRIGGER_RE = /^[=+\-@]/u;
|
|
5
5
|
export const MAX_CSV_CELL_LENGTH = 32_768;
|
|
6
6
|
function csvText(value) {
|
|
7
7
|
const raw = Array.isArray(value)
|
|
@@ -21,7 +21,7 @@ function appendGroups(lines, groups) {
|
|
|
21
21
|
function formatLookupEvidenceMarkdown(document, options = {}) {
|
|
22
22
|
const report = buildLookupEvidenceReport(document, options);
|
|
23
23
|
const lines = [
|
|
24
|
-
`# Lookup evidence report
|
|
24
|
+
`# Lookup evidence report: ${escapeMarkdownValue(report.title)}`,
|
|
25
25
|
'', `> ${report.notice}`, '',
|
|
26
26
|
];
|
|
27
27
|
appendFields(lines, report.metadata);
|
|
@@ -42,7 +42,7 @@ function formatLookupEvidenceMarkdown(document, options = {}) {
|
|
|
42
42
|
}
|
|
43
43
|
else {
|
|
44
44
|
for (const field of report.comparison.fields) {
|
|
45
|
-
lines.push(`- **${escapeMarkdownValue(field.label)}
|
|
45
|
+
lines.push(`- **${escapeMarkdownValue(field.label)} (${escapeMarkdownValue(field.status)}):** RDAP ${escapeMarkdownValue(field.rdap)}; WHOIS ${escapeMarkdownValue(field.whois)}`);
|
|
46
46
|
}
|
|
47
47
|
if (report.comparison.omitted)
|
|
48
48
|
lines.push(`- ${report.comparison.omitted} additional comparison fields omitted.`);
|
|
@@ -51,7 +51,7 @@ function formatLookupEvidenceMarkdown(document, options = {}) {
|
|
|
51
51
|
lines.push('', '## Registry / registrar RDAP comparison');
|
|
52
52
|
appendFields(lines, report.registrarComparison.health);
|
|
53
53
|
for (const field of report.registrarComparison.fields) {
|
|
54
|
-
lines.push(`- **${escapeMarkdownValue(field.label)}
|
|
54
|
+
lines.push(`- **${escapeMarkdownValue(field.label)} (${escapeMarkdownValue(field.status)}):** Registry RDAP ${escapeMarkdownValue(field.registry)}; registrar RDAP ${escapeMarkdownValue(field.registrar)}`);
|
|
55
55
|
}
|
|
56
56
|
if (report.registrarComparison.omitted)
|
|
57
57
|
lines.push(`- ${report.registrarComparison.omitted} additional comparison fields omitted.`);
|
package/package.json
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
"name": "@slicedearth/whoisleuth-cli",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"license": "AGPL-3.0-only",
|
|
5
|
+
"author": "slicedearth",
|
|
5
6
|
"contentPolicy": {
|
|
6
7
|
"class": "dual-use"
|
|
7
8
|
},
|
|
@@ -32,11 +33,11 @@
|
|
|
32
33
|
"bugs": {
|
|
33
34
|
"url": "https://github.com/slicedearth/whoisleuth/issues"
|
|
34
35
|
},
|
|
35
|
-
"version": "1.
|
|
36
|
+
"version": "1.34.0",
|
|
36
37
|
"dependencies": {
|
|
37
|
-
"parse5": "
|
|
38
|
-
"tldts": "
|
|
39
|
-
"undici": "
|
|
38
|
+
"parse5": "8.0.1",
|
|
39
|
+
"tldts": "7.4.10",
|
|
40
|
+
"undici": "8.9.0"
|
|
40
41
|
},
|
|
41
42
|
"files": [
|
|
42
43
|
"bin/**/*.mjs",
|
|
@@ -49,6 +50,7 @@
|
|
|
49
50
|
"LICENSES/*.txt",
|
|
50
51
|
"NOTICE",
|
|
51
52
|
"README.md",
|
|
53
|
+
"SECURITY.md",
|
|
52
54
|
"TRADEMARKS.md",
|
|
53
55
|
"third-party-notices.txt"
|
|
54
56
|
],
|
package/third-party-notices.txt
CHANGED
|
@@ -5130,7 +5130,7 @@ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
|
|
5130
5130
|
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
|
5131
5131
|
DEALINGS IN THE SOFTWARE.
|
|
5132
5132
|
================================================================================
|
|
5133
|
-
tldts@7.4.
|
|
5133
|
+
tldts@7.4.10
|
|
5134
5134
|
Relationship: direct production dependency
|
|
5135
5135
|
Declared licence: MIT
|
|
5136
5136
|
Licence source: LICENSE
|
|
@@ -5149,7 +5149,7 @@ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLI
|
|
|
5149
5149
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
|
5150
5150
|
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
|
5151
5151
|
================================================================================
|
|
5152
|
-
tldts-core@7.4.
|
|
5152
|
+
tldts-core@7.4.10
|
|
5153
5153
|
Relationship: transitive production dependency
|
|
5154
5154
|
Declared licence: MIT
|
|
5155
5155
|
Licence source: LICENSE
|