@slicedearth/whoisleuth-cli 1.32.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/LICENSE +661 -0
- package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
- package/NOTICE +76 -0
- package/README.md +84 -0
- package/TRADEMARKS.md +41 -0
- package/bin/whoisleuth.mjs +35 -0
- package/cli/archive-inspect.mjs +198 -0
- package/cli/arguments.mjs +1022 -0
- package/cli/artifact-verify.mjs +210 -0
- package/cli/bulk-checkpoint.mjs +218 -0
- package/cli/bulk-command-runner.mjs +109 -0
- package/cli/bulk-output.mjs +112 -0
- package/cli/bulk.mjs +133 -0
- package/cli/compare.mjs +238 -0
- package/cli/completion.mjs +175 -0
- package/cli/csv.mjs +1 -0
- package/cli/discover.mjs +50 -0
- package/cli/discovery-command-runner.mjs +36 -0
- package/cli/discovery-observation-snapshot.mjs +315 -0
- package/cli/discovery-scan-command-runner.mjs +141 -0
- package/cli/discovery-scan.mjs +325 -0
- package/cli/discovery-snapshot.mjs +133 -0
- package/cli/discovery-workflow.mjs +73 -0
- package/cli/doctor.mjs +137 -0
- package/cli/errors.mjs +21 -0
- package/cli/evidence-command-arguments.mjs +145 -0
- package/cli/evidence-command-runner.mjs +106 -0
- package/cli/evidence-signing.mjs +215 -0
- package/cli/exit-codes.mjs +9 -0
- package/cli/export-evidence.mjs +28 -0
- package/cli/formatters/evidence-report.mjs +3 -0
- package/cli/formatters/html.mjs +69 -0
- package/cli/formatters/json.mjs +135 -0
- package/cli/formatters/markdown.mjs +3 -0
- package/cli/formatters/terminal.mjs +658 -0
- package/cli/http.mjs +27 -0
- package/cli/lookup-command-runner.mjs +87 -0
- package/cli/lookup-diff.mjs +171 -0
- package/cli/mail-review.mjs +204 -0
- package/cli/manual.mjs +34 -0
- package/cli/network-command-runner.mjs +88 -0
- package/cli/output-file.mjs +97 -0
- package/cli/page-compare.mjs +123 -0
- package/cli/posture.mjs +20 -0
- package/cli/progress-events.mjs +53 -0
- package/cli/progress.mjs +56 -0
- package/cli/registry-access.mjs +3 -0
- package/cli/registry-support.mjs +120 -0
- package/cli/risk-calibration.mjs +294 -0
- package/cli/runner-types.mjs +1 -0
- package/cli/runner.mjs +726 -0
- package/cli/saved-lookup.mjs +87 -0
- package/cli/source-reliability.mjs +727 -0
- package/cli/strict-exit.mjs +55 -0
- package/cli/terminal-presentation.mjs +147 -0
- package/docs/cli.md +827 -0
- package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
- package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
- package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
- package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
- package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
- package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
- package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
- package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
- package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
- package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
- package/frontend/src/lib/analysis/bulk-sort.js +49 -0
- package/frontend/src/lib/analysis/campaign-model.js +265 -0
- package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
- package/frontend/src/lib/analysis/case-migration-model.js +248 -0
- package/frontend/src/lib/analysis/case-model.js +5 -0
- package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
- package/frontend/src/lib/analysis/case-record-core.js +232 -0
- package/frontend/src/lib/analysis/case-record-model.js +7 -0
- package/frontend/src/lib/analysis/case-record-operations.js +224 -0
- package/frontend/src/lib/analysis/case-response-model.js +640 -0
- package/frontend/src/lib/analysis/case-response-packet.js +573 -0
- package/frontend/src/lib/analysis/case-storage-model.js +93 -0
- package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
- package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
- package/frontend/src/lib/analysis/http-summary.js +165 -0
- package/frontend/src/lib/analysis/investigation-guide.js +500 -0
- package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
- package/frontend/src/lib/analysis/page-baseline.js +272 -0
- package/frontend/src/lib/analysis/page-similarity.js +179 -0
- package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
- package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
- package/frontend/src/lib/analysis/scoring.js +135 -0
- package/frontend/src/lib/analysis/shortlist-model.js +209 -0
- package/frontend/src/lib/analysis/utils.js +265 -0
- package/frontend/src/lib/analysis/watchlist-history.js +643 -0
- package/frontend/src/lib/analysis/watchlist-store.js +127 -0
- package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
- package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
- package/frontend/src/lib/analysis/workspace-archive.js +472 -0
- package/lib/abort.mjs +26 -0
- package/lib/abusech-auth.mjs +19 -0
- package/lib/availability.mjs +716 -0
- package/lib/bounded-contract-normalizers.mjs +61 -0
- package/lib/browser-library-profile.mjs +310 -0
- package/lib/bulk-comparison-evidence.mjs +83 -0
- package/lib/classify.mjs +93 -0
- package/lib/client-behavior-profile.mjs +83 -0
- package/lib/credential-surface-profile.mjs +66 -0
- package/lib/csv.mjs +23 -0
- package/lib/ct-query.mjs +27 -0
- package/lib/ct-search.mjs +393 -0
- package/lib/distributed-operation-budget.mjs +461 -0
- package/lib/dns-delegation-health.mjs +344 -0
- package/lib/dns-intelligence.mjs +739 -0
- package/lib/dns-mx.mjs +24 -0
- package/lib/dns-resolver-selection.mjs +33 -0
- package/lib/domain-posture-analysis.mjs +254 -0
- package/lib/domain-posture-parsers.mjs +358 -0
- package/lib/domain-posture.mjs +745 -0
- package/lib/error-detail.mjs +10 -0
- package/lib/evidence-export.mjs +431 -0
- package/lib/evidence-report-markdown.mjs +71 -0
- package/lib/evidence-report.mjs +391 -0
- package/lib/external-intelligence-risk.mjs +108 -0
- package/lib/favicon.mjs +146 -0
- package/lib/feature-policy.mjs +70 -0
- package/lib/generated/retire-browser-catalog.mjs +8724 -0
- package/lib/generated/unicode-confusables-17.mjs +93 -0
- package/lib/hostname.mjs +38 -0
- package/lib/html-signals.mjs +680 -0
- package/lib/http-intelligence.mjs +232 -0
- package/lib/idn-confusable-policy.mjs +86 -0
- package/lib/idn-confusables.mjs +465 -0
- package/lib/lookup-cache.mjs +130 -0
- package/lib/lookup-progress.mjs +291 -0
- package/lib/lookup-source-progress.mjs +109 -0
- package/lib/lookup-threat-provider-inventory.mjs +13 -0
- package/lib/lookup.mjs +592 -0
- package/lib/observation.mjs +114 -0
- package/lib/observed-network-context.mjs +355 -0
- package/lib/operation-budget.mjs +489 -0
- package/lib/page-fingerprints.mjs +481 -0
- package/lib/page-role-profile.mjs +117 -0
- package/lib/perceptual-hash.mjs +451 -0
- package/lib/rdap-attempts.mjs +34 -0
- package/lib/rdap-bootstrap.mjs +173 -0
- package/lib/rdap-capabilities.mjs +292 -0
- package/lib/rdap-client.mjs +125 -0
- package/lib/rdap-normalization.mjs +691 -0
- package/lib/rdap-registrar.mjs +196 -0
- package/lib/rdap-transport.mjs +39 -0
- package/lib/rdap-types.mjs +1 -0
- package/lib/rdap-validation.mjs +94 -0
- package/lib/rdap.mjs +16 -0
- package/lib/registry-access.mjs +32 -0
- package/lib/registry-capabilities.mjs +117 -0
- package/lib/registry-capability-catalogue.mjs +1206 -0
- package/lib/registry-comparison.mjs +318 -0
- package/lib/registry-dates.mjs +190 -0
- package/lib/registry-insights.mjs +325 -0
- package/lib/response-policy.mjs +256 -0
- package/lib/risk-scoring.mjs +164 -0
- package/lib/safe-fetch.mjs +446 -0
- package/lib/security-txt.mjs +359 -0
- package/lib/service-binding-dns.mjs +608 -0
- package/lib/sslbl-certificates.generated.mjs +143 -0
- package/lib/sslbl-intelligence.mjs +241 -0
- package/lib/static-html-analysis.mjs +340 -0
- package/lib/structured-data-identity.mjs +323 -0
- package/lib/threat-intelligence-contract.mjs +5 -0
- package/lib/threat-intelligence-definition-registry.mjs +34 -0
- package/lib/threat-intelligence-fixtures.mjs +82 -0
- package/lib/threat-intelligence-manifests.mjs +512 -0
- package/lib/threat-intelligence-runtime.mjs +662 -0
- package/lib/threat-intelligence-targets.mjs +168 -0
- package/lib/threat-intelligence-types.mjs +3 -0
- package/lib/threatfox-intelligence.mjs +307 -0
- package/lib/tls-intelligence.mjs +814 -0
- package/lib/typosquat-generator.mjs +884 -0
- package/lib/urlhaus-intelligence.mjs +361 -0
- package/lib/urlscan-intelligence.mjs +349 -0
- package/lib/website-security-posture.mjs +325 -0
- package/lib/website-technology.mjs +437 -0
- package/lib/whois-authority.mjs +98 -0
- package/lib/whois-chain.mjs +261 -0
- package/lib/whois-contacts.mjs +163 -0
- package/lib/whois-contracts.mjs +3 -0
- package/lib/whois-normalization.mjs +27 -0
- package/lib/whois-transport.mjs +100 -0
- package/lib/whois-values.mjs +92 -0
- package/lib/whois.mjs +1187 -0
- package/package.json +55 -0
- package/third-party-notices.txt +5571 -0
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
// Bounded interpretation of RDAP conformance declarations already present in
|
|
2
|
+
// a normalized response. This module performs no help request or reverse
|
|
3
|
+
// search and does not treat an absent extension declaration as proof that a
|
|
4
|
+
// server cannot support the related operation.
|
|
5
|
+
export const RDAP_CAPABILITY_INSPECTION_VERSION = 1;
|
|
6
|
+
export const RDAP_EXTENSION_CATALOG_REVIEWED_AT = '2026-07-31';
|
|
7
|
+
export const MAX_RDAP_CAPABILITY_DECLARATIONS = 50;
|
|
8
|
+
export const MAX_RDAP_UNKNOWN_CAPABILITIES = 20;
|
|
9
|
+
const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
|
|
10
|
+
// Reviewed against the protocol registry on 2026-07-31. The registry-drift
|
|
11
|
+
// audit owns freshness checks; runtime lookup behavior never depends on this
|
|
12
|
+
// catalogue being exhaustive.
|
|
13
|
+
const EXTENSION_CATALOG = Object.freeze({
|
|
14
|
+
rdap_level_0: {
|
|
15
|
+
label: 'Core RDAP response',
|
|
16
|
+
category: 'core',
|
|
17
|
+
capability: 'Baseline lookup response conformance.',
|
|
18
|
+
},
|
|
19
|
+
arin_originas0: {
|
|
20
|
+
label: 'Origin AS extension',
|
|
21
|
+
category: 'operator',
|
|
22
|
+
capability: 'Operator-specific origin autonomous-system data.',
|
|
23
|
+
},
|
|
24
|
+
artrecord: {
|
|
25
|
+
label: 'Registry record extension',
|
|
26
|
+
category: 'operator',
|
|
27
|
+
capability: 'Operator-specific registry record data.',
|
|
28
|
+
},
|
|
29
|
+
autnums: {
|
|
30
|
+
label: 'Autonomous-system search resource',
|
|
31
|
+
category: 'standard',
|
|
32
|
+
capability: 'Autonomous-system search path support declaration.',
|
|
33
|
+
},
|
|
34
|
+
autnumsearchresults: {
|
|
35
|
+
label: 'Autonomous-system search results',
|
|
36
|
+
category: 'standard',
|
|
37
|
+
capability: 'Autonomous-system search result container declaration.',
|
|
38
|
+
},
|
|
39
|
+
cidr0: {
|
|
40
|
+
label: 'CIDR network extension',
|
|
41
|
+
category: 'standard',
|
|
42
|
+
capability: 'CIDR prefix representation in network responses.',
|
|
43
|
+
},
|
|
44
|
+
farv1: {
|
|
45
|
+
label: 'Federated authentication',
|
|
46
|
+
category: 'standard',
|
|
47
|
+
capability: 'Federated authorization profile declaration.',
|
|
48
|
+
},
|
|
49
|
+
fred: {
|
|
50
|
+
label: 'Registry-system extension',
|
|
51
|
+
category: 'operator',
|
|
52
|
+
capability: 'Operator-specific registry-system fields.',
|
|
53
|
+
},
|
|
54
|
+
geofeed1: {
|
|
55
|
+
label: 'Geofeed extension',
|
|
56
|
+
category: 'standard',
|
|
57
|
+
capability: 'Geofeed publication through RDAP.',
|
|
58
|
+
},
|
|
59
|
+
icann_rdap_response_profile_0: {
|
|
60
|
+
label: 'Response profile 0',
|
|
61
|
+
category: 'profile',
|
|
62
|
+
status: 'obsolete',
|
|
63
|
+
capability: 'Obsolete generic-domain response profile declaration.',
|
|
64
|
+
},
|
|
65
|
+
icann_rdap_response_profile_1: {
|
|
66
|
+
label: 'Response profile 1',
|
|
67
|
+
category: 'profile',
|
|
68
|
+
capability: 'Generic-domain response profile declaration.',
|
|
69
|
+
},
|
|
70
|
+
icann_rdap_technical_implementation_guide_0: {
|
|
71
|
+
label: 'Implementation guide 0',
|
|
72
|
+
category: 'profile',
|
|
73
|
+
status: 'obsolete',
|
|
74
|
+
capability: 'Obsolete generic-domain implementation profile declaration.',
|
|
75
|
+
},
|
|
76
|
+
icann_rdap_technical_implementation_guide_1: {
|
|
77
|
+
label: 'Implementation guide 1',
|
|
78
|
+
category: 'profile',
|
|
79
|
+
capability: 'Generic-domain technical implementation profile declaration.',
|
|
80
|
+
},
|
|
81
|
+
ips: {
|
|
82
|
+
label: 'IP network search resource',
|
|
83
|
+
category: 'standard',
|
|
84
|
+
capability: 'IP network search path support declaration.',
|
|
85
|
+
},
|
|
86
|
+
ipsearchresults: {
|
|
87
|
+
label: 'IP network search results',
|
|
88
|
+
category: 'standard',
|
|
89
|
+
capability: 'IP network search result container declaration.',
|
|
90
|
+
},
|
|
91
|
+
nask: {
|
|
92
|
+
label: 'Registry response extension',
|
|
93
|
+
category: 'operator',
|
|
94
|
+
capability: 'Operator-specific registry response fields.',
|
|
95
|
+
},
|
|
96
|
+
nro_rdap_profile_0: {
|
|
97
|
+
label: 'Address-registry response profile',
|
|
98
|
+
category: 'profile',
|
|
99
|
+
capability: 'Regional address-registry response profile declaration.',
|
|
100
|
+
},
|
|
101
|
+
nro_rdap_profile_asn_flat_0: {
|
|
102
|
+
label: 'Flat ASN profile',
|
|
103
|
+
category: 'profile',
|
|
104
|
+
capability: 'Flat autonomous-system hierarchy profile declaration.',
|
|
105
|
+
},
|
|
106
|
+
nro_rdap_profile_asn_hierarchical_0: {
|
|
107
|
+
label: 'Hierarchical ASN profile',
|
|
108
|
+
category: 'profile',
|
|
109
|
+
capability: 'Hierarchical autonomous-system profile declaration.',
|
|
110
|
+
},
|
|
111
|
+
paging: {
|
|
112
|
+
label: 'Result paging',
|
|
113
|
+
category: 'standard',
|
|
114
|
+
capability: 'Paged search-result behavior.',
|
|
115
|
+
},
|
|
116
|
+
platformns: {
|
|
117
|
+
label: 'Nameserver platform extension',
|
|
118
|
+
category: 'operator',
|
|
119
|
+
capability: 'Operator-specific nameserver platform context.',
|
|
120
|
+
},
|
|
121
|
+
rdap_objecttag: {
|
|
122
|
+
label: 'Object tags',
|
|
123
|
+
category: 'standard',
|
|
124
|
+
capability: 'Structured entity identifiers for query bootstrapping.',
|
|
125
|
+
},
|
|
126
|
+
redacted: {
|
|
127
|
+
label: 'Redaction metadata',
|
|
128
|
+
category: 'standard',
|
|
129
|
+
capability: 'Machine-readable response redaction markers.',
|
|
130
|
+
},
|
|
131
|
+
redirect_with_content: {
|
|
132
|
+
label: 'Redirect with content',
|
|
133
|
+
category: 'operator',
|
|
134
|
+
capability: 'Operator-specific redirect response content.',
|
|
135
|
+
},
|
|
136
|
+
regtype: {
|
|
137
|
+
label: 'Registration type extension',
|
|
138
|
+
category: 'operator',
|
|
139
|
+
capability: 'Operator-specific registration-type context.',
|
|
140
|
+
},
|
|
141
|
+
reverse_search: {
|
|
142
|
+
label: 'Reverse search',
|
|
143
|
+
category: 'standard',
|
|
144
|
+
capability: 'Server advertises the reverse-search protocol extension.',
|
|
145
|
+
},
|
|
146
|
+
rirsearch1: {
|
|
147
|
+
label: 'Address-registry search',
|
|
148
|
+
category: 'standard',
|
|
149
|
+
capability: 'Versioned network and autonomous-system search behavior.',
|
|
150
|
+
},
|
|
151
|
+
sorting: {
|
|
152
|
+
label: 'Result sorting',
|
|
153
|
+
category: 'standard',
|
|
154
|
+
capability: 'Sorted search-result behavior.',
|
|
155
|
+
},
|
|
156
|
+
subsetting: {
|
|
157
|
+
label: 'Partial response',
|
|
158
|
+
category: 'standard',
|
|
159
|
+
capability: 'Field-subset response behavior.',
|
|
160
|
+
},
|
|
161
|
+
ttl0: {
|
|
162
|
+
label: 'DNS TTL extension',
|
|
163
|
+
category: 'standard',
|
|
164
|
+
capability: 'DNS time-to-live values in RDAP responses.',
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
export function reviewedRdapExtensionCatalog() {
|
|
168
|
+
return Object.entries(EXTENSION_CATALOG)
|
|
169
|
+
.filter(([, entry]) => entry.category !== 'core')
|
|
170
|
+
.map(([identifier, entry]) => Object.freeze({
|
|
171
|
+
identifier,
|
|
172
|
+
status: entry.status ?? 'current',
|
|
173
|
+
}))
|
|
174
|
+
.sort((left, right) => left.identifier.localeCompare(right.identifier));
|
|
175
|
+
}
|
|
176
|
+
function record(value) {
|
|
177
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
178
|
+
? value
|
|
179
|
+
: null;
|
|
180
|
+
}
|
|
181
|
+
function identifier(value) {
|
|
182
|
+
if (typeof value !== 'string' || CONTROL_RE.test(value) || value.length > 160)
|
|
183
|
+
return '';
|
|
184
|
+
return value.trim().toLowerCase();
|
|
185
|
+
}
|
|
186
|
+
function sourceState(parsed, status) {
|
|
187
|
+
if (!parsed || !['success', 'complete', 'partial'].includes(typeof status === 'string' ? status : '')) {
|
|
188
|
+
return 'unavailable';
|
|
189
|
+
}
|
|
190
|
+
return status === 'partial'
|
|
191
|
+
|| parsed.serverTruncated === true
|
|
192
|
+
|| parsed.conformanceTruncated === true
|
|
193
|
+
? 'partial'
|
|
194
|
+
: 'complete';
|
|
195
|
+
}
|
|
196
|
+
function reverseSearchCapability(state, declared) {
|
|
197
|
+
if (declared) {
|
|
198
|
+
return {
|
|
199
|
+
state: 'advertised',
|
|
200
|
+
execution: 'not_attempted',
|
|
201
|
+
helpDiscoveryRequired: true,
|
|
202
|
+
actionAvailable: false,
|
|
203
|
+
detail: 'This response advertises the reverse-search extension. The server help document must still be inspected before any property or query can be offered.',
|
|
204
|
+
limitations: [
|
|
205
|
+
'An extension declaration does not prove that this endpoint permits anonymous use, supports a particular property, or will return complete results.',
|
|
206
|
+
'No reverse-search request was made.',
|
|
207
|
+
],
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
if (state === 'complete') {
|
|
211
|
+
return {
|
|
212
|
+
state: 'not_advertised',
|
|
213
|
+
execution: 'not_attempted',
|
|
214
|
+
helpDiscoveryRequired: true,
|
|
215
|
+
actionAvailable: false,
|
|
216
|
+
detail: 'This individual response did not advertise reverse search. A separate help response could still declare server-level support.',
|
|
217
|
+
limitations: [
|
|
218
|
+
'Absence from an individual response is not proof that the server does not support reverse search.',
|
|
219
|
+
'No help or reverse-search request was made.',
|
|
220
|
+
],
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
return {
|
|
224
|
+
state: 'unknown',
|
|
225
|
+
execution: 'not_attempted',
|
|
226
|
+
helpDiscoveryRequired: true,
|
|
227
|
+
actionAvailable: false,
|
|
228
|
+
detail: 'The response was unavailable or incomplete, so reverse-search support cannot be assessed.',
|
|
229
|
+
limitations: [
|
|
230
|
+
'Missing or truncated conformance data is not evidence that reverse search is unsupported.',
|
|
231
|
+
'No help or reverse-search request was made.',
|
|
232
|
+
],
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
export function inspectRdapCapabilities(parsedValue, status) {
|
|
236
|
+
const parsed = record(parsedValue);
|
|
237
|
+
const state = sourceState(parsed, status);
|
|
238
|
+
const rawDeclarations = Array.isArray(parsed?.conformance) ? parsed.conformance : [];
|
|
239
|
+
const unique = new Set();
|
|
240
|
+
for (const value of rawDeclarations.slice(0, MAX_RDAP_CAPABILITY_DECLARATIONS * 2)) {
|
|
241
|
+
const normalized = identifier(value);
|
|
242
|
+
if (normalized)
|
|
243
|
+
unique.add(normalized);
|
|
244
|
+
if (unique.size >= MAX_RDAP_CAPABILITY_DECLARATIONS)
|
|
245
|
+
break;
|
|
246
|
+
}
|
|
247
|
+
const declarations = [...unique].sort().map((value) => {
|
|
248
|
+
const entry = EXTENSION_CATALOG[value];
|
|
249
|
+
return entry
|
|
250
|
+
? {
|
|
251
|
+
identifier: value,
|
|
252
|
+
label: entry.label,
|
|
253
|
+
category: entry.category,
|
|
254
|
+
registered: entry.category === 'core' ? null : true,
|
|
255
|
+
status: entry.status ?? 'current',
|
|
256
|
+
capability: entry.capability,
|
|
257
|
+
}
|
|
258
|
+
: {
|
|
259
|
+
identifier: value,
|
|
260
|
+
label: 'Unclassified declaration',
|
|
261
|
+
category: 'unknown',
|
|
262
|
+
registered: null,
|
|
263
|
+
status: 'unknown',
|
|
264
|
+
capability: 'The identifier is retained for analyst review but is not interpreted by this catalogue.',
|
|
265
|
+
};
|
|
266
|
+
});
|
|
267
|
+
const omittedDeclarations = Math.max(0, rawDeclarations.length - unique.size);
|
|
268
|
+
const unknownIdentifiers = declarations
|
|
269
|
+
.filter((entry) => entry.category === 'unknown')
|
|
270
|
+
.map((entry) => entry.identifier)
|
|
271
|
+
.slice(0, MAX_RDAP_UNKNOWN_CAPABILITIES);
|
|
272
|
+
const limitations = [
|
|
273
|
+
'Declarations describe what the server claims for this response; they do not prove that an operation is authorized, implemented correctly, complete, or available anonymously.',
|
|
274
|
+
'Unknown identifiers remain visible and neutral. Catalogue recognition never affects availability or Risk scoring.',
|
|
275
|
+
];
|
|
276
|
+
if (state === 'unavailable') {
|
|
277
|
+
limitations.unshift('No usable RDAP publication was available for capability inspection.');
|
|
278
|
+
}
|
|
279
|
+
else if (state === 'partial' || omittedDeclarations > 0) {
|
|
280
|
+
limitations.unshift('The conformance declaration set was incomplete or locally capped.');
|
|
281
|
+
}
|
|
282
|
+
return {
|
|
283
|
+
version: RDAP_CAPABILITY_INSPECTION_VERSION,
|
|
284
|
+
catalogReviewedAt: RDAP_EXTENSION_CATALOG_REVIEWED_AT,
|
|
285
|
+
state,
|
|
286
|
+
declarations,
|
|
287
|
+
unknownIdentifiers,
|
|
288
|
+
omittedDeclarations,
|
|
289
|
+
reverseSearch: reverseSearchCapability(state, declarations.some((entry) => entry.identifier === 'reverse_search')),
|
|
290
|
+
limitations,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
import { cached } from "./lookup-cache.mjs";
|
|
2
|
+
import { findRdapBases, uniqueRdapBases, } from "./rdap-bootstrap.mjs";
|
|
3
|
+
import { rdapAttempt, rdapFailure } from "./rdap-attempts.mjs";
|
|
4
|
+
import { fetchRdapWithTimeout, } from "./rdap-transport.mjs";
|
|
5
|
+
import { validateRdapResponse } from "./rdap-validation.mjs";
|
|
6
|
+
const UPSTREAM_TIMEOUT_MS = 7_000;
|
|
7
|
+
const UPSTREAM_TOTAL_DEADLINE_MS = 12_000;
|
|
8
|
+
const MAX_RDAP_ENDPOINTS = 3;
|
|
9
|
+
function errorProperty(value, key) {
|
|
10
|
+
if (!value || typeof value !== 'object' || Array.isArray(value))
|
|
11
|
+
return null;
|
|
12
|
+
return value[key];
|
|
13
|
+
}
|
|
14
|
+
function rdapPathFor(type, value) {
|
|
15
|
+
if (type === 'domain')
|
|
16
|
+
return `domain/${value}`;
|
|
17
|
+
if (type === 'ipv4' || type === 'ipv6')
|
|
18
|
+
return `ip/${value}`;
|
|
19
|
+
if (type === 'asn')
|
|
20
|
+
return `autnum/${value.replace(/^AS/i, '')}`;
|
|
21
|
+
throw new Error(`Unsupported RDAP type: ${type}`);
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Query the bounded bootstrap candidate set while retaining every attempted
|
|
25
|
+
* endpoint as separately attributed transport evidence.
|
|
26
|
+
*/
|
|
27
|
+
export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapWithTimeout) {
|
|
28
|
+
const candidates = uniqueRdapBases(bases).slice(0, MAX_RDAP_ENDPOINTS);
|
|
29
|
+
if (candidates.length === 0)
|
|
30
|
+
return null;
|
|
31
|
+
const startedAt = Date.now();
|
|
32
|
+
const attempts = [];
|
|
33
|
+
for (const base of candidates) {
|
|
34
|
+
const elapsed = Date.now() - startedAt;
|
|
35
|
+
const remaining = UPSTREAM_TOTAL_DEADLINE_MS - elapsed;
|
|
36
|
+
if (remaining <= 0)
|
|
37
|
+
break;
|
|
38
|
+
const url = `${base.replace(/\/$/, '')}/${rdapPathFor(type, value)}`;
|
|
39
|
+
try {
|
|
40
|
+
const upstream = await fetchUpstream(url, { headers: { Accept: 'application/rdap+json' } }, Math.min(UPSTREAM_TIMEOUT_MS, remaining));
|
|
41
|
+
if (upstream.status !== 404 && !upstream.ok) {
|
|
42
|
+
const outcome = upstream.status === 429
|
|
43
|
+
? 'rate_limited'
|
|
44
|
+
: upstream.status >= 500
|
|
45
|
+
? 'server_error'
|
|
46
|
+
: 'client_error';
|
|
47
|
+
attempts.push(rdapAttempt(url, outcome, {
|
|
48
|
+
status: upstream.status,
|
|
49
|
+
detail: `The endpoint returned HTTP ${upstream.status}.`,
|
|
50
|
+
}));
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
let data;
|
|
54
|
+
try {
|
|
55
|
+
data = JSON.parse(upstream.text);
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
attempts.push(rdapAttempt(url, 'invalid_json', {
|
|
59
|
+
status: upstream.status,
|
|
60
|
+
detail: 'The endpoint returned invalid JSON.',
|
|
61
|
+
}));
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
if (upstream.status === 404) {
|
|
65
|
+
attempts.push(rdapAttempt(url, 'not_found', {
|
|
66
|
+
status: upstream.status,
|
|
67
|
+
detail: 'The authoritative endpoint reported no matching object.',
|
|
68
|
+
selected: true,
|
|
69
|
+
}));
|
|
70
|
+
return {
|
|
71
|
+
rdapServer: url,
|
|
72
|
+
transportSecurity: /^https:\/\//i.test(url) ? 'https' : 'http',
|
|
73
|
+
upstreamStatus: upstream.status,
|
|
74
|
+
fetchedAt: new Date().toISOString(),
|
|
75
|
+
data,
|
|
76
|
+
parsed: null,
|
|
77
|
+
attempts,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const parsed = parseRdap(type, data);
|
|
81
|
+
const validation = validateRdapResponse(type, value, parsed);
|
|
82
|
+
if (!validation.valid) {
|
|
83
|
+
attempts.push(rdapAttempt(url, 'invalid_response', {
|
|
84
|
+
status: upstream.status,
|
|
85
|
+
detail: validation.detail,
|
|
86
|
+
}));
|
|
87
|
+
continue;
|
|
88
|
+
}
|
|
89
|
+
attempts.push(rdapAttempt(url, 'success', {
|
|
90
|
+
status: upstream.status,
|
|
91
|
+
detail: 'The endpoint returned the requested RDAP object.',
|
|
92
|
+
selected: true,
|
|
93
|
+
}));
|
|
94
|
+
return {
|
|
95
|
+
rdapServer: url,
|
|
96
|
+
transportSecurity: /^https:\/\//i.test(url) ? 'https' : 'http',
|
|
97
|
+
upstreamStatus: upstream.status,
|
|
98
|
+
fetchedAt: new Date().toISOString(),
|
|
99
|
+
data,
|
|
100
|
+
parsed,
|
|
101
|
+
attempts,
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
catch (cause) {
|
|
105
|
+
const detail = String(errorProperty(cause, 'message') || 'request failed');
|
|
106
|
+
const outcome = errorProperty(cause, 'name') === 'AbortError' ||
|
|
107
|
+
/timed? out|time limit/i.test(detail)
|
|
108
|
+
? 'timeout'
|
|
109
|
+
: 'network_error';
|
|
110
|
+
attempts.push(rdapAttempt(url, outcome, { detail }));
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
const detail = attempts.length
|
|
114
|
+
? attempts
|
|
115
|
+
.map((attempt) => `${attempt.endpoint} ${rdapFailure(attempt.outcome, attempt.status)}`)
|
|
116
|
+
.join('; ')
|
|
117
|
+
: 'the total upstream deadline expired';
|
|
118
|
+
throw Object.assign(new Error(`RDAP lookup failed across ${candidates.length} endpoint(s): ${detail}`), { attempts });
|
|
119
|
+
}
|
|
120
|
+
export async function fetchRdapRecordWithParser(type, value, parseRdap) {
|
|
121
|
+
return cached(`rdap:${type}:${value}`, async () => {
|
|
122
|
+
const bases = await findRdapBases(type, value);
|
|
123
|
+
return fetchRdapFromBasesWithParser(type, value, bases, parseRdap);
|
|
124
|
+
});
|
|
125
|
+
}
|