@slicedearth/whoisleuth-cli 1.47.4 → 2.0.1

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 (236) hide show
  1. package/README.md +74 -177
  2. package/SECURITY.md +6 -3
  3. package/bin/whoisleuth.mjs +2 -1
  4. package/cli/archive-inspect.mjs +28 -10
  5. package/cli/arguments.mjs +204 -174
  6. package/cli/artifact-structure.mjs +837 -163
  7. package/cli/artifact-verify.mjs +90 -61
  8. package/cli/bounded-json.mjs +1 -1
  9. package/cli/bulk-checkpoint.mjs +33 -20
  10. package/cli/bulk.mjs +9 -2
  11. package/cli/case-pack.mjs +180 -576
  12. package/cli/ci-report.mjs +6 -1
  13. package/cli/collection-preflight.mjs +2 -2
  14. package/cli/command-catalogue.mjs +12 -3
  15. package/cli/command-reference.mjs +910 -92
  16. package/cli/completion.mjs +504 -226
  17. package/cli/config-profile.mjs +13 -3
  18. package/cli/discover.mjs +7 -1
  19. package/cli/discovery-observation-snapshot.mjs +24 -28
  20. package/cli/discovery-scan-command-runner.mjs +9 -9
  21. package/cli/discovery-scan.mjs +17 -3
  22. package/cli/discovery-snapshot.mjs +15 -7
  23. package/cli/domain-control-monitor.mjs +203 -30
  24. package/cli/domain-control-observations.mjs +15 -21
  25. package/cli/errors.mjs +8 -3
  26. package/cli/evidence-signing.mjs +10 -20
  27. package/cli/external-observation-mapping.mjs +3 -3
  28. package/cli/formatters/json.mjs +20 -32
  29. package/cli/formatters/terminal.mjs +583 -22
  30. package/cli/input.mjs +7 -2
  31. package/cli/interactive-launcher.mjs +226 -0
  32. package/cli/interchange-report.mjs +19 -19
  33. package/cli/investigation-manifest.mjs +1 -1
  34. package/cli/investigation-plan.mjs +167 -16
  35. package/cli/investigation-run.mjs +9 -1
  36. package/cli/lookalike-calibration.mjs +3 -1
  37. package/cli/lookup-brief.mjs +13 -8
  38. package/cli/lookup-browser.mjs +844 -0
  39. package/cli/lookup-command-runner.mjs +94 -31
  40. package/cli/lookup-diff.mjs +2 -2
  41. package/cli/lookup-plan.mjs +1 -0
  42. package/cli/mail-review.mjs +206 -94
  43. package/cli/manual.mjs +5 -5
  44. package/cli/offline-evidence-review.mjs +18 -7
  45. package/cli/open-asset-model-bridge.mjs +1 -1
  46. package/cli/output-file.mjs +70 -14
  47. package/cli/page-compare.mjs +11 -6
  48. package/cli/progress-events.mjs +2 -1
  49. package/cli/progress.mjs +3 -3
  50. package/cli/registry-cohort.mjs +9 -51
  51. package/cli/registry-doctor.mjs +67 -28
  52. package/cli/retained-artifact-diff.mjs +7 -8
  53. package/cli/risk-calibration.mjs +85 -38
  54. package/cli/runner.mjs +189 -75
  55. package/cli/saved-lookup.mjs +424 -57
  56. package/cli/sharing-review.mjs +6 -1
  57. package/cli/source-reliability.mjs +75 -36
  58. package/cli/terminal-presentation.mjs +43 -18
  59. package/docs/cli-reference.md +125 -1708
  60. package/docs/cli.md +86 -254
  61. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -145
  62. package/frontend/src/lib/analysis/domain-control-records.js +3 -88
  63. package/lib/availability.mjs +58 -40
  64. package/lib/bounded-contract-normalizers.mjs +2 -4
  65. package/lib/bounded-file.mjs +78 -4
  66. package/lib/bounded-json.mjs +51 -13
  67. package/lib/browser-library-profile.mjs +1 -1
  68. package/lib/classify.mjs +56 -5
  69. package/lib/client-behavior-profile.mjs +1 -1
  70. package/lib/credential-surface-profile.mjs +1 -1
  71. package/lib/cryptographic-assurance.mjs +35 -17
  72. package/lib/ct-query.mjs +3 -3
  73. package/lib/ct-search.mjs +37 -21
  74. package/lib/distributed-operation-budget.mjs +4 -2
  75. package/lib/dns-delegation-health.mjs +9 -6
  76. package/lib/dns-intelligence.mjs +1 -1
  77. package/lib/dnssec-chain-validation.mjs +2 -3
  78. package/lib/dnssec-evidence-validation.mjs +13 -7
  79. package/lib/domain-assurance.mjs +12 -12
  80. package/lib/domain-change-packet.mjs +1 -1
  81. package/lib/domain-control-flight-recorder.mjs +442 -78
  82. package/lib/domain-control-manifest.mjs +314 -59
  83. package/lib/domain-posture-analysis.mjs +5 -2
  84. package/lib/encrypted-dns-contract.mjs +2 -3
  85. package/lib/evidence-export.mjs +518 -36
  86. package/lib/external-intelligence-risk.mjs +26 -10
  87. package/lib/feature-policy.mjs +1 -1
  88. package/lib/homepage-metadata-contract.mjs +290 -0
  89. package/lib/hostname.mjs +1 -1
  90. package/lib/html-signals.mjs +89 -5
  91. package/lib/http-intelligence.mjs +331 -2
  92. package/lib/interchange-fidelity-registry.mjs +28 -26
  93. package/lib/lookup-cache.mjs +6 -4
  94. package/lib/lookup.mjs +25 -2
  95. package/lib/observed-network-context.mjs +1 -1
  96. package/lib/page-fingerprints.mjs +2 -2
  97. package/lib/page-role-profile.mjs +1 -1
  98. package/lib/portable-generator.mjs +8 -0
  99. package/lib/project-metadata.mjs +1 -1
  100. package/lib/provider-policy-admission.mjs +3 -1
  101. package/lib/rdap-bootstrap.mjs +76 -16
  102. package/lib/rdap-client.mjs +20 -12
  103. package/lib/rdap-normalization.mjs +306 -75
  104. package/lib/rdap-transport.mjs +2 -2
  105. package/lib/rdap-validation.mjs +65 -14
  106. package/lib/rdap.mjs +2 -2
  107. package/lib/registrable-domain.mjs +36 -0
  108. package/lib/registry-insights.mjs +7 -0
  109. package/lib/response-policy.mjs +27 -4
  110. package/lib/risk-calibration-summary.mjs +35 -33
  111. package/lib/risk-scoring.mjs +4 -3
  112. package/lib/safe-fetch.mjs +62 -40
  113. package/lib/security-txt.mjs +1 -1
  114. package/lib/smtp-transport-review.mjs +8 -5
  115. package/lib/sslbl-intelligence.mjs +6 -8
  116. package/lib/static-html-analysis.mjs +301 -4
  117. package/lib/structured-data-identity.mjs +51 -19
  118. package/lib/threat-intelligence-fixtures.mjs +2 -4
  119. package/lib/threat-intelligence-manifests.mjs +2 -7
  120. package/lib/threat-intelligence-runtime.mjs +3 -5
  121. package/lib/threat-intelligence-types.mjs +3 -0
  122. package/lib/threatfox-intelligence.mjs +7 -3
  123. package/lib/tls-intelligence.mjs +4 -4
  124. package/lib/urlhaus-intelligence.mjs +4 -2
  125. package/lib/urlscan-intelligence.mjs +6 -5
  126. package/lib/website-security-posture.mjs +1 -1
  127. package/lib/website-technology.mjs +1 -1
  128. package/lib/whois-authority.mjs +43 -8
  129. package/lib/whois-chain.mjs +25 -3
  130. package/lib/whois-normalization.mjs +3 -1
  131. package/lib/whois-transport.mjs +12 -3
  132. package/lib/zone-intent-review.mjs +1 -1
  133. package/package.json +13 -5
  134. package/{frontend/src/lib/analysis/case-brand-profile-references.js → packages/cases/case-brand-profile-references.mjs} +2 -2
  135. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +35 -14
  136. package/{frontend/src/lib/analysis/case-investigation-branch-model.js → packages/cases/case-investigation-branch-model.mjs} +10 -12
  137. package/packages/cases/case-migration-model.mjs +476 -0
  138. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  139. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -21
  140. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  141. package/packages/cases/case-record-model.mjs +9 -0
  142. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +72 -29
  143. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +108 -14
  144. package/packages/cases/case-response-model.mjs +1604 -0
  145. package/packages/cases/case-response-packet.mjs +1312 -0
  146. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  147. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  148. package/{frontend/src/lib/analysis/comparison-ledger-bulk.js → packages/comparison/comparison-ledger-bulk.mjs} +2 -2
  149. package/{frontend/src/lib/analysis/comparison-ledger-contract.js → packages/comparison/comparison-ledger-contract.mjs} +4 -4
  150. package/{frontend/src/lib/analysis/comparison-ledger-json.js → packages/comparison/comparison-ledger-json.mjs} +1 -0
  151. package/{frontend/src/lib/analysis/comparison-ledger-serialization.js → packages/comparison/comparison-ledger-serialization.mjs} +1 -0
  152. package/packages/comparison/favicon-similarity.mjs +78 -0
  153. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  154. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  155. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  156. package/packages/contracts/analyst-review-state.mjs +50 -0
  157. package/packages/contracts/case-portability.mjs +1310 -0
  158. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  159. package/packages/contracts/cli-lookup.mjs +394 -0
  160. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  161. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  162. package/packages/contracts/domain-control-monitor.mjs +310 -0
  163. package/packages/contracts/domain-control-review.mjs +828 -0
  164. package/packages/contracts/domain-name.mjs +4 -0
  165. package/packages/contracts/external-observation-interchange.mjs +224 -0
  166. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  167. package/packages/contracts/investigation-portability.mjs +331 -0
  168. package/packages/contracts/offline-comparison.mjs +160 -0
  169. package/packages/contracts/risk-calibration.mjs +1288 -0
  170. package/packages/contracts/schema-compatibility.mjs +71 -0
  171. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  172. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  173. package/packages/contracts/workspace-portability.mjs +1347 -0
  174. package/{frontend/src/lib/analysis/artifact-integrity.js → packages/evidence/artifact-integrity.mjs} +13 -11
  175. package/packages/evidence/decision-fact.mjs +735 -0
  176. package/packages/evidence/domain-control-runtime.mjs +547 -0
  177. package/packages/evidence/domain-name.mjs +41 -0
  178. package/packages/evidence/observation.mjs +384 -0
  179. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +24 -22
  180. package/packages/interchange/retained-text.mjs +8 -0
  181. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +9 -6
  182. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +5 -6
  183. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +4 -6
  184. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +5 -5
  185. package/packages/investigation/bulk-review-policy.mjs +1 -0
  186. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +30 -32
  187. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  188. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  189. package/{frontend/src/lib/analysis/lookup-claim-passport.js → packages/investigation/lookup-claim-passport.mjs} +6 -10
  190. package/{frontend/src/lib/analysis/lookup-claim-readiness.js → packages/investigation/lookup-claim-readiness.mjs} +9 -2
  191. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  192. package/packages/monitoring/analyst-review-state.mjs +680 -0
  193. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +71 -48
  194. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +13 -14
  195. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +33 -59
  196. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +8 -5
  197. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  198. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  199. package/packages/workspace/hostile-input.mjs +172 -0
  200. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +8 -45
  201. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +18 -21
  202. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  203. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  204. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  205. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  206. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  207. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +13 -13
  208. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  209. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +110 -89
  210. package/third-party-notices.txt +14 -14
  211. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  212. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  213. package/frontend/src/lib/analysis/case-migration-model.js +0 -305
  214. package/frontend/src/lib/analysis/case-record-model.js +0 -9
  215. package/frontend/src/lib/analysis/case-response-model.js +0 -728
  216. package/frontend/src/lib/analysis/case-response-packet.js +0 -583
  217. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -542
  218. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  219. package/frontend/src/lib/analysis/lookup-presentation.js +0 -120
  220. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  221. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  222. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  223. package/frontend/src/lib/analysis/scoring.js +0 -55
  224. package/frontend/src/lib/analysis/utils.js +0 -255
  225. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  226. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  227. package/frontend/src/lib/analysis/visualization-matrix.js +0 -131
  228. package/frontend/src/lib/analysis/visualization-models.js +0 -338
  229. package/frontend/src/lib/analysis/visualization-time-series.js +0 -185
  230. package/lib/bounded-json-response.mjs +0 -170
  231. package/lib/lookup-response-contract.mjs +0 -1034
  232. package/lib/observation.mjs +0 -115
  233. /package/{frontend/src/lib/analysis/opaque-reference-id.js → packages/cases/opaque-reference-id.mjs} +0 -0
  234. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  235. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  236. /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,25 +731,42 @@ 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 : [];
766
+ if (ipAddresses.v4 !== undefined && !Array.isArray(ipAddresses.v4)
767
+ || ipAddresses.v6 !== undefined && !Array.isArray(ipAddresses.v6)) {
768
+ nameserverAddressesTruncated = true;
769
+ }
567
770
  const maximumCandidates = 200;
568
771
  const maximumAddresses = 20;
569
772
  const addresses = [];
@@ -576,6 +779,8 @@ function parseRdapObject(type, data) {
576
779
  const address = boundedString(candidates[index], 80);
577
780
  if (address && net.isIP(address) === family)
578
781
  addresses.push(address);
782
+ else
783
+ nameserverAddressesTruncated = true;
579
784
  }
580
785
  if (visited >= maximumCandidates || addresses.length >= maximumAddresses)
581
786
  break;
@@ -583,8 +788,13 @@ function parseRdapObject(type, data) {
583
788
  if (v4.length + v6.length > visited || v4.length + v6.length > maximumAddresses) {
584
789
  nameserverAddressesTruncated = true;
585
790
  }
791
+ const name = boundedString(nameserver.ldhName || nameserver.unicodeName, 253);
792
+ if (!name) {
793
+ invalidNameservers += 1;
794
+ return null;
795
+ }
586
796
  return {
587
- name: boundedString(nameserver.ldhName || nameserver.unicodeName, 253),
797
+ name,
588
798
  addresses,
589
799
  };
590
800
  }).filter((ns) => Boolean(ns?.name))
@@ -595,10 +805,13 @@ function parseRdapObject(type, data) {
595
805
  ? data.secureDNS
596
806
  : null;
597
807
  const variantInfo = normalizeDomainVariants(data.variants);
808
+ let invalidDsData = 0;
598
809
  const dsData = secureDns && Array.isArray(secureDns.dsData)
599
810
  ? secureDns.dsData.slice(0, 50).map((ds) => {
600
- if (!ds || typeof ds !== 'object' || Array.isArray(ds))
811
+ if (!ds || typeof ds !== 'object' || Array.isArray(ds)) {
812
+ invalidDsData += 1;
601
813
  return null;
814
+ }
602
815
  const record = ds;
603
816
  const digest = boundedString(record.digest, 512);
604
817
  const normalized = {
@@ -606,9 +819,11 @@ function parseRdapObject(type, data) {
606
819
  digestType: boundedInteger(record.digestType, 0, 255),
607
820
  digest: digest && digest.length % 2 === 0 && /^[0-9a-f]+$/i.test(digest) ? digest : null,
608
821
  };
609
- return Object.values(normalized).every((value) => value !== null)
610
- ? normalized
611
- : null;
822
+ if (Object.values(normalized).every((value) => value !== null)) {
823
+ return normalized;
824
+ }
825
+ invalidDsData += 1;
826
+ return null;
612
827
  }).filter((value) => value !== null)
613
828
  : [];
614
829
  return {
@@ -619,7 +834,9 @@ function parseRdapObject(type, data) {
619
834
  handle: boundedString(data.handle, 300),
620
835
  nameservers: nameserverDetails.map((ns) => ns.name),
621
836
  nameserverDetails,
622
- 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),
623
840
  nameserverAddressesTruncated,
624
841
  dnssec: secureDns && secureDns.delegationSigned === true
625
842
  ? 'Signed' : secureDns && secureDns.delegationSigned === false ? 'Unsigned' : 'Unknown',
@@ -627,7 +844,10 @@ function parseRdapObject(type, data) {
627
844
  delegationSigned: secureDns && typeof secureDns.delegationSigned === 'boolean'
628
845
  ? secureDns.delegationSigned : null,
629
846
  dsData,
630
- 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)),
631
851
  ...variantInfo,
632
852
  registrarIanaId: publicId(registrarEntity, /iana registrar id/i),
633
853
  entitiesByRole,
@@ -643,17 +863,26 @@ function parseRdapObject(type, data) {
643
863
  }
644
864
  if (type === 'ipv4' || type === 'ipv6') {
645
865
  const { entitiesByRole, entitiesTruncated, truncatedEntityRoles } = entityInventory(data.entities);
866
+ let invalidCidrs = 0;
646
867
  const cidrs = Array.isArray(data.cidr0_cidrs)
647
868
  ? data.cidr0_cidrs.slice(0, 200)
648
869
  .map((c) => {
649
- if (!c || typeof c !== 'object' || Array.isArray(c))
870
+ if (!c || typeof c !== 'object' || Array.isArray(c)) {
871
+ invalidCidrs += 1;
650
872
  return null;
873
+ }
651
874
  const expectedFamily = type === 'ipv4' ? 4 : 6;
652
875
  const prefix = boundedString(expectedFamily === 4 ? c.v4prefix : c.v6prefix, 80);
653
- if (!prefix || net.isIP(prefix) !== expectedFamily)
876
+ if (!prefix || net.isIP(prefix) !== expectedFamily) {
877
+ invalidCidrs += 1;
654
878
  return null;
879
+ }
655
880
  const length = boundedInteger(c.length, 0, expectedFamily === 4 ? 32 : 128);
656
- return prefix && length !== null ? `${prefix}/${length}` : null;
881
+ if (length === null) {
882
+ invalidCidrs += 1;
883
+ return null;
884
+ }
885
+ return `${prefix}/${length}`;
657
886
  })
658
887
  .filter((value) => value !== null)
659
888
  : [];
@@ -664,7 +893,9 @@ function parseRdapObject(type, data) {
664
893
  startAddress: boundedString(data.startAddress, 80),
665
894
  endAddress: boundedString(data.endAddress, 80),
666
895
  cidrs,
667
- 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),
668
899
  country: boundedString(data.country, 2),
669
900
  networkType: boundedString(data.type, 160),
670
901
  entitiesByRole,