@slicedearth/whoisleuth-cli 1.36.1 → 1.43.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (139) hide show
  1. package/README.md +32 -2
  2. package/bin/whoisleuth.mjs +52 -24
  3. package/cli/archive-inspect.mjs +37 -2
  4. package/cli/arguments.mjs +493 -157
  5. package/cli/artifact-verify.mjs +91 -15
  6. package/cli/bulk-checkpoint.mjs +7 -11
  7. package/cli/bulk-command-runner.mjs +19 -2
  8. package/cli/case-pack.mjs +136 -0
  9. package/cli/ci-report.mjs +73 -0
  10. package/cli/collection-preflight.mjs +59 -0
  11. package/cli/command-reference.mjs +395 -0
  12. package/cli/compare.mjs +1 -1
  13. package/cli/completion.mjs +49 -11
  14. package/cli/config-profile.mjs +180 -0
  15. package/cli/ct-event-intake.mjs +159 -0
  16. package/cli/discovery-scan-command-runner.mjs +15 -1
  17. package/cli/doctor.mjs +11 -5
  18. package/cli/domain-control-monitor.mjs +104 -0
  19. package/cli/domain-control-observations.mjs +235 -0
  20. package/cli/evidence-command-arguments.mjs +12 -1
  21. package/cli/evidence-command-runner.mjs +3 -2
  22. package/cli/evidence-signing.mjs +4 -4
  23. package/cli/external-observation-mapping.mjs +162 -0
  24. package/cli/fail-policy.mjs +64 -0
  25. package/cli/formatters/html.mjs +2 -2
  26. package/cli/formatters/json.mjs +13 -1
  27. package/cli/formatters/terminal.mjs +7 -3
  28. package/cli/investigation-manifest.mjs +113 -0
  29. package/cli/investigation-plan.mjs +2 -2
  30. package/cli/investigation-run.mjs +112 -0
  31. package/cli/local-mmdb-review.mjs +76 -0
  32. package/cli/lookalike-calibration.mjs +121 -0
  33. package/cli/lookup-brief.mjs +116 -0
  34. package/cli/lookup-command-runner.mjs +12 -2
  35. package/cli/lookup-diff.mjs +17 -19
  36. package/cli/lookup-plan.mjs +3 -3
  37. package/cli/lookup-reconcile.mjs +141 -0
  38. package/cli/lookup-timeline.mjs +1 -1
  39. package/cli/mail-review.mjs +1 -1
  40. package/cli/network-command-runner.mjs +4 -1
  41. package/cli/offline-evidence-review.mjs +125 -10
  42. package/cli/open-asset-model-bridge.mjs +169 -0
  43. package/cli/registry-cohort.mjs +121 -0
  44. package/cli/registry-doctor.mjs +220 -0
  45. package/cli/registry-fixture-scaffold.mjs +73 -0
  46. package/cli/risk-calibration.mjs +117 -9
  47. package/cli/runner.mjs +561 -280
  48. package/cli/saved-lookup.mjs +2 -2
  49. package/cli/sharing-review.mjs +202 -0
  50. package/cli/source-reliability.mjs +1 -1
  51. package/docs/cli-reference.md +1336 -0
  52. package/docs/cli.md +259 -940
  53. package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
  54. package/frontend/src/lib/analysis/acquisition-due-diligence.js +7 -7
  55. package/frontend/src/lib/analysis/brand-profile-model.js +46 -4
  56. package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
  57. package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
  58. package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
  59. package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
  60. package/frontend/src/lib/analysis/case-migration-model.js +6 -1
  61. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  62. package/frontend/src/lib/analysis/case-record-core.js +5 -1
  63. package/frontend/src/lib/analysis/case-record-operations.js +6 -1
  64. package/frontend/src/lib/analysis/case-report.js +573 -0
  65. package/frontend/src/lib/analysis/case-response-model.js +30 -1
  66. package/frontend/src/lib/analysis/case-response-packet.js +7 -7
  67. package/frontend/src/lib/analysis/detection-rule-model.js +36 -0
  68. package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
  69. package/frontend/src/lib/analysis/external-findings-import.js +320 -0
  70. package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
  71. package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
  72. package/frontend/src/lib/analysis/lookup-decision-support.js +487 -0
  73. package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
  74. package/frontend/src/lib/analysis/lookup-presentation.js +64 -0
  75. package/frontend/src/lib/analysis/lookup-response.js +2 -0
  76. package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
  77. package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
  78. package/frontend/src/lib/analysis/page-similarity.js +1 -1
  79. package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
  80. package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
  81. package/frontend/src/lib/analysis/scoring.js +9 -89
  82. package/frontend/src/lib/analysis/shortlist-model.js +8 -4
  83. package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
  84. package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
  85. package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
  86. package/frontend/src/lib/analysis/visualization-models.js +369 -0
  87. package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
  88. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
  89. package/frontend/src/lib/analysis/workspace-archive.js +4 -2
  90. package/lib/abusech-auth.mjs +1 -1
  91. package/lib/analyst-taxonomy.mjs +40 -0
  92. package/lib/availability.mjs +2 -2
  93. package/lib/bounded-contract-normalizers.mjs +33 -0
  94. package/lib/browser-library-profile.mjs +15 -2
  95. package/lib/certificate-extension-profile.mjs +71 -0
  96. package/lib/credential-surface-profile.mjs +1 -1
  97. package/lib/dns-convergence-review.mjs +183 -0
  98. package/lib/dns-delegation-health.mjs +196 -3
  99. package/lib/domain-assurance.mjs +368 -0
  100. package/lib/domain-change-packet.mjs +133 -0
  101. package/lib/domain-change-review.mjs +307 -0
  102. package/lib/domain-control-flight-recorder.mjs +268 -0
  103. package/lib/domain-control-manifest.mjs +50 -7
  104. package/lib/domain-portfolio-review.mjs +151 -0
  105. package/lib/domain-posture.mjs +9 -9
  106. package/lib/encrypted-dns-contract.mjs +1 -1
  107. package/lib/evidence-export.mjs +1 -1
  108. package/lib/evidence-report-markdown.mjs +1 -1
  109. package/lib/evidence-report.mjs +1 -1
  110. package/lib/generated/cisa-kev-catalog.mjs +1668 -0
  111. package/lib/html-signals.mjs +5 -11
  112. package/lib/ip-prefix.mjs +34 -1
  113. package/lib/local-geoip-evidence.mjs +2 -2
  114. package/lib/lookup-response-contract.mjs +346 -0
  115. package/lib/nameserver-preflight-review.mjs +159 -0
  116. package/lib/observed-network-context.mjs +1 -1
  117. package/lib/opportunity-scoring.mjs +107 -0
  118. package/lib/page-fingerprints.mjs +1 -1
  119. package/lib/page-language-signals.mjs +124 -0
  120. package/lib/rdap-capabilities.mjs +6 -6
  121. package/lib/rdap-search-workbench.mjs +96 -6
  122. package/lib/registry-access.mjs +1 -1
  123. package/lib/registry-capability-catalogue.mjs +4 -4
  124. package/lib/registry-insights.mjs +6 -6
  125. package/lib/risk-scoring.mjs +247 -83
  126. package/lib/scoring-evidence-quality.mjs +107 -0
  127. package/lib/security-txt.mjs +3 -3
  128. package/lib/static-html-analysis.mjs +29 -1
  129. package/lib/threat-intelligence-manifests.mjs +1 -1
  130. package/lib/threatfox-intelligence.mjs +1 -1
  131. package/lib/tls-intelligence.mjs +13 -3
  132. package/lib/trust-store-comparison.mjs +146 -0
  133. package/lib/urlhaus-intelligence.mjs +1 -1
  134. package/lib/website-security-posture.mjs +5 -5
  135. package/lib/website-technology.mjs +25 -11
  136. package/lib/whois-chain.mjs +1 -1
  137. package/lib/zone-intent-review.mjs +534 -0
  138. package/package.json +5 -1
  139. package/third-party-notices.txt +911 -19
@@ -0,0 +1,395 @@
1
+ import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../lib/project-metadata.mjs";
2
+ // Human-facing command reference kept separate from command execution so
3
+ // additions do not expand the already broad runtime controller.
4
+ const HELP = `WHOISleuth CLI
5
+ Source-aware domain investigation from your terminal.
6
+
7
+ Quick start:
8
+ whoisleuth lookup example.test
9
+ whoisleuth lookup example.test --deep
10
+ cat domains.txt | whoisleuth bulk --jsonl
11
+
12
+ Investigate:
13
+ lookup Collect one domain, IP, or ASN.
14
+ bulk Triage newline-delimited targets with bounded concurrency.
15
+ http Inspect one homepage request and redirect chain.
16
+ tls Inspect one hostname's current certificate connection.
17
+ posture Review DNS mail and domain-control posture.
18
+
19
+ Discover:
20
+ ct-search Search certificate-transparency observations.
21
+ ct-intake Normalise a local certificate event batch for case import.
22
+ discover Generate offline lookalike candidates.
23
+ discover-scan Collect a bounded candidate review queue.
24
+ registry-support Explain local registry coverage without a request.
25
+ registry-doctor Diagnose a saved registry collection against local policy.
26
+ registry-cohort Aggregate target-free registry quality cohorts.
27
+ registry-scaffold Create a sanitised synthetic fixture scaffold.
28
+
29
+ Review saved evidence:
30
+ map-observations Apply a declarative profile to local observations.
31
+ oam-export Project external findings into an open asset graph.
32
+ source-report Summarise source reliability without retaining targets.
33
+ compare Compare saved registry publications.
34
+ page-compare Compare saved static page and TLS evidence.
35
+ mail-review Review saved passive mail exposure evidence.
36
+ review-evidence Review supplied DNS, routing, GeoIP, RDAP, or trust-store evidence offline.
37
+ brief Turn one saved Lookup into a bounded decision brief.
38
+ case-pack Build a reviewed browser-importable case package.
39
+ domain-control Build or review an integrity-protected desired-state manifest.
40
+ monitor-once Run one bounded domain-control review and checkpoint.
41
+ assurance Review change, recovery, concentration, or retirement plans.
42
+ change-packet Assemble a digest-protected domain change review packet.
43
+ sharing-review Lint a reviewed artifact before deliberate sharing.
44
+ workflow-plan Plan a fixed investigation recipe without executing it.
45
+ workflow-run Execute resumable approved steps from a fixed recipe.
46
+ diff Compare two saved domain observations.
47
+ reconcile Reconcile independently labelled observations.
48
+ timeline Compare a sequence of observations for one domain.
49
+ export Convert a saved lookup into an evidence report.
50
+ inspect-archive Inspect a workspace archive, redacted by default.
51
+ verify-artifact Validate saved evidence or an integrity envelope offline.
52
+
53
+ Integrity and calibration:
54
+ manifest Record ordered artefact and configuration digests.
55
+ sign-artifact Sign one reviewed packet or manifest locally.
56
+ verify-signature Verify one signed evidence package locally.
57
+ risk-calibrate Replay reviewed labels without changing the model.
58
+ lookalike-calibrate Summarise reviewed mutation-family yield without tuning generation.
59
+
60
+ Terminal:
61
+ doctor Check the local runtime; network tests require --network.
62
+ commands List command contracts for people or local tooling.
63
+ completion Print completion for bash, zsh, fish, or PowerShell.
64
+ manual Print the generated manual page.
65
+
66
+ Run "whoisleuth <command> --help" for focused usage and an example.
67
+ Use --json or --jsonl where supported for machine-readable stdout.
68
+ Use --output <file> for atomic private file output and --force to replace it.
69
+ Use --config <file> and --profile <name> for explicit versioned safe defaults.
70
+ Diagnostics are written to stderr. Fast lookup is the default; deep collection
71
+ must be requested explicitly and can disclose a target to additional sources.
72
+
73
+ Copyright 2026 slicedearth. Licensed under AGPL-3.0-only.
74
+ Source and licence: ${WHOISLEUTH_SOURCE_REPOSITORY_URL}
75
+ `;
76
+ const COMMAND_USAGE = Object.freeze({
77
+ completion: 'whoisleuth completion <bash|zsh|fish|powershell>',
78
+ doctor: 'whoisleuth doctor [--network] [--json] [--quiet] [--no-color]',
79
+ commands: 'whoisleuth commands [--json] [--quiet] [--no-color]',
80
+ manual: 'whoisleuth manual',
81
+ manifest: 'whoisleuth manifest <artefact.json> [...] --workflow <label> [--configuration-digest <sha256:digest>] [--json] [--quiet] [--no-color]',
82
+ 'map-observations': 'whoisleuth map-observations [mapping.json] [--json] [--quiet] [--no-color]',
83
+ 'oam-export': 'whoisleuth oam-export [external-findings.json] [--json] [--quiet] [--no-color]',
84
+ lookup: 'whoisleuth lookup <domain|IP|ASN> [--json|--junit|--markdown|--html] [--fast|--deep] [--observer <label>] [--vantage <label>] [--plan] [--summary|--verbose] [--strict-exit] [--fail-on <policies>] [--events] [--quiet] [--no-color]',
85
+ bulk: 'whoisleuth bulk [file] [--json|--jsonl|--junit|--csv|--domains|--queries] [--registered-only|--inconclusive-only|--errors-only] [--fast|--deep] [--concurrency <1-8>] [--checkpoint <file> [--resume]] [--events] [--plan] [--fail-on <policies>]',
86
+ 'ct-search': 'whoisleuth ct-search <keyword> [--json] [--quiet] [--no-color]',
87
+ 'ct-intake': 'whoisleuth ct-intake [events.json] [--json] [--quiet] [--no-color]',
88
+ discover: 'whoisleuth discover <brand|domain> [--tlds <list>] [--preset <name>|--families <ids>] [--keyboard <layout>] [--dictionary <file>] [--snapshot <file>] [--json|--jsonl|--domains]',
89
+ 'discover-scan': 'whoisleuth discover-scan <brand|domain> [--fast|--deep] [--scan-limit <n>] [--chunk-size <n>] [--concurrency <n>] [--resolver <IPs>] [--allowlist <file>] [--checkpoint <file> [--resume]] [--observation-snapshot <file>] [--json|--jsonl|--csv|--domains] [--plan] [--fail-on <policies>]',
90
+ posture: 'whoisleuth posture <domain> [--selectors <list>] [--retired-selectors <list>] [--mail-profile <profile>] [--json|--sarif --owned-domain] [--quiet] [--no-color]',
91
+ http: 'whoisleuth http <domain> [--json] [--quiet] [--no-color]',
92
+ tls: 'whoisleuth tls <hostname> [--json] [--quiet] [--no-color]',
93
+ 'registry-support': 'whoisleuth registry-support <domain|suffix> [--json] [--quiet] [--no-color]',
94
+ 'registry-doctor': 'whoisleuth registry-doctor [lookup.json] [--json] [--quiet] [--no-color]',
95
+ 'registry-cohort': 'whoisleuth registry-cohort [lookups.json|lookups.jsonl] [--json] [--quiet] [--no-color]',
96
+ 'registry-scaffold': 'whoisleuth registry-scaffold --profile <id> --suffix <suffix> --scenario <registered|not_found|inconclusive>',
97
+ 'risk-calibrate': 'whoisleuth risk-calibrate [dataset.json] [--json] [--quiet] [--no-color]',
98
+ 'lookalike-calibrate': 'whoisleuth lookalike-calibrate [dataset.json] [--json] [--quiet] [--no-color]',
99
+ 'verify-artifact': 'whoisleuth verify-artifact [artifact.json] [--passphrase-file <file>] [--json] [--quiet] [--no-color]',
100
+ 'inspect-archive': 'whoisleuth inspect-archive [archive.json] [--passphrase-file <file>] [--search <value>] [--require-match] [--reveal] [--expect-content-digest <sha256:digest>] [--json]',
101
+ 'sign-artifact': 'whoisleuth sign-artifact [artifact.json] --private-key-file <file>',
102
+ 'verify-signature': 'whoisleuth verify-signature [package.json] [--public-key-file <file>] [--json] [--quiet] [--no-color]',
103
+ 'source-report': 'whoisleuth source-report [lookup.json] [--json] [--quiet] [--no-color]',
104
+ compare: 'whoisleuth compare [lookup.json] [--json] [--quiet] [--no-color]',
105
+ 'page-compare': 'whoisleuth page-compare <left.json> <right.json> [--json] [--quiet] [--no-color]',
106
+ 'mail-review': 'whoisleuth mail-review [bulk.json|bulk.jsonl] [--json] [--quiet] [--no-color]',
107
+ 'review-evidence': 'whoisleuth review-evidence [evidence.json] [--mmdb <database-file>] [--json] [--strict-exit] [--quiet] [--no-color]',
108
+ brief: 'whoisleuth brief [lookup.json] [--json] [--quiet] [--no-color]',
109
+ 'case-pack': 'whoisleuth case-pack [cases.json] --audience <internal|trusted|public> --reviewed [--json] [--quiet] [--no-color]',
110
+ 'domain-control': 'whoisleuth domain-control [manifest-input.json|review-input.json] [--json] [--quiet] [--no-color]',
111
+ 'monitor-once': 'whoisleuth monitor-once [manifest.json] [--previous <snapshot.json>] [--limit <1-20>] [--concurrency <1-3>] [--fail-on <policies>] [--json|--junit] [--quiet] [--no-color]',
112
+ assurance: 'whoisleuth assurance [assurance-input.json] [--json] [--quiet] [--no-color]',
113
+ 'change-packet': 'whoisleuth change-packet [change-packet-input.json] [--json] [--quiet] [--no-color]',
114
+ 'sharing-review': 'whoisleuth sharing-review [artifact.json] --marking <level> --recipient-scope <scope> --purpose <text> [--human-reviewed] [--personal-data-reviewed] [--redactions-confirmed] [--json]',
115
+ 'workflow-plan': 'whoisleuth workflow-plan <recipe> <domain|brand> [--json] [--quiet] [--no-color]',
116
+ 'workflow-run': 'whoisleuth workflow-run <recipe> <domain|brand> [--approve-network] [--resume <state.json>] [--json] [--quiet] [--no-color]',
117
+ diff: 'whoisleuth diff <left.json> <right.json> [--json] [--quiet] [--no-color]',
118
+ reconcile: 'whoisleuth reconcile <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
119
+ timeline: 'whoisleuth timeline <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
120
+ export: 'whoisleuth export [lookup.json] [--markdown|--html|--compact]',
121
+ });
122
+ const COMMAND_DETAILS = Object.freeze({
123
+ completion: {
124
+ description: 'Print a static shell-completion script for the installed CLI.',
125
+ example: 'whoisleuth completion zsh > ~/.zfunc/_whoisleuth',
126
+ boundary: 'Generation is offline and writes only the script to stdout. The command never modifies shell configuration.',
127
+ },
128
+ doctor: {
129
+ description: 'Check the supported runtime and local terminal capabilities.',
130
+ example: 'whoisleuth doctor --json',
131
+ boundary: 'The default check is offline. Public DNS and port 43 checks run only when --network is explicitly supplied.',
132
+ },
133
+ commands: {
134
+ description: 'List the installed command contracts in terminal or versioned JSON form.',
135
+ example: 'whoisleuth commands --json',
136
+ boundary: 'Catalogue generation is offline. It reports declared command modes and limits without executing collection or inspecting local evidence.',
137
+ },
138
+ manual: {
139
+ description: 'Print a generated roff manual page for local installation.',
140
+ example: 'whoisleuth manual | man -l -',
141
+ boundary: 'Generation is offline and derives from the same command catalogue as focused help.',
142
+ },
143
+ manifest: {
144
+ description: 'Record an ordered, path-free manifest for up to 16 local JSON artefacts.',
145
+ example: 'whoisleuth manifest lookup.json comparison.json --workflow "domain review" --json',
146
+ boundary: 'The command records hashes and bounded schema metadata only. It omits source paths and artefact contents and performs no network collection.',
147
+ },
148
+ 'map-observations': {
149
+ description: 'Apply one bounded declarative field-mapping profile to local source observations.',
150
+ example: 'whoisleuth map-observations mapping.json --json',
151
+ boundary: 'Profiles select allowlisted dotted fields only. They execute no scripts, make no requests, and emit the browser-compatible external-findings contract.',
152
+ },
153
+ 'oam-export': {
154
+ description: 'Project browser-compatible external findings into a bounded Open Asset Model bridge document.',
155
+ example: 'whoisleuth oam-export external-findings.json --json',
156
+ boundary: 'The projection is offline, preserves source completeness without inventing confidence, and covers only bounded FQDN, IP address, certificate, and related edge vocabulary.',
157
+ },
158
+ lookup: {
159
+ description: 'Collect registration evidence for one domain, IP, or ASN.',
160
+ example: 'whoisleuth lookup example.test --deep',
161
+ boundary: 'Fast is the default. Deep mode adds bounded WHOIS, DNS, HTTP, TLS, technology, posture, and network context where applicable.',
162
+ },
163
+ bulk: {
164
+ description: 'Triage newline-delimited domains, IPs, or ASNs with bounded concurrency.',
165
+ example: 'cat domains.txt | whoisleuth bulk --jsonl',
166
+ boundary: 'Fast and deep jobs use separate concurrency ceilings. Filters affect output only; collection failures and inconclusive authority states remain explicit in JSON, JSONL, and CSV.',
167
+ },
168
+ 'ct-search': {
169
+ description: 'Search certificate-transparency observations for one bounded keyword.',
170
+ example: 'whoisleuth ct-search "example brand" --json',
171
+ boundary: 'Certificate observations do not prove website activity, registration ownership, or malicious intent.',
172
+ },
173
+ 'ct-intake': {
174
+ description: 'Normalise source-qualified local certificate events into browser-compatible findings.',
175
+ example: 'whoisleuth ct-intake certificate-events.json --json',
176
+ boundary: 'The command is offline, caps output at 100 findings, and treats every event as a review lead rather than proof of serving or control.',
177
+ },
178
+ discover: {
179
+ description: 'Generate bounded lookalike-domain candidates from local mutation rules.',
180
+ example: 'whoisleuth discover example.test --preset common --jsonl',
181
+ boundary: 'Generation and optional local snapshot comparison are offline. Candidates are leads only and are not resolved, registered, or classified as malicious.',
182
+ },
183
+ 'discover-scan': {
184
+ description: 'Generate a bounded candidate set, collect a selected subset, and produce a supervised review queue.',
185
+ example: 'whoisleuth discover-scan example.test --scan-limit 50 --checkpoint scan.json --json',
186
+ boundary: 'This command performs network collection. Fast compact lookup is the default; deep mode is capped at 50 candidates. Allowlisting changes review priority only and shared infrastructure remains a lead, not attribution.',
187
+ },
188
+ posture: {
189
+ description: 'Review bounded DNS mail, delegation, and domain-control posture.',
190
+ example: 'whoisleuth posture example.test --mail-profile standard --json',
191
+ boundary: 'Missing or failed DNS observations remain inconclusive and are not reported as absent controls.',
192
+ },
193
+ http: {
194
+ description: 'Inspect one homepage request, redirects, and bounded response metadata.',
195
+ example: 'whoisleuth http example.test --json',
196
+ boundary: 'Requests use the shared public-address and redirect guards. This is not a rendered browser or vulnerability scan.',
197
+ },
198
+ tls: {
199
+ description: 'Inspect one hostname certificate through a bounded TLS connection.',
200
+ example: 'whoisleuth tls example.test --json',
201
+ boundary: 'One observed connection is point-in-time evidence and does not establish every address, edge, or historical certificate.',
202
+ },
203
+ 'registry-support': {
204
+ description: 'Explain the local registry capability profile for one domain or suffix.',
205
+ example: 'whoisleuth registry-support example.test --json',
206
+ boundary: 'This command is offline. Catalogue coverage does not test live reachability or decide registration or availability.',
207
+ },
208
+ 'registry-doctor': {
209
+ description: 'Compare a saved Lookup registry result with the reviewed local capability profile.',
210
+ example: 'whoisleuth registry-doctor lookup.json --json',
211
+ boundary: 'The command is offline. It distinguishes expected access constraints from collection results and does not contact a live registry.',
212
+ },
213
+ 'registry-cohort': {
214
+ description: 'Aggregate saved registry observations into privacy-safe suffix and capability-profile cohorts.',
215
+ example: 'whoisleuth registry-cohort saved-lookups.jsonl --json',
216
+ boundary: 'This command is offline and omits domains, queries, and raw evidence. Cohorts below the fixed minimum sample remain insufficient.',
217
+ },
218
+ 'registry-scaffold': {
219
+ description: 'Create a bounded synthetic WHOIS fixture scaffold for one existing capability profile.',
220
+ example: 'whoisleuth registry-scaffold --profile example-profile --suffix test --scenario registered',
221
+ boundary: 'The output is a sanitised template only. Contributors must not paste live responses or personal registration data into fixtures.',
222
+ },
223
+ 'risk-calibrate': {
224
+ description: 'Replay reviewed labels against the current explainable Risk model.',
225
+ example: 'whoisleuth risk-calibrate calibration.json --json',
226
+ boundary: 'Calibration is offline and diagnostic. It never trains, tunes, or changes the scoring model automatically.',
227
+ },
228
+ 'lookalike-calibrate': {
229
+ description: 'Summarise reviewed candidate dispositions by mutation family without retaining domains.',
230
+ example: 'whoisleuth lookalike-calibrate reviewed-candidates.json --json',
231
+ boundary: 'Calibration is offline and diagnostic. It omits candidate identifiers, domains, notes, and evidence and never tunes generation or filtering automatically.',
232
+ },
233
+ 'verify-artifact': {
234
+ description: 'Validate a supported archive, packet, manifest, or saved Lookup without printing evidence contents.',
235
+ example: 'whoisleuth verify-artifact workspace.json --json',
236
+ boundary: 'Verification is offline and redacted. Encrypted archives require an explicitly supplied passphrase file.',
237
+ },
238
+ 'inspect-archive': {
239
+ description: 'Summarise or search one workspace archive with redacted output by default.',
240
+ example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
241
+ boundary: 'Exact matches require --reveal. Content identity excludes export time and formatting. The archive is read locally and is never uploaded.',
242
+ },
243
+ 'sign-artifact': {
244
+ description: 'Sign one reviewed response packet or supported manifest with a local private key.',
245
+ example: 'whoisleuth sign-artifact packet.json --private-key-file analyst-private.pem',
246
+ boundary: 'The command never creates, stores, or transmits keys. Key custody and signer identity remain the operator\'s responsibility.',
247
+ },
248
+ 'verify-signature': {
249
+ description: 'Verify the integrity and signature of one signed evidence package.',
250
+ example: 'whoisleuth verify-signature packet.signed.json --json',
251
+ boundary: 'A valid signature proves package consistency for the embedded key, not the real-world identity or authority of its holder.',
252
+ },
253
+ 'source-report': {
254
+ description: 'Create a target-free reliability summary from a saved lookup.',
255
+ example: 'whoisleuth source-report lookup.json --json',
256
+ boundary: 'The report retains source states and timings but excludes targets, queries, endpoints, and raw evidence.',
257
+ },
258
+ compare: {
259
+ description: 'Compare separately attributed registry publications in a saved lookup.',
260
+ example: 'whoisleuth compare lookup.json --json',
261
+ boundary: 'Comparison is offline. Differences are review context and do not by themselves prove which publication is current.',
262
+ },
263
+ 'page-compare': {
264
+ description: 'Compare static page identity, favicon, technology, and TLS evidence in two saved deep lookups.',
265
+ example: 'whoisleuth page-compare official.json candidate.json --json',
266
+ boundary: 'Comparison is offline and component-based. It executes no page code and produces no aggregate similarity or maliciousness score.',
267
+ },
268
+ 'mail-review': {
269
+ description: 'Review passive MX, null MX, SPF, DMARC, and shared mail-provider evidence from saved Bulk results.',
270
+ example: 'whoisleuth mail-review candidates.json --json',
271
+ boundary: 'Review is offline and sends no SMTP traffic. Missing or partial DNS evidence remains inconclusive.',
272
+ },
273
+ 'review-evidence': {
274
+ description: 'Review one versioned DNS, domain-change, routing, GeoIP, RDAP, or trust-store document offline.',
275
+ example: 'whoisleuth review-evidence domain-change.json --json --strict-exit',
276
+ boundary: 'The command reads only the supplied document. It performs no DNS, RDAP, BGP, GeoIP-provider, TLS, HTTP, certificate-authority, or SMTP request.',
277
+ },
278
+ brief: {
279
+ description: 'Turn one saved Lookup into a compact decision brief with facts, unknowns, contradictions, and next actions.',
280
+ example: 'whoisleuth brief lookup.json --json',
281
+ boundary: 'The command is offline, excludes raw upstream payloads, and does not create an analyst assertion or claim that the saved observation is current.',
282
+ },
283
+ 'case-pack': {
284
+ description: 'Build a reviewed, audience-specific and browser-importable case package.',
285
+ example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
286
+ boundary: 'The command is offline, creates a new package, never mutates the source archive, and requires an explicit review acknowledgement.',
287
+ },
288
+ 'domain-control': {
289
+ description: 'Build an integrity-protected desired-state manifest or compare one with supplied observations.',
290
+ example: 'whoisleuth domain-control domain-control-input.json --json',
291
+ boundary: 'The command is offline and changes no registrar, DNS, mail, or certificate configuration. Only complete supplied observations can produce drift.',
292
+ },
293
+ 'monitor-once': {
294
+ description: 'Collect one bounded owned-domain review and compare it with an optional prior checkpoint.',
295
+ example: 'whoisleuth monitor-once manifest.json --previous previous.json --json --output next.json',
296
+ boundary: 'This is an operator-scheduled one-shot collection, not a daemon. It caps targets and concurrency, retains normalised observations, and never changes domain configuration.',
297
+ },
298
+ assurance: {
299
+ description: 'Review a versioned domain change, recovery-dependency, or retirement plan.',
300
+ example: 'whoisleuth assurance domain-assurance.json --json',
301
+ boundary: 'The command is offline and treats every provider label, readiness state, and evidence reference as analyst-authored input. It changes no configuration.',
302
+ },
303
+ 'change-packet': {
304
+ description: 'Assemble pre-change, post-change, and planning evidence into one integrity-protected packet.',
305
+ example: 'whoisleuth change-packet change-review.json --json',
306
+ boundary: 'Assembly is offline. Readiness reflects only the supplied bounded evidence and does not authorise or perform a domain change.',
307
+ },
308
+ 'sharing-review': {
309
+ description: 'Lint one reviewed artefact against local integrity, marking, recipient, personal-data, and redaction controls.',
310
+ example: 'whoisleuth sharing-review packet.json --marking amber --recipient-scope organization --purpose "Reviewed incident handoff" --human-reviewed --personal-data-reviewed --redactions-confirmed --json',
311
+ boundary: 'The command is offline and emits only bounded schema/version metadata, no content values, and no raw evidence. Its result is a review aid, not legal advice or recipient authorisation.',
312
+ },
313
+ 'workflow-plan': {
314
+ description: 'Build a fixed domain-investigation plan from existing bounded CLI commands.',
315
+ example: 'whoisleuth workflow-plan domain-triage example.test --json',
316
+ boundary: 'Planning is offline and plan-only. It does not execute commands, expand placeholders, read files, make requests, or submit evidence.',
317
+ },
318
+ 'workflow-run': {
319
+ description: 'Execute approved concrete steps from a fixed investigation recipe and emit a resumable checkpoint.',
320
+ example: 'whoisleuth workflow-run domain-triage example.test --approve-network --json --output run.json',
321
+ boundary: 'Only installed recipe commands can run. Network steps require explicit approval for each invocation, and analyst-selection placeholders always pause without interpretation.',
322
+ },
323
+ diff: {
324
+ description: 'Compare bounded evidence retained in two saved domain lookups.',
325
+ example: 'whoisleuth diff first.json second.json --json',
326
+ boundary: 'Comparison is offline. Missing, unavailable, equal, and different evidence remain separate states.',
327
+ },
328
+ reconcile: {
329
+ description: 'Reconcile bounded values across independently labelled observations of one domain.',
330
+ example: 'whoisleuth reconcile office.json mobile.json external.json --json',
331
+ boundary: 'The command is offline, accepts 2 to 5 saved observations for one domain, and never treats labels as proof of network independence or majority agreement as truth.',
332
+ },
333
+ timeline: {
334
+ description: 'Build an ordered same-domain history from saved Lookup observations.',
335
+ example: 'whoisleuth timeline first.json second.json latest.json --json',
336
+ boundary: 'The command is offline, accepts 2 to 20 bounded inputs for one domain, retains no filenames or raw registry payloads, and does not treat changed collection conditions as a domain change.',
337
+ },
338
+ export: {
339
+ description: 'Convert one saved lookup into a versioned evidence report.',
340
+ example: 'whoisleuth export lookup.json --markdown',
341
+ boundary: 'Exports preserve source attribution and limitations. Compact output intentionally omits raw registry payloads.',
342
+ },
343
+ });
344
+ const COMMAND_COLLECTION = Object.freeze({
345
+ completion: { mode: 'offline', scope: 'Prints one static script and changes no shell configuration.' },
346
+ doctor: { mode: 'network', scope: 'Network access is opt-in with --network and is limited to fixed public DNS, HTTPS, and WHOIS diagnostics.' },
347
+ commands: { mode: 'offline', scope: 'Reads the embedded command catalogue and performs no collection.' },
348
+ manual: { mode: 'offline', scope: 'Builds documentation from the embedded command catalogue.' },
349
+ manifest: { mode: 'offline', scope: 'Reads 1 to 16 local JSON artefacts capped at 32 MiB in total and retains no source paths.' },
350
+ 'map-observations': { mode: 'offline', scope: 'Reads one mapping document capped at 4 MiB and executes no scripts or requests.' },
351
+ 'oam-export': { mode: 'offline', scope: 'Reads one browser-compatible external-findings document and projects bounded graph records locally.' },
352
+ lookup: { mode: 'network', scope: 'Accepts one target. Fast is the default; deep collection must be selected explicitly.' },
353
+ bulk: { mode: 'network', scope: 'Accepts at most 500 fast or 50 deep targets, with concurrency capped at 8 fast or 3 deep.' },
354
+ 'ct-search': { mode: 'network', scope: 'Accepts one bounded search keyword and queries the fixed certificate-transparency source.' },
355
+ 'ct-intake': { mode: 'offline', scope: 'Reads one source-qualified event batch capped at 4 MiB and makes no request.' },
356
+ discover: { mode: 'offline', scope: 'Generates a bounded candidate set from local rules, dictionaries, and optional saved snapshots.' },
357
+ 'discover-scan': { mode: 'network', scope: 'Scans at most 500 fast or 50 deep candidates, with concurrency capped at 8 fast or 3 deep.' },
358
+ posture: { mode: 'network', scope: 'Accepts one domain and performs bounded DNS queries only.' },
359
+ http: { mode: 'network', scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
360
+ tls: { mode: 'network', scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
361
+ 'registry-support': { mode: 'offline', scope: 'Reads the embedded registry capability catalogue for one domain or suffix.' },
362
+ 'registry-doctor': { mode: 'offline', scope: 'Reads one saved Lookup and the embedded registry capability catalogue.' },
363
+ 'registry-cohort': { mode: 'offline', scope: 'Reads at most 500 saved Lookups and emits target-free suffix/profile cohort counts.' },
364
+ 'registry-scaffold': { mode: 'offline', scope: 'Reads the embedded registry capability catalogue and prints one synthetic fixture template.' },
365
+ 'risk-calibrate': { mode: 'offline', scope: 'Reads one bounded reviewed-label dataset and changes no model or evidence.' },
366
+ 'lookalike-calibrate': { mode: 'offline', scope: 'Reads at most 5,000 reviewed candidate labels from one dataset capped at 2 MiB.' },
367
+ 'verify-artifact': { mode: 'offline', scope: 'Reads one selected bounded archive, packet, manifest, or saved Lookup document.' },
368
+ 'inspect-archive': { mode: 'offline', scope: 'Reads one selected bounded workspace archive with redacted output by default.' },
369
+ 'sign-artifact': { mode: 'offline', scope: 'Reads one selected artefact and one local private key without transmitting either.' },
370
+ 'verify-signature': { mode: 'offline', scope: 'Reads one selected signed package and optional local public key.' },
371
+ 'source-report': { mode: 'offline', scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
372
+ compare: { mode: 'offline', scope: 'Reads one saved Lookup and compares its separately attributed registry publications.' },
373
+ 'page-compare': { mode: 'offline', scope: 'Reads two saved Lookup documents and executes no page code.' },
374
+ 'mail-review': { mode: 'offline', scope: 'Reads one saved Bulk result and sends no DNS or SMTP traffic.' },
375
+ 'review-evidence': { mode: 'offline', scope: 'Reads one bounded versioned evidence or request-planning document and performs no collection.' },
376
+ brief: { mode: 'offline', scope: 'Reads one bounded saved Lookup and emits a compact source-aware decision brief.' },
377
+ 'case-pack': { mode: 'offline', scope: 'Reads one bounded browser case export and writes a separate audience-specific package.' },
378
+ 'domain-control': { mode: 'offline', scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
379
+ 'monitor-once': { mode: 'network', scope: 'Runs deep collection for at most 20 manifest domains with concurrency capped at 3.' },
380
+ assurance: { mode: 'offline', scope: 'Reads one versioned plan capped at 2 MiB and makes no request or configuration change.' },
381
+ 'change-packet': { mode: 'offline', scope: 'Reads one versioned packet input capped at 6 MiB and makes no request or configuration change.' },
382
+ 'sharing-review': { mode: 'offline', scope: 'Reads one artefact capped at 15 MiB, emits only bounded schema/version metadata and no content values, and performs no transmission.' },
383
+ 'workflow-plan': { mode: 'offline', scope: 'Builds a fixed typed recipe and executes none of its network or file steps.' },
384
+ 'workflow-run': { mode: 'network', scope: 'Runs only concrete fixed-recipe steps; network collection requires --approve-network and analyst-selection steps always pause.' },
385
+ diff: { mode: 'offline', scope: 'Reads two saved Lookup documents for different domains.' },
386
+ reconcile: { mode: 'offline', scope: 'Reads 2 to 5 saved observations for one domain, capped at 32 MiB in total.' },
387
+ timeline: { mode: 'offline', scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
388
+ export: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
389
+ });
390
+ function commandHelp(command) {
391
+ const detail = COMMAND_DETAILS[command];
392
+ const collection = COMMAND_COLLECTION[command];
393
+ return `WHOISleuth ${command}\n${detail.description}\n\nUsage:\n ${COMMAND_USAGE[command]}\n\nExample:\n ${detail.example}\n\nCollection:\n ${collection.mode === 'offline' ? 'Offline' : 'Network'}: ${collection.scope}\n\nBoundary:\n ${detail.boundary}\n\nRun "whoisleuth --help" to see the grouped command list.\n`;
394
+ }
395
+ export { COMMAND_COLLECTION, COMMAND_DETAILS, COMMAND_USAGE, HELP, commandHelp };
package/cli/compare.mjs CHANGED
@@ -150,7 +150,7 @@ function projectCliRegistrarPublicationInput(document) {
150
150
  }
151
151
  const parsed = objectOrNull(registrar?.parsed);
152
152
  if (status === 'success' && !parsed) {
153
- throw new CliUsageError('Successful registrar RDAP input is missing normalized parsed data.');
153
+ throw new CliUsageError('Successful registrar RDAP input is missing normalised parsed data.');
154
154
  }
155
155
  const projected = projectRdapSource(parsed, 'rdap.registrarRdap.parsed');
156
156
  delete projected.handle;
@@ -1,29 +1,40 @@
1
1
  import { CLI_COMMANDS } from "./arguments.mjs";
2
2
  import { INVESTIGATION_PLAN_RECIPES } from "./investigation-plan.mjs";
3
- const COMMON_OPTIONS = Object.freeze(['--help', '--output', '--force']);
3
+ const COMMON_OPTIONS = Object.freeze(['--help', '--output', '--force', '--config', '--profile']);
4
4
  const OPTIONS_BY_COMMAND = Object.freeze({
5
- lookup: ['--json', '--markdown', '--html', '--fast', '--deep', '--plan', '--summary', '--verbose', '--strict-exit', '--events', '--quiet', '--no-color'],
6
- bulk: ['--json', '--jsonl', '--csv', '--domains', '--queries', '--registered-only', '--inconclusive-only', '--errors-only', '--fast', '--deep', '--concurrency', '--checkpoint', '--resume', '--events', '--quiet', '--no-color'],
5
+ lookup: ['--json', '--junit', '--markdown', '--html', '--fast', '--deep', '--observer', '--vantage', '--plan', '--summary', '--verbose', '--strict-exit', '--fail-on', '--events', '--quiet', '--no-color'],
6
+ bulk: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries', '--registered-only', '--inconclusive-only', '--errors-only', '--fast', '--deep', '--concurrency', '--checkpoint', '--resume', '--events', '--plan', '--fail-on', '--quiet', '--no-color'],
7
7
  'ct-search': ['--json', '--quiet', '--no-color'],
8
8
  discover: ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--snapshot', '--json', '--jsonl', '--domains', '--quiet', '--no-color'],
9
- 'discover-scan': ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--fast', '--deep', '--scan-limit', '--chunk-size', '--concurrency', '--resolver', '--allowlist', '--checkpoint', '--resume', '--observation-snapshot', '--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only', '--events', '--json', '--jsonl', '--csv', '--domains', '--quiet', '--no-color'],
10
- posture: ['--selectors', '--retired-selectors', '--mail-profile', '--json', '--quiet', '--no-color'],
9
+ 'discover-scan': ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--fast', '--deep', '--scan-limit', '--chunk-size', '--concurrency', '--resolver', '--allowlist', '--checkpoint', '--resume', '--observation-snapshot', '--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only', '--events', '--plan', '--fail-on', '--json', '--jsonl', '--csv', '--domains', '--quiet', '--no-color'],
10
+ posture: ['--selectors', '--retired-selectors', '--mail-profile', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
11
11
  http: ['--json', '--quiet', '--no-color'],
12
12
  tls: ['--json', '--quiet', '--no-color'],
13
13
  'registry-support': ['--json', '--quiet', '--no-color'],
14
+ 'registry-doctor': ['--json', '--quiet', '--no-color'],
15
+ 'registry-cohort': ['--json', '--quiet', '--no-color'],
16
+ 'registry-scaffold': ['--profile', '--suffix', '--scenario'],
14
17
  'risk-calibrate': ['--json', '--quiet', '--no-color'],
18
+ 'lookalike-calibrate': ['--json', '--quiet', '--no-color'],
15
19
  'verify-artifact': ['--passphrase-file', '--json', '--quiet', '--no-color'],
16
- 'inspect-archive': ['--passphrase-file', '--search', '--require-match', '--reveal', '--json', '--quiet', '--no-color'],
20
+ 'inspect-archive': ['--passphrase-file', '--search', '--require-match', '--reveal', '--expect-content-digest', '--json', '--quiet', '--no-color'],
17
21
  'sign-artifact': ['--private-key-file'],
18
22
  'verify-signature': ['--public-key-file', '--json', '--quiet', '--no-color'],
19
23
  'source-report': ['--json', '--quiet', '--no-color'],
20
24
  compare: ['--json', '--quiet', '--no-color'],
21
25
  'page-compare': ['--json', '--quiet', '--no-color'],
22
26
  'mail-review': ['--json', '--quiet', '--no-color'],
23
- 'review-evidence': ['--json', '--quiet', '--no-color'],
27
+ 'review-evidence': ['--mmdb', '--json', '--strict-exit', '--quiet', '--no-color'],
28
+ brief: ['--json', '--quiet', '--no-color'],
29
+ 'case-pack': ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
24
30
  'domain-control': ['--json', '--quiet', '--no-color'],
31
+ 'monitor-once': ['--previous', '--limit', '--concurrency', '--fail-on', '--json', '--junit', '--quiet', '--no-color'],
32
+ assurance: ['--json', '--quiet', '--no-color'],
33
+ 'sharing-review': ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
25
34
  'workflow-plan': ['--json', '--quiet', '--no-color'],
35
+ 'workflow-run': ['--approve-network', '--resume', '--json', '--quiet', '--no-color'],
26
36
  diff: ['--json', '--quiet', '--no-color'],
37
+ reconcile: ['--json', '--quiet', '--no-color'],
27
38
  timeline: ['--json', '--quiet', '--no-color'],
28
39
  export: ['--markdown', '--html', '--compact'],
29
40
  completion: [],
@@ -41,19 +52,30 @@ const COMMAND_DESCRIPTIONS = Object.freeze({
41
52
  http: 'Inspect one homepage request',
42
53
  tls: 'Inspect one TLS connection',
43
54
  'registry-support': 'Explain local registry coverage',
55
+ 'registry-doctor': 'Diagnose saved registry collection',
56
+ 'registry-cohort': 'Aggregate target-free registry quality cohorts',
57
+ 'registry-scaffold': 'Create a sanitised registry fixture scaffold',
44
58
  'risk-calibrate': 'Replay reviewed Risk labels offline',
59
+ 'lookalike-calibrate': 'Summarise reviewed lookalike yield offline',
45
60
  'verify-artifact': 'Validate saved evidence offline',
46
61
  'inspect-archive': 'Inspect an archive locally',
47
- 'sign-artifact': 'Sign a reviewed artifact locally',
62
+ 'sign-artifact': 'Sign a reviewed artefact locally',
48
63
  'verify-signature': 'Verify a signed evidence package',
49
64
  'source-report': 'Build a target-free source report',
50
65
  compare: 'Compare registry publications in one lookup',
51
66
  'page-compare': 'Compare saved static page evidence',
52
67
  'mail-review': 'Review saved passive mail evidence',
53
68
  'review-evidence': 'Review supplied evidence offline',
69
+ brief: 'Build a decision brief from a saved lookup',
70
+ 'case-pack': 'Build a reviewed case package',
54
71
  'domain-control': 'Build or review a domain control manifest',
72
+ 'monitor-once': 'Run one bounded domain control review',
73
+ assurance: 'Review domain change, recovery, or retirement plans',
74
+ 'sharing-review': 'Lint an artefact before deliberate sharing',
55
75
  'workflow-plan': 'Plan a fixed investigation recipe',
76
+ 'workflow-run': 'Execute approved fixed-recipe steps',
56
77
  diff: 'Compare two saved domain lookups',
78
+ reconcile: 'Reconcile independently labelled observations',
57
79
  timeline: 'Build same-domain history from saved lookups',
58
80
  export: 'Convert a lookup to an evidence report',
59
81
  completion: 'Print shell completion',
@@ -65,10 +87,14 @@ const VALUE_OPTIONS = Object.freeze({
65
87
  '--preset': ['common', 'impersonation', 'all'],
66
88
  '--keyboard': ['qwerty', 'azerty', 'qwertz', 'all'],
67
89
  '--mail-profile': ['standard', 'defensive-no-mail', 'parked'],
90
+ '--marking': ['clear', 'green', 'amber', 'amber-strict', 'red'],
91
+ '--recipient-scope': ['public', 'community', 'organization', 'named-recipients'],
92
+ '--audience': ['internal', 'trusted', 'public'],
68
93
  '--concurrency': ['1', '2', '3', '4', '5', '6', '7', '8'],
69
94
  });
70
95
  const FILE_OPTIONS = Object.freeze([
71
96
  '--checkpoint',
97
+ '--config',
72
98
  '--allowlist',
73
99
  '--dictionary',
74
100
  '--output',
@@ -77,14 +103,22 @@ const FILE_OPTIONS = Object.freeze([
77
103
  '--public-key-file',
78
104
  '--snapshot',
79
105
  '--observation-snapshot',
106
+ '--previous',
107
+ '--resume',
80
108
  ]);
81
109
  const TEXT_OPTIONS = Object.freeze([
82
110
  '--families',
83
111
  '--resolver',
112
+ '--fail-on',
113
+ '--expect-content-digest',
114
+ '--profile',
115
+ '--purpose',
116
+ '--observer',
84
117
  '--retired-selectors',
85
118
  '--search',
86
119
  '--selectors',
87
120
  '--tlds',
121
+ '--vantage',
88
122
  ]);
89
123
  function commandOptions(command) {
90
124
  return [...COMMON_OPTIONS, ...(OPTIONS_BY_COMMAND[command] || [])].join(' ');
@@ -167,12 +201,15 @@ fi
167
201
  }
168
202
  function fishCompletion() {
169
203
  const commandLines = CLI_COMMANDS.map((command) => (`complete -c whoisleuth -n '__fish_use_subcommand' -a '${command}' -d '${COMMAND_DESCRIPTIONS[command] || command}'`));
170
- const optionLines = Object.entries(OPTIONS_BY_COMMAND).flatMap(([command, options]) => ([...COMMON_OPTIONS, ...options].map((option) => {
204
+ const optionLine = (condition, option) => {
171
205
  const name = option.replace(/^--/u, '');
172
206
  const fileValue = FILE_OPTIONS.includes(option);
173
207
  const requiresValue = fileValue || [...Object.keys(VALUE_OPTIONS), ...TEXT_OPTIONS].includes(option);
174
- return `complete -c whoisleuth -n '__fish_seen_subcommand_from ${command}' -l ${name}${requiresValue ? ' -r' : ''}${fileValue ? ' -F' : ''}`;
175
- })));
208
+ return `complete -c whoisleuth -n '${condition}' -l ${name}${requiresValue ? ' -r' : ''}${fileValue ? ' -F' : ''}`;
209
+ };
210
+ const commonCondition = `__fish_seen_subcommand_from ${CLI_COMMANDS.join(' ')}`;
211
+ const commonOptionLines = COMMON_OPTIONS.map((option) => optionLine(commonCondition, option));
212
+ const optionLines = Object.entries(OPTIONS_BY_COMMAND).flatMap(([command, options]) => (options.map((option) => optionLine(`__fish_seen_subcommand_from ${command}`, option))));
176
213
  const valueLines = Object.entries(VALUE_OPTIONS).flatMap(([option, values]) => (values.map((value) => `complete -c whoisleuth -n '__fish_prev_arg_in ${option}' -a '${value}'`)));
177
214
  return `# WHOISleuth fish completion
178
215
  complete -c whoisleuth -f
@@ -181,6 +218,7 @@ complete -c whoisleuth -n '__fish_use_subcommand' -l version
181
218
  ${commandLines.join('\n')}
182
219
  complete -c whoisleuth -n '__fish_seen_subcommand_from completion' -a 'bash zsh fish powershell'
183
220
  complete -c whoisleuth -n '__fish_seen_subcommand_from workflow-plan' -a '${INVESTIGATION_PLAN_RECIPES.join(' ')}'
221
+ ${commonOptionLines.join('\n')}
184
222
  ${optionLines.join('\n')}
185
223
  ${valueLines.join('\n')}
186
224
  `;