@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
@@ -5,13 +5,15 @@
5
5
  // are treated as untrusted inputs and are not retained in the result.
6
6
  import { createHash } from 'node:crypto';
7
7
  import { Worker } from 'node:worker_threads';
8
+ import { createInlineLibraryScanner } from "./browser-library-worker.mjs";
8
9
  import { RETIRE_BROWSER_CATALOG } from "./generated/retire-browser-catalog.mjs";
9
10
  import { CISA_KEV_CATALOG } from "./generated/cisa-kev-catalog.mjs";
10
- import { createObservation } from "../packages/evidence/observation.mjs";
11
+ import { createObservation, normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
12
+ import { isCveIdentifier } from "../packages/contracts/vulnerability-identifiers.mjs";
11
13
  import { BROWSER_LIBRARY_PROFILE_VERSION, MAX_LIBRARY_FINDINGS, } from "./lookup-child-profile-contract.mjs";
12
- import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
14
+ import { MAX_INLINE_SCRIPT_CHARS, MAX_INLINE_SCRIPT_TOTAL_CHARS, MAX_SCRIPT_ELEMENTS, MAX_SCRIPT_REFERENCE_LENGTH, MAX_STATIC_HTML_CHARS, analyzeStaticHtml, } from "./static-html-analysis.mjs";
13
15
  // Catalogue expressions are reviewed third-party inputs. Keep every individual
14
- // regular-expression evaluation and the aggregate inline-signature work small
16
+ // regular-expression evaluation and aggregate reference/inline-signature work small
15
17
  // enough that a target-controlled script cannot monopolise the Node event loop.
16
18
  // Full-content hashes still use the complete already-capped script body.
17
19
  const MAX_INLINE_LIBRARY_SCAN_CHARS = 1_024;
@@ -21,11 +23,9 @@ const MAX_INLINE_LIBRARY_WORKER_BYTES = 64 * 1024;
21
23
  const MAX_LIBRARY_HTML_CHARS = MAX_STATIC_HTML_CHARS;
22
24
  const MAX_ADVISORY_IDENTIFIERS = 16;
23
25
  const MAX_WEAKNESS_CLASSES = 12;
24
- const MAX_MATCHES_PER_PATTERN = 4;
25
26
  const MAX_VERSION_LENGTH = 64;
26
27
  const COMPONENT_RE = /^[a-z0-9._-]{1,80}$/i;
27
28
  const VERSION_RE = /^[0-9][0-9.a-z_-]{0,63}$/i;
28
- const CVE_RE = /^CVE-[0-9X-]+$/;
29
29
  const GHSA_RE = /^GHSA-[A-Z0-9-]+$/;
30
30
  const CWE_RE = /^CWE-[0-9]+$/;
31
31
  const SEVERITY_WEIGHT = Object.freeze({
@@ -37,38 +37,64 @@ const SEVERITY_WEIGHT = Object.freeze({
37
37
  });
38
38
  const CATALOG_COMPONENTS = RETIRE_BROWSER_CATALOG.components;
39
39
  const KNOWN_EXPLOITED_IDENTIFIERS = new Set(CISA_KEV_CATALOG.identifiers);
40
- const INLINE_EXTRACTOR_CATALOGUE = Object.freeze(Object.entries(CATALOG_COMPONENTS).map(([component, value]) => {
40
+ const KNOWN_EXPLOITED_RELEASED_AT = normalizeExplicitIsoTimestamp(CISA_KEV_CATALOG.releasedAt);
41
+ if (KNOWN_EXPLOITED_RELEASED_AT === null)
42
+ throw new Error('Pinned known-exploited catalogue has an invalid release timestamp.');
43
+ const PATTERN_EXTRACTOR_CATALOGUE = Object.freeze(Object.entries(CATALOG_COMPONENTS).map(([component, value]) => {
41
44
  const extractors = record(record(value).extractors);
45
+ const patterns = (name) => Object.freeze((Array.isArray(extractors[name]) ? extractors[name] : [])
46
+ .filter((pattern) => typeof pattern === 'string' && pattern.length > 0 && pattern.length <= 2_048)
47
+ .slice(0, 64));
42
48
  return Object.freeze({
43
49
  component,
44
- patterns: Object.freeze((Array.isArray(extractors.filecontent) ? extractors.filecontent : [])
45
- .filter((pattern) => typeof pattern === 'string' && pattern.length > 0 && pattern.length <= 2_048)
46
- .slice(0, 64)),
47
- replacements: Object.freeze((Array.isArray(extractors.filecontentreplace) ? extractors.filecontentreplace : [])
48
- .filter((pattern) => typeof pattern === 'string' && pattern.length > 0 && pattern.length <= 2_048)
49
- .slice(0, 64)),
50
+ uri: patterns('uri'), filename: patterns('filename'),
51
+ patterns: patterns('filecontent'), replacements: patterns('filecontentreplace'),
50
52
  });
51
53
  }));
52
- const INLINE_REGEX_WORKER_SOURCE = String.raw `
54
+ const LIBRARY_REGEX_WORKER_SOURCE = String.raw `
53
55
  const { parentPort, workerData } = require('node:worker_threads');
54
56
  const versions = /^[0-9][0-9.a-z_-]{0,63}$/i;
55
57
  parentPort.on('message', (job) => {
56
58
  const control = new Int32Array(job.control);
57
59
  const output = new Uint8Array(job.output);
58
- const matches = [];
59
- const add = (component, version) => {
60
+ const matches = new Map();
61
+ const add = (component, version, method) => {
60
62
  const normalized = String(version).replace(/(?:\.|-)?min$/i, '').slice(0, 64);
61
- if (versions.test(normalized) && matches.length < 64) matches.push([component, normalized]);
63
+ const key = component + '\u0000' + normalized;
64
+ if (!versions.test(normalized)) return;
65
+ if (!matches.has(key) && matches.size < 64) matches.set(key, [component, normalized, new Set()]);
66
+ matches.get(key)?.[2].add(method);
62
67
  };
63
68
  try {
69
+ const input = JSON.parse(job.value);
70
+ for (const reference of input.references) {
71
+ const filename = (reference.replace(/\\/g, '/').split('/').pop() || '').split(/[?#]/, 1)[0];
72
+ for (const entry of workerData.catalogue) {
73
+ for (const [patterns, value, method, anchored] of [
74
+ [entry.uri, reference, 'script URL', false],
75
+ [entry.filename, filename, 'script filename', true],
76
+ ]) {
77
+ if (!value) continue;
78
+ for (const pattern of patterns) {
79
+ let expression;
80
+ try { expression = new RegExp(anchored ? '^(?:' + pattern + ')$' : pattern, 'g'); } catch { continue; }
81
+ let match, count = 0;
82
+ while ((match = expression.exec(value)) && count < 4) {
83
+ if (typeof match[1] === 'string' && versions.test(match[1])) { add(entry.component, match[1], method); count += 1; }
84
+ if (match[0] === '') expression.lastIndex += 1;
85
+ }
86
+ }
87
+ }
88
+ }
89
+ }
64
90
  for (const entry of workerData.catalogue) {
65
91
  for (const pattern of entry.patterns) {
66
92
  let expression;
67
93
  try { expression = new RegExp(pattern, 'g'); } catch { continue; }
68
94
  let match;
69
95
  let count = 0;
70
- while ((match = expression.exec(job.value)) && count < 4) {
71
- if (typeof match[1] === 'string') add(entry.component, match[1]);
96
+ while ((match = expression.exec(input.inline)) && count < 4) {
97
+ if (typeof match[1] === 'string') add(entry.component, match[1], 'inline signature');
72
98
  if (match[0] === '') expression.lastIndex += 1;
73
99
  count += 1;
74
100
  }
@@ -80,25 +106,32 @@ parentPort.on('message', (job) => {
80
106
  try { expression = new RegExp(descriptor[1], 'g'); } catch { continue; }
81
107
  let match;
82
108
  let count = 0;
83
- while ((match = expression.exec(job.value)) && count < 4) {
109
+ while ((match = expression.exec(input.inline)) && count < 4) {
84
110
  let replaced = match[0];
85
111
  try { replaced = match[0].replace(new RegExp(descriptor[1]), descriptor[2]); } catch { break; }
86
- add(entry.component, replaced);
112
+ add(entry.component, replaced, 'inline signature');
87
113
  if (match[0] === '') expression.lastIndex += 1;
88
114
  count += 1;
89
115
  }
90
116
  }
91
117
  }
92
- const encoded = Buffer.from(JSON.stringify(matches));
118
+ const encoded = Buffer.from(JSON.stringify([...matches.values()].flatMap(([component, version, methods]) => [...methods].map(method => [component, version, method]))));
93
119
  if (encoded.length > output.length) Atomics.store(control, 0, -2);
94
120
  else { output.set(encoded); Atomics.store(control, 0, encoded.length); }
95
121
  } catch {
96
122
  Atomics.store(control, 0, -1);
97
123
  }
98
- Atomics.notify(control, 0);
124
+ parentPort.postMessage({ id: job.id });
99
125
  });
100
126
  `;
101
- let inlineRegexWorker = null;
127
+ const scanLibraryPatterns = createInlineLibraryScanner(() => new Worker(LIBRARY_REGEX_WORKER_SOURCE, { eval: true, workerData: { catalogue: PATTERN_EXTRACTOR_CATALOGUE } }), {
128
+ // JSON can escape each admitted character to six characters; include separators and keys.
129
+ maximumCharacters: 6 * (MAX_INLINE_LIBRARY_SCAN_TOTAL_CHARS + MAX_SCRIPT_ELEMENTS * MAX_SCRIPT_REFERENCE_LENGTH) + MAX_SCRIPT_ELEMENTS * 10 + 128,
130
+ outputBytes: MAX_INLINE_LIBRARY_WORKER_BYTES,
131
+ deadlineMs: MAX_INLINE_LIBRARY_SCAN_MS,
132
+ // One worker/output buffer, with at most sixteen bounded pending inputs.
133
+ pending: 16,
134
+ });
102
135
  function record(value) {
103
136
  return value !== null && typeof value === 'object' && !Array.isArray(value)
104
137
  ? value
@@ -156,26 +189,6 @@ function matchingVulnerabilities(component, version) {
156
189
  }
157
190
  return matched;
158
191
  }
159
- function matchPattern(pattern, value) {
160
- if (typeof pattern !== 'string' || pattern.length === 0 || pattern.length > 2_048)
161
- return [];
162
- let expression;
163
- try {
164
- expression = new RegExp(pattern, 'g');
165
- }
166
- catch {
167
- return [];
168
- }
169
- const matches = [];
170
- let match;
171
- while ((match = expression.exec(value)) && matches.length < MAX_MATCHES_PER_PATTERN) {
172
- if (typeof match[1] === 'string' && VERSION_RE.test(match[1]))
173
- matches.push(match[1]);
174
- if (match[0] === '')
175
- expression.lastIndex += 1;
176
- }
177
- return matches;
178
- }
179
192
  function addDetected(detected, component, version, method) {
180
193
  const normalizedVersion = version.replace(/(?:\.|-)?min$/i, '').slice(0, MAX_VERSION_LENGTH);
181
194
  if (!COMPONENT_RE.test(component) || !VERSION_RE.test(normalizedVersion))
@@ -190,84 +203,24 @@ function addDetected(detected, component, version, method) {
190
203
  return;
191
204
  detected.set(key, { component, version: normalizedVersion, detections: new Set([method]) });
192
205
  }
193
- function scanExtractor(detected, extractorName, value, method) {
194
- for (const [componentName, rawComponent] of Object.entries(CATALOG_COMPONENTS)) {
195
- const component = record(rawComponent);
196
- const extractors = record(component.extractors);
197
- const patterns = Array.isArray(extractors[extractorName]) ? extractors[extractorName] : [];
198
- for (const pattern of patterns.slice(0, 64)) {
199
- for (const version of matchPattern(pattern, value))
200
- addDetected(detected, componentName, version, method);
201
- }
202
- }
203
- }
204
- function scanFilename(detected, reference) {
205
- const filename = (reference.replace(/\\/g, '/').split('/').pop() || '').split(/[?#]/, 1)[0];
206
- if (!filename)
207
- return;
208
- for (const [componentName, rawComponent] of Object.entries(CATALOG_COMPONENTS)) {
209
- const component = record(rawComponent);
210
- const extractors = record(component.extractors);
211
- const patterns = Array.isArray(extractors.filename) ? extractors.filename : [];
212
- for (const pattern of patterns.slice(0, 64)) {
213
- if (typeof pattern !== 'string' || pattern.length > 2_048)
214
- continue;
215
- for (const version of matchPattern(`^(?:${pattern})$`, filename)) {
216
- addDetected(detected, componentName, version, 'script filename');
217
- }
218
- }
219
- }
220
- }
221
- function scanInlineSignatures(detected, value) {
222
- if (!value)
206
+ async function scanSignatures(detected, value, references, signal) {
207
+ if (!value && !references.length)
223
208
  return { timedOut: false, unavailable: false };
224
- const controlBuffer = new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT);
225
- const outputBuffer = new SharedArrayBuffer(MAX_INLINE_LIBRARY_WORKER_BYTES);
226
- const control = new Int32Array(controlBuffer);
227
- try {
228
- if (!inlineRegexWorker) {
229
- const createdWorker = new Worker(INLINE_REGEX_WORKER_SOURCE, {
230
- eval: true,
231
- workerData: { catalogue: INLINE_EXTRACTOR_CATALOGUE },
232
- });
233
- inlineRegexWorker = createdWorker;
234
- createdWorker.unref();
235
- createdWorker.once('exit', () => {
236
- if (inlineRegexWorker === createdWorker)
237
- inlineRegexWorker = null;
238
- });
239
- createdWorker.once('error', () => {
240
- if (inlineRegexWorker === createdWorker)
241
- inlineRegexWorker = null;
242
- });
243
- }
244
- inlineRegexWorker.postMessage({ value, control: controlBuffer, output: outputBuffer });
245
- }
246
- catch {
247
- void inlineRegexWorker?.terminate().catch(() => { });
248
- inlineRegexWorker = null;
249
- return { timedOut: false, unavailable: true };
250
- }
251
- const wait = Atomics.wait(control, 0, 0, MAX_INLINE_LIBRARY_SCAN_MS);
252
- const length = Atomics.load(control, 0);
253
- if (wait === 'timed-out') {
254
- void inlineRegexWorker?.terminate().catch(() => { });
255
- inlineRegexWorker = null;
256
- return { timedOut: true, unavailable: false };
257
- }
258
- if (length < 1 || length > MAX_INLINE_LIBRARY_WORKER_BYTES) {
259
- return { timedOut: false, unavailable: true };
260
- }
209
+ const result = await scanLibraryPatterns(JSON.stringify({ inline: value, references }), signal);
210
+ signal?.throwIfAborted();
211
+ if (result.output === null)
212
+ return result;
261
213
  try {
262
- const parsed = JSON.parse(Buffer.from(new Uint8Array(outputBuffer, 0, length)).toString('utf8'));
214
+ const parsed = JSON.parse(result.output);
263
215
  if (!Array.isArray(parsed))
264
216
  return { timedOut: false, unavailable: true };
265
- for (const item of parsed.slice(0, MAX_LIBRARY_FINDINGS * 4)) {
266
- if (!Array.isArray(item) || item.length !== 2)
217
+ for (const item of parsed.slice(0, MAX_LIBRARY_FINDINGS * 4 * 3)) {
218
+ if (!Array.isArray(item) || item.length !== 3)
267
219
  continue;
268
- const [component, version] = item;
269
- if (typeof component === 'string' && typeof version === 'string') {
270
- addDetected(detected, component, version, 'inline signature');
220
+ const [component, version, method] = item;
221
+ if (typeof component === 'string' && typeof version === 'string'
222
+ && (method === 'inline signature' || method === 'script URL' || method === 'script filename')) {
223
+ addDetected(detected, component, version, method);
271
224
  }
272
225
  }
273
226
  return { timedOut: false, unavailable: false };
@@ -311,13 +264,20 @@ function findingFromDetection(detected) {
311
264
  let highestSeverity = null;
312
265
  const identifiers = new Set();
313
266
  const weaknesses = new Set();
267
+ let omittedCveIdentifiers = 0;
314
268
  for (const vulnerability of vulnerabilities) {
315
269
  const candidateSeverity = severity(vulnerability.severity);
316
270
  if (candidateSeverity
317
271
  && (!highestSeverity || SEVERITY_WEIGHT[candidateSeverity] > SEVERITY_WEIGHT[highestSeverity]))
318
272
  highestSeverity = candidateSeverity;
319
273
  const vulnerabilityIdentifiers = record(vulnerability.identifiers);
320
- for (const cve of boundedStringArray(vulnerabilityIdentifiers.CVE, 16, CVE_RE)) {
274
+ omittedCveIdentifiers += typeof vulnerability.omittedCveIdentifiers === 'number'
275
+ && Number.isSafeInteger(vulnerability.omittedCveIdentifiers) && vulnerability.omittedCveIdentifiers > 0
276
+ ? vulnerability.omittedCveIdentifiers : 0;
277
+ const suppliedCve = Array.isArray(vulnerabilityIdentifiers.CVE) ? vulnerabilityIdentifiers.CVE : [];
278
+ const validCve = suppliedCve.filter(isCveIdentifier);
279
+ omittedCveIdentifiers += suppliedCve.length - validCve.length;
280
+ for (const cve of validCve.slice(0, 16)) {
321
281
  if (identifiers.size < MAX_ADVISORY_IDENTIFIERS)
322
282
  identifiers.add(cve);
323
283
  }
@@ -332,7 +292,7 @@ function findingFromDetection(detected) {
332
292
  }
333
293
  const advisoryIdentifiers = [...identifiers].sort();
334
294
  const knownExploitedIdentifiers = advisoryIdentifiers.filter((identifier) => KNOWN_EXPLOITED_IDENTIFIERS.has(identifier));
335
- return {
295
+ const finding = {
336
296
  id: detected.component,
337
297
  name: detected.component,
338
298
  apparentVersion: detected.version,
@@ -344,8 +304,10 @@ function findingFromDetection(detected) {
344
304
  knownExploitedIdentifiers,
345
305
  weaknessClasses: [...weaknesses].sort(),
346
306
  };
307
+ return { finding, omittedCveIdentifiers };
347
308
  }
348
- function analyzeBrowserLibraries(input = {}) {
309
+ async function analyzeBrowserLibraries(input = {}) {
310
+ input.signal?.throwIfAborted();
349
311
  const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html);
350
312
  const detected = new Map();
351
313
  let referencesExamined = 0;
@@ -353,11 +315,11 @@ function analyzeBrowserLibraries(input = {}) {
353
315
  let inlineSignatureCharactersExamined = 0;
354
316
  let inlineSignatureLimitReached = false;
355
317
  const inlineSignatureSamples = [];
318
+ const references = [];
356
319
  for (const script of htmlAnalysis.scripts) {
357
320
  if (script.reference) {
358
321
  referencesExamined += 1;
359
- scanExtractor(detected, 'uri', script.reference, 'script URL');
360
- scanFilename(detected, script.reference);
322
+ references.push(script.reference);
361
323
  }
362
324
  else if (script.inlineContent && script.mediaType !== 'application/ld+json') {
363
325
  inlineScriptsExamined += 1;
@@ -369,10 +331,12 @@ function analyzeBrowserLibraries(input = {}) {
369
331
  inlineSignatureSamples.push(signatureScan.signatureContent);
370
332
  }
371
333
  }
372
- const inlineSignatureResult = scanInlineSignatures(detected, inlineSignatureSamples.join('\u0000'));
334
+ const inlineSignatureResult = await scanSignatures(detected, inlineSignatureSamples.join('\u0000'), references, input.signal);
335
+ input.signal?.throwIfAborted();
373
336
  const inlineSignatureUnavailable = inlineSignatureResult.timedOut || inlineSignatureResult.unavailable;
374
- const allFindings = [...detected.values()]
375
- .map(findingFromDetection)
337
+ const projected = [...detected.values()].map(findingFromDetection);
338
+ const omittedCveIdentifiers = projected.reduce((total, entry) => total + entry.omittedCveIdentifiers, 0);
339
+ const allFindings = projected.map((entry) => entry.finding)
376
340
  .sort((left, right) => ((right.highestSeverity ? SEVERITY_WEIGHT[right.highestSeverity] : -1)
377
341
  - (left.highestSeverity ? SEVERITY_WEIGHT[left.highestSeverity] : -1)
378
342
  || right.advisoryCount - left.advisoryCount
@@ -386,7 +350,8 @@ function analyzeBrowserLibraries(input = {}) {
386
350
  || htmlAnalysis.inlineLimitReached
387
351
  || inlineSignatureLimitReached
388
352
  || inlineSignatureUnavailable
389
- || findingLimitReached;
353
+ || findingLimitReached
354
+ || omittedCveIdentifiers > 0;
390
355
  const limitations = [
391
356
  'Library versions are inferred from passive static signatures and may be absent, transformed, or misleading.',
392
357
  'A catalogue advisory match identifies a component version associated with a published advisory; it does not establish reachability or exploitability.',
@@ -406,11 +371,13 @@ function analyzeBrowserLibraries(input = {}) {
406
371
  if (inlineSignatureLimitReached)
407
372
  limitations.push(`Passive library signature matching evaluated a deterministic ${MAX_INLINE_LIBRARY_SCAN_TOTAL_CHARS}-character cumulative sample across inline scripts; full-content hashes still used the complete bounded script content.`);
408
373
  if (inlineSignatureResult.timedOut)
409
- limitations.push(`Passive library signature matching exceeded its ${MAX_INLINE_LIBRARY_SCAN_MS} ms isolated-worker deadline; hash evidence was still evaluated.`);
374
+ limitations.push(`Passive library URL, filename and inline signature matching exceeded its ${MAX_INLINE_LIBRARY_SCAN_MS} ms isolated-worker deadline; hash evidence was still evaluated.`);
410
375
  if (inlineSignatureResult.unavailable)
411
- limitations.push('Passive library signature matching was unavailable in its isolated worker; hash evidence was still evaluated.');
376
+ limitations.push('Passive library URL, filename and inline signature matching was unavailable in its isolated worker; hash evidence was still evaluated.');
412
377
  if (findingLimitReached)
413
378
  limitations.push(`Only the first ${MAX_LIBRARY_FINDINGS} library findings were retained.`);
379
+ if (omittedCveIdentifiers)
380
+ limitations.push(`${omittedCveIdentifiers} supplied CVE identifier ${omittedCveIdentifiers === 1 ? 'entry was' : 'entries were'} omitted from matching catalogue advisories because of invalid syntax or a source limit. Advisory matches are still counted.`);
414
381
  return {
415
382
  profileVersion: BROWSER_LIBRARY_PROFILE_VERSION,
416
383
  catalog: {
@@ -421,7 +388,7 @@ function analyzeBrowserLibraries(input = {}) {
421
388
  knownExploitedCatalog: {
422
389
  name: 'CISA KEV',
423
390
  version: CISA_KEV_CATALOG.catalogVersion,
424
- releasedAt: CISA_KEV_CATALOG.releasedAt,
391
+ releasedAt: KNOWN_EXPLOITED_RELEASED_AT,
425
392
  },
426
393
  ...createObservation({
427
394
  status: truncated ? 'partial' : 'success',
@@ -437,8 +404,10 @@ function analyzeBrowserLibraries(input = {}) {
437
404
  inlineScriptsExamined,
438
405
  inlineCharactersExamined: htmlAnalysis.inlineCharactersExamined,
439
406
  inlineSignatureCharactersExamined,
440
- inlineSignatureTimedOut: inlineSignatureResult.timedOut,
441
- inlineSignatureUnavailable: inlineSignatureResult.unavailable,
407
+ inlineSignatureTimedOut: inlineSignatureSamples.length > 0 && inlineSignatureResult.timedOut,
408
+ inlineSignatureUnavailable: inlineSignatureSamples.length > 0 && inlineSignatureResult.unavailable,
409
+ referenceSignatureTimedOut: references.length > 0 && inlineSignatureResult.timedOut,
410
+ referenceSignatureUnavailable: references.length > 0 && inlineSignatureResult.unavailable,
442
411
  catalogComponents: Object.keys(CATALOG_COMPONENTS).length,
443
412
  findings: allFindings.length,
444
413
  advisoryMatches: allFindings.reduce((sum, finding) => sum + finding.advisoryCount, 0),
@@ -0,0 +1,112 @@
1
+ /** One bounded matcher worker. Queue deadlines include waiting; cancellation never stops a different scan. */
2
+ export function createInlineLibraryScanner(createWorker, limits) {
3
+ let worker = null;
4
+ let active = null;
5
+ let closing = false;
6
+ let nextId = 0;
7
+ const queue = [];
8
+ const pending = new Set();
9
+ function retireWorker() {
10
+ if (!worker)
11
+ return;
12
+ const retired = worker;
13
+ worker = null;
14
+ closing = true;
15
+ void Promise.resolve().then(() => retired.terminate()).catch(() => { }).finally(() => { closing = false; pump(); });
16
+ }
17
+ function pump() {
18
+ if (active || closing)
19
+ return;
20
+ const job = queue.shift();
21
+ if (!job) {
22
+ worker?.unref();
23
+ return;
24
+ }
25
+ active = job;
26
+ let current;
27
+ const control = new Int32Array(new SharedArrayBuffer(Int32Array.BYTES_PER_ELEMENT));
28
+ const output = new Uint8Array(new SharedArrayBuffer(limits.outputBytes));
29
+ const cleanup = job.cleanup;
30
+ const failed = () => settle(job, { timedOut: false, unavailable: true, output: null }, true);
31
+ const message = (value) => {
32
+ if (!value || typeof value !== 'object' || !('id' in value) || value.id !== job.id)
33
+ return;
34
+ const length = Atomics.load(control, 0);
35
+ if (length < 1 || length > output.byteLength) {
36
+ failed();
37
+ return;
38
+ }
39
+ settle(job, { timedOut: false, unavailable: false, output: Buffer.from(output.subarray(0, length)).toString('utf8') });
40
+ };
41
+ job.cleanup = () => {
42
+ cleanup();
43
+ current?.off('message', message);
44
+ current?.off('error', failed);
45
+ current?.off('exit', failed);
46
+ };
47
+ try {
48
+ if (!worker) {
49
+ const created = createWorker();
50
+ worker = created;
51
+ const idleStopped = () => { if (!active && worker === created)
52
+ worker = null; };
53
+ created.on('error', idleStopped);
54
+ created.on('exit', idleStopped);
55
+ }
56
+ current = worker;
57
+ if (active !== job) {
58
+ retireWorker();
59
+ return;
60
+ }
61
+ current.ref();
62
+ current.on('message', message);
63
+ current.on('error', failed);
64
+ current.on('exit', failed);
65
+ current.postMessage({ id: job.id, value: job.value, control: control.buffer, output: output.buffer });
66
+ }
67
+ catch {
68
+ failed();
69
+ }
70
+ }
71
+ function settle(job, result, retire = false, aborted = false) {
72
+ if (!pending.delete(job))
73
+ return;
74
+ job.cleanup();
75
+ if (active === job) {
76
+ active = null;
77
+ if (retire)
78
+ retireWorker();
79
+ }
80
+ else {
81
+ const index = queue.indexOf(job);
82
+ if (index >= 0)
83
+ queue.splice(index, 1);
84
+ }
85
+ if (aborted)
86
+ job.reject(job.signal?.reason ?? new DOMException('Aborted', 'AbortError'));
87
+ else
88
+ job.resolve(result);
89
+ queueMicrotask(pump);
90
+ }
91
+ return function scan(value, signal) {
92
+ signal?.throwIfAborted();
93
+ if (!value)
94
+ return Promise.resolve({ timedOut: false, unavailable: false, output: '[]' });
95
+ if (value.length > limits.maximumCharacters || pending.size >= limits.pending) {
96
+ return Promise.resolve({ timedOut: false, unavailable: true, output: null });
97
+ }
98
+ return new Promise((resolve, reject) => {
99
+ const job = { id: ++nextId, value, ...(signal ? { signal } : {}), resolve, reject, cleanup: () => { } };
100
+ const timer = setTimeout(() => settle(job, { timedOut: true, unavailable: false, output: null }, true), limits.deadlineMs);
101
+ const abort = () => settle(job, { timedOut: false, unavailable: true, output: null }, true, true);
102
+ job.cleanup = () => { clearTimeout(timer); signal?.removeEventListener('abort', abort); };
103
+ pending.add(job);
104
+ queue.push(job);
105
+ signal?.addEventListener('abort', abort, { once: true });
106
+ if (signal?.aborted)
107
+ abort();
108
+ else
109
+ pump();
110
+ });
111
+ };
112
+ }
@@ -0,0 +1,2 @@
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/candidate-provenance-bounds.mjs";
package/lib/classify.mjs CHANGED
@@ -16,7 +16,7 @@
16
16
  import { isIP } from 'node:net';
17
17
  import { domainToASCII } from 'node:url';
18
18
  import { parse } from 'tldts';
19
- import { canonicalRegistrableDomain } from "./registrable-domain.mjs";
19
+ import { canonicalRegistrableDomain } from "../packages/analysis/registrable-domain.mjs";
20
20
  // A CR/LF here would let one query become multiple lines once it reaches
21
21
  // lib/whois.mts's raw `socket.write(query + '\r\n')` - a normal URL-encoded
22
22
  // query string (?q=example.com%0D%0AHELP) decodes back into literal control
@@ -1,5 +1,5 @@
1
1
  // Static client-side behaviour indicators derived from the existing bounded
2
- // HTML tokenizer pass. Scripts are never fetched or executed. Only fixed
2
+ // HTML parse. Scripts are never fetched or executed. Only fixed
3
3
  // indicator identifiers, descriptions, evidence classes, and bounded counts
4
4
  // leave this module; script references and contents are discarded.
5
5
  import { createObservation } from "../packages/evidence/observation.mjs";
@@ -20,7 +20,6 @@ function patternCount(value, pattern) {
20
20
  }
21
21
  function analyzeClientBehavior(input = {}) {
22
22
  const htmlAnalysis = input.htmlAnalysis ?? analyzeStaticHtml(input.html);
23
- const markup = htmlAnalysis.markup;
24
23
  const inlineScripts = htmlAnalysis.scripts
25
24
  .filter((script) => script.reference === null)
26
25
  .map((script) => script.inlineContent)
@@ -31,7 +30,8 @@ function analyzeClientBehavior(input = {}) {
31
30
  return;
32
31
  indicators.push({ id, label, evidenceClass, occurrences: boundedOccurrences(occurrences), explanation });
33
32
  }
34
- add('inline_event_handlers', 'Inline event handlers', 'static_markup', patternCount(markup, /\son[a-z]{2,24}="/gu), 'Inline HTML event-handler attributes were observed.');
33
+ add('inline_event_handlers', 'Inline event handlers', 'static_markup', htmlAnalysis.elements.reduce((count, element) => count
34
+ + element.attributes.filter((attribute) => /^on[a-z]{2,24}$/u.test(attribute.name)).length, 0), 'Inline HTML event-handler attributes were observed.');
35
35
  add('client_navigation', 'Client-side navigation', 'inline_script', patternCount(inlineScripts, /\b(?:history\.(?:pushState|replaceState)|location\.(?:assign|replace)|window\.location)\b/gu), 'Inline script contains a client-side navigation API.');
36
36
  add('form_interception', 'Form interception', 'inline_script', patternCount(inlineScripts, /(?:addEventListener\s*\(\s*['"]submit['"]|\.onsubmit\s*=|preventDefault\s*\()/gu), 'Inline script contains a form-submission or default-action interception marker.');
37
37
  add('service_worker', 'Service worker registration', 'inline_script', patternCount(inlineScripts, /\bnavigator\.serviceWorker\.register\s*\(/gu), 'Inline script contains a service-worker registration call.');
package/lib/ct-query.mjs CHANGED
@@ -1,27 +1,2 @@
1
- const MAX_CT_QUERY_LENGTH = 200;
2
- const CT_QUERY_ERROR_CODE = 'INVALID_CT_QUERY';
3
- const CT_QUERY_ERROR_MESSAGE = `Certificate Transparency query must be at most ${MAX_CT_QUERY_LENGTH} characters and contain no control or invisible formatting characters.`;
4
- const UNSAFE_QUERY_CHARACTER_RE = /[\u0000-\u001f\u007f-\u009f]|\p{Default_Ignorable_Code_Point}/u;
5
- class CtQueryError extends Error {
6
- code = CT_QUERY_ERROR_CODE;
7
- constructor() {
8
- super(CT_QUERY_ERROR_MESSAGE);
9
- this.name = 'CtQueryError';
10
- }
11
- }
12
- function normalizeCtQuery(value) {
13
- if (value === undefined || value === null)
14
- return '';
15
- if (typeof value !== 'string'
16
- || value.length > MAX_CT_QUERY_LENGTH
17
- || UNSAFE_QUERY_CHARACTER_RE.test(value)) {
18
- throw new CtQueryError();
19
- }
20
- return value.trim();
21
- }
22
- function isCtQueryError(value) {
23
- return value instanceof Error
24
- && 'code' in value
25
- && value.code === CT_QUERY_ERROR_CODE;
26
- }
27
- export { CT_QUERY_ERROR_CODE, CT_QUERY_ERROR_MESSAGE, CtQueryError, isCtQueryError, MAX_CT_QUERY_LENGTH, normalizeCtQuery, };
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/ct-query.mjs";
@@ -1,9 +1,2 @@
1
- // Shared response-contract bounds for certificate-log search. The collector
2
- // and browser normalizer import these values so a valid backend response is
3
- // not accidentally clipped by a stale client-side mirror.
4
- export const MAX_CT_RESPONSE_RESULTS = 500;
5
- export const MAX_CT_RESPONSE_HOSTNAMES_PER_MATCH = 50;
6
- export const MAX_CT_RESPONSE_TIMESTAMP_LENGTH = 64;
7
- export const MAX_CT_RESPONSE_CERTIFICATE_GROUPS = 100;
8
- export const MAX_CT_RESPONSE_DOMAINS_PER_GROUP = 20;
9
- export const MAX_CT_RESPONSE_HOSTNAMES_PER_GROUP = 30;
1
+ // Retained public import; the runtime-neutral implementation lives in the domain layer.
2
+ export * from "../packages/analysis/ct-response-bounds.mjs";