@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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 +7 -3
- package/NOTICE +5 -4
- package/README.md +74 -124
- package/SECURITY.md +6 -3
- package/bin/whoisleuth.mjs +7 -4
- package/cli/archive-inspect.mjs +28 -10
- package/cli/arguments.mjs +450 -173
- package/cli/artifact-structure.mjs +2507 -0
- package/cli/artifact-verify.mjs +286 -64
- package/cli/bounded-json.mjs +1 -0
- package/cli/bulk-checkpoint.mjs +35 -23
- package/cli/bulk-command-runner.mjs +2 -5
- package/cli/bulk.mjs +9 -2
- package/cli/case-pack.mjs +435 -46
- package/cli/ci-report.mjs +6 -1
- package/cli/collection-preflight.mjs +2 -2
- package/cli/command-catalogue.mjs +12 -3
- package/cli/command-reference.mjs +948 -106
- package/cli/completion.mjs +504 -218
- package/cli/config-profile.mjs +13 -3
- package/cli/ct-event-intake.mjs +32 -2
- package/cli/discover.mjs +7 -1
- package/cli/discovery-observation-snapshot.mjs +24 -28
- package/cli/discovery-scan-command-runner.mjs +13 -14
- package/cli/discovery-scan.mjs +17 -3
- package/cli/discovery-snapshot.mjs +15 -7
- package/cli/discovery-workflow.mjs +2 -3
- package/cli/doctor.mjs +2 -2
- package/cli/domain-control-monitor.mjs +215 -36
- package/cli/domain-control-observations.mjs +34 -17
- package/cli/errors.mjs +8 -3
- package/cli/evidence-command-runner.mjs +17 -11
- package/cli/evidence-signing.mjs +68 -50
- package/cli/export-evidence.mjs +11 -4
- package/cli/external-observation-mapping.mjs +3 -3
- package/cli/formatters/html.mjs +3 -3
- package/cli/formatters/json.mjs +20 -32
- package/cli/formatters/terminal.mjs +586 -22
- package/cli/input.mjs +62 -0
- package/cli/interactive-launcher.mjs +226 -0
- package/cli/interchange-report.mjs +226 -0
- package/cli/investigation-manifest.mjs +16 -17
- package/cli/investigation-plan.mjs +167 -16
- package/cli/investigation-run.mjs +80 -24
- package/cli/local-mmdb-review.mjs +45 -14
- package/cli/lookalike-calibration.mjs +3 -1
- package/cli/lookup-brief.mjs +13 -8
- package/cli/lookup-browser.mjs +844 -0
- package/cli/lookup-command-runner.mjs +96 -33
- package/cli/lookup-diff.mjs +11 -2
- package/cli/lookup-plan.mjs +1 -0
- package/cli/mail-review.mjs +206 -94
- package/cli/manual.mjs +5 -5
- package/cli/network-command-runner.mjs +78 -0
- package/cli/offline-evidence-review.mjs +45 -14
- package/cli/open-asset-model-bridge.mjs +1 -1
- package/cli/output-file.mjs +70 -14
- package/cli/page-compare.mjs +18 -10
- package/cli/progress-events.mjs +2 -1
- package/cli/progress.mjs +3 -3
- package/cli/registry-cohort.mjs +343 -61
- package/cli/registry-doctor.mjs +67 -28
- package/cli/registry-support.mjs +2 -1
- package/cli/retained-artifact-diff.mjs +375 -0
- package/cli/risk-calibration.mjs +89 -40
- package/cli/runner.mjs +322 -246
- package/cli/saved-lookup.mjs +439 -49
- package/cli/sharing-review.mjs +18 -7
- package/cli/source-reliability.mjs +75 -36
- package/cli/terminal-presentation.mjs +43 -18
- package/docs/cli-reference.md +125 -1334
- package/docs/cli.md +87 -236
- package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
- package/frontend/src/lib/analysis/domain-control-records.js +3 -0
- package/lib/application-version.mjs +3 -0
- package/lib/availability.mjs +72 -52
- package/lib/bounded-contract-normalizers.mjs +2 -4
- package/lib/bounded-file.mjs +91 -4
- package/lib/bounded-json.mjs +287 -0
- package/lib/browser-library-profile.mjs +1 -1
- package/lib/certificate-extension-profile.mjs +2 -1
- package/lib/classify.mjs +56 -5
- package/lib/client-behavior-profile.mjs +1 -1
- package/lib/credential-surface-profile.mjs +1 -1
- package/lib/cryptographic-assurance.mjs +173 -0
- package/lib/ct-query.mjs +3 -3
- package/lib/ct-search.mjs +120 -64
- package/lib/distributed-operation-budget.mjs +4 -2
- package/lib/dns-delegation-health.mjs +9 -6
- package/lib/dns-intelligence.mjs +4 -3
- package/lib/dns-resolver-selection.mjs +6 -4
- package/lib/dnssec-chain-validation.mjs +1052 -0
- package/lib/dnssec-evidence-validation.mjs +23 -10
- package/lib/domain-assurance.mjs +12 -12
- package/lib/domain-change-packet.mjs +7 -5
- package/lib/domain-control-flight-recorder.mjs +442 -78
- package/lib/domain-control-manifest.mjs +314 -171
- package/lib/domain-posture-analysis.mjs +5 -2
- package/lib/domain-posture.mjs +7 -4
- package/lib/encrypted-dns-contract.mjs +2 -3
- package/lib/evidence-export.mjs +1243 -88
- package/lib/evidence-report-markdown.mjs +3 -0
- package/lib/evidence-report.mjs +5 -0
- package/lib/external-intelligence-risk.mjs +26 -10
- package/lib/favicon.mjs +8 -5
- package/lib/feature-policy.mjs +1 -1
- package/lib/homepage-metadata-contract.mjs +290 -0
- package/lib/hostname.mjs +1 -1
- package/lib/html-signals.mjs +89 -5
- package/lib/http-evidence-bounds.mjs +3 -0
- package/lib/http-intelligence.mjs +332 -6
- package/lib/interchange-fidelity-registry.mjs +124 -0
- package/lib/local-geoip-evidence.mjs +2 -1
- package/lib/lookup-cache.mjs +6 -4
- package/lib/lookup-network-evidence-bounds.mjs +9 -0
- package/lib/lookup.mjs +27 -4
- package/lib/observed-network-context.mjs +1 -1
- package/lib/outbound-identity.mjs +2 -3
- package/lib/page-fingerprints.mjs +2 -2
- package/lib/page-role-profile.mjs +1 -1
- package/lib/perceptual-hash.mjs +12 -4
- package/lib/portable-generator.mjs +29 -0
- package/lib/provider-policy-admission.mjs +11 -4
- package/lib/rdap-bootstrap.mjs +76 -16
- package/lib/rdap-client.mjs +20 -12
- package/lib/rdap-normalization.mjs +324 -80
- package/lib/rdap-registrar.mjs +5 -1
- package/lib/rdap-transport.mjs +2 -2
- package/lib/rdap-validation.mjs +65 -14
- package/lib/rdap.mjs +2 -2
- package/lib/registrable-domain.mjs +36 -0
- package/lib/registry-capabilities.mjs +3 -0
- package/lib/registry-capability-catalogue.mjs +12 -2
- package/lib/registry-comparison.mjs +28 -11
- package/lib/registry-insights.mjs +7 -0
- package/lib/response-policy.mjs +27 -4
- package/lib/risk-calibration-summary.mjs +340 -0
- package/lib/risk-scoring.mjs +4 -3
- package/lib/rpki-evidence.mjs +9 -8
- package/lib/safe-fetch.mjs +121 -50
- package/lib/security-txt.mjs +27 -4
- package/lib/service-binding-dns.mjs +6 -1
- package/lib/smtp-transport-review.mjs +787 -0
- package/lib/sslbl-certificates.generated.mjs +136 -134
- package/lib/sslbl-intelligence.mjs +6 -8
- package/lib/static-html-analysis.mjs +301 -4
- package/lib/structured-data-identity.mjs +51 -19
- package/lib/threat-intelligence-fixtures.mjs +2 -4
- package/lib/threat-intelligence-manifests.mjs +2 -7
- package/lib/threat-intelligence-runtime.mjs +5 -15
- package/lib/threat-intelligence-types.mjs +21 -1
- package/lib/threatfox-intelligence.mjs +7 -3
- package/lib/tls-intelligence.mjs +15 -9
- package/lib/tlsa-evidence.mjs +37 -7
- package/lib/urlhaus-intelligence.mjs +4 -2
- package/lib/urlscan-intelligence.mjs +6 -5
- package/lib/website-security-posture.mjs +1 -1
- package/lib/website-technology.mjs +1 -1
- package/lib/whois-authority.mjs +43 -8
- package/lib/whois-chain.mjs +25 -3
- package/lib/whois-normalization.mjs +3 -1
- package/lib/whois-transport.mjs +12 -3
- package/lib/zone-intent-review.mjs +1 -1
- package/package.json +12 -4
- package/packages/cases/case-brand-profile-references.mjs +85 -0
- package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
- package/packages/cases/case-investigation-branch-model.mjs +132 -0
- package/packages/cases/case-migration-model.mjs +476 -0
- package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
- package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
- package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
- package/packages/cases/case-record-model.mjs +9 -0
- package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
- package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
- package/packages/cases/case-response-model.mjs +1604 -0
- package/packages/cases/case-response-packet.mjs +1312 -0
- package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
- package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
- package/packages/cases/opaque-reference-id.mjs +9 -0
- package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
- package/packages/comparison/comparison-ledger-contract.mjs +241 -0
- package/packages/comparison/comparison-ledger-json.mjs +80 -0
- package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
- package/packages/comparison/favicon-similarity.mjs +78 -0
- package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
- package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
- package/packages/contracts/analyst-review-state-contract.mjs +61 -0
- package/packages/contracts/analyst-review-state.mjs +50 -0
- package/packages/contracts/case-portability.mjs +1310 -0
- package/packages/contracts/cli-command-catalogue.mjs +2 -0
- package/packages/contracts/cli-lookup.mjs +394 -0
- package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
- package/packages/contracts/domain-control-manifest.mjs +1027 -0
- package/packages/contracts/domain-control-monitor.mjs +310 -0
- package/packages/contracts/domain-control-review.mjs +828 -0
- package/packages/contracts/domain-name.mjs +4 -0
- package/packages/contracts/external-observation-interchange.mjs +224 -0
- package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
- package/packages/contracts/investigation-portability.mjs +331 -0
- package/packages/contracts/offline-comparison.mjs +160 -0
- package/packages/contracts/risk-calibration.mjs +1288 -0
- package/packages/contracts/schema-compatibility.mjs +71 -0
- package/packages/contracts/schema-lifecycle.mjs +1702 -0
- package/packages/contracts/sslbl-snapshot.mjs +17 -0
- package/packages/contracts/workspace-portability.mjs +1347 -0
- package/packages/evidence/artifact-integrity.mjs +65 -0
- package/packages/evidence/decision-fact.mjs +735 -0
- package/packages/evidence/domain-control-runtime.mjs +547 -0
- package/packages/evidence/domain-name.mjs +41 -0
- package/packages/evidence/observation.mjs +384 -0
- package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
- package/packages/interchange/retained-text.mjs +8 -0
- package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
- package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
- package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
- package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
- package/packages/investigation/bulk-review-policy.mjs +1 -0
- package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
- package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
- package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
- package/packages/investigation/lookup-claim-passport.mjs +232 -0
- package/packages/investigation/lookup-claim-readiness.mjs +287 -0
- package/packages/investigation/lookup-investigation-brief.mjs +172 -0
- package/packages/monitoring/analyst-review-state.mjs +680 -0
- package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
- package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
- package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
- package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
- package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
- package/packages/workspace/hostile-input.mjs +172 -0
- package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
- package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
- package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
- package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
- package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
- package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
- package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
- package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
- package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
- package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
- package/third-party-notices.txt +112 -5834
- package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
- package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
- package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
- package/frontend/src/lib/analysis/case-migration-model.js +0 -253
- package/frontend/src/lib/analysis/case-record-model.js +0 -7
- package/frontend/src/lib/analysis/case-response-model.js +0 -669
- package/frontend/src/lib/analysis/case-response-packet.js +0 -573
- package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
- package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
- package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
- package/frontend/src/lib/analysis/lookup-response.js +0 -2
- package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
- package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
- package/frontend/src/lib/analysis/scoring.js +0 -55
- package/frontend/src/lib/analysis/utils.js +0 -232
- package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
- package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
- package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
- package/frontend/src/lib/analysis/visualization-models.js +0 -369
- package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
- package/lib/bounded-json-response.mjs +0 -157
- package/lib/lookup-response-contract.mjs +0 -346
- package/lib/observation.mjs +0 -114
- /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
- /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
- /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
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 };
|
package/lib/rdap-transport.mjs
CHANGED
|
@@ -8,7 +8,7 @@ async function fetchRdapWithTimeout(url, options, timeoutMs, dependencies = {})
|
|
|
8
8
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
9
9
|
try {
|
|
10
10
|
const response = await (dependencies.fetch ?? safeFetch)(url, { ...options, signal: controller.signal });
|
|
11
|
-
const { text, truncated } = await (dependencies.readText ?? readTextCapped)(response, MAX_RDAP_BYTES);
|
|
11
|
+
const { text, truncated } = await (dependencies.readText ?? readTextCapped)(response, MAX_RDAP_BYTES, { fatalUtf8: true });
|
|
12
12
|
if (truncated)
|
|
13
13
|
throw new Error(`Response from ${url} exceeded ${MAX_RDAP_BYTES} bytes`);
|
|
14
14
|
return { status: response.status, ok: response.ok, text };
|
|
@@ -22,7 +22,7 @@ async function fetchRdapDetailedWithTimeout(url, options, timeoutMs, dependencie
|
|
|
22
22
|
const timeout = setTimeout(() => controller.abort(), timeoutMs);
|
|
23
23
|
try {
|
|
24
24
|
const result = await (dependencies.fetchDetailed ?? safeFetchDetailed)(url, { ...options, signal: controller.signal });
|
|
25
|
-
const { text, truncated } = await (dependencies.readText ?? readTextCapped)(result.response, MAX_RDAP_BYTES);
|
|
25
|
+
const { text, truncated } = await (dependencies.readText ?? readTextCapped)(result.response, MAX_RDAP_BYTES, { fatalUtf8: true });
|
|
26
26
|
if (truncated)
|
|
27
27
|
throw new Error(`Response from ${url} exceeded ${MAX_RDAP_BYTES} bytes`);
|
|
28
28
|
return {
|
package/lib/rdap-validation.mjs
CHANGED
|
@@ -1,24 +1,75 @@
|
|
|
1
|
-
import
|
|
1
|
+
import { isIP } from 'node:net';
|
|
2
2
|
import { domainToASCII } from 'node:url';
|
|
3
|
-
import { ipv4ToLong, ipv6ToBigInt } from "./rdap-bootstrap.mjs";
|
|
3
|
+
import { admitRdapEndpoint, ipv4ToLong, ipv6ToBigInt } from "./rdap-bootstrap.mjs";
|
|
4
4
|
export function canonicalRdapDomain(value) {
|
|
5
5
|
if (typeof value !== 'string')
|
|
6
6
|
return null;
|
|
7
7
|
const ascii = domainToASCII(value.trim().replace(/\.+$/, ''));
|
|
8
8
|
return ascii ? ascii.toLowerCase() : null;
|
|
9
9
|
}
|
|
10
|
-
function
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const embedded = ip.slice(lastColon + 1);
|
|
15
|
-
if (net.isIP(embedded) !== 4)
|
|
10
|
+
function decodedTerminalPathParts(value) {
|
|
11
|
+
try {
|
|
12
|
+
const parts = new URL(value).pathname.split('/').filter(Boolean);
|
|
13
|
+
if (parts.length < 2)
|
|
16
14
|
return null;
|
|
17
|
-
const
|
|
18
|
-
|
|
15
|
+
const objectType = decodeURIComponent(parts.at(-2) ?? '');
|
|
16
|
+
const objectValue = decodeURIComponent(parts.at(-1) ?? '');
|
|
17
|
+
return objectType && objectValue ? [objectType, objectValue] : null;
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Admit final RDAP provenance only when its terminal path still identifies the
|
|
25
|
+
* requested object. Safe redirect transport proves where bytes came from; this
|
|
26
|
+
* binding separately prevents a redirected response for another object from
|
|
27
|
+
* deciding a lookup, especially when a bodyless 404 is otherwise authoritative.
|
|
28
|
+
*/
|
|
29
|
+
export function admitRdapObjectEndpoint(type, requestedValue, value) {
|
|
30
|
+
const admitted = admitRdapEndpoint(value);
|
|
31
|
+
if (!admitted)
|
|
32
|
+
return null;
|
|
33
|
+
const terminal = decodedTerminalPathParts(admitted);
|
|
34
|
+
if (!terminal)
|
|
35
|
+
return null;
|
|
36
|
+
const [objectType, objectValue] = terminal;
|
|
37
|
+
if (type === 'domain') {
|
|
38
|
+
const requested = canonicalRdapDomain(requestedValue);
|
|
39
|
+
const selected = canonicalRdapDomain(objectValue);
|
|
40
|
+
return objectType === 'domain' && requested && selected === requested ? admitted : null;
|
|
19
41
|
}
|
|
42
|
+
if (type === 'ipv4' || type === 'ipv6') {
|
|
43
|
+
const version = type === 'ipv4' ? 4 : 6;
|
|
44
|
+
if (objectType !== 'ip' || isIP(requestedValue) !== version || isIP(objectValue) !== version)
|
|
45
|
+
return null;
|
|
46
|
+
try {
|
|
47
|
+
const convert = version === 4 ? ipv4ToLong : ipv6ToBigInt;
|
|
48
|
+
return convert(requestedValue) === convert(objectValue) ? admitted : null;
|
|
49
|
+
}
|
|
50
|
+
catch {
|
|
51
|
+
return null;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
if (type === 'asn') {
|
|
55
|
+
const requestedMatch = /^(?:AS)?([0-9]{1,10})$/iu.exec(requestedValue);
|
|
56
|
+
if (objectType !== 'autnum' || !requestedMatch || !/^[0-9]{1,10}$/u.test(objectValue))
|
|
57
|
+
return null;
|
|
58
|
+
const requested = Number(requestedMatch[1]);
|
|
59
|
+
const selected = Number(objectValue);
|
|
60
|
+
return Number.isSafeInteger(requested)
|
|
61
|
+
&& requested >= 0
|
|
62
|
+
&& requested <= 4_294_967_295
|
|
63
|
+
&& selected === requested
|
|
64
|
+
&& objectValue === String(selected)
|
|
65
|
+
? admitted
|
|
66
|
+
: null;
|
|
67
|
+
}
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
function ipv6ToComparableBigInt(ip) {
|
|
20
71
|
try {
|
|
21
|
-
return ipv6ToBigInt(
|
|
72
|
+
return ipv6ToBigInt(ip);
|
|
22
73
|
}
|
|
23
74
|
catch {
|
|
24
75
|
return null;
|
|
@@ -71,11 +122,11 @@ export function validateRdapResponse(type, requestedValue, parsed) {
|
|
|
71
122
|
const version = type === 'ipv4' ? 4 : 6;
|
|
72
123
|
const startAddress = parsed.startAddress;
|
|
73
124
|
const endAddress = parsed.endAddress;
|
|
74
|
-
if (
|
|
125
|
+
if (isIP(requestedValue) !== version ||
|
|
75
126
|
typeof startAddress !== 'string' ||
|
|
76
|
-
|
|
127
|
+
isIP(startAddress) !== version ||
|
|
77
128
|
typeof endAddress !== 'string' ||
|
|
78
|
-
|
|
129
|
+
isIP(endAddress) !== version) {
|
|
79
130
|
return { valid: false, detail: 'The response did not contain a compatible IP range.' };
|
|
80
131
|
}
|
|
81
132
|
const convert = version === 4 ? ipv4ToLong : ipv6ToComparableBigInt;
|
package/lib/rdap.mjs
CHANGED
|
@@ -3,9 +3,9 @@ import { BOOTSTRAP_STALE_TTL_MS, BOOTSTRAP_TTL_MS, clearRdapBootstrapCache, fetc
|
|
|
3
3
|
import { fetchRdapFromBasesWithParser, fetchRdapRecordWithParser, } from "./rdap-client.mjs";
|
|
4
4
|
import { normalizeRdapEvents, parseRdap, summarizeLifecycle, } from "./rdap-normalization.mjs";
|
|
5
5
|
import { fetchRegistrarRdapRecordWithParser, selectRegistrarRdapLink, } from "./rdap-registrar.mjs";
|
|
6
|
-
import {
|
|
6
|
+
import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
|
|
7
7
|
import { registryServiceAdmissionFor } from "./registry-capabilities.mjs";
|
|
8
|
-
async function fetchRdapFromBases(type, value, bases, fetchUpstream =
|
|
8
|
+
async function fetchRdapFromBases(type, value, bases, fetchUpstream = fetchRdapDetailedWithTimeout) {
|
|
9
9
|
return fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream);
|
|
10
10
|
}
|
|
11
11
|
async function fetchRdapRecord(type, value, options = {}) {
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { getDomain } from 'tldts';
|
|
2
|
+
import { isValidAsciiDomainName } from "./hostname.mjs";
|
|
3
|
+
// Returns one canonical registrable-domain identity for an already public DNS
|
|
4
|
+
// hostname. Callers retain the original hostname separately when subdomain
|
|
5
|
+
// context matters; authority, evidence binding and scoring use this value.
|
|
6
|
+
export function canonicalRegistrableDomain(value) {
|
|
7
|
+
if (typeof value !== 'string')
|
|
8
|
+
return null;
|
|
9
|
+
const trimmed = value.trim();
|
|
10
|
+
if (!trimmed || /[\u0000-\u001f\u007f]/u.test(trimmed))
|
|
11
|
+
return null;
|
|
12
|
+
const withoutScheme = trimmed.replace(/^[a-z]+:\/\//iu, '').split(/[/?#]/u)[0] ?? '';
|
|
13
|
+
if (withoutScheme.endsWith('..'))
|
|
14
|
+
return null;
|
|
15
|
+
const candidate = withoutScheme.replace(/\.$/u, '');
|
|
16
|
+
if (!candidate)
|
|
17
|
+
return null;
|
|
18
|
+
let hostname;
|
|
19
|
+
try {
|
|
20
|
+
hostname = new URL(`https://${candidate}/`).hostname.toLowerCase().replace(/\.$/u, '');
|
|
21
|
+
}
|
|
22
|
+
catch {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
if (!isValidAsciiDomainName(hostname, { requireDot: true, requireLowercase: true }))
|
|
26
|
+
return null;
|
|
27
|
+
// Keep this boundary identical to classifyQuery() and the collection
|
|
28
|
+
// runtime: registrable identity follows the ICANN Public Suffix List, while
|
|
29
|
+
// privately operated suffixes remain ordinary host context.
|
|
30
|
+
const domain = getDomain(hostname);
|
|
31
|
+
return domain
|
|
32
|
+
&& isValidAsciiDomainName(domain, { requireDot: true, requireLowercase: true })
|
|
33
|
+
&& (hostname === domain || hostname.endsWith(`.${domain}`))
|
|
34
|
+
? domain
|
|
35
|
+
: null;
|
|
36
|
+
}
|
|
@@ -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,
|
|
@@ -87,6 +87,13 @@ function disclosure(source, parsedValue, status) {
|
|
|
87
87
|
};
|
|
88
88
|
}
|
|
89
89
|
const parsed = record(parsedValue);
|
|
90
|
+
if (parsed.contactsExcluded === true) {
|
|
91
|
+
return {
|
|
92
|
+
source,
|
|
93
|
+
state: 'unavailable',
|
|
94
|
+
detail: 'Contact fields are deliberately excluded from this portable projection, so publication disclosure remains unavailable.',
|
|
95
|
+
};
|
|
96
|
+
}
|
|
90
97
|
const partial = source === 'whois'
|
|
91
98
|
? parsed.chainStatus === 'partial' || strings(parsed.fieldsTruncated, 20).length > 0
|
|
92
99
|
: parsed.serverTruncated === true
|
package/lib/response-policy.mjs
CHANGED
|
@@ -184,9 +184,23 @@ function analyzeHsts(header, signals) {
|
|
|
184
184
|
const directives = header.value.split(';').map((part) => part.trim()).filter(Boolean);
|
|
185
185
|
if (directives.length > MAX_RESPONSE_POLICY_DIRECTIVES)
|
|
186
186
|
return 'partial';
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
const seen = new Set();
|
|
188
|
+
let maxAge = null;
|
|
189
|
+
for (const directive of directives) {
|
|
190
|
+
const match = /^([a-z][a-z0-9-]{0,63})(?:\s*=\s*(.*))?$/iu.exec(directive);
|
|
191
|
+
if (!match)
|
|
192
|
+
return 'malformed';
|
|
193
|
+
const name = match[1]?.toLowerCase() ?? '';
|
|
194
|
+
if (seen.has(name))
|
|
195
|
+
return 'malformed';
|
|
196
|
+
seen.add(name);
|
|
197
|
+
if (name === 'max-age') {
|
|
198
|
+
const value = match[2];
|
|
199
|
+
if (!value || !/^\d+$/u.test(value))
|
|
200
|
+
return 'malformed';
|
|
201
|
+
maxAge = value;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
190
204
|
if (!maxAge || maxAge.length > 12)
|
|
191
205
|
return 'malformed';
|
|
192
206
|
const seconds = Number(maxAge);
|
|
@@ -256,10 +270,15 @@ function analyzeCookies(headers, signals) {
|
|
|
256
270
|
let missingHttpOnly = 0;
|
|
257
271
|
let missingSameSite = 0;
|
|
258
272
|
let sameSiteNoneWithoutSecure = 0;
|
|
273
|
+
let attributesTruncated = false;
|
|
259
274
|
for (const value of result.values) {
|
|
260
275
|
const segments = value.split(';');
|
|
261
276
|
if (!segments[0]?.includes('='))
|
|
262
277
|
return { state: 'malformed', count: result.values.length, truncated: result.truncated };
|
|
278
|
+
if (segments.length - 1 > MAX_RESPONSE_POLICY_TOKENS) {
|
|
279
|
+
attributesTruncated = true;
|
|
280
|
+
continue;
|
|
281
|
+
}
|
|
263
282
|
const attributes = new Map();
|
|
264
283
|
for (const rawAttribute of segments.slice(1, MAX_RESPONSE_POLICY_TOKENS + 1)) {
|
|
265
284
|
const [rawName = '', ...rawValue] = rawAttribute.trim().split('=');
|
|
@@ -289,7 +308,11 @@ function analyzeCookies(headers, signals) {
|
|
|
289
308
|
if (sameSiteNoneWithoutSecure) {
|
|
290
309
|
addSignal(signals, 'cookies_same_site_none_without_secure', sameSiteNoneWithoutSecure);
|
|
291
310
|
}
|
|
292
|
-
return {
|
|
311
|
+
return {
|
|
312
|
+
state: result.state === 'partial' || attributesTruncated ? 'partial' : result.state,
|
|
313
|
+
count: result.values.length,
|
|
314
|
+
truncated: result.truncated || attributesTruncated,
|
|
315
|
+
};
|
|
293
316
|
}
|
|
294
317
|
export function analyzeResponsePolicyHeaders(headers) {
|
|
295
318
|
const signals = [];
|