@slicedearth/whoisleuth-cli 2.0.1 → 2.2.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 (97) hide show
  1. package/cli/arguments.mjs +21 -5
  2. package/cli/artifact-validation/case-response.mjs +771 -0
  3. package/cli/artifact-validation/investigation-capsule.mjs +426 -0
  4. package/cli/artifact-validation/lookup-evidence.mjs +718 -0
  5. package/cli/artifact-validation/signed-review.mjs +598 -0
  6. package/cli/artifact-validation/structure-primitives.mjs +105 -0
  7. package/cli/artifact-verify.mjs +1 -1
  8. package/cli/assurance-command-runner.mjs +324 -0
  9. package/cli/command-reference.mjs +1175 -843
  10. package/cli/completion.mjs +56 -9
  11. package/cli/discriminated-command-handlers.mjs +11 -0
  12. package/cli/doctor.mjs +2 -2
  13. package/cli/evidence-command-runner.mjs +41 -31
  14. package/cli/export-evidence.mjs +1 -1
  15. package/cli/fail-policy.mjs +33 -8
  16. package/cli/formatters/terminal-command-formats.mjs +437 -0
  17. package/cli/formatters/terminal-lookup.mjs +758 -0
  18. package/cli/formatters/terminal-metadata.mjs +58 -0
  19. package/cli/formatters/terminal-shared.mjs +83 -0
  20. package/cli/formatters/terminal.mjs +5 -1232
  21. package/cli/history-command-runner.mjs +132 -0
  22. package/cli/inline-command-families.mjs +45 -0
  23. package/cli/inline-command-runner.mjs +55 -0
  24. package/cli/investigation-plan.mjs +9 -2
  25. package/cli/investigation-run.mjs +114 -9
  26. package/cli/lookup-brief.mjs +141 -23
  27. package/cli/lookup-browser-view.mjs +289 -0
  28. package/cli/lookup-browser.mjs +3 -288
  29. package/cli/mail-review.mjs +2 -6
  30. package/cli/offline-artifact-validation.mjs +19 -0
  31. package/cli/review-command-runner.mjs +269 -0
  32. package/cli/runner.mjs +15 -972
  33. package/cli/saved-lookup.mjs +19 -2
  34. package/cli/support-command-runner.mjs +229 -0
  35. package/cli/workflow-command-runner.mjs +129 -0
  36. package/docs/cli-reference.md +16 -9
  37. package/docs/cli.md +16 -2
  38. package/lib/availability.mjs +12 -9
  39. package/lib/bounded-contract-normalizers.mjs +2 -9
  40. package/lib/browser-library-profile.mjs +1 -2
  41. package/lib/client-behavior-profile.mjs +1 -2
  42. package/lib/credential-surface-profile.mjs +1 -1
  43. package/lib/domain-posture-analysis.mjs +10 -1
  44. package/lib/domain-posture.mjs +51 -23
  45. package/lib/evidence-export.mjs +47 -5
  46. package/lib/evidence-report-markdown.mjs +4 -0
  47. package/lib/evidence-report.mjs +50 -0
  48. package/lib/generated/cisa-kev-catalog.mjs +34 -4
  49. package/lib/generated/registrar-standing-catalogue.mjs +367 -0
  50. package/lib/json-record.mjs +12 -0
  51. package/lib/lookup-child-profile-contract.mjs +858 -0
  52. package/lib/lookup-contract-primitives.mjs +98 -0
  53. package/lib/lookup-diagnostics.mjs +96 -0
  54. package/lib/lookup-network-evidence-bounds.mjs +1 -0
  55. package/lib/lookup-response.mjs +367 -0
  56. package/lib/lookup.mjs +28 -443
  57. package/lib/page-role-profile.mjs +1 -3
  58. package/lib/registrar-standing-catalogue-contract.mjs +70 -0
  59. package/lib/registrar-standing-contract.mjs +300 -0
  60. package/lib/registrar-standing.mjs +269 -0
  61. package/lib/registry-insights.mjs +2 -8
  62. package/lib/risk-scoring.mjs +26 -10
  63. package/lib/sslbl-certificates.generated.mjs +138 -136
  64. package/lib/structured-data-identity.mjs +1 -3
  65. package/lib/technology-evidence-role.mjs +51 -0
  66. package/lib/threat-intelligence-runtime.mjs +3 -3
  67. package/lib/threat-intelligence-types.mjs +13 -0
  68. package/lib/threatfox-intelligence.mjs +1 -3
  69. package/lib/tls-intelligence.mjs +1 -2
  70. package/lib/urlhaus-intelligence.mjs +1 -3
  71. package/lib/urlscan-intelligence.mjs +1 -3
  72. package/lib/website-security-posture.mjs +1 -2
  73. package/lib/website-technology.mjs +72 -23
  74. package/lib/whois-common-formats.mjs +276 -0
  75. package/lib/whois-dialects.mjs +534 -0
  76. package/lib/whois-parser-support.mjs +305 -0
  77. package/lib/whois-parser.mjs +135 -0
  78. package/lib/whois.mjs +6 -1187
  79. package/package.json +2 -2
  80. package/packages/cases/case-evidence-model.mjs +9 -0
  81. package/packages/cases/case-migration-model.mjs +17 -11
  82. package/packages/cases/case-record-operations.mjs +235 -0
  83. package/packages/cases/case-response-model.mjs +13 -9
  84. package/packages/cases/case-response-packet.mjs +297 -112
  85. package/packages/comparison/page-similarity.mjs +23 -0
  86. package/packages/contracts/case-portability.mjs +235 -61
  87. package/packages/contracts/cli-lookup.mjs +4 -3
  88. package/packages/contracts/domain-control-manifest.mjs +1 -1
  89. package/packages/contracts/investigation-portability.mjs +1 -1
  90. package/packages/contracts/risk-calibration.mjs +2 -2
  91. package/packages/contracts/workspace-portability.mjs +34 -6
  92. package/packages/interchange/external-findings-import.mjs +113 -45
  93. package/packages/investigation/evidence-coverage-ledger.mjs +21 -0
  94. package/packages/investigation/lookup-claim-readiness.mjs +2 -2
  95. package/packages/workspace/website-snapshot-model.mjs +79 -6
  96. package/third-party-notices.txt +1 -1
  97. package/cli/artifact-structure.mjs +0 -2507
@@ -0,0 +1,98 @@
1
+ // Shared strict primitives for the Lookup response envelope and its owned
2
+ // child-profile contracts. These checks validate without normalising or
3
+ // copying untrusted values.
4
+ import { MAX_OBSERVATION_DIAGNOSTICS } from "../packages/evidence/observation.mjs";
5
+ import { MAX_HTTP_PROVENANCE_URL } from "./http-evidence-bounds.mjs";
6
+ const LOOKUP_CONTROL_CHAR_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
7
+ const MAX_LOOKUP_CONTRACT_HOST_LENGTH = 253;
8
+ function isJsonObject(value) {
9
+ return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
10
+ }
11
+ function validBoundedString(value, maximum, allowEmpty = false) {
12
+ return typeof value === 'string'
13
+ && (allowEmpty || Boolean(value))
14
+ && value.length <= maximum
15
+ && !LOOKUP_CONTROL_CHAR_RE.test(value);
16
+ }
17
+ function validUint(value, maximum) {
18
+ return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum;
19
+ }
20
+ function validStringArray(value, maximumItems, maximumLength) {
21
+ return Array.isArray(value)
22
+ && value.length <= maximumItems
23
+ && value.every((item) => validBoundedString(item, maximumLength));
24
+ }
25
+ function validOptionalNullableText(value, maximumLength) {
26
+ return value === undefined || value === null || validBoundedString(value, maximumLength);
27
+ }
28
+ function validHttpProvenanceUrl(value) {
29
+ if (typeof value !== 'string'
30
+ || !value
31
+ || value.length > MAX_HTTP_PROVENANCE_URL
32
+ || LOOKUP_CONTROL_CHAR_RE.test(value))
33
+ return false;
34
+ try {
35
+ const parsed = new URL(value);
36
+ return ['http:', 'https:'].includes(parsed.protocol)
37
+ && Boolean(parsed.hostname)
38
+ && !parsed.username
39
+ && !parsed.password
40
+ && !parsed.search
41
+ && !parsed.hash
42
+ && parsed.href === value;
43
+ }
44
+ catch {
45
+ return false;
46
+ }
47
+ }
48
+ function normalizedDomain(value) {
49
+ if (typeof value !== 'string'
50
+ || !value.trim()
51
+ || value.length > MAX_LOOKUP_CONTRACT_HOST_LENGTH
52
+ || LOOKUP_CONTROL_CHAR_RE.test(value)
53
+ || /[\s/?#@\\:]/u.test(value)) {
54
+ return null;
55
+ }
56
+ try {
57
+ const parsed = new URL(`https://${value.trim().replace(/\.$/u, '')}/`);
58
+ const hostname = parsed.hostname.toLowerCase().replace(/\.$/u, '');
59
+ const labels = hostname.split('.');
60
+ return labels.length >= 2
61
+ && hostname.length <= MAX_LOOKUP_CONTRACT_HOST_LENGTH
62
+ && labels.every((label) => (label.length <= 63
63
+ && /^[a-z0-9](?:[a-z0-9-]*[a-z0-9])?$/u.test(label)))
64
+ ? hostname
65
+ : null;
66
+ }
67
+ catch {
68
+ return null;
69
+ }
70
+ }
71
+ function validObservationDiagnosticValue(value, depth = 0) {
72
+ if (typeof value === 'boolean')
73
+ return true;
74
+ if (typeof value === 'number')
75
+ return Number.isFinite(value);
76
+ if (typeof value === 'string') {
77
+ return value.length <= 240 && !LOOKUP_CONTROL_CHAR_RE.test(value);
78
+ }
79
+ if (!isJsonObject(value) || depth >= 2)
80
+ return false;
81
+ const keys = Object.keys(value);
82
+ return keys.length <= 6
83
+ && keys.every((key) => ['status', 'error', 'detail', 'truncated', 'discarded', 'count'].includes(key))
84
+ && keys.every((key) => validObservationDiagnosticValue(value[key], depth + 1));
85
+ }
86
+ function validObservationDiagnostics(value) {
87
+ if (value === undefined)
88
+ return true;
89
+ if (!isJsonObject(value))
90
+ return false;
91
+ const entries = Object.entries(value);
92
+ return entries.length <= MAX_OBSERVATION_DIAGNOSTICS
93
+ && entries.every(([key, item]) => (Boolean(key)
94
+ && key.length <= 40
95
+ && /^[a-z0-9_-]+$/iu.test(key)
96
+ && validObservationDiagnosticValue(item)));
97
+ }
98
+ export { LOOKUP_CONTROL_CHAR_RE, isJsonObject, normalizedDomain, validBoundedString, validHttpProvenanceUrl, validObservationDiagnostics, validOptionalNullableText, validStringArray, validUint, };
@@ -0,0 +1,96 @@
1
+ // Lookup diagnostics, bounded source errors and optional deep-lookup timing.
2
+ import { OPERATION_BUDGET_ERROR_CODE } from "./operation-budget.mjs";
3
+ import { FEATURE_DISABLED_ERROR_CODE } from "./feature-policy.mjs";
4
+ const LOOKUP_DIAGNOSTICS_VERSION = 8;
5
+ const LOOKUP_LEGACY_DIAGNOSTICS_VERSION = 7;
6
+ const LOOKUP_TIMING_VERSION = 1;
7
+ const MAX_LOOKUP_TIMING_MS = 120_000;
8
+ const LOOKUP_TIMING_SOURCE_ORDER = Object.freeze([
9
+ 'rdap',
10
+ 'whois',
11
+ 'domain_evidence',
12
+ 'reverse_dns',
13
+ 'registrar_rdap',
14
+ 'network_context',
15
+ 'security_txt',
16
+ 'external_intelligence',
17
+ 'malware_host_intelligence',
18
+ 'malware_ioc_intelligence',
19
+ ]);
20
+ const LOOKUP_ERROR_CODES = Object.freeze({
21
+ AUTH_REQUIRED: 'AUTH_REQUIRED',
22
+ RATE_LIMITED: 'RATE_LIMITED',
23
+ CONCURRENCY_LIMITED: OPERATION_BUDGET_ERROR_CODE,
24
+ FEATURE_DISABLED: FEATURE_DISABLED_ERROR_CODE,
25
+ MISSING_QUERY: 'MISSING_QUERY',
26
+ INVALID_QUERY: 'INVALID_QUERY',
27
+ LOOKUP_FAILED: 'LOOKUP_FAILED',
28
+ RDAP_UPSTREAM_FAILED: 'RDAP_UPSTREAM_FAILED',
29
+ RDAP_UNSUPPORTED: 'RDAP_UNSUPPORTED',
30
+ WHOIS_UPSTREAM_FAILED: 'WHOIS_UPSTREAM_FAILED',
31
+ AVAILABILITY_CHECK_FAILED: 'AVAILABILITY_CHECK_FAILED',
32
+ });
33
+ function errorMessage(err, fallback) {
34
+ const message = err && typeof err === 'object' && 'message' in err
35
+ ? err.message
36
+ : null;
37
+ return String(message || fallback);
38
+ }
39
+ function boundedSourceDetail(err, fallback) {
40
+ return errorMessage(err, fallback)
41
+ .replace(/[\u0000-\u001f\u007f]+/g, ' ')
42
+ .replace(/\s+/g, ' ')
43
+ .trim()
44
+ .slice(0, 240) || fallback;
45
+ }
46
+ function boundedTimingMs(value) {
47
+ if (!Number.isFinite(value))
48
+ return 0;
49
+ return Math.min(MAX_LOOKUP_TIMING_MS, Math.max(0, Math.round(value)));
50
+ }
51
+ function createLookupTimingTracker(enabled, now) {
52
+ const lookupStartedAt = now();
53
+ const entries = new Map();
54
+ function measure(source, operation) {
55
+ const sourceStartedAt = now();
56
+ return Promise.resolve()
57
+ .then(operation)
58
+ .then((value) => {
59
+ const finishedAt = now();
60
+ entries.set(source, {
61
+ source,
62
+ outcome: 'fulfilled',
63
+ durationMs: boundedTimingMs(finishedAt - sourceStartedAt),
64
+ completedAfterMs: boundedTimingMs(finishedAt - lookupStartedAt),
65
+ });
66
+ return value;
67
+ }, (error) => {
68
+ const finishedAt = now();
69
+ entries.set(source, {
70
+ source,
71
+ outcome: 'rejected',
72
+ durationMs: boundedTimingMs(finishedAt - sourceStartedAt),
73
+ completedAfterMs: boundedTimingMs(finishedAt - lookupStartedAt),
74
+ });
75
+ throw error;
76
+ });
77
+ }
78
+ return {
79
+ measure(source, operation) {
80
+ return enabled ? measure(source, operation) : Promise.resolve().then(operation);
81
+ },
82
+ snapshot() {
83
+ if (!enabled)
84
+ return null;
85
+ const totalMs = boundedTimingMs(now() - lookupStartedAt);
86
+ return {
87
+ version: LOOKUP_TIMING_VERSION,
88
+ totalMs,
89
+ sources: LOOKUP_TIMING_SOURCE_ORDER
90
+ .map((source) => entries.get(source))
91
+ .filter((entry) => Boolean(entry)),
92
+ };
93
+ },
94
+ };
95
+ }
96
+ export { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, LOOKUP_TIMING_VERSION, MAX_LOOKUP_TIMING_MS, LOOKUP_ERROR_CODES, boundedSourceDetail, createLookupTimingTracker, errorMessage, };
@@ -2,6 +2,7 @@
2
2
  // the Lookup response trust boundary, and the display projection.
3
3
  export const MAX_LOOKUP_DNS_RECORDS_PER_TYPE = 16;
4
4
  export const MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS = 8;
5
+ export const TLS_PROFILE_VERSION = 3;
5
6
  export const MAX_LOOKUP_TLS_CHAIN_CERTIFICATES = 8;
6
7
  export const MAX_LOOKUP_TLS_ALT_NAMES = 50;
7
8
  export const MAX_LOOKUP_TLS_NAME_VALUES = 4;
@@ -0,0 +1,367 @@
1
+ // Lookup result shaping after the source plan has settled. This module starts no
2
+ // collection and preserves compact, diagnostics and enrichment response shapes.
3
+ import { buildBulkComparisonEvidence } from "./bulk-comparison-evidence.mjs";
4
+ import { failedReverseDnsIntelligence, skippedReverseDnsIntelligence, } from "./dns-intelligence.mjs";
5
+ import { LOOKUP_DIAGNOSTICS_VERSION, LOOKUP_ERROR_CODES, LOOKUP_LEGACY_DIAGNOSTICS_VERSION, boundedSourceDetail, errorMessage, } from "./lookup-diagnostics.mjs";
6
+ import { createThreatIntelligenceResult } from "./threat-intelligence-contract.mjs";
7
+ import { THREATFOX_PROVIDER, URLHAUS_PROVIDER, URLSCAN_PROVIDER, } from "./lookup-threat-provider-inventory.mjs";
8
+ import { THREAT_INTELLIGENCE_ENVELOPE_VERSION } from "./threat-intelligence-types.mjs";
9
+ import { registryAccessDiagnosticFor } from "./registry-capabilities.mjs";
10
+ import { buildRegistryInsights } from "./registry-insights.mjs";
11
+ import { buildRegistrarStanding } from "./registrar-standing.mjs";
12
+ import { resolveRegistrarIanaId } from "./registrar-standing-contract.mjs";
13
+ import { securityTxtUnavailable } from "./security-txt.mjs";
14
+ import { parseWhoisChain } from "./whois.mjs";
15
+ function withoutNestedPublicationMetadata(value) {
16
+ if (!value || typeof value !== 'object' || Array.isArray(value))
17
+ return value;
18
+ const output = { ...value };
19
+ delete output.publicationMetadata;
20
+ return output;
21
+ }
22
+ function withoutNestedDeliveryMetadata(value) {
23
+ if (!value || typeof value !== 'object' || Array.isArray(value))
24
+ return value;
25
+ const output = { ...value };
26
+ if (output.response && typeof output.response === 'object' && !Array.isArray(output.response)) {
27
+ const response = { ...output.response };
28
+ delete response.deliveryMetadata;
29
+ output.response = response;
30
+ }
31
+ return output;
32
+ }
33
+ async function buildUnifiedLookupResponse(context) {
34
+ const { availabilityEnabled, availabilityPromise, classified, compact, dnsIntelligenceEnabled, fast, inspectCertificateWarningData, networkContextPromise, options, rdapEnabled, rdapPromise, registrarRdapPromise, reverseDnsEligible, reverseDnsPromise, securityTxtPromise, skipWhois, threatfoxIntelligencePromise, timing, urlhausIntelligencePromise, urlscanIntelligencePromise, whoisEnabled, whoisPromise, } = context;
35
+ const [rdapResult, whoisResult, availabilityResult, reverseDnsResult, registrarRdapResult, networkContextResult, securityTxtResult, urlscanIntelligenceResult, urlhausIntelligenceResult, threatfoxIntelligenceResult] = await Promise.allSettled([
36
+ rdapPromise,
37
+ whoisPromise,
38
+ availabilityPromise,
39
+ reverseDnsPromise,
40
+ registrarRdapPromise,
41
+ networkContextPromise,
42
+ securityTxtPromise,
43
+ urlscanIntelligencePromise,
44
+ urlhausIntelligencePromise,
45
+ threatfoxIntelligencePromise,
46
+ ]);
47
+ options.signal?.throwIfAborted();
48
+ const rdapRecord = rdapResult.status === 'fulfilled' ? rdapResult.value : null;
49
+ const whoisChain = whoisResult.status === 'fulfilled' ? whoisResult.value : null;
50
+ const rdapAttempts = rdapRecord && Array.isArray(rdapRecord.attempts)
51
+ ? rdapRecord.attempts
52
+ : rdapResult.status === 'rejected' && Array.isArray(rdapResult.reason?.attempts)
53
+ ? rdapResult.reason.attempts
54
+ : [];
55
+ let registrarRdap = null;
56
+ const registryRdapUsable = rdapRecord && rdapRecord.upstreamStatus === 200 && rdapRecord.parsed;
57
+ if (classified.type === 'domain' && rdapEnabled && !compact && registryRdapUsable) {
58
+ if (fast) {
59
+ registrarRdap = {
60
+ status: 'skipped',
61
+ detail: 'Registrar RDAP is omitted in fast RDAP-only mode.',
62
+ endpoint: null,
63
+ transportSecurity: null,
64
+ upstreamStatus: null,
65
+ fetchedAt: null,
66
+ attempt: null,
67
+ };
68
+ }
69
+ else if (registrarRdapResult.status === 'fulfilled') {
70
+ registrarRdap = registrarRdapResult.value;
71
+ }
72
+ else {
73
+ registrarRdap = registrarRdapResult.reason?.registrarRdap || {
74
+ status: 'error',
75
+ detail: boundedSourceDetail(registrarRdapResult.reason, 'Registrar RDAP lookup failed'),
76
+ endpoint: null,
77
+ transportSecurity: null,
78
+ upstreamStatus: null,
79
+ fetchedAt: null,
80
+ attempt: null,
81
+ };
82
+ }
83
+ }
84
+ const rdap = !rdapEnabled
85
+ ? { skipped: true, detail: 'RDAP is disabled by deployment policy.' }
86
+ : rdapRecord
87
+ ? { ...rdapRecord, ...(registrarRdap ? { registrarRdap } : {}) }
88
+ : {
89
+ error: rdapResult.status === 'rejected'
90
+ ? boundedSourceDetail(rdapResult.reason, 'RDAP lookup failed')
91
+ : 'No RDAP registry found for this query via IANA bootstrap',
92
+ attempts: rdapAttempts,
93
+ };
94
+ let whois;
95
+ if (!whoisEnabled) {
96
+ whois = { skipped: true, detail: 'WHOIS is disabled by deployment policy.' };
97
+ }
98
+ else if (skipWhois) {
99
+ whois = { skipped: true, detail: 'WHOIS is omitted in fast RDAP-only mode.' };
100
+ }
101
+ else if (Array.isArray(whoisChain)) {
102
+ whois = { chain: whoisChain, parsed: parseWhoisChain(whoisChain) };
103
+ }
104
+ else {
105
+ whois = {
106
+ error: whoisResult.status === 'rejected'
107
+ ? boundedSourceDetail(whoisResult.reason, 'WHOIS lookup failed')
108
+ : 'WHOIS returned no referral chain',
109
+ };
110
+ }
111
+ let availability = { applicable: false, type: classified.type };
112
+ if (classified.type === 'domain') {
113
+ if (!availabilityEnabled) {
114
+ availability = {
115
+ applicable: true,
116
+ domain: classified.value,
117
+ state: 'unknown',
118
+ confidence: 'low',
119
+ disabled: true,
120
+ detail: 'Availability analysis is disabled by deployment policy.',
121
+ };
122
+ }
123
+ else if (availabilityResult.status === 'fulfilled') {
124
+ const result = availabilityResult.value;
125
+ availability = {
126
+ applicable: true,
127
+ domain: classified.value,
128
+ inputHostname: classified.inputHostname,
129
+ registrableDomain: classified.registrableDomain,
130
+ isSubdomain: classified.isSubdomain,
131
+ ...result,
132
+ };
133
+ }
134
+ else {
135
+ availability = {
136
+ applicable: true,
137
+ domain: classified.value,
138
+ state: 'unknown',
139
+ confidence: 'low',
140
+ detail: errorMessage(availabilityResult.reason, 'Availability lookup failed'),
141
+ };
142
+ }
143
+ }
144
+ const rdapStatus = !rdapEnabled
145
+ ? 'disabled'
146
+ : rdapResult.status === 'rejected'
147
+ ? 'error'
148
+ : !rdapRecord
149
+ ? 'unsupported'
150
+ : rdapRecord.upstreamStatus === 404 ? 'not_found' : 'success';
151
+ const whoisStatus = !whoisEnabled
152
+ ? 'disabled'
153
+ : skipWhois
154
+ ? 'skipped'
155
+ : whoisResult.status === 'rejected' || !Array.isArray(whoisChain)
156
+ ? 'error'
157
+ : whoisChain.length === 1
158
+ ? 'unsupported'
159
+ : whois.parsed && whois.parsed.chainStatus === 'complete' ? 'complete' : 'partial';
160
+ const availabilityStatus = classified.type !== 'domain'
161
+ ? 'not_applicable'
162
+ : !availabilityEnabled ? 'disabled'
163
+ : availabilityResult.status === 'rejected' ? 'error' : 'complete';
164
+ const reverseDns = reverseDnsEligible
165
+ ? !dnsIntelligenceEnabled
166
+ ? skippedReverseDnsIntelligence()
167
+ : reverseDnsResult.status === 'fulfilled' && reverseDnsResult.value
168
+ ? reverseDnsResult.value
169
+ : failedReverseDnsIntelligence(reverseDnsResult.status === 'rejected'
170
+ ? reverseDnsResult.reason
171
+ : 'Reverse DNS lookup returned no result')
172
+ : null;
173
+ // This is static access-policy context only. It performs no network work and
174
+ // is deliberately excluded from compact Bulk responses and from every
175
+ // availability input.
176
+ const registryAccess = classified.type === 'domain' && !compact
177
+ ? registryAccessDiagnosticFor(classified.value)
178
+ : null;
179
+ const networkContext = networkContextPromise && networkContextResult.status === 'fulfilled'
180
+ ? networkContextResult.value
181
+ : null;
182
+ const securityTxt = securityTxtPromise
183
+ ? securityTxtResult.status === 'fulfilled'
184
+ ? securityTxtResult.value
185
+ : securityTxtUnavailable(classified.inputHostname, securityTxtResult.reason)
186
+ : null;
187
+ // SSLBL comparison is an exact, local, deep-only enrichment over the leaf
188
+ // certificate already observed by the bounded TLS collector. It performs no
189
+ // request, never contributes to availability, and is omitted from compact
190
+ // Bulk and monitoring responses.
191
+ const sslbl = classified.type === 'domain' && !fast && !compact
192
+ ? inspectCertificateWarningData(availability && typeof availability.tls === 'object' ? availability.tls : null, {
193
+ ...(options.sslblSnapshot === undefined ? {} : { snapshot: options.sslblSnapshot }),
194
+ ...(options.sslblNow === undefined ? {} : { now: options.sslblNow }),
195
+ })
196
+ : null;
197
+ const networkContextRdap = networkContext && typeof networkContext.rdap === 'object' && networkContext.rdap
198
+ ? networkContext.rdap
199
+ : null;
200
+ const networkContextEndpoint = networkContext && typeof networkContext.endpoint === 'object' && networkContext.endpoint
201
+ ? networkContext.endpoint
202
+ : null;
203
+ const lookupTiming = timing.snapshot();
204
+ const diagnostics = {
205
+ version: lookupTiming ? LOOKUP_DIAGNOSTICS_VERSION : LOOKUP_LEGACY_DIAGNOSTICS_VERSION,
206
+ ...(lookupTiming ? { timing: lookupTiming } : {}),
207
+ ...(registryAccess ? { registryAccess } : {}),
208
+ rdap: {
209
+ status: rdapStatus,
210
+ errorCode: rdapStatus === 'disabled'
211
+ ? LOOKUP_ERROR_CODES.FEATURE_DISABLED
212
+ : rdapStatus === 'error'
213
+ ? LOOKUP_ERROR_CODES.RDAP_UPSTREAM_FAILED
214
+ : rdapStatus === 'unsupported' ? LOOKUP_ERROR_CODES.RDAP_UNSUPPORTED : null,
215
+ endpoint: rdapRecord ? rdapRecord.rdapServer || null : null,
216
+ transportSecurity: rdapRecord ? rdapRecord.transportSecurity || null : null,
217
+ httpStatus: rdapRecord ? rdapRecord.upstreamStatus ?? null : null,
218
+ fetchedAt: rdapRecord ? rdapRecord.fetchedAt || null : null,
219
+ attempts: rdapAttempts,
220
+ ...(registrarRdap ? {
221
+ registrar: {
222
+ status: registrarRdap.status,
223
+ endpoint: registrarRdap.endpoint || null,
224
+ transportSecurity: registrarRdap.transportSecurity || null,
225
+ httpStatus: registrarRdap.upstreamStatus ?? null,
226
+ fetchedAt: registrarRdap.fetchedAt || null,
227
+ attempt: registrarRdap.attempt || null,
228
+ },
229
+ } : {}),
230
+ },
231
+ whois: {
232
+ status: whoisStatus,
233
+ errorCode: whoisStatus === 'disabled'
234
+ ? LOOKUP_ERROR_CODES.FEATURE_DISABLED
235
+ : whoisStatus === 'error' ? LOOKUP_ERROR_CODES.WHOIS_UPSTREAM_FAILED : null,
236
+ queriedAt: Array.isArray(whoisChain) && whoisChain[0] ? whoisChain[0].queriedAt || null : null,
237
+ authoritativeHop: whois.parsed ? whois.parsed.authoritativeHop || null : null,
238
+ failedHop: whois.parsed ? whois.parsed.failedHop || null : null,
239
+ conflictingHop: whois.parsed ? whois.parsed.conflictingHop || null : null,
240
+ },
241
+ availability: {
242
+ status: availabilityStatus,
243
+ errorCode: availabilityStatus === 'disabled'
244
+ ? LOOKUP_ERROR_CODES.FEATURE_DISABLED
245
+ : availabilityStatus === 'error' ? LOOKUP_ERROR_CODES.AVAILABILITY_CHECK_FAILED : null,
246
+ resultState: availability.applicable === true ? availability.state || 'unknown' : null,
247
+ },
248
+ ...(reverseDns ? {
249
+ reverseDns: {
250
+ status: reverseDns.status,
251
+ observedAt: reverseDns.observedAt,
252
+ complete: reverseDns.complete,
253
+ truncated: reverseDns.truncated,
254
+ },
255
+ } : {}),
256
+ ...(networkContext ? {
257
+ network: {
258
+ status: networkContext.status,
259
+ address: networkContextEndpoint?.address || null,
260
+ family: networkContextEndpoint?.family || null,
261
+ addressSource: networkContextEndpoint?.selectedFrom || null,
262
+ endpoint: networkContextRdap?.endpoint || null,
263
+ transportSecurity: networkContextRdap?.transportSecurity || null,
264
+ httpStatus: networkContextRdap?.httpStatus ?? null,
265
+ fetchedAt: networkContextRdap?.fetchedAt || null,
266
+ attempts: networkContextRdap?.attempts || [],
267
+ },
268
+ } : {}),
269
+ ...(securityTxt ? {
270
+ securityTxt: {
271
+ status: securityTxt.status,
272
+ state: securityTxt.state,
273
+ endpoint: securityTxt.finalUrl,
274
+ httpStatus: securityTxt.httpStatus,
275
+ observedAt: securityTxt.observedAt,
276
+ complete: securityTxt.complete,
277
+ truncated: securityTxt.truncated,
278
+ },
279
+ } : {}),
280
+ ...(sslbl ? {
281
+ sslbl: {
282
+ status: sslbl.status,
283
+ verdict: sslbl.verdict,
284
+ observedAt: sslbl.observedAt,
285
+ complete: sslbl.complete,
286
+ snapshotUpdatedAt: sslbl.snapshot.sourceUpdatedAt,
287
+ },
288
+ } : {}),
289
+ };
290
+ // Bulk triage only consumes the derived availability evidence and source
291
+ // diagnostics. Omitting raw RDAP JSON and multi-hop WHOIS bodies from that
292
+ // opt-in response prevents large scans from downloading and retaining the
293
+ // same registry payloads the backend already used to build `availability`.
294
+ if (compact) {
295
+ const bulkComparison = !fast
296
+ ? buildBulkComparisonEvidence(availability)
297
+ : null;
298
+ const { credentialSurfaceProfile: _credentialSurfaceProfile, structuredDataIdentity: _structuredDataIdentity, technologyProfile: _technologyProfile, pageRoleProfile: _pageRoleProfile, clientBehaviorProfile: _clientBehaviorProfile, securityPosture: _securityPosture, pageIdentity: richPageIdentity, http: richHttp, ...compactAvailability } = availability;
299
+ return {
300
+ availability: {
301
+ ...compactAvailability,
302
+ ...(richPageIdentity !== undefined ? { pageIdentity: withoutNestedPublicationMetadata(richPageIdentity) } : {}),
303
+ ...(richHttp !== undefined ? { http: withoutNestedDeliveryMetadata(richHttp) } : {}),
304
+ ...(bulkComparison ? { bulkComparison } : {}),
305
+ },
306
+ diagnostics,
307
+ };
308
+ }
309
+ const targetDomain = classified.registrableDomain || classified.value;
310
+ const threatIntelligenceProviders = [];
311
+ if (urlscanIntelligencePromise) {
312
+ threatIntelligenceProviders.push(urlscanIntelligenceResult.status === 'fulfilled' && urlscanIntelligenceResult.value
313
+ ? urlscanIntelligenceResult.value
314
+ : createThreatIntelligenceResult(URLSCAN_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Archived provider intelligence could not be completed.' }));
315
+ }
316
+ if (urlhausIntelligencePromise) {
317
+ threatIntelligenceProviders.push(urlhausIntelligenceResult.status === 'fulfilled' && urlhausIntelligenceResult.value
318
+ ? urlhausIntelligenceResult.value
319
+ : createThreatIntelligenceResult(URLHAUS_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Malware-host intelligence could not be completed.' }));
320
+ }
321
+ if (threatfoxIntelligencePromise) {
322
+ threatIntelligenceProviders.push(threatfoxIntelligenceResult.status === 'fulfilled' && threatfoxIntelligenceResult.value
323
+ ? threatfoxIntelligenceResult.value
324
+ : createThreatIntelligenceResult(THREATFOX_PROVIDER, { type: 'domain', value: targetDomain }, { state: 'error', detail: 'Malware-IOC intelligence could not be completed.' }));
325
+ }
326
+ const threatIntelligence = threatIntelligenceProviders.length
327
+ ? { version: THREAT_INTELLIGENCE_ENVELOPE_VERSION, providers: threatIntelligenceProviders }
328
+ : null;
329
+ const registrarRdapParsed = registrarRdap
330
+ && typeof registrarRdap.parsed === 'object'
331
+ && registrarRdap.parsed
332
+ && !Array.isArray(registrarRdap.parsed)
333
+ ? registrarRdap.parsed
334
+ : null;
335
+ const registryInsights = classified.type === 'domain' && !fast && !compact
336
+ ? buildRegistryInsights({
337
+ rdapParsed: rdapRecord?.parsed,
338
+ rdapStatus,
339
+ rdapFetchedAt: rdapRecord?.fetchedAt,
340
+ whoisParsed: whois.parsed,
341
+ whoisStatus,
342
+ whoisQueriedAt: Array.isArray(whoisChain) && whoisChain[0] ? whoisChain[0].queriedAt : null,
343
+ registrarRdapParsed,
344
+ registrarRdapStatus: registrarRdap?.status,
345
+ registrarRdapFetchedAt: registrarRdap?.fetchedAt,
346
+ })
347
+ : null;
348
+ const registrarStanding = classified.type === 'domain' && !compact
349
+ ? buildRegistrarStanding({
350
+ registrarIanaId: resolveRegistrarIanaId(rdapRecord?.parsed, whois.parsed),
351
+ })
352
+ : null;
353
+ return {
354
+ rdap,
355
+ whois,
356
+ availability,
357
+ diagnostics,
358
+ ...(registryInsights ? { registryInsights } : {}),
359
+ ...(registrarStanding ? { registrarStanding } : {}),
360
+ ...(reverseDns ? { reverseDns } : {}),
361
+ ...(networkContext ? { networkContext } : {}),
362
+ ...(securityTxt ? { securityTxt } : {}),
363
+ ...(sslbl ? { sslbl } : {}),
364
+ ...(threatIntelligence ? { threatIntelligence } : {}),
365
+ };
366
+ }
367
+ export { buildUnifiedLookupResponse };