@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
@@ -85,11 +85,14 @@ export function selectRegistrarRdapLink(domain, links, registryEndpoint = null)
85
85
  }
86
86
  return null;
87
87
  }
88
+ function registrarRdapTransport(override, safeDefault = fetchRdapDetailedWithTimeout) {
89
+ return override ?? safeDefault;
90
+ }
88
91
  export async function fetchRegistrarRdapRecordWithParser(domain, registryRecord, parseRdap, options = {}) {
89
92
  const canonical = canonicalRdapDomain(domain);
90
93
  if (!canonical)
91
94
  throw new Error('A valid domain is required for registrar RDAP.');
92
- const fetchUpstream = options.fetchUpstream || fetchRdapDetailedWithTimeout;
95
+ const fetchUpstream = registrarRdapTransport(options.fetchUpstream);
93
96
  return cached(`rdap-registrar:domain:${canonical}`, async () => {
94
97
  const registryParsed = recordOrNull(registryRecord?.parsed);
95
98
  const registryEndpoint = typeof registryRecord?.rdapServer === 'string' ? registryRecord.rdapServer : null;
@@ -194,3 +197,4 @@ export async function fetchRegistrarRdapRecordWithParser(domain, registryRecord,
194
197
  };
195
198
  });
196
199
  }
200
+ export { registrarRdapTransport };
@@ -8,7 +8,7 @@ async function fetchRdapWithTimeout(url, options, timeoutMs, dependencies = {})
8
8
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
9
9
  try {
10
10
  const response = await (dependencies.fetch ?? safeFetch)(url, { ...options, signal: controller.signal });
11
- const { text, truncated } = await (dependencies.readText ?? readTextCapped)(response, MAX_RDAP_BYTES);
11
+ const { text, truncated } = await (dependencies.readText ?? readTextCapped)(response, MAX_RDAP_BYTES, { fatalUtf8: true });
12
12
  if (truncated)
13
13
  throw new Error(`Response from ${url} exceeded ${MAX_RDAP_BYTES} bytes`);
14
14
  return { status: response.status, ok: response.ok, text };
@@ -22,7 +22,7 @@ async function fetchRdapDetailedWithTimeout(url, options, timeoutMs, dependencie
22
22
  const timeout = setTimeout(() => controller.abort(), timeoutMs);
23
23
  try {
24
24
  const result = await (dependencies.fetchDetailed ?? safeFetchDetailed)(url, { ...options, signal: controller.signal });
25
- const { text, truncated } = await (dependencies.readText ?? readTextCapped)(result.response, MAX_RDAP_BYTES);
25
+ const { text, truncated } = await (dependencies.readText ?? readTextCapped)(result.response, MAX_RDAP_BYTES, { fatalUtf8: true });
26
26
  if (truncated)
27
27
  throw new Error(`Response from ${url} exceeded ${MAX_RDAP_BYTES} bytes`);
28
28
  return {
@@ -1,24 +1,75 @@
1
- import net from 'node:net';
1
+ import { isIP } from 'node:net';
2
2
  import { domainToASCII } from 'node:url';
3
- import { ipv4ToLong, ipv6ToBigInt } from "./rdap-bootstrap.mjs";
3
+ import { admitRdapEndpoint, ipv4ToLong, ipv6ToBigInt } from "./rdap-bootstrap.mjs";
4
4
  export function canonicalRdapDomain(value) {
5
5
  if (typeof value !== 'string')
6
6
  return null;
7
7
  const ascii = domainToASCII(value.trim().replace(/\.+$/, ''));
8
8
  return ascii ? ascii.toLowerCase() : null;
9
9
  }
10
- function ipv6ToComparableBigInt(ip) {
11
- let normalized = ip;
12
- if (ip.includes('.')) {
13
- const lastColon = ip.lastIndexOf(':');
14
- const embedded = ip.slice(lastColon + 1);
15
- if (net.isIP(embedded) !== 4)
10
+ function decodedTerminalPathParts(value) {
11
+ try {
12
+ const parts = new URL(value).pathname.split('/').filter(Boolean);
13
+ if (parts.length < 2)
16
14
  return null;
17
- const value = ipv4ToLong(embedded);
18
- normalized = `${ip.slice(0, lastColon)}:${(value >>> 16).toString(16)}:${(value & 0xffff).toString(16)}`;
15
+ const objectType = decodeURIComponent(parts.at(-2) ?? '');
16
+ const objectValue = decodeURIComponent(parts.at(-1) ?? '');
17
+ return objectType && objectValue ? [objectType, objectValue] : null;
18
+ }
19
+ catch {
20
+ return null;
21
+ }
22
+ }
23
+ /**
24
+ * Admit final RDAP provenance only when its terminal path still identifies the
25
+ * requested object. Safe redirect transport proves where bytes came from; this
26
+ * binding separately prevents a redirected response for another object from
27
+ * deciding a lookup, especially when a bodyless 404 is otherwise authoritative.
28
+ */
29
+ export function admitRdapObjectEndpoint(type, requestedValue, value) {
30
+ const admitted = admitRdapEndpoint(value);
31
+ if (!admitted)
32
+ return null;
33
+ const terminal = decodedTerminalPathParts(admitted);
34
+ if (!terminal)
35
+ return null;
36
+ const [objectType, objectValue] = terminal;
37
+ if (type === 'domain') {
38
+ const requested = canonicalRdapDomain(requestedValue);
39
+ const selected = canonicalRdapDomain(objectValue);
40
+ return objectType === 'domain' && requested && selected === requested ? admitted : null;
19
41
  }
42
+ if (type === 'ipv4' || type === 'ipv6') {
43
+ const version = type === 'ipv4' ? 4 : 6;
44
+ if (objectType !== 'ip' || isIP(requestedValue) !== version || isIP(objectValue) !== version)
45
+ return null;
46
+ try {
47
+ const convert = version === 4 ? ipv4ToLong : ipv6ToBigInt;
48
+ return convert(requestedValue) === convert(objectValue) ? admitted : null;
49
+ }
50
+ catch {
51
+ return null;
52
+ }
53
+ }
54
+ if (type === 'asn') {
55
+ const requestedMatch = /^(?:AS)?([0-9]{1,10})$/iu.exec(requestedValue);
56
+ if (objectType !== 'autnum' || !requestedMatch || !/^[0-9]{1,10}$/u.test(objectValue))
57
+ return null;
58
+ const requested = Number(requestedMatch[1]);
59
+ const selected = Number(objectValue);
60
+ return Number.isSafeInteger(requested)
61
+ && requested >= 0
62
+ && requested <= 4_294_967_295
63
+ && selected === requested
64
+ && objectValue === String(selected)
65
+ ? admitted
66
+ : null;
67
+ }
68
+ return null;
69
+ }
70
+ function ipv6ToComparableBigInt(ip) {
20
71
  try {
21
- return ipv6ToBigInt(normalized);
72
+ return ipv6ToBigInt(ip);
22
73
  }
23
74
  catch {
24
75
  return null;
@@ -71,11 +122,11 @@ export function validateRdapResponse(type, requestedValue, parsed) {
71
122
  const version = type === 'ipv4' ? 4 : 6;
72
123
  const startAddress = parsed.startAddress;
73
124
  const endAddress = parsed.endAddress;
74
- if (net.isIP(requestedValue) !== version ||
125
+ if (isIP(requestedValue) !== version ||
75
126
  typeof startAddress !== 'string' ||
76
- net.isIP(startAddress) !== version ||
127
+ isIP(startAddress) !== version ||
77
128
  typeof endAddress !== 'string' ||
78
- net.isIP(endAddress) !== version) {
129
+ isIP(endAddress) !== version) {
79
130
  return { valid: false, detail: 'The response did not contain a compatible IP range.' };
80
131
  }
81
132
  const convert = version === 4 ? ipv4ToLong : ipv6ToComparableBigInt;
package/lib/rdap.mjs CHANGED
@@ -3,9 +3,9 @@ import { BOOTSTRAP_STALE_TTL_MS, BOOTSTRAP_TTL_MS, clearRdapBootstrapCache, fetc
3
3
  import { fetchRdapFromBasesWithParser, fetchRdapRecordWithParser, } from "./rdap-client.mjs";
4
4
  import { normalizeRdapEvents, parseRdap, summarizeLifecycle, } from "./rdap-normalization.mjs";
5
5
  import { fetchRegistrarRdapRecordWithParser, selectRegistrarRdapLink, } from "./rdap-registrar.mjs";
6
- import { fetchRdapWithTimeout, } from "./rdap-transport.mjs";
6
+ import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
7
7
  import { registryServiceAdmissionFor } from "./registry-capabilities.mjs";
8
- async function fetchRdapFromBases(type, value, bases, fetchUpstream = fetchRdapWithTimeout) {
8
+ async function fetchRdapFromBases(type, value, bases, fetchUpstream = fetchRdapDetailedWithTimeout) {
9
9
  return fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream);
10
10
  }
11
11
  async function fetchRdapRecord(type, value, options = {}) {
@@ -0,0 +1,36 @@
1
+ import { getDomain } from 'tldts';
2
+ import { isValidAsciiDomainName } from "./hostname.mjs";
3
+ // Returns one canonical registrable-domain identity for an already public DNS
4
+ // hostname. Callers retain the original hostname separately when subdomain
5
+ // context matters; authority, evidence binding and scoring use this value.
6
+ export function canonicalRegistrableDomain(value) {
7
+ if (typeof value !== 'string')
8
+ return null;
9
+ const trimmed = value.trim();
10
+ if (!trimmed || /[\u0000-\u001f\u007f]/u.test(trimmed))
11
+ return null;
12
+ const withoutScheme = trimmed.replace(/^[a-z]+:\/\//iu, '').split(/[/?#]/u)[0] ?? '';
13
+ if (withoutScheme.endsWith('..'))
14
+ return null;
15
+ const candidate = withoutScheme.replace(/\.$/u, '');
16
+ if (!candidate)
17
+ return null;
18
+ let hostname;
19
+ try {
20
+ hostname = new URL(`https://${candidate}/`).hostname.toLowerCase().replace(/\.$/u, '');
21
+ }
22
+ catch {
23
+ return null;
24
+ }
25
+ if (!isValidAsciiDomainName(hostname, { requireDot: true, requireLowercase: true }))
26
+ return null;
27
+ // Keep this boundary identical to classifyQuery() and the collection
28
+ // runtime: registrable identity follows the ICANN Public Suffix List, while
29
+ // privately operated suffixes remain ordinary host context.
30
+ const domain = getDomain(hostname);
31
+ return domain
32
+ && isValidAsciiDomainName(domain, { requireDot: true, requireLowercase: true })
33
+ && (hostname === domain || hostname.endsWith(`.${domain}`))
34
+ ? domain
35
+ : null;
36
+ }
@@ -86,6 +86,9 @@ function registryAccessDiagnosticFor(value) {
86
86
  coverageState: capability.coverageState,
87
87
  whoisAccessProfile: capability.whoisAccessProfile,
88
88
  rdapAccessProfile: capability.rdapAccessProfile,
89
+ ...(capability.officialLookupUrl
90
+ ? { officialLookupUrl: capability.officialLookupUrl }
91
+ : {}),
89
92
  limitation: capability.limitation,
90
93
  authority: 'context_only',
91
94
  };
@@ -1,7 +1,7 @@
1
1
  // Registry compatibility metadata. IANA bootstrap and referral discovery
2
2
  // remain authoritative. Explicit query profiles may alter only the first
3
3
  // referred registry query after a fixture-backed adapter is integrated.
4
- const REGISTRY_CAPABILITIES_VERSION = 28;
4
+ const REGISTRY_CAPABILITIES_VERSION = 29;
5
5
  const MAX_CAPABILITY_INPUT_LENGTH = 253;
6
6
  const REGISTRY_STANDARDS_COVERAGE_SNAPSHOT = Object.freeze({
7
7
  schema: 'whoisleuth.registry-standards-coverage',
@@ -165,6 +165,7 @@ const DEFAULT_CAPABILITY = freezeCapability({
165
165
  fixtureScenarios: ['registered', 'not_found', 'rate_limited'],
166
166
  verificationFiles: ['fixtures/whois-registry-fixtures.mts'],
167
167
  documentationUrls: [],
168
+ officialLookupUrl: null,
168
169
  limitation: DISCOVERY_LIMITATION,
169
170
  });
170
171
  const EXPLICIT_CAPABILITY_SEEDS = [
@@ -260,6 +261,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
260
261
  'https://www.iana.org/domains/root/db/ch.html',
261
262
  'https://www.iana.org/domains/root/db/li.html',
262
263
  ],
264
+ officialLookupUrl: 'https://www.nic.ch/whois/',
263
265
  limitation: SWITCH_ACCESS_LIMITATION,
264
266
  },
265
267
  {
@@ -644,9 +646,11 @@ const EXPLICIT_CAPABILITY_SEEDS = [
644
646
  coverageState: 'access_documented', whoisAccessProfile: 'source-ip-authorization-required',
645
647
  rdapAccessProfile: 'no-iana-service', verificationFiles: [],
646
648
  documentationUrls: [
649
+ 'https://www.dominios.es/es',
647
650
  'https://www.iana.org/domains/root/db/es.html',
648
651
  'https://www.dominios.es/es/sobre-dominios/valores-anadidos/whois-43',
649
652
  ],
653
+ officialLookupUrl: 'https://www.dominios.es/es',
650
654
  limitation: ES_ACCESS_LIMITATION,
651
655
  },
652
656
  {
@@ -665,7 +669,11 @@ const EXPLICIT_CAPABILITY_SEEDS = [
665
669
  whoisParserProfile: 'generic-colon', fixtureScenarios: [],
666
670
  coverageState: 'access_documented', whoisAccessProfile: 'no-iana-service',
667
671
  rdapAccessProfile: 'no-iana-service', verificationFiles: [],
668
- documentationUrls: ['https://www.iana.org/domains/root/db/gt.html'],
672
+ documentationUrls: [
673
+ 'https://www.gt/sitio/',
674
+ 'https://www.iana.org/domains/root/db/gt.html',
675
+ ],
676
+ officialLookupUrl: 'https://www.gt/sitio/',
669
677
  limitation: 'IANA publishes no RDAP service or domain WHOIS service for this suffix. WHOISleuth does not automate the registry website.',
670
678
  },
671
679
  {
@@ -1166,6 +1174,7 @@ const EXPLICIT_CAPABILITY_SEEDS = [
1166
1174
  'https://www.iana.org/domains/root/db/vn.html',
1167
1175
  'https://whois.vnnic.vn/',
1168
1176
  ],
1177
+ officialLookupUrl: 'https://whois.vnnic.vn/',
1169
1178
  limitation: VN_ACCESS_LIMITATION,
1170
1179
  },
1171
1180
  ];
@@ -1191,6 +1200,7 @@ const EXPLICIT_CAPABILITIES = EXPLICIT_CAPABILITY_SEEDS.map((entry) => {
1191
1200
  coverageState: entry.coverageState || 'fixture_verified',
1192
1201
  verificationFiles: entry.verificationFiles || ['fixtures/whois-registry-fixtures.mts'],
1193
1202
  documentationUrls: entry.documentationUrls || [],
1203
+ officialLookupUrl: entry.officialLookupUrl || null,
1194
1204
  limitation: entry.limitation || (rdapAccessProfile === 'no-iana-service'
1195
1205
  ? WHOIS_ONLY_FIXTURE_LIMITATION
1196
1206
  : FIXTURE_LIMITATION),
@@ -107,10 +107,11 @@ function normalizeRdapPublicationStatus(value) {
107
107
  function normalizeNameserver(value) {
108
108
  return normalizeDomain(value);
109
109
  }
110
- function normalizeSet(values, normalizeItem) {
110
+ function normalizeSet(values, normalizeItem, maximum = 200) {
111
111
  if (!Array.isArray(values))
112
112
  return [];
113
- return [...new Set(values.map(normalizeItem).filter((value) => Boolean(value)))].sort();
113
+ return [...new Set(values.slice(0, maximum).map(normalizeItem)
114
+ .filter((value) => Boolean(value)))].sort();
114
115
  }
115
116
  function sameNormalizedValue(left, right) {
116
117
  if (Array.isArray(left) || Array.isArray(right)) {
@@ -138,14 +139,20 @@ function displayValue(value, state, status) {
138
139
  return 'Disabled by deployment policy';
139
140
  return 'Source unavailable';
140
141
  }
141
- if (Array.isArray(value))
142
- return value.join(', ');
143
- return String(value);
142
+ if (Array.isArray(value)) {
143
+ return value.slice(0, 200)
144
+ .filter((item) => typeof item === 'string')
145
+ .map((item) => item.slice(0, 253))
146
+ .join(', ');
147
+ }
148
+ return typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean'
149
+ ? String(value).slice(0, 1_024)
150
+ : 'Not published';
144
151
  }
145
152
  function findRdapEvent(rdap, action) {
146
153
  if (!Array.isArray(rdap.events))
147
154
  return null;
148
- const event = rdap.events.map(record).find((item) => item.action === action);
155
+ const event = rdap.events.slice(0, 100).map(record).find((item) => item.action === action);
149
156
  return event?.date || null;
150
157
  }
151
158
  function rdapLifecycleDate(rdap, field, fallbackAction) {
@@ -223,8 +230,8 @@ export function compareRegistrySources(rdapParsed, whoisParsed, options = {}) {
223
230
  dateField('Expires', 'expiryDate', 'expiration'),
224
231
  dateField('Last updated', 'updatedDate', 'last changed'),
225
232
  compareField('DNSSEC', rdap.dnssec, whois.dnssec, normalizeDnssec, sourceHealth),
226
- compareField('Statuses', rdap.statuses, whois.statuses, (values) => normalizeSet(values, statusNormalizer), sourceHealth),
227
- compareField('Name servers', rdap.nameservers, whois.nameservers, (values) => normalizeSet(values, normalizeNameserver), sourceHealth),
233
+ compareField('Statuses', rdap.statuses, whois.statuses, (values) => normalizeSet(values, statusNormalizer, 100), sourceHealth),
234
+ compareField('Name servers', rdap.nameservers, whois.nameservers, (values) => normalizeSet(values, normalizeNameserver, 200), sourceHealth),
228
235
  ].filter((field) => field !== null);
229
236
  const counts = {
230
237
  equivalent: 0, conflict: 0, rdap_only: 0, whois_only: 0,
@@ -265,7 +272,17 @@ export function compareRdapPublications(registryParsed, registrarParsed, options
265
272
  const registry = record(registryParsed);
266
273
  const registrar = record(registrarParsed);
267
274
  const registrarLifecycle = record(registrar.lifecycle);
268
- const comparison = compareRegistrySources({ ...registry, handle: null }, {
275
+ const comparison = compareRegistrySources({
276
+ domain: registry.domain,
277
+ registrar: registry.registrar,
278
+ registrarIanaId: registry.registrarIanaId,
279
+ lifecycle: registry.lifecycle,
280
+ events: Array.isArray(registry.events) ? registry.events.slice(0, 100) : [],
281
+ dnssec: registry.dnssec,
282
+ statuses: Array.isArray(registry.statuses) ? registry.statuses.slice(0, 100) : [],
283
+ nameservers: Array.isArray(registry.nameservers) ? registry.nameservers.slice(0, 200) : [],
284
+ handle: null,
285
+ }, {
269
286
  domainName: registrar.domain,
270
287
  registrar: registrarValue(registrar.registrar),
271
288
  registrarIanaId: registrar.registrarIanaId,
@@ -276,8 +293,8 @@ export function compareRdapPublications(registryParsed, registrarParsed, options
276
293
  updatedDate: registrarLifecycle.updatedDate,
277
294
  updatedDateIso: registrarLifecycle.updatedDateIso,
278
295
  dnssec: registrar.dnssec,
279
- statuses: registrar.statuses,
280
- nameservers: registrar.nameservers,
296
+ statuses: Array.isArray(registrar.statuses) ? registrar.statuses.slice(0, 100) : [],
297
+ nameservers: Array.isArray(registrar.nameservers) ? registrar.nameservers.slice(0, 200) : [],
281
298
  }, {
282
299
  rdapStatus: options.registryStatus,
283
300
  whoisStatus: options.registrarStatus,
@@ -87,6 +87,13 @@ function disclosure(source, parsedValue, status) {
87
87
  };
88
88
  }
89
89
  const parsed = record(parsedValue);
90
+ if (parsed.contactsExcluded === true) {
91
+ return {
92
+ source,
93
+ state: 'unavailable',
94
+ detail: 'Contact fields are deliberately excluded from this portable projection, so publication disclosure remains unavailable.',
95
+ };
96
+ }
90
97
  const partial = source === 'whois'
91
98
  ? parsed.chainStatus === 'partial' || strings(parsed.fieldsTruncated, 20).length > 0
92
99
  : parsed.serverTruncated === true
@@ -184,9 +184,23 @@ function analyzeHsts(header, signals) {
184
184
  const directives = header.value.split(';').map((part) => part.trim()).filter(Boolean);
185
185
  if (directives.length > MAX_RESPONSE_POLICY_DIRECTIVES)
186
186
  return 'partial';
187
- const maxAge = directives
188
- .map((part) => part.match(/^max-age\s*=\s*(\d+)$/iu)?.[1] || null)
189
- .find((value) => value !== null);
187
+ const seen = new Set();
188
+ let maxAge = null;
189
+ for (const directive of directives) {
190
+ const match = /^([a-z][a-z0-9-]{0,63})(?:\s*=\s*(.*))?$/iu.exec(directive);
191
+ if (!match)
192
+ return 'malformed';
193
+ const name = match[1]?.toLowerCase() ?? '';
194
+ if (seen.has(name))
195
+ return 'malformed';
196
+ seen.add(name);
197
+ if (name === 'max-age') {
198
+ const value = match[2];
199
+ if (!value || !/^\d+$/u.test(value))
200
+ return 'malformed';
201
+ maxAge = value;
202
+ }
203
+ }
190
204
  if (!maxAge || maxAge.length > 12)
191
205
  return 'malformed';
192
206
  const seconds = Number(maxAge);
@@ -256,10 +270,15 @@ function analyzeCookies(headers, signals) {
256
270
  let missingHttpOnly = 0;
257
271
  let missingSameSite = 0;
258
272
  let sameSiteNoneWithoutSecure = 0;
273
+ let attributesTruncated = false;
259
274
  for (const value of result.values) {
260
275
  const segments = value.split(';');
261
276
  if (!segments[0]?.includes('='))
262
277
  return { state: 'malformed', count: result.values.length, truncated: result.truncated };
278
+ if (segments.length - 1 > MAX_RESPONSE_POLICY_TOKENS) {
279
+ attributesTruncated = true;
280
+ continue;
281
+ }
263
282
  const attributes = new Map();
264
283
  for (const rawAttribute of segments.slice(1, MAX_RESPONSE_POLICY_TOKENS + 1)) {
265
284
  const [rawName = '', ...rawValue] = rawAttribute.trim().split('=');
@@ -289,7 +308,11 @@ function analyzeCookies(headers, signals) {
289
308
  if (sameSiteNoneWithoutSecure) {
290
309
  addSignal(signals, 'cookies_same_site_none_without_secure', sameSiteNoneWithoutSecure);
291
310
  }
292
- return { state: result.state, count: result.values.length, truncated: result.truncated };
311
+ return {
312
+ state: result.state === 'partial' || attributesTruncated ? 'partial' : result.state,
313
+ count: result.values.length,
314
+ truncated: result.truncated || attributesTruncated,
315
+ };
293
316
  }
294
317
  export function analyzeResponsePolicyHeaders(headers) {
295
318
  const signals = [];