@slicedearth/whoisleuth-cli 2.2.0 → 2.4.0

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 (282) hide show
  1. package/NOTICE +6 -0
  2. package/README.md +20 -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/arguments.mjs +454 -1748
  7. package/cli/artifact-validation/case-response.mjs +66 -35
  8. package/cli/artifact-validation/investigation-capsule.mjs +52 -31
  9. package/cli/artifact-validation/lookup-evidence.mjs +12 -6
  10. package/cli/artifact-validation/signed-review.mjs +112 -72
  11. package/cli/artifact-validation/structure-primitives.mjs +1 -105
  12. package/cli/artifact-verify.mjs +90 -24
  13. package/cli/assurance-command-runner.mjs +39 -4
  14. package/cli/bagit-folder.mjs +57 -0
  15. package/cli/bagit-review.mjs +23 -0
  16. package/cli/bounded-json.mjs +1 -1
  17. package/cli/bulk-checkpoint.mjs +34 -3
  18. package/cli/bulk-command-runner.mjs +2 -0
  19. package/cli/bulk-output.mjs +29 -3
  20. package/cli/case-command.mjs +195 -0
  21. package/cli/case-pack.mjs +66 -111
  22. package/cli/command-argument-grammar.mjs +215 -0
  23. package/cli/command-reference.mjs +342 -285
  24. package/cli/completion.mjs +303 -80
  25. package/cli/config-profile.mjs +7 -5
  26. package/cli/ct-event-intake.mjs +12 -12
  27. package/cli/discover.mjs +4 -3
  28. package/cli/discovery-observation-snapshot.mjs +59 -18
  29. package/cli/discovery-scan-command-runner.mjs +21 -0
  30. package/cli/discovery-scan.mjs +13 -4
  31. package/cli/discovery-workflow.mjs +1 -1
  32. package/cli/domain-control-monitor.mjs +50 -20
  33. package/cli/domain-control-observations.mjs +149 -102
  34. package/cli/errors.mjs +33 -1
  35. package/cli/evidence-command-runner.mjs +16 -5
  36. package/cli/evidence-signing.mjs +2 -2
  37. package/cli/external-observation-mapping.mjs +7 -8
  38. package/cli/formatters/terminal-lookup.mjs +18 -46
  39. package/cli/formatters/terminal-shared.mjs +5 -3
  40. package/cli/formatters/terminal-technology.mjs +114 -0
  41. package/cli/inline-command-families.mjs +2 -0
  42. package/cli/input.mjs +116 -0
  43. package/cli/interactive-launcher.mjs +2 -160
  44. package/cli/interchange-report.mjs +18 -8
  45. package/cli/investigation-artifacts.mjs +176 -0
  46. package/cli/investigation-checkpoint.mjs +10 -0
  47. package/cli/investigation-folder.mjs +114 -0
  48. package/cli/investigation-manifest.mjs +1 -112
  49. package/cli/investigation-package-review.mjs +103 -0
  50. package/cli/investigation-plan.mjs +32 -19
  51. package/cli/investigation-run.mjs +188 -33
  52. package/cli/local-document-checkpoint.mjs +170 -0
  53. package/cli/lookalike-calibration.mjs +7 -2
  54. package/cli/lookup-brief.mjs +4 -2
  55. package/cli/lookup-browser-view.mjs +1 -1
  56. package/cli/lookup-browser.mjs +5 -12
  57. package/cli/lookup-command-runner.mjs +17 -6
  58. package/cli/lookup-diff.mjs +37 -3
  59. package/cli/lookup-plan.mjs +11 -5
  60. package/cli/lookup-reconcile.mjs +10 -8
  61. package/cli/mail-header-review.mjs +467 -0
  62. package/cli/mail-review.mjs +66 -14
  63. package/cli/manual.mjs +1 -0
  64. package/cli/network-command-runner.mjs +2 -0
  65. package/cli/output-file.mjs +31 -5
  66. package/cli/registry-doctor.mjs +3 -1
  67. package/cli/registry-support.mjs +1 -1
  68. package/cli/retained-artifact-diff.mjs +2 -2
  69. package/cli/review-command-runner.mjs +70 -0
  70. package/cli/risk-calibration.mjs +19 -17
  71. package/cli/runner.mjs +62 -7
  72. package/cli/sharing-review.mjs +21 -34
  73. package/cli/signer-trust.mjs +104 -0
  74. package/cli/source-reliability.mjs +74 -83
  75. package/cli/terminal-input.mjs +168 -0
  76. package/cli/workflow-command-runner.mjs +68 -13
  77. package/docs/cli-reference.md +77 -24
  78. package/docs/cli.md +300 -25
  79. package/lib/abort.mjs +4 -1
  80. package/lib/availability.mjs +161 -112
  81. package/lib/bounded-file.mjs +3 -1
  82. package/lib/bounded-html-document.mjs +125 -0
  83. package/lib/bounded-json.mjs +51 -8
  84. package/lib/browser-library-profile.mjs +42 -51
  85. package/lib/browser-library-worker.mjs +112 -0
  86. package/lib/client-behavior-profile.mjs +3 -3
  87. package/lib/ct-search.mjs +11 -4
  88. package/lib/dns-convergence-review.mjs +1 -0
  89. package/lib/dns-delegation-health.mjs +24 -14
  90. package/lib/dns-inheritance-review.mjs +217 -0
  91. package/lib/dns-intelligence.mjs +73 -26
  92. package/lib/dns-mx.mjs +1 -1
  93. package/lib/dnssec-chain-validation.mjs +6 -0
  94. package/lib/dnssec-evidence-validation.mjs +11 -3
  95. package/lib/domain-change-packet.mjs +49 -16
  96. package/lib/domain-change-review.mjs +14 -8
  97. package/lib/domain-control-flight-recorder.mjs +221 -117
  98. package/lib/domain-control-manifest.mjs +129 -69
  99. package/lib/domain-posture-analysis.mjs +20 -7
  100. package/lib/domain-posture-parsers.mjs +201 -8
  101. package/lib/domain-posture.mjs +147 -29
  102. package/lib/evidence-export-privacy.mjs +561 -0
  103. package/lib/evidence-export.mjs +11 -543
  104. package/lib/evidence-report.mjs +5 -3
  105. package/lib/favicon.mjs +117 -41
  106. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  107. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  108. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  109. package/lib/hostname.mjs +2 -38
  110. package/lib/html-signals.mjs +111 -108
  111. package/lib/http-evidence-bounds.mjs +2 -1
  112. package/lib/http-intelligence.mjs +39 -26
  113. package/lib/local-geoip-evidence.mjs +19 -3
  114. package/lib/lookup-cache.mjs +34 -11
  115. package/lib/lookup-child-profile-contract.mjs +24 -15
  116. package/lib/lookup-diagnostics.mjs +24 -1
  117. package/lib/lookup-progress.mjs +30 -13
  118. package/lib/lookup-response-contract.mjs +1048 -0
  119. package/lib/lookup-response.mjs +8 -10
  120. package/lib/lookup-source-progress.mjs +40 -14
  121. package/lib/lookup.mjs +48 -27
  122. package/lib/nameserver-preflight-review.mjs +1 -0
  123. package/lib/observed-network-context.mjs +43 -27
  124. package/lib/outbound-request-bounds.mjs +4 -1
  125. package/lib/page-fingerprints.mjs +69 -133
  126. package/lib/page-role-profile.mjs +12 -8
  127. package/lib/perceptual-hash.mjs +43 -14
  128. package/lib/rdap-bootstrap.mjs +72 -22
  129. package/lib/rdap-capabilities.mjs +1 -1
  130. package/lib/rdap-client.mjs +15 -7
  131. package/lib/rdap-normalization.mjs +33 -18
  132. package/lib/rdap-search-workbench.mjs +1 -1
  133. package/lib/rdap-transport.mjs +47 -20
  134. package/lib/rdap-types.mjs +2 -1
  135. package/lib/rdap.mjs +18 -3
  136. package/lib/registrable-domain.mjs +21 -1
  137. package/lib/registrar-standing-contract.mjs +1 -0
  138. package/lib/registry-capabilities.mjs +5 -1
  139. package/lib/registry-capability-catalogue.mjs +2 -1
  140. package/lib/registry-dates.mjs +2 -190
  141. package/lib/registry-insights.mjs +85 -34
  142. package/lib/response-policy.mjs +86 -48
  143. package/lib/risk-calibration-summary.mjs +18 -6
  144. package/lib/rpki-evidence.mjs +8 -5
  145. package/lib/safe-fetch.mjs +54 -47
  146. package/lib/security-txt.mjs +42 -7
  147. package/lib/service-binding-dns.mjs +8 -0
  148. package/lib/smtp-transport-review.mjs +27 -15
  149. package/lib/sslbl-certificates.generated.mjs +136 -135
  150. package/lib/static-html-analysis.mjs +178 -151
  151. package/lib/structured-data-identity.mjs +1 -0
  152. package/lib/technology-markup.mjs +100 -0
  153. package/lib/threat-intelligence-manifests.mjs +11 -139
  154. package/lib/threat-intelligence-runtime.mjs +24 -52
  155. package/lib/threat-intelligence-targets.mjs +6 -34
  156. package/lib/threat-intelligence-types.mjs +39 -0
  157. package/lib/typosquat-generator.mjs +19 -15
  158. package/lib/urlhaus-intelligence.mjs +8 -3
  159. package/lib/urlscan-intelligence.mjs +1 -1
  160. package/lib/website-security-posture.mjs +30 -6
  161. package/lib/website-technology.mjs +153 -45
  162. package/lib/whois-authority.mjs +14 -2
  163. package/lib/whois-chain.mjs +9 -2
  164. package/lib/whois-common-formats.mjs +5 -5
  165. package/lib/whois-contracts.mjs +2 -1
  166. package/lib/whois-dialects.mjs +2 -2
  167. package/lib/whois-parser-support.mjs +8 -1
  168. package/lib/whois-transport.mjs +23 -7
  169. package/lib/zone-intent-review.mjs +16 -6
  170. package/package.json +2 -1
  171. package/packages/cases/case-attachment-model.mjs +83 -0
  172. package/packages/cases/case-evidence-model.mjs +156 -62
  173. package/packages/cases/case-export-input.mjs +53 -0
  174. package/packages/cases/case-migration-model.mjs +127 -44
  175. package/packages/cases/case-model.mjs +1 -0
  176. package/packages/cases/case-recheck-model.mjs +91 -0
  177. package/packages/cases/case-record-contracts.mjs +5 -20
  178. package/packages/cases/case-record-core.mjs +12 -22
  179. package/packages/cases/case-record-decisions.mjs +72 -0
  180. package/packages/cases/case-record-model.mjs +1 -0
  181. package/packages/cases/case-record-operations.mjs +95 -31
  182. package/packages/cases/case-record-projection.mjs +221 -0
  183. package/packages/cases/case-report.mjs +70 -54
  184. package/packages/cases/case-response-actions.mjs +654 -0
  185. package/packages/cases/case-response-model.mjs +64 -1115
  186. package/packages/cases/case-response-outcomes.mjs +317 -0
  187. package/packages/cases/case-response-packet-render.mjs +153 -0
  188. package/packages/cases/case-response-packet-types.mjs +1 -0
  189. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  190. package/packages/cases/case-response-packet.mjs +74 -572
  191. package/packages/cases/case-response-records.mjs +94 -0
  192. package/packages/cases/case-response-review-inputs.mjs +371 -0
  193. package/packages/cases/case-response-values.mjs +85 -0
  194. package/packages/cases/case-review-package.mjs +45 -0
  195. package/packages/cases/case-selection.mjs +46 -0
  196. package/packages/cases/case-storage-model.mjs +31 -2
  197. package/packages/cases/response-route-freshness.mjs +16 -0
  198. package/packages/comparison/capture-context.mjs +61 -0
  199. package/packages/comparison/comparison-ledger-bulk.mjs +13 -11
  200. package/packages/comparison/page-similarity.mjs +5 -2
  201. package/packages/comparison/relationship-evidence.mjs +85 -8
  202. package/packages/comparison/relationship-provenance.mjs +93 -0
  203. package/packages/contracts/analyst-interchange.mjs +216 -0
  204. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  205. package/packages/contracts/analyst-review-state.mjs +5 -4
  206. package/packages/contracts/case-portability.mjs +366 -83
  207. package/packages/contracts/case-views-contract.mjs +12 -0
  208. package/packages/contracts/case-views.mjs +26 -0
  209. package/packages/contracts/cli-command-semantics.mjs +94 -0
  210. package/packages/contracts/cli-lookup.mjs +0 -1
  211. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  212. package/packages/contracts/domain-control-manifest.mjs +127 -73
  213. package/packages/contracts/domain-control-monitor.mjs +48 -45
  214. package/packages/contracts/domain-control-review.mjs +179 -121
  215. package/packages/contracts/domain-name.mjs +21 -0
  216. package/packages/contracts/external-observation-interchange.mjs +2 -1
  217. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  218. package/packages/contracts/http-url.mjs +2 -0
  219. package/packages/contracts/investigation-package-limits.mjs +24 -0
  220. package/packages/contracts/investigation-portability.mjs +56 -27
  221. package/packages/contracts/investigation-run.mjs +16 -0
  222. package/packages/contracts/ip-address.mjs +34 -0
  223. package/packages/contracts/lookup-evidence.mjs +35 -0
  224. package/packages/contracts/offline-comparison.mjs +1 -2
  225. package/packages/contracts/page-fingerprints.mjs +15 -0
  226. package/packages/contracts/risk-calibration.mjs +15 -11
  227. package/packages/contracts/schema-compatibility.mjs +1 -1
  228. package/packages/contracts/schema-lifecycle.mjs +83 -153
  229. package/packages/contracts/selected-file-limits.mjs +6 -0
  230. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  231. package/packages/contracts/web-capture.mjs +10 -0
  232. package/packages/contracts/workspace-portability.mjs +183 -211
  233. package/packages/evidence/artifact-integrity.mjs +8 -2
  234. package/packages/evidence/artifact-structure.mjs +105 -0
  235. package/packages/evidence/decision-fact.mjs +4 -1
  236. package/packages/evidence/domain-control-runtime.mjs +201 -33
  237. package/packages/evidence/domain-posture-context.mjs +79 -0
  238. package/packages/evidence/image-regions.mjs +56 -0
  239. package/packages/evidence/latest-observations.mjs +28 -0
  240. package/packages/evidence/lookup-target.mjs +104 -0
  241. package/packages/evidence/observation.mjs +16 -1
  242. package/packages/evidence/passphrase-encryption.mjs +44 -0
  243. package/packages/evidence/record-identity.mjs +72 -0
  244. package/packages/evidence/registry-dates.mjs +190 -0
  245. package/packages/evidence/retained-file.mjs +69 -0
  246. package/packages/evidence/review-age.mjs +14 -0
  247. package/packages/interchange/bagit.mjs +342 -0
  248. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  249. package/packages/interchange/crc32.mjs +17 -0
  250. package/packages/interchange/external-findings-import.mjs +92 -51
  251. package/packages/interchange/web-capture-import.mjs +382 -0
  252. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  253. package/packages/investigation/investigation-bagit.mjs +14 -0
  254. package/packages/investigation/investigation-capsule.mjs +33 -5
  255. package/packages/investigation/investigation-manifest.mjs +200 -0
  256. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  257. package/packages/investigation/investigation-package.mjs +200 -0
  258. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  259. package/packages/investigation/lookup-asset-graph.mjs +193 -165
  260. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  261. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  262. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  263. package/packages/monitoring/analyst-review-state.mjs +38 -90
  264. package/packages/workspace/brand-profile-model.mjs +276 -22
  265. package/packages/workspace/bulk-columns.mjs +17 -0
  266. package/packages/workspace/bulk-review-model.mjs +23 -11
  267. package/packages/workspace/bulk-session-model.mjs +100 -24
  268. package/packages/workspace/bulk-sort.mjs +1 -0
  269. package/packages/workspace/case-views.mjs +102 -0
  270. package/packages/workspace/detection-rule-model.mjs +70 -22
  271. package/packages/workspace/hostile-input.mjs +29 -8
  272. package/packages/workspace/investigation-template-model.mjs +19 -4
  273. package/packages/workspace/page-baseline.mjs +18 -15
  274. package/packages/workspace/relationship-observation-model.mjs +41 -23
  275. package/packages/workspace/watchlist-history.mjs +2 -2
  276. package/packages/workspace/watchlist-store.mjs +8 -2
  277. package/packages/workspace/website-snapshot-model.mjs +164 -26
  278. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  279. package/packages/workspace/workspace-archive.mjs +87 -21
  280. package/third-party-notices.txt +29 -2
  281. package/cli/evidence-command-arguments.mjs +0 -156
  282. package/packages/investigation/bulk-review-export.mjs +0 -63
@@ -1,24 +1,25 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { createHash } from 'node:crypto';
3
- import { parseBoundedJsonObject } from "./bounded-json.mjs";
3
+ import { readEditableCaseExport } from "../packages/cases/case-export-input.mjs";
4
+ import { boundedJsonLimitsForBytes, parseBoundedJsonObject } from "./bounded-json.mjs";
4
5
  import { validateInvestigationCapsuleStructure, validateLookupEvidenceArtifactStructure, validateOfflineArtifactStructure, validateSignedDigestArtifactStructure, } from "./offline-artifact-validation.mjs";
5
6
  import { verifyCaseResponsePacketIntegrity, } from "../packages/cases/case-response-packet.mjs";
6
7
  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
8
  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";
9
+ import { decryptWorkspaceArchiveWithMetadata, inspectEncryptedWorkspaceArchive, isEncryptedWorkspaceArchive, MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES, } from "../packages/workspace/workspace-archive-crypto.mjs";
10
+ import { WORKSPACE_ARCHIVE_SCHEMA, prepareWorkspaceArchive, readWorkspaceArchive, } from "../packages/workspace/workspace-archive.mjs";
10
11
  import { canonicalArtifactJsonFor, resolveArtifactCanonicalization, sha256ArtifactDigestFor, SORTED_JSON_V2, } from "../packages/evidence/artifact-integrity.mjs";
11
12
  import { SAVED_LOOKUP_SCHEMA, SUPPORTED_SAVED_LOOKUP_SCHEMA_VERSIONS, parseCliLookupDocument, } from "./saved-lookup.mjs";
12
13
  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";
14
+ import { DOMAIN_CONTROL_MANIFEST_CANONICALIZATION_ROUTES, MAX_DOMAIN_CONTROL_MANIFEST_BYTES, } from "../packages/contracts/domain-control-manifest.mjs";
15
+ import { DOMAIN_CHANGE_PACKET_SCHEMA, SUPPORTED_DOMAIN_CHANGE_PACKET_VERSIONS, } from "../lib/domain-change-packet.mjs";
15
16
  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";
17
+ import { INVESTIGATION_MANIFEST_SCHEMA, SUPPORTED_INVESTIGATION_MANIFEST_VERSIONS, } from "./investigation-manifest.mjs";
17
18
  import { verifyCliCasePack } from "./case-pack.mjs";
18
19
  import { LOOKUP_EVIDENCE_PORTABLE_MAX_BYTES, LOOKUP_EVIDENCE_SCHEMA, SUPPORTED_LOOKUP_EVIDENCE_SCHEMA_VERSIONS, } from "../lib/evidence-export.mjs";
19
20
  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;
21
+ export const OFFLINE_ARTIFACT_VERIFICATION_VERSION = 4;
22
+ export const MAX_OFFLINE_ARTIFACT_BYTES = Math.max(MAX_DOMAIN_CONTROL_MANIFEST_BYTES, MAX_ENCRYPTED_WORKSPACE_ARCHIVE_BYTES);
22
23
  export const MAX_OFFLINE_PASSPHRASE_FILE_BYTES = 1024;
23
24
  export class UnsupportedOfflineArtifactError extends TypeError {
24
25
  code = 'unsupported_artifact';
@@ -41,8 +42,8 @@ const SIGNED_ARTIFACT_ROUTES = Object.freeze({
41
42
  [BULK_MAIL_EXPOSURE_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_MAIL_EXPOSURE_EXPORT_VERSIONS),
42
43
  [BULK_REVIEW_MANIFEST_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_BULK_REVIEW_MANIFEST_VERSIONS),
43
44
  [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]),
45
+ [DOMAIN_CHANGE_PACKET_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_DOMAIN_CHANGE_PACKET_VERSIONS),
46
+ [INVESTIGATION_MANIFEST_SCHEMA]: currentCanonicalizationRoutes(SUPPORTED_INVESTIGATION_MANIFEST_VERSIONS),
46
47
  });
47
48
  function record(value) {
48
49
  return value && typeof value === 'object' && !Array.isArray(value)
@@ -83,8 +84,16 @@ function rejectUnsupportedCasePortabilityVersion(label, version, currentVersion)
83
84
  }
84
85
  throw new UnsupportedOfflineArtifactError(`This ${label} version is malformed or unsupported; no data was changed.`);
85
86
  }
86
- function parseJson(raw) {
87
- return parseBoundedJsonObject(raw, { maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES });
87
+ export function parseOfflineArtifactJson(raw) {
88
+ const value = parseBoundedJsonObject(raw, {
89
+ maximumBytes: MAX_OFFLINE_ARTIFACT_BYTES,
90
+ limits: boundedJsonLimitsForBytes(MAX_OFFLINE_ARTIFACT_BYTES),
91
+ });
92
+ if (value.schema !== WORKSPACE_ARCHIVE_SCHEMA && !isEncryptedWorkspaceArchive(value)
93
+ && Buffer.byteLength(raw, 'utf8') > MAX_DOMAIN_CONTROL_MANIFEST_BYTES) {
94
+ throw new TypeError(`Non-workspace artefacts are limited to ${MAX_DOMAIN_CONTROL_MANIFEST_BYTES} bytes.`);
95
+ }
96
+ return value;
88
97
  }
89
98
  export function hasVerifiedArtifactStructure(report) {
90
99
  return report.checks.structure === 'verified';
@@ -92,6 +101,7 @@ export function hasVerifiedArtifactStructure(report) {
92
101
  export function hasVerifiedApplicableIntegrity(report) {
93
102
  return report.checks.contentIntegrity === 'verified'
94
103
  && (report.checks.contentIntegrityScope === 'whole_artifact'
104
+ || report.checks.contentIntegrityScope === 'manifest_and_files'
95
105
  || report.checks.contentIntegrityScope === 'embedded_projections');
96
106
  }
97
107
  export function hasVerifiedWholeArtifactIntegrity(report) {
@@ -108,6 +118,9 @@ export function offlineArtifactSatisfiesAssurance(report, requirement) {
108
118
  return hasVerifiedWholeArtifactIntegrity(report);
109
119
  }
110
120
  export function isCompleteOfflineArtifactVerification(report) {
121
+ if (report.artifact.kind === 'bagit')
122
+ return report.state === 'integrity_valid' && report.bagit?.state === 'valid'
123
+ && report.checks.structure === 'verified' && report.checks.contentIntegrity === 'verified';
111
124
  return (report.state === 'verified' || report.state === 'structure_valid')
112
125
  && (report.manifestIdentity === null || report.manifestIdentity.state === 'identity_verified');
113
126
  }
@@ -124,8 +137,7 @@ function artifactVersion(value) {
124
137
  function inputBytes(raw) {
125
138
  return Buffer.byteLength(raw, 'utf8');
126
139
  }
127
- async function archiveReport(raw, source, archive, encrypted, ciphertextBytes) {
128
- const preview = await previewWorkspaceArchive(source, {});
140
+ function archiveReport(raw, archive, preview, encrypted, ciphertextBytes) {
129
141
  const readySectionCount = preview.sections.filter((section) => section.status === 'ready').length;
130
142
  const unsupportedSectionCount = preview.sections.filter((section) => section.status === 'unsupported').length;
131
143
  const blockedSectionCount = preview.sections.filter((section) => section.status === 'blocked').length;
@@ -172,6 +184,24 @@ async function archiveReport(raw, source, archive, encrypted, ciphertextBytes) {
172
184
  ]),
173
185
  });
174
186
  }
187
+ async function verifyWorkspaceArchiveValue(raw, value, passphrase) {
188
+ const encrypted = isEncryptedWorkspaceArchive(value);
189
+ if (encrypted && !passphrase) {
190
+ throw new TypeError('Encrypted archive inspection requires a separate passphrase file.');
191
+ }
192
+ if (!encrypted)
193
+ artifactVersion(value);
194
+ const decrypted = encrypted ? await decryptWorkspaceArchiveWithMetadata(value, passphrase) : null;
195
+ const source = decrypted ? decrypted.archive : value;
196
+ const prepared = await prepareWorkspaceArchive(source);
197
+ const archive = prepared.read();
198
+ const report = archiveReport(raw, archive, prepared.preview({}), encrypted, decrypted?.ciphertextBytes ?? null);
199
+ return { archive, report, encrypted };
200
+ }
201
+ /** Verify an archive once and retain its sections for bounded offline inspection. */
202
+ export async function verifyOfflineWorkspaceArchive(raw, options = {}) {
203
+ return verifyWorkspaceArchiveValue(raw, parseOfflineArtifactJson(raw), options.passphrase);
204
+ }
175
205
  async function verifySignedArtifact(raw, value, schema, version) {
176
206
  const routes = SIGNED_ARTIFACT_ROUTES[schema] ?? [];
177
207
  if (!routes.some((route) => route.version === version)) {
@@ -215,7 +245,7 @@ async function verifySignedArtifact(raw, value, schema, version) {
215
245
  });
216
246
  }
217
247
  async function verifyOfflineArtifactCore(raw, options = {}) {
218
- const value = parseJson(raw);
248
+ const value = parseOfflineArtifactJson(raw);
219
249
  const casePortabilityVerifier = selectCasePortabilityVerifier(value);
220
250
  if (casePortabilityVerifier?.id === 'cli-case-pack') {
221
251
  const casePackVersion = casePortabilityVersion(casePortabilityVerifier, value);
@@ -249,9 +279,24 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
249
279
  ]),
250
280
  });
251
281
  }
282
+ if (value.schema === undefined && Array.isArray(value.cases)) {
283
+ const cases = readEditableCaseExport(raw);
284
+ return Object.freeze({
285
+ schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA, version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
286
+ artifact: Object.freeze({ kind: 'case_export', schema: null, version: artifactVersion(value) }),
287
+ state: 'structure_valid',
288
+ checks: Object.freeze({ structure: 'verified', contentIntegrity: 'not_checked', contentIntegrityScope: 'not_applicable', authenticatedEncryption: 'not_applicable' }),
289
+ summary: Object.freeze({ inputBytes: inputBytes(raw), sectionCount: 1, recordCount: cases.length, ciphertextBytes: null }),
290
+ limitations: Object.freeze([
291
+ 'The ordinary Case export matches its exact supported structure without repairing or removing retained records. It has no embedded checksum or signature.',
292
+ 'Original-file references do not include original bytes. Verify a selected evidence package to check which references have matching bytes.',
293
+ 'The Case includes private analyst content. Structural validity does not authenticate its author or establish factual accuracy, currentness or response authority.',
294
+ ]),
295
+ });
296
+ }
252
297
  if (isEncryptedWorkspaceArchive(value)) {
253
- const inspected = inspectEncryptedWorkspaceArchive(value);
254
298
  if (!options.passphrase) {
299
+ const inspected = inspectEncryptedWorkspaceArchive(value);
255
300
  return Object.freeze({
256
301
  schema: OFFLINE_ARTIFACT_VERIFICATION_SCHEMA,
257
302
  version: OFFLINE_ARTIFACT_VERIFICATION_VERSION,
@@ -279,14 +324,12 @@ async function verifyOfflineArtifactCore(raw, options = {}) {
279
324
  ]),
280
325
  });
281
326
  }
282
- const decrypted = await decryptWorkspaceArchive(value, options.passphrase);
283
- const archive = await readWorkspaceArchive(decrypted);
284
- return archiveReport(raw, decrypted, archive, true, inspected.ciphertextBytes);
327
+ return (await verifyWorkspaceArchiveValue(raw, value, options.passphrase)).report;
285
328
  }
286
329
  const schema = typeof value.schema === 'string' ? value.schema : '';
287
330
  const version = artifactVersion(value);
288
331
  if (schema === WORKSPACE_ARCHIVE_SCHEMA) {
289
- return archiveReport(raw, value, await readWorkspaceArchive(value), false, null);
332
+ return (await verifyWorkspaceArchiveValue(raw, value)).report;
290
333
  }
291
334
  if (casePortabilityVerifier?.id === 'case-response-packet') {
292
335
  if (!supportsCasePortabilityVersion(casePortabilityVerifier, version)) {
@@ -432,13 +475,13 @@ async function verifyManifestIdentity(artifactRaw, manifestRaw, entryId) {
432
475
  || manifestReport.checks.contentIntegrityScope !== 'whole_artifact') {
433
476
  throw new TypeError('The selected manifest is not a fully integrity-verified investigation manifest.');
434
477
  }
435
- const manifest = parseJson(manifestRaw);
478
+ const manifest = parseOfflineArtifactJson(manifestRaw);
436
479
  const entries = Array.isArray(manifest.artifacts) ? manifest.artifacts : [];
437
480
  const entry = entries.find((candidate) => record(candidate)?.id === entryId);
438
481
  const item = record(entry);
439
482
  if (!item)
440
483
  throw new TypeError('The requested investigation manifest entry was not found.');
441
- const artifactValue = parseJson(artifactRaw);
484
+ const artifactValue = parseOfflineArtifactJson(artifactRaw);
442
485
  const metadata = artifactMetadata(artifactValue);
443
486
  const actualByteLength = inputBytes(artifactRaw);
444
487
  const expectedByteLength = Number(item.byteLength);
@@ -466,7 +509,7 @@ async function verifyManifestIdentity(artifactRaw, manifestRaw, entryId) {
466
509
  : 'mismatch';
467
510
  return Object.freeze({
468
511
  state,
469
- manifest: Object.freeze({ schema: INVESTIGATION_MANIFEST_SCHEMA, version: INVESTIGATION_MANIFEST_VERSION, entryId }),
512
+ manifest: Object.freeze({ schema: INVESTIGATION_MANIFEST_SCHEMA, version: manifestReport.artifact.version, entryId }),
470
513
  checks,
471
514
  expectedByteLength,
472
515
  actualByteLength,
@@ -490,8 +533,9 @@ export async function verifyOfflineArtifact(raw, options = {}) {
490
533
  export function formatOfflineArtifactVerification(report) {
491
534
  const lines = [
492
535
  'WHOISleuth offline artefact verification',
493
- `Artifact: ${report.artifact.kind} · ${report.artifact.schema} v${report.artifact.version}`,
536
+ `Artifact: ${report.artifact.kind} · ${report.artifact.schema ?? 'no schema identifier'} v${report.artifact.version}`,
494
537
  `State: ${report.state}`,
538
+ 'Automation: use --strict-exit for exit 4 on incomplete verification; default exit 0 means the report was produced.',
495
539
  `Structure: ${report.checks.structure}`,
496
540
  `Content integrity: ${report.checks.contentIntegrity}`,
497
541
  `Content integrity scope: ${report.checks.contentIntegrityScope}`,
@@ -534,6 +578,28 @@ export function formatOfflineArtifactVerification(report) {
534
578
  for (const limitation of report.manifestIdentity.limitations)
535
579
  lines.push(`Manifest limitation: ${limitation}`);
536
580
  }
581
+ if (report.bagit) {
582
+ const bag = report.bagit;
583
+ 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}`);
584
+ for (const entry of bag.entries)
585
+ lines.push(`${entry.id}: ${entry.state} · ${entry.byteLength ?? 'unknown'} bytes`);
586
+ for (const issue of bag.issues)
587
+ lines.push(`BagIt issue: ${issue}`);
588
+ }
589
+ if (report.package) {
590
+ 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');
591
+ for (const entry of report.package.entries)
592
+ lines.push(`${entry.id}: ${entry.state} · ${entry.byteLength} bytes · ${entry.mediaType}${entry.issue ? ` · ${entry.issue}` : ''}`);
593
+ for (const link of report.package.links)
594
+ lines.push(`Source link ${link.capsuleEntryId}: ${link.state}${link.sourceEntryId ? ` (${link.sourceEntryId})` : ''}`);
595
+ for (const capture of report.package.captureManifests) {
596
+ lines.push(`Capture manifest ${capture.entryId}: ${capture.state}`);
597
+ for (const artifact of capture.artifacts)
598
+ lines.push(`Capture ${artifact.capture} ${artifact.kind}: ${artifact.state}${artifact.matchingIds.length ? ` (${artifact.matchingIds.join(', ')})` : ''}`);
599
+ }
600
+ for (const item of report.package.caseFiles)
601
+ lines.push(`Case files ${item.entryId} (${item.caseCount} Case${item.caseCount === 1 ? '' : 's'}): ${item.matched} of ${item.references} references matched; ${item.missing} missing`);
602
+ }
537
603
  for (const limitation of report.limitations)
538
604
  lines.push(`Limitation: ${limitation}`);
539
605
  return `${lines.join('\n')}\n`;
@@ -1,4 +1,9 @@
1
1
  import { Buffer } from 'node:buffer';
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";
2
7
  import { scanBoundedJson } from "../lib/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";
@@ -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
+ }
@@ -1 +1 @@
1
- export { MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../lib/bounded-json.mjs";
1
+ export { boundedJsonLimitsForBytes, MAX_BOUNDED_JSON_DEPTH, MAX_BOUNDED_JSON_KEYS, MAX_BOUNDED_JSON_VALUES, parseBoundedJson, parseBoundedJsonObject, scanBoundedJson, } from "../lib/bounded-json.mjs";
@@ -54,6 +54,24 @@ function normalizeCompactResult(value) {
54
54
  return null;
55
55
  return result;
56
56
  }
57
+ function normalizeCollectionContext(value) {
58
+ if (value === undefined)
59
+ return undefined;
60
+ const context = recordOrNull(value);
61
+ if (!context || Object.keys(context).some((key) => !['dnsResolver', 'resolverServers'].includes(key)))
62
+ return null;
63
+ const resolverServers = context.resolverServers;
64
+ if (!Array.isArray(resolverServers) || resolverServers.length > 3
65
+ || resolverServers.some((server) => typeof server !== 'string' || !server || server.length > 45))
66
+ return null;
67
+ if (context.dnsResolver === 'system_default' && resolverServers.length === 0) {
68
+ return { dnsResolver: 'system_default', resolverServers: [] };
69
+ }
70
+ if (context.dnsResolver === 'analyst_selected' && resolverServers.length > 0) {
71
+ return { dnsResolver: 'analyst_selected', resolverServers: [...resolverServers] };
72
+ }
73
+ return null;
74
+ }
57
75
  function normalizeCheckpointResult(value, queries, classifyQuery) {
58
76
  const item = recordOrNull(value);
59
77
  const index = item?.index;
@@ -67,10 +85,16 @@ function normalizeCheckpointResult(value, queries, classifyQuery) {
67
85
  : normalizedTimestamp(item.observedAt) ?? undefined;
68
86
  if (observedAt === undefined)
69
87
  return null;
88
+ const collectionContext = normalizeCollectionContext(item.collectionContext);
89
+ if (collectionContext === null)
90
+ return null;
70
91
  if (item.ok === false) {
71
92
  if (typeof item.error !== 'string' || !item.error || item.error.length > 300)
72
93
  return null;
73
- return { index: Number(index), query, ok: false, error: item.error, observedAt };
94
+ return {
95
+ index: Number(index), query, ok: false, error: item.error, observedAt,
96
+ ...(collectionContext ? { collectionContext } : {}),
97
+ };
74
98
  }
75
99
  const result = normalizeCompactResult(item.result);
76
100
  if (item.ok !== true || !result)
@@ -79,7 +103,10 @@ function normalizeCheckpointResult(value, queries, classifyQuery) {
79
103
  const storedClassified = recordOrNull(item.classified);
80
104
  if (storedClassified?.type !== classified.type || storedClassified.value !== classified.value)
81
105
  return null;
82
- return { index: Number(index), query, ok: true, classified, result, observedAt };
106
+ return {
107
+ index: Number(index), query, ok: true, classified, result, observedAt,
108
+ ...(collectionContext ? { collectionContext } : {}),
109
+ };
83
110
  }
84
111
  function parseBulkCheckpoint(text, options) {
85
112
  if (typeof text !== 'string' || Buffer.byteLength(text, 'utf8') > MAX_BULK_CHECKPOINT_BYTES) {
@@ -214,8 +241,10 @@ async function createBulkCheckpointWriter(options) {
214
241
  }
215
242
  }
216
243
  function scheduleWrite() {
244
+ if (writeFailure)
245
+ return;
217
246
  dirty = true;
218
- if (activeWrite || writeFailure)
247
+ if (activeWrite)
219
248
  return;
220
249
  const pending = drainWrites();
221
250
  activeWrite = pending;
@@ -250,6 +279,8 @@ async function createBulkCheckpointWriter(options) {
250
279
  },
251
280
  async flush() {
252
281
  while (activeWrite || dirty) {
282
+ if (writeFailure && !activeWrite)
283
+ break;
253
284
  if (!activeWrite)
254
285
  scheduleWrite();
255
286
  if (activeWrite)
@@ -113,6 +113,8 @@ async function runBulkCommand(args, dependencies, context) {
113
113
  context.writeStdout(formatJsonLines(selectedItems, metadata));
114
114
  else if (args.output === 'csv')
115
115
  context.writeStdout(formatBulkCsv(selectedItems));
116
+ else if (args.output === 'csv_metadata')
117
+ context.writeStdout(formatBulkCsv(selectedItems, selectedDocument));
116
118
  else if (args.output === 'domains')
117
119
  context.writeStdout(formatBulkDomainList(selectedItems));
118
120
  else if (args.output === 'queries')
@@ -1,6 +1,25 @@
1
1
  import { cliCsvCell } from "./csv.mjs";
2
+ import { lookupDiagnosticStates } from "../lib/lookup-diagnostics.mjs";
3
+ import { normalizeExplicitIsoTimestamp } from "../packages/evidence/observation.mjs";
2
4
  const REGISTERED_STATES = new Set(['expiring', 'for_sale', 'registered']);
3
5
  const MAX_DNS_VALUES_PER_TYPE = 100;
6
+ const BULK_CSV_METADATA_COLUMNS = Object.freeze([
7
+ 'source_schema', 'source_version', 'observed_at', 'report_generated_at',
8
+ 'collection_origin', 'scan_mode', 'diagnostics_version', 'source_health',
9
+ ]);
10
+ function bulkCsvMetadataValues(item, metadata) {
11
+ const diagnostics = record(item.diagnostics);
12
+ return [
13
+ metadata.schema,
14
+ metadata.version,
15
+ normalizeExplicitIsoTimestamp(item.observedAt) ?? 'unknown',
16
+ normalizeExplicitIsoTimestamp(metadata.generatedAt) ?? 'unknown',
17
+ item.collectionOrigin === 'current_run' || item.collectionOrigin === 'resumed_checkpoint' ? item.collectionOrigin : 'unknown',
18
+ metadata.mode,
19
+ Number.isSafeInteger(diagnostics.version) && Number(diagnostics.version) > 0 ? diagnostics.version : 'unknown',
20
+ JSON.stringify(lookupDiagnosticStates(item.diagnostics)),
21
+ ];
22
+ }
4
23
  function record(value) {
5
24
  return value && typeof value === 'object' && !Array.isArray(value)
6
25
  ? value
@@ -78,12 +97,18 @@ function bulkDnsSummary(item) {
78
97
  function booleanCell(value) {
79
98
  return value === null ? 'unknown' : value ? 'observed' : 'not_observed';
80
99
  }
81
- function formatBulkCsv(items) {
100
+ function formatBulkCsv(items, metadata) {
101
+ if (metadata && metadata.results.length !== items.length)
102
+ throw new TypeError('CSV metadata does not match the selected result count.');
82
103
  const header = [
83
104
  'query', 'domain', 'outcome', 'availability', 'confidence', 'dns_status',
84
105
  'a', 'aaaa', 'ns', 'mx', 'null_mx', 'spf', 'dmarc', 'error',
106
+ ...(metadata ? BULK_CSV_METADATA_COLUMNS : []),
85
107
  ];
86
- const rows = items.map((item) => {
108
+ const rows = items.map((item, index) => {
109
+ const retained = metadata?.results[index];
110
+ if (metadata && (!retained || retained.index !== item.index || retained.query !== item.query))
111
+ throw new TypeError('CSV metadata does not match the selected result identity.');
87
112
  const availability = item.ok ? record(record(item.result).availability) : {};
88
113
  const dns = bulkDnsSummary(item);
89
114
  return [
@@ -101,6 +126,7 @@ function formatBulkCsv(items) {
101
126
  booleanCell(dns.hasSpf),
102
127
  booleanCell(dns.hasDmarc),
103
128
  item.ok ? '' : item.error,
129
+ ...(metadata && retained ? bulkCsvMetadataValues(retained, metadata) : []),
104
130
  ].map(cliCsvCell).join(',');
105
131
  });
106
132
  return `${[header.join(','), ...rows].join('\n')}\n`;
@@ -114,4 +140,4 @@ function formatBulkDomainList(items) {
114
140
  function formatBulkQueryList(items) {
115
141
  return items.length ? `${items.map((item) => item.query).join('\n')}\n` : '';
116
142
  }
117
- export { REGISTERED_STATES, availabilityState, bulkDnsSummary, formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems, };
143
+ export { REGISTERED_STATES, BULK_CSV_METADATA_COLUMNS, availabilityState, bulkDnsSummary, bulkCsvMetadataValues, formatBulkCsv, formatBulkDomainList, formatBulkQueryList, selectBulkItems, };