@slicedearth/whoisleuth-cli 1.45.0 → 1.47.4
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 +7 -3
- package/NOTICE +5 -4
- package/README.md +59 -6
- package/bin/whoisleuth.mjs +5 -3
- package/cli/arguments.mjs +256 -9
- package/cli/artifact-structure.mjs +1833 -0
- package/cli/artifact-verify.mjs +242 -49
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +2 -3
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/case-pack.mjs +819 -34
- package/cli/command-reference.mjs +48 -24
- package/cli/completion.mjs +16 -8
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discovery-scan-command-runner.mjs +4 -5
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +21 -15
- package/cli/domain-control-observations.mjs +25 -2
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +73 -45
- package/cli/export-evidence.mjs +11 -4
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/terminal.mjs +3 -0
- package/cli/input.mjs +57 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-run.mjs +71 -23
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookup-command-runner.mjs +2 -2
- package/cli/lookup-diff.mjs +9 -0
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +28 -8
- package/cli/page-compare.mjs +8 -5
- package/cli/registry-cohort.mjs +385 -61
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +376 -0
- package/cli/risk-calibration.mjs +7 -5
- package/cli/runner.mjs +147 -185
- package/cli/saved-lookup.mjs +26 -3
- package/cli/sharing-review.mjs +12 -6
- package/docs/cli-reference.md +444 -70
- package/docs/cli.md +24 -5
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
- package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +29 -5
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
- package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
- package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
- package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
- package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
- package/frontend/src/lib/analysis/bulk-sort.js +4 -0
- package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +8 -0
- package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
- package/frontend/src/lib/analysis/case-migration-model.js +83 -31
- package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
- package/frontend/src/lib/analysis/case-record-model.js +2 -0
- package/frontend/src/lib/analysis/case-record-operations.js +42 -12
- package/frontend/src/lib/analysis/case-report.js +39 -6
- package/frontend/src/lib/analysis/case-response-model.js +66 -7
- package/frontend/src/lib/analysis/case-response-packet.js +15 -5
- package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
- package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
- package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
- package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
- package/frontend/src/lib/analysis/domain-control-records.js +88 -0
- package/frontend/src/lib/analysis/external-findings-import.js +70 -6
- package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
- package/frontend/src/lib/analysis/investigation-guide.js +52 -10
- package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
- package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
- package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
- package/frontend/src/lib/analysis/lookup-decision-support.js +61 -6
- package/frontend/src/lib/analysis/lookup-presentation.js +62 -0
- package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
- package/frontend/src/lib/analysis/utils.js +26 -3
- package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
- package/frontend/src/lib/analysis/visualization-models.js +63 -94
- package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
- package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
- package/frontend/src/lib/analysis/workspace-archive.js +104 -14
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +14 -12
- package/lib/bounded-file.mjs +13 -0
- package/lib/bounded-json-response.mjs +17 -4
- package/lib/bounded-json.mjs +249 -0
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/cryptographic-assurance.mjs +155 -0
- package/lib/ct-search.mjs +87 -47
- package/lib/dns-intelligence.mjs +3 -2
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1053 -0
- package/lib/dnssec-evidence-validation.mjs +10 -3
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-manifest.mjs +17 -129
- package/lib/domain-posture.mjs +7 -4
- package/lib/evidence-export.mjs +751 -78
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/favicon.mjs +8 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +2 -5
- package/lib/interchange-fidelity-registry.mjs +122 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup-response-contract.mjs +691 -3
- package/lib/lookup.mjs +2 -2
- package/lib/observation.mjs +7 -6
- package/lib/outbound-identity.mjs +2 -3
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +21 -0
- package/lib/provider-policy-admission.mjs +9 -4
- package/lib/rdap-normalization.mjs +18 -5
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/risk-calibration-summary.mjs +338 -0
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +59 -10
- package/lib/security-txt.mjs +26 -3
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +784 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/threat-intelligence-runtime.mjs +3 -11
- package/lib/threat-intelligence-types.mjs +18 -1
- package/lib/tls-intelligence.mjs +11 -5
- package/lib/tlsa-evidence.mjs +37 -7
- package/package.json +1 -1
- package/third-party-notices.txt +95 -5817
package/lib/lookup.mjs
CHANGED
|
@@ -320,7 +320,7 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
320
320
|
? { ...rdapRecord, ...(registrarRdap ? { registrarRdap } : {}) }
|
|
321
321
|
: {
|
|
322
322
|
error: rdapResult.status === 'rejected'
|
|
323
|
-
?
|
|
323
|
+
? boundedSourceDetail(rdapResult.reason, 'RDAP lookup failed')
|
|
324
324
|
: 'No RDAP registry found for this query via IANA bootstrap',
|
|
325
325
|
attempts: rdapAttempts,
|
|
326
326
|
};
|
|
@@ -337,7 +337,7 @@ async function runUnifiedLookup(classified, options = {}) {
|
|
|
337
337
|
else {
|
|
338
338
|
whois = {
|
|
339
339
|
error: whoisResult.status === 'rejected'
|
|
340
|
-
?
|
|
340
|
+
? boundedSourceDetail(whoisResult.reason, 'WHOIS lookup failed')
|
|
341
341
|
: 'WHOIS returned no referral chain',
|
|
342
342
|
};
|
|
343
343
|
}
|
package/lib/observation.mjs
CHANGED
|
@@ -12,9 +12,10 @@ const STATUSES = new Set([
|
|
|
12
12
|
'not_applicable',
|
|
13
13
|
]);
|
|
14
14
|
const SCAN_MODES = new Set(['fast', 'deep']);
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
const
|
|
15
|
+
const MAX_OBSERVATION_LIMITATIONS = 10;
|
|
16
|
+
const MAX_OBSERVATION_LIMITATION_LENGTH = 300;
|
|
17
|
+
const MAX_OBSERVATION_DIAGNOSTICS = 20;
|
|
18
|
+
const MAX_DIAGNOSTICS = MAX_OBSERVATION_DIAGNOSTICS;
|
|
18
19
|
const MAX_DIAGNOSTIC_KEY = 40;
|
|
19
20
|
const MAX_DIAGNOSTIC_STRING = 240;
|
|
20
21
|
const MAX_DURATION_MS = 120_000;
|
|
@@ -76,9 +77,9 @@ function createObservation(input = {}) {
|
|
|
76
77
|
const observedAt = isoTimestamp(input.observedAt) || new Date().toISOString();
|
|
77
78
|
const duration = Number(input.durationMs);
|
|
78
79
|
const limitations = [...new Set((Array.isArray(input.limitations) ? input.limitations : [])
|
|
79
|
-
.map((item) => safeString(item,
|
|
80
|
+
.map((item) => safeString(item, MAX_OBSERVATION_LIMITATION_LENGTH))
|
|
80
81
|
.filter((item) => item !== null))]
|
|
81
|
-
.slice(0,
|
|
82
|
+
.slice(0, MAX_OBSERVATION_LIMITATIONS);
|
|
82
83
|
return {
|
|
83
84
|
version: OBSERVATION_VERSION,
|
|
84
85
|
status,
|
|
@@ -111,4 +112,4 @@ function readObservationEnvelope(value) {
|
|
|
111
112
|
}
|
|
112
113
|
return { state: 'supported', observation: createObservation(record) };
|
|
113
114
|
}
|
|
114
|
-
export { OBSERVATION_VERSION, createObservation, readObservationEnvelope, };
|
|
115
|
+
export { MAX_OBSERVATION_DIAGNOSTICS, MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, OBSERVATION_VERSION, createObservation, readObservationEnvelope, };
|
|
@@ -1,8 +1,7 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { WHOISLEUTH_APPLICATION_VERSION } from "./application-version.mjs";
|
|
2
2
|
import { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL, } from "./project-metadata.mjs";
|
|
3
|
-
import { normalizeBoundedSemanticVersion } from "./semantic-version.mjs";
|
|
4
3
|
export { WHOISLEUTH_PROJECT_URL, WHOISLEUTH_REQUEST_POLICY_URL };
|
|
5
|
-
export const WHOISLEUTH_USER_AGENT_VERSION =
|
|
4
|
+
export const WHOISLEUTH_USER_AGENT_VERSION = WHOISLEUTH_APPLICATION_VERSION;
|
|
6
5
|
export const WHOISLEUTH_USER_AGENT = `WHOISleuth/${WHOISLEUTH_USER_AGENT_VERSION} (+${WHOISLEUTH_REQUEST_POLICY_URL})`;
|
|
7
6
|
export function whoisleuthRequestHeaders(headers = {}) {
|
|
8
7
|
return { ...headers, 'User-Agent': WHOISLEUTH_USER_AGENT };
|
package/lib/perceptual-hash.mjs
CHANGED
|
@@ -406,18 +406,26 @@ function dHash(image) {
|
|
|
406
406
|
* back to the exact hash.
|
|
407
407
|
*/
|
|
408
408
|
function faviconPerceptualHash(buf) {
|
|
409
|
+
return inspectDecodedImage(buf).perceptualHash;
|
|
410
|
+
}
|
|
411
|
+
function inspectDecodedImage(buf) {
|
|
409
412
|
try {
|
|
410
413
|
const image = decodeImage(buf);
|
|
411
414
|
if (!image)
|
|
412
|
-
return null;
|
|
413
|
-
return
|
|
415
|
+
return Object.freeze({ decodable: false, width: null, height: null, perceptualHash: null });
|
|
416
|
+
return Object.freeze({
|
|
417
|
+
decodable: true,
|
|
418
|
+
width: image.width,
|
|
419
|
+
height: image.height,
|
|
420
|
+
perceptualHash: dHash(image),
|
|
421
|
+
});
|
|
414
422
|
}
|
|
415
423
|
catch {
|
|
416
|
-
return null;
|
|
424
|
+
return Object.freeze({ decodable: false, width: null, height: null, perceptualHash: null });
|
|
417
425
|
}
|
|
418
426
|
}
|
|
419
427
|
// The same bounded decoder is also suitable for fixed-size PNG screenshots
|
|
420
428
|
// produced by the optional local capture package. Keep the generic name as an
|
|
421
429
|
// alias so the evidence contract does not imply that a screenshot is a favicon.
|
|
422
430
|
const imagePerceptualHash = faviconPerceptualHash;
|
|
423
|
-
export { faviconPerceptualHash, hammingDistanceHex, imagePerceptualHash };
|
|
431
|
+
export { faviconPerceptualHash, hammingDistanceHex, imagePerceptualHash, inspectDecodedImage };
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import { WHOISLEUTH_SOURCE_REPOSITORY_URL, } from "./project-metadata.mjs";
|
|
2
|
+
import { normalizeBoundedSemanticVersion } from "./semantic-version.mjs";
|
|
3
|
+
export function buildPortableGeneratorMetadata(applicationVersion) {
|
|
4
|
+
let version = null;
|
|
5
|
+
if (applicationVersion !== null && applicationVersion !== undefined && applicationVersion !== '') {
|
|
6
|
+
try {
|
|
7
|
+
version = normalizeBoundedSemanticVersion(applicationVersion, 'Portable report application');
|
|
8
|
+
}
|
|
9
|
+
catch {
|
|
10
|
+
version = null;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
return Object.freeze({
|
|
14
|
+
name: 'WHOISleuth',
|
|
15
|
+
version,
|
|
16
|
+
projectUrl: WHOISLEUTH_SOURCE_REPOSITORY_URL,
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
export function portableGeneratorAttribution(generator) {
|
|
20
|
+
return `Generated with ${generator.name}${generator.version ? ` ${generator.version}` : ''} · Source: ${generator.projectUrl}`;
|
|
21
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export const PROVIDER_POLICY_MAX_REVIEW_AGE_DAYS = 180;
|
|
2
|
+
export const PROVIDER_POLICY_MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
|
2
3
|
export const DEPLOYMENT_PURPOSES = ['personal', 'internal', 'commercial'];
|
|
3
4
|
function deploymentPurpose(value) {
|
|
4
5
|
const normalized = String(value ?? '').trim().toLowerCase();
|
|
@@ -6,13 +7,17 @@ function deploymentPurpose(value) {
|
|
|
6
7
|
? normalized
|
|
7
8
|
: null;
|
|
8
9
|
}
|
|
10
|
+
export function providerPolicyReviewAgeDays(reviewedAtValue, now) {
|
|
11
|
+
const reviewedAt = typeof reviewedAtValue === 'string' ? Date.parse(reviewedAtValue) : Number.NaN;
|
|
12
|
+
if (!Number.isFinite(reviewedAt) || !Number.isFinite(now) || reviewedAt > now + PROVIDER_POLICY_MAX_FUTURE_SKEW_MS) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
return Math.max(0, Math.floor((now - reviewedAt) / 86_400_000));
|
|
16
|
+
}
|
|
9
17
|
export function providerPolicyAdmission(terms, env = process.env, now = Date.now()) {
|
|
10
18
|
const source = env && typeof env === 'object' ? env : {};
|
|
11
19
|
const purpose = deploymentPurpose(source.WHOISLEUTH_DEPLOYMENT_PURPOSE);
|
|
12
|
-
const
|
|
13
|
-
const reviewAgeDays = Number.isFinite(reviewedAt)
|
|
14
|
-
? Math.max(0, Math.floor((now - reviewedAt) / 86_400_000))
|
|
15
|
-
: null;
|
|
20
|
+
const reviewAgeDays = providerPolicyReviewAgeDays(terms.reviewedAt, now);
|
|
16
21
|
if (!purpose) {
|
|
17
22
|
return {
|
|
18
23
|
allowed: false,
|
|
@@ -564,12 +564,25 @@ function parseRdapObject(type, data) {
|
|
|
564
564
|
: {};
|
|
565
565
|
const v4 = Array.isArray(ipAddresses.v4) ? ipAddresses.v4 : [];
|
|
566
566
|
const v6 = Array.isArray(ipAddresses.v6) ? ipAddresses.v6 : [];
|
|
567
|
-
|
|
567
|
+
const maximumCandidates = 200;
|
|
568
|
+
const maximumAddresses = 20;
|
|
569
|
+
const addresses = [];
|
|
570
|
+
let visited = 0;
|
|
571
|
+
for (const [candidates, family] of [[v4, 4], [v6, 6]]) {
|
|
572
|
+
for (let index = 0; index < candidates.length; index += 1) {
|
|
573
|
+
if (visited >= maximumCandidates || addresses.length >= maximumAddresses)
|
|
574
|
+
break;
|
|
575
|
+
visited += 1;
|
|
576
|
+
const address = boundedString(candidates[index], 80);
|
|
577
|
+
if (address && net.isIP(address) === family)
|
|
578
|
+
addresses.push(address);
|
|
579
|
+
}
|
|
580
|
+
if (visited >= maximumCandidates || addresses.length >= maximumAddresses)
|
|
581
|
+
break;
|
|
582
|
+
}
|
|
583
|
+
if (v4.length + v6.length > visited || v4.length + v6.length > maximumAddresses) {
|
|
568
584
|
nameserverAddressesTruncated = true;
|
|
569
|
-
|
|
570
|
-
...v4.map((address) => boundedString(address, 80)).filter((address) => address && net.isIP(address) === 4),
|
|
571
|
-
...v6.map((address) => boundedString(address, 80)).filter((address) => address && net.isIP(address) === 6),
|
|
572
|
-
].slice(0, 20);
|
|
585
|
+
}
|
|
573
586
|
return {
|
|
574
587
|
name: boundedString(nameserver.ldhName || nameserver.unicodeName, 253),
|
|
575
588
|
addresses,
|
package/lib/rdap-registrar.mjs
CHANGED
|
@@ -85,11 +85,14 @@ export function selectRegistrarRdapLink(domain, links, registryEndpoint = null)
|
|
|
85
85
|
}
|
|
86
86
|
return null;
|
|
87
87
|
}
|
|
88
|
+
function registrarRdapTransport(override, safeDefault = fetchRdapDetailedWithTimeout) {
|
|
89
|
+
return override ?? safeDefault;
|
|
90
|
+
}
|
|
88
91
|
export async function fetchRegistrarRdapRecordWithParser(domain, registryRecord, parseRdap, options = {}) {
|
|
89
92
|
const canonical = canonicalRdapDomain(domain);
|
|
90
93
|
if (!canonical)
|
|
91
94
|
throw new Error('A valid domain is required for registrar RDAP.');
|
|
92
|
-
const fetchUpstream = options.fetchUpstream
|
|
95
|
+
const fetchUpstream = registrarRdapTransport(options.fetchUpstream);
|
|
93
96
|
return cached(`rdap-registrar:domain:${canonical}`, async () => {
|
|
94
97
|
const registryParsed = recordOrNull(registryRecord?.parsed);
|
|
95
98
|
const registryEndpoint = typeof registryRecord?.rdapServer === 'string' ? registryRecord.rdapServer : null;
|
|
@@ -194,3 +197,4 @@ export async function fetchRegistrarRdapRecordWithParser(domain, registryRecord,
|
|
|
194
197
|
};
|
|
195
198
|
});
|
|
196
199
|
}
|
|
200
|
+
export { registrarRdapTransport };
|
|
@@ -86,6 +86,9 @@ function registryAccessDiagnosticFor(value) {
|
|
|
86
86
|
coverageState: capability.coverageState,
|
|
87
87
|
whoisAccessProfile: capability.whoisAccessProfile,
|
|
88
88
|
rdapAccessProfile: capability.rdapAccessProfile,
|
|
89
|
+
...(capability.officialLookupUrl
|
|
90
|
+
? { officialLookupUrl: capability.officialLookupUrl }
|
|
91
|
+
: {}),
|
|
89
92
|
limitation: capability.limitation,
|
|
90
93
|
authority: 'context_only',
|
|
91
94
|
};
|
|
@@ -1,7 +1,7 @@
|
|
|
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 = 29;
|
|
5
5
|
const MAX_CAPABILITY_INPUT_LENGTH = 253;
|
|
6
6
|
const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
|
|
7
7
|
schema: 'whoisleuth.registry-standards-coverage',
|
|
@@ -165,6 +165,7 @@ const DEFAULT_CAPABILITY = freezeCapability({
|
|
|
165
165
|
fixtureScenarios: ['registered', 'not_found', 'rate_limited'],
|
|
166
166
|
verificationFiles: ['fixtures/whois-registry-fixtures.mts'],
|
|
167
167
|
documentationUrls: [],
|
|
168
|
+
officialLookupUrl: null,
|
|
168
169
|
limitation: DISCOVERY_LIMITATION,
|
|
169
170
|
});
|
|
170
171
|
const EXPLICIT_CAPABILITY_SEEDS = [
|
|
@@ -260,6 +261,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
260
261
|
'https://www.iana.org/domains/root/db/ch.html',
|
|
261
262
|
'https://www.iana.org/domains/root/db/li.html',
|
|
262
263
|
],
|
|
264
|
+
officialLookupUrl: 'https://www.nic.ch/whois/',
|
|
263
265
|
limitation: SWITCH_ACCESS_LIMITATION,
|
|
264
266
|
},
|
|
265
267
|
{
|
|
@@ -644,9 +646,11 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
644
646
|
coverageState: 'access_documented', whoisAccessProfile: 'source-ip-authorization-required',
|
|
645
647
|
rdapAccessProfile: 'no-iana-service', verificationFiles: [],
|
|
646
648
|
documentationUrls: [
|
|
649
|
+
'https://www.dominios.es/es',
|
|
647
650
|
'https://www.iana.org/domains/root/db/es.html',
|
|
648
651
|
'https://www.dominios.es/es/sobre-dominios/valores-anadidos/whois-43',
|
|
649
652
|
],
|
|
653
|
+
officialLookupUrl: 'https://www.dominios.es/es',
|
|
650
654
|
limitation: ES_ACCESS_LIMITATION,
|
|
651
655
|
},
|
|
652
656
|
{
|
|
@@ -665,7 +669,11 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
665
669
|
whoisParserProfile: 'generic-colon', fixtureScenarios: [],
|
|
666
670
|
coverageState: 'access_documented', whoisAccessProfile: 'no-iana-service',
|
|
667
671
|
rdapAccessProfile: 'no-iana-service', verificationFiles: [],
|
|
668
|
-
documentationUrls: [
|
|
672
|
+
documentationUrls: [
|
|
673
|
+
'https://www.gt/sitio/',
|
|
674
|
+
'https://www.iana.org/domains/root/db/gt.html',
|
|
675
|
+
],
|
|
676
|
+
officialLookupUrl: 'https://www.gt/sitio/',
|
|
669
677
|
limitation: 'IANA publishes no RDAP service or domain WHOIS service for this suffix. WHOISleuth does not automate the registry website.',
|
|
670
678
|
},
|
|
671
679
|
{
|
|
@@ -1166,6 +1174,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
|
|
|
1166
1174
|
'https://www.iana.org/domains/root/db/vn.html',
|
|
1167
1175
|
'https://whois.vnnic.vn/',
|
|
1168
1176
|
],
|
|
1177
|
+
officialLookupUrl: 'https://whois.vnnic.vn/',
|
|
1169
1178
|
limitation: VN_ACCESS_LIMITATION,
|
|
1170
1179
|
},
|
|
1171
1180
|
];
|
|
@@ -1191,6 +1200,7 @@ const EXPLICIT_CAPABILITIES = EXPLICIT_CAPABILITY_SEEDS.map((entry) => {
|
|
|
1191
1200
|
coverageState: entry.coverageState || 'fixture_verified',
|
|
1192
1201
|
verificationFiles: entry.verificationFiles || ['fixtures/whois-registry-fixtures.mts'],
|
|
1193
1202
|
documentationUrls: entry.documentationUrls || [],
|
|
1203
|
+
officialLookupUrl: entry.officialLookupUrl || null,
|
|
1194
1204
|
limitation: entry.limitation || (rdapAccessProfile === 'no-iana-service'
|
|
1195
1205
|
? WHOIS_ONLY_FIXTURE_LIMITATION
|
|
1196
1206
|
: FIXTURE_LIMITATION),
|
|
@@ -107,10 +107,11 @@ function normalizeRdapPublicationStatus(value) {
|
|
|
107
107
|
function normalizeNameserver(value) {
|
|
108
108
|
return normalizeDomain(value);
|
|
109
109
|
}
|
|
110
|
-
function normalizeSet(values, normalizeItem) {
|
|
110
|
+
function normalizeSet(values, normalizeItem, maximum = 200) {
|
|
111
111
|
if (!Array.isArray(values))
|
|
112
112
|
return [];
|
|
113
|
-
return [...new Set(values.
|
|
113
|
+
return [...new Set(values.slice(0, maximum).map(normalizeItem)
|
|
114
|
+
.filter((value) => Boolean(value)))].sort();
|
|
114
115
|
}
|
|
115
116
|
function sameNormalizedValue(left, right) {
|
|
116
117
|
if (Array.isArray(left) || Array.isArray(right)) {
|
|
@@ -138,14 +139,20 @@ function displayValue(value, state, status) {
|
|
|
138
139
|
return 'Disabled by deployment policy';
|
|
139
140
|
return 'Source unavailable';
|
|
140
141
|
}
|
|
141
|
-
if (Array.isArray(value))
|
|
142
|
-
return value.
|
|
143
|
-
|
|
142
|
+
if (Array.isArray(value)) {
|
|
143
|
+
return value.slice(0, 200)
|
|
144
|
+
.filter((item) => typeof item === 'string')
|
|
145
|
+
.map((item) => item.slice(0, 253))
|
|
146
|
+
.join(', ');
|
|
147
|
+
}
|
|
148
|
+
return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
|
|
149
|
+
? String(value).slice(0, 1_024)
|
|
150
|
+
: 'Not published';
|
|
144
151
|
}
|
|
145
152
|
function findRdapEvent(rdap, action) {
|
|
146
153
|
if (!Array.isArray(rdap.events))
|
|
147
154
|
return null;
|
|
148
|
-
const event = rdap.events.map(record).find((item) => item.action === action);
|
|
155
|
+
const event = rdap.events.slice(0, 100).map(record).find((item) => item.action === action);
|
|
149
156
|
return event?.date || null;
|
|
150
157
|
}
|
|
151
158
|
function rdapLifecycleDate(rdap, field, fallbackAction) {
|
|
@@ -223,8 +230,8 @@ export function compareRegistrySources(rdapParsed, whoisParsed, options = {}) {
|
|
|
223
230
|
dateField('Expires', 'expiryDate', 'expiration'),
|
|
224
231
|
dateField('Last updated', 'updatedDate', 'last changed'),
|
|
225
232
|
compareField('DNSSEC', rdap.dnssec, whois.dnssec, normalizeDnssec, sourceHealth),
|
|
226
|
-
compareField('Statuses', rdap.statuses, whois.statuses, (values) => normalizeSet(values, statusNormalizer), sourceHealth),
|
|
227
|
-
compareField('Name servers', rdap.nameservers, whois.nameservers, (values) => normalizeSet(values, normalizeNameserver), sourceHealth),
|
|
233
|
+
compareField('Statuses', rdap.statuses, whois.statuses, (values) => normalizeSet(values, statusNormalizer, 100), sourceHealth),
|
|
234
|
+
compareField('Name servers', rdap.nameservers, whois.nameservers, (values) => normalizeSet(values, normalizeNameserver, 200), sourceHealth),
|
|
228
235
|
].filter((field) => field !== null);
|
|
229
236
|
const counts = {
|
|
230
237
|
equivalent: 0, conflict: 0, rdap_only: 0, whois_only: 0,
|
|
@@ -265,7 +272,17 @@ export function compareRdapPublications(registryParsed, registrarParsed, options
|
|
|
265
272
|
const registry = record(registryParsed);
|
|
266
273
|
const registrar = record(registrarParsed);
|
|
267
274
|
const registrarLifecycle = record(registrar.lifecycle);
|
|
268
|
-
const comparison = compareRegistrySources({
|
|
275
|
+
const comparison = compareRegistrySources({
|
|
276
|
+
domain: registry.domain,
|
|
277
|
+
registrar: registry.registrar,
|
|
278
|
+
registrarIanaId: registry.registrarIanaId,
|
|
279
|
+
lifecycle: registry.lifecycle,
|
|
280
|
+
events: Array.isArray(registry.events) ? registry.events.slice(0, 100) : [],
|
|
281
|
+
dnssec: registry.dnssec,
|
|
282
|
+
statuses: Array.isArray(registry.statuses) ? registry.statuses.slice(0, 100) : [],
|
|
283
|
+
nameservers: Array.isArray(registry.nameservers) ? registry.nameservers.slice(0, 200) : [],
|
|
284
|
+
handle: null,
|
|
285
|
+
}, {
|
|
269
286
|
domainName: registrar.domain,
|
|
270
287
|
registrar: registrarValue(registrar.registrar),
|
|
271
288
|
registrarIanaId: registrar.registrarIanaId,
|
|
@@ -276,8 +293,8 @@ export function compareRdapPublications(registryParsed, registrarParsed, options
|
|
|
276
293
|
updatedDate: registrarLifecycle.updatedDate,
|
|
277
294
|
updatedDateIso: registrarLifecycle.updatedDateIso,
|
|
278
295
|
dnssec: registrar.dnssec,
|
|
279
|
-
statuses: registrar.statuses,
|
|
280
|
-
nameservers: registrar.nameservers,
|
|
296
|
+
statuses: Array.isArray(registrar.statuses) ? registrar.statuses.slice(0, 100) : [],
|
|
297
|
+
nameservers: Array.isArray(registrar.nameservers) ? registrar.nameservers.slice(0, 200) : [],
|
|
281
298
|
}, {
|
|
282
299
|
rdapStatus: options.registryStatus,
|
|
283
300
|
whoisStatus: options.registrarStatus,
|