@slicedearth/whoisleuth-cli 2.4.0 → 2.6.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 (340) hide show
  1. package/DISCLOSURE +1 -1
  2. package/NOTICE +2 -2
  3. package/cli/argument-values.mjs +55 -0
  4. package/cli/arguments.mjs +24 -470
  5. package/cli/artifact-validation/case-response.mjs +67 -9
  6. package/cli/artifact-validation/lookup-evidence.mjs +18 -4
  7. package/cli/artifact-validation/signed-review.mjs +61 -64
  8. package/cli/artifact-verify.mjs +11 -2
  9. package/cli/assurance-arguments.mjs +56 -0
  10. package/cli/assurance-command-definitions.mjs +179 -0
  11. package/cli/assurance-command-runner.mjs +1 -1
  12. package/cli/bounded-json.mjs +1 -1
  13. package/cli/bulk-checkpoint.mjs +1 -1
  14. package/cli/bulk-command-runner.mjs +6 -8
  15. package/cli/bulk.mjs +2 -3
  16. package/cli/case-command.mjs +99 -6
  17. package/cli/case-pack.mjs +19 -4
  18. package/cli/ci-report.mjs +6 -3
  19. package/cli/collection-arguments.mjs +123 -0
  20. package/cli/collection-command-definitions.mjs +330 -0
  21. package/cli/collection-preflight.mjs +8 -1
  22. package/cli/command-argument-grammar.mjs +8 -210
  23. package/cli/command-definition.mjs +330 -0
  24. package/cli/command-reference.mjs +55 -1388
  25. package/cli/command-seeds.mjs +18 -0
  26. package/cli/compare.mjs +1 -1
  27. package/cli/config-profile.mjs +1 -1
  28. package/cli/ct-event-intake.mjs +4 -4
  29. package/cli/discover.mjs +2 -2
  30. package/cli/discovery-observation-snapshot.mjs +2 -2
  31. package/cli/discovery-scan-command-runner.mjs +7 -9
  32. package/cli/discovery-scan.mjs +1 -1
  33. package/cli/discovery-snapshot.mjs +2 -2
  34. package/cli/discovery-workflow.mjs +4 -4
  35. package/cli/domain-control-monitor.mjs +6 -6
  36. package/cli/domain-control-observations.mjs +2 -1
  37. package/cli/domain-feed.mjs +61 -0
  38. package/cli/errors.mjs +5 -6
  39. package/cli/evidence-arguments.mjs +46 -0
  40. package/cli/evidence-command-definitions.mjs +69 -0
  41. package/cli/evidence-command-runner.mjs +2 -3
  42. package/cli/evidence-signing.mjs +3 -2
  43. package/cli/external-observation-mapping.mjs +2 -2
  44. package/cli/formatters/json.mjs +1 -13
  45. package/cli/formatters/terminal-command-formats.mjs +6 -0
  46. package/cli/formatters/terminal-lookup.mjs +6 -0
  47. package/cli/history-arguments.mjs +39 -0
  48. package/cli/history-command-definitions.mjs +88 -0
  49. package/cli/indicator-set-command.mjs +32 -0
  50. package/cli/inline-command-families.mjs +6 -47
  51. package/cli/inline-command-runner.mjs +4 -31
  52. package/cli/input.mjs +2 -2
  53. package/cli/intake-command.mjs +147 -0
  54. package/cli/interchange-report.mjs +4 -2
  55. package/cli/investigation-artifacts.mjs +3 -4
  56. package/cli/investigation-package-review.mjs +2 -0
  57. package/cli/investigation-plan.mjs +32 -154
  58. package/cli/investigation-recipes.mjs +177 -0
  59. package/cli/investigation-run.mjs +78 -12
  60. package/cli/local-mmdb-review.mjs +220 -17
  61. package/cli/lookalike-calibration.mjs +1 -1
  62. package/cli/lookup-brief.mjs +2 -2
  63. package/cli/lookup-command-runner.mjs +19 -35
  64. package/cli/lookup-diff.mjs +10 -9
  65. package/cli/lookup-plan.mjs +10 -14
  66. package/cli/lookup-reconcile.mjs +6 -5
  67. package/cli/lookup-timeline.mjs +7 -6
  68. package/cli/mail-header-review.mjs +24 -247
  69. package/cli/mail-review.mjs +9 -7
  70. package/cli/network-arguments.mjs +51 -0
  71. package/cli/network-command-definitions.mjs +130 -0
  72. package/cli/network-command-runner.mjs +43 -31
  73. package/cli/offline-evidence-review.mjs +85 -6
  74. package/cli/posture.mjs +1 -1
  75. package/cli/registry-fixture-scaffold.mjs +1 -1
  76. package/cli/retained-artifact-diff.mjs +8 -7
  77. package/cli/review-arguments.mjs +132 -0
  78. package/cli/review-command-definitions.mjs +279 -0
  79. package/cli/review-command-runner.mjs +72 -159
  80. package/cli/risk-calibration.mjs +14 -12
  81. package/cli/runner.mjs +20 -32
  82. package/cli/saved-lookup.mjs +2 -2
  83. package/cli/selected-input-worker.mjs +73 -0
  84. package/cli/sharing-review.mjs +5 -64
  85. package/cli/source-reliability.mjs +2 -2
  86. package/cli/support-arguments.mjs +49 -0
  87. package/cli/support-command-definitions.mjs +209 -0
  88. package/cli/support-command-runner.mjs +4 -4
  89. package/cli/watchlist-review.mjs +43 -0
  90. package/cli/workflow-arguments.mjs +100 -0
  91. package/cli/workflow-command-definitions.mjs +103 -0
  92. package/cli/workflow-command-runner.mjs +40 -1
  93. package/docs/cli-reference.md +27 -0
  94. package/docs/cli.md +322 -4
  95. package/docs/contextual-reviews.md +178 -0
  96. package/docs/infrastructure-observations.md +126 -0
  97. package/docs/offline-intake-context.md +213 -0
  98. package/lib/analyst-taxonomy.mjs +2 -40
  99. package/lib/application-version.mjs +1 -1
  100. package/lib/availability.mjs +13 -789
  101. package/lib/bounded-contract-normalizers.mjs +4 -0
  102. package/lib/bounded-json.mjs +2 -330
  103. package/lib/browser-library-profile.mjs +62 -84
  104. package/lib/candidate-provenance-bounds.mjs +2 -0
  105. package/lib/classify.mjs +1 -1
  106. package/lib/credential-form-attribution.mjs +136 -0
  107. package/lib/credential-surface-profile.mjs +6 -2
  108. package/lib/ct-query.mjs +2 -27
  109. package/lib/ct-response-bounds.mjs +2 -9
  110. package/lib/ct-search.mjs +83 -47
  111. package/lib/dmarc-discovery.mjs +62 -0
  112. package/lib/dns-convergence-review.mjs +21 -8
  113. package/lib/dns-delegation-health.mjs +35 -15
  114. package/lib/dns-inheritance-review.mjs +53 -63
  115. package/lib/dns-intelligence.mjs +27 -11
  116. package/lib/dns-query-coverage.mjs +40 -0
  117. package/lib/domain-assurance.mjs +1 -1
  118. package/lib/domain-change-packet.mjs +12 -13
  119. package/lib/domain-change-review.mjs +32 -14
  120. package/lib/domain-control-flight-recorder.mjs +103 -27
  121. package/lib/domain-control-manifest.mjs +25 -17
  122. package/lib/domain-posture-analysis.mjs +75 -7
  123. package/lib/domain-posture-parsers.mjs +9 -0
  124. package/lib/domain-posture.mjs +45 -25
  125. package/lib/domain-registration.mjs +392 -0
  126. package/lib/evidence-export-privacy.mjs +27 -17
  127. package/lib/evidence-export.mjs +3 -3
  128. package/lib/evidence-report.mjs +5 -1
  129. package/lib/external-intelligence-risk.mjs +2 -124
  130. package/lib/favicon.mjs +13 -4
  131. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  132. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  133. package/lib/generated/retire-browser-catalog.mjs +332 -19
  134. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  135. package/lib/html-signals.mjs +13 -2
  136. package/lib/http-evidence-bounds.mjs +2 -8
  137. package/lib/http-intelligence.mjs +13 -1
  138. package/lib/idn-confusable-policy.mjs +6 -4
  139. package/lib/idn-confusables.mjs +1 -1
  140. package/lib/interchange-fidelity-registry.mjs +10 -0
  141. package/lib/lookup-child-profile-contract.mjs +28 -11
  142. package/lib/lookup-contract-primitives.mjs +1 -1
  143. package/lib/lookup-diagnostics.mjs +1 -23
  144. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  145. package/lib/lookup-progress.mjs +50 -1
  146. package/lib/lookup-response-contract.mjs +70 -40
  147. package/lib/lookup-response.mjs +5 -15
  148. package/lib/lookup-source-progress.mjs +8 -22
  149. package/lib/lookup.mjs +1 -1
  150. package/lib/operation-budget.mjs +4 -0
  151. package/lib/opportunity-scoring.mjs +2 -107
  152. package/lib/outbound-identity.mjs +1 -1
  153. package/lib/page-fingerprints.mjs +18 -1
  154. package/lib/perceptual-hash-comparison.mjs +2 -34
  155. package/lib/perceptual-hash.mjs +36 -10
  156. package/lib/portable-generator.mjs +2 -29
  157. package/lib/project-metadata.mjs +2 -8
  158. package/lib/rdap-client.mjs +11 -7
  159. package/lib/registrable-domain.mjs +2 -56
  160. package/lib/registrar-standing-contract.mjs +1 -1
  161. package/lib/registrar-standing.mjs +9 -13
  162. package/lib/registry-capabilities.mjs +2 -2
  163. package/lib/registry-capability-catalogue.mjs +27 -19
  164. package/lib/risk-calibration-summary.mjs +2 -352
  165. package/lib/risk-scoring.mjs +2 -345
  166. package/lib/scoring-evidence-quality.mjs +2 -107
  167. package/lib/semantic-version.mjs +2 -54
  168. package/lib/service-binding-dns.mjs +46 -2
  169. package/lib/smtp-transport-review.mjs +48 -19
  170. package/lib/sslbl-certificates.generated.mjs +142 -139
  171. package/lib/static-html-analysis.mjs +32 -4
  172. package/lib/structured-data-identity.mjs +1 -1
  173. package/lib/technology-evidence-role.mjs +1 -51
  174. package/lib/threat-intelligence-manifests.mjs +1 -1
  175. package/lib/threat-intelligence-runtime.mjs +1 -1
  176. package/lib/threat-intelligence-targets.mjs +1 -1
  177. package/lib/threat-intelligence-types.mjs +2 -75
  178. package/lib/tls-intelligence.mjs +30 -5
  179. package/lib/typosquat-generator.mjs +11 -2
  180. package/lib/website-enrichment.mjs +404 -0
  181. package/lib/whois-authority.mjs +43 -5
  182. package/lib/whois-chain.mjs +12 -3
  183. package/lib/whois-common-formats.mjs +3 -1
  184. package/lib/whois-contacts.mjs +13 -2
  185. package/lib/whois-normalization.mjs +3 -1
  186. package/lib/whois-parser.mjs +5 -3
  187. package/package.json +15 -14
  188. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  189. package/packages/analysis/bounded-json.mjs +330 -0
  190. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  191. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  192. package/packages/analysis/ct-query.mjs +27 -0
  193. package/packages/analysis/ct-response-bounds.mjs +9 -0
  194. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  195. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  196. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  197. package/packages/analysis/opportunity-scoring.mjs +107 -0
  198. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  199. package/packages/analysis/portable-generator.mjs +29 -0
  200. package/packages/analysis/project-metadata.mjs +8 -0
  201. package/packages/analysis/registrable-domain.mjs +56 -0
  202. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  203. package/packages/analysis/risk-scoring.mjs +348 -0
  204. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  205. package/packages/analysis/semantic-version.mjs +54 -0
  206. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  207. package/packages/cases/case-containment-handoff.mjs +195 -0
  208. package/packages/cases/case-evidence-links.mjs +151 -0
  209. package/packages/cases/case-evidence-model.mjs +89 -12
  210. package/packages/cases/case-evidence-quality.mjs +5 -0
  211. package/packages/cases/case-export-input.mjs +7 -5
  212. package/packages/cases/case-incident-context.mjs +52 -0
  213. package/packages/cases/case-markdown.mjs +11 -0
  214. package/packages/cases/case-migration-model.mjs +40 -24
  215. package/packages/cases/case-object-coverage.mjs +33 -0
  216. package/packages/cases/case-packet-correction.mjs +106 -0
  217. package/packages/cases/case-recheck-model.mjs +124 -36
  218. package/packages/cases/case-record-contracts.mjs +1 -1
  219. package/packages/cases/case-record-core.mjs +1 -1
  220. package/packages/cases/case-record-operations.mjs +111 -118
  221. package/packages/cases/case-record-projection.mjs +23 -4
  222. package/packages/cases/case-record-version-input.mjs +41 -0
  223. package/packages/cases/case-report.mjs +93 -27
  224. package/packages/cases/case-requested-evidence.mjs +146 -0
  225. package/packages/cases/case-response-actions.mjs +159 -1
  226. package/packages/cases/case-response-model.mjs +16 -1
  227. package/packages/cases/case-response-object.mjs +119 -0
  228. package/packages/cases/case-response-outcomes.mjs +108 -20
  229. package/packages/cases/case-response-packet-render.mjs +35 -12
  230. package/packages/cases/case-response-packet.mjs +55 -13
  231. package/packages/cases/case-response-records.mjs +0 -1
  232. package/packages/cases/case-response-review-inputs.mjs +60 -8
  233. package/packages/cases/case-storage-model.mjs +1 -1
  234. package/packages/cases/case-workflow-metadata.mjs +217 -0
  235. package/packages/cases/case-workflow-migration.mjs +43 -0
  236. package/packages/cases/http-summary.mjs +6 -1
  237. package/packages/comparison/favicon-similarity.mjs +1 -1
  238. package/packages/comparison/page-similarity.mjs +1 -1
  239. package/packages/comparison/relationship-evidence.mjs +21 -9
  240. package/packages/comparison/relationship-provenance.mjs +7 -1
  241. package/packages/contracts/analyst-interchange.mjs +35 -15
  242. package/packages/contracts/candidate-watch-review.mjs +54 -0
  243. package/packages/contracts/case-portability.mjs +360 -49
  244. package/packages/contracts/cli-command-semantics.mjs +5 -0
  245. package/packages/contracts/cli-grammar.mjs +6 -0
  246. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  247. package/packages/contracts/context-review.mjs +25 -0
  248. package/packages/contracts/document-intake.mjs +11 -0
  249. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  250. package/packages/contracts/domain-control-manifest.mjs +1 -1
  251. package/packages/contracts/domain-control-monitor.mjs +13 -2
  252. package/packages/contracts/domain-control-review.mjs +36 -15
  253. package/packages/contracts/external-observation-interchange.mjs +96 -28
  254. package/packages/contracts/har-review.mjs +2 -0
  255. package/packages/contracts/identity-events.mjs +4 -0
  256. package/packages/contracts/investigation-run.mjs +2 -0
  257. package/packages/contracts/ip-address.mjs +10 -0
  258. package/packages/contracts/lookup-evidence.mjs +3 -2
  259. package/packages/contracts/mail-authentication.mjs +5 -0
  260. package/packages/contracts/message-intake.mjs +37 -0
  261. package/packages/contracts/offline-comparison.mjs +4 -1
  262. package/packages/contracts/passive-mail.mjs +10 -0
  263. package/packages/contracts/risk-calibration.mjs +44 -29
  264. package/packages/contracts/schema-lifecycle.mjs +3 -1
  265. package/packages/contracts/web-capture.mjs +2 -2
  266. package/packages/contracts/workspace-portability.mjs +144 -28
  267. package/packages/evidence/collection-quality.mjs +67 -0
  268. package/packages/evidence/credential-form-attribution.mjs +60 -0
  269. package/packages/evidence/dns-query-outcome.mjs +72 -0
  270. package/packages/evidence/email-recipient.mjs +18 -0
  271. package/packages/evidence/image-regions.mjs +15 -0
  272. package/packages/evidence/lookup-target.mjs +5 -1
  273. package/packages/evidence/observation.mjs +2 -0
  274. package/packages/evidence/png-pixels.mjs +117 -0
  275. package/packages/evidence/public-address-policy.mjs +83 -0
  276. package/packages/evidence/record-identity.mjs +60 -16
  277. package/packages/evidence/technology-evidence-role.mjs +51 -0
  278. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  279. package/packages/interchange/external-findings-import.mjs +36 -29
  280. package/packages/interchange/json-output.mjs +14 -0
  281. package/packages/interchange/managed-indicator-export.mjs +36 -0
  282. package/packages/interchange/managed-indicator-set.mjs +223 -0
  283. package/packages/interchange/misp-indicator-export.mjs +93 -0
  284. package/packages/interchange/sharing-policy.mjs +56 -0
  285. package/packages/interchange/stix-indicator-export.mjs +129 -0
  286. package/packages/interchange/web-capture-import.mjs +22 -15
  287. package/packages/investigation/acquisition-decision-packet.mjs +79 -1
  288. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  289. package/packages/investigation/capture-coverage.mjs +57 -0
  290. package/packages/investigation/connector-provenance-review.mjs +100 -0
  291. package/packages/investigation/context-review.mjs +35 -0
  292. package/packages/investigation/document-intake.mjs +87 -0
  293. package/packages/investigation/docx-intake.mjs +133 -0
  294. package/packages/investigation/domain-history-review.mjs +132 -0
  295. package/packages/investigation/domain-transition-review.mjs +49 -0
  296. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  297. package/packages/investigation/har-intake.mjs +68 -0
  298. package/packages/investigation/identity-event-intake.mjs +131 -0
  299. package/packages/investigation/identity-incident-review.mjs +39 -0
  300. package/packages/investigation/incident-sequence-review.mjs +58 -0
  301. package/packages/investigation/infrastructure-observation.mjs +234 -0
  302. package/packages/investigation/intake-context.mjs +284 -0
  303. package/packages/investigation/intake-indicators.mjs +116 -0
  304. package/packages/investigation/intake-phones.mjs +155 -0
  305. package/packages/investigation/intake-report.mjs +20 -0
  306. package/packages/investigation/investigation-manifest.mjs +35 -9
  307. package/packages/investigation/link-intake.mjs +209 -0
  308. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  309. package/packages/investigation/mail-authentication-review.mjs +222 -0
  310. package/packages/investigation/mail-header-identity.mjs +209 -0
  311. package/packages/investigation/message-intake.mjs +235 -0
  312. package/packages/investigation/page-behaviour.mjs +113 -0
  313. package/packages/investigation/pdf-intake.mjs +155 -0
  314. package/packages/investigation/platform-continuity-review.mjs +60 -0
  315. package/packages/investigation/qr-intake.mjs +56 -0
  316. package/packages/investigation/requested-action-hints.mjs +11 -0
  317. package/packages/investigation/selected-input-review.mjs +17 -0
  318. package/packages/investigation/storefront-review.mjs +70 -0
  319. package/packages/monitoring/domain-feed.mjs +321 -0
  320. package/packages/workspace/brand-candidate-workflow.mjs +351 -0
  321. package/packages/workspace/brand-keyword-campaign.mjs +231 -0
  322. package/packages/workspace/brand-profile-model.mjs +48 -4
  323. package/packages/workspace/bulk-review-model.mjs +28 -26
  324. package/packages/workspace/bulk-session-model.mjs +27 -13
  325. package/packages/workspace/campaign-model.mjs +1 -1
  326. package/packages/workspace/candidate-watch-handoff.mjs +195 -0
  327. package/packages/workspace/detection-rule-model.mjs +11 -3
  328. package/packages/workspace/hostile-input.mjs +1 -1
  329. package/packages/workspace/investigation-guide.mjs +2 -2
  330. package/packages/workspace/investigation-template-model.mjs +26 -2
  331. package/packages/workspace/page-baseline.mjs +2 -2
  332. package/packages/workspace/relationship-observation-model.mjs +17 -13
  333. package/packages/workspace/shortlist-model.mjs +24 -7
  334. package/packages/workspace/watchlist-history.mjs +59 -16
  335. package/packages/workspace/watchlist-store.mjs +152 -11
  336. package/packages/workspace/website-snapshot-model.mjs +28 -3
  337. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  338. package/packages/workspace/workspace-archive.mjs +5 -4
  339. package/third-party-notices.txt +549 -16
  340. package/lib/hostname.mjs +0 -2
@@ -0,0 +1,392 @@
1
+ // Registration authority, lifecycle dates and positive-only delegation evidence.
2
+ import { promises as dns } from 'node:dns';
3
+ import { abortable } from "./abort.mjs";
4
+ import { fetchRdapRecord } from "./rdap.mjs";
5
+ import { buildWhoisChain, parseWhoisChain } from "./whois.mjs";
6
+ import { featureDecision, networkFeaturePolicy } from "./feature-policy.mjs";
7
+ import { parseRegistryDate, registryDateIso } from "./registry-dates.mjs";
8
+ import { nonEmptyErrorMessage } from "./error-detail.mjs";
9
+ import { registryServiceAdmissionFor } from "./registry-capabilities.mjs";
10
+ import { recordOrEmpty as errorRecord } from "./json-record.mjs";
11
+ const DNS_DELEGATION_TIMEOUT_MS = 4000;
12
+ const MAX_DELEGATION_NAMESERVERS = 50;
13
+ const MISSING_DNS_CODES = new Set(['ENODATA', 'ENOTFOUND', 'ENONAME', 'NXDOMAIN']);
14
+ function registryStatusToken(value) {
15
+ return value.toLowerCase().replace(/[^a-z0-9]+/gu, '');
16
+ }
17
+ function rdapEventDate(events, action) {
18
+ const date = events.find((event) => event.action === action)?.date;
19
+ return typeof date === 'string' ? date : null;
20
+ }
21
+ function computeAgeDays(dateStr) {
22
+ const d = parseRegistryDate(dateStr);
23
+ return d ? Math.floor((Date.now() - d.getTime()) / 86400000) : null;
24
+ }
25
+ function computeDaysUntil(dateStr) {
26
+ const d = parseRegistryDate(dateStr);
27
+ return d ? Math.ceil((d.getTime() - Date.now()) / 86400000) : null;
28
+ }
29
+ const PRIVACY_MARKERS = [
30
+ /redacted for privacy/i,
31
+ /data protected/i,
32
+ /privacy\s*protect/i,
33
+ /whoisguard/i,
34
+ /domains by proxy/i,
35
+ /perfect privacy/i,
36
+ /contact privacy/i,
37
+ /private registration/i,
38
+ /identity protect/i,
39
+ /not disclosed/i,
40
+ /withheld for privacy/i,
41
+ ];
42
+ // Explicit privacy/redaction markers establish a positive privacy signal.
43
+ // Usable public contact data establishes a negative one. A missing or blank
44
+ // contact can instead reflect registry policy, access tier, parser coverage,
45
+ // or ordinary omission, so it remains inconclusive rather than being promoted
46
+ // to an affirmative privacy claim.
47
+ function isPrivacyProtected(registrant) {
48
+ if (!registrant)
49
+ return null;
50
+ const blob = [registrant.name, registrant.org, registrant.email].filter(Boolean).join(' ');
51
+ if (!blob)
52
+ return null;
53
+ return PRIVACY_MARKERS.some((re) => re.test(blob));
54
+ }
55
+ // RDAP Lookup retains a bounded multi-value contact inventory, but Bulk and
56
+ // watchlist availability records deliberately keep the historical compact
57
+ // shape. This prevents repeated contact arrays and registry links from
58
+ // expanding browser-local stores or flowing into case evidence implicitly.
59
+ function compactContact(contact) {
60
+ if (!contact || typeof contact !== 'object' || Array.isArray(contact))
61
+ return null;
62
+ const value = contact;
63
+ const compact = {
64
+ handle: value.handle || null,
65
+ name: value.name || null,
66
+ org: value.org || null,
67
+ email: value.email || null,
68
+ phone: value.phone || null,
69
+ address: value.address || null,
70
+ };
71
+ return Object.values(compact).some(Boolean) ? compact : null;
72
+ }
73
+ // A number of ccTLD registries publish neither an RDAP bootstrap entry nor a
74
+ // generally reachable port-43 WHOIS service. A positive NS answer cannot
75
+ // replace registry registration data, but it does prove that the registrable
76
+ // domain has an active DNS delegation. Use that as a bounded, positive-only
77
+ // fallback: no answer is never interpreted as availability because registered
78
+ // domains can legitimately be undelegated.
79
+ async function checkDnsDelegation(domain, options = {}) {
80
+ options.signal?.throwIfAborted();
81
+ // Cancel only this lookup's c-ares requests, never the shared DNS resolver.
82
+ const ownedResolver = options.resolver ? null : new dns.Resolver();
83
+ const resolve = options.resolver || ((name) => ownedResolver.resolveNs(name));
84
+ const controller = new AbortController();
85
+ const signal = options.signal ? AbortSignal.any([options.signal, controller.signal]) : controller.signal;
86
+ const cancel = () => ownedResolver?.cancel();
87
+ signal.addEventListener('abort', cancel, { once: true });
88
+ const timer = setTimeout(() => controller.abort(new Error('DNS delegation lookup timed out')), DNS_DELEGATION_TIMEOUT_MS);
89
+ try {
90
+ const records = await abortable(() => resolve(domain), signal);
91
+ const validNameservers = [...new Set((Array.isArray(records) ? records : [])
92
+ .filter((value) => typeof value === 'string')
93
+ .map((value) => value.trim().replace(/\.+$/, '').toLowerCase())
94
+ .filter((value) => value.length > 0 && value.length <= 253)
95
+ .filter((value) => value.split('.').every((label) => /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/i.test(label))))]
96
+ .sort();
97
+ const normalized = validNameservers.slice(0, MAX_DELEGATION_NAMESERVERS);
98
+ return {
99
+ delegated: normalized.length > 0,
100
+ nameservers: normalized,
101
+ nameserversTruncated: validNameservers.length > MAX_DELEGATION_NAMESERVERS,
102
+ error: null,
103
+ };
104
+ }
105
+ catch (err) {
106
+ options.signal?.throwIfAborted();
107
+ const error = errorRecord(err);
108
+ if (typeof error.code === 'string' && MISSING_DNS_CODES.has(error.code)) {
109
+ return { delegated: false, nameservers: [], nameserversTruncated: false, error: null };
110
+ }
111
+ return {
112
+ delegated: false,
113
+ nameservers: [],
114
+ nameserversTruncated: false,
115
+ error: nonEmptyErrorMessage(err, String(err)).slice(0, 180),
116
+ };
117
+ }
118
+ finally {
119
+ clearTimeout(timer);
120
+ signal.removeEventListener('abort', cancel);
121
+ }
122
+ }
123
+ function registryPolicyDetail(domain, fast) {
124
+ const details = [];
125
+ const rdapAdmission = registryServiceAdmissionFor(domain, 'rdap');
126
+ if (rdapAdmission && !rdapAdmission.allowed) {
127
+ details.push('RDAP was not queried because the registry access policy does not permit collection.');
128
+ }
129
+ const whoisAdmission = fast ? null : registryServiceAdmissionFor(domain, 'whois');
130
+ if (whoisAdmission && !whoisAdmission.allowed) {
131
+ details.push(whoisAdmission.state === 'permission_required'
132
+ ? 'WHOIS was not queried because registry permission or source authorisation is required.'
133
+ : 'WHOIS was not queried because IANA publishes no domain WHOIS service for this suffix.');
134
+ }
135
+ return details.length ? ` ${details.join(' ')}` : '';
136
+ }
137
+ // fast: true skips the WHOIS fallback (no TCP:43 chain) and the homepage
138
+ // fetch (no for-sale/parking detection) - just RDAP plus the signals
139
+ // derivable from it (age, expiry proximity, privacy). Meant for scanning
140
+ // large sourcing candidate lists quickly and gently on registry rate
141
+ // limits; anything it can't resolve (state "unknown") is meant to get a
142
+ // follow-up deep check (fast: false, the default) on the shortlist only.
143
+ async function resolveDomainRegistration(domain, options, featurePolicy) {
144
+ options.signal?.throwIfAborted();
145
+ const fast = options.fast === true;
146
+ const rdapEnabled = featureDecision('rdap', featurePolicy).enabled;
147
+ const whoisEnabled = featureDecision('whois', featurePolicy).enabled;
148
+ const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
149
+ const hasPreloadedRdap = Object.prototype.hasOwnProperty.call(options, 'rdapRecord');
150
+ const hasPreloadedWhois = Object.prototype.hasOwnProperty.call(options, 'whoisChain');
151
+ const hasPreloadedRdapPromise = Object.prototype.hasOwnProperty.call(options, 'rdapRecordPromise');
152
+ const hasPreloadedWhoisPromise = Object.prototype.hasOwnProperty.call(options, 'whoisChainPromise');
153
+ const hasPreloadedDnsDelegation = Object.prototype.hasOwnProperty.call(options, 'dnsDelegation');
154
+ let nameservers = [];
155
+ let statuses = [];
156
+ let rdapServer = null;
157
+ let rdapFound = false;
158
+ let registrar = null;
159
+ let registrant = null;
160
+ let abuse = null;
161
+ let createdDate = null;
162
+ let expiryDate = null;
163
+ let createdDateIso = null;
164
+ let expiryDateIso = null;
165
+ let registrationSource = null;
166
+ let registrationConfidence = 'high';
167
+ let dnssec = null;
168
+ let registryDnsEvidence = null;
169
+ const assessment = (registration) => ({
170
+ registration, registryDnsEvidence, nameservers, dnssec,
171
+ });
172
+ if (rdapEnabled) {
173
+ try {
174
+ // Shared with /api/rdap (lib/rdap.mts's fetchRdapRecord) rather than a
175
+ // separate fetch+parse here - same registry data either way, and this
176
+ // also picks up that function's short-TTL cache (lib/lookup-cache.mts)
177
+ // and upstream timeout for free.
178
+ const recordValue = hasPreloadedRdapPromise
179
+ ? await abortable(() => options.rdapRecordPromise, options.signal)
180
+ : hasPreloadedRdap
181
+ ? options.rdapRecord
182
+ : await fetchRdapRecord('domain', domain, options.signal ? { signal: options.signal } : {});
183
+ options.signal?.throwIfAborted();
184
+ const record = errorRecord(recordValue);
185
+ const recordRdapServer = typeof record.rdapServer === 'string' ? record.rdapServer : null;
186
+ const upstreamStatus = typeof record.upstreamStatus === 'number' ? record.upstreamStatus : null;
187
+ if (Object.keys(record).length) {
188
+ rdapServer = recordRdapServer;
189
+ if (upstreamStatus === 404) {
190
+ const cleartext = record.transportSecurity === 'http'
191
+ || (recordRdapServer !== null && /^http:\/\//iu.test(recordRdapServer));
192
+ return assessment({
193
+ state: 'available',
194
+ confidence: cleartext ? 'medium' : 'high',
195
+ detail: `The registry's RDAP service has no record for this domain.${cleartext ? ' This answer arrived over unencrypted HTTP, so its transport was not authenticated.' : ''}`,
196
+ source: 'rdap',
197
+ rdapServer: recordRdapServer,
198
+ });
199
+ }
200
+ const parsed = errorRecord(record.parsed);
201
+ if (Object.keys(parsed).length) {
202
+ registryDnsEvidence = parsed;
203
+ statuses = Array.isArray(parsed.statuses)
204
+ ? parsed.statuses.filter((status) => typeof status === 'string').map((status) => status.toLowerCase())
205
+ : [];
206
+ nameservers = Array.isArray(parsed.nameservers)
207
+ ? parsed.nameservers.filter((nameserver) => typeof nameserver === 'string')
208
+ : [];
209
+ registrar = compactContact(parsed.registrar);
210
+ registrant = compactContact(parsed.registrant);
211
+ abuse = compactContact(parsed.abuse);
212
+ const events = Array.isArray(parsed.events)
213
+ ? parsed.events.map(errorRecord)
214
+ : [];
215
+ const lifecycle = errorRecord(parsed.lifecycle);
216
+ createdDate = typeof lifecycle.createdDate === 'string'
217
+ ? lifecycle.createdDate
218
+ : rdapEventDate(events, 'registration');
219
+ expiryDate = typeof lifecycle.expiryDate === 'string'
220
+ ? lifecycle.expiryDate
221
+ : rdapEventDate(events, 'expiration');
222
+ createdDateIso = typeof lifecycle.createdDateIso === 'string'
223
+ ? lifecycle.createdDateIso
224
+ : registryDateIso(createdDate);
225
+ expiryDateIso = typeof lifecycle.expiryDateIso === 'string'
226
+ ? lifecycle.expiryDateIso
227
+ : registryDateIso(expiryDate);
228
+ dnssec = typeof parsed.dnssec === 'string' ? parsed.dnssec : null;
229
+ rdapFound = true;
230
+ registrationSource = 'rdap';
231
+ }
232
+ }
233
+ }
234
+ catch {
235
+ options.signal?.throwIfAborted();
236
+ /* fall through to WHOIS-based detection (deep mode only) */
237
+ }
238
+ }
239
+ const dnsDelegationPromise = !rdapFound && dnsIntelligenceEnabled
240
+ ? hasPreloadedDnsDelegation
241
+ ? Promise.resolve(options.dnsDelegation)
242
+ : checkDnsDelegation(domain, {
243
+ ...(options.resolveNs ? { resolver: options.resolveNs } : {}),
244
+ ...(options.signal ? { signal: options.signal } : {}),
245
+ })
246
+ : null;
247
+ // DNS may settle while the independent WHOIS source is still pending. Keep
248
+ // its rejection observed even if WHOIS supplies an early conclusive return.
249
+ void dnsDelegationPromise?.catch(() => { });
250
+ let whoisChain = null;
251
+ let whoisParsed = null;
252
+ if (!rdapFound && !fast && whoisEnabled) {
253
+ try {
254
+ whoisChain = (hasPreloadedWhoisPromise
255
+ ? await abortable(() => options.whoisChainPromise, options.signal)
256
+ : hasPreloadedWhois
257
+ ? options.whoisChain
258
+ : await buildWhoisChain(domain, options.signal ? { signal: options.signal } : {})) ?? null;
259
+ options.signal?.throwIfAborted();
260
+ if (!Array.isArray(whoisChain))
261
+ throw new Error('WHOIS chain unavailable');
262
+ const parsed = parseWhoisChain(whoisChain, domain);
263
+ whoisParsed = parsed;
264
+ if (parsed.notFound) {
265
+ return assessment({
266
+ state: 'available',
267
+ confidence: 'medium',
268
+ detail: `WHOIS reports no matching record for this domain${parsed.notFoundSource ? ` (per ${parsed.notFoundSource})` : ''}.`,
269
+ source: 'whois',
270
+ });
271
+ }
272
+ if (parsed.registrationStatus === 'registered') {
273
+ if (parsed.nameservers.length)
274
+ nameservers = parsed.nameservers;
275
+ if (parsed.statuses.length)
276
+ statuses = parsed.statuses.map((status) => status.toLowerCase());
277
+ if (parsed.registrar) {
278
+ registrar = {
279
+ handle: null,
280
+ ianaId: parsed.registrarIanaId || null,
281
+ name: parsed.registrar,
282
+ org: null,
283
+ email: parsed.abuseEmail || null,
284
+ phone: parsed.abusePhone || null,
285
+ };
286
+ }
287
+ if (parsed.registrantName || parsed.registrantOrg || parsed.registrantEmail || parsed.registrantPhone) {
288
+ registrant = {
289
+ handle: null,
290
+ name: parsed.registrantName || null,
291
+ org: parsed.registrantOrg || null,
292
+ email: parsed.registrantEmail || null,
293
+ phone: parsed.registrantPhone || null,
294
+ };
295
+ }
296
+ if (parsed.abuseEmail || parsed.abusePhone) {
297
+ abuse = { handle: null, name: null, org: parsed.registrar || null, email: parsed.abuseEmail || null, phone: parsed.abusePhone || null };
298
+ }
299
+ createdDate = parsed.createdDate || null;
300
+ expiryDate = parsed.expiryDate || null;
301
+ createdDateIso = parsed.createdDateIso || parsed.lifecycle?.createdDateIso || registryDateIso(createdDate);
302
+ expiryDateIso = parsed.expiryDateIso || parsed.lifecycle?.expiryDateIso || registryDateIso(expiryDate);
303
+ if (!dnssec)
304
+ dnssec = parsed.dnssec || null;
305
+ registrationSource = 'whois';
306
+ }
307
+ }
308
+ catch {
309
+ options.signal?.throwIfAborted();
310
+ /* if both RDAP and WHOIS fail, we simply can't determine availability */
311
+ }
312
+ }
313
+ // WHOIS ran and was not a confirmed not-found, but produced no positive
314
+ // registration evidence either - e.g. the registry answered inconclusively
315
+ // or every referral hop failed/rate-limited. Report "unknown" rather than
316
+ // fabricating "registered" from an empty record.
317
+ const hasWhoisRegistrationData = whoisParsed?.registrationStatus === 'registered';
318
+ let dnsDelegated = false;
319
+ if (!rdapFound && !hasWhoisRegistrationData && dnsDelegationPromise) {
320
+ const delegation = await dnsDelegationPromise;
321
+ if (delegation && delegation.delegated === true && Array.isArray(delegation.nameservers) && delegation.nameservers.length) {
322
+ dnsDelegated = true;
323
+ nameservers = delegation.nameservers;
324
+ registrationSource = 'dns';
325
+ registrationConfidence = 'medium';
326
+ }
327
+ }
328
+ options.signal?.throwIfAborted();
329
+ if (!rdapFound && !hasWhoisRegistrationData && !dnsDelegated) {
330
+ const disabledSources = [
331
+ !rdapEnabled ? 'RDAP' : null,
332
+ !fast && !whoisEnabled ? 'WHOIS' : null,
333
+ !dnsIntelligenceEnabled ? 'DNS intelligence' : null,
334
+ ].filter(Boolean);
335
+ const disabledDetail = disabledSources.length
336
+ ? ` ${disabledSources.join(', ')} ${disabledSources.length === 1 ? 'is' : 'are'} disabled by deployment policy.`
337
+ : '';
338
+ const policyDetail = registryPolicyDetail(domain, fast);
339
+ return assessment({
340
+ state: 'unknown',
341
+ confidence: 'low',
342
+ detail: fast
343
+ ? `No enabled registration source produced a record or authoritative delegation. A fast scan cannot determine registration status.${disabledDetail}${policyDetail}`
344
+ : whoisParsed && whoisParsed.failedHop
345
+ ? `WHOIS was inconclusive - a referral hop did not answer conclusively (${whoisParsed.failedHop}).${policyDetail}`
346
+ : `No enabled registration source returned conclusive data or an authoritative DNS delegation.${disabledDetail}${policyDetail}`,
347
+ ...(!fast && whoisEnabled ? { source: 'whois' } : {}),
348
+ });
349
+ }
350
+ const domainAgeDays = computeAgeDays(createdDateIso || createdDate);
351
+ const expiresInDays = computeDaysUntil(expiryDateIso || expiryDate);
352
+ // DNS proves delegation only; it says nothing about whether registry
353
+ // contact data is privacy-protected or merely unavailable.
354
+ const privacyProtected = registrationSource === 'dns' ? null : isPrivacyProtected(registrant);
355
+ const baseInfo = {
356
+ nameservers,
357
+ statuses,
358
+ registrar,
359
+ registrant,
360
+ abuse,
361
+ createdDate,
362
+ expiryDate,
363
+ createdDateIso,
364
+ expiryDateIso,
365
+ rdapServer,
366
+ domainAgeDays,
367
+ expiresInDays,
368
+ privacyProtected,
369
+ dnssec,
370
+ source: registrationSource,
371
+ };
372
+ if (statuses.some((status) => {
373
+ const normalizedStatus = registryStatusToken(status);
374
+ return normalizedStatus.includes('pendingdelete') || normalizedStatus.includes('redemptionperiod');
375
+ })) {
376
+ return assessment({
377
+ state: 'expiring',
378
+ confidence: 'medium',
379
+ detail: 'Domain is in redemption/pending-delete status and may become available soon.',
380
+ ...baseInfo,
381
+ });
382
+ }
383
+ return assessment({
384
+ state: 'registered',
385
+ confidence: registrationConfidence,
386
+ detail: registrationSource === 'dns'
387
+ ? 'Authoritative DNS delegation confirms the domain is registered, but RDAP/WHOIS registration details were unavailable.'
388
+ : 'Domain is registered. Run a deep check for parking/for-sale detection.',
389
+ ...baseInfo,
390
+ });
391
+ }
392
+ export { resolveDomainRegistration, checkDnsDelegation, isPrivacyProtected };
@@ -1,9 +1,10 @@
1
1
  // Portable tree bounds, private-field exclusion and availability projection.
2
2
  // Source-specific publication assembly remains in evidence-export.mts.
3
- import { isSafeJsonObjectKey } from "./bounded-json.mjs";
3
+ import { isSafeJsonObjectKey } from "../packages/analysis/bounded-json.mjs";
4
4
  import { recordOrNull } from "./json-record.mjs";
5
- import { isUriShapedLabel } from "./portable-generator.mjs";
5
+ import { isUriShapedLabel } from "../packages/analysis/portable-generator.mjs";
6
6
  import { validHttpDeliveryMetadata, validPagePublicationMetadata } from "./homepage-metadata-contract.mjs";
7
+ import { normalizeWebCollectionQuality } from "../packages/evidence/collection-quality.mjs";
7
8
  import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES, LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS, LOOKUP_EVIDENCE_PORTABLE_MAX_KEY_LENGTH, LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH, } from "../packages/contracts/lookup-evidence.mjs";
8
9
  const PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
9
10
  'applicable', 'type', 'domain', 'state', 'confidence', 'detail', 'source',
@@ -12,7 +13,7 @@ const PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
12
13
  'expiresInDays', 'privacyProtected', 'dnssec', 'activityStatus',
13
14
  'websiteProbeStatus', 'websiteProbeDetail', 'http', 'deepScanComplete',
14
15
  'faviconHash', 'faviconPHash', 'pageTitle', 'hasPasswordField',
15
- 'phishingLanguageMatch', 'hasExternalFormAction', 'externalAssetHosts',
16
+ 'phishingLanguageMatch', 'hasExternalFormAction', 'hasExternalPasswordForm', 'externalAssetHosts',
16
17
  'pageIdentity', 'credentialSurfaceProfile', 'structuredDataIdentity',
17
18
  'technologyProfile', 'pageRoleProfile', 'clientBehaviorProfile',
18
19
  'securityPosture', 'dns', 'tls', 'hasMx', 'hasNullMx', 'mxHosts', 'hasSpf',
@@ -22,7 +23,9 @@ const LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
22
23
  ...[...PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS]
23
24
  .filter((key) => !['registrar', 'registrant', 'abuse'].includes(key)),
24
25
  'registryContactsExcluded', 'observationHostname', 'webObservationMode',
26
+ 'webCollectionQuality',
25
27
  ]);
28
+ const HISTORICAL_LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([...LOOKUP_AVAILABILITY_ANALYSIS_KEYS].filter(key => key !== 'webCollectionQuality'));
26
29
  const PRIVATE_EVIDENCE_KEYS = new Set([
27
30
  'authorization', 'proxyauthorization', 'cookie', 'cookies', 'setcookie',
28
31
  'session', 'sessionid', 'sessiontoken', 'token', 'accesstoken', 'refreshtoken',
@@ -102,6 +105,7 @@ const LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS = new Set([
102
105
  // Credential, structured identity, role, behaviour, technology, and posture
103
106
  // projections. Only fixed categories/counts and bounded descriptions enter.
104
107
  'credentialSurfaceVersion', 'methods', 'actions', 'missing', 'get', 'post',
108
+ 'formAttribution', 'unassociatedInputs', 'index', 'destinations', 'relationship', 'origin',
105
109
  'dialog', 'other', 'sameOrigin', 'external', 'cleartext', 'unclassified',
106
110
  'inputs', 'classifiedCount', 'categories', 'password', 'email', 'username',
107
111
  'one_time_code', 'payment', 'inputsObserved', 'classifiedInputs',
@@ -174,7 +178,7 @@ function privateEvidenceKey(value, item) {
174
178
  || /(?:request|response|authorization|proxy)headers?$/u.test(normalized)
175
179
  || (normalized.includes('credential') && !normalized.startsWith('credentialsurface'));
176
180
  }
177
- export function portableUri(value) {
181
+ export function portableUri(value, legacyUris = false) {
178
182
  const text = boundedString(value, 2048);
179
183
  if (!text)
180
184
  return null;
@@ -184,7 +188,7 @@ export function portableUri(value) {
184
188
  || url.username
185
189
  || url.password)
186
190
  return null;
187
- if (url.protocol === 'http:' || url.protocol === 'https:') {
191
+ if (!legacyUris || url.protocol === 'http:' || url.protocol === 'https:') {
188
192
  url.search = '';
189
193
  url.hash = '';
190
194
  }
@@ -268,7 +272,7 @@ function projectLookupEvidenceAvailabilityPublicValue(value, path, state, depth)
268
272
  if (typeof value === 'string') {
269
273
  const normalizedParent = normalizedEvidenceKey(path.at(-1) || '');
270
274
  if (PORTABLE_URL_KEYS.has(normalizedParent))
271
- return portableUri(value);
275
+ return portableUri(value, true);
272
276
  if (PORTABLE_ORIGIN_COLLECTION_KEYS.has(normalizedParent))
273
277
  return portableOrigin(value);
274
278
  return projectPortableString(value);
@@ -313,21 +317,23 @@ function projectLookupEvidenceAvailabilityPublicValue(value, path, state, depth)
313
317
  }
314
318
  return output;
315
319
  }
316
- function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
320
+ function projectLookupEvidenceAvailabilityValue(value, path, state, depth, legacyUris) {
317
321
  consumePortableProjectionEntry(state, depth);
318
322
  if (value === null || typeof value === 'boolean' || typeof value === 'number')
319
323
  return value;
320
324
  if (typeof value === 'string') {
321
325
  const normalizedParent = normalizedEvidenceKey(path.at(-1) || '');
326
+ if (path.join('.') === 'credentialSurfaceProfile.formAttribution.forms.destinations.origin')
327
+ return portableOrigin(value);
322
328
  if (PORTABLE_URL_KEYS.has(normalizedParent))
323
- return portableUri(value);
329
+ return portableUri(value, legacyUris);
324
330
  if (PORTABLE_ORIGIN_COLLECTION_KEYS.has(normalizedParent))
325
331
  return portableOrigin(value);
326
332
  return projectLookupEvidenceAvailabilityString(value);
327
333
  }
328
334
  if (Array.isArray(value)) {
329
335
  return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
330
- .map((item) => projectLookupEvidenceAvailabilityValue(item, path, state, depth + 1));
336
+ .map((item) => projectLookupEvidenceAvailabilityValue(item, path, state, depth + 1, legacyUris));
331
337
  }
332
338
  const source = recordOrNull(value);
333
339
  if (!source)
@@ -341,7 +347,7 @@ function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
341
347
  const boundedCount = Number.isSafeInteger(item)
342
348
  && Number(item) >= 0
343
349
  && Number(item) <= LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS;
344
- const credentialCategoryCount = parentPath === 'credentialSurfaceProfile.inputs.categories'
350
+ const credentialCategoryCount = ['credentialSurfaceProfile.inputs.categories', 'credentialSurfaceProfile.formAttribution.forms.categories'].includes(parentPath)
345
351
  && boundedCount;
346
352
  const sanClassCount = parentPath === 'tls.certificate.subjectAltNames.classes'
347
353
  && LOOKUP_AVAILABILITY_TLS_SAN_CLASS_KEYS.has(key)
@@ -371,7 +377,7 @@ function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
371
377
  && (!LOOKUP_AVAILABILITY_VALUE_PATHS.has(parentPath)
372
378
  || (item !== null && typeof item !== 'string')))
373
379
  continue;
374
- output[key] = projectLookupEvidenceAvailabilityValue(item, [...path, key], state, depth + 1);
380
+ output[key] = projectLookupEvidenceAvailabilityValue(item, [...path, key], state, depth + 1, legacyUris);
375
381
  }
376
382
  return output;
377
383
  }
@@ -381,7 +387,7 @@ function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
381
387
  * extension cannot become authenticated evidence merely by avoiding a
382
388
  * credential-key denylist.
383
389
  */
384
- function projectLookupEvidenceAvailabilityWithKeys(value, rootKeys, registryContactsExcluded, currentPrivacyRules) {
390
+ function projectLookupEvidenceAvailabilityWithKeys(value, rootKeys, registryContactsExcluded, currentPrivacyRules, legacyUris = false) {
385
391
  const source = recordOrNull(value);
386
392
  if (!source)
387
393
  return null;
@@ -394,8 +400,12 @@ function projectLookupEvidenceAvailabilityWithKeys(value, rootKeys, registryCont
394
400
  const item = source[key];
395
401
  if (privateEvidenceKey(key, item))
396
402
  continue;
403
+ if (key === 'webCollectionQuality') {
404
+ output[key] = normalizeWebCollectionQuality(item);
405
+ continue;
406
+ }
397
407
  output[key] = currentPrivacyRules
398
- ? projectLookupEvidenceAvailabilityValue(item, [key], state, 1)
408
+ ? projectLookupEvidenceAvailabilityValue(item, [key], state, 1, legacyUris)
399
409
  : projectLookupEvidenceAvailabilityPublicValue(item, [key], state, 1);
400
410
  }
401
411
  if (registryContactsExcluded)
@@ -437,10 +447,10 @@ export function projectLookupEvidenceAvailabilityPublic(value) {
437
447
  * and abuse contact routes are deliberately excluded at the root boundary;
438
448
  * the marker prevents that privacy omission from being read as source absence.
439
449
  */
440
- export function projectLookupEvidenceAvailability(value) {
441
- return projectLookupEvidenceAvailabilityWithKeys(value, LOOKUP_AVAILABILITY_ANALYSIS_KEYS, true, true);
450
+ export function projectLookupEvidenceAvailability(value, { legacyUris = false } = {}) {
451
+ return projectLookupEvidenceAvailabilityWithKeys(value, legacyUris ? HISTORICAL_LOOKUP_AVAILABILITY_ANALYSIS_KEYS : LOOKUP_AVAILABILITY_ANALYSIS_KEYS, true, true, legacyUris);
442
452
  }
443
- export function assertLookupEvidencePrivacySafeTree(value) {
453
+ export function assertLookupEvidencePrivacySafeTree(value, { legacyUris = false } = {}) {
444
454
  assertLookupEvidencePortableTree(value);
445
455
  const pending = [value];
446
456
  while (pending.length) {
@@ -466,7 +476,7 @@ export function assertLookupEvidencePrivacySafeTree(value) {
466
476
  if (privateEvidenceKey(normalized, item)) {
467
477
  throw new TypeError('Lookup evidence contains excluded private request or session material.');
468
478
  }
469
- if (typeof item === 'string' && PORTABLE_URL_KEYS.has(normalized) && portableUri(item) !== item) {
479
+ if (typeof item === 'string' && PORTABLE_URL_KEYS.has(normalized) && portableUri(item, legacyUris) !== item) {
470
480
  throw new TypeError('Lookup evidence contains a credential-bearing or non-portable URL.');
471
481
  }
472
482
  pending.push(item);
@@ -3,12 +3,12 @@ import { projectLookupEvidencePrivacySafeTree, projectLookupEvidenceAvailability
3
3
  export { projectLookupEvidencePrivacySafeTree, projectLookupEvidenceAvailabilityPublic, projectLookupEvidenceAvailability, assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, serializeLookupEvidence, } from "./evidence-export-privacy.mjs";
4
4
  import { compareRdapPublications, compareRegistrySources } from "./registry-comparison.mjs";
5
5
  import { buildRegistryInsights } from "./registry-insights.mjs";
6
- import { buildPortableGeneratorMetadata } from "./portable-generator.mjs";
7
- import { assertBoundedJsonStructure } from "./bounded-json.mjs";
6
+ import { buildPortableGeneratorMetadata } from "../packages/analysis/portable-generator.mjs";
7
+ import { assertBoundedJsonStructure } from "../packages/analysis/bounded-json.mjs";
8
8
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
9
9
  import { LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../packages/contracts/lookup-evidence.mjs";
10
10
  export * from "../packages/contracts/lookup-evidence.mjs";
11
- import { isValidAsciiHostname } from "./hostname.mjs";
11
+ import { isValidAsciiHostname } from "../packages/contracts/domain-name.mjs";
12
12
  import { validLookupObservationScope } from "../packages/evidence/lookup-target.mjs";
13
13
  import { REGISTRAR_STANDING_SCHEMA, REGISTRAR_STANDING_VERSION, registrarIanaIds, registrarStandingObservedBy, resolveRegistrarIanaId, validRegistrarStanding, } from "./registrar-standing-contract.mjs";
14
14
  const REGISTRAR_RDAP_STATUSES = new Set([
@@ -1,5 +1,6 @@
1
1
  import { registryAccessProfileLabel } from "./registry-access.mjs";
2
- import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "./portable-generator.mjs";
2
+ import { dnsAddressFamilyEvidence } from "../packages/evidence/dns-query-outcome.mjs";
3
+ import { buildPortableGeneratorMetadata, portableGeneratorAttribution, } from "../packages/analysis/portable-generator.mjs";
3
4
  import { technologyEvidenceRoles, } from "./technology-evidence-role.mjs";
4
5
  const MAX_REPORT_VALUE_LENGTH = 300;
5
6
  const MAX_REPORT_LIST_ITEMS = 50;
@@ -241,6 +242,8 @@ function buildLookupEvidenceReport(document, options = {}) {
241
242
  title: 'DNS and mail',
242
243
  fields: [
243
244
  reportField('DNS status', displayLabel(dns.status)),
245
+ reportField('A address result', dnsAddressFamilyEvidence(dns, 'a').value),
246
+ reportField('AAAA address result', dnsAddressFamilyEvidence(dns, 'aaaa').value),
244
247
  reportField('Observed', dns.observedAt),
245
248
  reportField('Name servers', listText(availability.nameservers)),
246
249
  reportField('MX hosts', listText(availability.mxHosts)),
@@ -264,6 +267,7 @@ function buildLookupEvidenceReport(document, options = {}) {
264
267
  reportField('Redirects', http.redirectCount),
265
268
  reportField('Page title', availability.pageTitle),
266
269
  reportField('Password field observed', yesNoUnknown(availability.hasPasswordField)),
270
+ reportField('Password form declares external destination', yesNoUnknown(availability.hasExternalPasswordForm)),
267
271
  reportField('Authoritative nameserver evidence', listText(availability.nameservers)),
268
272
  reportField('Observed edge, CDN, reverse proxy or WAF', reportTechnologyNames(technologyFindings, 'observed_edge')),
269
273
  reportField('Application-platform indicators', reportTechnologyNames(technologyFindings, 'application_platform')),