@slicedearth/whoisleuth-cli 2.3.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (418) hide show
  1. package/NOTICE +8 -2
  2. package/README.md +19 -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/argument-values.mjs +55 -0
  7. package/cli/arguments.mjs +24 -408
  8. package/cli/artifact-validation/case-response.mjs +65 -44
  9. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  10. package/cli/artifact-validation/lookup-evidence.mjs +15 -9
  11. package/cli/artifact-validation/signed-review.mjs +136 -68
  12. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  13. package/cli/artifact-verify.mjs +92 -24
  14. package/cli/assurance-arguments.mjs +56 -0
  15. package/cli/assurance-command-definitions.mjs +179 -0
  16. package/cli/assurance-command-runner.mjs +40 -5
  17. package/cli/bagit-folder.mjs +57 -0
  18. package/cli/bagit-review.mjs +23 -0
  19. package/cli/bounded-json.mjs +1 -1
  20. package/cli/bulk-checkpoint.mjs +1 -1
  21. package/cli/bulk-command-runner.mjs +4 -2
  22. package/cli/bulk-output.mjs +29 -3
  23. package/cli/bulk.mjs +2 -3
  24. package/cli/case-command.mjs +212 -0
  25. package/cli/case-pack.mjs +50 -32
  26. package/cli/ci-report.mjs +6 -3
  27. package/cli/collection-arguments.mjs +123 -0
  28. package/cli/collection-command-definitions.mjs +330 -0
  29. package/cli/collection-preflight.mjs +8 -1
  30. package/cli/command-argument-grammar.mjs +8 -206
  31. package/cli/command-definition.mjs +329 -0
  32. package/cli/command-reference.mjs +94 -1357
  33. package/cli/command-seeds.mjs +18 -0
  34. package/cli/compare.mjs +1 -1
  35. package/cli/completion.mjs +298 -79
  36. package/cli/config-profile.mjs +8 -6
  37. package/cli/ct-event-intake.mjs +16 -16
  38. package/cli/discover.mjs +5 -4
  39. package/cli/discovery-observation-snapshot.mjs +46 -18
  40. package/cli/discovery-scan-command-runner.mjs +5 -3
  41. package/cli/discovery-scan.mjs +7 -4
  42. package/cli/discovery-snapshot.mjs +2 -2
  43. package/cli/discovery-workflow.mjs +5 -5
  44. package/cli/domain-control-monitor.mjs +50 -20
  45. package/cli/domain-control-observations.mjs +151 -103
  46. package/cli/errors.mjs +37 -6
  47. package/cli/evidence-arguments.mjs +46 -0
  48. package/cli/evidence-command-definitions.mjs +69 -0
  49. package/cli/evidence-command-runner.mjs +18 -8
  50. package/cli/evidence-signing.mjs +5 -4
  51. package/cli/external-observation-mapping.mjs +9 -10
  52. package/cli/formatters/terminal-command-formats.mjs +6 -0
  53. package/cli/formatters/terminal-lookup.mjs +24 -46
  54. package/cli/formatters/terminal-shared.mjs +5 -3
  55. package/cli/formatters/terminal-technology.mjs +114 -0
  56. package/cli/history-arguments.mjs +39 -0
  57. package/cli/history-command-definitions.mjs +88 -0
  58. package/cli/indicator-set-command.mjs +33 -0
  59. package/cli/inline-command-families.mjs +6 -46
  60. package/cli/inline-command-runner.mjs +4 -31
  61. package/cli/input.mjs +2 -2
  62. package/cli/intake-command.mjs +107 -0
  63. package/cli/interactive-launcher.mjs +2 -160
  64. package/cli/interchange-report.mjs +8 -10
  65. package/cli/investigation-artifacts.mjs +176 -0
  66. package/cli/investigation-checkpoint.mjs +10 -0
  67. package/cli/investigation-folder.mjs +114 -0
  68. package/cli/investigation-manifest.mjs +1 -112
  69. package/cli/investigation-package-review.mjs +103 -0
  70. package/cli/investigation-plan.mjs +38 -147
  71. package/cli/investigation-recipes.mjs +177 -0
  72. package/cli/investigation-run.mjs +256 -35
  73. package/cli/local-document-checkpoint.mjs +170 -0
  74. package/cli/local-mmdb-review.mjs +197 -17
  75. package/cli/lookalike-calibration.mjs +1 -1
  76. package/cli/lookup-brief.mjs +2 -2
  77. package/cli/lookup-browser.mjs +5 -12
  78. package/cli/lookup-command-runner.mjs +33 -39
  79. package/cli/lookup-diff.mjs +47 -12
  80. package/cli/lookup-plan.mjs +16 -14
  81. package/cli/lookup-reconcile.mjs +15 -12
  82. package/cli/lookup-timeline.mjs +7 -6
  83. package/cli/mail-header-review.mjs +24 -247
  84. package/cli/mail-review.mjs +75 -21
  85. package/cli/manual.mjs +1 -0
  86. package/cli/network-arguments.mjs +51 -0
  87. package/cli/network-command-definitions.mjs +130 -0
  88. package/cli/network-command-runner.mjs +45 -31
  89. package/cli/offline-evidence-review.mjs +39 -6
  90. package/cli/output-file.mjs +31 -5
  91. package/cli/posture.mjs +1 -1
  92. package/cli/registry-doctor.mjs +3 -1
  93. package/cli/registry-fixture-scaffold.mjs +1 -1
  94. package/cli/registry-support.mjs +1 -1
  95. package/cli/retained-artifact-diff.mjs +10 -9
  96. package/cli/review-arguments.mjs +131 -0
  97. package/cli/review-command-definitions.mjs +273 -0
  98. package/cli/review-command-runner.mjs +104 -149
  99. package/cli/risk-calibration.mjs +8 -8
  100. package/cli/runner.mjs +74 -37
  101. package/cli/saved-lookup.mjs +2 -2
  102. package/cli/selected-input-worker.mjs +63 -0
  103. package/cli/sharing-review.mjs +8 -80
  104. package/cli/signer-trust.mjs +104 -0
  105. package/cli/source-reliability.mjs +75 -84
  106. package/cli/support-arguments.mjs +49 -0
  107. package/cli/support-command-definitions.mjs +209 -0
  108. package/cli/support-command-runner.mjs +2 -2
  109. package/cli/terminal-input.mjs +168 -0
  110. package/cli/workflow-arguments.mjs +86 -0
  111. package/cli/workflow-command-definitions.mjs +78 -0
  112. package/cli/workflow-command-runner.mjs +75 -13
  113. package/docs/cli-reference.md +103 -30
  114. package/docs/cli.md +510 -25
  115. package/docs/contextual-reviews.md +155 -0
  116. package/lib/abort.mjs +4 -1
  117. package/lib/analyst-taxonomy.mjs +2 -40
  118. package/lib/application-version.mjs +1 -1
  119. package/lib/availability.mjs +27 -754
  120. package/lib/bounded-contract-normalizers.mjs +4 -0
  121. package/lib/bounded-file.mjs +3 -1
  122. package/lib/bounded-html-document.mjs +125 -0
  123. package/lib/bounded-json.mjs +2 -290
  124. package/lib/browser-library-profile.mjs +99 -130
  125. package/lib/browser-library-worker.mjs +112 -0
  126. package/lib/candidate-provenance-bounds.mjs +2 -0
  127. package/lib/classify.mjs +1 -1
  128. package/lib/client-behavior-profile.mjs +3 -3
  129. package/lib/ct-query.mjs +2 -27
  130. package/lib/ct-response-bounds.mjs +2 -9
  131. package/lib/ct-search.mjs +89 -46
  132. package/lib/dmarc-discovery.mjs +62 -0
  133. package/lib/dns-convergence-review.mjs +21 -8
  134. package/lib/dns-delegation-health.mjs +36 -22
  135. package/lib/dns-inheritance-review.mjs +207 -0
  136. package/lib/dns-intelligence.mjs +44 -29
  137. package/lib/dns-mx.mjs +1 -1
  138. package/lib/dns-query-coverage.mjs +40 -0
  139. package/lib/dnssec-evidence-validation.mjs +11 -3
  140. package/lib/domain-assurance.mjs +1 -1
  141. package/lib/domain-change-packet.mjs +18 -14
  142. package/lib/domain-change-review.mjs +40 -16
  143. package/lib/domain-control-flight-recorder.mjs +301 -124
  144. package/lib/domain-control-manifest.mjs +126 -58
  145. package/lib/domain-posture-analysis.mjs +41 -11
  146. package/lib/domain-posture-parsers.mjs +88 -6
  147. package/lib/domain-posture.mjs +120 -32
  148. package/lib/domain-registration.mjs +392 -0
  149. package/lib/evidence-export-privacy.mjs +568 -0
  150. package/lib/evidence-export.mjs +12 -593
  151. package/lib/evidence-report.mjs +7 -2
  152. package/lib/external-intelligence-risk.mjs +2 -124
  153. package/lib/favicon.mjs +117 -41
  154. package/lib/generated/cisa-kev-catalog.mjs +50 -4
  155. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  156. package/lib/generated/retire-browser-catalog.mjs +2742 -1034
  157. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  158. package/lib/html-signals.mjs +87 -85
  159. package/lib/http-evidence-bounds.mjs +2 -7
  160. package/lib/http-intelligence.mjs +52 -27
  161. package/lib/idn-confusable-policy.mjs +6 -4
  162. package/lib/idn-confusables.mjs +1 -1
  163. package/lib/interchange-fidelity-registry.mjs +10 -0
  164. package/lib/lookup-cache.mjs +34 -11
  165. package/lib/lookup-child-profile-contract.mjs +24 -15
  166. package/lib/lookup-contract-primitives.mjs +1 -1
  167. package/lib/lookup-diagnostics.mjs +2 -1
  168. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  169. package/lib/lookup-progress.mjs +79 -13
  170. package/lib/lookup-response-contract.mjs +1077 -0
  171. package/lib/lookup-response.mjs +11 -24
  172. package/lib/lookup-source-progress.mjs +24 -35
  173. package/lib/lookup.mjs +49 -28
  174. package/lib/observed-network-context.mjs +43 -27
  175. package/lib/opportunity-scoring.mjs +2 -107
  176. package/lib/outbound-identity.mjs +1 -1
  177. package/lib/outbound-request-bounds.mjs +4 -1
  178. package/lib/page-fingerprints.mjs +87 -134
  179. package/lib/page-role-profile.mjs +12 -8
  180. package/lib/perceptual-hash-comparison.mjs +2 -34
  181. package/lib/perceptual-hash.mjs +79 -24
  182. package/lib/portable-generator.mjs +2 -29
  183. package/lib/project-metadata.mjs +2 -8
  184. package/lib/rdap-bootstrap.mjs +72 -22
  185. package/lib/rdap-client.mjs +26 -14
  186. package/lib/rdap-normalization.mjs +33 -18
  187. package/lib/rdap-search-workbench.mjs +1 -1
  188. package/lib/rdap-transport.mjs +47 -20
  189. package/lib/rdap-types.mjs +2 -1
  190. package/lib/rdap.mjs +18 -3
  191. package/lib/registrable-domain.mjs +2 -36
  192. package/lib/registrar-standing-contract.mjs +2 -1
  193. package/lib/registrar-standing.mjs +9 -13
  194. package/lib/registry-capabilities.mjs +7 -3
  195. package/lib/registry-capability-catalogue.mjs +29 -20
  196. package/lib/registry-dates.mjs +2 -190
  197. package/lib/registry-insights.mjs +56 -18
  198. package/lib/response-policy.mjs +86 -48
  199. package/lib/risk-calibration-summary.mjs +2 -338
  200. package/lib/risk-scoring.mjs +2 -345
  201. package/lib/rpki-evidence.mjs +8 -5
  202. package/lib/safe-fetch.mjs +54 -47
  203. package/lib/scoring-evidence-quality.mjs +2 -107
  204. package/lib/security-txt.mjs +10 -7
  205. package/lib/semantic-version.mjs +2 -54
  206. package/lib/service-binding-dns.mjs +35 -0
  207. package/lib/smtp-transport-review.mjs +75 -34
  208. package/lib/sslbl-certificates.generated.mjs +141 -138
  209. package/lib/static-html-analysis.mjs +117 -146
  210. package/lib/structured-data-identity.mjs +2 -1
  211. package/lib/technology-markup.mjs +100 -0
  212. package/lib/threat-intelligence-manifests.mjs +11 -139
  213. package/lib/threat-intelligence-runtime.mjs +24 -52
  214. package/lib/threat-intelligence-targets.mjs +6 -34
  215. package/lib/threat-intelligence-types.mjs +2 -36
  216. package/lib/typosquat-generator.mjs +29 -16
  217. package/lib/urlhaus-intelligence.mjs +8 -3
  218. package/lib/urlscan-intelligence.mjs +1 -1
  219. package/lib/website-enrichment.mjs +388 -0
  220. package/lib/website-security-posture.mjs +30 -6
  221. package/lib/website-technology.mjs +111 -71
  222. package/lib/whois-authority.mjs +55 -5
  223. package/lib/whois-chain.mjs +9 -2
  224. package/lib/whois-common-formats.mjs +5 -5
  225. package/lib/whois-contracts.mjs +2 -1
  226. package/lib/whois-dialects.mjs +2 -2
  227. package/lib/whois-normalization.mjs +3 -1
  228. package/lib/whois-parser-support.mjs +8 -1
  229. package/lib/whois-parser.mjs +5 -3
  230. package/lib/whois-transport.mjs +23 -7
  231. package/package.json +14 -14
  232. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  233. package/packages/analysis/bounded-json.mjs +330 -0
  234. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  235. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  236. package/packages/analysis/ct-query.mjs +27 -0
  237. package/packages/analysis/ct-response-bounds.mjs +9 -0
  238. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  239. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  240. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  241. package/packages/analysis/opportunity-scoring.mjs +107 -0
  242. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  243. package/packages/analysis/portable-generator.mjs +29 -0
  244. package/packages/analysis/project-metadata.mjs +8 -0
  245. package/packages/analysis/registrable-domain.mjs +56 -0
  246. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  247. package/packages/analysis/risk-scoring.mjs +345 -0
  248. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  249. package/packages/analysis/semantic-version.mjs +54 -0
  250. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  251. package/packages/cases/case-attachment-model.mjs +83 -0
  252. package/packages/cases/case-evidence-links.mjs +135 -0
  253. package/packages/cases/case-evidence-model.mjs +229 -69
  254. package/packages/cases/case-evidence-quality.mjs +5 -0
  255. package/packages/cases/case-export-input.mjs +55 -0
  256. package/packages/cases/case-incident-context.mjs +52 -0
  257. package/packages/cases/case-markdown.mjs +11 -0
  258. package/packages/cases/case-migration-model.mjs +98 -48
  259. package/packages/cases/case-model.mjs +1 -0
  260. package/packages/cases/case-recheck-model.mjs +149 -0
  261. package/packages/cases/case-record-contracts.mjs +1 -1
  262. package/packages/cases/case-record-core.mjs +4 -3
  263. package/packages/cases/case-record-operations.mjs +123 -133
  264. package/packages/cases/case-record-projection.mjs +30 -20
  265. package/packages/cases/case-record-version-input.mjs +19 -0
  266. package/packages/cases/case-report.mjs +114 -67
  267. package/packages/cases/case-requested-evidence.mjs +146 -0
  268. package/packages/cases/case-response-actions.mjs +756 -0
  269. package/packages/cases/case-response-model.mjs +49 -1129
  270. package/packages/cases/case-response-outcomes.mjs +338 -0
  271. package/packages/cases/case-response-packet-render.mjs +157 -0
  272. package/packages/cases/case-response-packet-types.mjs +1 -0
  273. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  274. package/packages/cases/case-response-packet.mjs +58 -593
  275. package/packages/cases/case-response-records.mjs +93 -0
  276. package/packages/cases/case-response-review-inputs.mjs +382 -0
  277. package/packages/cases/case-response-values.mjs +85 -0
  278. package/packages/cases/case-review-package.mjs +45 -0
  279. package/packages/cases/case-selection.mjs +46 -0
  280. package/packages/cases/case-storage-model.mjs +26 -1
  281. package/packages/cases/case-workflow-metadata.mjs +225 -0
  282. package/packages/cases/case-workflow-migration.mjs +43 -0
  283. package/packages/cases/http-summary.mjs +6 -1
  284. package/packages/cases/response-route-freshness.mjs +16 -0
  285. package/packages/comparison/capture-context.mjs +61 -0
  286. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  287. package/packages/comparison/favicon-similarity.mjs +1 -1
  288. package/packages/comparison/page-similarity.mjs +3 -2
  289. package/packages/comparison/relationship-evidence.mjs +103 -14
  290. package/packages/comparison/relationship-provenance.mjs +99 -0
  291. package/packages/contracts/analyst-interchange.mjs +234 -0
  292. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  293. package/packages/contracts/analyst-review-state.mjs +5 -4
  294. package/packages/contracts/case-portability.mjs +370 -114
  295. package/packages/contracts/case-views-contract.mjs +12 -0
  296. package/packages/contracts/case-views.mjs +26 -0
  297. package/packages/contracts/cli-command-semantics.mjs +3 -0
  298. package/packages/contracts/cli-grammar.mjs +6 -0
  299. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  300. package/packages/contracts/cli-lookup.mjs +0 -1
  301. package/packages/contracts/context-review.mjs +24 -0
  302. package/packages/contracts/document-intake.mjs +11 -0
  303. package/packages/contracts/domain-control-flight-recorder.mjs +98 -44
  304. package/packages/contracts/domain-control-manifest.mjs +128 -74
  305. package/packages/contracts/domain-control-monitor.mjs +59 -45
  306. package/packages/contracts/domain-control-review.mjs +200 -121
  307. package/packages/contracts/domain-name.mjs +21 -0
  308. package/packages/contracts/external-observation-interchange.mjs +2 -1
  309. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  310. package/packages/contracts/har-review.mjs +2 -0
  311. package/packages/contracts/http-url.mjs +2 -0
  312. package/packages/contracts/identity-events.mjs +4 -0
  313. package/packages/contracts/investigation-package-limits.mjs +24 -0
  314. package/packages/contracts/investigation-portability.mjs +56 -27
  315. package/packages/contracts/investigation-run.mjs +18 -0
  316. package/packages/contracts/ip-address.mjs +44 -0
  317. package/packages/contracts/lookup-evidence.mjs +35 -0
  318. package/packages/contracts/mail-authentication.mjs +5 -0
  319. package/packages/contracts/message-intake.mjs +31 -0
  320. package/packages/contracts/offline-comparison.mjs +4 -2
  321. package/packages/contracts/page-fingerprints.mjs +15 -0
  322. package/packages/contracts/passive-mail.mjs +10 -0
  323. package/packages/contracts/risk-calibration.mjs +0 -1
  324. package/packages/contracts/schema-compatibility.mjs +1 -1
  325. package/packages/contracts/schema-lifecycle.mjs +83 -153
  326. package/packages/contracts/selected-file-limits.mjs +6 -0
  327. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  328. package/packages/contracts/web-capture.mjs +10 -0
  329. package/packages/contracts/workspace-portability.mjs +219 -238
  330. package/packages/evidence/artifact-integrity.mjs +8 -2
  331. package/packages/evidence/artifact-structure.mjs +105 -0
  332. package/packages/evidence/collection-quality.mjs +67 -0
  333. package/packages/evidence/decision-fact.mjs +4 -1
  334. package/packages/evidence/dns-query-outcome.mjs +72 -0
  335. package/packages/evidence/domain-control-runtime.mjs +155 -27
  336. package/packages/evidence/domain-posture-context.mjs +79 -0
  337. package/packages/evidence/email-recipient.mjs +18 -0
  338. package/packages/evidence/image-regions.mjs +56 -0
  339. package/packages/evidence/latest-observations.mjs +28 -0
  340. package/packages/evidence/lookup-target.mjs +108 -0
  341. package/packages/evidence/observation.mjs +18 -1
  342. package/packages/evidence/passphrase-encryption.mjs +44 -0
  343. package/packages/evidence/png-pixels.mjs +117 -0
  344. package/packages/evidence/public-address-policy.mjs +83 -0
  345. package/packages/evidence/record-identity.mjs +72 -0
  346. package/packages/evidence/registry-dates.mjs +190 -0
  347. package/packages/evidence/retained-file.mjs +69 -0
  348. package/packages/evidence/review-age.mjs +14 -0
  349. package/packages/interchange/bagit.mjs +342 -0
  350. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  351. package/packages/interchange/crc32.mjs +17 -0
  352. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  353. package/packages/interchange/external-findings-import.mjs +93 -61
  354. package/packages/interchange/managed-indicator-export.mjs +35 -0
  355. package/packages/interchange/managed-indicator-set.mjs +210 -0
  356. package/packages/interchange/misp-indicator-export.mjs +93 -0
  357. package/packages/interchange/sharing-policy.mjs +56 -0
  358. package/packages/interchange/stix-indicator-export.mjs +129 -0
  359. package/packages/interchange/web-capture-import.mjs +389 -0
  360. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  361. package/packages/investigation/bulk-mail-exposure.mjs +9 -14
  362. package/packages/investigation/capture-coverage.mjs +57 -0
  363. package/packages/investigation/connector-provenance-review.mjs +100 -0
  364. package/packages/investigation/context-review.mjs +35 -0
  365. package/packages/investigation/document-intake.mjs +81 -0
  366. package/packages/investigation/docx-intake.mjs +133 -0
  367. package/packages/investigation/domain-history-review.mjs +100 -0
  368. package/packages/investigation/domain-transition-review.mjs +47 -0
  369. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  370. package/packages/investigation/har-intake.mjs +68 -0
  371. package/packages/investigation/identity-event-intake.mjs +131 -0
  372. package/packages/investigation/identity-incident-review.mjs +39 -0
  373. package/packages/investigation/incident-sequence-review.mjs +58 -0
  374. package/packages/investigation/intake-report.mjs +19 -0
  375. package/packages/investigation/investigation-bagit.mjs +14 -0
  376. package/packages/investigation/investigation-capsule.mjs +33 -5
  377. package/packages/investigation/investigation-manifest.mjs +200 -0
  378. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  379. package/packages/investigation/investigation-package.mjs +200 -0
  380. package/packages/investigation/link-intake.mjs +179 -0
  381. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  382. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  383. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  384. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  385. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  386. package/packages/investigation/mail-authentication-review.mjs +222 -0
  387. package/packages/investigation/mail-header-identity.mjs +209 -0
  388. package/packages/investigation/message-intake.mjs +195 -0
  389. package/packages/investigation/page-behaviour.mjs +113 -0
  390. package/packages/investigation/pdf-intake.mjs +155 -0
  391. package/packages/investigation/platform-continuity-review.mjs +60 -0
  392. package/packages/investigation/qr-intake.mjs +56 -0
  393. package/packages/investigation/requested-action-hints.mjs +11 -0
  394. package/packages/investigation/selected-input-review.mjs +17 -0
  395. package/packages/investigation/storefront-review.mjs +70 -0
  396. package/packages/monitoring/analyst-review-state.mjs +38 -90
  397. package/packages/workspace/brand-profile-model.mjs +160 -22
  398. package/packages/workspace/bulk-columns.mjs +17 -0
  399. package/packages/workspace/bulk-review-model.mjs +39 -31
  400. package/packages/workspace/bulk-session-model.mjs +112 -27
  401. package/packages/workspace/bulk-sort.mjs +1 -0
  402. package/packages/workspace/campaign-model.mjs +1 -1
  403. package/packages/workspace/case-views.mjs +102 -0
  404. package/packages/workspace/detection-rule-model.mjs +1 -1
  405. package/packages/workspace/hostile-input.mjs +29 -8
  406. package/packages/workspace/investigation-guide.mjs +2 -2
  407. package/packages/workspace/investigation-template-model.mjs +45 -6
  408. package/packages/workspace/page-baseline.mjs +20 -17
  409. package/packages/workspace/relationship-observation-model.mjs +54 -35
  410. package/packages/workspace/shortlist-model.mjs +14 -5
  411. package/packages/workspace/watchlist-history.mjs +30 -14
  412. package/packages/workspace/watchlist-store.mjs +24 -6
  413. package/packages/workspace/website-snapshot-model.mjs +98 -33
  414. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  415. package/packages/workspace/workspace-archive.mjs +90 -25
  416. package/third-party-notices.txt +577 -17
  417. package/lib/hostname.mjs +0 -38
  418. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -0,0 +1,1077 @@
1
+ // Shared HTTP response boundary for the unified Lookup API.
2
+ //
3
+ // The backend owns the evidence collectors, but an HTTP response is still an
4
+ // untrusted value when it reaches the browser. This module keeps the response
5
+ // envelope typed and validates the small set of fields the Lookup controller
6
+ // relies on before any evidence is derived or rendered. Nested source records
7
+ // stay separately attributed and additive; their own normalizers remain the
8
+ // authority for source-specific fields.
9
+ import { THREAT_INTELLIGENCE_CONTRACT_VERSION, THREAT_INTELLIGENCE_CATEGORIES, THREAT_INTELLIGENCE_CONFIDENCES, THREAT_INTELLIGENCE_ENVELOPE_VERSION, THREAT_INTELLIGENCE_RESULT_STATES, THREAT_INTELLIGENCE_SCHEMA, THREAT_INTELLIGENCE_SEVERITIES, } from "../packages/analysis/threat-intelligence-types.mjs";
10
+ import { MAX_HTTP_ATTEMPTS, MAX_HTTP_ERROR_LENGTH, MAX_HTTP_EVIDENCE_REDIRECTS, } from "../packages/analysis/http-evidence-bounds.mjs";
11
+ import { MAX_OBSERVATION_LIMITATIONS, MAX_OBSERVATION_LIMITATION_LENGTH, normalizeExplicitIsoTimestamp, } from "../packages/evidence/observation.mjs";
12
+ import { assertBoundedJsonStructure } from "../packages/analysis/bounded-json.mjs";
13
+ import { canonicalRegistrableDomain } from "../packages/analysis/registrable-domain.mjs";
14
+ import { validLookupObservationScope } from "../packages/evidence/lookup-target.mjs";
15
+ import { isWebCollectionQuality } from "../packages/evidence/collection-quality.mjs";
16
+ import { MAX_LOOKUP_DNS_RECORDS_PER_TYPE, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS, MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, } from "./lookup-network-evidence-bounds.mjs";
17
+ import { MAX_SECURITY_POSTURE_FINDINGS, sanitizeLookupChildProfiles, validSecurityPostureFinding, validTlsChainCertificate, validTlsFinding, } from "./lookup-child-profile-contract.mjs";
18
+ import { validHttpDeliveryMetadata, validPagePublicationMetadata, } from "./homepage-metadata-contract.mjs";
19
+ import { resolveRegistrarIanaId, validRegistrarStanding, } from "./registrar-standing-contract.mjs";
20
+ import { LOOKUP_CONTROL_CHAR_RE as CONTROL_CHAR_RE, isJsonObject, normalizedDomain, validBoundedString, validHttpProvenanceUrl, validObservationDiagnostics, validOptionalNullableText, validStringArray, validUint, } from "./lookup-contract-primitives.mjs";
21
+ const INVALID_LOOKUP_RESPONSE = 'INVALID_LOOKUP_RESPONSE';
22
+ const INVALID_LOOKUP_RESPONSE_MESSAGE = 'Lookup returned an invalid response.';
23
+ const INVALID_COMPACT_LOOKUP_RESPONSE = 'INVALID_COMPACT_LOOKUP_RESPONSE';
24
+ const INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE = 'Bulk lookup returned an invalid response.';
25
+ const MAX_LOOKUP_RESPONSE_QUERY_LENGTH = 4096;
26
+ const MAX_LOOKUP_RESPONSE_HOST_LENGTH = 253;
27
+ const MAX_LOOKUP_RESPONSE_PAGE_TITLE_LENGTH = 200;
28
+ const MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS = 32;
29
+ const MAX_LOOKUP_RESPONSE_ERROR_LENGTH = 240;
30
+ const MAX_LOOKUP_RESPONSE_CONTAINER_ITEMS = 500;
31
+ const MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS = 8;
32
+ const MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS = 128;
33
+ const MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS = 16;
34
+ const MAX_COMPACT_BULK_TECHNOLOGY_IDS = 12;
35
+ const MAX_THREAT_INTELLIGENCE_PROVIDERS = 10;
36
+ const MAX_THREAT_INTELLIGENCE_FINDINGS = 100;
37
+ const MAX_THREAT_INTELLIGENCE_LIMITATIONS = 10;
38
+ const MAX_THREAT_INTELLIGENCE_TEXT_LENGTH = 500;
39
+ const MAX_LOOKUP_TIMING_MS = 120_000;
40
+ const MAX_LOOKUP_TIMING_SOURCES = 10;
41
+ const QUERY_TYPES = new Set(['domain', 'ipv4', 'ipv6', 'asn']);
42
+ const COMPACT_AVAILABILITY_STATES = new Set([
43
+ 'available',
44
+ 'expiring',
45
+ 'for_sale',
46
+ 'registered',
47
+ 'unknown',
48
+ ]);
49
+ const COMPACT_CONFIDENCE_LEVELS = new Set(['high', 'low', 'medium']);
50
+ const COMPACT_AVAILABILITY_DIAGNOSTIC_STATES = new Set(['complete', 'disabled', 'error']);
51
+ const COMPACT_BULK_COMPARISON_STATES = new Set([
52
+ 'error',
53
+ 'not_found',
54
+ 'partial',
55
+ 'success',
56
+ 'unavailable',
57
+ ]);
58
+ const COMPACT_BULK_TECHNOLOGY_ID_RE = /^[a-z0-9][a-z0-9_-]{0,79}$/u;
59
+ const SHA256_RE = /^[a-f0-9]{64}$/u;
60
+ const THREAT_INTELLIGENCE_PROVIDERS = Object.freeze({
61
+ urlscan_search: Object.freeze({ label: 'URLscan archived verdicts', host: 'urlscan.io' }),
62
+ urlhaus_host: Object.freeze({ label: 'URLhaus malware-host records', host: 'urlhaus.abuse.ch' }),
63
+ threatfox_domain_ioc: Object.freeze({ label: 'ThreatFox malware IOCs', host: 'threatfox.abuse.ch' }),
64
+ });
65
+ const THREAT_INTELLIGENCE_STATES = new Set(THREAT_INTELLIGENCE_RESULT_STATES);
66
+ const THREAT_INTELLIGENCE_CATEGORY_SET = new Set(THREAT_INTELLIGENCE_CATEGORIES);
67
+ const THREAT_INTELLIGENCE_SEVERITY_SET = new Set(THREAT_INTELLIGENCE_SEVERITIES);
68
+ const THREAT_INTELLIGENCE_CONFIDENCE_SET = new Set(THREAT_INTELLIGENCE_CONFIDENCES);
69
+ const LOOKUP_TIMING_SOURCES = new Set([
70
+ 'rdap',
71
+ 'whois',
72
+ 'domain_evidence',
73
+ 'reverse_dns',
74
+ 'registrar_rdap',
75
+ 'network_context',
76
+ 'security_txt',
77
+ 'external_intelligence',
78
+ 'malware_host_intelligence',
79
+ 'malware_ioc_intelligence',
80
+ ]);
81
+ const LOOKUP_TIMING_OUTCOMES = new Set(['fulfilled', 'rejected']);
82
+ const EMPTY_RECORD = Object.freeze({});
83
+ function record(value) {
84
+ return isJsonObject(value) ? value : EMPTY_RECORD;
85
+ }
86
+ function boundedThreatText(value, maximum = MAX_THREAT_INTELLIGENCE_TEXT_LENGTH) {
87
+ if (typeof value !== 'string' || CONTROL_CHAR_RE.test(value))
88
+ return null;
89
+ const text = value.slice(0, maximum * 2).trim();
90
+ return text ? text.slice(0, maximum) : null;
91
+ }
92
+ function threatTimestamp(value) {
93
+ const text = boundedThreatText(value, 40);
94
+ return text ? normalizeExplicitIsoTimestamp(text) : null;
95
+ }
96
+ function attributedThreatUrl(value, providerId) {
97
+ const expectedHost = THREAT_INTELLIGENCE_PROVIDERS[providerId]?.host;
98
+ if (!expectedHost || typeof value !== 'string' || value.length > 1_024 || CONTROL_CHAR_RE.test(value))
99
+ return null;
100
+ try {
101
+ const url = new URL(value);
102
+ return url.protocol === 'https:' && !url.username && !url.password && url.hostname === expectedHost
103
+ ? url.href
104
+ : null;
105
+ }
106
+ catch {
107
+ return null;
108
+ }
109
+ }
110
+ function normalizeThreatFinding(value, providerId) {
111
+ const input = record(value);
112
+ const category = boundedThreatText(input.category, 32);
113
+ if (!category || !THREAT_INTELLIGENCE_CATEGORY_SET.has(category))
114
+ return null;
115
+ const referenceUrl = attributedThreatUrl(input.referenceUrl, providerId);
116
+ const tags = Array.isArray(input.tags)
117
+ ? [...new Set(input.tags.slice(0, 40).map((tag) => boundedThreatText(tag, 80)).filter((tag) => tag !== null))].slice(0, 20)
118
+ : [];
119
+ const firstObservedAt = threatTimestamp(input.firstObservedAt);
120
+ const lastObservedAt = threatTimestamp(input.lastObservedAt);
121
+ const severity = boundedThreatText(input.severity, 32);
122
+ const confidence = boundedThreatText(input.confidence, 32);
123
+ if (firstObservedAt && lastObservedAt && Date.parse(firstObservedAt) > Date.parse(lastObservedAt))
124
+ return null;
125
+ return {
126
+ id: boundedThreatText(input.id, 160),
127
+ category,
128
+ severity: severity && THREAT_INTELLIGENCE_SEVERITY_SET.has(severity) ? severity : 'unknown',
129
+ confidence: confidence && THREAT_INTELLIGENCE_CONFIDENCE_SET.has(confidence) ? confidence : 'unknown',
130
+ providerVerdict: boundedThreatText(input.providerVerdict, 120),
131
+ detail: boundedThreatText(input.detail),
132
+ firstObservedAt,
133
+ lastObservedAt,
134
+ referenceUrl,
135
+ tags,
136
+ };
137
+ }
138
+ function normalizeThreatProvider(value, expectedDomain) {
139
+ const input = record(value);
140
+ const identity = record(input.provider);
141
+ const providerId = boundedThreatText(identity.id, 80);
142
+ const providerDefinition = providerId && Object.hasOwn(THREAT_INTELLIGENCE_PROVIDERS, providerId)
143
+ ? THREAT_INTELLIGENCE_PROVIDERS[providerId] : null;
144
+ const withheld = (reason) => ({ provider: null, withheld: {
145
+ providerId: providerDefinition ? providerId : null,
146
+ label: providerDefinition ? `${providerDefinition.label} record` : 'Unrecognised provider record',
147
+ count: 1, reason,
148
+ } });
149
+ if (input.schema !== THREAT_INTELLIGENCE_SCHEMA
150
+ || input.version !== THREAT_INTELLIGENCE_CONTRACT_VERSION)
151
+ return withheld('Unsupported provider format; evidence was withheld.');
152
+ const target = record(input.target);
153
+ if (target.type !== 'domain'
154
+ || target.exposure !== 'registrable_domain'
155
+ || target.value !== expectedDomain)
156
+ return withheld('The provider target did not match this Lookup; evidence was withheld.');
157
+ if (!providerId || !providerDefinition)
158
+ return withheld('The provider identity was not recognised; evidence was withheld.');
159
+ const state = boundedThreatText(input.state, 32);
160
+ if (!state || !THREAT_INTELLIGENCE_STATES.has(state))
161
+ return withheld('The provider result state was not supported; evidence was withheld.');
162
+ const observationInput = record(input.observation);
163
+ const limitations = Array.isArray(observationInput.limitations)
164
+ ? observationInput.limitations
165
+ .slice(0, MAX_THREAT_INTELLIGENCE_LIMITATIONS * 2)
166
+ .map((limitation) => boundedThreatText(limitation))
167
+ .filter((limitation) => limitation !== null)
168
+ .slice(0, MAX_THREAT_INTELLIGENCE_LIMITATIONS)
169
+ : [];
170
+ const inputFindings = Array.isArray(input.findings) ? input.findings : [];
171
+ const normalizedFindings = inputFindings
172
+ .slice(0, MAX_THREAT_INTELLIGENCE_FINDINGS * 2)
173
+ .map((finding) => normalizeThreatFinding(finding, providerId))
174
+ .filter((finding) => finding !== null);
175
+ const findings = normalizedFindings.slice(0, MAX_THREAT_INTELLIGENCE_FINDINGS);
176
+ const omitted = inputFindings.length - findings.length;
177
+ const projectionIncomplete = omitted > 0 || !Array.isArray(input.findings);
178
+ const projectionTruncated = inputFindings.length > MAX_THREAT_INTELLIGENCE_FINDINGS * 2
179
+ || normalizedFindings.length > MAX_THREAT_INTELLIGENCE_FINDINGS;
180
+ if (projectionIncomplete) {
181
+ // Local qualification precedes supplied notices so a full limitation
182
+ // list cannot conceal evidence discarded at this trust boundary.
183
+ limitations.unshift(omitted > 0
184
+ ? `${omitted} invalid or over-limit provider finding${omitted === 1 ? ' was' : 's were'} omitted from this view.`
185
+ : 'Provider findings were not a supported array and were withheld from this view.');
186
+ }
187
+ return { withheld: null, provider: {
188
+ schema: THREAT_INTELLIGENCE_SCHEMA,
189
+ version: THREAT_INTELLIGENCE_CONTRACT_VERSION,
190
+ provider: {
191
+ id: providerId,
192
+ label: providerDefinition.label,
193
+ },
194
+ target: {
195
+ type: 'domain',
196
+ value: expectedDomain,
197
+ exposure: 'registrable_domain',
198
+ },
199
+ state: projectionIncomplete && ['success', 'not_found'].includes(state) ? 'partial' : state,
200
+ detail: boundedThreatText(input.detail),
201
+ findings,
202
+ observation: {
203
+ observedAt: threatTimestamp(observationInput.observedAt),
204
+ limitations: limitations.slice(0, MAX_THREAT_INTELLIGENCE_LIMITATIONS),
205
+ complete: projectionIncomplete ? false : typeof observationInput.complete === 'boolean' ? observationInput.complete : null,
206
+ truncated: projectionTruncated ? true : typeof observationInput.truncated === 'boolean' ? observationInput.truncated : null,
207
+ },
208
+ } };
209
+ }
210
+ function projectThreatProviders(values, expectedDomain) {
211
+ const grouped = new Map();
212
+ const withheld = [];
213
+ const unexamined = Math.max(0, values.length - MAX_THREAT_INTELLIGENCE_PROVIDERS * 2);
214
+ for (const value of values.slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS * 2)) {
215
+ const projection = normalizeThreatProvider(value, expectedDomain);
216
+ if (projection.withheld) {
217
+ withheld.push(projection.withheld);
218
+ continue;
219
+ }
220
+ const provider = projection.provider;
221
+ const providerId = String(record(provider.provider).id || '');
222
+ if (!providerId)
223
+ continue;
224
+ const signature = JSON.stringify(provider);
225
+ const existing = grouped.get(providerId);
226
+ if (existing) {
227
+ existing.signatures.add(signature);
228
+ }
229
+ else {
230
+ grouped.set(providerId, { first: provider, signatures: new Set([signature]) });
231
+ }
232
+ }
233
+ if (unexamined)
234
+ withheld.push({ providerId: null, label: 'Additional provider records', count: unexamined,
235
+ reason: 'The provider-record review bound was exceeded; these records were not examined.' });
236
+ const withheldIds = new Set(withheld.map((item) => item.providerId).filter(Boolean));
237
+ const providers = [...grouped.values()].slice(0, MAX_THREAT_INTELLIGENCE_PROVIDERS).map(({ first, signatures }) => (signatures.size === 1 && !withheldIds.has(String(record(first.provider).id)) && unexamined === 0
238
+ ? first
239
+ : {
240
+ ...first,
241
+ state: 'partial',
242
+ detail: 'Conflicting or unvalidated provider records prevent a conclusive provider projection.',
243
+ findings: [],
244
+ observation: {
245
+ observedAt: null,
246
+ limitations: ['Conflicting or unvalidated records were withheld rather than resolved by array order.'],
247
+ complete: false,
248
+ truncated: null,
249
+ },
250
+ }));
251
+ return { providers, withheld };
252
+ }
253
+ function optionalBoundedText(value, maxLength) {
254
+ return value === undefined || (typeof value === 'string'
255
+ && value.length <= maxLength
256
+ && !CONTROL_CHAR_RE.test(value));
257
+ }
258
+ function validCompactBulkComparison(value) {
259
+ if (!isJsonObject(value)
260
+ || Object.keys(value).length > 3
261
+ || value.version !== 1
262
+ || !isJsonObject(value.technology)
263
+ || Object.keys(value.technology).length > 3
264
+ || typeof value.technology.state !== 'string'
265
+ || !COMPACT_BULK_COMPARISON_STATES.has(value.technology.state)
266
+ || !Array.isArray(value.technology.ids)
267
+ || value.technology.ids.length > MAX_COMPACT_BULK_TECHNOLOGY_IDS
268
+ || typeof value.technology.truncated !== 'boolean'
269
+ || !isJsonObject(value.tls)
270
+ || Object.keys(value.tls).length > 3
271
+ || typeof value.tls.state !== 'string'
272
+ || !COMPACT_BULK_COMPARISON_STATES.has(value.tls.state)
273
+ || (value.tls.issuerLabel !== null && !optionalBoundedText(value.tls.issuerLabel, 240))
274
+ || (value.tls.spkiSha256 !== null
275
+ && (typeof value.tls.spkiSha256 !== 'string' || !SHA256_RE.test(value.tls.spkiSha256)))) {
276
+ return false;
277
+ }
278
+ const ids = value.technology.ids;
279
+ return ids.every((id) => typeof id === 'string' && COMPACT_BULK_TECHNOLOGY_ID_RE.test(id))
280
+ && new Set(ids).size === ids.length;
281
+ }
282
+ function invalidLookupResponse() {
283
+ return {
284
+ ok: false,
285
+ errorCode: INVALID_LOOKUP_RESPONSE,
286
+ error: INVALID_LOOKUP_RESPONSE_MESSAGE,
287
+ };
288
+ }
289
+ function invalidCompactLookupResponse() {
290
+ return {
291
+ ok: false,
292
+ errorCode: INVALID_COMPACT_LOOKUP_RESPONSE,
293
+ error: INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE,
294
+ };
295
+ }
296
+ function validOptionalHttpUrl(value) {
297
+ return value === undefined || value === null || validHttpProvenanceUrl(value);
298
+ }
299
+ function validOptionalHttpStatus(value) {
300
+ return value === undefined || value === null
301
+ || Number.isInteger(value) && Number(value) >= 100 && Number(value) <= 599;
302
+ }
303
+ function validObservationLimitations(value) {
304
+ return value === undefined || Array.isArray(value)
305
+ && value.length <= MAX_OBSERVATION_LIMITATIONS
306
+ && value.every((item) => typeof item === 'string'
307
+ && Boolean(item)
308
+ && item.length <= MAX_OBSERVATION_LIMITATION_LENGTH
309
+ && !CONTROL_CHAR_RE.test(item));
310
+ }
311
+ function validObservationFields(value) {
312
+ if (!isJsonObject(value))
313
+ return false;
314
+ return optionalBoundedText(value.status, 40)
315
+ && optionalBoundedText(value.source, 40)
316
+ && optionalBoundedText(value.observedAt, 64)
317
+ && (value.scanMode === undefined || value.scanMode === null
318
+ || typeof value.scanMode === 'string' && ['fast', 'deep'].includes(value.scanMode))
319
+ && (value.durationMs === undefined || value.durationMs === null
320
+ || Number.isFinite(value.durationMs) && Number(value.durationMs) >= 0
321
+ && Number(value.durationMs) <= 120_000)
322
+ && (value.complete === undefined || typeof value.complete === 'boolean')
323
+ && (value.truncated === undefined || typeof value.truncated === 'boolean')
324
+ && validObservationLimitations(value.limitations)
325
+ && validObservationDiagnostics(value.diagnostics);
326
+ }
327
+ function validSourceStatus(value) {
328
+ if (!isJsonObject(value))
329
+ return false;
330
+ return optionalBoundedText(value.status, 40)
331
+ && (value.error === undefined || value.error === null || optionalBoundedText(value.error, 240))
332
+ && (value.endpoint === undefined || value.endpoint === null || optionalBoundedText(value.endpoint, 2_048))
333
+ && (value.detail === undefined || value.detail === null || optionalBoundedText(value.detail, 500))
334
+ && (value.fetchedAt === undefined || value.fetchedAt === null || optionalBoundedText(value.fetchedAt, 64))
335
+ && (value.queriedAt === undefined || value.queriedAt === null || optionalBoundedText(value.queriedAt, 64))
336
+ && (value.attempts === undefined || Array.isArray(value.attempts)
337
+ && value.attempts.length <= 3
338
+ && value.attempts.every((item) => isJsonObject(item)
339
+ && optionalBoundedText(item.outcome, 40)));
340
+ }
341
+ function validNormalizedHttpEvidence(value) {
342
+ if (!validObservationFields(value)
343
+ || !validOptionalHttpUrl(value.requestUrl)
344
+ || !validOptionalHttpUrl(value.finalUrl)
345
+ || !validObservationLimitations(value.limitations))
346
+ return false;
347
+ const redirects = value.redirects;
348
+ if (redirects !== undefined && (!Array.isArray(redirects)
349
+ || redirects.length > MAX_HTTP_EVIDENCE_REDIRECTS
350
+ || !redirects.every((item) => isJsonObject(item)
351
+ && validHttpProvenanceUrl(item.from)
352
+ && validHttpProvenanceUrl(item.to)
353
+ && validOptionalHttpStatus(item.status)
354
+ && (item.queryOmitted === undefined || typeof item.queryOmitted === 'boolean'))))
355
+ return false;
356
+ const attempts = value.attempts;
357
+ if (attempts !== undefined && (!Array.isArray(attempts)
358
+ || attempts.length > MAX_HTTP_ATTEMPTS
359
+ || !attempts.every((item) => isJsonObject(item)
360
+ && validOptionalHttpUrl(item.url)
361
+ && validOptionalHttpStatus(item.httpStatus)
362
+ && (item.queryOmitted === undefined || typeof item.queryOmitted === 'boolean')
363
+ && (item.outcome === undefined || typeof item.outcome === 'string'
364
+ && ['error', 'response', 'unknown'].includes(item.outcome))
365
+ && (item.error === undefined || item.error === null || typeof item.error === 'string'
366
+ && item.error.length <= MAX_HTTP_ERROR_LENGTH && !CONTROL_CHAR_RE.test(item.error)))))
367
+ return false;
368
+ if (value.redirectCount !== undefined
369
+ && (!Number.isInteger(value.redirectCount)
370
+ || Number(value.redirectCount) < 0
371
+ || Number(value.redirectCount) > MAX_HTTP_EVIDENCE_REDIRECTS))
372
+ return false;
373
+ if (Array.isArray(redirects) && value.redirectCount !== undefined
374
+ && value.redirectCount !== redirects.length)
375
+ return false;
376
+ if (value.response !== undefined && value.response !== null) {
377
+ if (!isJsonObject(value.response)
378
+ || !validOptionalHttpStatus(value.response.status)
379
+ || value.response.deliveryMetadata !== undefined
380
+ && (!['success', 'partial'].includes(String(value.status))
381
+ || !validHttpDeliveryMetadata(value.response.deliveryMetadata)))
382
+ return false;
383
+ }
384
+ return true;
385
+ }
386
+ function optionalStringArrayWithin(value, maximum) {
387
+ return value === undefined || Array.isArray(value)
388
+ && value.length <= maximum
389
+ && value.every((item) => typeof item === 'string');
390
+ }
391
+ function optionalRecordArrayWithin(value, maximum) {
392
+ return value === undefined || Array.isArray(value)
393
+ && value.length <= maximum
394
+ && value.every(isJsonObject);
395
+ }
396
+ function validDnsMxRecord(value) {
397
+ return isJsonObject(value)
398
+ && validUint(value.priority, 0xffff)
399
+ && validBoundedString(value.exchange, 253, true);
400
+ }
401
+ function validDnsCaaRecord(value) {
402
+ return isJsonObject(value)
403
+ && validUint(value.critical, 0xff)
404
+ && typeof value.tag === 'string'
405
+ && /^[a-z0-9-]{1,15}$/u.test(value.tag)
406
+ && validBoundedString(value.value, 1_024);
407
+ }
408
+ function validDnsSoaRecord(value) {
409
+ return isJsonObject(value)
410
+ && validBoundedString(value.nsname, 253)
411
+ && validBoundedString(value.hostmaster, 253)
412
+ && ['serial', 'refresh', 'retry', 'expire', 'minttl']
413
+ .every((key) => validUint(value[key], 0xffff_ffff));
414
+ }
415
+ function validUintArray(value, maximumItems) {
416
+ return Array.isArray(value)
417
+ && value.length <= maximumItems
418
+ && value.every((item) => validUint(item, 0xffff));
419
+ }
420
+ function validDnsHttpsRecord(value) {
421
+ if (!isJsonObject(value)
422
+ || value.type !== 'HTTPS'
423
+ || !validBoundedString(value.owner, 253)
424
+ || !validUint(value.ttl, 0xffff_ffff)
425
+ || !validUint(value.priority, 0xffff)
426
+ || typeof value.mode !== 'string'
427
+ || !['alias', 'service'].includes(value.mode)
428
+ || !(value.target === null || validBoundedString(value.target, 253))
429
+ || typeof value.targetIsOwner !== 'boolean'
430
+ || typeof value.serviceUnavailable !== 'boolean'
431
+ || typeof value.compatible !== 'boolean'
432
+ || typeof value.parametersIgnored !== 'boolean'
433
+ || !isJsonObject(value.parameters))
434
+ return false;
435
+ const parameters = value.parameters;
436
+ if (Object.keys(parameters).length > 24
437
+ || !validUintArray(parameters.mandatory, 24)
438
+ || !validStringArray(parameters.alpn, 16, 132)
439
+ || typeof parameters.noDefaultAlpn !== 'boolean'
440
+ || !(parameters.port === null || validUint(parameters.port, 0xffff))
441
+ || !validStringArray(parameters.ipv4hint, 8, 64)
442
+ || !validStringArray(parameters.ipv6hint, 8, 64)
443
+ || !validUintArray(parameters.unknownKeys, 24)
444
+ || !validUintArray(parameters.unsupportedMandatoryKeys, 24)
445
+ || !Array.isArray(parameters.opaque)
446
+ || parameters.opaque.length > 24)
447
+ return false;
448
+ return parameters.opaque.every((item) => isJsonObject(item)
449
+ && validUint(item.key, 0xffff)
450
+ && (item.name === null || typeof item.name === 'string' && /^[a-z0-9-]{1,63}$/u.test(item.name))
451
+ && validUint(item.length, 0xffff));
452
+ }
453
+ function validOptionalStringArray(value, maximumItems, maximumLength) {
454
+ return value === undefined || validStringArray(value, maximumItems, maximumLength);
455
+ }
456
+ function validOptionalRecordArray(value, maximumItems, validate) {
457
+ return value === undefined || Array.isArray(value)
458
+ && value.length <= maximumItems
459
+ && value.every(validate);
460
+ }
461
+ function validRdapLink(value) {
462
+ return isJsonObject(value)
463
+ && validBoundedString(value.href, 2_048)
464
+ && validOptionalNullableText(value.rel, 100)
465
+ && validOptionalNullableText(value.type, 160)
466
+ && validOptionalNullableText(value.title, 300);
467
+ }
468
+ function validRdapContact(value) {
469
+ if (!isJsonObject(value))
470
+ return false;
471
+ return validOptionalNullableText(value.handle, 200)
472
+ && validOptionalNullableText(value.name, 300)
473
+ && validOptionalNullableText(value.org, 300)
474
+ && validOptionalNullableText(value.email, 320)
475
+ && validOptionalNullableText(value.phone, 100)
476
+ && validOptionalNullableText(value.address, 1_000)
477
+ && validOptionalStringArray(value.roles, 12, 80)
478
+ && validOptionalStringArray(value.names, 8, 300)
479
+ && validOptionalStringArray(value.organizations, 8, 300)
480
+ && validOptionalStringArray(value.emails, 8, 320)
481
+ && validOptionalStringArray(value.phones, 8, 100)
482
+ && validOptionalStringArray(value.addresses, 8, 1_000)
483
+ && validOptionalRecordArray(value.publicIds, 20, (item) => isJsonObject(item)
484
+ && validBoundedString(item.type, 160)
485
+ && validBoundedString(item.identifier, 300))
486
+ && validOptionalRecordArray(value.links, 10, validRdapLink);
487
+ }
488
+ function validRdapTextBlock(value) {
489
+ return isJsonObject(value)
490
+ && validBoundedString(value.title, 160)
491
+ && validOptionalNullableText(value.type, 160)
492
+ && validStringArray(value.descriptions, 6, 800);
493
+ }
494
+ function validRdapParsed(value) {
495
+ if (!isJsonObject(value))
496
+ return false;
497
+ if (!validOptionalStringArray(value.statuses, 100, 160)
498
+ || !validOptionalStringArray(value.nameservers, 200, 253)
499
+ || !validOptionalStringArray(value.conformance, 50, 160)
500
+ || !validOptionalStringArray(value.serverTruncationReasons, 8, 160)
501
+ || !validOptionalStringArray(value.truncatedEntityRoles, 11, 80)
502
+ || !validOptionalRecordArray(value.events, 100, (item) => isJsonObject(item)
503
+ && validOptionalNullableText(item.action, 160)
504
+ && validOptionalNullableText(item.date, 64))
505
+ || !validOptionalRecordArray(value.links, 20, validRdapLink)
506
+ || !validOptionalRecordArray(value.nameserverDetails, 200, (item) => isJsonObject(item)
507
+ && validBoundedString(item.name, 253)
508
+ && validStringArray(item.addresses, 20, 80))
509
+ || !validOptionalRecordArray(value.dsData, 50, (item) => isJsonObject(item)
510
+ && validUint(item.keyTag, 0xffff)
511
+ && validUint(item.algorithm, 0xff)
512
+ && validUint(item.digestType, 0xff)
513
+ && validBoundedString(item.digest, 512))
514
+ || !validOptionalRecordArray(value.notices, 12, validRdapTextBlock)
515
+ || !validOptionalRecordArray(value.remarks, 12, validRdapTextBlock)
516
+ || !validOptionalRecordArray(value.redactions, 100, (item) => isJsonObject(item)
517
+ && ['name', 'method', 'reason'].every((key) => validOptionalNullableText(item[key], 500))
518
+ && ['prePath', 'postPath', 'replacementPath']
519
+ .every((key) => validOptionalNullableText(item[key], 512)))
520
+ || !validOptionalRecordArray(value.variants, 20, (group) => isJsonObject(group)
521
+ && validOptionalNullableText(group.idnTable, 300)
522
+ && validOptionalStringArray(group.relation, 20, 100)
523
+ && validOptionalRecordArray(group.variantNames, 50, (name) => isJsonObject(name)
524
+ && validOptionalNullableText(name.ldhName, 253)
525
+ && validOptionalNullableText(name.unicodeName, 253))))
526
+ return false;
527
+ const entitiesByRole = value.entitiesByRole;
528
+ if (entitiesByRole !== undefined) {
529
+ if (!isJsonObject(entitiesByRole) || Object.keys(entitiesByRole).length > 11)
530
+ return false;
531
+ for (const [role, contacts] of Object.entries(entitiesByRole)) {
532
+ if (!['registrar', 'registrant', 'administrative', 'technical', 'billing', 'abuse', 'noc', 'reseller', 'sponsor', 'proxy', 'notifications'].includes(role)
533
+ || !Array.isArray(contacts)
534
+ || contacts.length > 5
535
+ || !contacts.every(validRdapContact))
536
+ return false;
537
+ }
538
+ }
539
+ return true;
540
+ }
541
+ function validWhoisParsed(value) {
542
+ if (!isJsonObject(value)
543
+ || !validOptionalStringArray(value.statuses, 100, 160)
544
+ || !validOptionalStringArray(value.nameservers, 200, 253)
545
+ || !validOptionalStringArray(value.fieldsTruncated, 64, 80))
546
+ return false;
547
+ const contactsByRole = value.contactsByRole;
548
+ if (contactsByRole !== undefined) {
549
+ if (!isJsonObject(contactsByRole) || Object.keys(contactsByRole).length > 5)
550
+ return false;
551
+ for (const contacts of Object.values(contactsByRole)) {
552
+ if (!Array.isArray(contacts) || contacts.length > 1 || !contacts.every(validRdapContact))
553
+ return false;
554
+ }
555
+ }
556
+ return true;
557
+ }
558
+ function validRegistrationEvidence(value) {
559
+ const rdap = value.rdap;
560
+ const whois = value.whois;
561
+ const rdapDiagnostics = isJsonObject(value.diagnostics.rdap) ? value.diagnostics.rdap : null;
562
+ const whoisDiagnostics = isJsonObject(value.diagnostics.whois) ? value.diagnostics.whois : null;
563
+ if (!validSourceStatus(rdap)
564
+ || !validSourceStatus(whois)
565
+ || rdapDiagnostics !== null && !validSourceStatus(rdapDiagnostics)
566
+ || whoisDiagnostics !== null && !validSourceStatus(whoisDiagnostics)
567
+ || rdapDiagnostics?.status === 'success' && !isJsonObject(rdap.parsed)
568
+ || whoisDiagnostics?.status === 'complete' && !isJsonObject(whois.parsed))
569
+ return false;
570
+ if (rdap.parsed !== undefined && rdap.parsed !== null && !validRdapParsed(rdap.parsed))
571
+ return false;
572
+ if (whois.parsed !== undefined && whois.parsed !== null && !validWhoisParsed(whois.parsed))
573
+ return false;
574
+ const registrar = rdap.registrarRdap;
575
+ return registrar === undefined || registrar === null || validSourceStatus(registrar)
576
+ && (registrar.parsed === undefined || registrar.parsed === null || validRdapParsed(registrar.parsed));
577
+ }
578
+ function validPageEvidence(value) {
579
+ const availability = value.availability;
580
+ const page = availability.pageIdentity;
581
+ if (page !== undefined && page !== null) {
582
+ if (!validObservationFields(page)
583
+ || page.publicationMetadata !== undefined
584
+ && (!['success', 'partial'].includes(String(page.status))
585
+ || !validPagePublicationMetadata(page.publicationMetadata))
586
+ || !validOptionalStringArray(page.embeddedOrigins, 20, 2_048)
587
+ || !validOptionalStringArray(page.contactDomains, 20, 253)
588
+ || !validOptionalRecordArray(page.trackingIdentifiers, 30, (item) => isJsonObject(item)
589
+ && validBoundedString(item.type, 80)
590
+ && validBoundedString(item.value, 240)))
591
+ return false;
592
+ if (page.forms !== undefined && (!isJsonObject(page.forms)
593
+ || !validOptionalStringArray(page.forms.externalActionOrigins, 10, 2_048)))
594
+ return false;
595
+ if (page.resources !== undefined && (!isJsonObject(page.resources)
596
+ || !validOptionalStringArray(page.resources.externalOrigins, 30, 2_048)))
597
+ return false;
598
+ if (page.downloads !== undefined && (!isJsonObject(page.downloads)
599
+ || !validOptionalStringArray(page.downloads.riskyFileTypes, 20, 80)
600
+ || !validOptionalStringArray(page.downloads.externalOrigins, 20, 2_048)))
601
+ return false;
602
+ }
603
+ const technology = availability.technologyProfile;
604
+ if (technology !== undefined && technology !== null) {
605
+ if (!validObservationFields(technology)
606
+ || !validOptionalRecordArray(technology.findings, 24, (finding) => isJsonObject(finding)
607
+ && validOptionalRecordArray(finding.evidence, 4, (item) => isJsonObject(item)
608
+ && validBoundedString(item.source, 80)
609
+ && validBoundedString(item.description, 180))))
610
+ return false;
611
+ const libraries = technology.browserLibraryProfile;
612
+ if (libraries !== undefined && libraries !== null && (!validObservationFields(libraries)
613
+ || !validOptionalRecordArray(libraries.findings, 16, (item) => isJsonObject(item))))
614
+ return false;
615
+ }
616
+ const structured = availability.structuredDataIdentity;
617
+ if (structured !== undefined && structured !== null && (!validObservationFields(structured)
618
+ || !validOptionalRecordArray(structured.entities, 16, (entity) => isJsonObject(entity)
619
+ && validOptionalStringArray(entity.types, 8, 160)
620
+ && validOptionalStringArray(entity.sameAsHosts, 12, 253))))
621
+ return false;
622
+ const roles = availability.pageRoleProfile;
623
+ if (roles !== undefined && roles !== null && (!validObservationFields(roles)
624
+ || !validOptionalRecordArray(roles.findings, 4, (finding) => isJsonObject(finding)
625
+ && validOptionalStringArray(finding.evidence, 4, 300))))
626
+ return false;
627
+ const behavior = availability.clientBehaviorProfile;
628
+ if (behavior !== undefined && behavior !== null && (!validObservationFields(behavior)
629
+ || !validOptionalRecordArray(behavior.indicators, 12, (item) => isJsonObject(item))))
630
+ return false;
631
+ const credential = availability.credentialSurfaceProfile;
632
+ return credential === undefined || credential === null || validObservationFields(credential);
633
+ }
634
+ function validAvailabilityScalars(value) {
635
+ return (value.applicable === undefined || typeof value.applicable === 'boolean')
636
+ && validOptionalNullableText(value.domain, MAX_LOOKUP_RESPONSE_HOST_LENGTH)
637
+ && validOptionalNullableText(value.state, 40)
638
+ && validOptionalNullableText(value.confidence, 40)
639
+ && validOptionalNullableText(value.activityStatus, 40)
640
+ && validOptionalNullableText(value.websiteProbeDetail, 500)
641
+ && validOptionalNullableText(value.pageTitle, MAX_LOOKUP_RESPONSE_PAGE_TITLE_LENGTH)
642
+ && validOptionalNullableText(value.dnssec, 40)
643
+ && (value.deepScanComplete === undefined || typeof value.deepScanComplete === 'boolean')
644
+ && (value.webCollectionQuality === undefined || isWebCollectionQuality(value.webCollectionQuality))
645
+ && (value.hasMx === undefined || value.hasMx === null || typeof value.hasMx === 'boolean')
646
+ && (value.hasNullMx === undefined || value.hasNullMx === null || typeof value.hasNullMx === 'boolean')
647
+ && (value.hasSpf === undefined || value.hasSpf === null || typeof value.hasSpf === 'boolean')
648
+ && (value.hasDmarc === undefined || value.hasDmarc === null || typeof value.hasDmarc === 'boolean')
649
+ && validOptionalStringArray(value.mxHosts, MAX_LOOKUP_DNS_RECORDS_PER_TYPE, 253);
650
+ }
651
+ function validLookupDomainIdentity(value) {
652
+ if (value.type !== 'domain')
653
+ return true;
654
+ const availability = record(value.availability);
655
+ const queryDomain = canonicalRegistrableDomain(value.query);
656
+ if (!queryDomain)
657
+ return false;
658
+ if (availability.domain !== undefined && availability.domain !== null
659
+ && (canonicalRegistrableDomain(availability.domain) !== queryDomain
660
+ || normalizedDomain(availability.domain) !== queryDomain))
661
+ return false;
662
+ if (!validLookupObservationScope(availability, {
663
+ inputHostname: normalizedDomain(value.inputHostname ?? value.query), registrableDomain: queryDomain,
664
+ }))
665
+ return false;
666
+ if (value.inputHostname !== undefined
667
+ && canonicalRegistrableDomain(value.inputHostname) !== queryDomain)
668
+ return false;
669
+ if (value.registrableDomain !== undefined
670
+ && normalizedDomain(value.registrableDomain) !== queryDomain)
671
+ return false;
672
+ if (value.isSubdomain !== undefined) {
673
+ const inputHostname = normalizedDomain(value.inputHostname ?? value.query);
674
+ if (!inputHostname || value.isSubdomain !== (inputHostname !== queryDomain))
675
+ return false;
676
+ }
677
+ return true;
678
+ }
679
+ function optionalTlsNameWithin(value) {
680
+ if (value === undefined || value === null)
681
+ return true;
682
+ if (!isJsonObject(value))
683
+ return false;
684
+ return optionalStringArrayWithin(value.commonNames, MAX_LOOKUP_TLS_NAME_VALUES)
685
+ && (!Array.isArray(value.commonNames)
686
+ || value.commonNames.every((item) => validBoundedString(item, 256)))
687
+ && optionalStringArrayWithin(value.organizations, MAX_LOOKUP_TLS_NAME_VALUES)
688
+ && (!Array.isArray(value.organizations)
689
+ || value.organizations.every((item) => validBoundedString(item, 256)));
690
+ }
691
+ function validNormalizedNetworkEvidence(value) {
692
+ const availability = value.availability;
693
+ const dns = availability.dns;
694
+ if (dns !== undefined) {
695
+ if (!validObservationFields(dns))
696
+ return false;
697
+ if (dns.records !== undefined) {
698
+ if (!isJsonObject(dns.records))
699
+ return false;
700
+ for (const [key, maximumLength] of [
701
+ ['a', 64], ['aaaa', 64], ['cname', 253], ['ns', 253], ['spf', 1_024], ['dmarc', 1_024],
702
+ ]) {
703
+ if (!optionalStringArrayWithin(dns.records[key], MAX_LOOKUP_DNS_RECORDS_PER_TYPE)
704
+ || Array.isArray(dns.records[key])
705
+ && !dns.records[key].every((item) => validBoundedString(item, maximumLength)))
706
+ return false;
707
+ }
708
+ if (dns.records.mx !== undefined && (!Array.isArray(dns.records.mx)
709
+ || dns.records.mx.length > MAX_LOOKUP_DNS_RECORDS_PER_TYPE
710
+ || !dns.records.mx.every(validDnsMxRecord)))
711
+ return false;
712
+ if (dns.records.caa !== undefined && (!Array.isArray(dns.records.caa)
713
+ || dns.records.caa.length > MAX_LOOKUP_DNS_RECORDS_PER_TYPE
714
+ || !dns.records.caa.every(validDnsCaaRecord)))
715
+ return false;
716
+ if (dns.records.soa !== undefined && (!Array.isArray(dns.records.soa)
717
+ || dns.records.soa.length > 1
718
+ || !dns.records.soa.every(validDnsSoaRecord)))
719
+ return false;
720
+ if (dns.records.https !== undefined && (!Array.isArray(dns.records.https)
721
+ || dns.records.https.length > MAX_LOOKUP_DNS_RECORDS_PER_TYPE
722
+ || !dns.records.https.every(validDnsHttpsRecord)))
723
+ return false;
724
+ }
725
+ }
726
+ const reverseDns = value.reverseDns;
727
+ if (reverseDns !== undefined) {
728
+ if (!validObservationFields(reverseDns)
729
+ || reverseDns.records !== undefined
730
+ && (!isJsonObject(reverseDns.records)
731
+ || !optionalStringArrayWithin(reverseDns.records.ptr, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS)
732
+ || Array.isArray(reverseDns.records.ptr)
733
+ && !reverseDns.records.ptr.every((item) => validBoundedString(item, 253))))
734
+ return false;
735
+ }
736
+ const tls = availability.tls;
737
+ if (tls !== undefined) {
738
+ if (!validObservationFields(tls)
739
+ || !optionalRecordArrayWithin(tls.findings, MAX_LOOKUP_TLS_FINDINGS)
740
+ || Array.isArray(tls.findings) && !tls.findings.every(validTlsFinding)
741
+ || !optionalRecordArrayWithin(tls.chain, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES)
742
+ || Array.isArray(tls.chain) && !tls.chain.every(validTlsChainCertificate))
743
+ return false;
744
+ const certificate = tls.certificate;
745
+ if (certificate !== undefined && certificate !== null) {
746
+ if (!isJsonObject(certificate)
747
+ || !optionalTlsNameWithin(certificate.subject)
748
+ || !optionalTlsNameWithin(certificate.issuer))
749
+ return false;
750
+ const altNames = certificate.subjectAltNames;
751
+ if (altNames !== undefined && altNames !== null) {
752
+ if (!isJsonObject(altNames)
753
+ || !optionalStringArrayWithin(altNames.dnsNames, MAX_LOOKUP_TLS_ALT_NAMES)
754
+ || Array.isArray(altNames.dnsNames)
755
+ && !altNames.dnsNames.every((item) => validBoundedString(item, 253))
756
+ || !optionalStringArrayWithin(altNames.ipAddresses, MAX_LOOKUP_TLS_ALT_NAMES)
757
+ || Array.isArray(altNames.ipAddresses)
758
+ && !altNames.ipAddresses.every((item) => validBoundedString(item, 64)))
759
+ return false;
760
+ const totalAltNames = (Array.isArray(altNames.dnsNames) ? altNames.dnsNames.length : 0)
761
+ + (Array.isArray(altNames.ipAddresses) ? altNames.ipAddresses.length : 0);
762
+ if (totalAltNames > MAX_LOOKUP_TLS_ALT_NAMES)
763
+ return false;
764
+ }
765
+ const extensionProfile = certificate.extensionProfile;
766
+ if (extensionProfile !== undefined && extensionProfile !== null) {
767
+ if (!isJsonObject(extensionProfile))
768
+ return false;
769
+ const certificatePolicies = extensionProfile.certificatePolicies;
770
+ if (certificatePolicies !== undefined && certificatePolicies !== null
771
+ && (!isJsonObject(certificatePolicies)
772
+ || !optionalStringArrayWithin(certificatePolicies.oids, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES)
773
+ || Array.isArray(certificatePolicies.oids)
774
+ && !certificatePolicies.oids.every((item) => validBoundedString(item, 128))))
775
+ return false;
776
+ }
777
+ }
778
+ for (const child of [tls.authorization, tls.hostname]) {
779
+ if (child !== undefined && child !== null && (!isJsonObject(child)
780
+ || !validOptionalNullableText(child.error, 240)))
781
+ return false;
782
+ }
783
+ }
784
+ const securityPosture = availability.securityPosture;
785
+ if (securityPosture !== undefined && securityPosture !== null) {
786
+ if (!validObservationFields(securityPosture)
787
+ || !optionalRecordArrayWithin(securityPosture.findings, MAX_SECURITY_POSTURE_FINDINGS)
788
+ || Array.isArray(securityPosture.findings)
789
+ && !securityPosture.findings.every(validSecurityPostureFinding))
790
+ return false;
791
+ }
792
+ return true;
793
+ }
794
+ function parseLookupHttpResponse(value) {
795
+ try {
796
+ assertBoundedJsonStructure(value, 'Lookup response', {
797
+ maximumContainerItems: MAX_LOOKUP_RESPONSE_CONTAINER_ITEMS,
798
+ });
799
+ }
800
+ catch {
801
+ return invalidLookupResponse();
802
+ }
803
+ if (!isJsonObject(value) || Object.keys(value).length > MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS) {
804
+ return invalidLookupResponse();
805
+ }
806
+ if (typeof value.query !== 'string'
807
+ || !value.query.trim()
808
+ || value.query.length > MAX_LOOKUP_RESPONSE_QUERY_LENGTH
809
+ || CONTROL_CHAR_RE.test(value.query)
810
+ || typeof value.type !== 'string'
811
+ || !QUERY_TYPES.has(value.type)
812
+ || !isJsonObject(value.rdap)
813
+ || !isJsonObject(value.whois)
814
+ || !isJsonObject(value.availability)
815
+ || !isJsonObject(value.diagnostics)
816
+ || !optionalBoundedText(value.inputHostname, MAX_LOOKUP_RESPONSE_HOST_LENGTH)
817
+ || !optionalBoundedText(value.registrableDomain, MAX_LOOKUP_RESPONSE_HOST_LENGTH)
818
+ || (value.isSubdomain !== undefined && typeof value.isSubdomain !== 'boolean')) {
819
+ return invalidLookupResponse();
820
+ }
821
+ for (const key of ['reverseDns', 'networkContext', 'securityTxt', 'sslbl', 'threatIntelligence', 'registryInsights', 'registrarStanding']) {
822
+ const section = value[key];
823
+ if (section !== undefined && !isJsonObject(section))
824
+ return invalidLookupResponse();
825
+ }
826
+ const lookupResponse = sanitizeLookupChildProfiles(value);
827
+ if (lookupResponse.registrarStanding !== undefined
828
+ && (!validRegistrarStanding(lookupResponse.registrarStanding)
829
+ || lookupResponse.registrarStanding.ianaId !== resolveRegistrarIanaId(lookupResponse.rdap.parsed, lookupResponse.whois.parsed)))
830
+ return invalidLookupResponse();
831
+ if (!validAvailabilityScalars(lookupResponse.availability)
832
+ || !validLookupDomainIdentity(lookupResponse)
833
+ || !validRegistrationEvidence(lookupResponse))
834
+ return invalidLookupResponse();
835
+ if (lookupResponse.availability.http !== undefined && !validNormalizedHttpEvidence(lookupResponse.availability.http)) {
836
+ return invalidLookupResponse();
837
+ }
838
+ for (const section of [value.reverseDns, value.networkContext, value.securityTxt]) {
839
+ if (section !== undefined && !validObservationFields(section))
840
+ return invalidLookupResponse();
841
+ }
842
+ if (!validNormalizedNetworkEvidence(lookupResponse)
843
+ || !validPageEvidence(lookupResponse))
844
+ return invalidLookupResponse();
845
+ return { ok: true, value: lookupResponse };
846
+ }
847
+ function parseCompactLookupHttpResponse(value, expectedDomain) {
848
+ try {
849
+ assertBoundedJsonStructure(value, 'Compact Lookup response');
850
+ }
851
+ catch {
852
+ return invalidCompactLookupResponse();
853
+ }
854
+ if (!isJsonObject(value) || Object.keys(value).length > MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS) {
855
+ return invalidCompactLookupResponse();
856
+ }
857
+ const availability = value.availability;
858
+ const diagnostics = value.diagnostics;
859
+ const expectedHostname = normalizedDomain(expectedDomain);
860
+ const expectedRegistrableDomain = canonicalRegistrableDomain(expectedDomain);
861
+ const compactPageIdentity = isJsonObject(availability) && isJsonObject(availability.pageIdentity)
862
+ ? availability.pageIdentity
863
+ : null;
864
+ const compactHttpResponse = isJsonObject(availability) && isJsonObject(availability.http)
865
+ && isJsonObject(availability.http.response)
866
+ ? availability.http.response
867
+ : null;
868
+ if (!isJsonObject(availability)
869
+ || !expectedHostname
870
+ || !expectedRegistrableDomain
871
+ || Object.keys(availability).length > MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS
872
+ || availability.observationHostname !== undefined
873
+ || availability.webObservationMode !== undefined
874
+ || availability.applicable !== true
875
+ || normalizedDomain(availability.domain) !== expectedRegistrableDomain
876
+ || typeof availability.state !== 'string'
877
+ || !COMPACT_AVAILABILITY_STATES.has(availability.state)
878
+ || typeof availability.confidence !== 'string'
879
+ || !COMPACT_CONFIDENCE_LEVELS.has(availability.confidence)
880
+ || (availability.deepScanComplete !== undefined && typeof availability.deepScanComplete !== 'boolean')
881
+ || (availability.webCollectionQuality !== undefined && !isWebCollectionQuality(availability.webCollectionQuality))
882
+ || (availability.bulkComparison !== undefined
883
+ && !validCompactBulkComparison(availability.bulkComparison))
884
+ || compactPageIdentity !== null && Object.hasOwn(compactPageIdentity, 'publicationMetadata')
885
+ || compactHttpResponse !== null && Object.hasOwn(compactHttpResponse, 'deliveryMetadata')
886
+ || (value.query !== undefined && normalizedDomain(value.query) !== expectedHostname)
887
+ || (value.type !== undefined && value.type !== 'domain')
888
+ || (value.inputHostname !== undefined && normalizedDomain(value.inputHostname) !== expectedHostname)
889
+ || (value.registrableDomain !== undefined
890
+ && normalizedDomain(value.registrableDomain) !== expectedRegistrableDomain)
891
+ || (value.isSubdomain !== undefined
892
+ && (typeof value.isSubdomain !== 'boolean'
893
+ || value.isSubdomain !== (expectedHostname !== expectedRegistrableDomain)))
894
+ || !isJsonObject(diagnostics)
895
+ || Object.keys(diagnostics).length > MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS
896
+ || diagnostics.version !== 7
897
+ || !isJsonObject(diagnostics.rdap)
898
+ || !isJsonObject(diagnostics.whois)
899
+ || !isJsonObject(diagnostics.availability)
900
+ || typeof diagnostics.availability.status !== 'string'
901
+ || !COMPACT_AVAILABILITY_DIAGNOSTIC_STATES.has(diagnostics.availability.status)) {
902
+ return invalidCompactLookupResponse();
903
+ }
904
+ return { ok: true, value: value };
905
+ }
906
+ function lookupHttpErrorMessage(value, status) {
907
+ const source = record(value);
908
+ const message = typeof source.error === 'string'
909
+ ? source.error
910
+ .replace(/[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/gu, ' ')
911
+ .replace(/\s+/gu, ' ')
912
+ .trim()
913
+ .slice(0, MAX_LOOKUP_RESPONSE_ERROR_LENGTH)
914
+ : '';
915
+ return message || `Lookup failed (${status})`;
916
+ }
917
+ function isBoundedTimingMs(value, totalMs) {
918
+ return Number.isInteger(value)
919
+ && Number(value) >= 0
920
+ && Number(value) <= totalMs
921
+ && Number(value) <= MAX_LOOKUP_TIMING_MS;
922
+ }
923
+ function normalizeLookupTiming(value) {
924
+ if (!isJsonObject(value)
925
+ || value.version !== 1
926
+ || !Number.isInteger(value.totalMs)
927
+ || Number(value.totalMs) < 0
928
+ || Number(value.totalMs) > MAX_LOOKUP_TIMING_MS
929
+ || !Array.isArray(value.sources)
930
+ || value.sources.length > MAX_LOOKUP_TIMING_SOURCES) {
931
+ return null;
932
+ }
933
+ const totalMs = Number(value.totalMs);
934
+ const seen = new Set();
935
+ const sources = [];
936
+ for (const candidate of value.sources) {
937
+ if (!isJsonObject(candidate)
938
+ || typeof candidate.source !== 'string'
939
+ || !LOOKUP_TIMING_SOURCES.has(candidate.source)
940
+ || seen.has(candidate.source)
941
+ || typeof candidate.outcome !== 'string'
942
+ || !LOOKUP_TIMING_OUTCOMES.has(candidate.outcome)
943
+ || !isBoundedTimingMs(candidate.durationMs, totalMs)
944
+ || !isBoundedTimingMs(candidate.completedAfterMs, totalMs)
945
+ || Number(candidate.durationMs) > Number(candidate.completedAfterMs)) {
946
+ return null;
947
+ }
948
+ const source = candidate.source;
949
+ seen.add(source);
950
+ sources.push({
951
+ source,
952
+ outcome: candidate.outcome,
953
+ durationMs: Number(candidate.durationMs),
954
+ completedAfterMs: Number(candidate.completedAfterMs),
955
+ });
956
+ }
957
+ return {
958
+ version: 1,
959
+ totalMs,
960
+ sources,
961
+ };
962
+ }
963
+ function createLookupHttpResponse(query, classified, result) {
964
+ return {
965
+ ...result,
966
+ query,
967
+ type: classified.type,
968
+ inputHostname: classified.inputHostname,
969
+ registrableDomain: classified.registrableDomain,
970
+ isSubdomain: classified.isSubdomain,
971
+ };
972
+ }
973
+ function createLookupViewModel(response) {
974
+ const availability = record(response?.availability);
975
+ const rdap = record(response?.rdap);
976
+ const registrarRdap = record(rdap.registrarRdap);
977
+ const whois = record(response?.whois);
978
+ const diagnostics = record(response?.diagnostics);
979
+ const reverseDns = record(response?.reverseDns);
980
+ const observedNetworkContext = record(response?.networkContext);
981
+ const registryInsights = record(response?.registryInsights);
982
+ const registrarStanding = record(response?.registrarStanding);
983
+ const securityTxt = record(response?.securityTxt);
984
+ const sslbl = record(response?.sslbl);
985
+ const rawThreatIntelligence = record(response?.threatIntelligence);
986
+ const expectedThreatDomain = response?.type === 'domain'
987
+ ? canonicalRegistrableDomain(response.registrableDomain)
988
+ ?? canonicalRegistrableDomain(availability.domain)
989
+ : null;
990
+ const threatProjection = expectedThreatDomain
991
+ && rawThreatIntelligence.version === THREAT_INTELLIGENCE_ENVELOPE_VERSION
992
+ && Array.isArray(rawThreatIntelligence.providers)
993
+ ? projectThreatProviders(rawThreatIntelligence.providers, expectedThreatDomain)
994
+ : { providers: [], withheld: Object.keys(rawThreatIntelligence).length ? [{
995
+ providerId: null,
996
+ label: 'External-intelligence data',
997
+ count: Array.isArray(rawThreatIntelligence.providers) ? rawThreatIntelligence.providers.length : null,
998
+ reason: !expectedThreatDomain ? 'External intelligence was not bound to a domain Lookup and was withheld.'
999
+ : rawThreatIntelligence.version !== THREAT_INTELLIGENCE_ENVELOPE_VERSION
1000
+ ? 'The external-intelligence envelope uses an unsupported version; its records were withheld.'
1001
+ : 'External-intelligence records were not a supported array and were withheld.',
1002
+ }] : [] };
1003
+ const { providers, withheld } = threatProjection;
1004
+ const threatIntelligence = providers.length
1005
+ ? { version: THREAT_INTELLIGENCE_ENVELOPE_VERSION, providers }
1006
+ : {};
1007
+ const dnsEvidence = record(availability.dns);
1008
+ const httpEvidence = record(availability.http);
1009
+ const httpResponse = record(httpEvidence.response);
1010
+ const tlsEvidence = record(availability.tls);
1011
+ const tlsCertificate = record(tlsEvidence.certificate);
1012
+ const pageIdentity = record(availability.pageIdentity);
1013
+ const pageOpenGraph = record(pageIdentity.openGraph);
1014
+ const pageResources = record(pageIdentity.resources);
1015
+ const securityPosture = record(availability.securityPosture);
1016
+ return {
1017
+ availability,
1018
+ rdap,
1019
+ registrarRdap,
1020
+ registrarRdapParsed: record(registrarRdap.parsed),
1021
+ whois,
1022
+ rdapParsed: record(rdap.parsed),
1023
+ whoisParsed: record(whois.parsed),
1024
+ diagnostics,
1025
+ timing: normalizeLookupTiming(diagnostics.timing),
1026
+ registryAccess: record(diagnostics.registryAccess),
1027
+ registryInsights,
1028
+ registrarStanding,
1029
+ reverseDns,
1030
+ reverseDnsRecords: record(reverseDns.records),
1031
+ observedNetworkContext,
1032
+ observedNetworkEndpoint: record(observedNetworkContext.endpoint),
1033
+ observedNetworkRdap: record(observedNetworkContext.rdap),
1034
+ observedNetwork: record(observedNetworkContext.network),
1035
+ securityTxt,
1036
+ sslbl,
1037
+ threatIntelligence,
1038
+ threatIntelligenceProviders: providers,
1039
+ threatIntelligenceWithheld: withheld,
1040
+ dnsEvidence,
1041
+ dnsRecords: record(dnsEvidence.records),
1042
+ httpEvidence,
1043
+ httpResponse,
1044
+ httpSecurityHeaders: record(httpResponse.securityHeaders),
1045
+ httpDeliveryMetadata: record(httpResponse.deliveryMetadata),
1046
+ tlsEvidence,
1047
+ tlsCertificate,
1048
+ tlsSubject: record(tlsCertificate.subject),
1049
+ tlsIssuer: record(tlsCertificate.issuer),
1050
+ tlsAltNames: record(tlsCertificate.subjectAltNames),
1051
+ tlsPublicKey: record(tlsCertificate.publicKey),
1052
+ tlsCipher: record(tlsEvidence.cipher),
1053
+ tlsAuthorization: record(tlsEvidence.authorization),
1054
+ tlsHostname: record(tlsEvidence.hostname),
1055
+ tlsValidity: record(tlsEvidence.validity),
1056
+ tlsDiagnostics: record(tlsEvidence.diagnostics),
1057
+ pageIdentity,
1058
+ pagePublicationMetadata: record(pageIdentity.publicationMetadata),
1059
+ pageCanonical: record(pageIdentity.canonical),
1060
+ pageMetaRefresh: record(pageIdentity.metaRefresh),
1061
+ pageOpenGraph,
1062
+ pageOpenGraphUrl: record(pageOpenGraph.url),
1063
+ pageForms: record(pageIdentity.forms),
1064
+ pageResources,
1065
+ pageResourceTypes: record(pageResources.byType),
1066
+ pageDownloads: record(pageIdentity.downloads),
1067
+ pageFingerprints: record(pageIdentity.fingerprints),
1068
+ credentialSurfaceProfile: record(availability.credentialSurfaceProfile),
1069
+ structuredDataIdentity: record(availability.structuredDataIdentity),
1070
+ technologyProfile: record(availability.technologyProfile),
1071
+ pageRoleProfile: record(availability.pageRoleProfile),
1072
+ clientBehaviorProfile: record(availability.clientBehaviorProfile),
1073
+ securityPosture,
1074
+ securityPostureSummary: record(securityPosture.summary),
1075
+ };
1076
+ }
1077
+ export { INVALID_COMPACT_LOOKUP_RESPONSE, INVALID_COMPACT_LOOKUP_RESPONSE_MESSAGE, INVALID_LOOKUP_RESPONSE, INVALID_LOOKUP_RESPONSE_MESSAGE, MAX_COMPACT_LOOKUP_AVAILABILITY_KEYS, MAX_COMPACT_LOOKUP_DIAGNOSTIC_KEYS, MAX_COMPACT_LOOKUP_RESPONSE_TOP_LEVEL_KEYS, MAX_LOOKUP_RESPONSE_ERROR_LENGTH, MAX_LOOKUP_RESPONSE_CONTAINER_ITEMS, MAX_LOOKUP_RESPONSE_HOST_LENGTH, MAX_LOOKUP_RESPONSE_QUERY_LENGTH, MAX_LOOKUP_RESPONSE_TOP_LEVEL_KEYS, MAX_LOOKUP_DNS_RECORDS_PER_TYPE, MAX_LOOKUP_REVERSE_DNS_PTR_RECORDS, MAX_LOOKUP_TLS_ALT_NAMES, MAX_LOOKUP_TLS_CERTIFICATE_POLICIES, MAX_LOOKUP_TLS_CHAIN_CERTIFICATES, MAX_LOOKUP_TLS_FINDINGS, MAX_LOOKUP_TLS_NAME_VALUES, MAX_LOOKUP_TIMING_MS, MAX_LOOKUP_TIMING_SOURCES, MAX_THREAT_INTELLIGENCE_PROVIDERS, MAX_THREAT_INTELLIGENCE_FINDINGS, MAX_THREAT_INTELLIGENCE_LIMITATIONS, validNormalizedHttpEvidence, createLookupHttpResponse, createLookupViewModel, isJsonObject, lookupHttpErrorMessage, normalizeLookupTiming, parseCompactLookupHttpResponse, parseLookupHttpResponse, record as lookupRecord, };