@slicedearth/whoisleuth-cli 1.34.0 → 1.39.1
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/README.md +19 -2
- package/bin/whoisleuth.mjs +41 -24
- package/cli/arguments.mjs +326 -3
- package/cli/artifact-verify.mjs +60 -14
- package/cli/bulk.mjs +1 -2
- package/cli/compare.mjs +1 -1
- package/cli/completion.mjs +36 -2
- package/cli/doctor.mjs +11 -5
- package/cli/evidence-command-arguments.mjs +1 -1
- package/cli/evidence-command-runner.mjs +2 -2
- package/cli/evidence-signing.mjs +4 -4
- package/cli/export-evidence.mjs +9 -2
- package/cli/formatters/html.mjs +1 -1
- package/cli/formatters/json.mjs +13 -1
- package/cli/formatters/terminal.mjs +14 -4
- package/cli/investigation-plan.mjs +112 -0
- package/cli/local-mmdb-review.mjs +76 -0
- package/cli/lookalike-calibration.mjs +121 -0
- package/cli/lookup-command-runner.mjs +4 -1
- package/cli/lookup-plan.mjs +3 -3
- package/cli/lookup-reconcile.mjs +141 -0
- package/cli/lookup-timeline.mjs +1 -1
- package/cli/mail-review.mjs +48 -3
- package/cli/offline-evidence-review.mjs +159 -0
- package/cli/registry-doctor.mjs +125 -0
- package/cli/registry-support.mjs +1 -4
- package/cli/risk-calibration.mjs +117 -9
- package/cli/runner.mjs +342 -15
- package/cli/saved-lookup.mjs +2 -2
- package/cli/sharing-review.mjs +202 -0
- package/cli/source-reliability.mjs +1 -1
- package/docs/cli-reference.md +1110 -0
- package/docs/cli.md +224 -847
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +1 -1
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +6 -6
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +12 -12
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +1 -1
- package/frontend/src/lib/analysis/bulk-retry-plan.js +3 -10
- package/frontend/src/lib/analysis/bulk-session-model.js +7 -2
- package/frontend/src/lib/analysis/bulk-source-coverage.js +76 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +26 -4
- package/frontend/src/lib/analysis/case-migration-model.js +6 -1
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-core.js +5 -1
- package/frontend/src/lib/analysis/case-record-operations.js +6 -1
- package/frontend/src/lib/analysis/case-response-packet.js +7 -7
- package/frontend/src/lib/analysis/evidence-coverage-ledger.js +328 -0
- package/frontend/src/lib/analysis/http-summary.js +2 -1
- package/frontend/src/lib/analysis/investigation-capsule.js +105 -0
- package/frontend/src/lib/analysis/lookup-asset-graph.js +806 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +480 -0
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +153 -0
- package/frontend/src/lib/analysis/lookup-presentation.js +74 -0
- package/frontend/src/lib/analysis/lookup-response.js +2 -0
- package/frontend/src/lib/analysis/lookup-source-refresh.js +304 -0
- package/frontend/src/lib/analysis/lookup-summary-model.js +398 -0
- package/frontend/src/lib/analysis/page-similarity.js +1 -1
- package/frontend/src/lib/analysis/relationship-evidence.js +3 -3
- package/frontend/src/lib/analysis/relationship-observation-model.js +4 -4
- package/frontend/src/lib/analysis/scoring.js +8 -88
- package/frontend/src/lib/analysis/shortlist-model.js +8 -4
- package/frontend/src/lib/analysis/utils.js +7 -40
- package/frontend/src/lib/analysis/visualization-bounds.js +26 -0
- package/frontend/src/lib/analysis/visualization-force-graph.js +352 -0
- package/frontend/src/lib/analysis/visualization-matrix.js +88 -0
- package/frontend/src/lib/analysis/visualization-models.js +370 -0
- package/frontend/src/lib/analysis/visualization-time-series.js +144 -0
- package/frontend/src/lib/analysis/website-snapshot-model.js +57 -3
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -1
- package/frontend/src/lib/analysis/workspace-archive.js +2 -2
- package/lib/analyst-taxonomy.mjs +40 -0
- package/lib/availability.mjs +30 -10
- package/lib/browser-library-profile.mjs +15 -2
- package/lib/bulk-limits.mjs +13 -0
- package/lib/certificate-extension-profile.mjs +71 -0
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/ct-response-bounds.mjs +9 -0
- package/lib/ct-search.mjs +66 -12
- package/lib/dns-delegation-health.mjs +56 -5
- package/lib/dnssec-evidence-validation.mjs +235 -0
- package/lib/domain-assurance.mjs +368 -0
- package/lib/domain-control-manifest.mjs +382 -0
- package/lib/domain-posture.mjs +11 -10
- package/lib/encrypted-dns-contract.mjs +81 -0
- package/lib/evidence-export.mjs +17 -3
- package/lib/evidence-report-markdown.mjs +1 -1
- package/lib/evidence-report.mjs +4 -1
- package/lib/favicon.mjs +3 -8
- package/lib/generated/cisa-kev-catalog.mjs +1668 -0
- package/lib/html-signals.mjs +4 -0
- package/lib/http-evidence-bounds.mjs +4 -0
- package/lib/http-intelligence.mjs +2 -1
- package/lib/ip-prefix.mjs +87 -0
- package/lib/local-geoip-evidence.mjs +89 -0
- package/lib/lookup-response-contract.mjs +346 -0
- package/lib/observed-network-context.mjs +1 -1
- package/lib/opportunity-scoring.mjs +107 -0
- package/lib/outbound-identity.mjs +9 -0
- package/lib/outbound-request-bounds.mjs +9 -0
- package/lib/page-fingerprints.mjs +1 -1
- package/lib/perceptual-hash-comparison.mjs +34 -0
- package/lib/perceptual-hash.mjs +3 -31
- package/lib/project-metadata.mjs +8 -0
- package/lib/provider-policy-admission.mjs +49 -0
- package/lib/rdap-capabilities.mjs +6 -6
- package/lib/rdap-search-workbench.mjs +247 -0
- package/lib/rdap.mjs +7 -2
- package/lib/registry-access.mjs +1 -1
- package/lib/registry-capabilities.mjs +31 -1
- package/lib/registry-capability-catalogue.mjs +15 -14
- package/lib/registry-insights.mjs +6 -6
- package/lib/response-policy.mjs +81 -6
- package/lib/risk-scoring.mjs +206 -83
- package/lib/rpki-evidence.mjs +105 -0
- package/lib/safe-fetch.mjs +2 -1
- package/lib/scoring-evidence-quality.mjs +107 -0
- package/lib/security-txt.mjs +3 -3
- package/lib/semantic-version.mjs +54 -0
- package/lib/static-html-analysis.mjs +27 -0
- package/lib/threat-intelligence-manifests.mjs +1 -1
- package/lib/threatfox-intelligence.mjs +11 -6
- package/lib/tls-intelligence.mjs +13 -3
- package/lib/tlsa-evidence.mjs +194 -0
- package/lib/urlhaus-intelligence.mjs +11 -6
- package/lib/urlscan-intelligence.mjs +10 -5
- package/lib/website-security-posture.mjs +16 -9
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-chain.mjs +14 -152
- package/lib/whois.mjs +2 -2
- package/package.json +6 -2
- package/third-party-notices.txt +911 -19
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import packageDocument from '../package.json' with { type: 'json' };
|
|
2
|
+
import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "./project-metadata.mjs";
|
|
3
|
+
import { normalizeBoundedSemanticVersion } from "./semantic-version.mjs";
|
|
4
|
+
export { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL };
|
|
5
|
+
export const WHOISLEUTH_USER_AGENT_VERSION = normalizeBoundedSemanticVersion(packageDocument.version, 'Application manifest');
|
|
6
|
+
export const WHOISLEUTH_USER_AGENT = `WHOISleuth/${WHOISLEUTH_USER_AGENT_VERSION} (+${WHOISLEUTH_REQUEST_POLICY_URL})`;
|
|
7
|
+
export function whoisleuthRequestHeaders(headers = {}) {
|
|
8
|
+
return { ...headers, 'User-Agent': WHOISLEUTH_USER_AGENT };
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// Shared declarative limits for direct website observation. Runtime collectors
|
|
2
|
+
// and the public request-policy page import these values so operator-facing
|
|
3
|
+
// disclosures cannot drift from the enforced request boundaries.
|
|
4
|
+
export const MAX_OUTBOUND_REDIRECTS = 5;
|
|
5
|
+
export const HOMEPAGE_FETCH_TIMEOUT_MS = 6_000;
|
|
6
|
+
export const MAX_HOMEPAGE_BYTES = 300_000;
|
|
7
|
+
export const FAVICON_FETCH_TIMEOUT_MS = 5_000;
|
|
8
|
+
export const MAX_FAVICON_BYTES = 200_000;
|
|
9
|
+
export const MAX_FAVICON_CANDIDATES = 4;
|
|
@@ -430,7 +430,7 @@ function createPageFingerprints(html, options = {}) {
|
|
|
430
430
|
|| parsedStructure.structureLimitReached || text?.truncated === true || forms?.truncated === true
|
|
431
431
|
|| resourceHosts.truncated || identifiers.truncated;
|
|
432
432
|
const limitations = [
|
|
433
|
-
'Fingerprints
|
|
433
|
+
'Fingerprints summarise capped static HTML and are comparison aids, not cryptographic proof of page authorship or intent.',
|
|
434
434
|
'Visible-text SimHash is a fuzzy similarity fingerprint and must not be treated as a cryptographic digest.',
|
|
435
435
|
'DOM-structure SimHash is derived from a standards-compliant static token stream and is a review aid, not proof that two pages share an author or template.',
|
|
436
436
|
];
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
// Browser-safe comparison contract for the 64-bit perceptual hashes produced
|
|
2
|
+
// by the bounded image decoder. Keeping comparison and informativeness here
|
|
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
|
+
}
|
package/lib/perceptual-hash.mjs
CHANGED
|
@@ -21,6 +21,7 @@
|
|
|
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 "./perceptual-hash-comparison.mjs";
|
|
24
25
|
// Bound the decode work on attacker-controlled image bytes. Real favicons top
|
|
25
26
|
// out around 256x256; anything claiming to be larger is refused rather than
|
|
26
27
|
// decoded (it would also never be a legitimate favicon to compare against).
|
|
@@ -371,21 +372,11 @@ function grayscaleGrid(image, cols, rows) {
|
|
|
371
372
|
// (callers fall back to the exact hash) rather than a collision-prone one.
|
|
372
373
|
// isInformativeHash re-checks this at comparison time too, since a Brand
|
|
373
374
|
// Profile saved before this guard existed may already hold a degenerate hash.
|
|
374
|
-
const MIN_INFORMATIVE_BITS = 10;
|
|
375
|
-
function popcountHex(hex) {
|
|
376
|
-
let bits = 0;
|
|
377
|
-
for (let i = 0; i < hex.length; i += 1)
|
|
378
|
-
bits += POPCOUNT[parseInt(hex.charAt(i), 16)] ?? 0;
|
|
379
|
-
return bits;
|
|
380
|
-
}
|
|
381
375
|
function isInformativeHash(hex) {
|
|
382
|
-
|
|
383
|
-
return false;
|
|
384
|
-
const bits = popcountHex(hex);
|
|
385
|
-
return bits >= MIN_INFORMATIVE_BITS && bits <= 64 - MIN_INFORMATIVE_BITS;
|
|
376
|
+
return isInformativePerceptualHash(hex);
|
|
386
377
|
}
|
|
387
378
|
// 64-bit dHash over a 9x8 grid -> 16 hex chars, or null when the resulting
|
|
388
|
-
// hash is degenerate (see
|
|
379
|
+
// hash is degenerate (see the shared comparison contract). Each row contributes 8 bits:
|
|
389
380
|
// bit set when a cell is brighter than its right-hand neighbour.
|
|
390
381
|
function dHash(image) {
|
|
391
382
|
const cols = 9;
|
|
@@ -429,23 +420,4 @@ function faviconPerceptualHash(buf) {
|
|
|
429
420
|
// produced by the optional local capture package. Keep the generic name as an
|
|
430
421
|
// alias so the evidence contract does not imply that a screenshot is a favicon.
|
|
431
422
|
const imagePerceptualHash = faviconPerceptualHash;
|
|
432
|
-
const HEX_HASH_RE = /^[0-9a-f]{16}$/;
|
|
433
|
-
const POPCOUNT = new Uint8Array(16);
|
|
434
|
-
for (let i = 0; i < 16; i += 1)
|
|
435
|
-
POPCOUNT[i] = (i & 1) + ((i >> 1) & 1) + ((i >> 2) & 1) + ((i >> 3) & 1);
|
|
436
|
-
/**
|
|
437
|
-
* Hamming distance (0-64) between two 16-hex dHash strings, or null if either
|
|
438
|
-
* isn't a well-formed hash. Smaller = more visually similar.
|
|
439
|
-
*/
|
|
440
|
-
function hammingDistanceHex(a, b) {
|
|
441
|
-
if (typeof a !== 'string' || typeof b !== 'string')
|
|
442
|
-
return null;
|
|
443
|
-
if (!HEX_HASH_RE.test(a) || !HEX_HASH_RE.test(b))
|
|
444
|
-
return null;
|
|
445
|
-
let distance = 0;
|
|
446
|
-
for (let i = 0; i < 16; i += 1) {
|
|
447
|
-
distance += POPCOUNT[parseInt(a.charAt(i), 16) ^ parseInt(b.charAt(i), 16)] ?? 0;
|
|
448
|
-
}
|
|
449
|
-
return distance;
|
|
450
|
-
}
|
|
451
423
|
export { faviconPerceptualHash, hammingDistanceHex, imagePerceptualHash };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Browser-safe public project metadata shared by request identity, package
|
|
2
|
+
// verification, canonical URLs, structured data, and public policy pages.
|
|
3
|
+
export const WHOISLEUTH_SITE_ORIGIN = 'https://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`;
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
export const PROVIDER_POLICY_MAX_REVIEW_AGE_DAYS = 180;
|
|
2
|
+
export const DEPLOYMENT_PURPOSES = ['personal', 'internal', 'commercial'];
|
|
3
|
+
function deploymentPurpose(value) {
|
|
4
|
+
const normalized = String(value ?? '').trim().toLowerCase();
|
|
5
|
+
return DEPLOYMENT_PURPOSES.includes(normalized)
|
|
6
|
+
? normalized
|
|
7
|
+
: null;
|
|
8
|
+
}
|
|
9
|
+
export function providerPolicyAdmission(terms, env = process.env, now = Date.now()) {
|
|
10
|
+
const source = env && typeof env === 'object' ? env : {};
|
|
11
|
+
const purpose = deploymentPurpose(source.WHOISLEUTH_DEPLOYMENT_PURPOSE);
|
|
12
|
+
const reviewedAt = Date.parse(terms.reviewedAt);
|
|
13
|
+
const reviewAgeDays = Number.isFinite(reviewedAt)
|
|
14
|
+
? Math.max(0, Math.floor((now - reviewedAt) / 86_400_000))
|
|
15
|
+
: null;
|
|
16
|
+
if (!purpose) {
|
|
17
|
+
return {
|
|
18
|
+
allowed: false,
|
|
19
|
+
purpose: null,
|
|
20
|
+
reviewAgeDays,
|
|
21
|
+
reason: 'Optional intelligence requires WHOISLEUTH_DEPLOYMENT_PURPOSE=personal, internal, or commercial.',
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (reviewAgeDays === null || reviewAgeDays > PROVIDER_POLICY_MAX_REVIEW_AGE_DAYS) {
|
|
25
|
+
return {
|
|
26
|
+
allowed: false,
|
|
27
|
+
purpose,
|
|
28
|
+
reviewAgeDays,
|
|
29
|
+
reason: `Optional intelligence provider policy review is older than ${PROVIDER_POLICY_MAX_REVIEW_AGE_DAYS} days or invalid.`,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
if (terms.commercialUse === 'unknown') {
|
|
33
|
+
return {
|
|
34
|
+
allowed: false,
|
|
35
|
+
purpose,
|
|
36
|
+
reviewAgeDays,
|
|
37
|
+
reason: 'Optional intelligence commercial-use permission is unknown.',
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
if (terms.commercialUse === 'restricted' && purpose !== 'personal') {
|
|
41
|
+
return {
|
|
42
|
+
allowed: false,
|
|
43
|
+
purpose,
|
|
44
|
+
reviewAgeDays,
|
|
45
|
+
reason: `Optional intelligence is classified as restricted for the declared ${purpose} deployment purpose.`,
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
return { allowed: true, purpose, reviewAgeDays, reason: null };
|
|
49
|
+
}
|
|
@@ -44,7 +44,7 @@ const EXTENSION_CATALOG = Object.freeze({
|
|
|
44
44
|
farv1: {
|
|
45
45
|
label: 'Federated authentication',
|
|
46
46
|
category: 'standard',
|
|
47
|
-
capability: 'Federated
|
|
47
|
+
capability: 'Federated authorisation profile declaration.',
|
|
48
48
|
},
|
|
49
49
|
fred: {
|
|
50
50
|
label: 'Registry-system extension',
|
|
@@ -111,7 +111,7 @@ const EXTENSION_CATALOG = Object.freeze({
|
|
|
111
111
|
paging: {
|
|
112
112
|
label: 'Result paging',
|
|
113
113
|
category: 'standard',
|
|
114
|
-
capability: 'Paged search-result
|
|
114
|
+
capability: 'Paged search-result behaviour.',
|
|
115
115
|
},
|
|
116
116
|
platformns: {
|
|
117
117
|
label: 'Nameserver platform extension',
|
|
@@ -146,17 +146,17 @@ const EXTENSION_CATALOG = Object.freeze({
|
|
|
146
146
|
rirsearch1: {
|
|
147
147
|
label: 'Address-registry search',
|
|
148
148
|
category: 'standard',
|
|
149
|
-
capability: 'Versioned network and autonomous-system search
|
|
149
|
+
capability: 'Versioned network and autonomous-system search behaviour.',
|
|
150
150
|
},
|
|
151
151
|
sorting: {
|
|
152
152
|
label: 'Result sorting',
|
|
153
153
|
category: 'standard',
|
|
154
|
-
capability: 'Sorted search-result
|
|
154
|
+
capability: 'Sorted search-result behaviour.',
|
|
155
155
|
},
|
|
156
156
|
subsetting: {
|
|
157
157
|
label: 'Partial response',
|
|
158
158
|
category: 'standard',
|
|
159
|
-
capability: 'Field-subset response
|
|
159
|
+
capability: 'Field-subset response behaviour.',
|
|
160
160
|
},
|
|
161
161
|
ttl0: {
|
|
162
162
|
label: 'DNS TTL extension',
|
|
@@ -270,7 +270,7 @@ export function inspectRdapCapabilities(parsedValue, status) {
|
|
|
270
270
|
.map((entry) => entry.identifier)
|
|
271
271
|
.slice(0, MAX_RDAP_UNKNOWN_CAPABILITIES);
|
|
272
272
|
const limitations = [
|
|
273
|
-
'Declarations describe what the server claims for this response; they do not prove that an operation is
|
|
273
|
+
'Declarations describe what the server claims for this response; they do not prove that an operation is authorised, implemented correctly, complete, or available anonymously.',
|
|
274
274
|
'Unknown identifiers remain visible and neutral. Catalogue recognition never affects availability or Risk scoring.',
|
|
275
275
|
];
|
|
276
276
|
if (state === 'unavailable') {
|
|
@@ -0,0 +1,247 @@
|
|
|
1
|
+
const RDAP_SEARCH_WORKBENCH_SCHEMA = 'whoisleuth.rdap-search-workbench';
|
|
2
|
+
const RDAP_SEARCH_WORKBENCH_VERSION = 1;
|
|
3
|
+
const MAX_HELP_ENTRIES = 64;
|
|
4
|
+
const MAX_QUERY_VALUE_LENGTH = 254;
|
|
5
|
+
const SEARCHABLE_RESOURCE_TYPES = new Set(['domains', 'nameservers', 'entities']);
|
|
6
|
+
const RELATED_RESOURCE_TYPES = new Set(['entity']);
|
|
7
|
+
const RECOGNIZED_PROPERTIES = new Set(['handle', 'fn', 'email', 'role']);
|
|
8
|
+
const SENSITIVE_PROPERTIES = new Set(['fn', 'email']);
|
|
9
|
+
const REGISTERED_PROPERTY_PATHS = Object.freeze({
|
|
10
|
+
email: Object.freeze(["$.entities[*].vcardArray[1][?(@[0]=='email')][3]"]),
|
|
11
|
+
fn: Object.freeze(["$.entities[*].vcardArray[1][?(@[0]=='fn')][3]"]),
|
|
12
|
+
handle: Object.freeze(['$.entities[*].handle']),
|
|
13
|
+
role: Object.freeze(['$.entities[*].roles']),
|
|
14
|
+
});
|
|
15
|
+
function record(value) {
|
|
16
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
17
|
+
? value
|
|
18
|
+
: null;
|
|
19
|
+
}
|
|
20
|
+
function boundedToken(value) {
|
|
21
|
+
if (typeof value !== 'string')
|
|
22
|
+
return null;
|
|
23
|
+
const normalized = value.trim().toLowerCase();
|
|
24
|
+
if (!normalized || normalized.length > 48 || !/^[a-z][a-z0-9_-]*$/u.test(normalized))
|
|
25
|
+
return null;
|
|
26
|
+
return normalized;
|
|
27
|
+
}
|
|
28
|
+
function declarationKey(value) {
|
|
29
|
+
return `${value.searchableResourceType}:${value.relatedResourceType}:${value.property}`;
|
|
30
|
+
}
|
|
31
|
+
function emptySummary(state, limitation, rejectedCount = 0) {
|
|
32
|
+
return Object.freeze({
|
|
33
|
+
schema: RDAP_SEARCH_WORKBENCH_SCHEMA,
|
|
34
|
+
version: RDAP_SEARCH_WORKBENCH_VERSION,
|
|
35
|
+
state,
|
|
36
|
+
capabilities: Object.freeze([]),
|
|
37
|
+
rejectedCount,
|
|
38
|
+
truncated: false,
|
|
39
|
+
limitations: Object.freeze([limitation]),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
function normalizeRdapSearchHelp(value) {
|
|
43
|
+
const document = record(value);
|
|
44
|
+
if (!document)
|
|
45
|
+
return emptySummary('invalid', 'The RDAP help response was not an object.');
|
|
46
|
+
const raw = document.reverse_search_properties;
|
|
47
|
+
if (raw === undefined) {
|
|
48
|
+
return emptySummary('unsupported', 'The help response did not advertise reverse-search properties.');
|
|
49
|
+
}
|
|
50
|
+
if (!Array.isArray(raw)) {
|
|
51
|
+
return emptySummary('invalid', 'The advertised reverse-search properties were malformed.', 1);
|
|
52
|
+
}
|
|
53
|
+
const capabilities = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
let rejectedCount = 0;
|
|
56
|
+
const truncated = raw.length > MAX_HELP_ENTRIES;
|
|
57
|
+
for (const entry of raw.slice(0, MAX_HELP_ENTRIES)) {
|
|
58
|
+
const candidate = record(entry);
|
|
59
|
+
const searchableResourceType = boundedToken(candidate?.searchableResourceType);
|
|
60
|
+
const relatedResourceType = boundedToken(candidate?.relatedResourceType);
|
|
61
|
+
const property = boundedToken(candidate?.property);
|
|
62
|
+
if (!searchableResourceType || !relatedResourceType || !property) {
|
|
63
|
+
rejectedCount += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
const declaration = { searchableResourceType, relatedResourceType, property };
|
|
67
|
+
const key = declarationKey(declaration);
|
|
68
|
+
if (seen.has(key))
|
|
69
|
+
continue;
|
|
70
|
+
seen.add(key);
|
|
71
|
+
const supported = SEARCHABLE_RESOURCE_TYPES.has(searchableResourceType)
|
|
72
|
+
&& RELATED_RESOURCE_TYPES.has(relatedResourceType)
|
|
73
|
+
&& RECOGNIZED_PROPERTIES.has(property);
|
|
74
|
+
capabilities.push(Object.freeze({
|
|
75
|
+
...declaration,
|
|
76
|
+
state: supported ? 'supported' : 'unsupported',
|
|
77
|
+
disclosure: SENSITIVE_PROPERTIES.has(property) ? 'contact' : 'identifier',
|
|
78
|
+
limitation: supported ? null : 'The server advertised a tuple that this release does not construct.',
|
|
79
|
+
}));
|
|
80
|
+
}
|
|
81
|
+
if (truncated)
|
|
82
|
+
rejectedCount += raw.length - MAX_HELP_ENTRIES;
|
|
83
|
+
const supportedCount = capabilities.filter((entry) => entry.state === 'supported').length;
|
|
84
|
+
const limitations = [];
|
|
85
|
+
if (truncated)
|
|
86
|
+
limitations.push(`Only the first ${MAX_HELP_ENTRIES} advertised properties were reviewed.`);
|
|
87
|
+
if (rejectedCount > 0)
|
|
88
|
+
limitations.push(`${rejectedCount} malformed or excess properties were not used.`);
|
|
89
|
+
if (capabilities.some((entry) => entry.state === 'unsupported')) {
|
|
90
|
+
limitations.push('Unrecognized advertised tuples remain visible but cannot be planned by this release.');
|
|
91
|
+
}
|
|
92
|
+
if (supportedCount === 0)
|
|
93
|
+
limitations.push('No supported reverse-search tuple was advertised.');
|
|
94
|
+
return Object.freeze({
|
|
95
|
+
schema: RDAP_SEARCH_WORKBENCH_SCHEMA,
|
|
96
|
+
version: RDAP_SEARCH_WORKBENCH_VERSION,
|
|
97
|
+
state: supportedCount === 0 ? 'unsupported' : limitations.length > 0 ? 'partial' : 'supported',
|
|
98
|
+
capabilities: Object.freeze(capabilities),
|
|
99
|
+
rejectedCount,
|
|
100
|
+
truncated,
|
|
101
|
+
limitations: Object.freeze(limitations),
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
function normalizeQueryValue(value) {
|
|
105
|
+
if (typeof value !== 'string')
|
|
106
|
+
return null;
|
|
107
|
+
const normalized = value.trim();
|
|
108
|
+
if (!normalized || normalized.length > MAX_QUERY_VALUE_LENGTH)
|
|
109
|
+
return null;
|
|
110
|
+
if (/[\x00-\x1f\x7f]/u.test(normalized))
|
|
111
|
+
return null;
|
|
112
|
+
return normalized;
|
|
113
|
+
}
|
|
114
|
+
function planRdapReverseSearch(help, requested) {
|
|
115
|
+
const searchableResourceType = boundedToken(requested.searchableResourceType);
|
|
116
|
+
const relatedResourceType = boundedToken(requested.relatedResourceType);
|
|
117
|
+
const property = boundedToken(requested.property);
|
|
118
|
+
const value = normalizeQueryValue(requested.value);
|
|
119
|
+
if (!searchableResourceType || !relatedResourceType || !property || !value) {
|
|
120
|
+
return Object.freeze({
|
|
121
|
+
schema: RDAP_SEARCH_WORKBENCH_SCHEMA,
|
|
122
|
+
version: RDAP_SEARCH_WORKBENCH_VERSION,
|
|
123
|
+
state: 'invalid',
|
|
124
|
+
requestPath: null,
|
|
125
|
+
query: null,
|
|
126
|
+
disclosure: null,
|
|
127
|
+
limitation: 'The requested resource types, property, or value were invalid or outside the supported bounds.',
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
const capability = help.capabilities.find((entry) => (entry.state === 'supported'
|
|
131
|
+
&& entry.searchableResourceType === searchableResourceType
|
|
132
|
+
&& entry.relatedResourceType === relatedResourceType
|
|
133
|
+
&& entry.property === property));
|
|
134
|
+
if (!capability) {
|
|
135
|
+
return Object.freeze({
|
|
136
|
+
schema: RDAP_SEARCH_WORKBENCH_SCHEMA,
|
|
137
|
+
version: RDAP_SEARCH_WORKBENCH_VERSION,
|
|
138
|
+
state: 'unsupported',
|
|
139
|
+
requestPath: null,
|
|
140
|
+
query: null,
|
|
141
|
+
disclosure: null,
|
|
142
|
+
limitation: 'The supplied help response did not advertise this exact reverse-search tuple.',
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
const contactDisclosure = capability.disclosure === 'contact';
|
|
146
|
+
return Object.freeze({
|
|
147
|
+
schema: RDAP_SEARCH_WORKBENCH_SCHEMA,
|
|
148
|
+
version: RDAP_SEARCH_WORKBENCH_VERSION,
|
|
149
|
+
state: 'ready',
|
|
150
|
+
requestPath: `/${searchableResourceType}/reverse_search/${relatedResourceType}`,
|
|
151
|
+
query: Object.freeze({ [property]: value }),
|
|
152
|
+
disclosure: Object.freeze({
|
|
153
|
+
class: capability.disclosure,
|
|
154
|
+
summary: contactDisclosure
|
|
155
|
+
? `Would disclose the supplied ${property} contact value to the selected RDAP server.`
|
|
156
|
+
: `Would disclose the supplied ${property} identifier to the selected RDAP server.`,
|
|
157
|
+
requiresApproval: true,
|
|
158
|
+
}),
|
|
159
|
+
limitation: 'This is an offline request plan. It neither sends the query nor establishes result authority, completeness, or currentness.',
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
function inspectRdapReverseSearchResponse(value, requestedProperties = []) {
|
|
163
|
+
const document = record(value);
|
|
164
|
+
if (!document) {
|
|
165
|
+
return Object.freeze({
|
|
166
|
+
schema: RDAP_SEARCH_WORKBENCH_SCHEMA,
|
|
167
|
+
version: RDAP_SEARCH_WORKBENCH_VERSION,
|
|
168
|
+
state: 'invalid',
|
|
169
|
+
mappings: Object.freeze([]),
|
|
170
|
+
missingProperties: Object.freeze([]),
|
|
171
|
+
rejectedCount: 0,
|
|
172
|
+
truncated: false,
|
|
173
|
+
limitations: Object.freeze(['The reverse-search response was not an object.']),
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
const requested = [...new Set(requestedProperties
|
|
177
|
+
.map(boundedToken)
|
|
178
|
+
.filter((property) => Boolean(property)))]
|
|
179
|
+
.slice(0, RECOGNIZED_PROPERTIES.size);
|
|
180
|
+
const raw = document.reverse_search_properties_mapping;
|
|
181
|
+
if (!Array.isArray(raw)) {
|
|
182
|
+
return Object.freeze({
|
|
183
|
+
schema: RDAP_SEARCH_WORKBENCH_SCHEMA,
|
|
184
|
+
version: RDAP_SEARCH_WORKBENCH_VERSION,
|
|
185
|
+
state: 'invalid',
|
|
186
|
+
mappings: Object.freeze([]),
|
|
187
|
+
missingProperties: Object.freeze(requested),
|
|
188
|
+
rejectedCount: raw === undefined ? 0 : 1,
|
|
189
|
+
truncated: false,
|
|
190
|
+
limitations: Object.freeze([
|
|
191
|
+
'The response did not provide a valid reverse-search property mapping, so its result fields cannot be reconciled with the requested predicates.',
|
|
192
|
+
]),
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
const mappings = [];
|
|
196
|
+
const seen = new Set();
|
|
197
|
+
let rejectedCount = 0;
|
|
198
|
+
const truncated = raw.length > MAX_HELP_ENTRIES;
|
|
199
|
+
for (const entry of raw.slice(0, MAX_HELP_ENTRIES)) {
|
|
200
|
+
const candidate = record(entry);
|
|
201
|
+
const property = boundedToken(candidate?.property);
|
|
202
|
+
const propertyPath = typeof candidate?.propertyPath === 'string'
|
|
203
|
+
? candidate.propertyPath.trim().slice(0, 512)
|
|
204
|
+
: '';
|
|
205
|
+
if (!property || !propertyPath || /[\x00-\x1f\x7f]/u.test(propertyPath)) {
|
|
206
|
+
rejectedCount += 1;
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const key = `${property}\u0000${propertyPath}`;
|
|
210
|
+
if (seen.has(key))
|
|
211
|
+
continue;
|
|
212
|
+
seen.add(key);
|
|
213
|
+
mappings.push(Object.freeze({
|
|
214
|
+
property,
|
|
215
|
+
propertyPath,
|
|
216
|
+
state: REGISTERED_PROPERTY_PATHS[property]?.includes(propertyPath)
|
|
217
|
+
? 'registered'
|
|
218
|
+
: 'unrecognized',
|
|
219
|
+
}));
|
|
220
|
+
}
|
|
221
|
+
if (truncated)
|
|
222
|
+
rejectedCount += raw.length - MAX_HELP_ENTRIES;
|
|
223
|
+
const mappedProperties = new Set(mappings.map((mapping) => mapping.property));
|
|
224
|
+
const missingProperties = requested.filter((property) => !mappedProperties.has(property));
|
|
225
|
+
const limitations = [];
|
|
226
|
+
if (missingProperties.length) {
|
|
227
|
+
limitations.push(`The response omitted mappings for requested properties: ${missingProperties.join(', ')}.`);
|
|
228
|
+
}
|
|
229
|
+
if (mappings.some((mapping) => mapping.state === 'unrecognized')) {
|
|
230
|
+
limitations.push('At least one mapping differs from the locally reviewed IANA registration and requires manual specification review.');
|
|
231
|
+
}
|
|
232
|
+
if (rejectedCount)
|
|
233
|
+
limitations.push(`${rejectedCount} malformed or excess mapping entr${rejectedCount === 1 ? 'y was' : 'ies were'} not used.`);
|
|
234
|
+
if (!mappings.length)
|
|
235
|
+
limitations.push('No usable reverse-search property mapping was retained.');
|
|
236
|
+
return Object.freeze({
|
|
237
|
+
schema: RDAP_SEARCH_WORKBENCH_SCHEMA,
|
|
238
|
+
version: RDAP_SEARCH_WORKBENCH_VERSION,
|
|
239
|
+
state: mappings.length && !limitations.length ? 'complete' : mappings.length ? 'partial' : 'invalid',
|
|
240
|
+
mappings: Object.freeze(mappings),
|
|
241
|
+
missingProperties: Object.freeze(missingProperties),
|
|
242
|
+
rejectedCount,
|
|
243
|
+
truncated,
|
|
244
|
+
limitations: Object.freeze(limitations),
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
export { MAX_HELP_ENTRIES, MAX_QUERY_VALUE_LENGTH, RDAP_SEARCH_WORKBENCH_SCHEMA, RDAP_SEARCH_WORKBENCH_VERSION, normalizeRdapSearchHelp, planRdapReverseSearch, inspectRdapReverseSearchResponse, };
|
package/lib/rdap.mjs
CHANGED
|
@@ -4,11 +4,16 @@ import { fetchRdapFromBasesWithParser, fetchRdapRecordWithParser, } from "./rdap
|
|
|
4
4
|
import { normalizeRdapEvents, parseRdap, summarizeLifecycle, } from "./rdap-normalization.mjs";
|
|
5
5
|
import { fetchRegistrarRdapRecordWithParser, selectRegistrarRdapLink, } from "./rdap-registrar.mjs";
|
|
6
6
|
import { fetchRdapWithTimeout, } from "./rdap-transport.mjs";
|
|
7
|
+
import { registryServiceAdmissionFor } from "./registry-capabilities.mjs";
|
|
7
8
|
async function fetchRdapFromBases(type, value, bases, fetchUpstream = fetchRdapWithTimeout) {
|
|
8
9
|
return fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream);
|
|
9
10
|
}
|
|
10
|
-
async function fetchRdapRecord(type, value) {
|
|
11
|
-
|
|
11
|
+
async function fetchRdapRecord(type, value, options = {}) {
|
|
12
|
+
if (type === 'domain' && registryServiceAdmissionFor(value, 'rdap')?.allowed === false) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
const fetchRecord = options.fetchRecord ?? fetchRdapRecordWithParser;
|
|
16
|
+
return fetchRecord(type, value, parseRdap);
|
|
12
17
|
}
|
|
13
18
|
async function fetchRegistrarRdapRecord(domain, registryRecord, options = {}) {
|
|
14
19
|
return fetchRegistrarRdapRecordWithParser(domain, registryRecord, parseRdap, options);
|
package/lib/registry-access.mjs
CHANGED
|
@@ -3,7 +3,7 @@ const REGISTRY_ACCESS_PROFILE_LABELS = Object.freeze({
|
|
|
3
3
|
'iana-referral': 'IANA referral discovery',
|
|
4
4
|
'no-iana-service': 'No service published by IANA',
|
|
5
5
|
'registry-policy-restricted': 'Registry policy restricted',
|
|
6
|
-
'source-ip-authorization-required': 'Source-IP
|
|
6
|
+
'source-ip-authorization-required': 'Source-IP authorisation required',
|
|
7
7
|
});
|
|
8
8
|
const WHOIS_REGISTRY_ACCESS_PROFILES = new Set([
|
|
9
9
|
'iana-referral',
|
|
@@ -90,6 +90,36 @@ function registryAccessDiagnosticFor(value) {
|
|
|
90
90
|
authority: 'context_only',
|
|
91
91
|
};
|
|
92
92
|
}
|
|
93
|
+
function registryServiceAdmissionFor(value, service) {
|
|
94
|
+
const capability = registryCapabilityFor(value);
|
|
95
|
+
if (!capability || (service !== 'rdap' && service !== 'whois'))
|
|
96
|
+
return null;
|
|
97
|
+
if (service === 'rdap') {
|
|
98
|
+
const allowed = capability.rdapAccessProfile === 'iana-bootstrap';
|
|
99
|
+
return {
|
|
100
|
+
service,
|
|
101
|
+
state: allowed ? 'allowed' : 'unsupported',
|
|
102
|
+
allowed,
|
|
103
|
+
limitation: capability.limitation,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
if (capability.whoisAccessProfile === 'iana-referral') {
|
|
107
|
+
return {
|
|
108
|
+
service,
|
|
109
|
+
state: 'allowed',
|
|
110
|
+
allowed: true,
|
|
111
|
+
limitation: capability.limitation,
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
const permissionRequired = capability.whoisAccessProfile === 'registry-policy-restricted'
|
|
115
|
+
|| capability.whoisAccessProfile === 'source-ip-authorization-required';
|
|
116
|
+
return {
|
|
117
|
+
service,
|
|
118
|
+
state: permissionRequired ? 'permission_required' : 'unsupported',
|
|
119
|
+
allowed: false,
|
|
120
|
+
limitation: capability.limitation,
|
|
121
|
+
};
|
|
122
|
+
}
|
|
93
123
|
function listRegistryCapabilities() {
|
|
94
124
|
return EXPLICIT_CAPABILITIES.map((capability) => cloneCapability(capability));
|
|
95
125
|
}
|
|
@@ -114,4 +144,4 @@ function registryCompatibilityMatrix() {
|
|
|
114
144
|
})))
|
|
115
145
|
.sort((left, right) => (left.suffixes[0] ?? '').localeCompare(right.suffixes[0] ?? ''));
|
|
116
146
|
}
|
|
117
|
-
export { REGISTRY_CAPABILITIES_VERSION, VERSION_26_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, registryCapabilityFor, registryCompatibilityMatrix, listRegistryCapabilities, registryAccessDiagnosticFor, registryStandardsCoverageSnapshot, };
|
|
147
|
+
export { REGISTRY_CAPABILITIES_VERSION, VERSION_26_NO_RDAP_SUFFIXES, VERSION_27_RDAP_ONLY_GENERIC_SUFFIXES, registryCapabilityFor, registryCompatibilityMatrix, listRegistryCapabilities, registryAccessDiagnosticFor, registryServiceAdmissionFor, registryStandardsCoverageSnapshot, };
|
|
@@ -1,15 +1,16 @@
|
|
|
1
1
|
// Registry compatibility metadata. IANA bootstrap and referral discovery
|
|
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
|
-
const REGISTRY_CAPABILITIES_VERSION =
|
|
4
|
+
const REGISTRY_CAPABILITIES_VERSION = 28;
|
|
5
5
|
const MAX_CAPABILITY_INPUT_LENGTH = 253;
|
|
6
6
|
const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
|
|
7
7
|
schema: 'whoisleuth.registry-standards-coverage',
|
|
8
8
|
version: 1,
|
|
9
|
-
verifiedAt: '2026-08-
|
|
9
|
+
verifiedAt: '2026-08-03',
|
|
10
10
|
sources: Object.freeze({
|
|
11
|
-
rootZoneVersion: '
|
|
12
|
-
rootZoneLastUpdatedAt: '2026-
|
|
11
|
+
rootZoneVersion: '2026080200',
|
|
12
|
+
rootZoneLastUpdatedAt: '2026-08-02T07:07:01.000Z',
|
|
13
|
+
rootZoneTldSetSha256: 'aa0a75a9860b2cba07d7fe8172f4546d981be3674bf6764fb0d5f39a45940d25',
|
|
13
14
|
rdapBootstrapPublication: '2026-07-23T02:00:03.000Z',
|
|
14
15
|
rdapBootstrapVersion: '1.0',
|
|
15
16
|
urls: Object.freeze([
|
|
@@ -50,9 +51,9 @@ const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
|
|
|
50
51
|
]),
|
|
51
52
|
interpretation: 'This official-source snapshot describes published service coverage at the verification date. It does not test current reachability or decide registration, availability, ownership, safety, or maliciousness.',
|
|
52
53
|
});
|
|
53
|
-
const DISCOVERY_LIMITATION = 'IANA discovery is available, but no suffix-specific query, encoding, or parser
|
|
54
|
+
const DISCOVERY_LIMITATION = 'IANA discovery is available, but no suffix-specific query, encoding, or parser behaviour is fixture-verified.';
|
|
54
55
|
const FIXTURE_LIMITATION = 'Synthetic fixtures verify the current parser profile; they do not prove current live-registry reachability, policy, or field publication.';
|
|
55
|
-
const ES_ACCESS_LIMITATION = 'The registry WHOIS service requires advance source-IP
|
|
56
|
+
const ES_ACCESS_LIMITATION = 'The registry WHOIS service requires advance source-IP authorisation. A failed or unavailable query is not evidence that the domain is unregistered.';
|
|
56
57
|
const SWITCH_ACCESS_LIMITATION = 'The registry directs public domain searches to its official lookup. Its non-standard-port Domain Check is not integrated, and IANA publishes no RDAP service. Missing registry data is not evidence that the domain is unregistered.';
|
|
57
58
|
const VN_ACCESS_LIMITATION = 'IANA publishes no domain WHOIS or RDAP service for this suffix. The official browser lookup is not integrated, and missing registry data is not evidence that the domain is unregistered.';
|
|
58
59
|
const UK_TRANSITION_LIMITATION = 'Synthetic fixtures verify the documented sectioned port-43 response while that WHOIS service is phased out. RDAP remains the preferred registry source, and fixture coverage does not prove current reachability or field publication.';
|
|
@@ -63,8 +64,8 @@ const SHARED_WHOIS_NO_RDAP_LIMITATION = 'Synthetic fixture reuse is limited to a
|
|
|
63
64
|
const WHOIS_ONLY_FIXTURE_LIMITATION = 'Synthetic fixtures verify the current WHOIS parser profile. IANA publishes no RDAP bootstrap service for this suffix, and missing registry data is not evidence that the domain is unregistered.';
|
|
64
65
|
const NEGATIVE_ONLY_FIXTURE_LIMITATION = 'Synthetic fixtures verify only the authoritative no-record dialect; registered-field compatibility is not claimed, and fixture coverage does not prove current live-registry reachability or policy.';
|
|
65
66
|
const WHOIS_ONLY_NEGATIVE_FIXTURE_LIMITATION = 'Synthetic fixtures verify only the authoritative WHOIS no-record dialect; registered-field compatibility is not claimed, IANA publishes no RDAP bootstrap service, and missing registry data is not evidence that the domain is unregistered.';
|
|
66
|
-
const UNVERIFIED_WHOIS_REFERRAL_LIMITATION = 'IANA publishes a domain WHOIS referral, but its response
|
|
67
|
-
const UNVERIFIED_WHOIS_WITH_RDAP_LIMITATION = 'IANA publishes domain WHOIS and RDAP discovery, but the WHOIS response
|
|
67
|
+
const UNVERIFIED_WHOIS_REFERRAL_LIMITATION = 'IANA publishes a domain WHOIS referral, but its response behaviour is not fixture-verified. A failed, restricted, prohibited, reserved, or otherwise inconclusive response is not evidence that the domain is unregistered, and IANA publishes no RDAP bootstrap service for this suffix.';
|
|
68
|
+
const UNVERIFIED_WHOIS_WITH_RDAP_LIMITATION = 'IANA publishes domain WHOIS and RDAP discovery, but the WHOIS response behaviour is not fixture-verified. A failed, restricted, prohibited, reserved, or otherwise inconclusive WHOIS response is not evidence that the domain is unregistered; RDAP remains separately attributed.';
|
|
68
69
|
const NORID_CLOSED_SUFFIX_LIMITATION = 'The registry has not opened this suffix for registrations, and IANA publishes no domain WHOIS or RDAP service. Missing registry data is contextual only and must not be interpreted as a live availability result.';
|
|
69
70
|
const MIL_ACCESS_LIMITATION = 'IANA publishes no public domain WHOIS or RDAP service for this sponsored suffix. Missing registry data is contextual only and is not evidence that a name is unregistered or available.';
|
|
70
71
|
const ARPA_ACCESS_LIMITATION = 'This infrastructure suffix is not ordinary public registration space. IANA publishes WHOIS metadata but no RDAP bootstrap service, and missing or non-domain-shaped data must not be interpreted as registration availability.';
|
|
@@ -158,7 +159,6 @@ const DEFAULT_CAPABILITY = freezeCapability({
|
|
|
158
159
|
whoisQueryScope: 'first-referral',
|
|
159
160
|
whoisEncodingProfile: 'utf-8',
|
|
160
161
|
whoisParserProfile: 'generic-colon',
|
|
161
|
-
fallbackProfile: null,
|
|
162
162
|
whoisAccessProfile: 'iana-referral',
|
|
163
163
|
rdapAccessProfile: 'iana-bootstrap',
|
|
164
164
|
coverageState: 'discovery_only',
|
|
@@ -661,10 +661,12 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
661
661
|
],
|
|
662
662
|
},
|
|
663
663
|
{
|
|
664
|
-
id: 'gt-
|
|
665
|
-
whoisParserProfile: 'generic-colon',
|
|
666
|
-
|
|
667
|
-
|
|
664
|
+
id: 'gt-no-iana-service', suffixes: ['gt'], registryClass: 'country-code',
|
|
665
|
+
whoisParserProfile: 'generic-colon', fixtureScenarios: [],
|
|
666
|
+
coverageState: 'access_documented', whoisAccessProfile: 'no-iana-service',
|
|
667
|
+
rdapAccessProfile: 'no-iana-service', verificationFiles: [],
|
|
668
|
+
documentationUrls: ['https://www.iana.org/domains/root/db/gt.html'],
|
|
669
|
+
limitation: 'IANA publishes no RDAP service or domain WHOIS service for this suffix. WHOISleuth does not automate the registry website.',
|
|
668
670
|
},
|
|
669
671
|
{
|
|
670
672
|
id: 'carnet-icann-colon', suffixes: ['hr'], registryClass: 'country-code',
|
|
@@ -1179,7 +1181,6 @@ const EXPLICIT_CAPABILITIES = EXPLICIT_CAPABILITY_SEEDS.map((entry) => {
|
|
|
1179
1181
|
const rdapAccessProfile = resolvedSeedRdapAccessProfile(entry);
|
|
1180
1182
|
return freezeCapability({
|
|
1181
1183
|
...entry,
|
|
1182
|
-
fallbackProfile: entry.fallbackProfile || null,
|
|
1183
1184
|
rdapDiscovery: 'iana-bootstrap',
|
|
1184
1185
|
whoisDiscovery: 'iana-referral',
|
|
1185
1186
|
whoisQueryProfile: entry.whoisQueryProfile || 'plain-domain',
|