@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/manual.mjs
CHANGED
|
@@ -12,20 +12,20 @@ function buildCliManual(options) {
|
|
|
12
12
|
}).join('\n');
|
|
13
13
|
return `.TH WHOISLEUTH 1 "" "WHOISleuth ${roffText(options.version)}" "User Commands"
|
|
14
14
|
.SH NAME
|
|
15
|
-
whoisleuth \-
|
|
15
|
+
whoisleuth \- domain investigation from the terminal
|
|
16
16
|
.SH SYNOPSIS
|
|
17
17
|
.B whoisleuth
|
|
18
|
-
command [options]
|
|
18
|
+
[command|target] [options]
|
|
19
19
|
.SH DESCRIPTION
|
|
20
|
-
WHOISleuth performs
|
|
20
|
+
WHOISleuth performs WHOIS, RDAP, DNS, HTTP, TLS, certificate-transparency, posture, lookalike and offline evidence operations with explicit source and collection states. With no arguments, an eligible interactive terminal opens the Lookup and command launcher; unsupported or redirected terminals print help.
|
|
21
21
|
.SH COMMANDS
|
|
22
22
|
${commands}
|
|
23
23
|
.SH OUTPUT
|
|
24
|
-
Human-readable output is the default. Versioned JSON and JSONL are available where documented. Diagnostics and optional progress events
|
|
24
|
+
Human-readable output is the default. Versioned JSON and JSONL are available where documented. Diagnostics and optional progress events use standard error. Use --output and optional --force for atomic private file output. Use --palette auto, light, or dark for a fixed terminal palette. Lookup --browse provides an interactive terminal view.
|
|
25
25
|
.SH EXIT STATUS
|
|
26
26
|
0 indicates command completion, 2 invalid usage, 3 a collection or comparison failure, 4 an explicitly detected partial result, 70 an internal bootstrap failure, 130 analyst cancellation, and 143 service termination.
|
|
27
27
|
.SH PRIVACY
|
|
28
|
-
Network commands disclose the target to the
|
|
28
|
+
Network commands disclose the target to the sources named in focused help. Offline commands read local input only. Output files use private permissions and remain on the operator's machine.
|
|
29
29
|
.SH LICENSE
|
|
30
30
|
AGPL-3.0-only. Copyright 2026 slicedearth.
|
|
31
31
|
.SH SEE ALSO
|
|
@@ -2,6 +2,8 @@ import { fetchHomepage } from "../lib/availability.mjs";
|
|
|
2
2
|
import { searchCertificateTransparency } from "../lib/ct-search.mjs";
|
|
3
3
|
import { checkDomainPosture, normalizeAuditDomain, normalizeDkimSelectors } from "../lib/domain-posture.mjs";
|
|
4
4
|
import { collectTlsIntelligence, normalizeTlsHostname } from "../lib/tls-intelligence.mjs";
|
|
5
|
+
import { MAX_DNSSEC_TRUST_ANCHOR_BYTES, formatDnssecChainReport, validateDnssecChain, } from "../lib/dnssec-chain-validation.mjs";
|
|
6
|
+
import { MAX_MAIL_TRANSPORT_INPUT_BYTES, collectMailTransportReview, formatMailTransportReview, } from "../lib/smtp-transport-review.mjs";
|
|
5
7
|
import { CliUsageError } from "./errors.mjs";
|
|
6
8
|
import { buildCliCtSearchDocument, buildCliHttpDocument, buildCliPostureDocument, buildCliTlsDocument, formatJsonDocument, } from "./formatters/json.mjs";
|
|
7
9
|
import { formatTerminalCtSearch, formatTerminalHttp, formatTerminalPosture, formatTerminalTls, } from "./formatters/terminal.mjs";
|
|
@@ -71,6 +73,82 @@ async function runNetworkCommand(args, dependencies, context) {
|
|
|
71
73
|
}
|
|
72
74
|
return EXIT_CODES.SUCCESS;
|
|
73
75
|
}
|
|
76
|
+
if (args.action === 'dnssec-validate') {
|
|
77
|
+
let anchorInput;
|
|
78
|
+
try {
|
|
79
|
+
anchorInput = dependencies.readTrustAnchorInput
|
|
80
|
+
? await dependencies.readTrustAnchorInput(args.trustAnchorSource)
|
|
81
|
+
: await context.readInput(args.trustAnchorSource, MAX_DNSSEC_TRUST_ANCHOR_BYTES, 'DNSSEC trust anchor');
|
|
82
|
+
}
|
|
83
|
+
catch (error) {
|
|
84
|
+
if (error instanceof CliUsageError)
|
|
85
|
+
throw error;
|
|
86
|
+
throw new CliUsageError(`Could not read DNSSEC trust anchor: ${String(error instanceof Error ? error.message : error).slice(0, 240)}`);
|
|
87
|
+
}
|
|
88
|
+
const validate = dependencies.validateDnssecChain ?? validateDnssecChain;
|
|
89
|
+
const report = await context.withProgress('Validating the isolated DNSSEC chain', () => validate({
|
|
90
|
+
target: args.target,
|
|
91
|
+
resolver: args.resolver,
|
|
92
|
+
trustAnchor: anchorInput,
|
|
93
|
+
observedAt: context.now(),
|
|
94
|
+
ownedOrAuthorized: args.ownedOrAuthorized,
|
|
95
|
+
}));
|
|
96
|
+
if (!args.quiet) {
|
|
97
|
+
context.writeStdout(args.output === 'json'
|
|
98
|
+
? formatJsonDocument(report)
|
|
99
|
+
: context.terminal(formatDnssecChainReport(report), args.color));
|
|
100
|
+
}
|
|
101
|
+
return report.state === 'secure' || report.state === 'insecure'
|
|
102
|
+
? EXIT_CODES.SUCCESS
|
|
103
|
+
: EXIT_CODES.PARTIAL_FAILURE;
|
|
104
|
+
}
|
|
105
|
+
if (args.action === 'mail-transport') {
|
|
106
|
+
let input;
|
|
107
|
+
let anchorInput;
|
|
108
|
+
try {
|
|
109
|
+
input = await (dependencies.readMailTransportInput
|
|
110
|
+
? dependencies.readMailTransportInput(args.source)
|
|
111
|
+
: context.readInput(args.source, MAX_MAIL_TRANSPORT_INPUT_BYTES, 'Mail transport input'));
|
|
112
|
+
}
|
|
113
|
+
catch (error) {
|
|
114
|
+
if (error instanceof CliUsageError)
|
|
115
|
+
throw error;
|
|
116
|
+
throw new CliUsageError(`Could not read mail transport input: ${String(error instanceof Error ? error.message : error).slice(0, 240)}`);
|
|
117
|
+
}
|
|
118
|
+
try {
|
|
119
|
+
anchorInput = await (dependencies.readTrustAnchorInput
|
|
120
|
+
? dependencies.readTrustAnchorInput(args.trustAnchorSource)
|
|
121
|
+
: context.readInput(args.trustAnchorSource, MAX_DNSSEC_TRUST_ANCHOR_BYTES, 'DNSSEC trust anchor'));
|
|
122
|
+
}
|
|
123
|
+
catch (error) {
|
|
124
|
+
if (error instanceof CliUsageError)
|
|
125
|
+
throw error;
|
|
126
|
+
throw new CliUsageError(`Could not read DNSSEC trust anchor: ${String(error instanceof Error ? error.message : error).slice(0, 240)}`);
|
|
127
|
+
}
|
|
128
|
+
if (!input.trim())
|
|
129
|
+
throw new CliUsageError('mail-transport requires one versioned JSON file or a document on stdin.');
|
|
130
|
+
const collect = dependencies.collectMailTransportReview ?? collectMailTransportReview;
|
|
131
|
+
let review;
|
|
132
|
+
try {
|
|
133
|
+
review = await context.withProgress('Reviewing selected authorised mail transports', () => collect(input, {
|
|
134
|
+
resolver: args.resolver,
|
|
135
|
+
trustAnchor: anchorInput,
|
|
136
|
+
ownedOrAuthorized: args.ownedOrAuthorized,
|
|
137
|
+
activeProbeAcknowledged: args.activeProbeAcknowledged,
|
|
138
|
+
}));
|
|
139
|
+
}
|
|
140
|
+
catch (error) {
|
|
141
|
+
if (error instanceof TypeError)
|
|
142
|
+
throw new CliUsageError(error.message);
|
|
143
|
+
throw error;
|
|
144
|
+
}
|
|
145
|
+
if (!args.quiet) {
|
|
146
|
+
context.writeStdout(args.output === 'json'
|
|
147
|
+
? formatJsonDocument(review)
|
|
148
|
+
: context.terminal(formatMailTransportReview(review), args.color));
|
|
149
|
+
}
|
|
150
|
+
return review.runState === 'complete' ? EXIT_CODES.SUCCESS : EXIT_CODES.PARTIAL_FAILURE;
|
|
151
|
+
}
|
|
74
152
|
const requestedHostname = args.hostname || await context.readSingleInput();
|
|
75
153
|
if (!requestedHostname)
|
|
76
154
|
throw new CliUsageError('tls requires one hostname as an argument or on stdin.');
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { Buffer } from 'node:buffer';
|
|
2
|
+
import { scanBoundedJson } from "../lib/bounded-json.mjs";
|
|
2
3
|
import { normalizeEncryptedDnsAdapter, planEncryptedDnsQuery, } from "../lib/encrypted-dns-contract.mjs";
|
|
3
4
|
import { validateDnssecEvidence } from "../lib/dnssec-evidence-validation.mjs";
|
|
4
5
|
import { reviewDomainChange } from "../lib/domain-change-review.mjs";
|
|
@@ -8,14 +9,23 @@ import { buildLocalGeoIpDatabase, lookupLocalGeoIp, } from "../lib/local-geoip-e
|
|
|
8
9
|
import { inspectRdapReverseSearchResponse, normalizeRdapSearchHelp, planRdapReverseSearch, } from "../lib/rdap-search-workbench.mjs";
|
|
9
10
|
import { reviewRpkiRoute } from "../lib/rpki-evidence.mjs";
|
|
10
11
|
import { analyzeTlsaEvidence } from "../lib/tlsa-evidence.mjs";
|
|
12
|
+
import { CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA, buildCryptographicAssuranceReview, } from "../lib/cryptographic-assurance.mjs";
|
|
11
13
|
import { reviewZoneIntent } from "../lib/zone-intent-review.mjs";
|
|
12
14
|
import { reviewDnsConvergence } from "../lib/dns-convergence-review.mjs";
|
|
13
15
|
import { compareTrustStoreEvidence } from "../lib/trust-store-comparison.mjs";
|
|
14
16
|
import { CliUsageError } from "./errors.mjs";
|
|
17
|
+
import { safeTerminalValue } from "./formatters/terminal.mjs";
|
|
15
18
|
import { LOCAL_MMDB_QUERY_SCHEMA, reviewLocalMmdb } from "./local-mmdb-review.mjs";
|
|
16
19
|
const OFFLINE_EVIDENCE_REVIEW_SCHEMA = 'whoisleuth.cli.offline-evidence-review';
|
|
17
20
|
const OFFLINE_EVIDENCE_REVIEW_VERSION = 1;
|
|
18
21
|
const MAX_OFFLINE_EVIDENCE_INPUT_BYTES = 16 * 1024 * 1024;
|
|
22
|
+
const RDAP_SEARCH_INPUT_SCHEMA = 'whoisleuth.rdap-search-input';
|
|
23
|
+
const DNSSEC_EVIDENCE_INPUT_SCHEMA = 'whoisleuth.dnssec-evidence-input';
|
|
24
|
+
const TLSA_EVIDENCE_INPUT_SCHEMA = 'whoisleuth.tlsa-evidence-input';
|
|
25
|
+
const RPKI_ROUTE_INPUT_SCHEMA = 'whoisleuth.rpki-route-input';
|
|
26
|
+
const LOCAL_GEOIP_QUERY_SCHEMA = 'whoisleuth.local-geoip-query';
|
|
27
|
+
const ENCRYPTED_DNS_PLAN_INPUT_SCHEMA = 'whoisleuth.encrypted-dns-plan-input';
|
|
28
|
+
const OFFLINE_EVIDENCE_INPUT_VERSION = 1;
|
|
19
29
|
function record(value) {
|
|
20
30
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
21
31
|
}
|
|
@@ -23,12 +33,14 @@ function parseInput(value) {
|
|
|
23
33
|
if (typeof value !== 'string' || Buffer.byteLength(value, 'utf8') > MAX_OFFLINE_EVIDENCE_INPUT_BYTES) {
|
|
24
34
|
throw new CliUsageError(`Offline evidence input is limited to ${MAX_OFFLINE_EVIDENCE_INPUT_BYTES} bytes.`);
|
|
25
35
|
}
|
|
36
|
+
const normalized = value.replace(/^\uFEFF/u, '');
|
|
26
37
|
let parsed;
|
|
27
38
|
try {
|
|
28
|
-
|
|
39
|
+
scanBoundedJson(normalized);
|
|
40
|
+
parsed = JSON.parse(normalized);
|
|
29
41
|
}
|
|
30
42
|
catch {
|
|
31
|
-
throw new CliUsageError('Offline evidence review requires one valid JSON document.');
|
|
43
|
+
throw new CliUsageError('Offline evidence review requires one valid bounded JSON document without duplicate keys.');
|
|
32
44
|
}
|
|
33
45
|
const document = record(parsed);
|
|
34
46
|
if (document.version !== 1 || typeof document.schema !== 'string') {
|
|
@@ -40,7 +52,7 @@ function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString(
|
|
|
40
52
|
const input = parseInput(value);
|
|
41
53
|
let kind;
|
|
42
54
|
let result;
|
|
43
|
-
if (input.schema ===
|
|
55
|
+
if (input.schema === RDAP_SEARCH_INPUT_SCHEMA) {
|
|
44
56
|
kind = 'rdap_search';
|
|
45
57
|
const request = record(input.request);
|
|
46
58
|
const help = normalizeRdapSearchHelp(input.help);
|
|
@@ -58,7 +70,7 @@ function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString(
|
|
|
58
70
|
: inspectRdapReverseSearchResponse(input.response, [request.property]),
|
|
59
71
|
});
|
|
60
72
|
}
|
|
61
|
-
else if (input.schema ===
|
|
73
|
+
else if (input.schema === DNSSEC_EVIDENCE_INPUT_SCHEMA) {
|
|
62
74
|
kind = 'dnssec';
|
|
63
75
|
result = validateDnssecEvidence({
|
|
64
76
|
ownerName: input.ownerName,
|
|
@@ -69,19 +81,19 @@ function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString(
|
|
|
69
81
|
observedAt: input.observedAt,
|
|
70
82
|
});
|
|
71
83
|
}
|
|
72
|
-
else if (input.schema ===
|
|
84
|
+
else if (input.schema === TLSA_EVIDENCE_INPUT_SCHEMA) {
|
|
73
85
|
kind = 'tlsa';
|
|
74
86
|
result = analyzeTlsaEvidence({
|
|
75
87
|
serviceName: input.serviceName,
|
|
76
|
-
dnssecState:
|
|
77
|
-
pkixValidationState:
|
|
88
|
+
dnssecState: 'unavailable',
|
|
89
|
+
pkixValidationState: 'unavailable',
|
|
78
90
|
records: input.records,
|
|
79
91
|
certificateDerBase64: input.certificateDerBase64,
|
|
80
92
|
spkiDerBase64: input.spkiDerBase64,
|
|
81
93
|
authorityMaterials: input.authorityMaterials,
|
|
82
94
|
});
|
|
83
95
|
}
|
|
84
|
-
else if (input.schema ===
|
|
96
|
+
else if (input.schema === RPKI_ROUTE_INPUT_SCHEMA) {
|
|
85
97
|
kind = 'rpki';
|
|
86
98
|
result = reviewRpkiRoute({
|
|
87
99
|
routePrefix: input.routePrefix,
|
|
@@ -89,11 +101,15 @@ function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString(
|
|
|
89
101
|
authorizations: input.authorizations,
|
|
90
102
|
});
|
|
91
103
|
}
|
|
92
|
-
else if (input.schema ===
|
|
104
|
+
else if (input.schema === CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA) {
|
|
105
|
+
kind = 'cryptographic_assurance';
|
|
106
|
+
result = buildCryptographicAssuranceReview(input, generatedAt);
|
|
107
|
+
}
|
|
108
|
+
else if (input.schema === LOCAL_GEOIP_QUERY_SCHEMA) {
|
|
93
109
|
kind = 'geoip';
|
|
94
110
|
result = lookupLocalGeoIp(buildLocalGeoIpDatabase(input.database), input.address);
|
|
95
111
|
}
|
|
96
|
-
else if (input.schema ===
|
|
112
|
+
else if (input.schema === ENCRYPTED_DNS_PLAN_INPUT_SCHEMA) {
|
|
97
113
|
kind = 'encrypted_dns';
|
|
98
114
|
const query = record(input.query);
|
|
99
115
|
result = planEncryptedDnsQuery(normalizeEncryptedDnsAdapter(input.adapter), { name: query.name, type: query.type });
|
|
@@ -168,7 +184,8 @@ function formatOfflineEvidenceReview(document) {
|
|
|
168
184
|
: 'review'
|
|
169
185
|
: typeof gate.pass === 'boolean'
|
|
170
186
|
? gate.pass ? 'ready' : 'review'
|
|
171
|
-
: document.kind === 'domain_portfolio' ? 'inventory'
|
|
187
|
+
: document.kind === 'domain_portfolio' ? 'inventory'
|
|
188
|
+
: document.kind === 'cryptographic_assurance' ? 'separate' : 'reviewed';
|
|
172
189
|
const lines = [
|
|
173
190
|
'Offline evidence review',
|
|
174
191
|
`Kind ${document.kind.replaceAll('_', ' ')}`,
|
|
@@ -220,6 +237,20 @@ function formatOfflineEvidenceReview(document) {
|
|
|
220
237
|
lines.push(`Recovery ${listLength(result.recoveryCycles)}`);
|
|
221
238
|
lines.push(`Unknown ${Object.values(unknownCounts).reduce((total, value) => total + count(value), 0)}`);
|
|
222
239
|
}
|
|
240
|
+
else if (document.kind === 'cryptographic_assurance') {
|
|
241
|
+
const cards = Array.isArray(result.cards) ? result.cards : [];
|
|
242
|
+
for (const value of cards) {
|
|
243
|
+
const card = record(value);
|
|
244
|
+
lines.push(`${safeTerminalValue(card.label ?? card.family, 'Evidence')} ${safeTerminalValue(card.state, 'unavailable').replaceAll('_', ' ')} (${safeTerminalValue(card.completeness, 'unavailable')})`);
|
|
245
|
+
lines.push(` Source ${safeTerminalValue(card.source, 'not supplied')}`);
|
|
246
|
+
lines.push(` Authority ${safeTerminalValue(card.authority, 'not supplied')}`);
|
|
247
|
+
const cardLimitations = Array.isArray(card.limitations) ? card.limitations : [];
|
|
248
|
+
for (const limitation of cardLimitations) {
|
|
249
|
+
if (typeof limitation === 'string')
|
|
250
|
+
lines.push(` Limitation: ${safeTerminalValue(limitation)}`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
223
254
|
else {
|
|
224
255
|
for (const [label, key] of [
|
|
225
256
|
['Records', 'records'],
|
|
@@ -238,7 +269,7 @@ function formatOfflineEvidenceReview(document) {
|
|
|
238
269
|
lines.push('', 'Review reasons:');
|
|
239
270
|
for (const reason of reasons.slice(0, 8)) {
|
|
240
271
|
if (typeof reason === 'string')
|
|
241
|
-
lines.push(` - ${reason
|
|
272
|
+
lines.push(` - ${safeTerminalValue(reason)}`);
|
|
242
273
|
}
|
|
243
274
|
if (reasons.length > 8)
|
|
244
275
|
lines.push(` - ${reasons.length - 8} more reason(s) omitted.`);
|
|
@@ -249,8 +280,8 @@ function formatOfflineEvidenceReview(document) {
|
|
|
249
280
|
const nestedLimitations = Array.isArray(result.limitations) ? result.limitations : [];
|
|
250
281
|
for (const limitation of nestedLimitations) {
|
|
251
282
|
if (typeof limitation === 'string')
|
|
252
|
-
lines.push(` - ${limitation}`);
|
|
283
|
+
lines.push(` - ${safeTerminalValue(limitation)}`);
|
|
253
284
|
}
|
|
254
285
|
return `${lines.join('\n')}\n`;
|
|
255
286
|
}
|
|
256
|
-
export { MAX_OFFLINE_EVIDENCE_INPUT_BYTES, OFFLINE_EVIDENCE_REVIEW_SCHEMA, OFFLINE_EVIDENCE_REVIEW_VERSION, buildOfflineEvidenceReview, buildOfflineEvidenceReviewWithLocalResources, formatOfflineEvidenceReview, };
|
|
287
|
+
export { DNSSEC_EVIDENCE_INPUT_SCHEMA, ENCRYPTED_DNS_PLAN_INPUT_SCHEMA, LOCAL_GEOIP_QUERY_SCHEMA, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, OFFLINE_EVIDENCE_INPUT_VERSION, OFFLINE_EVIDENCE_REVIEW_SCHEMA, OFFLINE_EVIDENCE_REVIEW_VERSION, RDAP_SEARCH_INPUT_SCHEMA, RPKI_ROUTE_INPUT_SCHEMA, TLSA_EVIDENCE_INPUT_SCHEMA, buildOfflineEvidenceReview, buildOfflineEvidenceReviewWithLocalResources, formatOfflineEvidenceReview, };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isIP } from 'node:net';
|
|
2
|
-
import { parseExternalFindingsDocument, } from "../
|
|
2
|
+
import { parseExternalFindingsDocument, } from "../packages/interchange/external-findings-import.mjs";
|
|
3
3
|
import { requireIsoTimestamp } from "../lib/bounded-contract-normalizers.mjs";
|
|
4
4
|
export const OPEN_ASSET_MODEL_BRIDGE_SCHEMA = 'whoisleuth.open-asset-model-bridge';
|
|
5
5
|
export const OPEN_ASSET_MODEL_BRIDGE_VERSION = 1;
|
package/cli/output-file.mjs
CHANGED
|
@@ -5,7 +5,14 @@ import { basename, dirname, join, resolve } from 'node:path';
|
|
|
5
5
|
import { CliUsageError } from "./errors.mjs";
|
|
6
6
|
export const MAX_CLI_OUTPUT_BYTES = 32 * 1024 * 1024;
|
|
7
7
|
const MAX_CLI_OUTPUT_PATH_LENGTH = 4096;
|
|
8
|
-
const pendingOutputFiles = new
|
|
8
|
+
const pendingOutputFiles = new Map();
|
|
9
|
+
const OUTPUT_FILE_OPERATIONS = Object.freeze({
|
|
10
|
+
randomUUID,
|
|
11
|
+
open,
|
|
12
|
+
link,
|
|
13
|
+
rename,
|
|
14
|
+
unlink,
|
|
15
|
+
});
|
|
9
16
|
function safeOutputPath(value) {
|
|
10
17
|
if (typeof value !== 'string' || !value || value.length > MAX_CLI_OUTPUT_PATH_LENGTH) {
|
|
11
18
|
throw new CliUsageError(`Output paths are limited to ${MAX_CLI_OUTPUT_PATH_LENGTH} characters.`);
|
|
@@ -35,18 +42,22 @@ function createBufferedOutput() {
|
|
|
35
42
|
value: () => chunks.join(''),
|
|
36
43
|
});
|
|
37
44
|
}
|
|
38
|
-
async function writePrivateFile(pathValue, content, options = {}) {
|
|
45
|
+
async function writePrivateFile(pathValue, content, options = {}, operations = OUTPUT_FILE_OPERATIONS) {
|
|
39
46
|
const target = safeOutputPath(pathValue);
|
|
40
47
|
if (Buffer.byteLength(content, 'utf8') > MAX_CLI_OUTPUT_BYTES) {
|
|
41
48
|
throw new CliUsageError(`Generated output is limited to ${MAX_CLI_OUTPUT_BYTES} bytes.`);
|
|
42
49
|
}
|
|
43
50
|
const directory = dirname(target);
|
|
44
|
-
const temporary = join(directory, `.${basename(target)}.${process.pid}.${randomUUID()}.tmp`);
|
|
51
|
+
const temporary = join(directory, `.${basename(target)}.${process.pid}.${operations.randomUUID()}.tmp`);
|
|
45
52
|
let temporaryCreated = false;
|
|
46
53
|
try {
|
|
47
|
-
const handle = await open(temporary, 'wx', 0o600);
|
|
54
|
+
const handle = await operations.open(temporary, 'wx', 0o600);
|
|
48
55
|
temporaryCreated = true;
|
|
49
|
-
pendingOutputFiles.
|
|
56
|
+
pendingOutputFiles.set(temporary, {
|
|
57
|
+
remove: () => operations.unlink(temporary),
|
|
58
|
+
published: false,
|
|
59
|
+
settled: false,
|
|
60
|
+
});
|
|
50
61
|
try {
|
|
51
62
|
await handle.writeFile(content, { encoding: 'utf8' });
|
|
52
63
|
await handle.sync();
|
|
@@ -55,12 +66,12 @@ async function writePrivateFile(pathValue, content, options = {}) {
|
|
|
55
66
|
await handle.close();
|
|
56
67
|
}
|
|
57
68
|
if (options.force) {
|
|
58
|
-
await rename(temporary, target);
|
|
69
|
+
await operations.rename(temporary, target);
|
|
59
70
|
temporaryCreated = false;
|
|
60
71
|
}
|
|
61
72
|
else {
|
|
62
73
|
try {
|
|
63
|
-
await link(temporary, target);
|
|
74
|
+
await operations.link(temporary, target);
|
|
64
75
|
}
|
|
65
76
|
catch (error) {
|
|
66
77
|
const code = error && typeof error === 'object' && 'code' in error ? error.code : null;
|
|
@@ -69,19 +80,64 @@ async function writePrivateFile(pathValue, content, options = {}) {
|
|
|
69
80
|
}
|
|
70
81
|
throw error;
|
|
71
82
|
}
|
|
72
|
-
|
|
73
|
-
|
|
83
|
+
const pending = pendingOutputFiles.get(temporary);
|
|
84
|
+
if (pending)
|
|
85
|
+
pending.published = true;
|
|
86
|
+
try {
|
|
87
|
+
await operations.unlink(temporary);
|
|
88
|
+
temporaryCreated = false;
|
|
89
|
+
}
|
|
90
|
+
catch {
|
|
91
|
+
// The target is already published. A second best-effort cleanup runs
|
|
92
|
+
// below, and signal cleanup retains ownership if it still cannot run.
|
|
93
|
+
}
|
|
74
94
|
}
|
|
75
95
|
return target;
|
|
76
96
|
}
|
|
77
97
|
finally {
|
|
78
|
-
if (temporaryCreated)
|
|
79
|
-
|
|
80
|
-
|
|
98
|
+
if (temporaryCreated) {
|
|
99
|
+
try {
|
|
100
|
+
await operations.unlink(temporary);
|
|
101
|
+
temporaryCreated = false;
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
// Keep the path registered for best-effort signal cleanup.
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
if (temporaryCreated) {
|
|
108
|
+
const pending = pendingOutputFiles.get(temporary);
|
|
109
|
+
if (pending)
|
|
110
|
+
pending.settled = true;
|
|
111
|
+
}
|
|
112
|
+
if (!temporaryCreated)
|
|
113
|
+
pendingOutputFiles.delete(temporary);
|
|
81
114
|
}
|
|
82
115
|
}
|
|
116
|
+
async function cleanupPendingOutputFiles() {
|
|
117
|
+
const entries = [...pendingOutputFiles].filter(([, pending]) => pending.settled);
|
|
118
|
+
let removed = 0;
|
|
119
|
+
for (const [path, pending] of entries) {
|
|
120
|
+
try {
|
|
121
|
+
await pending.remove();
|
|
122
|
+
pendingOutputFiles.delete(path);
|
|
123
|
+
removed += 1;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
// The caller receives an explicit retained count and can disclose that
|
|
127
|
+
// the published output is intact while temporary cleanup remains due.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
const retainedFiles = [...pendingOutputFiles.values()].filter((item) => item.settled);
|
|
131
|
+
return Object.freeze({
|
|
132
|
+
attempted: entries.length,
|
|
133
|
+
removed,
|
|
134
|
+
retained: retainedFiles.length,
|
|
135
|
+
retainedPublished: retainedFiles.filter((item) => item.published).length,
|
|
136
|
+
retainedUnpublished: retainedFiles.filter((item) => !item.published).length,
|
|
137
|
+
});
|
|
138
|
+
}
|
|
83
139
|
function cleanupPendingOutputFilesSync() {
|
|
84
|
-
for (const path of pendingOutputFiles) {
|
|
140
|
+
for (const path of pendingOutputFiles.keys()) {
|
|
85
141
|
try {
|
|
86
142
|
unlinkSync(path);
|
|
87
143
|
}
|
|
@@ -94,4 +150,4 @@ function cleanupPendingOutputFilesSync() {
|
|
|
94
150
|
}
|
|
95
151
|
}
|
|
96
152
|
}
|
|
97
|
-
export { cleanupPendingOutputFilesSync, createBufferedOutput, writePrivateFile };
|
|
153
|
+
export { cleanupPendingOutputFiles, cleanupPendingOutputFilesSync, createBufferedOutput, writePrivateFile };
|
package/cli/page-compare.mjs
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { buildBulkComparisonEvidence } from "../lib/bulk-comparison-evidence.mjs";
|
|
2
|
-
import { createPageBaseline } from "../
|
|
3
|
-
import { comparePageBaselines } from "../
|
|
2
|
+
import { createPageBaseline } from "../packages/workspace/page-baseline.mjs";
|
|
3
|
+
import { comparePageBaselines } from "../packages/comparison/page-similarity.mjs";
|
|
4
4
|
import { CliUsageError } from "./errors.mjs";
|
|
5
5
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
6
|
-
|
|
7
|
-
export
|
|
6
|
+
import { CLI_PAGE_COMPARE_SCHEMA, CLI_PAGE_COMPARE_VERSION, } from "../packages/contracts/offline-comparison.mjs";
|
|
7
|
+
export { CLI_PAGE_COMPARE_SCHEMA, CLI_PAGE_COMPARE_VERSION };
|
|
8
8
|
function record(value) {
|
|
9
9
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
10
10
|
}
|
|
@@ -56,6 +56,9 @@ function buildCliPageComparison(leftText, rightText, generatedAt = new Date().to
|
|
|
56
56
|
&& ['success', 'partial'].includes(right.compact.technology.state);
|
|
57
57
|
const tlsUsable = ['success', 'partial'].includes(left.compact.tls.state)
|
|
58
58
|
&& ['success', 'partial'].includes(right.compact.tls.state);
|
|
59
|
+
const tlsPartial = left.compact.tls.state === 'partial' || right.compact.tls.state === 'partial';
|
|
60
|
+
const technologyPartial = left.compact.technology.truncated || right.compact.technology.truncated
|
|
61
|
+
|| left.compact.technology.state === 'partial' || right.compact.technology.state === 'partial';
|
|
59
62
|
return {
|
|
60
63
|
schema: CLI_PAGE_COMPARE_SCHEMA,
|
|
61
64
|
version: CLI_PAGE_COMPARE_VERSION,
|
|
@@ -72,25 +75,30 @@ function buildCliPageComparison(leftText, rightText, generatedAt = new Date().to
|
|
|
72
75
|
},
|
|
73
76
|
page,
|
|
74
77
|
technology: {
|
|
75
|
-
state: technologyUsable
|
|
78
|
+
state: technologyUsable
|
|
79
|
+
? technologyPartial ? 'partial' : setComparison(leftTechnology, rightTechnology)
|
|
80
|
+
: 'unavailable',
|
|
76
81
|
leftSourceState: left.compact.technology.state,
|
|
77
82
|
rightSourceState: right.compact.technology.state,
|
|
78
83
|
leftIds: leftTechnology,
|
|
79
84
|
rightIds: rightTechnology,
|
|
80
85
|
sharedIds: sharedTechnology,
|
|
81
|
-
partial:
|
|
82
|
-
|| left.compact.technology.state === 'partial' || right.compact.technology.state === 'partial',
|
|
86
|
+
partial: technologyPartial,
|
|
83
87
|
},
|
|
84
88
|
tls: {
|
|
85
89
|
leftSourceState: left.compact.tls.state,
|
|
86
90
|
rightSourceState: right.compact.tls.state,
|
|
87
91
|
issuer: {
|
|
88
|
-
state: tlsUsable
|
|
92
|
+
state: tlsUsable
|
|
93
|
+
? tlsPartial ? 'partial' : scalarComparison(left.compact.tls.issuerLabel, right.compact.tls.issuerLabel)
|
|
94
|
+
: 'unavailable',
|
|
89
95
|
left: left.compact.tls.issuerLabel,
|
|
90
96
|
right: right.compact.tls.issuerLabel,
|
|
91
97
|
},
|
|
92
98
|
publicKey: {
|
|
93
|
-
state: tlsUsable
|
|
99
|
+
state: tlsUsable
|
|
100
|
+
? tlsPartial ? 'partial' : scalarComparison(left.compact.tls.spkiSha256, right.compact.tls.spkiSha256)
|
|
101
|
+
: 'unavailable',
|
|
94
102
|
leftSha256: left.compact.tls.spkiSha256,
|
|
95
103
|
rightSha256: right.compact.tls.spkiSha256,
|
|
96
104
|
},
|
|
@@ -115,7 +123,7 @@ function formatCliPageComparison(document) {
|
|
|
115
123
|
for (const component of document.page.components) {
|
|
116
124
|
lines.push(`${component.label.padEnd(18)} ${component.outcome}`);
|
|
117
125
|
}
|
|
118
|
-
lines.push('', 'Technology', `Relationship ${document.technology.state}`, `Shared ${document.technology.sharedIds.join(', ') || 'None observed'}`, `Left ${document.technology.leftIds.join(', ') || 'Unavailable'}`, `Right ${document.technology.rightIds.join(', ') || 'Unavailable'}`, '', 'TLS', `Issuer ${document.tls.issuer.state}`, `Public key ${document.tls.publicKey.state}`, '', 'Limitations:');
|
|
126
|
+
lines.push('', 'Technology', `Relationship ${document.technology.state}`, `Evidence ${document.technology.partial ? 'Partial; equality and disjointness withheld' : 'Complete retained sets'}`, `Shared ${document.technology.sharedIds.join(', ') || 'None observed'}`, `Left ${document.technology.leftIds.join(', ') || 'Unavailable'}`, `Right ${document.technology.rightIds.join(', ') || 'Unavailable'}`, '', 'TLS', `Issuer ${document.tls.issuer.state}`, `Public key ${document.tls.publicKey.state}`, '', 'Limitations:');
|
|
119
127
|
for (const limitation of document.limitations)
|
|
120
128
|
lines.push(` - ${limitation}`);
|
|
121
129
|
return `${lines.join('\n')}\n`;
|
package/cli/progress-events.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { terminalSafeJson } from "./formatters/json.mjs";
|
|
1
2
|
export const CLI_PROGRESS_EVENT_SCHEMA = 'whoisleuth.cli.progress';
|
|
2
3
|
export const CLI_PROGRESS_EVENT_VERSION = 1;
|
|
3
4
|
const MAX_CLI_PROGRESS_EVENTS = 2_100;
|
|
@@ -45,7 +46,7 @@ function createCliProgressEvents(stream, options) {
|
|
|
45
46
|
...(typeof fields.ok === 'boolean' ? { ok: fields.ok } : {}),
|
|
46
47
|
...(exitCode !== null ? { exitCode } : {}),
|
|
47
48
|
};
|
|
48
|
-
stream.write(`${
|
|
49
|
+
stream.write(`${terminalSafeJson(document)}\n`);
|
|
49
50
|
sequence += 1;
|
|
50
51
|
},
|
|
51
52
|
});
|
package/cli/progress.mjs
CHANGED
|
@@ -10,7 +10,7 @@ function safeProgressMessage(value) {
|
|
|
10
10
|
}
|
|
11
11
|
function createTerminalProgress(stream, options) {
|
|
12
12
|
const environment = options.environment || process.env;
|
|
13
|
-
const presentation = terminalPresentation(stream, options.color, environment);
|
|
13
|
+
const presentation = terminalPresentation(stream, options.color, environment, options.palette);
|
|
14
14
|
const enabled = options.enabled
|
|
15
15
|
&& presentation.interactive
|
|
16
16
|
&& environment.WHOISLEUTH_NO_PROGRESS !== '1';
|
|
@@ -23,8 +23,8 @@ function createTerminalProgress(stream, options) {
|
|
|
23
23
|
if (!enabled || !stream)
|
|
24
24
|
return;
|
|
25
25
|
const elapsedSeconds = Math.max(0, Math.floor((now() - startedAt) / 1000));
|
|
26
|
-
const marker = tone(PROGRESS_FRAMES[frame % PROGRESS_FRAMES.length] || '·', 'accent', presentation.color);
|
|
27
|
-
const elapsed = tone(`${elapsedSeconds}s`, 'dim', presentation.color);
|
|
26
|
+
const marker = tone(PROGRESS_FRAMES[frame % PROGRESS_FRAMES.length] || '·', 'accent', presentation.color ? presentation.palette : false);
|
|
27
|
+
const elapsed = tone(`${elapsedSeconds}s`, 'dim', presentation.color ? presentation.palette : false);
|
|
28
28
|
stream.write(`\r\u001b[2K${marker} ${message} · ${elapsed}`);
|
|
29
29
|
frame += 1;
|
|
30
30
|
}
|