@slicedearth/whoisleuth-cli 1.32.2 → 1.36.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +43 -5
- package/SECURITY.md +35 -0
- package/cli/arguments.mjs +180 -8
- package/cli/artifact-verify.mjs +32 -0
- package/cli/bulk-command-runner.mjs +3 -1
- package/cli/bulk-output.mjs +6 -1
- package/cli/bulk.mjs +1 -2
- package/cli/command-catalogue.mjs +33 -0
- package/cli/completion.mjs +70 -7
- package/cli/doctor.mjs +78 -43
- package/cli/export-evidence.mjs +9 -2
- package/cli/formatters/html.mjs +1 -1
- package/cli/formatters/terminal.mjs +13 -7
- package/cli/investigation-plan.mjs +112 -0
- package/cli/lookup-command-runner.mjs +8 -0
- package/cli/lookup-diff.mjs +16 -5
- package/cli/lookup-plan.mjs +99 -0
- package/cli/lookup-timeline.mjs +123 -0
- package/cli/mail-review.mjs +47 -2
- package/cli/manual.mjs +2 -1
- package/cli/offline-evidence-review.mjs +141 -0
- package/cli/registry-support.mjs +1 -4
- package/cli/runner.mjs +214 -11
- package/docs/cli.md +200 -45
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -6
- package/frontend/src/lib/analysis/bulk-retry-plan.js +3 -10
- package/frontend/src/lib/analysis/bulk-source-coverage.js +76 -0
- package/frontend/src/lib/analysis/http-summary.js +2 -1
- package/frontend/src/lib/analysis/utils.js +7 -40
- package/frontend/src/lib/analysis/website-snapshot-model.js +57 -3
- package/lib/availability.mjs +30 -10
- package/lib/bulk-limits.mjs +13 -0
- package/lib/csv.mjs +1 -1
- package/lib/ct-response-bounds.mjs +9 -0
- package/lib/ct-search.mjs +66 -12
- package/lib/dns-delegation-health.mjs +56 -5
- package/lib/dnssec-evidence-validation.mjs +235 -0
- package/lib/domain-control-manifest.mjs +339 -0
- package/lib/domain-posture.mjs +2 -1
- package/lib/encrypted-dns-contract.mjs +81 -0
- package/lib/evidence-export.mjs +17 -3
- package/lib/evidence-report-markdown.mjs +3 -3
- package/lib/evidence-report.mjs +3 -0
- package/lib/favicon.mjs +3 -8
- package/lib/html-signals.mjs +4 -0
- package/lib/http-evidence-bounds.mjs +4 -0
- package/lib/http-intelligence.mjs +2 -1
- package/lib/ip-prefix.mjs +54 -0
- package/lib/local-geoip-evidence.mjs +89 -0
- package/lib/outbound-identity.mjs +9 -0
- package/lib/outbound-request-bounds.mjs +9 -0
- package/lib/perceptual-hash-comparison.mjs +34 -0
- package/lib/perceptual-hash.mjs +3 -31
- package/lib/project-metadata.mjs +8 -0
- package/lib/provider-policy-admission.mjs +49 -0
- package/lib/rdap-search-workbench.mjs +157 -0
- package/lib/rdap.mjs +7 -2
- package/lib/registry-capabilities.mjs +31 -1
- package/lib/registry-capability-catalogue.mjs +11 -10
- package/lib/response-policy.mjs +81 -6
- package/lib/rpki-evidence.mjs +105 -0
- package/lib/safe-fetch.mjs +2 -1
- package/lib/semantic-version.mjs +54 -0
- package/lib/static-html-analysis.mjs +27 -0
- package/lib/threatfox-intelligence.mjs +10 -5
- package/lib/tlsa-evidence.mjs +194 -0
- package/lib/urlhaus-intelligence.mjs +10 -5
- package/lib/urlscan-intelligence.mjs +10 -5
- package/lib/website-security-posture.mjs +11 -4
- package/lib/whois-chain.mjs +14 -152
- package/lib/whois.mjs +2 -2
- package/package.json +7 -5
- package/third-party-notices.txt +2 -2
package/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
|
|
@@ -65,13 +77,19 @@ node bin/whoisleuth.mts lookup example.com --deep --json > lookup.json
|
|
|
65
77
|
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
|
|
80
|
+
node bin/whoisleuth.mts review-evidence dnssec-evidence.json --json
|
|
81
|
+
node bin/whoisleuth.mts domain-control domain-control-input.json --json
|
|
82
|
+
node bin/whoisleuth.mts workflow-plan domain-triage example.test --json
|
|
68
83
|
node bin/whoisleuth.mts diff first-lookup.json second-lookup.json --json
|
|
84
|
+
node bin/whoisleuth.mts timeline first-observation.json second-observation.json latest-observation.json --json
|
|
69
85
|
node bin/whoisleuth.mts export lookup.json > evidence.json
|
|
70
86
|
node bin/whoisleuth.mts export lookup.json --markdown > evidence.md
|
|
71
87
|
node bin/whoisleuth.mts export lookup.json --html > evidence.html
|
|
72
88
|
node bin/whoisleuth.mts doctor
|
|
73
89
|
node bin/whoisleuth.mts doctor --network --json
|
|
90
|
+
node bin/whoisleuth.mts commands --json
|
|
74
91
|
node bin/whoisleuth.mts completion zsh
|
|
92
|
+
node bin/whoisleuth.mts completion powershell
|
|
75
93
|
node bin/whoisleuth.mts manual | man -l -
|
|
76
94
|
```
|
|
77
95
|
|
|
@@ -87,10 +105,13 @@ npm run cli:package:check
|
|
|
87
105
|
|
|
88
106
|
The check includes only the executable, reachable TypeScript modules, CLI
|
|
89
107
|
guide, licence, notices, and trademark terms. It excludes application routes,
|
|
90
|
-
deployment adapters, tests, and development tools.
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
108
|
+
deployment adapters, tests, and development tools. Direct runtime dependencies
|
|
109
|
+
are pinned to the exact versions exercised by the reviewed repository lockfile.
|
|
110
|
+
The package and its dependencies require no lifecycle scripts, so the install
|
|
111
|
+
examples disable them explicitly.
|
|
112
|
+
Its generated manifest stays private. Release-candidate assembly is a separate
|
|
113
|
+
explicit operation and never publishes from a developer checkout. Use the
|
|
114
|
+
scoped package name; an unqualified `npx whoisleuth` is not this project.
|
|
94
115
|
|
|
95
116
|
Lookup defaults to the conservative fast profile. `--deep` must be requested
|
|
96
117
|
explicitly and can add bounded WHOIS, DNS, website, TLS, registrar RDAP, and
|
|
@@ -104,6 +125,14 @@ up to five normalized PTR names; JSON retains up to eight. PTR names are
|
|
|
104
125
|
operator-published routing context, not proof of hosting control or ownership.
|
|
105
126
|
Fast lookups do not run this query.
|
|
106
127
|
|
|
128
|
+
`lookup --plan` performs classification and prints a versioned preflight without
|
|
129
|
+
starting collection. It lists the normalized target, selected fast or deep
|
|
130
|
+
mode, planned source families, conditional operations, and the data each family
|
|
131
|
+
may receive. The plan cannot predict live feature configuration, cache state,
|
|
132
|
+
redirects, referrals, source availability, or an exact request count. It can be
|
|
133
|
+
combined with `--json`, but not with report, detail, strict-exit, progress-event,
|
|
134
|
+
or quiet options.
|
|
135
|
+
|
|
107
136
|
Only one query is accepted by `lookup`. Multiple-input processing belongs to
|
|
108
137
|
the explicit `bulk` command rather than being silently inferred by `lookup`.
|
|
109
138
|
Standard input is capped at 4 KiB and must contain one non-empty line.
|
|
@@ -114,32 +143,49 @@ request. Its output is described below.
|
|
|
114
143
|
|
|
115
144
|
`doctor` is offline by default. It checks the Node runtime, platform, terminal
|
|
116
145
|
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
|
-
|
|
146
|
+
adds independent bounded public-DNS, HTTPS/RDAP bootstrap, and WHOIS port 43
|
|
147
|
+
checks against fixed IANA diagnostic infrastructure. The three probes run
|
|
148
|
+
within the same per-probe ceiling so one stalled transport does not block the
|
|
149
|
+
others. The report retains only state, HTTP status, and a bounded explanation,
|
|
150
|
+
not resolved addresses or response content. A failed optional network check
|
|
151
|
+
returns the partial-failure exit code rather than claiming that all CLI
|
|
152
|
+
collection is unavailable.
|
|
153
|
+
|
|
154
|
+
`completion bash`, `completion zsh`, `completion fish`, and `completion
|
|
155
|
+
powershell` print static shell completion scripts to stdout. The command does
|
|
156
|
+
not edit a shell profile or make a network request. Review the generated script
|
|
157
|
+
before sourcing it or placing it in the relevant shell completion directory.
|
|
158
|
+
`manual` prints a generated roff manual page and likewise changes no local
|
|
159
|
+
configuration.
|
|
160
|
+
|
|
161
|
+
`commands` prints the installed command catalogue without running collection or
|
|
162
|
+
reading evidence. `commands --json` emits version 1 of the
|
|
163
|
+
`whoisleuth.cli.command-catalogue` schema, including each command's usage,
|
|
164
|
+
example, declared collection mode, collection scope, and boundary. This is the
|
|
165
|
+
supported discovery contract for local wrappers; it is not a permission grant
|
|
166
|
+
or a substitute for focused help.
|
|
167
|
+
|
|
168
|
+
Focused command help and the generated manual classify every operation as
|
|
169
|
+
offline or networked and state the relevant target, input, and concurrency
|
|
170
|
+
ceiling. These labels describe observable behavior rather than granting
|
|
171
|
+
authorization. The packaged `DISCLOSURE` defines the intended defensive scope;
|
|
172
|
+
operators remain responsible for law, provider terms, and authorization.
|
|
128
173
|
|
|
129
174
|
## Deployment boundary
|
|
130
175
|
|
|
131
|
-
The CLI
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
176
|
+
The CLI runs locally. The serverless deployment publishes only `frontend/build`
|
|
177
|
+
and packages functions only from `netlify/functions`, so `bin/` and `cli/` are
|
|
178
|
+
not part of the hosted static site or function bundle. The application root and
|
|
179
|
+
ordinary package-check candidate remain private. Explicitly reviewed CLI
|
|
180
|
+
release candidates can be published under the scoped public package described
|
|
181
|
+
in the installation section.
|
|
136
182
|
|
|
137
183
|
Commands that query RDAP, WHOIS, DNS, HTTP, TLS, or Certificate Transparency do
|
|
138
184
|
so directly from the machine running the CLI. They do not use the hosted login,
|
|
139
185
|
hosted session, or deployment usage controls; upstream providers can see and
|
|
140
186
|
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
|
|
187
|
+
`page-compare`, `mail-review`, `review-evidence`, `domain-control`, `workflow-plan`, `diff`, `timeline`, `risk-calibrate`, `verify-artifact`, `source-report`, `export`,
|
|
188
|
+
`commands`, `completion`, and `manual` operations make no network requests. Commands write
|
|
143
189
|
to stdout unless the analyst deliberately selects a local output file.
|
|
144
190
|
|
|
145
191
|
## Output
|
|
@@ -266,6 +312,15 @@ registration, DNS, page identity, mail, certificate, and relationship fields,
|
|
|
266
312
|
keeps missing and unavailable evidence distinct, and makes no ownership or
|
|
267
313
|
maliciousness inference. The output records both original observation times.
|
|
268
314
|
|
|
315
|
+
`timeline <observation.json> <observation.json> [...]` accepts from 2 to 20
|
|
316
|
+
saved Lookup documents for one domain, orders them by their validated
|
|
317
|
+
observation times, and compares each adjacent pair offline. The 32 MiB aggregate
|
|
318
|
+
input ceiling is enforced while files are read and again by the timeline
|
|
319
|
+
builder. Output retains normalized field states and observation times but no
|
|
320
|
+
input filenames or raw registry payloads. A difference can reflect a domain
|
|
321
|
+
change or changed collection conditions, so the command makes no current-state,
|
|
322
|
+
ownership, intent, safety, or maliciousness conclusion.
|
|
323
|
+
|
|
269
324
|
When diagnostics version 5, 6, 7, or 8 reports a documented registry collection
|
|
270
325
|
constraint, terminal output also shows the suffix, WHOIS and RDAP access
|
|
271
326
|
profiles, and the bounded limitation. This is static access-policy context: it
|
|
@@ -286,7 +341,7 @@ machine access is not evidence that a domain is unregistered or safe.
|
|
|
286
341
|
This release supports `lookup`, `bulk`, `ct-search`, `discover`, `discover-scan`, `posture`,
|
|
287
342
|
`http`, `tls`, `registry-support`, `risk-calibrate`, `verify-artifact`,
|
|
288
343
|
`inspect-archive`, `sign-artifact`, `verify-signature`, `source-report`,
|
|
289
|
-
`compare`, `page-compare`, `mail-review`, `diff`, `export`, `doctor`, `completion`, and `manual`. Additional command families
|
|
344
|
+
`compare`, `page-compare`, `mail-review`, `review-evidence`, `domain-control`, `workflow-plan`, `diff`, `timeline`, `export`, `doctor`, `commands`, `completion`, and `manual`. Additional command families
|
|
290
345
|
are added as separate bounded increments rather than exposing incomplete
|
|
291
346
|
aliases.
|
|
292
347
|
|
|
@@ -296,13 +351,13 @@ aliases.
|
|
|
296
351
|
catalogue for one domain or suffix. It distinguishes an explicit
|
|
297
352
|
fixture-backed or access-documented suffix profile from the generic IANA
|
|
298
353
|
discovery profile and reports the RDAP and WHOIS discovery/access paths,
|
|
299
|
-
WHOIS query and parser profiles,
|
|
354
|
+
WHOIS query and parser profiles, fixture scenarios,
|
|
300
355
|
verification references, documentation references, and the catalogue's
|
|
301
356
|
limitation. The same document includes a dated, official-source aggregate of
|
|
302
357
|
generic, generic-restricted, sponsored, and infrastructure RDAP coverage. That
|
|
303
358
|
snapshot is separate from the suffix profile and from live reachability.
|
|
304
359
|
|
|
305
|
-
Terminal output is bounded and control-safe. `--json` emits version
|
|
360
|
+
Terminal output is bounded and control-safe. `--json` emits version 3 of the
|
|
306
361
|
`whoisleuth.cli.registry-support` schema. Unknown but syntactically valid
|
|
307
362
|
suffixes retain the generic `discovery_only` profile; malformed input exits
|
|
308
363
|
with code 2. The command never probes a registry or tests current reachability.
|
|
@@ -385,7 +440,11 @@ Risk model.
|
|
|
385
440
|
evidence contents. Input is capped at 15 MiB. It currently recognises ordinary
|
|
386
441
|
and encrypted workspace archives, case-response packets, acquisition-decision
|
|
387
442
|
exports, domain-comparison exports, Bulk mail-exposure exports, and Bulk review
|
|
388
|
-
manifests.
|
|
443
|
+
manifests. It also validates the bounded versioned structure of saved CLI
|
|
444
|
+
Lookup JSON. Because saved Lookup documents do not embed a checksum or
|
|
445
|
+
signature, that result is reported as `structure_valid` with content integrity
|
|
446
|
+
explicitly unchecked. Saved Lookup validation retains its stricter 8 MiB
|
|
447
|
+
document ceiling.
|
|
389
448
|
|
|
390
449
|
For an ordinary workspace archive, the command validates the versioned
|
|
391
450
|
structure, section byte counts, record counts, and per-section checksums. For a
|
|
@@ -469,19 +528,25 @@ deep mode.
|
|
|
469
528
|
|
|
470
529
|
Bulk uses the shared compact lookup response, so it does not retain raw RDAP
|
|
471
530
|
objects or WHOIS response bodies. `--json` returns one bounded collection;
|
|
472
|
-
`--jsonl` emits one self-contained versioned item per line.
|
|
531
|
+
`--jsonl` emits one self-contained versioned item per line. `--domains` emits
|
|
532
|
+
only successfully normalized domain names, while `--queries` preserves the
|
|
533
|
+
selected bounded input queries and is suitable for an exact retry queue.
|
|
534
|
+
Version 2 adds a
|
|
473
535
|
bounded `dnsSummary` projection for observed A, AAAA, NS, and MX records plus
|
|
474
536
|
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
|
-
|
|
537
|
+
including the DNS summaries and explicit outcome.
|
|
538
|
+
|
|
539
|
+
`--registered-only`, `--inconclusive-only`, and `--errors-only` are mutually
|
|
540
|
+
exclusive output filters. The first retains registered, for-sale, and expiring
|
|
541
|
+
authority-aware states. The second retains unknown authority states and failed
|
|
542
|
+
rows. The third retains only rows whose lookup operation failed, which is useful
|
|
543
|
+
for producing an exact retry queue without treating an inconclusive successful
|
|
544
|
+
lookup as an operational error. Filtering does not change collection and never
|
|
545
|
+
converts an unavailable or failed source into an unregistered result. Machine
|
|
546
|
+
documents record collected and emitted counts. A mixture of successful and
|
|
547
|
+
failed queries exits with code 4 while preserving every collected result before
|
|
548
|
+
output filtering. For example, `bulk domains.txt --queries --errors-only`
|
|
549
|
+
produces only the exact failed inputs while retaining the partial-failure exit.
|
|
485
550
|
|
|
486
551
|
## Certificate Transparency search
|
|
487
552
|
|
|
@@ -492,11 +557,15 @@ does not call the hosted WHOISleuth deployment. Quote a multi-word keyword so
|
|
|
492
557
|
the shell passes it as one argument.
|
|
493
558
|
|
|
494
559
|
Terminal output summarizes certificate rows, observed hostnames, canonical
|
|
495
|
-
registrable-domain matches, observation times,
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
560
|
+
registrable-domain matches, observation times, bounded certificate-issuance
|
|
561
|
+
groups, and completeness. An issuance group contains names observed together
|
|
562
|
+
in one public certificate record; a cross-domain group is a review lead, not
|
|
563
|
+
an attribution or ownership finding. Terminal output shows at most 100 matches
|
|
564
|
+
and five hostnames per match, with explicit omission notes. `--json` returns
|
|
565
|
+
the complete bounded structured result in the versioned
|
|
566
|
+
`whoisleuth.cli.ct-search` schema. The certificate-group cap is reported
|
|
567
|
+
separately from the registrable-domain result cap. CT observations do not prove
|
|
568
|
+
that a website is active or malicious.
|
|
500
569
|
|
|
501
570
|
## Lookalike discovery
|
|
502
571
|
|
|
@@ -729,6 +798,92 @@ The command makes no DNS or SMTP request and retains no source path. It does
|
|
|
729
798
|
not test message acceptance, relay behavior, mailbox existence, catch-all
|
|
730
799
|
behavior, SMTP banners, or whether a mail server is rogue, safe, or malicious.
|
|
731
800
|
|
|
801
|
+
An analyst may add a bounded `tlsaEvidence` object to a version-2 Bulk item for
|
|
802
|
+
offline DANE review. It must name the exact `_25._tcp.<mx-host>` service, and
|
|
803
|
+
that host must occur in the same item's retained MX evidence. A certificate
|
|
804
|
+
association match is complete only when `dnssecState` is `validated`. TLSA
|
|
805
|
+
usages 0 and 1 additionally require `pkixValidationState: "validated"`.
|
|
806
|
+
Usages 0 and 2 compare against the bounded `authorityMaterials` array; usages
|
|
807
|
+
1 and 3 compare against the supplied leaf certificate or SPKI. Mismatched
|
|
808
|
+
service names, missing certificate roles, unvalidated prerequisites, malformed
|
|
809
|
+
material, and truncation remain invalid, partial, or untrusted rather than
|
|
810
|
+
becoming a DANE match.
|
|
811
|
+
|
|
812
|
+
## Offline supplied-evidence review
|
|
813
|
+
|
|
814
|
+
`review-evidence [evidence.json]` accepts one version-1 JSON input and performs
|
|
815
|
+
only deterministic local parsing, comparison, or request planning. Input is
|
|
816
|
+
capped at 16 MiB. The command never retrieves DNS, opens SMTP, contacts an
|
|
817
|
+
RDAP server, submits a resolver query, or looks up an address in a hosted
|
|
818
|
+
GeoIP service.
|
|
819
|
+
|
|
820
|
+
Supported input schemas are:
|
|
821
|
+
|
|
822
|
+
- `whoisleuth.dnssec-evidence-input`: compares bounded DS and DNSKEY records,
|
|
823
|
+
recalculates DNSKEY key tags and supported DS digests, and reviews supplied
|
|
824
|
+
RRSIG validity windows at `observedAt`. `consistent` describes only the
|
|
825
|
+
supplied DS/DNSKEY relationship. It does not authenticate a chain to a root
|
|
826
|
+
trust anchor, validate RRSIG cryptography, or prove that a missing record is
|
|
827
|
+
absent.
|
|
828
|
+
- `whoisleuth.tlsa-evidence-input`: binds TLSA records to one explicit
|
|
829
|
+
`_port._transport.hostname` service and compares supplied leaf or authority
|
|
830
|
+
certificate/SPKI bytes. `matched` requires independently validated DNSSEC;
|
|
831
|
+
PKIX-TA and PKIX-EE usages also require an independently validated PKIX path.
|
|
832
|
+
The command does not retrieve the certificate or negotiate STARTTLS.
|
|
833
|
+
- `whoisleuth.rdap-search-input`: normalizes a supplied RDAP search-help
|
|
834
|
+
response and prepares an exact supported reverse-search request without
|
|
835
|
+
sending it.
|
|
836
|
+
- `whoisleuth.rpki-route-input`: compares a route prefix and origin ASN with a
|
|
837
|
+
bounded analyst-supplied VRP set.
|
|
838
|
+
- `whoisleuth.local-geoip-query`: queries an analyst-supplied bounded prefix
|
|
839
|
+
database whose source, version, and licence metadata remain in the result.
|
|
840
|
+
- `whoisleuth.encrypted-dns-plan-input`: validates an explicitly reviewed
|
|
841
|
+
encrypted-DNS provider contract and prepares a bounded query plan. It does
|
|
842
|
+
not execute the plan.
|
|
843
|
+
|
|
844
|
+
The common output is `whoisleuth.cli.offline-evidence-review` version 1. It
|
|
845
|
+
retains the nested result's explicit state and limitations. A locally
|
|
846
|
+
consistent relationship is not converted into a claim about current
|
|
847
|
+
publication, ownership, safety, or maliciousness.
|
|
848
|
+
|
|
849
|
+
## Domain control manifests
|
|
850
|
+
|
|
851
|
+
`domain-control [input.json]` builds or reviews a bounded desired-state
|
|
852
|
+
manifest without making a request or changing configuration. A manifest input
|
|
853
|
+
uses `whoisleuth.domain-control-manifest-input` version 1, includes an expiry,
|
|
854
|
+
and may record expected nameservers, DS, MX, CAA, TLS issuer, TLS SPKI
|
|
855
|
+
fingerprint, registrar-lock preference, and a renewal-review date for up to 100
|
|
856
|
+
domains. Empty fields mean unconfigured; they do not require a record to be
|
|
857
|
+
absent.
|
|
858
|
+
|
|
859
|
+
The emitted `whoisleuth.domain-control-manifest` version 1 is normalized and
|
|
860
|
+
protected by a canonical SHA-256 digest. It can also be passed to
|
|
861
|
+
`sign-artifact` for an optional local Ed25519 signature. Integrity and
|
|
862
|
+
signature verification detect changes and authenticate possession of the
|
|
863
|
+
selected key; neither establishes that the desired state is correct.
|
|
864
|
+
|
|
865
|
+
A review input uses `whoisleuth.domain-control-review-input` version 1 and
|
|
866
|
+
contains one manifest plus separately attributed observations. Only a complete
|
|
867
|
+
`observed` field may produce `drift`. Partial, unavailable, unsupported, and
|
|
868
|
+
missing observations remain inconclusive, and unrelated observations are
|
|
869
|
+
counted but ignored. The review performs no DNS, RDAP, HTTP, TLS, SMTP, or
|
|
870
|
+
registrar request.
|
|
871
|
+
|
|
872
|
+
## Fixed investigation plans
|
|
873
|
+
|
|
874
|
+
`workflow-plan <recipe> <domain|brand>` turns one of four fixed recipes into a
|
|
875
|
+
versioned `whoisleuth.cli.investigation-plan` document. Supported recipes are
|
|
876
|
+
`domain-triage`, `lookalike-review`, `owned-domain-review`, and
|
|
877
|
+
`historical-comparison`. Each step names an existing CLI command, carries its
|
|
878
|
+
arguments as a JSON array, declares whether it is offline or networked, and
|
|
879
|
+
marks network-disclosure or analyst-selection approval gates.
|
|
880
|
+
|
|
881
|
+
The command is plan-only. It does not execute a step, construct a shell script,
|
|
882
|
+
interpret a placeholder as a file path, read an artifact, make a request,
|
|
883
|
+
change a case, or submit evidence. Analysts deliberately run selected commands
|
|
884
|
+
after reviewing their collection boundaries. This provides repeatable
|
|
885
|
+
domain-specific workflows without an arbitrary automation or plugin surface.
|
|
886
|
+
|
|
732
887
|
## Optional local rendered capture
|
|
733
888
|
|
|
734
889
|
`packages/web-capture` is a private repo-local Playwright package, not part of
|
|
@@ -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()) {
|
|
@@ -1,13 +1,7 @@
|
|
|
1
1
|
import { normalizeBulkSessionResult, } from "./bulk-session-model.js";
|
|
2
|
+
import { limitedBulkSources } from "./bulk-source-coverage.js";
|
|
2
3
|
export const MAX_BULK_RETRY_ROWS = 200;
|
|
3
4
|
export const BULK_REVIEW_STALE_AFTER_DAYS = 7;
|
|
4
|
-
const LIMITED_STATES = new Set([
|
|
5
|
-
'error',
|
|
6
|
-
'partial',
|
|
7
|
-
'skipped',
|
|
8
|
-
'unavailable',
|
|
9
|
-
'unsupported',
|
|
10
|
-
]);
|
|
11
5
|
function freshness(value, now = Date.now()) {
|
|
12
6
|
if (typeof value !== 'string' || !Number.isFinite(Date.parse(value))) {
|
|
13
7
|
return { observedAt: null, ageDays: null, state: 'unknown' };
|
|
@@ -21,9 +15,7 @@ function freshness(value, now = Date.now()) {
|
|
|
21
15
|
};
|
|
22
16
|
}
|
|
23
17
|
function retryRow(value, isStale) {
|
|
24
|
-
const limitedSources = value.sourceCoverage
|
|
25
|
-
.filter((item) => LIMITED_STATES.has(item.state))
|
|
26
|
-
.map((item) => item.source)
|
|
18
|
+
const limitedSources = limitedBulkSources(value.domain, value.sourceCoverage)
|
|
27
19
|
.sort()
|
|
28
20
|
.slice(0, 12);
|
|
29
21
|
const reasons = [];
|
|
@@ -56,6 +48,7 @@ export function buildBulkRetryPlan(values, mode, observedAtRaw = null, now = Dat
|
|
|
56
48
|
'This repeats the selected lookup profile; the current API does not request only one upstream source.',
|
|
57
49
|
`Evidence is marked stale after ${BULK_REVIEW_STALE_AFTER_DAYS} days for review purposes; this is a local observation age, not an upstream record age.`,
|
|
58
50
|
'Optional third-party intelligence providers are not contacted by Bulk.',
|
|
51
|
+
'Sources deliberately skipped by the selected scan profile or deployment policy do not independently trigger a retry.',
|
|
59
52
|
'A retry can remain partial or fail and does not convert unavailable evidence into absence.',
|
|
60
53
|
],
|
|
61
54
|
};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
// Authority-aware interpretation for compact Bulk source observations. Raw
|
|
2
|
+
// source states remain unchanged; this layer only decides whether an observed
|
|
3
|
+
// limitation was unexpected for the registry's published service paths.
|
|
4
|
+
import { registryCapabilityFor } from "../../../../lib/registry-capabilities.mjs";
|
|
5
|
+
const MAX_SOURCE_OBSERVATIONS = 32;
|
|
6
|
+
const MAX_SOURCE_TEXT_LENGTH = 40;
|
|
7
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
8
|
+
const LIMITED_SOURCE_STATES = new Set([
|
|
9
|
+
'error',
|
|
10
|
+
'failed',
|
|
11
|
+
'inconclusive',
|
|
12
|
+
'partial',
|
|
13
|
+
'rate_limited',
|
|
14
|
+
'unavailable',
|
|
15
|
+
'unsupported',
|
|
16
|
+
]);
|
|
17
|
+
function text(value) {
|
|
18
|
+
return typeof value === 'string'
|
|
19
|
+
&& value.length <= MAX_SOURCE_TEXT_LENGTH
|
|
20
|
+
&& !CONTROL_RE.test(value)
|
|
21
|
+
? value.trim().toLowerCase()
|
|
22
|
+
: '';
|
|
23
|
+
}
|
|
24
|
+
function record(value) {
|
|
25
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
26
|
+
? value
|
|
27
|
+
: null;
|
|
28
|
+
}
|
|
29
|
+
function observations(value) {
|
|
30
|
+
return Array.isArray(value)
|
|
31
|
+
? value.slice(0, MAX_SOURCE_OBSERVATIONS).flatMap((entry) => {
|
|
32
|
+
const item = record(entry);
|
|
33
|
+
const source = text(item?.source);
|
|
34
|
+
const state = text(item?.state);
|
|
35
|
+
return source && state ? [{ source, state }] : [];
|
|
36
|
+
})
|
|
37
|
+
: [];
|
|
38
|
+
}
|
|
39
|
+
function expectedUnsupportedSources(domain) {
|
|
40
|
+
const capability = registryCapabilityFor(domain);
|
|
41
|
+
if (!capability)
|
|
42
|
+
return new Set();
|
|
43
|
+
const expected = new Set();
|
|
44
|
+
if (capability.rdapAccessProfile === 'no-iana-service')
|
|
45
|
+
expected.add('rdap');
|
|
46
|
+
if (capability.whoisAccessProfile === 'no-iana-service')
|
|
47
|
+
expected.add('whois');
|
|
48
|
+
return expected;
|
|
49
|
+
}
|
|
50
|
+
export function isExpectedUnsupportedBulkSource(domain, observation) {
|
|
51
|
+
const source = text(observation.source);
|
|
52
|
+
const state = text(observation.state);
|
|
53
|
+
return state === 'unsupported' && expectedUnsupportedSources(domain).has(source);
|
|
54
|
+
}
|
|
55
|
+
export function limitedBulkSources(domain, coverage) {
|
|
56
|
+
const expectedUnsupported = expectedUnsupportedSources(domain);
|
|
57
|
+
return observations(coverage)
|
|
58
|
+
.filter(({ source, state }) => (LIMITED_SOURCE_STATES.has(state)
|
|
59
|
+
&& !(state === 'unsupported' && expectedUnsupported.has(source))))
|
|
60
|
+
.map(({ source }) => source);
|
|
61
|
+
}
|
|
62
|
+
export function classifyBulkSourceCoverage(domain, coverage) {
|
|
63
|
+
const retained = observations(coverage);
|
|
64
|
+
if (!retained.length)
|
|
65
|
+
return 'unrecorded';
|
|
66
|
+
return limitedBulkSources(domain, retained).length ? 'limited' : 'complete';
|
|
67
|
+
}
|
|
68
|
+
export function describeBulkSourceCoverage(domain, observation) {
|
|
69
|
+
const source = text(observation.source);
|
|
70
|
+
const state = text(observation.state);
|
|
71
|
+
if (!source || !state)
|
|
72
|
+
return 'unrecorded source';
|
|
73
|
+
return isExpectedUnsupportedBulkSource(domain, observation)
|
|
74
|
+
? `${source}: unsupported (no IANA-published service)`
|
|
75
|
+
: `${source}: ${state}`;
|
|
76
|
+
}
|
|
@@ -3,10 +3,11 @@
|
|
|
3
3
|
// connection attempts. Bulk/watchlist/case records deliberately keep only this
|
|
4
4
|
// smaller derived shape: no paths, queries, header values, attempt errors, or
|
|
5
5
|
// redirect inventories.
|
|
6
|
+
import { MAX_HTTP_EVIDENCE_REDIRECTS } from "../../../../lib/http-evidence-bounds.mjs";
|
|
6
7
|
export const HTTP_SUMMARY_VERSION = 1;
|
|
7
8
|
export const MAX_HTTP_SUMMARY_ORIGIN_LENGTH = 300;
|
|
8
9
|
export const MAX_HTTP_SUMMARY_CONTENT_TYPE_LENGTH = 100;
|
|
9
|
-
export const MAX_HTTP_SUMMARY_REDIRECTS =
|
|
10
|
+
export const MAX_HTTP_SUMMARY_REDIRECTS = MAX_HTTP_EVIDENCE_REDIRECTS;
|
|
10
11
|
export const MAX_HTTP_SECURITY_HEADER_INPUTS = 20;
|
|
11
12
|
export const HTTP_SECURITY_HEADER_TOKENS = Object.freeze([
|
|
12
13
|
'content-security-policy',
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Browser-safe analysis helpers shared by the Svelte tools.
|
|
2
2
|
import { serializeCsvCell, serializeCsvRows } from "../../../../lib/csv.mjs";
|
|
3
|
+
import { hammingDistanceHex, isInformativePerceptualHash, isPerceptualHash, } from "../../../../lib/perceptual-hash-comparison.mjs";
|
|
3
4
|
// Deliberately conservative (no +tags, no comments, no quoted local parts) -
|
|
4
5
|
// this only gates whether a WHOIS/RDAP-sourced string is safe to drop into a
|
|
5
6
|
// mailto: URI as the recipient, not a general email validator. mailto:
|
|
@@ -38,47 +39,13 @@ export function entityDisplayName(value) {
|
|
|
38
39
|
.slice(0, MAX_ENTITY_DISPLAY_LENGTH);
|
|
39
40
|
return normalized || null;
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
// lib/perceptual-hash.mts), or null if either isn't a well-formed hash. Smaller
|
|
43
|
-
// = more visually similar. Shared by the brand-profile near-match check and
|
|
44
|
-
// bulk favicon clustering; mirrors the backend's hammingDistanceHex. The two
|
|
45
|
-
// implementations target separate Node and browser execution boundaries.
|
|
46
|
-
const HEX_HASH_RE = /^[0-9a-f]{16}$/;
|
|
47
|
-
export function hammingDistanceHex(a, b) {
|
|
48
|
-
if (typeof a !== 'string' || typeof b !== 'string')
|
|
49
|
-
return null;
|
|
50
|
-
if (!HEX_HASH_RE.test(a) || !HEX_HASH_RE.test(b))
|
|
51
|
-
return null;
|
|
52
|
-
let distance = 0;
|
|
53
|
-
for (let i = 0; i < 16; i += 1) {
|
|
54
|
-
let diff = parseInt(a.charAt(i), 16) ^ parseInt(b.charAt(i), 16);
|
|
55
|
-
while (diff) {
|
|
56
|
-
distance += diff & 1;
|
|
57
|
-
diff >>= 1;
|
|
58
|
-
}
|
|
59
|
-
}
|
|
60
|
-
return distance;
|
|
61
|
-
}
|
|
42
|
+
export { hammingDistanceHex };
|
|
62
43
|
// A dHash carries usable structure only when its set-bit count is away from
|
|
63
|
-
// the degenerate extremes
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
// already rejects these, but this is re-checked at every comparison so a
|
|
67
|
-
// degenerate hash saved into a Brand Profile before that guard existed can't
|
|
68
|
-
// keep producing false near-matches.
|
|
69
|
-
const MIN_INFORMATIVE_HASH_BITS = 10;
|
|
44
|
+
// the degenerate extremes. The shared browser-safe contract applies the same
|
|
45
|
+
// threshold during generation and comparison, including for older retained
|
|
46
|
+
// profile values.
|
|
70
47
|
export function isInformativeFaviconHash(hex) {
|
|
71
|
-
|
|
72
|
-
return false;
|
|
73
|
-
let bits = 0;
|
|
74
|
-
for (let i = 0; i < 16; i += 1) {
|
|
75
|
-
let n = parseInt(hex.charAt(i), 16);
|
|
76
|
-
while (n) {
|
|
77
|
-
bits += n & 1;
|
|
78
|
-
n >>= 1;
|
|
79
|
-
}
|
|
80
|
-
}
|
|
81
|
-
return bits >= MIN_INFORMATIVE_HASH_BITS && bits <= 64 - MIN_INFORMATIVE_HASH_BITS;
|
|
48
|
+
return isInformativePerceptualHash(hex);
|
|
82
49
|
}
|
|
83
50
|
function faviconRecord(value) {
|
|
84
51
|
const record = plainRecord(value);
|
|
@@ -87,7 +54,7 @@ function faviconRecord(value) {
|
|
|
87
54
|
const faviconHash = typeof record.faviconHash === 'string' && record.faviconHash
|
|
88
55
|
? record.faviconHash
|
|
89
56
|
: null;
|
|
90
|
-
const faviconPHash =
|
|
57
|
+
const faviconPHash = isPerceptualHash(record.faviconPHash)
|
|
91
58
|
? record.faviconPHash
|
|
92
59
|
: null;
|
|
93
60
|
return faviconHash || faviconPHash
|