@slicedearth/whoisleuth-cli 1.45.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
@@ -49,18 +49,24 @@ function truncatedText(value, maxLength) {
49
49
  return null;
50
50
  return value.replace(/\s+/g, ' ').trim().slice(0, maxLength).trim() || null;
51
51
  }
52
- function flattenScalarValues(value, maxValues = 32) {
52
+ function flattenScalarValuesWithCompleteness(value, maxValues = 32) {
53
53
  const output = [];
54
54
  const stack = [{ value, depth: 0 }];
55
55
  let visited = 0;
56
+ let truncated = false;
56
57
  while (stack.length && output.length < maxValues && visited < 128) {
57
58
  const current = stack.pop();
58
59
  if (!current)
59
60
  continue;
60
61
  visited += 1;
61
62
  if (Array.isArray(current.value)) {
62
- if (current.depth >= MAX_RDAP_ENTITY_DEPTH)
63
+ if (current.depth >= MAX_RDAP_ENTITY_DEPTH) {
64
+ if (current.value.length > 0)
65
+ truncated = true;
63
66
  continue;
67
+ }
68
+ if (current.value.length > maxValues)
69
+ truncated = true;
64
70
  for (let i = Math.min(current.value.length, maxValues) - 1; i >= 0; i -= 1) {
65
71
  stack.push({ value: current.value[i], depth: current.depth + 1 });
66
72
  }
@@ -71,8 +77,16 @@ function flattenScalarValues(value, maxValues = 32) {
71
77
  else if (typeof current.value === 'number' && Number.isFinite(current.value)) {
72
78
  output.push(String(current.value));
73
79
  }
80
+ else {
81
+ truncated = true;
82
+ }
74
83
  }
75
- return output;
84
+ if (stack.length > 0)
85
+ truncated = true;
86
+ return { values: output, truncated };
87
+ }
88
+ function flattenScalarValues(value, maxValues = 32) {
89
+ return flattenScalarValuesWithCompleteness(value, maxValues).values;
76
90
  }
77
91
  function vcardRawValues(vcardArray, field) {
78
92
  if (!Array.isArray(vcardArray) || !Array.isArray(vcardArray[1]))
@@ -119,79 +133,156 @@ function normalizeAddresses(vcardArray) {
119
133
  return addresses;
120
134
  }
121
135
  function contactValuesTruncated(vcardArray) {
122
- for (const field of ['fn', 'org', 'email', 'tel']) {
136
+ const scalarFields = [
137
+ { field: 'fn', maxLength: 300, lower: false },
138
+ { field: 'org', maxLength: 300, lower: false },
139
+ { field: 'email', maxLength: 320, lower: true },
140
+ { field: 'tel', maxLength: 100, lower: false },
141
+ ];
142
+ for (const { field, maxLength, lower } of scalarFields) {
123
143
  let count = 0;
124
144
  for (const raw of vcardRawValues(vcardArray, field)) {
125
- count += flattenScalarValues(raw, MAX_CONTACT_VALUES + 1).length;
145
+ const flattened = flattenScalarValuesWithCompleteness(raw, MAX_CONTACT_VALUES + 1);
146
+ if (flattened.truncated
147
+ || flattened.values.some((value) => boundedString(value, maxLength, { lower }) === null))
148
+ return true;
149
+ count += flattened.values.length;
126
150
  if (count > MAX_CONTACT_VALUES)
127
151
  return true;
128
152
  }
129
153
  }
130
- return vcardRawValues(vcardArray, 'adr').length > MAX_CONTACT_VALUES;
154
+ const addressValues = vcardRawValues(vcardArray, 'adr');
155
+ if (addressValues.length > MAX_CONTACT_VALUES)
156
+ return true;
157
+ for (const raw of addressValues) {
158
+ const flattened = flattenScalarValuesWithCompleteness(raw, 33);
159
+ if (flattened.truncated || flattened.values.length > 32)
160
+ return true;
161
+ const parts = flattened.values.map((part) => boundedString(part, 300));
162
+ const rejectedNonBlankPart = parts.some((part, index) => {
163
+ if (part !== null)
164
+ return false;
165
+ const rawPart = flattened.values[index] ?? '';
166
+ return rawPart.length > 300 || /[\u0000-\u001f\u007f]/u.test(rawPart) || rawPart.trim() !== '';
167
+ });
168
+ const retainedParts = parts.filter((part) => part !== null);
169
+ if (rejectedNonBlankPart
170
+ || boundedString(retainedParts.join(', '), 1000) === null)
171
+ return true;
172
+ }
173
+ return false;
174
+ }
175
+ function malformedVcardEvidence(vcardArray) {
176
+ if (vcardArray === undefined)
177
+ return false;
178
+ if (!Array.isArray(vcardArray)
179
+ || String(vcardArray[0]).toLowerCase() !== 'vcard'
180
+ || !Array.isArray(vcardArray[1]))
181
+ return true;
182
+ const entries = vcardArray[1];
183
+ if (entries.length > MAX_VCARD_ENTRIES)
184
+ return true;
185
+ return entries.slice(0, MAX_VCARD_ENTRIES).some((entry) => (!Array.isArray(entry)
186
+ || entry.length < 4
187
+ || typeof entry[0] !== 'string'
188
+ || !entry[0].trim()));
131
189
  }
132
190
  function normalizeLinks(links, maxLinks = MAX_RDAP_LINKS) {
133
- if (!Array.isArray(links))
134
- return [];
135
191
  const normalized = [];
192
+ if (links === undefined)
193
+ return { items: normalized, truncated: false };
194
+ if (!Array.isArray(links))
195
+ return { items: normalized, truncated: true };
196
+ let truncated = links.length > 100 || links.length > maxLinks;
136
197
  for (const linkValue of links.slice(0, 100)) {
137
- if (!linkValue || typeof linkValue !== 'object' || Array.isArray(linkValue))
198
+ if (!linkValue || typeof linkValue !== 'object' || Array.isArray(linkValue)) {
199
+ truncated = true;
138
200
  continue;
201
+ }
139
202
  const link = linkValue;
140
203
  const href = boundedString(link.href, 2048);
141
- if (!href)
204
+ if (!href) {
205
+ truncated = true;
142
206
  continue;
207
+ }
143
208
  try {
144
209
  const parsed = new URL(href);
145
- if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:')
210
+ if (parsed.protocol !== 'https:' && parsed.protocol !== 'http:') {
211
+ truncated = true;
146
212
  continue;
213
+ }
147
214
  }
148
215
  catch {
216
+ truncated = true;
149
217
  continue;
150
218
  }
219
+ const rel = boundedString(link.rel, 100, { lower: true });
220
+ const type = boundedString(link.type, 160, { lower: true });
221
+ const title = boundedString(link.title, 300);
222
+ if (link.rel !== undefined && !rel
223
+ || link.type !== undefined && !type
224
+ || link.title !== undefined && !title)
225
+ truncated = true;
151
226
  normalized.push({
152
- rel: boundedString(link.rel, 100, { lower: true }),
227
+ rel,
153
228
  href,
154
- type: boundedString(link.type, 160, { lower: true }),
155
- title: boundedString(link.title, 300),
229
+ type,
230
+ title,
156
231
  });
157
232
  if (normalized.length >= maxLinks)
158
233
  break;
159
234
  }
160
- return normalized;
235
+ return { items: normalized, truncated };
161
236
  }
162
237
  function normalizePublicIds(publicIds) {
163
- if (!Array.isArray(publicIds))
164
- return [];
165
238
  const normalized = [];
239
+ if (publicIds === undefined)
240
+ return { items: normalized, truncated: false };
241
+ if (!Array.isArray(publicIds))
242
+ return { items: normalized, truncated: true };
243
+ let truncated = publicIds.length > 100 || publicIds.length > 20;
166
244
  for (const itemValue of publicIds.slice(0, 100)) {
167
- if (!itemValue || typeof itemValue !== 'object' || Array.isArray(itemValue))
245
+ if (!itemValue || typeof itemValue !== 'object' || Array.isArray(itemValue)) {
246
+ truncated = true;
168
247
  continue;
248
+ }
169
249
  const item = itemValue;
170
250
  const type = boundedString(item.type, 160);
171
251
  const identifier = boundedString(item.identifier, 300);
172
- if (!type || !identifier)
252
+ if (!type || !identifier) {
253
+ truncated = true;
173
254
  continue;
255
+ }
174
256
  normalized.push({ type, identifier });
175
257
  if (normalized.length >= 20)
176
258
  break;
177
259
  }
178
- return normalized;
260
+ return { items: normalized, truncated };
179
261
  }
180
262
  function normalizeStringList(value, maxItems, maxLength, { lower = false } = {}) {
263
+ if (value === undefined)
264
+ return { items: [], truncated: false };
181
265
  if (!Array.isArray(value))
182
- return [];
266
+ return { items: [], truncated: true };
183
267
  const output = [];
184
268
  const seen = new Set();
185
- for (const item of value.slice(0, Math.max(maxItems * 4, maxItems))) {
269
+ const inspectionLimit = Math.max(maxItems * 4, maxItems);
270
+ let truncated = value.length > inspectionLimit;
271
+ for (const item of value.slice(0, inspectionLimit)) {
186
272
  const normalized = boundedString(item, maxLength, { lower });
187
- if (!normalized || seen.has(normalized.toLowerCase()))
273
+ if (!normalized) {
274
+ truncated = true;
275
+ continue;
276
+ }
277
+ if (seen.has(normalized.toLowerCase()))
188
278
  continue;
189
279
  seen.add(normalized.toLowerCase());
190
- output.push(normalized);
191
- if (output.length >= maxItems)
192
- break;
280
+ if (output.length < maxItems)
281
+ output.push(normalized);
282
+ else
283
+ truncated = true;
193
284
  }
194
- return output;
285
+ return { items: output, truncated };
195
286
  }
196
287
  function redactionLabel(value) {
197
288
  if (typeof value === 'string')
@@ -201,16 +292,36 @@ function redactionLabel(value) {
201
292
  const record = value;
202
293
  return boundedString(record.type, 160) || boundedString(record.description, 300);
203
294
  }
295
+ function suppliedBoundedStringRejected(record, field, maxLength, options = {}) {
296
+ return Object.hasOwn(record, field) && boundedString(record[field], maxLength, options) === null;
297
+ }
298
+ function redactionLabelHasRejectedContent(value) {
299
+ if (typeof value === 'string')
300
+ return boundedString(value, 300) === null;
301
+ if (!value || typeof value !== 'object' || Array.isArray(value))
302
+ return true;
303
+ const label = value;
304
+ const hasType = Object.hasOwn(label, 'type');
305
+ const hasDescription = Object.hasOwn(label, 'description');
306
+ return (!hasType && !hasDescription)
307
+ || (hasType && boundedString(label.type, 160) === null)
308
+ || (hasDescription && boundedString(label.description, 300) === null);
309
+ }
204
310
  function normalizeRedactions(redacted) {
205
- if (!Array.isArray(redacted))
311
+ if (redacted === undefined)
206
312
  return { redactions: [], redactionsTruncated: false };
313
+ if (!Array.isArray(redacted))
314
+ return { redactions: [], redactionsTruncated: true };
207
315
  const redactions = [];
208
316
  const candidates = redacted.slice(0, MAX_RDAP_REDACTIONS * 2);
209
317
  let stoppedAt = candidates.length;
318
+ let truncated = redacted.length > candidates.length;
210
319
  for (let index = 0; index < candidates.length; index += 1) {
211
320
  const itemValue = candidates[index];
212
- if (!itemValue || typeof itemValue !== 'object' || Array.isArray(itemValue))
321
+ if (!itemValue || typeof itemValue !== 'object' || Array.isArray(itemValue)) {
322
+ truncated = true;
213
323
  continue;
324
+ }
214
325
  const item = itemValue;
215
326
  const entry = {
216
327
  name: redactionLabel(item.name),
@@ -221,8 +332,19 @@ function normalizeRedactions(redacted) {
221
332
  postPath: boundedString(item.postPath, 512),
222
333
  replacementPath: boundedString(item.replacementPath, 512),
223
334
  };
224
- if (!Object.values(entry).some(Boolean))
335
+ if (Object.hasOwn(item, 'name') && redactionLabelHasRejectedContent(item.name)
336
+ || Object.hasOwn(item, 'reason') && redactionLabelHasRejectedContent(item.reason)
337
+ || suppliedBoundedStringRejected(item, 'method', 80, { lower: true })
338
+ || suppliedBoundedStringRejected(item, 'pathLang', 80, { lower: true })
339
+ || suppliedBoundedStringRejected(item, 'prePath', 512)
340
+ || suppliedBoundedStringRejected(item, 'postPath', 512)
341
+ || suppliedBoundedStringRejected(item, 'replacementPath', 512)) {
342
+ truncated = true;
343
+ }
344
+ if (!Object.values(entry).some(Boolean)) {
345
+ truncated = true;
225
346
  continue;
347
+ }
226
348
  redactions.push(entry);
227
349
  if (redactions.length >= MAX_RDAP_REDACTIONS) {
228
350
  stoppedAt = index + 1;
@@ -231,38 +353,58 @@ function normalizeRedactions(redacted) {
231
353
  }
232
354
  return {
233
355
  redactions,
234
- redactionsTruncated: stoppedAt < redacted.length,
356
+ redactionsTruncated: truncated || stoppedAt < redacted.length,
235
357
  };
236
358
  }
237
359
  function normalizeDomainVariants(value) {
238
- if (!Array.isArray(value))
360
+ if (value === undefined)
239
361
  return { variants: [], variantsTruncated: false };
362
+ if (!Array.isArray(value))
363
+ return { variants: [], variantsTruncated: true };
240
364
  const variants = [];
241
365
  let truncated = value.length > MAX_RDAP_VARIANT_GROUPS;
242
366
  for (const groupValue of value.slice(0, MAX_RDAP_VARIANT_GROUPS)) {
243
- if (!groupValue || typeof groupValue !== 'object' || Array.isArray(groupValue))
367
+ if (!groupValue || typeof groupValue !== 'object' || Array.isArray(groupValue)) {
368
+ truncated = true;
244
369
  continue;
370
+ }
245
371
  const group = groupValue;
246
372
  const sourceNames = Array.isArray(group.variantNames) ? group.variantNames : [];
373
+ if (group.variantNames !== undefined && !Array.isArray(group.variantNames))
374
+ truncated = true;
247
375
  const variantNames = [];
248
376
  for (const nameValue of sourceNames.slice(0, MAX_RDAP_VARIANT_NAMES * 2)) {
249
- if (!nameValue || typeof nameValue !== 'object' || Array.isArray(nameValue))
377
+ if (!nameValue || typeof nameValue !== 'object' || Array.isArray(nameValue)) {
378
+ truncated = true;
250
379
  continue;
380
+ }
251
381
  const name = nameValue;
252
382
  const ldhName = boundedString(name.ldhName, 253);
253
383
  const unicodeName = boundedString(name.unicodeName, 253);
254
- if (!ldhName && !unicodeName)
384
+ if (suppliedBoundedStringRejected(name, 'ldhName', 253)
385
+ || suppliedBoundedStringRejected(name, 'unicodeName', 253))
386
+ truncated = true;
387
+ if (!ldhName && !unicodeName) {
388
+ truncated = true;
255
389
  continue;
390
+ }
256
391
  variantNames.push({ ldhName, unicodeName });
257
392
  if (variantNames.length >= MAX_RDAP_VARIANT_NAMES)
258
393
  break;
259
394
  }
260
395
  if (sourceNames.length > MAX_RDAP_VARIANT_NAMES)
261
396
  truncated = true;
262
- const relation = normalizeStringList(group.relation, 20, 100, { lower: true });
397
+ const relationInfo = normalizeStringList(group.relation, 20, 100, { lower: true });
398
+ if (relationInfo.truncated)
399
+ truncated = true;
400
+ const relation = relationInfo.items;
263
401
  const idnTable = boundedString(group.idnTable, 300);
264
- if (!variantNames.length && !relation.length && !idnTable)
402
+ if (group.idnTable !== undefined && !idnTable)
403
+ truncated = true;
404
+ if (!variantNames.length && !relation.length && !idnTable) {
405
+ truncated = true;
265
406
  continue;
407
+ }
266
408
  variants.push({ relation, idnTable, variantNames });
267
409
  }
268
410
  return { variants, variantsTruncated: truncated };
@@ -279,36 +421,50 @@ function textWouldTruncate(value, maxLength) {
279
421
  return value.replace(/\s+/g, ' ').trim().length > maxLength;
280
422
  }
281
423
  function summarizeTextBlocks(blocks) {
282
- if (!Array.isArray(blocks))
283
- return { items: [], truncated: false };
284
424
  const output = [];
425
+ if (blocks === undefined)
426
+ return { items: output, truncated: false };
427
+ if (!Array.isArray(blocks))
428
+ return { items: output, truncated: true };
285
429
  let truncated = blocks.length > 50;
286
430
  for (const blockValue of blocks.slice(0, 50)) {
287
- if (!blockValue || typeof blockValue !== 'object' || Array.isArray(blockValue))
431
+ if (!blockValue || typeof blockValue !== 'object' || Array.isArray(blockValue)) {
432
+ truncated = true;
288
433
  continue;
434
+ }
289
435
  const block = blockValue;
290
436
  const descriptions = [];
291
437
  const sourceDescriptions = Array.isArray(block.description) ? block.description : [];
438
+ if (!Array.isArray(block.description))
439
+ truncated = true;
292
440
  if (sourceDescriptions.length > 20)
293
441
  truncated = true;
294
442
  for (const text of sourceDescriptions.slice(0, 20)) {
295
443
  if (textWouldTruncate(text, 800))
296
444
  truncated = true;
297
445
  const description = truncatedText(text, 800);
298
- if (!description)
446
+ if (!description) {
447
+ truncated = true;
299
448
  continue;
449
+ }
300
450
  if (descriptions.length < 6)
301
451
  descriptions.push(description);
302
452
  else
303
453
  truncated = true;
304
454
  }
305
- if (!descriptions.length)
455
+ if (!descriptions.length) {
456
+ truncated = true;
306
457
  continue;
458
+ }
307
459
  if (textWouldTruncate(block.title, 160))
308
460
  truncated = true;
461
+ const title = truncatedText(block.title, 160);
462
+ const type = boundedString(block.type, 160, { lower: true });
463
+ if (block.title !== undefined && !title || block.type !== undefined && !type)
464
+ truncated = true;
309
465
  const item = {
310
- title: truncatedText(block.title, 160) || 'Notice',
311
- type: boundedString(block.type, 160, { lower: true }),
466
+ title: title || 'Notice',
467
+ type,
312
468
  descriptions,
313
469
  };
314
470
  if (output.length < 12)
@@ -369,9 +525,10 @@ function lifecycleDate(events, action, newest) {
369
525
  for (const event of events) {
370
526
  if (event.action !== action || !event.date)
371
527
  continue;
372
- const time = Date.parse(event.date);
373
- if (!Number.isFinite(time))
528
+ const normalized = registryDateIso(event.date);
529
+ if (!normalized)
374
530
  continue;
531
+ const time = Date.parse(normalized);
375
532
  if ((newest && time > selectedTime) || (!newest && time < selectedTime)) {
376
533
  selected = event.date;
377
534
  selectedTime = time;
@@ -407,16 +564,25 @@ function summarizeEntity(entity) {
407
564
  return null;
408
565
  const record = entity;
409
566
  const roles = [];
567
+ let rolesTruncated = record.roles !== undefined && !Array.isArray(record.roles);
410
568
  if (Array.isArray(record.roles)) {
569
+ if (record.roles.length > MAX_ENTITY_ROLES)
570
+ rolesTruncated = true;
411
571
  for (const rawRole of record.roles.slice(0, 100)) {
412
572
  const role = boundedString(rawRole, 80, { lower: true });
413
- if (!role || roles.includes(role))
573
+ if (!role) {
574
+ rolesTruncated = true;
575
+ continue;
576
+ }
577
+ if (roles.includes(role))
414
578
  continue;
415
579
  roles.push(role);
416
580
  if (roles.length >= MAX_ENTITY_ROLES)
417
581
  break;
418
582
  }
419
583
  }
584
+ const handle = boundedString(record.handle, 200);
585
+ const handleTruncated = record.handle !== undefined && handle === null;
420
586
  const names = normalizeContactValues(record.vcardArray, 'fn', 300);
421
587
  const organizations = normalizeContactValues(record.vcardArray, 'org', 300);
422
588
  const emails = normalizeContactValues(record.vcardArray, 'email', 320, { lower: true });
@@ -425,8 +591,10 @@ function summarizeEntity(entity) {
425
591
  const vcardEntries = Array.isArray(record.vcardArray) && Array.isArray(record.vcardArray[1])
426
592
  ? record.vcardArray[1]
427
593
  : [];
594
+ const publicIds = normalizePublicIds(record.publicIds);
595
+ const links = normalizeLinks(record.links, MAX_ENTITY_LINKS);
428
596
  const summary = {
429
- handle: boundedString(record.handle, 200),
597
+ handle,
430
598
  roles,
431
599
  name: names[0] || null,
432
600
  names,
@@ -438,13 +606,15 @@ function summarizeEntity(entity) {
438
606
  phones,
439
607
  address: addresses[0] || null,
440
608
  addresses,
441
- publicIds: normalizePublicIds(record.publicIds),
442
- links: normalizeLinks(record.links, MAX_ENTITY_LINKS),
443
- truncated: Boolean((Array.isArray(record.roles) && record.roles.length > MAX_ENTITY_ROLES)
609
+ publicIds: publicIds.items,
610
+ links: links.items,
611
+ truncated: Boolean(rolesTruncated
612
+ || handleTruncated
444
613
  || vcardEntries.length > MAX_VCARD_ENTRIES
614
+ || malformedVcardEvidence(record.vcardArray)
445
615
  || contactValuesTruncated(record.vcardArray)
446
- || (Array.isArray(record.publicIds) && record.publicIds.length > 20)
447
- || (Array.isArray(record.links) && record.links.length > MAX_ENTITY_LINKS)),
616
+ || publicIds.truncated
617
+ || links.truncated),
448
618
  };
449
619
  const hasAny = Boolean(summary.handle || summary.name || summary.org || summary.email
450
620
  || summary.phone || summary.address || summary.publicIds.length || summary.links.length);
@@ -453,7 +623,8 @@ function summarizeEntity(entity) {
453
623
  function summarizeEntities(entities) {
454
624
  const summaries = [];
455
625
  const source = Array.isArray(entities) ? entities : [];
456
- let truncated = source.length > MAX_RDAP_ENTITIES;
626
+ let truncated = entities !== undefined && !Array.isArray(entities)
627
+ || source.length > MAX_RDAP_ENTITIES;
457
628
  const stack = source
458
629
  .slice(0, MAX_RDAP_ENTITIES)
459
630
  .reverse()
@@ -465,15 +636,26 @@ function summarizeEntities(entities) {
465
636
  if (!current)
466
637
  continue;
467
638
  const { entity, depth } = current;
468
- if (!entity || typeof entity !== 'object' || Array.isArray(entity) || seen.has(entity))
639
+ if (!entity || typeof entity !== 'object' || Array.isArray(entity) || seen.has(entity)) {
640
+ truncated = true;
469
641
  continue;
642
+ }
470
643
  seen.add(entity);
471
644
  visited += 1;
472
645
  const summary = summarizeEntity(entity);
473
646
  if (summary)
474
647
  summaries.push(summary);
648
+ else
649
+ truncated = true;
475
650
  const record = entity;
476
- if (depth >= MAX_RDAP_ENTITY_DEPTH || !Array.isArray(record.entities))
651
+ if (depth >= MAX_RDAP_ENTITY_DEPTH) {
652
+ if (Array.isArray(record.entities) && record.entities.length)
653
+ truncated = true;
654
+ continue;
655
+ }
656
+ if (record.entities !== undefined && !Array.isArray(record.entities))
657
+ truncated = true;
658
+ if (!Array.isArray(record.entities))
477
659
  continue;
478
660
  const remaining = Math.max(0, MAX_RDAP_ENTITIES - visited - stack.length);
479
661
  const nested = record.entities.slice(0, remaining);
@@ -519,6 +701,8 @@ function parseRdapObject(type, data) {
519
701
  const redactionInfo = normalizeRedactions(data.redacted);
520
702
  const noticesInfo = summarizeTextBlocks(data.notices);
521
703
  const remarksInfo = summarizeTextBlocks(data.remarks);
704
+ const linksInfo = normalizeLinks(data.links);
705
+ const conformanceInfo = normalizeStringList(data.rdapConformance, 50, 160, { lower: true });
522
706
  const serverTruncationReasons = summarizeServerTruncation(data.notices, data.remarks);
523
707
  // Preserve the established status-array contract while making it available
524
708
  // to every RDAP object type. Unlike set-like fields, status order and
@@ -528,16 +712,18 @@ function parseRdapObject(type, data) {
528
712
  .map((status) => boundedString(status, 160))
529
713
  .filter((status) => status !== null)
530
714
  : [];
715
+ const inspectedStatusCount = Array.isArray(data.status) ? Math.min(data.status.length, 100) : 0;
716
+ const inspectedEventCount = Array.isArray(data.events) ? Math.min(data.events.length, 100) : 0;
531
717
  const common = {
532
718
  objectClassName: boundedString(data.objectClassName, 80, { lower: true }),
533
719
  language: boundedString(data.lang, 35, { lower: true }),
534
- conformance: normalizeStringList(data.rdapConformance, 50, 160, { lower: true }),
535
- conformanceTruncated: Array.isArray(data.rdapConformance) && data.rdapConformance.length > 50,
720
+ conformance: conformanceInfo.items,
721
+ conformanceTruncated: conformanceInfo.truncated,
536
722
  ...redactionInfo,
537
723
  port43: boundedString(data.port43, 300),
538
724
  parentHandle: boundedString(data.parentHandle, 300),
539
- links: normalizeLinks(data.links),
540
- linksTruncated: Array.isArray(data.links) && data.links.length > MAX_RDAP_LINKS,
725
+ links: linksInfo.items,
726
+ linksTruncated: linksInfo.truncated,
541
727
  notices: noticesInfo.items,
542
728
  noticesTruncated: noticesInfo.truncated,
543
729
  remarks: remarksInfo.items,
@@ -545,33 +731,70 @@ function parseRdapObject(type, data) {
545
731
  serverTruncated: serverTruncationReasons.length > 0,
546
732
  serverTruncationReasons,
547
733
  statuses,
548
- statusesTruncated: Array.isArray(data.status) && data.status.length > 100,
734
+ statusesTruncated: data.status !== undefined && (!Array.isArray(data.status)
735
+ || data.status.length > 100
736
+ || statuses.length < inspectedStatusCount),
549
737
  events,
550
- eventsTruncated: Array.isArray(data.events) && data.events.length > 100,
738
+ eventsTruncated: data.events !== undefined && (!Array.isArray(data.events)
739
+ || data.events.length > 100
740
+ || events.length < inspectedEventCount),
551
741
  lifecycle: summarizeLifecycle(events),
552
742
  };
553
743
  if (type === 'domain') {
554
744
  const { entitiesByRole, entitiesTruncated, truncatedEntityRoles } = entityInventory(data.entities);
555
745
  const registrarEntity = entitiesByRole.registrar && entitiesByRole.registrar[0];
556
746
  let nameserverAddressesTruncated = false;
747
+ let invalidNameservers = 0;
557
748
  const nameserverDetails = Array.isArray(data.nameservers)
558
749
  ? data.nameservers.slice(0, 200).map((ns) => {
559
- if (!ns || typeof ns !== 'object' || Array.isArray(ns))
750
+ if (!ns || typeof ns !== 'object' || Array.isArray(ns)) {
751
+ invalidNameservers += 1;
560
752
  return null;
753
+ }
561
754
  const nameserver = ns;
562
- const ipAddresses = nameserver.ipAddresses && typeof nameserver.ipAddresses === 'object' && !Array.isArray(nameserver.ipAddresses)
755
+ const validIpAddresses = nameserver.ipAddresses !== null
756
+ && typeof nameserver.ipAddresses === 'object'
757
+ && !Array.isArray(nameserver.ipAddresses);
758
+ const ipAddresses = validIpAddresses
563
759
  ? nameserver.ipAddresses
564
760
  : {};
761
+ if (nameserver.ipAddresses !== undefined && !validIpAddresses) {
762
+ nameserverAddressesTruncated = true;
763
+ }
565
764
  const v4 = Array.isArray(ipAddresses.v4) ? ipAddresses.v4 : [];
566
765
  const v6 = Array.isArray(ipAddresses.v6) ? ipAddresses.v6 : [];
567
- if (v4.length + v6.length > 20)
766
+ if (ipAddresses.v4 !== undefined && !Array.isArray(ipAddresses.v4)
767
+ || ipAddresses.v6 !== undefined && !Array.isArray(ipAddresses.v6)) {
768
+ nameserverAddressesTruncated = true;
769
+ }
770
+ const maximumCandidates = 200;
771
+ const maximumAddresses = 20;
772
+ const addresses = [];
773
+ let visited = 0;
774
+ for (const [candidates, family] of [[v4, 4], [v6, 6]]) {
775
+ for (let index = 0; index < candidates.length; index += 1) {
776
+ if (visited >= maximumCandidates || addresses.length >= maximumAddresses)
777
+ break;
778
+ visited += 1;
779
+ const address = boundedString(candidates[index], 80);
780
+ if (address && net.isIP(address) === family)
781
+ addresses.push(address);
782
+ else
783
+ nameserverAddressesTruncated = true;
784
+ }
785
+ if (visited >= maximumCandidates || addresses.length >= maximumAddresses)
786
+ break;
787
+ }
788
+ if (v4.length + v6.length > visited || v4.length + v6.length > maximumAddresses) {
568
789
  nameserverAddressesTruncated = true;
569
- const addresses = [
570
- ...v4.map((address) => boundedString(address, 80)).filter((address) => address && net.isIP(address) === 4),
571
- ...v6.map((address) => boundedString(address, 80)).filter((address) => address && net.isIP(address) === 6),
572
- ].slice(0, 20);
790
+ }
791
+ const name = boundedString(nameserver.ldhName || nameserver.unicodeName, 253);
792
+ if (!name) {
793
+ invalidNameservers += 1;
794
+ return null;
795
+ }
573
796
  return {
574
- name: boundedString(nameserver.ldhName || nameserver.unicodeName, 253),
797
+ name,
575
798
  addresses,
576
799
  };
577
800
  }).filter((ns) => Boolean(ns?.name))
@@ -582,10 +805,13 @@ function parseRdapObject(type, data) {
582
805
  ? data.secureDNS
583
806
  : null;
584
807
  const variantInfo = normalizeDomainVariants(data.variants);
808
+ let invalidDsData = 0;
585
809
  const dsData = secureDns && Array.isArray(secureDns.dsData)
586
810
  ? secureDns.dsData.slice(0, 50).map((ds) => {
587
- if (!ds || typeof ds !== 'object' || Array.isArray(ds))
811
+ if (!ds || typeof ds !== 'object' || Array.isArray(ds)) {
812
+ invalidDsData += 1;
588
813
  return null;
814
+ }
589
815
  const record = ds;
590
816
  const digest = boundedString(record.digest, 512);
591
817
  const normalized = {
@@ -593,9 +819,11 @@ function parseRdapObject(type, data) {
593
819
  digestType: boundedInteger(record.digestType, 0, 255),
594
820
  digest: digest && digest.length % 2 === 0 && /^[0-9a-f]+$/i.test(digest) ? digest : null,
595
821
  };
596
- return Object.values(normalized).every((value) => value !== null)
597
- ? normalized
598
- : null;
822
+ if (Object.values(normalized).every((value) => value !== null)) {
823
+ return normalized;
824
+ }
825
+ invalidDsData += 1;
826
+ return null;
599
827
  }).filter((value) => value !== null)
600
828
  : [];
601
829
  return {
@@ -606,7 +834,9 @@ function parseRdapObject(type, data) {
606
834
  handle: boundedString(data.handle, 300),
607
835
  nameservers: nameserverDetails.map((ns) => ns.name),
608
836
  nameserverDetails,
609
- nameserversTruncated: Array.isArray(data.nameservers) && data.nameservers.length > 200,
837
+ nameserversTruncated: data.nameservers !== undefined && (!Array.isArray(data.nameservers)
838
+ || data.nameservers.length > 200
839
+ || invalidNameservers > 0),
610
840
  nameserverAddressesTruncated,
611
841
  dnssec: secureDns && secureDns.delegationSigned === true
612
842
  ? 'Signed' : secureDns && secureDns.delegationSigned === false ? 'Unsigned' : 'Unknown',
@@ -614,7 +844,10 @@ function parseRdapObject(type, data) {
614
844
  delegationSigned: secureDns && typeof secureDns.delegationSigned === 'boolean'
615
845
  ? secureDns.delegationSigned : null,
616
846
  dsData,
617
- dsDataTruncated: Boolean(secureDns && Array.isArray(secureDns.dsData) && secureDns.dsData.length > 50),
847
+ dsDataTruncated: Boolean(data.secureDNS !== undefined && !secureDns
848
+ || secureDns && secureDns.dsData !== undefined && (!Array.isArray(secureDns.dsData)
849
+ || secureDns.dsData.length > 50
850
+ || invalidDsData > 0)),
618
851
  ...variantInfo,
619
852
  registrarIanaId: publicId(registrarEntity, /iana registrar id/i),
620
853
  entitiesByRole,
@@ -630,17 +863,26 @@ function parseRdapObject(type, data) {
630
863
  }
631
864
  if (type === 'ipv4' || type === 'ipv6') {
632
865
  const { entitiesByRole, entitiesTruncated, truncatedEntityRoles } = entityInventory(data.entities);
866
+ let invalidCidrs = 0;
633
867
  const cidrs = Array.isArray(data.cidr0_cidrs)
634
868
  ? data.cidr0_cidrs.slice(0, 200)
635
869
  .map((c) => {
636
- if (!c || typeof c !== 'object' || Array.isArray(c))
870
+ if (!c || typeof c !== 'object' || Array.isArray(c)) {
871
+ invalidCidrs += 1;
637
872
  return null;
873
+ }
638
874
  const expectedFamily = type === 'ipv4' ? 4 : 6;
639
875
  const prefix = boundedString(expectedFamily === 4 ? c.v4prefix : c.v6prefix, 80);
640
- if (!prefix || net.isIP(prefix) !== expectedFamily)
876
+ if (!prefix || net.isIP(prefix) !== expectedFamily) {
877
+ invalidCidrs += 1;
641
878
  return null;
879
+ }
642
880
  const length = boundedInteger(c.length, 0, expectedFamily === 4 ? 32 : 128);
643
- return prefix && length !== null ? `${prefix}/${length}` : null;
881
+ if (length === null) {
882
+ invalidCidrs += 1;
883
+ return null;
884
+ }
885
+ return `${prefix}/${length}`;
644
886
  })
645
887
  .filter((value) => value !== null)
646
888
  : [];
@@ -651,7 +893,9 @@ function parseRdapObject(type, data) {
651
893
  startAddress: boundedString(data.startAddress, 80),
652
894
  endAddress: boundedString(data.endAddress, 80),
653
895
  cidrs,
654
- cidrsTruncated: Array.isArray(data.cidr0_cidrs) && data.cidr0_cidrs.length > 200,
896
+ cidrsTruncated: data.cidr0_cidrs !== undefined && (!Array.isArray(data.cidr0_cidrs)
897
+ || data.cidr0_cidrs.length > 200
898
+ || invalidCidrs > 0),
655
899
  country: boundedString(data.country, 2),
656
900
  networkType: boundedString(data.type, 160),
657
901
  entitiesByRole,