@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
@@ -1 +1 @@
1
- export { MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../lib/bounded-json.mjs";
1
+ export { boundedJsonLimitsForBytes, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../packages/analysis/bounded-json.mjs";
@@ -2,7 +2,7 @@ import { createHash } from 'node:crypto';
2
2
  import { lstat } from 'node:fs/promises';
3
3
  import { recordOrNull } from "../lib/bounded-contract-normalizers.mjs";
4
4
  import { readBoundedRegularTextFile } from "../lib/bounded-file.mjs";
5
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
5
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
6
6
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
7
7
  import { boundedCliInputError, CliUsageError } from "./errors.mjs";
8
8
  import { writePrivateFile } from "./output-file.mjs";
@@ -28,12 +28,12 @@ async function runBulkCommand(args, dependencies, context) {
28
28
  catch (error) {
29
29
  if (error instanceof CliUsageError)
30
30
  throw error;
31
- throw new CliUsageError(`Could not read bulk input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
31
+ throw new CliUsageError(`Could not read bulk input: ${boundedCliErrorMessage(error, 'Input could not be read')}`, 'input_unavailable');
32
32
  }
33
33
  const parsed = parseBulkQueries(input, { deep: args.deep });
34
34
  if (args.plan) {
35
35
  const document = buildCollectionPreflight({
36
- command: 'bulk', targetCount: parsed.queries.length, targetLimit: args.deep ? 50 : 500,
36
+ command: 'bulk', targetCount: parsed.queries.length,
37
37
  deep: args.deep, concurrency: args.concurrency, output: args.output, checkpoint: false,
38
38
  });
39
39
  context.writeStdout(args.output === 'json' ? formatJsonDocument(document) : context.terminal(formatCollectionPreflight(document), args.color));
@@ -113,6 +113,8 @@ async function runBulkCommand(args, dependencies, context) {
113
113
  context.writeStdout(formatJsonLines(selectedItems, metadata));
114
114
  else if (args.output === 'csv')
115
115
  context.writeStdout(formatBulkCsv(selectedItems));
116
+ else if (args.output === 'csv_metadata')
117
+ context.writeStdout(formatBulkCsv(selectedItems, selectedDocument));
116
118
  else if (args.output === 'domains')
117
119
  context.writeStdout(formatBulkDomainList(selectedItems));
118
120
  else if (args.output === 'queries')
@@ -1,6 +1,25 @@
1
1
  import { cliCsvCell } from "./csv.mjs";
2
+ import { lookupDiagnosticStates } from "../lib/lookup-diagnostics.mjs";
3
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
2
4
  const REGISTERED_STATES = new Set(['expiring', 'for_sale', 'registered']);
3
5
  const MAX_DNS_VALUES_PER_TYPE = 100;
6
+ const BULK_CSV_METADATA_COLUMNS = Object.freeze([
7
+ 'source_schema', 'source_version', 'observed_at', 'report_generated_at',
8
+ 'collection_origin', 'scan_mode', 'diagnostics_version', 'source_health',
9
+ ]);
10
+ function bulkCsvMetadataValues(item, metadata) {
11
+ const diagnostics = record(item.diagnostics);
12
+ return [
13
+ metadata.schema,
14
+ metadata.version,
15
+ normalizeExplicitIsoTimestamp(item.observedAt) ?? 'unknown',
16
+ normalizeExplicitIsoTimestamp(metadata.generatedAt) ?? 'unknown',
17
+ item.collectionOrigin === 'current_run' || item.collectionOrigin === 'resumed_checkpoint' ? item.collectionOrigin : 'unknown',
18
+ metadata.mode,
19
+ Number.isSafeInteger(diagnostics.version) && Number(diagnostics.version) > 0 ? diagnostics.version : 'unknown',
20
+ JSON.stringify(lookupDiagnosticStates(item.diagnostics)),
21
+ ];
22
+ }
4
23
  function record(value) {
5
24
  return value && typeof value === 'object' && !Array.isArray(value)
6
25
  ? value
@@ -78,12 +97,18 @@ function bulkDnsSummary(item) {
78
97
  function booleanCell(value) {
79
98
  return value === null ? 'unknown' : value ? 'observed' : 'not_observed';
80
99
  }
81
- function formatBulkCsv(items) {
100
+ function formatBulkCsv(items, metadata) {
101
+ if (metadata && metadata.results.length !== items.length)
102
+ throw new TypeError('CSV metadata does not match the selected result count.');
82
103
  const header = [
83
104
  'query', 'domain', 'outcome', 'availability', 'confidence', 'dns_status',
84
105
  'a', 'aaaa', 'ns', 'mx', 'null_mx', 'spf', 'dmarc', 'error',
106
+ ...(metadata ? BULK_CSV_METADATA_COLUMNS : []),
85
107
  ];
86
- const rows = items.map((item) => {
108
+ const rows = items.map((item, index) => {
109
+ const retained = metadata?.results[index];
110
+ if (metadata && (!retained || retained.index !== item.index || retained.query !== item.query))
111
+ throw new TypeError('CSV metadata does not match the selected result identity.');
87
112
  const availability = item.ok ? record(record(item.result).availability) : {};
88
113
  const dns = bulkDnsSummary(item);
89
114
  return [
@@ -101,6 +126,7 @@ function formatBulkCsv(items) {
101
126
  booleanCell(dns.hasSpf),
102
127
  booleanCell(dns.hasDmarc),
103
128
  item.ok ? '' : item.error,
129
+ ...(metadata && retained ? bulkCsvMetadataValues(retained, metadata) : []),
104
130
  ].map(cliCsvCell).join(',');
105
131
  });
106
132
  return `${[header.join(','), ...rows].join('\n')}\n`;
@@ -114,4 +140,4 @@ function formatBulkDomainList(items) {
114
140
  function formatBulkQueryList(items) {
115
141
  return items.length ? `${items.map((item) => item.query).join('\n')}\n` : '';
116
142
  }
117
- export { REGISTERED_STATES, availabilityState, bulkDnsSummary, formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems, };
143
+ export { REGISTERED_STATES, BULK_CSV_METADATA_COLUMNS, availabilityState, bulkDnsSummary, bulkCsvMetadataValues, formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems, };
package/cli/bulk.mjs CHANGED
@@ -1,8 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { abortable } from "../lib/abort.mjs";
3
3
  import { decodeBoundedUtf8 } from "../lib/bounded-file.mjs";
4
- import { CliUsageError } from "./arguments.mjs";
5
- import { hasUnsafeCliText } from "./errors.mjs";
4
+ import { CliUsageError, hasUnsafeCliText } from "./errors.mjs";
6
5
  import { MAX_DEEP_BULK_QUERIES, MAX_FAST_BULK_QUERIES, } from "../lib/bulk-limits.mjs";
7
6
  const MAX_BULK_INPUT_BYTES = 1024 * 1024;
8
7
  async function readTextStreamBounded(stream, limit = MAX_BULK_INPUT_BYTES, signal) {
@@ -65,7 +64,7 @@ function parseBulkQueries(text, { deep = false } = {}) {
65
64
  }
66
65
  }
67
66
  if (!queries.length)
68
- throw new CliUsageError('Bulk input did not contain any queries.');
67
+ throw new CliUsageError('Bulk input did not contain any queries.', 'missing_input');
69
68
  return { queries, duplicates, limit };
70
69
  }
71
70
  function boundedLookupError(error) {
@@ -0,0 +1,212 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "../packages/analysis/bounded-json.mjs";
3
+ import { canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
4
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
5
+ import { MAX_CASE_CHECKPOINT_FACTS, MAX_CASE_OBJECTIVE_LENGTH, MAX_CASE_STORE_BYTES, MAX_EDITABLE_CASE_INPUT_BYTES, MAX_NOTE_LENGTH, MAX_RESPONSE_RATIONALE_LENGTH, MAX_RESPONSE_VALUE_LENGTH } from "../packages/contracts/case-portability.mjs";
6
+ import { readEditableCaseExport } from "../packages/cases/case-export-input.mjs";
7
+ import { createCaseIncident, openOrCreateCase, recordCaseConclusion, recordCaseRecheckOutcome, updateCase } from "../packages/cases/case-record-operations.mjs";
8
+ import { buildCaseExport, serializeCaseStore } from "../packages/cases/case-storage-model.mjs";
9
+ import { appendCaseEvidencePin } from "../packages/cases/case-response-model.mjs";
10
+ import { readCaseRecheckAnswerContext } from "../packages/cases/case-recheck-model.mjs";
11
+ import { caseEvidenceLinkIssues, mergeCaseEvidenceLinks } from "../packages/cases/case-evidence-links.mjs";
12
+ import { dispositionLabel, statusLabel } from "../packages/cases/case-record-decisions.mjs";
13
+ import { normalizeDomain } from "../packages/cases/case-record-core.mjs";
14
+ import { safeTerminalValue } from "./formatters/terminal.mjs";
15
+ import { CliUsageError } from "./errors.mjs";
16
+ import EXIT_CODES from "./exit-codes.mjs";
17
+ function fields(input, allowed, label) {
18
+ if (Object.keys(input).some(key => !allowed.includes(key)))
19
+ throw new CliUsageError(`${label} contains an unsupported field.`);
20
+ }
21
+ function prose(input, maximum, label) {
22
+ if (typeof input !== 'string' || !input.trim() || input.trim().length > maximum
23
+ || /[\u0000-\u0008\u000b\u000c\u000e-\u001f\u007f]/u.test(input)) {
24
+ throw new CliUsageError(`${label} must contain 1–${maximum} characters without unsafe control characters.`);
25
+ }
26
+ return input.trim();
27
+ }
28
+ function selection(cases, args, required) {
29
+ const domain = args.domain === null ? null : normalizeDomain(args.domain);
30
+ if (args.domain !== null && !domain)
31
+ throw new CliUsageError('Select a valid Case domain.');
32
+ const selected = cases.filter(record => (!args.caseId || record.id === args.caseId) && (!domain || record.domain === domain));
33
+ if ((args.caseId || domain || required) && selected.length === 0)
34
+ throw new CliUsageError('The selected Case is not present in this file.');
35
+ if (required && selected.length !== 1)
36
+ throw new CliUsageError('Select exactly one Case with --case-id; no arbitrary Case was chosen.');
37
+ return selected;
38
+ }
39
+ function pin(input, now) {
40
+ if (!input || typeof input !== 'object' || Array.isArray(input))
41
+ throw new CliUsageError('An evidence pin must be a JSON object.');
42
+ const raw = input;
43
+ if (Object.hasOwn(raw, 'id') || Object.hasOwn(raw, 'createdAt'))
44
+ throw new CliUsageError('New pin identity and creation time are allocated by the Case operation.');
45
+ const normal = appendCaseEvidencePin([], raw, now)[0];
46
+ // Validate the supplied subset against the domain result, without duplicating
47
+ // its field inventory or accepting silent truncation/default substitution.
48
+ for (const [key, value] of Object.entries(raw)) {
49
+ if (!Object.hasOwn(normal, key) || canonicalArtifactJsonV2(value) !== canonicalArtifactJsonV2(normal[key])) {
50
+ throw new CliUsageError(`Pin field ${safeTerminalValue(key)} is not canonical or exceeds its bound. Use explicit ISO timestamps and supported values; no pin was saved.`);
51
+ }
52
+ }
53
+ return raw;
54
+ }
55
+ function retainExistingEntries(before, after) {
56
+ if (canonicalArtifactJsonV2(mergeCaseEvidenceLinks(before.evidenceLinks, after.evidenceLinks) ?? [])
57
+ !== canonicalArtifactJsonV2(mergeCaseEvidenceLinks(undefined, after.evidenceLinks) ?? [])) {
58
+ throw new CliUsageError('The operation would remove retained evidence relationships. Nothing was written.');
59
+ }
60
+ const pairs = [
61
+ [before.notes, after.notes], [before.evidenceHistory, after.evidenceHistory], [before.evidencePins, after.evidencePins],
62
+ [before.decisions, after.decisions], [before.actions, after.actions], [before.assertions, after.assertions],
63
+ [before.manualTrail, after.manualTrail], [before.sightings, after.sightings],
64
+ [before.observedEffects.reviews, after.observedEffects.reviews], [before.closures.records, after.closures.records],
65
+ [before.branches ?? [], after.branches ?? []], [before.attachments ?? [], after.attachments ?? []],
66
+ ];
67
+ for (const [prior, next] of pairs) {
68
+ const retained = new Set(next.map(value => canonicalArtifactJsonV2(value)));
69
+ if (prior.some(value => !retained.has(canonicalArtifactJsonV2(value)))) {
70
+ throw new CliUsageError('The operation would remove or alter earlier retained evidence. Export a smaller working set or review it in the console; nothing was written.');
71
+ }
72
+ }
73
+ }
74
+ export function applyCliCaseOperation(cases, args, input, note, now) {
75
+ if (args.operation === 'open') {
76
+ const title = args.title === null ? undefined : prose(args.title, MAX_CASE_OBJECTIVE_LENGTH, 'Incident title');
77
+ const result = args.newIncident
78
+ ? createCaseIncident(cases, { domain: args.domain, title }, now)
79
+ : openOrCreateCase(cases, { domain: args.domain, ...(title ? { title } : {}) }, now, args.caseId ? { caseId: args.caseId } : {});
80
+ if (!result.created && title && title !== result.record.title)
81
+ throw new CliUsageError('The selected Case already exists with another title. Use --new-incident for a separate investigation.');
82
+ return result.cases;
83
+ }
84
+ const current = selection(cases, args, true)[0];
85
+ let result;
86
+ if (args.operation === 'note') {
87
+ result = updateCase(cases, current.id, { note: prose(note, MAX_NOTE_LENGTH, 'Case note') }, now);
88
+ }
89
+ else if (args.operation === 'pin') {
90
+ result = updateCase(cases, current.id, { evidencePin: pin(input, now) }, now);
91
+ }
92
+ else if (args.operation === 'link' || args.operation === 'withdraw-link') {
93
+ result = updateCase(cases, current.id, args.operation === 'link' ? { evidenceLink: input } : { evidenceLinkWithdrawal: input }, now);
94
+ }
95
+ else if (args.operation === 'assess') {
96
+ if (!input)
97
+ throw new CliUsageError('An assessment requires JSON input.');
98
+ fields(input, ['disposition', 'reviewReasonCode', 'summary', 'rationale', 'evidence'], 'Assessment');
99
+ if (!Array.isArray(input.evidence) || !input.evidence.length || input.evidence.length > MAX_CASE_CHECKPOINT_FACTS) {
100
+ throw new CliUsageError(`Select 1–${MAX_CASE_CHECKPOINT_FACTS} evidence facts for the assessment.`);
101
+ }
102
+ const evidence = input.evidence.map(value => {
103
+ if (!value || typeof value !== 'object' || Array.isArray(value))
104
+ throw new CliUsageError('Each assessment fact needs a pin and stance.');
105
+ fields(value, ['pin', 'pinId', 'stance'], 'Assessment evidence');
106
+ if (!['supports', 'contradicts', 'unresolved'].includes(value.stance))
107
+ throw new CliUsageError('Use supports, contradicts or unresolved for each evidence stance.');
108
+ if (Object.hasOwn(value, 'pin') === Object.hasOwn(value, 'pinId'))
109
+ throw new CliUsageError('Select one new pin or one retained pinId for each assessment fact.');
110
+ return Object.hasOwn(value, 'pinId') ? { pinId: value.pinId, stance: value.stance } : { pin: pin(value.pin, now), stance: value.stance };
111
+ });
112
+ const summary = prose(input.summary, MAX_RESPONSE_VALUE_LENGTH, 'Assessment summary');
113
+ const rationale = prose(input.rationale, MAX_RESPONSE_RATIONALE_LENGTH, 'Assessment rationale');
114
+ result = recordCaseConclusion(cases, current.id, { disposition: input.disposition, reviewReasonCode: input.reviewReasonCode, summary, rationale, evidence }, now);
115
+ if (result.record.decisions.at(-1)?.summary !== summary || result.record.decisions.at(-1)?.rationale !== rationale) {
116
+ throw new CliUsageError('Assessment text needs canonical single-line values; no assessment was written.');
117
+ }
118
+ }
119
+ else if (args.operation === 'recheck') {
120
+ if (!input)
121
+ throw new CliUsageError('A recheck requires JSON input.');
122
+ fields(input, ['state', 'observedAt', 'completeness', 'comparisonSummary', 'source', 'followUpAt', 'limitations', 'collectionDepth', 'recheck', 'observationHostname'], 'Recheck');
123
+ const recheck = readCaseRecheckAnswerContext(input.recheck);
124
+ if (input.state === 'not_reproduced' && !recheck) {
125
+ throw new CliUsageError('Not reproduced requires a saved recheck question and confirmed comparable conditions. Import the planned Case or record unavailable for a limited observation.');
126
+ }
127
+ const summary = prose(input.comparisonSummary, MAX_RESPONSE_VALUE_LENGTH, 'Comparison summary');
128
+ result = recordCaseRecheckOutcome(cases, current.id, {
129
+ ...input, state: input.state, observedAt: input.observedAt, completeness: input.completeness,
130
+ comparisonSummary: summary, source: input.source, ...(recheck ? { recheck } : {}),
131
+ }, now);
132
+ const review = result.record.observedEffects.reviews.at(-1);
133
+ const comparison = result.record.evidencePins.find(value => value.id === review.evidencePinId);
134
+ const projected = { state: review.state, observedAt: review.observedAt, completeness: review.completeness,
135
+ comparisonSummary: comparison.value, source: review.source, followUpAt: review.followUpAt,
136
+ limitations: review.limitations, collectionDepth: comparison.collectionDepth,
137
+ ...(review.recheck ? { recheck: review.recheck } : {}),
138
+ ...(comparison.observationHostname ? { observationHostname: comparison.observationHostname } : {}),
139
+ };
140
+ for (const [key, value] of Object.entries(input)) {
141
+ if (canonicalArtifactJsonV2(value) !== canonicalArtifactJsonV2(projected[key]))
142
+ throw new CliUsageError(`Recheck field ${safeTerminalValue(key)} would be repaired or truncated; nothing was written.`);
143
+ }
144
+ }
145
+ else
146
+ throw new CliUsageError('Select a supported Case mutation.');
147
+ retainExistingEntries(current, result.record);
148
+ return result.cases;
149
+ }
150
+ function formatCases(cases, digest) {
151
+ const lines = ['Local Case review', `File digest ${digest}`, `Cases ${cases.length}`, ''];
152
+ for (const record of cases) {
153
+ lines.push(`Case ${safeTerminalValue(record.id)} · ${safeTerminalValue(record.title || record.domain)}`, `Domain ${safeTerminalValue(record.domain)}`, `Status ${statusLabel(record.status)}`, `Disposition ${dispositionLabel(record.disposition)} (analyst assessment)`, `Updated ${record.updatedAt}`);
154
+ for (const note of record.notes)
155
+ lines.push(`Note ${note.createdAt}`, safeTerminalValue(note.body, '—', MAX_NOTE_LENGTH));
156
+ for (const pin of record.evidencePins)
157
+ lines.push(`Pin ${safeTerminalValue(pin.id)} · ${safeTerminalValue(pin.label)}`, safeTerminalValue(pin.value, '—', MAX_RESPONSE_VALUE_LENGTH), ` Source ${safeTerminalValue(pin.source)} · ${safeTerminalValue(pin.observedAt, 'time unavailable')} · ${pin.completeness}`);
158
+ for (const decision of record.decisions)
159
+ lines.push(`Assessment ${decision.createdAt}`, safeTerminalValue(decision.summary, '—', MAX_RESPONSE_VALUE_LENGTH), safeTerminalValue(decision.rationale, '—', MAX_RESPONSE_RATIONALE_LENGTH));
160
+ for (const { link, missingPinIds, cyclic } of caseEvidenceLinkIssues(record.evidenceLinks ?? [], record.evidencePins)) {
161
+ lines.push(`Evidence relationship ${safeTerminalValue(link.id)} · analyst declaration`, ` ${safeTerminalValue(link.fromPinId)} ${link.kind.replaceAll('_', ' ')} ${safeTerminalValue(link.toPinId)}`, ` ${safeTerminalValue(link.basis, '—', MAX_RESPONSE_RATIONALE_LENGTH)}`);
162
+ if (link.withdrawal)
163
+ lines.push(` Withdrawn ${link.withdrawal.at}: ${safeTerminalValue(link.withdrawal.reason, '—', MAX_RESPONSE_RATIONALE_LENGTH)}`);
164
+ if (missingPinIds.length)
165
+ lines.push(` Referenced pins not retained: ${missingPinIds.map(id => safeTerminalValue(id)).join(', ')}`);
166
+ if (cyclic)
167
+ lines.push(' Conflicting imported derivation cycle; no order is inferred.');
168
+ }
169
+ for (const review of record.observedEffects.reviews)
170
+ lines.push(`Recheck ${safeTerminalValue(review.observedAt, 'time unavailable')} · ${review.state} · ${review.completeness} · ${safeTerminalValue(review.source)}`);
171
+ lines.push(`Other retained records: ${record.evidenceHistory.length} snapshots, ${record.actions.length} actions, ${record.assertions.length} assertions, ${record.attachments?.length ?? 0} file references.`, '');
172
+ }
173
+ lines.push('Use --json for the full Case export. Attached file bytes remain separate. This command makes no request.', '');
174
+ return lines.join('\n');
175
+ }
176
+ export async function runCaseCommand(args, dependencies, context) {
177
+ context.setFailureLabel('Local Case operation');
178
+ try {
179
+ const raw = args.source ? dependencies.caseFileInput ?? await context.readInput(args.source, MAX_EDITABLE_CASE_INPUT_BYTES, 'Case file') : null;
180
+ const digest = raw === null ? null : `sha256:${createHash('sha256').update(raw).digest('hex')}`;
181
+ if (args.expectedFileDigest && args.expectedFileDigest !== digest)
182
+ throw new CliUsageError('The Case file differs from the reviewed file digest. Nothing was written.');
183
+ const cases = raw === null ? [] : readEditableCaseExport(raw);
184
+ const now = normalizeExplicitIsoTimestamp(context.now());
185
+ if (!now)
186
+ throw new CliUsageError('A valid current timestamp is required.');
187
+ if (args.operation === 'show') {
188
+ const selected = selection(cases, args, false);
189
+ context.writeStdout(args.output === 'json' ? `${JSON.stringify(buildCaseExport(selected, now))}\n` : context.terminal(formatCases(selected, digest), args.color));
190
+ return EXIT_CODES.SUCCESS;
191
+ }
192
+ const input = args.inputSource ? parseBoundedJsonObject(await context.readInput(args.inputSource, MAX_EDITABLE_CASE_INPUT_BYTES, 'Case operation input'), {
193
+ maximumBytes: MAX_EDITABLE_CASE_INPUT_BYTES, limits: boundedJsonLimitsForBytes(MAX_CASE_STORE_BYTES), label: 'Case operation input',
194
+ }) : null;
195
+ const note = args.noteSource ? await context.readInput(args.noteSource, MAX_NOTE_LENGTH * 4 + 4, 'Case note') : args.text;
196
+ const next = applyCliCaseOperation(cases, args, input, note, now);
197
+ if (Buffer.byteLength(serializeCaseStore(next)) > MAX_CASE_STORE_BYTES)
198
+ throw new CliUsageError('The updated Case store exceeds its byte budget. No evidence was pruned and nothing was written.');
199
+ const output = `${JSON.stringify(buildCaseExport(next, now))}\n`;
200
+ // Apply the same strict admission to our output before atomic publication.
201
+ readEditableCaseExport(output);
202
+ dependencies.signal?.throwIfAborted();
203
+ context.writeStdout(output);
204
+ return EXIT_CODES.SUCCESS;
205
+ }
206
+ catch (cause) {
207
+ dependencies.signal?.throwIfAborted();
208
+ if (cause instanceof CliUsageError)
209
+ throw cause;
210
+ throw new CliUsageError(cause instanceof Error ? cause.message : 'The Case operation could not be completed.');
211
+ }
212
+ }
package/cli/case-pack.mjs CHANGED
@@ -1,12 +1,17 @@
1
1
  import { createHash } from 'node:crypto';
2
2
  import { Buffer } from 'node:buffer';
3
3
  import { canonicalArtifactJson, canonicalArtifactJsonV2, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
4
- import { buildCaseReport } from "../packages/cases/case-report.mjs";
4
+ import { buildCaseReport, buildCaseReportVerificationProjection } from "../packages/cases/case-report.mjs";
5
5
  import { WHOISLEUTH_APPLICATION_VERSION } from "../lib/application-version.mjs";
6
- import { assertBoundedJsonStructure, scanBoundedJson } from "../lib/bounded-json.mjs";
7
- import { assertCaseBrandProfileIds, CASE_AUDIENCE_SENSITIVE_FIELD_NAMES, CASE_RECORD_FIELD_NAMES, caseAudienceExclusions, normalizeDomain, normalizeCaseStore, projectCaseForAudience, safeId, } from "../packages/cases/case-model.mjs";
8
- import { CASE_REPORT_SCHEMA, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS as INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS as PACKET_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS as PACKET_KEYS, CLI_CASE_PACK_REPORT_KEYS as REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS as ROOT_KEYS, MAX_CASE_IMPORT_BYTES, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, PUBLIC_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, } from "../packages/contracts/case-portability.mjs";
6
+ import { assertBoundedJsonStructure, scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
7
+ import { assertCaseBrandProfileIds } from "../packages/cases/case-brand-profile-references.mjs";
8
+ import { CASE_AUDIENCE_SENSITIVE_FIELD_NAMES, caseAudienceExclusions, projectCaseForAudience, } from "../packages/cases/case-record-projection.mjs";
9
+ import { normalizeDomain } from "../packages/evidence/domain-name.mjs";
10
+ import { normalizeCaseStore } from "../packages/cases/case-migration-model.mjs";
11
+ import { safeId } from "../packages/cases/case-record-core.mjs";
12
+ import { CASE_REPORT_SCHEMA, CASE_IMPORT_VERSIONS, CASE_SCHEMA_VERSION, INCIDENT_CASE_SCHEMA_VERSION, CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_INPUT_CASE_VERSIONS, CLI_CASE_PACK_CURRENT_REDACTION_KEYS, CLI_CASE_PACK_PUBLIC_REPORT_KEYS, CLI_CASE_PACK_INTEGRITY_KEYS as INTEGRITY_KEYS, CLI_CASE_PACK_LIMITATIONS as PACKET_LIMITATIONS, CLI_CASE_PACK_PACKET_KEYS as PACKET_KEYS, CLI_CASE_PACK_REPORT_KEYS as REPORT_KEYS, CLI_CASE_PACK_ROOT_KEYS as ROOT_KEYS, MAX_CASE_IMPORT_BYTES, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, PUBLIC_CASE_SCHEMA_VERSION, PUBLISHED_V2_3_CASE_SCHEMA_VERSION, caseReportVersionMatchesCase, } from "../packages/contracts/case-portability.mjs";
9
13
  import { CliUsageError } from "./errors.mjs";
14
+ import { canonicalCaseExportProjection as canonicalPackProjection } from "../packages/cases/case-export-input.mjs";
10
15
  export { CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, MAX_CASE_PACK_CASES, MAX_CASE_PACK_INPUT_BYTES, };
11
16
  const MAX_CASE_PACK_REFERENCE_SCAN_DEPTH = 64;
12
17
  function record(value) {
@@ -61,6 +66,11 @@ function allowedSensitiveFieldPath(key, path) {
61
66
  return topCasePath(path) || reportCasePath(path);
62
67
  if (key === 'notes')
63
68
  return topCasePath(path) || reportCasePath(path);
69
+ if (key === 'workflowMetadata')
70
+ return topCasePath(path) || analystResponsePath(path);
71
+ if (key === 'incidentTargets' || key === 'investigationContext') {
72
+ return path.at(-1) === 'workflowMetadata' && (topCasePath(path.slice(0, -1)) || analystResponsePath(path.slice(0, -1)));
73
+ }
64
74
  if (key === 'actions' || key === 'assertions' || key === 'manualTrail' || key === 'observedEffects' || key === 'closures' || key === 'branches') {
65
75
  return topCasePath(path) || analystResponsePath(path);
66
76
  }
@@ -125,7 +135,7 @@ function canonicalValuesMatch(left, right) {
125
135
  return false;
126
136
  }
127
137
  }
128
- function assertCanonicalCaseIdentities(cases, label) {
138
+ function assertCanonicalCaseIdentities(cases, label, caseVersion) {
129
139
  const ids = new Set();
130
140
  const domains = new Set();
131
141
  for (const value of cases) {
@@ -136,23 +146,20 @@ function assertCanonicalCaseIdentities(cases, label) {
136
146
  || typeof item.domain !== 'string'
137
147
  || normalizeDomain(item.domain) !== item.domain
138
148
  || ids.has(item.id)
139
- || domains.has(item.domain)) {
149
+ || (caseVersion < INCIDENT_CASE_SCHEMA_VERSION && domains.has(item.domain))) {
140
150
  throw new TypeError(`${label} contains a missing, unsafe, non-canonical, or duplicate Case identity.`);
141
151
  }
142
152
  ids.add(item.id);
143
153
  domains.add(item.domain);
144
154
  }
145
155
  }
146
- function assertCurrentCaseProjection(rawCases, normalised, label) {
147
- const requiredFields = new Set(CASE_RECORD_FIELD_NAMES);
156
+ function assertCurrentCaseProjection(rawCases, normalised, label, caseVersion = CASE_SCHEMA_VERSION) {
148
157
  if (rawCases.length !== normalised.length || rawCases.some((item, index) => {
149
158
  const raw = record(item);
150
159
  return !raw
151
- || Object.keys(raw).length !== requiredFields.size
152
- || Object.keys(raw).some((key) => !requiredFields.has(key))
153
160
  || !canonicalValuesMatch(raw, normalised[index]);
154
161
  })) {
155
- throw new TypeError(`${label} contains a schema ${CASE_SCHEMA_VERSION} Case that would be repaired, truncated, or otherwise changed during normalisation.`);
162
+ throw new TypeError(`${label} contains a schema ${caseVersion} Case that would be repaired, truncated, or otherwise changed during normalisation.`);
156
163
  }
157
164
  }
158
165
  /** Historical packs keep their immutable wire shape, so only their retained audience guarantees are checked here. */
@@ -196,17 +203,17 @@ function assertLegacyAudienceFields(value, normalised, audience) {
196
203
  }
197
204
  }
198
205
  }
199
- function assertCurrentAudienceProjection(value, normalised, audience) {
200
- const expected = projectCaseForAudience(normalised, audience);
201
- for (const field of CASE_RECORD_FIELD_NAMES) {
206
+ function assertCurrentAudienceProjection(value, normalised, audience, caseVersion) {
207
+ const expected = canonicalPackProjection([projectCaseForAudience(normalised, audience)], caseVersion)[0];
208
+ for (const field of Object.keys(expected)) {
202
209
  if (!canonicalValuesMatch(value[field], expected[field])) {
203
210
  throw new TypeError(`The CLI case pack contains ${field} excluded by its audience or transformed incorrectly.`);
204
211
  }
205
212
  }
206
213
  }
207
- function assertAudienceProjection(value, normalised, audience, currentSchema) {
214
+ function assertAudienceProjection(value, normalised, audience, currentSchema, caseVersion) {
208
215
  if (currentSchema)
209
- assertCurrentAudienceProjection(value, normalised, audience);
216
+ assertCurrentAudienceProjection(value, normalised, audience, caseVersion);
210
217
  else
211
218
  assertLegacyAudienceFields(value, normalised, audience);
212
219
  }
@@ -217,11 +224,11 @@ function assertCurrentReportProjection(report, rawCase) {
217
224
  if (typeof report.generatedAt !== 'string') {
218
225
  throw new TypeError('The CLI case pack contains an invalid or mismatched Case report.');
219
226
  }
220
- const expected = buildCaseReport(rawCase, {
227
+ const expected = buildCaseReportVerificationProjection(rawCase, {
221
228
  applicationVersion: reportApplicationVersion(report),
222
229
  includeNotes: false,
223
230
  generatedAt: report.generatedAt,
224
- }).json;
231
+ }, report.schemaVersion);
225
232
  if (!canonicalValuesMatch(report, expected)) {
226
233
  throw new TypeError('The CLI case pack contains an invalid or mismatched Case report projection.');
227
234
  }
@@ -241,17 +248,18 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
241
248
  throw new CliUsageError('Case-pack input must be valid bounded JSON without duplicate keys.');
242
249
  }
243
250
  const root = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
244
- if (Number.isSafeInteger(root.version) && root.version < CASE_SCHEMA_VERSION) {
245
- throw new CliUsageError(`Case schema ${String(root.version)} is retired. Export it as schema ${CASE_SCHEMA_VERSION} with the last broad-reader release before packaging; no data was changed.`);
251
+ const supportedInput = CLI_CASE_PACK_INPUT_CASE_VERSIONS.some((version) => version === root.version);
252
+ if (Number.isSafeInteger(root.version) && root.version < CASE_SCHEMA_VERSION && !supportedInput) {
253
+ throw new CliUsageError(`Case schema ${String(root.version)} cannot be packaged directly. This command accepts schemas ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(', ')}. Use a supported Case import to create a current export first; no data was changed.`);
246
254
  }
247
255
  if (Number.isSafeInteger(root.version) && root.version > CASE_SCHEMA_VERSION) {
248
256
  throw new CliUsageError(`Case schema ${String(root.version)} is newer than the supported schema ${CASE_SCHEMA_VERSION}; no data was changed.`);
249
257
  }
250
- if (root.version !== CASE_SCHEMA_VERSION || !Array.isArray(root.cases)) {
251
- throw new CliUsageError(`Case-pack input must be a well-formed WHOISleuth Case schema ${CASE_SCHEMA_VERSION} export.`);
258
+ if (!supportedInput || !Array.isArray(root.cases)) {
259
+ throw new CliUsageError(`Case-pack input must be a well-formed WHOISleuth Case export with schema ${CLI_CASE_PACK_INPUT_CASE_VERSIONS.join(' or ')}.`);
252
260
  }
253
261
  try {
254
- assertCanonicalCaseIdentities(root.cases, 'Case-pack input');
262
+ assertCanonicalCaseIdentities(root.cases, 'Case-pack input', root.version);
255
263
  for (const item of root.cases) {
256
264
  const rawCase = record(item);
257
265
  if (!rawCase || !Object.hasOwn(rawCase, 'brandProfileIds'))
@@ -275,7 +283,7 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
275
283
  throw new CliUsageError(`Case packs are limited to ${MAX_CASE_PACK_CASES} reviewed cases. Export a smaller selected set so no case is silently omitted.`);
276
284
  }
277
285
  try {
278
- assertCurrentCaseProjection(root.cases, normalised, 'Case-pack input');
286
+ assertCurrentCaseProjection(root.cases, canonicalPackProjection(normalised, root.version), 'Case-pack input', root.version);
279
287
  }
280
288
  catch (cause) {
281
289
  throw new CliUsageError(cause instanceof Error ? cause.message : `Case-pack schema ${CASE_SCHEMA_VERSION} input is not exact.`);
@@ -313,7 +321,7 @@ export function buildCliCasePack(input, options, generatedAt = new Date().toISOS
313
321
  }),
314
322
  });
315
323
  if (Buffer.byteLength(JSON.stringify(document), 'utf8') > MAX_CASE_IMPORT_BYTES) {
316
- throw new CliUsageError('The generated case pack exceeds the browser 2 MiB import limit. Select fewer cases or a more restrictive audience so no evidence is silently omitted.');
324
+ throw new CliUsageError(`The generated case pack exceeds the browser import limit of ${MAX_CASE_IMPORT_BYTES} bytes. Select fewer cases or a more restrictive audience so no evidence is silently omitted.`);
317
325
  }
318
326
  return document;
319
327
  }
@@ -382,15 +390,16 @@ export function verifyCliCasePack(input) {
382
390
  || !stringListsMatch(redactionManifest.excluded, caseAudienceExclusions(audience, root.version))) {
383
391
  throw new TypeError('The CLI case pack has an invalid audience redaction manifest.');
384
392
  }
385
- assertCanonicalCaseIdentities(root.cases, 'The CLI case pack');
393
+ assertCanonicalCaseIdentities(root.cases, 'The CLI case pack', root.version);
386
394
  const normalised = normalizeCaseStore(root).cases;
387
395
  if (normalised.length !== root.cases.length) {
388
396
  throw new TypeError('The CLI case pack contains an invalid case collection.');
389
397
  }
390
- const currentCaseSchema = root.version === CASE_SCHEMA_VERSION;
398
+ const currentCaseSchema = root.version >= PUBLISHED_V2_3_CASE_SCHEMA_VERSION;
399
+ const projected = canonicalPackProjection(normalised, root.version);
391
400
  if (currentCaseSchema)
392
- assertCurrentCaseProjection(root.cases, normalised, 'The CLI case pack');
393
- const normalisedByDomain = new Map(normalised.map((item) => [item.domain, item]));
401
+ assertCurrentCaseProjection(root.cases, projected, 'The CLI case pack', root.version);
402
+ const normalisedById = new Map(projected.map((item) => [item.id, item]));
394
403
  const caseReferenceLists = [];
395
404
  const reportReferenceLists = [];
396
405
  for (let index = 0; index < root.cases.length; index += 1) {
@@ -401,17 +410,18 @@ export function verifyCliCasePack(input) {
401
410
  || !report
402
411
  || !reportCase
403
412
  || typeof rawCase.domain !== 'string'
413
+ || typeof rawCase.id !== 'string'
404
414
  || report.schema !== CASE_REPORT_SCHEMA
405
415
  || !caseReportVersionMatchesCase(root.version, report.schemaVersion)
406
416
  || reportCase.id !== rawCase.id
407
417
  || reportCase.domain !== rawCase.domain
408
- || !normalisedByDomain.has(rawCase.domain)) {
418
+ || !normalisedById.has(rawCase.id)) {
409
419
  throw new TypeError('The CLI case pack contains an invalid or mismatched Case report.');
410
420
  }
411
- const normalisedCase = normalisedByDomain.get(rawCase.domain);
421
+ const normalisedCase = normalisedById.get(rawCase.id);
412
422
  if (!normalisedCase)
413
423
  throw new TypeError('The CLI case pack contains an invalid Case projection.');
414
- assertAudienceProjection(rawCase, normalisedCase, audience, currentCaseSchema);
424
+ assertAudienceProjection(rawCase, normalisedCase, audience, currentCaseSchema, root.version);
415
425
  try {
416
426
  if (!Object.hasOwn(rawCase, 'brandProfileIds') || !Object.hasOwn(reportCase, 'brandProfileIds'))
417
427
  throw new Error('missing');
@@ -423,6 +433,13 @@ export function verifyCliCasePack(input) {
423
433
  }
424
434
  if (currentCaseSchema)
425
435
  assertCurrentReportProjection(report, normalisedCase);
436
+ else {
437
+ assertLegacyAudienceFields(reportCase, normalisedCase, audience);
438
+ const response = record(report.analystResponse);
439
+ if (!response)
440
+ throw new TypeError('The CLI case pack contains an invalid analyst-response report.');
441
+ assertLegacyAudienceFields(response, normalisedCase, audience);
442
+ }
426
443
  }
427
444
  const omitted = redactionManifest.brandProfileReferencesOmitted;
428
445
  if (!Object.hasOwn(redactionManifest, 'brandProfileReferencesOmitted')
@@ -465,6 +482,7 @@ export function formatCliCasePack(document) {
465
482
  `Cases ${document.cases.length}`,
466
483
  `Reports ${document.packet.reports.length}`,
467
484
  `Digest ${document.integrity.digestSha256}`,
485
+ 'Included for every audience: domain evidence, Case, pin and decision identifiers, tags, decision summaries and rationale. Review these before sharing.',
468
486
  '',
469
487
  ...document.packet.redactionManifest.excluded.map((item) => `Excluded: ${item}`),
470
488
  '',
package/cli/ci-report.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { WHOISLEUTH_REQUEST_POLICY_URL } from "../lib/project-metadata.mjs";
1
+ import { WHOISLEUTH_REQUEST_POLICY_URL } from "../packages/analysis/project-metadata.mjs";
2
2
  import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA } from "../packages/contracts/domain-control-monitor.mjs";
3
3
  function record(value) {
4
4
  return value && typeof value === 'object' && !Array.isArray(value) ? value : {};
@@ -45,9 +45,12 @@ function junitCases(document) {
45
45
  return [{ name: 'WHOISleuth result', failure: 'Unsupported document for JUnit conversion.' }];
46
46
  }
47
47
  export function formatCliJunit(document) {
48
- const cases = junitCases(record(document));
48
+ const root = record(document);
49
+ const cases = junitCases(root);
49
50
  const failures = cases.filter((item) => item.failure).length;
50
- return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="WHOISleuth" tests="${cases.length}" failures="${failures}">\n${cases.map((item) => ` <testcase classname="whoisleuth" name="${xml(item.name)}">${item.failure ? `<failure message="${xml(item.failure)}" />` : ''}</testcase>`).join('\n')}\n</testsuite>\n`;
51
+ const reminders = root.schema === CLI_DOMAIN_CONTROL_MONITOR_SCHEMA
52
+ ? `\n <system-out>${xml(`Renewal reviews due: ${Number(record(record(root.review).counts).due ?? 0)}`)}</system-out>` : '';
53
+ return `<?xml version="1.0" encoding="UTF-8"?>\n<testsuite name="WHOISleuth" tests="${cases.length}" failures="${failures}">\n${cases.map((item) => ` <testcase classname="whoisleuth" name="${xml(item.name)}">${item.failure ? `<failure message="${xml(item.failure)}" />` : ''}</testcase>`).join('\n')}${reminders}\n</testsuite>\n`;
51
54
  }
52
55
  export function buildPostureSarif(document) {
53
56
  const root = record(document);