@slicedearth/whoisleuth-cli 2.4.0 → 2.6.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 +1 -1
- package/NOTICE +2 -2
- package/cli/argument-values.mjs +55 -0
- package/cli/arguments.mjs +24 -470
- package/cli/artifact-validation/case-response.mjs +67 -9
- package/cli/artifact-validation/lookup-evidence.mjs +18 -4
- package/cli/artifact-validation/signed-review.mjs +61 -64
- package/cli/artifact-verify.mjs +11 -2
- package/cli/assurance-arguments.mjs +56 -0
- package/cli/assurance-command-definitions.mjs +179 -0
- package/cli/assurance-command-runner.mjs +1 -1
- package/cli/bounded-json.mjs +1 -1
- package/cli/bulk-checkpoint.mjs +1 -1
- package/cli/bulk-command-runner.mjs +6 -8
- package/cli/bulk.mjs +2 -3
- package/cli/case-command.mjs +99 -6
- package/cli/case-pack.mjs +19 -4
- package/cli/ci-report.mjs +6 -3
- package/cli/collection-arguments.mjs +123 -0
- package/cli/collection-command-definitions.mjs +330 -0
- package/cli/collection-preflight.mjs +8 -1
- package/cli/command-argument-grammar.mjs +8 -210
- package/cli/command-definition.mjs +330 -0
- package/cli/command-reference.mjs +55 -1388
- package/cli/command-seeds.mjs +18 -0
- package/cli/compare.mjs +1 -1
- package/cli/config-profile.mjs +1 -1
- package/cli/ct-event-intake.mjs +4 -4
- package/cli/discover.mjs +2 -2
- package/cli/discovery-observation-snapshot.mjs +2 -2
- package/cli/discovery-scan-command-runner.mjs +7 -9
- package/cli/discovery-scan.mjs +1 -1
- package/cli/discovery-snapshot.mjs +2 -2
- package/cli/discovery-workflow.mjs +4 -4
- package/cli/domain-control-monitor.mjs +6 -6
- package/cli/domain-control-observations.mjs +2 -1
- package/cli/domain-feed.mjs +61 -0
- package/cli/errors.mjs +5 -6
- package/cli/evidence-arguments.mjs +46 -0
- package/cli/evidence-command-definitions.mjs +69 -0
- package/cli/evidence-command-runner.mjs +2 -3
- package/cli/evidence-signing.mjs +3 -2
- package/cli/external-observation-mapping.mjs +2 -2
- package/cli/formatters/json.mjs +1 -13
- package/cli/formatters/terminal-command-formats.mjs +6 -0
- package/cli/formatters/terminal-lookup.mjs +6 -0
- package/cli/history-arguments.mjs +39 -0
- package/cli/history-command-definitions.mjs +88 -0
- package/cli/indicator-set-command.mjs +32 -0
- package/cli/inline-command-families.mjs +6 -47
- package/cli/inline-command-runner.mjs +4 -31
- package/cli/input.mjs +2 -2
- package/cli/intake-command.mjs +147 -0
- package/cli/interchange-report.mjs +4 -2
- package/cli/investigation-artifacts.mjs +3 -4
- package/cli/investigation-package-review.mjs +2 -0
- package/cli/investigation-plan.mjs +32 -154
- package/cli/investigation-recipes.mjs +177 -0
- package/cli/investigation-run.mjs +78 -12
- package/cli/local-mmdb-review.mjs +220 -17
- package/cli/lookalike-calibration.mjs +1 -1
- package/cli/lookup-brief.mjs +2 -2
- package/cli/lookup-command-runner.mjs +19 -35
- package/cli/lookup-diff.mjs +10 -9
- package/cli/lookup-plan.mjs +10 -14
- package/cli/lookup-reconcile.mjs +6 -5
- package/cli/lookup-timeline.mjs +7 -6
- package/cli/mail-header-review.mjs +24 -247
- package/cli/mail-review.mjs +9 -7
- package/cli/network-arguments.mjs +51 -0
- package/cli/network-command-definitions.mjs +130 -0
- package/cli/network-command-runner.mjs +43 -31
- package/cli/offline-evidence-review.mjs +85 -6
- package/cli/posture.mjs +1 -1
- package/cli/registry-fixture-scaffold.mjs +1 -1
- package/cli/retained-artifact-diff.mjs +8 -7
- package/cli/review-arguments.mjs +132 -0
- package/cli/review-command-definitions.mjs +279 -0
- package/cli/review-command-runner.mjs +72 -159
- package/cli/risk-calibration.mjs +14 -12
- package/cli/runner.mjs +20 -32
- package/cli/saved-lookup.mjs +2 -2
- package/cli/selected-input-worker.mjs +73 -0
- package/cli/sharing-review.mjs +5 -64
- package/cli/source-reliability.mjs +2 -2
- package/cli/support-arguments.mjs +49 -0
- package/cli/support-command-definitions.mjs +209 -0
- package/cli/support-command-runner.mjs +4 -4
- package/cli/watchlist-review.mjs +43 -0
- package/cli/workflow-arguments.mjs +100 -0
- package/cli/workflow-command-definitions.mjs +103 -0
- package/cli/workflow-command-runner.mjs +40 -1
- package/docs/cli-reference.md +27 -0
- package/docs/cli.md +322 -4
- package/docs/contextual-reviews.md +178 -0
- package/docs/infrastructure-observations.md +126 -0
- package/docs/offline-intake-context.md +213 -0
- package/lib/analyst-taxonomy.mjs +2 -40
- package/lib/application-version.mjs +1 -1
- package/lib/availability.mjs +13 -789
- package/lib/bounded-contract-normalizers.mjs +4 -0
- package/lib/bounded-json.mjs +2 -330
- package/lib/browser-library-profile.mjs +62 -84
- package/lib/candidate-provenance-bounds.mjs +2 -0
- package/lib/classify.mjs +1 -1
- package/lib/credential-form-attribution.mjs +136 -0
- package/lib/credential-surface-profile.mjs +6 -2
- package/lib/ct-query.mjs +2 -27
- package/lib/ct-response-bounds.mjs +2 -9
- package/lib/ct-search.mjs +83 -47
- package/lib/dmarc-discovery.mjs +62 -0
- package/lib/dns-convergence-review.mjs +21 -8
- package/lib/dns-delegation-health.mjs +35 -15
- package/lib/dns-inheritance-review.mjs +53 -63
- package/lib/dns-intelligence.mjs +27 -11
- package/lib/dns-query-coverage.mjs +40 -0
- package/lib/domain-assurance.mjs +1 -1
- package/lib/domain-change-packet.mjs +12 -13
- package/lib/domain-change-review.mjs +32 -14
- package/lib/domain-control-flight-recorder.mjs +103 -27
- package/lib/domain-control-manifest.mjs +25 -17
- package/lib/domain-posture-analysis.mjs +75 -7
- package/lib/domain-posture-parsers.mjs +9 -0
- package/lib/domain-posture.mjs +45 -25
- package/lib/domain-registration.mjs +392 -0
- package/lib/evidence-export-privacy.mjs +27 -17
- package/lib/evidence-export.mjs +3 -3
- package/lib/evidence-report.mjs +5 -1
- package/lib/external-intelligence-risk.mjs +2 -124
- package/lib/favicon.mjs +13 -4
- package/lib/generated/cisa-kev-catalog.mjs +41 -3
- package/lib/generated/registrar-standing-catalogue.mjs +18 -10
- package/lib/generated/retire-browser-catalog.mjs +332 -19
- package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
- package/lib/html-signals.mjs +13 -2
- package/lib/http-evidence-bounds.mjs +2 -8
- package/lib/http-intelligence.mjs +13 -1
- package/lib/idn-confusable-policy.mjs +6 -4
- package/lib/idn-confusables.mjs +1 -1
- package/lib/interchange-fidelity-registry.mjs +10 -0
- package/lib/lookup-child-profile-contract.mjs +28 -11
- package/lib/lookup-contract-primitives.mjs +1 -1
- package/lib/lookup-diagnostics.mjs +1 -23
- package/lib/lookup-network-evidence-bounds.mjs +1 -10
- package/lib/lookup-progress.mjs +50 -1
- package/lib/lookup-response-contract.mjs +70 -40
- package/lib/lookup-response.mjs +5 -15
- package/lib/lookup-source-progress.mjs +8 -22
- package/lib/lookup.mjs +1 -1
- package/lib/operation-budget.mjs +4 -0
- package/lib/opportunity-scoring.mjs +2 -107
- package/lib/outbound-identity.mjs +1 -1
- package/lib/page-fingerprints.mjs +18 -1
- package/lib/perceptual-hash-comparison.mjs +2 -34
- package/lib/perceptual-hash.mjs +36 -10
- package/lib/portable-generator.mjs +2 -29
- package/lib/project-metadata.mjs +2 -8
- package/lib/rdap-client.mjs +11 -7
- package/lib/registrable-domain.mjs +2 -56
- package/lib/registrar-standing-contract.mjs +1 -1
- package/lib/registrar-standing.mjs +9 -13
- package/lib/registry-capabilities.mjs +2 -2
- package/lib/registry-capability-catalogue.mjs +27 -19
- package/lib/risk-calibration-summary.mjs +2 -352
- package/lib/risk-scoring.mjs +2 -345
- package/lib/scoring-evidence-quality.mjs +2 -107
- package/lib/semantic-version.mjs +2 -54
- package/lib/service-binding-dns.mjs +46 -2
- package/lib/smtp-transport-review.mjs +48 -19
- package/lib/sslbl-certificates.generated.mjs +142 -139
- package/lib/static-html-analysis.mjs +32 -4
- package/lib/structured-data-identity.mjs +1 -1
- package/lib/technology-evidence-role.mjs +1 -51
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threat-intelligence-runtime.mjs +1 -1
- package/lib/threat-intelligence-targets.mjs +1 -1
- package/lib/threat-intelligence-types.mjs +2 -75
- package/lib/tls-intelligence.mjs +30 -5
- package/lib/typosquat-generator.mjs +11 -2
- package/lib/website-enrichment.mjs +404 -0
- package/lib/whois-authority.mjs +43 -5
- package/lib/whois-chain.mjs +12 -3
- package/lib/whois-common-formats.mjs +3 -1
- package/lib/whois-contacts.mjs +13 -2
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-parser.mjs +5 -3
- package/package.json +15 -14
- package/packages/analysis/analyst-taxonomy.mjs +40 -0
- package/packages/analysis/bounded-json.mjs +330 -0
- package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
- package/packages/analysis/cli-argument-grammar.mjs +216 -0
- package/packages/analysis/ct-query.mjs +27 -0
- package/packages/analysis/ct-response-bounds.mjs +9 -0
- package/packages/analysis/external-intelligence-risk.mjs +124 -0
- package/packages/analysis/http-evidence-bounds.mjs +8 -0
- package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
- package/packages/analysis/opportunity-scoring.mjs +107 -0
- package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
- package/packages/analysis/portable-generator.mjs +29 -0
- package/packages/analysis/project-metadata.mjs +8 -0
- package/packages/analysis/registrable-domain.mjs +56 -0
- package/packages/analysis/risk-calibration-summary.mjs +352 -0
- package/packages/analysis/risk-scoring.mjs +348 -0
- package/packages/analysis/scoring-evidence-quality.mjs +107 -0
- package/packages/analysis/semantic-version.mjs +54 -0
- package/packages/analysis/threat-intelligence-types.mjs +75 -0
- package/packages/cases/case-containment-handoff.mjs +195 -0
- package/packages/cases/case-evidence-links.mjs +151 -0
- package/packages/cases/case-evidence-model.mjs +89 -12
- package/packages/cases/case-evidence-quality.mjs +5 -0
- package/packages/cases/case-export-input.mjs +7 -5
- package/packages/cases/case-incident-context.mjs +52 -0
- package/packages/cases/case-markdown.mjs +11 -0
- package/packages/cases/case-migration-model.mjs +40 -24
- package/packages/cases/case-object-coverage.mjs +33 -0
- package/packages/cases/case-packet-correction.mjs +106 -0
- package/packages/cases/case-recheck-model.mjs +124 -36
- package/packages/cases/case-record-contracts.mjs +1 -1
- package/packages/cases/case-record-core.mjs +1 -1
- package/packages/cases/case-record-operations.mjs +111 -118
- package/packages/cases/case-record-projection.mjs +23 -4
- package/packages/cases/case-record-version-input.mjs +41 -0
- package/packages/cases/case-report.mjs +93 -27
- package/packages/cases/case-requested-evidence.mjs +146 -0
- package/packages/cases/case-response-actions.mjs +159 -1
- package/packages/cases/case-response-model.mjs +16 -1
- package/packages/cases/case-response-object.mjs +119 -0
- package/packages/cases/case-response-outcomes.mjs +108 -20
- package/packages/cases/case-response-packet-render.mjs +35 -12
- package/packages/cases/case-response-packet.mjs +55 -13
- package/packages/cases/case-response-records.mjs +0 -1
- package/packages/cases/case-response-review-inputs.mjs +60 -8
- package/packages/cases/case-storage-model.mjs +1 -1
- package/packages/cases/case-workflow-metadata.mjs +217 -0
- package/packages/cases/case-workflow-migration.mjs +43 -0
- package/packages/cases/http-summary.mjs +6 -1
- package/packages/comparison/favicon-similarity.mjs +1 -1
- package/packages/comparison/page-similarity.mjs +1 -1
- package/packages/comparison/relationship-evidence.mjs +21 -9
- package/packages/comparison/relationship-provenance.mjs +7 -1
- package/packages/contracts/analyst-interchange.mjs +35 -15
- package/packages/contracts/candidate-watch-review.mjs +54 -0
- package/packages/contracts/case-portability.mjs +360 -49
- package/packages/contracts/cli-command-semantics.mjs +5 -0
- package/packages/contracts/cli-grammar.mjs +6 -0
- package/packages/contracts/cli-lookup-brief.mjs +2 -0
- package/packages/contracts/context-review.mjs +25 -0
- package/packages/contracts/document-intake.mjs +11 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
- package/packages/contracts/domain-control-manifest.mjs +1 -1
- package/packages/contracts/domain-control-monitor.mjs +13 -2
- package/packages/contracts/domain-control-review.mjs +36 -15
- package/packages/contracts/external-observation-interchange.mjs +96 -28
- package/packages/contracts/har-review.mjs +2 -0
- package/packages/contracts/identity-events.mjs +4 -0
- package/packages/contracts/investigation-run.mjs +2 -0
- package/packages/contracts/ip-address.mjs +10 -0
- package/packages/contracts/lookup-evidence.mjs +3 -2
- package/packages/contracts/mail-authentication.mjs +5 -0
- package/packages/contracts/message-intake.mjs +37 -0
- package/packages/contracts/offline-comparison.mjs +4 -1
- package/packages/contracts/passive-mail.mjs +10 -0
- package/packages/contracts/risk-calibration.mjs +44 -29
- package/packages/contracts/schema-lifecycle.mjs +3 -1
- package/packages/contracts/web-capture.mjs +2 -2
- package/packages/contracts/workspace-portability.mjs +144 -28
- package/packages/evidence/collection-quality.mjs +67 -0
- package/packages/evidence/credential-form-attribution.mjs +60 -0
- package/packages/evidence/dns-query-outcome.mjs +72 -0
- package/packages/evidence/email-recipient.mjs +18 -0
- package/packages/evidence/image-regions.mjs +15 -0
- package/packages/evidence/lookup-target.mjs +5 -1
- package/packages/evidence/observation.mjs +2 -0
- package/packages/evidence/png-pixels.mjs +117 -0
- package/packages/evidence/public-address-policy.mjs +83 -0
- package/packages/evidence/record-identity.mjs +60 -16
- package/packages/evidence/technology-evidence-role.mjs +51 -0
- package/packages/interchange/defensive-indicator-export.mjs +264 -0
- package/packages/interchange/external-findings-import.mjs +36 -29
- package/packages/interchange/json-output.mjs +14 -0
- package/packages/interchange/managed-indicator-export.mjs +36 -0
- package/packages/interchange/managed-indicator-set.mjs +223 -0
- package/packages/interchange/misp-indicator-export.mjs +93 -0
- package/packages/interchange/sharing-policy.mjs +56 -0
- package/packages/interchange/stix-indicator-export.mjs +129 -0
- package/packages/interchange/web-capture-import.mjs +22 -15
- package/packages/investigation/acquisition-decision-packet.mjs +79 -1
- package/packages/investigation/bulk-mail-exposure.mjs +7 -13
- package/packages/investigation/capture-coverage.mjs +57 -0
- package/packages/investigation/connector-provenance-review.mjs +100 -0
- package/packages/investigation/context-review.mjs +35 -0
- package/packages/investigation/document-intake.mjs +87 -0
- package/packages/investigation/docx-intake.mjs +133 -0
- package/packages/investigation/domain-history-review.mjs +132 -0
- package/packages/investigation/domain-transition-review.mjs +49 -0
- package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
- package/packages/investigation/har-intake.mjs +68 -0
- package/packages/investigation/identity-event-intake.mjs +131 -0
- package/packages/investigation/identity-incident-review.mjs +39 -0
- package/packages/investigation/incident-sequence-review.mjs +58 -0
- package/packages/investigation/infrastructure-observation.mjs +234 -0
- package/packages/investigation/intake-context.mjs +284 -0
- package/packages/investigation/intake-indicators.mjs +116 -0
- package/packages/investigation/intake-phones.mjs +155 -0
- package/packages/investigation/intake-report.mjs +20 -0
- package/packages/investigation/investigation-manifest.mjs +35 -9
- package/packages/investigation/link-intake.mjs +209 -0
- package/packages/investigation/lookup-asset-graph.mjs +1 -1
- package/packages/investigation/mail-authentication-review.mjs +222 -0
- package/packages/investigation/mail-header-identity.mjs +209 -0
- package/packages/investigation/message-intake.mjs +235 -0
- package/packages/investigation/page-behaviour.mjs +113 -0
- package/packages/investigation/pdf-intake.mjs +155 -0
- package/packages/investigation/platform-continuity-review.mjs +60 -0
- package/packages/investigation/qr-intake.mjs +56 -0
- package/packages/investigation/requested-action-hints.mjs +11 -0
- package/packages/investigation/selected-input-review.mjs +17 -0
- package/packages/investigation/storefront-review.mjs +70 -0
- package/packages/monitoring/domain-feed.mjs +321 -0
- package/packages/workspace/brand-candidate-workflow.mjs +351 -0
- package/packages/workspace/brand-keyword-campaign.mjs +231 -0
- package/packages/workspace/brand-profile-model.mjs +48 -4
- package/packages/workspace/bulk-review-model.mjs +28 -26
- package/packages/workspace/bulk-session-model.mjs +27 -13
- package/packages/workspace/campaign-model.mjs +1 -1
- package/packages/workspace/candidate-watch-handoff.mjs +195 -0
- package/packages/workspace/detection-rule-model.mjs +11 -3
- package/packages/workspace/hostile-input.mjs +1 -1
- package/packages/workspace/investigation-guide.mjs +2 -2
- package/packages/workspace/investigation-template-model.mjs +26 -2
- package/packages/workspace/page-baseline.mjs +2 -2
- package/packages/workspace/relationship-observation-model.mjs +17 -13
- package/packages/workspace/shortlist-model.mjs +24 -7
- package/packages/workspace/watchlist-history.mjs +59 -16
- package/packages/workspace/watchlist-store.mjs +152 -11
- package/packages/workspace/website-snapshot-model.mjs +28 -3
- package/packages/workspace/workspace-archive-crypto.mjs +1 -1
- package/packages/workspace/workspace-archive.mjs +5 -4
- package/third-party-notices.txt +549 -16
- package/lib/hostname.mjs +0 -2
|
@@ -2,7 +2,50 @@
|
|
|
2
2
|
// injected so every traversal can be fixture-tested and the caller retains the
|
|
3
3
|
// existing DNS timeout and missing/error semantics.
|
|
4
4
|
import { parse as parseDomain } from 'tldts';
|
|
5
|
+
import { isValidAsciiHostname } from "../packages/contracts/domain-name.mjs";
|
|
5
6
|
import { parseDmarcRecords, parseDmarcReportingAuthorization, parseReportingDestination, parseSpfRecords } from "./domain-posture-parsers.mjs";
|
|
7
|
+
const SPF_PERMISSIVE_PATH_ISSUE = 'Observed permissive +all evidence on a potentially reachable positive SPF include or redirect path; review sender authorisation.';
|
|
8
|
+
// This is a conservative universal-result check for unconditional all/include
|
|
9
|
+
// chains, not a sender evaluator. Conditional mechanisms remain unknown.
|
|
10
|
+
function spfStaticResult(policy) {
|
|
11
|
+
const parsed = policy.parsed;
|
|
12
|
+
if (!parsed?.valid)
|
|
13
|
+
return { everySender: null, permissive: false };
|
|
14
|
+
let uncertain = false, permissive = false;
|
|
15
|
+
const finish = (result) => ({
|
|
16
|
+
everySender: !uncertain ? result : null,
|
|
17
|
+
permissive,
|
|
18
|
+
});
|
|
19
|
+
for (const mechanism of parsed.mechanisms) {
|
|
20
|
+
if (mechanism.kind === 'all') {
|
|
21
|
+
if (mechanism.qualifier === '+')
|
|
22
|
+
permissive = true;
|
|
23
|
+
return finish(mechanism.qualifier);
|
|
24
|
+
}
|
|
25
|
+
if (mechanism.kind === 'include') {
|
|
26
|
+
const child = policy.children.get(mechanism.includeIndex);
|
|
27
|
+
const result = child ? spfStaticResult(child) : { everySender: null, permissive: false };
|
|
28
|
+
if (mechanism.qualifier === '+' && result.permissive)
|
|
29
|
+
permissive = true;
|
|
30
|
+
if (result.everySender === '+')
|
|
31
|
+
return finish(mechanism.qualifier);
|
|
32
|
+
if (result.everySender !== null)
|
|
33
|
+
continue; // Only child pass matches include.
|
|
34
|
+
uncertain = true;
|
|
35
|
+
}
|
|
36
|
+
else {
|
|
37
|
+
// Without sender evaluation, even a literal mechanism remains conditional.
|
|
38
|
+
uncertain = true;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (parsed.redirect) {
|
|
42
|
+
const child = policy.children.get(parsed.includes.length);
|
|
43
|
+
const result = child ? spfStaticResult(child) : { everySender: null, permissive: false };
|
|
44
|
+
permissive ||= result.permissive;
|
|
45
|
+
return result.everySender === null ? { everySender: null, permissive } : finish(result.everySender);
|
|
46
|
+
}
|
|
47
|
+
return { everySender: null, permissive };
|
|
48
|
+
}
|
|
6
49
|
const SPF_LOOKUP_LIMIT = 10;
|
|
7
50
|
const SPF_VOID_LOOKUP_LIMIT = 2;
|
|
8
51
|
const SPF_MAX_DEPTH = 5;
|
|
@@ -40,6 +83,7 @@ function branchFor(item, state, terminalPolicy, dnsLookupTerms, issues) {
|
|
|
40
83
|
};
|
|
41
84
|
}
|
|
42
85
|
async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
|
|
86
|
+
const rootPolicy = { parsed: null, children: new Map() };
|
|
43
87
|
const queue = [{
|
|
44
88
|
domain: rootDomain,
|
|
45
89
|
parent: null,
|
|
@@ -48,6 +92,7 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
|
|
|
48
92
|
path: [rootDomain],
|
|
49
93
|
records: rootQuery.records,
|
|
50
94
|
error: rootQuery.error,
|
|
95
|
+
policy: rootPolicy,
|
|
51
96
|
}];
|
|
52
97
|
const branches = [];
|
|
53
98
|
const issues = [];
|
|
@@ -99,6 +144,7 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
|
|
|
99
144
|
branches.push(branchFor(item, parsed.valid ? 'success' : 'invalid', parsed.terminalPolicy, parsed.dnsLookupTerms, parsed.issues));
|
|
100
145
|
if (!parsed.valid)
|
|
101
146
|
continue;
|
|
147
|
+
item.policy.parsed = parsed;
|
|
102
148
|
if (dnsLookupTerms > SPF_LOOKUP_LIMIT) {
|
|
103
149
|
boundReached = true;
|
|
104
150
|
issues.push(`The expanded policy declares more than ${SPF_LOOKUP_LIMIT} DNS-querying terms.`);
|
|
@@ -110,7 +156,9 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
|
|
|
110
156
|
...parsed.includes.map((domain) => ({ relation: 'include', domain })),
|
|
111
157
|
...(parsed.redirect ? [{ relation: 'redirect', domain: parsed.redirect }] : []),
|
|
112
158
|
];
|
|
113
|
-
for (const dependency of dependencies) {
|
|
159
|
+
for (const [dependencyIndex, dependency] of dependencies.entries()) {
|
|
160
|
+
const policy = { parsed: null, children: new Map() };
|
|
161
|
+
item.policy.children.set(dependencyIndex, policy);
|
|
114
162
|
const normalized = strictHostname(dependency.domain);
|
|
115
163
|
if (!normalized) {
|
|
116
164
|
branches.push(branchFor({
|
|
@@ -119,6 +167,7 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
|
|
|
119
167
|
relation: dependency.relation,
|
|
120
168
|
depth: item.depth + 1,
|
|
121
169
|
path: item.path,
|
|
170
|
+
policy,
|
|
122
171
|
}, 'invalid', null, 0, ['The SPF dependency is not a literal public hostname; macros are not expanded by this audit.']));
|
|
123
172
|
continue;
|
|
124
173
|
}
|
|
@@ -128,6 +177,7 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
|
|
|
128
177
|
relation: dependency.relation,
|
|
129
178
|
depth: item.depth + 1,
|
|
130
179
|
path: [...item.path, normalized],
|
|
180
|
+
policy,
|
|
131
181
|
});
|
|
132
182
|
}
|
|
133
183
|
}
|
|
@@ -144,6 +194,8 @@ async function expandSpfPolicy(rootDomain, rootQuery, resolveTxt) {
|
|
|
144
194
|
: incomplete
|
|
145
195
|
? 'partial'
|
|
146
196
|
: 'complete';
|
|
197
|
+
if (rootPolicy.parsed?.terminalPolicy !== 'pass' && spfStaticResult(rootPolicy).permissive)
|
|
198
|
+
issues.unshift(SPF_PERMISSIVE_PATH_ISSUE);
|
|
147
199
|
return {
|
|
148
200
|
version: 1,
|
|
149
201
|
state,
|
|
@@ -166,9 +218,10 @@ function reportDestinationDomain(value) {
|
|
|
166
218
|
if (/[!,;]/u.test(uri))
|
|
167
219
|
return null;
|
|
168
220
|
const destination = parseReportingDestination(uri);
|
|
169
|
-
|
|
221
|
+
const domain = destination?.scheme === 'mailto' ? strictHostname(destination.host) : null;
|
|
222
|
+
return domain && isValidAsciiHostname(domain) ? domain : null;
|
|
170
223
|
}
|
|
171
|
-
async function validateDmarcExternalReporting(ownerDomain, dmarcQuery, resolveTxt) {
|
|
224
|
+
async function validateDmarcExternalReporting(ownerDomain, dmarcQuery, resolveTxt, resolveOrganisationalDomain) {
|
|
172
225
|
if (dmarcQuery.error)
|
|
173
226
|
return [];
|
|
174
227
|
const parsed = parseDmarcRecords(dmarcQuery.records);
|
|
@@ -192,7 +245,7 @@ async function validateDmarcExternalReporting(ownerDomain, dmarcQuery, resolveTx
|
|
|
192
245
|
});
|
|
193
246
|
continue;
|
|
194
247
|
}
|
|
195
|
-
if (destinationDomain === ownerDomain
|
|
248
|
+
if (destinationDomain === ownerDomain) {
|
|
196
249
|
results.push({
|
|
197
250
|
destination: destinationDomain,
|
|
198
251
|
reportType: item.reportType,
|
|
@@ -202,6 +255,20 @@ async function validateDmarcExternalReporting(ownerDomain, dmarcQuery, resolveTx
|
|
|
202
255
|
});
|
|
203
256
|
continue;
|
|
204
257
|
}
|
|
258
|
+
const ownerOrganisation = resolveOrganisationalDomain ? await resolveOrganisationalDomain(ownerDomain) : null;
|
|
259
|
+
const recipientOrganisation = ownerOrganisation && resolveOrganisationalDomain ? await resolveOrganisationalDomain(destinationDomain) : null;
|
|
260
|
+
if (ownerOrganisation && ownerOrganisation === recipientOrganisation) {
|
|
261
|
+
results.push({ destination: destinationDomain, reportType: item.reportType, recordName: null, state: 'self', error: null });
|
|
262
|
+
continue;
|
|
263
|
+
}
|
|
264
|
+
const externalConfirmed = Boolean(ownerOrganisation && recipientOrganisation);
|
|
265
|
+
// Descendant reporting previously made no authorisation request. Do not
|
|
266
|
+
// broaden default collection to resolve its unknown organisational scope.
|
|
267
|
+
if (!resolveOrganisationalDomain && destinationDomain.endsWith(`.${ownerDomain}`)) {
|
|
268
|
+
results.push({ destination: destinationDomain, reportType: item.reportType, recordName: null, state: 'unavailable',
|
|
269
|
+
error: 'The organisational boundary was not checked. Select the additional DNS review to determine whether external reporting authorisation is required.' });
|
|
270
|
+
continue;
|
|
271
|
+
}
|
|
205
272
|
const recordName = `${ownerDomain}._report._dmarc.${destinationDomain}`;
|
|
206
273
|
if (recordName.length > 253) {
|
|
207
274
|
results.push({ destination: destinationDomain, reportType: item.reportType, recordName: null, state: 'unavailable', error: 'The reporting authorisation query exceeds the DNS name limit.' });
|
|
@@ -223,8 +290,9 @@ async function validateDmarcExternalReporting(ownerDomain, dmarcQuery, resolveTx
|
|
|
223
290
|
destination: destinationDomain,
|
|
224
291
|
reportType: item.reportType,
|
|
225
292
|
recordName,
|
|
226
|
-
state: query.error ? 'unavailable' : authorized ? 'authorized' : 'not_found',
|
|
227
|
-
error: query.error
|
|
293
|
+
state: query.error ? 'unavailable' : authorized ? 'authorized' : externalConfirmed ? 'not_found' : 'unavailable',
|
|
294
|
+
error: query.error ?? (!authorized && !externalConfirmed
|
|
295
|
+
? 'The organisational boundary is unknown; no authorisation requirement can be inferred from the missing record.' : null),
|
|
228
296
|
});
|
|
229
297
|
}
|
|
230
298
|
return results;
|
|
@@ -276,4 +344,4 @@ function buildExternalDependencies({ domain, nameservers, mx, spfExpansion, dmar
|
|
|
276
344
|
|| left.target.localeCompare(right.target)
|
|
277
345
|
|| left.source.localeCompare(right.source)));
|
|
278
346
|
}
|
|
279
|
-
export { SPF_LOOKUP_LIMIT, SPF_MAX_DEPTH, SPF_VOID_LOOKUP_LIMIT, buildExternalDependencies, expandSpfPolicy, reportDestinationDomain, validateDmarcExternalReporting, };
|
|
347
|
+
export { SPF_PERMISSIVE_PATH_ISSUE, SPF_LOOKUP_LIMIT, SPF_MAX_DEPTH, SPF_VOID_LOOKUP_LIMIT, buildExternalDependencies, expandSpfPolicy, reportDestinationDomain, validateDmarcExternalReporting, };
|
|
@@ -48,6 +48,7 @@ function parseSpfRecords(records) {
|
|
|
48
48
|
terminalPolicy: null,
|
|
49
49
|
redirect: null,
|
|
50
50
|
includes: [],
|
|
51
|
+
mechanisms: [],
|
|
51
52
|
authorizingTerms: [],
|
|
52
53
|
dnsLookupTerms: 0,
|
|
53
54
|
issues: [],
|
|
@@ -175,6 +176,14 @@ function parseSpfRecords(records) {
|
|
|
175
176
|
result.includes = terms
|
|
176
177
|
.map((term) => term.match(/^[+?~-]?include:(.+)$/i)?.[1] || '')
|
|
177
178
|
.filter(Boolean);
|
|
179
|
+
let includeIndex = 0;
|
|
180
|
+
result.mechanisms = terms.filter(term => !/^[+?~-]?[a-z][a-z0-9_.-]*=/iu.test(term)).map(term => {
|
|
181
|
+
const qualifier = (/^[+?~-]/u.test(term) ? term[0] : '+');
|
|
182
|
+
const body = /^[+?~-]/u.test(term) ? term.slice(1) : term;
|
|
183
|
+
const include = body.match(/^include:(.+)$/iu);
|
|
184
|
+
return { kind: /^all$/iu.test(body) ? 'all' : include ? 'include' : 'other', qualifier,
|
|
185
|
+
includeIndex: include ? includeIndex++ : null };
|
|
186
|
+
});
|
|
178
187
|
if (terms.some((term) => /^[+?~-]?ptr(?::|$)/i.test(term))) {
|
|
179
188
|
result.issues.push('The deprecated ptr mechanism should not be used.');
|
|
180
189
|
}
|
package/lib/domain-posture.mjs
CHANGED
|
@@ -7,12 +7,12 @@ 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 {
|
|
10
|
+
import { collectDnsInheritanceReview } from "./dns-inheritance-review.mjs";
|
|
11
11
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
12
12
|
import { DOMAIN_POSTURE_COMPARISON_VERSION, MAX_POSTURE_CHECK_RECORDS, POSTURE_CHECK_SOURCES } from "../packages/evidence/domain-posture-context.mjs";
|
|
13
13
|
import { canonicalPostureRecords } from "../packages/evidence/domain-control-runtime.mjs";
|
|
14
14
|
import { parseSpfRecords, parseDmarcRecords, parseMtaStsDnsRecords, parseMtaStsPolicy, parseTlsRptRecords, parseBimiRecords, parseDkimRecords, } from "./domain-posture-parsers.mjs";
|
|
15
|
-
import { buildExternalDependencies, expandSpfPolicy, validateDmarcExternalReporting, } from "./domain-posture-analysis.mjs";
|
|
15
|
+
import { SPF_PERMISSIVE_PATH_ISSUE, buildExternalDependencies, expandSpfPolicy, validateDmarcExternalReporting, } from "./domain-posture-analysis.mjs";
|
|
16
16
|
const DNS_TIMEOUT_MS = 6000;
|
|
17
17
|
const POLICY_TIMEOUT_MS = 7000;
|
|
18
18
|
const MAX_POLICY_BYTES = 64 * 1024;
|
|
@@ -156,7 +156,8 @@ function spfCheck(query, expansion) {
|
|
|
156
156
|
remediation: 'Replace +all with an explicit sender allowlist and a restrictive terminal policy.',
|
|
157
157
|
});
|
|
158
158
|
}
|
|
159
|
-
|
|
159
|
+
const permissiveExpansion = expansion?.issues.includes(SPF_PERMISSIVE_PATH_ISSUE) ?? false;
|
|
160
|
+
if (parsed.terminalPolicy === 'fail' && parsed.issues.length === 0 && !permissiveExpansion && (!expansion || expansion.state === 'complete')) {
|
|
160
161
|
return check('spf', 'SPF', 'pass', 'Restrictive fail-all policy', { detail: details.join(' '), records: parsed.records });
|
|
161
162
|
}
|
|
162
163
|
if (expansion && expansion.state !== 'complete') {
|
|
@@ -166,6 +167,12 @@ function spfCheck(query, expansion) {
|
|
|
166
167
|
remediation: 'Review unresolved, invalid, cyclic, or budget-limited include and redirect branches before treating the policy as complete.',
|
|
167
168
|
});
|
|
168
169
|
}
|
|
170
|
+
if (permissiveExpansion) {
|
|
171
|
+
return check('spf', 'SPF', 'warning', 'SPF authorisation needs review', {
|
|
172
|
+
detail: details.join(' '), records: parsed.records,
|
|
173
|
+
remediation: 'Review the positive include or redirect path containing +all; complete collection does not establish restrictive sender authorisation.',
|
|
174
|
+
});
|
|
175
|
+
}
|
|
169
176
|
if (parsed.terminalPolicy === 'redirect') {
|
|
170
177
|
return check('spf', 'SPF', 'info', 'Policy delegates evaluation with redirect', {
|
|
171
178
|
detail: details.join(' '), records: parsed.records,
|
|
@@ -202,16 +209,18 @@ function dmarcCheck(query, authorizations = []) {
|
|
|
202
209
|
parsed.failureReporting ? 'Failure reporting is configured.' : 'No failure reporting destination is configured.',
|
|
203
210
|
...parsed.issues,
|
|
204
211
|
];
|
|
205
|
-
const
|
|
206
|
-
const unresolvedExternal = external.filter((authorization) => authorization.state !== 'authorized');
|
|
212
|
+
const unresolvedExternal = authorizations.filter((authorization) => !['self', 'authorized'].includes(authorization.state));
|
|
207
213
|
const intendedDestinations = parsed.aggregateDestinations.length + parsed.failureDestinations.length;
|
|
208
214
|
const omittedDestinations = Math.max(0, intendedDestinations - authorizations.length);
|
|
209
215
|
if (omittedDestinations) {
|
|
210
216
|
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
217
|
}
|
|
212
|
-
if (
|
|
213
|
-
details.push(`${
|
|
214
|
-
+ `${unresolvedExternal.length}
|
|
218
|
+
if (authorizations.length > 0) {
|
|
219
|
+
details.push(`${authorizations.length} reporting destination${authorizations.length === 1 ? '' : 's'} reviewed; `
|
|
220
|
+
+ `${unresolvedExternal.length} need further evidence.`);
|
|
221
|
+
}
|
|
222
|
+
if (unresolvedExternal.some(item => item.state === 'unavailable')) {
|
|
223
|
+
details.push('An unknown organisational boundary or unavailable query does not establish a missing authorisation requirement.');
|
|
215
224
|
}
|
|
216
225
|
if (parsed.testMode) {
|
|
217
226
|
return check('dmarc', 'DMARC', 'warning', 'Policy is in test mode (t=y)', {
|
|
@@ -235,9 +244,7 @@ function dmarcCheck(query, authorizations = []) {
|
|
|
235
244
|
return check('dmarc', 'DMARC', 'warning', `Enforced at p=${parsed.policy}; reporting authorisation is incomplete`, {
|
|
236
245
|
detail: `${details.join(' ')} ${unresolvedExternal.map((authorization) => `${authorization.destination}: ${authorization.state}.`).join(' ')}`,
|
|
237
246
|
records: parsed.records,
|
|
238
|
-
remediation:
|
|
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.',
|
|
247
|
+
remediation: 'Review unresolved destinations and their organisational boundaries, then confirm any required external reporting authorisation before relying on delivery.',
|
|
241
248
|
});
|
|
242
249
|
}
|
|
243
250
|
if (!parsed.aggregateReporting) {
|
|
@@ -301,6 +308,11 @@ function dnssecDelegationConsistencyCheck(input, registry) {
|
|
|
301
308
|
}
|
|
302
309
|
const value = String(input.value || '').toLowerCase();
|
|
303
310
|
const hasDsRecords = registry.dsRecordCount > 0;
|
|
311
|
+
if (registry.dsDataTruncated) {
|
|
312
|
+
return check('dnssec_delegation_consistency', 'DNSSEC delegation consistency', 'info', 'Retained DS evidence is incomplete', {
|
|
313
|
+
detail: 'The normalised registry response reported truncated DS data, so delegation consistency is inconclusive.',
|
|
314
|
+
});
|
|
315
|
+
}
|
|
304
316
|
if (value === 'signed' && hasDsRecords) {
|
|
305
317
|
return check('dnssec_delegation_consistency', 'DNSSEC delegation consistency', 'pass', 'Signed state and retained DS records agree', {
|
|
306
318
|
detail: `${registry.dsRecordCount} registry DS record${registry.dsRecordCount === 1 ? '' : 's'} retained.`,
|
|
@@ -311,11 +323,6 @@ function dnssecDelegationConsistencyCheck(input, registry) {
|
|
|
311
323
|
detail: 'This is a consistency result, not a recommendation to leave DNSSEC disabled.',
|
|
312
324
|
});
|
|
313
325
|
}
|
|
314
|
-
if (registry.dsDataTruncated) {
|
|
315
|
-
return check('dnssec_delegation_consistency', 'DNSSEC delegation consistency', 'info', 'Retained DS evidence is incomplete', {
|
|
316
|
-
detail: 'The normalised registry response reported truncated DS data, so apparent disagreement is inconclusive.',
|
|
317
|
-
});
|
|
318
|
-
}
|
|
319
326
|
if (value === 'signed' && !hasDsRecords) {
|
|
320
327
|
return check('dnssec_delegation_consistency', 'DNSSEC delegation consistency', 'warning', 'Signed state reported without retained DS records', {
|
|
321
328
|
detail: 'This can reflect registry publication differences or incomplete upstream data and should be reviewed before changing delegation.',
|
|
@@ -360,13 +367,25 @@ function matchesMtaPattern(host, pattern) {
|
|
|
360
367
|
}
|
|
361
368
|
return normalizedHost === normalizedPattern;
|
|
362
369
|
}
|
|
370
|
+
function mailPolicyApplicability(mxQuery) {
|
|
371
|
+
if (mxQuery.error)
|
|
372
|
+
return { hasMx: false, detail: 'Receiving-mail applicability is unknown because the MX lookup failed.' };
|
|
373
|
+
const records = completeMxRecords(mxQuery.records);
|
|
374
|
+
if (!records)
|
|
375
|
+
return { hasMx: false, detail: 'Receiving-mail applicability is unknown because the MX evidence is incomplete.' };
|
|
376
|
+
const { hasMx, hasNullMx } = classifyMxRecords(records);
|
|
377
|
+
return { hasMx, detail: hasMx ? '' : hasNullMx
|
|
378
|
+
? 'Null MX explicitly declares that this domain does not accept inbound mail.'
|
|
379
|
+
: 'No MX records were observed. This alone does not establish that the domain cannot receive mail.' };
|
|
380
|
+
}
|
|
363
381
|
function mtaStsCheck(dnsQuery, policyFetch, mxQuery) {
|
|
364
382
|
if (dnsQuery.error)
|
|
365
383
|
return queryFailureCheck('mta_sts', 'MTA-STS', dnsQuery.error);
|
|
366
384
|
const dnsPolicy = parseMtaStsDnsRecords(dnsQuery.records);
|
|
367
|
-
const hasMx = !mxQuery.error && classifyMxRecords(asMxRecords(mxQuery.records)).hasMx;
|
|
368
385
|
if (dnsPolicy.records.length === 0) {
|
|
386
|
+
const { hasMx, detail } = mailPolicyApplicability(mxQuery);
|
|
369
387
|
return check('mta_sts', 'MTA-STS', hasMx ? 'warning' : 'info', 'No MTA-STS policy advertised', {
|
|
388
|
+
detail,
|
|
370
389
|
remediation: hasMx ? 'Publish _mta-sts and serve a valid policy over HTTPS to require authenticated TLS for inbound mail.' : '',
|
|
371
390
|
});
|
|
372
391
|
}
|
|
@@ -436,9 +455,9 @@ function tlsRptCheck(query, mxQuery) {
|
|
|
436
455
|
return queryFailureCheck('tls_rpt', 'TLS-RPT', query.error);
|
|
437
456
|
const parsed = parseTlsRptRecords(query.records);
|
|
438
457
|
if (parsed.records.length === 0) {
|
|
439
|
-
const hasMx
|
|
458
|
+
const { hasMx, detail } = mailPolicyApplicability(mxQuery);
|
|
440
459
|
return check('tls_rpt', 'TLS-RPT', hasMx ? 'warning' : 'info', 'No SMTP TLS reporting policy', {
|
|
441
|
-
detail
|
|
460
|
+
detail,
|
|
442
461
|
remediation: hasMx ? 'Publish a v=TLSRPTv1 record at _smtp._tls with a monitored rua destination.' : '',
|
|
443
462
|
});
|
|
444
463
|
}
|
|
@@ -785,17 +804,18 @@ async function collectDomainPosture(domain, { dkimSelectors = [], retiredDkimSel
|
|
|
785
804
|
const enrichment = [
|
|
786
805
|
parsedMtaDns?.valid ? dependencies.fetchMtaStsPolicy(domain, signal) : Promise.resolve(null),
|
|
787
806
|
expandSpfPolicy(domain, spf, resolveEnrichmentTxt),
|
|
788
|
-
validateDmarcExternalReporting(domain, dmarc, resolveEnrichmentTxt),
|
|
789
807
|
includeInheritedDns === true
|
|
790
|
-
? (dependencies.
|
|
791
|
-
? dependencies.
|
|
808
|
+
? (dependencies.collectDnsInheritanceReview
|
|
809
|
+
? dependencies.collectDnsInheritanceReview(domain, dmarc, nameservers, signal ? { signal } : {})
|
|
792
810
|
: Promise.reject(new TypeError('The explicit DNS review collector is unavailable.')))
|
|
793
|
-
:
|
|
811
|
+
: validateDmarcExternalReporting(domain, dmarc, resolveEnrichmentTxt)
|
|
812
|
+
.then(dmarcAuthorizations => ({ checks: [], dmarcAuthorizations })),
|
|
794
813
|
];
|
|
795
814
|
// A rejected policy request must not release the operation's capacity while
|
|
796
815
|
// other started enrichment collectors are still settling.
|
|
797
|
-
const [mtaStsPolicy, spfExpansion,
|
|
816
|
+
const [mtaStsPolicy, spfExpansion, dnsReview] = await Promise.all(enrichment)
|
|
798
817
|
.finally(() => Promise.allSettled(enrichment));
|
|
818
|
+
const { dmarcAuthorizations, checks: inheritanceChecks } = dnsReview;
|
|
799
819
|
signal?.throwIfAborted();
|
|
800
820
|
const dnssec = !rdap
|
|
801
821
|
? { value: null, error: 'RDAP did not return a domain record.' }
|
|
@@ -884,7 +904,7 @@ async function checkDomainPosture(domain, options = {}, dependencies) {
|
|
|
884
904
|
now: () => new Date(),
|
|
885
905
|
setTimer: (callback, milliseconds) => setTimeout(callback, milliseconds),
|
|
886
906
|
clearTimer: handle => clearTimeout(handle),
|
|
887
|
-
|
|
907
|
+
collectDnsInheritanceReview,
|
|
888
908
|
});
|
|
889
909
|
}
|
|
890
910
|
finally {
|