@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/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,29 @@
|
|
|
1
|
+
import { WHOISLEUTH_SOURCE_REPOSITORY_URL, } from "./project-metadata.mjs";
|
|
2
|
+
import { normalizeBoundedSemanticVersion } from "./semantic-version.mjs";
|
|
3
|
+
const URI_LABEL_UNSAFE_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
|
|
4
|
+
const URI_LABEL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/|[^\s])/u;
|
|
5
|
+
export function isUriShapedLabel(value) {
|
|
6
|
+
if (typeof value !== 'string')
|
|
7
|
+
return false;
|
|
8
|
+
const candidate = value.replace(URI_LABEL_UNSAFE_GLOBAL_RE, '').trim();
|
|
9
|
+
return URI_LABEL_SCHEME_RE.test(candidate);
|
|
10
|
+
}
|
|
11
|
+
export function buildPortableGeneratorMetadata(applicationVersion) {
|
|
12
|
+
let version = null;
|
|
13
|
+
if (applicationVersion !== null && applicationVersion !== undefined && applicationVersion !== '') {
|
|
14
|
+
try {
|
|
15
|
+
version = normalizeBoundedSemanticVersion(applicationVersion, 'Portable report application');
|
|
16
|
+
}
|
|
17
|
+
catch {
|
|
18
|
+
version = null;
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
return Object.freeze({
|
|
22
|
+
name: 'WHOISleuth',
|
|
23
|
+
version,
|
|
24
|
+
projectUrl: WHOISLEUTH_SOURCE_REPOSITORY_URL,
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
export function portableGeneratorAttribution(generator) {
|
|
28
|
+
return `Generated with ${generator.name}${generator.version ? ` ${generator.version}` : ''} · Source: ${generator.projectUrl}`;
|
|
29
|
+
}
|
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
|
|
1
2
|
export const PROVIDER_POLICY_MAX_REVIEW_AGE_DAYS = 180;
|
|
3
|
+
export const PROVIDER_POLICY_MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
|
|
2
4
|
export const DEPLOYMENT_PURPOSES = ['personal', 'internal', 'commercial'];
|
|
3
5
|
function deploymentPurpose(value) {
|
|
4
6
|
const normalized = String(value ?? '').trim().toLowerCase();
|
|
@@ -6,13 +8,18 @@ function deploymentPurpose(value) {
|
|
|
6
8
|
? normalized
|
|
7
9
|
: null;
|
|
8
10
|
}
|
|
11
|
+
export function providerPolicyReviewAgeDays(reviewedAtValue, now) {
|
|
12
|
+
const normalizedReviewedAt = normalizeLegacyIsoTimestamp(reviewedAtValue);
|
|
13
|
+
const reviewedAt = normalizedReviewedAt ? Date.parse(normalizedReviewedAt) : Number.NaN;
|
|
14
|
+
if (!Number.isFinite(reviewedAt) || !Number.isFinite(now) || reviewedAt > now + PROVIDER_POLICY_MAX_FUTURE_SKEW_MS) {
|
|
15
|
+
return null;
|
|
16
|
+
}
|
|
17
|
+
return Math.max(0, Math.floor((now - reviewedAt) / 86_400_000));
|
|
18
|
+
}
|
|
9
19
|
export function providerPolicyAdmission(terms, env = process.env, now = Date.now()) {
|
|
10
20
|
const source = env && typeof env === 'object' ? env : {};
|
|
11
21
|
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;
|
|
22
|
+
const reviewAgeDays = providerPolicyReviewAgeDays(terms.reviewedAt, now);
|
|
16
23
|
if (!purpose) {
|
|
17
24
|
return {
|
|
18
25
|
allowed: false,
|
package/lib/rdap-bootstrap.mjs
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// IANA RDAP bootstrap retrieval, bounded stale caching, and authority
|
|
2
2
|
// selection for domain, IP, and ASN queries.
|
|
3
|
+
import net from 'node:net';
|
|
3
4
|
import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
|
|
4
5
|
const BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
|
|
5
6
|
const BOOTSTRAP_STALE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
|
|
@@ -8,29 +9,71 @@ const BOOTSTRAP_FETCH_TIMEOUT_MS = 7000;
|
|
|
8
9
|
const MAX_RDAP_ENDPOINT_LENGTH = 2048;
|
|
9
10
|
const bootstrapCache = new Map();
|
|
10
11
|
const bootstrapInflight = new Map();
|
|
12
|
+
function admitRdapEndpoint(value, options = {}) {
|
|
13
|
+
if (typeof value !== 'string'
|
|
14
|
+
|| !value
|
|
15
|
+
|| value !== value.trim()
|
|
16
|
+
|| value.length > MAX_RDAP_ENDPOINT_LENGTH
|
|
17
|
+
|| /[\u0000-\u001f\u007f]/u.test(value))
|
|
18
|
+
return null;
|
|
19
|
+
try {
|
|
20
|
+
const parsed = new URL(value);
|
|
21
|
+
if (!['http:', 'https:'].includes(parsed.protocol)
|
|
22
|
+
|| !parsed.hostname
|
|
23
|
+
|| parsed.username
|
|
24
|
+
|| parsed.password
|
|
25
|
+
|| parsed.port
|
|
26
|
+
|| parsed.hash
|
|
27
|
+
|| (!options.allowQuery && parsed.search))
|
|
28
|
+
return null;
|
|
29
|
+
return value;
|
|
30
|
+
}
|
|
31
|
+
catch {
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
11
35
|
function ipv4ToLong(ip) {
|
|
12
36
|
return ip.split('.').reduce((accumulator, octet) => (accumulator << 8) + (parseInt(octet, 10) & 0xff), 0) >>> 0;
|
|
13
37
|
}
|
|
14
38
|
function ipInCidrV4(ip, cidr) {
|
|
15
|
-
const
|
|
16
|
-
if (
|
|
39
|
+
const parts = cidr.split('/');
|
|
40
|
+
if (parts.length > 2)
|
|
41
|
+
return false;
|
|
42
|
+
const [range, bitsString] = parts;
|
|
43
|
+
if (!range || net.isIP(ip) !== 4 || net.isIP(range) !== 4)
|
|
44
|
+
return false;
|
|
45
|
+
const bits = bitsString === undefined || /^\d{1,2}$/u.test(bitsString)
|
|
46
|
+
? Number(bitsString ?? 32)
|
|
47
|
+
: -1;
|
|
48
|
+
if (!Number.isSafeInteger(bits) || bits < 0 || bits > 32)
|
|
17
49
|
return false;
|
|
18
|
-
const bits = bitsString !== undefined ? parseInt(bitsString, 10) : 32;
|
|
19
50
|
const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
|
|
20
51
|
return (ipv4ToLong(ip) & mask) === (ipv4ToLong(range) & mask);
|
|
21
52
|
}
|
|
22
53
|
function expandIpv6(ip) {
|
|
23
|
-
let
|
|
54
|
+
let normalized = ip;
|
|
55
|
+
if (normalized.includes('.')) {
|
|
56
|
+
const lastColon = normalized.lastIndexOf(':');
|
|
57
|
+
const embedded = normalized.slice(lastColon + 1);
|
|
58
|
+
if (lastColon < 0 || net.isIP(embedded) !== 4) {
|
|
59
|
+
throw new TypeError('Invalid IPv4-embedded IPv6 address.');
|
|
60
|
+
}
|
|
61
|
+
const value = ipv4ToLong(embedded);
|
|
62
|
+
normalized = `${normalized.slice(0, lastColon)}:${(value >>> 16).toString(16)}:${(value & 0xffff).toString(16)}`;
|
|
63
|
+
}
|
|
64
|
+
if (net.isIP(normalized) !== 6)
|
|
65
|
+
throw new TypeError('Invalid IPv6 address.');
|
|
66
|
+
let head = normalized;
|
|
24
67
|
let tail = '';
|
|
25
|
-
if (
|
|
26
|
-
const [headPart = '', tailPart = ''] =
|
|
68
|
+
if (normalized.includes('::')) {
|
|
69
|
+
const [headPart = '', tailPart = ''] = normalized.split('::');
|
|
27
70
|
head = headPart;
|
|
28
71
|
tail = tailPart;
|
|
29
72
|
}
|
|
30
73
|
const headParts = head ? head.split(':').filter(Boolean) : [];
|
|
31
74
|
const tailParts = tail ? tail.split(':').filter(Boolean) : [];
|
|
32
75
|
const missing = 8 - headParts.length - tailParts.length;
|
|
33
|
-
const parts =
|
|
76
|
+
const parts = normalized.includes('::')
|
|
34
77
|
? [...headParts, ...Array(Math.max(missing, 0)).fill('0'), ...tailParts]
|
|
35
78
|
: headParts;
|
|
36
79
|
while (parts.length < 8)
|
|
@@ -41,13 +84,25 @@ function ipv6ToBigInt(ip) {
|
|
|
41
84
|
return expandIpv6(ip).reduce((accumulator, part) => (accumulator << 16n) + BigInt(parseInt(part || '0', 16)), 0n);
|
|
42
85
|
}
|
|
43
86
|
function ipInCidrV6(ip, cidr) {
|
|
44
|
-
const
|
|
87
|
+
const parts = cidr.split('/');
|
|
88
|
+
if (parts.length > 2)
|
|
89
|
+
return false;
|
|
90
|
+
const [range, bitsString] = parts;
|
|
45
91
|
if (!range)
|
|
46
92
|
return false;
|
|
47
|
-
const bits = bitsString
|
|
93
|
+
const bits = bitsString === undefined || /^\d{1,3}$/u.test(bitsString)
|
|
94
|
+
? Number(bitsString ?? 128)
|
|
95
|
+
: -1;
|
|
96
|
+
if (!Number.isSafeInteger(bits) || bits < 0 || bits > 128)
|
|
97
|
+
return false;
|
|
48
98
|
const full = (1n << 128n) - 1n;
|
|
49
99
|
const mask = bits === 0 ? 0n : (full << BigInt(128 - bits)) & full;
|
|
50
|
-
|
|
100
|
+
try {
|
|
101
|
+
return (ipv6ToBigInt(ip) & mask) === (ipv6ToBigInt(range) & mask);
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
51
106
|
}
|
|
52
107
|
function validBootstrap(data) {
|
|
53
108
|
const record = data && typeof data === 'object' && !Array.isArray(data)
|
|
@@ -74,7 +129,12 @@ async function fetchBootstrap(kind, options = {}) {
|
|
|
74
129
|
return inflight;
|
|
75
130
|
const request = (async () => {
|
|
76
131
|
try {
|
|
77
|
-
const
|
|
132
|
+
const requestedEndpoint = `https://data.iana.org/rdap/${kind}.json`;
|
|
133
|
+
const response = await fetchUpstream(requestedEndpoint, {}, BOOTSTRAP_FETCH_TIMEOUT_MS);
|
|
134
|
+
const finalEndpoint = admitRdapEndpoint(response.finalUrl ?? requestedEndpoint);
|
|
135
|
+
if (finalEndpoint !== requestedEndpoint) {
|
|
136
|
+
throw new Error(`IANA bootstrap redirected outside its fixed source endpoint for ${kind}`);
|
|
137
|
+
}
|
|
78
138
|
if (!response.ok) {
|
|
79
139
|
throw new Error(`IANA bootstrap fetch failed for ${kind} (${response.status})`);
|
|
80
140
|
}
|
|
@@ -112,10 +172,10 @@ function clearRdapBootstrapCache() {
|
|
|
112
172
|
function uniqueRdapBases(urls) {
|
|
113
173
|
const seen = new Set();
|
|
114
174
|
return (Array.isArray(urls) ? urls : [])
|
|
115
|
-
.
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
175
|
+
.flatMap((url) => {
|
|
176
|
+
const admitted = admitRdapEndpoint(url);
|
|
177
|
+
return admitted ? [admitted] : [];
|
|
178
|
+
})
|
|
119
179
|
.sort((left, right) => Number(/^http:\/\//i.test(left)) - Number(/^http:\/\//i.test(right)))
|
|
120
180
|
.filter((url) => {
|
|
121
181
|
const key = url.replace(/\/$/, '').toLowerCase();
|
|
@@ -170,4 +230,4 @@ async function findRdapBases(type, value) {
|
|
|
170
230
|
}
|
|
171
231
|
return [];
|
|
172
232
|
}
|
|
173
|
-
export { BOOTSTRAP_STALE_TTL_MS, BOOTSTRAP_TTL_MS, MAX_RDAP_ENDPOINT_LENGTH, clearRdapBootstrapCache, fetchBootstrap, findRdapBases, ipv4ToLong, ipv6ToBigInt, uniqueRdapBases, };
|
|
233
|
+
export { BOOTSTRAP_STALE_TTL_MS, BOOTSTRAP_TTL_MS, MAX_RDAP_ENDPOINT_LENGTH, admitRdapEndpoint, clearRdapBootstrapCache, fetchBootstrap, findRdapBases, ipv4ToLong, ipv6ToBigInt, uniqueRdapBases, };
|
package/lib/rdap-client.mjs
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { cached } from "./lookup-cache.mjs";
|
|
2
2
|
import { findRdapBases, uniqueRdapBases, } from "./rdap-bootstrap.mjs";
|
|
3
3
|
import { rdapAttempt, rdapFailure } from "./rdap-attempts.mjs";
|
|
4
|
-
import {
|
|
5
|
-
import { validateRdapResponse } from "./rdap-validation.mjs";
|
|
4
|
+
import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
|
|
5
|
+
import { admitRdapObjectEndpoint, validateRdapResponse, } from "./rdap-validation.mjs";
|
|
6
6
|
const UPSTREAM_TIMEOUT_MS = 7_000;
|
|
7
7
|
const UPSTREAM_TOTAL_DEADLINE_MS = 12_000;
|
|
8
8
|
const MAX_RDAP_ENDPOINTS = 3;
|
|
@@ -24,7 +24,7 @@ function rdapPathFor(type, value) {
|
|
|
24
24
|
* Query the bounded bootstrap candidate set while retaining every attempted
|
|
25
25
|
* endpoint as separately attributed transport evidence.
|
|
26
26
|
*/
|
|
27
|
-
export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream =
|
|
27
|
+
export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapDetailedWithTimeout) {
|
|
28
28
|
const candidates = uniqueRdapBases(bases).slice(0, MAX_RDAP_ENDPOINTS);
|
|
29
29
|
if (candidates.length === 0)
|
|
30
30
|
return null;
|
|
@@ -38,13 +38,21 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
|
|
|
38
38
|
const url = `${base.replace(/\/$/, '')}/${rdapPathFor(type, value)}`;
|
|
39
39
|
try {
|
|
40
40
|
const upstream = await fetchUpstream(url, { headers: { Accept: 'application/rdap+json' } }, Math.min(UPSTREAM_TIMEOUT_MS, remaining));
|
|
41
|
+
const selectedEndpoint = admitRdapObjectEndpoint(type, value, upstream.finalUrl ?? url);
|
|
42
|
+
if (!selectedEndpoint) {
|
|
43
|
+
attempts.push(rdapAttempt(url, 'invalid_response', {
|
|
44
|
+
status: upstream.status,
|
|
45
|
+
detail: 'The endpoint returned final URL provenance for an invalid or different RDAP object.',
|
|
46
|
+
}));
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
41
49
|
if (upstream.status !== 404 && !upstream.ok) {
|
|
42
50
|
const outcome = upstream.status === 429
|
|
43
51
|
? 'rate_limited'
|
|
44
52
|
: upstream.status >= 500
|
|
45
53
|
? 'server_error'
|
|
46
54
|
: 'client_error';
|
|
47
|
-
attempts.push(rdapAttempt(
|
|
55
|
+
attempts.push(rdapAttempt(selectedEndpoint, outcome, {
|
|
48
56
|
status: upstream.status,
|
|
49
57
|
detail: `The endpoint returned HTTP ${upstream.status}.`,
|
|
50
58
|
}));
|
|
@@ -55,21 +63,21 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
|
|
|
55
63
|
data = JSON.parse(upstream.text);
|
|
56
64
|
}
|
|
57
65
|
catch {
|
|
58
|
-
attempts.push(rdapAttempt(
|
|
66
|
+
attempts.push(rdapAttempt(selectedEndpoint, 'invalid_json', {
|
|
59
67
|
status: upstream.status,
|
|
60
68
|
detail: 'The endpoint returned invalid JSON.',
|
|
61
69
|
}));
|
|
62
70
|
continue;
|
|
63
71
|
}
|
|
64
72
|
if (upstream.status === 404) {
|
|
65
|
-
attempts.push(rdapAttempt(
|
|
73
|
+
attempts.push(rdapAttempt(selectedEndpoint, 'not_found', {
|
|
66
74
|
status: upstream.status,
|
|
67
75
|
detail: 'The authoritative endpoint reported no matching object.',
|
|
68
76
|
selected: true,
|
|
69
77
|
}));
|
|
70
78
|
return {
|
|
71
|
-
rdapServer:
|
|
72
|
-
transportSecurity: /^https:\/\//i.test(
|
|
79
|
+
rdapServer: selectedEndpoint,
|
|
80
|
+
transportSecurity: /^https:\/\//i.test(selectedEndpoint) ? 'https' : 'http',
|
|
73
81
|
upstreamStatus: upstream.status,
|
|
74
82
|
fetchedAt: new Date().toISOString(),
|
|
75
83
|
data,
|
|
@@ -80,20 +88,20 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
|
|
|
80
88
|
const parsed = parseRdap(type, data);
|
|
81
89
|
const validation = validateRdapResponse(type, value, parsed);
|
|
82
90
|
if (!validation.valid) {
|
|
83
|
-
attempts.push(rdapAttempt(
|
|
91
|
+
attempts.push(rdapAttempt(selectedEndpoint, 'invalid_response', {
|
|
84
92
|
status: upstream.status,
|
|
85
93
|
detail: validation.detail,
|
|
86
94
|
}));
|
|
87
95
|
continue;
|
|
88
96
|
}
|
|
89
|
-
attempts.push(rdapAttempt(
|
|
97
|
+
attempts.push(rdapAttempt(selectedEndpoint, 'success', {
|
|
90
98
|
status: upstream.status,
|
|
91
99
|
detail: 'The endpoint returned the requested RDAP object.',
|
|
92
100
|
selected: true,
|
|
93
101
|
}));
|
|
94
102
|
return {
|
|
95
|
-
rdapServer:
|
|
96
|
-
transportSecurity: /^https:\/\//i.test(
|
|
103
|
+
rdapServer: selectedEndpoint,
|
|
104
|
+
transportSecurity: /^https:\/\//i.test(selectedEndpoint) ? 'https' : 'http',
|
|
97
105
|
upstreamStatus: upstream.status,
|
|
98
106
|
fetchedAt: new Date().toISOString(),
|
|
99
107
|
data,
|