@slicedearth/whoisleuth-cli 2.2.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 +20 -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 +454 -1748
- package/cli/artifact-validation/case-response.mjs +66 -35
- 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 +112 -72
- 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-checkpoint.mjs +34 -3
- 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 +66 -111
- package/cli/command-argument-grammar.mjs +215 -0
- package/cli/command-reference.mjs +342 -285
- package/cli/completion.mjs +303 -80
- 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 +59 -18
- package/cli/discovery-scan-command-runner.mjs +21 -0
- package/cli/discovery-scan.mjs +13 -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 +2 -0
- package/cli/input.mjs +116 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +18 -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/lookalike-calibration.mjs +7 -2
- package/cli/lookup-brief.mjs +4 -2
- package/cli/lookup-browser-view.mjs +1 -1
- 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-header-review.mjs +467 -0
- 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 +70 -0
- package/cli/risk-calibration.mjs +19 -17
- package/cli/runner.mjs +62 -7
- 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 +77 -24
- package/docs/cli.md +300 -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 +51 -8
- 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-convergence-review.mjs +1 -0
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +73 -26
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +6 -0
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +49 -16
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +221 -117
- package/lib/domain-control-manifest.mjs +129 -69
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +201 -8
- package/lib/domain-posture.mjs +147 -29
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -543
- package/lib/evidence-report.mjs +5 -3
- 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 +111 -108
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/local-geoip-evidence.mjs +19 -3
- 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 +8 -10
- package/lib/lookup-source-progress.mjs +40 -14
- package/lib/lookup.mjs +48 -27
- package/lib/nameserver-preflight-review.mjs +1 -0
- 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-capabilities.mjs +1 -1
- 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 +85 -34
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +18 -6
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +42 -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 +178 -151
- 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 +153 -45
- 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/lib/zone-intent-review.mjs +16 -6
- 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 +127 -44
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-contracts.mjs +5 -20
- package/packages/cases/case-record-core.mjs +12 -22
- package/packages/cases/case-record-decisions.mjs +72 -0
- package/packages/cases/case-record-model.mjs +1 -0
- package/packages/cases/case-record-operations.mjs +95 -31
- package/packages/cases/case-record-projection.mjs +221 -0
- package/packages/cases/case-report.mjs +70 -54
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +64 -1115
- 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 +74 -572
- 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 +31 -2
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
- package/packages/comparison/page-similarity.mjs +5 -2
- 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 +366 -83
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +94 -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 +15 -11
- 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 +183 -211
- 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 +201 -33
- 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 +92 -51
- 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 +193 -165
- 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 +276 -22
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +23 -11
- package/packages/workspace/bulk-session-model.mjs +100 -24
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/detection-rule-model.mjs +70 -22
- 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 -23
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +164 -26
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +87 -21
- package/third-party-notices.txt +29 -2
- package/cli/evidence-command-arguments.mjs +0 -156
- package/packages/investigation/bulk-review-export.mjs +0 -63
|
@@ -1,13 +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
|
+
import { CLI_COMMAND_SEMANTICS, CLI_HELP_GROUP_ORDER, } from "../packages/contracts/cli-command-semantics.mjs";
|
|
5
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";
|
|
6
8
|
const LEGACY_WORKSPACE_ARCHIVE_VERSIONS = SUPPORTED_WORKSPACE_ARCHIVE_VERSIONS
|
|
7
9
|
.filter((version) => version !== WORKSPACE_ARCHIVE_VERSION);
|
|
8
10
|
const LEGACY_WORKSPACE_ARCHIVE_DESCRIPTION = LEGACY_WORKSPACE_ARCHIVE_VERSIONS
|
|
9
11
|
.map((version) => `version-${version}`)
|
|
10
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(', ');
|
|
11
16
|
const LEGACY_WORKSPACE_ARCHIVE_SCOPE = LEGACY_WORKSPACE_ARCHIVE_VERSIONS
|
|
12
17
|
.map((version) => `v${version}`)
|
|
13
18
|
.join(' and ');
|
|
@@ -23,12 +28,8 @@ const INVESTIGATION_PLAN_RECIPES = Object.freeze([
|
|
|
23
28
|
'planned-domain-change',
|
|
24
29
|
'post-change-verification',
|
|
25
30
|
]);
|
|
26
|
-
const RUNNABLE_INVESTIGATION_PLAN_RECIPES =
|
|
27
|
-
|
|
28
|
-
'lookalike-review',
|
|
29
|
-
'owned-domain-review',
|
|
30
|
-
'historical-comparison',
|
|
31
|
-
]);
|
|
31
|
+
const RUNNABLE_INVESTIGATION_PLAN_RECIPES = INVESTIGATION_PLAN_RECIPES;
|
|
32
|
+
const CLI_CASE_OPERATIONS = ['show', 'open', 'note', 'pin', 'assess', 'recheck'];
|
|
32
33
|
const CLI_META_ACTIONS = Object.freeze([
|
|
33
34
|
Object.freeze({
|
|
34
35
|
id: 'help',
|
|
@@ -66,6 +67,8 @@ redirected or unsupported terminals continue to print this help. No request
|
|
|
66
67
|
starts until a Lookup plan is shown and the analyst confirms collection.
|
|
67
68
|
Use --json or --jsonl where supported for machine-readable stdout.
|
|
68
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.
|
|
69
72
|
Use --palette auto, light, or dark after the command to select a fixed terminal
|
|
70
73
|
colour palette; --no-color, NO_COLOR, and redirected output still suppress ANSI.
|
|
71
74
|
Use --config <file> and --profile <name> for explicit versioned safe defaults.
|
|
@@ -80,48 +83,13 @@ must be requested explicitly and can disclose a target to additional sources.
|
|
|
80
83
|
Copyright 2026 slicedearth. Licensed under AGPL-3.0-only.
|
|
81
84
|
Source and licence: ${WHOISLEUTH_SOURCE_REPOSITORY_URL}
|
|
82
85
|
`;
|
|
83
|
-
const COMMON_OPTIONS = Object.freeze([
|
|
86
|
+
const COMMON_OPTIONS = Object.freeze([
|
|
87
|
+
'--help', '--output', '--force', '--config', '--profile', '--palette',
|
|
88
|
+
]);
|
|
84
89
|
const REGISTRY_SCAFFOLD_COMMON_OPTIONS = Object.freeze(COMMON_OPTIONS.filter((option) => option !== '--config' && option !== '--profile'));
|
|
85
90
|
function commonOptionsSeedForCommand(command) {
|
|
86
91
|
return command === 'registry-scaffold' ? REGISTRY_SCAFFOLD_COMMON_OPTIONS : COMMON_OPTIONS;
|
|
87
92
|
}
|
|
88
|
-
const VALUE_OPTIONS = Object.freeze({
|
|
89
|
-
'--group': Object.freeze(['investigate', 'respond', 'assure', 'utilities']),
|
|
90
|
-
'--mode': Object.freeze(['offline', 'network']),
|
|
91
|
-
'--explain': INVESTIGATION_PLAN_RECIPES,
|
|
92
|
-
'--preset': Object.freeze(['common', 'impersonation', 'all']),
|
|
93
|
-
'--keyboard': Object.freeze(['qwerty', 'azerty', 'qwertz', 'all']),
|
|
94
|
-
'--mail-profile': Object.freeze(['standard', 'defensive-no-mail', 'parked']),
|
|
95
|
-
'--marking': Object.freeze(['clear', 'green', 'amber', 'amber-strict', 'red']),
|
|
96
|
-
'--recipient-scope': Object.freeze(['public', 'community', 'organization', 'named-recipients']),
|
|
97
|
-
'--audience': Object.freeze(['internal', 'trusted', 'public']),
|
|
98
|
-
'--fail-on': Object.freeze(['source-failure', 'inconclusive', 'danger', 'material-drift']),
|
|
99
|
-
'--manifest-entry': Object.freeze(Array.from({ length: 16 }, (_, index) => `artifact-${index + 1}`)),
|
|
100
|
-
'--palette': Object.freeze(['auto', 'light', 'dark']),
|
|
101
|
-
'--scenario': Object.freeze(['registered', 'not_found', 'inconclusive']),
|
|
102
|
-
});
|
|
103
|
-
const STANDARD_CONCURRENCY_VALUES = Object.freeze(['1', '2', '3', '4', '5', '6', '7', '8']);
|
|
104
|
-
const LIMITED_CONCURRENCY_VALUES = Object.freeze(['1', '2', '3']);
|
|
105
|
-
const FILE_OPTIONS = Object.freeze([
|
|
106
|
-
'--checkpoint',
|
|
107
|
-
'--config',
|
|
108
|
-
'--allowlist',
|
|
109
|
-
'--dictionary',
|
|
110
|
-
'--manifest',
|
|
111
|
-
'--mmdb',
|
|
112
|
-
'--output',
|
|
113
|
-
'--passphrase-file',
|
|
114
|
-
'--private-key-file',
|
|
115
|
-
'--public-key-file',
|
|
116
|
-
'--snapshot',
|
|
117
|
-
'--observation-snapshot',
|
|
118
|
-
'--previous',
|
|
119
|
-
'--save-lookup',
|
|
120
|
-
'--trust-anchor',
|
|
121
|
-
]);
|
|
122
|
-
const FILE_OPTIONS_BY_COMMAND = Object.freeze({
|
|
123
|
-
'workflow-run': Object.freeze(['--resume']),
|
|
124
|
-
});
|
|
125
93
|
function positional(name, valueKind, minimum, maximum, values = [], inputSource = 'argv', requiredWhenOptions = []) {
|
|
126
94
|
return Object.freeze({
|
|
127
95
|
name,
|
|
@@ -136,61 +104,145 @@ function positional(name, valueKind, minimum, maximum, values = [], inputSource
|
|
|
136
104
|
const NO_POSITIONALS = Object.freeze([]);
|
|
137
105
|
const OPTIONAL_FILE_POSITIONAL = Object.freeze([positional('source', 'file', 0, 1, [], 'argv_or_stdin')]);
|
|
138
106
|
const OPTIONAL_TEXT_POSITIONAL = Object.freeze([positional('subject', 'text', 0, 1, [], 'argv_or_stdin')]);
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
const
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
'--
|
|
162
|
-
'--
|
|
163
|
-
'--
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
'
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
'
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
107
|
+
const NO_INTEGER_RANGES = () => Object.freeze([]);
|
|
108
|
+
const BASE_INTEGER_RANGE = (minimum, maximum) => (Object.freeze({ minimum, maximum, whenOptionPresent: null }));
|
|
109
|
+
const DEEP_INTEGER_RANGE = (minimum, maximum) => (Object.freeze({ minimum, maximum, whenOptionPresent: '--deep' }));
|
|
110
|
+
function optionDefinition(valueKind, options = {}) {
|
|
111
|
+
const configuredValues = options.values;
|
|
112
|
+
return Object.freeze({
|
|
113
|
+
valueKind: typeof valueKind === 'function' ? valueKind : () => valueKind,
|
|
114
|
+
values: typeof configuredValues === 'function'
|
|
115
|
+
? configuredValues
|
|
116
|
+
: () => Object.freeze([...(configuredValues ?? [])]),
|
|
117
|
+
occurrence: options.occurrence ?? 'once',
|
|
118
|
+
acceptsOptionLikeValue: options.acceptsOptionLikeValue ?? false,
|
|
119
|
+
metaAction: options.metaAction ?? null,
|
|
120
|
+
integerRanges: options.integerRanges ?? NO_INTEGER_RANGES,
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
const flag = (occurrence = 'once') => optionDefinition('flag', { occurrence });
|
|
124
|
+
const file = () => optionDefinition('file');
|
|
125
|
+
const text = (acceptsOptionLikeValue = false) => optionDefinition('text', { acceptsOptionLikeValue });
|
|
126
|
+
const enumeration = (values) => optionDefinition('enum', { values });
|
|
127
|
+
const integer = (ranges) => (optionDefinition('integer', { integerRanges: ranges }));
|
|
128
|
+
const CLI_OPTION_DEFINITIONS = Object.freeze({
|
|
129
|
+
'--help': optionDefinition('flag', { metaAction: 'help' }),
|
|
130
|
+
'--output': file(),
|
|
131
|
+
'--force': flag(),
|
|
132
|
+
'--config': file(),
|
|
133
|
+
'--profile': text(true),
|
|
134
|
+
'--palette': enumeration(['auto', 'light', 'dark']),
|
|
135
|
+
'--network': flag(),
|
|
136
|
+
'--json': flag(),
|
|
137
|
+
'--package': flag(),
|
|
138
|
+
'--folder': file(),
|
|
139
|
+
'--quiet': flag('idempotent'),
|
|
140
|
+
'--no-color': flag('idempotent'),
|
|
141
|
+
'--common': flag(),
|
|
142
|
+
'--group': enumeration(['investigate', 'respond', 'assure', 'utilities']),
|
|
143
|
+
'--mode': enumeration(['offline', 'network']),
|
|
144
|
+
'--workflow': text(true),
|
|
145
|
+
'--configuration-digest': text(true),
|
|
146
|
+
'--junit': flag(),
|
|
147
|
+
'--markdown': flag(),
|
|
148
|
+
'--html': flag(),
|
|
149
|
+
'--no-attribution': flag(),
|
|
150
|
+
'--fast': flag(),
|
|
151
|
+
'--deep': flag(),
|
|
152
|
+
'--exact-url': flag(),
|
|
153
|
+
'--observer': text(),
|
|
154
|
+
'--vantage': text(),
|
|
155
|
+
'--plan': flag(),
|
|
156
|
+
'--summary': flag(),
|
|
157
|
+
'--verbose': flag(),
|
|
158
|
+
'--browse': flag(),
|
|
159
|
+
'--interactive': flag(),
|
|
160
|
+
'--save-lookup': file(),
|
|
161
|
+
'--strict-exit': flag(),
|
|
162
|
+
'--fail-on': optionDefinition('policy_list', {
|
|
163
|
+
values: (command) => CLI_FAIL_POLICIES_BY_COMMAND[command] ?? [],
|
|
193
164
|
}),
|
|
165
|
+
'--events': flag(),
|
|
166
|
+
'--jsonl': flag(),
|
|
167
|
+
'--csv': flag(),
|
|
168
|
+
'--csv-with-metadata': flag(),
|
|
169
|
+
'--domains': flag(),
|
|
170
|
+
'--queries': flag(),
|
|
171
|
+
'--registered-only': flag(),
|
|
172
|
+
'--inconclusive-only': flag(),
|
|
173
|
+
'--errors-only': flag(),
|
|
174
|
+
'--concurrency': integer((command) => command === 'monitor-once'
|
|
175
|
+
? Object.freeze([BASE_INTEGER_RANGE(1, 3)])
|
|
176
|
+
: Object.freeze([BASE_INTEGER_RANGE(1, 8), DEEP_INTEGER_RANGE(1, 3)])),
|
|
177
|
+
'--checkpoint': file(),
|
|
178
|
+
'--resume': optionDefinition((command) => command === 'workflow-run' ? 'file' : 'flag'),
|
|
179
|
+
'--tlds': text(true),
|
|
180
|
+
'--preset': enumeration(['common', 'impersonation', 'all']),
|
|
181
|
+
'--families': text(),
|
|
182
|
+
'--keyboard': enumeration(['qwerty', 'azerty', 'qwertz', 'all']),
|
|
183
|
+
'--dictionary': file(),
|
|
184
|
+
'--snapshot': file(),
|
|
185
|
+
'--scan-limit': integer(() => Object.freeze([
|
|
186
|
+
BASE_INTEGER_RANGE(1, 500),
|
|
187
|
+
DEEP_INTEGER_RANGE(1, 50),
|
|
188
|
+
])),
|
|
189
|
+
'--chunk-size': integer(() => Object.freeze([BASE_INTEGER_RANGE(1, 100)])),
|
|
190
|
+
'--resolver': text(),
|
|
191
|
+
'--allowlist': file(),
|
|
192
|
+
'--observation-snapshot': file(),
|
|
193
|
+
'--acquisition-only': flag(),
|
|
194
|
+
'--suppressed-only': flag(),
|
|
195
|
+
'--selectors': text(true),
|
|
196
|
+
'--retired-selectors': text(true),
|
|
197
|
+
'--mail-profile': enumeration(['standard', 'defensive-no-mail', 'parked']),
|
|
198
|
+
'--sarif': flag(),
|
|
199
|
+
'--owned-domain': flag(),
|
|
200
|
+
'--include-inherited-dns': flag(),
|
|
201
|
+
'--trust-anchor': file(),
|
|
202
|
+
'--owned-or-authorized': flag(),
|
|
203
|
+
'--active-probe': flag(),
|
|
204
|
+
'--suffix': text(true),
|
|
205
|
+
'--scenario': enumeration(['registered', 'not_found', 'inconclusive']),
|
|
206
|
+
'--summary-json': flag(),
|
|
207
|
+
'--passphrase-file': file(),
|
|
208
|
+
'--manifest': file(),
|
|
209
|
+
'--bagit': flag(),
|
|
210
|
+
'--manifest-entry': enumeration(Array.from({ length: MAX_INVESTIGATION_MANIFEST_ARTIFACTS }, (_, index) => `artifact-${index + 1}`)),
|
|
211
|
+
'--search': text(),
|
|
212
|
+
'--require-match': flag(),
|
|
213
|
+
'--reveal': flag(),
|
|
214
|
+
'--expect-content-digest': text(true),
|
|
215
|
+
'--private-key-file': file(),
|
|
216
|
+
'--public-key-file': file(),
|
|
217
|
+
'--trust-store-file': file(),
|
|
218
|
+
'--mmdb': file(),
|
|
219
|
+
'--audience': enumeration(['internal', 'trusted', 'public']),
|
|
220
|
+
'--reviewed': flag(),
|
|
221
|
+
'--previous': file(),
|
|
222
|
+
'--limit': integer(() => Object.freeze([BASE_INTEGER_RANGE(1, 20)])),
|
|
223
|
+
'--marking': enumeration(['clear', 'green', 'amber', 'amber-strict', 'red']),
|
|
224
|
+
'--recipient-scope': enumeration(['public', 'community', 'organization', 'named-recipients']),
|
|
225
|
+
'--purpose': text(),
|
|
226
|
+
'--human-reviewed': flag('idempotent'),
|
|
227
|
+
'--personal-data-reviewed': flag('idempotent'),
|
|
228
|
+
'--redactions-confirmed': flag('idempotent'),
|
|
229
|
+
'--list': flag(),
|
|
230
|
+
'--explain': enumeration(INVESTIGATION_PLAN_RECIPES),
|
|
231
|
+
'--select': optionDefinition('text', { occurrence: 'repeatable', acceptsOptionLikeValue: true }),
|
|
232
|
+
'--use-artifact': optionDefinition('text', { occurrence: 'repeatable' }),
|
|
233
|
+
'--confirm-review': optionDefinition('text', { occurrence: 'repeatable' }),
|
|
234
|
+
'--approve-network': flag(),
|
|
235
|
+
'--left-session': text(true),
|
|
236
|
+
'--right-session': text(true),
|
|
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(),
|
|
194
246
|
});
|
|
195
247
|
function constraint(value) {
|
|
196
248
|
if (value.kind === 'mutually_exclusive') {
|
|
@@ -212,37 +264,29 @@ const QUIET_OUTPUT_CONSTRAINT = constraint({
|
|
|
212
264
|
kind: 'mutually_exclusive',
|
|
213
265
|
options: ['--quiet', '--output'],
|
|
214
266
|
});
|
|
215
|
-
const
|
|
216
|
-
'--json', '
|
|
217
|
-
'--
|
|
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'],
|
|
218
274
|
]);
|
|
219
|
-
|
|
220
|
-
if (Object.hasOwn(VALUE_OPTIONS, option))
|
|
221
|
-
return option === '--fail-on' ? 'policy_list' : 'enum';
|
|
222
|
-
if (INTEGER_RANGE_SEED[command]?.[option])
|
|
223
|
-
return 'integer';
|
|
224
|
-
if (FILE_OPTIONS.includes(option) || FILE_OPTIONS_BY_COMMAND[command]?.includes(option))
|
|
225
|
-
return 'file';
|
|
226
|
-
if (TEXT_OPTIONS.includes(option))
|
|
227
|
-
return 'text';
|
|
228
|
-
return 'flag';
|
|
229
|
-
}
|
|
275
|
+
const MACHINE_OUTPUT_OPTIONS = Object.freeze(PRESENTATION_OPTIONS.map(([option]) => option));
|
|
230
276
|
function optionSpec(command, option, scope) {
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
: VALUE_OPTIONS[option] ?? [];
|
|
277
|
+
const definition = CLI_OPTION_DEFINITIONS[option];
|
|
278
|
+
const valueKind = definition.valueKind(command);
|
|
279
|
+
const values = definition.values(command);
|
|
235
280
|
return Object.freeze({
|
|
236
281
|
option,
|
|
237
282
|
scope,
|
|
238
283
|
arity: valueKind === 'flag' ? 0 : 1,
|
|
239
284
|
valueKind,
|
|
240
285
|
values: Object.freeze([...values]),
|
|
241
|
-
integerRanges: Object.freeze([...(
|
|
242
|
-
occurrence:
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
metaAction: option === '--help' ? 'help' : null,
|
|
286
|
+
integerRanges: Object.freeze([...definition.integerRanges(command)]),
|
|
287
|
+
occurrence: definition.occurrence,
|
|
288
|
+
acceptsOptionLikeValue: definition.acceptsOptionLikeValue,
|
|
289
|
+
metaAction: definition.metaAction,
|
|
246
290
|
});
|
|
247
291
|
}
|
|
248
292
|
function grammarConstraints(commandOptions, commandConstraints) {
|
|
@@ -278,7 +322,6 @@ function commandSeed(seed) {
|
|
|
278
322
|
const COMMAND_SEEDS = Object.freeze({
|
|
279
323
|
completion: commandSeed({
|
|
280
324
|
reference: {
|
|
281
|
-
usage: 'whoisleuth completion <bash|zsh|fish|powershell>',
|
|
282
325
|
description: 'Print a static shell-completion script for the installed CLI.',
|
|
283
326
|
example: 'whoisleuth completion zsh > ~/.zfunc/_whoisleuth',
|
|
284
327
|
boundary: 'Generation is offline and writes only the script to stdout. The command never modifies shell configuration.',
|
|
@@ -288,7 +331,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
288
331
|
options: [],
|
|
289
332
|
positionals: Object.freeze([positional('shell', 'enum', 1, 1, ['bash', 'zsh', 'fish', 'powershell'])]),
|
|
290
333
|
constraints: EMPTY_CONSTRAINTS,
|
|
291
|
-
helpGroup: 'utilities',
|
|
292
334
|
handlerOwner: 'inline',
|
|
293
335
|
networkEffect: 'offline',
|
|
294
336
|
common: false,
|
|
@@ -300,7 +342,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
300
342
|
}),
|
|
301
343
|
doctor: commandSeed({
|
|
302
344
|
reference: {
|
|
303
|
-
usage: 'whoisleuth doctor [--network] [--json] [--quiet] [--no-color]',
|
|
304
345
|
description: 'Check the supported runtime and local terminal capabilities.',
|
|
305
346
|
example: 'whoisleuth doctor --json',
|
|
306
347
|
boundary: 'The default check is offline. Public DNS and port 43 checks run only when --network is explicitly supplied.',
|
|
@@ -310,7 +351,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
310
351
|
options: ['--network', '--json', '--quiet', '--no-color'],
|
|
311
352
|
positionals: NO_POSITIONALS,
|
|
312
353
|
constraints: EMPTY_CONSTRAINTS,
|
|
313
|
-
helpGroup: 'utilities',
|
|
314
354
|
handlerOwner: 'inline',
|
|
315
355
|
networkEffect: 'conditional_network',
|
|
316
356
|
common: true,
|
|
@@ -322,7 +362,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
322
362
|
}),
|
|
323
363
|
commands: commandSeed({
|
|
324
364
|
reference: {
|
|
325
|
-
usage: 'whoisleuth commands [--common] [--group <group>] [--mode <offline|network>] [--json] [--quiet] [--no-color]',
|
|
326
365
|
description: 'List the installed command contracts in terminal or versioned JSON form.',
|
|
327
366
|
example: 'whoisleuth commands --json',
|
|
328
367
|
boundary: 'Catalogue generation is offline. It reports declared command modes and limits without executing collection or inspecting local evidence.',
|
|
@@ -332,7 +371,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
332
371
|
options: ['--common', '--group', '--mode', '--json', '--quiet', '--no-color'],
|
|
333
372
|
positionals: NO_POSITIONALS,
|
|
334
373
|
constraints: Object.freeze([]),
|
|
335
|
-
helpGroup: 'utilities',
|
|
336
374
|
handlerOwner: 'inline',
|
|
337
375
|
networkEffect: 'offline',
|
|
338
376
|
common: true,
|
|
@@ -344,7 +382,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
344
382
|
}),
|
|
345
383
|
manual: commandSeed({
|
|
346
384
|
reference: {
|
|
347
|
-
usage: 'whoisleuth manual',
|
|
348
385
|
description: 'Print a generated roff manual page for local installation.',
|
|
349
386
|
example: 'whoisleuth manual | man -l -',
|
|
350
387
|
boundary: 'Generation is offline and derives from the same command catalogue as focused help.',
|
|
@@ -354,7 +391,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
354
391
|
options: [],
|
|
355
392
|
positionals: NO_POSITIONALS,
|
|
356
393
|
constraints: EMPTY_CONSTRAINTS,
|
|
357
|
-
helpGroup: 'utilities',
|
|
358
394
|
handlerOwner: 'inline',
|
|
359
395
|
networkEffect: 'offline',
|
|
360
396
|
common: false,
|
|
@@ -366,19 +402,23 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
366
402
|
}),
|
|
367
403
|
manifest: commandSeed({
|
|
368
404
|
reference: {
|
|
369
|
-
|
|
370
|
-
description: 'Record an ordered, path-free manifest for up to 16 local JSON artefacts.',
|
|
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.`,
|
|
371
406
|
example: 'whoisleuth manifest lookup.json comparison.json --workflow "domain review" --json',
|
|
372
|
-
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.',
|
|
373
408
|
},
|
|
374
|
-
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.` },
|
|
375
410
|
summary: 'Build an evidence manifest offline',
|
|
376
|
-
options: ['--workflow', '--configuration-digest', '--json', '--quiet', '--no-color'],
|
|
377
|
-
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)]),
|
|
378
413
|
constraints: Object.freeze([
|
|
379
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'] }),
|
|
380
421
|
]),
|
|
381
|
-
helpGroup: 'assure',
|
|
382
422
|
handlerOwner: 'inline',
|
|
383
423
|
networkEffect: 'offline',
|
|
384
424
|
common: false,
|
|
@@ -390,7 +430,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
390
430
|
}),
|
|
391
431
|
"map-observations": commandSeed({
|
|
392
432
|
reference: {
|
|
393
|
-
usage: 'whoisleuth map-observations [mapping.json] [--json] [--quiet] [--no-color]',
|
|
394
433
|
description: 'Apply one bounded declarative field-mapping profile to local source observations.',
|
|
395
434
|
example: 'whoisleuth map-observations mapping.json --json',
|
|
396
435
|
boundary: 'Profiles select allowlisted dotted fields only. They execute no scripts, make no requests, and emit the browser-compatible external-findings contract.',
|
|
@@ -400,7 +439,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
400
439
|
options: ['--json', '--quiet', '--no-color'],
|
|
401
440
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
402
441
|
constraints: EMPTY_CONSTRAINTS,
|
|
403
|
-
helpGroup: 'respond',
|
|
404
442
|
handlerOwner: 'inline',
|
|
405
443
|
networkEffect: 'offline',
|
|
406
444
|
common: false,
|
|
@@ -412,7 +450,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
412
450
|
}),
|
|
413
451
|
"oam-export": commandSeed({
|
|
414
452
|
reference: {
|
|
415
|
-
usage: 'whoisleuth oam-export [external-findings.json] [--json] [--quiet] [--no-color]',
|
|
416
453
|
description: 'Project browser-compatible external findings into a bounded Open Asset Model bridge document.',
|
|
417
454
|
example: 'whoisleuth oam-export external-findings.json --json',
|
|
418
455
|
boundary: 'The projection is offline, preserves source completeness without inventing confidence, and covers only bounded FQDN, IP address, certificate, and related edge vocabulary.',
|
|
@@ -422,7 +459,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
422
459
|
options: ['--json', '--quiet', '--no-color'],
|
|
423
460
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
424
461
|
constraints: EMPTY_CONSTRAINTS,
|
|
425
|
-
helpGroup: 'respond',
|
|
426
462
|
handlerOwner: 'inline',
|
|
427
463
|
networkEffect: 'offline',
|
|
428
464
|
common: false,
|
|
@@ -434,27 +470,26 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
434
470
|
}),
|
|
435
471
|
lookup: commandSeed({
|
|
436
472
|
reference: {
|
|
437
|
-
usage: 'whoisleuth lookup [domain|IP|ASN] [--json|--junit|--markdown|--html] [--no-attribution] [--fast|--deep] [--observer <label>] [--vantage <label>] [--plan] [--summary|--verbose|--browse [--save-lookup <file>]] [--palette <auto|light|dark>] [--strict-exit] [--fail-on <policies>] [--events] [--quiet] [--no-color]',
|
|
438
473
|
description: 'Collect registration evidence for one domain, IP, or ASN.',
|
|
439
474
|
example: 'whoisleuth lookup example.test --deep --browse',
|
|
440
|
-
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.',
|
|
441
476
|
},
|
|
442
477
|
collection: { mode: 'network', scope: 'Accepts one target. Fast is the default; deep collection must be selected explicitly.' },
|
|
443
478
|
summary: 'Collect one domain, IP, or ASN',
|
|
444
|
-
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'],
|
|
445
480
|
positionals: Object.freeze([positional('target', 'text', 0, 1, [], 'argv_or_stdin', ['--browse'])]),
|
|
446
481
|
constraints: Object.freeze([
|
|
447
482
|
constraint({ kind: 'mutually_exclusive', options: ['--json', '--junit', '--markdown', '--html'] }),
|
|
448
483
|
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
449
484
|
constraint({ kind: 'mutually_exclusive', options: ['--summary', '--verbose'] }),
|
|
450
485
|
constraint({ kind: 'requires_all', option: '--save-lookup', requiredOptions: ['--browse'] }),
|
|
486
|
+
constraint({ kind: 'requires_all', option: '--exact-url', requiredOptions: ['--deep'] }),
|
|
451
487
|
constraint({ kind: 'excludes_all', option: '--summary', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
|
|
452
488
|
constraint({ kind: 'excludes_all', option: '--verbose', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
|
|
453
489
|
constraint({ kind: 'excludes_all', option: '--browse', excludedOptions: ['--json', '--junit', '--markdown', '--html', '--summary', '--verbose', '--events', '--plan', '--quiet'] }),
|
|
454
490
|
constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
|
|
455
491
|
constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--junit', '--markdown', '--html', '--summary', '--verbose', '--strict-exit', '--events', '--quiet', '--fail-on'] }),
|
|
456
492
|
]),
|
|
457
|
-
helpGroup: 'investigate',
|
|
458
493
|
handlerOwner: 'lookup',
|
|
459
494
|
networkEffect: 'conditional_network',
|
|
460
495
|
common: true,
|
|
@@ -466,23 +501,21 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
466
501
|
}),
|
|
467
502
|
bulk: commandSeed({
|
|
468
503
|
reference: {
|
|
469
|
-
usage: '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>] [--quiet] [--no-color]',
|
|
470
504
|
description: 'Triage newline-delimited domains, IPs, or ASNs with bounded concurrency.',
|
|
471
505
|
example: 'cat domains.txt | whoisleuth bulk --jsonl',
|
|
472
|
-
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.',
|
|
473
507
|
},
|
|
474
508
|
collection: { mode: 'network', scope: 'Accepts at most 500 fast or 50 deep targets, with concurrency capped at 8 fast or 3 deep.' },
|
|
475
509
|
summary: 'Run bounded multi-target collection',
|
|
476
|
-
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'],
|
|
477
511
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
478
512
|
constraints: Object.freeze([
|
|
479
|
-
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'] }),
|
|
480
514
|
constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--errors-only'] }),
|
|
481
515
|
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
482
516
|
constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
|
|
483
|
-
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'] }),
|
|
484
518
|
]),
|
|
485
|
-
helpGroup: 'investigate',
|
|
486
519
|
handlerOwner: 'bulk',
|
|
487
520
|
networkEffect: 'conditional_network',
|
|
488
521
|
common: true,
|
|
@@ -494,7 +527,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
494
527
|
}),
|
|
495
528
|
"ct-search": commandSeed({
|
|
496
529
|
reference: {
|
|
497
|
-
usage: 'whoisleuth ct-search [keyword] [--json] [--quiet] [--no-color]',
|
|
498
530
|
description: 'Search certificate-transparency observations for one bounded keyword.',
|
|
499
531
|
example: 'whoisleuth ct-search "example brand" --json',
|
|
500
532
|
boundary: 'Certificate observations do not prove website activity, registration ownership, or malicious intent.',
|
|
@@ -504,7 +536,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
504
536
|
options: ['--json', '--quiet', '--no-color'],
|
|
505
537
|
positionals: Object.freeze([positional('keyword', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
506
538
|
constraints: EMPTY_CONSTRAINTS,
|
|
507
|
-
helpGroup: 'investigate',
|
|
508
539
|
handlerOwner: 'network',
|
|
509
540
|
networkEffect: 'always_network',
|
|
510
541
|
common: false,
|
|
@@ -516,7 +547,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
516
547
|
}),
|
|
517
548
|
"ct-intake": commandSeed({
|
|
518
549
|
reference: {
|
|
519
|
-
usage: 'whoisleuth ct-intake [events.json] [--json] [--quiet] [--no-color]',
|
|
520
550
|
description: 'Normalise source-qualified local certificate events into browser-compatible findings.',
|
|
521
551
|
example: 'whoisleuth ct-intake certificate-events.json --json',
|
|
522
552
|
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.',
|
|
@@ -526,7 +556,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
526
556
|
options: ['--json', '--quiet', '--no-color'],
|
|
527
557
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
528
558
|
constraints: EMPTY_CONSTRAINTS,
|
|
529
|
-
helpGroup: 'investigate',
|
|
530
559
|
handlerOwner: 'inline',
|
|
531
560
|
networkEffect: 'offline',
|
|
532
561
|
common: false,
|
|
@@ -538,8 +567,7 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
538
567
|
}),
|
|
539
568
|
discover: commandSeed({
|
|
540
569
|
reference: {
|
|
541
|
-
|
|
542
|
-
description: 'Generate bounded lookalike-domain candidates from local mutation rules.',
|
|
570
|
+
description: 'Generate bounded lookalike-domain candidates from a brand or registrable domain, including multi-part public suffixes.',
|
|
543
571
|
example: 'whoisleuth discover example.test --preset common --jsonl',
|
|
544
572
|
boundary: 'Generation and optional local snapshot comparison are offline. Candidates are leads only and are not resolved, registered, or classified as malicious.',
|
|
545
573
|
},
|
|
@@ -552,7 +580,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
552
580
|
constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
|
|
553
581
|
constraint({ kind: 'value_excludes', option: '--preset', value: 'common', excludedOptions: ['--dictionary'] }),
|
|
554
582
|
]),
|
|
555
|
-
helpGroup: 'investigate',
|
|
556
583
|
handlerOwner: 'discovery',
|
|
557
584
|
networkEffect: 'offline',
|
|
558
585
|
common: true,
|
|
@@ -564,25 +591,23 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
564
591
|
}),
|
|
565
592
|
"discover-scan": commandSeed({
|
|
566
593
|
reference: {
|
|
567
|
-
usage: 'whoisleuth discover-scan [brand|domain] [--tlds <list>] [--preset <name>|--families <ids>] [--keyboard <layout>] [--dictionary <file>] [--fast|--deep] [--scan-limit <n>] [--chunk-size <n>] [--concurrency <n>] [--resolver <IPs>] [--allowlist <file>] [--checkpoint <file> [--resume]] [--observation-snapshot <file>] [--registered-only|--inconclusive-only|--acquisition-only|--suppressed-only] [--events] [--json|--jsonl|--csv|--domains] [--plan] [--fail-on <policies>] [--quiet] [--no-color]',
|
|
568
594
|
description: 'Generate a bounded candidate set, collect a selected subset, and produce a supervised review queue.',
|
|
569
595
|
example: 'whoisleuth discover-scan example.test --scan-limit 50 --checkpoint scan.json --json',
|
|
570
|
-
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.',
|
|
571
597
|
},
|
|
572
598
|
collection: { mode: 'network', scope: 'Scans at most 500 fast or 50 deep candidates, with concurrency capped at 8 fast or 3 deep.' },
|
|
573
599
|
summary: 'Collect a supervised candidate review queue',
|
|
574
|
-
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'],
|
|
575
601
|
positionals: OPTIONAL_TEXT_POSITIONAL,
|
|
576
602
|
constraints: Object.freeze([
|
|
577
|
-
constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--csv', '--domains'] }),
|
|
603
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--csv', '--csv-with-metadata', '--domains'] }),
|
|
578
604
|
constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
|
|
579
605
|
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
580
606
|
constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only'] }),
|
|
581
607
|
constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
|
|
582
608
|
constraint({ kind: 'value_excludes', option: '--preset', value: 'common', excludedOptions: ['--dictionary'] }),
|
|
583
|
-
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'] }),
|
|
584
610
|
]),
|
|
585
|
-
helpGroup: 'investigate',
|
|
586
611
|
handlerOwner: 'discovery_scan',
|
|
587
612
|
networkEffect: 'conditional_network',
|
|
588
613
|
common: true,
|
|
@@ -594,20 +619,18 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
594
619
|
}),
|
|
595
620
|
posture: commandSeed({
|
|
596
621
|
reference: {
|
|
597
|
-
usage: 'whoisleuth posture [domain] [--selectors <list>] [--retired-selectors <list>] [--mail-profile <profile>] [--json|--sarif --owned-domain] [--quiet] [--no-color]',
|
|
598
622
|
description: 'Review bounded DNS mail, delegation, and domain-control posture.',
|
|
599
623
|
example: 'whoisleuth posture example.test --mail-profile standard --json',
|
|
600
|
-
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.',
|
|
601
625
|
},
|
|
602
|
-
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.' },
|
|
603
627
|
summary: 'Review DNS and mail posture',
|
|
604
|
-
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'],
|
|
605
629
|
positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
606
630
|
constraints: Object.freeze([
|
|
607
631
|
constraint({ kind: 'mutually_exclusive', options: ['--json', '--sarif'] }),
|
|
608
632
|
constraint({ kind: 'requires_all', option: '--sarif', requiredOptions: ['--owned-domain'] }),
|
|
609
633
|
]),
|
|
610
|
-
helpGroup: 'investigate',
|
|
611
634
|
handlerOwner: 'network',
|
|
612
635
|
networkEffect: 'always_network',
|
|
613
636
|
common: false,
|
|
@@ -619,7 +642,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
619
642
|
}),
|
|
620
643
|
http: commandSeed({
|
|
621
644
|
reference: {
|
|
622
|
-
usage: 'whoisleuth http [domain] [--json] [--quiet] [--no-color]',
|
|
623
645
|
description: 'Inspect one homepage request, redirects, and bounded response metadata.',
|
|
624
646
|
example: 'whoisleuth http example.test --json',
|
|
625
647
|
boundary: 'Requests use the shared public-address and redirect guards. Fixed content-coding and cache-policy metadata describes only the selected response, excludes raw header values, and does not prove caching, transfer savings, performance, privacy, or safety. This is not a rendered browser or vulnerability scan.',
|
|
@@ -629,7 +651,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
629
651
|
options: ['--json', '--quiet', '--no-color'],
|
|
630
652
|
positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
631
653
|
constraints: EMPTY_CONSTRAINTS,
|
|
632
|
-
helpGroup: 'investigate',
|
|
633
654
|
handlerOwner: 'network',
|
|
634
655
|
networkEffect: 'always_network',
|
|
635
656
|
common: false,
|
|
@@ -641,7 +662,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
641
662
|
}),
|
|
642
663
|
tls: commandSeed({
|
|
643
664
|
reference: {
|
|
644
|
-
usage: 'whoisleuth tls [hostname] [--json] [--quiet] [--no-color]',
|
|
645
665
|
description: 'Inspect one hostname certificate through a bounded TLS connection.',
|
|
646
666
|
example: 'whoisleuth tls example.test --json',
|
|
647
667
|
boundary: 'One observed connection is point-in-time evidence and does not establish every address, edge, or historical certificate.',
|
|
@@ -651,7 +671,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
651
671
|
options: ['--json', '--quiet', '--no-color'],
|
|
652
672
|
positionals: Object.freeze([positional('hostname', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
653
673
|
constraints: EMPTY_CONSTRAINTS,
|
|
654
|
-
helpGroup: 'investigate',
|
|
655
674
|
handlerOwner: 'network',
|
|
656
675
|
networkEffect: 'always_network',
|
|
657
676
|
common: false,
|
|
@@ -663,7 +682,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
663
682
|
}),
|
|
664
683
|
"dnssec-validate": commandSeed({
|
|
665
684
|
reference: {
|
|
666
|
-
usage: 'whoisleuth dnssec-validate <domain> --resolver <public-IP> --trust-anchor <anchor.json> --owned-or-authorized [--json] [--quiet] [--no-color]',
|
|
667
685
|
description: 'Cryptographically validate one authorised DNSSEC chain from a supplied trust anchor through one selected public resolver.',
|
|
668
686
|
example: 'whoisleuth dnssec-validate example.test --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --json',
|
|
669
687
|
boundary: 'This isolated action is never invoked by Lookup, Bulk, monitoring, or recipes. It caps DNS queries, aliases, delegations, bytes, and duration; transport and validation failures remain separate, and secure is not a general safety verdict.',
|
|
@@ -675,7 +693,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
675
693
|
constraints: Object.freeze([
|
|
676
694
|
constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized'] }),
|
|
677
695
|
]),
|
|
678
|
-
helpGroup: 'assure',
|
|
679
696
|
handlerOwner: 'network',
|
|
680
697
|
networkEffect: 'always_network',
|
|
681
698
|
common: false,
|
|
@@ -687,7 +704,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
687
704
|
}),
|
|
688
705
|
"mail-transport": commandSeed({
|
|
689
706
|
reference: {
|
|
690
|
-
usage: 'whoisleuth mail-transport [input.json] --resolver <public-IP> --trust-anchor <anchor.json> --owned-or-authorized --active-probe [--json] [--quiet] [--no-color]',
|
|
691
707
|
description: 'Review selected authorised MX endpoints, DNSSEC-qualified TLSA evidence, SMTP capabilities, and optional STARTTLS certificates.',
|
|
692
708
|
example: 'whoisleuth mail-transport selected-mx.json --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --active-probe --json',
|
|
693
709
|
boundary: 'This isolated action probes at most three selected MX hosts sequentially, reports selection, public revalidation, connection, and address authentication separately, sends only EHLO and optional STARTTLS, never retries, and performs no authentication, relay, recipient, mailbox, catch-all, or message test. If a DANE-TA TLSA usage 2 association is published, active collection retains only the leaf certificate and leaves that comparison partial without certificate-path construction and trust-anchor path validation. SMTP relay PKIX-TA usage 0 and PKIX-EE usage 1 records remain unsupported and cannot complete SMTP DANE assurance; a separate usage 3 match remains eligible.',
|
|
@@ -699,7 +715,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
699
715
|
constraints: Object.freeze([
|
|
700
716
|
constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe'] }),
|
|
701
717
|
]),
|
|
702
|
-
helpGroup: 'assure',
|
|
703
718
|
handlerOwner: 'network',
|
|
704
719
|
networkEffect: 'always_network',
|
|
705
720
|
common: false,
|
|
@@ -711,7 +726,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
711
726
|
}),
|
|
712
727
|
"registry-support": commandSeed({
|
|
713
728
|
reference: {
|
|
714
|
-
usage: 'whoisleuth registry-support [domain|suffix] [--json] [--quiet] [--no-color]',
|
|
715
729
|
description: 'Explain the local registry capability profile for one domain or suffix.',
|
|
716
730
|
example: 'whoisleuth registry-support example.test --json',
|
|
717
731
|
boundary: 'This command is offline. Catalogue coverage does not test live reachability or decide registration or availability.',
|
|
@@ -721,7 +735,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
721
735
|
options: ['--json', '--quiet', '--no-color'],
|
|
722
736
|
positionals: Object.freeze([positional('domain-or-suffix', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
723
737
|
constraints: EMPTY_CONSTRAINTS,
|
|
724
|
-
helpGroup: 'investigate',
|
|
725
738
|
handlerOwner: 'inline',
|
|
726
739
|
networkEffect: 'offline',
|
|
727
740
|
common: false,
|
|
@@ -733,7 +746,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
733
746
|
}),
|
|
734
747
|
"registry-doctor": commandSeed({
|
|
735
748
|
reference: {
|
|
736
|
-
usage: 'whoisleuth registry-doctor [lookup.json] [--json] [--quiet] [--no-color]',
|
|
737
749
|
description: 'Compare a saved Lookup registry result with the reviewed local capability profile.',
|
|
738
750
|
example: 'whoisleuth registry-doctor lookup.json --json',
|
|
739
751
|
boundary: 'The command is offline. It distinguishes expected access constraints from collection results and does not contact a live registry.',
|
|
@@ -743,7 +755,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
743
755
|
options: ['--json', '--quiet', '--no-color'],
|
|
744
756
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
745
757
|
constraints: EMPTY_CONSTRAINTS,
|
|
746
|
-
helpGroup: 'investigate',
|
|
747
758
|
handlerOwner: 'inline',
|
|
748
759
|
networkEffect: 'offline',
|
|
749
760
|
common: false,
|
|
@@ -755,7 +766,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
755
766
|
}),
|
|
756
767
|
"registry-cohort": commandSeed({
|
|
757
768
|
reference: {
|
|
758
|
-
usage: 'whoisleuth registry-cohort [lookups-or-reports.json|jsonl] [--json] [--quiet] [--no-color]',
|
|
759
769
|
description: 'Build privacy-safe suffix and capability-profile timelines from saved observations or retained cohort reports.',
|
|
760
770
|
example: 'whoisleuth registry-cohort saved-lookups.jsonl --json',
|
|
761
771
|
boundary: 'This command is offline and omits domains, queries, and raw evidence. Input families cannot be mixed, and retained samples are never assumed independent.',
|
|
@@ -765,7 +775,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
765
775
|
options: ['--json', '--quiet', '--no-color'],
|
|
766
776
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
767
777
|
constraints: EMPTY_CONSTRAINTS,
|
|
768
|
-
helpGroup: 'investigate',
|
|
769
778
|
handlerOwner: 'inline',
|
|
770
779
|
networkEffect: 'offline',
|
|
771
780
|
common: false,
|
|
@@ -777,7 +786,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
777
786
|
}),
|
|
778
787
|
"registry-scaffold": commandSeed({
|
|
779
788
|
reference: {
|
|
780
|
-
usage: 'whoisleuth registry-scaffold --profile <id> --suffix <suffix> --scenario <registered|not_found|inconclusive>',
|
|
781
789
|
description: 'Create a bounded synthetic WHOIS fixture scaffold for one existing capability profile.',
|
|
782
790
|
example: 'whoisleuth registry-scaffold --profile example-profile --suffix test --scenario registered',
|
|
783
791
|
boundary: 'The output is a sanitised template only. Its command-owned --profile selects fixture capability, --config is rejected, and contributors must not paste live responses or personal registration data into fixtures.',
|
|
@@ -789,7 +797,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
789
797
|
constraints: Object.freeze([
|
|
790
798
|
constraint({ kind: 'required', options: ['--profile', '--suffix', '--scenario'] }),
|
|
791
799
|
]),
|
|
792
|
-
helpGroup: 'utilities',
|
|
793
800
|
handlerOwner: 'inline',
|
|
794
801
|
networkEffect: 'offline',
|
|
795
802
|
common: false,
|
|
@@ -801,7 +808,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
801
808
|
}),
|
|
802
809
|
"risk-calibrate": commandSeed({
|
|
803
810
|
reference: {
|
|
804
|
-
usage: 'whoisleuth risk-calibrate [dataset.json] [--json|--summary-json] [--quiet] [--no-color]',
|
|
805
811
|
description: 'Replay reviewed labels against the current explainable Risk model.',
|
|
806
812
|
example: 'whoisleuth risk-calibrate calibration.json --summary-json',
|
|
807
813
|
boundary: 'Calibration is offline and diagnostic. The summary form omits record identifiers, domains, and evidence; neither form trains, tunes, or changes the scoring model automatically.',
|
|
@@ -813,7 +819,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
813
819
|
constraints: Object.freeze([
|
|
814
820
|
constraint({ kind: 'mutually_exclusive', options: ['--json', '--summary-json'] }),
|
|
815
821
|
]),
|
|
816
|
-
helpGroup: 'assure',
|
|
817
822
|
handlerOwner: 'inline',
|
|
818
823
|
networkEffect: 'offline',
|
|
819
824
|
common: false,
|
|
@@ -825,7 +830,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
825
830
|
}),
|
|
826
831
|
"lookalike-calibrate": commandSeed({
|
|
827
832
|
reference: {
|
|
828
|
-
usage: 'whoisleuth lookalike-calibrate [dataset.json] [--json] [--quiet] [--no-color]',
|
|
829
833
|
description: 'Summarise reviewed candidate dispositions by mutation family without retaining domains.',
|
|
830
834
|
example: 'whoisleuth lookalike-calibrate reviewed-candidates.json --json',
|
|
831
835
|
boundary: 'Calibration is offline and diagnostic. It omits candidate identifiers, domains, notes, and evidence and never tunes generation or filtering automatically.',
|
|
@@ -835,7 +839,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
835
839
|
options: ['--json', '--quiet', '--no-color'],
|
|
836
840
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
837
841
|
constraints: EMPTY_CONSTRAINTS,
|
|
838
|
-
helpGroup: 'assure',
|
|
839
842
|
handlerOwner: 'inline',
|
|
840
843
|
networkEffect: 'offline',
|
|
841
844
|
common: false,
|
|
@@ -847,20 +850,22 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
847
850
|
}),
|
|
848
851
|
"verify-artifact": commandSeed({
|
|
849
852
|
reference: {
|
|
850
|
-
|
|
851
|
-
description: 'Validate a supported archive, claim passport, packet, manifest, saved Lookup, or supported Lookup-evidence export without printing evidence contents.',
|
|
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.',
|
|
852
854
|
example: 'whoisleuth verify-artifact report.json --manifest manifest.json --manifest-entry artifact-2 --json --strict-exit',
|
|
853
|
-
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.',
|
|
854
856
|
},
|
|
855
|
-
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.' },
|
|
856
858
|
summary: 'Validate saved evidence offline',
|
|
857
|
-
options: ['--passphrase-file', '--manifest', '--manifest-entry', '--json', '--strict-exit', '--quiet', '--no-color'],
|
|
858
|
-
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'])]),
|
|
859
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'] }),
|
|
860
864
|
constraint({ kind: 'requires_all', option: '--manifest', requiredOptions: ['--manifest-entry'] }),
|
|
861
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'] }),
|
|
862
868
|
]),
|
|
863
|
-
helpGroup: 'assure',
|
|
864
869
|
handlerOwner: 'inline',
|
|
865
870
|
networkEffect: 'offline',
|
|
866
871
|
common: true,
|
|
@@ -872,7 +877,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
872
877
|
}),
|
|
873
878
|
"interchange-report": commandSeed({
|
|
874
879
|
reference: {
|
|
875
|
-
usage: 'whoisleuth interchange-report [artifact.json] [--passphrase-file <file>] [--json] [--quiet] [--no-color]',
|
|
876
880
|
description: 'Report what one recognised portable artefact preserves, excludes, and supports across browser and CLI workflows.',
|
|
877
881
|
example: 'whoisleuth interchange-report workspace.json --json',
|
|
878
882
|
boundary: 'The report is offline and metadata-only. It does not echo targets, contacts, notes, passphrases, evidence values, or an unrecognised schema string.',
|
|
@@ -882,7 +886,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
882
886
|
options: ['--passphrase-file', '--json', '--quiet', '--no-color'],
|
|
883
887
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
884
888
|
constraints: EMPTY_CONSTRAINTS,
|
|
885
|
-
helpGroup: 'assure',
|
|
886
889
|
handlerOwner: 'inline',
|
|
887
890
|
networkEffect: 'offline',
|
|
888
891
|
common: false,
|
|
@@ -894,10 +897,9 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
894
897
|
}),
|
|
895
898
|
"inspect-archive": commandSeed({
|
|
896
899
|
reference: {
|
|
897
|
-
usage: 'whoisleuth inspect-archive [archive.json] [--passphrase-file <file>] [--search <value>] [--require-match] [--reveal] [--expect-content-digest <sha256:digest>] [--json] [--quiet] [--no-color]',
|
|
898
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.`,
|
|
899
901
|
example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
|
|
900
|
-
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.',
|
|
901
903
|
},
|
|
902
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.` },
|
|
903
905
|
summary: 'Inspect an archive locally',
|
|
@@ -907,7 +909,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
907
909
|
constraint({ kind: 'requires_all', option: '--reveal', requiredOptions: ['--search'] }),
|
|
908
910
|
constraint({ kind: 'requires_all', option: '--require-match', requiredOptions: ['--search'] }),
|
|
909
911
|
]),
|
|
910
|
-
helpGroup: 'assure',
|
|
911
912
|
handlerOwner: 'evidence',
|
|
912
913
|
networkEffect: 'offline',
|
|
913
914
|
common: false,
|
|
@@ -919,7 +920,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
919
920
|
}),
|
|
920
921
|
"sign-artifact": commandSeed({
|
|
921
922
|
reference: {
|
|
922
|
-
usage: 'whoisleuth sign-artifact [artifact.json] --private-key-file <file>',
|
|
923
923
|
description: 'Sign one reviewed response packet or supported manifest with a local private key.',
|
|
924
924
|
example: 'whoisleuth sign-artifact packet.json --private-key-file analyst-private.pem',
|
|
925
925
|
boundary: 'The command never creates, stores, or transmits keys. Key custody and signer identity remain the operator\'s responsibility.',
|
|
@@ -931,7 +931,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
931
931
|
constraints: Object.freeze([
|
|
932
932
|
constraint({ kind: 'required', options: ['--private-key-file'] }),
|
|
933
933
|
]),
|
|
934
|
-
helpGroup: 'assure',
|
|
935
934
|
handlerOwner: 'evidence',
|
|
936
935
|
networkEffect: 'offline',
|
|
937
936
|
common: false,
|
|
@@ -943,21 +942,19 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
943
942
|
}),
|
|
944
943
|
"verify-signature": commandSeed({
|
|
945
944
|
reference: {
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
boundary: 'A valid signature proves package consistency for the embedded key. It does not upgrade failed or unsupported embedded-artefact assurance or establish the holder\'s real-world identity or authority.',
|
|
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.',
|
|
950
948
|
},
|
|
951
|
-
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.' },
|
|
952
950
|
summary: 'Verify a signed evidence package',
|
|
953
|
-
options: ['--public-key-file', '--json', '--quiet', '--no-color'],
|
|
951
|
+
options: ['--public-key-file', '--trust-store-file', '--json', '--quiet', '--no-color'],
|
|
954
952
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
955
953
|
constraints: EMPTY_CONSTRAINTS,
|
|
956
|
-
helpGroup: 'assure',
|
|
957
954
|
handlerOwner: 'evidence',
|
|
958
955
|
networkEffect: 'offline',
|
|
959
956
|
common: false,
|
|
960
|
-
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']),
|
|
961
958
|
primaryArtefacts: Object.freeze([]),
|
|
962
959
|
planSupport: false,
|
|
963
960
|
additionalOutputFormats: Object.freeze([]),
|
|
@@ -965,7 +962,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
965
962
|
}),
|
|
966
963
|
"source-report": commandSeed({
|
|
967
964
|
reference: {
|
|
968
|
-
usage: 'whoisleuth source-report [lookup.json] [--json] [--quiet] [--no-color]',
|
|
969
965
|
description: 'Create a target-free reliability summary from a saved lookup.',
|
|
970
966
|
example: 'whoisleuth source-report lookup.json --json',
|
|
971
967
|
boundary: 'The report retains source states and timings but excludes targets, queries, endpoints, and raw evidence.',
|
|
@@ -975,7 +971,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
975
971
|
options: ['--json', '--quiet', '--no-color'],
|
|
976
972
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
977
973
|
constraints: EMPTY_CONSTRAINTS,
|
|
978
|
-
helpGroup: 'investigate',
|
|
979
974
|
handlerOwner: 'inline',
|
|
980
975
|
networkEffect: 'offline',
|
|
981
976
|
common: false,
|
|
@@ -987,7 +982,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
987
982
|
}),
|
|
988
983
|
compare: commandSeed({
|
|
989
984
|
reference: {
|
|
990
|
-
usage: 'whoisleuth compare [lookup.json] [--json] [--quiet] [--no-color]',
|
|
991
985
|
description: 'Compare separately attributed registry publications in a saved lookup.',
|
|
992
986
|
example: 'whoisleuth compare lookup.json --json',
|
|
993
987
|
boundary: 'Comparison is offline. Differences are review context and do not by themselves prove which publication is current.',
|
|
@@ -997,7 +991,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
997
991
|
options: ['--json', '--quiet', '--no-color'],
|
|
998
992
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
999
993
|
constraints: EMPTY_CONSTRAINTS,
|
|
1000
|
-
helpGroup: 'investigate',
|
|
1001
994
|
handlerOwner: 'inline',
|
|
1002
995
|
networkEffect: 'offline',
|
|
1003
996
|
common: false,
|
|
@@ -1009,7 +1002,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1009
1002
|
}),
|
|
1010
1003
|
"page-compare": commandSeed({
|
|
1011
1004
|
reference: {
|
|
1012
|
-
usage: 'whoisleuth page-compare <left.json> <right.json> [--json] [--quiet] [--no-color]',
|
|
1013
1005
|
description: 'Compare static page identity, favicon, technology, and TLS evidence in two saved deep lookups.',
|
|
1014
1006
|
example: 'whoisleuth page-compare official.json candidate.json --json',
|
|
1015
1007
|
boundary: 'Comparison is offline and component-based. It executes no page code and produces no aggregate similarity or maliciousness score.',
|
|
@@ -1019,7 +1011,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1019
1011
|
options: ['--json', '--quiet', '--no-color'],
|
|
1020
1012
|
positionals: Object.freeze([positional('sources', 'file', 2, 2)]),
|
|
1021
1013
|
constraints: EMPTY_CONSTRAINTS,
|
|
1022
|
-
helpGroup: 'investigate',
|
|
1023
1014
|
handlerOwner: 'inline',
|
|
1024
1015
|
networkEffect: 'offline',
|
|
1025
1016
|
common: false,
|
|
@@ -1031,7 +1022,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1031
1022
|
}),
|
|
1032
1023
|
"mail-review": commandSeed({
|
|
1033
1024
|
reference: {
|
|
1034
|
-
usage: 'whoisleuth mail-review [bulk.json|bulk.jsonl] [--json] [--quiet] [--no-color]',
|
|
1035
1025
|
description: 'Review passive MX, null MX, SPF, DMARC, and shared mail-provider evidence from saved Bulk results.',
|
|
1036
1026
|
example: 'whoisleuth mail-review candidates.json --json',
|
|
1037
1027
|
boundary: 'Review is offline and sends no SMTP traffic. Missing or partial DNS evidence remains inconclusive.',
|
|
@@ -1041,7 +1031,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1041
1031
|
options: ['--json', '--quiet', '--no-color'],
|
|
1042
1032
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
1043
1033
|
constraints: EMPTY_CONSTRAINTS,
|
|
1044
|
-
helpGroup: 'investigate',
|
|
1045
1034
|
handlerOwner: 'inline',
|
|
1046
1035
|
networkEffect: 'offline',
|
|
1047
1036
|
common: false,
|
|
@@ -1051,9 +1040,28 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1051
1040
|
additionalOutputFormats: Object.freeze([]),
|
|
1052
1041
|
bootstrapProfile: 'allowed',
|
|
1053
1042
|
}),
|
|
1043
|
+
"mail-headers": commandSeed({
|
|
1044
|
+
reference: {
|
|
1045
|
+
description: 'Review identity, reported authentication, domain alignment, and Received routing from selected message headers.',
|
|
1046
|
+
example: 'whoisleuth mail-headers message.eml --json',
|
|
1047
|
+
boundary: 'Review is offline. It makes no DNS, SMTP, HTTP, registry, or provider request, and does not retain address local parts, display names, subjects, message bodies, attachments, or raw header values. Reported authentication is not independently validated.',
|
|
1048
|
+
},
|
|
1049
|
+
collection: { mode: 'offline', scope: 'Parses only the bounded header block from one selected message or standard input.' },
|
|
1050
|
+
summary: 'Review message headers offline',
|
|
1051
|
+
options: ['--json', '--quiet', '--no-color'],
|
|
1052
|
+
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
1053
|
+
constraints: EMPTY_CONSTRAINTS,
|
|
1054
|
+
handlerOwner: 'inline',
|
|
1055
|
+
networkEffect: 'offline',
|
|
1056
|
+
common: false,
|
|
1057
|
+
schemaIdentifiers: Object.freeze(['whoisleuth.cli.mail-header-review']),
|
|
1058
|
+
primaryArtefacts: Object.freeze([]),
|
|
1059
|
+
planSupport: false,
|
|
1060
|
+
additionalOutputFormats: Object.freeze([]),
|
|
1061
|
+
bootstrapProfile: 'allowed',
|
|
1062
|
+
}),
|
|
1054
1063
|
"review-evidence": commandSeed({
|
|
1055
1064
|
reference: {
|
|
1056
|
-
usage: 'whoisleuth review-evidence [evidence.json] [--mmdb <database-file>] [--json] [--strict-exit] [--quiet] [--no-color]',
|
|
1057
1065
|
description: 'Review one versioned DNS, domain-change, routing, GeoIP, RDAP, or trust-store document offline.',
|
|
1058
1066
|
example: 'whoisleuth review-evidence domain-change.json --json --strict-exit',
|
|
1059
1067
|
boundary: 'The command reads only the supplied document. It performs no DNS, RDAP, BGP, GeoIP-provider, TLS, HTTP, certificate-authority, or SMTP request.',
|
|
@@ -1063,7 +1071,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1063
1071
|
options: ['--mmdb', '--json', '--strict-exit', '--quiet', '--no-color'],
|
|
1064
1072
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
1065
1073
|
constraints: EMPTY_CONSTRAINTS,
|
|
1066
|
-
helpGroup: 'investigate',
|
|
1067
1074
|
handlerOwner: 'inline',
|
|
1068
1075
|
networkEffect: 'offline',
|
|
1069
1076
|
common: true,
|
|
@@ -1083,7 +1090,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1083
1090
|
}),
|
|
1084
1091
|
brief: commandSeed({
|
|
1085
1092
|
reference: {
|
|
1086
|
-
usage: 'whoisleuth brief [lookup.json] [--json] [--quiet] [--no-color]',
|
|
1087
1093
|
description: 'Turn one saved Lookup into a compact decision brief with facts, unknowns, contradictions, and next actions.',
|
|
1088
1094
|
example: 'whoisleuth brief lookup.json --json',
|
|
1089
1095
|
boundary: 'The command is offline, excludes raw upstream payloads, and does not create an analyst assertion or claim that the saved observation is current.',
|
|
@@ -1093,7 +1099,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1093
1099
|
options: ['--json', '--quiet', '--no-color'],
|
|
1094
1100
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
1095
1101
|
constraints: EMPTY_CONSTRAINTS,
|
|
1096
|
-
helpGroup: 'investigate',
|
|
1097
1102
|
handlerOwner: 'inline',
|
|
1098
1103
|
networkEffect: 'offline',
|
|
1099
1104
|
common: false,
|
|
@@ -1103,21 +1108,35 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1103
1108
|
additionalOutputFormats: Object.freeze([]),
|
|
1104
1109
|
bootstrapProfile: 'allowed',
|
|
1105
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
|
+
}),
|
|
1106
1127
|
"case-pack": commandSeed({
|
|
1107
1128
|
reference: {
|
|
1108
|
-
|
|
1109
|
-
description: `Package browser-created Case-schema-${CASE_SCHEMA_VERSION} records as a reviewed, audience-specific Case-pack v2.`,
|
|
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}.`,
|
|
1110
1130
|
example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
|
|
1111
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.',
|
|
1112
1132
|
},
|
|
1113
|
-
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.` },
|
|
1114
1134
|
summary: 'Build a reviewed case package',
|
|
1115
1135
|
options: ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
|
|
1116
1136
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
1117
1137
|
constraints: Object.freeze([
|
|
1118
1138
|
constraint({ kind: 'required', options: ['--audience', '--reviewed'] }),
|
|
1119
1139
|
]),
|
|
1120
|
-
helpGroup: 'respond',
|
|
1121
1140
|
handlerOwner: 'inline',
|
|
1122
1141
|
networkEffect: 'offline',
|
|
1123
1142
|
common: true,
|
|
@@ -1129,17 +1148,15 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1129
1148
|
}),
|
|
1130
1149
|
"domain-control": commandSeed({
|
|
1131
1150
|
reference: {
|
|
1132
|
-
usage: 'whoisleuth domain-control [manifest-input.json|review-input.json] [--json] [--quiet] [--no-color]',
|
|
1133
1151
|
description: 'Build an integrity-protected desired-state manifest or compare one with supplied observations.',
|
|
1134
1152
|
example: 'whoisleuth domain-control domain-control-input.json --json',
|
|
1135
|
-
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.',
|
|
1136
1154
|
},
|
|
1137
1155
|
collection: { mode: 'offline', scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
|
|
1138
1156
|
summary: 'Build or review a domain control manifest',
|
|
1139
1157
|
options: ['--json', '--quiet', '--no-color'],
|
|
1140
1158
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
1141
1159
|
constraints: EMPTY_CONSTRAINTS,
|
|
1142
|
-
helpGroup: 'assure',
|
|
1143
1160
|
handlerOwner: 'inline',
|
|
1144
1161
|
networkEffect: 'offline',
|
|
1145
1162
|
common: false,
|
|
@@ -1151,7 +1168,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1151
1168
|
}),
|
|
1152
1169
|
"monitor-once": commandSeed({
|
|
1153
1170
|
reference: {
|
|
1154
|
-
usage: 'whoisleuth monitor-once [manifest.json] [--previous <snapshot.json>] [--limit <1-20>] [--concurrency <1-3>] [--fail-on <policies>] [--json|--junit] [--quiet] [--no-color]',
|
|
1155
1171
|
description: 'Collect one bounded owned-domain review and compare it with an optional prior checkpoint.',
|
|
1156
1172
|
example: 'whoisleuth monitor-once manifest.json --previous previous.json --json --output next.json',
|
|
1157
1173
|
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.',
|
|
@@ -1163,7 +1179,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1163
1179
|
constraints: Object.freeze([
|
|
1164
1180
|
constraint({ kind: 'mutually_exclusive', options: ['--json', '--junit'] }),
|
|
1165
1181
|
]),
|
|
1166
|
-
helpGroup: 'assure',
|
|
1167
1182
|
handlerOwner: 'inline',
|
|
1168
1183
|
networkEffect: 'always_network',
|
|
1169
1184
|
common: false,
|
|
@@ -1175,7 +1190,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1175
1190
|
}),
|
|
1176
1191
|
assurance: commandSeed({
|
|
1177
1192
|
reference: {
|
|
1178
|
-
usage: 'whoisleuth assurance [assurance-input.json] [--json] [--quiet] [--no-color]',
|
|
1179
1193
|
description: 'Review a versioned domain change, recovery-dependency, or retirement plan.',
|
|
1180
1194
|
example: 'whoisleuth assurance domain-assurance.json --json',
|
|
1181
1195
|
boundary: 'The command is offline and treats every provider label, readiness state, and evidence reference as analyst-authored input. It changes no configuration.',
|
|
@@ -1185,7 +1199,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1185
1199
|
options: ['--json', '--quiet', '--no-color'],
|
|
1186
1200
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
1187
1201
|
constraints: EMPTY_CONSTRAINTS,
|
|
1188
|
-
helpGroup: 'assure',
|
|
1189
1202
|
handlerOwner: 'inline',
|
|
1190
1203
|
networkEffect: 'offline',
|
|
1191
1204
|
common: false,
|
|
@@ -1197,7 +1210,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1197
1210
|
}),
|
|
1198
1211
|
"change-packet": commandSeed({
|
|
1199
1212
|
reference: {
|
|
1200
|
-
usage: 'whoisleuth change-packet [change-packet-input.json] [--json] [--quiet] [--no-color]',
|
|
1201
1213
|
description: 'Assemble pre-change, post-change, and planning evidence into one integrity-protected packet.',
|
|
1202
1214
|
example: 'whoisleuth change-packet change-review.json --json',
|
|
1203
1215
|
boundary: 'Assembly is offline. Readiness reflects only the supplied bounded evidence and does not authorise or perform a domain change.',
|
|
@@ -1207,7 +1219,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1207
1219
|
options: ['--json', '--quiet', '--no-color'],
|
|
1208
1220
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
1209
1221
|
constraints: EMPTY_CONSTRAINTS,
|
|
1210
|
-
helpGroup: 'respond',
|
|
1211
1222
|
handlerOwner: 'inline',
|
|
1212
1223
|
networkEffect: 'offline',
|
|
1213
1224
|
common: false,
|
|
@@ -1219,19 +1230,17 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1219
1230
|
}),
|
|
1220
1231
|
"sharing-review": commandSeed({
|
|
1221
1232
|
reference: {
|
|
1222
|
-
usage: 'whoisleuth sharing-review [artifact.json] --marking <level> --recipient-scope <scope> --purpose <text> [--human-reviewed] [--personal-data-reviewed] [--redactions-confirmed] [--json] [--quiet] [--no-color]',
|
|
1223
1233
|
description: 'Lint one reviewed artefact against local integrity, marking, recipient, personal-data, and redaction controls.',
|
|
1224
1234
|
example: 'whoisleuth sharing-review packet.json --marking amber --recipient-scope organization --purpose "Reviewed incident handoff" --human-reviewed --personal-data-reviewed --redactions-confirmed --json',
|
|
1225
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.',
|
|
1226
1236
|
},
|
|
1227
|
-
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.' },
|
|
1228
1238
|
summary: 'Lint an artefact before deliberate sharing',
|
|
1229
1239
|
options: ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
|
|
1230
1240
|
positionals: OPTIONAL_FILE_POSITIONAL,
|
|
1231
1241
|
constraints: Object.freeze([
|
|
1232
1242
|
constraint({ kind: 'required', options: ['--marking', '--recipient-scope', '--purpose'] }),
|
|
1233
1243
|
]),
|
|
1234
|
-
helpGroup: 'respond',
|
|
1235
1244
|
handlerOwner: 'inline',
|
|
1236
1245
|
networkEffect: 'offline',
|
|
1237
1246
|
common: false,
|
|
@@ -1243,7 +1252,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1243
1252
|
}),
|
|
1244
1253
|
"workflow-plan": commandSeed({
|
|
1245
1254
|
reference: {
|
|
1246
|
-
usage: 'whoisleuth workflow-plan <recipe> <domain|brand> | --list | --explain <recipe> [--json] [--quiet] [--no-color]',
|
|
1247
1255
|
description: 'Build a fixed domain-investigation plan from existing bounded CLI commands.',
|
|
1248
1256
|
example: 'whoisleuth workflow-plan domain-triage example.test --json',
|
|
1249
1257
|
boundary: 'Planning is offline and plan-only. It does not execute commands, expand placeholders, read files, make requests, or submit evidence.',
|
|
@@ -1258,7 +1266,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1258
1266
|
constraints: Object.freeze([
|
|
1259
1267
|
constraint({ kind: 'mutually_exclusive', options: ['--list', '--explain'] }),
|
|
1260
1268
|
]),
|
|
1261
|
-
helpGroup: 'assure',
|
|
1262
1269
|
handlerOwner: 'inline',
|
|
1263
1270
|
networkEffect: 'offline',
|
|
1264
1271
|
common: true,
|
|
@@ -1270,20 +1277,18 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1270
1277
|
}),
|
|
1271
1278
|
"workflow-run": commandSeed({
|
|
1272
1279
|
reference: {
|
|
1273
|
-
usage: 'whoisleuth workflow-run <recipe> <domain|brand> [--select <step-id>=<path-or-value>]... [--approve-network] [--resume <state.json>] [--json] [--quiet] [--no-color]',
|
|
1274
1280
|
description: 'Execute approved steps from a fixed investigation recipe and emit a resumable checkpoint.',
|
|
1275
|
-
example: 'whoisleuth workflow-run domain-triage example.test --
|
|
1276
|
-
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.',
|
|
1277
1283
|
},
|
|
1278
1284
|
collection: { mode: 'network', scope: 'Runs only fixed-recipe steps; network collection requires --approve-network and unresolved analyst selections pause.' },
|
|
1279
1285
|
summary: 'Execute approved fixed-recipe steps',
|
|
1280
|
-
options: ['--select', '--approve-network', '--resume', '--json', '--quiet', '--no-color'],
|
|
1286
|
+
options: ['--select', '--use-artifact', '--confirm-review', '--approve-network', '--resume', '--interactive', '--json', '--quiet', '--no-color'],
|
|
1281
1287
|
positionals: Object.freeze([
|
|
1282
1288
|
positional('recipe', 'enum', 1, 1, RUNNABLE_INVESTIGATION_PLAN_RECIPES),
|
|
1283
1289
|
positional('subject', 'text', 1, 1),
|
|
1284
1290
|
]),
|
|
1285
1291
|
constraints: EMPTY_CONSTRAINTS,
|
|
1286
|
-
helpGroup: 'assure',
|
|
1287
1292
|
handlerOwner: 'inline',
|
|
1288
1293
|
networkEffect: 'conditional_network',
|
|
1289
1294
|
common: false,
|
|
@@ -1295,17 +1300,15 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1295
1300
|
}),
|
|
1296
1301
|
diff: commandSeed({
|
|
1297
1302
|
reference: {
|
|
1298
|
-
usage: 'whoisleuth diff <left.json> <right.json> [--left-session <id> --right-session <id>] [--json] [--quiet] [--no-color]',
|
|
1299
1303
|
description: 'Compare an earlier and later artefact from the same retained Lookup, Bulk-session, or domain-portfolio family.',
|
|
1300
1304
|
example: 'whoisleuth diff earlier.json later.json --json',
|
|
1301
|
-
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.',
|
|
1302
1306
|
},
|
|
1303
1307
|
collection: { mode: 'offline', scope: 'Reads two compatible retained artefacts capped at 8 MiB each and retains no source paths.' },
|
|
1304
1308
|
summary: 'Compare two compatible retained artefacts',
|
|
1305
1309
|
options: ['--left-session', '--right-session', '--json', '--quiet', '--no-color'],
|
|
1306
1310
|
positionals: Object.freeze([positional('sources', 'file', 2, 2)]),
|
|
1307
1311
|
constraints: EMPTY_CONSTRAINTS,
|
|
1308
|
-
helpGroup: 'assure',
|
|
1309
1312
|
handlerOwner: 'inline',
|
|
1310
1313
|
networkEffect: 'offline',
|
|
1311
1314
|
common: true,
|
|
@@ -1317,7 +1320,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1317
1320
|
}),
|
|
1318
1321
|
reconcile: commandSeed({
|
|
1319
1322
|
reference: {
|
|
1320
|
-
usage: 'whoisleuth reconcile <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
|
|
1321
1323
|
description: 'Reconcile bounded values across independently labelled observations of one domain.',
|
|
1322
1324
|
example: 'whoisleuth reconcile office.json mobile.json external.json --json',
|
|
1323
1325
|
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.',
|
|
@@ -1327,7 +1329,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1327
1329
|
options: ['--json', '--quiet', '--no-color'],
|
|
1328
1330
|
positionals: Object.freeze([positional('sources', 'file', 2, 5)]),
|
|
1329
1331
|
constraints: EMPTY_CONSTRAINTS,
|
|
1330
|
-
helpGroup: 'assure',
|
|
1331
1332
|
handlerOwner: 'inline',
|
|
1332
1333
|
networkEffect: 'offline',
|
|
1333
1334
|
common: false,
|
|
@@ -1339,7 +1340,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1339
1340
|
}),
|
|
1340
1341
|
timeline: commandSeed({
|
|
1341
1342
|
reference: {
|
|
1342
|
-
usage: 'whoisleuth timeline <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
|
|
1343
1343
|
description: 'Build an ordered same-domain history from saved Lookup observations.',
|
|
1344
1344
|
example: 'whoisleuth timeline first.json second.json latest.json --json',
|
|
1345
1345
|
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.',
|
|
@@ -1349,7 +1349,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1349
1349
|
options: ['--json', '--quiet', '--no-color'],
|
|
1350
1350
|
positionals: Object.freeze([positional('sources', 'file', 2, 20)]),
|
|
1351
1351
|
constraints: EMPTY_CONSTRAINTS,
|
|
1352
|
-
helpGroup: 'assure',
|
|
1353
1352
|
handlerOwner: 'inline',
|
|
1354
1353
|
networkEffect: 'offline',
|
|
1355
1354
|
common: false,
|
|
@@ -1361,10 +1360,9 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1361
1360
|
}),
|
|
1362
1361
|
export: commandSeed({
|
|
1363
1362
|
reference: {
|
|
1364
|
-
usage: 'whoisleuth export [lookup.json] [--markdown|--html|--compact] [--no-attribution]',
|
|
1365
1363
|
description: 'Convert one saved lookup into a versioned evidence report.',
|
|
1366
1364
|
example: 'whoisleuth export lookup.json --markdown',
|
|
1367
|
-
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.`,
|
|
1368
1366
|
},
|
|
1369
1367
|
collection: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
|
|
1370
1368
|
summary: 'Convert a lookup to an evidence report',
|
|
@@ -1375,7 +1373,6 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1375
1373
|
constraint({ kind: 'excludes_all', option: '--compact', excludedOptions: ['--markdown', '--html'] }),
|
|
1376
1374
|
constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
|
|
1377
1375
|
]),
|
|
1378
|
-
helpGroup: 'respond',
|
|
1379
1376
|
handlerOwner: 'inline',
|
|
1380
1377
|
networkEffect: 'offline',
|
|
1381
1378
|
common: true,
|
|
@@ -1387,24 +1384,18 @@ const COMMAND_SEEDS = Object.freeze({
|
|
|
1387
1384
|
}),
|
|
1388
1385
|
});
|
|
1389
1386
|
const COMMAND_ORDER = Object.freeze(Object.keys(COMMAND_SEEDS));
|
|
1390
|
-
const
|
|
1391
|
-
const HELP_COMMANDS_BY_GROUP = Object.freeze(Object.fromEntries(HELP_GROUP_ORDER.map((group) => [
|
|
1387
|
+
const HELP_COMMANDS_BY_GROUP = Object.freeze(Object.fromEntries(CLI_HELP_GROUP_ORDER.map((group) => [
|
|
1392
1388
|
group,
|
|
1393
|
-
Object.freeze(COMMAND_ORDER.filter((command) =>
|
|
1389
|
+
Object.freeze(COMMAND_ORDER.filter((command) => CLI_COMMAND_SEMANTICS[command].group === group)),
|
|
1394
1390
|
])));
|
|
1395
1391
|
function documentationMetadata(seed, commonOptions, commandOptions, positionals) {
|
|
1396
1392
|
const explicitAuthorisationRequired = commandOptions.some((option) => (option === '--owned-or-authorized' || option === '--active-probe' || option === '--approve-network'));
|
|
1397
|
-
const
|
|
1398
|
-
[
|
|
1399
|
-
[
|
|
1400
|
-
['--markdown', 'Markdown'], ['--html', 'HTML'], ['--sarif', 'SARIF'],
|
|
1401
|
-
['--summary-json', 'summary JSON'],
|
|
1402
|
-
];
|
|
1393
|
+
const presentationOptions = Object.freeze(PRESENTATION_OPTIONS
|
|
1394
|
+
.filter(([option]) => commandOptions.includes(option))
|
|
1395
|
+
.map(([option, format]) => Object.freeze({ option, format })));
|
|
1403
1396
|
const outputFormats = new Set(['terminal']);
|
|
1404
|
-
for (const
|
|
1405
|
-
|
|
1406
|
-
outputFormats.add(label);
|
|
1407
|
-
}
|
|
1397
|
+
for (const { format } of presentationOptions)
|
|
1398
|
+
outputFormats.add(format);
|
|
1408
1399
|
for (const format of seed.additionalOutputFormats)
|
|
1409
1400
|
outputFormats.add(format);
|
|
1410
1401
|
const positionalLimits = positionals.map((item) => (`${item.name}: ${item.minimum}-${item.maximum} ${item.valueKind} value${item.maximum === 1 ? '' : 's'}`));
|
|
@@ -1429,9 +1420,53 @@ function documentationMetadata(seed, commonOptions, commandOptions, positionals)
|
|
|
1429
1420
|
: []),
|
|
1430
1421
|
]),
|
|
1431
1422
|
outputFormats: Object.freeze([...outputFormats]),
|
|
1423
|
+
presentationOptions,
|
|
1424
|
+
fileOutput: commonOptions.includes('--output'),
|
|
1432
1425
|
primaryEvidenceArtefacts: seed.primaryArtefacts,
|
|
1433
1426
|
});
|
|
1434
1427
|
}
|
|
1428
|
+
function expandedOptionValues(specification) {
|
|
1429
|
+
return specification.valueKind === 'enum' && specification.values.join('|').length > 60;
|
|
1430
|
+
}
|
|
1431
|
+
function optionUsage(specification) {
|
|
1432
|
+
if (specification.arity === 0)
|
|
1433
|
+
return specification.option;
|
|
1434
|
+
if (specification.valueKind === 'enum') {
|
|
1435
|
+
return `${specification.option} <${expandedOptionValues(specification) ? specification.option.slice(2) : specification.values.join('|')}>`;
|
|
1436
|
+
}
|
|
1437
|
+
if (specification.valueKind === 'policy_list')
|
|
1438
|
+
return `${specification.option} <policy[,policy...]>`;
|
|
1439
|
+
if (specification.valueKind === 'integer')
|
|
1440
|
+
return `${specification.option} <integer>`;
|
|
1441
|
+
if (specification.valueKind === 'file')
|
|
1442
|
+
return `${specification.option} <file>`;
|
|
1443
|
+
return `${specification.option} <value>`;
|
|
1444
|
+
}
|
|
1445
|
+
function positionalUsage(specification) {
|
|
1446
|
+
const value = specification.valueKind === 'enum'
|
|
1447
|
+
? `<${specification.values.join('|')}>`
|
|
1448
|
+
: `<${specification.name}${specification.maximum > 1 ? '...' : ''}>`;
|
|
1449
|
+
return specification.minimum === 0 ? `[${value}]` : value;
|
|
1450
|
+
}
|
|
1451
|
+
function generatedCommandUsage(command, options, positionals, constraints) {
|
|
1452
|
+
const commandOptions = options.filter((option) => option.scope === 'command');
|
|
1453
|
+
const required = new Set(constraints
|
|
1454
|
+
.filter((item) => item.kind === 'required')
|
|
1455
|
+
.flatMap((item) => item.options));
|
|
1456
|
+
const mutuallyExclusive = constraints
|
|
1457
|
+
.filter((item) => item.kind === 'mutually_exclusive')
|
|
1458
|
+
.map((item) => item.options.filter((option) => commandOptions.some((candidate) => candidate.option === option)))
|
|
1459
|
+
.filter((group) => group.length > 1);
|
|
1460
|
+
const grouped = new Set(mutuallyExclusive.flat());
|
|
1461
|
+
const renderedGroups = mutuallyExclusive.map((group) => `[${group.map((option) => (optionUsage(commandOptions.find((candidate) => candidate.option === option)))).join('|')}]`);
|
|
1462
|
+
const renderedOptions = commandOptions
|
|
1463
|
+
.filter((option) => !grouped.has(option.option))
|
|
1464
|
+
.map((option) => {
|
|
1465
|
+
const rendered = optionUsage(option);
|
|
1466
|
+
return required.has(option.option) ? rendered : `[${rendered}]`;
|
|
1467
|
+
});
|
|
1468
|
+
return ['whoisleuth', command, ...positionals.map(positionalUsage), ...renderedGroups, ...renderedOptions].join(' ');
|
|
1469
|
+
}
|
|
1435
1470
|
const CLI_COMMAND_REGISTRY = Object.freeze(COMMAND_ORDER.map((command, order) => {
|
|
1436
1471
|
const seed = COMMAND_SEEDS[command];
|
|
1437
1472
|
const commonOptions = Object.freeze([...commonOptionsSeedForCommand(command)]);
|
|
@@ -1440,10 +1475,14 @@ const CLI_COMMAND_REGISTRY = Object.freeze(COMMAND_ORDER.map((command, order) =>
|
|
|
1440
1475
|
...commonOptions.map((option) => optionSpec(command, option, 'common')),
|
|
1441
1476
|
...commandOptions.map((option) => optionSpec(command, option, 'command')),
|
|
1442
1477
|
]);
|
|
1478
|
+
const constraints = grammarConstraints(commandOptions, seed.constraints);
|
|
1443
1479
|
return Object.freeze({
|
|
1444
1480
|
command,
|
|
1445
1481
|
order,
|
|
1446
|
-
reference:
|
|
1482
|
+
reference: Object.freeze({
|
|
1483
|
+
...seed.reference,
|
|
1484
|
+
usage: generatedCommandUsage(command, grammarOptions, seed.positionals, constraints),
|
|
1485
|
+
}),
|
|
1447
1486
|
collection: seed.collection,
|
|
1448
1487
|
completion: Object.freeze({
|
|
1449
1488
|
description: seed.summary,
|
|
@@ -1455,7 +1494,7 @@ const CLI_COMMAND_REGISTRY = Object.freeze(COMMAND_ORDER.map((command, order) =>
|
|
|
1455
1494
|
bootstrapProfile: seed.bootstrapProfile,
|
|
1456
1495
|
options: grammarOptions,
|
|
1457
1496
|
positionals: seed.positionals,
|
|
1458
|
-
constraints
|
|
1497
|
+
constraints,
|
|
1459
1498
|
metaActions: Object.freeze(['help']),
|
|
1460
1499
|
}),
|
|
1461
1500
|
execution: Object.freeze({
|
|
@@ -1463,7 +1502,7 @@ const CLI_COMMAND_REGISTRY = Object.freeze(COMMAND_ORDER.map((command, order) =>
|
|
|
1463
1502
|
networkEffect: seed.networkEffect,
|
|
1464
1503
|
}),
|
|
1465
1504
|
help: Object.freeze({
|
|
1466
|
-
group:
|
|
1505
|
+
group: CLI_COMMAND_SEMANTICS[command].group,
|
|
1467
1506
|
summary: seed.summary,
|
|
1468
1507
|
}),
|
|
1469
1508
|
documentation: documentationMetadata(seed, commonOptions, commandOptions, seed.positionals),
|
|
@@ -1483,11 +1522,34 @@ function commandDefinition(command) {
|
|
|
1483
1522
|
function metaActionDefinition(id) {
|
|
1484
1523
|
return CLI_META_ACTION_BY_ID[id];
|
|
1485
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
|
+
}
|
|
1486
1547
|
function cliMetaActionForInvocation(argv) {
|
|
1548
|
+
const optionIndices = cliInvocationOptionIndices(argv);
|
|
1487
1549
|
for (const action of CLI_META_ACTIONS) {
|
|
1488
1550
|
const matches = action.scope === 'root_only'
|
|
1489
1551
|
? action.aliases.includes(argv[0] ?? '')
|
|
1490
|
-
: argv.some((argument) => action.aliases.includes(argument));
|
|
1552
|
+
: argv.some((argument, index) => optionIndices.has(index) && action.aliases.includes(argument));
|
|
1491
1553
|
if (matches)
|
|
1492
1554
|
return action;
|
|
1493
1555
|
}
|
|
@@ -1535,15 +1597,8 @@ function commandOwnsOption(command, option) {
|
|
|
1535
1597
|
return commandDefinition(command).grammar.options.some((candidate) => candidate.option === option);
|
|
1536
1598
|
}
|
|
1537
1599
|
function invocationHasFlag(command, args, flag) {
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
if (argument === flag)
|
|
1541
|
-
return true;
|
|
1542
|
-
const specification = argument ? commandOptionSpec(command, argument) : null;
|
|
1543
|
-
if (specification?.arity === 1)
|
|
1544
|
-
index += 1;
|
|
1545
|
-
}
|
|
1546
|
-
return false;
|
|
1600
|
+
const argv = [command, ...args];
|
|
1601
|
+
return [...cliInvocationOptionIndices(argv)].some((index) => argv[index] === flag);
|
|
1547
1602
|
}
|
|
1548
1603
|
function cliInvocationNetworkEffect(command, args) {
|
|
1549
1604
|
const effect = commandDefinition(command).execution.networkEffect;
|
|
@@ -1564,6 +1619,8 @@ function cliInvocationNetworkEffect(command, args) {
|
|
|
1564
1619
|
function commandHelp(command) {
|
|
1565
1620
|
const detail = COMMAND_DETAILS[command];
|
|
1566
1621
|
const collection = COMMAND_COLLECTION[command];
|
|
1567
|
-
|
|
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`;
|
|
1568
1625
|
}
|
|
1569
|
-
export { CLI_COMMAND_REGISTRY, CLI_COMMANDS, CLI_META_ACTIONS, COMMAND_COLLECTION, COMMAND_DESCRIPTIONS, COMMAND_DETAILS, COMMAND_USAGE,
|
|
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, };
|