@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
package/cli/runner.mjs
CHANGED
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
-
import { createReadStream } from 'node:fs';
|
|
3
2
|
import { createRequire } from 'node:module';
|
|
4
3
|
import { abortable } from "../lib/abort.mjs";
|
|
4
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
5
5
|
import { REGISTRY_CAPABILITIES_VERSION, registryCapabilityFor } from "../lib/registry-capabilities.mjs";
|
|
6
6
|
import { explainRiskScore, explainRiskScoreV6, RISK_MODEL_VERSION, RISK_REVIEW_THRESHOLD } from "../lib/risk-scoring.mjs";
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import {
|
|
7
|
+
import { buildRiskCalibrationSummaryReport } from "../lib/risk-calibration-summary.mjs";
|
|
8
|
+
import { parseCliArguments } from "./arguments.mjs";
|
|
9
|
+
import { buildCliCommandCatalogue, formatCliCommandCatalogue, selectCliCommands } from "./command-catalogue.mjs";
|
|
10
|
+
import { COMMAND_COLLECTION, COMMAND_DETAILS, COMMAND_USAGE, HELP, CLI_COMMANDS, CLI_COMMAND_REGISTRY, commandDefinition, commandHelp, } from "./command-reference.mjs";
|
|
10
11
|
import { buildShellCompletion } from "./completion.mjs";
|
|
11
12
|
import { buildDoctorReport, formatDoctorReport } from "./doctor.mjs";
|
|
12
|
-
import { MAX_COMPARE_INPUT_BYTES, compareLookupDocument, parseCliLookupDocument,
|
|
13
|
+
import { MAX_COMPARE_INPUT_BYTES, compareLookupDocument, parseCliLookupDocument, } from "./compare.mjs";
|
|
13
14
|
import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
|
|
14
15
|
import { evidenceCommandFailureLabel, isEvidenceCommand, runEvidenceCommand, } from "./evidence-command-runner.mjs";
|
|
15
16
|
import { buildCliEvidenceExport, formatCliEvidenceExport } from "./export-evidence.mjs";
|
|
@@ -18,7 +19,7 @@ import { formatLookupEvidenceHtml } from "./formatters/html.mjs";
|
|
|
18
19
|
import { buildCliCompareDocument, formatJsonDocument, } from "./formatters/json.mjs";
|
|
19
20
|
import { formatLookupEvidenceMarkdown } from "./formatters/markdown.mjs";
|
|
20
21
|
import { formatTerminalCompare, formatTerminalRegistrySupport, formatTerminalRiskCalibration, } from "./formatters/terminal.mjs";
|
|
21
|
-
import {
|
|
22
|
+
import { MAX_RETAINED_ARTIFACT_DIFF_BYTES, buildCliRetainedArtifactDiff, formatCliRetainedArtifactDiff, } from "./retained-artifact-diff.mjs";
|
|
22
23
|
import { MAX_LOOKUP_RECONCILIATION_INPUT_BYTES, buildCliLookupReconciliation, formatCliLookupReconciliation, } from "./lookup-reconcile.mjs";
|
|
23
24
|
import { MAX_SHARING_REVIEW_BYTES, buildSharingReview, formatSharingReview, } from "./sharing-review.mjs";
|
|
24
25
|
import { MAX_LOOKUP_TIMELINE_INPUT_BYTES, buildCliLookupTimeline, formatCliLookupTimeline, } from "./lookup-timeline.mjs";
|
|
@@ -26,61 +27,48 @@ import { buildCliPageComparison, formatCliPageComparison } from "./page-compare.
|
|
|
26
27
|
import { MAX_MAIL_REVIEW_INPUT_BYTES, buildCliMailReview, formatCliMailReview, } from "./mail-review.mjs";
|
|
27
28
|
import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES, buildOfflineEvidenceReview, buildOfflineEvidenceReviewWithLocalResources, formatOfflineEvidenceReview, } from "./offline-evidence-review.mjs";
|
|
28
29
|
import { DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA, DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, buildDomainControlManifest, formatDomainControlResult, reviewDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
|
|
29
|
-
import {
|
|
30
|
-
import {
|
|
30
|
+
import { serializeDomainControlManifest } from "../packages/evidence/domain-control-runtime.mjs";
|
|
31
|
+
import { MAX_DOMAIN_CONTROL_REVIEW_COMMAND_BYTES } from "../packages/contracts/domain-control-review.mjs";
|
|
32
|
+
import { DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, buildDomainControlFlightRecorder, formatDomainControlFlightRecorder, serializeDomainControlFlightRecorder, } from "../lib/domain-control-flight-recorder.mjs";
|
|
33
|
+
import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, buildCliDomainControlReview, formatCliDomainControlReview, } from "./domain-control-observations.mjs";
|
|
31
34
|
import { buildCliLookupBrief, formatCliLookupBrief } from "./lookup-brief.mjs";
|
|
32
35
|
import { buildCliCasePack, formatCliCasePack } from "./case-pack.mjs";
|
|
33
36
|
import { formatDomainControlMonitor, runDomainControlMonitor } from "./domain-control-monitor.mjs";
|
|
34
37
|
import { MAX_ASSURANCE_INPUT_BYTES, buildDomainAssurance, formatDomainAssurance, } from "../lib/domain-assurance.mjs";
|
|
35
38
|
import { MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, buildDomainChangePacket, formatDomainChangePacket, } from "../lib/domain-change-packet.mjs";
|
|
36
39
|
import { buildCliManual } from "./manual.mjs";
|
|
37
|
-
import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, buildInvestigationManifest, formatInvestigationManifest, } from "./investigation-manifest.mjs";
|
|
40
|
+
import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, buildInvestigationManifest, formatInvestigationManifest, } from "./investigation-manifest.mjs";
|
|
38
41
|
import { MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, mapExternalObservations, formatExternalObservationMapping, } from "./external-observation-mapping.mjs";
|
|
39
42
|
import { buildOpenAssetModelBridge, formatOpenAssetModelBridge, } from "./open-asset-model-bridge.mjs";
|
|
40
43
|
import { MAX_CT_EVENT_INPUT_BYTES, buildCtEventFindings, formatCtEventFindings, } from "./ct-event-intake.mjs";
|
|
41
|
-
import { buildInvestigationPlan, formatInvestigationPlan } from "./investigation-plan.mjs";
|
|
42
|
-
import { formatInvestigationRun, runInvestigationRecipe } from "./investigation-run.mjs";
|
|
44
|
+
import { buildInvestigationPlan, buildWorkflowRecipeCatalogue, formatInvestigationPlan, formatWorkflowRecipeCatalogue, } from "./investigation-plan.mjs";
|
|
45
|
+
import { MAX_INVESTIGATION_RUN_BYTES, formatInvestigationRun, runInvestigationRecipe } from "./investigation-run.mjs";
|
|
46
|
+
import { readCliTextInput } from "./input.mjs";
|
|
43
47
|
import { evaluateCliFailPolicies, formatFailPolicyNotice } from "./fail-policy.mjs";
|
|
44
48
|
import { formatCliJunit } from "./ci-report.mjs";
|
|
45
|
-
import { createBufferedOutput, writePrivateFile } from "./output-file.mjs";
|
|
49
|
+
import { cleanupPendingOutputFiles, createBufferedOutput, writePrivateFile } from "./output-file.mjs";
|
|
50
|
+
import { canLaunchInteractiveCli, launchInteractiveCli, } from "./interactive-launcher.mjs";
|
|
46
51
|
import { createTerminalProgress } from "./progress.mjs";
|
|
47
52
|
import { buildRegistrySupportDocument } from "./registry-support.mjs";
|
|
48
53
|
import { buildRegistryDoctorReport, formatRegistryDoctorReport } from "./registry-doctor.mjs";
|
|
49
54
|
import { MAX_REGISTRY_COHORT_INPUT_BYTES, buildRegistryCohortReport, formatRegistryCohortReport, } from "./registry-cohort.mjs";
|
|
50
55
|
import { buildRegistryFixtureScaffold } from "./registry-fixture-scaffold.mjs";
|
|
51
|
-
import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, formatOfflineArtifactVerification, verifyOfflineArtifact, } from "./artifact-verify.mjs";
|
|
56
|
+
import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, formatOfflineArtifactVerification, isCompleteOfflineArtifactVerification, verifyOfflineArtifact, } from "./artifact-verify.mjs";
|
|
57
|
+
import { buildInterchangeFidelityReport, formatInterchangeFidelityReport, } from "./interchange-report.mjs";
|
|
52
58
|
import { MAX_SOURCE_RELIABILITY_INPUT_BYTES, buildSourceReliabilityReport, formatSourceReliabilityReport, } from "./source-reliability.mjs";
|
|
53
|
-
import { MAX_RISK_CALIBRATION_INPUT_BYTES, buildRiskCalibrationReport, parseRiskCalibrationDataset,
|
|
59
|
+
import { MAX_RISK_CALIBRATION_INPUT_BYTES, buildRiskCalibrationReport, parseRiskCalibrationDataset, serializeRiskCalibrationReport, } from "./risk-calibration.mjs";
|
|
54
60
|
import { MAX_LOOKALIKE_CALIBRATION_BYTES, buildLookalikeCalibration, formatLookalikeCalibration, } from "./lookalike-calibration.mjs";
|
|
55
|
-
import { MAX_SAVED_LOOKUP_INPUT_BYTES
|
|
61
|
+
import { MAX_SAVED_LOOKUP_INPUT_BYTES } from "./saved-lookup.mjs";
|
|
56
62
|
import { presentTerminalOutput, terminalPresentation, } from "./terminal-presentation.mjs";
|
|
57
63
|
const require = createRequire(import.meta.url);
|
|
58
64
|
const { version: VERSION } = require('../package.json');
|
|
59
65
|
const MAX_STDIN_BYTES = 4096;
|
|
60
66
|
async function readStdinBounded(stream, limit = MAX_STDIN_BYTES, signal) {
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
signal?.addEventListener('abort', abort, { once: true });
|
|
67
|
-
const chunks = [];
|
|
68
|
-
let total = 0;
|
|
69
|
-
try {
|
|
70
|
-
for await (const chunk of stream) {
|
|
71
|
-
if (signal?.aborted)
|
|
72
|
-
throw new DOMException('Aborted', 'AbortError');
|
|
73
|
-
const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
|
|
74
|
-
total += buffer.length;
|
|
75
|
-
if (total > limit)
|
|
76
|
-
throw new CliUsageError(`Standard input is limited to ${limit} bytes.`);
|
|
77
|
-
chunks.push(buffer);
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
finally {
|
|
81
|
-
signal?.removeEventListener('abort', abort);
|
|
82
|
-
}
|
|
83
|
-
const text = Buffer.concat(chunks).toString('utf8').trim();
|
|
67
|
+
const text = (await readCliTextInput(null, stream, {
|
|
68
|
+
maximumBytes: limit,
|
|
69
|
+
label: 'Standard input',
|
|
70
|
+
...(signal ? { signal } : {}),
|
|
71
|
+
})).trim();
|
|
84
72
|
const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
|
|
85
73
|
if (lines.length > 1)
|
|
86
74
|
throw new CliUsageError('Single-value commands accept one stdin line. Use the bulk command for multiple inputs.');
|
|
@@ -90,10 +78,13 @@ function write(stream, value) {
|
|
|
90
78
|
if (stream && typeof stream.write === 'function')
|
|
91
79
|
stream.write(value);
|
|
92
80
|
}
|
|
93
|
-
function formatForTerminal(value, stream, color, environment) {
|
|
94
|
-
return presentTerminalOutput(value, terminalPresentation(stream, color, environment));
|
|
81
|
+
function formatForTerminal(value, stream, color, environment, palette) {
|
|
82
|
+
return presentTerminalOutput(value, terminalPresentation(stream, color, environment, palette));
|
|
95
83
|
}
|
|
96
84
|
function isCancellation(error, signal) {
|
|
85
|
+
if (error instanceof AggregateError) {
|
|
86
|
+
return error.errors.length > 0 && error.errors.every((item) => isCancellation(item));
|
|
87
|
+
}
|
|
97
88
|
return signal?.aborted === true
|
|
98
89
|
|| Boolean(error && typeof error === 'object' && 'name' in error && error.name === 'AbortError');
|
|
99
90
|
}
|
|
@@ -107,6 +98,16 @@ function usageEventReason(error) {
|
|
|
107
98
|
return 'missing_input';
|
|
108
99
|
return 'invalid_input';
|
|
109
100
|
}
|
|
101
|
+
const INLINE_CLI_COMMANDS = Object.freeze([
|
|
102
|
+
'completion', 'doctor', 'commands', 'manual', 'manifest', 'map-observations',
|
|
103
|
+
'oam-export', 'ct-intake', 'registry-support', 'registry-doctor',
|
|
104
|
+
'registry-cohort', 'registry-scaffold', 'risk-calibrate', 'lookalike-calibrate',
|
|
105
|
+
'verify-artifact', 'interchange-report', 'source-report', 'compare',
|
|
106
|
+
'page-compare', 'mail-review', 'review-evidence', 'brief', 'case-pack',
|
|
107
|
+
'domain-control', 'monitor-once', 'assurance', 'change-packet',
|
|
108
|
+
'sharing-review', 'workflow-plan', 'workflow-run', 'diff', 'reconcile',
|
|
109
|
+
'timeline', 'export',
|
|
110
|
+
]);
|
|
110
111
|
async function runParsedCli(args, dependencies = {}) {
|
|
111
112
|
const stdout = dependencies.stdout || process.stdout;
|
|
112
113
|
const stderr = dependencies.stderr || process.stderr;
|
|
@@ -115,7 +116,8 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
115
116
|
const eventProgress = { current: null };
|
|
116
117
|
let failureLabel = 'Lookup';
|
|
117
118
|
try {
|
|
118
|
-
const
|
|
119
|
+
const palette = args.palette || 'auto';
|
|
120
|
+
const terminal = (value, color = true) => formatForTerminal(value, stdout, color, environment, palette);
|
|
119
121
|
const beginProgress = (message) => {
|
|
120
122
|
const terminalOutput = 'output' in args
|
|
121
123
|
&& args.output === 'terminal'
|
|
@@ -127,6 +129,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
127
129
|
enabled,
|
|
128
130
|
color: terminalOutput ? args.color : false,
|
|
129
131
|
environment,
|
|
132
|
+
palette,
|
|
130
133
|
...(dependencies.nowMs ? { now: dependencies.nowMs } : {}),
|
|
131
134
|
});
|
|
132
135
|
progress.start(message);
|
|
@@ -148,6 +151,29 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
148
151
|
const readSingleInput = async () => (dependencies.readStdin
|
|
149
152
|
? await dependencies.readStdin()
|
|
150
153
|
: await readStdinBounded(dependencies.stdin || process.stdin, MAX_STDIN_BYTES, dependencies.signal));
|
|
154
|
+
const readInput = async (source, maximumBytes, label) => (readCliTextInput(source, dependencies.stdin || process.stdin, {
|
|
155
|
+
maximumBytes,
|
|
156
|
+
label,
|
|
157
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
158
|
+
}));
|
|
159
|
+
const readPassphraseSource = async (source) => {
|
|
160
|
+
let passphraseText;
|
|
161
|
+
try {
|
|
162
|
+
passphraseText = dependencies.readPassphraseFile
|
|
163
|
+
? await dependencies.readPassphraseFile(source)
|
|
164
|
+
: await readInput(source, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, 'Passphrase file');
|
|
165
|
+
}
|
|
166
|
+
catch (error) {
|
|
167
|
+
if (error instanceof CliUsageError)
|
|
168
|
+
throw error;
|
|
169
|
+
throw new CliUsageError(`Could not read passphrase file: ${boundedCliErrorMessage(error, 'File could not be read')}`);
|
|
170
|
+
}
|
|
171
|
+
const passphrase = passphraseText.replace(/\r?\n$/u, '');
|
|
172
|
+
if (!passphrase || /[\r\n\u0000]/u.test(passphrase)) {
|
|
173
|
+
throw new CliUsageError('Passphrase file must contain exactly one non-empty UTF-8 line.');
|
|
174
|
+
}
|
|
175
|
+
return passphrase;
|
|
176
|
+
};
|
|
151
177
|
const commandContext = Object.freeze({
|
|
152
178
|
stdout,
|
|
153
179
|
stderr,
|
|
@@ -155,6 +181,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
155
181
|
writeStdout: (value) => write(stdout, value),
|
|
156
182
|
writeStderr: (value) => write(stderr, value),
|
|
157
183
|
readSingleInput,
|
|
184
|
+
readInput,
|
|
158
185
|
now: () => dependencies.now ? dependencies.now() : new Date().toISOString(),
|
|
159
186
|
beginProgress,
|
|
160
187
|
endProgress,
|
|
@@ -171,13 +198,90 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
171
198
|
write(stdout, `${VERSION}\n`);
|
|
172
199
|
return EXIT_CODES.SUCCESS;
|
|
173
200
|
}
|
|
201
|
+
const handlerOwner = commandDefinition(args.action).execution.handlerOwner;
|
|
202
|
+
if (handlerOwner === 'bulk') {
|
|
203
|
+
if (args.action !== 'bulk')
|
|
204
|
+
throw new Error('Bulk command registry ownership is inconsistent.');
|
|
205
|
+
failureLabel = 'Bulk lookup';
|
|
206
|
+
const { runBulkCommand } = await import("./bulk-command-runner.mjs");
|
|
207
|
+
return await runBulkCommand(args, dependencies, commandContext);
|
|
208
|
+
}
|
|
209
|
+
if (handlerOwner === 'discovery') {
|
|
210
|
+
if (args.action !== 'discover')
|
|
211
|
+
throw new Error('Discovery command registry ownership is inconsistent.');
|
|
212
|
+
failureLabel = 'Candidate generation';
|
|
213
|
+
const { runDiscoveryCommand } = await import("./discovery-command-runner.mjs");
|
|
214
|
+
return await runDiscoveryCommand(args, dependencies, commandContext);
|
|
215
|
+
}
|
|
216
|
+
if (handlerOwner === 'discovery_scan') {
|
|
217
|
+
if (args.action !== 'discover-scan')
|
|
218
|
+
throw new Error('Discovery-scan command registry ownership is inconsistent.');
|
|
219
|
+
failureLabel = 'Candidate scan';
|
|
220
|
+
const { runDiscoveryScanCommand } = await import("./discovery-scan-command-runner.mjs");
|
|
221
|
+
return await runDiscoveryScanCommand(args, dependencies, commandContext);
|
|
222
|
+
}
|
|
223
|
+
if (handlerOwner === 'evidence') {
|
|
224
|
+
if (!isEvidenceCommand(args))
|
|
225
|
+
throw new Error('Evidence command registry ownership is inconsistent.');
|
|
226
|
+
failureLabel = evidenceCommandFailureLabel(args.action);
|
|
227
|
+
const evidenceStdout = args.action !== 'sign-artifact' && args.output === 'terminal'
|
|
228
|
+
? { write: (value) => write(stdout, terminal(value, args.color)) }
|
|
229
|
+
: stdout;
|
|
230
|
+
return await runEvidenceCommand(args, {
|
|
231
|
+
stdout: evidenceStdout,
|
|
232
|
+
stdin: dependencies.stdin || process.stdin,
|
|
233
|
+
readArtifactInput: dependencies.readArtifactInput,
|
|
234
|
+
readPassphraseFile: dependencies.readPassphraseFile,
|
|
235
|
+
readPrivateKeyFile: dependencies.readPrivateKeyFile,
|
|
236
|
+
readPublicKeyFile: dependencies.readPublicKeyFile,
|
|
237
|
+
now: dependencies.now,
|
|
238
|
+
signal: dependencies.signal,
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
if (handlerOwner === 'network') {
|
|
242
|
+
if (args.action !== 'ct-search'
|
|
243
|
+
&& args.action !== 'posture'
|
|
244
|
+
&& args.action !== 'http'
|
|
245
|
+
&& args.action !== 'tls'
|
|
246
|
+
&& args.action !== 'dnssec-validate'
|
|
247
|
+
&& args.action !== 'mail-transport') {
|
|
248
|
+
throw new Error('Network command registry ownership is inconsistent.');
|
|
249
|
+
}
|
|
250
|
+
failureLabel = args.action === 'ct-search'
|
|
251
|
+
? 'Certificate Transparency search'
|
|
252
|
+
: args.action === 'posture'
|
|
253
|
+
? 'Domain posture audit'
|
|
254
|
+
: args.action === 'http'
|
|
255
|
+
? 'HTTP probe'
|
|
256
|
+
: args.action === 'tls'
|
|
257
|
+
? 'TLS evidence collection'
|
|
258
|
+
: args.action === 'dnssec-validate'
|
|
259
|
+
? 'DNSSEC chain validation'
|
|
260
|
+
: 'Mail transport review';
|
|
261
|
+
const { runNetworkCommand } = await import("./network-command-runner.mjs");
|
|
262
|
+
return await runNetworkCommand(args, dependencies, commandContext);
|
|
263
|
+
}
|
|
264
|
+
if (handlerOwner === 'lookup') {
|
|
265
|
+
if (args.action !== 'lookup')
|
|
266
|
+
throw new Error('Lookup command registry ownership is inconsistent.');
|
|
267
|
+
const { runLookupCommand } = await import("./lookup-command-runner.mjs");
|
|
268
|
+
return await runLookupCommand(args, dependencies, commandContext);
|
|
269
|
+
}
|
|
270
|
+
if (handlerOwner !== 'inline' || !INLINE_CLI_COMMANDS.includes(args.action)) {
|
|
271
|
+
throw new Error('No CLI execution route is registered for the parsed command.');
|
|
272
|
+
}
|
|
174
273
|
if (args.action === 'completion') {
|
|
175
274
|
write(stdout, buildShellCompletion(args.shell));
|
|
176
275
|
return EXIT_CODES.SUCCESS;
|
|
177
276
|
}
|
|
178
277
|
if (args.action === 'commands') {
|
|
278
|
+
const selectedCommands = selectCliCommands(CLI_COMMAND_REGISTRY, {
|
|
279
|
+
common: args.common,
|
|
280
|
+
group: args.group,
|
|
281
|
+
mode: args.mode,
|
|
282
|
+
});
|
|
179
283
|
const catalogue = buildCliCommandCatalogue({
|
|
180
|
-
commands:
|
|
284
|
+
commands: selectedCommands,
|
|
181
285
|
collections: COMMAND_COLLECTION,
|
|
182
286
|
details: COMMAND_DETAILS,
|
|
183
287
|
usage: COMMAND_USAGE,
|
|
@@ -202,11 +306,16 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
202
306
|
if (args.action === 'manifest') {
|
|
203
307
|
failureLabel = 'Investigation manifest';
|
|
204
308
|
const artifacts = [];
|
|
309
|
+
let totalBytes = 0;
|
|
205
310
|
try {
|
|
206
311
|
for (const source of args.sources) {
|
|
207
312
|
const content = dependencies.readDiffInput
|
|
208
313
|
? await dependencies.readDiffInput(source)
|
|
209
|
-
: await
|
|
314
|
+
: await readInput(source, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, 'Manifest artefact input');
|
|
315
|
+
totalBytes += Buffer.byteLength(content, 'utf8');
|
|
316
|
+
if (totalBytes > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES) {
|
|
317
|
+
throw new CliUsageError(`Manifest artefacts exceed the ${MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES}-byte combined limit.`);
|
|
318
|
+
}
|
|
210
319
|
artifacts.push({ content });
|
|
211
320
|
}
|
|
212
321
|
}
|
|
@@ -239,12 +348,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
239
348
|
try {
|
|
240
349
|
input = dependencies.readArtifactInput
|
|
241
350
|
? await dependencies.readArtifactInput(args.source)
|
|
242
|
-
: await
|
|
243
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
244
|
-
: dependencies.stdin || process.stdin, {
|
|
245
|
-
limit: MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES,
|
|
246
|
-
label: mapping ? 'External observation mapping input' : 'Open Asset Model bridge input',
|
|
247
|
-
});
|
|
351
|
+
: await readInput(args.source, MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, mapping ? 'External observation mapping input' : 'Open Asset Model bridge input');
|
|
248
352
|
}
|
|
249
353
|
catch (error) {
|
|
250
354
|
if (error instanceof CliUsageError)
|
|
@@ -255,10 +359,11 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
255
359
|
throw new CliUsageError(`${args.action} requires one versioned JSON file or a document on stdin.`);
|
|
256
360
|
let parsed;
|
|
257
361
|
try {
|
|
362
|
+
scanBoundedJson(input);
|
|
258
363
|
parsed = JSON.parse(input);
|
|
259
364
|
}
|
|
260
365
|
catch {
|
|
261
|
-
throw new CliUsageError(`${mapping ? 'External observation mapping' : 'Open Asset Model bridge'} input is not valid JSON.`);
|
|
366
|
+
throw new CliUsageError(`${mapping ? 'External observation mapping' : 'Open Asset Model bridge'} input is not valid bounded JSON without duplicate keys.`);
|
|
262
367
|
}
|
|
263
368
|
let document;
|
|
264
369
|
try {
|
|
@@ -283,7 +388,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
283
388
|
version: VERSION,
|
|
284
389
|
generatedAt: dependencies.now ? dependencies.now() : new Date().toISOString(),
|
|
285
390
|
network: args.network,
|
|
286
|
-
presentation: terminalPresentation(stdout, args.color, environment),
|
|
391
|
+
presentation: terminalPresentation(stdout, args.color, environment, palette),
|
|
287
392
|
...(dependencies.resolvePublicAddresses ? { resolveAddresses: dependencies.resolvePublicAddresses } : {}),
|
|
288
393
|
...(dependencies.safeFetch ? { fetchHttps: dependencies.safeFetch } : {}),
|
|
289
394
|
...(dependencies.whoisQuery ? { queryWhois: dependencies.whoisQuery } : {}),
|
|
@@ -321,12 +426,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
321
426
|
try {
|
|
322
427
|
input = dependencies.readCompareInput
|
|
323
428
|
? await dependencies.readCompareInput(args.source)
|
|
324
|
-
: await
|
|
325
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
326
|
-
: dependencies.stdin || process.stdin, {
|
|
327
|
-
limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
|
|
328
|
-
label: 'Registry doctor input',
|
|
329
|
-
});
|
|
429
|
+
: await readInput(args.source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Registry doctor input');
|
|
330
430
|
}
|
|
331
431
|
catch (error) {
|
|
332
432
|
if (error instanceof CliUsageError)
|
|
@@ -348,12 +448,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
348
448
|
try {
|
|
349
449
|
input = dependencies.readCompareInput
|
|
350
450
|
? await dependencies.readCompareInput(args.source)
|
|
351
|
-
: await
|
|
352
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
353
|
-
: dependencies.stdin || process.stdin, {
|
|
354
|
-
limit: MAX_REGISTRY_COHORT_INPUT_BYTES,
|
|
355
|
-
label: 'Registry cohort input',
|
|
356
|
-
});
|
|
451
|
+
: await readInput(args.source, MAX_REGISTRY_COHORT_INPUT_BYTES, 'Registry cohort input');
|
|
357
452
|
}
|
|
358
453
|
catch (error) {
|
|
359
454
|
if (error instanceof CliUsageError)
|
|
@@ -385,9 +480,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
385
480
|
try {
|
|
386
481
|
input = dependencies.readRiskCalibrationInput
|
|
387
482
|
? await dependencies.readRiskCalibrationInput(args.source)
|
|
388
|
-
: await
|
|
389
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
390
|
-
: dependencies.stdin || process.stdin, MAX_RISK_CALIBRATION_INPUT_BYTES);
|
|
483
|
+
: await readInput(args.source, MAX_RISK_CALIBRATION_INPUT_BYTES, 'Risk calibration input');
|
|
391
484
|
}
|
|
392
485
|
catch (error) {
|
|
393
486
|
if (error instanceof CliUsageError)
|
|
@@ -407,7 +500,11 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
407
500
|
} : {}),
|
|
408
501
|
});
|
|
409
502
|
if (!args.quiet)
|
|
410
|
-
write(stdout, args.output === '
|
|
503
|
+
write(stdout, args.output === 'summary_json'
|
|
504
|
+
? serializeRiskCalibrationReport(buildRiskCalibrationSummaryReport(report))
|
|
505
|
+
: args.output === 'json'
|
|
506
|
+
? serializeRiskCalibrationReport(report)
|
|
507
|
+
: terminal(formatTerminalRiskCalibration(report), args.color));
|
|
411
508
|
return EXIT_CODES.SUCCESS;
|
|
412
509
|
}
|
|
413
510
|
if (args.action === 'lookalike-calibrate') {
|
|
@@ -416,12 +513,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
416
513
|
try {
|
|
417
514
|
input = dependencies.readRiskCalibrationInput
|
|
418
515
|
? await dependencies.readRiskCalibrationInput(args.source)
|
|
419
|
-
: await
|
|
420
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
421
|
-
: dependencies.stdin || process.stdin, {
|
|
422
|
-
limit: MAX_LOOKALIKE_CALIBRATION_BYTES,
|
|
423
|
-
label: 'Lookalike calibration input',
|
|
424
|
-
});
|
|
516
|
+
: await readInput(args.source, MAX_LOOKALIKE_CALIBRATION_BYTES, 'Lookalike calibration input');
|
|
425
517
|
}
|
|
426
518
|
catch (error) {
|
|
427
519
|
if (error instanceof CliUsageError)
|
|
@@ -449,12 +541,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
449
541
|
try {
|
|
450
542
|
input = dependencies.readArtifactInput
|
|
451
543
|
? await dependencies.readArtifactInput(args.source)
|
|
452
|
-
: await
|
|
453
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
454
|
-
: dependencies.stdin || process.stdin, {
|
|
455
|
-
limit: MAX_OFFLINE_ARTIFACT_BYTES,
|
|
456
|
-
label: 'Artefact input',
|
|
457
|
-
});
|
|
544
|
+
: await readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Artefact input');
|
|
458
545
|
}
|
|
459
546
|
catch (error) {
|
|
460
547
|
if (error instanceof CliUsageError)
|
|
@@ -463,49 +550,59 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
463
550
|
}
|
|
464
551
|
if (!input.trim())
|
|
465
552
|
throw new CliUsageError('verify-artifact requires one JSON file or an artefact on stdin.');
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
553
|
+
const passphrase = args.passphraseSource ? await readPassphraseSource(args.passphraseSource) : null;
|
|
554
|
+
let manifest = null;
|
|
555
|
+
if (args.manifestSource && args.manifestEntryId) {
|
|
556
|
+
let raw;
|
|
469
557
|
try {
|
|
470
|
-
|
|
471
|
-
? await dependencies.
|
|
472
|
-
: await
|
|
473
|
-
limit: MAX_OFFLINE_PASSPHRASE_FILE_BYTES,
|
|
474
|
-
label: 'Passphrase file',
|
|
475
|
-
});
|
|
558
|
+
raw = dependencies.readArtifactInput
|
|
559
|
+
? await dependencies.readArtifactInput(args.manifestSource)
|
|
560
|
+
: await readInput(args.manifestSource, MAX_OFFLINE_ARTIFACT_BYTES, 'Investigation manifest input');
|
|
476
561
|
}
|
|
477
562
|
catch (error) {
|
|
478
563
|
if (error instanceof CliUsageError)
|
|
479
564
|
throw error;
|
|
480
|
-
throw new CliUsageError(`Could not read
|
|
481
|
-
}
|
|
482
|
-
passphrase = passphraseText.replace(/\r?\n$/u, '');
|
|
483
|
-
if (!passphrase || /[\r\n\u0000]/u.test(passphrase)) {
|
|
484
|
-
throw new CliUsageError('Passphrase file must contain exactly one non-empty UTF-8 line.');
|
|
565
|
+
throw new CliUsageError(`Could not read investigation manifest input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
485
566
|
}
|
|
567
|
+
if (!raw.trim())
|
|
568
|
+
throw new CliUsageError('The investigation manifest input is empty.');
|
|
569
|
+
manifest = Object.freeze({ raw, entryId: args.manifestEntryId });
|
|
486
570
|
}
|
|
487
|
-
const report = await verifyOfflineArtifact(input, { passphrase });
|
|
571
|
+
const report = await verifyOfflineArtifact(input, { passphrase, manifest });
|
|
488
572
|
if (!args.quiet) {
|
|
489
573
|
write(stdout, args.output === 'json'
|
|
490
574
|
? formatJsonDocument(report)
|
|
491
575
|
: terminal(formatOfflineArtifactVerification(report), args.color));
|
|
492
576
|
}
|
|
493
|
-
return
|
|
577
|
+
return args.strictExit && !isCompleteOfflineArtifactVerification(report)
|
|
578
|
+
? EXIT_CODES.PARTIAL_FAILURE
|
|
579
|
+
: EXIT_CODES.SUCCESS;
|
|
494
580
|
}
|
|
495
|
-
if (
|
|
496
|
-
failureLabel =
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
581
|
+
if (args.action === 'interchange-report') {
|
|
582
|
+
failureLabel = 'Interchange fidelity report';
|
|
583
|
+
let input;
|
|
584
|
+
try {
|
|
585
|
+
input = dependencies.readArtifactInput
|
|
586
|
+
? await dependencies.readArtifactInput(args.source)
|
|
587
|
+
: await readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Interchange input');
|
|
588
|
+
}
|
|
589
|
+
catch (error) {
|
|
590
|
+
if (error instanceof CliUsageError)
|
|
591
|
+
throw error;
|
|
592
|
+
throw new CliUsageError(`Could not read interchange input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
593
|
+
}
|
|
594
|
+
if (!input.trim())
|
|
595
|
+
throw new CliUsageError('interchange-report requires one JSON file or an artefact on stdin.');
|
|
596
|
+
const passphrase = args.passphraseSource ? await readPassphraseSource(args.passphraseSource) : null;
|
|
597
|
+
const report = await buildInterchangeFidelityReport(input, {
|
|
598
|
+
generatedAt: commandContext.now(),
|
|
599
|
+
passphrase,
|
|
508
600
|
});
|
|
601
|
+
if (!args.quiet)
|
|
602
|
+
write(stdout, args.output === 'json'
|
|
603
|
+
? formatJsonDocument(report)
|
|
604
|
+
: terminal(formatInterchangeFidelityReport(report), args.color));
|
|
605
|
+
return EXIT_CODES.SUCCESS;
|
|
509
606
|
}
|
|
510
607
|
if (args.action === 'source-report') {
|
|
511
608
|
failureLabel = 'Source reliability report';
|
|
@@ -513,12 +610,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
513
610
|
try {
|
|
514
611
|
input = dependencies.readSourceReliabilityInput
|
|
515
612
|
? await dependencies.readSourceReliabilityInput(args.source)
|
|
516
|
-
: await
|
|
517
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
518
|
-
: dependencies.stdin || process.stdin, {
|
|
519
|
-
limit: MAX_SOURCE_RELIABILITY_INPUT_BYTES,
|
|
520
|
-
label: 'Source reliability input',
|
|
521
|
-
});
|
|
613
|
+
: await readInput(args.source, MAX_SOURCE_RELIABILITY_INPUT_BYTES, 'Source reliability input');
|
|
522
614
|
}
|
|
523
615
|
catch (error) {
|
|
524
616
|
if (error instanceof CliUsageError)
|
|
@@ -541,9 +633,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
541
633
|
try {
|
|
542
634
|
input = dependencies.readCompareInput
|
|
543
635
|
? await dependencies.readCompareInput(args.source)
|
|
544
|
-
: await
|
|
545
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
546
|
-
: dependencies.stdin || process.stdin, MAX_COMPARE_INPUT_BYTES);
|
|
636
|
+
: await readInput(args.source, MAX_COMPARE_INPUT_BYTES, 'Comparison input');
|
|
547
637
|
}
|
|
548
638
|
catch (error) {
|
|
549
639
|
if (error instanceof CliUsageError)
|
|
@@ -564,10 +654,8 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
564
654
|
}
|
|
565
655
|
if (args.action === 'page-compare') {
|
|
566
656
|
failureLabel = 'Static page comparison';
|
|
567
|
-
const readDiffInput = dependencies.readDiffInput
|
|
568
|
-
|
|
569
|
-
label: 'Page comparison input',
|
|
570
|
-
})));
|
|
657
|
+
const readDiffInput = dependencies.readDiffInput
|
|
658
|
+
|| ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Page comparison input'));
|
|
571
659
|
let leftInput;
|
|
572
660
|
let rightInput;
|
|
573
661
|
try {
|
|
@@ -594,12 +682,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
594
682
|
try {
|
|
595
683
|
input = dependencies.readMailReviewInput
|
|
596
684
|
? await dependencies.readMailReviewInput(args.source)
|
|
597
|
-
: await
|
|
598
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
599
|
-
: dependencies.stdin || process.stdin, {
|
|
600
|
-
limit: MAX_MAIL_REVIEW_INPUT_BYTES,
|
|
601
|
-
label: 'Mail review input',
|
|
602
|
-
});
|
|
685
|
+
: await readInput(args.source, MAX_MAIL_REVIEW_INPUT_BYTES, 'Mail review input');
|
|
603
686
|
}
|
|
604
687
|
catch (error) {
|
|
605
688
|
if (error instanceof CliUsageError)
|
|
@@ -619,12 +702,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
619
702
|
try {
|
|
620
703
|
input = dependencies.readArtifactInput
|
|
621
704
|
? await dependencies.readArtifactInput(args.source)
|
|
622
|
-
: await
|
|
623
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
624
|
-
: dependencies.stdin || process.stdin, {
|
|
625
|
-
limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
|
|
626
|
-
label: 'Offline evidence input',
|
|
627
|
-
});
|
|
705
|
+
: await readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Offline evidence input');
|
|
628
706
|
}
|
|
629
707
|
catch (error) {
|
|
630
708
|
if (error instanceof CliUsageError)
|
|
@@ -662,12 +740,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
662
740
|
try {
|
|
663
741
|
input = dependencies.readArtifactInput
|
|
664
742
|
? await dependencies.readArtifactInput(args.source)
|
|
665
|
-
: await
|
|
666
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
667
|
-
: dependencies.stdin || process.stdin, {
|
|
668
|
-
limit: isBrief ? MAX_SAVED_LOOKUP_INPUT_BYTES : 4 * 1024 * 1024,
|
|
669
|
-
label: isBrief ? 'Lookup brief input' : 'Case-pack input',
|
|
670
|
-
});
|
|
743
|
+
: await readInput(args.source, isBrief ? MAX_SAVED_LOOKUP_INPUT_BYTES : 4 * 1024 * 1024, isBrief ? 'Lookup brief input' : 'Case-pack input');
|
|
671
744
|
}
|
|
672
745
|
catch (error) {
|
|
673
746
|
if (error instanceof CliUsageError)
|
|
@@ -698,12 +771,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
698
771
|
try {
|
|
699
772
|
input = dependencies.readArtifactInput
|
|
700
773
|
? await dependencies.readArtifactInput(args.source)
|
|
701
|
-
: await
|
|
702
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
703
|
-
: dependencies.stdin || process.stdin, {
|
|
704
|
-
limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
|
|
705
|
-
label: 'Domain control input',
|
|
706
|
-
});
|
|
774
|
+
: await readInput(args.source, MAX_DOMAIN_CONTROL_REVIEW_COMMAND_BYTES, 'Domain control input');
|
|
707
775
|
}
|
|
708
776
|
catch (error) {
|
|
709
777
|
if (error instanceof CliUsageError)
|
|
@@ -712,11 +780,21 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
712
780
|
}
|
|
713
781
|
if (!input.trim())
|
|
714
782
|
throw new CliUsageError('domain-control requires one JSON file or a document on stdin.');
|
|
783
|
+
const normalizedDomainControlInput = input.replace(/^\uFEFF/u, '');
|
|
715
784
|
let parsed;
|
|
716
785
|
try {
|
|
717
|
-
|
|
786
|
+
scanBoundedJson(normalizedDomainControlInput, {
|
|
787
|
+
maximumDepth: MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH,
|
|
788
|
+
maximumKeys: MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS,
|
|
789
|
+
maximumValues: MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES,
|
|
790
|
+
});
|
|
791
|
+
parsed = JSON.parse(normalizedDomainControlInput);
|
|
718
792
|
}
|
|
719
|
-
catch {
|
|
793
|
+
catch (cause) {
|
|
794
|
+
const detail = cause instanceof Error ? cause.message : '';
|
|
795
|
+
if (detail.startsWith('Artefact JSON ')) {
|
|
796
|
+
throw new CliUsageError(`Domain control input ${detail.slice('Artefact JSON '.length)}`);
|
|
797
|
+
}
|
|
720
798
|
throw new CliUsageError('Domain control input is not valid JSON.');
|
|
721
799
|
}
|
|
722
800
|
const schema = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
@@ -741,7 +819,11 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
741
819
|
: formatDomainControlResult(document);
|
|
742
820
|
if (!args.quiet)
|
|
743
821
|
write(stdout, args.output === 'json'
|
|
744
|
-
?
|
|
822
|
+
? schema === DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA
|
|
823
|
+
? serializeDomainControlManifest(document)
|
|
824
|
+
: schema === DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA
|
|
825
|
+
? serializeDomainControlFlightRecorder(document)
|
|
826
|
+
: formatJsonDocument(document)
|
|
745
827
|
: terminal(terminalDocument, args.color));
|
|
746
828
|
return EXIT_CODES.SUCCESS;
|
|
747
829
|
}
|
|
@@ -752,19 +834,11 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
752
834
|
try {
|
|
753
835
|
manifestInput = dependencies.readArtifactInput
|
|
754
836
|
? await dependencies.readArtifactInput(args.source)
|
|
755
|
-
: await
|
|
756
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
757
|
-
: dependencies.stdin || process.stdin, {
|
|
758
|
-
limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
|
|
759
|
-
label: 'Domain-control manifest input',
|
|
760
|
-
});
|
|
837
|
+
: await readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Domain-control manifest input');
|
|
761
838
|
if (args.previousSource) {
|
|
762
839
|
previousInput = dependencies.readDiffInput
|
|
763
840
|
? await dependencies.readDiffInput(args.previousSource)
|
|
764
|
-
: await
|
|
765
|
-
limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
|
|
766
|
-
label: 'Prior monitor snapshot',
|
|
767
|
-
});
|
|
841
|
+
: await readInput(args.previousSource, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Prior monitor snapshot');
|
|
768
842
|
}
|
|
769
843
|
}
|
|
770
844
|
catch (error) {
|
|
@@ -807,12 +881,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
807
881
|
try {
|
|
808
882
|
input = dependencies.readArtifactInput
|
|
809
883
|
? await dependencies.readArtifactInput(args.source)
|
|
810
|
-
: await
|
|
811
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
812
|
-
: dependencies.stdin || process.stdin, {
|
|
813
|
-
limit: MAX_ASSURANCE_INPUT_BYTES,
|
|
814
|
-
label: 'Domain assurance input',
|
|
815
|
-
});
|
|
884
|
+
: await readInput(args.source, MAX_ASSURANCE_INPUT_BYTES, 'Domain assurance input');
|
|
816
885
|
}
|
|
817
886
|
catch (error) {
|
|
818
887
|
if (error instanceof CliUsageError)
|
|
@@ -823,10 +892,11 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
823
892
|
throw new CliUsageError('assurance requires one versioned JSON file or a document on stdin.');
|
|
824
893
|
let parsed;
|
|
825
894
|
try {
|
|
895
|
+
scanBoundedJson(input);
|
|
826
896
|
parsed = JSON.parse(input);
|
|
827
897
|
}
|
|
828
898
|
catch {
|
|
829
|
-
throw new CliUsageError('Domain assurance input is not valid JSON.');
|
|
899
|
+
throw new CliUsageError('Domain assurance input is not valid bounded JSON without duplicate keys.');
|
|
830
900
|
}
|
|
831
901
|
let document;
|
|
832
902
|
try {
|
|
@@ -847,12 +917,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
847
917
|
try {
|
|
848
918
|
input = dependencies.readArtifactInput
|
|
849
919
|
? await dependencies.readArtifactInput(args.source)
|
|
850
|
-
: await
|
|
851
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
852
|
-
: dependencies.stdin || process.stdin, {
|
|
853
|
-
limit: MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES,
|
|
854
|
-
label: 'Domain change packet input',
|
|
855
|
-
});
|
|
920
|
+
: await readInput(args.source, MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, 'Domain change packet input');
|
|
856
921
|
}
|
|
857
922
|
catch (error) {
|
|
858
923
|
if (error instanceof CliUsageError)
|
|
@@ -863,10 +928,11 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
863
928
|
throw new CliUsageError('change-packet requires one versioned JSON file or a document on stdin.');
|
|
864
929
|
let parsed;
|
|
865
930
|
try {
|
|
931
|
+
scanBoundedJson(input);
|
|
866
932
|
parsed = JSON.parse(input);
|
|
867
933
|
}
|
|
868
934
|
catch {
|
|
869
|
-
throw new CliUsageError('Domain change packet input is not valid JSON.');
|
|
935
|
+
throw new CliUsageError('Domain change packet input is not valid bounded JSON without duplicate keys.');
|
|
870
936
|
}
|
|
871
937
|
let document;
|
|
872
938
|
try {
|
|
@@ -887,12 +953,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
887
953
|
try {
|
|
888
954
|
input = dependencies.readArtifactInput
|
|
889
955
|
? await dependencies.readArtifactInput(args.source)
|
|
890
|
-
: await
|
|
891
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
892
|
-
: dependencies.stdin || process.stdin, {
|
|
893
|
-
limit: MAX_SHARING_REVIEW_BYTES,
|
|
894
|
-
label: 'Sharing review input',
|
|
895
|
-
});
|
|
956
|
+
: await readInput(args.source, MAX_SHARING_REVIEW_BYTES, 'Sharing review input');
|
|
896
957
|
}
|
|
897
958
|
catch (error) {
|
|
898
959
|
if (error instanceof CliUsageError)
|
|
@@ -923,6 +984,14 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
923
984
|
}
|
|
924
985
|
if (args.action === 'workflow-plan') {
|
|
925
986
|
failureLabel = 'Investigation plan';
|
|
987
|
+
if ('discovery' in args) {
|
|
988
|
+
const catalogue = buildWorkflowRecipeCatalogue(args.discovery === 'explain' ? args.recipe : null);
|
|
989
|
+
if (!args.quiet)
|
|
990
|
+
write(stdout, args.output === 'json'
|
|
991
|
+
? formatJsonDocument(catalogue)
|
|
992
|
+
: terminal(formatWorkflowRecipeCatalogue(catalogue), args.color));
|
|
993
|
+
return EXIT_CODES.SUCCESS;
|
|
994
|
+
}
|
|
926
995
|
const document = buildInvestigationPlan(args.recipe, args.subject, commandContext.now());
|
|
927
996
|
if (!args.quiet)
|
|
928
997
|
write(stdout, args.output === 'json'
|
|
@@ -937,10 +1006,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
937
1006
|
try {
|
|
938
1007
|
resumeInput = dependencies.readDiffInput
|
|
939
1008
|
? await dependencies.readDiffInput(args.resumeSource)
|
|
940
|
-
: await
|
|
941
|
-
limit: 24 * 1024 * 1024,
|
|
942
|
-
label: 'Investigation resume state',
|
|
943
|
-
});
|
|
1009
|
+
: await readInput(args.resumeSource, MAX_INVESTIGATION_RUN_BYTES, 'Investigation resume state');
|
|
944
1010
|
}
|
|
945
1011
|
catch (error) {
|
|
946
1012
|
if (error instanceof CliUsageError)
|
|
@@ -952,6 +1018,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
952
1018
|
approveNetwork: args.approveNetwork,
|
|
953
1019
|
resumeInput,
|
|
954
1020
|
generatedAt: commandContext.now(),
|
|
1021
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
955
1022
|
execute: async (command, stepArguments) => {
|
|
956
1023
|
const stepStdout = createBufferedOutput();
|
|
957
1024
|
const stepStderr = createBufferedOutput();
|
|
@@ -970,11 +1037,9 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
970
1037
|
return document.state === 'step_failed' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
|
|
971
1038
|
}
|
|
972
1039
|
if (args.action === 'diff') {
|
|
973
|
-
failureLabel = '
|
|
974
|
-
const readDiffInput = dependencies.readDiffInput
|
|
975
|
-
|
|
976
|
-
label: 'Lookup diff input',
|
|
977
|
-
})));
|
|
1040
|
+
failureLabel = 'Retained artifact diff';
|
|
1041
|
+
const readDiffInput = dependencies.readDiffInput
|
|
1042
|
+
|| ((source) => readInput(source, MAX_RETAINED_ARTIFACT_DIFF_BYTES, 'Retained diff input'));
|
|
978
1043
|
let leftInput;
|
|
979
1044
|
let rightInput;
|
|
980
1045
|
try {
|
|
@@ -986,21 +1051,19 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
986
1051
|
catch (error) {
|
|
987
1052
|
if (error instanceof CliUsageError)
|
|
988
1053
|
throw error;
|
|
989
|
-
throw new CliUsageError(`Could not read
|
|
1054
|
+
throw new CliUsageError(`Could not read retained diff input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
|
|
990
1055
|
}
|
|
991
|
-
const document =
|
|
1056
|
+
const document = buildCliRetainedArtifactDiff(leftInput, rightInput, { leftSessionId: args.leftSessionId, rightSessionId: args.rightSessionId }, dependencies.now ? dependencies.now() : new Date().toISOString());
|
|
992
1057
|
if (!args.quiet)
|
|
993
1058
|
write(stdout, args.output === 'json'
|
|
994
1059
|
? formatJsonDocument(document)
|
|
995
|
-
: terminal(
|
|
1060
|
+
: terminal(formatCliRetainedArtifactDiff(document), args.color));
|
|
996
1061
|
return EXIT_CODES.SUCCESS;
|
|
997
1062
|
}
|
|
998
1063
|
if (args.action === 'reconcile') {
|
|
999
1064
|
failureLabel = 'Lookup observation reconciliation';
|
|
1000
|
-
const readReconciliationInput = dependencies.readDiffInput
|
|
1001
|
-
|
|
1002
|
-
label: 'Lookup reconciliation input',
|
|
1003
|
-
})));
|
|
1065
|
+
const readReconciliationInput = dependencies.readDiffInput
|
|
1066
|
+
|| ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Lookup reconciliation input'));
|
|
1004
1067
|
const inputs = [];
|
|
1005
1068
|
let totalBytes = 0;
|
|
1006
1069
|
try {
|
|
@@ -1027,10 +1090,8 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
1027
1090
|
}
|
|
1028
1091
|
if (args.action === 'timeline') {
|
|
1029
1092
|
failureLabel = 'Lookup observation timeline';
|
|
1030
|
-
const readTimelineInput = dependencies.readDiffInput
|
|
1031
|
-
|
|
1032
|
-
label: 'Lookup timeline input',
|
|
1033
|
-
})));
|
|
1093
|
+
const readTimelineInput = dependencies.readDiffInput
|
|
1094
|
+
|| ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Lookup timeline input'));
|
|
1034
1095
|
const inputs = [];
|
|
1035
1096
|
let totalBytes = 0;
|
|
1036
1097
|
try {
|
|
@@ -1061,12 +1122,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
1061
1122
|
try {
|
|
1062
1123
|
input = dependencies.readExportInput
|
|
1063
1124
|
? await dependencies.readExportInput(args.source)
|
|
1064
|
-
: await
|
|
1065
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
1066
|
-
: dependencies.stdin || process.stdin, {
|
|
1067
|
-
limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
|
|
1068
|
-
label: 'Evidence export input',
|
|
1069
|
-
});
|
|
1125
|
+
: await readInput(args.source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Evidence export input');
|
|
1070
1126
|
}
|
|
1071
1127
|
catch (error) {
|
|
1072
1128
|
if (error instanceof CliUsageError)
|
|
@@ -1080,40 +1136,20 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
1080
1136
|
const now = dependencies.now ? dependencies.now() : new Date().toISOString();
|
|
1081
1137
|
const document = buildCliEvidenceExport(input, evidenceModule, now);
|
|
1082
1138
|
const output = args.format === 'markdown'
|
|
1083
|
-
? formatLookupEvidenceMarkdown(document)
|
|
1139
|
+
? formatLookupEvidenceMarkdown(document, { includeAttribution: args.includeAttribution })
|
|
1084
1140
|
: args.format === 'html'
|
|
1085
|
-
? formatLookupEvidenceHtml(document)
|
|
1141
|
+
? formatLookupEvidenceHtml(document, { includeAttribution: args.includeAttribution })
|
|
1086
1142
|
: formatCliEvidenceExport(document, args.compact);
|
|
1087
1143
|
write(stdout, output);
|
|
1088
1144
|
return EXIT_CODES.SUCCESS;
|
|
1089
1145
|
}
|
|
1090
|
-
if (args.action === 'bulk') {
|
|
1091
|
-
failureLabel = 'Bulk lookup';
|
|
1092
|
-
const { runBulkCommand } = await import("./bulk-command-runner.mjs");
|
|
1093
|
-
return await runBulkCommand(args, dependencies, commandContext);
|
|
1094
|
-
}
|
|
1095
|
-
if (args.action === 'discover') {
|
|
1096
|
-
failureLabel = 'Candidate generation';
|
|
1097
|
-
const { runDiscoveryCommand } = await import("./discovery-command-runner.mjs");
|
|
1098
|
-
return await runDiscoveryCommand(args, dependencies, commandContext);
|
|
1099
|
-
}
|
|
1100
|
-
if (args.action === 'discover-scan') {
|
|
1101
|
-
failureLabel = 'Candidate scan';
|
|
1102
|
-
const { runDiscoveryScanCommand } = await import("./discovery-scan-command-runner.mjs");
|
|
1103
|
-
return await runDiscoveryScanCommand(args, dependencies, commandContext);
|
|
1104
|
-
}
|
|
1105
1146
|
if (args.action === 'ct-intake') {
|
|
1106
1147
|
failureLabel = 'Certificate event intake';
|
|
1107
1148
|
let input;
|
|
1108
1149
|
try {
|
|
1109
1150
|
input = dependencies.readArtifactInput
|
|
1110
1151
|
? await dependencies.readArtifactInput(args.source)
|
|
1111
|
-
: await
|
|
1112
|
-
? createReadStream(args.source, { highWaterMark: 64 * 1024 })
|
|
1113
|
-
: dependencies.stdin || process.stdin, {
|
|
1114
|
-
limit: MAX_CT_EVENT_INPUT_BYTES,
|
|
1115
|
-
label: 'Certificate event input',
|
|
1116
|
-
});
|
|
1152
|
+
: await readInput(args.source, MAX_CT_EVENT_INPUT_BYTES, 'Certificate event input');
|
|
1117
1153
|
}
|
|
1118
1154
|
catch (error) {
|
|
1119
1155
|
if (error instanceof CliUsageError)
|
|
@@ -1124,10 +1160,11 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
1124
1160
|
throw new CliUsageError('ct-intake requires one versioned JSON file or a document on stdin.');
|
|
1125
1161
|
let parsed;
|
|
1126
1162
|
try {
|
|
1163
|
+
scanBoundedJson(input);
|
|
1127
1164
|
parsed = JSON.parse(input);
|
|
1128
1165
|
}
|
|
1129
1166
|
catch {
|
|
1130
|
-
throw new CliUsageError('Certificate event input is not valid JSON.');
|
|
1167
|
+
throw new CliUsageError('Certificate event input is not valid bounded JSON without duplicate keys.');
|
|
1131
1168
|
}
|
|
1132
1169
|
let document;
|
|
1133
1170
|
try {
|
|
@@ -1142,22 +1179,7 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
1142
1179
|
: terminal(formatCtEventFindings(document), args.color));
|
|
1143
1180
|
return EXIT_CODES.SUCCESS;
|
|
1144
1181
|
}
|
|
1145
|
-
|
|
1146
|
-
|| args.action === 'posture'
|
|
1147
|
-
|| args.action === 'http'
|
|
1148
|
-
|| args.action === 'tls') {
|
|
1149
|
-
failureLabel = args.action === 'ct-search'
|
|
1150
|
-
? 'Certificate Transparency search'
|
|
1151
|
-
: args.action === 'posture'
|
|
1152
|
-
? 'Domain posture audit'
|
|
1153
|
-
: args.action === 'http'
|
|
1154
|
-
? 'HTTP probe'
|
|
1155
|
-
: 'TLS intelligence';
|
|
1156
|
-
const { runNetworkCommand } = await import("./network-command-runner.mjs");
|
|
1157
|
-
return await runNetworkCommand(args, dependencies, commandContext);
|
|
1158
|
-
}
|
|
1159
|
-
const { runLookupCommand } = await import("./lookup-command-runner.mjs");
|
|
1160
|
-
return await runLookupCommand(args, dependencies, commandContext);
|
|
1182
|
+
throw new Error('No CLI execution route is registered for the parsed command.');
|
|
1161
1183
|
}
|
|
1162
1184
|
catch (error) {
|
|
1163
1185
|
progress?.stop();
|
|
@@ -1185,8 +1207,41 @@ async function runParsedCli(args, dependencies = {}) {
|
|
|
1185
1207
|
return EXIT_CODES.LOOKUP_FAILED;
|
|
1186
1208
|
}
|
|
1187
1209
|
}
|
|
1188
|
-
async function
|
|
1210
|
+
async function runCliCommand(argv, dependencies = {}) {
|
|
1211
|
+
const stdout = dependencies.stdout || process.stdout;
|
|
1189
1212
|
const stderr = dependencies.stderr || process.stderr;
|
|
1213
|
+
const environment = dependencies.environment || process.env;
|
|
1214
|
+
if (Array.isArray(argv) && argv.length === 0) {
|
|
1215
|
+
const input = (dependencies.stdin || process.stdin);
|
|
1216
|
+
const output = stdout;
|
|
1217
|
+
const supportsInteractiveLaunch = dependencies.canLaunchInteractiveCli || canLaunchInteractiveCli;
|
|
1218
|
+
if (supportsInteractiveLaunch(input, output, environment)) {
|
|
1219
|
+
const launch = dependencies.launchInteractiveCli || launchInteractiveCli;
|
|
1220
|
+
try {
|
|
1221
|
+
const launchedArgv = await launch({
|
|
1222
|
+
input,
|
|
1223
|
+
output,
|
|
1224
|
+
environment,
|
|
1225
|
+
...(dependencies.signal ? { signal: dependencies.signal } : {}),
|
|
1226
|
+
});
|
|
1227
|
+
if (launchedArgv === null)
|
|
1228
|
+
return EXIT_CODES.SUCCESS;
|
|
1229
|
+
argv = launchedArgv;
|
|
1230
|
+
}
|
|
1231
|
+
catch (error) {
|
|
1232
|
+
if (isCancellation(error, dependencies.signal)) {
|
|
1233
|
+
write(stderr, 'Cancelled by analyst.\n');
|
|
1234
|
+
return EXIT_CODES.CANCELLED;
|
|
1235
|
+
}
|
|
1236
|
+
if (error instanceof CliUsageError) {
|
|
1237
|
+
write(stderr, `Usage error: ${boundedCliErrorMessage(error, 'Invalid interactive selection')}\n`);
|
|
1238
|
+
return EXIT_CODES.USAGE;
|
|
1239
|
+
}
|
|
1240
|
+
write(stderr, `CLI startup failed: ${boundedCliErrorMessage(error, 'Interactive launch failed')}\n`);
|
|
1241
|
+
return EXIT_CODES.INTERNAL_ERROR;
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
}
|
|
1190
1245
|
let args;
|
|
1191
1246
|
try {
|
|
1192
1247
|
args = parseCliArguments(argv);
|
|
@@ -1218,4 +1273,25 @@ async function runCli(argv, dependencies = {}) {
|
|
|
1218
1273
|
return EXIT_CODES.LOOKUP_FAILED;
|
|
1219
1274
|
}
|
|
1220
1275
|
}
|
|
1221
|
-
|
|
1276
|
+
async function runCli(argv, dependencies = {}) {
|
|
1277
|
+
const stderr = dependencies.stderr || process.stderr;
|
|
1278
|
+
try {
|
|
1279
|
+
return await runCliCommand(argv, dependencies);
|
|
1280
|
+
}
|
|
1281
|
+
finally {
|
|
1282
|
+
const cleanup = dependencies.cleanupPendingOutputFiles || cleanupPendingOutputFiles;
|
|
1283
|
+
try {
|
|
1284
|
+
const report = await cleanup();
|
|
1285
|
+
if (report.retainedPublished > 0) {
|
|
1286
|
+
write(stderr, `Output cleanup warning: Published output is intact, but ${report.retainedPublished} linked temporary output ${report.retainedPublished === 1 ? 'file remains' : 'files remain'} in the selected output directory.\n`);
|
|
1287
|
+
}
|
|
1288
|
+
if (report.retainedUnpublished > 0) {
|
|
1289
|
+
write(stderr, `Output cleanup warning: ${report.retainedUnpublished} unpublished temporary output ${report.retainedUnpublished === 1 ? 'file remains' : 'files remain'} in the selected output directory.\n`);
|
|
1290
|
+
}
|
|
1291
|
+
}
|
|
1292
|
+
catch {
|
|
1293
|
+
write(stderr, 'Output cleanup warning: Temporary output cleanup could not be verified.\n');
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
export { HELP, INLINE_CLI_COMMANDS, MAX_STDIN_BYTES, VERSION, readStdinBounded, runCli };
|