@slicedearth/whoisleuth-cli 1.43.0 → 1.47.4
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 +67 -10
- package/bin/whoisleuth.mjs +22 -9
- package/cli/arguments.mjs +256 -9
- package/cli/artifact-structure.mjs +1833 -0
- package/cli/artifact-verify.mjs +242 -49
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +67 -31
- package/cli/bulk-command-runner.mjs +16 -6
- package/cli/bulk.mjs +18 -7
- package/cli/case-pack.mjs +819 -34
- package/cli/command-reference.mjs +48 -24
- package/cli/completion.mjs +50 -11
- package/cli/config-profile.mjs +15 -12
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discovery-observation-snapshot.mjs +8 -14
- package/cli/discovery-scan-command-runner.mjs +18 -6
- package/cli/discovery-snapshot.mjs +8 -15
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +21 -15
- package/cli/domain-control-observations.mjs +25 -2
- package/cli/errors.mjs +7 -1
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +73 -45
- package/cli/export-evidence.mjs +11 -4
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +12 -4
- package/cli/formatters/terminal.mjs +4 -1
- package/cli/http.mjs +2 -1
- package/cli/input.mjs +57 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-run.mjs +71 -23
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookup-command-runner.mjs +2 -2
- package/cli/lookup-diff.mjs +9 -0
- package/cli/lookup-reconcile.mjs +1 -1
- package/cli/mail-review.mjs +5 -5
- package/cli/manual.mjs +1 -1
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +28 -8
- package/cli/page-compare.mjs +8 -5
- package/cli/registry-cohort.mjs +385 -61
- package/cli/registry-fixture-scaffold.mjs +4 -3
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +376 -0
- package/cli/risk-calibration.mjs +7 -5
- package/cli/runner.mjs +150 -177
- package/cli/saved-lookup.mjs +26 -3
- package/cli/sharing-review.mjs +12 -6
- package/docs/cli-reference.md +467 -74
- package/docs/cli.md +25 -5
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
- package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +33 -7
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
- package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
- package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
- package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
- package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
- package/frontend/src/lib/analysis/bulk-sort.js +4 -0
- package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +9 -1
- package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
- package/frontend/src/lib/analysis/case-migration-model.js +83 -31
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-core.js +1 -1
- package/frontend/src/lib/analysis/case-record-model.js +2 -0
- package/frontend/src/lib/analysis/case-record-operations.js +43 -13
- package/frontend/src/lib/analysis/case-report.js +39 -6
- package/frontend/src/lib/analysis/case-response-model.js +66 -7
- package/frontend/src/lib/analysis/case-response-packet.js +15 -5
- package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
- package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
- package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
- package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
- package/frontend/src/lib/analysis/domain-control-records.js +88 -0
- package/frontend/src/lib/analysis/external-findings-import.js +70 -6
- package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
- package/frontend/src/lib/analysis/investigation-guide.js +52 -10
- package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
- package/frontend/src/lib/analysis/lookup-asset-graph.js +8 -8
- package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
- package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +63 -8
- package/frontend/src/lib/analysis/lookup-presentation.js +70 -14
- package/frontend/src/lib/analysis/lookup-source-refresh.js +36 -63
- package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
- package/frontend/src/lib/analysis/utils.js +27 -4
- package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
- package/frontend/src/lib/analysis/visualization-models.js +63 -94
- package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
- package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -4
- package/frontend/src/lib/analysis/workspace-archive.js +105 -19
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +14 -12
- package/lib/bounded-file.mjs +82 -0
- package/lib/bounded-json-response.mjs +170 -0
- package/lib/bounded-json.mjs +249 -0
- package/lib/browser-library-profile.mjs +167 -38
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/cryptographic-assurance.mjs +155 -0
- package/lib/ct-search.mjs +87 -47
- package/lib/dns-intelligence.mjs +3 -2
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1053 -0
- package/lib/dnssec-evidence-validation.mjs +10 -3
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-manifest.mjs +17 -129
- package/lib/domain-posture.mjs +7 -4
- package/lib/evidence-export.mjs +751 -78
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/favicon.mjs +8 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +2 -5
- package/lib/interchange-fidelity-registry.mjs +122 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +5 -1
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup-response-contract.mjs +691 -3
- package/lib/lookup.mjs +2 -2
- package/lib/observation.mjs +7 -6
- package/lib/outbound-identity.mjs +2 -3
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +21 -0
- package/lib/provider-policy-admission.mjs +9 -4
- package/lib/rdap-normalization.mjs +18 -5
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +6 -6
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/risk-calibration-summary.mjs +338 -0
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +96 -42
- package/lib/security-txt.mjs +26 -3
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +784 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/threat-intelligence-runtime.mjs +4 -13
- package/lib/threat-intelligence-types.mjs +18 -1
- package/lib/tls-intelligence.mjs +11 -5
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/website-technology.mjs +2 -6
- package/package.json +1 -1
- package/third-party-notices.txt +95 -5817
|
@@ -1,9 +1,11 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { constants as fsConstants } from 'node:fs';
|
|
2
|
+
import { open, realpath } from 'node:fs/promises';
|
|
2
3
|
import { isIP } from 'node:net';
|
|
3
|
-
import {
|
|
4
|
+
import { Reader } from 'maxmind';
|
|
4
5
|
import { formatIpPrefix, parseIpPrefix } from "../lib/ip-prefix.mjs";
|
|
5
6
|
import { CliUsageError } from "./errors.mjs";
|
|
6
7
|
export const LOCAL_MMDB_QUERY_SCHEMA = 'whoisleuth.local-mmdb-query';
|
|
8
|
+
export const LOCAL_MMDB_QUERY_VERSION = 1;
|
|
7
9
|
export const MAX_LOCAL_MMDB_BYTES = 512 * 1024 * 1024;
|
|
8
10
|
function record(value) {
|
|
9
11
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
@@ -17,6 +19,43 @@ function names(value) {
|
|
|
17
19
|
const source = record(value);
|
|
18
20
|
return boundedText(source.en, 120);
|
|
19
21
|
}
|
|
22
|
+
async function readStableMmdb(databasePath) {
|
|
23
|
+
let handle = null;
|
|
24
|
+
try {
|
|
25
|
+
const resolvedPath = await realpath(databasePath);
|
|
26
|
+
handle = await open(resolvedPath, fsConstants.O_RDONLY | fsConstants.O_NONBLOCK | fsConstants.O_NOFOLLOW);
|
|
27
|
+
const before = await handle.stat();
|
|
28
|
+
if (!before.isFile() || before.size <= 0 || before.size > MAX_LOCAL_MMDB_BYTES) {
|
|
29
|
+
throw new CliUsageError(`The supplied MMDB must be a file no larger than ${MAX_LOCAL_MMDB_BYTES} bytes.`);
|
|
30
|
+
}
|
|
31
|
+
const database = Buffer.allocUnsafe(before.size);
|
|
32
|
+
let offset = 0;
|
|
33
|
+
while (offset < database.length) {
|
|
34
|
+
const { bytesRead } = await handle.read(database, offset, database.length - offset, offset);
|
|
35
|
+
if (bytesRead === 0)
|
|
36
|
+
break;
|
|
37
|
+
offset += bytesRead;
|
|
38
|
+
}
|
|
39
|
+
const after = await handle.stat();
|
|
40
|
+
if (offset !== database.length
|
|
41
|
+
|| before.dev !== after.dev
|
|
42
|
+
|| before.ino !== after.ino
|
|
43
|
+
|| before.size !== after.size
|
|
44
|
+
|| before.mtimeMs !== after.mtimeMs
|
|
45
|
+
|| before.ctimeMs !== after.ctimeMs) {
|
|
46
|
+
throw new CliUsageError('The supplied MMDB changed while it was being read.');
|
|
47
|
+
}
|
|
48
|
+
return database;
|
|
49
|
+
}
|
|
50
|
+
catch (cause) {
|
|
51
|
+
if (cause instanceof CliUsageError)
|
|
52
|
+
throw cause;
|
|
53
|
+
throw new CliUsageError('The supplied MMDB file could not be read.');
|
|
54
|
+
}
|
|
55
|
+
finally {
|
|
56
|
+
await handle?.close().catch(() => { });
|
|
57
|
+
}
|
|
58
|
+
}
|
|
20
59
|
export async function reviewLocalMmdb(input, databasePath) {
|
|
21
60
|
const address = boundedText(input.address, 64)?.toLowerCase() ?? '';
|
|
22
61
|
const sourceLabel = boundedText(input.sourceLabel, 120);
|
|
@@ -27,21 +66,13 @@ export async function reviewLocalMmdb(input, databasePath) {
|
|
|
27
66
|
if (!sourceLabel || !databaseVersion || !license) {
|
|
28
67
|
throw new CliUsageError('Local MMDB review requires bounded sourceLabel, databaseVersion, and licence metadata.');
|
|
29
68
|
}
|
|
30
|
-
let info;
|
|
31
|
-
try {
|
|
32
|
-
info = await stat(databasePath);
|
|
33
|
-
}
|
|
34
|
-
catch {
|
|
35
|
-
throw new CliUsageError('The supplied MMDB file could not be read.');
|
|
36
|
-
}
|
|
37
|
-
if (!info.isFile() || info.size <= 0 || info.size > MAX_LOCAL_MMDB_BYTES) {
|
|
38
|
-
throw new CliUsageError(`The supplied MMDB must be a file no larger than ${MAX_LOCAL_MMDB_BYTES} bytes.`);
|
|
39
|
-
}
|
|
40
69
|
let reader;
|
|
41
70
|
try {
|
|
42
|
-
reader = await
|
|
71
|
+
reader = new Reader(await readStableMmdb(databasePath));
|
|
43
72
|
}
|
|
44
|
-
catch {
|
|
73
|
+
catch (cause) {
|
|
74
|
+
if (cause instanceof CliUsageError)
|
|
75
|
+
throw cause;
|
|
45
76
|
throw new CliUsageError('The supplied file is not a readable MaxMind DB database.');
|
|
46
77
|
}
|
|
47
78
|
const [rawMatch, prefixLength] = reader.getWithPrefixLength(address);
|
|
@@ -84,8 +84,8 @@ async function runLookupCommand(args, dependencies, context) {
|
|
|
84
84
|
const evidenceModule = await loadEvidence();
|
|
85
85
|
const report = buildCliEvidenceExport(JSON.stringify(document), evidenceModule, now);
|
|
86
86
|
context.writeStdout(args.output === 'markdown'
|
|
87
|
-
? formatLookupEvidenceMarkdown(report)
|
|
88
|
-
: formatLookupEvidenceHtml(report));
|
|
87
|
+
? formatLookupEvidenceMarkdown(report, { includeAttribution: args.includeAttribution })
|
|
88
|
+
: formatLookupEvidenceHtml(report, { includeAttribution: args.includeAttribution }));
|
|
89
89
|
}
|
|
90
90
|
else {
|
|
91
91
|
context.writeStdout(context.terminal(formatTerminalLookup(document, { detail: args.detail }), args.color));
|
package/cli/lookup-diff.mjs
CHANGED
|
@@ -95,6 +95,9 @@ function lookupComparisonInput(document) {
|
|
|
95
95
|
faviconMatch: false,
|
|
96
96
|
faviconNearMatch: false,
|
|
97
97
|
reusesOfficialAssets: false,
|
|
98
|
+
idnReferenceMatch: false,
|
|
99
|
+
pageBaselineMatch: false,
|
|
100
|
+
hasActiveBrandProfile: false,
|
|
98
101
|
hasPasswordField: recordOrEmpty(recordOrEmpty(availability.credentialSurfaceProfile).inputs).categories
|
|
99
102
|
? Number(recordOrEmpty(recordOrEmpty(recordOrEmpty(availability.credentialSurfaceProfile).inputs).categories).password) > 0
|
|
100
103
|
: false,
|
|
@@ -115,6 +118,12 @@ function lookupComparisonInput(document) {
|
|
|
115
118
|
comparisonEvidence,
|
|
116
119
|
relationship,
|
|
117
120
|
sourceCoverage: sourceCoverage(document, availability),
|
|
121
|
+
profileContext: {
|
|
122
|
+
sourceState: 'ready',
|
|
123
|
+
activeProfileId: null,
|
|
124
|
+
profileUpdatedAt: null,
|
|
125
|
+
limitation: '',
|
|
126
|
+
},
|
|
118
127
|
};
|
|
119
128
|
}
|
|
120
129
|
function buildCliLookupDiff(leftText, rightText, generatedAt = new Date().toISOString(), options = {}) {
|
package/cli/lookup-reconcile.mjs
CHANGED
|
@@ -37,7 +37,7 @@ function buildCliLookupReconciliation(inputs, generatedAt = new Date().toISOStri
|
|
|
37
37
|
label: `Lookup reconciliation observation ${index + 1}`,
|
|
38
38
|
}));
|
|
39
39
|
const domain = documents[0]?.registrableDomain;
|
|
40
|
-
if (!domain || documents.some((
|
|
40
|
+
if (!domain || documents.some((candidate) => candidate.registrableDomain !== domain)) {
|
|
41
41
|
throw new CliUsageError('Lookup reconciliation requires observations for one domain.');
|
|
42
42
|
}
|
|
43
43
|
const comparisons = documents.slice(1).map((document, index) => buildCliLookupDiff(inputs[0], inputs[index + 1], generatedAt, { domainMode: 'same' }));
|
package/cli/mail-review.mjs
CHANGED
|
@@ -128,14 +128,14 @@ function parseInput(textValue) {
|
|
|
128
128
|
try {
|
|
129
129
|
const parsed = JSON.parse(textInput);
|
|
130
130
|
const document = record(parsed);
|
|
131
|
-
if (document.schema === 'whoisleuth.cli.bulk' && document.version
|
|
131
|
+
if (document.schema === 'whoisleuth.cli.bulk' && [2, 3].includes(Number(document.version)) && Array.isArray(document.results)) {
|
|
132
132
|
values = document.results;
|
|
133
133
|
}
|
|
134
|
-
else if (document.schema === 'whoisleuth.cli.bulk.item' && document.version
|
|
134
|
+
else if (document.schema === 'whoisleuth.cli.bulk.item' && [2, 3].includes(Number(document.version))) {
|
|
135
135
|
values = [document];
|
|
136
136
|
}
|
|
137
137
|
else {
|
|
138
|
-
throw new CliUsageError('Mail review requires WHOISleuth Bulk JSON schema version 2.');
|
|
138
|
+
throw new CliUsageError('Mail review requires WHOISleuth Bulk JSON schema version 2 or 3.');
|
|
139
139
|
}
|
|
140
140
|
}
|
|
141
141
|
catch (error) {
|
|
@@ -152,8 +152,8 @@ function parseInput(textValue) {
|
|
|
152
152
|
throw new CliUsageError(`Mail review supports between 1 and ${MAX_MAIL_REVIEW_ROWS} Bulk rows.`);
|
|
153
153
|
}
|
|
154
154
|
const rows = values.map((value) => record(value));
|
|
155
|
-
if (rows.some((value) => value.schema !== 'whoisleuth.cli.bulk.item' || value.version
|
|
156
|
-
throw new CliUsageError('Every Mail review row must use WHOISleuth Bulk item schema version 2.');
|
|
155
|
+
if (rows.some((value) => value.schema !== 'whoisleuth.cli.bulk.item' || ![2, 3].includes(Number(value.version)))) {
|
|
156
|
+
throw new CliUsageError('Every Mail review row must use WHOISleuth Bulk item schema version 2 or 3.');
|
|
157
157
|
}
|
|
158
158
|
return rows;
|
|
159
159
|
}
|
package/cli/manual.mjs
CHANGED
|
@@ -23,7 +23,7 @@ ${commands}
|
|
|
23
23
|
.SH OUTPUT
|
|
24
24
|
Human-readable output is the default. Versioned JSON and JSONL are available where documented. Diagnostics and optional progress events are written to standard error. Use --output with an optional --force flag for atomic private file output.
|
|
25
25
|
.SH EXIT STATUS
|
|
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,
|
|
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
28
|
Network commands disclose the target to the directly queried upstream services. Offline commands do not make network requests. Output files are created with private permissions and are never uploaded by the CLI.
|
|
29
29
|
.SH LICENSE
|
|
@@ -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.');
|
|
@@ -8,6 +8,7 @@ import { buildLocalGeoIpDatabase, lookupLocalGeoIp, } from "../lib/local-geoip-e
|
|
|
8
8
|
import { inspectRdapReverseSearchResponse, normalizeRdapSearchHelp, planRdapReverseSearch, } from "../lib/rdap-search-workbench.mjs";
|
|
9
9
|
import { reviewRpkiRoute } from "../lib/rpki-evidence.mjs";
|
|
10
10
|
import { analyzeTlsaEvidence } from "../lib/tlsa-evidence.mjs";
|
|
11
|
+
import { CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA, buildCryptographicAssuranceReview, } from "../lib/cryptographic-assurance.mjs";
|
|
11
12
|
import { reviewZoneIntent } from "../lib/zone-intent-review.mjs";
|
|
12
13
|
import { reviewDnsConvergence } from "../lib/dns-convergence-review.mjs";
|
|
13
14
|
import { compareTrustStoreEvidence } from "../lib/trust-store-comparison.mjs";
|
|
@@ -16,6 +17,13 @@ import { LOCAL_MMDB_QUERY_SCHEMA, reviewLocalMmdb } from "./local-mmdb-review.mj
|
|
|
16
17
|
const OFFLINE_EVIDENCE_REVIEW_SCHEMA = 'whoisleuth.cli.offline-evidence-review';
|
|
17
18
|
const OFFLINE_EVIDENCE_REVIEW_VERSION = 1;
|
|
18
19
|
const MAX_OFFLINE_EVIDENCE_INPUT_BYTES = 16 * 1024 * 1024;
|
|
20
|
+
const RDAP_SEARCH_INPUT_SCHEMA = 'whoisleuth.rdap-search-input';
|
|
21
|
+
const DNSSEC_EVIDENCE_INPUT_SCHEMA = 'whoisleuth.dnssec-evidence-input';
|
|
22
|
+
const TLSA_EVIDENCE_INPUT_SCHEMA = 'whoisleuth.tlsa-evidence-input';
|
|
23
|
+
const RPKI_ROUTE_INPUT_SCHEMA = 'whoisleuth.rpki-route-input';
|
|
24
|
+
const LOCAL_GEOIP_QUERY_SCHEMA = 'whoisleuth.local-geoip-query';
|
|
25
|
+
const ENCRYPTED_DNS_PLAN_INPUT_SCHEMA = 'whoisleuth.encrypted-dns-plan-input';
|
|
26
|
+
const OFFLINE_EVIDENCE_INPUT_VERSION = 1;
|
|
19
27
|
function record(value) {
|
|
20
28
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
21
29
|
}
|
|
@@ -40,7 +48,7 @@ function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString(
|
|
|
40
48
|
const input = parseInput(value);
|
|
41
49
|
let kind;
|
|
42
50
|
let result;
|
|
43
|
-
if (input.schema ===
|
|
51
|
+
if (input.schema === RDAP_SEARCH_INPUT_SCHEMA) {
|
|
44
52
|
kind = 'rdap_search';
|
|
45
53
|
const request = record(input.request);
|
|
46
54
|
const help = normalizeRdapSearchHelp(input.help);
|
|
@@ -58,7 +66,7 @@ function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString(
|
|
|
58
66
|
: inspectRdapReverseSearchResponse(input.response, [request.property]),
|
|
59
67
|
});
|
|
60
68
|
}
|
|
61
|
-
else if (input.schema ===
|
|
69
|
+
else if (input.schema === DNSSEC_EVIDENCE_INPUT_SCHEMA) {
|
|
62
70
|
kind = 'dnssec';
|
|
63
71
|
result = validateDnssecEvidence({
|
|
64
72
|
ownerName: input.ownerName,
|
|
@@ -69,7 +77,7 @@ function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString(
|
|
|
69
77
|
observedAt: input.observedAt,
|
|
70
78
|
});
|
|
71
79
|
}
|
|
72
|
-
else if (input.schema ===
|
|
80
|
+
else if (input.schema === TLSA_EVIDENCE_INPUT_SCHEMA) {
|
|
73
81
|
kind = 'tlsa';
|
|
74
82
|
result = analyzeTlsaEvidence({
|
|
75
83
|
serviceName: input.serviceName,
|
|
@@ -81,7 +89,7 @@ function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString(
|
|
|
81
89
|
authorityMaterials: input.authorityMaterials,
|
|
82
90
|
});
|
|
83
91
|
}
|
|
84
|
-
else if (input.schema ===
|
|
92
|
+
else if (input.schema === RPKI_ROUTE_INPUT_SCHEMA) {
|
|
85
93
|
kind = 'rpki';
|
|
86
94
|
result = reviewRpkiRoute({
|
|
87
95
|
routePrefix: input.routePrefix,
|
|
@@ -89,11 +97,15 @@ function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString(
|
|
|
89
97
|
authorizations: input.authorizations,
|
|
90
98
|
});
|
|
91
99
|
}
|
|
92
|
-
else if (input.schema ===
|
|
100
|
+
else if (input.schema === CRYPTOGRAPHIC_ASSURANCE_INPUT_SCHEMA) {
|
|
101
|
+
kind = 'cryptographic_assurance';
|
|
102
|
+
result = buildCryptographicAssuranceReview(input, generatedAt);
|
|
103
|
+
}
|
|
104
|
+
else if (input.schema === LOCAL_GEOIP_QUERY_SCHEMA) {
|
|
93
105
|
kind = 'geoip';
|
|
94
106
|
result = lookupLocalGeoIp(buildLocalGeoIpDatabase(input.database), input.address);
|
|
95
107
|
}
|
|
96
|
-
else if (input.schema ===
|
|
108
|
+
else if (input.schema === ENCRYPTED_DNS_PLAN_INPUT_SCHEMA) {
|
|
97
109
|
kind = 'encrypted_dns';
|
|
98
110
|
const query = record(input.query);
|
|
99
111
|
result = planEncryptedDnsQuery(normalizeEncryptedDnsAdapter(input.adapter), { name: query.name, type: query.type });
|
|
@@ -168,7 +180,8 @@ function formatOfflineEvidenceReview(document) {
|
|
|
168
180
|
: 'review'
|
|
169
181
|
: typeof gate.pass === 'boolean'
|
|
170
182
|
? gate.pass ? 'ready' : 'review'
|
|
171
|
-
: document.kind === 'domain_portfolio' ? 'inventory'
|
|
183
|
+
: document.kind === 'domain_portfolio' ? 'inventory'
|
|
184
|
+
: document.kind === 'cryptographic_assurance' ? 'separate' : 'reviewed';
|
|
172
185
|
const lines = [
|
|
173
186
|
'Offline evidence review',
|
|
174
187
|
`Kind ${document.kind.replaceAll('_', ' ')}`,
|
|
@@ -220,6 +233,13 @@ function formatOfflineEvidenceReview(document) {
|
|
|
220
233
|
lines.push(`Recovery ${listLength(result.recoveryCycles)}`);
|
|
221
234
|
lines.push(`Unknown ${Object.values(unknownCounts).reduce((total, value) => total + count(value), 0)}`);
|
|
222
235
|
}
|
|
236
|
+
else if (document.kind === 'cryptographic_assurance') {
|
|
237
|
+
const cards = Array.isArray(result.cards) ? result.cards : [];
|
|
238
|
+
for (const value of cards) {
|
|
239
|
+
const card = record(value);
|
|
240
|
+
lines.push(`${String(card.label ?? card.family ?? 'Evidence')} ${String(card.state ?? 'unavailable').replaceAll('_', ' ')} (${String(card.completeness ?? 'unavailable')})`);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
223
243
|
else {
|
|
224
244
|
for (const [label, key] of [
|
|
225
245
|
['Records', 'records'],
|
|
@@ -253,4 +273,4 @@ function formatOfflineEvidenceReview(document) {
|
|
|
253
273
|
}
|
|
254
274
|
return `${lines.join('\n')}\n`;
|
|
255
275
|
}
|
|
256
|
-
export { MAX_OFFLINE_EVIDENCE_INPUT_BYTES, OFFLINE_EVIDENCE_REVIEW_SCHEMA, OFFLINE_EVIDENCE_REVIEW_VERSION, buildOfflineEvidenceReview, buildOfflineEvidenceReviewWithLocalResources, formatOfflineEvidenceReview, };
|
|
276
|
+
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, };
|
package/cli/page-compare.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { comparePageBaselines } from "../frontend/src/lib/analysis/page-similari
|
|
|
4
4
|
import { CliUsageError } from "./errors.mjs";
|
|
5
5
|
import { parseSavedLookupDocument } from "./saved-lookup.mjs";
|
|
6
6
|
export const CLI_PAGE_COMPARE_SCHEMA = 'whoisleuth.cli.page-compare';
|
|
7
|
-
export const CLI_PAGE_COMPARE_VERSION =
|
|
7
|
+
export const CLI_PAGE_COMPARE_VERSION = 3;
|
|
8
8
|
function record(value) {
|
|
9
9
|
return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
|
|
10
10
|
}
|
|
@@ -56,6 +56,8 @@ 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 technologyPartial = left.compact.technology.truncated || right.compact.technology.truncated
|
|
60
|
+
|| left.compact.technology.state === 'partial' || right.compact.technology.state === 'partial';
|
|
59
61
|
return {
|
|
60
62
|
schema: CLI_PAGE_COMPARE_SCHEMA,
|
|
61
63
|
version: CLI_PAGE_COMPARE_VERSION,
|
|
@@ -72,14 +74,15 @@ function buildCliPageComparison(leftText, rightText, generatedAt = new Date().to
|
|
|
72
74
|
},
|
|
73
75
|
page,
|
|
74
76
|
technology: {
|
|
75
|
-
state: technologyUsable
|
|
77
|
+
state: technologyUsable
|
|
78
|
+
? technologyPartial ? 'partial' : setComparison(leftTechnology, rightTechnology)
|
|
79
|
+
: 'unavailable',
|
|
76
80
|
leftSourceState: left.compact.technology.state,
|
|
77
81
|
rightSourceState: right.compact.technology.state,
|
|
78
82
|
leftIds: leftTechnology,
|
|
79
83
|
rightIds: rightTechnology,
|
|
80
84
|
sharedIds: sharedTechnology,
|
|
81
|
-
partial:
|
|
82
|
-
|| left.compact.technology.state === 'partial' || right.compact.technology.state === 'partial',
|
|
85
|
+
partial: technologyPartial,
|
|
83
86
|
},
|
|
84
87
|
tls: {
|
|
85
88
|
leftSourceState: left.compact.tls.state,
|
|
@@ -115,7 +118,7 @@ function formatCliPageComparison(document) {
|
|
|
115
118
|
for (const component of document.page.components) {
|
|
116
119
|
lines.push(`${component.label.padEnd(18)} ${component.outcome}`);
|
|
117
120
|
}
|
|
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:');
|
|
121
|
+
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
122
|
for (const limitation of document.limitations)
|
|
120
123
|
lines.push(` - ${limitation}`);
|
|
121
124
|
return `${lines.join('\n')}\n`;
|