@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
@@ -406,18 +406,26 @@ function dHash(image) {
406
406
  * back to the exact hash.
407
407
  */
408
408
  function faviconPerceptualHash(buf) {
409
+ return inspectDecodedImage(buf).perceptualHash;
410
+ }
411
+ function inspectDecodedImage(buf) {
409
412
  try {
410
413
  const image = decodeImage(buf);
411
414
  if (!image)
412
- return null;
413
- return dHash(image);
415
+ return Object.freeze({ decodable: false, width: null, height: null, perceptualHash: null });
416
+ return Object.freeze({
417
+ decodable: true,
418
+ width: image.width,
419
+ height: image.height,
420
+ perceptualHash: dHash(image),
421
+ });
414
422
  }
415
423
  catch {
416
- return null;
424
+ return Object.freeze({ decodable: false, width: null, height: null, perceptualHash: null });
417
425
  }
418
426
  }
419
427
  // The same bounded decoder is also suitable for fixed-size PNG screenshots
420
428
  // produced by the optional local capture package. Keep the generic name as an
421
429
  // alias so the evidence contract does not imply that a screenshot is a favicon.
422
430
  const imagePerceptualHash = faviconPerceptualHash;
423
- export { faviconPerceptualHash, hammingDistanceHex, imagePerceptualHash };
431
+ export { faviconPerceptualHash, hammingDistanceHex, imagePerceptualHash, inspectDecodedImage };
@@ -0,0 +1,29 @@
1
+ import { WHOISLEUTH_SOURCE_REPOSITORY_URL, } from "./project-metadata.mjs";
2
+ import { normalizeBoundedSemanticVersion } from "./semantic-version.mjs";
3
+ const URI_LABEL_UNSAFE_GLOBAL_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu;
4
+ const URI_LABEL_SCHEME_RE = /^[A-Za-z][A-Za-z0-9+.-]*:(?:\/\/|[^\s])/u;
5
+ export function isUriShapedLabel(value) {
6
+ if (typeof value !== 'string')
7
+ return false;
8
+ const candidate = value.replace(URI_LABEL_UNSAFE_GLOBAL_RE, '').trim();
9
+ return URI_LABEL_SCHEME_RE.test(candidate);
10
+ }
11
+ export function buildPortableGeneratorMetadata(applicationVersion) {
12
+ let version = null;
13
+ if (applicationVersion !== null && applicationVersion !== undefined && applicationVersion !== '') {
14
+ try {
15
+ version = normalizeBoundedSemanticVersion(applicationVersion, 'Portable report application');
16
+ }
17
+ catch {
18
+ version = null;
19
+ }
20
+ }
21
+ return Object.freeze({
22
+ name: 'WHOISleuth',
23
+ version,
24
+ projectUrl: WHOISLEUTH_SOURCE_REPOSITORY_URL,
25
+ });
26
+ }
27
+ export function portableGeneratorAttribution(generator) {
28
+ return `Generated with ${generator.name}${generator.version ? ` ${generator.version}` : ''} · Source: ${generator.projectUrl}`;
29
+ }
@@ -1,4 +1,6 @@
1
+ import { normalizeLegacyIsoTimestamp } from "../packages/evidence/observation.mjs";
1
2
  export const PROVIDER_POLICY_MAX_REVIEW_AGE_DAYS = 180;
3
+ export const PROVIDER_POLICY_MAX_FUTURE_SKEW_MS = 5 * 60 * 1000;
2
4
  export const DEPLOYMENT_PURPOSES = ['personal', 'internal', 'commercial'];
3
5
  function deploymentPurpose(value) {
4
6
  const normalized = String(value ?? '').trim().toLowerCase();
@@ -6,13 +8,18 @@ function deploymentPurpose(value) {
6
8
  ? normalized
7
9
  : null;
8
10
  }
11
+ export function providerPolicyReviewAgeDays(reviewedAtValue, now) {
12
+ const normalizedReviewedAt = normalizeLegacyIsoTimestamp(reviewedAtValue);
13
+ const reviewedAt = normalizedReviewedAt ? Date.parse(normalizedReviewedAt) : Number.NaN;
14
+ if (!Number.isFinite(reviewedAt) || !Number.isFinite(now) || reviewedAt > now + PROVIDER_POLICY_MAX_FUTURE_SKEW_MS) {
15
+ return null;
16
+ }
17
+ return Math.max(0, Math.floor((now - reviewedAt) / 86_400_000));
18
+ }
9
19
  export function providerPolicyAdmission(terms, env = process.env, now = Date.now()) {
10
20
  const source = env && typeof env === 'object' ? env : {};
11
21
  const purpose = deploymentPurpose(source.WHOISLEUTH_DEPLOYMENT_PURPOSE);
12
- const reviewedAt = Date.parse(terms.reviewedAt);
13
- const reviewAgeDays = Number.isFinite(reviewedAt)
14
- ? Math.max(0, Math.floor((now - reviewedAt) / 86_400_000))
15
- : null;
22
+ const reviewAgeDays = providerPolicyReviewAgeDays(terms.reviewedAt, now);
16
23
  if (!purpose) {
17
24
  return {
18
25
  allowed: false,
@@ -1,5 +1,6 @@
1
1
  // IANA RDAP bootstrap retrieval, bounded stale caching, and authority
2
2
  // selection for domain, IP, and ASN queries.
3
+ import net from 'node:net';
3
4
  import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
4
5
  const BOOTSTRAP_TTL_MS = 60 * 60 * 1000;
5
6
  const BOOTSTRAP_STALE_TTL_MS = 7 * 24 * 60 * 60 * 1000;
@@ -8,29 +9,71 @@ const BOOTSTRAP_FETCH_TIMEOUT_MS = 7000;
8
9
  const MAX_RDAP_ENDPOINT_LENGTH = 2048;
9
10
  const bootstrapCache = new Map();
10
11
  const bootstrapInflight = new Map();
12
+ function admitRdapEndpoint(value, options = {}) {
13
+ if (typeof value !== 'string'
14
+ || !value
15
+ || value !== value.trim()
16
+ || value.length > MAX_RDAP_ENDPOINT_LENGTH
17
+ || /[\u0000-\u001f\u007f]/u.test(value))
18
+ return null;
19
+ try {
20
+ const parsed = new URL(value);
21
+ if (!['http:', 'https:'].includes(parsed.protocol)
22
+ || !parsed.hostname
23
+ || parsed.username
24
+ || parsed.password
25
+ || parsed.port
26
+ || parsed.hash
27
+ || (!options.allowQuery && parsed.search))
28
+ return null;
29
+ return value;
30
+ }
31
+ catch {
32
+ return null;
33
+ }
34
+ }
11
35
  function ipv4ToLong(ip) {
12
36
  return ip.split('.').reduce((accumulator, octet) => (accumulator << 8) + (parseInt(octet, 10) & 0xff), 0) >>> 0;
13
37
  }
14
38
  function ipInCidrV4(ip, cidr) {
15
- const [range, bitsString] = cidr.split('/');
16
- if (!range)
39
+ const parts = cidr.split('/');
40
+ if (parts.length > 2)
41
+ return false;
42
+ const [range, bitsString] = parts;
43
+ if (!range || net.isIP(ip) !== 4 || net.isIP(range) !== 4)
44
+ return false;
45
+ const bits = bitsString === undefined || /^\d{1,2}$/u.test(bitsString)
46
+ ? Number(bitsString ?? 32)
47
+ : -1;
48
+ if (!Number.isSafeInteger(bits) || bits < 0 || bits > 32)
17
49
  return false;
18
- const bits = bitsString !== undefined ? parseInt(bitsString, 10) : 32;
19
50
  const mask = bits === 0 ? 0 : (0xffffffff << (32 - bits)) >>> 0;
20
51
  return (ipv4ToLong(ip) & mask) === (ipv4ToLong(range) & mask);
21
52
  }
22
53
  function expandIpv6(ip) {
23
- let head = ip;
54
+ let normalized = ip;
55
+ if (normalized.includes('.')) {
56
+ const lastColon = normalized.lastIndexOf(':');
57
+ const embedded = normalized.slice(lastColon + 1);
58
+ if (lastColon < 0 || net.isIP(embedded) !== 4) {
59
+ throw new TypeError('Invalid IPv4-embedded IPv6 address.');
60
+ }
61
+ const value = ipv4ToLong(embedded);
62
+ normalized = `${normalized.slice(0, lastColon)}:${(value >>> 16).toString(16)}:${(value & 0xffff).toString(16)}`;
63
+ }
64
+ if (net.isIP(normalized) !== 6)
65
+ throw new TypeError('Invalid IPv6 address.');
66
+ let head = normalized;
24
67
  let tail = '';
25
- if (ip.includes('::')) {
26
- const [headPart = '', tailPart = ''] = ip.split('::');
68
+ if (normalized.includes('::')) {
69
+ const [headPart = '', tailPart = ''] = normalized.split('::');
27
70
  head = headPart;
28
71
  tail = tailPart;
29
72
  }
30
73
  const headParts = head ? head.split(':').filter(Boolean) : [];
31
74
  const tailParts = tail ? tail.split(':').filter(Boolean) : [];
32
75
  const missing = 8 - headParts.length - tailParts.length;
33
- const parts = ip.includes('::')
76
+ const parts = normalized.includes('::')
34
77
  ? [...headParts, ...Array(Math.max(missing, 0)).fill('0'), ...tailParts]
35
78
  : headParts;
36
79
  while (parts.length < 8)
@@ -41,13 +84,25 @@ function ipv6ToBigInt(ip) {
41
84
  return expandIpv6(ip).reduce((accumulator, part) => (accumulator << 16n) + BigInt(parseInt(part || '0', 16)), 0n);
42
85
  }
43
86
  function ipInCidrV6(ip, cidr) {
44
- const [range, bitsString] = cidr.split('/');
87
+ const parts = cidr.split('/');
88
+ if (parts.length > 2)
89
+ return false;
90
+ const [range, bitsString] = parts;
45
91
  if (!range)
46
92
  return false;
47
- const bits = bitsString !== undefined ? parseInt(bitsString, 10) : 128;
93
+ const bits = bitsString === undefined || /^\d{1,3}$/u.test(bitsString)
94
+ ? Number(bitsString ?? 128)
95
+ : -1;
96
+ if (!Number.isSafeInteger(bits) || bits < 0 || bits > 128)
97
+ return false;
48
98
  const full = (1n << 128n) - 1n;
49
99
  const mask = bits === 0 ? 0n : (full << BigInt(128 - bits)) & full;
50
- return (ipv6ToBigInt(ip) & mask) === (ipv6ToBigInt(range) & mask);
100
+ try {
101
+ return (ipv6ToBigInt(ip) & mask) === (ipv6ToBigInt(range) & mask);
102
+ }
103
+ catch {
104
+ return false;
105
+ }
51
106
  }
52
107
  function validBootstrap(data) {
53
108
  const record = data && typeof data === 'object' && !Array.isArray(data)
@@ -74,7 +129,12 @@ async function fetchBootstrap(kind, options = {}) {
74
129
  return inflight;
75
130
  const request = (async () => {
76
131
  try {
77
- const response = await fetchUpstream(`https://data.iana.org/rdap/${kind}.json`, {}, BOOTSTRAP_FETCH_TIMEOUT_MS);
132
+ const requestedEndpoint = `https://data.iana.org/rdap/${kind}.json`;
133
+ const response = await fetchUpstream(requestedEndpoint, {}, BOOTSTRAP_FETCH_TIMEOUT_MS);
134
+ const finalEndpoint = admitRdapEndpoint(response.finalUrl ?? requestedEndpoint);
135
+ if (finalEndpoint !== requestedEndpoint) {
136
+ throw new Error(`IANA bootstrap redirected outside its fixed source endpoint for ${kind}`);
137
+ }
78
138
  if (!response.ok) {
79
139
  throw new Error(`IANA bootstrap fetch failed for ${kind} (${response.status})`);
80
140
  }
@@ -112,10 +172,10 @@ function clearRdapBootstrapCache() {
112
172
  function uniqueRdapBases(urls) {
113
173
  const seen = new Set();
114
174
  return (Array.isArray(urls) ? urls : [])
115
- .filter((url) => typeof url === 'string'
116
- && url.length <= MAX_RDAP_ENDPOINT_LENGTH
117
- && !/[\u0000-\u001f\u007f]/.test(url)
118
- && /^https?:\/\//i.test(url))
175
+ .flatMap((url) => {
176
+ const admitted = admitRdapEndpoint(url);
177
+ return admitted ? [admitted] : [];
178
+ })
119
179
  .sort((left, right) => Number(/^http:\/\//i.test(left)) - Number(/^http:\/\//i.test(right)))
120
180
  .filter((url) => {
121
181
  const key = url.replace(/\/$/, '').toLowerCase();
@@ -170,4 +230,4 @@ async function findRdapBases(type, value) {
170
230
  }
171
231
  return [];
172
232
  }
173
- export { BOOTSTRAP_STALE_TTL_MS, BOOTSTRAP_TTL_MS, MAX_RDAP_ENDPOINT_LENGTH, clearRdapBootstrapCache, fetchBootstrap, findRdapBases, ipv4ToLong, ipv6ToBigInt, uniqueRdapBases, };
233
+ export { BOOTSTRAP_STALE_TTL_MS, BOOTSTRAP_TTL_MS, MAX_RDAP_ENDPOINT_LENGTH, admitRdapEndpoint, clearRdapBootstrapCache, fetchBootstrap, findRdapBases, ipv4ToLong, ipv6ToBigInt, uniqueRdapBases, };
@@ -1,8 +1,8 @@
1
1
  import { cached } from "./lookup-cache.mjs";
2
2
  import { findRdapBases, uniqueRdapBases, } from "./rdap-bootstrap.mjs";
3
3
  import { rdapAttempt, rdapFailure } from "./rdap-attempts.mjs";
4
- import { fetchRdapWithTimeout, } from "./rdap-transport.mjs";
5
- import { validateRdapResponse } from "./rdap-validation.mjs";
4
+ import { fetchRdapDetailedWithTimeout, } from "./rdap-transport.mjs";
5
+ import { admitRdapObjectEndpoint, validateRdapResponse, } from "./rdap-validation.mjs";
6
6
  const UPSTREAM_TIMEOUT_MS = 7_000;
7
7
  const UPSTREAM_TOTAL_DEADLINE_MS = 12_000;
8
8
  const MAX_RDAP_ENDPOINTS = 3;
@@ -24,7 +24,7 @@ function rdapPathFor(type, value) {
24
24
  * Query the bounded bootstrap candidate set while retaining every attempted
25
25
  * endpoint as separately attributed transport evidence.
26
26
  */
27
- export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapWithTimeout) {
27
+ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap, fetchUpstream = fetchRdapDetailedWithTimeout) {
28
28
  const candidates = uniqueRdapBases(bases).slice(0, MAX_RDAP_ENDPOINTS);
29
29
  if (candidates.length === 0)
30
30
  return null;
@@ -38,13 +38,21 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
38
38
  const url = `${base.replace(/\/$/, '')}/${rdapPathFor(type, value)}`;
39
39
  try {
40
40
  const upstream = await fetchUpstream(url, { headers: { Accept: 'application/rdap+json' } }, Math.min(UPSTREAM_TIMEOUT_MS, remaining));
41
+ const selectedEndpoint = admitRdapObjectEndpoint(type, value, upstream.finalUrl ?? url);
42
+ if (!selectedEndpoint) {
43
+ attempts.push(rdapAttempt(url, 'invalid_response', {
44
+ status: upstream.status,
45
+ detail: 'The endpoint returned final URL provenance for an invalid or different RDAP object.',
46
+ }));
47
+ continue;
48
+ }
41
49
  if (upstream.status !== 404 && !upstream.ok) {
42
50
  const outcome = upstream.status === 429
43
51
  ? 'rate_limited'
44
52
  : upstream.status >= 500
45
53
  ? 'server_error'
46
54
  : 'client_error';
47
- attempts.push(rdapAttempt(url, outcome, {
55
+ attempts.push(rdapAttempt(selectedEndpoint, outcome, {
48
56
  status: upstream.status,
49
57
  detail: `The endpoint returned HTTP ${upstream.status}.`,
50
58
  }));
@@ -55,21 +63,21 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
55
63
  data = JSON.parse(upstream.text);
56
64
  }
57
65
  catch {
58
- attempts.push(rdapAttempt(url, 'invalid_json', {
66
+ attempts.push(rdapAttempt(selectedEndpoint, 'invalid_json', {
59
67
  status: upstream.status,
60
68
  detail: 'The endpoint returned invalid JSON.',
61
69
  }));
62
70
  continue;
63
71
  }
64
72
  if (upstream.status === 404) {
65
- attempts.push(rdapAttempt(url, 'not_found', {
73
+ attempts.push(rdapAttempt(selectedEndpoint, 'not_found', {
66
74
  status: upstream.status,
67
75
  detail: 'The authoritative endpoint reported no matching object.',
68
76
  selected: true,
69
77
  }));
70
78
  return {
71
- rdapServer: url,
72
- transportSecurity: /^https:\/\//i.test(url) ? 'https' : 'http',
79
+ rdapServer: selectedEndpoint,
80
+ transportSecurity: /^https:\/\//i.test(selectedEndpoint) ? 'https' : 'http',
73
81
  upstreamStatus: upstream.status,
74
82
  fetchedAt: new Date().toISOString(),
75
83
  data,
@@ -80,20 +88,20 @@ export async function fetchRdapFromBasesWithParser(type, value, bases, parseRdap
80
88
  const parsed = parseRdap(type, data);
81
89
  const validation = validateRdapResponse(type, value, parsed);
82
90
  if (!validation.valid) {
83
- attempts.push(rdapAttempt(url, 'invalid_response', {
91
+ attempts.push(rdapAttempt(selectedEndpoint, 'invalid_response', {
84
92
  status: upstream.status,
85
93
  detail: validation.detail,
86
94
  }));
87
95
  continue;
88
96
  }
89
- attempts.push(rdapAttempt(url, 'success', {
97
+ attempts.push(rdapAttempt(selectedEndpoint, 'success', {
90
98
  status: upstream.status,
91
99
  detail: 'The endpoint returned the requested RDAP object.',
92
100
  selected: true,
93
101
  }));
94
102
  return {
95
- rdapServer: url,
96
- transportSecurity: /^https:\/\//i.test(url) ? 'https' : 'http',
103
+ rdapServer: selectedEndpoint,
104
+ transportSecurity: /^https:\/\//i.test(selectedEndpoint) ? 'https' : 'http',
97
105
  upstreamStatus: upstream.status,
98
106
  fetchedAt: new Date().toISOString(),
99
107
  data,