@slicedearth/whoisleuth-cli 2.2.0 → 2.4.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 (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -15
  3. package/bin/whoisleuth.mjs +8 -1
  4. package/cli/archive-content-digest.mjs +14 -0
  5. package/cli/archive-inspect.mjs +20 -53
  6. package/cli/arguments.mjs +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +112 -72
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,43 +1,16 @@
1
+ import { recordOrNull } from "./json-record.mjs";
2
+ import { projectLookupEvidencePrivacySafeTree, projectLookupEvidenceAvailability, assertLookupEvidencePortableTree, boundedString, portableUri, projectedKnownRecord, } from "./evidence-export-privacy.mjs";
3
+ export { projectLookupEvidencePrivacySafeTree, projectLookupEvidenceAvailabilityPublic, projectLookupEvidenceAvailability, assertLookupEvidencePrivacySafeTree, assertLookupEvidencePortableTree, serializeLookupEvidence, } from "./evidence-export-privacy.mjs";
1
4
  import { compareRdapPublications, compareRegistrySources } from "./registry-comparison.mjs";
2
5
  import { buildRegistryInsights } from "./registry-insights.mjs";
3
- import { buildPortableGeneratorMetadata, isUriShapedLabel } from "./portable-generator.mjs";
4
- import { assertBoundedJsonStructure, isSafeJsonObjectKey } from "./bounded-json.mjs";
5
- import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "./homepage-metadata-contract.mjs";
6
+ import { buildPortableGeneratorMetadata } from "./portable-generator.mjs";
7
+ import { assertBoundedJsonStructure } from "./bounded-json.mjs";
6
8
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
7
- import { defineSchemaCompatibility } from "../packages/contracts/schema-compatibility.mjs";
9
+ import { LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, } from "../packages/contracts/lookup-evidence.mjs";
10
+ export * from "../packages/contracts/lookup-evidence.mjs";
8
11
  import { isValidAsciiHostname } from "./hostname.mjs";
12
+ import { validLookupObservationScope } from "../packages/evidence/lookup-target.mjs";
9
13
  import { REGISTRAR_STANDING_SCHEMA, REGISTRAR_STANDING_VERSION, registrarIanaIds, registrarStandingObservedBy, resolveRegistrarIanaId, validRegistrarStanding, } from "./registrar-standing-contract.mjs";
10
- export const LOOKUP_EVIDENCE_SCHEMA = 'whoisleuth.lookup-evidence';
11
- export const V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION = 26;
12
- export const PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION = 27;
13
- export const LATEST_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION = 28;
14
- export const PRIVACY_MINIMIZED_LOOKUP_EVIDENCE_SCHEMA_VERSION = PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION;
15
- export const LOOKUP_EVIDENCE_SCHEMA_VERSION = LATEST_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION;
16
- export const SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS = Object.freeze([
17
- V1_PUBLIC_LOOKUP_EVIDENCE_SCHEMA_VERSION,
18
- PUBLISHED_V2_LOOKUP_EVIDENCE_SCHEMA_VERSION,
19
- LOOKUP_EVIDENCE_SCHEMA_VERSION,
20
- ]);
21
- export const LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES = 5 * 1024 * 1024;
22
- export const LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES = 20_000;
23
- export const LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH = 24;
24
- export const LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS = 10_000;
25
- export const LOOKUP_EVIDENCE_PORTABLE_MAX_KEY_LENGTH = 256;
26
- export const LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH = 1024 * 1024;
27
- export const LOOKUP_EVIDENCE_COMPATIBILITY = defineSchemaCompatibility({
28
- id: 'export.lookup-evidence',
29
- kind: 'export',
30
- schema: LOOKUP_EVIDENCE_SCHEMA,
31
- currentVersion: LOOKUP_EVIDENCE_SCHEMA_VERSION,
32
- supportedVersions: SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS,
33
- acceptsUnversionedLegacy: false,
34
- futureVersionBehavior: 'reject',
35
- migration: 'read_only',
36
- writeSemantics: 'read_only',
37
- byteBudget: LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES,
38
- owner: 'lib/evidence-export.mts',
39
- note: 'Exact v1.47.4 version 26, published v2.0.1 version 27, and latest-public v2.1.0 version 28 remain replayable; version 28 adds a bounded official-source registrar-standing projection.',
40
- });
41
14
  const REGISTRAR_RDAP_STATUSES = new Set([
42
15
  'success', 'partial', 'error', 'unsupported', 'not_found', 'skipped', 'disabled',
43
16
  ]);
@@ -69,519 +42,11 @@ const LOOKUP_TIMING_SOURCES = new Set([
69
42
  'network_context', 'security_txt', 'external_intelligence',
70
43
  'malware_host_intelligence', 'malware_ioc_intelligence',
71
44
  ]);
72
- const PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
73
- 'applicable', 'type', 'domain', 'state', 'confidence', 'detail', 'source',
74
- 'rdapServer', 'nameservers', 'statuses', 'registrar', 'registrant', 'abuse',
75
- 'createdDate', 'expiryDate', 'createdDateIso', 'expiryDateIso', 'domainAgeDays',
76
- 'expiresInDays', 'privacyProtected', 'dnssec', 'activityStatus',
77
- 'websiteProbeStatus', 'websiteProbeDetail', 'http', 'deepScanComplete',
78
- 'faviconHash', 'faviconPHash', 'pageTitle', 'hasPasswordField',
79
- 'phishingLanguageMatch', 'hasExternalFormAction', 'externalAssetHosts',
80
- 'pageIdentity', 'credentialSurfaceProfile', 'structuredDataIdentity',
81
- 'technologyProfile', 'pageRoleProfile', 'clientBehaviorProfile',
82
- 'securityPosture', 'dns', 'tls', 'hasMx', 'hasNullMx', 'mxHosts', 'hasSpf',
83
- 'hasDmarc', 'bulkComparison', 'limitations',
84
- ]);
85
- const LOOKUP_AVAILABILITY_ANALYSIS_KEYS = new Set([
86
- ...[...PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS]
87
- .filter((key) => !['registrar', 'registrant', 'abuse'].includes(key)),
88
- 'registryContactsExcluded',
89
- ]);
90
45
  const LOOKUP_IDN_ANALYSIS_KEYS = new Set([
91
46
  'version', 'mappingVersion', 'asciiDomain', 'unicodeDomain', 'hasIdn',
92
47
  'scripts', 'labels', 'mixedScript', 'skeleton', 'referenceMatches', 'findings',
93
48
  'truncated', 'limitations',
94
49
  ]);
95
- const PRIVATE_EVIDENCE_KEYS = new Set([
96
- 'authorization', 'proxyauthorization', 'cookie', 'cookies', 'setcookie',
97
- 'session', 'sessionid', 'sessiontoken', 'token', 'accesstoken', 'refreshtoken',
98
- 'apikey', 'password', 'secret', 'credentials', 'requestheaders', 'responseheaders',
99
- 'headers', 'rawbody', 'responsebody',
100
- ]);
101
- const SAFE_EVIDENCE_KEYS = new Set([
102
- 'credentialsurfaceprofile', 'credentialsurfaceversion', 'haspasswordfield', 'tokencount',
103
- ]);
104
- const PORTABLE_URL_KEYS = new Set([
105
- 'endpoint', 'url', 'uri', 'href', 'rdapserver', 'finalurl', 'requestedurl',
106
- 'requesturl', 'officiallookupurl', 'declaredorigin', 'actionurl', 'from', 'to',
107
- ]);
108
- const PORTABLE_ORIGIN_COLLECTION_KEYS = new Set([
109
- 'embeddedorigins', 'externalactionorigins', 'externalorigins',
110
- ]);
111
- // Availability is assembled from a finite set of versioned, bounded analysis
112
- // models. Portable exports retain only their declared output vocabulary rather
113
- // than recursively copying arbitrary browser-local extensions. This set is
114
- // intentionally exact and case-sensitive; new model fields must be reviewed at
115
- // this privacy boundary before they can enter a portable artefact.
116
- const LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS = new Set([
117
- // Shared observation and summary fields.
118
- 'version', 'profileVersion', 'status', 'state', 'source', 'observedAt',
119
- 'scanMode', 'durationMs', 'complete', 'truncated', 'limitations',
120
- 'diagnostics', 'detail', 'error', 'count', 'total', 'discarded', 'summary',
121
- 'findings', 'id', 'label', 'tone', 'category', 'confidence', 'compatibility', 'role', 'roles', 'evidence',
122
- 'value', 'values', 'name', 'type', 'types', 'mode', 'owner', 'target',
123
- 'url', 'uri', 'href',
124
- 'priority', 'ttl', 'records',
125
- // HTTP observation and retained response metadata.
126
- 'requestUrl', 'finalUrl', 'transportSecurity', 'redirectCount',
127
- 'redirectLimitReached', 'redirects', 'crossOriginRedirect', 'httpsDowngrade',
128
- 'attempts', 'response', 'contentType', 'contentLanguage', 'server',
129
- 'declaredContentLength', 'capturedBodyBytes', 'bodyInspected', 'bodyTruncated',
130
- 'bodyHash', 'securityHeaders', 'strictTransportSecurity',
131
- 'contentSecurityPolicy', 'xFrameOptions', 'xContentTypeOptions',
132
- 'referrerPolicy', 'algorithm', 'scope', 'bytes', 'from', 'to',
133
- 'queryOmitted', 'pathTruncated', 'outcome', 'httpStatus', 'attemptCount',
134
- // DNS observation, effective CAA, and HTTPS/SVCB parameter summaries.
135
- 'a', 'aaaa', 'caa', 'cname', 'dmarc', 'https', 'mx', 'ns', 'ptr', 'soa',
136
- 'spf', 'delegation', 'caaPolicy', 'policyVersion', 'queryLimit',
137
- 'queriedOwners', 'effectiveOwner', 'inherited', 'tree', 'registryEvidence',
138
- 'exchange', 'nsname', 'hostmaster', 'serial', 'refresh', 'retry', 'expire', 'minttl',
139
- 'tag', 'critical', 'parameters', 'parametersIgnored', 'mandatory', 'alpn',
140
- 'noDefaultAlpn', 'port', 'ipv4hint', 'ipv6hint', 'opaque', 'key', 'length',
141
- 'unknownKeys', 'unsupportedMandatoryKeys', 'targetIsOwner',
142
- 'serviceUnavailable', 'compatible', 'effectiveOwner', 'queriedOwners',
143
- // TLS profile, certificate, extension, and connection summaries. Runtime
144
- // authorisation objects are deliberately absent from this portable set.
145
- 'connectedAddress', 'connectedFamily', 'sniHost', 'protocol', 'alpnProtocol',
146
- 'cipher', 'ephemeralKey', 'hostname', 'matches', 'validity', 'certificate',
147
- 'chain', 'chainTruncated', 'standardName', 'size', 'subject', 'issuer',
148
- 'serialNumber', 'validFrom', 'validTo', 'fingerprintSha1',
149
- 'fingerprintSha256', 'isCertificateAuthority', 'subjectAltNames',
150
- 'dnsNames', 'ipAddresses', 'otherNames', 'publicKey', 'bits', 'curve',
151
- 'signature', 'oid', 'extendedKeyUsage', 'authorityInformationAccess',
152
- 'ocsp', 'caIssuers', 'unknownMethods', 'extensionProfile', 'parsed', 'partial',
153
- 'certificatePolicies', 'crlDistributionPoints', 'commonNames',
154
- 'organizations', 'organizationalUnits', 'countries', 'localities', 'states',
155
- 'connectionAttempts', 'resolvedAddressCount', 'discardedFields',
156
- // Page identity, relationships, and bounded fingerprints.
157
- 'identityVersion', 'documentLanguage', 'canonical', 'metaRefresh',
158
- 'openGraph', 'title', 'siteName', 'generator', 'forms', 'postCount',
159
- 'insecureActionCount', 'externalActionOrigins', 'resources', 'byType',
160
- 'image', 'script', 'stylesheet', 'link', 'frame', 'media', 'object',
161
- 'externalOrigins', 'embeddedOrigins', 'contactDomains', 'downloads',
162
- 'explicitCount', 'riskyCount', 'riskyFileTypes', 'trackingIdentifiers',
163
- 'fingerprints', 'fingerprintVersion', 'exact', 'normalizedHtml',
164
- 'visibleText', 'domStructure', 'formStructure', 'resourceHosts',
165
- 'identifiers', 'featureCount', 'nodeCount', 'formCount', 'controlCount',
166
- 'parser', 'tokenCount', 'tagsExamined', 'discardedUrls', 'formsObserved',
167
- 'relationshipTagsExamined', 'relationshipUrlsDiscarded',
168
- 'trackingIdentifiersTruncated',
169
- // Credential, structured identity, role, behaviour, technology, and posture
170
- // projections. Only fixed categories/counts and bounded descriptions enter.
171
- 'credentialSurfaceVersion', 'methods', 'actions', 'missing', 'get', 'post',
172
- 'dialog', 'other', 'sameOrigin', 'external', 'cleartext', 'unclassified',
173
- 'inputs', 'classifiedCount', 'categories', 'password', 'email', 'username',
174
- 'one_time_code', 'payment', 'inputsObserved', 'classifiedInputs',
175
- 'unclassifiedActions', 'structuredDataVersion', 'scriptsObserved',
176
- 'scriptsExamined', 'charactersExamined', 'documentsParsed', 'malformedScripts',
177
- 'externalScriptsSkipped', 'objectsExamined', 'arrayItemsExamined',
178
- 'discardedProperties', 'entities', 'declaredOrigin', 'sameAsHosts',
179
- 'pageRoleProfileVersion', 'primaryRole', 'role', 'rolesObserved',
180
- 'clientBehaviorProfileVersion', 'scriptSummary', 'elementsObserved',
181
- 'referencedScripts', 'inlineScripts', 'moduleScripts', 'indicators',
182
- 'evidenceClass', 'occurrences', 'explanation', 'indicatorsObserved',
183
- 'scriptElementsExamined', 'inlineCharactersExamined', 'technologyProfile',
184
- 'browserLibraryProfile', 'catalog', 'knownExploitedCatalog', 'releasedAt',
185
- 'sourceRevision', 'apparentVersion', 'detectionMethods', 'advisoryCount',
186
- 'highestSeverity', 'advisoryIdentifiers', 'knownExploitedCount',
187
- 'knownExploitedIdentifiers', 'weaknessClasses', 'htmlEvaluated',
188
- 'generatorEvaluated', 'serverEvaluated', 'resourceOriginsEvaluated',
189
- 'passiveHeadersEvaluated', 'tagLimitReached', 'referencesExamined',
190
- 'inlineScriptsExamined', 'inlineSignatureCharactersExamined',
191
- 'inlineSignatureTimedOut', 'inlineSignatureUnavailable', 'catalogComponents',
192
- 'advisoryMatches', 'knownExploitedMatches', 'postureVersion',
193
- 'potentialExposure', 'observedAbsence', 'unavailable',
194
- // Bounded cross-row comparison evidence.
195
- 'bulkComparison', 'technology', 'tls', 'ids', 'issuerLabel', 'spkiSha256',
196
- ]);
197
- const LOOKUP_AVAILABILITY_CREDENTIAL_CATEGORY_KEYS = new Set([
198
- 'password', 'email', 'username', 'one_time_code', 'payment',
199
- ]);
200
- const LOOKUP_AVAILABILITY_VALUE_PATHS = new Set([
201
- 'dns.records.caa',
202
- 'dns.caaPolicy.records',
203
- 'http.response.bodyHash',
204
- 'pageIdentity.trackingIdentifiers',
205
- 'pageIdentity.fingerprints.exact',
206
- 'pageIdentity.fingerprints.normalizedHtml',
207
- 'pageIdentity.fingerprints.visibleText',
208
- 'pageIdentity.fingerprints.domStructure',
209
- 'pageIdentity.fingerprints.domStructure.similarity',
210
- 'pageIdentity.fingerprints.formStructure',
211
- 'pageIdentity.fingerprints.resourceHosts',
212
- 'pageIdentity.fingerprints.identifiers',
213
- 'pageIdentity.fingerprints.identifiers.values',
214
- ]);
215
- const LOOKUP_AVAILABILITY_OWNER_PATHS = new Set([
216
- 'dns.records.https',
217
- 'dns.caaPolicy.queriedOwners',
218
- ]);
219
- function recordOrNull(value) {
220
- return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
221
- }
222
- function normalizedEvidenceKey(value) {
223
- return value.toLowerCase().replace(/[^a-z0-9]/gu, '');
224
- }
225
- function privateEvidenceKey(value, item) {
226
- const normalized = normalizedEvidenceKey(value);
227
- if (SAFE_EVIDENCE_KEYS.has(normalized))
228
- return false;
229
- if (normalized === 'password' && (typeof item === 'number' || typeof item === 'boolean'))
230
- return false;
231
- return PRIVATE_EVIDENCE_KEYS.has(normalized)
232
- || normalized.includes('authorization')
233
- || normalized.includes('authheader')
234
- || normalized.includes('cookie')
235
- || normalized.includes('apikey')
236
- || normalized.includes('secret')
237
- || normalized.includes('sessionkey')
238
- || normalized.includes('passwordvalue')
239
- || normalized.endsWith('password')
240
- || /(?:access|auth|bearer|csrf|refresh|session)token$/u.test(normalized)
241
- || /(?:request|response|authorization|proxy)headers?$/u.test(normalized)
242
- || (normalized.includes('credential') && !normalized.startsWith('credentialsurface'));
243
- }
244
- function portableUri(value) {
245
- const text = boundedString(value, 2048);
246
- if (!text)
247
- return null;
248
- try {
249
- const url = new URL(text);
250
- if (!['http:', 'https:', 'mailto:', 'tel:', 'dns:', 'openpgp4fpr:'].includes(url.protocol)
251
- || url.username
252
- || url.password)
253
- return null;
254
- if (url.protocol === 'http:' || url.protocol === 'https:') {
255
- url.search = '';
256
- url.hash = '';
257
- }
258
- return url.toString().slice(0, 2048);
259
- }
260
- catch {
261
- return null;
262
- }
263
- }
264
- function portableOrigin(value) {
265
- const text = boundedString(value, 2048);
266
- if (!text)
267
- return null;
268
- try {
269
- const url = new URL(text);
270
- return ['http:', 'https:'].includes(url.protocol) && !url.username && !url.password
271
- ? url.origin
272
- : null;
273
- }
274
- catch {
275
- return null;
276
- }
277
- }
278
- function consumePortableProjectionEntry(state, depth) {
279
- state.entries += 1;
280
- if (state.entries > LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES) {
281
- throw new TypeError(`Lookup evidence projection exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES.toLocaleString('en')}-entry portable limit.`);
282
- }
283
- if (depth > LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH) {
284
- throw new TypeError(`Lookup evidence projection exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH}-level portable nesting limit.`);
285
- }
286
- }
287
- function projectPortableString(value) {
288
- const sanitized = value.replace(/[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/gu, ' ');
289
- return sanitized.length <= LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH
290
- ? sanitized
291
- : sanitized.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH);
292
- }
293
- function projectLookupEvidenceAvailabilityString(value) {
294
- const sanitized = projectPortableString(value)
295
- .replace(/[\u0080-\u009f]/gu, ' ')
296
- .replace(/\p{Default_Ignorable_Code_Point}/gu, '');
297
- return isUriShapedLabel(value) ? null : sanitized;
298
- }
299
- function projectLookupEvidencePrivacySafeTreeValue(value, state, depth) {
300
- consumePortableProjectionEntry(state, depth);
301
- if (value === null || typeof value === 'boolean' || typeof value === 'number')
302
- return value;
303
- if (typeof value === 'string')
304
- return projectPortableString(value);
305
- if (Array.isArray(value)) {
306
- return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
307
- .map((item) => projectLookupEvidencePrivacySafeTreeValue(item, state, depth + 1));
308
- }
309
- const source = recordOrNull(value);
310
- if (!source)
311
- return null;
312
- const output = {};
313
- for (const [key, item] of Object.entries(source).slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)) {
314
- if (!isSafeJsonObjectKey(key))
315
- throw new TypeError('Lookup evidence projection contains an unsafe object key.');
316
- const normalized = normalizedEvidenceKey(key);
317
- if (privateEvidenceKey(normalized, item))
318
- continue;
319
- if (typeof item === 'string' && PORTABLE_URL_KEYS.has(normalized)) {
320
- consumePortableProjectionEntry(state, depth + 1);
321
- output[key] = portableUri(item);
322
- continue;
323
- }
324
- output[key] = projectLookupEvidencePrivacySafeTreeValue(item, state, depth + 1);
325
- }
326
- return output;
327
- }
328
- export function projectLookupEvidencePrivacySafeTree(value) {
329
- return projectLookupEvidencePrivacySafeTreeValue(value, { entries: 0 }, 0);
330
- }
331
- function projectLookupEvidenceAvailabilityPublicValue(value, parentKey, state, depth) {
332
- consumePortableProjectionEntry(state, depth);
333
- if (value === null || typeof value === 'boolean' || typeof value === 'number')
334
- return value;
335
- if (typeof value === 'string') {
336
- const normalizedParent = normalizedEvidenceKey(parentKey || '');
337
- if (PORTABLE_URL_KEYS.has(normalizedParent))
338
- return portableUri(value);
339
- if (PORTABLE_ORIGIN_COLLECTION_KEYS.has(normalizedParent))
340
- return portableOrigin(value);
341
- return projectPortableString(value);
342
- }
343
- if (Array.isArray(value)) {
344
- return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
345
- .map((item) => projectLookupEvidenceAvailabilityPublicValue(item, parentKey, state, depth + 1));
346
- }
347
- const source = recordOrNull(value);
348
- if (!source)
349
- return null;
350
- const output = {};
351
- for (const [key, item] of Object.entries(source).slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)) {
352
- if (!LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS.has(key)
353
- || privateEvidenceKey(key, item))
354
- continue;
355
- output[key] = projectLookupEvidenceAvailabilityPublicValue(item, key, state, depth + 1);
356
- }
357
- return output;
358
- }
359
- function projectLookupEvidenceAvailabilityValue(value, path, state, depth) {
360
- consumePortableProjectionEntry(state, depth);
361
- if (value === null || typeof value === 'boolean' || typeof value === 'number')
362
- return value;
363
- if (typeof value === 'string') {
364
- const normalizedParent = normalizedEvidenceKey(path.at(-1) || '');
365
- if (PORTABLE_URL_KEYS.has(normalizedParent))
366
- return portableUri(value);
367
- if (PORTABLE_ORIGIN_COLLECTION_KEYS.has(normalizedParent))
368
- return portableOrigin(value);
369
- return projectLookupEvidenceAvailabilityString(value);
370
- }
371
- if (Array.isArray(value)) {
372
- return value.slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)
373
- .map((item) => projectLookupEvidenceAvailabilityValue(item, path, state, depth + 1));
374
- }
375
- const source = recordOrNull(value);
376
- if (!source)
377
- return null;
378
- const output = {};
379
- for (const [key, item] of Object.entries(source).slice(0, LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS)) {
380
- if (!LOOKUP_AVAILABILITY_PORTABLE_NESTED_KEYS.has(key)
381
- || privateEvidenceKey(key, item))
382
- continue;
383
- const parentPath = path.join('.');
384
- if (LOOKUP_AVAILABILITY_CREDENTIAL_CATEGORY_KEYS.has(key)
385
- && (parentPath !== 'credentialSurfaceProfile.inputs.categories'
386
- || !Number.isSafeInteger(item)
387
- || Number(item) < 0
388
- || Number(item) > LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS))
389
- continue;
390
- if (key === 'owner'
391
- && (!LOOKUP_AVAILABILITY_OWNER_PATHS.has(parentPath) || typeof item !== 'string'))
392
- continue;
393
- if (key === 'value'
394
- && (!LOOKUP_AVAILABILITY_VALUE_PATHS.has(parentPath)
395
- || (item !== null && typeof item !== 'string')))
396
- continue;
397
- output[key] = projectLookupEvidenceAvailabilityValue(item, [...path, key], state, depth + 1);
398
- }
399
- return output;
400
- }
401
- /**
402
- * Canonical portable projection for the bounded Lookup availability models.
403
- * The root and every nested record are positive allowlists, so an imported
404
- * extension cannot become authenticated evidence merely by avoiding a
405
- * credential-key denylist.
406
- */
407
- function projectLookupEvidenceAvailabilityWithKeys(value, rootKeys, registryContactsExcluded, currentPrivacyRules) {
408
- const source = recordOrNull(value);
409
- if (!source)
410
- return null;
411
- const state = { entries: 0 };
412
- consumePortableProjectionEntry(state, 0);
413
- const output = {};
414
- for (const key of rootKeys) {
415
- if (!Object.hasOwn(source, key))
416
- continue;
417
- const item = source[key];
418
- if (privateEvidenceKey(key, item))
419
- continue;
420
- output[key] = currentPrivacyRules
421
- ? projectLookupEvidenceAvailabilityValue(item, [key], state, 1)
422
- : projectLookupEvidenceAvailabilityPublicValue(item, key, state, 1);
423
- }
424
- if (registryContactsExcluded)
425
- output.registryContactsExcluded = true;
426
- const pageIdentity = recordOrNull(source.pageIdentity);
427
- const publicationMetadata = pageIdentity?.publicationMetadata;
428
- if (publicationMetadata !== undefined) {
429
- if (!['success', 'partial'].includes(String(pageIdentity?.status))
430
- || !validPagePublicationMetadata(publicationMetadata)) {
431
- throw new TypeError('Lookup evidence contains invalid page publication metadata.');
432
- }
433
- const projectedPageIdentity = recordOrNull(output.pageIdentity);
434
- if (projectedPageIdentity) {
435
- projectedPageIdentity.publicationMetadata = projectLookupEvidencePrivacySafeTree(publicationMetadata);
436
- }
437
- }
438
- const httpResponse = recordOrNull(recordOrNull(source.http)?.response);
439
- const sourceHttp = recordOrNull(source.http);
440
- const deliveryMetadata = httpResponse?.deliveryMetadata;
441
- if (deliveryMetadata !== undefined) {
442
- if (!['success', 'partial'].includes(String(sourceHttp?.status))
443
- || !validHttpDeliveryMetadata(deliveryMetadata)) {
444
- throw new TypeError('Lookup evidence contains invalid HTTP delivery metadata.');
445
- }
446
- const projectedHttp = recordOrNull(output.http);
447
- const projectedResponse = recordOrNull(projectedHttp?.response);
448
- if (projectedResponse) {
449
- projectedResponse.deliveryMetadata = projectLookupEvidencePrivacySafeTree(deliveryMetadata);
450
- }
451
- }
452
- return output;
453
- }
454
- /** Exact availability projection used only to validate public schema 26. */
455
- export function projectLookupEvidenceAvailabilityPublic(value) {
456
- return projectLookupEvidenceAvailabilityWithKeys(value, PUBLIC_LOOKUP_AVAILABILITY_ANALYSIS_KEYS, false, false);
457
- }
458
- /**
459
- * Current availability projection. Registry-derived registrar, registrant,
460
- * and abuse contact routes are deliberately excluded at the root boundary;
461
- * the marker prevents that privacy omission from being read as source absence.
462
- */
463
- export function projectLookupEvidenceAvailability(value) {
464
- return projectLookupEvidenceAvailabilityWithKeys(value, LOOKUP_AVAILABILITY_ANALYSIS_KEYS, true, true);
465
- }
466
- export function assertLookupEvidencePrivacySafeTree(value) {
467
- assertLookupEvidencePortableTree(value);
468
- const pending = [value];
469
- while (pending.length) {
470
- const current = pending.pop();
471
- if (current === null || typeof current === 'boolean' || typeof current === 'number')
472
- continue;
473
- if (typeof current === 'string') {
474
- if (projectLookupEvidencePrivacySafeTree(current) !== current) {
475
- throw new TypeError('Lookup evidence contains a non-portable string value.');
476
- }
477
- continue;
478
- }
479
- if (Array.isArray(current)) {
480
- for (const item of current)
481
- pending.push(item);
482
- continue;
483
- }
484
- const source = recordOrNull(current);
485
- if (!source)
486
- throw new TypeError('Lookup evidence contains a non-JSON value.');
487
- for (const [key, item] of Object.entries(source)) {
488
- const normalized = normalizedEvidenceKey(key);
489
- if (privateEvidenceKey(normalized, item)) {
490
- throw new TypeError('Lookup evidence contains excluded private request or session material.');
491
- }
492
- if (typeof item === 'string' && PORTABLE_URL_KEYS.has(normalized) && portableUri(item) !== item) {
493
- throw new TypeError('Lookup evidence contains a credential-bearing or non-portable URL.');
494
- }
495
- pending.push(item);
496
- }
497
- }
498
- }
499
- function projectedKnownRecord(value, keys) {
500
- const source = recordOrNull(value);
501
- if (!source)
502
- return null;
503
- const output = {};
504
- for (const key of keys) {
505
- if (Object.hasOwn(source, key))
506
- output[key] = projectLookupEvidencePrivacySafeTree(source[key]);
507
- }
508
- return output;
509
- }
510
- export function assertLookupEvidencePortableTree(value) {
511
- const pending = [{ value, depth: 0 }];
512
- let entries = 0;
513
- while (pending.length) {
514
- const current = pending.pop();
515
- if (!current)
516
- break;
517
- entries += 1;
518
- if (entries > LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES) {
519
- throw new TypeError(`Lookup evidence exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_ENTRIES.toLocaleString('en')}-entry portable limit.`);
520
- }
521
- if (current.depth > LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH) {
522
- throw new TypeError(`Lookup evidence exceeds the ${LOOKUP_EVIDENCE_PORTABLE_MAX_DEPTH}-level portable nesting limit.`);
523
- }
524
- if (typeof current.value === 'string') {
525
- if (current.value.length > LOOKUP_EVIDENCE_PORTABLE_MAX_STRING_LENGTH) {
526
- throw new TypeError('Lookup evidence contains an over-bound string.');
527
- }
528
- continue;
529
- }
530
- if (current.value === null || typeof current.value === 'boolean')
531
- continue;
532
- if (typeof current.value === 'number') {
533
- if (!Number.isFinite(current.value)) {
534
- throw new TypeError('Lookup evidence contains a non-finite number.');
535
- }
536
- continue;
537
- }
538
- if (Array.isArray(current.value)) {
539
- if (current.value.length > LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS) {
540
- throw new TypeError('Lookup evidence contains an over-bound array.');
541
- }
542
- for (const item of current.value)
543
- pending.push({ value: item, depth: current.depth + 1 });
544
- continue;
545
- }
546
- if (current.value && typeof current.value === 'object') {
547
- const source = current.value;
548
- const prototype = Object.getPrototypeOf(source);
549
- if (prototype !== Object.prototype && prototype !== null) {
550
- throw new TypeError('Lookup evidence contains a non-JSON object.');
551
- }
552
- const keys = Object.keys(source);
553
- if (keys.length > LOOKUP_EVIDENCE_PORTABLE_MAX_ARRAY_ITEMS
554
- || keys.some((key) => key.length > LOOKUP_EVIDENCE_PORTABLE_MAX_KEY_LENGTH
555
- || /[\u0000-\u001f\u007f]/u.test(key)
556
- || !isSafeJsonObjectKey(key))) {
557
- throw new TypeError('Lookup evidence contains an over-bound object.');
558
- }
559
- for (const item of Object.values(source))
560
- pending.push({ value: item, depth: current.depth + 1 });
561
- continue;
562
- }
563
- throw new TypeError('Lookup evidence contains a non-JSON value.');
564
- }
565
- }
566
- export function serializeLookupEvidence(value, pretty = false) {
567
- assertLookupEvidencePortableTree(value);
568
- let output;
569
- try {
570
- output = JSON.stringify(value, null, pretty ? 2 : 0);
571
- }
572
- catch {
573
- throw new TypeError('Lookup evidence could not be serialized as JSON.');
574
- }
575
- if (new TextEncoder().encode(output).byteLength > LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES) {
576
- throw new TypeError('Lookup evidence exceeds the 5 MiB portable file limit.');
577
- }
578
- return output;
579
- }
580
- function boundedString(value, maximum) {
581
- if (typeof value !== 'string' || value.length > maximum || /[\u0000-\u001f\u007f]/.test(value))
582
- return null;
583
- return value.replace(/\s+/g, ' ').trim() || null;
584
- }
585
50
  function boundedInteger(value, maximum) {
586
51
  return Number.isSafeInteger(value) && Number(value) >= 0 && Number(value) <= maximum ? Number(value) : null;
587
52
  }
@@ -1598,6 +1063,9 @@ export function buildLookupEvidence(response, options = {}) {
1598
1063
  });
1599
1064
  const query = projectLookupEvidenceQuery(body);
1600
1065
  const availabilityAnalysis = projectLookupEvidenceAvailability(body.availability);
1066
+ if (!validLookupObservationScope(recordOrNull(body.availability) ?? {}, query)) {
1067
+ throw new TypeError('Lookup evidence observation hostname must match its submitted registration context.');
1068
+ }
1601
1069
  const idn = projectedKnownRecord(idnAnalysis, LOOKUP_IDN_ANALYSIS_KEYS);
1602
1070
  const evidence = {
1603
1071
  schema: LOOKUP_EVIDENCE_SCHEMA,
@@ -331,7 +331,7 @@ function buildLookupEvidenceReport(document, options = {}) {
331
331
  : [];
332
332
  return {
333
333
  title: cleanReportText(titleTarget),
334
- notice: 'Human-readable summary. The versioned JSON export remains the full-fidelity evidence package; raw RDAP JSON and WHOIS response bodies are deliberately omitted here.',
334
+ notice: 'Human-readable summary of the bounded, privacy-minimised JSON evidence package. Raw RDAP JSON, WHOIS response bodies, contact records, and other excluded collection data are not included in either artefact.',
335
335
  metadata: [
336
336
  reportField('Generated', report.generatedAt),
337
337
  reportField('Generator', application.version ? `${application.name || 'WHOISleuth'} ${application.version}` : application.name || 'WHOISleuth'),
@@ -394,7 +394,9 @@ function buildLookupEvidenceReport(document, options = {}) {
394
394
  reportField('Complete publications', registryPublications.filter((item) => item.state === 'complete').length),
395
395
  reportField('Partial publications', registryPublications.filter((item) => item.state === 'partial').length),
396
396
  reportField('Unavailable publications', registryPublications.filter((item) => item.state === 'unavailable').length),
397
- reportField('Published escalation routes', Array.isArray(registryInsights.abuseRouting) ? registryInsights.abuseRouting.length : 0),
397
+ reportField('Published escalation routes', Array.isArray(registryInsights.abuseRouting)
398
+ ? registryInsights.abuseRouting.length
399
+ : 'Excluded from this privacy-minimised report; publication count unavailable'),
398
400
  reportField('Interpretation limit', contactDisclosure.limitation),
399
401
  ] : [],
400
402
  comparison: {
@@ -432,7 +434,7 @@ function buildLookupEvidenceReport(document, options = {}) {
432
434
  'This report summarises point-in-time observations and registry publications. It does not prove ownership, activity, availability, or maliciousness.',
433
435
  'Missing, skipped, partial, unsupported, or failed sources are inconclusive rather than negative evidence.',
434
436
  'The Risk score is a versioned heuristic review priority, not a probability or determination of maliciousness. Review its listed factors and the underlying source states.',
435
- 'Raw registry payloads and full WHOIS referral responses are available only in the JSON evidence package and may contain public contact data.',
437
+ 'The JSON evidence package is a bounded, privacy-minimised projection. It excludes raw registry and WHOIS payloads, contact records, authentication and session material, and unreviewed fields.',
436
438
  ...(networkContext.contextVersion === 1 ? [
437
439
  'Observed network registration describes one point-in-time public endpoint. It may identify an edge or shared network rather than the origin host and does not prove control, ownership, intent, or maliciousness.',
438
440
  ] : []),