@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,264 @@
1
+ // Pure defensive-domain export formatting for filtered Bulk findings. These
2
+ // files are generated locally and never submitted or applied automatically.
3
+ import { caseDispositionSupportsDefensiveResponse } from "../cases/case-record-decisions.mjs";
4
+ import { normalizeDomain } from "../evidence/domain-name.mjs";
5
+ import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
6
+ import { DEFENSIVE_INDICATOR_EXPORT_VERSION, DEFENSIVE_INDICATOR_MANIFEST_SCHEMA, DEFENSIVE_INDICATOR_ROLLBACK_SCHEMA, } from "../contracts/analyst-interchange.mjs";
7
+ export { DEFENSIVE_INDICATOR_EXPORT_VERSION, DEFENSIVE_INDICATOR_MANIFEST_SCHEMA, DEFENSIVE_INDICATOR_ROLLBACK_SCHEMA, } from "../contracts/analyst-interchange.mjs";
8
+ export const MAX_DEFENSIVE_INDICATORS = 2000;
9
+ export const MAX_DEFENSIVE_INDICATOR_INPUTS = MAX_DEFENSIVE_INDICATORS * 4;
10
+ export const DEFENSIVE_INDICATOR_FORMATS = Object.freeze(['domains', 'hosts', 'dnsmasq', 'rpz']);
11
+ const REGISTERED_STATES = new Set(['registered', 'for_sale', 'expiring']);
12
+ const MINIMUM_RISK_SCORE = 70;
13
+ const CONTROL_RE = /[\x00-\x1f\x7f]/;
14
+ function plainRecord(value) {
15
+ return value && typeof value === 'object' && !Array.isArray(value)
16
+ ? value
17
+ : null;
18
+ }
19
+ function riskScore(record) {
20
+ const value = record.risk ?? record.riskScore;
21
+ return typeof value === 'number' && Number.isFinite(value) ? value : null;
22
+ }
23
+ /** Source metadata shared by interchange writers; export time is not evidence time. */
24
+ export function defensiveIndicatorProvenance(value) {
25
+ const source = plainRecord(value) ?? {};
26
+ const saved = plainRecord(source.saved) ?? {};
27
+ const score = riskScore(source);
28
+ const version = source.riskModelVersion ?? saved.riskModelVersion;
29
+ const depth = source.scanDepth ?? saved.scanDepth;
30
+ return Object.freeze({
31
+ riskScore: score === null ? null : Math.max(0, Math.min(100, Math.round(score))),
32
+ riskModelVersion: typeof version === 'number' && Number.isSafeInteger(version) && version > 0 && version <= 1_000 ? version : null,
33
+ scanDepth: depth === 'fast' || depth === 'deep' ? depth : 'unknown',
34
+ observedAt: normalizeExplicitIsoTimestamp(source.observedAt) || normalizeExplicitIsoTimestamp(saved.observedAt),
35
+ });
36
+ }
37
+ function normalizedDomainSet(value) {
38
+ if (!Array.isArray(value))
39
+ return new Set();
40
+ return new Set(value.slice(0, MAX_DEFENSIVE_INDICATOR_INPUTS)
41
+ .map((item) => normalizeDomain(item))
42
+ .filter((item) => Boolean(item)));
43
+ }
44
+ function analystDisposition(record) {
45
+ return typeof record.analystDisposition === 'string'
46
+ ? record.analystDisposition
47
+ : typeof record.disposition === 'string'
48
+ ? record.disposition
49
+ : '';
50
+ }
51
+ function hasReadyProfileContext(record) {
52
+ const saved = plainRecord(record.saved);
53
+ const profileContext = plainRecord(saved?.profileContext ?? record.profileContext);
54
+ return profileContext?.sourceState === 'ready';
55
+ }
56
+ export function isDefensiveIndicatorCandidate(value) {
57
+ const record = plainRecord(value);
58
+ if (!record
59
+ || !hasReadyProfileContext(record)
60
+ || record.trusted
61
+ || record.status === 'error'
62
+ || typeof record.availability !== 'string'
63
+ || !REGISTERED_STATES.has(record.availability))
64
+ return false;
65
+ return Boolean(normalizeDomain(record.domain)) && (riskScore(record) ?? -1) >= MINIMUM_RISK_SCORE;
66
+ }
67
+ export function collectDefensiveIndicatorCandidates(records, limit = MAX_DEFENSIVE_INDICATORS) {
68
+ if (!Array.isArray(records))
69
+ throw new TypeError('Defensive indicator export requires an array of Bulk results.');
70
+ const retainedLimit = Number.isSafeInteger(limit) && limit > 0
71
+ ? Math.min(limit, MAX_DEFENSIVE_INDICATORS)
72
+ : MAX_DEFENSIVE_INDICATORS;
73
+ const byDomain = new Map();
74
+ for (const item of records.slice(0, MAX_DEFENSIVE_INDICATOR_INPUTS)) {
75
+ if (!isDefensiveIndicatorCandidate(item))
76
+ continue;
77
+ const record = plainRecord(item);
78
+ const domain = normalizeDomain(record?.domain);
79
+ if (record && domain && !byDomain.has(domain))
80
+ byDomain.set(domain, record);
81
+ }
82
+ const candidates = [...byDomain.entries()]
83
+ .sort(([left], [right]) => left.localeCompare(right))
84
+ .map(([domain, source]) => ({ domain, source }));
85
+ const entries = candidates.slice(0, retainedLimit);
86
+ return {
87
+ entries,
88
+ domains: entries.map((entry) => entry.domain),
89
+ truncated: records.length > MAX_DEFENSIVE_INDICATOR_INPUTS || candidates.length > retainedLimit,
90
+ };
91
+ }
92
+ export function prepareDefensiveIndicatorExport(records, options = {}, limit = MAX_DEFENSIVE_INDICATORS) {
93
+ if (!Array.isArray(records))
94
+ throw new TypeError('Defensive indicator export requires an array of Bulk results.');
95
+ const explicitSelection = Array.isArray(options.selectedDomains);
96
+ const selected = normalizedDomainSet(options.selectedDomains);
97
+ const official = normalizedDomainSet(options.officialDomains);
98
+ const allowlisted = normalizedDomainSet(options.allowlistedDomains);
99
+ const common = normalizedDomainSet(options.commonInfrastructureDomains);
100
+ const retainedLimit = Number.isSafeInteger(limit) && limit > 0
101
+ ? Math.min(limit, MAX_DEFENSIVE_INDICATORS)
102
+ : MAX_DEFENSIVE_INDICATORS;
103
+ const exclusions = [];
104
+ const seenExclusions = new Set();
105
+ const accepted = new Map();
106
+ const exclude = (domain, reason) => {
107
+ const key = `${domain}\u0000${reason}`;
108
+ if (!seenExclusions.has(key)) {
109
+ exclusions.push({ domain, reason });
110
+ seenExclusions.add(key);
111
+ }
112
+ };
113
+ for (const item of records.slice(0, MAX_DEFENSIVE_INDICATOR_INPUTS)) {
114
+ const source = plainRecord(item);
115
+ const domain = normalizeDomain(source?.domain);
116
+ if (!source || !domain)
117
+ continue;
118
+ if (explicitSelection && !selected.has(domain)) {
119
+ exclude(domain, 'not_selected');
120
+ continue;
121
+ }
122
+ if (!hasReadyProfileContext(source)) {
123
+ exclude(domain, 'profile_context_unavailable');
124
+ continue;
125
+ }
126
+ if (official.has(domain)) {
127
+ exclude(domain, 'official_domain');
128
+ continue;
129
+ }
130
+ if (allowlisted.has(domain)) {
131
+ exclude(domain, 'allowlisted_domain');
132
+ continue;
133
+ }
134
+ if (common.has(domain) || source.commonInfrastructure === true) {
135
+ exclude(domain, 'common_infrastructure');
136
+ continue;
137
+ }
138
+ if (!isDefensiveIndicatorCandidate(source)) {
139
+ exclude(domain, 'not_eligible');
140
+ continue;
141
+ }
142
+ const disposition = analystDisposition(source);
143
+ if (explicitSelection && !caseDispositionSupportsDefensiveResponse(disposition)) {
144
+ exclude(domain, 'unreviewed_disposition');
145
+ continue;
146
+ }
147
+ if (!accepted.has(domain))
148
+ accepted.set(domain, source);
149
+ }
150
+ const candidates = [...accepted.entries()]
151
+ .sort(([left], [right]) => left.localeCompare(right))
152
+ .map(([domain, source]) => ({ domain, source }));
153
+ const entries = candidates.slice(0, retainedLimit);
154
+ return {
155
+ entries,
156
+ domains: entries.map((entry) => entry.domain),
157
+ exclusions: exclusions.sort((left, right) => left.domain.localeCompare(right.domain) || left.reason.localeCompare(right.reason)),
158
+ truncated: records.length > MAX_DEFENSIVE_INDICATOR_INPUTS || candidates.length > retainedLimit,
159
+ explicitSelection,
160
+ };
161
+ }
162
+ function timestamp(value) {
163
+ if (typeof value !== 'string' || value.length > 64 || CONTROL_RE.test(value))
164
+ return null;
165
+ const parsed = Date.parse(value);
166
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
167
+ }
168
+ function header(prefix, generatedAt, count) {
169
+ return [
170
+ `${prefix} WHOISleuth defensive domain indicators v${DEFENSIVE_INDICATOR_EXPORT_VERSION}`,
171
+ `${prefix} Generated ${generatedAt}`,
172
+ `${prefix} ${count} high-risk registered domain${count === 1 ? '' : 's'}`,
173
+ `${prefix} Review before use. Heuristic findings can include false positives.`,
174
+ ];
175
+ }
176
+ function formatDomains(domains, generatedAt) {
177
+ return [...header('#', generatedAt, domains.length), ...domains].join('\n');
178
+ }
179
+ function formatHosts(domains, generatedAt) {
180
+ return [...header('#', generatedAt, domains.length), ...domains.map((domain) => `0.0.0.0 ${domain}`)].join('\n');
181
+ }
182
+ function formatDnsmasq(domains, generatedAt) {
183
+ return [...header('#', generatedAt, domains.length), ...domains.map((domain) => `address=/${domain}/0.0.0.0`)].join('\n');
184
+ }
185
+ function formatRpz(domains, generatedAt, includeWildcards) {
186
+ const serial = Math.floor(Date.parse(generatedAt) / 1000) >>> 0;
187
+ return [
188
+ ...header(';', generatedAt, domains.length),
189
+ '$TTL 60',
190
+ `@ IN SOA localhost. root.localhost. (${serial} 60 60 60 60)`,
191
+ '@ IN NS localhost.',
192
+ ...domains.flatMap((domain) => [
193
+ `${domain} CNAME .`,
194
+ ...(includeWildcards ? [`*.${domain} CNAME .`] : []),
195
+ ]),
196
+ ].join('\n');
197
+ }
198
+ function contentFor(format, domains, generatedAt, includeWildcards) {
199
+ if (format === 'hosts')
200
+ return formatHosts(domains, generatedAt);
201
+ if (format === 'dnsmasq')
202
+ return formatDnsmasq(domains, generatedAt);
203
+ if (format === 'rpz')
204
+ return formatRpz(domains, generatedAt, includeWildcards);
205
+ return formatDomains(domains, generatedAt);
206
+ }
207
+ export function buildDefensiveIndicatorExport(records, options = {}) {
208
+ const format = (typeof options.format === 'string'
209
+ && DEFENSIVE_INDICATOR_FORMATS.includes(options.format)) ? options.format : 'domains';
210
+ const generatedAt = timestamp(options.generatedAt) || new Date().toISOString();
211
+ const includeWildcards = options.includeWildcards === true;
212
+ const expiryDays = typeof options.expiryDays === 'number' && Number.isSafeInteger(options.expiryDays)
213
+ ? Math.max(1, Math.min(365, options.expiryDays))
214
+ : 30;
215
+ const collected = prepareDefensiveIndicatorExport(records, options);
216
+ const expiresAt = new Date(Date.parse(generatedAt) + expiryDays * 86_400_000).toISOString();
217
+ const manifest = {
218
+ schema: DEFENSIVE_INDICATOR_MANIFEST_SCHEMA,
219
+ version: DEFENSIVE_INDICATOR_EXPORT_VERSION,
220
+ generatedAt,
221
+ expiresAt,
222
+ reviewRequired: true,
223
+ explicitSelection: collected.explicitSelection,
224
+ includeWildcards,
225
+ entries: collected.entries.map(({ domain, source }) => ({
226
+ domain,
227
+ disposition: analystDisposition(source) || 'not_recorded',
228
+ reason: `High-risk registered Bulk finding (risk ${riskScore(source) ?? 'unknown'}).`,
229
+ source: 'bulk',
230
+ generatedAt,
231
+ expiresAt,
232
+ })),
233
+ exclusions: collected.exclusions,
234
+ };
235
+ const rollback = {
236
+ schema: DEFENSIVE_INDICATOR_ROLLBACK_SCHEMA,
237
+ version: DEFENSIVE_INDICATOR_EXPORT_VERSION,
238
+ generatedAt,
239
+ removes: collected.domains.map((domain) => ({
240
+ domain,
241
+ includeWildcard: includeWildcards,
242
+ reason: 'Remove the indicator generated by the paired reviewed manifest.',
243
+ })),
244
+ };
245
+ return {
246
+ version: DEFENSIVE_INDICATOR_EXPORT_VERSION,
247
+ format,
248
+ generatedAt,
249
+ expiresAt,
250
+ domains: collected.domains,
251
+ entries: collected.entries,
252
+ exclusions: collected.exclusions,
253
+ explicitSelection: collected.explicitSelection,
254
+ includeWildcards,
255
+ truncated: collected.truncated,
256
+ filename: `whoisleuth-defensive-domains-${generatedAt.slice(0, 10)}.${format === 'rpz' ? 'zone' : 'txt'}`,
257
+ manifestFilename: `whoisleuth-defensive-domains-${generatedAt.slice(0, 10)}.manifest.json`,
258
+ rollbackFilename: `whoisleuth-defensive-domains-${generatedAt.slice(0, 10)}.rollback.json`,
259
+ mimeType: 'text/plain;charset=utf-8',
260
+ content: `${contentFor(format, collected.domains, generatedAt, includeWildcards)}\n`,
261
+ manifestContent: `${JSON.stringify(manifest, null, 2)}\n`,
262
+ rollbackContent: `${JSON.stringify(rollback, null, 2)}\n`,
263
+ };
264
+ }
@@ -1,9 +1,13 @@
1
- import { normalizeDomain, openOrCreateCase, updateCase, } from "../cases/case-model.mjs";
2
- import { canonicalRegistrableDomain } from "../../lib/registrable-domain.mjs";
1
+ import { normalizeDomain } from "../evidence/domain-name.mjs";
2
+ import { openOrCreateCase, updateCase } from "../cases/case-record-operations.mjs";
3
+ import { canonicalRegistrableDomain } from "../analysis/registrable-domain.mjs";
3
4
  import { normalizeExplicitIsoTimestamp } from "../evidence/observation.mjs";
4
- import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, } from "../contracts/external-observation-interchange.mjs";
5
+ import { canonicalArtifactJsonV2 } from "../evidence/artifact-integrity.mjs";
6
+ import { sha256IdentityHex } from "../evidence/record-identity.mjs";
7
+ import { MAX_RESPONSE_LABEL_LENGTH, MAX_RESPONSE_VALUE_LENGTH, MAX_RESPONSE_LIMITATION_LENGTH, normalizeCaseEvidencePins, normalizeCaseSightings, } from "../cases/case-response-model.mjs";
8
+ import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, } from "../contracts/external-observation-interchange.mjs";
5
9
  import { hasUnsafeRetainedText } from "./retained-text.mjs";
6
- export { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, };
10
+ export { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, MAX_EXTERNAL_FINDING_DOMAINS, MAX_EXTERNAL_FINDING_LIMITATIONS, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, MAX_EXTERNAL_FINDINGS, MAX_EXTERNAL_FINDINGS_IMPORT_BYTES, MAX_EXTERNAL_FINDINGS_PER_DOMAIN, };
7
11
  export const EXTERNAL_FINDING_CATEGORIES = [
8
12
  'certificate',
9
13
  'dns',
@@ -83,14 +87,31 @@ function iso(value, label, optional = false) {
83
87
  function limitations(value, index) {
84
88
  if (value === undefined)
85
89
  return [];
86
- if (!Array.isArray(value) || value.length > 8) {
87
- throw new Error(`Finding ${index + 1} limitations must be an array with no more than 8 entries.`);
90
+ if (!Array.isArray(value) || value.length > MAX_EXTERNAL_FINDING_LIMITATIONS) {
91
+ throw new Error(`Finding ${index + 1} limitations must be an array with no more than ${MAX_EXTERNAL_FINDING_LIMITATIONS} entries.`);
88
92
  }
89
93
  const unique = new Set();
90
94
  for (const item of value)
91
- unique.add(requiredText(item, 240, `Finding ${index + 1} limitation`));
95
+ unique.add(requiredText(item, MAX_EXTERNAL_FINDING_LIMITATION_LENGTH, `Finding ${index + 1} limitation`));
92
96
  return [...unique];
93
97
  }
98
+ /** Keep provenance and omissions visible within the existing import bound. */
99
+ export function retainExternalFindingLimitations(supplied, mandatory) {
100
+ const required = limitations(mandatory, 0);
101
+ const optional = limitations(supplied, 0).filter((entry) => !required.includes(entry));
102
+ const available = MAX_EXTERNAL_FINDING_LIMITATIONS - required.length;
103
+ if (optional.length <= available)
104
+ return Object.freeze([...required, ...optional]);
105
+ if (available < 1)
106
+ throw new TypeError('Mandatory finding limitations leave no room to disclose omitted supplied limitations.');
107
+ const retained = optional.slice(0, available - 1);
108
+ const omitted = optional.length - retained.length;
109
+ return Object.freeze([
110
+ ...required,
111
+ ...retained,
112
+ `${omitted} supplied limitation${omitted === 1 ? ' was' : 's were'} omitted to retain mandatory provenance and omission notices; review the supplied input for the remaining qualifications.`,
113
+ ]);
114
+ }
94
115
  function structuredObservation(value, index) {
95
116
  if (value === undefined || value === null)
96
117
  return null;
@@ -162,16 +183,7 @@ function structuredObservation(value, index) {
162
183
  };
163
184
  }
164
185
  function findingKey(finding, sourceName) {
165
- return [
166
- finding.domain,
167
- finding.category,
168
- finding.evidenceClass,
169
- finding.summary,
170
- finding.observedAt,
171
- finding.completeness,
172
- sourceName,
173
- finding.structuredObservation?.eventId ?? '',
174
- ].join('\u0000');
186
+ return canonicalArtifactJsonV2({ sourceName, finding });
175
187
  }
176
188
  export function parseExternalFindingsDocument(value) {
177
189
  const root = record(value);
@@ -267,21 +279,19 @@ function importedSourceLabel(finding, sourceName) {
267
279
  ? `Deployment observation: ${sourceName}`
268
280
  : `Provider report: ${sourceName}`;
269
281
  }
270
- function existingPinKey(recordValue, finding, sourceName) {
271
- const expectedLabel = `External ${finding.category} finding`;
272
- const expectedValue = finding.structuredObservation?.value ?? pinValue(finding);
273
- const expectedSource = importedSourceLabel(finding, sourceName);
274
- const observation = finding.structuredObservation;
275
- return recordValue.evidencePins.some((pin) => (pin.label === expectedLabel
276
- && pin.value === expectedValue
277
- && pin.source === expectedSource
278
- && pin.observedAt === finding.observedAt
279
- && pin.completeness === finding.completeness
280
- && (!observation || (pin.field === observation.field
281
- && pin.sourceSchema?.collection === 'external_observations'
282
- && pin.sourceSchema.schema === observation.sourceSchema
283
- && pin.sourceSchema.version === observation.sourceVersion
284
- && (observation.eventId === null || pin.certificateObservation?.eventId === observation.eventId))))) ? findingKey(finding, sourceName) : '';
282
+ function alreadyRetained(recordValue, projection) {
283
+ const { importContentSha256: expectedIdentity, ...expected } = projection.evidencePin;
284
+ return recordValue.evidencePins.some((pin) => {
285
+ const { id: _id, createdAt: _createdAt, importContentSha256, ...material } = pin;
286
+ if (importContentSha256 && importContentSha256 !== expectedIdentity)
287
+ return false;
288
+ // Older pins have no content digest. Only a complete, exact retained
289
+ // projection can establish duplication; a shortened prefix cannot.
290
+ if (!importContentSha256 && (projection.shortenedFields.length || projection.omittedLimitations))
291
+ return false;
292
+ // An imported digest alone cannot substitute for matching retained fields.
293
+ return canonicalArtifactJsonV2(material) === canonicalArtifactJsonV2(expected);
294
+ });
285
295
  }
286
296
  function structuredPinFields(finding) {
287
297
  const observation = finding.structuredObservation;
@@ -313,37 +323,41 @@ function structuredPinFields(finding) {
313
323
  certificateObservation,
314
324
  };
315
325
  }
316
- function mergeExternalFindingIntoCase(current, caseId, finding, source, now) {
317
- const target = current.find((candidate) => candidate.id === caseId) ?? null;
318
- if (!target)
319
- throw new Error('The selected Case is unavailable for this finding.');
320
- if (existingPinKey(target, finding, source.name)) {
321
- return { cases: [...current], record: target, added: false };
322
- }
323
- const sourceLimitations = [
326
+ /** Exact material retained by both the import preview and Case mutation. */
327
+ export function externalFindingCaseProjection(finding, source) {
328
+ const identitySha256 = sha256IdentityHex(new TextEncoder().encode(canonicalArtifactJsonV2({
329
+ sourceName: source.name, sourceReference: source.reference, finding,
330
+ })));
331
+ const value = finding.structuredObservation?.value ?? pinValue(finding);
332
+ const sourceLabel = importedSourceLabel(finding, source.name);
333
+ const sourceReference = source.reference ? `Source reference: ${source.reference}` : null;
334
+ const shortenedFields = [
335
+ ...(value.length > MAX_RESPONSE_VALUE_LENGTH ? ['finding value'] : []),
336
+ ...(sourceLabel.length > MAX_RESPONSE_LABEL_LENGTH ? ['source label'] : []),
337
+ ...(sourceReference && sourceReference.length > MAX_RESPONSE_LIMITATION_LENGTH ? ['source reference'] : []),
338
+ ];
339
+ const mandatory = [
324
340
  finding.evidenceClass === 'deployment_observation'
325
341
  ? `Imported as an observation made by ${source.name}; this browser session did not collect or independently verify it.`
326
342
  : `Reported by ${source.name}; WHOISleuth did not collect or independently verify this provider finding.`,
327
- ...(source.reference ? [`Source reference: ${source.reference}`] : []),
328
- ...finding.limitations,
343
+ ...(sourceReference ? [sourceReference.length > MAX_RESPONSE_LIMITATION_LENGTH
344
+ ? `${sourceReference.slice(0, MAX_RESPONSE_LIMITATION_LENGTH - 13)}… [shortened]` : sourceReference] : []),
345
+ ...(shortenedFields.length ? [`Case storage shortened ${shortenedFields.join(', ')}. The content digest covers the complete accepted finding.`] : []),
329
346
  ];
330
- const existingPinIds = new Set(target.evidencePins.map((pin) => pin.id));
331
- let updated = updateCase([...current], target.id, {
332
- evidencePin: {
347
+ const sourceLimitations = retainExternalFindingLimitations(finding.limitations, mandatory);
348
+ const omittedLimitations = finding.limitations.filter((item) => !sourceLimitations.includes(item)).length;
349
+ const pin = normalizeCaseEvidencePins([{
333
350
  ...structuredPinFields(finding),
351
+ importContentSha256: identitySha256,
334
352
  label: `External ${finding.category} finding`,
335
- value: finding.structuredObservation?.value ?? pinValue(finding),
336
- source: importedSourceLabel(finding, source.name),
353
+ value,
354
+ source: sourceLabel,
337
355
  observedAt: finding.observedAt,
338
356
  completeness: finding.completeness,
357
+ truncated: shortenedFields.length || omittedLimitations ? true : null,
339
358
  limitations: sourceLimitations,
340
- },
341
- }, now);
342
- const addedPin = updated.record.evidencePins.find((pin) => !existingPinIds.has(pin.id)) ?? null;
343
- if (!addedPin)
344
- throw new Error('The imported finding could not be retained as Case evidence.');
345
- updated = updateCase(updated.cases, target.id, {
346
- sighting: {
359
+ }], finding.observedAt)[0];
360
+ const observed = normalizeCaseSightings([{
347
361
  state: finding.evidenceClass === 'deployment_observation'
348
362
  ? 'observed_by_deployment'
349
363
  : 'reported_by_provider',
@@ -359,9 +373,28 @@ function mergeExternalFindingIntoCase(current, caseId, finding, source, now) {
359
373
  source: source.name,
360
374
  observedAt: finding.observedAt,
361
375
  completeness: finding.completeness,
362
- evidencePinId: addedPin.id,
363
376
  limitations: sourceLimitations,
364
- },
377
+ }], finding.observedAt)[0];
378
+ if (!pin || !observed)
379
+ throw new TypeError('The finding has no valid retained Case projection.');
380
+ const { id: _pinId, createdAt: _pinCreatedAt, ...evidencePin } = pin;
381
+ const { id: _sightingId, createdAt: _sightingCreatedAt, evidencePinId: _pinReference, ...sighting } = observed;
382
+ return { evidencePin, sighting, identitySha256, shortenedFields, omittedLimitations };
383
+ }
384
+ function mergeExternalFindingIntoCase(current, caseId, finding, source, now) {
385
+ const target = current.find((candidate) => candidate.id === caseId) ?? null;
386
+ if (!target)
387
+ throw new Error('The selected Case is unavailable for this finding.');
388
+ const projection = externalFindingCaseProjection(finding, source);
389
+ if (alreadyRetained(target, projection))
390
+ return { cases: [...current], record: target, added: false };
391
+ const existingPinIds = new Set(target.evidencePins.map((pin) => pin.id));
392
+ let updated = updateCase([...current], target.id, { evidencePin: projection.evidencePin }, now);
393
+ const addedPin = updated.record.evidencePins.find((pin) => !existingPinIds.has(pin.id)) ?? null;
394
+ if (!addedPin)
395
+ throw new Error('The imported finding could not be retained as Case evidence.');
396
+ updated = updateCase(updated.cases, target.id, {
397
+ sighting: { ...projection.sighting, evidencePinId: addedPin.id },
365
398
  }, now);
366
399
  return { cases: updated.cases, record: updated.record, added: true };
367
400
  }
@@ -372,7 +405,6 @@ export function mergeExternalFindingsIntoCases(current, document, now = new Date
372
405
  let findingsAdded = 0;
373
406
  let duplicatesSkipped = 0;
374
407
  for (const finding of document.findings) {
375
- const existing = cases.find((candidate) => candidate.domain === finding.domain) ?? null;
376
408
  const opened = openOrCreateCase(cases, { domain: finding.domain, source: 'import' }, now);
377
409
  cases = opened.cases;
378
410
  const target = cases.find((candidate) => candidate.id === opened.record.id) ?? opened.record;
@@ -383,10 +415,10 @@ export function mergeExternalFindingsIntoCases(current, document, now = new Date
383
415
  continue;
384
416
  }
385
417
  findingsAdded += 1;
386
- if (existing)
387
- updatedDomains.add(finding.domain);
388
- else
418
+ if (opened.created)
389
419
  createdDomains.add(finding.domain);
420
+ else
421
+ updatedDomains.add(finding.domain);
390
422
  }
391
423
  return {
392
424
  cases,
@@ -422,7 +454,7 @@ export function externalFindingsCaseTargets(document, caseDomain) {
422
454
  }
423
455
  /**
424
456
  * Merges already-validated external findings into one selected Case. A Case is
425
- * keyed to its registrable domain, while rendered captures may retain an exact
457
+ * identified by its immutable ID with a registrable-domain pivot, while rendered captures may retain an exact
426
458
  * hostname. Only same-domain findings are accepted, the exact hostname remains
427
459
  * explicit in the retained evidence, and this operation can never open another
428
460
  * Case as a side effect.
@@ -0,0 +1,35 @@
1
+ import { readManagedIndicatorSet } from "./managed-indicator-set.mjs";
2
+ import { stixIndicatorObjects, stixIndicatorProducer } from "./stix-indicator-export.mjs";
3
+ import { mispIndicatorAttribute, mispIndicatorEvent } from "./misp-indicator-export.mjs";
4
+ /** Re-exporting a revision is deterministic; revoked object versions never advance. */
5
+ export async function exportManagedIndicators(input, format) {
6
+ const manifest = await readManagedIndicatorSet(input);
7
+ let document = manifest;
8
+ if (format === 'stix') {
9
+ const producer = `identity--${manifest.producerId}`;
10
+ document = { type: 'bundle', id: `bundle--${manifest.revisionId}`, objects: [
11
+ stixIndicatorProducer(producer, manifest.createdAt),
12
+ ...manifest.entries.flatMap(entry => stixIndicatorObjects(entry.domain, entry.observation.availability, entry.observation, entry.createdAt, producer, type => `${type}--${entry.id}`)
13
+ .map(object => object.type !== 'indicator' ? object : { ...object,
14
+ modified: entry.modifiedAt, valid_until: entry.expiresAt, revoked: entry.withdrawal !== null,
15
+ x_whoisleuth_managed_set: manifest.id, x_whoisleuth_original_review_basis: entry.basis,
16
+ x_whoisleuth_review_basis: entry.reviewBasis,
17
+ ...(entry.withdrawal ? { x_whoisleuth_withdrawal_reason: entry.withdrawal.reason } : {}),
18
+ })),
19
+ ] };
20
+ }
21
+ else if (format === 'misp') {
22
+ const attributes = manifest.entries.map(entry => {
23
+ const attribute = mispIndicatorAttribute(entry.domain, entry.observation.availability, entry.observation, entry.id, entry.modifiedAt);
24
+ return { ...attribute, deleted: entry.withdrawal !== null,
25
+ comment: `${attribute.comment}; review-expires=${entry.expiresAt}; expiry requires recipient review, not automatic deletion; original-basis=${entry.basis}; latest-review=${entry.reviewBasis}${entry.withdrawal ? `; withdrawn=${entry.withdrawal.reason}` : ''}` };
26
+ });
27
+ document = { Event: { ...mispIndicatorEvent(manifest.id, manifest.createdAt, attributes),
28
+ info: `${manifest.name} · reviewed domain indicators`, timestamp: String(Math.floor(Date.parse(manifest.modifiedAt) / 1000)) } };
29
+ }
30
+ else if (format !== 'manifest')
31
+ throw new TypeError('Unsupported managed indicator export format.');
32
+ return { document, content: `${JSON.stringify(document, null, 2)}\n`,
33
+ filename: `whoisleuth-indicators-${manifest.id}-r${manifest.revision}.${format}.json`,
34
+ mimeType: format === 'stix' ? 'application/stix+json;charset=utf-8' : 'application/json;charset=utf-8' };
35
+ }