@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,24 +1,26 @@
1
1
  import { Buffer } from 'node:buffer';
2
+ import { MANAGED_INDICATOR_SET_SCHEMA, MANAGED_INDICATOR_SET_VERSION } from "../packages/contracts/analyst-interchange.mjs";
2
3
  import { createHash } from 'node:crypto';
3
- import { parseBoundedJsonObject } from "./bounded-json.mjs";
4
+ import { readEditableCaseExport } from "../packages/cases/case-export-input.mjs";
5
+ import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "./bounded-json.mjs";
4
6
  import { validateInvestigationCapsuleStructure, validateLookupEvidenceArtifactStructure, validateOfflineArtifactStructure, validateSignedDigestArtifactStructure, } from "./offline-artifact-validation.mjs";
5
7
  import { verifyCaseResponsePacketIntegrity, } from "../packages/cases/case-response-packet.mjs";
6
8
  import { CASE_PORTABILITY_VERIFIER_DISPATCH, CASE_RESPONSE_PACKET_VERSION, CASE_RESPONSE_PACKET_SCHEMA, CLI_CASE_PACK_VERSION, CLI_CASE_PACK_SCHEMA, } from "../packages/contracts/case-portability.mjs";
7
9
  import { ACQUISITION_DECISION_PACKET_SCHEMA, BULK_DOMAIN_COMPARISON_SCHEMA, BULK_MAIL_EXPOSURE_SCHEMA, BULK_REVIEW_MANIFEST_SCHEMA, LOOKUP_CLAIM_PASSPORT_SCHEMA, LOOKUP_CLAIM_PASSPORT_VERSION, SUPPORTED_ACQUISITION_DECISION_PACKET_VERSIONS, SUPPORTED_BULK_DOMAIN_COMPARISON_EXPORT_VERSIONS, SUPPORTED_BULK_MAIL_EXPOSURE_EXPORT_VERSIONS, SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS, } from "../packages/contracts/investigation-portability.mjs";
8
- import { decryptWorkspaceArchive, inspectEncryptedWorkspaceArchive, isEncryptedWorkspaceArchive, } from "../packages/workspace/workspace-archive-crypto.mjs";
9
- import { WORKSPACE_ARCHIVE_SCHEMA, previewWorkspaceArchive, readWorkspaceArchive, } from "../packages/workspace/workspace-archive.mjs";
10
+ import { decryptWorkspaceArchiveWithMetadata, inspectEncryptedWorkspaceArchive, isEncryptedWorkspaceArchive, MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES, } from "../packages/workspace/workspace-archive-crypto.mjs";
11
+ import { WORKSPACE_ARCHIVE_SCHEMA, prepareWorkspaceArchive, readWorkspaceArchive, } from "../packages/workspace/workspace-archive.mjs";
10
12
  import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, sha256ArtifactDigestFor, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
11
13
  import { SAVED_LOOKUP_SCHEMA, SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, parseCliLookupDocument, } from "./saved-lookup.mjs";
12
14
  import { DOMAIN_CONTROL_MANIFEST_SCHEMA, verifyDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
13
- import { DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, } from "../packages/contracts/domain-control-manifest.mjs";
14
- import { DOMAIN_CHANGE_PACKET_SCHEMA, DOMAIN_CHANGE_PACKET_VERSION, } from "../lib/domain-change-packet.mjs";
15
+ import { DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, } from "../packages/contracts/domain-control-manifest.mjs";
16
+ import { DOMAIN_CHANGE_PACKET_SCHEMA, SUPPORTED_DOMAIN_CHANGE_PACKET_VERSIONS, } from "../lib/domain-change-packet.mjs";
15
17
  import { INVESTIGATION_CAPSULE_SCHEMA, SUPPORTED_INVESTIGATION_CAPSULE_VERSIONS, verifyInvestigationCapsule, } from "../packages/investigation/investigation-capsule.mjs";
16
- import { INVESTIGATION_MANIFEST_SCHEMA, INVESTIGATION_MANIFEST_VERSION, } from "./investigation-manifest.mjs";
18
+ import { INVESTIGATION_MANIFEST_SCHEMA, SUPPORTED_INVESTIGATION_MANIFEST_VERSIONS, } from "./investigation-manifest.mjs";
17
19
  import { verifyCliCasePack } from "./case-pack.mjs";
18
20
  import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, LOOKUP_EVIDENCE_SCHEMA, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../lib/evidence-export.mjs";
19
21
  export const OFFLINE_ARTIFACT_VERIFICATION_SCHEMA = 'whoisleuth.offline-artifact-verification';
20
- export const OFFLINE_ARTIFACT_VERIFICATION_VERSION = 3;
21
- export const MAX_OFFLINE_ARTIFACT_BYTES = 15 * 1024 * 1024;
22
+ export const OFFLINE_ARTIFACT_VERIFICATION_VERSION = 4;
23
+ export const MAX_OFFLINE_ARTIFACT_BYTES = Math.max(MAX_DOMAIN_CONTROL_MANIFEST_BYTES, MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES);
22
24
  export const MAX_OFFLINE_PASSPHRASE_FILE_BYTES = 1024;
23
25
  export class UnsupportedOfflineArtifactError extends TypeError {
24
26
  code = 'unsupported_artifact';
@@ -35,14 +37,15 @@ function currentCanonicalizationRoutes(versions) {
35
37
  })));
36
38
  }
37
39
  const SIGNED_ARTIFACT_ROUTES = Object.freeze({
40
+ [MANAGED_INDICATOR_SET_SCHEMA]: currentCanonicalizationRoutes([MANAGED_INDICATOR_SET_VERSION]),
38
41
  [ACQUISITION_DECISION_PACKET_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_ACQUISITION_DECISION_PACKET_VERSIONS),
39
42
  [LOOKUP_CLAIM_PASSPORT_SCHEMA]: currentCanonicalizationRoutes([LOOKUP_CLAIM_PASSPORT_VERSION]),
40
43
  [BULK_DOMAIN_COMPARISON_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_DOMAIN_COMPARISON_EXPORT_VERSIONS),
41
44
  [BULK_MAIL_EXPOSURE_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_MAIL_EXPOSURE_EXPORT_VERSIONS),
42
45
  [BULK_REVIEW_MANIFEST_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS),
43
46
  [DOMAIN_CONTROL_MANIFEST_SCHEMA]: DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES,
44
- [DOMAIN_CHANGE_PACKET_SCHEMA]: currentCanonicalizationRoutes([DOMAIN_CHANGE_PACKET_VERSION]),
45
- [INVESTIGATION_MANIFEST_SCHEMA]: currentCanonicalizationRoutes([INVESTIGATION_MANIFEST_VERSION]),
47
+ [DOMAIN_CHANGE_PACKET_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_DOMAIN_CHANGE_PACKET_VERSIONS),
48
+ [INVESTIGATION_MANIFEST_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_INVESTIGATION_MANIFEST_VERSIONS),
46
49
  });
47
50
  function record(value) {
48
51
  return value && typeof value === 'object' && !Array.isArray(value)
@@ -83,8 +86,16 @@ function rejectUnsupportedCasePortabilityVersion(label, version, currentVersion)
83
86
  }
84
87
  throw new UnsupportedOfflineArtifactError(`This ${label} version is malformed or unsupported; no data was changed.`);
85
88
  }
86
- function parseJson(raw) {
87
- return parseBoundedJsonObject(raw, { maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES });
89
+ export function parseOfflineArtifactJson(raw) {
90
+ const value = parseBoundedJsonObject(raw, {
91
+ maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES,
92
+ limits: boundedJsonLimitsForBytes(MAX_OFFLINE_ARTIFACT_BYTES),
93
+ });
94
+ if (value.schema !== WORKSPACE_ARCHIVE_SCHEMA && !isEncryptedWorkspaceArchive(value)
95
+ && Buffer.byteLength(raw, 'utf8') > MAX_DOMAIN_CONTROL_MANIFEST_BYTES) {
96
+ throw new TypeError(`Non-workspace artefacts are limited to ${MAX_DOMAIN_CONTROL_MANIFEST_BYTES} bytes.`);
97
+ }
98
+ return value;
88
99
  }
89
100
  export function hasVerifiedArtifactStructure(report) {
90
101
  return report.checks.structure === 'verified';
@@ -92,6 +103,7 @@ export function hasVerifiedArtifactStructure(report) {
92
103
  export function hasVerifiedApplicableIntegrity(report) {
93
104
  return report.checks.contentIntegrity === 'verified'
94
105
  && (report.checks.contentIntegrityScope === 'whole_artifact'
106
+ || report.checks.contentIntegrityScope === 'manifest_and_files'
95
107
  || report.checks.contentIntegrityScope === 'embedded_projections');
96
108
  }
97
109
  export function hasVerifiedWholeArtifactIntegrity(report) {
@@ -108,6 +120,9 @@ export function offlineArtifactSatisfiesAssurance(report, requirement) {
108
120
  return hasVerifiedWholeArtifactIntegrity(report);
109
121
  }
110
122
  export function isCompleteOfflineArtifactVerification(report) {
123
+ if (report.artifact.kind === 'bagit')
124
+ return report.state === 'integrity_valid' && report.bagit?.state === 'valid'
125
+ && report.checks.structure === 'verified' && report.checks.contentIntegrity === 'verified';
111
126
  return (report.state === 'verified' || report.state === 'structure_valid')
112
127
  && (report.manifestIdentity === null || report.manifestIdentity.state === 'identity_verified');
113
128
  }
@@ -124,8 +139,7 @@ function artifactVersion(value) {
124
139
  function inputBytes(raw) {
125
140
  return Buffer.byteLength(raw, 'utf8');
126
141
  }
127
- async function archiveReport(raw, source, archive, encrypted, ciphertextBytes) {
128
- const preview = await previewWorkspaceArchive(source, {});
142
+ function archiveReport(raw, archive, preview, encrypted, ciphertextBytes) {
129
143
  const readySectionCount = preview.sections.filter((section) => section.status === 'ready').length;
130
144
  const unsupportedSectionCount = preview.sections.filter((section) => section.status === 'unsupported').length;
131
145
  const blockedSectionCount = preview.sections.filter((section) => section.status === 'blocked').length;
@@ -172,6 +186,24 @@ async function archiveReport(raw, source, archive, encrypted, ciphertextBytes) {
172
186
  ]),
173
187
  });
174
188
  }
189
+ async function verifyWorkspaceArchiveValue(raw, value, passphrase) {
190
+ const encrypted = isEncryptedWorkspaceArchive(value);
191
+ if (encrypted && !passphrase) {
192
+ throw new TypeError('Encrypted archive inspection requires a separate passphrase file.');
193
+ }
194
+ if (!encrypted)
195
+ artifactVersion(value);
196
+ const decrypted = encrypted ? await decryptWorkspaceArchiveWithMetadata(value, passphrase) : null;
197
+ const source = decrypted ? decrypted.archive : value;
198
+ const prepared = await prepareWorkspaceArchive(source);
199
+ const archive = prepared.read();
200
+ const report = archiveReport(raw, archive, prepared.preview({}), encrypted, decrypted?.ciphertextBytes ?? null);
201
+ return { archive, report, encrypted };
202
+ }
203
+ /** Verify an archive once and retain its sections for bounded offline inspection. */
204
+ export async function verifyOfflineWorkspaceArchive(raw, options = {}) {
205
+ return verifyWorkspaceArchiveValue(raw, parseOfflineArtifactJson(raw), options.passphrase);
206
+ }
175
207
  async function verifySignedArtifact(raw, value, schema, version) {
176
208
  const routes = SIGNED_ARTIFACT_ROUTES[schema] ?? [];
177
209
  if (!routes.some((route) => route.version === version)) {
@@ -215,7 +247,7 @@ async function verifySignedArtifact(raw, value, schema, version) {
215
247
  });
216
248
  }
217
249
  async function verifyOfflineArtifactCore(raw, options = {}) {
218
- const value = parseJson(raw);
250
+ const value = parseOfflineArtifactJson(raw);
219
251
  const casePortabilityVerifier = selectCasePortabilityVerifier(value);
220
252
  if (casePortabilityVerifier?.id === 'cli-case-pack') {
221
253
  const casePackVersion = casePortabilityVersion(casePortabilityVerifier, value);
@@ -249,9 +281,24 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
249
281
  ]),
250
282
  });
251
283
  }
284
+ if (value.schema === undefined && Array.isArray(value.cases)) {
285
+ const cases = readEditableCaseExport(raw);
286
+ return Object.freeze({
287
+ schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA, version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
288
+ artifact: Object.freeze({ kind: 'case_export', schema: null, version: artifactVersion(value) }),
289
+ state: 'structure_valid',
290
+ checks: Object.freeze({ structure: 'verified', contentIntegrity: 'not_checked', contentIntegrityScope: 'not_applicable', authenticatedEncryption: 'not_applicable' }),
291
+ summary: Object.freeze({ inputBytes: inputBytes(raw), sectionCount: 1, recordCount: cases.length, ciphertextBytes: null }),
292
+ limitations: Object.freeze([
293
+ 'The ordinary Case export matches its exact supported structure without repairing or removing retained records. It has no embedded checksum or signature.',
294
+ 'Original-file references do not include original bytes. Verify a selected evidence package to check which references have matching bytes.',
295
+ 'The Case includes private analyst content. Structural validity does not authenticate its author or establish factual accuracy, currentness or response authority.',
296
+ ]),
297
+ });
298
+ }
252
299
  if (isEncryptedWorkspaceArchive(value)) {
253
- const inspected = inspectEncryptedWorkspaceArchive(value);
254
300
  if (!options.passphrase) {
301
+ const inspected = inspectEncryptedWorkspaceArchive(value);
255
302
  return Object.freeze({
256
303
  schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
257
304
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
@@ -279,14 +326,12 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
279
326
  ]),
280
327
  });
281
328
  }
282
- const decrypted = await decryptWorkspaceArchive(value, options.passphrase);
283
- const archive = await readWorkspaceArchive(decrypted);
284
- return archiveReport(raw, decrypted, archive, true, inspected.ciphertextBytes);
329
+ return (await verifyWorkspaceArchiveValue(raw, value, options.passphrase)).report;
285
330
  }
286
331
  const schema = typeof value.schema === 'string' ? value.schema : '';
287
332
  const version = artifactVersion(value);
288
333
  if (schema === WORKSPACE_ARCHIVE_SCHEMA) {
289
- return archiveReport(raw, value, await readWorkspaceArchive(value), false, null);
334
+ return (await verifyWorkspaceArchiveValue(raw, value)).report;
290
335
  }
291
336
  if (casePortabilityVerifier?.id === 'case-response-packet') {
292
337
  if (!supportsCasePortabilityVersion(casePortabilityVerifier, version)) {
@@ -432,13 +477,13 @@ async function verifyManifestIdentity(artifactRaw, manifestRaw, entryId) {
432
477
  || manifestReport.checks.contentIntegrityScope !== 'whole_artifact') {
433
478
  throw new TypeError('The selected manifest is not a fully integrity-verified investigation manifest.');
434
479
  }
435
- const manifest = parseJson(manifestRaw);
480
+ const manifest = parseOfflineArtifactJson(manifestRaw);
436
481
  const entries = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
437
482
  const entry = entries.find((candidate) => record(candidate)?.id === entryId);
438
483
  const item = record(entry);
439
484
  if (!item)
440
485
  throw new TypeError('The requested investigation manifest entry was not found.');
441
- const artifactValue = parseJson(artifactRaw);
486
+ const artifactValue = parseOfflineArtifactJson(artifactRaw);
442
487
  const metadata = artifactMetadata(artifactValue);
443
488
  const actualByteLength = inputBytes(artifactRaw);
444
489
  const expectedByteLength = Number(item.byteLength);
@@ -466,7 +511,7 @@ async function verifyManifestIdentity(artifactRaw, manifestRaw, entryId) {
466
511
  : 'mismatch';
467
512
  return Object.freeze({
468
513
  state,
469
- manifest: Object.freeze({ schema: INVESTIGATION_MANIFEST_SCHEMA, version: INVESTIGATION_MANIFEST_VERSION, entryId }),
514
+ manifest: Object.freeze({ schema: INVESTIGATION_MANIFEST_SCHEMA, version: manifestReport.artifact.version, entryId }),
470
515
  checks,
471
516
  expectedByteLength,
472
517
  actualByteLength,
@@ -490,8 +535,9 @@ export async function verifyOfflineArtifact(raw, options = {}) {
490
535
  export function formatOfflineArtifactVerification(report) {
491
536
  const lines = [
492
537
  'WHOISleuth offline artefact verification',
493
- `Artifact: ${report.artifact.kind} · ${report.artifact.schema} v${report.artifact.version}`,
538
+ `Artifact: ${report.artifact.kind} · ${report.artifact.schema ?? 'no schema identifier'} v${report.artifact.version}`,
494
539
  `State: ${report.state}`,
540
+ 'Automation: use --strict-exit for exit 4 on incomplete verification; default exit 0 means the report was produced.',
495
541
  `Structure: ${report.checks.structure}`,
496
542
  `Content integrity: ${report.checks.contentIntegrity}`,
497
543
  `Content integrity scope: ${report.checks.contentIntegrityScope}`,
@@ -534,6 +580,28 @@ export function formatOfflineArtifactVerification(report) {
534
580
  for (const limitation of report.manifestIdentity.limitations)
535
581
  lines.push(`Manifest limitation: ${limitation}`);
536
582
  }
583
+ if (report.bagit) {
584
+ const bag = report.bagit;
585
+ lines.push(`BagIt: ${bag.state}`, `Payload: ${bag.entries.length} files; ${bag.payloadBytes} present bytes`, `Tag checksums: ${bag.verifiedTagFiles} of ${bag.tagFiles} tag files checked`, `Fetch declarations: ${bag.fetchEntries}; ${bag.fetchMissing} files missing; no requests made`, `Unsupported manifests: ${bag.unsupportedManifests}`);
586
+ for (const entry of bag.entries)
587
+ lines.push(`${entry.id}: ${entry.state} · ${entry.byteLength ?? 'unknown'} bytes`);
588
+ for (const issue of bag.issues)
589
+ lines.push(`BagIt issue: ${issue}`);
590
+ }
591
+ if (report.package) {
592
+ lines.push(`Package bytes: ${report.package.digestSha256}`, 'Storage: unchanged; inspection only', 'Audience: private; review selected file contents before sharing', 'Signature trust: not checked', 'Trusted timestamp: not checked', 'Factual accuracy: not established');
593
+ for (const entry of report.package.entries)
594
+ lines.push(`${entry.id}: ${entry.state} · ${entry.byteLength} bytes · ${entry.mediaType}${entry.issue ? ` · ${entry.issue}` : ''}`);
595
+ for (const link of report.package.links)
596
+ lines.push(`Source link ${link.capsuleEntryId}: ${link.state}${link.sourceEntryId ? ` (${link.sourceEntryId})` : ''}`);
597
+ for (const capture of report.package.captureManifests) {
598
+ lines.push(`Capture manifest ${capture.entryId}: ${capture.state}`);
599
+ for (const artifact of capture.artifacts)
600
+ lines.push(`Capture ${artifact.capture} ${artifact.kind}: ${artifact.state}${artifact.matchingIds.length ? ` (${artifact.matchingIds.join(', ')})` : ''}`);
601
+ }
602
+ for (const item of report.package.caseFiles)
603
+ lines.push(`Case files ${item.entryId} (${item.caseCount} Case${item.caseCount === 1 ? '' : 's'}): ${item.matched} of ${item.references} references matched; ${item.missing} missing`);
604
+ }
537
605
  for (const limitation of report.limitations)
538
606
  lines.push(`Limitation: ${limitation}`);
539
607
  return `${lines.join('\n')}\n`;
@@ -0,0 +1,56 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import { CliUsageError } from "./errors.mjs";
3
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS } from "../packages/contracts/investigation-package-limits.mjs";
4
+ import { terminalOptions, jsonOutput, singleInputAction } from "./argument-values.mjs";
5
+ function parseManifestArguments(parsed) {
6
+ const sources = parsed.positionalValues('artefacts');
7
+ if (sources.some((source) => !source))
8
+ throw new CliUsageError(`manifest requires from 1 to ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} artefact files.`);
9
+ if (new Set(sources).size !== sources.length)
10
+ throw new CliUsageError('manifest artefact files must be different.');
11
+ const configurationDigestSha256 = parsed.optionValue('--configuration-digest');
12
+ if (configurationDigestSha256 !== null && !/^sha256:[a-f0-9]{64}$/u.test(configurationDigestSha256)) {
13
+ throw new CliUsageError('--configuration-digest requires a sha256: hexadecimal digest.');
14
+ }
15
+ return {
16
+ action: 'manifest',
17
+ sources,
18
+ workflow: parsed.optionValue('--workflow'),
19
+ configurationDigestSha256,
20
+ ...(parsed.optionValue('--passphrase-file')
21
+ ? { passphraseSource: parsed.optionValue('--passphrase-file') }
22
+ : {}),
23
+ ...(parsed.hasOption('--package') ? { package: true } : {}),
24
+ ...(parsed.hasOption('--bagit') ? { bagit: true } : {}),
25
+ ...(parsed.optionValue('--folder') ? { folder: parsed.optionValue('--folder') } : {}),
26
+ output: jsonOutput(parsed),
27
+ ...terminalOptions(parsed),
28
+ };
29
+ }
30
+ function parseSharingReviewArguments(parsed) {
31
+ const purpose = parsed.optionValue('--purpose').replace(/\s+/gu, ' ').trim();
32
+ if (!purpose || purpose.length > 200)
33
+ throw new CliUsageError('--purpose is limited to 200 characters.');
34
+ return {
35
+ action: 'sharing-review',
36
+ source: parsed.positionalValue('source'),
37
+ output: jsonOutput(parsed),
38
+ marking: parsed.optionValue('--marking'),
39
+ recipientScope: parsed.optionValue('--recipient-scope'),
40
+ purpose,
41
+ humanReviewed: parsed.hasOption('--human-reviewed'),
42
+ personalDataReviewed: parsed.hasOption('--personal-data-reviewed'),
43
+ redactionsConfirmed: parsed.hasOption('--redactions-confirmed'),
44
+ ...terminalOptions(parsed),
45
+ };
46
+ }
47
+ export const ASSURANCE_ARGUMENT_PARSERS = Object.freeze({
48
+ manifest: parseManifestArguments,
49
+ 'map-observations': (parsed) => singleInputAction('map-observations', parsed),
50
+ 'oam-export': (parsed) => singleInputAction('oam-export', parsed),
51
+ 'ct-intake': (parsed) => singleInputAction('ct-intake', parsed),
52
+ 'domain-control': (parsed) => singleInputAction('domain-control', parsed),
53
+ assurance: (parsed) => singleInputAction('assurance', parsed),
54
+ 'change-packet': (parsed) => singleInputAction('change-packet', parsed),
55
+ 'sharing-review': parseSharingReviewArguments,
56
+ });
@@ -0,0 +1,179 @@
1
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES } from "../packages/investigation/investigation-manifest.mjs";
2
+ import { positional, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed, flag, file } from "./command-definition.mjs";
3
+ export const ASSURANCE_COMMAND_DEFINITIONS = Object.freeze({
4
+ manifest: commandSeed({
5
+ reference: {
6
+ description: `Record an ordered, path-free manifest for up to ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} local files. Use --package --output evidence.zip for a ZIP, add --passphrase-file to encrypt the ordinary package, or use --folder ./evidence for a new unencrypted folder. Add --bagit to ZIP or folder output for BagIt 1.0 with SHA-512 checksums.`,
7
+ example: 'whoisleuth manifest lookup.json comparison.json --workflow "domain review" --json',
8
+ boundary: 'Ordinary output contains metadata only. ZIP and folder output include unchanged selected bytes and are private until reviewed for sharing. Folders must be new; existing destinations are never replaced and a failed write may leave explicit partial output. BagIt output is unencrypted; its checksums do not authenticate evidence. Filenames ending in .json are parsed as JSON; other files are opaque and never executed. Original paths are omitted. No network request is made.',
9
+ },
10
+ collection: { scope: `Reads 1 to ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} local files, at most ${MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES / 1024 / 1024} MiB each and ${MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES.toLocaleString('en-AU')} bytes combined; retains no source paths.` },
11
+ summary: 'Build an evidence manifest offline',
12
+ options: ['--workflow', '--configuration-digest', '--package', '--bagit', '--passphrase-file', '--folder', '--json', '--quiet', '--no-color'],
13
+ optionOverrides: {
14
+ '--package': flag('Create a portable evidence ZIP containing the selected files.'),
15
+ '--folder': file('Create a new evidence folder containing the selected files.'),
16
+ '--bagit': flag('Create a BagIt 1.0 package with SHA-512 checksums.'),
17
+ },
18
+ positionals: Object.freeze([positional('artefacts', 'file', 1, MAX_INVESTIGATION_MANIFEST_ARTIFACTS)]),
19
+ constraints: Object.freeze([
20
+ constraint({ kind: 'required', options: ['--workflow'] }),
21
+ constraint({ kind: 'requires_any', option: '--bagit', requiredOptions: ['--package', '--folder'] }),
22
+ constraint({ kind: 'excludes_all', option: '--bagit', excludedOptions: ['--passphrase-file'] }),
23
+ constraint({ kind: 'requires_all', option: '--package', requiredOptions: ['--output'] }),
24
+ constraint({ kind: 'requires_all', option: '--passphrase-file', requiredOptions: ['--package'] }),
25
+ constraint({ kind: 'mutually_exclusive', options: ['--package', '--json'] }),
26
+ constraint({ kind: 'excludes_all', option: '--folder', excludedOptions: ['--package', '--output', '--force'] }),
27
+ ]),
28
+ handlerOwner: 'assurance',
29
+ networkEffect: 'offline',
30
+ common: false,
31
+ schemaIdentifiers: Object.freeze(['whoisleuth.investigation-manifest']),
32
+ primaryArtefacts: Object.freeze([]),
33
+ planSupport: false,
34
+ additionalOutputFormats: Object.freeze([]),
35
+ bootstrapProfile: 'allowed',
36
+ }),
37
+ "map-observations": commandSeed({
38
+ reference: {
39
+ description: 'Apply one bounded declarative field-mapping profile to local source observations.',
40
+ example: 'whoisleuth map-observations mapping.json --json',
41
+ boundary: 'Profiles select allowlisted dotted fields only. They execute no scripts, make no requests, and emit the browser-compatible external-findings contract.',
42
+ },
43
+ collection: { scope: 'Reads one mapping document capped at 4 MiB and executes no scripts or requests.' },
44
+ summary: 'Apply a declarative observation map offline',
45
+ options: ['--json', '--quiet', '--no-color'],
46
+ positionals: OPTIONAL_FILE_POSITIONAL,
47
+ constraints: EMPTY_CONSTRAINTS,
48
+ handlerOwner: 'assurance',
49
+ networkEffect: 'offline',
50
+ common: false,
51
+ schemaIdentifiers: Object.freeze(['whoisleuth.external-observation-mapping']),
52
+ primaryArtefacts: Object.freeze([]),
53
+ planSupport: false,
54
+ additionalOutputFormats: Object.freeze([]),
55
+ bootstrapProfile: 'allowed',
56
+ }),
57
+ "oam-export": commandSeed({
58
+ reference: {
59
+ description: 'Project browser-compatible external findings into a bounded Open Asset Model bridge document.',
60
+ example: 'whoisleuth oam-export external-findings.json --json',
61
+ boundary: 'The projection is offline, preserves source completeness without inventing confidence, and covers only bounded FQDN, IP address, certificate, and related edge vocabulary.',
62
+ },
63
+ collection: { scope: 'Reads one browser-compatible external-findings document and projects bounded graph records locally.' },
64
+ summary: 'Project external findings to Open Asset Model',
65
+ options: ['--json', '--quiet', '--no-color'],
66
+ positionals: OPTIONAL_FILE_POSITIONAL,
67
+ constraints: EMPTY_CONSTRAINTS,
68
+ handlerOwner: 'assurance',
69
+ networkEffect: 'offline',
70
+ common: false,
71
+ schemaIdentifiers: Object.freeze(['whoisleuth.open-asset-model-bridge']),
72
+ primaryArtefacts: Object.freeze([]),
73
+ planSupport: false,
74
+ additionalOutputFormats: Object.freeze([]),
75
+ bootstrapProfile: 'allowed',
76
+ }),
77
+ "ct-intake": commandSeed({
78
+ reference: {
79
+ description: 'Normalise source-qualified local certificate events into browser-compatible findings.',
80
+ example: 'whoisleuth ct-intake certificate-events.json --json',
81
+ boundary: 'The command is offline, caps output at 100 findings, and treats every event as a review lead rather than proof of serving or control.',
82
+ },
83
+ collection: { scope: 'Reads one source-qualified event batch capped at 4 MiB and makes no request.' },
84
+ summary: 'Normalise certificate observations offline',
85
+ options: ['--json', '--quiet', '--no-color'],
86
+ positionals: OPTIONAL_FILE_POSITIONAL,
87
+ constraints: EMPTY_CONSTRAINTS,
88
+ handlerOwner: 'assurance',
89
+ networkEffect: 'offline',
90
+ common: false,
91
+ schemaIdentifiers: Object.freeze(['whoisleuth.ct-event-batch', 'whoisleuth.external-findings']),
92
+ primaryArtefacts: Object.freeze([]),
93
+ planSupport: false,
94
+ additionalOutputFormats: Object.freeze([]),
95
+ bootstrapProfile: 'allowed',
96
+ }),
97
+ "domain-control": commandSeed({
98
+ reference: {
99
+ description: 'Build an integrity-protected desired-state manifest or compare one with supplied observations.',
100
+ example: 'whoisleuth domain-control domain-control-input.json --json',
101
+ boundary: 'The command is offline and changes no registrar, DNS, mail, or certificate configuration. Only complete, recent source observations can establish drift or expected absence.',
102
+ },
103
+ collection: { scope: 'Reads one bounded desired-state or review document and performs no collection or configuration change.' },
104
+ summary: 'Build or review a domain control manifest',
105
+ options: ['--json', '--quiet', '--no-color'],
106
+ positionals: OPTIONAL_FILE_POSITIONAL,
107
+ constraints: EMPTY_CONSTRAINTS,
108
+ handlerOwner: 'assurance',
109
+ networkEffect: 'offline',
110
+ common: false,
111
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.domain-control-review-input', 'whoisleuth.cli.domain-control-review']),
112
+ primaryArtefacts: Object.freeze([]),
113
+ planSupport: false,
114
+ additionalOutputFormats: Object.freeze([]),
115
+ bootstrapProfile: 'allowed',
116
+ }),
117
+ assurance: commandSeed({
118
+ reference: {
119
+ description: 'Review a versioned domain change, recovery-dependency, or retirement plan.',
120
+ example: 'whoisleuth assurance domain-assurance.json --json',
121
+ boundary: 'The command is offline and treats every provider label, readiness state, and evidence reference as analyst-authored input. It changes no configuration.',
122
+ },
123
+ collection: { scope: 'Reads one versioned plan capped at 2 MiB and makes no request or configuration change.' },
124
+ summary: 'Review domain change, recovery, or retirement plans',
125
+ options: ['--json', '--quiet', '--no-color'],
126
+ positionals: OPTIONAL_FILE_POSITIONAL,
127
+ constraints: EMPTY_CONSTRAINTS,
128
+ handlerOwner: 'assurance',
129
+ networkEffect: 'offline',
130
+ common: false,
131
+ schemaIdentifiers: Object.freeze(['whoisleuth.domain-assurance.input', 'whoisleuth.domain-assurance']),
132
+ primaryArtefacts: Object.freeze([]),
133
+ planSupport: false,
134
+ additionalOutputFormats: Object.freeze([]),
135
+ bootstrapProfile: 'allowed',
136
+ }),
137
+ "change-packet": commandSeed({
138
+ reference: {
139
+ description: 'Assemble pre-change, post-change, and planning evidence into one integrity-protected packet.',
140
+ example: 'whoisleuth change-packet change-review.json --json',
141
+ boundary: 'Assembly is offline. Readiness reflects only the supplied bounded evidence and does not authorise or perform a domain change.',
142
+ },
143
+ collection: { scope: 'Reads one versioned packet input capped at 6 MiB and makes no request or configuration change.' },
144
+ summary: 'Build a reviewed change packet offline',
145
+ options: ['--json', '--quiet', '--no-color'],
146
+ positionals: OPTIONAL_FILE_POSITIONAL,
147
+ constraints: EMPTY_CONSTRAINTS,
148
+ handlerOwner: 'assurance',
149
+ networkEffect: 'offline',
150
+ common: false,
151
+ schemaIdentifiers: Object.freeze(['whoisleuth.domain-change-packet.input', 'whoisleuth.domain-change-packet']),
152
+ primaryArtefacts: Object.freeze([]),
153
+ planSupport: false,
154
+ additionalOutputFormats: Object.freeze([]),
155
+ bootstrapProfile: 'allowed',
156
+ }),
157
+ "sharing-review": commandSeed({
158
+ reference: {
159
+ description: 'Lint one reviewed artefact against local integrity, marking, recipient, personal-data, and redaction controls.',
160
+ example: 'whoisleuth sharing-review packet.json --marking amber --recipient-scope organization --purpose "Reviewed incident handoff" --human-reviewed --personal-data-reviewed --redactions-confirmed --json',
161
+ boundary: 'The command is offline and emits only bounded schema/version metadata, no content values, and no raw evidence. Its result is a review aid, not legal advice or recipient authorisation.',
162
+ },
163
+ collection: { scope: 'Reads one bounded artefact, emits only schema/version metadata and no content values, and performs no transmission.' },
164
+ summary: 'Lint an artefact before deliberate sharing',
165
+ options: ['--marking', '--recipient-scope', '--purpose', '--human-reviewed', '--personal-data-reviewed', '--redactions-confirmed', '--json', '--quiet', '--no-color'],
166
+ positionals: OPTIONAL_FILE_POSITIONAL,
167
+ constraints: Object.freeze([
168
+ constraint({ kind: 'required', options: ['--marking', '--recipient-scope', '--purpose'] }),
169
+ ]),
170
+ handlerOwner: 'assurance',
171
+ networkEffect: 'offline',
172
+ common: false,
173
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.sharing-review']),
174
+ primaryArtefacts: Object.freeze([]),
175
+ planSupport: false,
176
+ additionalOutputFormats: Object.freeze([]),
177
+ bootstrapProfile: 'allowed',
178
+ }),
179
+ });
@@ -1,5 +1,10 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
2
+ import { readBoundedRegularFile } from "../lib/bounded-file.mjs";
3
+ import { buildInvestigationPackage } from "../packages/investigation/investigation-package.mjs";
4
+ import { encryptInvestigationPackage } from "../packages/investigation/investigation-package-crypto.mjs";
5
+ import { writeInvestigationFolder, writePreparedEvidenceFolder } from "./investigation-folder.mjs";
6
+ import { buildInvestigationBagIt, prepareInvestigationBagIt } from "../packages/investigation/investigation-bagit.mjs";
7
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
3
8
  import { MAX_ASSURANCE_INPUT_BYTES, buildDomainAssurance, formatDomainAssurance, } from "../lib/domain-assurance.mjs";
4
9
  import { MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, buildDomainChangePacket, formatDomainChangePacket, } from "../lib/domain-change-packet.mjs";
5
10
  import { DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, buildDomainControlFlightRecorder, formatDomainControlFlightRecorder, serializeDomainControlFlightRecorder, } from "../lib/domain-control-flight-recorder.mjs";
@@ -12,7 +17,7 @@ import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
12
17
  import EXIT_CODES from "./exit-codes.mjs";
13
18
  import { MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, formatExternalObservationMapping, mapExternalObservations, } from "./external-observation-mapping.mjs";
14
19
  import { formatJsonDocument } from "./formatters/json.mjs";
15
- import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, buildInvestigationManifest, formatInvestigationManifest, } from "./investigation-manifest.mjs";
20
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, buildInvestigationManifest, formatInvestigationManifest, investigationFileMediaType, } from "./investigation-manifest.mjs";
16
21
  import { buildOpenAssetModelBridge, formatOpenAssetModelBridge, } from "./open-asset-model-bridge.mjs";
17
22
  import { ASSURANCE_INLINE_COMMANDS } from "./inline-command-families.mjs";
18
23
  import { MAX_SHARING_REVIEW_BYTES, buildSharingReview, formatSharingReview, } from "./sharing-review.mjs";
@@ -23,6 +28,15 @@ async function runManifestCommand(args, dependencies, context) {
23
28
  let totalBytes = 0;
24
29
  try {
25
30
  for (const source of args.sources) {
31
+ if (args.package || args.folder) {
32
+ const content = dependencies.readBinaryArtifactInput ? await dependencies.readBinaryArtifactInput(source)
33
+ : await readBoundedRegularFile(source, { maximumBytes: MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, minimumBytes: 1, label: 'Package artefact input', ...(dependencies.signal ? { signal: dependencies.signal } : {}) });
34
+ if (content.byteLength > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES - totalBytes)
35
+ throw new CliUsageError('Package artefacts exceed the combined byte limit.');
36
+ totalBytes += content.byteLength;
37
+ artifacts.push({ content, mediaType: investigationFileMediaType(source) });
38
+ continue;
39
+ }
26
40
  const content = dependencies.readDiffInput
27
41
  ? await dependencies.readDiffInput(source)
28
42
  : await context.readInput(source, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, 'Manifest artefact input');
@@ -40,11 +54,32 @@ async function runManifestCommand(args, dependencies, context) {
40
54
  }
41
55
  let document;
42
56
  try {
43
- document = await buildInvestigationManifest({
57
+ if (args.package) {
58
+ const built = await (args.bagit ? buildInvestigationBagIt : buildInvestigationPackage)({ workflow: args.workflow, configurationDigestSha256: args.configurationDigestSha256, artifacts }, context.now(), context.packageVersion);
59
+ dependencies.signal?.throwIfAborted();
60
+ if (!context.writeBinaryOutput)
61
+ throw new CliUsageError('Package output requires an explicit file destination.');
62
+ const output = args.passphraseSource
63
+ ? await encryptInvestigationPackage(built.bytes, await context.readPassphraseSource(args.passphraseSource))
64
+ : built.bytes;
65
+ dependencies.signal?.throwIfAborted();
66
+ context.writeBinaryOutput(output);
67
+ return EXIT_CODES.SUCCESS;
68
+ }
69
+ const input = {
44
70
  workflow: args.workflow,
45
71
  configurationDigestSha256: args.configurationDigestSha256,
46
72
  artifacts,
47
- }, context.now(), context.packageVersion);
73
+ };
74
+ if (args.bagit && args.folder) {
75
+ const prepared = await prepareInvestigationBagIt(input, context.now(), context.packageVersion);
76
+ await writePreparedEvidenceFolder(args.folder, prepared.files, 'data', 'tagmanifest-sha512.txt', dependencies.signal);
77
+ document = prepared.manifest;
78
+ }
79
+ else
80
+ document = args.folder
81
+ ? await writeInvestigationFolder(args.folder, input, context.now(), context.packageVersion, dependencies.signal)
82
+ : await buildInvestigationManifest(input, context.now(), context.packageVersion);
48
83
  }
49
84
  catch (error) {
50
85
  throw new CliUsageError(boundedCliErrorMessage(error, 'Investigation manifest input is invalid'));
@@ -52,7 +87,7 @@ async function runManifestCommand(args, dependencies, context) {
52
87
  if (!args.quiet) {
53
88
  context.writeStdout(args.output === 'json'
54
89
  ? formatJsonDocument(document)
55
- : context.terminal(formatInvestigationManifest(document), args.color));
90
+ : context.terminal(`${args.folder ? 'Created a new private evidence folder.\n' : ''}${formatInvestigationManifest(document)}`, args.color));
56
91
  }
57
92
  return EXIT_CODES.SUCCESS;
58
93
  }
@@ -0,0 +1,57 @@
1
+ import { lstat, opendir, realpath } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { readBoundedRegularFileWithin } from "../lib/bounded-file.mjs";
4
+ import { assertBagItSelection, bagItPath, bagItEntryMaximum, MAX_BAGIT_ENTRIES } from "../packages/interchange/bagit.mjs";
5
+ import { selectedEvidenceDirectory, evidenceDirectoryIdentity } from "./investigation-folder.mjs";
6
+ /** Bounded recursive enumeration; no symbolic links, special files or writes. */
7
+ export async function readBagItFolder(value, signal) {
8
+ const selected = selectedEvidenceDirectory(value);
9
+ const identity = await evidenceDirectoryIdentity(selected), root = await realpath(selected);
10
+ const directories = new Map([['', identity]]);
11
+ const entries = [];
12
+ const visit = async (relative) => {
13
+ signal?.throwIfAborted();
14
+ const path = join(root, relative);
15
+ if (await realpath(path) !== path)
16
+ throw new TypeError('BagIt directories must not contain symbolic links.');
17
+ for await (const item of await opendir(path)) {
18
+ signal?.throwIfAborted();
19
+ if (entries.length >= MAX_BAGIT_ENTRIES)
20
+ throw new TypeError('BagIt exceeds its entry limit.');
21
+ const name = `${relative ? `${relative}/` : ''}${item.name}`;
22
+ bagItPath(item.isDirectory() ? `${name}/` : name);
23
+ const stat = await lstat(join(root, name));
24
+ if (item.isDirectory() && stat.isDirectory() && !stat.isSymbolicLink()) {
25
+ directories.set(name, await evidenceDirectoryIdentity(join(root, name)));
26
+ entries.push({ path: `${name}/`, byteLength: 0 });
27
+ await visit(name);
28
+ }
29
+ else if (item.isFile() && stat.isFile() && !stat.isSymbolicLink()) {
30
+ if (stat.size > bagItEntryMaximum(name))
31
+ throw new TypeError('BagIt entry exceeds its byte limit.');
32
+ entries.push({ path: name, byteLength: stat.size });
33
+ }
34
+ else
35
+ throw new TypeError('BagIt entries must be ordinary files and directories, not symbolic links or special files.');
36
+ }
37
+ };
38
+ await visit('');
39
+ assertBagItSelection(entries);
40
+ const files = new Map();
41
+ for (const entry of entries) {
42
+ signal?.throwIfAborted();
43
+ files.set(entry.path, entry.path.endsWith('/') ? new Uint8Array() : await readBoundedRegularFileWithin(root, entry.path, {
44
+ maximumBytes: bagItEntryMaximum(entry.path), minimumBytes: 0, expectedBytes: entry.byteLength,
45
+ label: 'BagIt file', ...(signal ? { signal } : {}),
46
+ }));
47
+ }
48
+ for (const [path, before] of directories) {
49
+ signal?.throwIfAborted();
50
+ const currentPath = path ? join(root, path) : selected;
51
+ const after = await evidenceDirectoryIdentity(currentPath);
52
+ if (await realpath(currentPath) !== join(root, path) || before.device !== after.device || before.inode !== after.inode
53
+ || before.modified !== after.modified || before.changed !== after.changed)
54
+ throw new TypeError('BagIt folder changed while it was read.');
55
+ }
56
+ return files;
57
+ }
@@ -0,0 +1,23 @@
1
+ import { inspectBagItEntries, readBagItZip } from "../packages/interchange/bagit.mjs";
2
+ import { OFFLINE_ARTIFACT_VERIFICATION_SCHEMA, OFFLINE_ARTIFACT_VERIFICATION_VERSION } from "./artifact-verify.mjs";
3
+ export async function verifyOfflineBagIt(input) {
4
+ const files = input instanceof Uint8Array ? readBagItZip(input) : input;
5
+ const zipBytes = input instanceof Uint8Array ? input.byteLength : null;
6
+ const { review, inputBytes } = await inspectBagItEntries(files);
7
+ return {
8
+ schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA, version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
9
+ artifact: { kind: 'bagit', schema: null, version: '1.0' },
10
+ state: review.state === 'valid' ? 'integrity_valid' : 'partial',
11
+ checks: { structure: review.complete ? 'verified' : 'failed',
12
+ contentIntegrity: review.state === 'valid' ? 'verified' : review.state === 'invalid' ? 'failed' : 'not_checked',
13
+ contentIntegrityScope: 'manifest_and_files', authenticatedEncryption: 'not_applicable' },
14
+ summary: { inputBytes: zipBytes ?? inputBytes,
15
+ sectionCount: null, recordCount: review.entries.length, ciphertextBytes: null },
16
+ manifestIdentity: null, bagit: review,
17
+ limitations: [
18
+ 'BagIt verifies declared file checksums, not payload schemas, factual accuracy, source identity, signatures or trusted timestamps.',
19
+ 'Tag files without a checksum are not integrity-checked. No file is imported, executed or fetched; fetch.txt locations and original paths are omitted from this report.',
20
+ 'This bounded reader supports BagIt 1.0, UTF-8 text tags, SHA-256 and SHA-512, portable relative paths and the selected-file byte limits. Other encodings, algorithms and unsafe or platform-ambiguous paths are not silently accepted.',
21
+ ],
22
+ };
23
+ }