@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,107 @@
1
+ import { readBoundedRegularFile } from "../lib/bounded-file.mjs";
2
+ import { MAX_MESSAGE_INTAKE_BYTES, MESSAGE_INTAKE_INPUTS } from "../packages/contracts/message-intake.mjs";
3
+ import { reviewSelectedInputInWorker } from "./selected-input-worker.mjs";
4
+ import { reviewIdentityIncident } from "../packages/investigation/identity-incident-review.mjs";
5
+ import { selectReceiverTrust, authenticationHeaderLabel } from "../packages/investigation/mail-authentication-review.mjs";
6
+ import { CliUsageError } from "./errors.mjs";
7
+ import { formatJsonDocument } from "./formatters/json.mjs";
8
+ import EXIT_CODES from "./exit-codes.mjs";
9
+ export function formatMessageIntake(report) {
10
+ const lines = [`Offline ${report.source.kind} intake`, `Review: ${report.coverage.state} · Extracted links: ${report.links.length} · Reviewed parts: ${report.coverage.reviewedParts}`,
11
+ `Source: ${report.source.digestSha256} · ${report.source.byteLength} bytes`];
12
+ for (const identity of report.identities)
13
+ lines.push(`Part ${identity.part} ${identity.role.replaceAll('_', ' ')}: ${identity.domain}`);
14
+ for (const header of report.authenticationReview.headers) {
15
+ lines.push(`${authenticationHeaderLabel(header)} · ${header.state} · receiver trust: ${header.receiverTrust.replaceAll('_', ' ')}`);
16
+ for (const claim of header.claims)
17
+ lines.push(` ${claim.method}/${claim.methodVersion}: ${claim.result} (${claim.state})${claim.domains.map(value => ` · ${value.property}=${value.domain}`).join('')}${claim.duplicateProperties.length ? ` · ambiguous properties: ${claim.duplicateProperties.join(', ')}` : ''}`);
18
+ if (header.duplicateOf)
19
+ lines.push(` Duplicate of header ${header.duplicateOf} in this part.`);
20
+ lines.push(...header.issues.map(issue => ` ${issue}`));
21
+ }
22
+ for (const link of report.links) {
23
+ lines.push(`${link.id}: ${link.origin} · ${link.source.replaceAll('_', ' ')}${link.parentId ? ` · supplied inside ${link.parentId}` : ''}`);
24
+ if (link.displayedHostname)
25
+ lines.push(` Displayed: ${link.displayedHostname} · ${link.displayedDestination.replaceAll('_', ' ')}`);
26
+ if (link.authorisation) {
27
+ lines.push(` Authorisation: ${link.authorisation.kind.replaceAll('_', ' ')} · client ${link.authorisation.clientId ?? 'not retained or ambiguous'}`);
28
+ if (link.authorisation.scopes.length)
29
+ lines.push(` Requested scopes: ${link.authorisation.scopes.join(', ')}`);
30
+ if (link.authorisation.redirectOrigin)
31
+ lines.push(` Supplied return origin: ${link.authorisation.redirectOrigin}`);
32
+ if (link.authorisation.duplicateParameters.length)
33
+ lines.push(` Ambiguous parameters: ${link.authorisation.duplicateParameters.join(', ')}`);
34
+ }
35
+ }
36
+ if (report.actionHints.length)
37
+ lines.push(`Review wording: ${report.actionHints.map(value => value.replaceAll('_', ' ')).join(', ')}`);
38
+ if (report.documentReview) {
39
+ lines.push(`Document coverage: ${report.documentReview.state} · pages ${report.documentReview.reviewedPages}/${report.documentReview.pageCount ?? 'not a paginated review'}`);
40
+ for (const part of report.documentReview.parts)
41
+ lines.push(` ${part.id}${part.page ? ` · page ${part.page}` : ''} · ${part.kind} · ${part.identity} · ${part.digestSha256}`);
42
+ lines.push(...report.documentReview.notes);
43
+ }
44
+ if (report.harReview) {
45
+ lines.push('Recorded HTTP sequence (not replayed)');
46
+ for (const entry of report.harReview.entries)
47
+ lines.push(` ${entry.sequence}. ${entry.startedAt ?? 'time unavailable'} · ${entry.method} ${entry.origin ?? 'origin unavailable'} · status ${entry.status ?? 'unavailable'} · ${entry.mimeCategory} · ${entry.durationMs === null ? 'duration unavailable' : `${entry.durationMs} ms`}`);
48
+ if (report.harReview.invalidEntries)
49
+ lines.push(`Invalid request records omitted: ${report.harReview.invalidEntries}`);
50
+ }
51
+ if (report.identityEventReview) {
52
+ const review = report.identityEventReview;
53
+ const matches = new Map(review.comparison?.events.map(event => [event.sequence, event]));
54
+ lines.push(`Selected identity events: ${review.provider} · ${review.events.length} records · ${review.invalidEvents} invalid records omitted`);
55
+ for (const event of review.events) {
56
+ const match = matches.get(event.sequence);
57
+ lines.push(` ${event.sequence}. ${event.occurredAt ?? 'time unavailable'} · ${event.actorLabel ?? 'actor unavailable'} · ${event.kind} · ${event.result}`, ` applications ${event.applicationIds.join(', ') || 'unavailable'} · resource tenant ${event.tenantId ?? 'unavailable'} · protocol ${event.protocol}`);
58
+ if (match)
59
+ lines.push(` comparison ${match.state}: application ${match.application}, tenant ${match.tenant}, actor ${match.actor}, time ${match.time}`);
60
+ }
61
+ lines.push('Actor labels are file-local. Matching supplied fields does not establish account compromise.');
62
+ }
63
+ for (const step of report.identityRecovery.nextSteps)
64
+ lines.push(`Account response — ${step.title}: ${step.detail}`);
65
+ if (report.coverage.unreviewedAttachments)
66
+ lines.push(`Unreviewed attachments: ${report.coverage.unreviewedAttachments}`);
67
+ if (report.coverage.rejectedLinks)
68
+ lines.push(`Unsupported link values: ${report.coverage.rejectedLinks}`);
69
+ if (report.coverage.boundsReached.length)
70
+ lines.push(`Partial analysis: ${report.coverage.boundsReached.join('; ')}`);
71
+ if (report.source.kind === 'qr' && !report.coverage.reviewedParts)
72
+ lines.push('No QR text was decoded. This does not establish that the image contains no QR symbol.');
73
+ lines.push('Exact paths, queries, fragments and original message content are not included. Link relationships and authorisation parameters are supplied claims, not proof of a completed redirect or account compromise.');
74
+ return `${lines.join('\n')}\n`;
75
+ }
76
+ export async function runIntakeCommand(args, dependencies, context) {
77
+ context.setFailureLabel('Message intake');
78
+ if (MESSAGE_INTAKE_INPUTS[args.kind].binary && (!args.source || args.source === '-'))
79
+ throw new CliUsageError('Binary intake requires a selected file; binary stdin is not accepted.');
80
+ let bytes;
81
+ if (dependencies.readBinaryArtifactInput && args.source && args.source !== '-')
82
+ bytes = await dependencies.readBinaryArtifactInput(args.source);
83
+ else if (args.source && args.source !== '-')
84
+ bytes = await readBoundedRegularFile(args.source, { maximumBytes: MAX_MESSAGE_INTAKE_BYTES, minimumBytes: 1, label: 'Selected input', ...(dependencies.signal ? { signal: dependencies.signal } : {}) });
85
+ else
86
+ bytes = new TextEncoder().encode(await context.readInput(args.source, MAX_MESSAGE_INTAKE_BYTES, 'Selected input'));
87
+ try {
88
+ const result = await reviewSelectedInputInWorker(bytes, args.kind, context.now(), dependencies.signal);
89
+ let authenticationReview;
90
+ try {
91
+ authenticationReview = selectReceiverTrust(result.report.authenticationReview, args.trustedAuthHeaders ?? []);
92
+ }
93
+ catch (cause) {
94
+ if (cause instanceof TypeError)
95
+ throw new CliUsageError(cause.message);
96
+ throw cause;
97
+ }
98
+ const report = { ...result.report, authenticationReview, identityRecovery: reviewIdentityIncident({ reportedActions: args.reportedActions }) };
99
+ dependencies.signal?.throwIfAborted();
100
+ if (!args.quiet)
101
+ context.writeStdout(args.output === 'json' ? formatJsonDocument(report) : context.terminal(formatMessageIntake(report), args.color));
102
+ return args.strictExit && report.coverage.state === 'partial' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
103
+ }
104
+ finally {
105
+ bytes.fill(0);
106
+ }
107
+ }
@@ -1,171 +1,13 @@
1
1
  import { classifyQuery, isDirectLookupTarget } from "../lib/classify.mjs";
2
- import { StringDecoder } from 'node:string_decoder';
3
- import { boundedCliErrorMessage, CliUsageError, hasUnsafeCliText } from "./errors.mjs";
2
+ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
4
3
  import { INVESTIGATION_PLAN_RECIPE_LABELS, INVESTIGATION_PLAN_RECIPES, } from "./investigation-plan.mjs";
5
4
  import { buildCliLookupPlan, formatCliLookupPlan } from "./lookup-plan.mjs";
6
5
  import { canBrowseLookup, } from "./lookup-browser.mjs";
7
- const MAX_INTERACTIVE_ANSWER_SCALARS = 1_024;
8
- const MAX_INTERACTIVE_ANSWER_BYTES = 4_096;
6
+ import { MAX_INTERACTIVE_ANSWER_BYTES, MAX_INTERACTIVE_ANSWER_SCALARS, boundedInteractiveAnswer, readBoundedInteractiveLine } from "./terminal-input.mjs";
9
7
  function canLaunchInteractiveCli(input, output, environment = process.env) {
10
8
  return input?.isTTY === true
11
9
  && canBrowseLookup(input, output, environment);
12
10
  }
13
- function boundedInteractiveAnswer(value) {
14
- const supplied = typeof value === 'string' ? value : '';
15
- if (Buffer.byteLength(supplied, 'utf8') > MAX_INTERACTIVE_ANSWER_BYTES
16
- || Array.from(supplied).length > MAX_INTERACTIVE_ANSWER_SCALARS
17
- || hasUnsafeCliText(supplied)) {
18
- throw new CliUsageError('Interactive input must be bounded text without control characters.');
19
- }
20
- return supplied.trim();
21
- }
22
- function abortError() {
23
- return new DOMException('Aborted', 'AbortError');
24
- }
25
- function readBoundedInteractiveLine(prompt, options) {
26
- const { input, output, signal } = options;
27
- if (signal?.aborted)
28
- return Promise.reject(signal.reason || abortError());
29
- const previousRaw = input.isRaw === true;
30
- const wasPaused = input.isPaused?.() === true;
31
- let receivedBytes = 0;
32
- let receivedScalars = 0;
33
- let settled = false;
34
- let characters = [];
35
- const decoder = new StringDecoder('utf8');
36
- return new Promise((resolve, reject) => {
37
- const cleanup = () => {
38
- let failure = null;
39
- const attempt = (operation) => {
40
- try {
41
- operation();
42
- }
43
- catch (error) {
44
- failure ||= error instanceof Error ? error : new Error('Interactive terminal cleanup failed.');
45
- }
46
- };
47
- attempt(() => input.off?.('data', onData));
48
- attempt(() => input.off?.('end', onEnd));
49
- attempt(() => signal?.removeEventListener('abort', onAbort));
50
- attempt(() => input.setRawMode?.(previousRaw));
51
- if (wasPaused)
52
- attempt(() => input.pause?.());
53
- return failure;
54
- };
55
- const finish = (value, error) => {
56
- if (settled)
57
- return;
58
- settled = true;
59
- const cleanupFailure = cleanup();
60
- characters = [];
61
- if (error && cleanupFailure) {
62
- reject(new AggregateError([error, cleanupFailure], `${error.message}; terminal cleanup also failed: ${cleanupFailure.message}`));
63
- }
64
- else if (error || cleanupFailure)
65
- reject(error || cleanupFailure);
66
- else
67
- resolve(value || '');
68
- };
69
- const rejectUsage = () => finish(undefined, new CliUsageError('Interactive input must be bounded text without control characters.'));
70
- const onData = (chunk) => {
71
- if (settled)
72
- return;
73
- let rawChunk;
74
- if (Buffer.isBuffer(chunk)) {
75
- if (chunk.length > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes) {
76
- rejectUsage();
77
- return;
78
- }
79
- rawChunk = chunk;
80
- }
81
- else {
82
- const supplied = String(chunk ?? '');
83
- if (supplied.length > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes
84
- || Buffer.byteLength(supplied, 'utf8') > MAX_INTERACTIVE_ANSWER_BYTES - receivedBytes) {
85
- rejectUsage();
86
- return;
87
- }
88
- rawChunk = Buffer.from(supplied, 'utf8');
89
- }
90
- receivedBytes += rawChunk.length;
91
- const value = decoder.write(rawChunk);
92
- receivedScalars += Array.from(value).length;
93
- if (receivedBytes > MAX_INTERACTIVE_ANSWER_BYTES
94
- || receivedScalars > MAX_INTERACTIVE_ANSWER_SCALARS
95
- || value.includes('\ufffd')) {
96
- rejectUsage();
97
- return;
98
- }
99
- for (const character of value) {
100
- if (character === '\r' || character === '\n') {
101
- try {
102
- output.write('\n');
103
- }
104
- catch (error) {
105
- finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
106
- return;
107
- }
108
- finish(characters.join(''));
109
- return;
110
- }
111
- if (character === '\u0003' || character === '\u0004' || character === '\u001b') {
112
- finish(undefined, abortError());
113
- return;
114
- }
115
- if (character === '\u0008' || character === '\u007f') {
116
- if (characters.length) {
117
- characters.pop();
118
- try {
119
- output.write('\b \b');
120
- }
121
- catch (error) {
122
- finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
123
- return;
124
- }
125
- }
126
- continue;
127
- }
128
- if (hasUnsafeCliText(character)) {
129
- rejectUsage();
130
- return;
131
- }
132
- characters.push(character);
133
- try {
134
- output.write(character);
135
- }
136
- catch (error) {
137
- finish(undefined, error instanceof Error ? error : new Error('Interactive terminal output failed.'));
138
- return;
139
- }
140
- }
141
- };
142
- const onEnd = () => {
143
- const remainder = decoder.end();
144
- if (remainder)
145
- rejectUsage();
146
- else
147
- finish(undefined, abortError());
148
- };
149
- const onAbort = () => finish(undefined, signal?.reason instanceof Error ? signal.reason : abortError());
150
- try {
151
- output.write(prompt);
152
- input.on?.('data', onData);
153
- input.on?.('end', onEnd);
154
- signal?.addEventListener('abort', onAbort, { once: true });
155
- if (signal?.aborted) {
156
- onAbort();
157
- return;
158
- }
159
- input.setRawMode?.(true);
160
- if (settled)
161
- return;
162
- input.resume?.();
163
- }
164
- catch (error) {
165
- finish(undefined, error instanceof Error ? error : new Error('Interactive terminal input failed.'));
166
- }
167
- });
168
- }
169
11
  async function launchInteractiveCli(options) {
170
12
  const environment = options.environment || process.env;
171
13
  if (!canLaunchInteractiveCli(options.input, options.output, environment)) {
@@ -1,12 +1,11 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { INTERCHANGE_ARTIFACT_CONTRACTS, } from "../lib/interchange-fidelity-registry.mjs";
3
- import { offlineArtifactSatisfiesAssurance, verifyOfflineArtifact, } from "./artifact-verify.mjs";
3
+ import { offlineArtifactSatisfiesAssurance, MAX_OFFLINE_ARTIFACT_BYTES, parseOfflineArtifactJson, verifyOfflineArtifact, } from "./artifact-verify.mjs";
4
4
  import { mergeBrandProfiles, } from "../packages/workspace/brand-profile-model.mjs";
5
- import { parseBoundedJsonObject } from "./bounded-json.mjs";
6
5
  import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
7
6
  export const INTERCHANGE_FIDELITY_REPORT_SCHEMA = 'whoisleuth.interchange-fidelity-report';
8
7
  export const INTERCHANGE_FIDELITY_REPORT_VERSION = 2;
9
- export const MAX_INTERCHANGE_REPORT_BYTES = 15 * 1024 * 1024;
8
+ export const MAX_INTERCHANGE_REPORT_BYTES = MAX_OFFLINE_ARTIFACT_BYTES;
10
9
  function record(value) {
11
10
  return value && typeof value === 'object' && !Array.isArray(value)
12
11
  ? value
@@ -19,10 +18,7 @@ function parseInput(raw) {
19
18
  if (bytes < 1 || bytes > MAX_INTERCHANGE_REPORT_BYTES) {
20
19
  throw new TypeError(`Interchange input must be between 1 byte and ${MAX_INTERCHANGE_REPORT_BYTES} bytes.`);
21
20
  }
22
- return parseBoundedJsonObject(raw.replace(/^\uFEFF/u, ''), {
23
- label: 'Interchange input',
24
- maximumBytes: MAX_INTERCHANGE_REPORT_BYTES,
25
- });
21
+ return parseOfflineArtifactJson(raw.replace(/^\uFEFF/u, ''));
26
22
  }
27
23
  function nestedRecord(value, path) {
28
24
  let current = value;
@@ -93,6 +89,7 @@ export async function buildInterchangeFidelityReport(raw, options = {}) {
93
89
  }),
94
90
  limitations: Object.freeze([
95
91
  'The file did not match a registered interchange contract. Its supplied schema text and contents are not echoed.',
92
+ 'This is an import/export compatibility catalogue, not the complete verifier inventory. verify-artifact may support this file independently.',
96
93
  'This report makes no request and does not establish that evidence is accurate, current, complete, or safe to share.',
97
94
  ]),
98
95
  });
@@ -123,7 +120,7 @@ export async function buildInterchangeFidelityReport(raw, options = {}) {
123
120
  }
124
121
  else {
125
122
  const verification = await verifyOfflineArtifact(raw, { passphrase: options.passphrase ?? null });
126
- verificationState = verification.state;
123
+ verificationState = verification.state === 'partial' ? 'not_verified' : verification.state;
127
124
  assuranceSatisfied = contract.requiredAssurance === 'whole_integrity'
128
125
  ? offlineArtifactSatisfiesAssurance(verification, 'whole_integrity')
129
126
  : contract.requiredAssurance === 'applicable_integrity'
@@ -189,6 +186,7 @@ export async function buildInterchangeFidelityReport(raw, options = {}) {
189
186
  blockedSectionCount, prunedRecordCount,
190
187
  }),
191
188
  limitations: Object.freeze([
189
+ ...(contract.id === 'lookup_evidence' ? ['CLI read support means offline verification and packaging. brief, export, compare, registry-doctor and source-report require a saved CLI Lookup, not this browser evidence export.'] : []),
192
190
  'Compatibility describes declared field groups and supported operations, not byte-for-byte equality or evidence truth.',
193
191
  'The report emits only registered metadata, counts, and fixed field-group identifiers. Targets, notes, contacts, passphrases, and evidence values are not included.',
194
192
  'A verified integrity envelope detects file changes but does not establish author identity, source accuracy, currency, completeness, or sharing authority.',
@@ -199,7 +197,7 @@ export function formatInterchangeFidelityReport(report) {
199
197
  const browser = report.compatibility.browser;
200
198
  const cli = report.compatibility.cli;
201
199
  const nextAction = !report.recognised
202
- ? 'Choose a supported WHOISleuth export and run verify-artifact before trying to import it.'
200
+ ? 'Run verify-artifact for independent verification support. Import/export compatibility is not registered here.'
203
201
  : !report.artifact.versionSupported
204
202
  ? 'Use a WHOISleuth release that supports this exact version, or re-export it with its last supported release.'
205
203
  : !report.verification.assuranceSatisfied
@@ -213,7 +211,7 @@ export function formatInterchangeFidelityReport(report) {
213
211
  : 'Retain the verification result and use only the operations declared above.';
214
212
  return [
215
213
  'Interchange fidelity report',
216
- `Recognised ${report.recognised ? 'yes' : 'no'}`,
214
+ `Interchange contract ${report.recognised ? 'registered' : 'not registered'}`,
217
215
  `Artifact ${report.artifact.id ?? 'unrecognised'}`,
218
216
  `Version ${report.artifact.version ?? 'unavailable'}${report.artifact.versionSupported ? '' : ' (unsupported)'}`,
219
217
  `Verification ${report.verification.state}`,
@@ -0,0 +1,176 @@
1
+ import { createHash } from 'node:crypto';
2
+ import { isDeepStrictEqual } from 'node:util';
3
+ import { CLI_LOOKUP_SCHEMA, CLI_LOOKUP_VERSION } from "../packages/contracts/cli-lookup.mjs";
4
+ import { DOMAIN_CONTROL_REVIEW_SCHEMA, DOMAIN_CONTROL_REVIEW_VERSION } from "../packages/contracts/domain-control-review.mjs";
5
+ import { canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
6
+ import { validateDomainControlReviewDocument } from "../lib/domain-control-manifest.mjs";
7
+ import { LOOKUP_EVIDENCE_SCHEMA, LOOKUP_EVIDENCE_SCHEMA_VERSION, serializeLookupEvidence } from "../lib/evidence-export.mjs";
8
+ import { OFFLINE_ARTIFACT_VERIFICATION_SCHEMA, OFFLINE_ARTIFACT_VERIFICATION_VERSION } from "./artifact-verify.mjs";
9
+ import { validateLookupEvidenceArtifactStructure } from "./artifact-validation/lookup-evidence.mjs";
10
+ import { CLI_DISCOVERY_SCAN_SCHEMA, CLI_DISCOVERY_SCAN_VERSION } from "./discovery-scan.mjs";
11
+ import { CLI_DISCOVER_SCHEMA, CLI_DISCOVER_SCHEMA_VERSION, CLI_POSTURE_SCHEMA, CLI_POSTURE_SCHEMA_VERSION, CLI_CT_SEARCH_SCHEMA, CLI_CT_SEARCH_SCHEMA_VERSION, CLI_COMPARE_SCHEMA, CLI_COMPARE_SCHEMA_VERSION, } from "./formatters/json.mjs";
12
+ import { CLI_LOOKUP_BRIEF_SCHEMA, CLI_LOOKUP_BRIEF_VERSION } from "../packages/contracts/cli-lookup-brief.mjs";
13
+ import { EXTERNAL_FINDINGS_SCHEMA, EXTERNAL_FINDINGS_VERSION, parseExternalFindingsDocument } from "../packages/interchange/external-findings-import.mjs";
14
+ import { SOURCE_RELIABILITY_REPORT_SCHEMA, SOURCE_RELIABILITY_REPORT_VERSION } from "./source-reliability.mjs";
15
+ import { CLI_CASE_PACK_SCHEMA, CLI_CASE_PACK_VERSION, verifyCliCasePack } from "./case-pack.mjs";
16
+ import { SHARING_REVIEW_SCHEMA, SHARING_REVIEW_VERSION } from "./sharing-review.mjs";
17
+ import { DOMAIN_ASSURANCE_SCHEMA, DOMAIN_ASSURANCE_VERSION } from "../lib/domain-assurance.mjs";
18
+ import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION } from "../lib/domain-change-packet.mjs";
19
+ import { CLI_DOMAIN_CONTROL_MONITOR_SCHEMA, CLI_DOMAIN_CONTROL_MONITOR_VERSION } from "../packages/contracts/domain-control-monitor.mjs";
20
+ import { CLI_LOOKUP_DIFF_SCHEMA, CLI_LOOKUP_DIFF_VERSION } from "./lookup-diff.mjs";
21
+ import { CLI_LOOKUP_TIMELINE_SCHEMA, CLI_LOOKUP_TIMELINE_VERSION } from "./lookup-timeline.mjs";
22
+ import { normalizeCliLookupDocument } from "./saved-lookup.mjs";
23
+ import { CliUsageError } from "./errors.mjs";
24
+ import { MAX_INVESTIGATION_RUN_SELECTIONS } from "../packages/contracts/investigation-run.mjs";
25
+ // This is a fixed-recipe boundary, not a general command or format registry.
26
+ // Non-reusable reports have their envelope checked; reusable evidence also
27
+ // passes its existing input validator before it can enter another command.
28
+ export function validateWorkflowResult(step, result, retained) {
29
+ if (!result || typeof result !== 'object' || Array.isArray(result))
30
+ throw new TypeError('Expected a JSON document.');
31
+ const value = result;
32
+ let schema;
33
+ let version;
34
+ switch (step.command) {
35
+ case 'lookup':
36
+ schema = CLI_LOOKUP_SCHEMA;
37
+ version = CLI_LOOKUP_VERSION;
38
+ normalizeCliLookupDocument(value, { label: 'Workflow Lookup result' });
39
+ break;
40
+ case 'export':
41
+ schema = LOOKUP_EVIDENCE_SCHEMA;
42
+ version = LOOKUP_EVIDENCE_SCHEMA_VERSION;
43
+ validateLookupEvidenceArtifactStructure(value);
44
+ serializeLookupEvidence(value);
45
+ break;
46
+ case 'domain-control':
47
+ schema = DOMAIN_CONTROL_REVIEW_SCHEMA;
48
+ version = DOMAIN_CONTROL_REVIEW_VERSION;
49
+ validateDomainControlReviewDocument(value);
50
+ break;
51
+ case 'case-pack':
52
+ schema = CLI_CASE_PACK_SCHEMA;
53
+ version = CLI_CASE_PACK_VERSION;
54
+ verifyCliCasePack(value);
55
+ break;
56
+ case 'ct-search':
57
+ schema = CLI_CT_SEARCH_SCHEMA;
58
+ version = CLI_CT_SEARCH_SCHEMA_VERSION;
59
+ break;
60
+ case 'ct-intake':
61
+ schema = EXTERNAL_FINDINGS_SCHEMA;
62
+ version = EXTERNAL_FINDINGS_VERSION;
63
+ parseExternalFindingsDocument(value);
64
+ break;
65
+ case 'brief':
66
+ schema = CLI_LOOKUP_BRIEF_SCHEMA;
67
+ version = CLI_LOOKUP_BRIEF_VERSION;
68
+ break;
69
+ case 'compare':
70
+ schema = CLI_COMPARE_SCHEMA;
71
+ version = CLI_COMPARE_SCHEMA_VERSION;
72
+ break;
73
+ case 'source-report':
74
+ schema = SOURCE_RELIABILITY_REPORT_SCHEMA;
75
+ version = SOURCE_RELIABILITY_REPORT_VERSION;
76
+ break;
77
+ case 'sharing-review':
78
+ schema = SHARING_REVIEW_SCHEMA;
79
+ version = SHARING_REVIEW_VERSION;
80
+ break;
81
+ case 'assurance':
82
+ schema = DOMAIN_ASSURANCE_SCHEMA;
83
+ version = DOMAIN_ASSURANCE_VERSION;
84
+ break;
85
+ case 'change-packet':
86
+ schema = DOMAIN_CHANGE_PACKET_SCHEMA;
87
+ version = DOMAIN_CHANGE_PACKET_VERSION;
88
+ break;
89
+ case 'monitor-once':
90
+ schema = CLI_DOMAIN_CONTROL_MONITOR_SCHEMA;
91
+ version = CLI_DOMAIN_CONTROL_MONITOR_VERSION;
92
+ break;
93
+ case 'discover':
94
+ schema = CLI_DISCOVER_SCHEMA;
95
+ version = CLI_DISCOVER_SCHEMA_VERSION;
96
+ break;
97
+ case 'discover-scan':
98
+ schema = CLI_DISCOVERY_SCAN_SCHEMA;
99
+ version = CLI_DISCOVERY_SCAN_VERSION;
100
+ break;
101
+ case 'posture':
102
+ schema = CLI_POSTURE_SCHEMA;
103
+ version = CLI_POSTURE_SCHEMA_VERSION;
104
+ break;
105
+ case 'verify-artifact':
106
+ schema = OFFLINE_ARTIFACT_VERIFICATION_SCHEMA;
107
+ version = OFFLINE_ARTIFACT_VERIFICATION_VERSION;
108
+ break;
109
+ case 'diff':
110
+ schema = CLI_LOOKUP_DIFF_SCHEMA;
111
+ version = CLI_LOOKUP_DIFF_VERSION;
112
+ break;
113
+ case 'timeline':
114
+ schema = CLI_LOOKUP_TIMELINE_SCHEMA;
115
+ version = CLI_LOOKUP_TIMELINE_VERSION;
116
+ break;
117
+ default: throw new TypeError('This command has no fixed-workflow output contract.');
118
+ }
119
+ const envelope = step.command === 'case-pack' ? value.packet : value;
120
+ const actualVersion = step.command === 'export' || step.command === 'ct-intake' ? value.schemaVersion : envelope.version;
121
+ // Only existing format validators admit historical versions. Report-only
122
+ // steps use the same output versions as the latest public checkpoint writer.
123
+ const historicalReader = retained && ['lookup', 'export', 'domain-control'].includes(step.command);
124
+ if (envelope.schema !== schema || schema !== step.produces
125
+ || (!historicalReader && actualVersion !== version) || !Number.isSafeInteger(actualVersion)) {
126
+ throw new TypeError('Unsupported workflow output schema or version.');
127
+ }
128
+ const id = `sha256:${createHash('sha256').update(canonicalArtifactJsonV2(value)).digest('hex')}`;
129
+ return Object.freeze({ id, schema, version: Number(actualVersion) });
130
+ }
131
+ export function normalizeWorkflowBindings(plan, value) {
132
+ if (!Array.isArray(value) || value.length > MAX_INVESTIGATION_RUN_SELECTIONS) {
133
+ throw new CliUsageError(`Workflow artefact bindings are limited to ${MAX_INVESTIGATION_RUN_SELECTIONS}.`);
134
+ }
135
+ const slots = new Set();
136
+ const bindings = value.map((raw) => {
137
+ if (!raw || typeof raw !== 'object' || Array.isArray(raw)
138
+ || !isDeepStrictEqual(Object.keys(raw).sort(), ['input', 'sourceStepId', 'stepId'])) {
139
+ throw new CliUsageError('Workflow artefact bindings must identify a step, input number and earlier source step.');
140
+ }
141
+ const item = raw;
142
+ const destinationIndex = plan.steps.findIndex((step) => step.id === item.stepId);
143
+ const sourceIndex = plan.steps.findIndex((step) => step.id === item.sourceStepId);
144
+ const step = plan.steps[destinationIndex];
145
+ const source = plan.steps[sourceIndex];
146
+ const input = Number(item.input);
147
+ const expected = step ? workflowInputSchemas(step.command) : [];
148
+ const slot = `${destinationIndex}:${input}`;
149
+ if (!step || !source || sourceIndex >= destinationIndex || !expected.includes(source.produces)
150
+ || typeof item.input !== 'number' || !Number.isSafeInteger(input) || input < 1
151
+ || input > step.arguments.filter((argument) => /^<[^>]+>$/u.test(argument)).length || slots.has(slot)) {
152
+ throw new CliUsageError('Workflow artefact binding does not match a compatible earlier output and file input in the fixed recipe.');
153
+ }
154
+ slots.add(slot);
155
+ return Object.freeze({ stepId: step.id, input, sourceStepId: source.id });
156
+ });
157
+ return Object.freeze(bindings.sort((left, right) => plan.steps.findIndex((step) => step.id === left.stepId) - plan.steps.findIndex((step) => step.id === right.stepId)
158
+ || left.input - right.input));
159
+ }
160
+ /** Compatible file inputs in the installed recipes, not general CLI pipelines. */
161
+ export function workflowInputSchemas(command) {
162
+ switch (command) {
163
+ case 'export':
164
+ case 'diff':
165
+ case 'timeline':
166
+ case 'compare':
167
+ case 'source-report':
168
+ case 'brief': return [CLI_LOOKUP_SCHEMA];
169
+ case 'verify-artifact': return [LOOKUP_EVIDENCE_SCHEMA];
170
+ case 'sharing-review': return [CLI_CASE_PACK_SCHEMA];
171
+ default: return [];
172
+ }
173
+ }
174
+ export function workflowArtifactReference(artifact) {
175
+ return `workflow-artifact:${artifact.id}`;
176
+ }
@@ -0,0 +1,10 @@
1
+ import { MAX_INVESTIGATION_RUN_BYTES } from "../packages/contracts/investigation-run.mjs";
2
+ import { prepareLocalDocumentWrite } from "./local-document-checkpoint.mjs";
3
+ /** Preserve the workflow checkpoint interface and shared on-disk lease convention. */
4
+ export async function prepareInvestigationCheckpoint(options) {
5
+ const checkpoint = await prepareLocalDocumentWrite({
6
+ ...options, source: options.resumeSource, label: 'Workflow state',
7
+ maximumInputBytes: MAX_INVESTIGATION_RUN_BYTES, maximumOutputBytes: MAX_INVESTIGATION_RUN_BYTES,
8
+ });
9
+ return Object.freeze({ resumeInput: checkpoint.sourceInput, publish: checkpoint.publish, release: checkpoint.release });
10
+ }
@@ -0,0 +1,114 @@
1
+ import { lstat, mkdir, opendir, realpath } from 'node:fs/promises';
2
+ import { join, resolve } from 'node:path';
3
+ import { readBoundedRegularFileWithin } from "../lib/bounded-file.mjs";
4
+ import { writePrivateFile } from "./output-file.mjs";
5
+ import { INVESTIGATION_PACKAGE_MANIFEST_PATH, MAX_INVESTIGATION_PACKAGE_ENTRIES, investigationPackagePath, prepareInvestigationPackageEntries, } from "../packages/investigation/investigation-package.mjs";
6
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_DOCUMENT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, } from "../packages/investigation/investigation-manifest.mjs";
7
+ export function selectedEvidenceDirectory(value) {
8
+ if (typeof value !== 'string' || !value || value === '-' || value.length > 4096 || /[\x00-\x1f\x7f]/u.test(value))
9
+ throw new TypeError('Select one bounded local folder path.');
10
+ return resolve(value);
11
+ }
12
+ export async function evidenceDirectoryIdentity(path) {
13
+ const value = await lstat(path);
14
+ if (!value.isDirectory() || value.isSymbolicLink())
15
+ throw new TypeError('Evidence folders must be ordinary directories, not symbolic links.');
16
+ return { device: value.dev, inode: value.ino, modified: value.mtimeMs, changed: value.ctimeMs };
17
+ }
18
+ /** Enumerate only the two declared levels; never walk an arbitrary source tree. */
19
+ export async function readInvestigationFolder(value, signal) {
20
+ signal?.throwIfAborted();
21
+ const selected = selectedEvidenceDirectory(value);
22
+ const before = await evidenceDirectoryIdentity(selected), root = await realpath(selected);
23
+ const paths = [];
24
+ let nestedIdentity = null;
25
+ const directory = await opendir(root);
26
+ for await (const entry of directory) {
27
+ signal?.throwIfAborted();
28
+ if (entry.name === 'artifacts' && entry.isDirectory()) {
29
+ const nested = join(root, 'artifacts');
30
+ nestedIdentity = await evidenceDirectoryIdentity(nested);
31
+ for await (const file of await opendir(nested)) {
32
+ signal?.throwIfAborted();
33
+ if (!file.isFile())
34
+ throw new TypeError('Evidence folder entries must be regular files.');
35
+ if (paths.length >= MAX_INVESTIGATION_PACKAGE_ENTRIES)
36
+ throw new TypeError('Evidence folder has too many entries.');
37
+ paths.push(investigationPackagePath(`artifacts/${file.name}`));
38
+ }
39
+ }
40
+ else if (entry.name === INVESTIGATION_PACKAGE_MANIFEST_PATH && entry.isFile()) {
41
+ if (paths.length >= MAX_INVESTIGATION_PACKAGE_ENTRIES)
42
+ throw new TypeError('Evidence folder has too many entries.');
43
+ paths.push(entry.name);
44
+ }
45
+ else
46
+ throw new TypeError('Evidence folders contain only manifest.json and the declared artifacts directory.');
47
+ }
48
+ if (!nestedIdentity || !paths.includes(INVESTIGATION_PACKAGE_MANIFEST_PATH) || paths.length < 2)
49
+ throw new TypeError('Evidence folder is incomplete.');
50
+ const files = new Map();
51
+ let total = 0;
52
+ for (const name of paths.sort()) {
53
+ signal?.throwIfAborted();
54
+ const manifest = name === INVESTIGATION_PACKAGE_MANIFEST_PATH;
55
+ const maximumBytes = manifest ? MAX_INVESTIGATION_MANIFEST_DOCUMENT_BYTES : Math.min(MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES - total);
56
+ const bytes = await readBoundedRegularFileWithin(root, name, { maximumBytes, minimumBytes: 1, label: 'Evidence folder entry', ...(signal ? { signal } : {}) });
57
+ if (!manifest)
58
+ total += bytes.byteLength;
59
+ files.set(name, bytes);
60
+ }
61
+ const unchanged = (left, right) => left.device === right.device && left.inode === right.inode && left.modified === right.modified && left.changed === right.changed;
62
+ if (await realpath(selected) !== root || !unchanged(before, await evidenceDirectoryIdentity(selected))
63
+ || !unchanged(nestedIdentity, await evidenceDirectoryIdentity(join(root, 'artifacts'))))
64
+ throw new TypeError('Evidence folder changed while it was read.');
65
+ signal?.throwIfAborted();
66
+ return files;
67
+ }
68
+ /** Never replace an existing folder. The manifest is published only after files. */
69
+ export async function writeInvestigationFolder(value, input, generatedAt, version, signal) {
70
+ const prepared = await prepareInvestigationPackageEntries(input, generatedAt, version);
71
+ await writePreparedEvidenceFolder(value, prepared.files, 'artifacts', INVESTIGATION_PACKAGE_MANIFEST_PATH, signal);
72
+ return prepared.manifest;
73
+ }
74
+ /** Only first-party, generated layouts may use this fresh-directory writer. */
75
+ export async function writePreparedEvidenceFolder(value, files, payloadDirectory, completionFile, signal) {
76
+ const target = selectedEvidenceDirectory(value);
77
+ if (!files.has(completionFile) || !/^[a-z0-9.-]+$/u.test(completionFile)
78
+ || [...files.keys()].some(path => path !== `${payloadDirectory}/` && !/^[a-z0-9.-]+$/u.test(path) && !new RegExp(`^${payloadDirectory}/artifact-[1-9][0-9]{0,2}$`, 'u').test(path)))
79
+ throw new TypeError('The generated evidence folder layout is invalid.');
80
+ signal?.throwIfAborted();
81
+ try {
82
+ await mkdir(target, { mode: 0o700 });
83
+ }
84
+ catch {
85
+ throw new TypeError('The output folder must not already exist, and its parent must be writable. Existing files were not replaced.');
86
+ }
87
+ try {
88
+ const root = await realpath(target), identity = await evidenceDirectoryIdentity(target);
89
+ await mkdir(join(root, payloadDirectory), { mode: 0o700 });
90
+ const artifactsIdentity = await evidenceDirectoryIdentity(join(root, payloadDirectory));
91
+ const checkLocation = async () => {
92
+ signal?.throwIfAborted();
93
+ const current = await evidenceDirectoryIdentity(target), nested = await evidenceDirectoryIdentity(join(root, payloadDirectory));
94
+ if (await realpath(target) !== root || current.device !== identity.device || current.inode !== identity.inode
95
+ || nested.device !== artifactsIdentity.device || nested.inode !== artifactsIdentity.inode
96
+ || await realpath(join(root, payloadDirectory)) !== join(root, payloadDirectory))
97
+ throw new TypeError('Evidence output folder changed.');
98
+ };
99
+ for (const [name, content] of files) {
100
+ if (name === completionFile || name.endsWith('/'))
101
+ continue;
102
+ await checkLocation();
103
+ await writePrivateFile(join(root, name), content, { beforePublish: checkLocation });
104
+ }
105
+ await checkLocation();
106
+ await writePrivateFile(join(root, completionFile), files.get(completionFile), { beforePublish: checkLocation });
107
+ await checkLocation();
108
+ }
109
+ catch {
110
+ // Retain only this newly created partial output for deliberate inspection;
111
+ // recursive cleanup could remove unrelated files added by another process.
112
+ throw new TypeError('The new evidence folder could not be completed or confirmed. It may contain private partial output; inspect or remove it before choosing a new destination. Existing folders were not replaced.');
113
+ }
114
+ }