@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,388 @@
1
+ // Bounded website and network enrichment, independent of registration authority.
2
+ import { capturedWebCollectionQuality } from "../packages/evidence/collection-quality.mjs";
3
+ import { recordOrEmpty as errorRecord } from "./json-record.mjs";
4
+ import { safeFetchDetailed, readTextCapped } from "./safe-fetch.mjs";
5
+ import { whoisleuthRequestHeaders } from "./outbound-identity.mjs";
6
+ import { collectDnsIntelligence, skippedDnsIntelligence } from "./dns-intelligence.mjs";
7
+ import { fetchFaviconHash } from "./favicon.mjs";
8
+ import { extractHtmlSignals } from "./html-signals.mjs";
9
+ import { analyzeStaticHtml } from "./static-html-analysis.mjs";
10
+ import { featureDecision, networkFeaturePolicy } from "./feature-policy.mjs";
11
+ import { withoutHttpDeliveryMetadata, buildHttpObservation, failedHttpObservation, skippedHttpObservation } from "./http-intelligence.mjs";
12
+ import { collectTlsIntelligence, skippedTlsObservation } from "./tls-intelligence.mjs";
13
+ import { analyzeWebsiteSecurityPosture } from "./website-security-posture.mjs";
14
+ import { analyzeWebsiteTechnology, captureTechnologyResponseHeaders, } from "./website-technology.mjs";
15
+ import { analyzeResponsePolicyHeaders, qualifyResponsePolicyWithCspMeta, } from "./response-policy.mjs";
16
+ import { nonEmptyErrorMessage } from "./error-detail.mjs";
17
+ import { prepareSelectedLookupUrl } from "../packages/evidence/lookup-target.mjs";
18
+ import { HOMEPAGE_FETCH_TIMEOUT_MS, MAX_HOMEPAGE_BYTES, } from "./outbound-request-bounds.mjs";
19
+ // No marketplace (Afternic/Sedo/Dan.com/GoDaddy Auctions/etc.) offers a
20
+ // free, no-auth API to check "is this specific domain listed for sale" -
21
+ // what's checkable without credentials is the parking/landing-page
22
+ // nameservers and homepage copy these services actually use, which is
23
+ // broader coverage of the same signal, not a live cross-marketplace lookup.
24
+ const PARKING_NS_PATTERNS = [
25
+ /sedoparking\.com$/i,
26
+ /sedo\.com$/i,
27
+ /above\.com$/i,
28
+ /bodis\.com$/i,
29
+ /parkingcrew\.net$/i,
30
+ /dan\.com$/i,
31
+ /hugedomains\.com$/i,
32
+ /uniregistry/i,
33
+ /squadhelp/i,
34
+ /afternic/i,
35
+ /voodoo\.com$/i,
36
+ /fabulous\.com$/i,
37
+ /namedrive/i,
38
+ /smartname\.com$/i,
39
+ /domainsponsor\.com$/i,
40
+ /undeveloped\.com$/i,
41
+ /trafficz\.com$/i,
42
+ /dsredirection\.com$/i,
43
+ ];
44
+ const FOR_SALE_PATH_RE = /\/(?:premium-)?domains?-for-sale(?:\/|$)/i;
45
+ function forSaleRedirectSignal(httpObservation) {
46
+ if (!httpObservation || typeof httpObservation !== 'object')
47
+ return null;
48
+ const observation = httpObservation;
49
+ const urls = [
50
+ observation.finalUrl,
51
+ ...(Array.isArray(observation.redirects)
52
+ ? observation.redirects.map((redirect) => redirect && typeof redirect === 'object' ? redirect.to : null)
53
+ : []),
54
+ ];
55
+ for (const value of urls) {
56
+ if (typeof value !== 'string' || value.length > 2048)
57
+ continue;
58
+ try {
59
+ const url = new URL(value);
60
+ if (['http:', 'https:'].includes(url.protocol) && FOR_SALE_PATH_RE.test(url.pathname)) {
61
+ return `for-sale landing-page redirect (${url.origin}${url.pathname})`;
62
+ }
63
+ }
64
+ catch {
65
+ // Malformed retained provenance cannot establish a sale signal.
66
+ }
67
+ }
68
+ return null;
69
+ }
70
+ // Fetches enough of the homepage for the lightweight HTML signals below and
71
+ // preserves why the probe failed. A failed request is not evidence that a
72
+ // domain has "no site": transient DNS/TLS/network failures, a slow origin, or
73
+ // an HTTP error can all produce the same null body. Keeping that distinction
74
+ // avoids turning an inconclusive probe into a false inactivity claim.
75
+ async function fetchHomepage(domain, { fetcher = safeFetchDetailed, timeoutMs = HOMEPAGE_FETCH_TIMEOUT_MS, selectedUrl, signal } = {}) {
76
+ const selected = selectedUrl === undefined ? null : prepareSelectedLookupUrl(selectedUrl, domain);
77
+ const requestTimeoutMs = Number.isInteger(timeoutMs) && timeoutMs >= 10 && timeoutMs <= HOMEPAGE_FETCH_TIMEOUT_MS
78
+ ? timeoutMs
79
+ : HOMEPAGE_FETCH_TIMEOUT_MS;
80
+ const headers = whoisleuthRequestHeaders();
81
+ const failures = [];
82
+ const probeStartedAt = Date.now();
83
+ for (const requestUrl of selected ? [selected] : [`https://${domain}`, `http://${domain}`]) {
84
+ signal?.throwIfAborted();
85
+ const scheme = new URL(requestUrl).protocol.slice(0, -1);
86
+ const attemptStartedAt = Date.now();
87
+ const controller = new AbortController();
88
+ const timeout = setTimeout(() => controller.abort(), requestTimeoutMs);
89
+ try {
90
+ const fetched = await fetcher(requestUrl, { signal: signal ? AbortSignal.any([signal, controller.signal]) : controller.signal, headers });
91
+ const fetchedResponse = fetched instanceof Response ? fetched : fetched.response;
92
+ const detail = fetched && typeof fetched === 'object' && 'response' in fetched
93
+ ? fetched
94
+ : {
95
+ response: fetchedResponse,
96
+ requestedUrl: requestUrl,
97
+ finalUrl: requestUrl,
98
+ redirectCount: 0,
99
+ redirectLimitReached: false,
100
+ hops: [{ url: requestUrl, status: fetchedResponse.status, location: null, durationMs: Date.now() - attemptStartedAt }],
101
+ durationMs: Date.now() - attemptStartedAt,
102
+ };
103
+ const res = detail.response;
104
+ const analysisBase = new URL(detail.finalUrl);
105
+ analysisBase.search = '';
106
+ analysisBase.hash = '';
107
+ // Retain an explicitly partial prefix when the body exceeds its bound;
108
+ // downstream analyses must not treat it as a complete page. The
109
+ // timeout stays armed through this read (cleared in `finally` below,
110
+ // not here) - a malicious site could otherwise send headers
111
+ // immediately and then trickle or stall the body forever, hanging
112
+ // this worker with no deadline once the timer above is disarmed.
113
+ if (res.ok) {
114
+ const body = await readTextCapped(res, MAX_HOMEPAGE_BYTES, { includeSha256: true });
115
+ return {
116
+ text: body.text,
117
+ status: 'fetched',
118
+ detail: `${selected ? 'Selected URL' : 'Homepage'} responded over ${scheme.toUpperCase()} (HTTP ${res.status}).`,
119
+ responsePolicy: analyzeResponsePolicyHeaders(res.headers),
120
+ technologyHeaders: captureTechnologyResponseHeaders(res.headers),
121
+ ...(selected ? { analysisBaseUrl: analysisBase.toString() } : {}),
122
+ http: buildHttpObservation({ ...detail, durationMs: Date.now() - attemptStartedAt }, {
123
+ previousAttempts: failures,
124
+ capturedBodyBytes: body.bytesRead,
125
+ bodyInspected: true,
126
+ bodyTruncated: body.truncated,
127
+ bodySha256: body.sha256,
128
+ }),
129
+ };
130
+ }
131
+ // A non-2xx HTTP response still conclusively proves that a web service
132
+ // answered on this domain. It may block this probe (403), require auth
133
+ // (401), have a broken homepage route (404), or be unhealthy (5xx), but
134
+ // none of those are equivalent to "no website". We cannot inspect its
135
+ // HTML signals, so preserve a separate responded status and release the
136
+ // unused body.
137
+ await res.body?.cancel().catch(() => { });
138
+ return {
139
+ text: null,
140
+ status: 'responded',
141
+ detail: `Web server responded over ${scheme.toUpperCase()} (HTTP ${res.status}); ${selected ? 'selected page' : 'homepage'} content was not available for inspection.`,
142
+ responsePolicy: analyzeResponsePolicyHeaders(res.headers),
143
+ technologyHeaders: captureTechnologyResponseHeaders(res.headers),
144
+ http: buildHttpObservation({ ...detail, durationMs: Date.now() - attemptStartedAt }, {
145
+ previousAttempts: failures,
146
+ capturedBodyBytes: 0,
147
+ bodyInspected: false,
148
+ }),
149
+ };
150
+ }
151
+ catch (err) {
152
+ signal?.throwIfAborted();
153
+ const error = errorRecord(err);
154
+ const reason = error.name === 'AbortError'
155
+ ? `timed out after ${requestTimeoutMs} milliseconds`
156
+ : selected ? 'request failed; the selected URL was not retried at another path or scheme'
157
+ : nonEmptyErrorMessage(err, 'request failed')
158
+ .replace(/[\u0000-\u001f\u007f]+/g, ' ')
159
+ .slice(0, 180);
160
+ failures.push({ url: requestUrl, error: `${scheme.toUpperCase()} ${reason}` });
161
+ }
162
+ finally {
163
+ clearTimeout(timeout);
164
+ }
165
+ }
166
+ return {
167
+ text: null,
168
+ status: 'inconclusive',
169
+ detail: failures.length
170
+ ? `Could not confirm ${selected ? 'selected URL' : 'homepage'} activity: ${failures.map((attempt) => attempt.error).join('; ')}.`
171
+ : 'Could not confirm homepage activity.',
172
+ http: failedHttpObservation(failures, { durationMs: Date.now() - probeStartedAt }),
173
+ };
174
+ }
175
+ function deriveWebsiteActivity(homepageStatus, hasFavicon, alreadyParked = false) {
176
+ if (alreadyParked)
177
+ return 'parked';
178
+ if (homepageStatus === 'fetched' || homepageStatus === 'responded' || hasFavicon)
179
+ return 'active';
180
+ return 'unreachable';
181
+ }
182
+ async function enrichWebsite(context, options, featurePolicy) {
183
+ const { domain, observationHostname, selectedUrl, registration, registryDnsEvidence, nameservers, dnssec } = context;
184
+ const websiteProbeEnabled = featureDecision('website_probe', featurePolicy).enabled;
185
+ const collectDns = options.collectDnsIntelligence || collectDnsIntelligence;
186
+ const collectTls = options.collectTlsIntelligence || collectTlsIntelligence;
187
+ const fetchHomepageForDomain = options.fetchHomepage || fetchHomepage;
188
+ const fetchFaviconForDomain = options.fetchFaviconHash || fetchFaviconHash;
189
+ const dnsIntelligenceEnabled = featureDecision('dns_intelligence', featurePolicy).enabled;
190
+ const tlsIntelligenceEnabled = featureDecision('tls_intelligence', featurePolicy).enabled;
191
+ const deepScanComplete = ['rdap', 'whois', 'dns_intelligence', 'website_probe', 'tls_intelligence']
192
+ .every((feature) => featureDecision(feature, featurePolicy).enabled);
193
+ // Registered - look for for-sale/parked/website signals (homepage fetch,
194
+ // deep mode only), and check for a configured mail exchanger as a
195
+ // phishing-risk signal (a lookalike domain that can receive/send mail is
196
+ // capable of running credential-harvesting or BEC campaigns).
197
+ const nsSignal = registration.state === 'registered' ? nameservers.find((ns) => PARKING_NS_PATTERNS.some((re) => re.test(ns))) : undefined;
198
+ let forSaleSignal = nsSignal ? `parking nameserver (${nsSignal})` : null;
199
+ let activityStatus = nsSignal && observationHostname === domain ? 'parked' : 'unknown';
200
+ // Homepage + bounded DNS evidence resolve in parallel; the favicon fetch is sequenced after
201
+ // the homepage so it can use any <link rel="icon"> the page declares (many
202
+ // sites serve no /favicon.ico and only point to a CDN PNG this way). One
203
+ // extra round-trip on the already-slow deep path, in exchange for finding
204
+ // favicons the bare /favicon.ico probe would miss.
205
+ const [homepage, dnsIntelligence, tlsIntelligence] = await Promise.all([
206
+ websiteProbeEnabled ? fetchHomepageForDomain(observationHostname, {
207
+ ...(selectedUrl ? { selectedUrl } : {}), ...(options.signal ? { signal: options.signal } : {}),
208
+ }).catch((err) => ({
209
+ text: null,
210
+ status: 'inconclusive',
211
+ detail: selectedUrl ? 'Could not confirm selected URL activity.'
212
+ : `Could not confirm homepage activity: ${String(err && err.message ? err.message : 'request failed').slice(0, 180)}.`,
213
+ http: failedHttpObservation([
214
+ { url: selectedUrl ?? `https://${observationHostname}`, error: selectedUrl ? 'request failed' : String(err && err.message ? err.message : 'request failed') },
215
+ ]),
216
+ })) : Promise.resolve({
217
+ text: null,
218
+ status: 'skipped',
219
+ detail: 'Website probing is disabled by deployment policy.',
220
+ http: skippedHttpObservation(),
221
+ }),
222
+ dnsIntelligenceEnabled
223
+ ? collectDns(observationHostname, {
224
+ ...(options.dnsResolvers ? { resolvers: options.dnsResolvers } : {}),
225
+ includeExtendedContext: options.includeExtendedDnsContext === true,
226
+ includeInheritedCaa: options.includeInheritedCaa === true,
227
+ registryEvidence: registryDnsEvidence,
228
+ ...(observationHostname !== domain ? { registrationDomain: domain } : {}),
229
+ })
230
+ : Promise.resolve(skippedDnsIntelligence('DNS intelligence is disabled by deployment policy.', {
231
+ includeExtendedContext: options.includeExtendedDnsContext === true,
232
+ includeInheritedCaa: options.includeInheritedCaa === true,
233
+ })),
234
+ tlsIntelligenceEnabled
235
+ ? collectTls(observationHostname)
236
+ : Promise.resolve(skippedTlsObservation()),
237
+ ]);
238
+ options.signal?.throwIfAborted();
239
+ const page = homepage.text;
240
+ const pageBaseUrl = homepage.analysisBaseUrl ?? (typeof homepage.http?.finalUrl === 'string' ? homepage.http.finalUrl : `https://${observationHostname}/`);
241
+ let pageAnalysis = page ? analyzeStaticHtml(page, { baseUrl: pageBaseUrl, includeVisibleText: true }) : undefined;
242
+ let htmlSignals = {
243
+ domainSaleSignal: null,
244
+ pageTitle: null,
245
+ hasPasswordField: false,
246
+ phishingLanguageMatch: null,
247
+ hasExternalFormAction: null,
248
+ externalAssetHosts: [],
249
+ cspMetaPolicy: null,
250
+ pageIdentity: null,
251
+ credentialSurfaceProfile: null,
252
+ structuredDataIdentity: null,
253
+ technologyProfile: null,
254
+ pageRoleProfile: null,
255
+ clientBehaviorProfile: null,
256
+ };
257
+ if (homepage.status === 'fetched') {
258
+ if (page) {
259
+ const responseContentType = homepage.http?.response?.contentType;
260
+ const pageIdentityEligible = typeof responseContentType !== 'string'
261
+ || responseContentType.trim() === ''
262
+ || /^(?:text\/html|application\/xhtml\+xml)(?:\s*;|$)/i.test(responseContentType.trim());
263
+ htmlSignals = await extractHtmlSignals(page, observationHostname, {
264
+ ...(options.signal ? { signal: options.signal } : {}),
265
+ ...(pageAnalysis ? { htmlAnalysis: pageAnalysis } : {}),
266
+ baseUrl: pageBaseUrl,
267
+ ...(typeof homepage.http?.observedAt === 'string' ? { observedAt: homepage.http.observedAt } : {}),
268
+ sourceTruncated: homepage.http?.response?.bodyTruncated === true,
269
+ exactBodyHash: homepage.http?.response?.bodyHash,
270
+ httpServer: homepage.http?.response?.server,
271
+ responseHeaders: homepage.technologyHeaders,
272
+ activityStatus,
273
+ includePageIdentity: pageIdentityEligible,
274
+ includePublicationMetadata: options.includePublicationMetadata !== false,
275
+ ...(options.includeCredentialSurfaceProfile !== undefined
276
+ ? { includeCredentialSurfaceProfile: options.includeCredentialSurfaceProfile }
277
+ : {}),
278
+ ...(options.includeStructuredDataIdentity !== undefined
279
+ ? { includeStructuredDataIdentity: options.includeStructuredDataIdentity }
280
+ : {}),
281
+ ...(options.includeTechnologyProfile !== undefined
282
+ ? { includeTechnologyProfile: options.includeTechnologyProfile }
283
+ : {}),
284
+ });
285
+ if (pageIdentityEligible && htmlSignals.domainSaleSignal) {
286
+ if (!selectedUrl && observationHostname === domain)
287
+ forSaleSignal = forSaleSignal || htmlSignals.domainSaleSignal;
288
+ activityStatus = 'parked';
289
+ }
290
+ }
291
+ }
292
+ if (options.includeTechnologyProfile !== false && htmlSignals.technologyProfile === null
293
+ && ['fetched', 'responded'].includes(homepage.status)) {
294
+ htmlSignals.technologyProfile = await analyzeWebsiteTechnology({
295
+ ...(options.signal ? { signal: options.signal } : {}),
296
+ htmlAvailable: false,
297
+ httpServer: homepage.http?.response?.server,
298
+ responseHeaders: homepage.technologyHeaders,
299
+ observedAt: homepage.http?.observedAt,
300
+ });
301
+ }
302
+ // Complete document consumers before an optional network wait. Only the
303
+ // small icon projection, not every parsed element/token, crosses that wait.
304
+ const faviconEvidence = pageAnalysis ? {
305
+ iconLinks: pageAnalysis.iconLinks, effectiveBaseUrl: pageAnalysis.effectiveBaseUrl,
306
+ } : undefined;
307
+ const pageCollectionComplete = homepage.status === 'fetched' && homepage.http?.complete === true
308
+ && homepage.http.response?.bodyTruncated !== true && Boolean(pageAnalysis)
309
+ && !pageAnalysis?.inputLimitReached && !pageAnalysis?.tagLimitReached
310
+ && !pageAnalysis?.visibleTextLimitReached && !pageAnalysis?.forms.truncated;
311
+ pageAnalysis = undefined;
312
+ const favicon = websiteProbeEnabled
313
+ ? await fetchFaviconForDomain(observationHostname, {
314
+ baseUrl: pageBaseUrl, ...(faviconEvidence ? { htmlAnalysis: faviconEvidence } : {}),
315
+ }).catch(() => null)
316
+ : null;
317
+ const faviconHash = favicon ? favicon.hash : null;
318
+ const faviconPHash = favicon ? favicon.phash : null;
319
+ const responsePolicy = qualifyResponsePolicyWithCspMeta(homepage.responsePolicy, htmlSignals.cspMetaPolicy);
320
+ const retainedHttp = options.includeDeliveryMetadata === false
321
+ ? withoutHttpDeliveryMetadata(homepage.http)
322
+ : homepage.http;
323
+ const { cspMetaPolicy: _cspMetaPolicy, domainSaleSignal: _domainSaleSignal, ...retainedHtmlSignals } = htmlSignals;
324
+ const securityPosture = options.includeSecurityPosture === false ? null : analyzeWebsiteSecurityPosture({
325
+ http: homepage.http,
326
+ responsePolicy,
327
+ pageIdentity: htmlSignals.pageIdentity,
328
+ tls: tlsIntelligence,
329
+ dns: dnsIntelligence,
330
+ dnssec,
331
+ observedAt: homepage.http?.observedAt,
332
+ });
333
+ const redirectSaleSignal = forSaleRedirectSignal(homepage.http);
334
+ if (!selectedUrl && observationHostname === domain && !forSaleSignal && redirectSaleSignal) {
335
+ forSaleSignal = redirectSaleSignal;
336
+ activityStatus = 'parked';
337
+ }
338
+ const faviconProvedActive = !selectedUrl && homepage.status === 'inconclusive' && Boolean(favicon);
339
+ const websiteProbeStatus = faviconProvedActive ? 'responded' : homepage.status;
340
+ const websiteProbeDetail = faviconProvedActive
341
+ ? `${homepage.detail} A favicon responded successfully, confirming an active web service.`
342
+ : homepage.detail;
343
+ if (websiteProbeEnabled) {
344
+ activityStatus = deriveWebsiteActivity(homepage.status, !selectedUrl && Boolean(favicon), activityStatus === 'parked');
345
+ }
346
+ const registrationResult = registration.state !== 'registered' ? registration
347
+ : !forSaleSignal ? {
348
+ ...registration,
349
+ detail: 'source' in registration && registration.source === 'dns'
350
+ ? 'Authoritative DNS delegation confirms the domain is registered, but RDAP/WHOIS registration details were unavailable.'
351
+ : selectedUrl ? 'The domain is registered. Website observations concern the explicitly selected URL.'
352
+ : observationHostname === domain ? 'Domain is registered and shows no for-sale signals.'
353
+ : 'The domain is registered. Website observations concern the separately identified hostname.',
354
+ } : {
355
+ ...registration,
356
+ state: 'for_sale',
357
+ confidence: 'medium',
358
+ detail: `Detected a for-sale listing (${forSaleSignal}).`,
359
+ };
360
+ return {
361
+ ...registrationResult,
362
+ activityStatus,
363
+ websiteProbeStatus,
364
+ websiteProbeDetail,
365
+ http: retainedHttp,
366
+ ...(options.observationHostname !== undefined || selectedUrl ? { observationHostname } : {}),
367
+ ...(selectedUrl ? { webObservationMode: 'selected_url' } : {}),
368
+ deepScanComplete,
369
+ webCollectionQuality: capturedWebCollectionQuality(!websiteProbeEnabled ? 'not_collected' : pageCollectionComplete ? 'complete'
370
+ : homepage.status === 'fetched' ? 'partial' : 'unavailable',
371
+ // The bounded favicon collector returns no result for both failed and
372
+ // non-image candidates. A miss therefore cannot prove an icon was removed.
373
+ !websiteProbeEnabled ? 'not_collected' : favicon ? 'complete' : 'unknown'),
374
+ faviconHash,
375
+ faviconPHash,
376
+ ...retainedHtmlSignals,
377
+ securityPosture,
378
+ nameservers: nameservers.length || observationHostname !== domain ? nameservers : dnsIntelligence.records.ns,
379
+ dns: dnsIntelligence,
380
+ tls: tlsIntelligence,
381
+ hasMx: dnsIntelligence.hasMx,
382
+ hasNullMx: dnsIntelligence.hasNullMx,
383
+ mxHosts: dnsIntelligence.mxHosts,
384
+ hasSpf: dnsIntelligence.hasSpf,
385
+ hasDmarc: dnsIntelligence.hasDmarc,
386
+ };
387
+ }
388
+ export { enrichWebsite, fetchHomepage, deriveWebsiteActivity, forSaleRedirectSignal };
@@ -3,7 +3,27 @@
3
3
  // Findings use fixed explanatory text and bounded counts; upstream header
4
4
  // values, certificate errors, URLs, and DNS record contents are not copied.
5
5
  import { createObservation } from "../packages/evidence/observation.mjs";
6
+ import { RESPONSE_POLICY_VERSION } from "./response-policy.mjs";
6
7
  import { MAX_SECURITY_POSTURE_FINDINGS, WEBSITE_SECURITY_POSTURE_VERSION, } from "./lookup-child-profile-contract.mjs";
8
+ // Observation state and review significance are separate. These presentation
9
+ // projections leave retained findings, wire counts and model versions intact.
10
+ function securityPostureReview(value) {
11
+ const item = record(value);
12
+ if (item.state !== 'observed' && item.state !== 'potential_exposure' && item.state !== 'observed_absence') {
13
+ return { kind: 'unavailable', label: 'Could not assess' };
14
+ }
15
+ if (item.state === 'potential_exposure' || item.tone === 'review') {
16
+ return { kind: 'needsReview', label: 'Needs review' };
17
+ }
18
+ return { kind: 'otherFindings', label: null };
19
+ }
20
+ function summarizeSecurityPostureReview(value) {
21
+ const counts = { needsReview: 0, otherFindings: 0, unavailable: 0 };
22
+ for (const item of (Array.isArray(value) ? value : []).slice(0, MAX_SECURITY_POSTURE_FINDINGS)) {
23
+ counts[securityPostureReview(item).kind] += 1;
24
+ }
25
+ return counts;
26
+ }
7
27
  const MAX_RETAINED_ORIGINS = 30;
8
28
  function record(value) {
9
29
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
@@ -86,12 +106,16 @@ const RESPONSE_POLICY_FINDINGS = Object.freeze({
86
106
  detail: () => 'The effective selected script source included a wildcard or broad scheme source. This fixed observation does not establish that an unsafe script is reachable.',
87
107
  },
88
108
  csp_unsafe_eval: {
89
- label: 'CSP permits unsafe evaluation',
90
- detail: () => "The effective selected script source included 'unsafe-eval'.",
109
+ label: 'CSP evaluation allowance observed',
110
+ detail: () => "The selected response policies allow evaluation through script-src or default-src. Element and attribute directives do not govern this allowance; other browser controls were not evaluated.",
91
111
  },
92
112
  csp_unsafe_inline: {
93
- label: 'Response CSP header permits unqualified inline script',
94
- detail: () => "The selected response header included 'unsafe-inline' without a nonce or hash source in that directive. A separate page policy was not observed early enough to qualify this response-scoped finding.",
113
+ label: 'Response CSP allows unqualified inline script blocks',
114
+ detail: () => "The selected response policies allow inline script blocks through script-src-elem, script-src or default-src without a valid nonce/hash source or strict-dynamic suppressing unsafe-inline. No earlier page policy qualified this response-scoped finding.",
115
+ },
116
+ csp_unsafe_inline_attributes: {
117
+ label: 'Response CSP allows unqualified inline event handlers',
118
+ detail: () => 'The selected response policies allow inline event attributes through script-src-attr, script-src or default-src. This header-only observation does not establish whether a handler exists or executes; page-policy timing was not evaluated for attributes.',
95
119
  },
96
120
  csp_inline_constrained_by_meta: {
97
121
  label: 'Page CSP further constrains inline script',
@@ -131,7 +155,7 @@ const RESPONSE_POLICY_FINDINGS = Object.freeze({
131
155
  });
132
156
  function responsePolicyFindings(value) {
133
157
  const policy = record(value);
134
- if (![1, 2].includes(Number(policy.responsePolicyVersion)))
158
+ if (![1, 2, RESPONSE_POLICY_VERSION].includes(Number(policy.responsePolicyVersion)))
135
159
  return [];
136
160
  const findings = [];
137
161
  const components = record(policy.components);
@@ -328,4 +352,4 @@ function analyzeWebsiteSecurityPosture(input = {}) {
328
352
  findings,
329
353
  };
330
354
  }
331
- export { MAX_SECURITY_POSTURE_FINDINGS, WEBSITE_SECURITY_POSTURE_VERSION, analyzeWebsiteSecurityPosture, };
355
+ export { MAX_SECURITY_POSTURE_FINDINGS, WEBSITE_SECURITY_POSTURE_VERSION, analyzeWebsiteSecurityPosture, securityPostureReview, summarizeSecurityPostureReview, };