@slicedearth/whoisleuth-cli 2.3.0 → 2.4.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/NOTICE +6 -0
- package/README.md +19 -15
- package/bin/whoisleuth.mjs +8 -1
- package/cli/archive-content-digest.mjs +14 -0
- package/cli/archive-inspect.mjs +20 -53
- package/cli/arguments.mjs +68 -6
- package/cli/artifact-validation/case-response.mjs +47 -41
- package/cli/artifact-validation/investigation-capsule.mjs +52 -31
- package/cli/artifact-validation/lookup-evidence.mjs +12 -6
- package/cli/artifact-validation/signed-review.mjs +83 -61
- package/cli/artifact-validation/structure-primitives.mjs +1 -105
- package/cli/artifact-verify.mjs +90 -24
- package/cli/assurance-command-runner.mjs +39 -4
- package/cli/bagit-folder.mjs +57 -0
- package/cli/bagit-review.mjs +23 -0
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-command-runner.mjs +2 -0
- package/cli/bulk-output.mjs +29 -3
- package/cli/case-command.mjs +195 -0
- package/cli/case-pack.mjs +39 -31
- package/cli/command-argument-grammar.mjs +4 -0
- package/cli/command-reference.mjs +145 -75
- package/cli/completion.mjs +298 -79
- package/cli/config-profile.mjs +7 -5
- package/cli/ct-event-intake.mjs +12 -12
- package/cli/discover.mjs +4 -3
- package/cli/discovery-observation-snapshot.mjs +44 -16
- package/cli/discovery-scan-command-runner.mjs +2 -0
- package/cli/discovery-scan.mjs +7 -4
- package/cli/discovery-workflow.mjs +1 -1
- package/cli/domain-control-monitor.mjs +50 -20
- package/cli/domain-control-observations.mjs +149 -102
- package/cli/errors.mjs +33 -1
- package/cli/evidence-command-runner.mjs +16 -5
- package/cli/evidence-signing.mjs +2 -2
- package/cli/external-observation-mapping.mjs +7 -8
- package/cli/formatters/terminal-lookup.mjs +18 -46
- package/cli/formatters/terminal-shared.mjs +5 -3
- package/cli/formatters/terminal-technology.mjs +114 -0
- package/cli/inline-command-families.mjs +1 -0
- package/cli/interactive-launcher.mjs +2 -160
- package/cli/interchange-report.mjs +4 -8
- package/cli/investigation-artifacts.mjs +176 -0
- package/cli/investigation-checkpoint.mjs +10 -0
- package/cli/investigation-folder.mjs +114 -0
- package/cli/investigation-manifest.mjs +1 -112
- package/cli/investigation-package-review.mjs +103 -0
- package/cli/investigation-plan.mjs +32 -19
- package/cli/investigation-run.mjs +188 -33
- package/cli/local-document-checkpoint.mjs +170 -0
- package/cli/lookup-browser.mjs +5 -12
- package/cli/lookup-command-runner.mjs +17 -6
- package/cli/lookup-diff.mjs +37 -3
- package/cli/lookup-plan.mjs +11 -5
- package/cli/lookup-reconcile.mjs +10 -8
- package/cli/mail-review.mjs +66 -14
- package/cli/manual.mjs +1 -0
- package/cli/network-command-runner.mjs +2 -0
- package/cli/output-file.mjs +31 -5
- package/cli/registry-doctor.mjs +3 -1
- package/cli/registry-support.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +2 -2
- package/cli/review-command-runner.mjs +45 -0
- package/cli/runner.mjs +55 -6
- package/cli/sharing-review.mjs +21 -34
- package/cli/signer-trust.mjs +104 -0
- package/cli/source-reliability.mjs +74 -83
- package/cli/terminal-input.mjs +168 -0
- package/cli/workflow-command-runner.mjs +68 -13
- package/docs/cli-reference.md +76 -30
- package/docs/cli.md +289 -25
- package/lib/abort.mjs +4 -1
- package/lib/availability.mjs +161 -112
- package/lib/bounded-file.mjs +3 -1
- package/lib/bounded-html-document.mjs +125 -0
- package/lib/bounded-json.mjs +43 -3
- package/lib/browser-library-profile.mjs +42 -51
- package/lib/browser-library-worker.mjs +112 -0
- package/lib/client-behavior-profile.mjs +3 -3
- package/lib/ct-search.mjs +11 -4
- package/lib/dns-delegation-health.mjs +24 -14
- package/lib/dns-inheritance-review.mjs +217 -0
- package/lib/dns-intelligence.mjs +36 -23
- package/lib/dns-mx.mjs +1 -1
- package/lib/dnssec-evidence-validation.mjs +11 -3
- package/lib/domain-change-packet.mjs +7 -2
- package/lib/domain-change-review.mjs +14 -8
- package/lib/domain-control-flight-recorder.mjs +219 -118
- package/lib/domain-control-manifest.mjs +117 -57
- package/lib/domain-posture-analysis.mjs +20 -7
- package/lib/domain-posture-parsers.mjs +88 -6
- package/lib/domain-posture.mjs +113 -26
- package/lib/evidence-export-privacy.mjs +561 -0
- package/lib/evidence-export.mjs +11 -592
- package/lib/evidence-report.mjs +3 -1
- package/lib/favicon.mjs +117 -41
- package/lib/generated/cisa-kev-catalog.mjs +12 -4
- package/lib/generated/registrar-standing-catalogue.mjs +7 -7
- package/lib/generated/retire-browser-catalog.mjs +2335 -940
- package/lib/hostname.mjs +2 -38
- package/lib/html-signals.mjs +82 -85
- package/lib/http-evidence-bounds.mjs +2 -1
- package/lib/http-intelligence.mjs +39 -26
- package/lib/lookup-cache.mjs +34 -11
- package/lib/lookup-child-profile-contract.mjs +24 -15
- package/lib/lookup-diagnostics.mjs +24 -1
- package/lib/lookup-progress.mjs +30 -13
- package/lib/lookup-response-contract.mjs +1048 -0
- package/lib/lookup-response.mjs +7 -10
- package/lib/lookup-source-progress.mjs +17 -14
- package/lib/lookup.mjs +48 -27
- package/lib/observed-network-context.mjs +43 -27
- package/lib/outbound-request-bounds.mjs +4 -1
- package/lib/page-fingerprints.mjs +69 -133
- package/lib/page-role-profile.mjs +12 -8
- package/lib/perceptual-hash.mjs +43 -14
- package/lib/rdap-bootstrap.mjs +72 -22
- package/lib/rdap-client.mjs +15 -7
- package/lib/rdap-normalization.mjs +33 -18
- package/lib/rdap-search-workbench.mjs +1 -1
- package/lib/rdap-transport.mjs +47 -20
- package/lib/rdap-types.mjs +2 -1
- package/lib/rdap.mjs +18 -3
- package/lib/registrable-domain.mjs +21 -1
- package/lib/registrar-standing-contract.mjs +1 -0
- package/lib/registry-capabilities.mjs +5 -1
- package/lib/registry-capability-catalogue.mjs +2 -1
- package/lib/registry-dates.mjs +2 -190
- package/lib/registry-insights.mjs +56 -18
- package/lib/response-policy.mjs +86 -48
- package/lib/risk-calibration-summary.mjs +17 -3
- package/lib/rpki-evidence.mjs +8 -5
- package/lib/safe-fetch.mjs +54 -47
- package/lib/security-txt.mjs +10 -7
- package/lib/service-binding-dns.mjs +8 -0
- package/lib/smtp-transport-review.mjs +27 -15
- package/lib/sslbl-certificates.generated.mjs +136 -135
- package/lib/static-html-analysis.mjs +117 -146
- package/lib/structured-data-identity.mjs +1 -0
- package/lib/technology-markup.mjs +100 -0
- package/lib/threat-intelligence-manifests.mjs +11 -139
- package/lib/threat-intelligence-runtime.mjs +24 -52
- package/lib/threat-intelligence-targets.mjs +6 -34
- package/lib/threat-intelligence-types.mjs +39 -0
- package/lib/typosquat-generator.mjs +19 -15
- package/lib/urlhaus-intelligence.mjs +8 -3
- package/lib/urlscan-intelligence.mjs +1 -1
- package/lib/website-security-posture.mjs +30 -6
- package/lib/website-technology.mjs +111 -71
- package/lib/whois-authority.mjs +14 -2
- package/lib/whois-chain.mjs +9 -2
- package/lib/whois-common-formats.mjs +5 -5
- package/lib/whois-contracts.mjs +2 -1
- package/lib/whois-dialects.mjs +2 -2
- package/lib/whois-parser-support.mjs +8 -1
- package/lib/whois-transport.mjs +23 -7
- package/package.json +2 -1
- package/packages/cases/case-attachment-model.mjs +83 -0
- package/packages/cases/case-evidence-model.mjs +156 -62
- package/packages/cases/case-export-input.mjs +53 -0
- package/packages/cases/case-migration-model.mjs +59 -25
- package/packages/cases/case-model.mjs +1 -0
- package/packages/cases/case-recheck-model.mjs +91 -0
- package/packages/cases/case-record-core.mjs +3 -2
- package/packages/cases/case-record-operations.mjs +89 -27
- package/packages/cases/case-record-projection.mjs +23 -20
- package/packages/cases/case-report.mjs +63 -52
- package/packages/cases/case-response-actions.mjs +654 -0
- package/packages/cases/case-response-model.mjs +41 -1129
- package/packages/cases/case-response-outcomes.mjs +317 -0
- package/packages/cases/case-response-packet-render.mjs +153 -0
- package/packages/cases/case-response-packet-types.mjs +1 -0
- package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
- package/packages/cases/case-response-packet.mjs +38 -587
- package/packages/cases/case-response-records.mjs +94 -0
- package/packages/cases/case-response-review-inputs.mjs +371 -0
- package/packages/cases/case-response-values.mjs +85 -0
- package/packages/cases/case-review-package.mjs +45 -0
- package/packages/cases/case-selection.mjs +46 -0
- package/packages/cases/case-storage-model.mjs +25 -0
- package/packages/cases/response-route-freshness.mjs +16 -0
- package/packages/comparison/capture-context.mjs +61 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
- package/packages/comparison/page-similarity.mjs +2 -1
- package/packages/comparison/relationship-evidence.mjs +85 -8
- package/packages/comparison/relationship-provenance.mjs +93 -0
- package/packages/contracts/analyst-interchange.mjs +216 -0
- package/packages/contracts/analyst-review-state-contract.mjs +11 -0
- package/packages/contracts/analyst-review-state.mjs +5 -4
- package/packages/contracts/case-portability.mjs +279 -116
- package/packages/contracts/case-views-contract.mjs +12 -0
- package/packages/contracts/case-views.mjs +26 -0
- package/packages/contracts/cli-command-semantics.mjs +1 -0
- package/packages/contracts/cli-lookup.mjs +0 -1
- package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
- package/packages/contracts/domain-control-manifest.mjs +127 -73
- package/packages/contracts/domain-control-monitor.mjs +48 -45
- package/packages/contracts/domain-control-review.mjs +179 -121
- package/packages/contracts/domain-name.mjs +21 -0
- package/packages/contracts/external-observation-interchange.mjs +2 -1
- package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
- package/packages/contracts/http-url.mjs +2 -0
- package/packages/contracts/investigation-package-limits.mjs +24 -0
- package/packages/contracts/investigation-portability.mjs +56 -27
- package/packages/contracts/investigation-run.mjs +16 -0
- package/packages/contracts/ip-address.mjs +34 -0
- package/packages/contracts/lookup-evidence.mjs +35 -0
- package/packages/contracts/offline-comparison.mjs +1 -2
- package/packages/contracts/page-fingerprints.mjs +15 -0
- package/packages/contracts/risk-calibration.mjs +0 -1
- package/packages/contracts/schema-compatibility.mjs +1 -1
- package/packages/contracts/schema-lifecycle.mjs +83 -153
- package/packages/contracts/selected-file-limits.mjs +6 -0
- package/packages/contracts/vulnerability-identifiers.mjs +5 -0
- package/packages/contracts/web-capture.mjs +10 -0
- package/packages/contracts/workspace-portability.mjs +160 -225
- package/packages/evidence/artifact-integrity.mjs +8 -2
- package/packages/evidence/artifact-structure.mjs +105 -0
- package/packages/evidence/decision-fact.mjs +4 -1
- package/packages/evidence/domain-control-runtime.mjs +155 -27
- package/packages/evidence/domain-posture-context.mjs +79 -0
- package/packages/evidence/image-regions.mjs +56 -0
- package/packages/evidence/latest-observations.mjs +28 -0
- package/packages/evidence/lookup-target.mjs +104 -0
- package/packages/evidence/observation.mjs +16 -1
- package/packages/evidence/passphrase-encryption.mjs +44 -0
- package/packages/evidence/record-identity.mjs +72 -0
- package/packages/evidence/registry-dates.mjs +190 -0
- package/packages/evidence/retained-file.mjs +69 -0
- package/packages/evidence/review-age.mjs +14 -0
- package/packages/interchange/bagit.mjs +342 -0
- package/packages/interchange/bounded-zip-extraction.mjs +278 -0
- package/packages/interchange/crc32.mjs +17 -0
- package/packages/interchange/external-findings-import.mjs +89 -49
- package/packages/interchange/web-capture-import.mjs +382 -0
- package/packages/investigation/bulk-mail-exposure.mjs +2 -1
- package/packages/investigation/investigation-bagit.mjs +14 -0
- package/packages/investigation/investigation-capsule.mjs +33 -5
- package/packages/investigation/investigation-manifest.mjs +200 -0
- package/packages/investigation/investigation-package-crypto.mjs +98 -0
- package/packages/investigation/investigation-package.mjs +200 -0
- package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
- package/packages/investigation/lookup-asset-graph.mjs +161 -141
- package/packages/investigation/lookup-claim-readiness.mjs +18 -0
- package/packages/investigation/lookup-investigation-brief.mjs +3 -3
- package/packages/investigation/lookup-task-guidance.mjs +58 -0
- package/packages/monitoring/analyst-review-state.mjs +38 -90
- package/packages/workspace/brand-profile-model.mjs +158 -20
- package/packages/workspace/bulk-columns.mjs +17 -0
- package/packages/workspace/bulk-review-model.mjs +11 -5
- package/packages/workspace/bulk-session-model.mjs +95 -20
- package/packages/workspace/bulk-sort.mjs +1 -0
- package/packages/workspace/case-views.mjs +102 -0
- package/packages/workspace/hostile-input.mjs +29 -8
- package/packages/workspace/investigation-template-model.mjs +19 -4
- package/packages/workspace/page-baseline.mjs +18 -15
- package/packages/workspace/relationship-observation-model.mjs +41 -26
- package/packages/workspace/watchlist-history.mjs +2 -2
- package/packages/workspace/watchlist-store.mjs +8 -2
- package/packages/workspace/website-snapshot-model.mjs +70 -30
- package/packages/workspace/workspace-archive-crypto.mjs +41 -61
- package/packages/workspace/workspace-archive.mjs +85 -21
- package/third-party-notices.txt +29 -2
- package/packages/investigation/bulk-review-export.mjs +0 -63
package/lib/domain-posture.mjs
CHANGED
|
@@ -7,6 +7,10 @@ import { nonEmptyErrorMessage } from "./error-detail.mjs";
|
|
|
7
7
|
import { safeFetch, readTextCapped } from "./safe-fetch.mjs";
|
|
8
8
|
import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
|
|
9
9
|
import { classifyMxRecords } from "./dns-mx.mjs";
|
|
10
|
+
import { collectDnsInheritanceChecks } from "./dns-inheritance-review.mjs";
|
|
11
|
+
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
12
|
+
import { DOMAIN_POSTURE_COMPARISON_VERSION, MAX_POSTURE_CHECK_RECORDS, POSTURE_CHECK_SOURCES } from "../packages/evidence/domain-posture-context.mjs";
|
|
13
|
+
import { canonicalPostureRecords } from "../packages/evidence/domain-control-runtime.mjs";
|
|
10
14
|
import { parseSpfRecords, parseDmarcRecords, parseMtaStsDnsRecords, parseMtaStsPolicy, parseTlsRptRecords, parseBimiRecords, parseDkimRecords, } from "./domain-posture-parsers.mjs";
|
|
11
15
|
import { buildExternalDependencies, expandSpfPolicy, validateDmarcExternalReporting, } from "./domain-posture-analysis.mjs";
|
|
12
16
|
const DNS_TIMEOUT_MS = 6000;
|
|
@@ -88,14 +92,23 @@ function withTimeout(promise, label, timeoutMs = DNS_TIMEOUT_MS, timers = {
|
|
|
88
92
|
});
|
|
89
93
|
}
|
|
90
94
|
async function resolveDns(label, factory, timeoutMs = DNS_TIMEOUT_MS, timers) {
|
|
95
|
+
const completed = (result) => {
|
|
96
|
+
let observedAt = null;
|
|
97
|
+
try {
|
|
98
|
+
const at = timers?.now?.();
|
|
99
|
+
observedAt = at instanceof Date && Number.isFinite(at.getTime()) ? at.toISOString() : null;
|
|
100
|
+
}
|
|
101
|
+
catch { /* Unknown source time remains explicit. */ }
|
|
102
|
+
return { ...result, observedAt };
|
|
103
|
+
};
|
|
91
104
|
try {
|
|
92
|
-
return { records: await withTimeout(factory(), label, timeoutMs, timers), error: null };
|
|
105
|
+
return completed({ records: await withTimeout(factory(), label, timeoutMs, timers), error: null });
|
|
93
106
|
}
|
|
94
107
|
catch (err) {
|
|
95
108
|
const error = errorRecord(err);
|
|
96
109
|
if (typeof error.code === 'string' && MISSING_DNS_CODES.has(error.code))
|
|
97
|
-
return { records: [], error: null };
|
|
98
|
-
return { records: [], error: nonEmptyErrorMessage(err, String(err)) };
|
|
110
|
+
return completed({ records: [], error: null });
|
|
111
|
+
return completed({ records: [], error: nonEmptyErrorMessage(err, String(err)) });
|
|
99
112
|
}
|
|
100
113
|
}
|
|
101
114
|
function check(id, label, status, summary, { detail = '', records = [], remediation = '' } = {}) {
|
|
@@ -191,6 +204,11 @@ function dmarcCheck(query, authorizations = []) {
|
|
|
191
204
|
];
|
|
192
205
|
const external = authorizations.filter((authorization) => authorization.state !== 'self');
|
|
193
206
|
const unresolvedExternal = external.filter((authorization) => authorization.state !== 'authorized');
|
|
207
|
+
const intendedDestinations = parsed.aggregateDestinations.length + parsed.failureDestinations.length;
|
|
208
|
+
const omittedDestinations = Math.max(0, intendedDestinations - authorizations.length);
|
|
209
|
+
if (omittedDestinations) {
|
|
210
|
+
details.push(`Reporting authorisation coverage: ${authorizations.length} of ${intendedDestinations} destinations reviewed; ${omittedDestinations} not checked within the bounded collection. Omitted destinations are not assumed authorised.`);
|
|
211
|
+
}
|
|
194
212
|
if (external.length > 0) {
|
|
195
213
|
details.push(`${external.length} external reporting destination${external.length === 1 ? '' : 's'} checked; `
|
|
196
214
|
+ `${unresolvedExternal.length} could not be authorized.`);
|
|
@@ -213,11 +231,13 @@ function dmarcCheck(query, authorizations = []) {
|
|
|
213
231
|
remediation: 'Set sp and np to quarantine or reject unless weaker subdomain treatment is intentional.',
|
|
214
232
|
});
|
|
215
233
|
}
|
|
216
|
-
if (unresolvedExternal.length > 0) {
|
|
217
|
-
return check('dmarc', 'DMARC', 'warning', `Enforced at p=${parsed.policy};
|
|
234
|
+
if (unresolvedExternal.length > 0 || omittedDestinations > 0) {
|
|
235
|
+
return check('dmarc', 'DMARC', 'warning', `Enforced at p=${parsed.policy}; reporting authorisation is incomplete`, {
|
|
218
236
|
detail: `${details.join(' ')} ${unresolvedExternal.map((authorization) => `${authorization.destination}: ${authorization.state}.`).join(' ')}`,
|
|
219
237
|
records: parsed.records,
|
|
220
|
-
remediation:
|
|
238
|
+
remediation: omittedDestinations > 0
|
|
239
|
+
? 'Review the unchecked destinations and confirm any required external reporting authorisation before relying on delivery.'
|
|
240
|
+
: 'Publish the required external reporting authorisation record or remove the unavailable destination.',
|
|
221
241
|
});
|
|
222
242
|
}
|
|
223
243
|
if (!parsed.aggregateReporting) {
|
|
@@ -332,7 +352,11 @@ function matchesMtaPattern(host, pattern) {
|
|
|
332
352
|
const normalizedPattern = String(pattern || '').toLowerCase().replace(/\.+$/, '');
|
|
333
353
|
if (normalizedPattern.startsWith('*.')) {
|
|
334
354
|
const suffix = normalizedPattern.slice(1);
|
|
335
|
-
|
|
355
|
+
if (!normalizedHost.endsWith(suffix))
|
|
356
|
+
return false;
|
|
357
|
+
const label = normalizedHost.slice(0, -suffix.length);
|
|
358
|
+
// MTA-STS permits a wildcard for exactly one leftmost DNS label.
|
|
359
|
+
return /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/u.test(label);
|
|
336
360
|
}
|
|
337
361
|
return normalizedHost === normalizedPattern;
|
|
338
362
|
}
|
|
@@ -565,10 +589,11 @@ function defensiveMailProfileCheck(profile, input) {
|
|
|
565
589
|
function registrationLockCheck(registry) {
|
|
566
590
|
if (registry.error)
|
|
567
591
|
return queryFailureCheck('registration_lock', 'Registration controls', registry.error);
|
|
568
|
-
const statuses = registry.statuses
|
|
592
|
+
const statuses = canonicalPostureRecords('registration_lock', registry.statuses) ?? [];
|
|
569
593
|
if (statuses.length === 0) {
|
|
570
594
|
return check('registration_lock', 'Registration controls', 'info', 'Registry lock state is unavailable', {
|
|
571
595
|
detail: 'No normalised EPP status was returned. This does not describe registrar account security.',
|
|
596
|
+
records: registry.statuses,
|
|
572
597
|
});
|
|
573
598
|
}
|
|
574
599
|
const transferLocks = statuses.filter((status) => ['clienttransferprohibited', 'servertransferprohibited'].includes(status));
|
|
@@ -635,11 +660,36 @@ function buildPostureReport(domain, input) {
|
|
|
635
660
|
...(input.registry && input.nameservers ? [nameserverCheck(input.nameservers, input.registry)] : []),
|
|
636
661
|
];
|
|
637
662
|
const summary = { pass: 0, warning: 0, danger: 0, info: 0 };
|
|
638
|
-
for (const item of checks)
|
|
663
|
+
for (const item of checks) {
|
|
639
664
|
summary[item.status] += 1;
|
|
665
|
+
const source = POSTURE_CHECK_SOURCES[item.id];
|
|
666
|
+
const query = item.id === 'nameservers' ? input.nameservers : item.id === 'mx' ? input.mx : item.id === 'caa' ? input.caa : undefined;
|
|
667
|
+
const registry = item.id === 'registration_lock' ? input.registry : undefined;
|
|
668
|
+
if (!source || (!query && !registry))
|
|
669
|
+
continue;
|
|
670
|
+
const omittedRecords = Math.max(0, item.records.length - MAX_POSTURE_CHECK_RECORDS);
|
|
671
|
+
const records = item.records.slice(0, MAX_POSTURE_CHECK_RECORDS);
|
|
672
|
+
const error = query?.error ?? registry?.error;
|
|
673
|
+
const retained = canonicalPostureRecords(item.id, records);
|
|
674
|
+
const rawRecords = query?.records ?? registry?.statuses ?? [];
|
|
675
|
+
const raw = item.id === 'caa'
|
|
676
|
+
? canonicalPostureRecords(item.id, rawRecords.map((record) => record && typeof record === 'object' ? caaDisplay(record) : record))
|
|
677
|
+
: canonicalPostureRecords(item.id, rawRecords);
|
|
678
|
+
const complete = !error && omittedRecords === 0 && retained !== null && raw !== null
|
|
679
|
+
&& retained.length === raw.length && retained.every((value, index) => value === raw[index])
|
|
680
|
+
&& (!registry || (registry.statusesComplete === true && records.length > 0));
|
|
681
|
+
item.sourceContext = {
|
|
682
|
+
version: DOMAIN_POSTURE_COMPARISON_VERSION, source,
|
|
683
|
+
observedAt: normalizeExplicitIsoTimestamp(query?.observedAt ?? registry?.observedAt),
|
|
684
|
+
state: error ? 'unavailable' : complete ? 'complete' : 'partial',
|
|
685
|
+
omittedRecords: complete ? 0 : omittedRecords || null,
|
|
686
|
+
};
|
|
687
|
+
item.records = records;
|
|
688
|
+
}
|
|
640
689
|
return { domain, summary, checks };
|
|
641
690
|
}
|
|
642
|
-
async function fetchMtaStsPolicy(domain, fetcher = safeFetch) {
|
|
691
|
+
async function fetchMtaStsPolicy(domain, fetcher = safeFetch, signal) {
|
|
692
|
+
signal?.throwIfAborted();
|
|
643
693
|
const controller = new AbortController();
|
|
644
694
|
const timeout = setTimeout(() => controller.abort(), POLICY_TIMEOUT_MS);
|
|
645
695
|
try {
|
|
@@ -648,7 +698,7 @@ async function fetchMtaStsPolicy(domain, fetcher = safeFetch) {
|
|
|
648
698
|
// following HTTP redirects for policy discovery. A zero-hop safe fetch
|
|
649
699
|
// also keeps the returned body bound to the required policy host/path.
|
|
650
700
|
const res = await fetcher(url, {
|
|
651
|
-
signal: controller.signal,
|
|
701
|
+
signal: signal ? AbortSignal.any([signal, controller.signal]) : controller.signal,
|
|
652
702
|
headers: whoisleuthRequestHeaders({ Accept: 'text/plain' }),
|
|
653
703
|
}, 0);
|
|
654
704
|
if (res.status !== 200) {
|
|
@@ -659,11 +709,13 @@ async function fetchMtaStsPolicy(domain, fetcher = safeFetch) {
|
|
|
659
709
|
return { text: '', contentType: res.headers.get('content-type'), error: `Policy endpoint returned HTTP ${res.status}.` };
|
|
660
710
|
}
|
|
661
711
|
const body = await readTextCapped(res, MAX_POLICY_BYTES);
|
|
712
|
+
signal?.throwIfAborted();
|
|
662
713
|
if (body.truncated)
|
|
663
714
|
return { text: '', contentType: res.headers.get('content-type'), error: `Policy exceeds ${MAX_POLICY_BYTES} bytes.` };
|
|
664
715
|
return { text: body.text, contentType: res.headers.get('content-type'), error: null };
|
|
665
716
|
}
|
|
666
717
|
catch (err) {
|
|
718
|
+
signal?.throwIfAborted();
|
|
667
719
|
return {
|
|
668
720
|
text: '',
|
|
669
721
|
contentType: null,
|
|
@@ -676,17 +728,8 @@ async function fetchMtaStsPolicy(domain, fetcher = safeFetch) {
|
|
|
676
728
|
clearTimeout(timeout);
|
|
677
729
|
}
|
|
678
730
|
}
|
|
679
|
-
async function
|
|
680
|
-
|
|
681
|
-
resolveMx: (name) => dns.resolveMx(name),
|
|
682
|
-
resolveNs: (name) => dns.resolveNs(name),
|
|
683
|
-
resolveCaa: (name) => dns.resolveCaa(name),
|
|
684
|
-
fetchRdapRecord,
|
|
685
|
-
fetchMtaStsPolicy: (name) => fetchMtaStsPolicy(name),
|
|
686
|
-
now: () => new Date(),
|
|
687
|
-
setTimer: (callback, milliseconds) => setTimeout(callback, milliseconds),
|
|
688
|
-
clearTimer: (handle) => clearTimeout(handle),
|
|
689
|
-
}) {
|
|
731
|
+
async function collectDomainPosture(domain, { dkimSelectors = [], retiredDkimSelectors = [], mailProtectionProfile = 'standard', includeInheritedDns, signal, }, dependencies) {
|
|
732
|
+
signal?.throwIfAborted();
|
|
690
733
|
const normalizedDomain = normalizeAuditDomain(domain);
|
|
691
734
|
if (!normalizedDomain)
|
|
692
735
|
throw new Error('Invalid domain name for posture audit.');
|
|
@@ -717,10 +760,11 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
|
|
|
717
760
|
...await resolveDns(`TXT ${selector}._domainkey.${domain}`, () => dependencies.resolveTxt(`${selector}._domainkey.${domain}`), DNS_TIMEOUT_MS, dependencies),
|
|
718
761
|
})),
|
|
719
762
|
]),
|
|
720
|
-
dependencies.fetchRdapRecord('domain', domain).catch((err) => ({
|
|
763
|
+
dependencies.fetchRdapRecord('domain', domain, signal ? { signal } : {}).catch((err) => ({
|
|
721
764
|
error: nonEmptyErrorMessage(err, String(err)),
|
|
722
765
|
})),
|
|
723
766
|
]);
|
|
767
|
+
signal?.throwIfAborted();
|
|
724
768
|
const parsedMtaDns = mtaStsDns.error ? null : parseMtaStsDnsRecords(mtaStsDns.records);
|
|
725
769
|
const enrichmentStartedAt = dependencies.now();
|
|
726
770
|
if (!(enrichmentStartedAt instanceof Date) || !Number.isFinite(enrichmentStartedAt.getTime())) {
|
|
@@ -728,6 +772,7 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
|
|
|
728
772
|
}
|
|
729
773
|
const enrichmentDeadline = enrichmentStartedAt.getTime() + POSTURE_ENRICHMENT_DEADLINE_MS;
|
|
730
774
|
const resolveEnrichmentTxt = (name) => {
|
|
775
|
+
signal?.throwIfAborted();
|
|
731
776
|
const observedAt = dependencies.now();
|
|
732
777
|
if (!(observedAt instanceof Date) || !Number.isFinite(observedAt.getTime())) {
|
|
733
778
|
return Promise.resolve({ records: [], error: 'The bounded posture-enrichment clock was unavailable.' });
|
|
@@ -737,11 +782,21 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
|
|
|
737
782
|
return Promise.resolve({ records: [], error: 'The bounded posture-enrichment deadline was reached.' });
|
|
738
783
|
return resolveDns(`TXT ${name}`, () => dependencies.resolveTxt(name), Math.min(DNS_TIMEOUT_MS, remaining), dependencies);
|
|
739
784
|
};
|
|
740
|
-
const
|
|
741
|
-
parsedMtaDns?.valid ? dependencies.fetchMtaStsPolicy(domain) : Promise.resolve(null),
|
|
785
|
+
const enrichment = [
|
|
786
|
+
parsedMtaDns?.valid ? dependencies.fetchMtaStsPolicy(domain, signal) : Promise.resolve(null),
|
|
742
787
|
expandSpfPolicy(domain, spf, resolveEnrichmentTxt),
|
|
743
788
|
validateDmarcExternalReporting(domain, dmarc, resolveEnrichmentTxt),
|
|
744
|
-
|
|
789
|
+
includeInheritedDns === true
|
|
790
|
+
? (dependencies.collectDnsInheritanceChecks
|
|
791
|
+
? dependencies.collectDnsInheritanceChecks(domain, dmarc, nameservers, signal ? { signal } : {})
|
|
792
|
+
: Promise.reject(new TypeError('The explicit DNS review collector is unavailable.')))
|
|
793
|
+
: Promise.resolve([]),
|
|
794
|
+
];
|
|
795
|
+
// A rejected policy request must not release the operation's capacity while
|
|
796
|
+
// other started enrichment collectors are still settling.
|
|
797
|
+
const [mtaStsPolicy, spfExpansion, dmarcAuthorizations, inheritanceChecks] = await Promise.all(enrichment)
|
|
798
|
+
.finally(() => Promise.allSettled(enrichment));
|
|
799
|
+
signal?.throwIfAborted();
|
|
745
800
|
const dnssec = !rdap
|
|
746
801
|
? { value: null, error: 'RDAP did not return a domain record.' }
|
|
747
802
|
: 'error' in rdap
|
|
@@ -758,6 +813,8 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
|
|
|
758
813
|
dsRecordCount: parsedDomain.dsData.length,
|
|
759
814
|
dsDataTruncated: parsedDomain.dsDataTruncated,
|
|
760
815
|
error: null,
|
|
816
|
+
observedAt: normalizeExplicitIsoTimestamp(rdap && !('error' in rdap) ? rdap.fetchedAt : null),
|
|
817
|
+
statusesComplete: parsedDomain.statusesTruncated === false && parsedDomain.serverTruncated === false,
|
|
761
818
|
}
|
|
762
819
|
: {
|
|
763
820
|
statuses: [],
|
|
@@ -794,6 +851,10 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
|
|
|
794
851
|
if (!(checkedAt instanceof Date) || !Number.isFinite(checkedAt.getTime())) {
|
|
795
852
|
throw new TypeError('Domain-posture completion time must be valid.');
|
|
796
853
|
}
|
|
854
|
+
for (const item of inheritanceChecks) {
|
|
855
|
+
report.checks.push(item);
|
|
856
|
+
report.summary[item.status] += 1;
|
|
857
|
+
}
|
|
797
858
|
return {
|
|
798
859
|
...report,
|
|
799
860
|
checkedAt: checkedAt.toISOString(),
|
|
@@ -805,4 +866,30 @@ async function checkDomainPosture(domain, { dkimSelectors = [], retiredDkimSelec
|
|
|
805
866
|
externalDependencies,
|
|
806
867
|
};
|
|
807
868
|
}
|
|
869
|
+
async function checkDomainPosture(domain, options = {}, dependencies) {
|
|
870
|
+
options.signal?.throwIfAborted();
|
|
871
|
+
// Cancelling one audit must never cancel another audit's resolver.
|
|
872
|
+
const resolver = dependencies ? null : new dns.Resolver();
|
|
873
|
+
const cancel = () => resolver?.cancel();
|
|
874
|
+
options.signal?.addEventListener('abort', cancel, { once: true });
|
|
875
|
+
try {
|
|
876
|
+
options.signal?.throwIfAborted();
|
|
877
|
+
return await collectDomainPosture(domain, options, dependencies ?? {
|
|
878
|
+
resolveTxt: name => resolver.resolveTxt(name),
|
|
879
|
+
resolveMx: name => resolver.resolveMx(name),
|
|
880
|
+
resolveNs: name => resolver.resolveNs(name),
|
|
881
|
+
resolveCaa: name => resolver.resolveCaa(name),
|
|
882
|
+
fetchRdapRecord,
|
|
883
|
+
fetchMtaStsPolicy: (name, signal) => fetchMtaStsPolicy(name, safeFetch, signal),
|
|
884
|
+
now: () => new Date(),
|
|
885
|
+
setTimer: (callback, milliseconds) => setTimeout(callback, milliseconds),
|
|
886
|
+
clearTimer: handle => clearTimeout(handle),
|
|
887
|
+
collectDnsInheritanceChecks,
|
|
888
|
+
});
|
|
889
|
+
}
|
|
890
|
+
finally {
|
|
891
|
+
options.signal?.removeEventListener('abort', cancel);
|
|
892
|
+
cancel();
|
|
893
|
+
}
|
|
894
|
+
}
|
|
808
895
|
export { normalizeAuditDomain, normalizeDkimSelectors, normalizeMailProtectionProfile, matchesMtaPattern, fetchMtaStsPolicy, buildPostureReport, checkDomainPosture, };
|