@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,210 @@
1
+ import { array, digest, domain, enumeration, exact, exactOptional, integer, iso, text, validateIntegrity } from "../evidence/artifact-structure.mjs";
2
+ import { canonicalArtifactJsonV2, sha256ArtifactDigestV2, SORTED_JSON_V2 } from "../evidence/artifact-integrity.mjs";
3
+ import { assertWorkspaceInputGraph } from "../workspace/hostile-input.mjs";
4
+ import { parseBoundedJson, boundedJsonLimitsForBytes } from "../analysis/bounded-json.mjs";
5
+ import { MANAGED_INDICATOR_SET_SCHEMA, MANAGED_INDICATOR_SET_VERSION, MAX_MANAGED_INDICATOR_SET_BYTES, MAX_MANAGED_INDICATOR_PLAN_BYTES } from "../contracts/analyst-interchange.mjs";
6
+ import { defensiveIndicatorProvenance, prepareDefensiveIndicatorExport, MAX_DEFENSIVE_INDICATOR_INPUTS } from "./defensive-indicator-export.mjs";
7
+ import { MAX_STIX_INDICATORS } from "./stix-indicator-export.mjs";
8
+ export const MAX_MANAGED_INDICATORS = MAX_STIX_INDICATORS;
9
+ export const MAX_INDICATOR_REVIEW_BASIS = 1_000;
10
+ const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/u;
11
+ const AVAILABILITY = ['registered', 'for_sale', 'expiring'];
12
+ function uuid(value) {
13
+ if (typeof value !== 'string' || !UUID.test(value))
14
+ throw new TypeError('Indicator identities must be lowercase version-4 UUIDs.');
15
+ return value;
16
+ }
17
+ function timestamp(value, label) { iso(value, label); return value; }
18
+ function basis(value) {
19
+ const result = text(value, 'Indicator review basis', MAX_INDICATOR_REVIEW_BASIS).trim();
20
+ if (!result)
21
+ throw new TypeError('Explain the indicator revision.');
22
+ return result;
23
+ }
24
+ function bounded(value, maximumBytes) {
25
+ assertWorkspaceInputGraph(value, 'Indicator input', { maximumBytes });
26
+ if (new TextEncoder().encode(JSON.stringify(value)).byteLength > maximumBytes)
27
+ throw new TypeError('Indicator input exceeds its byte limit.');
28
+ }
29
+ /** Structural validation is independent of digest verification and author trust. */
30
+ export function validateManagedIndicatorSet(value) {
31
+ bounded(value, MAX_MANAGED_INDICATOR_SET_BYTES);
32
+ const root = exact(value, ['schema', 'version', 'id', 'producerId', 'revisionId', 'name', 'revision', 'createdAt', 'modifiedAt', 'previous', 'entries', 'integrity'], 'Managed indicator set');
33
+ if (root.schema !== MANAGED_INDICATOR_SET_SCHEMA || root.version !== MANAGED_INDICATOR_SET_VERSION)
34
+ throw new TypeError('Unsupported managed indicator set schema or version.');
35
+ const identities = [uuid(root.id), uuid(root.producerId), uuid(root.revisionId)];
36
+ if (!text(root.name, 'Indicator set name', 120).trim())
37
+ throw new TypeError('Name the indicator set.');
38
+ const revision = integer(root.revision, 'Indicator revision', 1);
39
+ const created = Date.parse(timestamp(root.createdAt, 'Set creation time')), modified = Date.parse(timestamp(root.modifiedAt, 'Set modification time'));
40
+ if (modified < created || (revision === 1 && modified !== created))
41
+ throw new TypeError('Indicator revision times are inconsistent.');
42
+ if (revision === 1) {
43
+ if (root.previous !== null)
44
+ throw new TypeError('An initial indicator revision has no predecessor.');
45
+ }
46
+ else {
47
+ const previous = exact(root.previous, ['revisionId', 'digestSha256'], 'Preceding indicator revision');
48
+ identities.push(uuid(previous.revisionId));
49
+ digest(previous.digestSha256, 'Preceding revision digest');
50
+ }
51
+ const activeDomains = new Set();
52
+ for (const item of array(root.entries, 'Managed indicators', MAX_MANAGED_INDICATORS, 1)) {
53
+ const entry = exact(item, ['id', 'domain', 'createdAt', 'modifiedAt', 'expiresAt', 'basis', 'reviewBasis', 'observation', 'withdrawal'], 'Managed indicator');
54
+ identities.push(uuid(entry.id));
55
+ domain(entry.domain, 'Managed indicator domain');
56
+ const first = Date.parse(timestamp(entry.createdAt, 'Indicator creation time'));
57
+ const last = Date.parse(timestamp(entry.modifiedAt, 'Indicator modification time'));
58
+ const expires = Date.parse(timestamp(entry.expiresAt, 'Indicator review expiry'));
59
+ if (first < created || last < first || last > modified || expires <= first || (entry.withdrawal === null && expires <= last))
60
+ throw new TypeError('Indicator entry times are inconsistent.');
61
+ basis(entry.basis);
62
+ basis(entry.reviewBasis);
63
+ const observation = exact(entry.observation, ['availability', 'riskScore', 'riskModelVersion', 'scanDepth', 'observedAt'], 'Indicator observation');
64
+ enumeration(observation.availability, AVAILABILITY, 'Indicator registration observation');
65
+ if (observation.riskScore !== null)
66
+ integer(observation.riskScore, 'Retained risk score', 0, 100);
67
+ if (observation.riskModelVersion !== null)
68
+ integer(observation.riskModelVersion, 'Retained risk model', 1, 1_000);
69
+ enumeration(observation.scanDepth, ['fast', 'deep', 'unknown'], 'Retained scan depth');
70
+ iso(observation.observedAt, 'Retained observation time', true);
71
+ if (entry.withdrawal === null) {
72
+ if (activeDomains.has(entry.domain))
73
+ throw new TypeError('Only one active identity per domain is allowed in a managed set.');
74
+ activeDomains.add(entry.domain);
75
+ }
76
+ else {
77
+ const withdrawal = exact(entry.withdrawal, ['at', 'reason'], 'Indicator withdrawal');
78
+ if (timestamp(withdrawal.at, 'Withdrawal time') !== entry.modifiedAt)
79
+ throw new TypeError('Withdrawal must be the final indicator modification.');
80
+ basis(withdrawal.reason);
81
+ }
82
+ }
83
+ if (new Set(identities).size !== identities.length)
84
+ throw new TypeError('Managed indicator identities must be distinct.');
85
+ validateIntegrity(root.integrity, 'Managed indicator integrity', root.version, MANAGED_INDICATOR_SET_VERSION);
86
+ return value;
87
+ }
88
+ export async function readManagedIndicatorSet(value) {
89
+ const checked = structuredClone(validateManagedIndicatorSet(value));
90
+ const { integrity, ...unsigned } = checked;
91
+ if (await sha256ArtifactDigestV2(unsigned) !== integrity.digestSha256)
92
+ throw new TypeError('Managed indicator content does not match its recorded digest.');
93
+ return checked;
94
+ }
95
+ export function parseManagedIndicatorJson(raw, plan = false) {
96
+ const maximumBytes = plan ? MAX_MANAGED_INDICATOR_PLAN_BYTES : MAX_MANAGED_INDICATOR_SET_BYTES;
97
+ return parseBoundedJson(raw, { maximumBytes, label: 'Managed indicator input', limits: boundedJsonLimitsForBytes(maximumBytes) });
98
+ }
99
+ export function managedIndicatorState(entry, at) {
100
+ iso(at, 'Review time');
101
+ return entry.withdrawal ? 'withdrawn' : Date.parse(entry.expiresAt) <= Date.parse(at) ? 'expired' : 'active';
102
+ }
103
+ /** A known predecessor detects removal, rollback and edits to original observations. */
104
+ export function compareManagedIndicatorRevisions(previous, next) {
105
+ validateManagedIndicatorSet(previous);
106
+ validateManagedIndicatorSet(next);
107
+ if (previous.id !== next.id || previous.producerId !== next.producerId || previous.name !== next.name || previous.createdAt !== next.createdAt
108
+ || next.revision !== previous.revision + 1 || next.previous?.revisionId !== previous.revisionId || next.previous.digestSha256 !== previous.integrity.digestSha256
109
+ || Math.floor(Date.parse(next.modifiedAt) / 1000) <= Math.floor(Date.parse(previous.modifiedAt) / 1000)) {
110
+ throw new TypeError('This is not the next revision of the selected indicator set. Keep both files and review the predecessor.');
111
+ }
112
+ const old = new Map(previous.entries.map(entry => [entry.id, entry]));
113
+ const changes = [];
114
+ for (const entry of next.entries) {
115
+ const retained = old.get(entry.id);
116
+ if (!retained) {
117
+ if (entry.createdAt !== next.modifiedAt || entry.modifiedAt !== next.modifiedAt || entry.withdrawal)
118
+ throw new TypeError('A new indicator must start in the new revision.');
119
+ changes.push({ id: entry.id, domain: entry.domain, kind: 'added' });
120
+ continue;
121
+ }
122
+ old.delete(entry.id);
123
+ if (canonicalArtifactJsonV2(retained) === canonicalArtifactJsonV2(entry))
124
+ continue;
125
+ if (retained.withdrawal || entry.modifiedAt !== next.modifiedAt || retained.domain !== entry.domain || retained.createdAt !== entry.createdAt
126
+ || retained.basis !== entry.basis || canonicalArtifactJsonV2(retained.observation) !== canonicalArtifactJsonV2(entry.observation)
127
+ || (entry.withdrawal && retained.expiresAt !== entry.expiresAt))
128
+ throw new TypeError('A revision cannot rewrite observations or modify a withdrawn identity.');
129
+ changes.push({ id: entry.id, domain: entry.domain, kind: entry.withdrawal ? 'withdrawn' : 'renewed' });
130
+ }
131
+ if (old.size)
132
+ throw new TypeError('A revision cannot remove retained indicator history.');
133
+ if (!changes.length)
134
+ throw new TypeError('No indicator change was selected.');
135
+ return changes;
136
+ }
137
+ export async function buildManagedIndicatorRevision(input, now = new Date().toISOString()) {
138
+ bounded(input, MAX_MANAGED_INDICATOR_PLAN_BYTES);
139
+ const plan = exactOptional(structuredClone(input), ['basis'], ['name', 'previous', 'rows', 'selectedDomains', 'officialDomains', 'allowlistedDomains', 'commonInfrastructureDomains', 'renewIds', 'withdrawIds', 'reintroduceDomains', 'expiresAt'], 'Indicator revision plan');
140
+ const previous = plan.previous === undefined ? null : await readManagedIndicatorSet(plan.previous);
141
+ const generatedAt = timestamp(now, 'Revision time');
142
+ const reviewBasis = basis(plan.basis);
143
+ const name = plan.name === undefined && previous ? previous.name : text(plan.name, 'Indicator set name', 120).trim();
144
+ if (!name || (previous && name !== previous.name))
145
+ throw new TypeError('A revision retains the original set name.');
146
+ function values(key, maximum, read) {
147
+ const result = array(plan[key] ?? [], key, maximum).map(read);
148
+ if (new Set(result).size !== result.length)
149
+ throw new TypeError('Revision selections must not contain duplicates.');
150
+ return result;
151
+ }
152
+ const domains = (key) => values(key, MAX_DEFENSIVE_INDICATOR_INPUTS, item => { domain(item, 'Selected domain'); return item; });
153
+ const selectedDomains = domains('selectedDomains');
154
+ const preflight = prepareDefensiveIndicatorExport(array(plan.rows ?? [], 'Reviewed rows', MAX_DEFENSIVE_INDICATOR_INPUTS), {
155
+ selectedDomains, officialDomains: domains('officialDomains'), allowlistedDomains: domains('allowlistedDomains'), commonInfrastructureDomains: domains('commonInfrastructureDomains'),
156
+ }, MAX_MANAGED_INDICATORS);
157
+ if (preflight.truncated)
158
+ throw new TypeError('The selected revision exceeds the managed-set entry limit; no partial revision was created.');
159
+ const renew = new Set(values('renewIds', MAX_MANAGED_INDICATORS, uuid));
160
+ const withdraw = new Set(values('withdrawIds', MAX_MANAGED_INDICATORS, uuid));
161
+ const reintroduce = new Set(domains('reintroduceDomains'));
162
+ if ([...renew].some(id => withdraw.has(id)))
163
+ throw new TypeError('An indicator cannot be renewed and withdrawn in one revision.');
164
+ const entries = structuredClone(previous?.entries ?? []);
165
+ const known = new Map(entries.map(entry => [entry.id, entry]));
166
+ for (const id of [...renew, ...withdraw])
167
+ if (!known.has(id) || known.get(id).withdrawal)
168
+ throw new TypeError('Select an existing, non-withdrawn indicator identity.');
169
+ const active = new Set(entries.filter(entry => !entry.withdrawal).map(entry => entry.domain));
170
+ const withdrawn = new Set(entries.filter(entry => entry.withdrawal).map(entry => entry.domain));
171
+ const additions = preflight.entries.filter(entry => !active.has(entry.domain));
172
+ if ([...reintroduce].some(value => !withdrawn.has(value) || !additions.some(entry => entry.domain === value)))
173
+ throw new TypeError('Reintroduction must select a withdrawn domain being added with a new identity.');
174
+ for (const entry of additions)
175
+ if (withdrawn.has(entry.domain) && !reintroduce.has(entry.domain))
176
+ throw new TypeError('Explicitly approve a new identity before reintroducing a withdrawn domain.');
177
+ let expiresAt = null;
178
+ if (additions.length || renew.size) {
179
+ expiresAt = timestamp(plan.expiresAt, 'New review expiry');
180
+ if (Date.parse(expiresAt) <= Date.parse(generatedAt))
181
+ throw new TypeError('The new review expiry must be in the future.');
182
+ }
183
+ else if (plan.expiresAt !== undefined)
184
+ timestamp(plan.expiresAt, 'New review expiry');
185
+ for (const entry of entries) {
186
+ if (renew.has(entry.id)) {
187
+ entry.expiresAt = expiresAt;
188
+ entry.modifiedAt = generatedAt;
189
+ entry.reviewBasis = reviewBasis;
190
+ }
191
+ if (withdraw.has(entry.id)) {
192
+ entry.withdrawal = { at: generatedAt, reason: reviewBasis };
193
+ entry.modifiedAt = generatedAt;
194
+ entry.reviewBasis = reviewBasis;
195
+ }
196
+ }
197
+ const randomId = () => globalThis.crypto.randomUUID();
198
+ for (const entry of additions)
199
+ entries.push({ id: randomId(), domain: entry.domain, createdAt: generatedAt, modifiedAt: generatedAt, expiresAt: expiresAt, basis: reviewBasis, reviewBasis,
200
+ observation: { availability: entry.source.availability, ...defensiveIndicatorProvenance(entry.source) }, withdrawal: null });
201
+ if (!additions.length && !renew.size && !withdraw.size)
202
+ throw new TypeError('No eligible addition, renewal or withdrawal was selected.');
203
+ const unsigned = { schema: MANAGED_INDICATOR_SET_SCHEMA, version: MANAGED_INDICATOR_SET_VERSION,
204
+ id: previous?.id ?? randomId(), producerId: previous?.producerId ?? randomId(), revisionId: randomId(), name,
205
+ revision: (previous?.revision ?? 0) + 1, createdAt: previous?.createdAt ?? generatedAt, modifiedAt: generatedAt,
206
+ previous: previous ? { revisionId: previous.revisionId, digestSha256: previous.integrity.digestSha256 } : null, entries };
207
+ const manifest = validateManagedIndicatorSet({ ...unsigned, integrity: { algorithm: 'SHA-256', canonicalization: SORTED_JSON_V2, digestSha256: await sha256ArtifactDigestV2(unsigned) } });
208
+ const changes = previous ? compareManagedIndicatorRevisions(previous, manifest) : manifest.entries.map(entry => ({ id: entry.id, domain: entry.domain, kind: 'added' }));
209
+ return { manifest, changes, unchanged: manifest.entries.length - changes.length, exclusions: preflight.exclusions };
210
+ }
@@ -0,0 +1,93 @@
1
+ // Pure MISP event JSON for local analyst import. The event stays unpublished,
2
+ // organization-only, non-IDS, and non-correlating until a MISP user reviews it.
3
+ import { collectDefensiveIndicatorCandidates, defensiveIndicatorProvenance, MAX_DEFENSIVE_INDICATORS, } from "./defensive-indicator-export.mjs";
4
+ import { MISP_INDICATOR_EXPORT_VERSION } from "../contracts/analyst-interchange.mjs";
5
+ export { MISP_INDICATOR_EXPORT_VERSION } from "../contracts/analyst-interchange.mjs";
6
+ export const MAX_MISP_ATTRIBUTES = MAX_DEFENSIVE_INDICATORS;
7
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
8
+ const CONTROL_RE = /[\x00-\x1f\x7f]/;
9
+ const WARNING = 'Review before operational use; false positives are possible.';
10
+ function record(value) {
11
+ return value && typeof value === 'object' && !Array.isArray(value)
12
+ ? value
13
+ : {};
14
+ }
15
+ function isoTimestamp(value) {
16
+ if (typeof value !== 'string' || value.length > 64 || CONTROL_RE.test(value))
17
+ return null;
18
+ const parsed = Date.parse(value);
19
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
20
+ }
21
+ function defaultUuidFactory() {
22
+ const uuid = globalThis.crypto?.randomUUID?.();
23
+ if (!uuid)
24
+ throw new Error('Secure random identifiers are unavailable for the MISP export.');
25
+ return uuid;
26
+ }
27
+ function nextUuid(uuidFactory, used) {
28
+ const value = uuidFactory();
29
+ if (typeof value !== 'string' || !UUID_RE.test(value)) {
30
+ throw new Error('The MISP identifier factory returned an invalid UUID.');
31
+ }
32
+ const uuid = value.toLowerCase();
33
+ if (used.has(uuid))
34
+ throw new Error('The MISP identifier factory returned a duplicate UUID.');
35
+ used.add(uuid);
36
+ return uuid;
37
+ }
38
+ function attributeComment(availability, provenance) {
39
+ const { riskScore: score, riskModelVersion: modelVersion, scanDepth, observedAt } = provenance;
40
+ return [
41
+ 'Heuristic Bulk finding',
42
+ `availability=${availability}`,
43
+ `risk=${score ?? 'unknown'}`,
44
+ ...(modelVersion === null ? [] : [`risk-model=v${modelVersion}`]),
45
+ `scan-depth=${scanDepth}`,
46
+ `observed-at=${observedAt ?? 'unknown'}`,
47
+ `timestamp-basis=${observedAt ? 'scan' : 'unknown'}`,
48
+ WARNING,
49
+ ].join('; ');
50
+ }
51
+ export function mispIndicatorAttribute(domain, availability, provenance, uuid, modifiedAt) {
52
+ return {
53
+ uuid, type: 'domain', category: 'Network activity', value: domain,
54
+ to_ids: false, distribution: '5', timestamp: String(Math.floor(Date.parse(modifiedAt) / 1000)),
55
+ ...(provenance.observedAt ? { first_seen: provenance.observedAt, last_seen: provenance.observedAt } : {}),
56
+ comment: attributeComment(availability, provenance), disable_correlation: true, deleted: false,
57
+ };
58
+ }
59
+ export function mispIndicatorEvent(uuid, generatedAt, attributes) {
60
+ return { uuid, date: generatedAt.slice(0, 10),
61
+ info: `WHOISleuth heuristic defensive-domain candidates for analyst review (export v${MISP_INDICATOR_EXPORT_VERSION})`,
62
+ threat_level_id: '4', analysis: '0', distribution: '0', published: false,
63
+ timestamp: String(Math.floor(Date.parse(generatedAt) / 1000)), publish_timestamp: '0', disable_correlation: true, Attribute: attributes };
64
+ }
65
+ export function buildMispIndicatorExport(records, options = {}) {
66
+ const generatedAt = isoTimestamp(options.generatedAt) || new Date().toISOString();
67
+ const uuidFactory = typeof options.uuidFactory === 'function'
68
+ ? options.uuidFactory
69
+ : defaultUuidFactory;
70
+ let collected;
71
+ try {
72
+ collected = collectDefensiveIndicatorCandidates(records, MAX_MISP_ATTRIBUTES);
73
+ }
74
+ catch (cause) {
75
+ if (cause instanceof TypeError)
76
+ throw new TypeError('MISP indicator export requires an array of Bulk results.');
77
+ throw cause;
78
+ }
79
+ const used = new Set();
80
+ const eventUuid = nextUuid(uuidFactory, used);
81
+ const attributes = collected.entries.map(({ domain, source }) => mispIndicatorAttribute(domain, record(source).availability, defensiveIndicatorProvenance(source), nextUuid(uuidFactory, used), generatedAt));
82
+ const payload = { Event: mispIndicatorEvent(eventUuid, generatedAt, attributes) };
83
+ return {
84
+ version: MISP_INDICATOR_EXPORT_VERSION,
85
+ format: 'misp',
86
+ generatedAt,
87
+ domains: collected.domains,
88
+ truncated: collected.truncated,
89
+ filename: `whoisleuth-defensive-domains-${generatedAt.slice(0, 10)}.misp.json`,
90
+ mimeType: 'application/json;charset=utf-8',
91
+ content: `${JSON.stringify(payload, null, 2)}\n`,
92
+ };
93
+ }
@@ -0,0 +1,56 @@
1
+ /** Sharing declarations are not encryption, redaction or an authority to disclose. */
2
+ export const TLP_MARKINGS = ['clear', 'green', 'amber', 'amber-strict', 'red'];
3
+ export const RECIPIENT_SCOPES = ['public', 'community', 'organization', 'named-recipients'];
4
+ const RISKY_KEYS = new Set([
5
+ 'authorization', 'cookie', 'cookies', 'credential', 'credentials', 'email', 'emails',
6
+ 'entities', 'password', 'phone', 'raw', 'rawrdap', 'rawwhois', 'registrant', 'session', 'token',
7
+ ]);
8
+ const MARKING_KEYS = new Set([
9
+ 'informationmarking', 'marking', 'markings', 'sharingmarking',
10
+ 'tlp', 'tlplabel', 'tlpmarking', 'trafficlightprotocol',
11
+ ]);
12
+ export function markingFromText(value) {
13
+ const normalized = value.toUpperCase().replace(/\s+/gu, '').replace('TLP:', '').replace('+STRICT', '-STRICT').toLowerCase();
14
+ return TLP_MARKINGS.includes(normalized) ? normalized : null;
15
+ }
16
+ export function expectedScope(marking) {
17
+ if (marking === 'clear')
18
+ return 'public';
19
+ if (marking === 'green')
20
+ return 'community';
21
+ if (marking === 'red')
22
+ return 'named-recipients';
23
+ return 'organization';
24
+ }
25
+ export function tlpLabel(marking) {
26
+ return `TLP:${marking === 'amber-strict' ? 'AMBER+STRICT' : marking.toUpperCase()}`;
27
+ }
28
+ /** Scan an already admitted, byte/node/depth-bounded JSON tree, not arbitrary objects. */
29
+ export function scanSharingMetadata(root) {
30
+ const stack = [{ value: root, marking: false }];
31
+ const markings = new Set();
32
+ let riskyKeyCount = 0;
33
+ while (stack.length) {
34
+ const next = stack.pop();
35
+ if (typeof next.value === 'string') {
36
+ if (next.marking) {
37
+ const marking = markingFromText(next.value);
38
+ if (marking)
39
+ markings.add(marking);
40
+ }
41
+ }
42
+ else if (Array.isArray(next.value)) {
43
+ for (const value of next.value)
44
+ stack.push({ value, marking: next.marking });
45
+ }
46
+ else if (next.value && typeof next.value === 'object') {
47
+ for (const [key, value] of Object.entries(next.value)) {
48
+ const normalized = key.toLowerCase().replace(/[^a-z0-9]/gu, '');
49
+ if (RISKY_KEYS.has(normalized))
50
+ riskyKeyCount++;
51
+ stack.push({ value, marking: MARKING_KEYS.has(normalized) });
52
+ }
53
+ }
54
+ }
55
+ return { importedMarkings: [...markings], riskyKeyCount };
56
+ }
@@ -0,0 +1,129 @@
1
+ // Pure STIX 2.1 interchange for locally reviewed Bulk candidates. Direct
2
+ // observations and heuristic inferences remain separate objects so consumers
3
+ // do not mistake a Risk score for confirmed maliciousness.
4
+ import { collectDefensiveIndicatorCandidates, defensiveIndicatorProvenance, } from "./defensive-indicator-export.mjs";
5
+ import { STIX_INDICATOR_EXPORT_VERSION } from "../contracts/analyst-interchange.mjs";
6
+ export { STIX_INDICATOR_EXPORT_VERSION } from "../contracts/analyst-interchange.mjs";
7
+ export const MAX_STIX_INDICATORS = 1000;
8
+ const UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
9
+ const CONTROL_RE = /[\x00-\x1f\x7f]/;
10
+ const WARNING = 'Heuristic finding; review before operational use because false positives are possible.';
11
+ function record(value) {
12
+ return value && typeof value === 'object' && !Array.isArray(value)
13
+ ? value
14
+ : {};
15
+ }
16
+ function isoTimestamp(value) {
17
+ if (typeof value !== 'string' || value.length > 64 || CONTROL_RE.test(value))
18
+ return null;
19
+ const parsed = Date.parse(value);
20
+ return Number.isFinite(parsed) ? new Date(parsed).toISOString() : null;
21
+ }
22
+ function defaultIdFactory(type) {
23
+ const uuid = globalThis.crypto?.randomUUID?.();
24
+ if (!uuid)
25
+ throw new Error('Secure random identifiers are unavailable for the STIX export.');
26
+ return `${type}--${uuid}`;
27
+ }
28
+ function stixId(type, idFactory) {
29
+ const value = idFactory(type);
30
+ const prefix = `${type}--`;
31
+ if (typeof value !== 'string' || !value.startsWith(prefix) || !UUID_RE.test(value.slice(prefix.length))) {
32
+ throw new Error(`The STIX identifier factory returned an invalid ${type} identifier.`);
33
+ }
34
+ return value.toLowerCase();
35
+ }
36
+ export function stixIndicatorProducer(producerId, generatedAt) {
37
+ return {
38
+ type: 'identity', spec_version: '2.1', id: producerId,
39
+ created: generatedAt, modified: generatedAt,
40
+ name: 'WHOISleuth', identity_class: 'system',
41
+ description: 'Producer of locally generated defensive-domain observations and heuristic inferences.',
42
+ x_whoisleuth_export_version: STIX_INDICATOR_EXPORT_VERSION,
43
+ x_whoisleuth_generated_at: generatedAt,
44
+ x_whoisleuth_false_positive_warning: WARNING,
45
+ };
46
+ }
47
+ /** The caller owns validated candidates and either fresh or retained identities. */
48
+ export function stixIndicatorObjects(domain, availability, provenance, generatedAt, producerId, nextId) {
49
+ const { observedAt, riskScore: score, riskModelVersion: modelVersion } = provenance;
50
+ const domainId = nextId('domain-name');
51
+ const observationId = nextId(observedAt ? 'observed-data' : 'note');
52
+ const indicatorId = nextId('indicator');
53
+ const observationContext = {
54
+ created_by_ref: producerId, created: generatedAt, modified: generatedAt,
55
+ x_whoisleuth_observed_at_basis: observedAt ? 'scan' : 'unknown',
56
+ x_whoisleuth_source: 'bulk', x_whoisleuth_availability: availability,
57
+ x_whoisleuth_scan_depth: provenance.scanDepth,
58
+ };
59
+ const objects = [
60
+ { type: 'domain-name', spec_version: '2.1', id: domainId, value: domain },
61
+ observedAt ? {
62
+ type: 'observed-data', spec_version: '2.1', id: observationId, ...observationContext,
63
+ first_observed: observedAt, last_observed: observedAt, number_observed: 1, object_refs: [domainId],
64
+ x_whoisleuth_evidence_kind: 'direct-observation',
65
+ } : {
66
+ type: 'note', spec_version: '2.1', id: observationId, ...observationContext,
67
+ content: 'Observation time was not retained. This candidate is supplied for heuristic review; its export time is not a sighting time.',
68
+ object_refs: [domainId, indicatorId], x_whoisleuth_evidence_kind: 'observation-context',
69
+ },
70
+ {
71
+ type: 'indicator', spec_version: '2.1', id: indicatorId,
72
+ created_by_ref: producerId, created: generatedAt, modified: generatedAt,
73
+ name: `Heuristic domain candidate: ${domain}`, description: WARNING,
74
+ pattern: `[domain-name:value = '${domain}']`, pattern_type: 'stix', pattern_version: '2.1',
75
+ valid_from: generatedAt, x_whoisleuth_validity_basis: 'export',
76
+ labels: ['heuristic', 'defensive-review'], x_whoisleuth_evidence_kind: 'heuristic-inference',
77
+ x_whoisleuth_risk_score: score,
78
+ ...(modelVersion === null ? {} : { x_whoisleuth_risk_model_version: modelVersion }),
79
+ x_whoisleuth_false_positive_warning: WARNING,
80
+ },
81
+ ];
82
+ if (observedAt)
83
+ objects.push({
84
+ type: 'relationship', spec_version: '2.1', id: nextId('relationship'),
85
+ created_by_ref: producerId, created: generatedAt, modified: generatedAt,
86
+ relationship_type: 'based-on', source_ref: indicatorId, target_ref: observationId,
87
+ description: 'The heuristic Indicator is based on the separately represented domain observation.',
88
+ });
89
+ return objects;
90
+ }
91
+ export function buildStixIndicatorExport(records, options = {}) {
92
+ const generatedAt = isoTimestamp(options.generatedAt) || new Date().toISOString();
93
+ const idFactory = typeof options.idFactory === 'function'
94
+ ? options.idFactory
95
+ : defaultIdFactory;
96
+ let collected;
97
+ try {
98
+ collected = collectDefensiveIndicatorCandidates(records, MAX_STIX_INDICATORS);
99
+ }
100
+ catch (cause) {
101
+ if (cause instanceof TypeError)
102
+ throw new TypeError('STIX indicator export requires an array of Bulk results.');
103
+ throw cause;
104
+ }
105
+ const usedIds = new Set();
106
+ const nextId = (type) => {
107
+ const id = stixId(type, idFactory);
108
+ if (usedIds.has(id))
109
+ throw new Error('The STIX identifier factory returned a duplicate identifier.');
110
+ usedIds.add(id);
111
+ return id;
112
+ };
113
+ const producerId = nextId('identity');
114
+ const objects = [stixIndicatorProducer(producerId, generatedAt)];
115
+ for (const { domain, source } of collected.entries) {
116
+ objects.push(...stixIndicatorObjects(domain, record(source).availability, defensiveIndicatorProvenance(source), generatedAt, producerId, nextId));
117
+ }
118
+ const bundle = { type: 'bundle', id: nextId('bundle'), objects };
119
+ return {
120
+ version: STIX_INDICATOR_EXPORT_VERSION,
121
+ format: 'stix',
122
+ generatedAt,
123
+ domains: collected.domains,
124
+ truncated: collected.truncated,
125
+ filename: `whoisleuth-defensive-domains-${generatedAt.slice(0, 10)}.stix.json`,
126
+ mimeType: 'application/stix+json;charset=utf-8',
127
+ content: `${JSON.stringify(bundle, null, 2)}\n`,
128
+ };
129
+ }