@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
package/NOTICE CHANGED
@@ -38,6 +38,12 @@ distributed as third-party-notices.txt. The website inventory is served at
38
38
  only the CLI runtime dependency closure. Both are generated from the reviewed
39
39
  lockfile, and the website inventory is checked for lockfile drift in CI.
40
40
 
41
+ The development-only fixtures/risk-evaluation/rows.json is a minimised,
42
+ stratified adaptation of PhiUSIIL Phishing URL (Website), Arvind Prasad and
43
+ Shalini Chandra (2024), licensed under CC BY 4.0 without warranties. The source
44
+ and licence links, modifications and reproduction instructions are in that
45
+ directory's README.md. This dataset is not included in the website or CLI.
46
+
41
47
  The generated Unicode confusable projection is derived from Unicode UTS #39
42
48
  data file confusables.txt, Unicode version 17.0.0.
43
49
 
package/README.md CHANGED
@@ -41,6 +41,7 @@ whoisleuth lookup example.test --deep --summary
41
41
  whoisleuth bulk domains.txt --csv
42
42
  whoisleuth discover example.test --preset common --jsonl
43
43
  whoisleuth compare lookup.json --json
44
+ whoisleuth mail-headers message.eml --json
44
45
  whoisleuth verify-artifact lookup.json --json --strict-exit
45
46
  ```
46
47
 
@@ -50,14 +51,7 @@ only `bulk` accepts multiple targets.
50
51
 
51
52
  ## Command groups
52
53
 
53
- | Group | Common commands |
54
- | --- | --- |
55
- | Investigate | `lookup`, `bulk`, `discover`, `ct-search`, `posture`, `http`, `tls`, `compare`, `brief` |
56
- | Respond | `case-pack`, `change-packet`, `sharing-review`, `export` |
57
- | Assure | `dnssec-validate`, `mail-transport`, `domain-control`, `assurance`, `workflow-plan`, `diff`, `inspect-archive`, `verify-artifact` |
58
- | Utilities | `doctor`, `commands`, `completion`, `manual`, `registry-scaffold` |
59
-
60
- Use the installed registry for the complete inventory:
54
+ Use the installed registry to browse Investigate, Respond, Assure and Utilities:
61
55
 
62
56
  ```bash
63
57
  whoisleuth commands --group investigate
@@ -66,10 +60,6 @@ whoisleuth commands --json
66
60
  whoisleuth manual | man -l -
67
61
  ```
68
62
 
69
- `registry-scaffold` has a separate fixture contract: its `--profile` selects a
70
- fixed fixture profile and it rejects shared `--config` profiles. It creates
71
- sanitised local fixture material.
72
-
73
63
  ## Collection and output
74
64
 
75
65
  Fast Lookup is registration-first. Deep Lookup explicitly adds the applicable
@@ -85,9 +75,24 @@ or domain-only output only where declared. `--output` writes a private local
85
75
  file atomically and refuses an existing path unless `--force` is selected.
86
76
  `--strict-exit` and `--fail-on` expose selected evidence states to automation.
87
77
 
88
- Exit codes are 0 for completion, 2 for invalid input, 3 for collection or
89
- comparison failure, 4 for a partial result or unmet selected evidence policy,
90
- 70 for internal bootstrap failure, 130 for cancellation and 143 for SIGTERM.
78
+ `case open --domain example.test --output cases.json` creates an ordinary local
79
+ Case file. `case show`, `note`, `pin`, `assess` and `recheck` review or update it
80
+ offline. Mutations require explicit output and preserve earlier retained evidence;
81
+ see `docs/cli.md#local-case-files` for JSON inputs and conflict handling. Working
82
+ files contain private analyst content, not attached file bytes.
83
+
84
+ `manifest --package --output evidence.wlep --passphrase-file ./passphrase.txt`
85
+ encrypts selected evidence files and their manifest. Use the same explicit
86
+ passphrase file with `verify-artifact --package` to authenticate and verify it
87
+ offline. Ordinary ZIPs and folder exports remain unencrypted; see `docs/cli.md`
88
+ for complete examples and file-handling boundaries.
89
+
90
+ `workflow-run` pauses on a partial collection. Resume retains it without
91
+ recollection and keeps exit code 4 even if later steps finish. Failed validation
92
+ or export steps remain retryable. See `docs/cli.md` for checkpoint compatibility.
93
+
94
+ The included `docs/cli-reference.md` lists common exit codes and compatibility
95
+ contracts. A completed command can still contain partial source evidence.
91
96
 
92
97
  ## Repository checkout
93
98
 
@@ -1,7 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { createRequire } from 'node:module';
3
3
  import EXIT_CODES from "../cli/exit-codes.mjs";
4
- import { metaActionDefinition } from "../cli/command-reference.mjs";
4
+ import { HELP, commandHelp, isCliCommand, metaActionDefinition } from "../cli/command-reference.mjs";
5
+ import { presentTerminalOutput, terminalPresentation } from "../cli/terminal-presentation.mjs";
5
6
  for (const stream of [process.stdout, process.stderr]) {
6
7
  stream.on('error', (error) => {
7
8
  if (error.code === 'EPIPE')
@@ -10,6 +11,9 @@ for (const stream of [process.stdout, process.stderr]) {
10
11
  });
11
12
  }
12
13
  const argv = process.argv.slice(2);
14
+ const rootHelp = argv.length === 1 && metaActionDefinition('help').aliases.includes(argv[0] ?? '');
15
+ const command = argv.length === 2 && isCliCommand(argv[0])
16
+ && metaActionDefinition('help').aliases.includes(argv[1] ?? '') ? argv[0] : null;
13
17
  if (argv.length === 1 && metaActionDefinition('version').aliases.includes(argv[0] ?? '')) {
14
18
  const require = createRequire(import.meta.url);
15
19
  const metadata = require('../package.json');
@@ -21,6 +25,9 @@ if (argv.length === 1 && metaActionDefinition('version').aliases.includes(argv[0
21
25
  process.stdout.write(`${metadata.version}\n`);
22
26
  }
23
27
  }
28
+ else if (rootHelp || command) {
29
+ process.stdout.write(presentTerminalOutput(command ? commandHelp(command) : HELP, terminalPresentation(process.stdout)));
30
+ }
24
31
  else {
25
32
  const [configModule, errorsModule, outputModule, runnerModule] = await Promise.all([
26
33
  import("../cli/config-profile.mjs"),
@@ -0,0 +1,14 @@
1
+ /** Bare hashes retain their original v1 meaning; new identities declare v2. */
2
+ export function parseArchiveContentDigest(value) {
3
+ if (value === null || value === undefined || value === '')
4
+ return null;
5
+ if (typeof value !== 'string' || value.length > 'sorted-json-v2:sha256:'.length + 64) {
6
+ throw new TypeError('Expected archive content digest must be sha256:<64 lowercase hex> or sorted-json-v2:sha256:<64 lowercase hex>.');
7
+ }
8
+ const explicit = value.startsWith('sorted-json-v2:');
9
+ const digestSha256 = explicit ? value.slice('sorted-json-v2:'.length) : value;
10
+ if (!/^sha256:[a-f0-9]{64}$/u.test(digestSha256)) {
11
+ throw new TypeError('Expected archive content digest must be sha256:<64 lowercase hex> or sorted-json-v2:sha256:<64 lowercase hex>.');
12
+ }
13
+ return Object.freeze({ canonicalization: explicit ? 'sorted-json-v2' : 'sorted-json-v1', digestSha256 });
14
+ }
@@ -1,14 +1,13 @@
1
1
  import { Buffer } from 'node:buffer';
2
2
  import { createHash } from 'node:crypto';
3
3
  import { domainToASCII } from 'node:url';
4
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
5
- import { decryptWorkspaceArchive, isEncryptedWorkspaceArchive, } from "../packages/workspace/workspace-archive-crypto.mjs";
6
4
  import { WORKSPACE_ARCHIVE_SCHEMA, readWorkspaceArchive, } from "../packages/workspace/workspace-archive.mjs";
7
- import { canonicalArtifactJson } from "../packages/evidence/artifact-integrity.mjs";
8
- import { MAX_OFFLINE_ARTIFACT_BYTES, verifyOfflineArtifact, } from "./artifact-verify.mjs";
5
+ import { canonicalArtifactJson, canonicalArtifactJsonV2 } from "../packages/evidence/artifact-integrity.mjs";
6
+ import { parseArchiveContentDigest } from "./archive-content-digest.mjs";
7
+ import { verifyOfflineWorkspaceArchive } from "./artifact-verify.mjs";
9
8
  import { safeTerminalValue } from "./formatters/terminal.mjs";
10
9
  export const ARCHIVE_INSPECTION_SCHEMA = 'whoisleuth.workspace-archive-inspection';
11
- export const ARCHIVE_INSPECTION_VERSION = 2;
10
+ export const ARCHIVE_INSPECTION_VERSION = 3;
12
11
  export const MAX_ARCHIVE_SEARCH_LENGTH = 253;
13
12
  export const MAX_ARCHIVE_SEARCH_MATCHES = 100;
14
13
  export const MAX_ARCHIVE_SEARCH_NODES = 100_000;
@@ -22,32 +21,11 @@ const SEARCHABLE_FIELDS = new Set([
22
21
  'target',
23
22
  ]);
24
23
  const CONTROL_RE = /[\u0000-\u001f\u007f]/u;
25
- const SHA256_DIGEST_RE = /^sha256:[a-f0-9]{64}$/u;
26
24
  function record(value) {
27
25
  return value && typeof value === 'object' && !Array.isArray(value)
28
26
  ? value
29
27
  : null;
30
28
  }
31
- function parseJson(raw) {
32
- if (typeof raw !== 'string')
33
- throw new TypeError('Archive input must be UTF-8 JSON text.');
34
- const bytes = Buffer.byteLength(raw, 'utf8');
35
- if (bytes === 0 || bytes > MAX_OFFLINE_ARTIFACT_BYTES) {
36
- throw new TypeError(`Archive input must be between 1 byte and ${MAX_OFFLINE_ARTIFACT_BYTES} bytes.`);
37
- }
38
- let parsed;
39
- try {
40
- scanBoundedJson(raw);
41
- parsed = JSON.parse(raw);
42
- }
43
- catch {
44
- throw new TypeError('Archive input must be valid bounded JSON without duplicate keys.');
45
- }
46
- const value = record(parsed);
47
- if (!value)
48
- throw new TypeError('Archive input must contain one JSON object.');
49
- return value;
50
- }
51
29
  function normalizeSearch(value) {
52
30
  if (value === null || value === undefined || value === '')
53
31
  return null;
@@ -71,7 +49,7 @@ function canonicalSearchValue(value) {
71
49
  function valueDigest(value) {
72
50
  return createHash('sha256').update(value, 'utf8').digest('hex');
73
51
  }
74
- function archiveContentDigest(sections) {
52
+ function archiveContentDigests(sections) {
75
53
  const identity = sections.map((section) => ({
76
54
  id: section.id,
77
55
  schema: section.schema,
@@ -85,15 +63,10 @@ function archiveContentDigest(sections) {
85
63
  return content;
86
64
  })(),
87
65
  }));
88
- return `sha256:${createHash('sha256').update(canonicalArtifactJson(identity), 'utf8').digest('hex')}`;
89
- }
90
- function expectedContentDigest(value) {
91
- if (value === null || value === undefined || value === '')
92
- return null;
93
- if (typeof value !== 'string' || !SHA256_DIGEST_RE.test(value)) {
94
- throw new TypeError('Expected archive content digest must use the form sha256 followed by 64 lowercase hexadecimal characters.');
95
- }
96
- return value;
66
+ return Object.freeze({
67
+ current: `sha256:${createHash('sha256').update(canonicalArtifactJsonV2(identity), 'utf8').digest('hex')}`,
68
+ legacy: `sha256:${createHash('sha256').update(canonicalArtifactJson(identity), 'utf8').digest('hex')}`,
69
+ });
97
70
  }
98
71
  function searchSection(section, value, search, reveal, results, state) {
99
72
  const stack = [{ value, depth: 0 }];
@@ -146,21 +119,10 @@ function searchSection(section, value, search, reveal, results, state) {
146
119
  }
147
120
  }
148
121
  export async function inspectWorkspaceArchive(raw, options = {}) {
149
- const parsed = parseJson(raw);
150
- const encrypted = isEncryptedWorkspaceArchive(parsed);
151
- if (encrypted && !options.passphrase) {
152
- throw new TypeError('Encrypted archive inspection requires a separate passphrase file.');
153
- }
154
- await verifyOfflineArtifact(raw, options.passphrase === undefined
155
- ? {}
156
- : { passphrase: options.passphrase });
157
- const archiveValue = encrypted
158
- ? await decryptWorkspaceArchive(parsed, options.passphrase)
159
- : parsed;
160
- const archive = await readWorkspaceArchive(archiveValue);
161
- const contentDigestSha256 = archiveContentDigest(archive.sections);
162
- const expectedDigest = expectedContentDigest(options.expectedContentDigest);
163
- if (expectedDigest && expectedDigest !== contentDigestSha256) {
122
+ const expectedDigest = parseArchiveContentDigest(options.expectedContentDigest);
123
+ const { archive, encrypted } = await verifyOfflineWorkspaceArchive(raw, options);
124
+ const digests = archiveContentDigests(archive.sections);
125
+ if (expectedDigest && expectedDigest.digestSha256 !== (expectedDigest.canonicalization === 'sorted-json-v2' ? digests.current : digests.legacy)) {
164
126
  throw new TypeError('Workspace archive content digest did not match the expected value.');
165
127
  }
166
128
  const search = normalizeSearch(options.search);
@@ -196,7 +158,9 @@ export async function inspectWorkspaceArchive(raw, options = {}) {
196
158
  inputBytes: Buffer.byteLength(raw, 'utf8'),
197
159
  sectionCount: archive.sections.length,
198
160
  recordCount: archive.sections.reduce((sum, section) => sum + section.recordCount, 0),
199
- contentDigestSha256,
161
+ contentDigest: `sorted-json-v2:${digests.current}`,
162
+ contentDigestSha256: digests.legacy,
163
+ expectedContentDigestCanonicalization: expectedDigest?.canonicalization ?? null,
200
164
  }),
201
165
  sections: Object.freeze(archive.sections.map((section) => Object.freeze({
202
166
  id: section.id,
@@ -218,6 +182,7 @@ export async function inspectWorkspaceArchive(raw, options = {}) {
218
182
  limitations: Object.freeze([
219
183
  'Inspection validates the archive and reports bounded section metadata without printing retained evidence by default.',
220
184
  'The content digest covers ordered section identities and normalised content after excluding each section export timestamp. Equality detects matching retained archive content but does not authenticate its source or accuracy.',
185
+ 'Use the versioned contentDigest identity for new comparisons. The compatibility contentDigestSha256 field and bare expected hashes retain the original locale-sensitive digest; they are not a cross-locale identity guarantee.',
221
186
  'Search checks exact values in a small allowlist of target fields and never searches analyst notes, contacts, or raw evidence.',
222
187
  ...(reveal
223
188
  ? ['Search values were revealed only because the operator supplied --reveal; handle the output as sensitive evidence.']
@@ -234,7 +199,9 @@ export function formatArchiveInspection(report) {
234
199
  `Archive: ${report.archive.encrypted ? 'encrypted' : 'plain'} · v${report.archive.version}`,
235
200
  `Sections: ${report.summary.sectionCount}`,
236
201
  `Records: ${report.summary.recordCount}`,
237
- `Content digest: ${report.summary.contentDigestSha256}`,
202
+ `Content identity: ${report.summary.contentDigest}`,
203
+ ...(report.summary.expectedContentDigestCanonicalization === 'sorted-json-v1'
204
+ ? ['Expected digest matched using the legacy locale-sensitive form.'] : []),
238
205
  ];
239
206
  for (const section of report.sections) {
240
207
  lines.push(`${safeTerminalValue(section.label)}: ${section.recordCount} records · ${safeTerminalValue(section.status)} · ${section.bytes} bytes`);