@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +74 -177
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +2 -1
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +204 -174
- package/cli/artifact-structure.mjs +837 -163
- package/cli/artifact-verify.mjs +90 -61
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +33 -20
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +180 -576
- 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 +910 -92
- package/cli/completion.mjs +504 -226
- package/cli/config-profile.mjs +13 -3
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +9 -9
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/domain-control-monitor.mjs +203 -30
- package/cli/domain-control-observations.mjs +15 -21
- package/cli/errors.mjs +8 -3
- package/cli/evidence-signing.mjs +10 -20
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +583 -22
- package/cli/input.mjs +7 -2
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +19 -19
- package/cli/investigation-manifest.mjs +1 -1
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +9 -1
- 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 +94 -31
- package/cli/lookup-diff.mjs +2 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/offline-evidence-review.mjs +18 -7
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +11 -6
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +9 -51
- package/cli/registry-doctor.mjs +67 -28
- package/cli/retained-artifact-diff.mjs +7 -8
- package/cli/risk-calibration.mjs +85 -38
- package/cli/runner.mjs +189 -75
- package/cli/saved-lookup.mjs +424 -57
- package/cli/sharing-review.mjs +6 -1
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1708
- package/docs/cli.md +86 -254
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
- package/frontend/src/lib/analysis/domain-control-records.js +3 -88
- package/lib/availability.mjs +58 -40
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +78 -4
- package/lib/bounded-json.mjs +51 -13
- package/lib/browser-library-profile.mjs +1 -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 +35 -17
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +37 -21
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +1 -1
- package/lib/dnssec-chain-validation.mjs +2 -3
- package/lib/dnssec-evidence-validation.mjs +13 -7
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +1 -1
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -59
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +518 -36
- package/lib/external-intelligence-risk.mjs +26 -10
- 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-intelligence.mjs +331 -2
- package/lib/interchange-fidelity-registry.mjs +28 -26
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup.mjs +25 -2
- package/lib/observed-network-context.mjs +1 -1
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/portable-generator.mjs +8 -0
- package/lib/project-metadata.mjs +1 -1
- package/lib/provider-policy-admission.mjs +3 -1
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +306 -75
- 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-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +35 -33
- package/lib/risk-scoring.mjs +4 -3
- package/lib/safe-fetch.mjs +62 -40
- package/lib/security-txt.mjs +1 -1
- package/lib/smtp-transport-review.mjs +8 -5
- 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 +3 -5
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +4 -4
- 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 +13 -5
- package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
- package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
- 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 -21
- 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} +72 -29
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
- 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/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
- package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
- package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
- package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -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/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
- 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} +24 -22
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
- 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/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
- package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
- 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} +71 -48
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -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} +8 -45
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
- 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} +13 -13
- 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} +110 -89
- package/third-party-notices.txt +14 -14
- 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 -305
- package/frontend/src/lib/analysis/case-record-model.js +0 -9
- package/frontend/src/lib/analysis/case-response-model.js +0 -728
- package/frontend/src/lib/analysis/case-response-packet.js +0 -583
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
- 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 -255
- 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 -131
- package/frontend/src/lib/analysis/visualization-models.js +0 -338
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
- package/lib/bounded-json-response.mjs +0 -170
- package/lib/lookup-response-contract.mjs +0 -1034
- package/lib/observation.mjs +0 -115
- /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
- /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,101 +1,145 @@
|
|
|
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
|
-
dnssec-validate Validate one authorised DNSSEC chain in isolation.
|
|
18
|
-
mail-transport Review selected authorised MX transports and STARTTLS.
|
|
19
|
-
posture Review DNS mail and domain-control posture.
|
|
20
|
-
|
|
21
|
-
Discover:
|
|
22
|
-
ct-search Search certificate-transparency observations.
|
|
23
|
-
ct-intake Normalise a local certificate event batch for case import.
|
|
24
|
-
discover Generate offline lookalike candidates.
|
|
25
|
-
discover-scan Collect a bounded candidate review queue.
|
|
26
|
-
registry-support Explain local registry coverage without a request.
|
|
27
|
-
registry-doctor Diagnose a saved registry collection against local policy.
|
|
28
|
-
registry-cohort Build target-free registry quality timelines.
|
|
29
|
-
registry-scaffold Create a sanitised synthetic fixture scaffold.
|
|
30
|
-
|
|
31
|
-
Review saved evidence:
|
|
32
|
-
map-observations Apply a declarative profile to local observations.
|
|
33
|
-
oam-export Project external findings into an open asset graph.
|
|
34
|
-
source-report Summarise source reliability without retaining targets.
|
|
35
|
-
compare Compare saved registry publications.
|
|
36
|
-
page-compare Compare saved static page and TLS evidence.
|
|
37
|
-
mail-review Review saved passive mail exposure evidence.
|
|
38
|
-
review-evidence Review supplied DNS, routing, cryptographic, GeoIP, RDAP, or trust-store evidence offline.
|
|
39
|
-
brief Turn one saved Lookup into a bounded decision brief.
|
|
40
|
-
case-pack Build a reviewed browser-importable case package.
|
|
41
|
-
domain-control Build or review an integrity-protected desired-state manifest.
|
|
42
|
-
monitor-once Run one bounded domain-control review and checkpoint.
|
|
43
|
-
assurance Review change, recovery, concentration, or retirement plans.
|
|
44
|
-
change-packet Assemble a digest-protected domain change review packet.
|
|
45
|
-
sharing-review Lint a reviewed artifact before deliberate sharing.
|
|
46
|
-
workflow-plan Plan a fixed investigation recipe without executing it.
|
|
47
|
-
workflow-run Execute resumable approved steps from a fixed recipe.
|
|
48
|
-
diff Compare two compatible retained artefacts.
|
|
49
|
-
reconcile Reconcile independently labelled observations.
|
|
50
|
-
timeline Compare a sequence of observations for one domain.
|
|
51
|
-
export Convert a saved lookup into an evidence report.
|
|
52
|
-
inspect-archive Inspect a workspace archive, redacted by default.
|
|
53
|
-
verify-artifact Validate saved evidence or an integrity envelope offline.
|
|
54
|
-
interchange-report Report preserved and excluded field groups offline.
|
|
55
|
-
|
|
56
|
-
Integrity and calibration:
|
|
57
|
-
manifest Record ordered artefact and configuration digests.
|
|
58
|
-
sign-artifact Sign one reviewed packet or manifest locally.
|
|
59
|
-
verify-signature Verify one signed evidence package locally.
|
|
60
|
-
risk-calibrate Replay reviewed labels without changing the model.
|
|
61
|
-
lookalike-calibrate Summarise reviewed mutation-family yield without tuning generation.
|
|
62
|
-
|
|
63
|
-
Terminal:
|
|
64
|
-
doctor Check the local runtime; network tests require --network.
|
|
65
|
-
commands List command contracts for people or local tooling.
|
|
66
|
-
completion Print completion for bash, zsh, fish, or PowerShell.
|
|
67
|
-
manual Print the generated manual page.
|
|
68
|
-
|
|
101
|
+
`;
|
|
102
|
+
const HELP_FOOTER = `
|
|
69
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.
|
|
70
107
|
Use --json or --jsonl where supported for machine-readable stdout.
|
|
71
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.
|
|
72
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.
|
|
73
117
|
Diagnostics are written to stderr. Fast lookup is the default; deep collection
|
|
74
118
|
must be requested explicitly and can disclose a target to additional sources.
|
|
75
119
|
|
|
76
120
|
Copyright 2026 slicedearth. Licensed under AGPL-3.0-only.
|
|
77
121
|
Source and licence: ${WHOISLEUTH_SOURCE_REPOSITORY_URL}
|
|
78
122
|
`;
|
|
79
|
-
const
|
|
123
|
+
const COMMAND_USAGE_SEED = Object.freeze({
|
|
80
124
|
completion: 'whoisleuth completion <bash|zsh|fish|powershell>',
|
|
81
125
|
doctor: 'whoisleuth doctor [--network] [--json] [--quiet] [--no-color]',
|
|
82
|
-
commands: 'whoisleuth commands [--json] [--quiet] [--no-color]',
|
|
126
|
+
commands: 'whoisleuth commands [--common] [--group <group>] [--mode <offline|network>] [--json] [--quiet] [--no-color]',
|
|
83
127
|
manual: 'whoisleuth manual',
|
|
84
128
|
manifest: 'whoisleuth manifest <artefact.json> [...] --workflow <label> [--configuration-digest <sha256:digest>] [--json] [--quiet] [--no-color]',
|
|
85
129
|
'map-observations': 'whoisleuth map-observations [mapping.json] [--json] [--quiet] [--no-color]',
|
|
86
130
|
'oam-export': 'whoisleuth oam-export [external-findings.json] [--json] [--quiet] [--no-color]',
|
|
87
|
-
lookup: 'whoisleuth lookup
|
|
88
|
-
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>]',
|
|
89
|
-
'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]',
|
|
90
134
|
'ct-intake': 'whoisleuth ct-intake [events.json] [--json] [--quiet] [--no-color]',
|
|
91
|
-
discover: 'whoisleuth discover
|
|
92
|
-
'discover-scan': 'whoisleuth discover-scan
|
|
93
|
-
posture: 'whoisleuth posture
|
|
94
|
-
http: 'whoisleuth http
|
|
95
|
-
tls: 'whoisleuth tls
|
|
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]',
|
|
96
140
|
'dnssec-validate': 'whoisleuth dnssec-validate <domain> --resolver <public-IP> --trust-anchor <anchor.json> --owned-or-authorized [--json] [--quiet] [--no-color]',
|
|
97
141
|
'mail-transport': 'whoisleuth mail-transport [input.json] --resolver <public-IP> --trust-anchor <anchor.json> --owned-or-authorized --active-probe [--json] [--quiet] [--no-color]',
|
|
98
|
-
'registry-support': 'whoisleuth registry-support
|
|
142
|
+
'registry-support': 'whoisleuth registry-support [domain|suffix] [--json] [--quiet] [--no-color]',
|
|
99
143
|
'registry-doctor': 'whoisleuth registry-doctor [lookup.json] [--json] [--quiet] [--no-color]',
|
|
100
144
|
'registry-cohort': 'whoisleuth registry-cohort [lookups-or-reports.json|jsonl] [--json] [--quiet] [--no-color]',
|
|
101
145
|
'registry-scaffold': 'whoisleuth registry-scaffold --profile <id> --suffix <suffix> --scenario <registered|not_found|inconclusive>',
|
|
@@ -103,7 +147,7 @@ const COMMAND_USAGE = Object.freeze({
|
|
|
103
147
|
'lookalike-calibrate': 'whoisleuth lookalike-calibrate [dataset.json] [--json] [--quiet] [--no-color]',
|
|
104
148
|
'verify-artifact': 'whoisleuth verify-artifact [artifact.json] [--passphrase-file <file>] [--manifest <manifest.json> --manifest-entry <artifact-N>] [--json] [--strict-exit] [--quiet] [--no-color]',
|
|
105
149
|
'interchange-report': 'whoisleuth interchange-report [artifact.json] [--passphrase-file <file>] [--json] [--quiet] [--no-color]',
|
|
106
|
-
'inspect-archive': 'whoisleuth inspect-archive [archive.json] [--passphrase-file <file>] [--search <value>] [--require-match] [--reveal] [--expect-content-digest <sha256:digest>] [--json]',
|
|
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]',
|
|
107
151
|
'sign-artifact': 'whoisleuth sign-artifact [artifact.json] --private-key-file <file>',
|
|
108
152
|
'verify-signature': 'whoisleuth verify-signature [package.json] [--public-key-file <file>] [--json] [--quiet] [--no-color]',
|
|
109
153
|
'source-report': 'whoisleuth source-report [lookup.json] [--json] [--quiet] [--no-color]',
|
|
@@ -117,15 +161,15 @@ const COMMAND_USAGE = Object.freeze({
|
|
|
117
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]',
|
|
118
162
|
assurance: 'whoisleuth assurance [assurance-input.json] [--json] [--quiet] [--no-color]',
|
|
119
163
|
'change-packet': 'whoisleuth change-packet [change-packet-input.json] [--json] [--quiet] [--no-color]',
|
|
120
|
-
'sharing-review': 'whoisleuth sharing-review [artifact.json] --marking <level> --recipient-scope <scope> --purpose <text> [--human-reviewed] [--personal-data-reviewed] [--redactions-confirmed] [--json]',
|
|
121
|
-
'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]',
|
|
122
166
|
'workflow-run': 'whoisleuth workflow-run <recipe> <domain|brand> [--approve-network] [--resume <state.json>] [--json] [--quiet] [--no-color]',
|
|
123
167
|
diff: 'whoisleuth diff <left.json> <right.json> [--left-session <id> --right-session <id>] [--json] [--quiet] [--no-color]',
|
|
124
168
|
reconcile: 'whoisleuth reconcile <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
|
|
125
169
|
timeline: 'whoisleuth timeline <observation.json> <observation.json> [...] [--json] [--quiet] [--no-color]',
|
|
126
170
|
export: 'whoisleuth export [lookup.json] [--markdown|--html|--compact] [--no-attribution]',
|
|
127
171
|
});
|
|
128
|
-
const
|
|
172
|
+
const COMMAND_DETAILS_SEED = Object.freeze({
|
|
129
173
|
completion: {
|
|
130
174
|
description: 'Print a static shell-completion script for the installed CLI.',
|
|
131
175
|
example: 'whoisleuth completion zsh > ~/.zfunc/_whoisleuth',
|
|
@@ -163,8 +207,8 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
163
207
|
},
|
|
164
208
|
lookup: {
|
|
165
209
|
description: 'Collect registration evidence for one domain, IP, or ASN.',
|
|
166
|
-
example: 'whoisleuth lookup example.test --deep',
|
|
167
|
-
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.',
|
|
168
212
|
},
|
|
169
213
|
bulk: {
|
|
170
214
|
description: 'Triage newline-delimited domains, IPs, or ASNs with bounded concurrency.',
|
|
@@ -199,7 +243,7 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
199
243
|
http: {
|
|
200
244
|
description: 'Inspect one homepage request, redirects, and bounded response metadata.',
|
|
201
245
|
example: 'whoisleuth http example.test --json',
|
|
202
|
-
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.',
|
|
203
247
|
},
|
|
204
248
|
tls: {
|
|
205
249
|
description: 'Inspect one hostname certificate through a bounded TLS connection.',
|
|
@@ -234,7 +278,7 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
234
278
|
'registry-scaffold': {
|
|
235
279
|
description: 'Create a bounded synthetic WHOIS fixture scaffold for one existing capability profile.',
|
|
236
280
|
example: 'whoisleuth registry-scaffold --profile example-profile --suffix test --scenario registered',
|
|
237
|
-
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.',
|
|
238
282
|
},
|
|
239
283
|
'risk-calibrate': {
|
|
240
284
|
description: 'Replay reviewed labels against the current explainable Risk model.',
|
|
@@ -257,9 +301,9 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
257
301
|
boundary: 'The report is offline and metadata-only. It does not echo targets, contacts, notes, passphrases, evidence values, or an unrecognised schema string.',
|
|
258
302
|
},
|
|
259
303
|
'inspect-archive': {
|
|
260
|
-
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.`,
|
|
261
305
|
example: 'whoisleuth inspect-archive workspace.json --search example.test --json',
|
|
262
|
-
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.',
|
|
263
307
|
},
|
|
264
308
|
'sign-artifact': {
|
|
265
309
|
description: 'Sign one reviewed response packet or supported manifest with a local private key.',
|
|
@@ -267,9 +311,9 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
267
311
|
boundary: 'The command never creates, stores, or transmits keys. Key custody and signer identity remain the operator\'s responsibility.',
|
|
268
312
|
},
|
|
269
313
|
'verify-signature': {
|
|
270
|
-
description: 'Verify the cryptographic signature of one signed evidence package and report embedded-
|
|
314
|
+
description: 'Verify the cryptographic signature of one signed evidence package and report embedded-artefact assurance separately.',
|
|
271
315
|
example: 'whoisleuth verify-signature packet.signed.json --json',
|
|
272
|
-
boundary: 'A valid signature proves package consistency for the embedded key. It does not upgrade failed or unsupported embedded-
|
|
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.',
|
|
273
317
|
},
|
|
274
318
|
'source-report': {
|
|
275
319
|
description: 'Create a target-free reliability summary from a saved lookup.',
|
|
@@ -302,7 +346,7 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
302
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.',
|
|
303
347
|
},
|
|
304
348
|
'case-pack': {
|
|
305
|
-
description:
|
|
349
|
+
description: `Build a reviewed, audience-specific Case-pack v2 from an exact Case-schema-${CASE_SCHEMA_VERSION} export.`,
|
|
306
350
|
example: 'whoisleuth case-pack cases.json --audience trusted --reviewed --json',
|
|
307
351
|
boundary: 'The command is offline, creates a new package, never mutates the source archive, and requires an explicit review acknowledgement.',
|
|
308
352
|
},
|
|
@@ -359,10 +403,10 @@ const COMMAND_DETAILS = Object.freeze({
|
|
|
359
403
|
export: {
|
|
360
404
|
description: 'Convert one saved lookup into a versioned evidence report.',
|
|
361
405
|
example: 'whoisleuth export lookup.json --markdown',
|
|
362
|
-
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.`,
|
|
363
407
|
},
|
|
364
408
|
});
|
|
365
|
-
const
|
|
409
|
+
const COMMAND_COLLECTION_SEED = Object.freeze({
|
|
366
410
|
completion: { mode: 'offline', scope: 'Prints one static script and changes no shell configuration.' },
|
|
367
411
|
doctor: { mode: 'network', scope: 'Network access is opt-in with --network and is limited to fixed public DNS, HTTPS, and WHOIS diagnostics.' },
|
|
368
412
|
commands: { mode: 'offline', scope: 'Reads the embedded command catalogue and performs no collection.' },
|
|
@@ -376,7 +420,7 @@ const COMMAND_COLLECTION = Object.freeze({
|
|
|
376
420
|
'ct-intake': { mode: 'offline', scope: 'Reads one source-qualified event batch capped at 4 MiB and makes no request.' },
|
|
377
421
|
discover: { mode: 'offline', scope: 'Generates a bounded candidate set from local rules, dictionaries, and optional saved snapshots.' },
|
|
378
422
|
'discover-scan': { mode: 'network', scope: 'Scans at most 500 fast or 50 deep candidates, with concurrency capped at 8 fast or 3 deep.' },
|
|
379
|
-
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.' },
|
|
380
424
|
http: { mode: 'network', scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
|
|
381
425
|
tls: { mode: 'network', scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
|
|
382
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.' },
|
|
@@ -389,7 +433,7 @@ const COMMAND_COLLECTION = Object.freeze({
|
|
|
389
433
|
'lookalike-calibrate': { mode: 'offline', scope: 'Reads at most 5,000 reviewed candidate labels from one dataset capped at 2 MiB.' },
|
|
390
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.' },
|
|
391
435
|
'interchange-report': { mode: 'offline', scope: 'Reads one selected bounded portable artefact and emits fixed compatibility metadata only.' },
|
|
392
|
-
'inspect-archive': { mode: 'offline', scope:
|
|
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.` },
|
|
393
437
|
'sign-artifact': { mode: 'offline', scope: 'Reads one selected artefact and one local private key without transmitting either.' },
|
|
394
438
|
'verify-signature': { mode: 'offline', scope: 'Reads one selected signed package and optional local public key.' },
|
|
395
439
|
'source-report': { mode: 'offline', scope: 'Reads bounded saved evidence and emits target-free source reliability data.' },
|
|
@@ -397,8 +441,8 @@ const COMMAND_COLLECTION = Object.freeze({
|
|
|
397
441
|
'page-compare': { mode: 'offline', scope: 'Reads two saved Lookup documents and executes no page code.' },
|
|
398
442
|
'mail-review': { mode: 'offline', scope: 'Reads one saved Bulk result and sends no DNS or SMTP traffic.' },
|
|
399
443
|
'review-evidence': { mode: 'offline', scope: 'Reads one bounded versioned evidence or request-planning document and performs no collection.' },
|
|
400
|
-
brief: { mode: 'offline', scope: 'Reads one bounded saved Lookup and emits a compact source-
|
|
401
|
-
'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.` },
|
|
402
446
|
'domain-control': { mode: 'offline', scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
|
|
403
447
|
'monitor-once': { mode: 'network', scope: 'Runs deep collection for at most 20 manifest domains with concurrency capped at 3.' },
|
|
404
448
|
assurance: { mode: 'offline', scope: 'Reads one versioned plan capped at 2 MiB and makes no request or configuration change.' },
|
|
@@ -411,9 +455,783 @@ const COMMAND_COLLECTION = Object.freeze({
|
|
|
411
455
|
timeline: { mode: 'offline', scope: 'Reads 2 to 20 saved observations for one domain, capped at 32 MiB in total.' },
|
|
412
456
|
export: { mode: 'offline', scope: 'Reads one saved Lookup and writes one bounded report.' },
|
|
413
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
|
+
}
|
|
414
1232
|
function commandHelp(command) {
|
|
415
1233
|
const detail = COMMAND_DETAILS[command];
|
|
416
1234
|
const collection = COMMAND_COLLECTION[command];
|
|
417
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`;
|
|
418
1236
|
}
|
|
419
|
-
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, };
|