@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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/DISCLOSURE +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
|
@@ -1,103 +1,153 @@
|
|
|
1
1
|
import { WHOISLEUTH_SOURCE_REPOSITORY_URL } from "../lib/project-metadata.mjs";
|
|
2
|
+
import { LOOKUP_EVIDENCE_SCHEMA_VERSION, PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../lib/evidence-export.mjs";
|
|
3
|
+
import { RISK_CALIBRATION_DATASET_SCHEMA, RISK_CALIBRATION_REPORT_SCHEMA, } from "../packages/contracts/risk-calibration.mjs";
|
|
4
|
+
import { CASE_SCHEMA_VERSION, PUBLIC_WORKSPACE_ARCHIVE_VERSION, WORKSPACE_ARCHIVE_VERSION, } from "../packages/contracts/case-portability.mjs";
|
|
5
|
+
const COMMAND_ORDER = Object.freeze([
|
|
6
|
+
'completion',
|
|
7
|
+
'doctor',
|
|
8
|
+
'commands',
|
|
9
|
+
'manual',
|
|
10
|
+
'manifest',
|
|
11
|
+
'map-observations',
|
|
12
|
+
'oam-export',
|
|
13
|
+
'lookup',
|
|
14
|
+
'bulk',
|
|
15
|
+
'ct-search',
|
|
16
|
+
'ct-intake',
|
|
17
|
+
'discover',
|
|
18
|
+
'discover-scan',
|
|
19
|
+
'posture',
|
|
20
|
+
'http',
|
|
21
|
+
'tls',
|
|
22
|
+
'dnssec-validate',
|
|
23
|
+
'mail-transport',
|
|
24
|
+
'registry-support',
|
|
25
|
+
'registry-doctor',
|
|
26
|
+
'registry-cohort',
|
|
27
|
+
'registry-scaffold',
|
|
28
|
+
'risk-calibrate',
|
|
29
|
+
'lookalike-calibrate',
|
|
30
|
+
'verify-artifact',
|
|
31
|
+
'interchange-report',
|
|
32
|
+
'inspect-archive',
|
|
33
|
+
'sign-artifact',
|
|
34
|
+
'verify-signature',
|
|
35
|
+
'source-report',
|
|
36
|
+
'compare',
|
|
37
|
+
'page-compare',
|
|
38
|
+
'mail-review',
|
|
39
|
+
'review-evidence',
|
|
40
|
+
'brief',
|
|
41
|
+
'case-pack',
|
|
42
|
+
'domain-control',
|
|
43
|
+
'monitor-once',
|
|
44
|
+
'assurance',
|
|
45
|
+
'change-packet',
|
|
46
|
+
'sharing-review',
|
|
47
|
+
'workflow-plan',
|
|
48
|
+
'workflow-run',
|
|
49
|
+
'diff',
|
|
50
|
+
'reconcile',
|
|
51
|
+
'timeline',
|
|
52
|
+
'export',
|
|
53
|
+
]);
|
|
54
|
+
const INVESTIGATION_PLAN_RECIPES = Object.freeze([
|
|
55
|
+
'domain-triage',
|
|
56
|
+
'lookalike-review',
|
|
57
|
+
'owned-domain-review',
|
|
58
|
+
'historical-comparison',
|
|
59
|
+
'campaign-review',
|
|
60
|
+
'certificate-anomaly',
|
|
61
|
+
'registry-disagreement',
|
|
62
|
+
'evidence-handoff',
|
|
63
|
+
'planned-domain-change',
|
|
64
|
+
'post-change-verification',
|
|
65
|
+
]);
|
|
66
|
+
const RUNNABLE_INVESTIGATION_PLAN_RECIPES = Object.freeze([
|
|
67
|
+
'domain-triage',
|
|
68
|
+
'lookalike-review',
|
|
69
|
+
'owned-domain-review',
|
|
70
|
+
'historical-comparison',
|
|
71
|
+
]);
|
|
72
|
+
const CLI_META_ACTIONS = Object.freeze([
|
|
73
|
+
Object.freeze({
|
|
74
|
+
id: 'help',
|
|
75
|
+
aliases: Object.freeze(['--help', '-h']),
|
|
76
|
+
scope: 'root_or_command',
|
|
77
|
+
precedence: 'before_command_grammar',
|
|
78
|
+
bypassesOrdinaryRequirements: true,
|
|
79
|
+
acceptsAdditionalArguments: false,
|
|
80
|
+
}),
|
|
81
|
+
Object.freeze({
|
|
82
|
+
id: 'version',
|
|
83
|
+
aliases: Object.freeze(['--version', '-V']),
|
|
84
|
+
scope: 'root_only',
|
|
85
|
+
precedence: 'before_command_grammar',
|
|
86
|
+
bypassesOrdinaryRequirements: true,
|
|
87
|
+
acceptsAdditionalArguments: false,
|
|
88
|
+
}),
|
|
89
|
+
]);
|
|
90
|
+
const CLI_META_ACTION_BY_ID = Object.freeze(Object.fromEntries(CLI_META_ACTIONS.map((action) => [action.id, action])));
|
|
2
91
|
// Human-facing command reference kept separate from command execution so
|
|
3
92
|
// additions do not expand the already broad runtime controller.
|
|
4
|
-
const
|
|
5
|
-
|
|
93
|
+
const HELP_INTRO = `WHOISleuth CLI
|
|
94
|
+
Domain investigation from your terminal.
|
|
6
95
|
|
|
7
96
|
Quick start:
|
|
8
|
-
whoisleuth
|
|
97
|
+
whoisleuth
|
|
98
|
+
whoisleuth example.test
|
|
9
99
|
whoisleuth lookup example.test --deep
|
|
10
100
|
cat domains.txt | whoisleuth bulk --jsonl
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
lookup Collect one domain, IP, or ASN.
|
|
14
|
-
bulk Triage newline-delimited targets with bounded concurrency.
|
|
15
|
-
http Inspect one homepage request and redirect chain.
|
|
16
|
-
tls Inspect one hostname's current certificate connection.
|
|
17
|
-
posture Review DNS mail and domain-control posture.
|
|
18
|
-
|
|
19
|
-
Discover:
|
|
20
|
-
ct-search Search certificate-transparency observations.
|
|
21
|
-
ct-intake Normalise a local certificate event batch for case import.
|
|
22
|
-
discover Generate offline lookalike candidates.
|
|
23
|
-
discover-scan Collect a bounded candidate review queue.
|
|
24
|
-
registry-support Explain local registry coverage without a request.
|
|
25
|
-
registry-doctor Diagnose a saved registry collection against local policy.
|
|
26
|
-
registry-cohort Aggregate target-free registry quality cohorts.
|
|
27
|
-
registry-scaffold Create a sanitised synthetic fixture scaffold.
|
|
28
|
-
|
|
29
|
-
Review saved evidence:
|
|
30
|
-
map-observations Apply a declarative profile to local observations.
|
|
31
|
-
oam-export Project external findings into an open asset graph.
|
|
32
|
-
source-report Summarise source reliability without retaining targets.
|
|
33
|
-
compare Compare saved registry publications.
|
|
34
|
-
page-compare Compare saved static page and TLS evidence.
|
|
35
|
-
mail-review Review saved passive mail exposure evidence.
|
|
36
|
-
review-evidence Review supplied DNS, routing, GeoIP, RDAP, or trust-store evidence offline.
|
|
37
|
-
brief Turn one saved Lookup into a bounded decision brief.
|
|
38
|
-
case-pack Build a reviewed browser-importable case package.
|
|
39
|
-
domain-control Build or review an integrity-protected desired-state manifest.
|
|
40
|
-
monitor-once Run one bounded domain-control review and checkpoint.
|
|
41
|
-
assurance Review change, recovery, concentration, or retirement plans.
|
|
42
|
-
change-packet Assemble a digest-protected domain change review packet.
|
|
43
|
-
sharing-review Lint a reviewed artifact before deliberate sharing.
|
|
44
|
-
workflow-plan Plan a fixed investigation recipe without executing it.
|
|
45
|
-
workflow-run Execute resumable approved steps from a fixed recipe.
|
|
46
|
-
diff Compare two saved domain observations.
|
|
47
|
-
reconcile Reconcile independently labelled observations.
|
|
48
|
-
timeline Compare a sequence of observations for one domain.
|
|
49
|
-
export Convert a saved lookup into an evidence report.
|
|
50
|
-
inspect-archive Inspect a workspace archive, redacted by default.
|
|
51
|
-
verify-artifact Validate saved evidence or an integrity envelope offline.
|
|
52
|
-
|
|
53
|
-
Integrity and calibration:
|
|
54
|
-
manifest Record ordered artefact and configuration digests.
|
|
55
|
-
sign-artifact Sign one reviewed packet or manifest locally.
|
|
56
|
-
verify-signature Verify one signed evidence package locally.
|
|
57
|
-
risk-calibrate Replay reviewed labels without changing the model.
|
|
58
|
-
lookalike-calibrate Summarise reviewed mutation-family yield without tuning generation.
|
|
59
|
-
|
|
60
|
-
Terminal:
|
|
61
|
-
doctor Check the local runtime; network tests require --network.
|
|
62
|
-
commands List command contracts for people or local tooling.
|
|
63
|
-
completion Print completion for bash, zsh, fish, or PowerShell.
|
|
64
|
-
manual Print the generated manual page.
|
|
65
|
-
|
|
101
|
+
`;
|
|
102
|
+
const HELP_FOOTER = `
|
|
66
103
|
Run "whoisleuth <command> --help" for focused usage and an example.
|
|
104
|
+
With no arguments, an eligible interactive terminal opens a bounded launcher;
|
|
105
|
+
redirected or unsupported terminals continue to print this help. No request
|
|
106
|
+
starts until a Lookup plan is shown and the analyst confirms collection.
|
|
67
107
|
Use --json or --jsonl where supported for machine-readable stdout.
|
|
68
108
|
Use --output <file> for atomic private file output and --force to replace it.
|
|
109
|
+
Use --palette auto, light, or dark after the command to select a fixed terminal
|
|
110
|
+
colour palette; --no-color, NO_COLOR, and redirected output still suppress ANSI.
|
|
69
111
|
Use --config <file> and --profile <name> for explicit versioned safe defaults.
|
|
112
|
+
Registry scaffold is the exception: its --profile selects a fixture capability
|
|
113
|
+
profile and --config is rejected, so bootstrap defaults cannot alter fixtures.
|
|
114
|
+
An ICANN-recognised public domain, reserved documentation domain, IP, or ASN
|
|
115
|
+
may replace "lookup"; URL-like or ambiguous input requires the explicit
|
|
116
|
+
command. Both forms use the same Lookup options.
|
|
70
117
|
Diagnostics are written to stderr. Fast lookup is the default; deep collection
|
|
71
118
|
must be requested explicitly and can disclose a target to additional sources.
|
|
72
119
|
|
|
73
120
|
Copyright 2026 slicedearth. Licensed under AGPL-3.0-only.
|
|
74
121
|
Source and licence: ${WHOISLEUTH_SOURCE_REPOSITORY_URL}
|
|
75
122
|
`;
|
|
76
|
-
const
|
|
123
|
+
const COMMAND_USAGE_SEED = Object.freeze({
|
|
77
124
|
completion: 'whoisleuth completion <bash|zsh|fish|powershell>',
|
|
78
125
|
doctor: 'whoisleuth doctor [--network] [--json] [--quiet] [--no-color]',
|
|
79
|
-
commands: 'whoisleuth commands [--json] [--quiet] [--no-color]',
|
|
126
|
+
commands: 'whoisleuth commands [--common] [--group <group>] [--mode <offline|network>] [--json] [--quiet] [--no-color]',
|
|
80
127
|
manual: 'whoisleuth manual',
|
|
81
128
|
manifest: 'whoisleuth manifest <artefact.json> [...] --workflow <label> [--configuration-digest <sha256:digest>] [--json] [--quiet] [--no-color]',
|
|
82
129
|
'map-observations': 'whoisleuth map-observations [mapping.json] [--json] [--quiet] [--no-color]',
|
|
83
130
|
'oam-export': 'whoisleuth oam-export [external-findings.json] [--json] [--quiet] [--no-color]',
|
|
84
|
-
lookup: 'whoisleuth lookup
|
|
85
|
-
bulk: 'whoisleuth bulk [file] [--json|--jsonl|--junit|--csv|--domains|--queries] [--registered-only|--inconclusive-only|--errors-only] [--fast|--deep] [--concurrency <1-8>] [--checkpoint <file> [--resume]] [--events] [--plan] [--fail-on <policies>]',
|
|
86
|
-
'ct-search': 'whoisleuth ct-search
|
|
131
|
+
lookup: '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]',
|
|
132
|
+
bulk: 'whoisleuth bulk [file] [--json|--jsonl|--junit|--csv|--domains|--queries] [--registered-only|--inconclusive-only|--errors-only] [--fast|--deep] [--concurrency <1-8>] [--checkpoint <file> [--resume]] [--events] [--plan] [--fail-on <policies>] [--quiet] [--no-color]',
|
|
133
|
+
'ct-search': 'whoisleuth ct-search [keyword] [--json] [--quiet] [--no-color]',
|
|
87
134
|
'ct-intake': 'whoisleuth ct-intake [events.json] [--json] [--quiet] [--no-color]',
|
|
88
|
-
discover: 'whoisleuth discover
|
|
89
|
-
'discover-scan': 'whoisleuth discover-scan
|
|
90
|
-
posture: 'whoisleuth posture
|
|
91
|
-
http: 'whoisleuth http
|
|
92
|
-
tls: 'whoisleuth tls
|
|
93
|
-
'
|
|
135
|
+
discover: 'whoisleuth discover [brand|domain] [--tlds <list>] [--preset <name>|--families <ids>] [--keyboard <layout>] [--dictionary <file>] [--snapshot <file>] [--json|--jsonl|--domains] [--quiet] [--no-color]',
|
|
136
|
+
'discover-scan': '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]',
|
|
137
|
+
posture: 'whoisleuth posture [domain] [--selectors <list>] [--retired-selectors <list>] [--mail-profile <profile>] [--json|--sarif --owned-domain] [--quiet] [--no-color]',
|
|
138
|
+
http: 'whoisleuth http [domain] [--json] [--quiet] [--no-color]',
|
|
139
|
+
tls: 'whoisleuth tls [hostname] [--json] [--quiet] [--no-color]',
|
|
140
|
+
'dnssec-validate': 'whoisleuth dnssec-validate <domain> --resolver <public-IP> --trust-anchor <anchor.json> --owned-or-authorized [--json] [--quiet] [--no-color]',
|
|
141
|
+
'mail-transport': 'whoisleuth mail-transport [input.json] --resolver <public-IP> --trust-anchor <anchor.json> --owned-or-authorized --active-probe [--json] [--quiet] [--no-color]',
|
|
142
|
+
'registry-support': 'whoisleuth registry-support [domain|suffix] [--json] [--quiet] [--no-color]',
|
|
94
143
|
'registry-doctor': 'whoisleuth registry-doctor [lookup.json] [--json] [--quiet] [--no-color]',
|
|
95
|
-
'registry-cohort': 'whoisleuth registry-cohort [lookups.json|
|
|
144
|
+
'registry-cohort': 'whoisleuth registry-cohort [lookups-or-reports.json|jsonl] [--json] [--quiet] [--no-color]',
|
|
96
145
|
'registry-scaffold': 'whoisleuth registry-scaffold --profile <id> --suffix <suffix> --scenario <registered|not_found|inconclusive>',
|
|
97
|
-
'risk-calibrate': 'whoisleuth risk-calibrate [dataset.json] [--json] [--quiet] [--no-color]',
|
|
146
|
+
'risk-calibrate': 'whoisleuth risk-calibrate [dataset.json] [--json|--summary-json] [--quiet] [--no-color]',
|
|
98
147
|
'lookalike-calibrate': 'whoisleuth lookalike-calibrate [dataset.json] [--json] [--quiet] [--no-color]',
|
|
99
|
-
'verify-artifact': 'whoisleuth verify-artifact [artifact.json] [--passphrase-file <file>] [--json] [--quiet] [--no-color]',
|
|
100
|
-
'
|
|
148
|
+
'verify-artifact': 'whoisleuth verify-artifact [artifact.json] [--passphrase-file <file>] [--manifest <manifest.json> --manifest-entry <artifact-N>] [--json] [--strict-exit] [--quiet] [--no-color]',
|
|
149
|
+
'interchange-report': 'whoisleuth interchange-report [artifact.json] [--passphrase-file <file>] [--json] [--quiet] [--no-color]',
|
|
150
|
+
'inspect-archive': 'whoisleuth inspect-archive [archive.json] [--passphrase-file <file>] [--search <value>] [--require-match] [--reveal] [--expect-content-digest <sha256:digest>] [--json] [--quiet] [--no-color]',
|
|
101
151
|
'sign-artifact': 'whoisleuth sign-artifact [artifact.json] --private-key-file <file>',
|
|
102
152
|
'verify-signature': 'whoisleuth verify-signature [package.json] [--public-key-file <file>] [--json] [--quiet] [--no-color]',
|
|
103
153
|
'source-report': 'whoisleuth source-report [lookup.json] [--json] [--quiet] [--no-color]',
|
|
@@ -111,15 +161,15 @@ const COMMAND_USAGE = Object.freeze({
|
|
|
111
161
|
'monitor-once': 'whoisleuth monitor-once [manifest.json] [--previous <snapshot.json>] [--limit <1-20>] [--concurrency <1-3>] [--fail-on <policies>] [--json|--junit] [--quiet] [--no-color]',
|
|
112
162
|
assurance: 'whoisleuth assurance [assurance-input.json] [--json] [--quiet] [--no-color]',
|
|
113
163
|
'change-packet': 'whoisleuth change-packet [change-packet-input.json] [--json] [--quiet] [--no-color]',
|
|
114
|
-
'sharing-review': 'whoisleuth sharing-review [artifact.json] --marking <level> --recipient-scope <scope> --purpose <text> [--human-reviewed] [--personal-data-reviewed] [--redactions-confirmed] [--json]',
|
|
115
|
-
'workflow-plan': 'whoisleuth workflow-plan <recipe> <domain|brand> [--json] [--quiet] [--no-color]',
|
|
164
|
+
'sharing-review': 'whoisleuth sharing-review [artifact.json] --marking <level> --recipient-scope <scope> --purpose <text> [--human-reviewed] [--personal-data-reviewed] [--redactions-confirmed] [--json] [--quiet] [--no-color]',
|
|
165
|
+
'workflow-plan': 'whoisleuth workflow-plan <recipe> <domain|brand> | --list | --explain <recipe> [--json] [--quiet] [--no-color]',
|
|
116
166
|
'workflow-run': 'whoisleuth workflow-run <recipe> <domain|brand> [--approve-network] [--resume <state.json>] [--json] [--quiet] [--no-color]',
|
|
117
|
-
diff: 'whoisleuth diff <left.json> <right.json> [--json] [--quiet] [--no-color]',
|
|
167
|
+
diff: 'whoisleuth diff <left.json> <right.json> [--left-session <id> --right-session <id>] [--json] [--quiet] [--no-color]',
|
|
118
168
|
reconcile: 'whoisleuth reconcile <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
|
|
119
169
|
timeline: 'whoisleuth timeline <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
|
|
120
|
-
export: 'whoisleuth export [lookup.json] [--markdown|--html|--compact]',
|
|
170
|
+
export: 'whoisleuth export [lookup.json] [--markdown|--html|--compact] [--no-attribution]',
|
|
121
171
|
});
|
|
122
|
-
const
|
|
172
|
+
const COMMAND_DETAILS_SEED = Object.freeze({
|
|
123
173
|
completion: {
|
|
124
174
|
description: 'Print a static shell-completion script for the installed CLI.',
|
|
125
175
|
example: 'whoisleuth completion zsh > ~/.zfunc/_whoisleuth',
|
|
@@ -157,8 +207,8 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
157
207
|
},
|
|
158
208
|
lookup: {
|
|
159
209
|
description: 'Collect registration evidence for one domain, IP, or ASN.',
|
|
160
|
-
example: 'whoisleuth lookup example.test --deep',
|
|
161
|
-
boundary: 'Fast is the default. Deep mode adds bounded WHOIS, DNS, HTTP, TLS, technology, posture, and network context where applicable.',
|
|
210
|
+
example: 'whoisleuth lookup example.test --deep --browse',
|
|
211
|
+
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.',
|
|
162
212
|
},
|
|
163
213
|
bulk: {
|
|
164
214
|
description: 'Triage newline-delimited domains, IPs, or ASNs with bounded concurrency.',
|
|
@@ -193,13 +243,23 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
193
243
|
http: {
|
|
194
244
|
description: 'Inspect one homepage request, redirects, and bounded response metadata.',
|
|
195
245
|
example: 'whoisleuth http example.test --json',
|
|
196
|
-
boundary: 'Requests use the shared public-address and redirect guards. This is not a rendered browser or vulnerability scan.',
|
|
246
|
+
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.',
|
|
197
247
|
},
|
|
198
248
|
tls: {
|
|
199
249
|
description: 'Inspect one hostname certificate through a bounded TLS connection.',
|
|
200
250
|
example: 'whoisleuth tls example.test --json',
|
|
201
251
|
boundary: 'One observed connection is point-in-time evidence and does not establish every address, edge, or historical certificate.',
|
|
202
252
|
},
|
|
253
|
+
'dnssec-validate': {
|
|
254
|
+
description: 'Cryptographically validate one authorised DNSSEC chain from a supplied trust anchor through one selected public resolver.',
|
|
255
|
+
example: 'whoisleuth dnssec-validate example.test --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --json',
|
|
256
|
+
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.',
|
|
257
|
+
},
|
|
258
|
+
'mail-transport': {
|
|
259
|
+
description: 'Review selected authorised MX endpoints, DNSSEC-qualified TLSA evidence, SMTP capabilities, and optional STARTTLS certificates.',
|
|
260
|
+
example: 'whoisleuth mail-transport selected-mx.json --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --active-probe --json',
|
|
261
|
+
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.',
|
|
262
|
+
},
|
|
203
263
|
'registry-support': {
|
|
204
264
|
description: 'Explain the local registry capability profile for one domain or suffix.',
|
|
205
265
|
example: 'whoisleuth registry-support example.test --json',
|
|
@@ -211,19 +271,19 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
211
271
|
boundary: 'The command is offline. It distinguishes expected access constraints from collection results and does not contact a live registry.',
|
|
212
272
|
},
|
|
213
273
|
'registry-cohort': {
|
|
214
|
-
description: '
|
|
274
|
+
description: 'Build privacy-safe suffix and capability-profile timelines from saved observations or retained cohort reports.',
|
|
215
275
|
example: 'whoisleuth registry-cohort saved-lookups.jsonl --json',
|
|
216
|
-
boundary: 'This command is offline and omits domains, queries, and raw evidence.
|
|
276
|
+
boundary: 'This command is offline and omits domains, queries, and raw evidence. Input families cannot be mixed, and retained samples are never assumed independent.',
|
|
217
277
|
},
|
|
218
278
|
'registry-scaffold': {
|
|
219
279
|
description: 'Create a bounded synthetic WHOIS fixture scaffold for one existing capability profile.',
|
|
220
280
|
example: 'whoisleuth registry-scaffold --profile example-profile --suffix test --scenario registered',
|
|
221
|
-
boundary: 'The output is a sanitised template only.
|
|
281
|
+
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.',
|
|
222
282
|
},
|
|
223
283
|
'risk-calibrate': {
|
|
224
284
|
description: 'Replay reviewed labels against the current explainable Risk model.',
|
|
225
|
-
example: 'whoisleuth risk-calibrate calibration.json --json',
|
|
226
|
-
boundary: 'Calibration is offline and diagnostic.
|
|
285
|
+
example: 'whoisleuth risk-calibrate calibration.json --summary-json',
|
|
286
|
+
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.',
|
|
227
287
|
},
|
|
228
288
|
'lookalike-calibrate': {
|
|
229
289
|
description: 'Summarise reviewed candidate dispositions by mutation family without retaining domains.',
|
|
@@ -231,14 +291,19 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
231
291
|
boundary: 'Calibration is offline and diagnostic. It omits candidate identifiers, domains, notes, and evidence and never tunes generation or filtering automatically.',
|
|
232
292
|
},
|
|
233
293
|
'verify-artifact': {
|
|
234
|
-
description: 'Validate a supported archive, packet, manifest, or
|
|
235
|
-
example: 'whoisleuth verify-artifact
|
|
236
|
-
boundary: 'Verification is offline and redacted. Encrypted archives require an explicitly supplied passphrase file.',
|
|
294
|
+
description: 'Validate a supported archive, claim passport, packet, manifest, saved Lookup, or supported Lookup-evidence export without printing evidence contents.',
|
|
295
|
+
example: 'whoisleuth verify-artifact report.json --manifest manifest.json --manifest-entry artifact-2 --json --strict-exit',
|
|
296
|
+
boundary: 'Verification is offline and redacted. Encrypted archives require an explicitly supplied passphrase file; --strict-exit returns 4 when only an envelope or legacy projection integrity was verified.',
|
|
297
|
+
},
|
|
298
|
+
'interchange-report': {
|
|
299
|
+
description: 'Report what one recognised portable artefact preserves, excludes, and supports across browser and CLI workflows.',
|
|
300
|
+
example: 'whoisleuth interchange-report workspace.json --json',
|
|
301
|
+
boundary: 'The report is offline and metadata-only. It does not echo targets, contacts, notes, passphrases, evidence values, or an unrecognised schema string.',
|
|
237
302
|
},
|
|
238
303
|
'inspect-archive': {
|
|
239
|
-
description:
|
|
304
|
+
description: `Summarise or search one current version-${WORKSPACE_ARCHIVE_VERSION} workspace archive, with exact public version-${PUBLIC_WORKSPACE_ARCHIVE_VERSION} support and redacted output by default.`,
|
|
240
305
|
example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
|
|
241
|
-
boundary: 'Exact matches require --reveal.
|
|
306
|
+
boundary: 'Exact matches require --reveal. Retired and future archive versions are rejected without changing data. The archive is read locally and is never uploaded.',
|
|
242
307
|
},
|
|
243
308
|
'sign-artifact': {
|
|
244
309
|
description: 'Sign one reviewed response packet or supported manifest with a local private key.',
|
|
@@ -246,9 +311,9 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
246
311
|
boundary: 'The command never creates, stores, or transmits keys. Key custody and signer identity remain the operator\'s responsibility.',
|
|
247
312
|
},
|
|
248
313
|
'verify-signature': {
|
|
249
|
-
description: 'Verify the
|
|
314
|
+
description: 'Verify the cryptographic signature of one signed evidence package and report embedded-artefact assurance separately.',
|
|
250
315
|
example: 'whoisleuth verify-signature packet.signed.json --json',
|
|
251
|
-
boundary: 'A valid signature proves package consistency for the embedded key
|
|
316
|
+
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.',
|
|
252
317
|
},
|
|
253
318
|
'source-report': {
|
|
254
319
|
description: 'Create a target-free reliability summary from a saved lookup.',
|
|
@@ -281,7 +346,7 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
281
346
|
boundary: 'The command is offline, excludes raw upstream payloads, and does not create an analyst assertion or claim that the saved observation is current.',
|
|
282
347
|
},
|
|
283
348
|
'case-pack': {
|
|
284
|
-
description:
|
|
349
|
+
description: `Build a reviewed, audience-specific Case-pack v2 from an exact Case-schema-${CASE_SCHEMA_VERSION} export.`,
|
|
285
350
|
example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
|
|
286
351
|
boundary: 'The command is offline, creates a new package, never mutates the source archive, and requires an explicit review acknowledgement.',
|
|
287
352
|
},
|
|
@@ -321,9 +386,9 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
321
386
|
boundary: 'Only installed recipe commands can run. Network steps require explicit approval for each invocation, and analyst-selection placeholders always pause without interpretation.',
|
|
322
387
|
},
|
|
323
388
|
diff: {
|
|
324
|
-
description: 'Compare
|
|
389
|
+
description: 'Compare two compatible retained Lookup, Bulk-session, or domain-portfolio artefacts.',
|
|
325
390
|
example: 'whoisleuth diff first.json second.json --json',
|
|
326
|
-
boundary: 'Comparison is offline.
|
|
391
|
+
boundary: 'Comparison is offline. Multi-session Bulk exports require explicit session IDs, and missing, unavailable, equal, and different evidence remain separate states.',
|
|
327
392
|
},
|
|
328
393
|
reconcile: {
|
|
329
394
|
description: 'Reconcile bounded values across independently labelled observations of one domain.',
|
|
@@ -338,10 +403,10 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
338
403
|
export: {
|
|
339
404
|
description: 'Convert one saved lookup into a versioned evidence report.',
|
|
340
405
|
example: 'whoisleuth export lookup.json --markdown',
|
|
341
|
-
boundary:
|
|
406
|
+
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; exact public schema ${PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION} remains readable with its strict source/publication binding, 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.`,
|
|
342
407
|
},
|
|
343
408
|
});
|
|
344
|
-
const
|
|
409
|
+
const COMMAND_COLLECTION_SEED = Object.freeze({
|
|
345
410
|
completion: { mode: 'offline', scope: 'Prints one static script and changes no shell configuration.' },
|
|
346
411
|
doctor: { mode: 'network', scope: 'Network access is opt-in with --network and is limited to fixed public DNS, HTTPS, and WHOIS diagnostics.' },
|
|
347
412
|
commands: { mode: 'offline', scope: 'Reads the embedded command catalogue and performs no collection.' },
|
|
@@ -355,17 +420,20 @@ const COMMAND_COLLECTION = Object.freeze({
|
|
|
355
420
|
'ct-intake': { mode: 'offline', scope: 'Reads one source-qualified event batch capped at 4 MiB and makes no request.' },
|
|
356
421
|
discover: { mode: 'offline', scope: 'Generates a bounded candidate set from local rules, dictionaries, and optional saved snapshots.' },
|
|
357
422
|
'discover-scan': { mode: 'network', scope: 'Scans at most 500 fast or 50 deep candidates, with concurrency capped at 8 fast or 3 deep.' },
|
|
358
|
-
posture: { mode: 'network', scope: 'Accepts one domain and performs bounded DNS
|
|
423
|
+
posture: { mode: 'network', scope: 'Accepts one domain and performs bounded RDAP, DNS, and conditional MTA-STS HTTPS requests.' },
|
|
359
424
|
http: { mode: 'network', scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
|
|
360
425
|
tls: { mode: 'network', scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
|
|
426
|
+
'dnssec-validate': { mode: 'network', scope: 'Accepts one authorised domain, one public resolver IP, and one local trust-anchor file; DNS-over-TCP validation is capped at 32 queries and 15 seconds.' },
|
|
427
|
+
'mail-transport': { mode: 'network', scope: 'Accepts at most three selected authorised MX hosts, uses one public resolver, and performs sequential bounded SMTP connections with no retries.' },
|
|
361
428
|
'registry-support': { mode: 'offline', scope: 'Reads the embedded registry capability catalogue for one domain or suffix.' },
|
|
362
429
|
'registry-doctor': { mode: 'offline', scope: 'Reads one saved Lookup and the embedded registry capability catalogue.' },
|
|
363
|
-
'registry-cohort': { mode: 'offline', scope: 'Reads at most 500 saved Lookups and emits target-free
|
|
430
|
+
'registry-cohort': { mode: 'offline', scope: 'Reads at most 500 saved Lookups or retained cohort reports from one unmixed family and emits bounded target-free timelines.' },
|
|
364
431
|
'registry-scaffold': { mode: 'offline', scope: 'Reads the embedded registry capability catalogue and prints one synthetic fixture template.' },
|
|
365
432
|
'risk-calibrate': { mode: 'offline', scope: 'Reads one bounded reviewed-label dataset and changes no model or evidence.' },
|
|
366
433
|
'lookalike-calibrate': { mode: 'offline', scope: 'Reads at most 5,000 reviewed candidate labels from one dataset capped at 2 MiB.' },
|
|
367
|
-
'verify-artifact': { mode: 'offline', scope: 'Reads one selected bounded
|
|
368
|
-
'
|
|
434
|
+
'verify-artifact': { 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.' },
|
|
435
|
+
'interchange-report': { mode: 'offline', scope: 'Reads one selected bounded portable artefact and emits fixed compatibility metadata only.' },
|
|
436
|
+
'inspect-archive': { mode: 'offline', scope: `Reads one selected bounded workspace archive v${WORKSPACE_ARCHIVE_VERSION}, retains exact v${PUBLIC_WORKSPACE_ARCHIVE_VERSION} compatibility, and redacts output by default.` },
|
|
369
437
|
'sign-artifact': { mode: 'offline', scope: 'Reads one selected artefact and one local private key without transmitting either.' },
|
|
370
438
|
'verify-signature': { mode: 'offline', scope: 'Reads one selected signed package and optional local public key.' },
|
|
371
439
|
'source-report': { mode: 'offline', scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
|
|
@@ -373,8 +441,8 @@ const COMMAND_COLLECTION = Object.freeze({
|
|
|
373
441
|
'page-compare': { mode: 'offline', scope: 'Reads two saved Lookup documents and executes no page code.' },
|
|
374
442
|
'mail-review': { mode: 'offline', scope: 'Reads one saved Bulk result and sends no DNS or SMTP traffic.' },
|
|
375
443
|
'review-evidence': { mode: 'offline', scope: 'Reads one bounded versioned evidence or request-planning document and performs no collection.' },
|
|
376
|
-
brief: { mode: 'offline', scope: 'Reads one bounded saved Lookup and emits a compact source-
|
|
377
|
-
'case-pack': { mode: 'offline', scope:
|
|
444
|
+
brief: { mode: 'offline', scope: 'Reads one bounded saved Lookup and emits a compact source-attributed decision brief.' },
|
|
445
|
+
'case-pack': { mode: 'offline', scope: `Reads one bounded Case-schema-${CASE_SCHEMA_VERSION} browser export and writes a separate audience-specific Case-pack v2.` },
|
|
378
446
|
'domain-control': { mode: 'offline', scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
|
|
379
447
|
'monitor-once': { mode: 'network', scope: 'Runs deep collection for at most 20 manifest domains with concurrency capped at 3.' },
|
|
380
448
|
assurance: { mode: 'offline', scope: 'Reads one versioned plan capped at 2 MiB and makes no request or configuration change.' },
|
|
@@ -382,14 +450,788 @@ const COMMAND_COLLECTION = Object.freeze({
|
|
|
382
450
|
'sharing-review': { mode: 'offline', scope: 'Reads one artefact capped at 15 MiB, emits only bounded schema/version metadata and no content values, and performs no transmission.' },
|
|
383
451
|
'workflow-plan': { mode: 'offline', scope: 'Builds a fixed typed recipe and executes none of its network or file steps.' },
|
|
384
452
|
'workflow-run': { mode: 'network', scope: 'Runs only concrete fixed-recipe steps; network collection requires --approve-network and analyst-selection steps always pause.' },
|
|
385
|
-
diff: { mode: 'offline', scope: 'Reads two
|
|
453
|
+
diff: { mode: 'offline', scope: 'Reads two compatible retained artefacts capped at 8 MiB each and retains no source paths.' },
|
|
386
454
|
reconcile: { mode: 'offline', scope: 'Reads 2 to 5 saved observations for one domain, capped at 32 MiB in total.' },
|
|
387
455
|
timeline: { mode: 'offline', scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
|
|
388
456
|
export: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
|
|
389
457
|
});
|
|
458
|
+
const COMMON_OPTIONS = Object.freeze(['--help', '--output', '--force', '--config', '--profile', '--palette']);
|
|
459
|
+
const REGISTRY_SCAFFOLD_COMMON_OPTIONS = Object.freeze(COMMON_OPTIONS.filter((option) => option !== '--config' && option !== '--profile'));
|
|
460
|
+
function commonOptionsSeedForCommand(command) {
|
|
461
|
+
return command === 'registry-scaffold' ? REGISTRY_SCAFFOLD_COMMON_OPTIONS : COMMON_OPTIONS;
|
|
462
|
+
}
|
|
463
|
+
const OPTIONS_BY_COMMAND_SEED = Object.freeze({
|
|
464
|
+
manifest: ['--workflow', '--configuration-digest', '--json', '--quiet', '--no-color'],
|
|
465
|
+
'map-observations': ['--json', '--quiet', '--no-color'],
|
|
466
|
+
'oam-export': ['--json', '--quiet', '--no-color'],
|
|
467
|
+
lookup: ['--json', '--junit', '--markdown', '--html', '--no-attribution', '--fast', '--deep', '--observer', '--vantage', '--plan', '--summary', '--verbose', '--browse', '--save-lookup', '--strict-exit', '--fail-on', '--events', '--quiet', '--no-color'],
|
|
468
|
+
bulk: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries', '--registered-only', '--inconclusive-only', '--errors-only', '--fast', '--deep', '--concurrency', '--checkpoint', '--resume', '--events', '--plan', '--fail-on', '--quiet', '--no-color'],
|
|
469
|
+
'ct-search': ['--json', '--quiet', '--no-color'],
|
|
470
|
+
'ct-intake': ['--json', '--quiet', '--no-color'],
|
|
471
|
+
discover: ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--snapshot', '--json', '--jsonl', '--domains', '--quiet', '--no-color'],
|
|
472
|
+
'discover-scan': ['--tlds', '--preset', '--families', '--keyboard', '--dictionary', '--fast', '--deep', '--scan-limit', '--chunk-size', '--concurrency', '--resolver', '--allowlist', '--checkpoint', '--resume', '--observation-snapshot', '--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only', '--events', '--plan', '--fail-on', '--json', '--jsonl', '--csv', '--domains', '--quiet', '--no-color'],
|
|
473
|
+
posture: ['--selectors', '--retired-selectors', '--mail-profile', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
|
|
474
|
+
http: ['--json', '--quiet', '--no-color'],
|
|
475
|
+
tls: ['--json', '--quiet', '--no-color'],
|
|
476
|
+
'dnssec-validate': ['--resolver', '--trust-anchor', '--owned-or-authorized', '--json', '--quiet', '--no-color'],
|
|
477
|
+
'mail-transport': ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe', '--json', '--quiet', '--no-color'],
|
|
478
|
+
'registry-support': ['--json', '--quiet', '--no-color'],
|
|
479
|
+
'registry-doctor': ['--json', '--quiet', '--no-color'],
|
|
480
|
+
'registry-cohort': ['--json', '--quiet', '--no-color'],
|
|
481
|
+
'registry-scaffold': ['--profile', '--suffix', '--scenario'],
|
|
482
|
+
'risk-calibrate': ['--json', '--summary-json', '--quiet', '--no-color'],
|
|
483
|
+
'lookalike-calibrate': ['--json', '--quiet', '--no-color'],
|
|
484
|
+
'verify-artifact': ['--passphrase-file', '--manifest', '--manifest-entry', '--json', '--strict-exit', '--quiet', '--no-color'],
|
|
485
|
+
'interchange-report': ['--passphrase-file', '--json', '--quiet', '--no-color'],
|
|
486
|
+
'inspect-archive': ['--passphrase-file', '--search', '--require-match', '--reveal', '--expect-content-digest', '--json', '--quiet', '--no-color'],
|
|
487
|
+
'sign-artifact': ['--private-key-file'],
|
|
488
|
+
'verify-signature': ['--public-key-file', '--json', '--quiet', '--no-color'],
|
|
489
|
+
'source-report': ['--json', '--quiet', '--no-color'],
|
|
490
|
+
compare: ['--json', '--quiet', '--no-color'],
|
|
491
|
+
'page-compare': ['--json', '--quiet', '--no-color'],
|
|
492
|
+
'mail-review': ['--json', '--quiet', '--no-color'],
|
|
493
|
+
'review-evidence': ['--mmdb', '--json', '--strict-exit', '--quiet', '--no-color'],
|
|
494
|
+
brief: ['--json', '--quiet', '--no-color'],
|
|
495
|
+
'case-pack': ['--audience', '--reviewed', '--json', '--quiet', '--no-color'],
|
|
496
|
+
'domain-control': ['--json', '--quiet', '--no-color'],
|
|
497
|
+
'monitor-once': ['--previous', '--limit', '--concurrency', '--fail-on', '--json', '--junit', '--quiet', '--no-color'],
|
|
498
|
+
assurance: ['--json', '--quiet', '--no-color'],
|
|
499
|
+
'change-packet': ['--json', '--quiet', '--no-color'],
|
|
500
|
+
'sharing-review': ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
|
|
501
|
+
'workflow-plan': ['--list', '--explain', '--json', '--quiet', '--no-color'],
|
|
502
|
+
'workflow-run': ['--approve-network', '--resume', '--json', '--quiet', '--no-color'],
|
|
503
|
+
diff: ['--left-session', '--right-session', '--json', '--quiet', '--no-color'],
|
|
504
|
+
reconcile: ['--json', '--quiet', '--no-color'],
|
|
505
|
+
timeline: ['--json', '--quiet', '--no-color'],
|
|
506
|
+
export: ['--markdown', '--html', '--compact', '--no-attribution'],
|
|
507
|
+
completion: [],
|
|
508
|
+
commands: ['--common', '--group', '--mode', '--json', '--quiet', '--no-color'],
|
|
509
|
+
doctor: ['--network', '--json', '--quiet', '--no-color'],
|
|
510
|
+
manual: [],
|
|
511
|
+
});
|
|
512
|
+
const VALUE_OPTIONS = Object.freeze({
|
|
513
|
+
'--group': Object.freeze(['investigate', 'respond', 'assure', 'utilities']),
|
|
514
|
+
'--mode': Object.freeze(['offline', 'network']),
|
|
515
|
+
'--explain': INVESTIGATION_PLAN_RECIPES,
|
|
516
|
+
'--preset': Object.freeze(['common', 'impersonation', 'all']),
|
|
517
|
+
'--keyboard': Object.freeze(['qwerty', 'azerty', 'qwertz', 'all']),
|
|
518
|
+
'--mail-profile': Object.freeze(['standard', 'defensive-no-mail', 'parked']),
|
|
519
|
+
'--marking': Object.freeze(['clear', 'green', 'amber', 'amber-strict', 'red']),
|
|
520
|
+
'--recipient-scope': Object.freeze(['public', 'community', 'organization', 'named-recipients']),
|
|
521
|
+
'--audience': Object.freeze(['internal', 'trusted', 'public']),
|
|
522
|
+
'--fail-on': Object.freeze(['source-failure', 'inconclusive', 'danger', 'material-drift']),
|
|
523
|
+
'--manifest-entry': Object.freeze(Array.from({ length: 16 }, (_, index) => `artifact-${index + 1}`)),
|
|
524
|
+
'--palette': Object.freeze(['auto', 'light', 'dark']),
|
|
525
|
+
'--scenario': Object.freeze(['registered', 'not_found', 'inconclusive']),
|
|
526
|
+
});
|
|
527
|
+
const STANDARD_CONCURRENCY_VALUES = Object.freeze(['1', '2', '3', '4', '5', '6', '7', '8']);
|
|
528
|
+
const LIMITED_CONCURRENCY_VALUES = Object.freeze(['1', '2', '3']);
|
|
529
|
+
const FILE_OPTIONS = Object.freeze([
|
|
530
|
+
'--checkpoint',
|
|
531
|
+
'--config',
|
|
532
|
+
'--allowlist',
|
|
533
|
+
'--dictionary',
|
|
534
|
+
'--manifest',
|
|
535
|
+
'--mmdb',
|
|
536
|
+
'--output',
|
|
537
|
+
'--passphrase-file',
|
|
538
|
+
'--private-key-file',
|
|
539
|
+
'--public-key-file',
|
|
540
|
+
'--snapshot',
|
|
541
|
+
'--observation-snapshot',
|
|
542
|
+
'--previous',
|
|
543
|
+
'--save-lookup',
|
|
544
|
+
'--trust-anchor',
|
|
545
|
+
]);
|
|
546
|
+
const FILE_OPTIONS_BY_COMMAND = Object.freeze({
|
|
547
|
+
'workflow-run': Object.freeze(['--resume']),
|
|
548
|
+
});
|
|
549
|
+
function positional(name, valueKind, minimum, maximum, values = [], inputSource = 'argv', requiredWhenOptions = []) {
|
|
550
|
+
return Object.freeze({
|
|
551
|
+
name,
|
|
552
|
+
valueKind,
|
|
553
|
+
minimum,
|
|
554
|
+
maximum,
|
|
555
|
+
values: Object.freeze([...values]),
|
|
556
|
+
inputSource,
|
|
557
|
+
requiredWhenOptions: Object.freeze([...requiredWhenOptions]),
|
|
558
|
+
});
|
|
559
|
+
}
|
|
560
|
+
const NO_POSITIONALS = Object.freeze([]);
|
|
561
|
+
const OPTIONAL_FILE_POSITIONAL = Object.freeze([positional('source', 'file', 0, 1, [], 'argv_or_stdin')]);
|
|
562
|
+
const OPTIONAL_TEXT_POSITIONAL = Object.freeze([positional('subject', 'text', 0, 1, [], 'argv_or_stdin')]);
|
|
563
|
+
const POSITIONALS_BY_COMMAND_SEED = Object.freeze({
|
|
564
|
+
completion: Object.freeze([positional('shell', 'enum', 1, 1, ['bash', 'zsh', 'fish', 'powershell'])]),
|
|
565
|
+
doctor: NO_POSITIONALS,
|
|
566
|
+
commands: NO_POSITIONALS,
|
|
567
|
+
manual: NO_POSITIONALS,
|
|
568
|
+
manifest: Object.freeze([positional('artefacts', 'file', 1, 16)]),
|
|
569
|
+
'map-observations': OPTIONAL_FILE_POSITIONAL,
|
|
570
|
+
'oam-export': OPTIONAL_FILE_POSITIONAL,
|
|
571
|
+
lookup: Object.freeze([positional('target', 'text', 0, 1, [], 'argv_or_stdin', ['--browse'])]),
|
|
572
|
+
bulk: OPTIONAL_FILE_POSITIONAL,
|
|
573
|
+
'ct-search': Object.freeze([positional('keyword', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
574
|
+
'ct-intake': OPTIONAL_FILE_POSITIONAL,
|
|
575
|
+
discover: OPTIONAL_TEXT_POSITIONAL,
|
|
576
|
+
'discover-scan': OPTIONAL_TEXT_POSITIONAL,
|
|
577
|
+
posture: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
578
|
+
http: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
579
|
+
tls: Object.freeze([positional('hostname', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
580
|
+
'dnssec-validate': Object.freeze([positional('domain', 'text', 1, 1)]),
|
|
581
|
+
'mail-transport': OPTIONAL_FILE_POSITIONAL,
|
|
582
|
+
'registry-support': Object.freeze([positional('domain-or-suffix', 'text', 0, 1, [], 'argv_or_stdin')]),
|
|
583
|
+
'registry-doctor': OPTIONAL_FILE_POSITIONAL,
|
|
584
|
+
'registry-cohort': OPTIONAL_FILE_POSITIONAL,
|
|
585
|
+
'registry-scaffold': NO_POSITIONALS,
|
|
586
|
+
'risk-calibrate': OPTIONAL_FILE_POSITIONAL,
|
|
587
|
+
'lookalike-calibrate': OPTIONAL_FILE_POSITIONAL,
|
|
588
|
+
'verify-artifact': OPTIONAL_FILE_POSITIONAL,
|
|
589
|
+
'interchange-report': OPTIONAL_FILE_POSITIONAL,
|
|
590
|
+
'inspect-archive': OPTIONAL_FILE_POSITIONAL,
|
|
591
|
+
'sign-artifact': OPTIONAL_FILE_POSITIONAL,
|
|
592
|
+
'verify-signature': OPTIONAL_FILE_POSITIONAL,
|
|
593
|
+
'source-report': OPTIONAL_FILE_POSITIONAL,
|
|
594
|
+
compare: OPTIONAL_FILE_POSITIONAL,
|
|
595
|
+
'page-compare': Object.freeze([positional('sources', 'file', 2, 2)]),
|
|
596
|
+
'mail-review': OPTIONAL_FILE_POSITIONAL,
|
|
597
|
+
'review-evidence': OPTIONAL_FILE_POSITIONAL,
|
|
598
|
+
brief: OPTIONAL_FILE_POSITIONAL,
|
|
599
|
+
'case-pack': OPTIONAL_FILE_POSITIONAL,
|
|
600
|
+
'domain-control': OPTIONAL_FILE_POSITIONAL,
|
|
601
|
+
'monitor-once': OPTIONAL_FILE_POSITIONAL,
|
|
602
|
+
assurance: OPTIONAL_FILE_POSITIONAL,
|
|
603
|
+
'change-packet': OPTIONAL_FILE_POSITIONAL,
|
|
604
|
+
'sharing-review': OPTIONAL_FILE_POSITIONAL,
|
|
605
|
+
'workflow-plan': Object.freeze([
|
|
606
|
+
positional('recipe', 'enum', 0, 1, INVESTIGATION_PLAN_RECIPES),
|
|
607
|
+
positional('subject', 'text', 0, 1),
|
|
608
|
+
]),
|
|
609
|
+
'workflow-run': Object.freeze([
|
|
610
|
+
positional('recipe', 'enum', 1, 1, RUNNABLE_INVESTIGATION_PLAN_RECIPES),
|
|
611
|
+
positional('subject', 'text', 1, 1),
|
|
612
|
+
]),
|
|
613
|
+
diff: Object.freeze([positional('sources', 'file', 2, 2)]),
|
|
614
|
+
reconcile: Object.freeze([positional('sources', 'file', 2, 5)]),
|
|
615
|
+
timeline: Object.freeze([positional('sources', 'file', 2, 20)]),
|
|
616
|
+
export: OPTIONAL_FILE_POSITIONAL,
|
|
617
|
+
});
|
|
618
|
+
const TEXT_OPTIONS = Object.freeze([
|
|
619
|
+
'--families',
|
|
620
|
+
'--workflow',
|
|
621
|
+
'--configuration-digest',
|
|
622
|
+
'--resolver',
|
|
623
|
+
'--expect-content-digest',
|
|
624
|
+
'--profile',
|
|
625
|
+
'--suffix',
|
|
626
|
+
'--left-session',
|
|
627
|
+
'--right-session',
|
|
628
|
+
'--purpose',
|
|
629
|
+
'--observer',
|
|
630
|
+
'--retired-selectors',
|
|
631
|
+
'--search',
|
|
632
|
+
'--selectors',
|
|
633
|
+
'--tlds',
|
|
634
|
+
'--vantage',
|
|
635
|
+
]);
|
|
636
|
+
const IDEMPOTENT_OPTIONS = Object.freeze([
|
|
637
|
+
'--human-reviewed',
|
|
638
|
+
'--no-color',
|
|
639
|
+
'--personal-data-reviewed',
|
|
640
|
+
'--quiet',
|
|
641
|
+
'--redactions-confirmed',
|
|
642
|
+
]);
|
|
643
|
+
const INTEGER_RANGE_SEED = Object.freeze({
|
|
644
|
+
bulk: Object.freeze({
|
|
645
|
+
'--concurrency': Object.freeze([
|
|
646
|
+
Object.freeze({ minimum: 1, maximum: 8, whenOptionPresent: null }),
|
|
647
|
+
Object.freeze({ minimum: 1, maximum: 3, whenOptionPresent: '--deep' }),
|
|
648
|
+
]),
|
|
649
|
+
}),
|
|
650
|
+
'discover-scan': Object.freeze({
|
|
651
|
+
'--scan-limit': Object.freeze([
|
|
652
|
+
Object.freeze({ minimum: 1, maximum: 500, whenOptionPresent: null }),
|
|
653
|
+
Object.freeze({ minimum: 1, maximum: 50, whenOptionPresent: '--deep' }),
|
|
654
|
+
]),
|
|
655
|
+
'--chunk-size': Object.freeze([
|
|
656
|
+
Object.freeze({ minimum: 1, maximum: 100, whenOptionPresent: null }),
|
|
657
|
+
]),
|
|
658
|
+
'--concurrency': Object.freeze([
|
|
659
|
+
Object.freeze({ minimum: 1, maximum: 8, whenOptionPresent: null }),
|
|
660
|
+
Object.freeze({ minimum: 1, maximum: 3, whenOptionPresent: '--deep' }),
|
|
661
|
+
]),
|
|
662
|
+
}),
|
|
663
|
+
'monitor-once': Object.freeze({
|
|
664
|
+
'--limit': Object.freeze([
|
|
665
|
+
Object.freeze({ minimum: 1, maximum: 20, whenOptionPresent: null }),
|
|
666
|
+
]),
|
|
667
|
+
'--concurrency': Object.freeze([
|
|
668
|
+
Object.freeze({ minimum: 1, maximum: 3, whenOptionPresent: null }),
|
|
669
|
+
]),
|
|
670
|
+
}),
|
|
671
|
+
});
|
|
672
|
+
function constraint(value) {
|
|
673
|
+
if (value.kind === 'mutually_exclusive') {
|
|
674
|
+
return Object.freeze({ ...value, options: Object.freeze([...value.options]) });
|
|
675
|
+
}
|
|
676
|
+
if (value.kind === 'required') {
|
|
677
|
+
return Object.freeze({ ...value, options: Object.freeze([...value.options]) });
|
|
678
|
+
}
|
|
679
|
+
if (value.kind === 'excludes_all' || value.kind === 'value_excludes') {
|
|
680
|
+
return Object.freeze({ ...value, excludedOptions: Object.freeze([...value.excludedOptions]) });
|
|
681
|
+
}
|
|
682
|
+
return Object.freeze({ ...value, requiredOptions: Object.freeze([...value.requiredOptions]) });
|
|
683
|
+
}
|
|
684
|
+
const EMPTY_CONSTRAINTS = Object.freeze([]);
|
|
685
|
+
const FILE_OUTPUT_CONSTRAINTS = Object.freeze([
|
|
686
|
+
constraint({ kind: 'requires_all', option: '--force', requiredOptions: ['--output'] }),
|
|
687
|
+
]);
|
|
688
|
+
const QUIET_OUTPUT_CONSTRAINT = constraint({
|
|
689
|
+
kind: 'mutually_exclusive',
|
|
690
|
+
options: ['--quiet', '--output'],
|
|
691
|
+
});
|
|
692
|
+
const MACHINE_OUTPUT_OPTIONS = Object.freeze([
|
|
693
|
+
'--json', '--jsonl', '--junit', '--csv', '--domains', '--queries', '--markdown',
|
|
694
|
+
'--html', '--sarif', '--summary-json',
|
|
695
|
+
]);
|
|
696
|
+
const GRAMMAR_CONSTRAINTS_SEED = Object.freeze({
|
|
697
|
+
commands: Object.freeze([]),
|
|
698
|
+
manifest: Object.freeze([
|
|
699
|
+
constraint({ kind: 'required', options: ['--workflow'] }),
|
|
700
|
+
]),
|
|
701
|
+
lookup: Object.freeze([
|
|
702
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--junit', '--markdown', '--html'] }),
|
|
703
|
+
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
704
|
+
constraint({ kind: 'mutually_exclusive', options: ['--summary', '--verbose'] }),
|
|
705
|
+
constraint({ kind: 'requires_all', option: '--save-lookup', requiredOptions: ['--browse'] }),
|
|
706
|
+
constraint({ kind: 'excludes_all', option: '--summary', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
|
|
707
|
+
constraint({ kind: 'excludes_all', option: '--verbose', excludedOptions: ['--json', '--junit', '--markdown', '--html'] }),
|
|
708
|
+
constraint({ kind: 'excludes_all', option: '--browse', excludedOptions: ['--json', '--junit', '--markdown', '--html', '--summary', '--verbose', '--events', '--plan', '--quiet'] }),
|
|
709
|
+
constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
|
|
710
|
+
constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--junit', '--markdown', '--html', '--summary', '--verbose', '--strict-exit', '--events', '--quiet', '--fail-on'] }),
|
|
711
|
+
]),
|
|
712
|
+
bulk: Object.freeze([
|
|
713
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--junit', '--csv', '--domains', '--queries'] }),
|
|
714
|
+
constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--errors-only'] }),
|
|
715
|
+
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
716
|
+
constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
|
|
717
|
+
constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--junit', '--csv', '--domains', '--queries', '--events', '--checkpoint', '--resume', '--quiet', '--fail-on'] }),
|
|
718
|
+
]),
|
|
719
|
+
discover: Object.freeze([
|
|
720
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--domains'] }),
|
|
721
|
+
constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
|
|
722
|
+
constraint({ kind: 'value_excludes', option: '--preset', value: 'common', excludedOptions: ['--dictionary'] }),
|
|
723
|
+
]),
|
|
724
|
+
'discover-scan': Object.freeze([
|
|
725
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--jsonl', '--csv', '--domains'] }),
|
|
726
|
+
constraint({ kind: 'mutually_exclusive', options: ['--preset', '--families'] }),
|
|
727
|
+
constraint({ kind: 'mutually_exclusive', options: ['--fast', '--deep'] }),
|
|
728
|
+
constraint({ kind: 'mutually_exclusive', options: ['--registered-only', '--inconclusive-only', '--acquisition-only', '--suppressed-only'] }),
|
|
729
|
+
constraint({ kind: 'requires_all', option: '--resume', requiredOptions: ['--checkpoint'] }),
|
|
730
|
+
constraint({ kind: 'value_excludes', option: '--preset', value: 'common', excludedOptions: ['--dictionary'] }),
|
|
731
|
+
constraint({ kind: 'excludes_all', option: '--plan', excludedOptions: ['--jsonl', '--csv', '--domains', '--events', '--checkpoint', '--resume', '--observation-snapshot', '--quiet', '--fail-on'] }),
|
|
732
|
+
]),
|
|
733
|
+
posture: Object.freeze([
|
|
734
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--sarif'] }),
|
|
735
|
+
constraint({ kind: 'requires_all', option: '--sarif', requiredOptions: ['--owned-domain'] }),
|
|
736
|
+
]),
|
|
737
|
+
'dnssec-validate': Object.freeze([
|
|
738
|
+
constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized'] }),
|
|
739
|
+
]),
|
|
740
|
+
'mail-transport': Object.freeze([
|
|
741
|
+
constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe'] }),
|
|
742
|
+
]),
|
|
743
|
+
'registry-scaffold': Object.freeze([
|
|
744
|
+
constraint({ kind: 'required', options: ['--profile', '--suffix', '--scenario'] }),
|
|
745
|
+
]),
|
|
746
|
+
'risk-calibrate': Object.freeze([
|
|
747
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--summary-json'] }),
|
|
748
|
+
]),
|
|
749
|
+
'verify-artifact': Object.freeze([
|
|
750
|
+
constraint({ kind: 'requires_all', option: '--manifest', requiredOptions: ['--manifest-entry'] }),
|
|
751
|
+
constraint({ kind: 'requires_all', option: '--manifest-entry', requiredOptions: ['--manifest'] }),
|
|
752
|
+
]),
|
|
753
|
+
'inspect-archive': Object.freeze([
|
|
754
|
+
constraint({ kind: 'requires_all', option: '--reveal', requiredOptions: ['--search'] }),
|
|
755
|
+
constraint({ kind: 'requires_all', option: '--require-match', requiredOptions: ['--search'] }),
|
|
756
|
+
]),
|
|
757
|
+
'sign-artifact': Object.freeze([
|
|
758
|
+
constraint({ kind: 'required', options: ['--private-key-file'] }),
|
|
759
|
+
]),
|
|
760
|
+
'case-pack': Object.freeze([
|
|
761
|
+
constraint({ kind: 'required', options: ['--audience', '--reviewed'] }),
|
|
762
|
+
]),
|
|
763
|
+
'monitor-once': Object.freeze([
|
|
764
|
+
constraint({ kind: 'mutually_exclusive', options: ['--json', '--junit'] }),
|
|
765
|
+
]),
|
|
766
|
+
'sharing-review': Object.freeze([
|
|
767
|
+
constraint({ kind: 'required', options: ['--marking', '--recipient-scope', '--purpose'] }),
|
|
768
|
+
]),
|
|
769
|
+
'workflow-plan': Object.freeze([
|
|
770
|
+
constraint({ kind: 'mutually_exclusive', options: ['--list', '--explain'] }),
|
|
771
|
+
]),
|
|
772
|
+
export: Object.freeze([
|
|
773
|
+
constraint({ kind: 'mutually_exclusive', options: ['--markdown', '--html'] }),
|
|
774
|
+
constraint({ kind: 'excludes_all', option: '--compact', excludedOptions: ['--markdown', '--html'] }),
|
|
775
|
+
constraint({ kind: 'requires_any', option: '--no-attribution', requiredOptions: ['--markdown', '--html'] }),
|
|
776
|
+
]),
|
|
777
|
+
});
|
|
778
|
+
function optionValueKind(command, option) {
|
|
779
|
+
if (Object.hasOwn(VALUE_OPTIONS, option))
|
|
780
|
+
return option === '--fail-on' ? 'policy_list' : 'enum';
|
|
781
|
+
if (INTEGER_RANGE_SEED[command]?.[option])
|
|
782
|
+
return 'integer';
|
|
783
|
+
if (FILE_OPTIONS.includes(option) || FILE_OPTIONS_BY_COMMAND[command]?.includes(option))
|
|
784
|
+
return 'file';
|
|
785
|
+
if (TEXT_OPTIONS.includes(option))
|
|
786
|
+
return 'text';
|
|
787
|
+
return 'flag';
|
|
788
|
+
}
|
|
789
|
+
function optionSpec(command, option, scope) {
|
|
790
|
+
const valueKind = optionValueKind(command, option);
|
|
791
|
+
return Object.freeze({
|
|
792
|
+
option,
|
|
793
|
+
scope,
|
|
794
|
+
arity: valueKind === 'flag' ? 0 : 1,
|
|
795
|
+
valueKind,
|
|
796
|
+
values: Object.freeze([...(VALUE_OPTIONS[option] ?? [])]),
|
|
797
|
+
integerRanges: Object.freeze([...(INTEGER_RANGE_SEED[command]?.[option] ?? [])]),
|
|
798
|
+
occurrence: IDEMPOTENT_OPTIONS.includes(option) ? 'idempotent' : 'once',
|
|
799
|
+
metaAction: option === '--help' ? 'help' : null,
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
function grammarConstraints(command) {
|
|
803
|
+
const commandOptions = OPTIONS_BY_COMMAND_SEED[command];
|
|
804
|
+
const machineOutputOptions = commandOptions.filter((option) => MACHINE_OUTPUT_OPTIONS.includes(option));
|
|
805
|
+
return Object.freeze([
|
|
806
|
+
...FILE_OUTPUT_CONSTRAINTS,
|
|
807
|
+
...(commandOptions.includes('--quiet') ? [QUIET_OUTPUT_CONSTRAINT] : []),
|
|
808
|
+
...(commandOptions.includes('--quiet') && machineOutputOptions.length > 0
|
|
809
|
+
? [constraint({ kind: 'excludes_all', option: '--quiet', excludedOptions: machineOutputOptions })]
|
|
810
|
+
: []),
|
|
811
|
+
...(commandOptions.includes('--events')
|
|
812
|
+
? [constraint({ kind: 'mutually_exclusive', options: ['--events', '--output'] })]
|
|
813
|
+
: []),
|
|
814
|
+
...(commandOptions.includes('--browse')
|
|
815
|
+
? [constraint({ kind: 'mutually_exclusive', options: ['--browse', '--output'] })]
|
|
816
|
+
: []),
|
|
817
|
+
...(GRAMMAR_CONSTRAINTS_SEED[command] ?? EMPTY_CONSTRAINTS),
|
|
818
|
+
]);
|
|
819
|
+
}
|
|
820
|
+
const COMMAND_DESCRIPTIONS_SEED = Object.freeze({
|
|
821
|
+
manifest: 'Build an evidence manifest offline',
|
|
822
|
+
'map-observations': 'Apply a declarative observation map offline',
|
|
823
|
+
'oam-export': 'Project external findings to Open Asset Model',
|
|
824
|
+
lookup: 'Collect one domain, IP, or ASN',
|
|
825
|
+
bulk: 'Run bounded multi-target collection',
|
|
826
|
+
'ct-search': 'Search certificate observations',
|
|
827
|
+
'ct-intake': 'Normalise certificate observations offline',
|
|
828
|
+
discover: 'Generate lookalike candidates offline',
|
|
829
|
+
'discover-scan': 'Collect a supervised candidate review queue',
|
|
830
|
+
posture: 'Review DNS and mail posture',
|
|
831
|
+
http: 'Inspect one homepage request',
|
|
832
|
+
tls: 'Inspect one TLS connection',
|
|
833
|
+
'dnssec-validate': 'Validate an authorised DNSSEC chain',
|
|
834
|
+
'mail-transport': 'Review selected authorised SMTP transports',
|
|
835
|
+
'registry-support': 'Explain local registry coverage',
|
|
836
|
+
'registry-doctor': 'Diagnose saved registry collection',
|
|
837
|
+
'registry-cohort': 'Build target-free registry quality timelines',
|
|
838
|
+
'registry-scaffold': 'Create a sanitised registry fixture scaffold',
|
|
839
|
+
'risk-calibrate': 'Replay reviewed Risk labels offline',
|
|
840
|
+
'lookalike-calibrate': 'Summarise reviewed lookalike yield offline',
|
|
841
|
+
'verify-artifact': 'Validate saved evidence offline',
|
|
842
|
+
'interchange-report': 'Report portable artefact fidelity offline',
|
|
843
|
+
'inspect-archive': 'Inspect an archive locally',
|
|
844
|
+
'sign-artifact': 'Sign a reviewed artefact locally',
|
|
845
|
+
'verify-signature': 'Verify a signed evidence package',
|
|
846
|
+
'source-report': 'Build a target-free source report',
|
|
847
|
+
compare: 'Compare registry publications in one lookup',
|
|
848
|
+
'page-compare': 'Compare saved static page evidence',
|
|
849
|
+
'mail-review': 'Review saved passive mail evidence',
|
|
850
|
+
'review-evidence': 'Review supplied evidence offline',
|
|
851
|
+
brief: 'Build a decision brief from a saved lookup',
|
|
852
|
+
'case-pack': 'Build a reviewed case package',
|
|
853
|
+
'domain-control': 'Build or review a domain control manifest',
|
|
854
|
+
'monitor-once': 'Run one bounded domain control review',
|
|
855
|
+
assurance: 'Review domain change, recovery, or retirement plans',
|
|
856
|
+
'change-packet': 'Build a reviewed change packet offline',
|
|
857
|
+
'sharing-review': 'Lint an artefact before deliberate sharing',
|
|
858
|
+
'workflow-plan': 'Plan a fixed investigation recipe',
|
|
859
|
+
'workflow-run': 'Execute approved fixed-recipe steps',
|
|
860
|
+
diff: 'Compare two compatible retained artefacts',
|
|
861
|
+
reconcile: 'Reconcile independently labelled observations',
|
|
862
|
+
timeline: 'Build same-domain history from saved lookups',
|
|
863
|
+
export: 'Convert a lookup to an evidence report',
|
|
864
|
+
completion: 'Print shell completion',
|
|
865
|
+
commands: 'List installed command contracts',
|
|
866
|
+
doctor: 'Check the local CLI runtime',
|
|
867
|
+
manual: 'Print the generated manual page',
|
|
868
|
+
});
|
|
869
|
+
const HELP_COMMANDS_BY_GROUP = Object.freeze({
|
|
870
|
+
investigate: Object.freeze([
|
|
871
|
+
'lookup', 'bulk', 'ct-search', 'ct-intake', 'discover', 'discover-scan',
|
|
872
|
+
'posture', 'http', 'tls', 'registry-support', 'registry-doctor',
|
|
873
|
+
'registry-cohort', 'source-report', 'compare', 'page-compare', 'mail-review',
|
|
874
|
+
'review-evidence', 'brief',
|
|
875
|
+
]),
|
|
876
|
+
respond: Object.freeze([
|
|
877
|
+
'map-observations', 'oam-export', 'case-pack', 'change-packet',
|
|
878
|
+
'sharing-review', 'export',
|
|
879
|
+
]),
|
|
880
|
+
assure: Object.freeze([
|
|
881
|
+
'dnssec-validate', 'mail-transport', 'domain-control', 'monitor-once',
|
|
882
|
+
'assurance', 'workflow-plan', 'workflow-run', 'diff', 'reconcile', 'timeline',
|
|
883
|
+
'inspect-archive', 'verify-artifact', 'interchange-report', 'manifest',
|
|
884
|
+
'sign-artifact', 'verify-signature', 'risk-calibrate', 'lookalike-calibrate',
|
|
885
|
+
]),
|
|
886
|
+
utilities: Object.freeze([
|
|
887
|
+
'registry-scaffold', 'doctor', 'commands', 'completion', 'manual',
|
|
888
|
+
]),
|
|
889
|
+
});
|
|
890
|
+
const HELP_GROUP_BY_COMMAND = Object.freeze(Object.fromEntries(Object.entries(HELP_COMMANDS_BY_GROUP).flatMap(([group, commands]) => (commands.map((command) => [command, group])))));
|
|
891
|
+
const HANDLER_OWNER_BY_COMMAND = Object.freeze({
|
|
892
|
+
completion: 'inline',
|
|
893
|
+
doctor: 'inline',
|
|
894
|
+
commands: 'inline',
|
|
895
|
+
manual: 'inline',
|
|
896
|
+
manifest: 'inline',
|
|
897
|
+
'map-observations': 'inline',
|
|
898
|
+
'oam-export': 'inline',
|
|
899
|
+
lookup: 'lookup',
|
|
900
|
+
bulk: 'bulk',
|
|
901
|
+
'ct-search': 'network',
|
|
902
|
+
'ct-intake': 'inline',
|
|
903
|
+
discover: 'discovery',
|
|
904
|
+
'discover-scan': 'discovery_scan',
|
|
905
|
+
posture: 'network',
|
|
906
|
+
http: 'network',
|
|
907
|
+
tls: 'network',
|
|
908
|
+
'dnssec-validate': 'network',
|
|
909
|
+
'mail-transport': 'network',
|
|
910
|
+
'registry-support': 'inline',
|
|
911
|
+
'registry-doctor': 'inline',
|
|
912
|
+
'registry-cohort': 'inline',
|
|
913
|
+
'registry-scaffold': 'inline',
|
|
914
|
+
'risk-calibrate': 'inline',
|
|
915
|
+
'lookalike-calibrate': 'inline',
|
|
916
|
+
'verify-artifact': 'inline',
|
|
917
|
+
'interchange-report': 'inline',
|
|
918
|
+
'inspect-archive': 'evidence',
|
|
919
|
+
'sign-artifact': 'evidence',
|
|
920
|
+
'verify-signature': 'evidence',
|
|
921
|
+
'source-report': 'inline',
|
|
922
|
+
compare: 'inline',
|
|
923
|
+
'page-compare': 'inline',
|
|
924
|
+
'mail-review': 'inline',
|
|
925
|
+
'review-evidence': 'inline',
|
|
926
|
+
brief: 'inline',
|
|
927
|
+
'case-pack': 'inline',
|
|
928
|
+
'domain-control': 'inline',
|
|
929
|
+
'monitor-once': 'inline',
|
|
930
|
+
assurance: 'inline',
|
|
931
|
+
'change-packet': 'inline',
|
|
932
|
+
'sharing-review': 'inline',
|
|
933
|
+
'workflow-plan': 'inline',
|
|
934
|
+
'workflow-run': 'inline',
|
|
935
|
+
diff: 'inline',
|
|
936
|
+
reconcile: 'inline',
|
|
937
|
+
timeline: 'inline',
|
|
938
|
+
export: 'inline',
|
|
939
|
+
});
|
|
940
|
+
const NETWORK_EFFECT_BY_COMMAND = Object.freeze({
|
|
941
|
+
completion: 'offline',
|
|
942
|
+
doctor: 'conditional_network',
|
|
943
|
+
commands: 'offline',
|
|
944
|
+
manual: 'offline',
|
|
945
|
+
manifest: 'offline',
|
|
946
|
+
'map-observations': 'offline',
|
|
947
|
+
'oam-export': 'offline',
|
|
948
|
+
lookup: 'conditional_network',
|
|
949
|
+
bulk: 'conditional_network',
|
|
950
|
+
'ct-search': 'always_network',
|
|
951
|
+
'ct-intake': 'offline',
|
|
952
|
+
discover: 'offline',
|
|
953
|
+
'discover-scan': 'conditional_network',
|
|
954
|
+
posture: 'always_network',
|
|
955
|
+
http: 'always_network',
|
|
956
|
+
tls: 'always_network',
|
|
957
|
+
'dnssec-validate': 'always_network',
|
|
958
|
+
'mail-transport': 'always_network',
|
|
959
|
+
'registry-support': 'offline',
|
|
960
|
+
'registry-doctor': 'offline',
|
|
961
|
+
'registry-cohort': 'offline',
|
|
962
|
+
'registry-scaffold': 'offline',
|
|
963
|
+
'risk-calibrate': 'offline',
|
|
964
|
+
'lookalike-calibrate': 'offline',
|
|
965
|
+
'verify-artifact': 'offline',
|
|
966
|
+
'interchange-report': 'offline',
|
|
967
|
+
'inspect-archive': 'offline',
|
|
968
|
+
'sign-artifact': 'offline',
|
|
969
|
+
'verify-signature': 'offline',
|
|
970
|
+
'source-report': 'offline',
|
|
971
|
+
compare: 'offline',
|
|
972
|
+
'page-compare': 'offline',
|
|
973
|
+
'mail-review': 'offline',
|
|
974
|
+
'review-evidence': 'offline',
|
|
975
|
+
brief: 'offline',
|
|
976
|
+
'case-pack': 'offline',
|
|
977
|
+
'domain-control': 'offline',
|
|
978
|
+
'monitor-once': 'always_network',
|
|
979
|
+
assurance: 'offline',
|
|
980
|
+
'change-packet': 'offline',
|
|
981
|
+
'sharing-review': 'offline',
|
|
982
|
+
'workflow-plan': 'offline',
|
|
983
|
+
'workflow-run': 'conditional_network',
|
|
984
|
+
diff: 'offline',
|
|
985
|
+
reconcile: 'offline',
|
|
986
|
+
timeline: 'offline',
|
|
987
|
+
export: 'offline',
|
|
988
|
+
});
|
|
989
|
+
const COMMON_COMMANDS = Object.freeze([
|
|
990
|
+
'doctor', 'commands', 'lookup', 'bulk', 'discover', 'discover-scan', 'verify-artifact',
|
|
991
|
+
'review-evidence', 'case-pack', 'workflow-plan', 'diff', 'export',
|
|
992
|
+
]);
|
|
993
|
+
const SCHEMA_IDENTIFIERS_BY_COMMAND = Object.freeze({
|
|
994
|
+
commands: Object.freeze(['whoisleuth.cli.command-catalogue']),
|
|
995
|
+
doctor: Object.freeze(['whoisleuth.cli.doctor']),
|
|
996
|
+
manifest: Object.freeze(['whoisleuth.investigation-manifest']),
|
|
997
|
+
'map-observations': Object.freeze(['whoisleuth.external-observation-mapping']),
|
|
998
|
+
'oam-export': Object.freeze(['whoisleuth.open-asset-model-bridge']),
|
|
999
|
+
lookup: Object.freeze(['whoisleuth.cli.lookup', 'whoisleuth.cli.lookup-plan']),
|
|
1000
|
+
bulk: Object.freeze(['whoisleuth.cli.bulk', 'whoisleuth.cli.bulk.item', 'whoisleuth.cli.bulk-checkpoint']),
|
|
1001
|
+
'ct-search': Object.freeze(['whoisleuth.cli.ct-search']),
|
|
1002
|
+
'ct-intake': Object.freeze(['whoisleuth.ct-event-batch', 'whoisleuth.external-findings']),
|
|
1003
|
+
discover: Object.freeze(['whoisleuth.cli.discover', 'whoisleuth.cli.discover.item', 'whoisleuth.cli.discovery-snapshot']),
|
|
1004
|
+
'discover-scan': Object.freeze(['whoisleuth.cli.discovery-scan', 'whoisleuth.cli.discovery-scan.item', 'whoisleuth.cli.discovery-observation-snapshot']),
|
|
1005
|
+
posture: Object.freeze(['whoisleuth.cli.posture']),
|
|
1006
|
+
http: Object.freeze(['whoisleuth.cli.http']),
|
|
1007
|
+
tls: Object.freeze(['whoisleuth.cli.tls']),
|
|
1008
|
+
'dnssec-validate': Object.freeze(['whoisleuth.dnssec-chain-validation', 'whoisleuth.dnssec-trust-anchor']),
|
|
1009
|
+
'mail-transport': Object.freeze(['whoisleuth.mail-transport.input', 'whoisleuth.cli.mail-transport-review']),
|
|
1010
|
+
'registry-support': Object.freeze(['whoisleuth.cli.registry-support', 'whoisleuth.registry-standards-coverage']),
|
|
1011
|
+
'registry-doctor': Object.freeze(['whoisleuth.cli.registry-doctor']),
|
|
1012
|
+
'registry-cohort': Object.freeze(['whoisleuth.cli.registry-cohort']),
|
|
1013
|
+
'risk-calibrate': Object.freeze([RISK_CALIBRATION_DATASET_SCHEMA, RISK_CALIBRATION_REPORT_SCHEMA]),
|
|
1014
|
+
'lookalike-calibrate': Object.freeze(['whoisleuth.lookalike-calibration-input', 'whoisleuth.lookalike-calibration']),
|
|
1015
|
+
'verify-artifact': Object.freeze(['whoisleuth.offline-artifact-verification']),
|
|
1016
|
+
'interchange-report': Object.freeze(['whoisleuth.interchange-fidelity-report']),
|
|
1017
|
+
'inspect-archive': Object.freeze(['whoisleuth.workspace-archive-inspection']),
|
|
1018
|
+
'sign-artifact': Object.freeze(['whoisleuth.signed-evidence-package']),
|
|
1019
|
+
'verify-signature': Object.freeze(['whoisleuth.evidence-signature-verification']),
|
|
1020
|
+
'source-report': Object.freeze(['whoisleuth.source-reliability-report']),
|
|
1021
|
+
compare: Object.freeze(['whoisleuth.cli.compare']),
|
|
1022
|
+
'page-compare': Object.freeze(['whoisleuth.cli.page-compare']),
|
|
1023
|
+
'mail-review': Object.freeze(['whoisleuth.cli.mail-review']),
|
|
1024
|
+
'review-evidence': Object.freeze([
|
|
1025
|
+
'whoisleuth.cli.offline-evidence-review',
|
|
1026
|
+
'whoisleuth.rdap-search-input',
|
|
1027
|
+
'whoisleuth.dnssec-evidence-input',
|
|
1028
|
+
'whoisleuth.tlsa-evidence-input',
|
|
1029
|
+
'whoisleuth.rpki-route-input',
|
|
1030
|
+
'whoisleuth.local-geoip-query',
|
|
1031
|
+
'whoisleuth.encrypted-dns-plan-input',
|
|
1032
|
+
]),
|
|
1033
|
+
brief: Object.freeze(['whoisleuth.cli.lookup-brief']),
|
|
1034
|
+
'case-pack': Object.freeze(['whoisleuth.cli.case-pack', 'whoisleuth.case-report']),
|
|
1035
|
+
'domain-control': Object.freeze(['whoisleuth.cli.domain-control-review-input', 'whoisleuth.cli.domain-control-review']),
|
|
1036
|
+
'monitor-once': Object.freeze(['whoisleuth.cli.domain-control-monitor', 'whoisleuth.domain-control-flight-recorder.input']),
|
|
1037
|
+
assurance: Object.freeze(['whoisleuth.domain-assurance.input', 'whoisleuth.domain-assurance']),
|
|
1038
|
+
'change-packet': Object.freeze(['whoisleuth.domain-change-packet.input', 'whoisleuth.domain-change-packet']),
|
|
1039
|
+
'sharing-review': Object.freeze(['whoisleuth.cli.sharing-review']),
|
|
1040
|
+
'workflow-plan': Object.freeze(['whoisleuth.cli.investigation-plan', 'whoisleuth.cli.workflow-recipe-catalogue']),
|
|
1041
|
+
'workflow-run': Object.freeze(['whoisleuth.cli.investigation-run']),
|
|
1042
|
+
diff: Object.freeze(['whoisleuth.cli.lookup-diff']),
|
|
1043
|
+
reconcile: Object.freeze(['whoisleuth.cli.lookup-reconciliation']),
|
|
1044
|
+
timeline: Object.freeze(['whoisleuth.cli.lookup-timeline']),
|
|
1045
|
+
export: Object.freeze(['whoisleuth.lookup-evidence']),
|
|
1046
|
+
});
|
|
1047
|
+
const PRIMARY_ARTEFACTS_BY_COMMAND = Object.freeze({
|
|
1048
|
+
lookup: Object.freeze(['Source-qualified Lookup', 'Lookup request plan']),
|
|
1049
|
+
bulk: Object.freeze(['Bulk result', 'Bulk checkpoint']),
|
|
1050
|
+
discover: Object.freeze(['Candidate set', 'Discovery snapshot']),
|
|
1051
|
+
'discover-scan': Object.freeze(['Reviewed candidate queue', 'Observation snapshot']),
|
|
1052
|
+
'verify-artifact': Object.freeze(['Offline verification report']),
|
|
1053
|
+
'case-pack': Object.freeze(['Reviewed Case-pack v2']),
|
|
1054
|
+
'workflow-plan': Object.freeze(['Plan-only workflow document']),
|
|
1055
|
+
'workflow-run': Object.freeze(['Resumable workflow state']),
|
|
1056
|
+
diff: Object.freeze(['Retained-evidence comparison']),
|
|
1057
|
+
timeline: Object.freeze(['Bounded retained-observation timeline']),
|
|
1058
|
+
export: Object.freeze(['Portable evidence report']),
|
|
1059
|
+
});
|
|
1060
|
+
function documentationMetadata(command, commandOptions, positionals) {
|
|
1061
|
+
const effect = NETWORK_EFFECT_BY_COMMAND[command];
|
|
1062
|
+
const explicitAuthorisationRequired = commandOptions.some((option) => (option === '--owned-or-authorized' || option === '--active-probe' || option === '--approve-network'));
|
|
1063
|
+
const outputOptionFormats = [
|
|
1064
|
+
['--json', 'JSON'], ['--jsonl', 'JSON Lines'], ['--junit', 'JUnit XML'],
|
|
1065
|
+
['--csv', 'CSV'], ['--domains', 'domain list'], ['--queries', 'query list'],
|
|
1066
|
+
['--markdown', 'Markdown'], ['--html', 'HTML'], ['--sarif', 'SARIF'],
|
|
1067
|
+
['--summary-json', 'summary JSON'],
|
|
1068
|
+
];
|
|
1069
|
+
const outputFormats = new Set(['terminal']);
|
|
1070
|
+
for (const [option, label] of outputOptionFormats) {
|
|
1071
|
+
if (commandOptions.includes(option))
|
|
1072
|
+
outputFormats.add(label);
|
|
1073
|
+
}
|
|
1074
|
+
if (command === 'export')
|
|
1075
|
+
outputFormats.add('JSON');
|
|
1076
|
+
const positionalLimits = positionals.map((item) => (`${item.name}: ${item.minimum}-${item.maximum} ${item.valueKind} value${item.maximum === 1 ? '' : 's'}`));
|
|
1077
|
+
return Object.freeze({
|
|
1078
|
+
common: COMMON_COMMANDS.includes(command),
|
|
1079
|
+
disclosureClass: effect === 'offline'
|
|
1080
|
+
? 'none'
|
|
1081
|
+
: explicitAuthorisationRequired
|
|
1082
|
+
? 'bounded_authorised_active'
|
|
1083
|
+
: effect === 'conditional_network'
|
|
1084
|
+
? 'conditional_bounded_passive'
|
|
1085
|
+
: 'bounded_passive',
|
|
1086
|
+
explicitAuthorisationRequired,
|
|
1087
|
+
planSupport: commandOptions.includes('--plan') || command === 'workflow-plan',
|
|
1088
|
+
failurePolicySupport: commandOptions.includes('--fail-on') || commandOptions.includes('--strict-exit'),
|
|
1089
|
+
supportedSchemaIdentifiers: Object.freeze([...(SCHEMA_IDENTIFIERS_BY_COMMAND[command] ?? [])]),
|
|
1090
|
+
inputLimits: Object.freeze([COMMAND_COLLECTION_SEED[command].scope, ...positionalLimits]),
|
|
1091
|
+
outputLimits: Object.freeze([
|
|
1092
|
+
'Output is bounded by the command-owned formatter and document contract.',
|
|
1093
|
+
...(commonOptionsSeedForCommand(command).includes('--output')
|
|
1094
|
+
? ['Selected file output is atomic and replacement requires --force.']
|
|
1095
|
+
: []),
|
|
1096
|
+
]),
|
|
1097
|
+
outputFormats: Object.freeze([...outputFormats]),
|
|
1098
|
+
primaryEvidenceArtefacts: Object.freeze([...(PRIMARY_ARTEFACTS_BY_COMMAND[command] ?? [])]),
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
const CLI_COMMAND_REGISTRY = Object.freeze(COMMAND_ORDER.map((command, order) => {
|
|
1102
|
+
const commonOptions = Object.freeze([...commonOptionsSeedForCommand(command)]);
|
|
1103
|
+
const commandOptions = Object.freeze([...OPTIONS_BY_COMMAND_SEED[command]]);
|
|
1104
|
+
const grammarOptions = Object.freeze([
|
|
1105
|
+
...commonOptions.map((option) => optionSpec(command, option, 'common')),
|
|
1106
|
+
...commandOptions.map((option) => optionSpec(command, option, 'command')),
|
|
1107
|
+
]);
|
|
1108
|
+
return Object.freeze({
|
|
1109
|
+
command,
|
|
1110
|
+
order,
|
|
1111
|
+
reference: Object.freeze({
|
|
1112
|
+
usage: COMMAND_USAGE_SEED[command],
|
|
1113
|
+
...COMMAND_DETAILS_SEED[command],
|
|
1114
|
+
}),
|
|
1115
|
+
collection: Object.freeze({ ...COMMAND_COLLECTION_SEED[command] }),
|
|
1116
|
+
completion: Object.freeze({
|
|
1117
|
+
description: COMMAND_DESCRIPTIONS_SEED[command],
|
|
1118
|
+
commonOptions,
|
|
1119
|
+
options: commandOptions,
|
|
1120
|
+
}),
|
|
1121
|
+
grammar: Object.freeze({
|
|
1122
|
+
parserKey: command,
|
|
1123
|
+
bootstrapProfile: command === 'registry-scaffold' ? 'command_owned' : 'allowed',
|
|
1124
|
+
options: grammarOptions,
|
|
1125
|
+
positionals: POSITIONALS_BY_COMMAND_SEED[command],
|
|
1126
|
+
constraints: grammarConstraints(command),
|
|
1127
|
+
metaActions: Object.freeze(['help']),
|
|
1128
|
+
}),
|
|
1129
|
+
execution: Object.freeze({
|
|
1130
|
+
handlerOwner: HANDLER_OWNER_BY_COMMAND[command],
|
|
1131
|
+
networkEffect: NETWORK_EFFECT_BY_COMMAND[command],
|
|
1132
|
+
}),
|
|
1133
|
+
help: Object.freeze({
|
|
1134
|
+
group: HELP_GROUP_BY_COMMAND[command],
|
|
1135
|
+
summary: COMMAND_DESCRIPTIONS_SEED[command],
|
|
1136
|
+
}),
|
|
1137
|
+
documentation: documentationMetadata(command, commandOptions, POSITIONALS_BY_COMMAND_SEED[command]),
|
|
1138
|
+
});
|
|
1139
|
+
}));
|
|
1140
|
+
const CLI_COMMANDS = Object.freeze(CLI_COMMAND_REGISTRY.map((definition) => definition.command));
|
|
1141
|
+
const FILE_POSITIONAL_COMMANDS = Object.freeze(CLI_COMMAND_REGISTRY
|
|
1142
|
+
.filter((definition) => definition.grammar.positionals.some((item) => item.valueKind === 'file'))
|
|
1143
|
+
.map((definition) => definition.command));
|
|
1144
|
+
const CLI_COMMAND_BY_NAME = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, definition])));
|
|
1145
|
+
function isCliCommand(value) {
|
|
1146
|
+
return typeof value === 'string' && Object.hasOwn(CLI_COMMAND_BY_NAME, value);
|
|
1147
|
+
}
|
|
1148
|
+
function commandDefinition(command) {
|
|
1149
|
+
return CLI_COMMAND_BY_NAME[command];
|
|
1150
|
+
}
|
|
1151
|
+
function metaActionDefinition(id) {
|
|
1152
|
+
return CLI_META_ACTION_BY_ID[id];
|
|
1153
|
+
}
|
|
1154
|
+
function cliMetaActionForInvocation(argv) {
|
|
1155
|
+
for (const action of CLI_META_ACTIONS) {
|
|
1156
|
+
const matches = action.scope === 'root_only'
|
|
1157
|
+
? action.aliases.includes(argv[0] ?? '')
|
|
1158
|
+
: argv.some((argument) => action.aliases.includes(argument));
|
|
1159
|
+
if (matches)
|
|
1160
|
+
return action;
|
|
1161
|
+
}
|
|
1162
|
+
return null;
|
|
1163
|
+
}
|
|
1164
|
+
const COMMAND_USAGE = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, definition.reference.usage])));
|
|
1165
|
+
const COMMAND_DETAILS = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, Object.freeze({
|
|
1166
|
+
description: definition.reference.description,
|
|
1167
|
+
example: definition.reference.example,
|
|
1168
|
+
boundary: definition.reference.boundary,
|
|
1169
|
+
})])));
|
|
1170
|
+
const COMMAND_COLLECTION = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, definition.collection])));
|
|
1171
|
+
const COMMAND_DESCRIPTIONS = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, definition.completion.description])));
|
|
1172
|
+
const OPTIONS_BY_COMMAND = Object.freeze(Object.fromEntries(CLI_COMMAND_REGISTRY.map((definition) => [definition.command, Object.freeze(definition.grammar.options
|
|
1173
|
+
.filter((option) => option.scope === 'command')
|
|
1174
|
+
.map((option) => option.option))])));
|
|
1175
|
+
function commonOptionsForCommand(command) {
|
|
1176
|
+
return Object.freeze(commandDefinition(command).grammar.options
|
|
1177
|
+
.filter((option) => option.scope === 'common')
|
|
1178
|
+
.map((option) => option.option));
|
|
1179
|
+
}
|
|
1180
|
+
function commandOptionSpec(command, option) {
|
|
1181
|
+
return commandDefinition(command).grammar.options.find((candidate) => candidate.option === option) ?? null;
|
|
1182
|
+
}
|
|
1183
|
+
function commandPositionalSpecs(command) {
|
|
1184
|
+
return commandDefinition(command).grammar.positionals;
|
|
1185
|
+
}
|
|
1186
|
+
const HELP_GROUP_LABELS = Object.freeze({
|
|
1187
|
+
investigate: 'Investigate',
|
|
1188
|
+
respond: 'Respond',
|
|
1189
|
+
assure: 'Assure',
|
|
1190
|
+
utilities: 'Utilities',
|
|
1191
|
+
});
|
|
1192
|
+
function renderRootHelpCommands() {
|
|
1193
|
+
return Object.keys(HELP_COMMANDS_BY_GROUP).map((group) => {
|
|
1194
|
+
const lines = HELP_COMMANDS_BY_GROUP[group].map((command) => {
|
|
1195
|
+
const definition = commandDefinition(command);
|
|
1196
|
+
return ` ${command.padEnd(20, ' ')} ${definition.help.summary}.`;
|
|
1197
|
+
});
|
|
1198
|
+
return `${HELP_GROUP_LABELS[group]}:\n${lines.join('\n')}`;
|
|
1199
|
+
}).join('\n\n');
|
|
1200
|
+
}
|
|
1201
|
+
const HELP = `${HELP_INTRO}\n${renderRootHelpCommands()}\n${HELP_FOOTER}`;
|
|
1202
|
+
function commandOwnsOption(command, option) {
|
|
1203
|
+
return commandDefinition(command).grammar.options.some((candidate) => candidate.option === option);
|
|
1204
|
+
}
|
|
1205
|
+
function invocationHasFlag(command, args, flag) {
|
|
1206
|
+
for (let index = 0; index < args.length; index += 1) {
|
|
1207
|
+
const argument = args[index];
|
|
1208
|
+
if (argument === flag)
|
|
1209
|
+
return true;
|
|
1210
|
+
const specification = argument ? commandOptionSpec(command, argument) : null;
|
|
1211
|
+
if (specification?.arity === 1)
|
|
1212
|
+
index += 1;
|
|
1213
|
+
}
|
|
1214
|
+
return false;
|
|
1215
|
+
}
|
|
1216
|
+
function cliInvocationNetworkEffect(command, args) {
|
|
1217
|
+
const effect = commandDefinition(command).execution.networkEffect;
|
|
1218
|
+
if (effect === 'offline')
|
|
1219
|
+
return 'offline';
|
|
1220
|
+
if (effect === 'always_network')
|
|
1221
|
+
return 'network';
|
|
1222
|
+
if (command === 'doctor')
|
|
1223
|
+
return invocationHasFlag(command, args, '--network') ? 'network' : 'offline';
|
|
1224
|
+
if (command === 'lookup' || command === 'bulk' || command === 'discover-scan') {
|
|
1225
|
+
return invocationHasFlag(command, args, '--plan') ? 'offline' : 'network';
|
|
1226
|
+
}
|
|
1227
|
+
if (command === 'workflow-run') {
|
|
1228
|
+
return invocationHasFlag(command, args, '--approve-network') ? 'network' : 'offline';
|
|
1229
|
+
}
|
|
1230
|
+
throw new Error(`Conditional network effect is not implemented for ${command}.`);
|
|
1231
|
+
}
|
|
390
1232
|
function commandHelp(command) {
|
|
391
1233
|
const detail = COMMAND_DETAILS[command];
|
|
392
1234
|
const collection = COMMAND_COLLECTION[command];
|
|
393
1235
|
return `WHOISleuth ${command}\n${detail.description}\n\nUsage:\n ${COMMAND_USAGE[command]}\n\nExample:\n ${detail.example}\n\nCollection:\n ${collection.mode === 'offline' ? 'Offline' : 'Network'}: ${collection.scope}\n\nBoundary:\n ${detail.boundary}\n\nRun "whoisleuth --help" to see the grouped command list.\n`;
|
|
394
1236
|
}
|
|
395
|
-
export { COMMAND_COLLECTION, COMMAND_DETAILS, COMMAND_USAGE, HELP, commandHelp };
|
|
1237
|
+
export { CLI_COMMAND_REGISTRY, CLI_COMMANDS, CLI_META_ACTIONS, COMMAND_COLLECTION, COMMAND_DESCRIPTIONS, COMMAND_DETAILS, COMMAND_USAGE, COMMON_OPTIONS, FILE_OPTIONS, FILE_OPTIONS_BY_COMMAND, FILE_POSITIONAL_COMMANDS, HELP, HELP_COMMANDS_BY_GROUP, INVESTIGATION_PLAN_RECIPES, RUNNABLE_INVESTIGATION_PLAN_RECIPES, LIMITED_CONCURRENCY_VALUES, OPTIONS_BY_COMMAND, STANDARD_CONCURRENCY_VALUES, TEXT_OPTIONS, VALUE_OPTIONS, cliMetaActionForInvocation, cliInvocationNetworkEffect, commandOptionSpec, commandOwnsOption, commandPositionalSpecs, commonOptionsForCommand, commandDefinition, commandHelp, isCliCommand, metaActionDefinition, };
|