@slicedearth/whoisleuth-cli 2.3.0 → 2.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/NOTICE +6 -0
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +68 -6
- package/cli/artifact-validation/case-response.mjs +47 -41
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +83 -61
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +39 -31
- package/cli/command-argument-grammar.mjs +4 -0
- package/cli/command-reference.mjs +145 -75
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +44 -16
- package/cli/discovery-scan-command-runner.mjs +2 -0
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +1 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +4 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +45 -0
- package/cli/runner.mjs +55 -6
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +76 -30
- package/docs/cli.md +289 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +43 -3
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +36 -23
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +7 -2
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +219 -118
- package/lib/domain-control-manifest.mjs +117 -57
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +113 -26
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -592
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +82 -85
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +7 -10
- package/lib/lookup-source-progress.mjs +17 -14
- package/lib/lookup.mjs +48 -27
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +17 -3
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +10 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +59 -25
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-core.mjs +3 -2
- package/packages/cases/case-record-operations.mjs +89 -27
- package/packages/cases/case-record-projection.mjs +23 -20
- package/packages/cases/case-report.mjs +63 -52
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +41 -1129
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +38 -587
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +25 -0
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/page-similarity.mjs +2 -1
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +279 -116
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +1 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +160 -225
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +89 -49
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +158 -20
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +11 -5
- package/packages/workspace/bulk-session-model.mjs +95 -20
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -26
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +70 -30
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +85 -21
- package/third-party-notices.txt +29 -2
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,14 +1,18 @@
|
|
|
1
1
|
import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../lib/project-metadata.mjs";
|
|
2
|
-
import { LOOKUP_EVIDENCE_SCHEMA_VERSION,
|
|
2
|
+
import { LOOKUP_EVIDENCE_SCHEMA_VERSION, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../packages/contracts/lookup-evidence.mjs";
|
|
3
3
|
import { RISK_CALIBRATION_DATASET_SCHEMA, RISK_CALIBRATION_REPORT_SCHEMA, } from "../packages/contracts/risk-calibration.mjs";
|
|
4
|
-
import { CASE_SCHEMA_VERSION, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, WORKSPACE_ARCHIVE_VERSION, } from "../packages/contracts/case-portability.mjs";
|
|
4
|
+
import { CASE_SCHEMA_VERSION, CLI_CASE_PACK_INPUT_CASE_VERSIONS, MAX_CASE_STORE_BYTES, MAX_EDITABLE_CASE_INPUT_BYTES, SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS, WORKSPACE_ARCHIVE_VERSION, } from "../packages/contracts/case-portability.mjs";
|
|
5
5
|
import { CLI_COMMAND_SEMANTICS, CLI_HELP_GROUP_ORDER, } from "../packages/contracts/cli-command-semantics.mjs";
|
|
6
6
|
import { CLI_FAIL_POLICIES_BY_COMMAND } from "./fail-policy.mjs";
|
|
7
|
+
import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES } from "../packages/investigation/investigation-manifest.mjs";
|
|
7
8
|
const LEGACY_WORKSPACE_ARCHIVE_VERSIONS = SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS
|
|
8
9
|
.filter((version) => version !== WORKSPACE_ARCHIVE_VERSION);
|
|
9
10
|
const LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION = LEGACY_WORKSPACE_ARCHIVE_VERSIONS
|
|
10
11
|
.map((version) => `version-${version}`)
|
|
11
12
|
.join(' and ');
|
|
13
|
+
const PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS = SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS
|
|
14
|
+
.filter((version) => version > V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION && version !== LOOKUP_EVIDENCE_SCHEMA_VERSION)
|
|
15
|
+
.join(', ');
|
|
12
16
|
const LEGACY_WORKSPACE_ARCHIVE_SCOPE = LEGACY_WORKSPACE_ARCHIVE_VERSIONS
|
|
13
17
|
.map((version) => `v${version}`)
|
|
14
18
|
.join(' and ');
|
|
@@ -24,12 +28,8 @@ const INVESTIGATION_PLAN_RECIPES = Object.freeze([
|
|
|
24
28
|
'planned-domain-change',
|
|
25
29
|
'post-change-verification',
|
|
26
30
|
]);
|
|
27
|
-
const RUNNABLE_INVESTIGATION_PLAN_RECIPES =
|
|
28
|
-
|
|
29
|
-
'lookalike-review',
|
|
30
|
-
'owned-domain-review',
|
|
31
|
-
'historical-comparison',
|
|
32
|
-
]);
|
|
31
|
+
const RUNNABLE_INVESTIGATION_PLAN_RECIPES = INVESTIGATION_PLAN_RECIPES;
|
|
32
|
+
const CLI_CASE_OPERATIONS = ['show', 'open', 'note', 'pin', 'assess', 'recheck'];
|
|
33
33
|
const CLI_META_ACTIONS = Object.freeze([
|
|
34
34
|
Object.freeze({
|
|
35
35
|
id: 'help',
|
|
@@ -67,6 +67,8 @@ redirected or unsupported terminals continue to print this help. No request
|
|
|
67
67
|
starts until a Lookup plan is shown and the analyst confirms collection.
|
|
68
68
|
Use --json or --jsonl where supported for machine-readable stdout.
|
|
69
69
|
Use --output <file> for atomic private file output and --force to replace it.
|
|
70
|
+
Use -- before positional filenames that begin with a hyphen; ./ also makes a
|
|
71
|
+
filename unambiguous, for example: whoisleuth verify-artifact -- ./-evidence.json.
|
|
70
72
|
Use --palette auto, light, or dark after the command to select a fixed terminal
|
|
71
73
|
colour palette; --no-color, NO_COLOR, and redirected output still suppress ANSI.
|
|
72
74
|
Use --config <file> and --profile <name> for explicit versioned safe defaults.
|
|
@@ -132,6 +134,8 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
|
|
|
132
134
|
'--palette': enumeration(['auto', 'light', 'dark']),
|
|
133
135
|
'--network': flag(),
|
|
134
136
|
'--json': flag(),
|
|
137
|
+
'--package': flag(),
|
|
138
|
+
'--folder': file(),
|
|
135
139
|
'--quiet': flag('idempotent'),
|
|
136
140
|
'--no-color': flag('idempotent'),
|
|
137
141
|
'--common': flag(),
|
|
@@ -145,12 +149,14 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
|
|
|
145
149
|
'--no-attribution': flag(),
|
|
146
150
|
'--fast': flag(),
|
|
147
151
|
'--deep': flag(),
|
|
152
|
+
'--exact-url': flag(),
|
|
148
153
|
'--observer': text(),
|
|
149
154
|
'--vantage': text(),
|
|
150
155
|
'--plan': flag(),
|
|
151
156
|
'--summary': flag(),
|
|
152
157
|
'--verbose': flag(),
|
|
153
158
|
'--browse': flag(),
|
|
159
|
+
'--interactive': flag(),
|
|
154
160
|
'--save-lookup': file(),
|
|
155
161
|
'--strict-exit': flag(),
|
|
156
162
|
'--fail-on': optionDefinition('policy_list', {
|
|
@@ -159,6 +165,7 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
|
|
|
159
165
|
'--events': flag(),
|
|
160
166
|
'--jsonl': flag(),
|
|
161
167
|
'--csv': flag(),
|
|
168
|
+
'--csv-with-metadata': flag(),
|
|
162
169
|
'--domains': flag(),
|
|
163
170
|
'--queries': flag(),
|
|
164
171
|
'--registered-only': flag(),
|
|
@@ -190,6 +197,7 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
|
|
|
190
197
|
'--mail-profile': enumeration(['standard', 'defensive-no-mail', 'parked']),
|
|
191
198
|
'--sarif': flag(),
|
|
192
199
|
'--owned-domain': flag(),
|
|
200
|
+
'--include-inherited-dns': flag(),
|
|
193
201
|
'--trust-anchor': file(),
|
|
194
202
|
'--owned-or-authorized': flag(),
|
|
195
203
|
'--active-probe': flag(),
|
|
@@ -198,13 +206,15 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
|
|
|
198
206
|
'--summary-json': flag(),
|
|
199
207
|
'--passphrase-file': file(),
|
|
200
208
|
'--manifest': file(),
|
|
201
|
-
'--
|
|
209
|
+
'--bagit': flag(),
|
|
210
|
+
'--manifest-entry': enumeration(Array.from({ length: MAX_INVESTIGATION_MANIFEST_ARTIFACTS }, (_, index) => `artifact-${index + 1}`)),
|
|
202
211
|
'--search': text(),
|
|
203
212
|
'--require-match': flag(),
|
|
204
213
|
'--reveal': flag(),
|
|
205
214
|
'--expect-content-digest': text(true),
|
|
206
215
|
'--private-key-file': file(),
|
|
207
216
|
'--public-key-file': file(),
|
|
217
|
+
'--trust-store-file': file(),
|
|
208
218
|
'--mmdb': file(),
|
|
209
219
|
'--audience': enumeration(['internal', 'trusted', 'public']),
|
|
210
220
|
'--reviewed': flag(),
|
|
@@ -219,10 +229,20 @@ const CLI_OPTION_DEFINITIONS = Object.freeze({
|
|
|
219
229
|
'--list': flag(),
|
|
220
230
|
'--explain': enumeration(INVESTIGATION_PLAN_RECIPES),
|
|
221
231
|
'--select': optionDefinition('text', { occurrence: 'repeatable', acceptsOptionLikeValue: true }),
|
|
232
|
+
'--use-artifact': optionDefinition('text', { occurrence: 'repeatable' }),
|
|
233
|
+
'--confirm-review': optionDefinition('text', { occurrence: 'repeatable' }),
|
|
222
234
|
'--approve-network': flag(),
|
|
223
235
|
'--left-session': text(true),
|
|
224
236
|
'--right-session': text(true),
|
|
225
237
|
'--compact': flag(),
|
|
238
|
+
'--case-id': text(),
|
|
239
|
+
'--domain': text(),
|
|
240
|
+
'--title': text(),
|
|
241
|
+
'--new-incident': flag(),
|
|
242
|
+
'--text': text(),
|
|
243
|
+
'--note-file': file(),
|
|
244
|
+
'--input': file(),
|
|
245
|
+
'--expect-file-digest': text(),
|
|
226
246
|
});
|
|
227
247
|
function constraint(value) {
|
|
228
248
|
if (value.kind === 'mutually_exclusive') {
|
|
@@ -244,10 +264,15 @@ const QUIET_OUTPUT_CONSTRAINT = constraint({
|
|
|
244
264
|
kind: 'mutually_exclusive',
|
|
245
265
|
options: ['--quiet', '--output'],
|
|
246
266
|
});
|
|
247
|
-
const
|
|
248
|
-
'--json', '
|
|
249
|
-
'--
|
|
267
|
+
const PRESENTATION_OPTIONS = Object.freeze([
|
|
268
|
+
['--json', 'JSON'], ['--jsonl', 'JSON Lines'], ['--junit', 'JUnit XML'],
|
|
269
|
+
['--csv', 'CSV'],
|
|
270
|
+
['--csv-with-metadata', 'CSV with evidence metadata'],
|
|
271
|
+
['--domains', 'domain list'], ['--queries', 'query list'],
|
|
272
|
+
['--markdown', 'Markdown'], ['--html', 'HTML'], ['--sarif', 'SARIF'],
|
|
273
|
+
['--summary-json', 'summary JSON'],
|
|
250
274
|
]);
|
|
275
|
+
const MACHINE_OUTPUT_OPTIONS = Object.freeze(PRESENTATION_OPTIONS.map(([option]) => option));
|
|
251
276
|
function optionSpec(command, option, scope) {
|
|
252
277
|
const definition = CLI_OPTION_DEFINITIONS[option];
|
|
253
278
|
const valueKind = definition.valueKind(command);
|
|
@@ -377,16 +402,22 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
377
402
|
}),
|
|
378
403
|
manifest: commandSeed({
|
|
379
404
|
reference: {
|
|
380
|
-
description:
|
|
405
|
+
description: `Record an ordered, path-free manifest for up to ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} local files. Use --package --output evidence.zip for a ZIP, add --passphrase-file to encrypt the ordinary package, or use --folder ./evidence for a new unencrypted folder. Add --bagit to ZIP or folder output for BagIt 1.0 with SHA-512 checksums.`,
|
|
381
406
|
example: 'whoisleuth manifest lookup.json comparison.json --workflow "domain review" --json',
|
|
382
|
-
boundary: '
|
|
407
|
+
boundary: 'Ordinary output contains metadata only. ZIP and folder output include unchanged selected bytes and are private until reviewed for sharing. Folders must be new; existing destinations are never replaced and a failed write may leave explicit partial output. BagIt output is unencrypted; its checksums do not authenticate evidence. Filenames ending in .json are parsed as JSON; other files are opaque and never executed. Original paths are omitted. No network request is made.',
|
|
383
408
|
},
|
|
384
|
-
collection: { mode: 'offline', scope:
|
|
409
|
+
collection: { mode: 'offline', scope: `Reads 1 to ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} local files, at most ${MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES / 1024 / 1024} MiB each and ${MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES.toLocaleString('en-AU')} bytes combined; retains no source paths.` },
|
|
385
410
|
summary: 'Build an evidence manifest offline',
|
|
386
|
-
options: ['--workflow', '--configuration-digest', '--json', '--quiet', '--no-color'],
|
|
387
|
-
positionals: Object.freeze([positional('artefacts', 'file', 1,
|
|
411
|
+
options: ['--workflow', '--configuration-digest', '--package', '--bagit', '--passphrase-file', '--folder', '--json', '--quiet', '--no-color'],
|
|
412
|
+
positionals: Object.freeze([positional('artefacts', 'file', 1, MAX_INVESTIGATION_MANIFEST_ARTIFACTS)]),
|
|
388
413
|
constraints: Object.freeze([
|
|
389
414
|
constraint({ kind: 'required', options: ['--workflow'] }),
|
|
415
|
+
constraint({ kind: 'requires_any', option: '--bagit', requiredOptions: ['--package', '--folder'] }),
|
|
416
|
+
constraint({ kind: 'excludes_all', option: '--bagit', excludedOptions: ['--passphrase-file'] }),
|
|
417
|
+
constraint({ kind: 'requires_all', option: '--package', requiredOptions: ['--output'] }),
|
|
418
|
+
constraint({ kind: 'requires_all', option: '--passphrase-file', requiredOptions: ['--package'] }),
|
|
419
|
+
constraint({ kind: 'mutually_exclusive', options: ['--package', '--json'] }),
|
|
420
|
+
constraint({ kind: 'excludes_all', option: '--folder', excludedOptions: ['--package', '--output', '--force'] }),
|
|
390
421
|
]),
|
|
391
422
|
handlerOwner: 'inline',
|
|
392
423
|
networkEffect: 'offline',
|
|
@@ -441,17 +472,18 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
441
472
|
reference: {
|
|
442
473
|
description: 'Collect registration evidence for one domain, IP, or ASN.',
|
|
443
474
|
example: 'whoisleuth lookup example.test --deep --browse',
|
|
444
|
-
boundary: 'Fast is the default. An ICANN-recognised public domain, reserved documentation domain, IP, or ASN may occupy command position as shorthand; it delegates to this same parser and URL-like input requires the explicit lookup command. Deep mode adds bounded WHOIS, DNS, HTTP, TLS, technology, posture, and network context where applicable. A full Deep homepage observation can derive fixed publication and delivery/cache summaries from the same response without retaining raw metadata values or making another request. --browse opens before collection, shows aggregate Fast progress or independently settled planned Deep sources, and then navigates allowlisted retained fields in the completed document. Press ? for help and / to search rendered panel text only. Closing during collection cancels without a partial document. --save-lookup writes the exact completed private JSON only after a normal browser close; it can contain normalised evidence omitted from panels and refuses an existing path.',
|
|
475
|
+
boundary: 'Fast is the default. An ICANN-recognised public domain, reserved documentation domain, IP, or ASN may occupy command position as shorthand; it delegates to this same parser and URL-like input requires the explicit lookup command. Deep mode adds bounded WHOIS, DNS, HTTP, TLS, technology, posture, and network context where applicable. --deep --exact-url explicitly sends the input URL path and query to the website, without its fragment; ordinary URL input sends only the hostname. --plan remains offline and omits the selected URL. Retained paths and page-derived text require review before sharing. A full Deep homepage observation can derive fixed publication and delivery/cache summaries from the same response without retaining raw metadata values or making another request. --browse opens before collection, shows aggregate Fast progress or independently settled planned Deep sources, and then navigates allowlisted retained fields in the completed document. Press ? for help and / to search rendered panel text only. Closing during collection cancels without a partial document. --save-lookup writes the exact completed private JSON only after a normal browser close; it can contain normalised evidence omitted from panels and refuses an existing path.',
|
|
445
476
|
},
|
|
446
477
|
collection: { mode: 'network', scope: 'Accepts one target. Fast is the default; deep collection must be selected explicitly.' },
|
|
447
478
|
summary: 'Collect one domain, IP, or ASN',
|
|
448
|
-
options: ['--json', '--junit', '--markdown', '--html', '--no-attribution', '--fast', '--deep', '--observer', '--vantage', '--plan', '--summary', '--verbose', '--browse', '--save-lookup', '--strict-exit', '--fail-on', '--events', '--quiet', '--no-color'],
|
|
479
|
+
options: ['--json', '--junit', '--markdown', '--html', '--no-attribution', '--fast', '--deep', '--exact-url', '--observer', '--vantage', '--plan', '--summary', '--verbose', '--browse', '--save-lookup', '--strict-exit', '--fail-on', '--events', '--quiet', '--no-color'],
|
|
449
480
|
positionals: Object.freeze([positional('target', 'text', 0, 1, [], 'argv_or_stdin', ['--browse'])]),
|
|
450
481
|
constraints: Object.freeze([
|
|
451
482
|
constraint({ kind: 'mutually_exclusive', options: ['--json', '--junit', '--markdown', '--html'] }),
|
|
452
483
|
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
453
484
|
constraint({ kind: 'mutually_exclusive', options: ['--summary', '--verbose'] }),
|
|
454
485
|
constraint({ kind: 'requires_all', option: '--save-lookup', requiredOptions: ['--browse'] }),
|
|
486
|
+
constraint({ kind: 'requires_all', option: '--exact-url', requiredOptions: ['--deep'] }),
|
|
455
487
|
constraint({ kind: 'excludes_all', option: '--summary', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
|
|
456
488
|
constraint({ kind: 'excludes_all', option: '--verbose', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
|
|
457
489
|
constraint({ kind: 'excludes_all', option: '--browse', excludedOptions: ['--json', '--junit', '--markdown', '--html', '--summary', '--verbose', '--events', '--plan', '--quiet'] }),
|
|
@@ -471,18 +503,18 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
471
503
|
reference: {
|
|
472
504
|
description: 'Triage newline-delimited domains, IPs, or ASNs with bounded concurrency.',
|
|
473
505
|
example: 'cat domains.txt | whoisleuth bulk --jsonl',
|
|
474
|
-
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.',
|
|
506
|
+
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. --csv-with-metadata adds source versions, separate observation and report times, collection origin and diagnostic states; --csv retains the compact columns.',
|
|
475
507
|
},
|
|
476
508
|
collection: { mode: 'network', scope: 'Accepts at most 500 fast or 50 deep targets, with concurrency capped at 8 fast or 3 deep.' },
|
|
477
509
|
summary: 'Run bounded multi-target collection',
|
|
478
|
-
options: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries', '--registered-only', '--inconclusive-only', '--errors-only', '--fast', '--deep', '--concurrency', '--checkpoint', '--resume', '--events', '--plan', '--fail-on', '--quiet', '--no-color'],
|
|
510
|
+
options: ['--json', '--jsonl', '--junit', '--csv', '--csv-with-metadata', '--domains', '--queries', '--registered-only', '--inconclusive-only', '--errors-only', '--fast', '--deep', '--concurrency', '--checkpoint', '--resume', '--events', '--plan', '--fail-on', '--quiet', '--no-color'],
|
|
479
511
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
480
512
|
constraints: Object.freeze([
|
|
481
|
-
constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries'] }),
|
|
513
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--junit', '--csv', '--csv-with-metadata', '--domains', '--queries'] }),
|
|
482
514
|
constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--errors-only'] }),
|
|
483
515
|
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
484
516
|
constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
|
|
485
|
-
constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--junit', '--csv', '--domains', '--queries', '--events', '--checkpoint', '--resume', '--quiet', '--fail-on'] }),
|
|
517
|
+
constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--junit', '--csv', '--csv-with-metadata', '--domains', '--queries', '--events', '--checkpoint', '--resume', '--quiet', '--fail-on'] }),
|
|
486
518
|
]),
|
|
487
519
|
handlerOwner: 'bulk',
|
|
488
520
|
networkEffect: 'conditional_network',
|
|
@@ -535,7 +567,7 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
535
567
|
}),
|
|
536
568
|
discover: commandSeed({
|
|
537
569
|
reference: {
|
|
538
|
-
description: 'Generate bounded lookalike-domain candidates from
|
|
570
|
+
description: 'Generate bounded lookalike-domain candidates from a brand or registrable domain, including multi-part public suffixes.',
|
|
539
571
|
example: 'whoisleuth discover example.test --preset common --jsonl',
|
|
540
572
|
boundary: 'Generation and optional local snapshot comparison are offline. Candidates are leads only and are not resolved, registered, or classified as malicious.',
|
|
541
573
|
},
|
|
@@ -561,20 +593,20 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
561
593
|
reference: {
|
|
562
594
|
description: 'Generate a bounded candidate set, collect a selected subset, and produce a supervised review queue.',
|
|
563
595
|
example: 'whoisleuth discover-scan example.test --scan-limit 50 --checkpoint scan.json --json',
|
|
564
|
-
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.',
|
|
596
|
+
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. --csv-with-metadata adds source versions, separate observation and report times, collection origin and diagnostic states; --csv retains the compact columns.',
|
|
565
597
|
},
|
|
566
598
|
collection: { mode: 'network', scope: 'Scans at most 500 fast or 50 deep candidates, with concurrency capped at 8 fast or 3 deep.' },
|
|
567
599
|
summary: 'Collect a supervised candidate review queue',
|
|
568
|
-
options: ['--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'],
|
|
600
|
+
options: ['--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', '--csv-with-metadata', '--domains', '--quiet', '--no-color'],
|
|
569
601
|
positionals: OPTIONAL_TEXT_POSITIONAL,
|
|
570
602
|
constraints: Object.freeze([
|
|
571
|
-
constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--csv', '--domains'] }),
|
|
603
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--csv', '--csv-with-metadata', '--domains'] }),
|
|
572
604
|
constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
|
|
573
605
|
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
574
606
|
constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only'] }),
|
|
575
607
|
constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
|
|
576
608
|
constraint({ kind: 'value_excludes', option: '--preset', value: 'common', excludedOptions: ['--dictionary'] }),
|
|
577
|
-
constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--csv', '--domains', '--events', '--checkpoint', '--resume', '--observation-snapshot', '--quiet', '--fail-on'] }),
|
|
609
|
+
constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--csv', '--csv-with-metadata', '--domains', '--events', '--checkpoint', '--resume', '--observation-snapshot', '--quiet', '--fail-on'] }),
|
|
578
610
|
]),
|
|
579
611
|
handlerOwner: 'discovery_scan',
|
|
580
612
|
networkEffect: 'conditional_network',
|
|
@@ -589,11 +621,11 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
589
621
|
reference: {
|
|
590
622
|
description: 'Review bounded DNS mail, delegation, and domain-control posture.',
|
|
591
623
|
example: 'whoisleuth posture example.test --mail-profile standard --json',
|
|
592
|
-
boundary: 'Missing or failed DNS observations remain inconclusive and
|
|
624
|
+
boundary: 'Missing or failed DNS observations remain inconclusive. --include-inherited-dns explicitly adds a bounded DMARC tree walk and direct parent-delegation sample; records retain their queried owner and source. No message is sent and receiver enforcement is not inferred.',
|
|
593
625
|
},
|
|
594
|
-
collection: { mode: 'network', scope: 'Accepts one domain and performs bounded RDAP, DNS, and conditional MTA-STS HTTPS requests.' },
|
|
626
|
+
collection: { mode: 'network', scope: 'Accepts one domain and performs bounded RDAP, DNS, and conditional MTA-STS HTTPS requests. --include-inherited-dns separately adds ancestor DMARC questions and direct DNS/TCP to sampled parent servers.' },
|
|
595
627
|
summary: 'Review DNS and mail posture',
|
|
596
|
-
options: ['--selectors', '--retired-selectors', '--mail-profile', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
|
|
628
|
+
options: ['--selectors', '--retired-selectors', '--mail-profile', '--include-inherited-dns', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
|
|
597
629
|
positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
598
630
|
constraints: Object.freeze([
|
|
599
631
|
constraint({ kind: 'mutually_exclusive', options: ['--json', '--sarif'] }),
|
|
@@ -818,17 +850,21 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
818
850
|
}),
|
|
819
851
|
"verify-artifact": commandSeed({
|
|
820
852
|
reference: {
|
|
821
|
-
description: 'Validate a supported archive, claim passport, packet, manifest, saved Lookup
|
|
853
|
+
description: 'Validate a supported archive, ordinary Case export, claim passport, packet, manifest, saved Lookup or Lookup-evidence export without printing evidence contents. Use --package for an evidence ZIP or encrypted package, with --passphrase-file to unlock it; use --folder ./evidence for an unencrypted evidence folder. Add --bagit with --package or --folder to verify BagIt 1.0.',
|
|
822
854
|
example: 'whoisleuth verify-artifact report.json --manifest manifest.json --manifest-entry artifact-2 --json --strict-exit',
|
|
823
|
-
boundary: 'Verification is offline and redacted.
|
|
855
|
+
boundary: 'Verification is offline and redacted. ZIP and folder entries are reported separately without importing them. Case exports are checked without repairing content; ordinary package review also counts original references with matching bytes. Ordinary folders allow only the declared layout; BagIt allows bounded nested payloads and checks SHA-256/SHA-512 manifests without interpreting payloads. Symbolic links are refused. BagIt fetch.txt is never fetched; missing files, mismatches and unsupported algorithms remain explicit. Package digests describe bytes, not filesystem metadata or authenticity. In scripts, use --strict-exit: incomplete verification returns 4. Default exit 0 means the report was produced, not that its checks passed.',
|
|
824
856
|
},
|
|
825
|
-
collection: { mode: 'offline', scope: 'Reads one selected bounded artefact and, when explicitly supplied, one manifest whose selected entry is compared by exact bytes and canonical identity.' },
|
|
857
|
+
collection: { mode: 'offline', scope: 'Reads one selected bounded artefact, ZIP or explicit evidence folder and, when explicitly supplied, one manifest whose selected entry is compared by exact bytes and canonical identity.' },
|
|
826
858
|
summary: 'Validate saved evidence offline',
|
|
827
|
-
options: ['--passphrase-file', '--manifest', '--manifest-entry', '--json', '--strict-exit', '--quiet', '--no-color'],
|
|
828
|
-
positionals:
|
|
859
|
+
options: ['--passphrase-file', '--manifest', '--manifest-entry', '--package', '--bagit', '--folder', '--json', '--strict-exit', '--quiet', '--no-color'],
|
|
860
|
+
positionals: Object.freeze([positional('source', 'file', 0, 1, [], 'argv_or_stdin', ['--package'])]),
|
|
829
861
|
constraints: Object.freeze([
|
|
862
|
+
constraint({ kind: 'requires_any', option: '--bagit', requiredOptions: ['--package', '--folder'] }),
|
|
863
|
+
constraint({ kind: 'excludes_all', option: '--bagit', excludedOptions: ['--passphrase-file', '--manifest', '--manifest-entry'] }),
|
|
830
864
|
constraint({ kind: 'requires_all', option: '--manifest', requiredOptions: ['--manifest-entry'] }),
|
|
831
865
|
constraint({ kind: 'requires_all', option: '--manifest-entry', requiredOptions: ['--manifest'] }),
|
|
866
|
+
constraint({ kind: 'excludes_all', option: '--package', excludedOptions: ['--manifest', '--manifest-entry'] }),
|
|
867
|
+
constraint({ kind: 'excludes_all', option: '--folder', excludedOptions: ['--package', '--manifest', '--manifest-entry', '--passphrase-file'] }),
|
|
832
868
|
]),
|
|
833
869
|
handlerOwner: 'inline',
|
|
834
870
|
networkEffect: 'offline',
|
|
@@ -863,7 +899,7 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
863
899
|
reference: {
|
|
864
900
|
description: `Summarise or search one current version-${WORKSPACE_ARCHIVE_VERSION} workspace archive, with exact ${LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION} support and redacted output by default.`,
|
|
865
901
|
example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
|
|
866
|
-
boundary: 'Exact
|
|
902
|
+
boundary: 'Exact values require --reveal. New content comparisons use the reported sorted-json-v2:sha256 identity with --expect-content-digest; bare sha256 hashes retain their legacy locale-sensitive meaning. Retired and future archives are rejected. The archive is read locally and is never uploaded.',
|
|
867
903
|
},
|
|
868
904
|
collection: { mode: 'offline', scope: `Reads one selected bounded workspace archive v${WORKSPACE_ARCHIVE_VERSION}, retains exact ${LEGACY_WORKSPACE_ARCHIVE_SCOPE} compatibility, and redacts output by default.` },
|
|
869
905
|
summary: 'Inspect an archive locally',
|
|
@@ -906,19 +942,19 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
906
942
|
}),
|
|
907
943
|
"verify-signature": commandSeed({
|
|
908
944
|
reference: {
|
|
909
|
-
description: 'Verify
|
|
910
|
-
example: 'whoisleuth verify-signature packet.signed.json --json',
|
|
911
|
-
boundary: 'A valid signature proves package consistency for the embedded key
|
|
945
|
+
description: 'Verify one signed package and report embedded-artefact assurance separately. --trust-store-file also checks an explicit local fingerprint policy and emits a signer-trust report; unknown, retired, revoked or future-reviewed entries exit 4.',
|
|
946
|
+
example: 'whoisleuth verify-signature packet.signed.json --trust-store-file trust.json --json',
|
|
947
|
+
boundary: 'A valid signature proves package consistency for the embedded key, not identity, authority or evidence accuracy. With --trust-store-file, unknown, retired, revoked or future-reviewed entries exit 4 even if --public-key-file matches. Replacement fingerprints need their own trusted entry; no signing date overrides current revocation.',
|
|
912
948
|
},
|
|
913
|
-
collection: { mode: 'offline', scope: 'Reads one selected signed package
|
|
949
|
+
collection: { mode: 'offline', scope: 'Reads one selected signed package, optional local public key and explicit fingerprint trust file. No automatic trust discovery or network requests.' },
|
|
914
950
|
summary: 'Verify a signed evidence package',
|
|
915
|
-
options: ['--public-key-file', '--json', '--quiet', '--no-color'],
|
|
951
|
+
options: ['--public-key-file', '--trust-store-file', '--json', '--quiet', '--no-color'],
|
|
916
952
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
917
953
|
constraints: EMPTY_CONSTRAINTS,
|
|
918
954
|
handlerOwner: 'evidence',
|
|
919
955
|
networkEffect: 'offline',
|
|
920
956
|
common: false,
|
|
921
|
-
schemaIdentifiers: Object.freeze(['whoisleuth.evidence-signature-verification']),
|
|
957
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.evidence-signature-verification', 'whoisleuth.evidence-signer-trust-store', 'whoisleuth.evidence-signer-trust-report']),
|
|
922
958
|
primaryArtefacts: Object.freeze([]),
|
|
923
959
|
planSupport: false,
|
|
924
960
|
additionalOutputFormats: Object.freeze([]),
|
|
@@ -1072,13 +1108,29 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1072
1108
|
additionalOutputFormats: Object.freeze([]),
|
|
1073
1109
|
bootstrapProfile: 'allowed',
|
|
1074
1110
|
}),
|
|
1111
|
+
case: commandSeed({
|
|
1112
|
+
reference: {
|
|
1113
|
+
description: 'Show or open a local Case, append a note or evidence pin, record an assessment, or retain an offline recheck. Use --input for pin, assessment and recheck JSON; --text or --note-file for a note. Mutations require --output and always write the complete current Case export.',
|
|
1114
|
+
example: 'whoisleuth case open --domain example.test --output cases.json\n whoisleuth case show cases.json\n whoisleuth case note cases.json --text "Review the retained observation" --output cases.json --force',
|
|
1115
|
+
boundary: 'No database, browser launch, request or external report is created. Select --case-id when a file contains multiple Cases. Existing files require --force; --expect-file-digest sha256:<digest> additionally checks the exact file reviewed earlier. Source and output leases reject concurrent changes. Interrupted .workflow.lock files require deliberate inspection. Recheck records supplied observations; it does not collect them. Not reproduced requires an existing saved question, a complete observation and comparable conditions. Working exports include private analyst content and file references, not attached file bytes.',
|
|
1116
|
+
},
|
|
1117
|
+
collection: { mode: 'offline', scope: `Reads exact Case schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')}. Input is bounded to ${MAX_EDITABLE_CASE_INPUT_BYTES / 1024 / 1024} MiB including formatting; the complete canonical Case store must fit ${MAX_CASE_STORE_BYTES / 1024 / 1024} MiB without pruning. Writes current schema ${CASE_SCHEMA_VERSION}.` },
|
|
1118
|
+
summary: 'Review and update ordinary local Case files',
|
|
1119
|
+
options: ['--case-id', '--domain', '--title', '--new-incident', '--text', '--note-file', '--input', '--expect-file-digest', '--json', '--no-color'],
|
|
1120
|
+
positionals: Object.freeze([positional('operation', 'enum', 1, 1, CLI_CASE_OPERATIONS), positional('source', 'file', 0, 1)]),
|
|
1121
|
+
constraints: Object.freeze([constraint({ kind: 'mutually_exclusive', options: ['--text', '--note-file'] })]),
|
|
1122
|
+
handlerOwner: 'inline', networkEffect: 'offline', common: true,
|
|
1123
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.case-export']),
|
|
1124
|
+
primaryArtefacts: Object.freeze(['Case export']), planSupport: false,
|
|
1125
|
+
additionalOutputFormats: Object.freeze([]), bootstrapProfile: 'allowed',
|
|
1126
|
+
}),
|
|
1075
1127
|
"case-pack": commandSeed({
|
|
1076
1128
|
reference: {
|
|
1077
|
-
description: `Package browser-created Case
|
|
1129
|
+
description: `Package browser-created Case records from schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')} as a reviewed, audience-specific Case-pack v2 with current schema ${CASE_SCHEMA_VERSION}.`,
|
|
1078
1130
|
example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
|
|
1079
1131
|
boundary: 'The command is an offline handoff from the browser Case workflow: it creates a new package, never creates or mutates a durable Case, never mutates the source archive, and requires an explicit review acknowledgement.',
|
|
1080
1132
|
},
|
|
1081
|
-
collection: { mode: 'offline', scope: `Reads one bounded Case
|
|
1133
|
+
collection: { mode: 'offline', scope: `Reads one bounded Case export from schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')} and writes a separate audience-specific Case-pack v2.` },
|
|
1082
1134
|
summary: 'Build a reviewed case package',
|
|
1083
1135
|
options: ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
|
|
1084
1136
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
@@ -1098,7 +1150,7 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1098
1150
|
reference: {
|
|
1099
1151
|
description: 'Build an integrity-protected desired-state manifest or compare one with supplied observations.',
|
|
1100
1152
|
example: 'whoisleuth domain-control domain-control-input.json --json',
|
|
1101
|
-
boundary: 'The command is offline and changes no registrar, DNS, mail, or certificate configuration. Only complete
|
|
1153
|
+
boundary: 'The command is offline and changes no registrar, DNS, mail, or certificate configuration. Only complete, recent source observations can establish drift or expected absence.',
|
|
1102
1154
|
},
|
|
1103
1155
|
collection: { mode: 'offline', scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
|
|
1104
1156
|
summary: 'Build or review a domain control manifest',
|
|
@@ -1182,7 +1234,7 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1182
1234
|
example: 'whoisleuth sharing-review packet.json --marking amber --recipient-scope organization --purpose "Reviewed incident handoff" --human-reviewed --personal-data-reviewed --redactions-confirmed --json',
|
|
1183
1235
|
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.',
|
|
1184
1236
|
},
|
|
1185
|
-
collection: { mode: 'offline', scope: 'Reads one artefact
|
|
1237
|
+
collection: { mode: 'offline', scope: 'Reads one bounded artefact, emits only schema/version metadata and no content values, and performs no transmission.' },
|
|
1186
1238
|
summary: 'Lint an artefact before deliberate sharing',
|
|
1187
1239
|
options: ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
|
|
1188
1240
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
@@ -1226,12 +1278,12 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1226
1278
|
"workflow-run": commandSeed({
|
|
1227
1279
|
reference: {
|
|
1228
1280
|
description: 'Execute approved steps from a fixed investigation recipe and emit a resumable checkpoint.',
|
|
1229
|
-
example: 'whoisleuth workflow-run domain-triage example.test --
|
|
1230
|
-
boundary: 'Only installed recipe commands can run. Network steps require explicit approval for each invocation. Repeat --select in
|
|
1281
|
+
example: 'whoisleuth workflow-run domain-triage example.test --approve-network --json --output run.json',
|
|
1282
|
+
boundary: 'Only installed recipe commands can run. Network steps require explicit approval for each invocation. New runs connect compatible earlier outputs using the recipe defaults. Use --use-artifact <step-id>:<input-number>=<earlier-step-id> to override a connection; input numbers start at 1. Repeat --select for remaining placeholders in order, or supply every input for a step to replace its connections with files. Values stay literal and cannot start with a hyphen or invoke a shell. Optional --interactive prompts on terminal stderr for missing inputs; a blank answer pauses. It grants neither network approval nor human-review confirmation. A step declaring human review still requires --confirm-review <step-id> for that invocation. Checkpoints do not grant later approvals. Resumes preserve recorded connections. Content digests identify retained output, not authenticity or freshness. Partial collections pause for review and are not recollected on resume; failed validation or export steps remain retryable. Diagnostics go to stderr. File output holds exclusive adjacent locks and refuses concurrently changed state files.',
|
|
1231
1283
|
},
|
|
1232
1284
|
collection: { mode: 'network', scope: 'Runs only fixed-recipe steps; network collection requires --approve-network and unresolved analyst selections pause.' },
|
|
1233
1285
|
summary: 'Execute approved fixed-recipe steps',
|
|
1234
|
-
options: ['--select', '--approve-network', '--resume', '--json', '--quiet', '--no-color'],
|
|
1286
|
+
options: ['--select', '--use-artifact', '--confirm-review', '--approve-network', '--resume', '--interactive', '--json', '--quiet', '--no-color'],
|
|
1235
1287
|
positionals: Object.freeze([
|
|
1236
1288
|
positional('recipe', 'enum', 1, 1, RUNNABLE_INVESTIGATION_PLAN_RECIPES),
|
|
1237
1289
|
positional('subject', 'text', 1, 1),
|
|
@@ -1250,7 +1302,7 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1250
1302
|
reference: {
|
|
1251
1303
|
description: 'Compare an earlier and later artefact from the same retained Lookup, Bulk-session, or domain-portfolio family.',
|
|
1252
1304
|
example: 'whoisleuth diff earlier.json later.json --json',
|
|
1253
|
-
boundary: 'Comparison is offline: the left input is earlier and the right input is later. Inputs must belong to the same supported family. For a multi-session Bulk export, --left-session selects a session from the left file and --right-session selects one from the right; missing, unavailable, equal, and different evidence remain separate states.',
|
|
1305
|
+
boundary: 'Comparison is offline: the left input is earlier and the right input is later. Inputs must belong to the same supported family. Saved Lookups can describe the same or different domains; same-domain comparisons preserve observation times and collection uncertainty. For a multi-session Bulk export, --left-session selects a session from the left file and --right-session selects one from the right; missing, unavailable, equal, and different evidence remain separate states.',
|
|
1254
1306
|
},
|
|
1255
1307
|
collection: { mode: 'offline', scope: 'Reads two compatible retained artefacts capped at 8 MiB each and retains no source paths.' },
|
|
1256
1308
|
summary: 'Compare two compatible retained artefacts',
|
|
@@ -1310,7 +1362,7 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1310
1362
|
reference: {
|
|
1311
1363
|
description: 'Convert one saved lookup into a versioned evidence report.',
|
|
1312
1364
|
example: 'whoisleuth export lookup.json --markdown',
|
|
1313
|
-
boundary: `Saved Lookup versions 1 and 2 are capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Current schema-${LOOKUP_EVIDENCE_SCHEMA_VERSION} exports preserve evidence-source attribution and limitations; published v2
|
|
1365
|
+
boundary: `Saved Lookup versions 1 and 2 are capped at 8 MiB and scanned for duplicate keys, the prototype-sensitive __proto__ key, and bounded nesting, key, value, and per-container counts before parsing. Current schema-${LOOKUP_EVIDENCE_SCHEMA_VERSION} exports preserve evidence-source attribution and limitations; published v2 schemas ${PUBLISHED_V2_LOOKUP_EVIDENCE_VERSIONS} and exact v1 schema ${V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION} remain readable, while other historical and unreleased shapes are unsupported. Markdown and HTML include a presentation-only generator footer unless --no-attribution is selected; JSON retains bounded generator provenance. Compact output intentionally omits raw registry payloads.`,
|
|
1314
1366
|
},
|
|
1315
1367
|
collection: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
|
|
1316
1368
|
summary: 'Convert a lookup to an evidence report',
|
|
@@ -1338,17 +1390,12 @@ const HELP_COMMANDS_BY_GROUP = Object.freeze(Object.fromEntries(CLI_HELP_GROUP_O
|
|
|
1338
1390
|
])));
|
|
1339
1391
|
function documentationMetadata(seed, commonOptions, commandOptions, positionals) {
|
|
1340
1392
|
const explicitAuthorisationRequired = commandOptions.some((option) => (option === '--owned-or-authorized' || option === '--active-probe' || option === '--approve-network'));
|
|
1341
|
-
const
|
|
1342
|
-
[
|
|
1343
|
-
[
|
|
1344
|
-
['--markdown', 'Markdown'], ['--html', 'HTML'], ['--sarif', 'SARIF'],
|
|
1345
|
-
['--summary-json', 'summary JSON'],
|
|
1346
|
-
];
|
|
1393
|
+
const presentationOptions = Object.freeze(PRESENTATION_OPTIONS
|
|
1394
|
+
.filter(([option]) => commandOptions.includes(option))
|
|
1395
|
+
.map(([option, format]) => Object.freeze({ option, format })));
|
|
1347
1396
|
const outputFormats = new Set(['terminal']);
|
|
1348
|
-
for (const
|
|
1349
|
-
|
|
1350
|
-
outputFormats.add(label);
|
|
1351
|
-
}
|
|
1397
|
+
for (const { format } of presentationOptions)
|
|
1398
|
+
outputFormats.add(format);
|
|
1352
1399
|
for (const format of seed.additionalOutputFormats)
|
|
1353
1400
|
outputFormats.add(format);
|
|
1354
1401
|
const positionalLimits = positionals.map((item) => (`${item.name}: ${item.minimum}-${item.maximum} ${item.valueKind} value${item.maximum === 1 ? '' : 's'}`));
|
|
@@ -1373,14 +1420,19 @@ function documentationMetadata(seed, commonOptions, commandOptions, positionals)
|
|
|
1373
1420
|
: []),
|
|
1374
1421
|
]),
|
|
1375
1422
|
outputFormats: Object.freeze([...outputFormats]),
|
|
1423
|
+
presentationOptions,
|
|
1424
|
+
fileOutput: commonOptions.includes('--output'),
|
|
1376
1425
|
primaryEvidenceArtefacts: seed.primaryArtefacts,
|
|
1377
1426
|
});
|
|
1378
1427
|
}
|
|
1428
|
+
function expandedOptionValues(specification) {
|
|
1429
|
+
return specification.valueKind === 'enum' && specification.values.join('|').length > 60;
|
|
1430
|
+
}
|
|
1379
1431
|
function optionUsage(specification) {
|
|
1380
1432
|
if (specification.arity === 0)
|
|
1381
1433
|
return specification.option;
|
|
1382
1434
|
if (specification.valueKind === 'enum') {
|
|
1383
|
-
return `${specification.option} <${specification.values.join('|')}>`;
|
|
1435
|
+
return `${specification.option} <${expandedOptionValues(specification) ? specification.option.slice(2) : specification.values.join('|')}>`;
|
|
1384
1436
|
}
|
|
1385
1437
|
if (specification.valueKind === 'policy_list')
|
|
1386
1438
|
return `${specification.option} <policy[,policy...]>`;
|
|
@@ -1470,11 +1522,34 @@ function commandDefinition(command) {
|
|
|
1470
1522
|
function metaActionDefinition(id) {
|
|
1471
1523
|
return CLI_META_ACTION_BY_ID[id];
|
|
1472
1524
|
}
|
|
1525
|
+
function cliInvocationOptionIndices(argv) {
|
|
1526
|
+
const namedCommand = isCliCommand(argv[0]) ? argv[0] : null;
|
|
1527
|
+
const options = new Map(commandDefinition(namedCommand ?? 'lookup').grammar.options
|
|
1528
|
+
.map((option) => [option.option, option]));
|
|
1529
|
+
const indices = new Set();
|
|
1530
|
+
for (let index = namedCommand ? 1 : 0; index < argv.length; index += 1) {
|
|
1531
|
+
const argument = argv[index];
|
|
1532
|
+
if (argument === '--')
|
|
1533
|
+
break;
|
|
1534
|
+
if (!argument.startsWith('-'))
|
|
1535
|
+
continue;
|
|
1536
|
+
indices.add(index);
|
|
1537
|
+
const option = options.get(argument);
|
|
1538
|
+
const value = argv[index + 1];
|
|
1539
|
+
if (option?.arity === 1 && value !== undefined) {
|
|
1540
|
+
if (value === '--' && !option.acceptsOptionLikeValue)
|
|
1541
|
+
break;
|
|
1542
|
+
index += 1;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
return indices;
|
|
1546
|
+
}
|
|
1473
1547
|
function cliMetaActionForInvocation(argv) {
|
|
1548
|
+
const optionIndices = cliInvocationOptionIndices(argv);
|
|
1474
1549
|
for (const action of CLI_META_ACTIONS) {
|
|
1475
1550
|
const matches = action.scope === 'root_only'
|
|
1476
1551
|
? action.aliases.includes(argv[0] ?? '')
|
|
1477
|
-
: argv.some((argument) => action.aliases.includes(argument));
|
|
1552
|
+
: argv.some((argument, index) => optionIndices.has(index) && action.aliases.includes(argument));
|
|
1478
1553
|
if (matches)
|
|
1479
1554
|
return action;
|
|
1480
1555
|
}
|
|
@@ -1522,15 +1597,8 @@ function commandOwnsOption(command, option) {
|
|
|
1522
1597
|
return commandDefinition(command).grammar.options.some((candidate) => candidate.option === option);
|
|
1523
1598
|
}
|
|
1524
1599
|
function invocationHasFlag(command, args, flag) {
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
if (argument === flag)
|
|
1528
|
-
return true;
|
|
1529
|
-
const specification = argument ? commandOptionSpec(command, argument) : null;
|
|
1530
|
-
if (specification?.arity === 1)
|
|
1531
|
-
index += 1;
|
|
1532
|
-
}
|
|
1533
|
-
return false;
|
|
1600
|
+
const argv = [command, ...args];
|
|
1601
|
+
return [...cliInvocationOptionIndices(argv)].some((index) => argv[index] === flag);
|
|
1534
1602
|
}
|
|
1535
1603
|
function cliInvocationNetworkEffect(command, args) {
|
|
1536
1604
|
const effect = commandDefinition(command).execution.networkEffect;
|
|
@@ -1551,6 +1619,8 @@ function cliInvocationNetworkEffect(command, args) {
|
|
|
1551
1619
|
function commandHelp(command) {
|
|
1552
1620
|
const detail = COMMAND_DETAILS[command];
|
|
1553
1621
|
const collection = COMMAND_COLLECTION[command];
|
|
1554
|
-
|
|
1622
|
+
const values = commandDefinition(command).grammar.options.filter(expandedOptionValues)
|
|
1623
|
+
.map(option => `\n${option.option} values:\n${option.values.map(value => ` ${value}`).join('\n')}\n`).join('');
|
|
1624
|
+
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${values}\nRun "whoisleuth --help" to see the grouped command list.\n`;
|
|
1555
1625
|
}
|
|
1556
|
-
export { CLI_COMMAND_REGISTRY, CLI_COMMANDS, CLI_META_ACTIONS, COMMAND_COLLECTION, COMMAND_DESCRIPTIONS, COMMAND_DETAILS, COMMAND_USAGE, FILE_POSITIONAL_COMMANDS, HELP, HELP_COMMANDS_BY_GROUP, INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, OPTIONS_BY_COMMAND, cliMetaActionForInvocation, cliInvocationNetworkEffect, commandOptionSpec, commandOwnsOption, commandPositionalSpecs, commonOptionsForCommand, commandDefinition, commandHelp, isCliCommand, metaActionDefinition, };
|
|
1626
|
+
export { CLI_COMMAND_REGISTRY, CLI_CASE_OPERATIONS, CLI_COMMANDS, CLI_META_ACTIONS, COMMAND_COLLECTION, COMMAND_DESCRIPTIONS, COMMAND_DETAILS, COMMAND_USAGE, FILE_POSITIONAL_COMMANDS, HELP, HELP_COMMANDS_BY_GROUP, INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, OPTIONS_BY_COMMAND, cliMetaActionForInvocation, cliInvocationOptionIndices, cliInvocationNetworkEffect, commandOptionSpec, commandOwnsOption, commandPositionalSpecs, commonOptionsForCommand, commandDefinition, commandHelp, isCliCommand, metaActionDefinition, };
|