@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,195 @@
1
+ import PostalMime, {} from 'postal-mime';
2
+ import { parse, defaultTreeAdapter } from 'parse5';
3
+ import { sha256ArtifactBytes } from "../evidence/artifact-integrity.mjs";
4
+ import { createLinkIntake } from "./link-intake.mjs";
5
+ import { AUTHENTICATION_METHODS, addressDomains, authenticationServiceDomains, dkimSigningDomains } from "./mail-header-identity.mjs";
6
+ import { createIntakeReport } from "./intake-report.mjs";
7
+ export { assertMessageBytes } from "./intake-report.mjs";
8
+ import { requestedActionHints } from "./requested-action-hints.mjs";
9
+ import { reviewMailAuthentication, aggregateMailAuthentication } from "./mail-authentication-review.mjs";
10
+ import { MAX_AUTHENTICATION_HEADERS } from "../contracts/mail-authentication.mjs";
11
+ import { MAX_MESSAGE_INTAKE_BYTES, MAX_MESSAGE_PARTS, MAX_MESSAGE_DEPTH, MAX_MESSAGE_HTML_NODES } from "../contracts/message-intake.mjs";
12
+ /** All parsing is inert. Only HTTP(S) link targets are offered for deliberate collection. */
13
+ export async function reviewMessageInput(bytes, kind, reviewedAt, qrText) {
14
+ const base = await createIntakeReport(bytes, kind, reviewedAt);
15
+ if (!['text', 'email', 'calendar', 'qr'].includes(kind))
16
+ throw new TypeError('Use the selected-file intake entry point for this format.');
17
+ const links = createLinkIntake(), identities = [], authenticationClaims = [];
18
+ const actionHints = new Set(), bounds = new Set();
19
+ const authenticationHeaders = [];
20
+ const messageParts = [];
21
+ let omittedAuthenticationHeaders = 0;
22
+ let partialAuthentication = false;
23
+ let reviewedParts = 0, unreviewedAttachments = 0, decodedBytes = 0;
24
+ const text = (value, source) => {
25
+ for (const hint of requestedActionHints(value))
26
+ actionHints.add(hint);
27
+ links.addText(value, source);
28
+ };
29
+ function html(value) {
30
+ // parse5 never renders, loads resources, or runs supplied script.
31
+ let allocated = 0;
32
+ const htmlBound = new Error('HTML work bound');
33
+ const admit = () => { if (++allocated > MAX_MESSAGE_HTML_NODES)
34
+ throw htmlBound; };
35
+ let tree;
36
+ try {
37
+ tree = parse(value, { treeAdapter: { ...defaultTreeAdapter,
38
+ createElement(...args) { admit(); return defaultTreeAdapter.createElement(...args); },
39
+ createTextNode(...args) { admit(); return defaultTreeAdapter.createTextNode(...args); },
40
+ createCommentNode(...args) { admit(); return defaultTreeAdapter.createCommentNode(...args); },
41
+ } });
42
+ }
43
+ catch (cause) {
44
+ if (cause !== htmlBound)
45
+ throw cause;
46
+ bounds.add('HTML nodes');
47
+ return;
48
+ }
49
+ const queue = [tree];
50
+ let nodes = 0;
51
+ while (queue.length) {
52
+ if (++nodes > MAX_MESSAGE_HTML_NODES) {
53
+ bounds.add('HTML nodes');
54
+ break;
55
+ }
56
+ const node = queue.pop();
57
+ if ('tagName' in node) {
58
+ const attrs = new Map(node.attrs.map(attr => [attr.name, attr.value]));
59
+ if (node.tagName === 'a' && attrs.has('href')) {
60
+ const shown = [], children = [...node.childNodes];
61
+ let count = 0;
62
+ while (children.length && ++count <= MAX_MESSAGE_HTML_NODES) {
63
+ const child = children.pop();
64
+ if ('value' in child)
65
+ shown.push(child.value);
66
+ if ('childNodes' in child)
67
+ children.push(...child.childNodes);
68
+ }
69
+ links.add(attrs.get('href'), 'html_link', shown.reverse().join('').trim());
70
+ }
71
+ if (node.tagName === 'form' && attrs.has('action'))
72
+ links.add(attrs.get('action'), 'html_form');
73
+ if ((node.tagName === 'iframe' || node.tagName === 'frame') && attrs.has('src'))
74
+ links.add(attrs.get('src'), 'html_frame');
75
+ if (node.tagName === 'script' || node.tagName === 'style')
76
+ continue;
77
+ }
78
+ if ('value' in node)
79
+ text(node.value, 'text');
80
+ if ('childNodes' in node)
81
+ for (let index = node.childNodes.length - 1; index >= 0; index--)
82
+ queue.push(node.childNodes[index]);
83
+ }
84
+ }
85
+ function calendar(value) {
86
+ const unfolded = value.replace(/\r?\n[ \t]/gu, '');
87
+ // No attendee names, addresses, descriptions or event identifiers enter the report.
88
+ for (const line of unfolded.split(/\r?\n/u)) {
89
+ const colon = line.indexOf(':');
90
+ if (colon < 0)
91
+ continue;
92
+ const key = line.slice(0, colon).split(';', 1)[0]?.toUpperCase();
93
+ if (['URL', 'DESCRIPTION', 'LOCATION', 'X-ALT-DESC'].includes(key ?? ''))
94
+ text(line.slice(colon + 1).replace(/\\n/giu, '\n').replace(/\\([,;\\])/gu, '$1'), 'calendar');
95
+ }
96
+ }
97
+ function identity(part, role, domains) {
98
+ for (const domain of domains)
99
+ identities.push({ part, role, domain });
100
+ }
101
+ function headers(mail, part) {
102
+ const sourceReview = reviewMailAuthentication(mail.headers.map(header => ({ name: header.key, value: header.value })), part, MAX_AUTHENTICATION_HEADERS - authenticationHeaders.length);
103
+ authenticationHeaders.push(...sourceReview.headers);
104
+ omittedAuthenticationHeaders += sourceReview.omittedHeaders;
105
+ partialAuthentication ||= sourceReview.headers.some(header => !['parsed', 'none'].includes(header.state));
106
+ if (sourceReview.omittedHeaders)
107
+ bounds.add('Authentication headers');
108
+ const values = (name) => mail.headers.filter(header => header.key === name).map(header => header.value);
109
+ identity(part, 'from', addressDomains(values('from')));
110
+ identity(part, 'reply_to', addressDomains(values('reply-to')));
111
+ identity(part, 'return_path', addressDomains(values('return-path')));
112
+ identity(part, 'dkim', dkimSigningDomains(values('dkim-signature')));
113
+ const reported = values('authentication-results');
114
+ identity(part, 'authentication_service', authenticationServiceDomains(reported));
115
+ const aggregate = aggregateMailAuthentication(sourceReview);
116
+ for (const method of AUTHENTICATION_METHODS) {
117
+ const claim = aggregate[method];
118
+ if (claim.observations)
119
+ authenticationClaims.push({ part, method, result: claim.state });
120
+ }
121
+ }
122
+ async function message(input, depth, parentPart) {
123
+ if (depth > MAX_MESSAGE_DEPTH || reviewedParts >= MAX_MESSAGE_PARTS) {
124
+ bounds.add('Message nesting or parts');
125
+ unreviewedAttachments++;
126
+ return;
127
+ }
128
+ decodedBytes += input.byteLength;
129
+ if (decodedBytes > MAX_MESSAGE_INTAKE_BYTES * 2) {
130
+ bounds.add('Decoded message bytes');
131
+ unreviewedAttachments++;
132
+ return;
133
+ }
134
+ let mail;
135
+ try {
136
+ mail = await PostalMime.parse(input, { maxNestingDepth: MAX_MESSAGE_DEPTH, maxHeadersSize: 256 * 1024, rfc822Attachments: true, forceRfc822Attachments: true, attachmentEncoding: 'arraybuffer' });
137
+ }
138
+ catch {
139
+ throw new TypeError('The message could not be decoded within the MIME nesting and header limits. No input was retained.');
140
+ }
141
+ const part = ++reviewedParts;
142
+ messageParts.push({ part, parentPart, digestSha256: parentPart === null ? base.source.digestSha256 : await sha256ArtifactBytes(input), byteLength: input.byteLength });
143
+ headers(mail, part);
144
+ if (mail.text)
145
+ text(mail.text, 'text');
146
+ if (mail.html)
147
+ html(mail.html);
148
+ for (const attachment of mail.attachments) {
149
+ if (reviewedParts >= MAX_MESSAGE_PARTS) {
150
+ bounds.add('Message parts');
151
+ unreviewedAttachments++;
152
+ continue;
153
+ }
154
+ const body = typeof attachment.content === 'string' ? new TextEncoder().encode(attachment.content) : new Uint8Array(attachment.content);
155
+ if (attachment.mimeType === 'message/rfc822')
156
+ await message(body, depth + 1, part);
157
+ else if (attachment.mimeType === 'text/calendar') {
158
+ reviewedParts++;
159
+ calendar(new TextDecoder().decode(body));
160
+ }
161
+ else {
162
+ unreviewedAttachments++;
163
+ }
164
+ }
165
+ }
166
+ if (kind === 'email')
167
+ await message(bytes, 0, null);
168
+ else if (kind === 'qr') {
169
+ if (!Array.isArray(qrText) || qrText.length > MAX_MESSAGE_PARTS || qrText.some(value => typeof value !== 'string' || value.length > 8_192))
170
+ throw new TypeError('QR review requires bounded decoded text.');
171
+ reviewedParts = qrText.length;
172
+ for (const value of qrText) {
173
+ for (const hint of requestedActionHints(value))
174
+ actionHints.add(hint);
175
+ links.addQr(value);
176
+ }
177
+ }
178
+ else {
179
+ reviewedParts = 1;
180
+ const value = new TextDecoder('utf-8', { fatal: true }).decode(bytes);
181
+ if (kind === 'calendar')
182
+ calendar(value);
183
+ else
184
+ text(value, 'text');
185
+ }
186
+ const result = links.result();
187
+ for (const limitation of result.limitations)
188
+ bounds.add(limitation);
189
+ if (result.bounded)
190
+ bounds.add('Link extraction');
191
+ return { report: { ...base,
192
+ coverage: { state: bounds.size || unreviewedAttachments || partialAuthentication ? 'partial' : 'reviewed', reviewedParts, unreviewedAttachments, rejectedLinks: result.rejected, boundsReached: [...bounds] },
193
+ identities, authenticationClaims, authenticationReview: { headers: authenticationHeaders, omittedHeaders: omittedAuthenticationHeaders }, messageParts,
194
+ links: result.links, actionHints: [...actionHints] }, targets: result.targets };
195
+ }
@@ -0,0 +1,113 @@
1
+ import { array, boolean, enumeration, exact, integer, text, HEX_DIGEST_RE } from "../evidence/artifact-structure.mjs";
2
+ import { MESSAGE_ACTION_HINTS } from "../contracts/message-intake.mjs";
3
+ import { readCaptureCoverage, captureCoverageIsPartial, captureAttemptDescription } from "./capture-coverage.mjs";
4
+ export const PAGE_BEHAVIOUR_VERSION = 1;
5
+ // Separate from the capture's request, DOM traversal and aggregate byte bounds.
6
+ export const MAX_PAGE_OBSERVATIONS = 500;
7
+ export const PAGE_ACTION_HINTS = MESSAGE_ACTION_HINTS;
8
+ export function pageObservationOrigin(raw, base) {
9
+ if (typeof raw !== 'string' || raw.length > 8_192 || /[\u0000-\u0020\u007f]/u.test(raw))
10
+ return null;
11
+ try {
12
+ const url = new URL(raw, base);
13
+ return ['https:', 'http:'].includes(url.protocol) && !url.username && !url.password ? url.origin : null;
14
+ }
15
+ catch {
16
+ return null;
17
+ }
18
+ }
19
+ function origin(value, nullable = false) {
20
+ if (nullable && value === null)
21
+ return null;
22
+ const candidate = text(value, 'Page observation origin', 500);
23
+ if (pageObservationOrigin(candidate) !== candidate)
24
+ throw new TypeError('Page observations retain origins only.');
25
+ return candidate;
26
+ }
27
+ function hash(value) {
28
+ if (value === null)
29
+ return null;
30
+ if (typeof value !== 'string' || !HEX_DIGEST_RE.test(value))
31
+ throw new TypeError('Page observation digest must be SHA-256.');
32
+ return value;
33
+ }
34
+ function ordered(values) {
35
+ if (values.some((value, index) => index > 0 && value.position <= values[index - 1].position))
36
+ throw new TypeError('Page observation positions must be unique and increasing.');
37
+ return values;
38
+ }
39
+ export function readPageBehaviour(raw) {
40
+ const value = exact(raw, ['version', 'state', 'requests', 'elements', 'actionHints', 'clipboardWriteAttempts', 'coverage'], 'Page behaviour');
41
+ if (value.version !== PAGE_BEHAVIOUR_VERSION)
42
+ throw new TypeError('Unsupported page behaviour version.');
43
+ const requests = ordered(array(value.requests, 'Page requests', MAX_PAGE_OBSERVATIONS).map(raw => {
44
+ const item = exact(raw, ['position', 'kind', 'origin', 'contentSha256', 'status', 'cspEnforced', 'cspReportOnly'], 'Page request');
45
+ boolean(item.cspEnforced, 'Enforced policy presence');
46
+ boolean(item.cspReportOnly, 'Report-only policy presence');
47
+ const kind = enumeration(item.kind, ['navigation', 'script', 'frame', 'document'], 'Page request kind'), contentSha256 = hash(item.contentSha256);
48
+ if (kind !== 'script' && contentSha256 !== null)
49
+ throw new TypeError('Only script response bodies are hashed in page observations.');
50
+ return { position: integer(item.position, 'Request position', 1, 10_000), kind, origin: origin(item.origin), contentSha256,
51
+ status: integer(item.status, 'Response status', 100, 599), cspEnforced: item.cspEnforced, cspReportOnly: item.cspReportOnly };
52
+ }));
53
+ const elements = ordered(array(value.elements, 'Page elements', MAX_PAGE_OBSERVATIONS).map(raw => {
54
+ const item = exact(raw, ['position', 'kind', 'origin', 'inline', 'integrity', 'method', 'passwordFields', 'scriptSha256'], 'Page element');
55
+ const kind = enumeration(item.kind, ['script', 'frame', 'form'], 'Page element kind');
56
+ boolean(item.inline, 'Inline script');
57
+ const method = item.method === null ? null : enumeration(item.method, ['get', 'post', 'other'], 'Form method');
58
+ const passwordFields = integer(item.passwordFields, 'Password field count', 0, 20_000), scriptSha256 = hash(item.scriptSha256);
59
+ const integrity = enumeration(item.integrity, ['present', 'absent', 'not_applicable'], 'Integrity attribute');
60
+ if (kind !== 'form' && (method !== null || passwordFields !== 0) || kind === 'form' && method === null
61
+ || kind !== 'script' && (item.inline || scriptSha256 !== null || integrity !== 'not_applicable')
62
+ || kind === 'script' && (item.inline && item.origin !== null || !item.inline && scriptSha256 !== null))
63
+ throw new TypeError('Page element fields do not match their kind.');
64
+ return { position: integer(item.position, 'Element position', 1, 20_000), kind, origin: origin(item.origin, true), inline: item.inline, integrity, method, passwordFields, scriptSha256 };
65
+ }));
66
+ const actionHints = array(value.actionHints, 'Requested-action hints', PAGE_ACTION_HINTS.length).map(raw => enumeration(raw, PAGE_ACTION_HINTS, 'Requested-action hint'));
67
+ if (new Set(actionHints).size !== actionHints.length)
68
+ throw new TypeError('Page action hints must not repeat.');
69
+ const coverage = readCaptureCoverage(value.coverage);
70
+ if (value.state === 'observed' && captureCoverageIsPartial(coverage))
71
+ throw new TypeError('Partial request coverage cannot declare complete page observations.');
72
+ return { version: PAGE_BEHAVIOUR_VERSION, state: enumeration(value.state, ['observed', 'partial'], 'Page observation state'), requests, elements, actionHints,
73
+ clipboardWriteAttempts: integer(value.clipboardWriteAttempts, 'Blocked clipboard attempts', 0, 1_000_000), coverage };
74
+ }
75
+ export function readManifestPageBehaviour(value, manifestVersion) {
76
+ if (manifestVersion === 2 && value === undefined)
77
+ return null;
78
+ if (manifestVersion !== 3)
79
+ throw new TypeError('Page behaviour requires capture manifest version 3.');
80
+ return readPageBehaviour(value);
81
+ }
82
+ /** A missing/partial observation cannot establish removal or unchanged content. */
83
+ export function pageBehaviourRows(input) {
84
+ return [
85
+ ...input.requests.map(value => ({ identity: JSON.stringify(['request', value.kind, value.origin, value.contentSha256, value.status, value.cspEnforced, value.cspReportOnly]),
86
+ label: `${value.kind} response ${value.position} · HTTP ${value.status} · enforced CSP ${value.cspEnforced ? 'present' : 'absent'} · report-only CSP ${value.cspReportOnly ? 'present' : 'absent'}`, origin: value.origin, digest: value.contentSha256 })),
87
+ ...input.elements.map(value => ({ identity: JSON.stringify(['element', value.kind, value.origin, value.inline, value.integrity, value.method, value.passwordFields, value.scriptSha256]),
88
+ label: `${value.kind} element ${value.position}${value.inline ? ' · inline' : ''}${value.kind === 'form' ? ` · ${value.method} · ${value.passwordFields} password fields` : value.kind === 'script' ? ` · integrity attribute ${value.integrity}` : ''}`,
89
+ origin: value.origin, digest: value.scriptSha256 })),
90
+ ...input.actionHints.map(value => ({ identity: JSON.stringify(['action_wording', value]), label: `Body text wording · ${value.replaceAll('_', ' ')}`, origin: null, digest: null })),
91
+ ...input.coverage.attempts.filter(value => !['navigation', 'script', 'frame', 'document'].includes(value.channel) || value.state !== 'observed').map(value => ({
92
+ identity: JSON.stringify(['request_channel', value.channel, value.method, value.origin, value.state, value.reason, value.collectionStarted]),
93
+ label: `${value.channel.replaceAll('_', ' ')} request ${value.position} · ${captureAttemptDescription(value)}`,
94
+ origin: value.origin, digest: null,
95
+ })),
96
+ ];
97
+ }
98
+ export function comparePageBehaviour(before, after) {
99
+ const counts = (input) => { const map = new Map(); for (const row of input ? pageBehaviourRows(input) : [])
100
+ map.set(row.identity, { ...row, count: (map.get(row.identity)?.count ?? 0) + 1 }); return map; };
101
+ const left = counts(before), right = counts(after), complete = before?.state === 'observed' && after?.state === 'observed';
102
+ const difference = (a, b) => [...a.values()].filter(row => row.count > (b.get(row.identity)?.count ?? 0))
103
+ .map(row => ({ ...row, count: row.count - (b.get(row.identity)?.count ?? 0) }));
104
+ const added = difference(right, left), notReobserved = difference(left, right);
105
+ const navigation = (value) => value.requests.filter(row => row.kind === 'navigation').map(row => [row.origin, row.status]);
106
+ const navigationChanged = before && after ? JSON.stringify(navigation(before)) !== JSON.stringify(navigation(after)) : null;
107
+ const clipboardWriteDelta = before && after ? after.clipboardWriteAttempts - before.clipboardWriteAttempts : null;
108
+ const requestChannelsChanged = before && after ? JSON.stringify(before.coverage) !== JSON.stringify(after.coverage) : null;
109
+ return { state: !before || !after ? 'unavailable' : complete ? added.length || notReobserved.length || navigationChanged || clipboardWriteDelta || requestChannelsChanged ? 'changed' : 'unchanged' : 'inconclusive',
110
+ navigationChanged, clipboardWriteDelta, requestChannelsChanged,
111
+ added: before ? added : [], notReobserved: after ? notReobserved : [], removalEstablished: false,
112
+ boundary: 'Selected capture observations, not proof of authorisation, worldwide removal or compromise. Script hashes identify collected bytes; an integrity attribute is not a verified integrity result.' };
113
+ }
@@ -0,0 +1,155 @@
1
+ import { getDocument, ImageKind, OPS } from 'pdfjs-dist/legacy/build/pdf.mjs';
2
+ import { createDocumentIntake } from "./document-intake.mjs";
3
+ import { MAX_DOCUMENT_IMAGE_PIXELS, MAX_DOCUMENT_PAGES, MAX_DOCUMENT_TEXT_BYTES, MAX_DOCUMENT_PAGE_OPERATIONS } from "../contracts/document-intake.mjs";
4
+ import { readEvidenceImageDimensions } from "../evidence/image-regions.mjs";
5
+ import { MAX_INTAKE_LINKS, MAX_INTAKE_URL_LENGTH } from "../contracts/message-intake.mjs";
6
+ /** No document-controlled resource can reach a filesystem or network fetcher. */
7
+ class NoExternalBinaryData {
8
+ async fetch() { throw new TypeError('External PDF resources are not loaded.'); }
9
+ }
10
+ function imagePixels(raw) {
11
+ if (!raw || typeof raw !== 'object')
12
+ throw new TypeError('The PDF image is unavailable.');
13
+ const value = raw;
14
+ const { width, height } = readEvidenceImageDimensions(value.width, value.height);
15
+ if (!(value.data instanceof Uint8Array) && !(value.data instanceof Uint8ClampedArray))
16
+ throw new TypeError('The PDF image has no supported pixel data.');
17
+ const data = value.data, count = width * height, pixels = new Uint8ClampedArray(count * 4);
18
+ if (value.kind === ImageKind.RGBA_32BPP && data.length === pixels.length)
19
+ pixels.set(data);
20
+ else if (value.kind === ImageKind.RGB_24BPP && data.length === count * 3) {
21
+ for (let index = 0; index < count; index++) {
22
+ pixels.set(data.subarray(index * 3, index * 3 + 3), index * 4);
23
+ pixels[index * 4 + 3] = 255;
24
+ }
25
+ }
26
+ else if (value.kind === ImageKind.GRAYSCALE_1BPP && data.length === Math.ceil(width / 8) * height) {
27
+ const stride = Math.ceil(width / 8);
28
+ for (let y = 0; y < height; y++)
29
+ for (let x = 0; x < width; x++) {
30
+ const colour = data[y * stride + (x >> 3)] & (128 >> (x % 8)) ? 255 : 0;
31
+ pixels.set([colour, colour, colour, 255], (y * width + x) * 4);
32
+ }
33
+ }
34
+ else
35
+ throw new TypeError('The PDF image pixel layout is unsupported.');
36
+ return { width, height, pixels };
37
+ }
38
+ export async function reviewPdfInput(bytes, reviewedAt, worker) {
39
+ const review = await createDocumentIntake(bytes, 'pdf', reviewedAt);
40
+ // PDF.js takes ownership of its input. The selected original stays unchanged.
41
+ const task = getDocument({ data: bytes.slice(), ...(worker ? { worker } : {}),
42
+ BinaryDataFactory: NoExternalBinaryData, useWorkerFetch: false, useWasm: false,
43
+ useSystemFonts: false, disableFontFace: true, enableXfa: false,
44
+ isOffscreenCanvasSupported: false, isImageDecoderSupported: false,
45
+ maxImageSize: MAX_DOCUMENT_IMAGE_PIXELS, stopAtErrors: true, verbosity: 0,
46
+ disableAutoFetch: true, disableStream: true, disableRange: true });
47
+ try {
48
+ const document = await task.promise;
49
+ const total = document.numPages, admitted = Math.min(total, MAX_DOCUMENT_PAGES);
50
+ review.pages(total, 0);
51
+ if (total > admitted)
52
+ review.partial('Further PDF pages exceed the page review bound.');
53
+ if (document.isPureXfa) {
54
+ review.unavailable('unsupported', 'XFA documents are not rendered or executed.');
55
+ return review.finish();
56
+ }
57
+ let textCharacters = 0;
58
+ for (let pageNumber = 1; pageNumber <= admitted; pageNumber++) {
59
+ const page = await document.getPage(pageNumber);
60
+ try {
61
+ const reader = page.streamTextContent().getReader();
62
+ let text = '';
63
+ try {
64
+ for (;;) {
65
+ const chunk = await reader.read();
66
+ if (chunk.done)
67
+ break;
68
+ for (const item of chunk.value.items) {
69
+ if (typeof item.str !== 'string')
70
+ continue;
71
+ textCharacters += item.str.length + 1;
72
+ if (textCharacters > MAX_DOCUMENT_TEXT_BYTES) {
73
+ review.partial('Further PDF text exceeds the text review bound.');
74
+ break;
75
+ }
76
+ text += item.str + (item.hasEOL ? '\n' : ' ');
77
+ }
78
+ if (textCharacters > MAX_DOCUMENT_TEXT_BYTES) {
79
+ await reader.cancel();
80
+ break;
81
+ }
82
+ }
83
+ await review.text(text, pageNumber);
84
+ }
85
+ catch {
86
+ review.partial(`Text on page ${pageNumber} could not be fully decoded.`);
87
+ }
88
+ finally {
89
+ reader.releaseLock();
90
+ }
91
+ try {
92
+ const annotations = await page.getAnnotations(), destinations = [];
93
+ for (const annotation of annotations) {
94
+ if (typeof annotation.url !== 'string')
95
+ continue;
96
+ if (annotation.url.length > MAX_INTAKE_URL_LENGTH || destinations.length >= MAX_INTAKE_LINKS) {
97
+ review.partial(`Some links on page ${pageNumber} exceed the link review bound.`);
98
+ continue;
99
+ }
100
+ destinations.push(annotation.url);
101
+ }
102
+ await review.destinations(destinations, pageNumber);
103
+ }
104
+ catch {
105
+ review.partial(`Links on page ${pageNumber} could not be fully decoded.`);
106
+ }
107
+ try {
108
+ const operations = await page.getOperatorList(), seen = new Set();
109
+ if (operations.fnArray.length > MAX_DOCUMENT_PAGE_OPERATIONS)
110
+ review.partial(`Image operations on page ${pageNumber} exceed the review bound.`);
111
+ else
112
+ for (let index = 0; index < operations.fnArray.length; index++) {
113
+ const operation = operations.fnArray[index], args = operations.argsArray[index];
114
+ let raw;
115
+ if (operation === OPS.paintImageXObject || operation === OPS.paintImageXObjectRepeat) {
116
+ const id = args?.[0];
117
+ if (typeof id !== 'string' || seen.has(id))
118
+ continue;
119
+ seen.add(id);
120
+ raw = await new Promise(resolve => (id.startsWith('g_') ? page.commonObjs : page.objs).get(id, resolve));
121
+ }
122
+ else if (operation === OPS.paintInlineImageXObject)
123
+ raw = args?.[0];
124
+ else
125
+ continue;
126
+ try {
127
+ await review.image(imagePixels(raw), pageNumber);
128
+ }
129
+ catch {
130
+ review.partial(`An image on page ${pageNumber} has unsupported or unavailable pixels.`);
131
+ }
132
+ }
133
+ }
134
+ catch {
135
+ review.partial(`Image operations on page ${pageNumber} could not be fully decoded.`);
136
+ }
137
+ review.pages(total, pageNumber);
138
+ }
139
+ finally {
140
+ page.cleanup();
141
+ }
142
+ }
143
+ review.partial('QR review covers decoded embedded raster images, not vector artwork or a rendered-page reconstruction. Scripts, forms, embedded files and external resources are not executed or opened.');
144
+ }
145
+ catch (cause) {
146
+ if (cause instanceof Error && cause.name === 'PasswordException')
147
+ review.unavailable('encrypted', 'The PDF requires a password. Review an explicitly decrypted copy; no password was requested or retained.');
148
+ else
149
+ review.unavailable('unsupported', 'The PDF could not be decoded completely within this review. The original bytes are unchanged.');
150
+ }
151
+ finally {
152
+ await task.destroy();
153
+ }
154
+ return review.finish();
155
+ }
@@ -0,0 +1,60 @@
1
+ import { array, enumeration, exact, iso, text } from "../evidence/artifact-structure.mjs";
2
+ import { pageObservationOrigin } from "./page-behaviour.mjs";
3
+ import { CASE_PROVIDER_OUTCOMES, CASE_OBSERVED_EFFECT_STATES } from "../cases/case-response-records.mjs";
4
+ import { CONTEXT_REVIEW_SCHEMA, CONTEXT_REVIEW_VERSION, MAX_CONTEXT_RECORDS } from "../contracts/context-review.mjs";
5
+ export { PLATFORM_CONTINUITY_INPUT_SCHEMA, PLATFORM_CONTINUITY_INPUT_VERSION } from "../contracts/context-review.mjs";
6
+ export const PLATFORM_OBJECT_TYPES = ['account', 'tenant', 'application', 'extension', 'package', 'page', 'channel', 'post'];
7
+ export const PLATFORM_REPORT_STATES = ['not_reported', 'submitted', 'acknowledged'];
8
+ export function readPlatformObjects(raw) {
9
+ const rows = array(raw, 'Platform objects', MAX_CONTEXT_RECORDS).map(value => {
10
+ const row = exact(value, ['platformOrigin', 'objectType', 'objectId', 'version', 'observedAt', 'source', 'report', 'providerOutcome', 'recheck', 'recheckedAt'], 'Platform object');
11
+ const platformOrigin = text(row.platformOrigin, 'Platform origin', 500);
12
+ if (pageObservationOrigin(platformOrigin) !== platformOrigin)
13
+ throw new TypeError('Platform origin must be an HTTP(S) origin without credentials, paths or query data.');
14
+ const objectId = text(row.objectId, 'Stable object ID', 240);
15
+ if (!/^[A-Za-z0-9_@.:%/+~-]+$/u.test(objectId))
16
+ throw new TypeError('Stable object ID must contain identifier characters, not free text.');
17
+ iso(row.observedAt, 'Object observation time');
18
+ iso(row.recheckedAt, 'Object recheck time', true);
19
+ const recheck = enumeration(row.recheck, CASE_OBSERVED_EFFECT_STATES, 'Object recheck');
20
+ if ((recheck === 'not_checked') !== (row.recheckedAt === null))
21
+ throw new TypeError('A recheck result needs its own observation time; unchecked objects must not carry one.');
22
+ if (row.recheckedAt !== null && Date.parse(row.recheckedAt) < Date.parse(row.observedAt))
23
+ throw new TypeError('Object recheck predates its observation.');
24
+ const report = enumeration(row.report, PLATFORM_REPORT_STATES, 'Report status');
25
+ const providerOutcome = row.providerOutcome === null ? null : enumeration(row.providerOutcome, CASE_PROVIDER_OUTCOMES, 'Provider outcome');
26
+ if (providerOutcome !== null && report === 'not_reported')
27
+ throw new TypeError('A provider outcome needs a reported object.');
28
+ return { platformOrigin, objectType: enumeration(row.objectType, PLATFORM_OBJECT_TYPES, 'Platform object type'), objectId,
29
+ version: row.version === null ? null : text(row.version, 'Object version', 100), observedAt: row.observedAt, source: text(row.source, 'Object source', 500), report, providerOutcome, recheck, recheckedAt: row.recheckedAt };
30
+ });
31
+ const identities = rows.map(row => JSON.stringify([row.platformOrigin, row.objectType, row.objectId, row.version, row.observedAt]));
32
+ if (new Set(identities).size !== rows.length)
33
+ throw new TypeError('Duplicate platform object observations must be reconciled explicitly.');
34
+ return rows;
35
+ }
36
+ export function groupPlatformObjects(rows) {
37
+ const groups = new Map();
38
+ for (const row of rows) {
39
+ const key = JSON.stringify([row.platformOrigin, row.objectType, row.objectId]);
40
+ groups.set(key, [...(groups.get(key) ?? []), row]);
41
+ }
42
+ return [...groups.values()].map(values => values.sort((a, b) => Date.parse(a.observedAt) - Date.parse(b.observedAt)));
43
+ }
44
+ export function reviewPlatformContinuity(raw, reviewedAt) {
45
+ iso(reviewedAt, 'Review time');
46
+ const rows = readPlatformObjects(raw), groups = groupPlatformObjects(rows);
47
+ const observations = [];
48
+ for (const values of groups) {
49
+ const versions = new Set(values.map(row => row.version).filter(value => value !== null));
50
+ for (const row of values)
51
+ observations.push({ label: `${row.objectType} · ${row.objectId}${row.version ? ` · version ${row.version}` : ''}`, state: 'reported',
52
+ detail: `${row.report.replaceAll('_', ' ')}; provider outcome: ${row.providerOutcome?.replaceAll('_', ' ') ?? 'not supplied'}; independent recheck: ${row.recheck.replaceAll('_', ' ')}${row.recheckedAt ? ` at ${row.recheckedAt}` : ''}. ${versions.size > 1 ? `${versions.size} versions retained for this same declared object.` : 'No version change established.'}`,
53
+ source: `${row.platformOrigin} · ${row.source}`, observedAt: row.observedAt, hostname: new URL(row.platformOrigin).hostname });
54
+ }
55
+ const unresolved = rows.filter(row => ['not_checked', 'still_observed', 'changed', 'unavailable'].includes(row.recheck)).length;
56
+ return { schema: CONTEXT_REVIEW_SCHEMA, version: CONTEXT_REVIEW_VERSION, kind: 'platform_continuity', reviewedAt, title: 'Platform object continuity',
57
+ state: rows.length ? 'reviewed' : 'partial', summary: `Recorded observations: ${rows.length}; distinct platform objects: ${groups.length}; observations needing follow-up or a qualified outcome: ${unresolved}.`, observations,
58
+ nextSteps: ['Use the exact object and version when preparing a provider report; do not report an entire shared platform from a hosted URL alone.', 'Record each report and recheck separately. New versions, changed content and newly observed objects require their own review.'],
59
+ limitations: ['Platform origin, object identity and outcomes are analyst-supplied metadata, not verified provider telemetry.', 'A matching object ID establishes only declared continuity within that platform and object type. Provider resolution is separate from independent recheck; not reproduced does not establish worldwide removal.'] };
60
+ }
@@ -0,0 +1,56 @@
1
+ import { binarize, Decoder, Detector } from '@nuintun/qrcode';
2
+ import { decodeEvidencePng } from "../evidence/png-pixels.mjs";
3
+ import { assertMessageBytes, reviewMessageInput } from "./message-intake.mjs";
4
+ import { readEvidenceImageDimensions } from "../evidence/image-regions.mjs";
5
+ export const MAX_QR_DETECTION_ATTEMPTS = 256;
6
+ export function decodeQrPixels(image) {
7
+ readEvidenceImageDimensions(image.width, image.height);
8
+ if (!(image.pixels instanceof Uint8ClampedArray) || image.pixels.length !== image.width * image.height * 4)
9
+ throw new TypeError('Invalid QR pixel surface.');
10
+ const luminance = new Uint8Array(image.width * image.height);
11
+ for (let index = 0; index < luminance.length; index++) {
12
+ const offset = index * 4, alpha = image.pixels[offset + 3] / 255;
13
+ luminance[index] = Math.round((image.pixels[offset] * 0.299 + image.pixels[offset + 1] * 0.587 + image.pixels[offset + 2] * 0.114) * alpha + 255 * (1 - alpha));
14
+ }
15
+ const texts = new Set();
16
+ let attempts = 0, bounded = false, structured = false;
17
+ // Normal and inverted symbols are both considered, without altering originals.
18
+ for (const inverted of [false, true]) {
19
+ if (inverted)
20
+ for (let index = 0; index < luminance.length; index++)
21
+ luminance[index] = 255 - luminance[index];
22
+ const candidates = new Detector().detect(binarize(luminance, image.width, image.height)), decoder = new Decoder();
23
+ let candidate = candidates.next();
24
+ while (!candidate.done) {
25
+ if (++attempts > MAX_QR_DETECTION_ATTEMPTS) {
26
+ bounded = true;
27
+ candidates.return();
28
+ break;
29
+ }
30
+ let decoded = false;
31
+ try {
32
+ const symbol = decoder.decode(candidate.value.matrix);
33
+ decoded = true;
34
+ if (symbol.structured)
35
+ structured = true;
36
+ else if (symbol.content.length <= 8_192)
37
+ texts.add(symbol.content);
38
+ else
39
+ bounded = true;
40
+ }
41
+ catch { /* A finder-pattern candidate need not contain a decodable symbol. */ }
42
+ candidate = candidates.next(decoded);
43
+ }
44
+ if (bounded)
45
+ break;
46
+ }
47
+ return { texts: [...texts], bounded, structured };
48
+ }
49
+ export async function reviewQrInput(bytes, reviewedAt) {
50
+ assertMessageBytes(bytes);
51
+ const { texts, bounded, structured } = decodeQrPixels(decodeEvidencePng(bytes));
52
+ const result = await reviewMessageInput(bytes, 'qr', reviewedAt, texts);
53
+ const boundsReached = [...result.report.coverage.boundsReached, ...(bounded ? ['QR candidate work'] : []), ...(structured ? ['Multi-symbol structured content requires reassembly'] : [])];
54
+ return { ...result, report: { ...result.report, coverage: { ...result.report.coverage,
55
+ state: boundsReached.length ? 'partial' : result.report.coverage.state, boundsReached } } };
56
+ }
@@ -0,0 +1,11 @@
1
+ const HINTS = {
2
+ clipboard_instruction: /(?:clipboard|copy.{0,40}(?:paste|command)|paste.{0,40}(?:terminal|run|console))/iu,
3
+ shell_instruction: /(?:powershell|cmd\.exe|curl\s|wget\s|terminal|windows\s*\+\s*r)/iu,
4
+ verification_prompt: /(?:verify.{0,40}(?:human|browser)|not a robot|captcha|browser.{0,40}(?:repair|update))/iu,
5
+ device_code_instruction: /(?:device[ -]code|enter.{0,40}code.{0,40}(?:sign|login|log in))/iu,
6
+ consent_instruction: /(?:grant.{0,40}(?:access|permission)|consent|approve.{0,40}(?:application|permission))/iu,
7
+ };
8
+ /** Wording matches are review leads, never a conclusion about intent or execution. */
9
+ export function requestedActionHints(value) {
10
+ return Object.entries(HINTS).filter(([, expression]) => expression.test(value)).map(([hint]) => hint);
11
+ }
@@ -0,0 +1,17 @@
1
+ /** One format dispatch for browser and CLI; parsers load only when selected. */
2
+ export async function reviewSelectedInput(bytes, kind, reviewedAt, pdfWorker) {
3
+ switch (kind) {
4
+ case 'text':
5
+ case 'email':
6
+ case 'calendar': return (await import("./message-intake.mjs")).reviewMessageInput(bytes, kind, reviewedAt);
7
+ case 'qr': return (await import("./qr-intake.mjs")).reviewQrInput(bytes, reviewedAt);
8
+ case 'docx': return (await import("./docx-intake.mjs")).reviewDocxInput(bytes, reviewedAt);
9
+ case 'pdf': return (await import("./pdf-intake.mjs")).reviewPdfInput(bytes, reviewedAt, pdfWorker);
10
+ case 'har': return (await import("./har-intake.mjs")).reviewHarInput(bytes, reviewedAt);
11
+ case 'identity': return (await import("./identity-event-intake.mjs")).reviewIdentityEventInput(bytes, reviewedAt);
12
+ default: {
13
+ const unsupported = kind;
14
+ throw new TypeError(`Unsupported selected input: ${String(unsupported)}`);
15
+ }
16
+ }
17
+ }