@slicedearth/whoisleuth-cli 1.36.1 → 1.39.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.
Files changed (97) hide show
  1. package/README.md +17 -2
  2. package/bin/whoisleuth.mjs +41 -24
  3. package/cli/arguments.mjs +236 -5
  4. package/cli/artifact-verify.mjs +56 -14
  5. package/cli/compare.mjs +1 -1
  6. package/cli/completion.mjs +18 -3
  7. package/cli/doctor.mjs +11 -5
  8. package/cli/evidence-command-arguments.mjs +1 -1
  9. package/cli/evidence-command-runner.mjs +2 -2
  10. package/cli/evidence-signing.mjs +4 -4
  11. package/cli/formatters/html.mjs +1 -1
  12. package/cli/formatters/json.mjs +13 -1
  13. package/cli/formatters/terminal.mjs +7 -3
  14. package/cli/investigation-plan.mjs +2 -2
  15. package/cli/local-mmdb-review.mjs +76 -0
  16. package/cli/lookalike-calibration.mjs +121 -0
  17. package/cli/lookup-command-runner.mjs +4 -1
  18. package/cli/lookup-plan.mjs +3 -3
  19. package/cli/lookup-reconcile.mjs +141 -0
  20. package/cli/lookup-timeline.mjs +1 -1
  21. package/cli/mail-review.mjs +1 -1
  22. package/cli/offline-evidence-review.mjs +20 -2
  23. package/cli/registry-doctor.mjs +125 -0
  24. package/cli/risk-calibration.mjs +117 -9
  25. package/cli/runner.mjs +236 -17
  26. package/cli/saved-lookup.mjs +2 -2
  27. package/cli/sharing-review.mjs +202 -0
  28. package/cli/source-reliability.mjs +1 -1
  29. package/docs/cli-reference.md +1110 -0
  30. package/docs/cli.md +224 -940
  31. package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
  32. package/frontend/src/lib/analysis/acquisition-due-diligence.js +6 -6
  33. package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
  34. package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
  35. package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
  36. package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
  37. package/frontend/src/lib/analysis/case-migration-model.js +6 -1
  38. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  39. package/frontend/src/lib/analysis/case-record-core.js +5 -1
  40. package/frontend/src/lib/analysis/case-record-operations.js +6 -1
  41. package/frontend/src/lib/analysis/case-response-packet.js +7 -7
  42. package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
  43. package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
  44. package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
  45. package/frontend/src/lib/analysis/lookup-decision-support.js +480 -0
  46. package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
  47. package/frontend/src/lib/analysis/lookup-presentation.js +74 -0
  48. package/frontend/src/lib/analysis/lookup-response.js +2 -0
  49. package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
  50. package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
  51. package/frontend/src/lib/analysis/page-similarity.js +1 -1
  52. package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
  53. package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
  54. package/frontend/src/lib/analysis/scoring.js +8 -88
  55. package/frontend/src/lib/analysis/shortlist-model.js +8 -4
  56. package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
  57. package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
  58. package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
  59. package/frontend/src/lib/analysis/visualization-models.js +370 -0
  60. package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
  61. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
  62. package/frontend/src/lib/analysis/workspace-archive.js +2 -2
  63. package/lib/analyst-taxonomy.mjs +40 -0
  64. package/lib/availability.mjs +1 -1
  65. package/lib/browser-library-profile.mjs +15 -2
  66. package/lib/certificate-extension-profile.mjs +71 -0
  67. package/lib/credential-surface-profile.mjs +1 -1
  68. package/lib/domain-assurance.mjs +368 -0
  69. package/lib/domain-control-manifest.mjs +50 -7
  70. package/lib/domain-posture.mjs +9 -9
  71. package/lib/encrypted-dns-contract.mjs +1 -1
  72. package/lib/evidence-report-markdown.mjs +1 -1
  73. package/lib/evidence-report.mjs +1 -1
  74. package/lib/generated/cisa-kev-catalog.mjs +1668 -0
  75. package/lib/ip-prefix.mjs +34 -1
  76. package/lib/local-geoip-evidence.mjs +2 -2
  77. package/lib/lookup-response-contract.mjs +346 -0
  78. package/lib/observed-network-context.mjs +1 -1
  79. package/lib/opportunity-scoring.mjs +107 -0
  80. package/lib/page-fingerprints.mjs +1 -1
  81. package/lib/rdap-capabilities.mjs +6 -6
  82. package/lib/rdap-search-workbench.mjs +96 -6
  83. package/lib/registry-access.mjs +1 -1
  84. package/lib/registry-capability-catalogue.mjs +4 -4
  85. package/lib/registry-insights.mjs +6 -6
  86. package/lib/risk-scoring.mjs +206 -83
  87. package/lib/scoring-evidence-quality.mjs +107 -0
  88. package/lib/security-txt.mjs +3 -3
  89. package/lib/threat-intelligence-manifests.mjs +1 -1
  90. package/lib/threatfox-intelligence.mjs +1 -1
  91. package/lib/tls-intelligence.mjs +13 -3
  92. package/lib/urlhaus-intelligence.mjs +1 -1
  93. package/lib/website-security-posture.mjs +5 -5
  94. package/lib/website-technology.mjs +1 -1
  95. package/lib/whois-chain.mjs +1 -1
  96. package/package.json +5 -1
  97. package/third-party-notices.txt +911 -19
@@ -0,0 +1,1110 @@
1
+ # WHOISleuth CLI reference
2
+
3
+ This complete reference documents every supported command contract. For a
4
+ short installation and first-use path, start with the [CLI guide](cli.md).
5
+
6
+ The first-party command-line interface runs the same local classification and
7
+ lookup modules as the Express and serverless adapters. It does not call the
8
+ hosted WHOISleuth deployment.
9
+
10
+ `whoisleuth --help` displays the copyright, AGPL-3.0-only licence, and official
11
+ source location. It groups commands by investigation, discovery, saved-evidence,
12
+ integrity, and calibration workflows. `whoisleuth <command> --help` displays the
13
+ command's purpose, focused invocation, example, and collection boundary without
14
+ printing the full command list. `whoisleuth commands --json` exposes those same
15
+ contracts as a stable local catalogue for wrappers and terminal tooling.
16
+ Packaged copies include `LICENSE`, `NOTICE`, and
17
+ `TRADEMARKS.md` alongside the CLI documentation.
18
+
19
+ ## Installation
20
+
21
+ Public releases require Node.js 24 or later and install the `whoisleuth`
22
+ command:
23
+
24
+ ```bash
25
+ npm exec --yes --ignore-scripts --package=@slicedearth/whoisleuth-cli -- whoisleuth --help
26
+ npm install --global --ignore-scripts @slicedearth/whoisleuth-cli
27
+ whoisleuth doctor
28
+ ```
29
+
30
+ Update and verify an existing global installation with:
31
+
32
+ ```bash
33
+ npm install --global --ignore-scripts @slicedearth/whoisleuth-cli@latest
34
+ whoisleuth --version
35
+ ```
36
+
37
+ The scoped package and application share one semantic version. The package runs
38
+ locally and does not call the hosted WHOISleuth deployment. Networked commands
39
+ contact only the sources described by their collection profile; offline review,
40
+ comparison, verification, completion, and manual commands remain local.
41
+
42
+ ## Commands
43
+
44
+ ```bash
45
+ node bin/whoisleuth.mts lookup example.com
46
+ node bin/whoisleuth.mts lookup AS13335 --json
47
+ printf 'example.com\n' | node bin/whoisleuth.mts lookup --json
48
+ node bin/whoisleuth.mts lookup example.com --deep
49
+ node bin/whoisleuth.mts lookup example.com --deep --plan --json
50
+ node bin/whoisleuth.mts lookup example.com --deep --summary
51
+ node bin/whoisleuth.mts lookup example.com --deep --verbose
52
+ node bin/whoisleuth.mts lookup example.com --deep --markdown --output lookup.md
53
+ node bin/whoisleuth.mts lookup example.com --deep --json --strict-exit --events
54
+ node bin/whoisleuth.mts lookup example.test --deep --observer workstation-a --vantage office-egress --json > office.json
55
+ cat domains.txt | node bin/whoisleuth.mts bulk --jsonl
56
+ node bin/whoisleuth.mts bulk domains.txt --csv --registered-only
57
+ node bin/whoisleuth.mts bulk domains.txt --domains --inconclusive-only
58
+ node bin/whoisleuth.mts bulk domains.txt --queries --errors-only
59
+ node bin/whoisleuth.mts bulk domains.txt --concurrency 4
60
+ node bin/whoisleuth.mts bulk domains.txt --deep --checkpoint bulk-checkpoint.json
61
+ node bin/whoisleuth.mts bulk domains.txt --deep --checkpoint bulk-checkpoint.json --resume
62
+ node bin/whoisleuth.mts ct-search 'example brand' --json
63
+ node bin/whoisleuth.mts discover example.com --preset common --jsonl
64
+ node bin/whoisleuth.mts discover example.test --dictionary private-terms.txt --snapshot discovery-state.json --json
65
+ node bin/whoisleuth.mts discover-scan example.test --scan-limit 50 --checkpoint candidate-scan.json --json
66
+ node bin/whoisleuth.mts discover-scan example.test --deep --scan-limit 20 --resolver 192.0.2.53 --observation-snapshot observed-candidates.json --csv
67
+ node bin/whoisleuth.mts posture example.com --selectors selector1 --retired-selectors selector0 --mail-profile defensive-no-mail --json
68
+ node bin/whoisleuth.mts http example.com --json
69
+ node bin/whoisleuth.mts tls example.com --json
70
+ node bin/whoisleuth.mts registry-support example.uk --json
71
+ node bin/whoisleuth.mts registry-doctor lookup.json --json
72
+ node bin/whoisleuth.mts risk-calibrate calibration.json --json
73
+ node bin/whoisleuth.mts lookalike-calibrate reviewed-candidates.json --json
74
+ node bin/whoisleuth.mts verify-artifact workspace.json --json
75
+ node bin/whoisleuth.mts verify-artifact lookup.json --json
76
+ node bin/whoisleuth.mts verify-artifact workspace-encrypted.json --passphrase-file passphrase.txt --json
77
+ node bin/whoisleuth.mts source-report lookup.json --json
78
+ node bin/whoisleuth.mts inspect-archive workspace.json --json
79
+ node bin/whoisleuth.mts inspect-archive workspace.json --search example.invalid --json
80
+ node bin/whoisleuth.mts sign-artifact response-packet.json --private-key-file analyst-private.pem > response-packet.signed.json
81
+ node bin/whoisleuth.mts verify-signature response-packet.signed.json --public-key-file analyst-public.pem
82
+ node bin/whoisleuth.mts lookup example.com --deep --json > lookup.json
83
+ node bin/whoisleuth.mts compare lookup.json --json
84
+ node bin/whoisleuth.mts page-compare official.json candidate.json --json
85
+ node bin/whoisleuth.mts mail-review bulk.json --json
86
+ node bin/whoisleuth.mts review-evidence dnssec-evidence.json --json
87
+ node bin/whoisleuth.mts domain-control domain-control-input.json --json
88
+ node bin/whoisleuth.mts assurance domain-assurance-input.json --json
89
+ node bin/whoisleuth.mts sharing-review response-packet.json --marking amber --recipient-scope organization --purpose 'Reviewed incident handoff' --human-reviewed --personal-data-reviewed --redactions-confirmed --json
90
+ node bin/whoisleuth.mts workflow-plan domain-triage example.test --json
91
+ node bin/whoisleuth.mts diff first-lookup.json second-lookup.json --json
92
+ node bin/whoisleuth.mts reconcile office.json mobile.json external.json --json
93
+ node bin/whoisleuth.mts timeline first-observation.json second-observation.json latest-observation.json --json
94
+ node bin/whoisleuth.mts export lookup.json > evidence.json
95
+ node bin/whoisleuth.mts export lookup.json --markdown > evidence.md
96
+ node bin/whoisleuth.mts export lookup.json --html > evidence.html
97
+ node bin/whoisleuth.mts doctor
98
+ node bin/whoisleuth.mts doctor --network --json
99
+ node bin/whoisleuth.mts commands --json
100
+ node bin/whoisleuth.mts completion zsh
101
+ node bin/whoisleuth.mts completion powershell
102
+ node bin/whoisleuth.mts manual | man -l -
103
+ ```
104
+
105
+ These examples run from a checked-out repository; replace
106
+ `node bin/whoisleuth.mts` with `whoisleuth` after installation. The root
107
+ application package remains private. A separate scoped CLI archive is assembled
108
+ from the exact executable dependency closure, installed in a temporary
109
+ directory, and smoke tested with:
110
+
111
+ ```bash
112
+ npm run cli:package:check
113
+ ```
114
+
115
+ The check includes only the executable, reachable TypeScript modules, CLI
116
+ guide, licence, notices, and trademark terms. It excludes application routes,
117
+ deployment adapters, tests, and development tools. Direct runtime dependencies
118
+ are pinned to the exact versions exercised by the reviewed repository lockfile.
119
+ The package and its dependencies require no lifecycle scripts, so the install
120
+ examples disable them explicitly.
121
+ Its generated manifest stays private. Release-candidate assembly is a separate
122
+ explicit operation and never publishes from a developer checkout. Use the
123
+ scoped package name; an unqualified `npx whoisleuth` is not this project.
124
+
125
+ Lookup defaults to the conservative fast profile. `--deep` must be requested
126
+ explicitly and can add bounded WHOIS, DNS, website, TLS, registrar RDAP, and
127
+ observed-network IP RDAP work for a domain. The browser Console's optional
128
+ security.txt and external intelligence selections are not CLI flags; the CLI
129
+ does not run those actions implicitly.
130
+
131
+ For a directly entered public IP address, `lookup --deep` can also run one
132
+ bounded reverse-DNS query. Terminal output shows the explicit source state and
133
+ up to five normalised PTR names; JSON retains up to eight. PTR names are
134
+ operator-published routing context, not proof of hosting control or ownership.
135
+ Fast lookups do not run this query.
136
+
137
+ `lookup --plan` performs classification and prints a versioned preflight without
138
+ starting collection. It lists the normalised target, selected fast or deep
139
+ mode, planned source families, conditional operations, and the data each family
140
+ may receive. The plan cannot predict live feature configuration, cache state,
141
+ redirects, referrals, source availability, or an exact request count. It can be
142
+ combined with `--json`, but not with report, detail, strict-exit, progress-event,
143
+ or quiet options.
144
+
145
+ Only one query is accepted by `lookup`. Multiple-input processing belongs to
146
+ the explicit `bulk` command rather than being silently inferred by `lookup`.
147
+ Standard input is capped at 4 KiB and must contain one non-empty line.
148
+
149
+ `registry-support` is an offline catalogue view. It accepts one domain, bare
150
+ suffix, or leading-dot suffix as an argument or on stdin and makes no network
151
+ request. Its output is described below.
152
+
153
+ `doctor` is offline by default. It checks the Node runtime, platform, terminal
154
+ capabilities, and availability of offline command families. `doctor --network`
155
+ adds independent bounded public-DNS, HTTPS/RDAP bootstrap, and WHOIS port 43
156
+ checks against fixed IANA diagnostic infrastructure. The three probes run
157
+ within the same per-probe ceiling so one stalled transport does not block the
158
+ others. The report retains only state, HTTP status, and a bounded explanation,
159
+ not resolved addresses or response content. A failed optional network check
160
+ returns the partial-failure exit code rather than claiming that all CLI
161
+ collection is unavailable.
162
+
163
+ `completion bash`, `completion zsh`, `completion fish`, and `completion
164
+ powershell` print static shell completion scripts to stdout. The command does
165
+ not edit a shell profile or make a network request. Review the generated script
166
+ before sourcing it or placing it in the relevant shell completion directory.
167
+ `manual` prints a generated roff manual page and likewise changes no local
168
+ configuration.
169
+
170
+ `commands` prints the installed command catalogue without running collection or
171
+ reading evidence. `commands --json` emits version 1 of the
172
+ `whoisleuth.cli.command-catalogue` schema, including each command's usage,
173
+ example, declared collection mode, collection scope, and boundary. This is the
174
+ supported discovery contract for local wrappers; it is not a permission grant
175
+ or a substitute for focused help.
176
+
177
+ Focused command help and the generated manual classify every operation as
178
+ offline or networked and state the relevant target, input, and concurrency
179
+ ceiling. These labels describe observable behaviour rather than granting
180
+ authorisation. The packaged `DISCLOSURE` defines the intended defensive scope;
181
+ operators remain responsible for law, provider terms, and authorisation.
182
+
183
+ ## Deployment boundary
184
+
185
+ The CLI runs locally. The serverless deployment publishes only `frontend/build`
186
+ and packages functions only from `netlify/functions`, so `bin/` and `cli/` are
187
+ not part of the hosted static site or function bundle. The application root and
188
+ ordinary package-check candidate remain private. Explicitly reviewed CLI
189
+ release candidates can be published under the scoped public package described
190
+ in the installation section.
191
+
192
+ Commands that query RDAP, WHOIS, DNS, HTTP, TLS, or Certificate Transparency do
193
+ so directly from the machine running the CLI. They do not use the hosted login,
194
+ hosted session, or deployment usage controls; upstream providers can see and
195
+ rate-limit the local machine's network address. Offline `discover`, `compare`,
196
+ `page-compare`, `mail-review`, `review-evidence`, `domain-control`, `assurance`,
197
+ `sharing-review`, `workflow-plan`, `diff`, `reconcile`, `timeline`,
198
+ `risk-calibrate`, `lookalike-calibrate`, `registry-doctor`, `verify-artifact`, `source-report`, `export`,
199
+ `commands`, `completion`, and `manual` operations make no network requests. Commands write
200
+ to stdout unless the analyst deliberately selects a local output file.
201
+
202
+ ## Output
203
+
204
+ Human-readable terminal output is the default. On an interactive terminal it
205
+ uses restrained semantic colour, groups Lookup evidence by purpose, wraps
206
+ prose to the detected terminal width, and shows transient progress on stderr
207
+ for slower collection. State remains explicit in text, so colour is never the
208
+ only distinction. Redirected terminal output, JSON, and JSONL contain no ANSI
209
+ sequences or progress text. `--no-color` and a non-empty conventional
210
+ `NO_COLOR` environment variable disable colour; `WHOISLEUTH_NO_PROGRESS=1`
211
+ disables the transient indicator. Command examples remain one copyable line
212
+ even when prose is wrapped for a narrow terminal.
213
+
214
+ Every command accepts `--output <file>` as a safer alternative to shell
215
+ redirection. WHOISleuth buffers bounded output, creates a private temporary file
216
+ beside the destination, syncs it, and publishes it atomically with mode `0600`.
217
+ An existing destination is refused. `--force` is valid only with `--output` and
218
+ allows an intentional atomic replacement. A failed or cancelled command does
219
+ not publish a partial output file. Output is capped at 32 MiB.
220
+
221
+ `lookup --markdown` and `lookup --html` build the existing normalised evidence
222
+ report directly after one completed domain lookup. They do not start a second
223
+ collection. These report formats reject IP and ASN inputs before collection;
224
+ JSON and terminal output remain available for those query types.
225
+
226
+ Lookup terminal output supports three detail levels. The standard view keeps
227
+ the normal bounded evidence summary. `--summary` keeps the source states and
228
+ key findings while omitting endpoints and supporting detail. `--verbose` adds
229
+ the document generation time and the existing bounded per-source collection
230
+ timings. These switches are presentation-only and cannot be combined with
231
+ `--json`; they do not change collection or the result document.
232
+
233
+ `--json` writes one versioned document to standard output:
234
+
235
+ ```json
236
+ {
237
+ "schema": "whoisleuth.cli.lookup",
238
+ "version": 1,
239
+ "generatedAt": "2026-07-14T00:00:00.000Z",
240
+ "mode": "fast",
241
+ "query": "example.com",
242
+ "type": "domain"
243
+ }
244
+ ```
245
+
246
+ The complete document also carries the normalised `rdap`, `whois`,
247
+ `availability`, and `diagnostics` sections returned by the shared lookup
248
+ orchestrator. Machine output goes to stdout. Usage and lookup errors go to
249
+ stderr, so redirected JSON is not mixed with diagnostics.
250
+
251
+ Human-readable domain lookup output separately shows the registrar RDAP status
252
+ and endpoint whenever the shared lookup diagnostics represent that follow-up.
253
+ Deep mode may report success, unsupported, not found, or an explicit failure;
254
+ fast mode reports the existing skipped state. IP, ASN, and lookup responses
255
+ without registrar diagnostics remain unchanged. These source states are
256
+ provenance only and do not decide availability or imply safety.
257
+
258
+ When a deep domain response includes registry interpretation version 1, the
259
+ terminal summary also shows the lifecycle label, separately attributed RDAP and
260
+ WHOIS disclosure states, reconciliation state, and complete, partial, and
261
+ unavailable publication counts. It does not print published contact routes.
262
+ The JSON document retains the bounded interpretation and its limitations.
263
+
264
+ A deep domain lookup can also show the status, selected public address, and
265
+ registered network name from the bounded observed network context. It uses the
266
+ same address and IP RDAP source represented in the JSON document. This is
267
+ point-in-time edge or network-registration context, not proof of the origin
268
+ host, hosting control, ownership, intent, or maliciousness. Fast and compact
269
+ commands do not run the enrichment.
270
+
271
+ The deep terminal summary also reports the website activity state, page title,
272
+ DNS, HTTP, and TLS source states, bounded publisher-declared structured
273
+ identity entities, up to six bounded technology indicators, a browser-library
274
+ profile count, and the four passive security-posture counts.
275
+ These are concise projections of the same evidence already present in the
276
+ lookup response. The browser-library line counts apparent components and
277
+ catalogue advisory matches without retaining script references or raw
278
+ signatures. These summaries make no extra request, omit raw evidence
279
+ descriptions, and do not turn a technology or advisory match, or a missing
280
+ posture signal, into proof of exploitability, hosting control, ownership, or
281
+ maliciousness. Use `--json` when the full bounded evidence, limitations, and
282
+ source diagnostics are required.
283
+
284
+ `lookup --strict-exit` is an opt-in automation policy. It still emits the
285
+ ordinary complete output document, but returns exit code 4 when a requested
286
+ diagnostic source is partial, unavailable, rate-limited, timed out, or failed.
287
+ The default exit contract remains unchanged, so inconclusive source health does
288
+ not silently break existing scripts.
289
+
290
+ `lookup --events` and `bulk --events` write versioned
291
+ `whoisleuth.cli.progress` version 1 JSONL lifecycle events to stderr while the
292
+ ordinary final result stays on stdout. Events contain only the command,
293
+ sequence, time, source state or Bulk item index, categorical failure reasons,
294
+ and final exit status. They do not contain a target, query, endpoint, file
295
+ path, error detail, or evidence value. Human progress and error text are
296
+ suppressed while events are active. `--events` cannot be combined with
297
+ `--output`, because the final event must describe the same completed stdout
298
+ delivery observed by the caller.
299
+
300
+ Pressing Ctrl-C asks an in-flight Lookup or Bulk run to stop, suppresses any
301
+ partial final result, flushes an enabled Bulk checkpoint, and exits with code
302
+ 130. Already started network operations remain subject to their existing hard
303
+ timeouts while the shared in-process API unwinds; the executable terminates
304
+ after CLI cleanup. A second Ctrl-C removes any known unpublished temporary
305
+ output file on a best-effort basis before exiting immediately.
306
+
307
+ Bulk can persist compact progress with `--checkpoint <file>`. A new checkpoint
308
+ is private, bounded to 16 MiB, versioned, and tied to the exact ordered input
309
+ and scan mode by SHA-256. It stores only compact per-item results, never full
310
+ Lookup responses. Reusing the same path is refused unless `--resume` is also
311
+ specified. Resume revalidates the complete untrusted document and skips only
312
+ the exact completed rows; changed input, mode, schema, digest, or malformed
313
+ results fail closed.
314
+
315
+ If checkpoint persistence fails after collection, WHOISleuth still emits the
316
+ completed final result, reports the checkpoint limitation, and exits with code
317
+ 4. The last successfully published checkpoint remains available, but it may
318
+ not contain the final completed items.
319
+
320
+ `diff <left.json> <right.json>` compares two different saved domain Lookup
321
+ documents entirely offline. It reuses the bounded Bulk comparison model for
322
+ registration, DNS, page identity, mail, certificate, and relationship fields,
323
+ keeps missing and unavailable evidence distinct, and makes no ownership or
324
+ maliciousness inference. The output records both original observation times.
325
+
326
+ `lookup --observer <label> --vantage <label>` can add two bounded analyst labels
327
+ to a saved Lookup document. The labels identify the person or process that ran
328
+ the observation and the collection context chosen by that analyst; they are
329
+ not verified identities, network measurements, or proof of independent
330
+ collection. They do not change which sources are queried.
331
+
332
+ `reconcile <observation.json> <observation.json> [...]` accepts from 2 to 5
333
+ saved Lookup documents for the same domain and compares their bounded fields
334
+ offline. It keeps agreement, disagreement, and non-comparable evidence
335
+ separate, retains observation times and optional analyst labels, and copies no
336
+ filenames or raw registration payloads. Distinct complete label pairs are
337
+ reported as labelled collection contexts only; majority agreement is not
338
+ treated as truth or authority.
339
+
340
+ `timeline <observation.json> <observation.json> [...]` accepts from 2 to 20
341
+ saved Lookup documents for one domain, orders them by their validated
342
+ observation times, and compares each adjacent pair offline. The 32 MiB aggregate
343
+ input ceiling is enforced while files are read and again by the timeline
344
+ builder. Output retains normalised field states and observation times but no
345
+ input filenames or raw registry payloads. A difference can reflect a domain
346
+ change or changed collection conditions, so the command makes no current-state,
347
+ ownership, intent, safety, or maliciousness conclusion.
348
+
349
+ When diagnostics version 5, 6, 7, or 8 reports a documented registry collection
350
+ constraint, terminal output also shows the suffix, WHOIS and RDAP access
351
+ profiles, and the bounded limitation. This is static access-policy context: it
352
+ does not make another request, and restricted, unpublished, or unavailable
353
+ machine access is not evidence that a domain is unregistered or safe.
354
+
355
+ ## Exit codes
356
+
357
+ | Code | Meaning |
358
+ | ---: | --- |
359
+ | 0 | Command completed. Individual sources may still be partial or inconclusive; inspect diagnostics. |
360
+ | 2 | Invalid command, option, input, or stdin shape. |
361
+ | 3 | The requested lookup, collection, or comparison operation could not run. |
362
+ | 4 | A bounded operation completed partially, such as Bulk item failures or failed explicit Doctor network checks. |
363
+ | 70 | Unexpected CLI bootstrap failure. |
364
+ | 130 | The analyst cancelled the command. No partial final result was emitted. |
365
+
366
+ This release supports `lookup`, `bulk`, `ct-search`, `discover`, `discover-scan`, `posture`,
367
+ `http`, `tls`, `registry-support`, `registry-doctor`, `risk-calibrate`,
368
+ `lookalike-calibrate`, `verify-artifact`,
369
+ `inspect-archive`, `sign-artifact`, `verify-signature`, `source-report`,
370
+ `compare`, `page-compare`, `mail-review`, `review-evidence`, `domain-control`,
371
+ `assurance`, `sharing-review`, `workflow-plan`, `diff`, `reconcile`, `timeline`,
372
+ `export`, `doctor`, `commands`, `completion`, and `manual`. Additional command families
373
+ are added as separate bounded increments rather than exposing incomplete
374
+ aliases.
375
+
376
+ ## Registry capability coverage
377
+
378
+ `registry-support` projects the embedded, versioned registry compatibility
379
+ catalogue for one domain or suffix. It distinguishes an explicit
380
+ fixture-backed or access-documented suffix profile from the generic IANA
381
+ discovery profile and reports the RDAP and WHOIS discovery/access paths,
382
+ WHOIS query and parser profiles, fixture scenarios,
383
+ verification references, documentation references, and the catalogue's
384
+ limitation. The same document includes a dated, official-source aggregate of
385
+ generic, generic-restricted, sponsored, and infrastructure RDAP coverage. That
386
+ snapshot is separate from the suffix profile and from live reachability.
387
+
388
+ Terminal output is bounded and control-safe. `--json` emits version 3 of the
389
+ `whoisleuth.cli.registry-support` schema. Unknown but syntactically valid
390
+ suffixes retain the generic `discovery_only` profile; malformed input exits
391
+ with code 2. The command never probes a registry or tests current reachability.
392
+ Coverage is context only and cannot decide registration, availability,
393
+ ownership, safety, or maliciousness.
394
+
395
+ `registry-doctor [lookup.json]` compares one saved domain Lookup with the same
396
+ reviewed local capability profile. It reports whether RDAP and WHOIS collection
397
+ states align with allowed, permission-required, or unsupported access, counts
398
+ bounded normalised publication fields, and reports whether a registry object
399
+ identifier was observed. A missing identifier is a publication omission, not
400
+ a failed lookup. The command is offline, makes no retry, and cannot establish
401
+ current registry reachability.
402
+
403
+ The offline RDAP supplied-evidence review implements the request shape defined
404
+ by RFC 9536 for `resources`, `domains`, `nameservers`, and `entities`. It also
405
+ validates a supplied server's `reverse_search_properties_mapping` response
406
+ before preparing a request. Only registered IANA property paths and exact
407
+ supported resource/property pairs are accepted. Preparing the request neither
408
+ proves that a particular server supports it nor sends a reverse search; active
409
+ execution remains gated on a reviewed server policy, privacy boundary, and
410
+ deterministic fixtures.
411
+
412
+ The separate maintenance command `npm run rdap-extensions:drift` compares the
413
+ pinned official RDAP extension fixture with the reviewed local interpretation
414
+ catalogue entirely offline. `--live` performs one bounded manual fetch from the
415
+ fixed official CSV URL and reports added, removed, renamed, status-changed,
416
+ unrecognized, and local-only identifiers. `--json` emits
417
+ `whoisleuth.rdap-extension-drift-audit` version 1. Drift exits with status 1 and
418
+ requires specification and fixture review; the command never enables an
419
+ extension, changes authority or availability logic, or issues reverse search.
420
+
421
+ `npm run service-dependencies:audit` validates the passive service-dependency
422
+ catalogue entirely offline. It checks the catalogue digest, duplicate
423
+ identifiers and target suffixes, evidence classes, source and licence
424
+ treatment, source dates, and a fixed 180-day review age. `--json` emits
425
+ `whoisleuth.service-dependency-signature-audit` version 1. A stale or changed
426
+ signature requires manual provider and benign-fixture review; the command
427
+ never resolves a target, checks an account, or tests claimability.
428
+
429
+ ## Offline Risk calibration
430
+
431
+ `risk-calibrate` replays a versioned analyst-labelled fixture dataset through
432
+ the exact Risk model shared with the web application. It does not perform a
433
+ lookup, contact the hosted deployment, persist the dataset, alter model weights,
434
+ choose a threshold, or tune the model automatically. The report compares fixed
435
+ thresholds from 40 through 90 and identifies 70 as the current review band. It
436
+ also reports F1, balanced accuracy, Wilson 95% intervals, deliberately small
437
+ scan-depth and review-reason strata, and a current-versus-previous model replay.
438
+ Strata with fewer than 20 included labels are marked insufficient rather than
439
+ presented as reliable performance estimates.
440
+
441
+ The JSON report derives a bounded `interoperabilityTags` list from the
442
+ analyst-owned disposition and review reason. The mapping uses reviewed MISP
443
+ taxonomy terms only where the meaning is unambiguous. Tags do not change a
444
+ score, disposition, or case, and no MISP service is contacted.
445
+
446
+ Input is capped at 2 MiB and 500 records. Each record requires a unique bounded
447
+ ID, an ASCII DNS hostname, one existing analyst disposition, and a strict
448
+ whitelist of current scoring inputs. Unknown fields are discarded; arrays,
449
+ strings, ages, booleans, provider records, and external findings are revalidated
450
+ and bounded before scoring. The accepted dataset envelope is:
451
+
452
+ ```json
453
+ {
454
+ "schema": "whoisleuth.risk-calibration-dataset",
455
+ "version": 2,
456
+ "records": [
457
+ {
458
+ "id": "fixture-1",
459
+ "domain": "login.example.test",
460
+ "analystDisposition": "confirmed_abuse",
461
+ "reviewReasonCode": "confirmed_credential_abuse",
462
+ "evidence": {
463
+ "availability": "registered",
464
+ "mutationTypes": ["dictionary"],
465
+ "hasPasswordField": true
466
+ }
467
+ }
468
+ ]
469
+ }
470
+ ```
471
+
472
+ `confirmed_abuse` is treated as a positive calibration label; `false_positive`
473
+ and `expected` are negative labels. `unreviewed`, `suspicious`, and
474
+ `closed_no_action` remain contextual and are excluded from threshold quality
475
+ metrics, as are records for which Risk is not applicable. Terminal output caps
476
+ its record list at 100 while `--json` retains the complete bounded report,
477
+ factor breakdowns, score bands, confidence intervals, bounded strata, model
478
+ comparison counts, and per-threshold confusion metrics. Analyst
479
+ dispositions and heuristic scores are review context: neither proves
480
+ maliciousness or safety.
481
+
482
+ The authenticated Monitor workspace can deliberately create the accepted
483
+ dataset from explicitly selected cases. Selection is available only when a case
484
+ has a reviewed disposition and retained normalised evidence. That local export
485
+ contains the case ID, domain, disposition, optional reviewed reason code, and a
486
+ bounded whitelist of current scoring inputs. A matched review-language signal
487
+ is exported as a fixed indicator, not as captured page text. It excludes notes,
488
+ tags, assertions, actions, contacts, raw
489
+ source data, provider payloads, and stored Risk scores. Before download, a local
490
+ review step shows the selected, included, and excluded counts, the domains and
491
+ dispositions that will be written, and the excluded evidence classes. The CLI
492
+ still validates every exported record and never trains, tunes, or changes the
493
+ Risk model.
494
+
495
+ ## Lookalike review-yield calibration
496
+
497
+ `lookalike-calibrate [dataset.json]` summarises reviewed candidate outcomes by
498
+ mutation family without retaining domains, candidate identifiers, analyst
499
+ notes, or evidence values in its output. Input uses
500
+ `whoisleuth.lookalike-calibration-input` version 1, is capped at 2 MiB and 5,000
501
+ unique opaque records, and requires one reviewed disposition plus from 1 to 12
502
+ bounded mutation-family identifiers per record.
503
+
504
+ The report keeps exact disposition counts, review-lead rate, false-positive
505
+ rate, and a per-family sample state. Fewer than 20 reviewed observations remains
506
+ `insufficient`. The command is diagnostic only: it does not tune candidate
507
+ generation, filtering, ranking, or Risk, and a useful historical yield does not
508
+ establish that a current candidate is malicious or safe.
509
+
510
+ ## Offline artefact verification
511
+
512
+ `verify-artifact` validates a supported local artefact without printing its
513
+ evidence contents. Input is capped at 15 MiB. It currently recognises ordinary
514
+ and encrypted workspace archives, case-response packets, acquisition-decision
515
+ exports, domain-comparison exports, Bulk mail-exposure exports, and Bulk review
516
+ manifests. It also verifies the internal digests of a complete investigation
517
+ capsule, including its evidence, brief, graph, and optional analyst-record
518
+ projections, and that capsule can be passed to `sign-artifact`. It also validates the bounded versioned structure of saved CLI
519
+ Lookup JSON. Because saved Lookup documents do not embed a checksum or
520
+ signature, that result is reported as `structure_valid` with content integrity
521
+ explicitly unchecked. Saved Lookup validation retains its stricter 8 MiB
522
+ document ceiling.
523
+
524
+ For an ordinary workspace archive, the command validates the versioned
525
+ structure, section byte counts, record counts, and per-section checksums. For a
526
+ supported signed review artefact, it validates the declared SHA-256 digest. An
527
+ encrypted archive without `--passphrase-file` can establish only that the
528
+ envelope is structurally valid; it reports `envelope_valid` and explicitly
529
+ leaves authenticated encryption and inner checksums unchecked.
530
+
531
+ Supplying a separate passphrase file, capped at 1 KiB, allows authenticated
532
+ decryption followed by the ordinary workspace checks. The passphrase is never
533
+ accepted as a command-line value, printed, or retained. Verification detects
534
+ changes against the artefact's declared contract; it does not authenticate the
535
+ analyst or establish that an observation was accurate or remains current.
536
+
537
+ ## Privacy-safe source reliability report
538
+
539
+ `source-report` summarises source states, durations, truncation, and rate-limit
540
+ counts from one version-1 CLI Lookup, Bulk, or Bulk-item document, a JSON array
541
+ of up to 100 such documents, or an array containing only previously generated
542
+ source-reliability reports. Lookup documents and reports cannot be mixed.
543
+ Merged reports preserve exact state and sample totals and add bounded ranges
544
+ for each input report's median and p95 duration, a bounded source timeline,
545
+ fast/deep/unknown cohorts, and the earliest and latest valid report generation
546
+ times. Exact duplicate reports are rejected so one saved report is not
547
+ accidentally counted twice. Per-source failure, partial, truncation, and
548
+ rate-limit rates remain separate. The command does not pretend that aggregate
549
+ timing distributions can reconstruct the original target-level samples. Input
550
+ is capped at 12 MiB and traversal, source, duration, and output counts are
551
+ bounded.
552
+
553
+ The report retains only fixed source identifiers and aggregate operational
554
+ counts. It does not output targets, queries, endpoints, source limitations, raw
555
+ evidence, or provider payloads. Observation-envelope duration and the
556
+ potentially overlapping Lookup timing are reported separately and never added
557
+ together. It is a local engineering diagnostic, not analytics, a source-quality
558
+ score, or evidence that a collector is currently healthy.
559
+
560
+ ## Workspace archive inspection
561
+
562
+ `inspect-archive` verifies an ordinary or encrypted workspace archive and then
563
+ reports its versioned sections, counts, byte sizes, and checksum states without
564
+ printing stored records. An encrypted archive requires
565
+ `--passphrase-file`; passphrases are never accepted on the command line.
566
+
567
+ An optional `--search` is exact and case-insensitive across a fixed allowlist
568
+ of target fields. Default matches disclose only the section, field name, and a
569
+ SHA-256 digest. `--reveal` must be selected explicitly to print the matched
570
+ value. Notes, contacts, raw evidence, arbitrary object fields, and partial text
571
+ search are excluded. Unicode domain searches are canonicalized to their
572
+ DNS-safe ASCII form before exact comparison. `--require-match` makes a
573
+ no-match result fail for automation and is valid only with `--search`.
574
+ Traversal, input size, matches, depth, arrays, and strings are bounded.
575
+
576
+ ## Optional evidence-package signing
577
+
578
+ `sign-artifact` signs a locally verified case-response packet or supported
579
+ review manifest with an externally managed Ed25519 private key:
580
+
581
+ ```bash
582
+ node bin/whoisleuth.mts sign-artifact packet.json --private-key-file analyst-private.pem > packet.signed.json
583
+ node bin/whoisleuth.mts verify-signature packet.signed.json --public-key-file analyst-public.pem
584
+ ```
585
+
586
+ The signed package embeds the public key, key identifier, signature time, and
587
+ original artefact. Verification first checks the exact package field set and
588
+ signature, then the artefact's own checksums or manifest. A result is reported
589
+ as `signature_valid`; signer trust is separately reported as `trusted_key` or
590
+ `embedded_key_only`. Without a separately supplied trusted public key,
591
+ verification establishes only that the package is internally self-consistent.
592
+ WHOISleuth does not generate, store, recover, rotate, publish, or establish
593
+ trust in signing keys.
594
+
595
+ ## Bulk lookup
596
+
597
+ `bulk` accepts a newline-delimited file or stdin. It preserves input order,
598
+ removes case-insensitive duplicates, and returns a result for every retained
599
+ query. Input is capped at 1 MiB. Fast mode is capped at 500 queries with four
600
+ workers by default. Deep mode is capped at 50 queries with two workers by
601
+ default. Explicit concurrency cannot exceed eight in fast mode or three in
602
+ deep mode.
603
+
604
+ Bulk uses the shared compact lookup response, so it does not retain raw RDAP
605
+ objects or WHOIS response bodies. `--json` returns one bounded collection;
606
+ `--jsonl` emits one self-contained versioned item per line. `--domains` emits
607
+ only successfully normalised domain names, while `--queries` preserves the
608
+ selected bounded input queries and is suitable for an exact retry queue.
609
+ Version 2 adds a
610
+ bounded `dnsSummary` projection for observed A, AAAA, NS, and MX records plus
611
+ null MX, SPF, and DMARC state. `--csv` writes fixed columns for automation,
612
+ including the DNS summaries and explicit outcome.
613
+
614
+ `--registered-only`, `--inconclusive-only`, and `--errors-only` are mutually
615
+ exclusive output filters. The first retains registered, for-sale, and expiring
616
+ authority-aware states. The second retains unknown authority states and failed
617
+ rows. The third retains only rows whose lookup operation failed, which is useful
618
+ for producing an exact retry queue without treating an inconclusive successful
619
+ lookup as an operational error. Filtering does not change collection and never
620
+ converts an unavailable or failed source into an unregistered result. Machine
621
+ documents record collected and emitted counts. A mixture of successful and
622
+ failed queries exits with code 4 while preserving every collected result before
623
+ output filtering. For example, `bulk domains.txt --queries --errors-only`
624
+ produces only the exact failed inputs while retaining the partial-failure exit.
625
+
626
+ ## Certificate Transparency search
627
+
628
+ `ct-search` accepts one keyword as an argument or on stdin and calls the same
629
+ bounded Certificate Transparency module as the web application. It contacts
630
+ the upstream public log search service directly from the local machine; it
631
+ does not call the hosted WHOISleuth deployment. Quote a multi-word keyword so
632
+ the shell passes it as one argument.
633
+
634
+ Terminal output summarises certificate rows, observed hostnames, canonical
635
+ registrable-domain matches, observation times, bounded certificate-issuance
636
+ groups, and completeness. An issuance group contains names observed together
637
+ in one public certificate record; a cross-domain group is a review lead, not
638
+ an attribution or ownership finding. Terminal output shows at most 100 matches
639
+ and five hostnames per match, with explicit omission notes. `--json` returns
640
+ the complete bounded structured result in the versioned
641
+ `whoisleuth.cli.ct-search` schema. The certificate-group cap is reported
642
+ separately from the registrable-domain result cap. CT observations do not prove
643
+ that a website is active or malicious.
644
+
645
+ ## Lookalike discovery
646
+
647
+ `discover` runs the same pure, bounded lookalike generator as the Discover
648
+ tool without making network requests. It accepts a brand label or a
649
+ domain with one suffix label. The default TLD set is `com,net,org`; replace it
650
+ with `--tlds com,net` when narrower coverage is wanted.
651
+
652
+ Generation presets are `common`, `impersonation`, and `all` (the default).
653
+ Keyboard-aware mutations support `qwerty` (the default), `azerty`, and
654
+ `qwertz`; `--keyboard all` uses their bounded combined neighbour set, including
655
+ adjacent number-row keys. Supply a UTF-8 text file with
656
+ `--dictionary terms.txt` to add up to 100 local terms when
657
+ using the `impersonation` or `all` preset. Dictionary input is capped at 4,096
658
+ bytes and individual terms at 32 characters. Machine output records accepted
659
+ and rejected term counts but never copies the private terms into its metadata.
660
+
661
+ Use `--families <ids>` instead of `--preset` to run an exact comma-separated
662
+ selection. Supported IDs are `character_addition`, `character_omission`,
663
+ `character_duplication`, `character_transposition`, `pluralization`,
664
+ `tld_embedding`, `www_prefix`, `hyphenation`, `separator_omission`,
665
+ `word_reordering`, `keyboard_substitution`, `keyboard_insertion`, `vowel_swap`,
666
+ `bitsquatting`, `ascii_homoglyph`, `unicode_homoglyph`,
667
+ `unicode_whole_label`, `unicode_homoglyph_depth_2`, `dictionary`,
668
+ `dictionary_token_replacement`, `tld_typo`, and `tld_substitution`. Machine
669
+ output records the normalised custom selection. A custom dictionary file
670
+ requires `dictionary` or `dictionary_token_replacement` in that selection.
671
+
672
+ `--domains` writes the unique candidate names only. `--snapshot <file>` keeps
673
+ one private, versioned local observation of the normalised seed, generation
674
+ configuration, custom-dictionary digest, and candidate set. The first run
675
+ creates the snapshot; later runs report added and removed candidates before
676
+ atomically replacing it. The snapshot never stores private dictionary terms.
677
+ Use the same command from a local scheduler such as cron or a system timer to
678
+ perform repeatable discovery runs. WHOISleuth does not install a scheduler,
679
+ run a background daemon, or infer that an added candidate is registered,
680
+ active, available, or malicious.
681
+
682
+ Dotted subdomain permutations are intentionally excluded because the lookup
683
+ pipeline validates a hostname's registrable parent. The CLI does not present
684
+ authoritative parent registration evidence as proof that a generated hostname
685
+ exists.
686
+
687
+ Terminal output is capped at 200 candidates with an explicit notice. Versioned
688
+ JSON and JSONL use `whoisleuth.cli.discover` schema version 2 and retain the
689
+ complete bounded candidate set and mutation provenance. Internationalised
690
+ candidates show the DNS-safe ASCII form and readable Unicode form together in
691
+ terminal output. The Impersonation and All presets can add up to six
692
+ deterministic whole-label Unicode candidates when every letter has a reviewed
693
+ same-script replacement. The command generates candidates only. It does not
694
+ claim that a domain is registered, active, or malicious.
695
+
696
+ `unicode_homoglyph_depth_2` is an explicit `--families` option and is never
697
+ enabled by a preset. It replaces exactly two ASCII letters with characters
698
+ from the same reviewed script, ranks curated mappings first, and returns at
699
+ most 256 candidates. JSON and terminal output report generated candidates,
700
+ cross-script or invalid combinations omitted by policy, and lower-ranked
701
+ label variants omitted by the family or overall generation budget.
702
+
703
+ ## Supervised candidate scan
704
+
705
+ `discover-scan` composes the same bounded generator with the compact Lookup
706
+ collector. Unlike `discover`, it is an explicitly networked command. It selects
707
+ the first deterministic candidates up to `--scan-limit`, processes fixed
708
+ chunks sequentially, and retains each registration and DNS source state. Fast
709
+ mode is the default and accepts at most 500 candidates. Explicit `--deep` mode
710
+ is capped at 50 candidates and concurrency 3 because it can add WHOIS, DNS,
711
+ HTTP, TLS, page, and technology work for every selected candidate.
712
+
713
+ Use `--chunk-size 1-100`, `--concurrency 1-8`, and the existing private
714
+ `--checkpoint <file> [--resume]` boundary to control a run. The checkpoint is
715
+ tied to the exact ordered candidate set and scan mode. A new generation order,
716
+ limit, or mode requires another checkpoint. `--resolver` accepts at most three
717
+ literal IPv4 or IPv6 recursive-resolver addresses and applies them only to DNS
718
+ observations in this local run. Resolver hostnames and automatic third-party
719
+ resolver selection are not supported. The selected resolvers can observe the
720
+ queried candidates and may apply their own logging and retention policies.
721
+
722
+ An optional newline-delimited `--allowlist` accepts at most 500 domains and 64
723
+ KiB. Matching candidates remain in evidence and are labelled `suppressed` in
724
+ the review queue; the full list and unmatched entries are not copied into
725
+ output, while each matching scanned candidate necessarily exposes that match.
726
+ Allowlisting never deletes a result or asserts that it is safe. Output filters
727
+ select registered, inconclusive, acquisition-review, or
728
+ suppressed rows without changing whole-run counts or source health. JSON,
729
+ JSONL, CSV, and domain-list forms retain mutation provenance and explicit
730
+ review lanes. The queue provides next manual actions, not automatic lookup,
731
+ acquisition, blocking, submission, or enforcement.
732
+
733
+ Deep compact results can group exact shared addresses, nameservers, and mail
734
+ servers observed across at least two candidates. These bounded relationships
735
+ are pivot leads only. Common hosting, delivery, registrar, and mail services
736
+ can produce the same relationship without common ownership or control.
737
+
738
+ `--observation-snapshot <file>` keeps a private versioned local projection of
739
+ registration state, confidence, and bounded A, AAAA, NS, MX, null-MX, SPF, and
740
+ DMARC summaries for the exact candidate set, mode, and resolver selection.
741
+ Version 2 records registration and DNS observation times and latest component
742
+ states separately; version 1 is read and normalised on the next write. A later
743
+ identical run reports material field changes only for components collected
744
+ with enough completeness to support the comparison. If registration or DNS is
745
+ partial or unavailable, the prior usable evidence for that component is
746
+ preserved and the attempt is reported as unavailable rather than converting
747
+ missing data into a removal. Raw registry
748
+ records, contacts, page content, and request details are excluded. Snapshot
749
+ differences are review prompts, not proof of ownership, control, intent,
750
+ safety, or maliciousness.
751
+
752
+ ## Domain posture audit
753
+
754
+ `posture` runs the standard-profile form of the owned-domain DNS and
755
+ email-security audit used by Brand Profiles. It queries registry status and
756
+ DNSSEC evidence, nameserver delegation, SPF, DMARC, MX, CAA, MTA-STS, TLS-RPT,
757
+ and BIMI directly from the local machine. Literal SPF include and redirect
758
+ branches are expanded within fixed depth, policy-query, DNS-term, void-answer,
759
+ cycle, and time bounds. External DMARC reporting authorisation and external
760
+ nameserver, mail, SPF, and reporting dependencies are separately reported.
761
+ Supply up to ten known active and retired DKIM selectors in total with
762
+ `--selectors selector1,selector2` and `--retired-selectors selector0`;
763
+ selectors cannot be reliably discovered from DNS, so no-selector output
764
+ reports DKIM as not checked. Use `--mail-profile standard`,
765
+ `--mail-profile defensive-no-mail`, or `--mail-profile parked` to make the
766
+ intended mail posture explicit rather than inferring it from DNS.
767
+
768
+ Terminal output shows each pass, review, action, or informational result and
769
+ caps displayed records at five per check with an explicit omission notice.
770
+ Versioned JSON retains the complete bounded report, including SPF traversal,
771
+ DMARC authorisation, and dependency provenance. Warnings and dangers are
772
+ findings rather than command failures; transient resolver or policy-fetch
773
+ failures remain informational and should be retried before changing DNS.
774
+
775
+ ## HTTP intelligence
776
+
777
+ `http` runs the same bounded safe homepage probe used by a deep lookup. It
778
+ tries HTTPS before HTTP, follows only validated public-network redirects, caps
779
+ each attempt at six seconds, and captures at most 300,000 response bytes for
780
+ hashing and metadata. A non-success HTTP response still proves that a web
781
+ service answered; failure of both schemes remains inconclusive rather than
782
+ evidence that no website exists.
783
+
784
+ The command reports normalised redirect, response, content, selected security-
785
+ header, body-hash, completeness, and attempt provenance. Captured homepage text
786
+ is never written to terminal or JSON output. Query strings are removed by the
787
+ shared HTTP evidence normalizer, and terminal values are additionally bounded
788
+ and control-safe.
789
+
790
+ ## TLS intelligence
791
+
792
+ `tls` runs the same bounded one-connection TLS collector used by deep lookups.
793
+ It resolves the hostname once through the public-address guard, validates every
794
+ answer, connects directly to the first validated address on port 443, and keeps
795
+ the original hostname for SNI and certificate identity checking. Resolution
796
+ and handshake work share a five-second deadline.
797
+
798
+ The report retains the negotiated protocol, ALPN and cipher, runtime trust and
799
+ hostname checks, certificate validity, bounded subject and issuer names, SANs,
800
+ SAN class counts, signature algorithm and OID, extended-key-usage purposes,
801
+ classified Authority Information Access presence counts, SHA-256 certificate
802
+ and public-key fingerprints, a bounded certificate chain, and neutral
803
+ findings. AIA responder and issuer locations are classified as HTTP, HTTPS, or
804
+ other and then discarded; they are not retained or followed. The collector
805
+ stores no certificate bytes, session material, or application data and does
806
+ not enumerate supported protocol or cipher suites.
807
+ A failed collection is inconclusive rather than proof that no TLS service
808
+ exists.
809
+
810
+ ## Registry-source comparison
811
+
812
+ `compare` reads one version-1 `whoisleuth.cli.lookup` domain document from a
813
+ file or stdin and emits a version-3 `whoisleuth.cli.compare` document. Its
814
+ primary comparison reconciles normalised registry RDAP and WHOIS fields. When
815
+ the saved deep lookup also represents the optional registrar RDAP follow-up,
816
+ an additional sibling comparison reconciles the portable registry and
817
+ registrar RDAP publication fields. The command does not repeat the lookup,
818
+ contact the hosted deployment, or treat a conflict as a command failure. Use a
819
+ deep lookup when multiple sources are wanted; a fast lookup deliberately
820
+ records WHOIS as skipped, and comparison preserves that state as unavailable
821
+ rather than misreporting RDAP-only publication.
822
+
823
+ Input is capped at 8 MiB and must retain the lookup schema, mode, source
824
+ diagnostics, and normalised parsed source sections. Per-value, list, and event
825
+ bounds are revalidated because saved JSON is treated as untrusted input. A
826
+ successful registrar source must include a normalised object and agree with
827
+ its diagnostic status. Raw RDAP JSON, registrar contacts and source-specific
828
+ handles, WHOIS response bodies, availability evidence, and unrelated lookup
829
+ fields are not copied into the comparison result.
830
+
831
+ When the saved Lookup contains valid version-5, version-6, or version-7, context-only
832
+ registry-access diagnostics, version 3 retains their bounded suffix, access profiles, and
833
+ limitation. Terminal output labels the same collection context explicitly.
834
+ It explains source reachability only and cannot decide registration,
835
+ availability, ownership, safety, or maliciousness. Older documents and saved
836
+ lookups without that context do not gain a registry-access field.
837
+
838
+ Terminal and versioned JSON output cover domain identity, registry object ID,
839
+ registrar, registrar IANA ID, lifecycle dates, DNSSEC, statuses, and name
840
+ servers. The shared comparison model normalises harmless case, punctuation,
841
+ ordering, and date-precision differences while distinguishing conflicts,
842
+ one-source publication, redaction, incomplete sources, and unavailable
843
+ sources. This is source reconciliation, not an availability, ownership, or
844
+ maliciousness decision.
845
+
846
+ ## Static page comparison
847
+
848
+ `page-compare <left.json> <right.json>` reads two different version-1 saved
849
+ Deep domain Lookup documents and compares the already-retained static page
850
+ identity, exact and perceptual favicon evidence, technology identifiers, TLS
851
+ issuer label, and TLS public-key fingerprint. DOM structure is compared first
852
+ by its exact bounded tag-sequence digest, then, when both captures provide it,
853
+ by a parse5-tokenized structural SimHash. It makes no request and requires
854
+ complete supported static page-identity observations on both sides.
855
+
856
+ Each page component remains independent. Exact, similar, overlapping,
857
+ different, unavailable, and partial evidence are not collapsed into a score.
858
+ Matching components are investigative relationships rather than proof of
859
+ copying, common ownership, control, intent, safety, or maliciousness. Static
860
+ comparison does not execute JavaScript; use the optional local rendered
861
+ capture package only when that additional active behaviour is authorised.
862
+
863
+ ## Passive mail exposure review
864
+
865
+ `mail-review [bulk.json|bulk.jsonl]` reads version-2 Bulk output locally and
866
+ summarises MX, null MX, SPF, DMARC, and mail-provider relationships. It keeps
867
+ authenticated mail, authentication gaps, incomplete authentication evidence,
868
+ no explicit MX, null MX, and incomplete DNS evidence as separate states.
869
+ Shared-provider relationships are based only on the registrable domain of an
870
+ observed MX hostname and do not establish shared ownership or control.
871
+
872
+ The command makes no DNS or SMTP request and retains no source path. It does
873
+ not test message acceptance, relay behaviour, mailbox existence, catch-all
874
+ behaviour, SMTP banners, or whether a mail server is rogue, safe, or malicious.
875
+
876
+ An analyst may add a bounded `tlsaEvidence` object to a version-2 Bulk item for
877
+ offline DANE review. It must name the exact `_25._tcp.<mx-host>` service, and
878
+ that host must occur in the same item's retained MX evidence. A certificate
879
+ association match is complete only when `dnssecState` is `validated`. TLSA
880
+ usages 0 and 1 additionally require `pkixValidationState: "validated"`.
881
+ Usages 0 and 2 compare against the bounded `authorityMaterials` array; usages
882
+ 1 and 3 compare against the supplied leaf certificate or SPKI. Mismatched
883
+ service names, missing certificate roles, unvalidated prerequisites, malformed
884
+ material, and truncation remain invalid, partial, or untrusted rather than
885
+ becoming a DANE match.
886
+
887
+ ## Offline supplied-evidence review
888
+
889
+ `review-evidence [evidence.json]` accepts one version-1 JSON input and performs
890
+ only deterministic local parsing, comparison, or request planning. Input is
891
+ capped at 16 MiB. The command never retrieves DNS, opens SMTP, contacts an
892
+ RDAP server, submits a resolver query, or looks up an address in a hosted
893
+ GeoIP service.
894
+
895
+ Supported input schemas are:
896
+
897
+ - `whoisleuth.dnssec-evidence-input`: compares bounded DS and DNSKEY records,
898
+ recalculates DNSKEY key tags and supported DS digests, and reviews supplied
899
+ RRSIG validity windows at `observedAt`. `consistent` describes only the
900
+ supplied DS/DNSKEY relationship. It does not authenticate a chain to a root
901
+ trust anchor, validate RRSIG cryptography, or prove that a missing record is
902
+ absent.
903
+ - `whoisleuth.tlsa-evidence-input`: binds TLSA records to one explicit
904
+ `_port._transport.hostname` service and compares supplied leaf or authority
905
+ certificate/SPKI bytes. `matched` requires independently validated DNSSEC;
906
+ PKIX-TA and PKIX-EE usages also require an independently validated PKIX path.
907
+ The command does not retrieve the certificate or negotiate STARTTLS.
908
+ - `whoisleuth.rdap-search-input`: normalises a supplied RDAP search-help
909
+ response and prepares an exact supported reverse-search request without
910
+ sending it.
911
+ - `whoisleuth.rpki-route-input`: compares a route prefix and origin ASN with a
912
+ bounded analyst-supplied VRP set.
913
+ - `whoisleuth.local-geoip-query`: queries an analyst-supplied bounded prefix
914
+ database whose source, version, and licence metadata remain in the result.
915
+ - `whoisleuth.local-mmdb-query`: with an explicit `--mmdb <database-file>`,
916
+ queries one analyst-supplied MaxMind DB file locally. The query document must
917
+ carry `address`, `sourceLabel`, `databaseVersion`, and `license`. WHOISleuth
918
+ does not bundle, download, update, license, transmit, or retain the database.
919
+ The nested result uses the same address, network, country, region, city, ASN,
920
+ source, and limitation fields as the bounded JSON-prefix path. This mode is
921
+ CLI-only because a hosted or browser deployment cannot open an analyst's
922
+ local file; its JSON result remains portable to website-compatible evidence
923
+ archives and reports.
924
+ - `whoisleuth.encrypted-dns-plan-input`: validates an explicitly reviewed
925
+ encrypted-DNS provider contract and prepares a bounded query plan. It does
926
+ not execute the plan.
927
+
928
+ The common output is `whoisleuth.cli.offline-evidence-review` version 1. It
929
+ retains the nested result's explicit state and limitations. A locally
930
+ consistent relationship is not converted into a claim about current
931
+ publication, ownership, safety, or maliciousness.
932
+
933
+ ## Domain control manifests
934
+
935
+ `domain-control [input.json]` builds or reviews a bounded desired-state
936
+ manifest without making a request or changing configuration. A manifest input
937
+ uses `whoisleuth.domain-control-manifest-input` version 1, includes an expiry,
938
+ and may record expected nameservers, DS, MX, CAA, TLS issuer, TLS SPKI
939
+ fingerprint, registrar-lock preference, and a renewal-review date for up to 100
940
+ domains. Empty fields mean unconfigured; they do not require a record to be
941
+ absent.
942
+
943
+ The emitted `whoisleuth.domain-control-manifest` version 1 is normalised and
944
+ protected by a canonical SHA-256 digest. It can also be passed to
945
+ `sign-artifact` for an optional local Ed25519 signature. Integrity and
946
+ signature verification detect changes and authenticate possession of the
947
+ selected key; neither establishes that the desired state is correct. Manifest
948
+ input and verification reject unknown fields, and verification rejects
949
+ non-canonical content rather than returning a shape that differs from the
950
+ normalised contract covered by the digest.
951
+
952
+ A review input uses `whoisleuth.domain-control-review-input` version 1 and
953
+ contains one manifest plus separately attributed observations. Only a complete
954
+ `observed` field may produce `drift`. Partial, unavailable, unsupported, and
955
+ missing observations remain inconclusive, and unrelated observations are
956
+ counted but ignored. The review performs no DNS, RDAP, HTTP, TLS, SMTP, or
957
+ registrar request.
958
+
959
+ ## Domain assurance review
960
+
961
+ `assurance [input.json]` reviews one bounded analyst-authored workflow entirely
962
+ offline. Version 1 inputs remain supported. Version 2 adds optional
963
+ `customChecks` to retirement inputs and remains the current normalised output
964
+ version. The input uses `whoisleuth.domain-assurance.input` and one of three
965
+ fixed kinds:
966
+
967
+ - `planned-change` records a bounded window, evidence-backed milestones,
968
+ rollback criteria, and post-change checks;
969
+ - `recovery-dependencies` reviews up to 100 domains across registrar, DNS,
970
+ mail, certificate, and recovery providers and highlights exact provider
971
+ concentration; and
972
+ - `retirement` checks the fixed decommissioning controls without treating an
973
+ unchecked item as complete. Version 2 can add up to 20 analyst-defined checks,
974
+ each with a bounded identifier, label, expected boolean, and true, false, or
975
+ null observed value.
976
+
977
+ Provider labels, readiness confirmations, evidence references, and expected
978
+ states remain analyst assertions. The output preserves incomplete and
979
+ needs-review states and gives every negative state an explicit review reason.
980
+ The versioned input rejects unknown fields and contradictory evidence attached
981
+ to an unfinished check. It stores no credentials, makes no request, and changes
982
+ no registrar, DNS, mail, certificate, or recovery configuration.
983
+
984
+ ## Pre-sharing review
985
+
986
+ `sharing-review [artifact.json]` performs a redacted local preflight before an
987
+ analyst deliberately shares an artefact. The analyst must select a TLP 2.0
988
+ marking, a recipient scope, and a bounded purpose. Optional confirmations record
989
+ human review, personal-data review, and redaction review. The command validates
990
+ supported artefact integrity where possible, detects stricter embedded TLP
991
+ markings, and reports only key-category counts plus bounded, control-free schema
992
+ and integer version metadata. It emits no content values and copies no raw
993
+ evidence.
994
+
995
+ An imported stricter marking cannot be downgraded by the requested marking, and
996
+ recipient scope is checked against the effective marking. Evading this local
997
+ preflight remains possible, so the result is a review aid rather than access
998
+ control, legal advice, or recipient authorisation.
999
+
1000
+ ## Fixed investigation plans
1001
+
1002
+ `workflow-plan <recipe> <domain|brand>` turns one of four fixed recipes into a
1003
+ versioned `whoisleuth.cli.investigation-plan` document. Supported recipes are
1004
+ `domain-triage`, `lookalike-review`, `owned-domain-review`, and
1005
+ `historical-comparison`. Each step names an existing CLI command, carries its
1006
+ arguments as a JSON array, declares whether it is offline or networked, and
1007
+ marks network-disclosure or analyst-selection approval gates.
1008
+
1009
+ The command is plan-only. It does not execute a step, construct a shell script,
1010
+ interpret a placeholder as a file path, read an artefact, make a request,
1011
+ change a case, or submit evidence. Analysts deliberately run selected commands
1012
+ after reviewing their collection boundaries. This provides repeatable
1013
+ domain-specific workflows without an arbitrary automation or plugin surface.
1014
+
1015
+ ## Optional local rendered capture
1016
+
1017
+ `packages/web-capture` is a private repo-local Playwright package, not part of
1018
+ the distributable core CLI or hosted application. It requires an explicit
1019
+ authorisation flag and one new output directory:
1020
+
1021
+ ```bash
1022
+ npm run capture:local -- https://example.test --output-dir ./capture-example --authorize-rendered-capture
1023
+ npm run capture:compare -- ./official/manifest.json ./candidate/manifest.json --json
1024
+ ```
1025
+
1026
+ It writes a fixed 1024x768 PNG, screenshot SHA-256 and perceptual dHash, a DOM
1027
+ digest containing hashes and bounded element counts rather than markup or page
1028
+ text, and a version-2 `whoisleuth.web-capture-manifest` compatible with the
1029
+ Cases importer. Version-1 manifests remain importable.
1030
+
1031
+ Rendered capture executes page JavaScript. It caps HTTP(S) requests and request
1032
+ hostnames, blocks downloads, service workers, WebSockets, non-read methods,
1033
+ credentials, non-default ports, and private or reserved addresses, and retains
1034
+ no request path, query, headers, bodies, cookies, credentials, DOM markup, or
1035
+ page text. The browser connection is not pinned to the address checked before
1036
+ each hostname's first request, so DNS rebinding remains a residual risk. Use a
1037
+ disposable network-restricted environment for untrusted targets.
1038
+
1039
+ The separate offline comparison command accepts two selected version-2 local
1040
+ capture manifests. It first verifies each referenced screenshot and DOM digest
1041
+ against its declared size, SHA-256, screenshot dimensions, and perceptual hash;
1042
+ it also validates bounded source and limitation metadata and requires the DOM,
1043
+ manifest, and source collection times to agree. It then
1044
+ compares screenshot distance, exact rendered DOM and visible-text digests,
1045
+ bounded element counts, page identity, technologies, and request-domain sets.
1046
+ It does not recrawl either target, reveal the input paths, or collapse the
1047
+ independent components into a similarity or maliciousness score. A missing
1048
+ perceptual hash remains unavailable rather than becoming a visual difference.
1049
+
1050
+ ## Lookup evidence export
1051
+
1052
+ `export` converts one version-1 `whoisleuth.cli.lookup` domain document from a
1053
+ file or stdin into the same versioned `whoisleuth.lookup-evidence` JSON package
1054
+ produced by the web Lookup tool. It performs no lookup and writes only to
1055
+ stdout, so use ordinary shell redirection when a file is wanted. Pretty JSON is
1056
+ the default; `--compact` emits one compact JSON line for pipeline use, while
1057
+ `--markdown` produces a readable source-attributed summary and `--html`
1058
+ produces a self-contained printable report. Compact, Markdown, and HTML output
1059
+ are mutually exclusive.
1060
+
1061
+ The saved input is capped at 8 MiB and revalidated using the same schema,
1062
+ source-status, parsed-data, scalar, list, and event boundaries as `compare`.
1063
+ The export retains query context, source diagnostics, normalised registry data,
1064
+ raw registry RDAP JSON, the raw WHOIS referral chain, availability analysis,
1065
+ and the shared registry-source comparison. Version 23 can add the exact local
1066
+ SSLBL comparison already represented by a deep full Lookup. Version 21 adds the bounded registry
1067
+ lifecycle, disclosure, publication-quality, reconciliation, and abuse-routing
1068
+ interpretation derived from the already-collected sources. Registrar RDAP raw
1069
+ data, contacts, entities, links, notices, and source-specific handles remain
1070
+ excluded. Schema version 20 replaces selected security-policy values in
1071
+ retained HTTP evidence
1072
+ with presence-only markers and can include fixed response-policy findings from
1073
+ an already-represented deep lookup. Version 19 additionally retains the bounded credential-surface projection
1074
+ when the saved deep lookup represents it. Version 18 retains the bounded structured identity projection
1075
+ when the saved deep lookup represents it. Version 17 retains the normalised portable-field comparison, explicit
1076
+ source-health states, a strict bounded projection of observed network
1077
+ registration, an optional normalised security.txt disclosure source, HTTPS
1078
+ service-binding publications, and the passive browser-library profile when the
1079
+ saved deep lookup represents them. Raw IP RDAP payloads, security.txt response
1080
+ bodies, script references, matched script content, and contact entities remain
1081
+ excluded.
1082
+
1083
+ Markdown output summarises query context, assessment state, registry sources,
1084
+ source reconciliation, network observations, and collection diagnostics. It
1085
+ escapes all upstream text as untrusted content, bounds displayed values and
1086
+ lists, and deliberately omits raw RDAP JSON and full WHOIS response bodies.
1087
+ Use JSON when the complete evidence package or machine processing is required.
1088
+ The browser Console reuses this bounded Markdown renderer for domain results
1089
+ after first reducing the typed Lookup response to the known normalised fields
1090
+ needed by the report. It separately offers bounded IP and ASN summaries from
1091
+ their normalised RDAP and source-health fields; IP reports can also include
1092
+ bounded reverse-DNS context already collected by Lookup. The browser action
1093
+ does not change the CLI input or output contract.
1094
+ When a version-5, version-6, or version-7 lookup records a documented registry-access constraint, both
1095
+ Markdown and HTML include that context in collection diagnostics without
1096
+ changing its non-authoritative interpretation. Both formats also include the
1097
+ selected address and bounded network registration when present, together with
1098
+ the edge-versus-origin limitation.
1099
+
1100
+ HTML uses the same bounded summary model as Markdown. The generated document
1101
+ contains inline styling for screen and print, but no scripts, forms, active
1102
+ links, or external resources. A restrictive embedded Content Security Policy
1103
+ provides defence in depth when the local file is opened in a browser. All
1104
+ registry values are HTML-escaped and displayed as text.
1105
+
1106
+ This is a deliberately rich evidence package. Raw registry sources can contain
1107
+ publicly published contact data, and deep availability evidence can contain
1108
+ bounded website, DNS, mail, page-identity, and TLS observations. Review and
1109
+ secure the output before sharing it. The CLI does not add browser-only IDN
1110
+ profile analysis, so that optional evidence field is `null`.