@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.
Files changed (189) hide show
  1. package/LICENSE +661 -0
  2. package/LICENSES/Retire.js-Apache-2.0.txt +199 -0
  3. package/NOTICE +76 -0
  4. package/README.md +84 -0
  5. package/TRADEMARKS.md +41 -0
  6. package/bin/whoisleuth.mjs +35 -0
  7. package/cli/archive-inspect.mjs +198 -0
  8. package/cli/arguments.mjs +1022 -0
  9. package/cli/artifact-verify.mjs +210 -0
  10. package/cli/bulk-checkpoint.mjs +218 -0
  11. package/cli/bulk-command-runner.mjs +109 -0
  12. package/cli/bulk-output.mjs +112 -0
  13. package/cli/bulk.mjs +133 -0
  14. package/cli/compare.mjs +238 -0
  15. package/cli/completion.mjs +175 -0
  16. package/cli/csv.mjs +1 -0
  17. package/cli/discover.mjs +50 -0
  18. package/cli/discovery-command-runner.mjs +36 -0
  19. package/cli/discovery-observation-snapshot.mjs +315 -0
  20. package/cli/discovery-scan-command-runner.mjs +141 -0
  21. package/cli/discovery-scan.mjs +325 -0
  22. package/cli/discovery-snapshot.mjs +133 -0
  23. package/cli/discovery-workflow.mjs +73 -0
  24. package/cli/doctor.mjs +137 -0
  25. package/cli/errors.mjs +21 -0
  26. package/cli/evidence-command-arguments.mjs +145 -0
  27. package/cli/evidence-command-runner.mjs +106 -0
  28. package/cli/evidence-signing.mjs +215 -0
  29. package/cli/exit-codes.mjs +9 -0
  30. package/cli/export-evidence.mjs +28 -0
  31. package/cli/formatters/evidence-report.mjs +3 -0
  32. package/cli/formatters/html.mjs +69 -0
  33. package/cli/formatters/json.mjs +135 -0
  34. package/cli/formatters/markdown.mjs +3 -0
  35. package/cli/formatters/terminal.mjs +658 -0
  36. package/cli/http.mjs +27 -0
  37. package/cli/lookup-command-runner.mjs +87 -0
  38. package/cli/lookup-diff.mjs +171 -0
  39. package/cli/mail-review.mjs +204 -0
  40. package/cli/manual.mjs +34 -0
  41. package/cli/network-command-runner.mjs +88 -0
  42. package/cli/output-file.mjs +97 -0
  43. package/cli/page-compare.mjs +123 -0
  44. package/cli/posture.mjs +20 -0
  45. package/cli/progress-events.mjs +53 -0
  46. package/cli/progress.mjs +56 -0
  47. package/cli/registry-access.mjs +3 -0
  48. package/cli/registry-support.mjs +120 -0
  49. package/cli/risk-calibration.mjs +294 -0
  50. package/cli/runner-types.mjs +1 -0
  51. package/cli/runner.mjs +726 -0
  52. package/cli/saved-lookup.mjs +87 -0
  53. package/cli/source-reliability.mjs +727 -0
  54. package/cli/strict-exit.mjs +55 -0
  55. package/cli/terminal-presentation.mjs +147 -0
  56. package/docs/cli.md +827 -0
  57. package/frontend/src/lib/analysis/acquisition-decision-packet.js +93 -0
  58. package/frontend/src/lib/analysis/acquisition-due-diligence.js +391 -0
  59. package/frontend/src/lib/analysis/artifact-integrity.js +21 -0
  60. package/frontend/src/lib/analysis/brand-profile-model.js +421 -0
  61. package/frontend/src/lib/analysis/bulk-domain-comparison.js +246 -0
  62. package/frontend/src/lib/analysis/bulk-mail-exposure.js +192 -0
  63. package/frontend/src/lib/analysis/bulk-retry-plan.js +86 -0
  64. package/frontend/src/lib/analysis/bulk-review-export.js +62 -0
  65. package/frontend/src/lib/analysis/bulk-review-model.js +266 -0
  66. package/frontend/src/lib/analysis/bulk-session-model.js +515 -0
  67. package/frontend/src/lib/analysis/bulk-sort.js +49 -0
  68. package/frontend/src/lib/analysis/campaign-model.js +265 -0
  69. package/frontend/src/lib/analysis/case-evidence-model.js +642 -0
  70. package/frontend/src/lib/analysis/case-migration-model.js +248 -0
  71. package/frontend/src/lib/analysis/case-model.js +5 -0
  72. package/frontend/src/lib/analysis/case-record-contracts.js +44 -0
  73. package/frontend/src/lib/analysis/case-record-core.js +232 -0
  74. package/frontend/src/lib/analysis/case-record-model.js +7 -0
  75. package/frontend/src/lib/analysis/case-record-operations.js +224 -0
  76. package/frontend/src/lib/analysis/case-response-model.js +640 -0
  77. package/frontend/src/lib/analysis/case-response-packet.js +573 -0
  78. package/frontend/src/lib/analysis/case-storage-model.js +93 -0
  79. package/frontend/src/lib/analysis/detection-rule-model.js +306 -0
  80. package/frontend/src/lib/analysis/dns-record-normalization.js +11 -0
  81. package/frontend/src/lib/analysis/http-summary.js +165 -0
  82. package/frontend/src/lib/analysis/investigation-guide.js +500 -0
  83. package/frontend/src/lib/analysis/investigation-template-model.js +144 -0
  84. package/frontend/src/lib/analysis/page-baseline.js +272 -0
  85. package/frontend/src/lib/analysis/page-similarity.js +179 -0
  86. package/frontend/src/lib/analysis/relationship-evidence.js +265 -0
  87. package/frontend/src/lib/analysis/relationship-observation-model.js +423 -0
  88. package/frontend/src/lib/analysis/scoring.js +135 -0
  89. package/frontend/src/lib/analysis/shortlist-model.js +209 -0
  90. package/frontend/src/lib/analysis/utils.js +265 -0
  91. package/frontend/src/lib/analysis/watchlist-history.js +643 -0
  92. package/frontend/src/lib/analysis/watchlist-store.js +127 -0
  93. package/frontend/src/lib/analysis/website-snapshot-model.js +302 -0
  94. package/frontend/src/lib/analysis/workspace-archive-crypto.js +282 -0
  95. package/frontend/src/lib/analysis/workspace-archive.js +472 -0
  96. package/lib/abort.mjs +26 -0
  97. package/lib/abusech-auth.mjs +19 -0
  98. package/lib/availability.mjs +716 -0
  99. package/lib/bounded-contract-normalizers.mjs +61 -0
  100. package/lib/browser-library-profile.mjs +310 -0
  101. package/lib/bulk-comparison-evidence.mjs +83 -0
  102. package/lib/classify.mjs +93 -0
  103. package/lib/client-behavior-profile.mjs +83 -0
  104. package/lib/credential-surface-profile.mjs +66 -0
  105. package/lib/csv.mjs +23 -0
  106. package/lib/ct-query.mjs +27 -0
  107. package/lib/ct-search.mjs +393 -0
  108. package/lib/distributed-operation-budget.mjs +461 -0
  109. package/lib/dns-delegation-health.mjs +344 -0
  110. package/lib/dns-intelligence.mjs +739 -0
  111. package/lib/dns-mx.mjs +24 -0
  112. package/lib/dns-resolver-selection.mjs +33 -0
  113. package/lib/domain-posture-analysis.mjs +254 -0
  114. package/lib/domain-posture-parsers.mjs +358 -0
  115. package/lib/domain-posture.mjs +745 -0
  116. package/lib/error-detail.mjs +10 -0
  117. package/lib/evidence-export.mjs +431 -0
  118. package/lib/evidence-report-markdown.mjs +71 -0
  119. package/lib/evidence-report.mjs +391 -0
  120. package/lib/external-intelligence-risk.mjs +108 -0
  121. package/lib/favicon.mjs +146 -0
  122. package/lib/feature-policy.mjs +70 -0
  123. package/lib/generated/retire-browser-catalog.mjs +8724 -0
  124. package/lib/generated/unicode-confusables-17.mjs +93 -0
  125. package/lib/hostname.mjs +38 -0
  126. package/lib/html-signals.mjs +680 -0
  127. package/lib/http-intelligence.mjs +232 -0
  128. package/lib/idn-confusable-policy.mjs +86 -0
  129. package/lib/idn-confusables.mjs +465 -0
  130. package/lib/lookup-cache.mjs +130 -0
  131. package/lib/lookup-progress.mjs +291 -0
  132. package/lib/lookup-source-progress.mjs +109 -0
  133. package/lib/lookup-threat-provider-inventory.mjs +13 -0
  134. package/lib/lookup.mjs +592 -0
  135. package/lib/observation.mjs +114 -0
  136. package/lib/observed-network-context.mjs +355 -0
  137. package/lib/operation-budget.mjs +489 -0
  138. package/lib/page-fingerprints.mjs +481 -0
  139. package/lib/page-role-profile.mjs +117 -0
  140. package/lib/perceptual-hash.mjs +451 -0
  141. package/lib/rdap-attempts.mjs +34 -0
  142. package/lib/rdap-bootstrap.mjs +173 -0
  143. package/lib/rdap-capabilities.mjs +292 -0
  144. package/lib/rdap-client.mjs +125 -0
  145. package/lib/rdap-normalization.mjs +691 -0
  146. package/lib/rdap-registrar.mjs +196 -0
  147. package/lib/rdap-transport.mjs +39 -0
  148. package/lib/rdap-types.mjs +1 -0
  149. package/lib/rdap-validation.mjs +94 -0
  150. package/lib/rdap.mjs +16 -0
  151. package/lib/registry-access.mjs +32 -0
  152. package/lib/registry-capabilities.mjs +117 -0
  153. package/lib/registry-capability-catalogue.mjs +1206 -0
  154. package/lib/registry-comparison.mjs +318 -0
  155. package/lib/registry-dates.mjs +190 -0
  156. package/lib/registry-insights.mjs +325 -0
  157. package/lib/response-policy.mjs +256 -0
  158. package/lib/risk-scoring.mjs +164 -0
  159. package/lib/safe-fetch.mjs +446 -0
  160. package/lib/security-txt.mjs +359 -0
  161. package/lib/service-binding-dns.mjs +608 -0
  162. package/lib/sslbl-certificates.generated.mjs +143 -0
  163. package/lib/sslbl-intelligence.mjs +241 -0
  164. package/lib/static-html-analysis.mjs +340 -0
  165. package/lib/structured-data-identity.mjs +323 -0
  166. package/lib/threat-intelligence-contract.mjs +5 -0
  167. package/lib/threat-intelligence-definition-registry.mjs +34 -0
  168. package/lib/threat-intelligence-fixtures.mjs +82 -0
  169. package/lib/threat-intelligence-manifests.mjs +512 -0
  170. package/lib/threat-intelligence-runtime.mjs +662 -0
  171. package/lib/threat-intelligence-targets.mjs +168 -0
  172. package/lib/threat-intelligence-types.mjs +3 -0
  173. package/lib/threatfox-intelligence.mjs +307 -0
  174. package/lib/tls-intelligence.mjs +814 -0
  175. package/lib/typosquat-generator.mjs +884 -0
  176. package/lib/urlhaus-intelligence.mjs +361 -0
  177. package/lib/urlscan-intelligence.mjs +349 -0
  178. package/lib/website-security-posture.mjs +325 -0
  179. package/lib/website-technology.mjs +437 -0
  180. package/lib/whois-authority.mjs +98 -0
  181. package/lib/whois-chain.mjs +261 -0
  182. package/lib/whois-contacts.mjs +163 -0
  183. package/lib/whois-contracts.mjs +3 -0
  184. package/lib/whois-normalization.mjs +27 -0
  185. package/lib/whois-transport.mjs +100 -0
  186. package/lib/whois-values.mjs +92 -0
  187. package/lib/whois.mjs +1187 -0
  188. package/package.json +55 -0
  189. package/third-party-notices.txt +5571 -0
package/lib/lookup.mjs ADDED
@@ -0,0 +1,592 @@
1
+ // Unified single-query orchestration shared by Express and Netlify.
2
+ //
3
+ // The browser used to call the RDAP, WHOIS, and availability functions
4
+ // separately. On serverless deployments those functions do not share an
5
+ // in-memory cache, so availability repeated registry work that RDAP/WHOIS had
6
+ // just performed. This module fetches each registration source once and feeds
7
+ // those exact results into availability classification.
8
+ import { fetchRdapRecord, fetchRegistrarRdapRecord } from "./rdap.mjs";
9
+ import { buildWhoisChain, parseWhoisChain } from "./whois.mjs";
10
+ import { OPERATION_BUDGET_ERROR_CODE } from "./operation-budget.mjs";
11
+ import { checkDomainAvailability } from "./availability.mjs";
12
+ import { abortable } from "./abort.mjs";
13
+ import { collectDnsIntelligence, collectReverseDnsIntelligence, failedReverseDnsIntelligence, skippedReverseDnsIntelligence, } from "./dns-intelligence.mjs";
14
+ import { createSelectedDnsResolvers } from "./dns-resolver-selection.mjs";
15
+ import { collectObservedNetworkContext } from "./observed-network-context.mjs";
16
+ import { collectSecurityTxt, securityTxtUnavailable } from "./security-txt.mjs";
17
+ import { registryAccessDiagnosticFor } from "./registry-capabilities.mjs";
18
+ import { FEATURE_DISABLED_ERROR_CODE, featureDecision, networkFeaturePolicy } from "./feature-policy.mjs";
19
+ import { lookupUrlscanDomain } from "./urlscan-intelligence.mjs";
20
+ import { lookupUrlhausDomain } from "./urlhaus-intelligence.mjs";
21
+ import { lookupThreatfoxDomain } from "./threatfox-intelligence.mjs";
22
+ import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
23
+ import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
24
+ import { buildRegistryInsights } from "./registry-insights.mjs";
25
+ import { inspectSslblCertificate } from "./sslbl-intelligence.mjs";
26
+ import { normalizeLookupSourceSettlement, plannedLookupProgressSources, } from "./lookup-source-progress.mjs";
27
+ import { buildBulkComparisonEvidence } from "./bulk-comparison-evidence.mjs";
28
+ const LOOKUP_DIAGNOSTICS_VERSION = 8;
29
+ const LOOKUP_LEGACY_DIAGNOSTICS_VERSION = 7;
30
+ const LOOKUP_TIMING_VERSION = 1;
31
+ const MAX_LOOKUP_TIMING_MS = 120_000;
32
+ const LOOKUP_TIMING_SOURCE_ORDER = Object.freeze([
33
+ 'rdap',
34
+ 'whois',
35
+ 'domain_evidence',
36
+ 'reverse_dns',
37
+ 'registrar_rdap',
38
+ 'network_context',
39
+ 'security_txt',
40
+ 'external_intelligence',
41
+ 'malware_host_intelligence',
42
+ 'malware_ioc_intelligence',
43
+ ]);
44
+ const LOOKUP_ERROR_CODES = Object.freeze({
45
+ AUTH_REQUIRED: 'AUTH_REQUIRED',
46
+ RATE_LIMITED: 'RATE_LIMITED',
47
+ CONCURRENCY_LIMITED: OPERATION_BUDGET_ERROR_CODE,
48
+ FEATURE_DISABLED: FEATURE_DISABLED_ERROR_CODE,
49
+ MISSING_QUERY: 'MISSING_QUERY',
50
+ INVALID_QUERY: 'INVALID_QUERY',
51
+ LOOKUP_FAILED: 'LOOKUP_FAILED',
52
+ RDAP_UPSTREAM_FAILED: 'RDAP_UPSTREAM_FAILED',
53
+ RDAP_UNSUPPORTED: 'RDAP_UNSUPPORTED',
54
+ WHOIS_UPSTREAM_FAILED: 'WHOIS_UPSTREAM_FAILED',
55
+ AVAILABILITY_CHECK_FAILED: 'AVAILABILITY_CHECK_FAILED',
56
+ });
57
+ function errorMessage(err, fallback) {
58
+ const message = err && typeof err === 'object' && 'message' in err
59
+ ? err.message
60
+ : null;
61
+ return String(message || fallback);
62
+ }
63
+ function boundedSourceDetail(err, fallback) {
64
+ return errorMessage(err, fallback)
65
+ .replace(/[\u0000-\u001f\u007f]+/g, ' ')
66
+ .replace(/\s+/g, ' ')
67
+ .trim()
68
+ .slice(0, 240) || fallback;
69
+ }
70
+ function boundedTimingMs(value) {
71
+ if (!Number.isFinite(value))
72
+ return 0;
73
+ return Math.min(MAX_LOOKUP_TIMING_MS, Math.max(0, Math.round(value)));
74
+ }
75
+ function createLookupTimingTracker(enabled, now) {
76
+ const lookupStartedAt = now();
77
+ const entries = new Map();
78
+ function measure(source, operation) {
79
+ const sourceStartedAt = now();
80
+ return Promise.resolve()
81
+ .then(operation)
82
+ .then((value) => {
83
+ const finishedAt = now();
84
+ entries.set(source, {
85
+ source,
86
+ outcome: 'fulfilled',
87
+ durationMs: boundedTimingMs(finishedAt - sourceStartedAt),
88
+ completedAfterMs: boundedTimingMs(finishedAt - lookupStartedAt),
89
+ });
90
+ return value;
91
+ }, (error) => {
92
+ const finishedAt = now();
93
+ entries.set(source, {
94
+ source,
95
+ outcome: 'rejected',
96
+ durationMs: boundedTimingMs(finishedAt - sourceStartedAt),
97
+ completedAfterMs: boundedTimingMs(finishedAt - lookupStartedAt),
98
+ });
99
+ throw error;
100
+ });
101
+ }
102
+ return {
103
+ measure(source, operation) {
104
+ return enabled ? measure(source, operation) : Promise.resolve().then(operation);
105
+ },
106
+ snapshot() {
107
+ if (!enabled)
108
+ return null;
109
+ const totalMs = boundedTimingMs(now() - lookupStartedAt);
110
+ return {
111
+ version: LOOKUP_TIMING_VERSION,
112
+ totalMs,
113
+ sources: LOOKUP_TIMING_SOURCE_ORDER
114
+ .map((source) => entries.get(source))
115
+ .filter((entry) => Boolean(entry)),
116
+ };
117
+ },
118
+ };
119
+ }
120
+ async function runUnifiedLookup(classified, options = {}) {
121
+ const fetchRdap = options.fetchRdapRecord || fetchRdapRecord;
122
+ const fetchRegistrarRdap = options.fetchRegistrarRdapRecord || fetchRegistrarRdapRecord;
123
+ const fetchWhois = options.buildWhoisChain || buildWhoisChain;
124
+ const checkAvailability = options.checkDomainAvailability || checkDomainAvailability;
125
+ const selectedDnsResolvers = options.dnsResolverServers?.length
126
+ ? createSelectedDnsResolvers(options.dnsResolverServers)
127
+ : null;
128
+ const collectReverseDns = options.collectReverseDnsIntelligence || collectReverseDnsIntelligence;
129
+ const collectNetworkContext = options.collectObservedNetworkContext || collectObservedNetworkContext;
130
+ const collectDisclosureContacts = options.collectSecurityTxt || collectSecurityTxt;
131
+ const fetchUrlscanIntelligence = options.lookupUrlscanDomain || lookupUrlscanDomain;
132
+ const fetchUrlhausIntelligence = options.lookupUrlhausDomain || lookupUrlhausDomain;
133
+ const fetchThreatfoxIntelligence = options.lookupThreatfoxDomain || lookupThreatfoxDomain;
134
+ const inspectCertificateWarningData = options.inspectSslblCertificate || inspectSslblCertificate;
135
+ const fast = options.fast === true;
136
+ const compact = options.compact === true;
137
+ const externalIntelligence = options.externalIntelligence === true;
138
+ const malwareHostIntelligence = options.malwareHostIntelligence === true;
139
+ const malwareIocIntelligence = options.malwareIocIntelligence === true;
140
+ const securityTxtRequested = options.securityTxt === true;
141
+ const featurePolicy = options.featurePolicy || networkFeaturePolicy();
142
+ const timing = createLookupTimingTracker(!fast && !compact, options.now || Date.now);
143
+ const measure = (source, operation) => (timing.measure(source, () => abortable(operation, options.signal)));
144
+ const rdapEnabled = featureDecision('rdap', featurePolicy).enabled;
145
+ const whoisEnabled = featureDecision('whois', featurePolicy).enabled;
146
+ const availabilityEnabled = featureDecision('availability', featurePolicy).enabled;
147
+ const websiteProbeEnabled = featureDecision('website_probe', featurePolicy).enabled;
148
+ const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
149
+ const skipWhois = fast || !whoisEnabled;
150
+ const rdapPromise = rdapEnabled
151
+ ? measure('rdap', () => fetchRdap(classified.type, classified.value))
152
+ : Promise.resolve(null);
153
+ const whoisPromise = skipWhois
154
+ ? Promise.resolve(null)
155
+ : measure('whois', () => fetchWhois(classified.value));
156
+ // Registrar RDAP is a separately attributed deep-lookup enrichment. It may
157
+ // overlap the WHOIS chain, but it never joins the promises used to decide
158
+ // availability and can add up to its own bounded timeout to a deep lookup.
159
+ const registrarRdapPromise = classified.type === 'domain' && rdapEnabled && !fast && !compact
160
+ ? rdapPromise.then((record) => record && record.upstreamStatus === 200 && record.parsed
161
+ ? measure('registrar_rdap', () => fetchRegistrarRdap(classified.value, record))
162
+ : null)
163
+ : null;
164
+ const availabilityPromise = classified.type === 'domain' && availabilityEnabled
165
+ ? measure('domain_evidence', () => checkAvailability(classified.value, {
166
+ fast,
167
+ includeExtendedDnsContext: !compact,
168
+ includeInheritedCaa: !fast && !compact,
169
+ includeCredentialSurfaceProfile: !fast && !compact,
170
+ includeStructuredDataIdentity: !fast && !compact,
171
+ includeTechnologyProfile: !fast,
172
+ includeSecurityPosture: !compact,
173
+ featurePolicy,
174
+ rdapRecordPromise: rdapPromise,
175
+ whoisChainPromise: whoisPromise,
176
+ ...(selectedDnsResolvers ? {
177
+ dnsResolvers: selectedDnsResolvers,
178
+ resolveNs: selectedDnsResolvers.resolveNs,
179
+ collectDnsIntelligence,
180
+ } : {}),
181
+ }))
182
+ : null;
183
+ // Reverse DNS is separately attributed operator context for deep public-IP
184
+ // lookups. It is never used by RDAP, WHOIS, availability, Risk, fast,
185
+ // compact, Bulk, or monitoring contracts.
186
+ const reverseDnsEligible = (classified.type === 'ipv4' || classified.type === 'ipv6')
187
+ && !fast
188
+ && !compact;
189
+ const reverseDnsPromise = reverseDnsEligible && dnsIntelligenceEnabled
190
+ ? measure('reverse_dns', () => collectReverseDns(classified.value))
191
+ : null;
192
+ // Network registration is an additive deep-only source. It starts only
193
+ // after availability has produced the existing TLS/DNS observations and
194
+ // never joins the evidence used to decide domain availability. The shared
195
+ // RDAP helper supplies its existing three-minute cache and safe transport.
196
+ const networkContextPromise = classified.type === 'domain'
197
+ && rdapEnabled
198
+ && availabilityEnabled
199
+ && !fast
200
+ && !compact
201
+ && availabilityPromise
202
+ ? availabilityPromise.then((availability) => measure('network_context', () => collectNetworkContext(availability, { fetchRdapRecord: fetchRdap })), () => measure('network_context', () => collectNetworkContext({}, { fetchRdapRecord: fetchRdap })))
203
+ : null;
204
+ // security.txt is an explicit, additive deep-lookup action. It uses the
205
+ // exact queried hostname because RFC 9116 scopes a file to that hostname;
206
+ // the result never joins registration, availability, or Risk inputs.
207
+ const securityTxtPromise = securityTxtRequested
208
+ && classified.type === 'domain'
209
+ && websiteProbeEnabled
210
+ && !fast
211
+ && !compact
212
+ ? measure('security_txt', () => collectDisclosureContacts(classified.inputHostname))
213
+ : null;
214
+ const urlscanIntelligencePromise = externalIntelligence
215
+ && classified.type === 'domain'
216
+ && !fast
217
+ && !compact
218
+ ? measure('external_intelligence', () => fetchUrlscanIntelligence(classified.registrableDomain || classified.value))
219
+ : null;
220
+ const urlhausIntelligencePromise = malwareHostIntelligence
221
+ && classified.type === 'domain'
222
+ && !fast
223
+ && !compact
224
+ ? measure('malware_host_intelligence', () => fetchUrlhausIntelligence(classified.registrableDomain || classified.value))
225
+ : null;
226
+ const threatfoxIntelligencePromise = malwareIocIntelligence
227
+ && classified.type === 'domain'
228
+ && !fast
229
+ && !compact
230
+ ? measure('malware_ioc_intelligence', () => fetchThreatfoxIntelligence(classified.registrableDomain || classified.value))
231
+ : null;
232
+ // Optional incremental presentation observes the same promises used by the
233
+ // ordinary Lookup result. It starts no additional collection and cannot
234
+ // alter, reject, or persist a source result. The callback receives only a
235
+ // bounded source-health summary; raw payloads remain inside the final
236
+ // response path.
237
+ if (!fast && !compact && options.onSourceSettled) {
238
+ const sourcePromises = new Map([
239
+ ['rdap', rdapPromise],
240
+ ['whois', whoisPromise],
241
+ ['domain_evidence', availabilityPromise],
242
+ ['reverse_dns', reverseDnsPromise],
243
+ ['registrar_rdap', registrarRdapPromise],
244
+ ['network_context', networkContextPromise],
245
+ ['security_txt', securityTxtPromise],
246
+ ['external_intelligence', urlscanIntelligencePromise],
247
+ ['malware_host_intelligence', urlhausIntelligencePromise],
248
+ ['malware_ioc_intelligence', threatfoxIntelligencePromise],
249
+ ]);
250
+ const notify = (source, outcome, value) => {
251
+ try {
252
+ options.onSourceSettled?.(normalizeLookupSourceSettlement(source, outcome, value));
253
+ }
254
+ catch {
255
+ // Presentation callbacks must never change evidence collection.
256
+ }
257
+ };
258
+ for (const source of plannedLookupProgressSources(classified, {
259
+ externalIntelligence,
260
+ malwareHostIntelligence,
261
+ malwareIocIntelligence,
262
+ securityTxt: securityTxtRequested,
263
+ })) {
264
+ const sourcePromise = sourcePromises.get(source) ?? null;
265
+ void Promise.resolve(sourcePromise).then((value) => notify(source, 'fulfilled', value), (error) => notify(source, 'rejected', error));
266
+ }
267
+ }
268
+ const [rdapResult, whoisResult, availabilityResult, reverseDnsResult, registrarRdapResult, networkContextResult, securityTxtResult, urlscanIntelligenceResult, urlhausIntelligenceResult, threatfoxIntelligenceResult] = await Promise.allSettled([
269
+ rdapPromise,
270
+ whoisPromise,
271
+ availabilityPromise,
272
+ reverseDnsPromise,
273
+ registrarRdapPromise,
274
+ networkContextPromise,
275
+ securityTxtPromise,
276
+ urlscanIntelligencePromise,
277
+ urlhausIntelligencePromise,
278
+ threatfoxIntelligencePromise,
279
+ ]);
280
+ options.signal?.throwIfAborted();
281
+ const rdapRecord = rdapResult.status === 'fulfilled' ? rdapResult.value : null;
282
+ const whoisChain = whoisResult.status === 'fulfilled' ? whoisResult.value : null;
283
+ const rdapAttempts = rdapRecord && Array.isArray(rdapRecord.attempts)
284
+ ? rdapRecord.attempts
285
+ : rdapResult.status === 'rejected' && Array.isArray(rdapResult.reason?.attempts)
286
+ ? rdapResult.reason.attempts
287
+ : [];
288
+ let registrarRdap = null;
289
+ const registryRdapUsable = rdapRecord && rdapRecord.upstreamStatus === 200 && rdapRecord.parsed;
290
+ if (classified.type === 'domain' && rdapEnabled && !compact && registryRdapUsable) {
291
+ if (fast) {
292
+ registrarRdap = {
293
+ status: 'skipped',
294
+ detail: 'Registrar RDAP is omitted in fast RDAP-only mode.',
295
+ endpoint: null,
296
+ transportSecurity: null,
297
+ upstreamStatus: null,
298
+ fetchedAt: null,
299
+ attempt: null,
300
+ };
301
+ }
302
+ else if (registrarRdapResult.status === 'fulfilled') {
303
+ registrarRdap = registrarRdapResult.value;
304
+ }
305
+ else {
306
+ registrarRdap = registrarRdapResult.reason?.registrarRdap || {
307
+ status: 'error',
308
+ detail: boundedSourceDetail(registrarRdapResult.reason, 'Registrar RDAP lookup failed'),
309
+ endpoint: null,
310
+ transportSecurity: null,
311
+ upstreamStatus: null,
312
+ fetchedAt: null,
313
+ attempt: null,
314
+ };
315
+ }
316
+ }
317
+ const rdap = !rdapEnabled
318
+ ? { skipped: true, detail: 'RDAP is disabled by deployment policy.' }
319
+ : rdapRecord
320
+ ? { ...rdapRecord, ...(registrarRdap ? { registrarRdap } : {}) }
321
+ : {
322
+ error: rdapResult.status === 'rejected'
323
+ ? errorMessage(rdapResult.reason, 'RDAP lookup failed')
324
+ : 'No RDAP registry found for this query via IANA bootstrap',
325
+ attempts: rdapAttempts,
326
+ };
327
+ let whois;
328
+ if (!whoisEnabled) {
329
+ whois = { skipped: true, detail: 'WHOIS is disabled by deployment policy.' };
330
+ }
331
+ else if (skipWhois) {
332
+ whois = { skipped: true, detail: 'WHOIS is omitted in fast RDAP-only mode.' };
333
+ }
334
+ else if (Array.isArray(whoisChain)) {
335
+ whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain) };
336
+ }
337
+ else {
338
+ whois = {
339
+ error: whoisResult.status === 'rejected'
340
+ ? errorMessage(whoisResult.reason, 'WHOIS lookup failed')
341
+ : 'WHOIS returned no referral chain',
342
+ };
343
+ }
344
+ let availability = { applicable: false, type: classified.type };
345
+ if (classified.type === 'domain') {
346
+ if (!availabilityEnabled) {
347
+ availability = {
348
+ applicable: true,
349
+ domain: classified.value,
350
+ state: 'unknown',
351
+ confidence: 'low',
352
+ disabled: true,
353
+ detail: 'Availability analysis is disabled by deployment policy.',
354
+ };
355
+ }
356
+ else if (availabilityResult.status === 'fulfilled') {
357
+ const result = availabilityResult.value;
358
+ availability = {
359
+ applicable: true,
360
+ domain: classified.value,
361
+ inputHostname: classified.inputHostname,
362
+ registrableDomain: classified.registrableDomain,
363
+ isSubdomain: classified.isSubdomain,
364
+ ...result,
365
+ };
366
+ }
367
+ else {
368
+ availability = {
369
+ applicable: true,
370
+ domain: classified.value,
371
+ state: 'unknown',
372
+ confidence: 'low',
373
+ detail: errorMessage(availabilityResult.reason, 'Availability lookup failed'),
374
+ };
375
+ }
376
+ }
377
+ const rdapStatus = !rdapEnabled
378
+ ? 'disabled'
379
+ : rdapResult.status === 'rejected'
380
+ ? 'error'
381
+ : !rdapRecord
382
+ ? 'unsupported'
383
+ : rdapRecord.upstreamStatus === 404 ? 'not_found' : 'success';
384
+ const whoisStatus = !whoisEnabled
385
+ ? 'disabled'
386
+ : skipWhois
387
+ ? 'skipped'
388
+ : whoisResult.status === 'rejected' || !Array.isArray(whoisChain)
389
+ ? 'error'
390
+ : whoisChain.length === 1
391
+ ? 'unsupported'
392
+ : whois.parsed && whois.parsed.chainStatus === 'complete' ? 'complete' : 'partial';
393
+ const availabilityStatus = classified.type !== 'domain'
394
+ ? 'not_applicable'
395
+ : !availabilityEnabled ? 'disabled'
396
+ : availabilityResult.status === 'rejected' ? 'error' : 'complete';
397
+ const reverseDns = reverseDnsEligible
398
+ ? !dnsIntelligenceEnabled
399
+ ? skippedReverseDnsIntelligence()
400
+ : reverseDnsResult.status === 'fulfilled' && reverseDnsResult.value
401
+ ? reverseDnsResult.value
402
+ : failedReverseDnsIntelligence(reverseDnsResult.status === 'rejected'
403
+ ? reverseDnsResult.reason
404
+ : 'Reverse DNS lookup returned no result')
405
+ : null;
406
+ // This is static access-policy context only. It performs no network work and
407
+ // is deliberately excluded from compact Bulk responses and from every
408
+ // availability input.
409
+ const registryAccess = classified.type === 'domain' && !compact
410
+ ? registryAccessDiagnosticFor(classified.value)
411
+ : null;
412
+ const networkContext = networkContextPromise && networkContextResult.status === 'fulfilled'
413
+ ? networkContextResult.value
414
+ : null;
415
+ const securityTxt = securityTxtPromise
416
+ ? securityTxtResult.status === 'fulfilled'
417
+ ? securityTxtResult.value
418
+ : securityTxtUnavailable(classified.inputHostname, securityTxtResult.reason)
419
+ : null;
420
+ // SSLBL comparison is an exact, local, deep-only enrichment over the leaf
421
+ // certificate already observed by the bounded TLS collector. It performs no
422
+ // request, never contributes to availability, and is omitted from compact
423
+ // Bulk and monitoring responses.
424
+ const sslbl = classified.type === 'domain' && !fast && !compact
425
+ ? inspectCertificateWarningData(availability && typeof availability.tls === 'object' ? availability.tls : null, {
426
+ ...(options.sslblSnapshot === undefined ? {} : { snapshot: options.sslblSnapshot }),
427
+ ...(options.sslblNow === undefined ? {} : { now: options.sslblNow }),
428
+ })
429
+ : null;
430
+ const networkContextRdap = networkContext && typeof networkContext.rdap === 'object' && networkContext.rdap
431
+ ? networkContext.rdap
432
+ : null;
433
+ const networkContextEndpoint = networkContext && typeof networkContext.endpoint === 'object' && networkContext.endpoint
434
+ ? networkContext.endpoint
435
+ : null;
436
+ const lookupTiming = timing.snapshot();
437
+ const diagnostics = {
438
+ version: lookupTiming ? LOOKUP_DIAGNOSTICS_VERSION : LOOKUP_LEGACY_DIAGNOSTICS_VERSION,
439
+ ...(lookupTiming ? { timing: lookupTiming } : {}),
440
+ ...(registryAccess ? { registryAccess } : {}),
441
+ rdap: {
442
+ status: rdapStatus,
443
+ errorCode: rdapStatus === 'disabled'
444
+ ? LOOKUP_ERROR_CODES.FEATURE_DISABLED
445
+ : rdapStatus === 'error'
446
+ ? LOOKUP_ERROR_CODES.RDAP_UPSTREAM_FAILED
447
+ : rdapStatus === 'unsupported' ? LOOKUP_ERROR_CODES.RDAP_UNSUPPORTED : null,
448
+ endpoint: rdapRecord ? rdapRecord.rdapServer || null : null,
449
+ transportSecurity: rdapRecord ? rdapRecord.transportSecurity || null : null,
450
+ httpStatus: rdapRecord ? rdapRecord.upstreamStatus ?? null : null,
451
+ fetchedAt: rdapRecord ? rdapRecord.fetchedAt || null : null,
452
+ attempts: rdapAttempts,
453
+ ...(registrarRdap ? {
454
+ registrar: {
455
+ status: registrarRdap.status,
456
+ endpoint: registrarRdap.endpoint || null,
457
+ transportSecurity: registrarRdap.transportSecurity || null,
458
+ httpStatus: registrarRdap.upstreamStatus ?? null,
459
+ fetchedAt: registrarRdap.fetchedAt || null,
460
+ attempt: registrarRdap.attempt || null,
461
+ },
462
+ } : {}),
463
+ },
464
+ whois: {
465
+ status: whoisStatus,
466
+ errorCode: whoisStatus === 'disabled'
467
+ ? LOOKUP_ERROR_CODES.FEATURE_DISABLED
468
+ : whoisStatus === 'error' ? LOOKUP_ERROR_CODES.WHOIS_UPSTREAM_FAILED : null,
469
+ queriedAt: Array.isArray(whoisChain) && whoisChain[0] ? whoisChain[0].queriedAt || null : null,
470
+ authoritativeHop: whois.parsed ? whois.parsed.authoritativeHop || null : null,
471
+ failedHop: whois.parsed ? whois.parsed.failedHop || null : null,
472
+ conflictingHop: whois.parsed ? whois.parsed.conflictingHop || null : null,
473
+ },
474
+ availability: {
475
+ status: availabilityStatus,
476
+ errorCode: availabilityStatus === 'disabled'
477
+ ? LOOKUP_ERROR_CODES.FEATURE_DISABLED
478
+ : availabilityStatus === 'error' ? LOOKUP_ERROR_CODES.AVAILABILITY_CHECK_FAILED : null,
479
+ resultState: availability.applicable === true ? availability.state || 'unknown' : null,
480
+ },
481
+ ...(reverseDns ? {
482
+ reverseDns: {
483
+ status: reverseDns.status,
484
+ observedAt: reverseDns.observedAt,
485
+ complete: reverseDns.complete,
486
+ truncated: reverseDns.truncated,
487
+ },
488
+ } : {}),
489
+ ...(networkContext ? {
490
+ network: {
491
+ status: networkContext.status,
492
+ address: networkContextEndpoint?.address || null,
493
+ family: networkContextEndpoint?.family || null,
494
+ addressSource: networkContextEndpoint?.selectedFrom || null,
495
+ endpoint: networkContextRdap?.endpoint || null,
496
+ transportSecurity: networkContextRdap?.transportSecurity || null,
497
+ httpStatus: networkContextRdap?.httpStatus ?? null,
498
+ fetchedAt: networkContextRdap?.fetchedAt || null,
499
+ attempts: networkContextRdap?.attempts || [],
500
+ },
501
+ } : {}),
502
+ ...(securityTxt ? {
503
+ securityTxt: {
504
+ status: securityTxt.status,
505
+ state: securityTxt.state,
506
+ endpoint: securityTxt.finalUrl,
507
+ httpStatus: securityTxt.httpStatus,
508
+ observedAt: securityTxt.observedAt,
509
+ complete: securityTxt.complete,
510
+ truncated: securityTxt.truncated,
511
+ },
512
+ } : {}),
513
+ ...(sslbl ? {
514
+ sslbl: {
515
+ status: sslbl.status,
516
+ verdict: sslbl.verdict,
517
+ observedAt: sslbl.observedAt,
518
+ complete: sslbl.complete,
519
+ snapshotUpdatedAt: sslbl.snapshot.sourceUpdatedAt,
520
+ },
521
+ } : {}),
522
+ };
523
+ // Bulk triage only consumes the derived availability evidence and source
524
+ // diagnostics. Omitting raw RDAP JSON and multi-hop WHOIS bodies from that
525
+ // opt-in response prevents large scans from downloading and retaining the
526
+ // same registry payloads the backend already used to build `availability`.
527
+ if (compact) {
528
+ const bulkComparison = !fast
529
+ ? buildBulkComparisonEvidence(availability)
530
+ : null;
531
+ const { credentialSurfaceProfile: _credentialSurfaceProfile, structuredDataIdentity: _structuredDataIdentity, technologyProfile: _technologyProfile, pageRoleProfile: _pageRoleProfile, clientBehaviorProfile: _clientBehaviorProfile, securityPosture: _securityPosture, ...compactAvailability } = availability;
532
+ return {
533
+ availability: {
534
+ ...compactAvailability,
535
+ ...(bulkComparison ? { bulkComparison } : {}),
536
+ },
537
+ diagnostics,
538
+ };
539
+ }
540
+ const targetDomain = classified.registrableDomain || classified.value;
541
+ const threatIntelligenceProviders = [];
542
+ if (urlscanIntelligencePromise) {
543
+ threatIntelligenceProviders.push(urlscanIntelligenceResult.status === 'fulfilled' && urlscanIntelligenceResult.value
544
+ ? urlscanIntelligenceResult.value
545
+ : createThreatIntelligenceResult(URLSCAN_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Archived provider intelligence could not be completed.' }));
546
+ }
547
+ if (urlhausIntelligencePromise) {
548
+ threatIntelligenceProviders.push(urlhausIntelligenceResult.status === 'fulfilled' && urlhausIntelligenceResult.value
549
+ ? urlhausIntelligenceResult.value
550
+ : createThreatIntelligenceResult(URLHAUS_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Malware-host intelligence could not be completed.' }));
551
+ }
552
+ if (threatfoxIntelligencePromise) {
553
+ threatIntelligenceProviders.push(threatfoxIntelligenceResult.status === 'fulfilled' && threatfoxIntelligenceResult.value
554
+ ? threatfoxIntelligenceResult.value
555
+ : createThreatIntelligenceResult(THREATFOX_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Malware-IOC intelligence could not be completed.' }));
556
+ }
557
+ const threatIntelligence = threatIntelligenceProviders.length
558
+ ? { version: 1, providers: threatIntelligenceProviders }
559
+ : null;
560
+ const registrarRdapParsed = registrarRdap
561
+ && typeof registrarRdap.parsed === 'object'
562
+ && registrarRdap.parsed
563
+ && !Array.isArray(registrarRdap.parsed)
564
+ ? registrarRdap.parsed
565
+ : null;
566
+ const registryInsights = classified.type === 'domain' && !fast && !compact
567
+ ? buildRegistryInsights({
568
+ rdapParsed: rdapRecord?.parsed,
569
+ rdapStatus,
570
+ rdapFetchedAt: rdapRecord?.fetchedAt,
571
+ whoisParsed: whois.parsed,
572
+ whoisStatus,
573
+ whoisQueriedAt: Array.isArray(whoisChain) && whoisChain[0] ? whoisChain[0].queriedAt : null,
574
+ registrarRdapParsed,
575
+ registrarRdapStatus: registrarRdap?.status,
576
+ registrarRdapFetchedAt: registrarRdap?.fetchedAt,
577
+ })
578
+ : null;
579
+ return {
580
+ rdap,
581
+ whois,
582
+ availability,
583
+ diagnostics,
584
+ ...(registryInsights ? { registryInsights } : {}),
585
+ ...(reverseDns ? { reverseDns } : {}),
586
+ ...(networkContext ? { networkContext } : {}),
587
+ ...(securityTxt ? { securityTxt } : {}),
588
+ ...(sslbl ? { sslbl } : {}),
589
+ ...(threatIntelligence ? { threatIntelligence } : {}),
590
+ };
591
+ }
592
+ export { runUnifiedLookup, LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, LOOKUP_TIMING_VERSION, MAX_LOOKUP_TIMING_MS, LOOKUP_ERROR_CODES, };