@slicedearth/whoisleuth-cli 1.43.0 → 1.47.4

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 (153) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +67 -10
  4. package/bin/whoisleuth.mjs +22 -9
  5. package/cli/arguments.mjs +256 -9
  6. package/cli/artifact-structure.mjs +1833 -0
  7. package/cli/artifact-verify.mjs +242 -49
  8. package/cli/bounded-json.mjs +1 -0
  9. package/cli/bulk-checkpoint.mjs +67 -31
  10. package/cli/bulk-command-runner.mjs +16 -6
  11. package/cli/bulk.mjs +18 -7
  12. package/cli/case-pack.mjs +819 -34
  13. package/cli/command-reference.mjs +48 -24
  14. package/cli/completion.mjs +50 -11
  15. package/cli/config-profile.mjs +15 -12
  16. package/cli/ct-event-intake.mjs +32 -2
  17. package/cli/discovery-observation-snapshot.mjs +8 -14
  18. package/cli/discovery-scan-command-runner.mjs +18 -6
  19. package/cli/discovery-snapshot.mjs +8 -15
  20. package/cli/discovery-workflow.mjs +2 -3
  21. package/cli/doctor.mjs +2 -2
  22. package/cli/domain-control-monitor.mjs +21 -15
  23. package/cli/domain-control-observations.mjs +25 -2
  24. package/cli/errors.mjs +7 -1
  25. package/cli/evidence-command-runner.mjs +17 -11
  26. package/cli/evidence-signing.mjs +73 -45
  27. package/cli/export-evidence.mjs +11 -4
  28. package/cli/formatters/html.mjs +3 -3
  29. package/cli/formatters/json.mjs +12 -4
  30. package/cli/formatters/terminal.mjs +4 -1
  31. package/cli/http.mjs +2 -1
  32. package/cli/input.mjs +57 -0
  33. package/cli/interchange-report.mjs +226 -0
  34. package/cli/investigation-manifest.mjs +16 -17
  35. package/cli/investigation-run.mjs +71 -23
  36. package/cli/local-mmdb-review.mjs +45 -14
  37. package/cli/lookup-command-runner.mjs +2 -2
  38. package/cli/lookup-diff.mjs +9 -0
  39. package/cli/lookup-reconcile.mjs +1 -1
  40. package/cli/mail-review.mjs +5 -5
  41. package/cli/manual.mjs +1 -1
  42. package/cli/network-command-runner.mjs +78 -0
  43. package/cli/offline-evidence-review.mjs +28 -8
  44. package/cli/page-compare.mjs +8 -5
  45. package/cli/registry-cohort.mjs +385 -61
  46. package/cli/registry-fixture-scaffold.mjs +4 -3
  47. package/cli/registry-support.mjs +2 -1
  48. package/cli/retained-artifact-diff.mjs +376 -0
  49. package/cli/risk-calibration.mjs +7 -5
  50. package/cli/runner.mjs +150 -177
  51. package/cli/saved-lookup.mjs +26 -3
  52. package/cli/sharing-review.mjs +12 -6
  53. package/docs/cli-reference.md +467 -74
  54. package/docs/cli.md +25 -5
  55. package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
  56. package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
  57. package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
  58. package/frontend/src/lib/analysis/brand-profile-model.js +33 -7
  59. package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
  60. package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
  61. package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
  62. package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
  63. package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
  64. package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
  65. package/frontend/src/lib/analysis/bulk-sort.js +4 -0
  66. package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
  67. package/frontend/src/lib/analysis/case-evidence-model.js +9 -1
  68. package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
  69. package/frontend/src/lib/analysis/case-migration-model.js +83 -31
  70. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  71. package/frontend/src/lib/analysis/case-record-core.js +1 -1
  72. package/frontend/src/lib/analysis/case-record-model.js +2 -0
  73. package/frontend/src/lib/analysis/case-record-operations.js +43 -13
  74. package/frontend/src/lib/analysis/case-report.js +39 -6
  75. package/frontend/src/lib/analysis/case-response-model.js +66 -7
  76. package/frontend/src/lib/analysis/case-response-packet.js +15 -5
  77. package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
  78. package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
  79. package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
  80. package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
  81. package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
  82. package/frontend/src/lib/analysis/domain-control-records.js +88 -0
  83. package/frontend/src/lib/analysis/external-findings-import.js +70 -6
  84. package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
  85. package/frontend/src/lib/analysis/investigation-guide.js +52 -10
  86. package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
  87. package/frontend/src/lib/analysis/lookup-asset-graph.js +8 -8
  88. package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
  89. package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
  90. package/frontend/src/lib/analysis/lookup-decision-support.js +63 -8
  91. package/frontend/src/lib/analysis/lookup-presentation.js +70 -14
  92. package/frontend/src/lib/analysis/lookup-source-refresh.js +36 -63
  93. package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
  94. package/frontend/src/lib/analysis/utils.js +27 -4
  95. package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
  96. package/frontend/src/lib/analysis/visualization-models.js +63 -94
  97. package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
  98. package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
  99. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -4
  100. package/frontend/src/lib/analysis/workspace-archive.js +105 -19
  101. package/lib/application-version.mjs +3 -0
  102. package/lib/availability.mjs +14 -12
  103. package/lib/bounded-file.mjs +82 -0
  104. package/lib/bounded-json-response.mjs +170 -0
  105. package/lib/bounded-json.mjs +249 -0
  106. package/lib/browser-library-profile.mjs +167 -38
  107. package/lib/certificate-extension-profile.mjs +2 -1
  108. package/lib/cryptographic-assurance.mjs +155 -0
  109. package/lib/ct-search.mjs +87 -47
  110. package/lib/dns-intelligence.mjs +3 -2
  111. package/lib/dns-resolver-selection.mjs +6 -4
  112. package/lib/dnssec-chain-validation.mjs +1053 -0
  113. package/lib/dnssec-evidence-validation.mjs +10 -3
  114. package/lib/domain-change-packet.mjs +7 -5
  115. package/lib/domain-control-manifest.mjs +17 -129
  116. package/lib/domain-posture.mjs +7 -4
  117. package/lib/evidence-export.mjs +751 -78
  118. package/lib/evidence-report-markdown.mjs +3 -0
  119. package/lib/evidence-report.mjs +5 -0
  120. package/lib/favicon.mjs +8 -5
  121. package/lib/http-evidence-bounds.mjs +3 -0
  122. package/lib/http-intelligence.mjs +2 -5
  123. package/lib/interchange-fidelity-registry.mjs +122 -0
  124. package/lib/local-geoip-evidence.mjs +2 -1
  125. package/lib/lookup-cache.mjs +5 -1
  126. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  127. package/lib/lookup-response-contract.mjs +691 -3
  128. package/lib/lookup.mjs +2 -2
  129. package/lib/observation.mjs +7 -6
  130. package/lib/outbound-identity.mjs +2 -3
  131. package/lib/perceptual-hash.mjs +12 -4
  132. package/lib/portable-generator.mjs +21 -0
  133. package/lib/provider-policy-admission.mjs +9 -4
  134. package/lib/rdap-normalization.mjs +18 -5
  135. package/lib/rdap-registrar.mjs +5 -1
  136. package/lib/rdap-transport.mjs +6 -6
  137. package/lib/registry-capabilities.mjs +3 -0
  138. package/lib/registry-capability-catalogue.mjs +12 -2
  139. package/lib/registry-comparison.mjs +28 -11
  140. package/lib/risk-calibration-summary.mjs +338 -0
  141. package/lib/rpki-evidence.mjs +9 -8
  142. package/lib/safe-fetch.mjs +96 -42
  143. package/lib/security-txt.mjs +26 -3
  144. package/lib/service-binding-dns.mjs +6 -1
  145. package/lib/smtp-transport-review.mjs +784 -0
  146. package/lib/sslbl-certificates.generated.mjs +136 -134
  147. package/lib/threat-intelligence-runtime.mjs +4 -13
  148. package/lib/threat-intelligence-types.mjs +18 -1
  149. package/lib/tls-intelligence.mjs +11 -5
  150. package/lib/tlsa-evidence.mjs +37 -7
  151. package/lib/website-technology.mjs +2 -6
  152. package/package.json +1 -1
  153. package/third-party-notices.txt +95 -5817
@@ -6,6 +6,12 @@
6
6
  // relies on before any evidence is derived or rendered. Nested source records
7
7
  // stay separately attributed and additive; their own normalizers remain the
8
8
  // authority for source-specific fields.
9
+ import { THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_RESULT_STATES, } from "./threat-intelligence-types.mjs";
10
+ import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, MAX_HTTP_PROVENANCE_URL, } from "./http-evidence-bounds.mjs";
11
+ import { MAX_OBSERVATION_DIAGNOSTICS, MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, } from "./observation.mjs";
12
+ import { assertBoundedJsonStructure } from "./bounded-json.mjs";
13
+ import { MAX_LOOKUP_DNS_RECORDS_PER_TYPE, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS, MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, } from "./lookup-network-evidence-bounds.mjs";
14
+ import { MAX_SECURITY_POSTURE_FINDINGS } from "./website-security-posture.mjs";
9
15
  const INVALID_LOOKUP_RESPONSE = 'INVALID_LOOKUP_RESPONSE';
10
16
  const INVALID_LOOKUP_RESPONSE_MESSAGE = 'Lookup returned an invalid response.';
11
17
  const INVALID_COMPACT_LOOKUP_RESPONSE = 'INVALID_COMPACT_LOOKUP_RESPONSE';
@@ -14,11 +20,15 @@ const MAX_LOOKUP_RESPONSE_QUERY_LENGTH = 4096;
14
20
  const MAX_LOOKUP_RESPONSE_HOST_LENGTH = 253;
15
21
  const MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS = 32;
16
22
  const MAX_LOOKUP_RESPONSE_ERROR_LENGTH = 240;
23
+ const MAX_LOOKUP_RESPONSE_CONTAINER_ITEMS = 500;
17
24
  const MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS = 8;
18
25
  const MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS = 128;
19
26
  const MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS = 16;
20
27
  const MAX_COMPACT_BULK_TECHNOLOGY_IDS = 12;
21
28
  const MAX_THREAT_INTELLIGENCE_PROVIDERS = 10;
29
+ const MAX_THREAT_INTELLIGENCE_FINDINGS = 100;
30
+ const MAX_THREAT_INTELLIGENCE_LIMITATIONS = 10;
31
+ const MAX_THREAT_INTELLIGENCE_TEXT_LENGTH = 500;
22
32
  const MAX_LOOKUP_TIMING_MS = 120_000;
23
33
  const MAX_LOOKUP_TIMING_SOURCES = 10;
24
34
  const CONTROL_CHAR_RE = /[\u0000-\u001f\u007f]/u;
@@ -41,6 +51,13 @@ const COMPACT_BULK_COMPARISON_STATES = new Set([
41
51
  ]);
42
52
  const COMPACT_BULK_TECHNOLOGY_ID_RE = /^[a-z0-9][a-z0-9_-]{0,79}$/u;
43
53
  const SHA256_RE = /^[a-f0-9]{64}$/u;
54
+ const THREAT_INTELLIGENCE_PROVIDERS = Object.freeze({
55
+ urlscan_search: Object.freeze({ label: 'URLscan archived verdicts', host: 'urlscan.io' }),
56
+ urlhaus_host: Object.freeze({ label: 'URLhaus malware-host records', host: 'urlhaus.abuse.ch' }),
57
+ threatfox_domain_ioc: Object.freeze({ label: 'ThreatFox malware IOCs', host: 'threatfox.abuse.ch' }),
58
+ });
59
+ const THREAT_INTELLIGENCE_STATES = new Set(THREAT_INTELLIGENCE_RESULT_STATES);
60
+ const THREAT_INTELLIGENCE_CATEGORY_SET = new Set(THREAT_INTELLIGENCE_CATEGORIES);
44
61
  const LOOKUP_TIMING_SOURCES = new Set([
45
62
  'rdap',
46
63
  'whois',
@@ -61,6 +78,94 @@ function isJsonObject(value) {
61
78
  function record(value) {
62
79
  return isJsonObject(value) ? value : EMPTY_RECORD;
63
80
  }
81
+ function boundedThreatText(value, maximum = MAX_THREAT_INTELLIGENCE_TEXT_LENGTH) {
82
+ if (typeof value !== 'string' || CONTROL_CHAR_RE.test(value))
83
+ return null;
84
+ const text = value.slice(0, maximum * 2).trim();
85
+ return text ? text.slice(0, maximum) : null;
86
+ }
87
+ function threatTimestamp(value) {
88
+ const text = boundedThreatText(value, 40);
89
+ if (!text)
90
+ return null;
91
+ const epoch = Date.parse(text);
92
+ return Number.isFinite(epoch) ? new Date(epoch).toISOString() : null;
93
+ }
94
+ function attributedThreatUrl(value, providerId) {
95
+ const expectedHost = THREAT_INTELLIGENCE_PROVIDERS[providerId]?.host;
96
+ if (!expectedHost || typeof value !== 'string' || value.length > 1_024 || CONTROL_CHAR_RE.test(value))
97
+ return null;
98
+ try {
99
+ const url = new URL(value);
100
+ return url.protocol === 'https:' && !url.username && !url.password && url.hostname === expectedHost
101
+ ? url.href
102
+ : null;
103
+ }
104
+ catch {
105
+ return null;
106
+ }
107
+ }
108
+ function normalizeThreatFinding(value, providerId) {
109
+ const input = record(value);
110
+ const category = boundedThreatText(input.category, 32);
111
+ if (!category || !THREAT_INTELLIGENCE_CATEGORY_SET.has(category))
112
+ return null;
113
+ const referenceUrl = attributedThreatUrl(input.referenceUrl, providerId);
114
+ const tags = Array.isArray(input.tags)
115
+ ? [...new Set(input.tags.slice(0, 40).map((tag) => boundedThreatText(tag, 80)).filter((tag) => tag !== null))].slice(0, 20)
116
+ : [];
117
+ return {
118
+ id: boundedThreatText(input.id, 160),
119
+ category,
120
+ providerVerdict: boundedThreatText(input.providerVerdict, 120),
121
+ detail: boundedThreatText(input.detail),
122
+ firstObservedAt: threatTimestamp(input.firstObservedAt),
123
+ lastObservedAt: threatTimestamp(input.lastObservedAt),
124
+ referenceUrl,
125
+ tags,
126
+ };
127
+ }
128
+ function normalizeThreatProvider(value) {
129
+ const input = record(value);
130
+ const identity = record(input.provider);
131
+ const providerId = boundedThreatText(identity.id, 80);
132
+ const providerDefinition = providerId ? THREAT_INTELLIGENCE_PROVIDERS[providerId] : null;
133
+ if (!providerId || !providerDefinition)
134
+ return null;
135
+ const state = boundedThreatText(input.state, 32);
136
+ if (!state || !THREAT_INTELLIGENCE_STATES.has(state))
137
+ return null;
138
+ const observationInput = record(input.observation);
139
+ const limitations = Array.isArray(observationInput.limitations)
140
+ ? observationInput.limitations
141
+ .slice(0, MAX_THREAT_INTELLIGENCE_LIMITATIONS * 2)
142
+ .map((limitation) => boundedThreatText(limitation))
143
+ .filter((limitation) => limitation !== null)
144
+ .slice(0, MAX_THREAT_INTELLIGENCE_LIMITATIONS)
145
+ : [];
146
+ const findings = Array.isArray(input.findings)
147
+ ? input.findings
148
+ .slice(0, MAX_THREAT_INTELLIGENCE_FINDINGS * 2)
149
+ .map((finding) => normalizeThreatFinding(finding, providerId))
150
+ .filter((finding) => finding !== null)
151
+ .slice(0, MAX_THREAT_INTELLIGENCE_FINDINGS)
152
+ : [];
153
+ return {
154
+ provider: {
155
+ id: providerId,
156
+ label: providerDefinition.label,
157
+ },
158
+ state,
159
+ detail: boundedThreatText(input.detail),
160
+ findings,
161
+ observation: {
162
+ observedAt: threatTimestamp(observationInput.observedAt),
163
+ limitations,
164
+ complete: typeof observationInput.complete === 'boolean' ? observationInput.complete : null,
165
+ truncated: typeof observationInput.truncated === 'boolean' ? observationInput.truncated : null,
166
+ },
167
+ };
168
+ }
64
169
  function optionalBoundedText(value, maxLength) {
65
170
  return value === undefined || (typeof value === 'string'
66
171
  && value.length <= maxLength
@@ -132,7 +237,560 @@ function invalidCompactLookupResponse() {
132
237
  error: INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE,
133
238
  };
134
239
  }
240
+ function validHttpProvenanceUrl(value) {
241
+ if (typeof value !== 'string'
242
+ || !value
243
+ || value.length > MAX_HTTP_PROVENANCE_URL
244
+ || CONTROL_CHAR_RE.test(value))
245
+ return false;
246
+ try {
247
+ const parsed = new URL(value);
248
+ return ['http:', 'https:'].includes(parsed.protocol)
249
+ && Boolean(parsed.hostname)
250
+ && !parsed.username
251
+ && !parsed.password
252
+ && !parsed.search
253
+ && !parsed.hash
254
+ && parsed.href === value;
255
+ }
256
+ catch {
257
+ return false;
258
+ }
259
+ }
260
+ function validOptionalHttpUrl(value) {
261
+ return value === undefined || value === null || validHttpProvenanceUrl(value);
262
+ }
263
+ function validOptionalHttpStatus(value) {
264
+ return value === undefined || value === null
265
+ || Number.isInteger(value) && Number(value) >= 100 && Number(value) <= 599;
266
+ }
267
+ function validObservationLimitations(value) {
268
+ return value === undefined || Array.isArray(value)
269
+ && value.length <= MAX_OBSERVATION_LIMITATIONS
270
+ && value.every((item) => typeof item === 'string'
271
+ && Boolean(item)
272
+ && item.length <= MAX_OBSERVATION_LIMITATION_LENGTH
273
+ && !CONTROL_CHAR_RE.test(item));
274
+ }
275
+ function validObservationDiagnosticValue(value, depth = 0) {
276
+ if (typeof value === 'boolean')
277
+ return true;
278
+ if (typeof value === 'number')
279
+ return Number.isFinite(value);
280
+ if (typeof value === 'string') {
281
+ return value.length <= 240 && !CONTROL_CHAR_RE.test(value);
282
+ }
283
+ if (!isJsonObject(value) || depth >= 2)
284
+ return false;
285
+ const keys = Object.keys(value);
286
+ return keys.length <= 6
287
+ && keys.every((key) => ['status', 'error', 'detail', 'truncated', 'discarded', 'count'].includes(key))
288
+ && keys.every((key) => validObservationDiagnosticValue(value[key], depth + 1));
289
+ }
290
+ function validObservationDiagnostics(value) {
291
+ if (value === undefined)
292
+ return true;
293
+ if (!isJsonObject(value))
294
+ return false;
295
+ const entries = Object.entries(value);
296
+ return entries.length <= MAX_OBSERVATION_DIAGNOSTICS
297
+ && entries.every(([key, item]) => (Boolean(key)
298
+ && key.length <= 40
299
+ && /^[a-z0-9_-]+$/iu.test(key)
300
+ && validObservationDiagnosticValue(item)));
301
+ }
302
+ function validObservationFields(value) {
303
+ if (!isJsonObject(value))
304
+ return false;
305
+ return optionalBoundedText(value.status, 40)
306
+ && optionalBoundedText(value.source, 40)
307
+ && optionalBoundedText(value.observedAt, 64)
308
+ && (value.scanMode === undefined || value.scanMode === null
309
+ || typeof value.scanMode === 'string' && ['fast', 'deep'].includes(value.scanMode))
310
+ && (value.durationMs === undefined || value.durationMs === null
311
+ || Number.isFinite(value.durationMs) && Number(value.durationMs) >= 0
312
+ && Number(value.durationMs) <= 120_000)
313
+ && (value.complete === undefined || typeof value.complete === 'boolean')
314
+ && (value.truncated === undefined || typeof value.truncated === 'boolean')
315
+ && validObservationLimitations(value.limitations)
316
+ && validObservationDiagnostics(value.diagnostics);
317
+ }
318
+ function validSourceStatus(value) {
319
+ if (!isJsonObject(value))
320
+ return false;
321
+ return optionalBoundedText(value.status, 40)
322
+ && (value.error === undefined || value.error === null || optionalBoundedText(value.error, 240))
323
+ && (value.endpoint === undefined || value.endpoint === null || optionalBoundedText(value.endpoint, 2_048))
324
+ && (value.detail === undefined || value.detail === null || optionalBoundedText(value.detail, 500))
325
+ && (value.fetchedAt === undefined || value.fetchedAt === null || optionalBoundedText(value.fetchedAt, 64))
326
+ && (value.queriedAt === undefined || value.queriedAt === null || optionalBoundedText(value.queriedAt, 64))
327
+ && (value.attempts === undefined || Array.isArray(value.attempts)
328
+ && value.attempts.length <= 3
329
+ && value.attempts.every((item) => isJsonObject(item)
330
+ && optionalBoundedText(item.outcome, 40)));
331
+ }
332
+ function validNormalizedHttpEvidence(value) {
333
+ if (!validObservationFields(value)
334
+ || !validOptionalHttpUrl(value.requestUrl)
335
+ || !validOptionalHttpUrl(value.finalUrl)
336
+ || !validObservationLimitations(value.limitations))
337
+ return false;
338
+ const redirects = value.redirects;
339
+ if (redirects !== undefined && (!Array.isArray(redirects)
340
+ || redirects.length > MAX_HTTP_EVIDENCE_REDIRECTS
341
+ || !redirects.every((item) => isJsonObject(item)
342
+ && validHttpProvenanceUrl(item.from)
343
+ && validHttpProvenanceUrl(item.to)
344
+ && validOptionalHttpStatus(item.status)
345
+ && (item.queryOmitted === undefined || typeof item.queryOmitted === 'boolean'))))
346
+ return false;
347
+ const attempts = value.attempts;
348
+ if (attempts !== undefined && (!Array.isArray(attempts)
349
+ || attempts.length > MAX_HTTP_ATTEMPTS
350
+ || !attempts.every((item) => isJsonObject(item)
351
+ && validOptionalHttpUrl(item.url)
352
+ && validOptionalHttpStatus(item.httpStatus)
353
+ && (item.queryOmitted === undefined || typeof item.queryOmitted === 'boolean')
354
+ && (item.outcome === undefined || typeof item.outcome === 'string'
355
+ && ['error', 'response', 'unknown'].includes(item.outcome))
356
+ && (item.error === undefined || item.error === null || typeof item.error === 'string'
357
+ && item.error.length <= MAX_HTTP_ERROR_LENGTH && !CONTROL_CHAR_RE.test(item.error)))))
358
+ return false;
359
+ if (value.redirectCount !== undefined
360
+ && (!Number.isInteger(value.redirectCount)
361
+ || Number(value.redirectCount) < 0
362
+ || Number(value.redirectCount) > MAX_HTTP_EVIDENCE_REDIRECTS))
363
+ return false;
364
+ if (Array.isArray(redirects) && value.redirectCount !== undefined
365
+ && value.redirectCount !== redirects.length)
366
+ return false;
367
+ if (value.response !== undefined && value.response !== null) {
368
+ if (!isJsonObject(value.response) || !validOptionalHttpStatus(value.response.status))
369
+ return false;
370
+ }
371
+ return true;
372
+ }
373
+ function optionalStringArrayWithin(value, maximum) {
374
+ return value === undefined || Array.isArray(value)
375
+ && value.length <= maximum
376
+ && value.every((item) => typeof item === 'string');
377
+ }
378
+ function optionalRecordArrayWithin(value, maximum) {
379
+ return value === undefined || Array.isArray(value)
380
+ && value.length <= maximum
381
+ && value.every(isJsonObject);
382
+ }
383
+ function validBoundedString(value, maximum, allowEmpty = false) {
384
+ return typeof value === 'string'
385
+ && (allowEmpty || Boolean(value))
386
+ && value.length <= maximum
387
+ && !CONTROL_CHAR_RE.test(value);
388
+ }
389
+ function validUint(value, maximum) {
390
+ return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum;
391
+ }
392
+ function validDnsMxRecord(value) {
393
+ return isJsonObject(value)
394
+ && validUint(value.priority, 0xffff)
395
+ && validBoundedString(value.exchange, 253, true);
396
+ }
397
+ function validDnsCaaRecord(value) {
398
+ return isJsonObject(value)
399
+ && validUint(value.critical, 0xff)
400
+ && typeof value.tag === 'string'
401
+ && /^[a-z0-9-]{1,15}$/u.test(value.tag)
402
+ && validBoundedString(value.value, 1_024);
403
+ }
404
+ function validDnsSoaRecord(value) {
405
+ return isJsonObject(value)
406
+ && validBoundedString(value.nsname, 253)
407
+ && validBoundedString(value.hostmaster, 253)
408
+ && ['serial', 'refresh', 'retry', 'expire', 'minttl']
409
+ .every((key) => validUint(value[key], 0xffff_ffff));
410
+ }
411
+ function validUintArray(value, maximumItems) {
412
+ return Array.isArray(value)
413
+ && value.length <= maximumItems
414
+ && value.every((item) => validUint(item, 0xffff));
415
+ }
416
+ function validStringArray(value, maximumItems, maximumLength) {
417
+ return Array.isArray(value)
418
+ && value.length <= maximumItems
419
+ && value.every((item) => validBoundedString(item, maximumLength));
420
+ }
421
+ function validDnsHttpsRecord(value) {
422
+ if (!isJsonObject(value)
423
+ || value.type !== 'HTTPS'
424
+ || !validBoundedString(value.owner, 253)
425
+ || !validUint(value.ttl, 0xffff_ffff)
426
+ || !validUint(value.priority, 0xffff)
427
+ || typeof value.mode !== 'string'
428
+ || !['alias', 'service'].includes(value.mode)
429
+ || !(value.target === null || validBoundedString(value.target, 253))
430
+ || typeof value.targetIsOwner !== 'boolean'
431
+ || typeof value.serviceUnavailable !== 'boolean'
432
+ || typeof value.compatible !== 'boolean'
433
+ || typeof value.parametersIgnored !== 'boolean'
434
+ || !isJsonObject(value.parameters))
435
+ return false;
436
+ const parameters = value.parameters;
437
+ if (Object.keys(parameters).length > 24
438
+ || !validUintArray(parameters.mandatory, 24)
439
+ || !validStringArray(parameters.alpn, 16, 132)
440
+ || typeof parameters.noDefaultAlpn !== 'boolean'
441
+ || !(parameters.port === null || validUint(parameters.port, 0xffff))
442
+ || !validStringArray(parameters.ipv4hint, 8, 64)
443
+ || !validStringArray(parameters.ipv6hint, 8, 64)
444
+ || !validUintArray(parameters.unknownKeys, 24)
445
+ || !validUintArray(parameters.unsupportedMandatoryKeys, 24)
446
+ || !Array.isArray(parameters.opaque)
447
+ || parameters.opaque.length > 24)
448
+ return false;
449
+ return parameters.opaque.every((item) => isJsonObject(item)
450
+ && validUint(item.key, 0xffff)
451
+ && (item.name === null || typeof item.name === 'string' && /^[a-z0-9-]{1,63}$/u.test(item.name))
452
+ && validUint(item.length, 0xffff));
453
+ }
454
+ function validTlsFinding(value) {
455
+ return isJsonObject(value)
456
+ && validBoundedString(value.id, 80)
457
+ && validBoundedString(value.tone, 32)
458
+ && validBoundedString(value.label, 160)
459
+ && validBoundedString(value.detail, 500);
460
+ }
461
+ function validTlsChainCertificate(value) {
462
+ if (!isJsonObject(value))
463
+ return false;
464
+ return (value.fingerprintSha256 === undefined
465
+ || value.fingerprintSha256 === null
466
+ || validBoundedString(value.fingerprintSha256, 64))
467
+ && optionalTlsNameWithin(value.subject);
468
+ }
469
+ function validSecurityPostureFinding(value) {
470
+ return isJsonObject(value)
471
+ && validBoundedString(value.id, 80)
472
+ && validBoundedString(value.category, 80)
473
+ && typeof value.state === 'string'
474
+ && ['observed', 'potential_exposure', 'observed_absence', 'unavailable'].includes(value.state)
475
+ && typeof value.tone === 'string'
476
+ && ['configured', 'review', 'neutral'].includes(value.tone)
477
+ && validBoundedString(value.label, 160)
478
+ && validBoundedString(value.detail, 300)
479
+ && validStringArray(value.evidence, 4, 120);
480
+ }
481
+ function validOptionalStringArray(value, maximumItems, maximumLength) {
482
+ return value === undefined || validStringArray(value, maximumItems, maximumLength);
483
+ }
484
+ function validOptionalRecordArray(value, maximumItems, validate) {
485
+ return value === undefined || Array.isArray(value)
486
+ && value.length <= maximumItems
487
+ && value.every(validate);
488
+ }
489
+ function validOptionalNullableText(value, maximumLength) {
490
+ return value === undefined || value === null || validBoundedString(value, maximumLength);
491
+ }
492
+ function validRdapLink(value) {
493
+ return isJsonObject(value)
494
+ && validBoundedString(value.href, 2_048)
495
+ && validOptionalNullableText(value.rel, 100)
496
+ && validOptionalNullableText(value.type, 160)
497
+ && validOptionalNullableText(value.title, 300);
498
+ }
499
+ function validRdapContact(value) {
500
+ if (!isJsonObject(value))
501
+ return false;
502
+ return validOptionalNullableText(value.handle, 200)
503
+ && validOptionalNullableText(value.name, 300)
504
+ && validOptionalNullableText(value.org, 300)
505
+ && validOptionalNullableText(value.email, 320)
506
+ && validOptionalNullableText(value.phone, 100)
507
+ && validOptionalNullableText(value.address, 1_000)
508
+ && validOptionalStringArray(value.roles, 12, 80)
509
+ && validOptionalStringArray(value.names, 8, 300)
510
+ && validOptionalStringArray(value.organizations, 8, 300)
511
+ && validOptionalStringArray(value.emails, 8, 320)
512
+ && validOptionalStringArray(value.phones, 8, 100)
513
+ && validOptionalStringArray(value.addresses, 8, 1_000)
514
+ && validOptionalRecordArray(value.publicIds, 20, (item) => isJsonObject(item)
515
+ && validBoundedString(item.type, 160)
516
+ && validBoundedString(item.identifier, 300))
517
+ && validOptionalRecordArray(value.links, 10, validRdapLink);
518
+ }
519
+ function validRdapTextBlock(value) {
520
+ return isJsonObject(value)
521
+ && validBoundedString(value.title, 160)
522
+ && validOptionalNullableText(value.type, 160)
523
+ && validStringArray(value.descriptions, 6, 800);
524
+ }
525
+ function validRdapParsed(value) {
526
+ if (!isJsonObject(value))
527
+ return false;
528
+ if (!validOptionalStringArray(value.statuses, 100, 160)
529
+ || !validOptionalStringArray(value.nameservers, 200, 253)
530
+ || !validOptionalStringArray(value.conformance, 50, 160)
531
+ || !validOptionalStringArray(value.serverTruncationReasons, 8, 160)
532
+ || !validOptionalStringArray(value.truncatedEntityRoles, 11, 80)
533
+ || !validOptionalRecordArray(value.events, 100, (item) => isJsonObject(item)
534
+ && validOptionalNullableText(item.action, 160)
535
+ && validOptionalNullableText(item.date, 64))
536
+ || !validOptionalRecordArray(value.links, 20, validRdapLink)
537
+ || !validOptionalRecordArray(value.nameserverDetails, 200, (item) => isJsonObject(item)
538
+ && validBoundedString(item.name, 253)
539
+ && validStringArray(item.addresses, 20, 80))
540
+ || !validOptionalRecordArray(value.dsData, 50, (item) => isJsonObject(item)
541
+ && validUint(item.keyTag, 0xffff)
542
+ && validUint(item.algorithm, 0xff)
543
+ && validUint(item.digestType, 0xff)
544
+ && validBoundedString(item.digest, 512))
545
+ || !validOptionalRecordArray(value.notices, 12, validRdapTextBlock)
546
+ || !validOptionalRecordArray(value.remarks, 12, validRdapTextBlock)
547
+ || !validOptionalRecordArray(value.redactions, 100, (item) => isJsonObject(item)
548
+ && ['name', 'method', 'reason'].every((key) => validOptionalNullableText(item[key], 500))
549
+ && ['prePath', 'postPath', 'replacementPath']
550
+ .every((key) => validOptionalNullableText(item[key], 512)))
551
+ || !validOptionalRecordArray(value.variants, 20, (group) => isJsonObject(group)
552
+ && validOptionalNullableText(group.idnTable, 300)
553
+ && validOptionalStringArray(group.relation, 20, 100)
554
+ && validOptionalRecordArray(group.variantNames, 50, (name) => isJsonObject(name)
555
+ && validOptionalNullableText(name.ldhName, 253)
556
+ && validOptionalNullableText(name.unicodeName, 253))))
557
+ return false;
558
+ const entitiesByRole = value.entitiesByRole;
559
+ if (entitiesByRole !== undefined) {
560
+ if (!isJsonObject(entitiesByRole) || Object.keys(entitiesByRole).length > 11)
561
+ return false;
562
+ for (const [role, contacts] of Object.entries(entitiesByRole)) {
563
+ if (!['registrar', 'registrant', 'administrative', 'technical', 'billing', 'abuse', 'noc', 'reseller', 'sponsor', 'proxy', 'notifications'].includes(role)
564
+ || !Array.isArray(contacts)
565
+ || contacts.length > 5
566
+ || !contacts.every(validRdapContact))
567
+ return false;
568
+ }
569
+ }
570
+ return true;
571
+ }
572
+ function validWhoisParsed(value) {
573
+ if (!isJsonObject(value)
574
+ || !validOptionalStringArray(value.statuses, 100, 160)
575
+ || !validOptionalStringArray(value.nameservers, 200, 253)
576
+ || !validOptionalStringArray(value.fieldsTruncated, 64, 80))
577
+ return false;
578
+ const contactsByRole = value.contactsByRole;
579
+ if (contactsByRole !== undefined) {
580
+ if (!isJsonObject(contactsByRole) || Object.keys(contactsByRole).length > 5)
581
+ return false;
582
+ for (const contacts of Object.values(contactsByRole)) {
583
+ if (!Array.isArray(contacts) || contacts.length > 1 || !contacts.every(validRdapContact))
584
+ return false;
585
+ }
586
+ }
587
+ return true;
588
+ }
589
+ function validRegistrationEvidence(value) {
590
+ const rdap = value.rdap;
591
+ const whois = value.whois;
592
+ if (!validSourceStatus(rdap) || !validSourceStatus(whois))
593
+ return false;
594
+ if (rdap.parsed !== undefined && rdap.parsed !== null && !validRdapParsed(rdap.parsed))
595
+ return false;
596
+ if (whois.parsed !== undefined && whois.parsed !== null && !validWhoisParsed(whois.parsed))
597
+ return false;
598
+ const registrar = rdap.registrarRdap;
599
+ return registrar === undefined || registrar === null || validSourceStatus(registrar)
600
+ && (registrar.parsed === undefined || registrar.parsed === null || validRdapParsed(registrar.parsed));
601
+ }
602
+ function validPageEvidence(value) {
603
+ const availability = value.availability;
604
+ const page = availability.pageIdentity;
605
+ if (page !== undefined && page !== null) {
606
+ if (!validObservationFields(page)
607
+ || !validOptionalStringArray(page.embeddedOrigins, 20, 2_048)
608
+ || !validOptionalStringArray(page.contactDomains, 20, 253)
609
+ || !validOptionalRecordArray(page.trackingIdentifiers, 30, (item) => isJsonObject(item)
610
+ && validBoundedString(item.type, 80)
611
+ && validBoundedString(item.value, 240)))
612
+ return false;
613
+ if (page.forms !== undefined && (!isJsonObject(page.forms)
614
+ || !validOptionalStringArray(page.forms.externalActionOrigins, 10, 2_048)))
615
+ return false;
616
+ if (page.resources !== undefined && (!isJsonObject(page.resources)
617
+ || !validOptionalStringArray(page.resources.externalOrigins, 30, 2_048)))
618
+ return false;
619
+ if (page.downloads !== undefined && (!isJsonObject(page.downloads)
620
+ || !validOptionalStringArray(page.downloads.riskyFileTypes, 20, 80)
621
+ || !validOptionalStringArray(page.downloads.externalOrigins, 20, 2_048)))
622
+ return false;
623
+ }
624
+ const technology = availability.technologyProfile;
625
+ if (technology !== undefined && technology !== null) {
626
+ if (!validObservationFields(technology)
627
+ || !validOptionalRecordArray(technology.findings, 24, (finding) => isJsonObject(finding)
628
+ && validOptionalRecordArray(finding.evidence, 4, (item) => isJsonObject(item)
629
+ && validBoundedString(item.source, 80)
630
+ && validBoundedString(item.description, 180))))
631
+ return false;
632
+ const libraries = technology.browserLibraryProfile;
633
+ if (libraries !== undefined && libraries !== null && (!validObservationFields(libraries)
634
+ || !validOptionalRecordArray(libraries.findings, 16, (item) => isJsonObject(item))))
635
+ return false;
636
+ }
637
+ const structured = availability.structuredDataIdentity;
638
+ if (structured !== undefined && structured !== null && (!validObservationFields(structured)
639
+ || !validOptionalRecordArray(structured.entities, 16, (entity) => isJsonObject(entity)
640
+ && validOptionalStringArray(entity.types, 8, 160)
641
+ && validOptionalStringArray(entity.sameAsHosts, 12, 253))))
642
+ return false;
643
+ const roles = availability.pageRoleProfile;
644
+ if (roles !== undefined && roles !== null && (!validObservationFields(roles)
645
+ || !validOptionalRecordArray(roles.findings, 4, (finding) => isJsonObject(finding)
646
+ && validOptionalStringArray(finding.evidence, 4, 300))))
647
+ return false;
648
+ const behavior = availability.clientBehaviorProfile;
649
+ if (behavior !== undefined && behavior !== null && (!validObservationFields(behavior)
650
+ || !validOptionalRecordArray(behavior.indicators, 12, (item) => isJsonObject(item))))
651
+ return false;
652
+ const credential = availability.credentialSurfaceProfile;
653
+ return credential === undefined || credential === null || validObservationFields(credential);
654
+ }
655
+ function validAvailabilityScalars(value) {
656
+ return (value.applicable === undefined || typeof value.applicable === 'boolean')
657
+ && validOptionalNullableText(value.domain, MAX_LOOKUP_RESPONSE_HOST_LENGTH)
658
+ && validOptionalNullableText(value.state, 40)
659
+ && validOptionalNullableText(value.confidence, 40)
660
+ && validOptionalNullableText(value.activityStatus, 40)
661
+ && validOptionalNullableText(value.websiteProbeDetail, 500)
662
+ && validOptionalNullableText(value.dnssec, 40)
663
+ && (value.deepScanComplete === undefined || typeof value.deepScanComplete === 'boolean')
664
+ && (value.hasMx === undefined || value.hasMx === null || typeof value.hasMx === 'boolean')
665
+ && (value.hasNullMx === undefined || value.hasNullMx === null || typeof value.hasNullMx === 'boolean')
666
+ && (value.hasSpf === undefined || value.hasSpf === null || typeof value.hasSpf === 'boolean')
667
+ && (value.hasDmarc === undefined || value.hasDmarc === null || typeof value.hasDmarc === 'boolean')
668
+ && validOptionalStringArray(value.mxHosts, MAX_LOOKUP_DNS_RECORDS_PER_TYPE, 253);
669
+ }
670
+ function optionalTlsNameWithin(value) {
671
+ if (value === undefined || value === null)
672
+ return true;
673
+ if (!isJsonObject(value))
674
+ return false;
675
+ return optionalStringArrayWithin(value.commonNames, MAX_LOOKUP_TLS_NAME_VALUES)
676
+ && (!Array.isArray(value.commonNames)
677
+ || value.commonNames.every((item) => validBoundedString(item, 256)))
678
+ && optionalStringArrayWithin(value.organizations, MAX_LOOKUP_TLS_NAME_VALUES)
679
+ && (!Array.isArray(value.organizations)
680
+ || value.organizations.every((item) => validBoundedString(item, 256)));
681
+ }
682
+ function validNormalizedNetworkEvidence(value) {
683
+ const availability = value.availability;
684
+ const dns = availability.dns;
685
+ if (dns !== undefined) {
686
+ if (!validObservationFields(dns))
687
+ return false;
688
+ if (dns.records !== undefined) {
689
+ if (!isJsonObject(dns.records))
690
+ return false;
691
+ for (const [key, maximumLength] of [
692
+ ['a', 64], ['aaaa', 64], ['cname', 253], ['ns', 253], ['spf', 1_024], ['dmarc', 1_024],
693
+ ]) {
694
+ if (!optionalStringArrayWithin(dns.records[key], MAX_LOOKUP_DNS_RECORDS_PER_TYPE)
695
+ || Array.isArray(dns.records[key])
696
+ && !dns.records[key].every((item) => validBoundedString(item, maximumLength)))
697
+ return false;
698
+ }
699
+ if (dns.records.mx !== undefined && (!Array.isArray(dns.records.mx)
700
+ || dns.records.mx.length > MAX_LOOKUP_DNS_RECORDS_PER_TYPE
701
+ || !dns.records.mx.every(validDnsMxRecord)))
702
+ return false;
703
+ if (dns.records.caa !== undefined && (!Array.isArray(dns.records.caa)
704
+ || dns.records.caa.length > MAX_LOOKUP_DNS_RECORDS_PER_TYPE
705
+ || !dns.records.caa.every(validDnsCaaRecord)))
706
+ return false;
707
+ if (dns.records.soa !== undefined && (!Array.isArray(dns.records.soa)
708
+ || dns.records.soa.length > 1
709
+ || !dns.records.soa.every(validDnsSoaRecord)))
710
+ return false;
711
+ if (dns.records.https !== undefined && (!Array.isArray(dns.records.https)
712
+ || dns.records.https.length > MAX_LOOKUP_DNS_RECORDS_PER_TYPE
713
+ || !dns.records.https.every(validDnsHttpsRecord)))
714
+ return false;
715
+ }
716
+ }
717
+ const reverseDns = value.reverseDns;
718
+ if (reverseDns !== undefined) {
719
+ if (!validObservationFields(reverseDns)
720
+ || reverseDns.records !== undefined
721
+ && (!isJsonObject(reverseDns.records)
722
+ || !optionalStringArrayWithin(reverseDns.records.ptr, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS)
723
+ || Array.isArray(reverseDns.records.ptr)
724
+ && !reverseDns.records.ptr.every((item) => validBoundedString(item, 253))))
725
+ return false;
726
+ }
727
+ const tls = availability.tls;
728
+ if (tls !== undefined) {
729
+ if (!validObservationFields(tls)
730
+ || !optionalRecordArrayWithin(tls.findings, MAX_LOOKUP_TLS_FINDINGS)
731
+ || Array.isArray(tls.findings) && !tls.findings.every(validTlsFinding)
732
+ || !optionalRecordArrayWithin(tls.chain, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES)
733
+ || Array.isArray(tls.chain) && !tls.chain.every(validTlsChainCertificate))
734
+ return false;
735
+ const certificate = tls.certificate;
736
+ if (certificate !== undefined && certificate !== null) {
737
+ if (!isJsonObject(certificate)
738
+ || !optionalTlsNameWithin(certificate.subject)
739
+ || !optionalTlsNameWithin(certificate.issuer))
740
+ return false;
741
+ const altNames = certificate.subjectAltNames;
742
+ if (altNames !== undefined && altNames !== null) {
743
+ if (!isJsonObject(altNames)
744
+ || !optionalStringArrayWithin(altNames.dnsNames, MAX_LOOKUP_TLS_ALT_NAMES)
745
+ || Array.isArray(altNames.dnsNames)
746
+ && !altNames.dnsNames.every((item) => validBoundedString(item, 253))
747
+ || !optionalStringArrayWithin(altNames.ipAddresses, MAX_LOOKUP_TLS_ALT_NAMES)
748
+ || Array.isArray(altNames.ipAddresses)
749
+ && !altNames.ipAddresses.every((item) => validBoundedString(item, 64)))
750
+ return false;
751
+ const totalAltNames = (Array.isArray(altNames.dnsNames) ? altNames.dnsNames.length : 0)
752
+ + (Array.isArray(altNames.ipAddresses) ? altNames.ipAddresses.length : 0);
753
+ if (totalAltNames > MAX_LOOKUP_TLS_ALT_NAMES)
754
+ return false;
755
+ }
756
+ const extensionProfile = certificate.extensionProfile;
757
+ if (extensionProfile !== undefined && extensionProfile !== null) {
758
+ if (!isJsonObject(extensionProfile))
759
+ return false;
760
+ const certificatePolicies = extensionProfile.certificatePolicies;
761
+ if (certificatePolicies !== undefined && certificatePolicies !== null
762
+ && (!isJsonObject(certificatePolicies)
763
+ || !optionalStringArrayWithin(certificatePolicies.oids, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES)
764
+ || Array.isArray(certificatePolicies.oids)
765
+ && !certificatePolicies.oids.every((item) => validBoundedString(item, 128))))
766
+ return false;
767
+ }
768
+ }
769
+ for (const child of [tls.authorization, tls.hostname]) {
770
+ if (child !== undefined && child !== null && (!isJsonObject(child)
771
+ || !validOptionalNullableText(child.error, 240)))
772
+ return false;
773
+ }
774
+ }
775
+ const securityPosture = availability.securityPosture;
776
+ if (securityPosture !== undefined && securityPosture !== null) {
777
+ if (!validObservationFields(securityPosture)
778
+ || !optionalRecordArrayWithin(securityPosture.findings, MAX_SECURITY_POSTURE_FINDINGS)
779
+ || Array.isArray(securityPosture.findings)
780
+ && !securityPosture.findings.every(validSecurityPostureFinding))
781
+ return false;
782
+ }
783
+ return true;
784
+ }
135
785
  function parseLookupHttpResponse(value) {
786
+ try {
787
+ assertBoundedJsonStructure(value, 'Lookup response', {
788
+ maximumContainerItems: MAX_LOOKUP_RESPONSE_CONTAINER_ITEMS,
789
+ });
790
+ }
791
+ catch {
792
+ return invalidLookupResponse();
793
+ }
136
794
  if (!isJsonObject(value) || Object.keys(value).length > MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS) {
137
795
  return invalidLookupResponse();
138
796
  }
@@ -156,9 +814,29 @@ function parseLookupHttpResponse(value) {
156
814
  if (section !== undefined && !isJsonObject(section))
157
815
  return invalidLookupResponse();
158
816
  }
159
- return { ok: true, value: value };
817
+ const lookupResponse = value;
818
+ if (!validAvailabilityScalars(value.availability)
819
+ || !validRegistrationEvidence(lookupResponse))
820
+ return invalidLookupResponse();
821
+ if (value.availability.http !== undefined && !validNormalizedHttpEvidence(value.availability.http)) {
822
+ return invalidLookupResponse();
823
+ }
824
+ for (const section of [value.reverseDns, value.networkContext, value.securityTxt]) {
825
+ if (section !== undefined && !validObservationFields(section))
826
+ return invalidLookupResponse();
827
+ }
828
+ if (!validNormalizedNetworkEvidence(lookupResponse)
829
+ || !validPageEvidence(lookupResponse))
830
+ return invalidLookupResponse();
831
+ return { ok: true, value: lookupResponse };
160
832
  }
161
833
  function parseCompactLookupHttpResponse(value, expectedDomain) {
834
+ try {
835
+ assertBoundedJsonStructure(value, 'Compact Lookup response');
836
+ }
837
+ catch {
838
+ return invalidCompactLookupResponse();
839
+ }
162
840
  if (!isJsonObject(value) || Object.keys(value).length > MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS) {
163
841
  return invalidCompactLookupResponse();
164
842
  }
@@ -272,9 +950,19 @@ function createLookupViewModel(response) {
272
950
  const securityTxt = record(response?.securityTxt);
273
951
  const sslbl = record(response?.sslbl);
274
952
  const threatIntelligence = record(response?.threatIntelligence);
953
+ const seenThreatIntelligenceProviders = new Set();
275
954
  const providers = Array.isArray(threatIntelligence.providers)
276
955
  ? threatIntelligence.providers
277
- .filter(isJsonObject)
956
+ .slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS * 2)
957
+ .map(normalizeThreatProvider)
958
+ .filter((provider) => provider !== null)
959
+ .filter((provider) => {
960
+ const providerId = String(record(provider.provider).id || '');
961
+ if (!providerId || seenThreatIntelligenceProviders.has(providerId))
962
+ return false;
963
+ seenThreatIntelligenceProviders.add(providerId);
964
+ return true;
965
+ })
278
966
  .slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS)
279
967
  : [];
280
968
  const dnsEvidence = record(availability.dns);
@@ -343,4 +1031,4 @@ function createLookupViewModel(response) {
343
1031
  securityPostureSummary: record(securityPosture.summary),
344
1032
  };
345
1033
  }
346
- export { INVALID_COMPACT_LOOKUP_RESPONSE, INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE, INVALID_LOOKUP_RESPONSE, INVALID_LOOKUP_RESPONSE_MESSAGE, MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS, MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS, MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS, MAX_LOOKUP_RESPONSE_ERROR_LENGTH, MAX_LOOKUP_RESPONSE_HOST_LENGTH, MAX_LOOKUP_RESPONSE_QUERY_LENGTH, MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS, MAX_LOOKUP_TIMING_MS, MAX_LOOKUP_TIMING_SOURCES, MAX_THREAT_INTELLIGENCE_PROVIDERS, createLookupHttpResponse, createLookupViewModel, isJsonObject, lookupHttpErrorMessage, normalizeLookupTiming, parseCompactLookupHttpResponse, parseLookupHttpResponse, record as lookupRecord, };
1034
+ export { INVALID_COMPACT_LOOKUP_RESPONSE, INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE, INVALID_LOOKUP_RESPONSE, INVALID_LOOKUP_RESPONSE_MESSAGE, MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS, MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS, MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS, MAX_LOOKUP_RESPONSE_ERROR_LENGTH, MAX_LOOKUP_RESPONSE_CONTAINER_ITEMS, MAX_LOOKUP_RESPONSE_HOST_LENGTH, MAX_LOOKUP_RESPONSE_QUERY_LENGTH, MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS, MAX_LOOKUP_DNS_RECORDS_PER_TYPE, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS, MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, MAX_LOOKUP_TIMING_MS, MAX_LOOKUP_TIMING_SOURCES, MAX_THREAT_INTELLIGENCE_PROVIDERS, MAX_THREAT_INTELLIGENCE_FINDINGS, MAX_THREAT_INTELLIGENCE_LIMITATIONS, validNormalizedHttpEvidence, createLookupHttpResponse, createLookupViewModel, isJsonObject, lookupHttpErrorMessage, normalizeLookupTiming, parseCompactLookupHttpResponse, parseLookupHttpResponse, record as lookupRecord, };