@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
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
// Registration authority, lifecycle dates and positive-only delegation evidence.
|
|
2
|
+
import { promises as dns } from 'node:dns';
|
|
3
|
+
import { abortable } from "./abort.mjs";
|
|
4
|
+
import { fetchRdapRecord } from "./rdap.mjs";
|
|
5
|
+
import { buildWhoisChain, parseWhoisChain } from "./whois.mjs";
|
|
6
|
+
import { featureDecision, networkFeaturePolicy } from "./feature-policy.mjs";
|
|
7
|
+
import { parseRegistryDate, registryDateIso } from "./registry-dates.mjs";
|
|
8
|
+
import { nonEmptyErrorMessage } from "./error-detail.mjs";
|
|
9
|
+
import { registryServiceAdmissionFor } from "./registry-capabilities.mjs";
|
|
10
|
+
import { recordOrEmpty as errorRecord } from "./json-record.mjs";
|
|
11
|
+
const DNS_DELEGATION_TIMEOUT_MS = 4000;
|
|
12
|
+
const MAX_DELEGATION_NAMESERVERS = 50;
|
|
13
|
+
const MISSING_DNS_CODES = new Set(['ENODATA', 'ENOTFOUND', 'ENONAME', 'NXDOMAIN']);
|
|
14
|
+
function registryStatusToken(value) {
|
|
15
|
+
return value.toLowerCase().replace(/[^a-z0-9]+/gu, '');
|
|
16
|
+
}
|
|
17
|
+
function rdapEventDate(events, action) {
|
|
18
|
+
const date = events.find((event) => event.action === action)?.date;
|
|
19
|
+
return typeof date === 'string' ? date : null;
|
|
20
|
+
}
|
|
21
|
+
function computeAgeDays(dateStr) {
|
|
22
|
+
const d = parseRegistryDate(dateStr);
|
|
23
|
+
return d ? Math.floor((Date.now() - d.getTime()) / 86400000) : null;
|
|
24
|
+
}
|
|
25
|
+
function computeDaysUntil(dateStr) {
|
|
26
|
+
const d = parseRegistryDate(dateStr);
|
|
27
|
+
return d ? Math.ceil((d.getTime() - Date.now()) / 86400000) : null;
|
|
28
|
+
}
|
|
29
|
+
const PRIVACY_MARKERS = [
|
|
30
|
+
/redacted for privacy/i,
|
|
31
|
+
/data protected/i,
|
|
32
|
+
/privacy\s*protect/i,
|
|
33
|
+
/whoisguard/i,
|
|
34
|
+
/domains by proxy/i,
|
|
35
|
+
/perfect privacy/i,
|
|
36
|
+
/contact privacy/i,
|
|
37
|
+
/private registration/i,
|
|
38
|
+
/identity protect/i,
|
|
39
|
+
/not disclosed/i,
|
|
40
|
+
/withheld for privacy/i,
|
|
41
|
+
];
|
|
42
|
+
// Explicit privacy/redaction markers establish a positive privacy signal.
|
|
43
|
+
// Usable public contact data establishes a negative one. A missing or blank
|
|
44
|
+
// contact can instead reflect registry policy, access tier, parser coverage,
|
|
45
|
+
// or ordinary omission, so it remains inconclusive rather than being promoted
|
|
46
|
+
// to an affirmative privacy claim.
|
|
47
|
+
function isPrivacyProtected(registrant) {
|
|
48
|
+
if (!registrant)
|
|
49
|
+
return null;
|
|
50
|
+
const blob = [registrant.name, registrant.org, registrant.email].filter(Boolean).join(' ');
|
|
51
|
+
if (!blob)
|
|
52
|
+
return null;
|
|
53
|
+
return PRIVACY_MARKERS.some((re) => re.test(blob));
|
|
54
|
+
}
|
|
55
|
+
// RDAP Lookup retains a bounded multi-value contact inventory, but Bulk and
|
|
56
|
+
// watchlist availability records deliberately keep the historical compact
|
|
57
|
+
// shape. This prevents repeated contact arrays and registry links from
|
|
58
|
+
// expanding browser-local stores or flowing into case evidence implicitly.
|
|
59
|
+
function compactContact(contact) {
|
|
60
|
+
if (!contact || typeof contact !== 'object' || Array.isArray(contact))
|
|
61
|
+
return null;
|
|
62
|
+
const value = contact;
|
|
63
|
+
const compact = {
|
|
64
|
+
handle: value.handle || null,
|
|
65
|
+
name: value.name || null,
|
|
66
|
+
org: value.org || null,
|
|
67
|
+
email: value.email || null,
|
|
68
|
+
phone: value.phone || null,
|
|
69
|
+
address: value.address || null,
|
|
70
|
+
};
|
|
71
|
+
return Object.values(compact).some(Boolean) ? compact : null;
|
|
72
|
+
}
|
|
73
|
+
// A number of ccTLD registries publish neither an RDAP bootstrap entry nor a
|
|
74
|
+
// generally reachable port-43 WHOIS service. A positive NS answer cannot
|
|
75
|
+
// replace registry registration data, but it does prove that the registrable
|
|
76
|
+
// domain has an active DNS delegation. Use that as a bounded, positive-only
|
|
77
|
+
// fallback: no answer is never interpreted as availability because registered
|
|
78
|
+
// domains can legitimately be undelegated.
|
|
79
|
+
async function checkDnsDelegation(domain, options = {}) {
|
|
80
|
+
options.signal?.throwIfAborted();
|
|
81
|
+
// Cancel only this lookup's c-ares requests, never the shared DNS resolver.
|
|
82
|
+
const ownedResolver = options.resolver ? null : new dns.Resolver();
|
|
83
|
+
const resolve = options.resolver || ((name) => ownedResolver.resolveNs(name));
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
|
|
86
|
+
const cancel = () => ownedResolver?.cancel();
|
|
87
|
+
signal.addEventListener('abort', cancel, { once: true });
|
|
88
|
+
const timer = setTimeout(() => controller.abort(new Error('DNS delegation lookup timed out')), DNS_DELEGATION_TIMEOUT_MS);
|
|
89
|
+
try {
|
|
90
|
+
const records = await abortable(() => resolve(domain), signal);
|
|
91
|
+
const validNameservers = [...new Set((Array.isArray(records) ? records : [])
|
|
92
|
+
.filter((value) => typeof value === 'string')
|
|
93
|
+
.map((value) => value.trim().replace(/\.+$/, '').toLowerCase())
|
|
94
|
+
.filter((value) => value.length > 0 && value.length <= 253)
|
|
95
|
+
.filter((value) => value.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label))))]
|
|
96
|
+
.sort();
|
|
97
|
+
const normalized = validNameservers.slice(0, MAX_DELEGATION_NAMESERVERS);
|
|
98
|
+
return {
|
|
99
|
+
delegated: normalized.length > 0,
|
|
100
|
+
nameservers: normalized,
|
|
101
|
+
nameserversTruncated: validNameservers.length > MAX_DELEGATION_NAMESERVERS,
|
|
102
|
+
error: null,
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
catch (err) {
|
|
106
|
+
options.signal?.throwIfAborted();
|
|
107
|
+
const error = errorRecord(err);
|
|
108
|
+
if (typeof error.code === 'string' && MISSING_DNS_CODES.has(error.code)) {
|
|
109
|
+
return { delegated: false, nameservers: [], nameserversTruncated: false, error: null };
|
|
110
|
+
}
|
|
111
|
+
return {
|
|
112
|
+
delegated: false,
|
|
113
|
+
nameservers: [],
|
|
114
|
+
nameserversTruncated: false,
|
|
115
|
+
error: nonEmptyErrorMessage(err, String(err)).slice(0, 180),
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
finally {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
signal.removeEventListener('abort', cancel);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
function registryPolicyDetail(domain, fast) {
|
|
124
|
+
const details = [];
|
|
125
|
+
const rdapAdmission = registryServiceAdmissionFor(domain, 'rdap');
|
|
126
|
+
if (rdapAdmission && !rdapAdmission.allowed) {
|
|
127
|
+
details.push('RDAP was not queried because the registry access policy does not permit collection.');
|
|
128
|
+
}
|
|
129
|
+
const whoisAdmission = fast ? null : registryServiceAdmissionFor(domain, 'whois');
|
|
130
|
+
if (whoisAdmission && !whoisAdmission.allowed) {
|
|
131
|
+
details.push(whoisAdmission.state === 'permission_required'
|
|
132
|
+
? 'WHOIS was not queried because registry permission or source authorisation is required.'
|
|
133
|
+
: 'WHOIS was not queried because IANA publishes no domain WHOIS service for this suffix.');
|
|
134
|
+
}
|
|
135
|
+
return details.length ? ` ${details.join(' ')}` : '';
|
|
136
|
+
}
|
|
137
|
+
// fast: true skips the WHOIS fallback (no TCP:43 chain) and the homepage
|
|
138
|
+
// fetch (no for-sale/parking detection) - just RDAP plus the signals
|
|
139
|
+
// derivable from it (age, expiry proximity, privacy). Meant for scanning
|
|
140
|
+
// large sourcing candidate lists quickly and gently on registry rate
|
|
141
|
+
// limits; anything it can't resolve (state "unknown") is meant to get a
|
|
142
|
+
// follow-up deep check (fast: false, the default) on the shortlist only.
|
|
143
|
+
async function resolveDomainRegistration(domain, options, featurePolicy) {
|
|
144
|
+
options.signal?.throwIfAborted();
|
|
145
|
+
const fast = options.fast === true;
|
|
146
|
+
const rdapEnabled = featureDecision('rdap', featurePolicy).enabled;
|
|
147
|
+
const whoisEnabled = featureDecision('whois', featurePolicy).enabled;
|
|
148
|
+
const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
|
|
149
|
+
const hasPreloadedRdap = Object.prototype.hasOwnProperty.call(options, 'rdapRecord');
|
|
150
|
+
const hasPreloadedWhois = Object.prototype.hasOwnProperty.call(options, 'whoisChain');
|
|
151
|
+
const hasPreloadedRdapPromise = Object.prototype.hasOwnProperty.call(options, 'rdapRecordPromise');
|
|
152
|
+
const hasPreloadedWhoisPromise = Object.prototype.hasOwnProperty.call(options, 'whoisChainPromise');
|
|
153
|
+
const hasPreloadedDnsDelegation = Object.prototype.hasOwnProperty.call(options, 'dnsDelegation');
|
|
154
|
+
let nameservers = [];
|
|
155
|
+
let statuses = [];
|
|
156
|
+
let rdapServer = null;
|
|
157
|
+
let rdapFound = false;
|
|
158
|
+
let registrar = null;
|
|
159
|
+
let registrant = null;
|
|
160
|
+
let abuse = null;
|
|
161
|
+
let createdDate = null;
|
|
162
|
+
let expiryDate = null;
|
|
163
|
+
let createdDateIso = null;
|
|
164
|
+
let expiryDateIso = null;
|
|
165
|
+
let registrationSource = null;
|
|
166
|
+
let registrationConfidence = 'high';
|
|
167
|
+
let dnssec = null;
|
|
168
|
+
let registryDnsEvidence = null;
|
|
169
|
+
const assessment = (registration) => ({
|
|
170
|
+
registration, registryDnsEvidence, nameservers, dnssec,
|
|
171
|
+
});
|
|
172
|
+
if (rdapEnabled) {
|
|
173
|
+
try {
|
|
174
|
+
// Shared with /api/rdap (lib/rdap.mts's fetchRdapRecord) rather than a
|
|
175
|
+
// separate fetch+parse here - same registry data either way, and this
|
|
176
|
+
// also picks up that function's short-TTL cache (lib/lookup-cache.mts)
|
|
177
|
+
// and upstream timeout for free.
|
|
178
|
+
const recordValue = hasPreloadedRdapPromise
|
|
179
|
+
? await abortable(() => options.rdapRecordPromise, options.signal)
|
|
180
|
+
: hasPreloadedRdap
|
|
181
|
+
? options.rdapRecord
|
|
182
|
+
: await fetchRdapRecord('domain', domain, options.signal ? { signal: options.signal } : {});
|
|
183
|
+
options.signal?.throwIfAborted();
|
|
184
|
+
const record = errorRecord(recordValue);
|
|
185
|
+
const recordRdapServer = typeof record.rdapServer === 'string' ? record.rdapServer : null;
|
|
186
|
+
const upstreamStatus = typeof record.upstreamStatus === 'number' ? record.upstreamStatus : null;
|
|
187
|
+
if (Object.keys(record).length) {
|
|
188
|
+
rdapServer = recordRdapServer;
|
|
189
|
+
if (upstreamStatus === 404) {
|
|
190
|
+
const cleartext = record.transportSecurity === 'http'
|
|
191
|
+
|| (recordRdapServer !== null && /^http:\/\//iu.test(recordRdapServer));
|
|
192
|
+
return assessment({
|
|
193
|
+
state: 'available',
|
|
194
|
+
confidence: cleartext ? 'medium' : 'high',
|
|
195
|
+
detail: `The registry's RDAP service has no record for this domain.${cleartext ? ' This answer arrived over unencrypted HTTP, so its transport was not authenticated.' : ''}`,
|
|
196
|
+
source: 'rdap',
|
|
197
|
+
rdapServer: recordRdapServer,
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
const parsed = errorRecord(record.parsed);
|
|
201
|
+
if (Object.keys(parsed).length) {
|
|
202
|
+
registryDnsEvidence = parsed;
|
|
203
|
+
statuses = Array.isArray(parsed.statuses)
|
|
204
|
+
? parsed.statuses.filter((status) => typeof status === 'string').map((status) => status.toLowerCase())
|
|
205
|
+
: [];
|
|
206
|
+
nameservers = Array.isArray(parsed.nameservers)
|
|
207
|
+
? parsed.nameservers.filter((nameserver) => typeof nameserver === 'string')
|
|
208
|
+
: [];
|
|
209
|
+
registrar = compactContact(parsed.registrar);
|
|
210
|
+
registrant = compactContact(parsed.registrant);
|
|
211
|
+
abuse = compactContact(parsed.abuse);
|
|
212
|
+
const events = Array.isArray(parsed.events)
|
|
213
|
+
? parsed.events.map(errorRecord)
|
|
214
|
+
: [];
|
|
215
|
+
const lifecycle = errorRecord(parsed.lifecycle);
|
|
216
|
+
createdDate = typeof lifecycle.createdDate === 'string'
|
|
217
|
+
? lifecycle.createdDate
|
|
218
|
+
: rdapEventDate(events, 'registration');
|
|
219
|
+
expiryDate = typeof lifecycle.expiryDate === 'string'
|
|
220
|
+
? lifecycle.expiryDate
|
|
221
|
+
: rdapEventDate(events, 'expiration');
|
|
222
|
+
createdDateIso = typeof lifecycle.createdDateIso === 'string'
|
|
223
|
+
? lifecycle.createdDateIso
|
|
224
|
+
: registryDateIso(createdDate);
|
|
225
|
+
expiryDateIso = typeof lifecycle.expiryDateIso === 'string'
|
|
226
|
+
? lifecycle.expiryDateIso
|
|
227
|
+
: registryDateIso(expiryDate);
|
|
228
|
+
dnssec = typeof parsed.dnssec === 'string' ? parsed.dnssec : null;
|
|
229
|
+
rdapFound = true;
|
|
230
|
+
registrationSource = 'rdap';
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
catch {
|
|
235
|
+
options.signal?.throwIfAborted();
|
|
236
|
+
/* fall through to WHOIS-based detection (deep mode only) */
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
const dnsDelegationPromise = !rdapFound && dnsIntelligenceEnabled
|
|
240
|
+
? hasPreloadedDnsDelegation
|
|
241
|
+
? Promise.resolve(options.dnsDelegation)
|
|
242
|
+
: checkDnsDelegation(domain, {
|
|
243
|
+
...(options.resolveNs ? { resolver: options.resolveNs } : {}),
|
|
244
|
+
...(options.signal ? { signal: options.signal } : {}),
|
|
245
|
+
})
|
|
246
|
+
: null;
|
|
247
|
+
// DNS may settle while the independent WHOIS source is still pending. Keep
|
|
248
|
+
// its rejection observed even if WHOIS supplies an early conclusive return.
|
|
249
|
+
void dnsDelegationPromise?.catch(() => { });
|
|
250
|
+
let whoisChain = null;
|
|
251
|
+
let whoisParsed = null;
|
|
252
|
+
if (!rdapFound && !fast && whoisEnabled) {
|
|
253
|
+
try {
|
|
254
|
+
whoisChain = (hasPreloadedWhoisPromise
|
|
255
|
+
? await abortable(() => options.whoisChainPromise, options.signal)
|
|
256
|
+
: hasPreloadedWhois
|
|
257
|
+
? options.whoisChain
|
|
258
|
+
: await buildWhoisChain(domain, options.signal ? { signal: options.signal } : {})) ?? null;
|
|
259
|
+
options.signal?.throwIfAborted();
|
|
260
|
+
if (!Array.isArray(whoisChain))
|
|
261
|
+
throw new Error('WHOIS chain unavailable');
|
|
262
|
+
const parsed = parseWhoisChain(whoisChain, domain);
|
|
263
|
+
whoisParsed = parsed;
|
|
264
|
+
if (parsed.notFound) {
|
|
265
|
+
return assessment({
|
|
266
|
+
state: 'available',
|
|
267
|
+
confidence: 'medium',
|
|
268
|
+
detail: `WHOIS reports no matching record for this domain${parsed.notFoundSource ? ` (per ${parsed.notFoundSource})` : ''}.`,
|
|
269
|
+
source: 'whois',
|
|
270
|
+
});
|
|
271
|
+
}
|
|
272
|
+
if (parsed.registrationStatus === 'registered') {
|
|
273
|
+
if (parsed.nameservers.length)
|
|
274
|
+
nameservers = parsed.nameservers;
|
|
275
|
+
if (parsed.statuses.length)
|
|
276
|
+
statuses = parsed.statuses.map((status) => status.toLowerCase());
|
|
277
|
+
if (parsed.registrar) {
|
|
278
|
+
registrar = {
|
|
279
|
+
handle: null,
|
|
280
|
+
ianaId: parsed.registrarIanaId || null,
|
|
281
|
+
name: parsed.registrar,
|
|
282
|
+
org: null,
|
|
283
|
+
email: parsed.abuseEmail || null,
|
|
284
|
+
phone: parsed.abusePhone || null,
|
|
285
|
+
};
|
|
286
|
+
}
|
|
287
|
+
if (parsed.registrantName || parsed.registrantOrg || parsed.registrantEmail || parsed.registrantPhone) {
|
|
288
|
+
registrant = {
|
|
289
|
+
handle: null,
|
|
290
|
+
name: parsed.registrantName || null,
|
|
291
|
+
org: parsed.registrantOrg || null,
|
|
292
|
+
email: parsed.registrantEmail || null,
|
|
293
|
+
phone: parsed.registrantPhone || null,
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
if (parsed.abuseEmail || parsed.abusePhone) {
|
|
297
|
+
abuse = { handle: null, name: null, org: parsed.registrar || null, email: parsed.abuseEmail || null, phone: parsed.abusePhone || null };
|
|
298
|
+
}
|
|
299
|
+
createdDate = parsed.createdDate || null;
|
|
300
|
+
expiryDate = parsed.expiryDate || null;
|
|
301
|
+
createdDateIso = parsed.createdDateIso || parsed.lifecycle?.createdDateIso || registryDateIso(createdDate);
|
|
302
|
+
expiryDateIso = parsed.expiryDateIso || parsed.lifecycle?.expiryDateIso || registryDateIso(expiryDate);
|
|
303
|
+
if (!dnssec)
|
|
304
|
+
dnssec = parsed.dnssec || null;
|
|
305
|
+
registrationSource = 'whois';
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
options.signal?.throwIfAborted();
|
|
310
|
+
/* if both RDAP and WHOIS fail, we simply can't determine availability */
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
// WHOIS ran and was not a confirmed not-found, but produced no positive
|
|
314
|
+
// registration evidence either - e.g. the registry answered inconclusively
|
|
315
|
+
// or every referral hop failed/rate-limited. Report "unknown" rather than
|
|
316
|
+
// fabricating "registered" from an empty record.
|
|
317
|
+
const hasWhoisRegistrationData = whoisParsed?.registrationStatus === 'registered';
|
|
318
|
+
let dnsDelegated = false;
|
|
319
|
+
if (!rdapFound && !hasWhoisRegistrationData && dnsDelegationPromise) {
|
|
320
|
+
const delegation = await dnsDelegationPromise;
|
|
321
|
+
if (delegation && delegation.delegated === true && Array.isArray(delegation.nameservers) && delegation.nameservers.length) {
|
|
322
|
+
dnsDelegated = true;
|
|
323
|
+
nameservers = delegation.nameservers;
|
|
324
|
+
registrationSource = 'dns';
|
|
325
|
+
registrationConfidence = 'medium';
|
|
326
|
+
}
|
|
327
|
+
}
|
|
328
|
+
options.signal?.throwIfAborted();
|
|
329
|
+
if (!rdapFound && !hasWhoisRegistrationData && !dnsDelegated) {
|
|
330
|
+
const disabledSources = [
|
|
331
|
+
!rdapEnabled ? 'RDAP' : null,
|
|
332
|
+
!fast && !whoisEnabled ? 'WHOIS' : null,
|
|
333
|
+
!dnsIntelligenceEnabled ? 'DNS intelligence' : null,
|
|
334
|
+
].filter(Boolean);
|
|
335
|
+
const disabledDetail = disabledSources.length
|
|
336
|
+
? ` ${disabledSources.join(', ')} ${disabledSources.length === 1 ? 'is' : 'are'} disabled by deployment policy.`
|
|
337
|
+
: '';
|
|
338
|
+
const policyDetail = registryPolicyDetail(domain, fast);
|
|
339
|
+
return assessment({
|
|
340
|
+
state: 'unknown',
|
|
341
|
+
confidence: 'low',
|
|
342
|
+
detail: fast
|
|
343
|
+
? `No enabled registration source produced a record or authoritative delegation. A fast scan cannot determine registration status.${disabledDetail}${policyDetail}`
|
|
344
|
+
: whoisParsed && whoisParsed.failedHop
|
|
345
|
+
? `WHOIS was inconclusive - a referral hop did not answer conclusively (${whoisParsed.failedHop}).${policyDetail}`
|
|
346
|
+
: `No enabled registration source returned conclusive data or an authoritative DNS delegation.${disabledDetail}${policyDetail}`,
|
|
347
|
+
...(!fast && whoisEnabled ? { source: 'whois' } : {}),
|
|
348
|
+
});
|
|
349
|
+
}
|
|
350
|
+
const domainAgeDays = computeAgeDays(createdDateIso || createdDate);
|
|
351
|
+
const expiresInDays = computeDaysUntil(expiryDateIso || expiryDate);
|
|
352
|
+
// DNS proves delegation only; it says nothing about whether registry
|
|
353
|
+
// contact data is privacy-protected or merely unavailable.
|
|
354
|
+
const privacyProtected = registrationSource === 'dns' ? null : isPrivacyProtected(registrant);
|
|
355
|
+
const baseInfo = {
|
|
356
|
+
nameservers,
|
|
357
|
+
statuses,
|
|
358
|
+
registrar,
|
|
359
|
+
registrant,
|
|
360
|
+
abuse,
|
|
361
|
+
createdDate,
|
|
362
|
+
expiryDate,
|
|
363
|
+
createdDateIso,
|
|
364
|
+
expiryDateIso,
|
|
365
|
+
rdapServer,
|
|
366
|
+
domainAgeDays,
|
|
367
|
+
expiresInDays,
|
|
368
|
+
privacyProtected,
|
|
369
|
+
dnssec,
|
|
370
|
+
source: registrationSource,
|
|
371
|
+
};
|
|
372
|
+
if (statuses.some((status) => {
|
|
373
|
+
const normalizedStatus = registryStatusToken(status);
|
|
374
|
+
return normalizedStatus.includes('pendingdelete') || normalizedStatus.includes('redemptionperiod');
|
|
375
|
+
})) {
|
|
376
|
+
return assessment({
|
|
377
|
+
state: 'expiring',
|
|
378
|
+
confidence: 'medium',
|
|
379
|
+
detail: 'Domain is in redemption/pending-delete status and may become available soon.',
|
|
380
|
+
...baseInfo,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return assessment({
|
|
384
|
+
state: 'registered',
|
|
385
|
+
confidence: registrationConfidence,
|
|
386
|
+
detail: registrationSource === 'dns'
|
|
387
|
+
? 'Authoritative DNS delegation confirms the domain is registered, but RDAP/WHOIS registration details were unavailable.'
|
|
388
|
+
: 'Domain is registered. Run a deep check for parking/for-sale detection.',
|
|
389
|
+
...baseInfo,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
export { resolveDomainRegistration, checkDnsDelegation, isPrivacyProtected };
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
// Portable tree bounds, private-field exclusion and availability projection.
|
|
2
2
|
// Source-specific publication assembly remains in evidence-export.mts.
|
|
3
|
-
import { isSafeJsonObjectKey } from "
|
|
3
|
+
import { isSafeJsonObjectKey } from "../packages/analysis/bounded-json.mjs";
|
|
4
4
|
import { recordOrNull } from "./json-record.mjs";
|
|
5
|
-
import { isUriShapedLabel } from "
|
|
5
|
+
import { isUriShapedLabel } from "../packages/analysis/portable-generator.mjs";
|
|
6
6
|
import { validHttpDeliveryMetadata, validPagePublicationMetadata } from "./homepage-metadata-contract.mjs";
|
|
7
|
+
import { normalizeWebCollectionQuality } from "../packages/evidence/collection-quality.mjs";
|
|
7
8
|
import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES, LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS, LOOKUP_EVIDENCE_PORTABLE_MAX_KEY_LENGTH, LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH, } from "../packages/contracts/lookup-evidence.mjs";
|
|
8
9
|
const PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
|
|
9
10
|
'applicable', 'type', 'domain', 'state', 'confidence', 'detail', 'source',
|
|
@@ -12,7 +13,7 @@ const PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
|
|
|
12
13
|
'expiresInDays', 'privacyProtected', 'dnssec', 'activityStatus',
|
|
13
14
|
'websiteProbeStatus', 'websiteProbeDetail', 'http', 'deepScanComplete',
|
|
14
15
|
'faviconHash', 'faviconPHash', 'pageTitle', 'hasPasswordField',
|
|
15
|
-
'phishingLanguageMatch', 'hasExternalFormAction', 'externalAssetHosts',
|
|
16
|
+
'phishingLanguageMatch', 'hasExternalFormAction', 'hasExternalPasswordForm', 'externalAssetHosts',
|
|
16
17
|
'pageIdentity', 'credentialSurfaceProfile', 'structuredDataIdentity',
|
|
17
18
|
'technologyProfile', 'pageRoleProfile', 'clientBehaviorProfile',
|
|
18
19
|
'securityPosture', 'dns', 'tls', 'hasMx', 'hasNullMx', 'mxHosts', 'hasSpf',
|
|
@@ -22,7 +23,9 @@ const LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
|
|
|
22
23
|
...[...PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS]
|
|
23
24
|
.filter((key) => !['registrar', 'registrant', 'abuse'].includes(key)),
|
|
24
25
|
'registryContactsExcluded', 'observationHostname', 'webObservationMode',
|
|
26
|
+
'webCollectionQuality',
|
|
25
27
|
]);
|
|
28
|
+
const HISTORICAL_LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([...LOOKUP_AVAILABILITY_ANALYSIS_KEYS].filter(key => key !== 'webCollectionQuality'));
|
|
26
29
|
const PRIVATE_EVIDENCE_KEYS = new Set([
|
|
27
30
|
'authorization', 'proxyauthorization', 'cookie', 'cookies', 'setcookie',
|
|
28
31
|
'session', 'sessionid', 'sessiontoken', 'token', 'accesstoken', 'refreshtoken',
|
|
@@ -102,6 +105,7 @@ const LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS = new Set([
|
|
|
102
105
|
// Credential, structured identity, role, behaviour, technology, and posture
|
|
103
106
|
// projections. Only fixed categories/counts and bounded descriptions enter.
|
|
104
107
|
'credentialSurfaceVersion', 'methods', 'actions', 'missing', 'get', 'post',
|
|
108
|
+
'formAttribution', 'unassociatedInputs', 'index', 'destinations', 'relationship', 'origin',
|
|
105
109
|
'dialog', 'other', 'sameOrigin', 'external', 'cleartext', 'unclassified',
|
|
106
110
|
'inputs', 'classifiedCount', 'categories', 'password', 'email', 'username',
|
|
107
111
|
'one_time_code', 'payment', 'inputsObserved', 'classifiedInputs',
|
|
@@ -174,7 +178,7 @@ function privateEvidenceKey(value, item) {
|
|
|
174
178
|
|| /(?:request|response|authorization|proxy)headers?$/u.test(normalized)
|
|
175
179
|
|| (normalized.includes('credential') && !normalized.startsWith('credentialsurface'));
|
|
176
180
|
}
|
|
177
|
-
export function portableUri(value) {
|
|
181
|
+
export function portableUri(value, legacyUris = false) {
|
|
178
182
|
const text = boundedString(value, 2048);
|
|
179
183
|
if (!text)
|
|
180
184
|
return null;
|
|
@@ -184,7 +188,7 @@ export function portableUri(value) {
|
|
|
184
188
|
|| url.username
|
|
185
189
|
|| url.password)
|
|
186
190
|
return null;
|
|
187
|
-
if (url.protocol === 'http:' || url.protocol === 'https:') {
|
|
191
|
+
if (!legacyUris || url.protocol === 'http:' || url.protocol === 'https:') {
|
|
188
192
|
url.search = '';
|
|
189
193
|
url.hash = '';
|
|
190
194
|
}
|
|
@@ -268,7 +272,7 @@ function projectLookupEvidenceAvailabilityPublicValue(value, path, state, depth)
|
|
|
268
272
|
if (typeof value === 'string') {
|
|
269
273
|
const normalizedParent = normalizedEvidenceKey(path.at(-1) || '');
|
|
270
274
|
if (PORTABLE_URL_KEYS.has(normalizedParent))
|
|
271
|
-
return portableUri(value);
|
|
275
|
+
return portableUri(value, true);
|
|
272
276
|
if (PORTABLE_ORIGIN_COLLECTION_KEYS.has(normalizedParent))
|
|
273
277
|
return portableOrigin(value);
|
|
274
278
|
return projectPortableString(value);
|
|
@@ -313,21 +317,23 @@ function projectLookupEvidenceAvailabilityPublicValue(value, path, state, depth)
|
|
|
313
317
|
}
|
|
314
318
|
return output;
|
|
315
319
|
}
|
|
316
|
-
function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
|
|
320
|
+
function projectLookupEvidenceAvailabilityValue(value, path, state, depth, legacyUris) {
|
|
317
321
|
consumePortableProjectionEntry(state, depth);
|
|
318
322
|
if (value === null || typeof value === 'boolean' || typeof value === 'number')
|
|
319
323
|
return value;
|
|
320
324
|
if (typeof value === 'string') {
|
|
321
325
|
const normalizedParent = normalizedEvidenceKey(path.at(-1) || '');
|
|
326
|
+
if (path.join('.') === 'credentialSurfaceProfile.formAttribution.forms.destinations.origin')
|
|
327
|
+
return portableOrigin(value);
|
|
322
328
|
if (PORTABLE_URL_KEYS.has(normalizedParent))
|
|
323
|
-
return portableUri(value);
|
|
329
|
+
return portableUri(value, legacyUris);
|
|
324
330
|
if (PORTABLE_ORIGIN_COLLECTION_KEYS.has(normalizedParent))
|
|
325
331
|
return portableOrigin(value);
|
|
326
332
|
return projectLookupEvidenceAvailabilityString(value);
|
|
327
333
|
}
|
|
328
334
|
if (Array.isArray(value)) {
|
|
329
335
|
return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
|
|
330
|
-
.map((item) => projectLookupEvidenceAvailabilityValue(item, path, state, depth + 1));
|
|
336
|
+
.map((item) => projectLookupEvidenceAvailabilityValue(item, path, state, depth + 1, legacyUris));
|
|
331
337
|
}
|
|
332
338
|
const source = recordOrNull(value);
|
|
333
339
|
if (!source)
|
|
@@ -341,7 +347,7 @@ function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
|
|
|
341
347
|
const boundedCount = Number.isSafeInteger(item)
|
|
342
348
|
&& Number(item) >= 0
|
|
343
349
|
&& Number(item) <= LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS;
|
|
344
|
-
const credentialCategoryCount =
|
|
350
|
+
const credentialCategoryCount = ['credentialSurfaceProfile.inputs.categories', 'credentialSurfaceProfile.formAttribution.forms.categories'].includes(parentPath)
|
|
345
351
|
&& boundedCount;
|
|
346
352
|
const sanClassCount = parentPath === 'tls.certificate.subjectAltNames.classes'
|
|
347
353
|
&& LOOKUP_AVAILABILITY_TLS_SAN_CLASS_KEYS.has(key)
|
|
@@ -371,7 +377,7 @@ function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
|
|
|
371
377
|
&& (!LOOKUP_AVAILABILITY_VALUE_PATHS.has(parentPath)
|
|
372
378
|
|| (item !== null && typeof item !== 'string')))
|
|
373
379
|
continue;
|
|
374
|
-
output[key] = projectLookupEvidenceAvailabilityValue(item, [...path, key], state, depth + 1);
|
|
380
|
+
output[key] = projectLookupEvidenceAvailabilityValue(item, [...path, key], state, depth + 1, legacyUris);
|
|
375
381
|
}
|
|
376
382
|
return output;
|
|
377
383
|
}
|
|
@@ -381,7 +387,7 @@ function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
|
|
|
381
387
|
* extension cannot become authenticated evidence merely by avoiding a
|
|
382
388
|
* credential-key denylist.
|
|
383
389
|
*/
|
|
384
|
-
function projectLookupEvidenceAvailabilityWithKeys(value, rootKeys, registryContactsExcluded, currentPrivacyRules) {
|
|
390
|
+
function projectLookupEvidenceAvailabilityWithKeys(value, rootKeys, registryContactsExcluded, currentPrivacyRules, legacyUris = false) {
|
|
385
391
|
const source = recordOrNull(value);
|
|
386
392
|
if (!source)
|
|
387
393
|
return null;
|
|
@@ -394,8 +400,12 @@ function projectLookupEvidenceAvailabilityWithKeys(value, rootKeys, registryCont
|
|
|
394
400
|
const item = source[key];
|
|
395
401
|
if (privateEvidenceKey(key, item))
|
|
396
402
|
continue;
|
|
403
|
+
if (key === 'webCollectionQuality') {
|
|
404
|
+
output[key] = normalizeWebCollectionQuality(item);
|
|
405
|
+
continue;
|
|
406
|
+
}
|
|
397
407
|
output[key] = currentPrivacyRules
|
|
398
|
-
? projectLookupEvidenceAvailabilityValue(item, [key], state, 1)
|
|
408
|
+
? projectLookupEvidenceAvailabilityValue(item, [key], state, 1, legacyUris)
|
|
399
409
|
: projectLookupEvidenceAvailabilityPublicValue(item, [key], state, 1);
|
|
400
410
|
}
|
|
401
411
|
if (registryContactsExcluded)
|
|
@@ -437,10 +447,10 @@ export function projectLookupEvidenceAvailabilityPublic(value) {
|
|
|
437
447
|
* and abuse contact routes are deliberately excluded at the root boundary;
|
|
438
448
|
* the marker prevents that privacy omission from being read as source absence.
|
|
439
449
|
*/
|
|
440
|
-
export function projectLookupEvidenceAvailability(value) {
|
|
441
|
-
return projectLookupEvidenceAvailabilityWithKeys(value, LOOKUP_AVAILABILITY_ANALYSIS_KEYS, true, true);
|
|
450
|
+
export function projectLookupEvidenceAvailability(value, { legacyUris = false } = {}) {
|
|
451
|
+
return projectLookupEvidenceAvailabilityWithKeys(value, legacyUris ? HISTORICAL_LOOKUP_AVAILABILITY_ANALYSIS_KEYS : LOOKUP_AVAILABILITY_ANALYSIS_KEYS, true, true, legacyUris);
|
|
442
452
|
}
|
|
443
|
-
export function assertLookupEvidencePrivacySafeTree(value) {
|
|
453
|
+
export function assertLookupEvidencePrivacySafeTree(value, { legacyUris = false } = {}) {
|
|
444
454
|
assertLookupEvidencePortableTree(value);
|
|
445
455
|
const pending = [value];
|
|
446
456
|
while (pending.length) {
|
|
@@ -466,7 +476,7 @@ export function assertLookupEvidencePrivacySafeTree(value) {
|
|
|
466
476
|
if (privateEvidenceKey(normalized, item)) {
|
|
467
477
|
throw new TypeError('Lookup evidence contains excluded private request or session material.');
|
|
468
478
|
}
|
|
469
|
-
if (typeof item === 'string' && PORTABLE_URL_KEYS.has(normalized) && portableUri(item) !== item) {
|
|
479
|
+
if (typeof item === 'string' && PORTABLE_URL_KEYS.has(normalized) && portableUri(item, legacyUris) !== item) {
|
|
470
480
|
throw new TypeError('Lookup evidence contains a credential-bearing or non-portable URL.');
|
|
471
481
|
}
|
|
472
482
|
pending.push(item);
|
package/lib/evidence-export.mjs
CHANGED
|
@@ -3,12 +3,12 @@ import { projectLookupEvidencePrivacySafeTree, projectLookupEvidenceAvailability
|
|
|
3
3
|
export { projectLookupEvidencePrivacySafeTree, projectLookupEvidenceAvailabilityPublic, projectLookupEvidenceAvailability, assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, serializeLookupEvidence, } from "./evidence-export-privacy.mjs";
|
|
4
4
|
import { compareRdapPublications, compareRegistrySources } from "./registry-comparison.mjs";
|
|
5
5
|
import { buildRegistryInsights } from "./registry-insights.mjs";
|
|
6
|
-
import { buildPortableGeneratorMetadata } from "
|
|
7
|
-
import { assertBoundedJsonStructure } from "
|
|
6
|
+
import { buildPortableGeneratorMetadata } from "../packages/analysis/portable-generator.mjs";
|
|
7
|
+
import { assertBoundedJsonStructure } from "../packages/analysis/bounded-json.mjs";
|
|
8
8
|
import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
9
9
|
import { LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../packages/contracts/lookup-evidence.mjs";
|
|
10
10
|
export * from "../packages/contracts/lookup-evidence.mjs";
|
|
11
|
-
import { isValidAsciiHostname } from "
|
|
11
|
+
import { isValidAsciiHostname } from "../packages/contracts/domain-name.mjs";
|
|
12
12
|
import { validLookupObservationScope } from "../packages/evidence/lookup-target.mjs";
|
|
13
13
|
import { REGISTRAR_STANDING_SCHEMA, REGISTRAR_STANDING_VERSION, registrarIanaIds, registrarStandingObservedBy, resolveRegistrarIanaId, validRegistrarStanding, } from "./registrar-standing-contract.mjs";
|
|
14
14
|
const REGISTRAR_RDAP_STATUSES = new Set([
|
package/lib/evidence-report.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { registryAccessProfileLabel } from "./registry-access.mjs";
|
|
2
|
-
import {
|
|
2
|
+
import { dnsAddressFamilyEvidence } from "../packages/evidence/dns-query-outcome.mjs";
|
|
3
|
+
import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../packages/analysis/portable-generator.mjs";
|
|
3
4
|
import { technologyEvidenceRoles, } from "./technology-evidence-role.mjs";
|
|
4
5
|
const MAX_REPORT_VALUE_LENGTH = 300;
|
|
5
6
|
const MAX_REPORT_LIST_ITEMS = 50;
|
|
@@ -241,6 +242,8 @@ function buildLookupEvidenceReport(document, options = {}) {
|
|
|
241
242
|
title: 'DNS and mail',
|
|
242
243
|
fields: [
|
|
243
244
|
reportField('DNS status', displayLabel(dns.status)),
|
|
245
|
+
reportField('A address result', dnsAddressFamilyEvidence(dns, 'a').value),
|
|
246
|
+
reportField('AAAA address result', dnsAddressFamilyEvidence(dns, 'aaaa').value),
|
|
244
247
|
reportField('Observed', dns.observedAt),
|
|
245
248
|
reportField('Name servers', listText(availability.nameservers)),
|
|
246
249
|
reportField('MX hosts', listText(availability.mxHosts)),
|
|
@@ -264,6 +267,7 @@ function buildLookupEvidenceReport(document, options = {}) {
|
|
|
264
267
|
reportField('Redirects', http.redirectCount),
|
|
265
268
|
reportField('Page title', availability.pageTitle),
|
|
266
269
|
reportField('Password field observed', yesNoUnknown(availability.hasPasswordField)),
|
|
270
|
+
reportField('Password form declares external destination', yesNoUnknown(availability.hasExternalPasswordForm)),
|
|
267
271
|
reportField('Authoritative nameserver evidence', listText(availability.nameservers)),
|
|
268
272
|
reportField('Observed edge, CDN, reverse proxy or WAF', reportTechnologyNames(technologyFindings, 'observed_edge')),
|
|
269
273
|
reportField('Application-platform indicators', reportTechnologyNames(technologyFindings, 'application_platform')),
|