@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
|
@@ -1,107 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
export
|
|
3
|
-
export const OPPORTUNITY_REVIEW_THRESHOLD = 70;
|
|
4
|
-
const STATE_BASE = Object.freeze({
|
|
5
|
-
available: 82,
|
|
6
|
-
for_sale: 70,
|
|
7
|
-
expiring: 52,
|
|
8
|
-
registered: 22,
|
|
9
|
-
});
|
|
10
|
-
const STATE_LABELS = Object.freeze({
|
|
11
|
-
available: 'available',
|
|
12
|
-
for_sale: 'registered with a sale signal',
|
|
13
|
-
expiring: 'registered in a lifecycle transition',
|
|
14
|
-
registered: 'registered',
|
|
15
|
-
});
|
|
16
|
-
function finite(value) {
|
|
17
|
-
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
18
|
-
}
|
|
19
|
-
function formatAge(days) {
|
|
20
|
-
if (days < 60)
|
|
21
|
-
return `${Math.max(0, Math.round(days))}d old`;
|
|
22
|
-
const years = days / 365.25;
|
|
23
|
-
return years < 1 ? `${Math.round(days / 30)}mo old` : `${years.toFixed(1)}y old`;
|
|
24
|
-
}
|
|
25
|
-
function addFactor(factors, dimensions, family, label, delta) {
|
|
26
|
-
factors.push({ family, label, delta });
|
|
27
|
-
dimensions.set(family, (dimensions.get(family) ?? 0) + delta);
|
|
28
|
-
}
|
|
29
|
-
export function normalizeOpportunityModelVersion(value) {
|
|
30
|
-
return typeof value === 'number' && Number.isSafeInteger(value) && value > 0 && value <= 1000 ? value : null;
|
|
31
|
-
}
|
|
32
|
-
// Opportunity is acquisition readiness, not price, value, eligibility, or a
|
|
33
|
-
// prediction that a registration will lapse. It uses only bounded evidence
|
|
34
|
-
// already collected for the current lookup.
|
|
35
|
-
export function explainOpportunityScore(value) {
|
|
36
|
-
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
37
|
-
return null;
|
|
38
|
-
const input = value;
|
|
39
|
-
const state = input.availability ?? input.state;
|
|
40
|
-
if (typeof state !== 'string' || !Object.hasOwn(STATE_BASE, state))
|
|
41
|
-
return null;
|
|
42
|
-
const factors = [];
|
|
43
|
-
const dimensions = new Map();
|
|
44
|
-
addFactor(factors, dimensions, 'registration', `Base readiness for “${STATE_LABELS[state] ?? state}”`, STATE_BASE[state] ?? 0);
|
|
45
|
-
if (input.confidence === 'high')
|
|
46
|
-
addFactor(factors, dimensions, 'registration', 'High-confidence registration decision', 8);
|
|
47
|
-
else if (input.confidence === 'medium')
|
|
48
|
-
addFactor(factors, dimensions, 'registration', 'Medium-confidence registration decision', 3);
|
|
49
|
-
else if (input.confidence === 'low')
|
|
50
|
-
addFactor(factors, dimensions, 'registration', 'Low-confidence registration decision', -8);
|
|
51
|
-
if (state === 'for_sale')
|
|
52
|
-
addFactor(factors, dimensions, 'listing', 'Bounded sale signal observed; listing remains unverified', 6);
|
|
53
|
-
else if (input.activityStatus === 'parked')
|
|
54
|
-
addFactor(factors, dimensions, 'listing', 'Parked page observed without a verified sale offer', 3);
|
|
55
|
-
else if (state === 'registered' && input.activityStatus === 'active') {
|
|
56
|
-
addFactor(factors, dimensions, 'listing', 'Active website suggests an in-use registration', -12);
|
|
57
|
-
}
|
|
58
|
-
if (input.hasPublicRegistrantContact === true) {
|
|
59
|
-
addFactor(factors, dimensions, 'contactability', 'Public registrant contact route observed; acquisition suitability is not established', 10);
|
|
60
|
-
}
|
|
61
|
-
const expiresInDays = finite(input.expiresInDays);
|
|
62
|
-
if (state === 'registered' && expiresInDays !== null && expiresInDays >= 0 && expiresInDays < 30) {
|
|
63
|
-
addFactor(factors, dimensions, 'lifecycle', 'Published expiry is within 30 days; release is not implied', 4);
|
|
64
|
-
}
|
|
65
|
-
if (typeof input.privacyProtected === 'boolean') {
|
|
66
|
-
addFactor(factors, dimensions, 'contactability', 'Registration privacy is neutral; it does not establish contactability', 0);
|
|
67
|
-
}
|
|
68
|
-
const ageDays = finite(input.domainAgeDays);
|
|
69
|
-
if (ageDays !== null && ageDays >= 0) {
|
|
70
|
-
addFactor(factors, dimensions, 'registration', `Domain age (${formatAge(ageDays)}) is context only`, 0);
|
|
71
|
-
}
|
|
72
|
-
const rawScore = Math.round(factors.reduce((total, factor) => total + factor.delta, 0));
|
|
73
|
-
const score = Math.max(0, Math.min(100, rawScore));
|
|
74
|
-
const dimensionLabels = {
|
|
75
|
-
registration: 'Registration certainty',
|
|
76
|
-
listing: 'Listing readiness',
|
|
77
|
-
contactability: 'Contactability',
|
|
78
|
-
lifecycle: 'Lifecycle timing',
|
|
79
|
-
};
|
|
80
|
-
const observedFamilies = [...dimensions.keys()];
|
|
81
|
-
const evidenceQuality = buildScoreEvidenceQuality(input, {
|
|
82
|
-
expectedDepth: 'fast',
|
|
83
|
-
observedFamilies,
|
|
84
|
-
expectedFamilies: ['registration'],
|
|
85
|
-
});
|
|
86
|
-
return {
|
|
87
|
-
modelVersion: OPPORTUNITY_MODEL_VERSION,
|
|
88
|
-
score,
|
|
89
|
-
rawScore,
|
|
90
|
-
capped: score !== rawScore,
|
|
91
|
-
factors,
|
|
92
|
-
dimensions: [...dimensions.entries()].map(([id, contribution]) => ({ id, label: dimensionLabels[id], contribution })),
|
|
93
|
-
evidenceQuality,
|
|
94
|
-
};
|
|
95
|
-
}
|
|
96
|
-
export function computeOpportunityScore(value) {
|
|
97
|
-
return explainOpportunityScore(value)?.score ?? null;
|
|
98
|
-
}
|
|
99
|
-
export function opportunityTone(score) {
|
|
100
|
-
if (score === null)
|
|
101
|
-
return 'neutral';
|
|
102
|
-
if (score >= OPPORTUNITY_REVIEW_THRESHOLD)
|
|
103
|
-
return 'good';
|
|
104
|
-
if (score >= 40)
|
|
105
|
-
return 'neutral';
|
|
106
|
-
return 'warn';
|
|
107
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/opportunity-scoring.mjs";
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { WHOISLEUTH_APPLICATION_VERSION } from "./application-version.mjs";
|
|
2
|
-
import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "
|
|
2
|
+
import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "../packages/analysis/project-metadata.mjs";
|
|
3
3
|
export { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL };
|
|
4
4
|
export const WHOISLEUTH_USER_AGENT_VERSION = WHOISLEUTH_APPLICATION_VERSION;
|
|
5
5
|
export const WHOISLEUTH_USER_AGENT = `WHOISleuth/${WHOISLEUTH_USER_AGENT_VERSION} (+${WHOISLEUTH_REQUEST_POLICY_URL})`;
|
|
@@ -64,7 +64,24 @@ function reduceDynamicValues(value) {
|
|
|
64
64
|
.replace(/\b\d{4}-\d{1,2}-\d{1,2}(?:[t\s]\d{1,2}:\d{2}(?::\d{2}(?:\.\d+)?)?(?:z|[+-]\d{2}:?\d{2})?)?\b/gi, '<time>')
|
|
65
65
|
.replace(/\b\d{10,13}\b/g, '<time>')
|
|
66
66
|
.replace(/\b[a-f0-9]{16,}\b/gi, '<id>')
|
|
67
|
-
.replace(
|
|
67
|
+
.replace(/[a-z0-9_-]+/gi, (token) => {
|
|
68
|
+
// Visit each maximal token once. Repeated lookaheads at word boundaries
|
|
69
|
+
// rescan hyphenated text quadratically when a required digit is absent.
|
|
70
|
+
// Only hyphens are outside the former ASCII word-boundary match;
|
|
71
|
+
// underscores remain part of the identifier, including at either end.
|
|
72
|
+
let start = 0;
|
|
73
|
+
let end = token.length;
|
|
74
|
+
while (start < end && token[start] === '-')
|
|
75
|
+
start += 1;
|
|
76
|
+
while (end > start && token[end - 1] === '-')
|
|
77
|
+
end -= 1;
|
|
78
|
+
if (end - start < 20)
|
|
79
|
+
return token;
|
|
80
|
+
const identifier = token.slice(start, end);
|
|
81
|
+
return /[a-z]/i.test(identifier) && /\d/.test(identifier)
|
|
82
|
+
? `${token.slice(0, start)}<id>${token.slice(end)}`
|
|
83
|
+
: token;
|
|
84
|
+
});
|
|
68
85
|
}
|
|
69
86
|
function normalizeText(value, maxLength = 512) {
|
|
70
87
|
const normalized = reduceDynamicValues(String(value || ''))
|
|
@@ -1,34 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
// prevents Node and browser consumers from drifting into different matches.
|
|
4
|
-
export const PERCEPTUAL_HASH_HEX_RE = /^[0-9a-f]{16}$/u;
|
|
5
|
-
export const MIN_INFORMATIVE_HASH_BITS = 10;
|
|
6
|
-
const POPCOUNT = new Uint8Array(16);
|
|
7
|
-
for (let index = 0; index < POPCOUNT.length; index += 1) {
|
|
8
|
-
POPCOUNT[index] = (index & 1) + ((index >> 1) & 1) + ((index >> 2) & 1) + ((index >> 3) & 1);
|
|
9
|
-
}
|
|
10
|
-
export function isPerceptualHash(value) {
|
|
11
|
-
return typeof value === 'string' && PERCEPTUAL_HASH_HEX_RE.test(value);
|
|
12
|
-
}
|
|
13
|
-
function popcountHex(hex) {
|
|
14
|
-
let bits = 0;
|
|
15
|
-
for (let index = 0; index < hex.length; index += 1) {
|
|
16
|
-
bits += POPCOUNT[parseInt(hex.charAt(index), 16)] ?? 0;
|
|
17
|
-
}
|
|
18
|
-
return bits;
|
|
19
|
-
}
|
|
20
|
-
export function isInformativePerceptualHash(value) {
|
|
21
|
-
if (!isPerceptualHash(value))
|
|
22
|
-
return false;
|
|
23
|
-
const bits = popcountHex(value);
|
|
24
|
-
return bits >= MIN_INFORMATIVE_HASH_BITS && bits <= 64 - MIN_INFORMATIVE_HASH_BITS;
|
|
25
|
-
}
|
|
26
|
-
export function hammingDistanceHex(left, right) {
|
|
27
|
-
if (!isPerceptualHash(left) || !isPerceptualHash(right))
|
|
28
|
-
return null;
|
|
29
|
-
let distance = 0;
|
|
30
|
-
for (let index = 0; index < 16; index += 1) {
|
|
31
|
-
distance += POPCOUNT[parseInt(left.charAt(index), 16) ^ parseInt(right.charAt(index), 16)] ?? 0;
|
|
32
|
-
}
|
|
33
|
-
return distance;
|
|
34
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/perceptual-hash-comparison.mjs";
|
package/lib/perceptual-hash.mjs
CHANGED
|
@@ -21,11 +21,23 @@
|
|
|
21
21
|
// case, while staying cheap (no DCT). Output is 16 lowercase hex chars;
|
|
22
22
|
// similarity is the Hamming distance between two such strings.
|
|
23
23
|
import * as zlib from 'node:zlib';
|
|
24
|
-
import { hammingDistanceHex, isInformativePerceptualHash, } from "
|
|
24
|
+
import { hammingDistanceHex, isInformativePerceptualHash, } from "../packages/analysis/perceptual-hash-comparison.mjs";
|
|
25
25
|
// Bound the decode work on attacker-controlled image bytes. Real favicons top
|
|
26
26
|
// out around 256x256; larger inputs above this ceiling remain exact-only
|
|
27
27
|
// evidence rather than causing unbounded pixel allocation.
|
|
28
28
|
const MAX_DIM = 1024;
|
|
29
|
+
// Permit two maximum-sized decode attempts (more than a normal resolution
|
|
30
|
+
// pyramid), while bounding aggregate inflation/filtering across a container.
|
|
31
|
+
// Encoded payloads may consume at most the container's byte length in total;
|
|
32
|
+
// ordinary non-overlapping entries never exceed that bound.
|
|
33
|
+
const MAX_DECODE_PIXELS = 2 * MAX_DIM * MAX_DIM;
|
|
34
|
+
function admitPixels(work, width, height) {
|
|
35
|
+
const count = width * height;
|
|
36
|
+
if (count > work.pixels)
|
|
37
|
+
return false;
|
|
38
|
+
work.pixels -= count;
|
|
39
|
+
return true;
|
|
40
|
+
}
|
|
29
41
|
const PNG_SIGNATURE = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
30
42
|
function byteAt(bytes, index) {
|
|
31
43
|
return bytes[index] ?? 0;
|
|
@@ -53,7 +65,7 @@ function paethPredictor(a, b, c) {
|
|
|
53
65
|
// { width, height, pixels } with pixels as RGBA bytes, or null for anything
|
|
54
66
|
// outside that subset (or malformed input). CRCs are intentionally not
|
|
55
67
|
// verified - we're computing a fuzzy hash, not validating integrity.
|
|
56
|
-
function decodePng(buf) {
|
|
68
|
+
function decodePng(buf, work) {
|
|
57
69
|
const CHANNELS = { 0: 1, 2: 3, 3: 1, 4: 2, 6: 4 };
|
|
58
70
|
let width = 0;
|
|
59
71
|
let height = 0;
|
|
@@ -90,7 +102,7 @@ function decodePng(buf) {
|
|
|
90
102
|
transparency = data;
|
|
91
103
|
}
|
|
92
104
|
else if (type === 'IDAT') {
|
|
93
|
-
idatParts.push(
|
|
105
|
+
idatParts.push(data);
|
|
94
106
|
}
|
|
95
107
|
else if (type === 'IEND') {
|
|
96
108
|
break;
|
|
@@ -113,6 +125,8 @@ function decodePng(buf) {
|
|
|
113
125
|
return null;
|
|
114
126
|
if (idatParts.length === 0)
|
|
115
127
|
return null;
|
|
128
|
+
if (!admitPixels(work, width, height))
|
|
129
|
+
return null;
|
|
116
130
|
// A valid non-interlaced 8-bit PNG decompresses to exactly one filter byte
|
|
117
131
|
// plus one scanline per row. Cap the inflate at that size so a malicious
|
|
118
132
|
// IDAT can't expand a sub-200KB favicon into hundreds of MB (a zip bomb)
|
|
@@ -218,7 +232,7 @@ function decodePng(buf) {
|
|
|
218
232
|
// palettized and 24/32-bit truecolour. The stored height is doubled (it
|
|
219
233
|
// includes the 1-bpp AND transparency mask). Non-alpha and legacy all-zero
|
|
220
234
|
// alpha images use that mask; meaningful 32-bit alpha remains authoritative.
|
|
221
|
-
function decodeDib(buf) {
|
|
235
|
+
function decodeDib(buf, work) {
|
|
222
236
|
if (buf.length < 40)
|
|
223
237
|
return null;
|
|
224
238
|
const headerSize = buf.readUInt32LE(0);
|
|
@@ -258,6 +272,8 @@ function decodeDib(buf) {
|
|
|
258
272
|
const maskPresent = maskStart + maskRowSize * height <= buf.length;
|
|
259
273
|
if (bitCount !== 32 && !maskPresent)
|
|
260
274
|
return null;
|
|
275
|
+
if (!admitPixels(work, width, height))
|
|
276
|
+
return null;
|
|
261
277
|
const paletteColor = (index) => {
|
|
262
278
|
if (!palette || index >= palette.count)
|
|
263
279
|
return [0, 0, 0];
|
|
@@ -323,9 +339,10 @@ function decodeDib(buf) {
|
|
|
323
339
|
// one is in a variant we don't decode (e.g. a 4-bit-palette PNG sub-entry
|
|
324
340
|
// alongside a plain BMP one). A multi-size favicon.ico only needs one usable
|
|
325
341
|
// entry, and any size hashes to nearly the same dHash since it downsamples.
|
|
326
|
-
function decodeIco(buf) {
|
|
342
|
+
function decodeIco(buf, work) {
|
|
327
343
|
const count = buf.readUInt16LE(4);
|
|
328
|
-
|
|
344
|
+
const directoryEnd = 6 + count * 16;
|
|
345
|
+
if (count < 1 || directoryEnd > buf.length)
|
|
329
346
|
return null;
|
|
330
347
|
const entries = [];
|
|
331
348
|
for (let i = 0; i < count; i += 1) {
|
|
@@ -334,14 +351,22 @@ function decodeIco(buf) {
|
|
|
334
351
|
const h = buf[entry + 1] || 256;
|
|
335
352
|
const size = buf.readUInt32LE(entry + 8);
|
|
336
353
|
const dataOffset = buf.readUInt32LE(entry + 12);
|
|
337
|
-
if (dataOffset + size > buf.length || size < 1)
|
|
354
|
+
if (dataOffset < directoryEnd || dataOffset + size > buf.length || size < 1)
|
|
338
355
|
continue;
|
|
339
356
|
entries.push({ area: w * h, dataOffset, size });
|
|
340
357
|
}
|
|
341
358
|
entries.sort((a, b) => b.area - a.area);
|
|
359
|
+
const visited = new Set();
|
|
342
360
|
for (const { dataOffset, size } of entries) {
|
|
361
|
+
const identity = `${dataOffset}:${size}`;
|
|
362
|
+
if (visited.has(identity))
|
|
363
|
+
continue;
|
|
364
|
+
visited.add(identity);
|
|
365
|
+
if (size > work.encodedBytes)
|
|
366
|
+
return null;
|
|
367
|
+
work.encodedBytes -= size;
|
|
343
368
|
const payload = buf.subarray(dataOffset, dataOffset + size);
|
|
344
|
-
const image = isPng(payload) ? decodePng(payload) : decodeDib(payload);
|
|
369
|
+
const image = isPng(payload) ? decodePng(payload, work) : decodeDib(payload, work);
|
|
345
370
|
if (image)
|
|
346
371
|
return image;
|
|
347
372
|
}
|
|
@@ -350,10 +375,11 @@ function decodeIco(buf) {
|
|
|
350
375
|
function decodeImage(buf) {
|
|
351
376
|
if (!Buffer.isBuffer(buf))
|
|
352
377
|
return null;
|
|
378
|
+
const work = { encodedBytes: buf.length, pixels: MAX_DECODE_PIXELS };
|
|
353
379
|
if (isPng(buf))
|
|
354
|
-
return decodePng(buf);
|
|
380
|
+
return decodePng(buf, work);
|
|
355
381
|
if (isIco(buf))
|
|
356
|
-
return decodeIco(buf);
|
|
382
|
+
return decodeIco(buf, work);
|
|
357
383
|
return null;
|
|
358
384
|
}
|
|
359
385
|
// Downscales to cols x rows of grayscale luma. Transparency is composited
|
|
@@ -1,29 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
const URI_LABEL_UNSAFE_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
|
|
4
|
-
const URI_LABEL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/|[^\s])/u;
|
|
5
|
-
export function isUriShapedLabel(value) {
|
|
6
|
-
if (typeof value !== 'string')
|
|
7
|
-
return false;
|
|
8
|
-
const candidate = value.replace(URI_LABEL_UNSAFE_GLOBAL_RE, '').trim();
|
|
9
|
-
return URI_LABEL_SCHEME_RE.test(candidate);
|
|
10
|
-
}
|
|
11
|
-
export function buildPortableGeneratorMetadata(applicationVersion) {
|
|
12
|
-
let version = null;
|
|
13
|
-
if (applicationVersion !== null && applicationVersion !== undefined && applicationVersion !== '') {
|
|
14
|
-
try {
|
|
15
|
-
version = normalizeBoundedSemanticVersion(applicationVersion, 'Portable report application');
|
|
16
|
-
}
|
|
17
|
-
catch {
|
|
18
|
-
version = null;
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
return Object.freeze({
|
|
22
|
-
name: 'WHOISleuth',
|
|
23
|
-
version,
|
|
24
|
-
projectUrl: WHOISLEUTH_SOURCE_REPOSITORY_URL,
|
|
25
|
-
});
|
|
26
|
-
}
|
|
27
|
-
export function portableGeneratorAttribution(generator) {
|
|
28
|
-
return `Generated with ${generator.name}${generator.version ? ` ${generator.version}` : ''} · Source: ${generator.projectUrl}`;
|
|
29
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/portable-generator.mjs";
|
package/lib/project-metadata.mjs
CHANGED
|
@@ -1,8 +1,2 @@
|
|
|
1
|
-
//
|
|
2
|
-
|
|
3
|
-
export const WHOISLEUTH_SITE_ORIGIN = 'https://www.whoisleuth.com';
|
|
4
|
-
export const WHOISLEUTH_PROJECT_URL = `${WHOISLEUTH_SITE_ORIGIN}/`;
|
|
5
|
-
export const WHOISLEUTH_REQUEST_POLICY_URL = `${WHOISLEUTH_SITE_ORIGIN}/request-policy`;
|
|
6
|
-
export const WHOISLEUTH_SOURCE_REPOSITORY_URL = 'https://github.com/slicedearth/whoisleuth';
|
|
7
|
-
export const WHOISLEUTH_SOURCE_REPOSITORY_GIT_URL = `git+${WHOISLEUTH_SOURCE_REPOSITORY_URL}.git`;
|
|
8
|
-
export const WHOISLEUTH_SOURCE_ISSUES_URL = `${WHOISLEUTH_SOURCE_REPOSITORY_URL}/issues`;
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/project-metadata.mjs";
|
package/lib/rdap-client.mjs
CHANGED
|
@@ -61,21 +61,25 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
|
|
|
61
61
|
}));
|
|
62
62
|
continue;
|
|
63
63
|
}
|
|
64
|
-
let data;
|
|
64
|
+
let data = null;
|
|
65
65
|
try {
|
|
66
66
|
data = JSON.parse(upstream.text);
|
|
67
67
|
}
|
|
68
68
|
catch {
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
// RFC 7480 section 5.3 makes the explanatory body optional for a
|
|
70
|
+
// negative answer. URL binding and transport/body bounds still apply.
|
|
71
|
+
if (upstream.status !== 404) {
|
|
72
|
+
attempts.push(rdapAttempt(selectedEndpoint, 'invalid_json', {
|
|
73
|
+
status: upstream.status,
|
|
74
|
+
detail: 'The endpoint returned invalid JSON.',
|
|
75
|
+
}));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
74
78
|
}
|
|
75
79
|
if (upstream.status === 404) {
|
|
76
80
|
attempts.push(rdapAttempt(selectedEndpoint, 'not_found', {
|
|
77
81
|
status: upstream.status,
|
|
78
|
-
detail:
|
|
82
|
+
detail: `The authoritative endpoint reported no matching object.${data === null ? ' No usable JSON explanation was supplied.' : ''}`,
|
|
79
83
|
selected: true,
|
|
80
84
|
}));
|
|
81
85
|
return {
|
|
@@ -1,56 +1,2 @@
|
|
|
1
|
-
import
|
|
2
|
-
|
|
3
|
-
import { MAX_DOMAIN_NAME_LENGTH, MAX_DOMAIN_LABEL_LENGTH } from "../packages/contracts/domain-name.mjs";
|
|
4
|
-
// Reserve the shortest generated name and its separating dot.
|
|
5
|
-
export const MAX_DISCOVERY_SUFFIX_LENGTH = MAX_DOMAIN_NAME_LENGTH - 2;
|
|
6
|
-
/** ICANN suffix rules for a plain ASCII hostname; private suffixes do not change identity. */
|
|
7
|
-
export function publicSuffixForAsciiHostname(value) {
|
|
8
|
-
if (!isValidAsciiDomainName(value, { requireDot: false, requireLowercase: true }))
|
|
9
|
-
return null;
|
|
10
|
-
return getPublicSuffix(value);
|
|
11
|
-
}
|
|
12
|
-
/** Preserve reserved single labels; compound suffixes must match the local ICANN suffix rules. */
|
|
13
|
-
export function normalizeDiscoverySuffix(raw) {
|
|
14
|
-
if (typeof raw !== 'string' || raw.length > MAX_DISCOVERY_SUFFIX_LENGTH + 1 || /[\u0000-\u001f\u007f]/u.test(raw))
|
|
15
|
-
return null;
|
|
16
|
-
const value = raw.trim().toLowerCase().replace(/^\./u, '');
|
|
17
|
-
if (!value || value.length > MAX_DISCOVERY_SUFFIX_LENGTH)
|
|
18
|
-
return null;
|
|
19
|
-
if (value.length <= MAX_DOMAIN_LABEL_LENGTH && /^[a-z]{2,}$/u.test(value))
|
|
20
|
-
return value;
|
|
21
|
-
return value.includes('.') && publicSuffixForAsciiHostname(value) === value ? value : null;
|
|
22
|
-
}
|
|
23
|
-
// Returns one canonical registrable-domain identity for an already public DNS
|
|
24
|
-
// hostname. Callers retain the original hostname separately when subdomain
|
|
25
|
-
// context matters; authority, evidence binding and scoring use this value.
|
|
26
|
-
export function canonicalRegistrableDomain(value) {
|
|
27
|
-
if (typeof value !== 'string')
|
|
28
|
-
return null;
|
|
29
|
-
const trimmed = value.trim();
|
|
30
|
-
if (!trimmed || /[\u0000-\u001f\u007f]/u.test(trimmed))
|
|
31
|
-
return null;
|
|
32
|
-
const withoutScheme = trimmed.replace(/^[a-z]+:\/\//iu, '').split(/[/?#]/u)[0] ?? '';
|
|
33
|
-
if (withoutScheme.endsWith('..'))
|
|
34
|
-
return null;
|
|
35
|
-
const candidate = withoutScheme.replace(/\.$/u, '');
|
|
36
|
-
if (!candidate)
|
|
37
|
-
return null;
|
|
38
|
-
let hostname;
|
|
39
|
-
try {
|
|
40
|
-
hostname = new URL(`https://${candidate}/`).hostname.toLowerCase().replace(/\.$/u, '');
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return null;
|
|
44
|
-
}
|
|
45
|
-
if (!isValidAsciiDomainName(hostname, { requireDot: true, requireLowercase: true }))
|
|
46
|
-
return null;
|
|
47
|
-
// Keep this boundary identical to classifyQuery() and the collection
|
|
48
|
-
// runtime: registrable identity follows the ICANN Public Suffix List, while
|
|
49
|
-
// privately operated suffixes remain ordinary host context.
|
|
50
|
-
const domain = getDomain(hostname);
|
|
51
|
-
return domain
|
|
52
|
-
&& isValidAsciiDomainName(domain, { requireDot: true, requireLowercase: true })
|
|
53
|
-
&& (hostname === domain || hostname.endsWith(`.${domain}`))
|
|
54
|
-
? domain
|
|
55
|
-
: null;
|
|
56
|
-
}
|
|
1
|
+
// Retained public import; the runtime-neutral implementation lives in the domain layer.
|
|
2
|
+
export * from "../packages/analysis/registrable-domain.mjs";
|
|
@@ -64,7 +64,7 @@ const SOURCE_HEALTH_STATES = new Set(REGISTRAR_STANDING_SOURCE_HEALTH_STATES);
|
|
|
64
64
|
const ISO_DATE_RE = /^\d{4}-\d{2}-\d{2}$/u;
|
|
65
65
|
const NOTICE_ID_RE = /^notice-[1-9]\d{0,7}$/u;
|
|
66
66
|
const IANA_ID_RE = /^[1-9]\d{0,7}$/u;
|
|
67
|
-
function normalizedRegistrarIanaId(value) {
|
|
67
|
+
export function normalizedRegistrarIanaId(value) {
|
|
68
68
|
if (typeof value === 'number' && Number.isSafeInteger(value))
|
|
69
69
|
value = String(value);
|
|
70
70
|
if (typeof value !== 'string')
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
import { createHash } from 'node:crypto';
|
|
6
6
|
import { REGISTRAR_STANDING_CATALOGUE } from "./generated/registrar-standing-catalogue.mjs";
|
|
7
7
|
import { ICANN_COMPLIANCE_SOURCE_URL, IANA_REGISTRAR_SOURCE_URL, REGISTRAR_STANDING_CATALOGUE_SCHEMA, REGISTRAR_STANDING_CATALOGUE_VERSION, REGISTRAR_STANDING_MAX_AGE_DAYS, REGISTRAR_STANDING_MAX_FUTURE_SKEW_MS, registrarStandingOfficialSourceUrl, } from "./registrar-standing-catalogue-contract.mjs";
|
|
8
|
-
import { deriveRegistrarStandingAssessment, MAX_REGISTRAR_COMPLIANCE_ACTIONS, REGISTRAR_STANDING_SCHEMA, REGISTRAR_STANDING_VERSION, } from "./registrar-standing-contract.mjs";
|
|
8
|
+
import { deriveRegistrarStandingAssessment, normalizedRegistrarIanaId, MAX_REGISTRAR_COMPLIANCE_ACTIONS, REGISTRAR_STANDING_SCHEMA, REGISTRAR_STANDING_VERSION, } from "./registrar-standing-contract.mjs";
|
|
9
9
|
const STATUS_BY_CODE = Object.freeze({
|
|
10
10
|
A: 'accredited',
|
|
11
11
|
R: 'reserved',
|
|
@@ -40,6 +40,10 @@ function ageDays(value, now) {
|
|
|
40
40
|
function sourceHealth(value, now) {
|
|
41
41
|
return ageDays(value, now) <= REGISTRAR_STANDING_MAX_AGE_DAYS ? 'current' : 'stale';
|
|
42
42
|
}
|
|
43
|
+
function complianceHealth(catalogue, now) {
|
|
44
|
+
return catalogue.icann.catalogueYear === now.getUTCFullYear()
|
|
45
|
+
? sourceHealth(catalogue.icann.reviewedAt, now) : 'stale';
|
|
46
|
+
}
|
|
43
47
|
function validatedCatalogue(value, now) {
|
|
44
48
|
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
45
49
|
return null;
|
|
@@ -157,14 +161,6 @@ function accreditationMap(catalogue) {
|
|
|
157
161
|
}
|
|
158
162
|
return result;
|
|
159
163
|
}
|
|
160
|
-
function normalizedIanaId(value) {
|
|
161
|
-
if (typeof value === 'number' && Number.isSafeInteger(value))
|
|
162
|
-
value = String(value);
|
|
163
|
-
if (typeof value !== 'string')
|
|
164
|
-
return null;
|
|
165
|
-
const trimmed = value.trim();
|
|
166
|
-
return /^\d{1,8}$/u.test(trimmed) && Number(trimmed) > 0 ? String(Number(trimmed)) : null;
|
|
167
|
-
}
|
|
168
164
|
function projectedActions(catalogue, ianaId) {
|
|
169
165
|
const matching = catalogue.icann.notices.filter((notice) => String(notice.ianaId) === ianaId);
|
|
170
166
|
const actions = matching.slice(0, MAX_REGISTRAR_COMPLIANCE_ACTIONS).map((notice) => {
|
|
@@ -183,9 +179,9 @@ export function buildRegistrarStanding(options) {
|
|
|
183
179
|
if (!(now instanceof Date) || !Number.isFinite(now.getTime()))
|
|
184
180
|
throw new TypeError('Registrar standing time must be valid.');
|
|
185
181
|
const catalogue = validatedCatalogue(options.catalogue ?? REGISTRAR_STANDING_CATALOGUE, now);
|
|
186
|
-
const ianaId =
|
|
182
|
+
const ianaId = normalizedRegistrarIanaId(options.registrarIanaId);
|
|
187
183
|
const ianaHealth = catalogue ? sourceHealth(catalogue.iana.observedAt, now) : 'unavailable';
|
|
188
|
-
const icannHealth = catalogue ?
|
|
184
|
+
const icannHealth = catalogue ? complianceHealth(catalogue, now) : 'unavailable';
|
|
189
185
|
const accreditation = catalogue && ianaId
|
|
190
186
|
? accreditationMap(catalogue).get(ianaId) ?? 'unknown'
|
|
191
187
|
: 'unknown';
|
|
@@ -205,7 +201,7 @@ export function buildRegistrarStanding(options) {
|
|
|
205
201
|
? `The reviewed ${catalogue.icann.catalogueYear} ICANN index is not a complete enforcement history and excludes third-party allegations.`
|
|
206
202
|
: 'The checked-in official-source catalogue could not be validated, so registrar standing is unavailable.',
|
|
207
203
|
...(!ianaId ? ['No single numeric registrar IANA ID was available across the registration evidence, so no registrar match was attempted.'] : []),
|
|
208
|
-
...(combinedHealth === 'stale' ? ['The catalogue is
|
|
204
|
+
...(combinedHealth === 'stale' ? ['The catalogue is outside its current-year coverage or review-age threshold; current absence and accreditation conclusions require a refresh.'] : []),
|
|
209
205
|
].slice(0, 4);
|
|
210
206
|
const nextActions = hasAction ? [
|
|
211
207
|
'Open the official notice and verify its dates, scope, and current outcome before acting.',
|
|
@@ -259,7 +255,7 @@ export function registrarStandingCatalogueHealth(now = new Date(), value = REGIS
|
|
|
259
255
|
});
|
|
260
256
|
const age = Math.max(ageDays(catalogue.iana.observedAt, now), ageDays(catalogue.icann.reviewedAt, now));
|
|
261
257
|
return Object.freeze({
|
|
262
|
-
state: age <= REGISTRAR_STANDING_MAX_AGE_DAYS ? 'current' : 'stale',
|
|
258
|
+
state: age <= REGISTRAR_STANDING_MAX_AGE_DAYS && complianceHealth(catalogue, now) === 'current' ? 'current' : 'stale',
|
|
263
259
|
sourceObservedAt: [catalogue.iana.observedAt, catalogue.icann.reviewedAt]
|
|
264
260
|
.sort((left, right) => Date.parse(left) - Date.parse(right))[0] ?? null,
|
|
265
261
|
ageDays: age,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Registry capability evaluation over the immutable reviewed catalogue.
|
|
2
|
-
import { CAPABILITY_BY_SUFFIX, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, MAX_CAPABILITY_INPUT_LENGTH, REGISTRY_CAPABILITIES_VERSION, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT,
|
|
2
|
+
import { CAPABILITY_BY_SUFFIX, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, MAX_CAPABILITY_INPUT_LENGTH, REGISTRY_CAPABILITIES_VERSION, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT, REVIEWED_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, } from "./registry-capability-catalogue.mjs";
|
|
3
3
|
const IANA_TLD_RECORD_PREFIX = 'https://www.iana.org/domains/root/db/';
|
|
4
4
|
function documentationUrlsForSuffix(capability, suffixes) {
|
|
5
5
|
if (suffixes.length !== 1 || capability.suffixes.length <= 1) {
|
|
@@ -151,4 +151,4 @@ function registryCompatibilityMatrix() {
|
|
|
151
151
|
})))
|
|
152
152
|
.sort((left, right) => (left.suffixes[0] ?? '').localeCompare(right.suffixes[0] ?? ''));
|
|
153
153
|
}
|
|
154
|
-
export { REGISTRY_CAPABILITIES_VERSION,
|
|
154
|
+
export { REGISTRY_CAPABILITIES_VERSION, REVIEWED_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, registryCapabilityFor, registryCompatibilityMatrix, listRegistryCapabilities, registryAccessDiagnosticFor, registryServiceAdmissionFor, registryStandardsCoverageSnapshot, };
|
|
@@ -2,17 +2,17 @@
|
|
|
2
2
|
// remain authoritative. Explicit query profiles may alter only the first
|
|
3
3
|
// referred registry query after a fixture-backed adapter is integrated.
|
|
4
4
|
export const REGISTRY_STANDARDS_COVERAGE_SCHEMA = 'whoisleuth.registry-standards-coverage';
|
|
5
|
-
const REGISTRY_CAPABILITIES_VERSION =
|
|
5
|
+
const REGISTRY_CAPABILITIES_VERSION = 31;
|
|
6
6
|
const MAX_CAPABILITY_INPUT_LENGTH = 253;
|
|
7
7
|
const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
|
|
8
8
|
schema: REGISTRY_STANDARDS_COVERAGE_SCHEMA,
|
|
9
9
|
version: 1,
|
|
10
|
-
verifiedAt: '2026-
|
|
10
|
+
verifiedAt: '2026-10-03',
|
|
11
11
|
sources: Object.freeze({
|
|
12
|
-
rootZoneVersion: '
|
|
13
|
-
rootZoneLastUpdatedAt: '2026-
|
|
14
|
-
rootZoneTldSetSha256: '
|
|
15
|
-
rdapBootstrapPublication: '2026-
|
|
12
|
+
rootZoneVersion: '2026100200',
|
|
13
|
+
rootZoneLastUpdatedAt: '2026-10-02T07:07:02.000Z',
|
|
14
|
+
rootZoneTldSetSha256: '71093802743b87ee52fac5dd4f662cafd0133dae5089ac6c2e34ee73cebfc836',
|
|
15
|
+
rdapBootstrapPublication: '2026-09-30T23:00:03.000Z',
|
|
16
16
|
rdapBootstrapVersion: '1.0',
|
|
17
17
|
urls: Object.freeze([
|
|
18
18
|
'https://data.iana.org/TLD/tlds-alpha-by-domain.txt',
|
|
@@ -21,15 +21,15 @@ const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
|
|
|
21
21
|
]),
|
|
22
22
|
}),
|
|
23
23
|
counts: Object.freeze({
|
|
24
|
-
activeTlds:
|
|
24
|
+
activeTlds: 1437,
|
|
25
25
|
countryCode: 309,
|
|
26
|
-
nonCountryCode:
|
|
27
|
-
generic:
|
|
26
|
+
nonCountryCode: 1128,
|
|
27
|
+
generic: 1110,
|
|
28
28
|
genericRestricted: 3,
|
|
29
29
|
sponsored: 14,
|
|
30
30
|
infrastructure: 1,
|
|
31
|
-
rdapBootstrapServiceGroups:
|
|
32
|
-
genericAndRestrictedRdapCovered:
|
|
31
|
+
rdapBootstrapServiceGroups: 592,
|
|
32
|
+
genericAndRestrictedRdapCovered: 1113,
|
|
33
33
|
sponsoredRdapCovered: 12,
|
|
34
34
|
infrastructureRdapCovered: 0,
|
|
35
35
|
}),
|
|
@@ -82,7 +82,7 @@ const VERSION_16_NO_IANA_MACHINE_SERVICE_SUFFIXES = Object.freeze([
|
|
|
82
82
|
]);
|
|
83
83
|
const VERSION_19_NO_IANA_MACHINE_SERVICE_SUFFIXES = Object.freeze([
|
|
84
84
|
'xn--mgbai9azgqp6j', 'xn--mgbayh7gpa', 'xn--mgbc0a9azcg',
|
|
85
|
-
'xn--
|
|
85
|
+
'xn--mgbpl2fh', 'xn--wgbh1c',
|
|
86
86
|
]);
|
|
87
87
|
const VERSION_19_RDAP_ONLY_SUFFIXES = Object.freeze(['na', 'pn']);
|
|
88
88
|
const VERSION_20_FIXTURE_SUFFIXES = Object.freeze([
|
|
@@ -117,13 +117,13 @@ const VERSION_24_PROMOTED_SUFFIXES = Object.freeze([
|
|
|
117
117
|
'xn--90ae', 'xn--l1acc', 'xn--wgbl6a',
|
|
118
118
|
]);
|
|
119
119
|
const CURRENT_UNVERIFIED_REFERRAL_SUFFIXES = Object.freeze(VERSION_23_UNVERIFIED_REFERRAL_SUFFIXES.filter((suffix) => !VERSION_24_PROMOTED_SUFFIXES.includes(suffix)));
|
|
120
|
-
const
|
|
120
|
+
const REVIEWED_NO_RDAP_SUFFIXES = Object.freeze([
|
|
121
121
|
'ac', 'ae', 'af', 'am', 'at', 'be', 'bg', 'by', 'cl', 'cn',
|
|
122
122
|
'co', 'de', 'dk', 'ee', 'eu', 'gf', 'gi', 'gt', 'hk', 'hr',
|
|
123
|
-
'hu', 'ie', 'il', 'io', 'ir', 'it', 'jp', '
|
|
123
|
+
'hu', 'ie', 'il', 'io', 'ir', 'it', 'jp', 'kz', 'la',
|
|
124
124
|
'lt', 'lu', 'lv', 'md', 'me', 'mk', 'mo', 'mq', 'mx', 'my',
|
|
125
125
|
'nz', 'pk', 'pt', 'ro', 'rs', 'ru', 'sa', 'se', 'sk', 'su',
|
|
126
|
-
'tn', 'tr', 'us', 'vc', 'xn--2scrj9c',
|
|
126
|
+
'tn', 'tr', 'us', 'vc', 'xn--2scrj9c',
|
|
127
127
|
'xn--3hcrj9c', 'xn--45br5cyl', 'xn--45brj9c', 'xn--80ao21a',
|
|
128
128
|
'xn--90a3ac', 'xn--90ais', 'xn--d1alf', 'xn--e1a4c',
|
|
129
129
|
'xn--fiqs8s', 'xn--fiqz9s', 'xn--fpcrj9c3d', 'xn--gecrj9c',
|
|
@@ -132,7 +132,7 @@ const VERSION_26_NO_RDAP_SUFFIXES = Object.freeze([
|
|
|
132
132
|
'xn--qxa6a', 'xn--rvc1e0am3e', 'xn--s9brj9c',
|
|
133
133
|
'xn--xkc2dl3a5ee0h', 'xn--y9a3aq',
|
|
134
134
|
]);
|
|
135
|
-
const
|
|
135
|
+
const REVIEWED_NO_RDAP_SUFFIX_SET = new Set(REVIEWED_NO_RDAP_SUFFIXES);
|
|
136
136
|
const VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES = Object.freeze([
|
|
137
137
|
'app', 'boo', 'channel', 'dad', 'day', 'dev', 'esq', 'foo', 'how', 'info',
|
|
138
138
|
'ing', 'meme', 'mov', 'new', 'nexus', 'page', 'phd', 'prof', 'rsvp', 'soy',
|
|
@@ -312,6 +312,14 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
312
312
|
documentationUrls: [`https://www.iana.org/domains/root/db/${suffix}.html`],
|
|
313
313
|
limitation: RDAP_ONLY_MACHINE_SERVICE_LIMITATION,
|
|
314
314
|
})),
|
|
315
|
+
{
|
|
316
|
+
id: 'no-iana-machine-service-xn--mgbcpq6gpa1a', suffixes: ['xn--mgbcpq6gpa1a'], registryClass: 'country-code',
|
|
317
|
+
whoisParserProfile: 'generic-colon', fixtureScenarios: [],
|
|
318
|
+
coverageState: 'access_documented', whoisAccessProfile: 'no-iana-service',
|
|
319
|
+
rdapAccessProfile: 'iana-bootstrap', verificationFiles: [],
|
|
320
|
+
documentationUrls: ['https://www.iana.org/domains/root/db/xn--mgbcpq6gpa1a.html'],
|
|
321
|
+
limitation: RDAP_ONLY_MACHINE_SERVICE_LIMITATION,
|
|
322
|
+
},
|
|
315
323
|
...VERSION_20_FIXTURE_SUFFIXES.map((suffix) => ({
|
|
316
324
|
id: `iana-cc-colon-${suffix}`,
|
|
317
325
|
suffixes: [suffix],
|
|
@@ -1044,7 +1052,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
1044
1052
|
{
|
|
1045
1053
|
id: 'nic-bh-icann-colon', suffixes: ['bh'], registryClass: 'country-code',
|
|
1046
1054
|
whoisParserProfile: 'icann-style-colon', fixtureScenarios: ['registered', 'not_found'],
|
|
1047
|
-
rdapAccessProfile: '
|
|
1055
|
+
rdapAccessProfile: 'iana-bootstrap',
|
|
1048
1056
|
documentationUrls: ['https://www.iana.org/domains/root/db/bh.html'],
|
|
1049
1057
|
},
|
|
1050
1058
|
{
|
|
@@ -1181,7 +1189,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
1181
1189
|
];
|
|
1182
1190
|
function resolvedSeedRdapAccessProfile(entry) {
|
|
1183
1191
|
const declared = entry.rdapAccessProfile || 'iana-bootstrap';
|
|
1184
|
-
const resolved = new Set(entry.suffixes.map((suffix) => (
|
|
1192
|
+
const resolved = new Set(entry.suffixes.map((suffix) => (REVIEWED_NO_RDAP_SUFFIX_SET.has(suffix) ? 'no-iana-service' : declared)));
|
|
1185
1193
|
if (resolved.size !== 1) {
|
|
1186
1194
|
throw new Error(`Registry capability ${entry.id} mixes RDAP access profiles and must be split by suffix.`);
|
|
1187
1195
|
}
|
|
@@ -1215,4 +1223,4 @@ for (const capability of EXPLICIT_CAPABILITIES) {
|
|
|
1215
1223
|
CAPABILITY_BY_SUFFIX.set(suffix, capability);
|
|
1216
1224
|
}
|
|
1217
1225
|
}
|
|
1218
|
-
export { REGISTRY_CAPABILITIES_VERSION, MAX_CAPABILITY_INPUT_LENGTH,
|
|
1226
|
+
export { REGISTRY_CAPABILITIES_VERSION, MAX_CAPABILITY_INPUT_LENGTH, REVIEWED_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, REGISTRY_STANDARDS_COVERAGE_SNAPSHOT, DEFAULT_CAPABILITY, EXPLICIT_CAPABILITIES, CAPABILITY_BY_SUFFIX, };
|