@slicedearth/whoisleuth-cli 2.3.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 (264) hide show
  1. package/NOTICE +6 -0
  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/arguments.mjs +68 -6
  7. package/cli/artifact-validation/case-response.mjs +47 -41
  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 +83 -61
  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-command-runner.mjs +2 -0
  18. package/cli/bulk-output.mjs +29 -3
  19. package/cli/case-command.mjs +195 -0
  20. package/cli/case-pack.mjs +39 -31
  21. package/cli/command-argument-grammar.mjs +4 -0
  22. package/cli/command-reference.mjs +145 -75
  23. package/cli/completion.mjs +298 -79
  24. package/cli/config-profile.mjs +7 -5
  25. package/cli/ct-event-intake.mjs +12 -12
  26. package/cli/discover.mjs +4 -3
  27. package/cli/discovery-observation-snapshot.mjs +44 -16
  28. package/cli/discovery-scan-command-runner.mjs +2 -0
  29. package/cli/discovery-scan.mjs +7 -4
  30. package/cli/discovery-workflow.mjs +1 -1
  31. package/cli/domain-control-monitor.mjs +50 -20
  32. package/cli/domain-control-observations.mjs +149 -102
  33. package/cli/errors.mjs +33 -1
  34. package/cli/evidence-command-runner.mjs +16 -5
  35. package/cli/evidence-signing.mjs +2 -2
  36. package/cli/external-observation-mapping.mjs +7 -8
  37. package/cli/formatters/terminal-lookup.mjs +18 -46
  38. package/cli/formatters/terminal-shared.mjs +5 -3
  39. package/cli/formatters/terminal-technology.mjs +114 -0
  40. package/cli/inline-command-families.mjs +1 -0
  41. package/cli/interactive-launcher.mjs +2 -160
  42. package/cli/interchange-report.mjs +4 -8
  43. package/cli/investigation-artifacts.mjs +176 -0
  44. package/cli/investigation-checkpoint.mjs +10 -0
  45. package/cli/investigation-folder.mjs +114 -0
  46. package/cli/investigation-manifest.mjs +1 -112
  47. package/cli/investigation-package-review.mjs +103 -0
  48. package/cli/investigation-plan.mjs +32 -19
  49. package/cli/investigation-run.mjs +188 -33
  50. package/cli/local-document-checkpoint.mjs +170 -0
  51. package/cli/lookup-browser.mjs +5 -12
  52. package/cli/lookup-command-runner.mjs +17 -6
  53. package/cli/lookup-diff.mjs +37 -3
  54. package/cli/lookup-plan.mjs +11 -5
  55. package/cli/lookup-reconcile.mjs +10 -8
  56. package/cli/mail-review.mjs +66 -14
  57. package/cli/manual.mjs +1 -0
  58. package/cli/network-command-runner.mjs +2 -0
  59. package/cli/output-file.mjs +31 -5
  60. package/cli/registry-doctor.mjs +3 -1
  61. package/cli/registry-support.mjs +1 -1
  62. package/cli/retained-artifact-diff.mjs +2 -2
  63. package/cli/review-command-runner.mjs +45 -0
  64. package/cli/runner.mjs +55 -6
  65. package/cli/sharing-review.mjs +21 -34
  66. package/cli/signer-trust.mjs +104 -0
  67. package/cli/source-reliability.mjs +74 -83
  68. package/cli/terminal-input.mjs +168 -0
  69. package/cli/workflow-command-runner.mjs +68 -13
  70. package/docs/cli-reference.md +76 -30
  71. package/docs/cli.md +289 -25
  72. package/lib/abort.mjs +4 -1
  73. package/lib/availability.mjs +161 -112
  74. package/lib/bounded-file.mjs +3 -1
  75. package/lib/bounded-html-document.mjs +125 -0
  76. package/lib/bounded-json.mjs +43 -3
  77. package/lib/browser-library-profile.mjs +42 -51
  78. package/lib/browser-library-worker.mjs +112 -0
  79. package/lib/client-behavior-profile.mjs +3 -3
  80. package/lib/ct-search.mjs +11 -4
  81. package/lib/dns-delegation-health.mjs +24 -14
  82. package/lib/dns-inheritance-review.mjs +217 -0
  83. package/lib/dns-intelligence.mjs +36 -23
  84. package/lib/dns-mx.mjs +1 -1
  85. package/lib/dnssec-evidence-validation.mjs +11 -3
  86. package/lib/domain-change-packet.mjs +7 -2
  87. package/lib/domain-change-review.mjs +14 -8
  88. package/lib/domain-control-flight-recorder.mjs +219 -118
  89. package/lib/domain-control-manifest.mjs +117 -57
  90. package/lib/domain-posture-analysis.mjs +20 -7
  91. package/lib/domain-posture-parsers.mjs +88 -6
  92. package/lib/domain-posture.mjs +113 -26
  93. package/lib/evidence-export-privacy.mjs +561 -0
  94. package/lib/evidence-export.mjs +11 -592
  95. package/lib/evidence-report.mjs +3 -1
  96. package/lib/favicon.mjs +117 -41
  97. package/lib/generated/cisa-kev-catalog.mjs +12 -4
  98. package/lib/generated/registrar-standing-catalogue.mjs +7 -7
  99. package/lib/generated/retire-browser-catalog.mjs +2335 -940
  100. package/lib/hostname.mjs +2 -38
  101. package/lib/html-signals.mjs +82 -85
  102. package/lib/http-evidence-bounds.mjs +2 -1
  103. package/lib/http-intelligence.mjs +39 -26
  104. package/lib/lookup-cache.mjs +34 -11
  105. package/lib/lookup-child-profile-contract.mjs +24 -15
  106. package/lib/lookup-diagnostics.mjs +24 -1
  107. package/lib/lookup-progress.mjs +30 -13
  108. package/lib/lookup-response-contract.mjs +1048 -0
  109. package/lib/lookup-response.mjs +7 -10
  110. package/lib/lookup-source-progress.mjs +17 -14
  111. package/lib/lookup.mjs +48 -27
  112. package/lib/observed-network-context.mjs +43 -27
  113. package/lib/outbound-request-bounds.mjs +4 -1
  114. package/lib/page-fingerprints.mjs +69 -133
  115. package/lib/page-role-profile.mjs +12 -8
  116. package/lib/perceptual-hash.mjs +43 -14
  117. package/lib/rdap-bootstrap.mjs +72 -22
  118. package/lib/rdap-client.mjs +15 -7
  119. package/lib/rdap-normalization.mjs +33 -18
  120. package/lib/rdap-search-workbench.mjs +1 -1
  121. package/lib/rdap-transport.mjs +47 -20
  122. package/lib/rdap-types.mjs +2 -1
  123. package/lib/rdap.mjs +18 -3
  124. package/lib/registrable-domain.mjs +21 -1
  125. package/lib/registrar-standing-contract.mjs +1 -0
  126. package/lib/registry-capabilities.mjs +5 -1
  127. package/lib/registry-capability-catalogue.mjs +2 -1
  128. package/lib/registry-dates.mjs +2 -190
  129. package/lib/registry-insights.mjs +56 -18
  130. package/lib/response-policy.mjs +86 -48
  131. package/lib/risk-calibration-summary.mjs +17 -3
  132. package/lib/rpki-evidence.mjs +8 -5
  133. package/lib/safe-fetch.mjs +54 -47
  134. package/lib/security-txt.mjs +10 -7
  135. package/lib/service-binding-dns.mjs +8 -0
  136. package/lib/smtp-transport-review.mjs +27 -15
  137. package/lib/sslbl-certificates.generated.mjs +136 -135
  138. package/lib/static-html-analysis.mjs +117 -146
  139. package/lib/structured-data-identity.mjs +1 -0
  140. package/lib/technology-markup.mjs +100 -0
  141. package/lib/threat-intelligence-manifests.mjs +11 -139
  142. package/lib/threat-intelligence-runtime.mjs +24 -52
  143. package/lib/threat-intelligence-targets.mjs +6 -34
  144. package/lib/threat-intelligence-types.mjs +39 -0
  145. package/lib/typosquat-generator.mjs +19 -15
  146. package/lib/urlhaus-intelligence.mjs +8 -3
  147. package/lib/urlscan-intelligence.mjs +1 -1
  148. package/lib/website-security-posture.mjs +30 -6
  149. package/lib/website-technology.mjs +111 -71
  150. package/lib/whois-authority.mjs +14 -2
  151. package/lib/whois-chain.mjs +9 -2
  152. package/lib/whois-common-formats.mjs +5 -5
  153. package/lib/whois-contracts.mjs +2 -1
  154. package/lib/whois-dialects.mjs +2 -2
  155. package/lib/whois-parser-support.mjs +8 -1
  156. package/lib/whois-transport.mjs +23 -7
  157. package/package.json +2 -1
  158. package/packages/cases/case-attachment-model.mjs +83 -0
  159. package/packages/cases/case-evidence-model.mjs +156 -62
  160. package/packages/cases/case-export-input.mjs +53 -0
  161. package/packages/cases/case-migration-model.mjs +59 -25
  162. package/packages/cases/case-model.mjs +1 -0
  163. package/packages/cases/case-recheck-model.mjs +91 -0
  164. package/packages/cases/case-record-core.mjs +3 -2
  165. package/packages/cases/case-record-operations.mjs +89 -27
  166. package/packages/cases/case-record-projection.mjs +23 -20
  167. package/packages/cases/case-report.mjs +63 -52
  168. package/packages/cases/case-response-actions.mjs +654 -0
  169. package/packages/cases/case-response-model.mjs +41 -1129
  170. package/packages/cases/case-response-outcomes.mjs +317 -0
  171. package/packages/cases/case-response-packet-render.mjs +153 -0
  172. package/packages/cases/case-response-packet-types.mjs +1 -0
  173. package/packages/cases/case-response-packet-vocabulary.mjs +48 -0
  174. package/packages/cases/case-response-packet.mjs +38 -587
  175. package/packages/cases/case-response-records.mjs +94 -0
  176. package/packages/cases/case-response-review-inputs.mjs +371 -0
  177. package/packages/cases/case-response-values.mjs +85 -0
  178. package/packages/cases/case-review-package.mjs +45 -0
  179. package/packages/cases/case-selection.mjs +46 -0
  180. package/packages/cases/case-storage-model.mjs +25 -0
  181. package/packages/cases/response-route-freshness.mjs +16 -0
  182. package/packages/comparison/capture-context.mjs +61 -0
  183. package/packages/comparison/comparison-ledger-bulk.mjs +4 -1
  184. package/packages/comparison/page-similarity.mjs +2 -1
  185. package/packages/comparison/relationship-evidence.mjs +85 -8
  186. package/packages/comparison/relationship-provenance.mjs +93 -0
  187. package/packages/contracts/analyst-interchange.mjs +216 -0
  188. package/packages/contracts/analyst-review-state-contract.mjs +11 -0
  189. package/packages/contracts/analyst-review-state.mjs +5 -4
  190. package/packages/contracts/case-portability.mjs +279 -116
  191. package/packages/contracts/case-views-contract.mjs +12 -0
  192. package/packages/contracts/case-views.mjs +26 -0
  193. package/packages/contracts/cli-command-semantics.mjs +1 -0
  194. package/packages/contracts/cli-lookup.mjs +0 -1
  195. package/packages/contracts/domain-control-flight-recorder.mjs +80 -42
  196. package/packages/contracts/domain-control-manifest.mjs +127 -73
  197. package/packages/contracts/domain-control-monitor.mjs +48 -45
  198. package/packages/contracts/domain-control-review.mjs +179 -121
  199. package/packages/contracts/domain-name.mjs +21 -0
  200. package/packages/contracts/external-observation-interchange.mjs +2 -1
  201. package/packages/contracts/extracted-domain-lifecycle.mjs +11 -32
  202. package/packages/contracts/http-url.mjs +2 -0
  203. package/packages/contracts/investigation-package-limits.mjs +24 -0
  204. package/packages/contracts/investigation-portability.mjs +56 -27
  205. package/packages/contracts/investigation-run.mjs +16 -0
  206. package/packages/contracts/ip-address.mjs +34 -0
  207. package/packages/contracts/lookup-evidence.mjs +35 -0
  208. package/packages/contracts/offline-comparison.mjs +1 -2
  209. package/packages/contracts/page-fingerprints.mjs +15 -0
  210. package/packages/contracts/risk-calibration.mjs +0 -1
  211. package/packages/contracts/schema-compatibility.mjs +1 -1
  212. package/packages/contracts/schema-lifecycle.mjs +83 -153
  213. package/packages/contracts/selected-file-limits.mjs +6 -0
  214. package/packages/contracts/vulnerability-identifiers.mjs +5 -0
  215. package/packages/contracts/web-capture.mjs +10 -0
  216. package/packages/contracts/workspace-portability.mjs +160 -225
  217. package/packages/evidence/artifact-integrity.mjs +8 -2
  218. package/packages/evidence/artifact-structure.mjs +105 -0
  219. package/packages/evidence/decision-fact.mjs +4 -1
  220. package/packages/evidence/domain-control-runtime.mjs +155 -27
  221. package/packages/evidence/domain-posture-context.mjs +79 -0
  222. package/packages/evidence/image-regions.mjs +56 -0
  223. package/packages/evidence/latest-observations.mjs +28 -0
  224. package/packages/evidence/lookup-target.mjs +104 -0
  225. package/packages/evidence/observation.mjs +16 -1
  226. package/packages/evidence/passphrase-encryption.mjs +44 -0
  227. package/packages/evidence/record-identity.mjs +72 -0
  228. package/packages/evidence/registry-dates.mjs +190 -0
  229. package/packages/evidence/retained-file.mjs +69 -0
  230. package/packages/evidence/review-age.mjs +14 -0
  231. package/packages/interchange/bagit.mjs +342 -0
  232. package/packages/interchange/bounded-zip-extraction.mjs +278 -0
  233. package/packages/interchange/crc32.mjs +17 -0
  234. package/packages/interchange/external-findings-import.mjs +89 -49
  235. package/packages/interchange/web-capture-import.mjs +382 -0
  236. package/packages/investigation/bulk-mail-exposure.mjs +2 -1
  237. package/packages/investigation/investigation-bagit.mjs +14 -0
  238. package/packages/investigation/investigation-capsule.mjs +33 -5
  239. package/packages/investigation/investigation-manifest.mjs +200 -0
  240. package/packages/investigation/investigation-package-crypto.mjs +98 -0
  241. package/packages/investigation/investigation-package.mjs +200 -0
  242. package/packages/investigation/lookup-asset-graph-inputs.mjs +38 -0
  243. package/packages/investigation/lookup-asset-graph.mjs +161 -141
  244. package/packages/investigation/lookup-claim-readiness.mjs +18 -0
  245. package/packages/investigation/lookup-investigation-brief.mjs +3 -3
  246. package/packages/investigation/lookup-task-guidance.mjs +58 -0
  247. package/packages/monitoring/analyst-review-state.mjs +38 -90
  248. package/packages/workspace/brand-profile-model.mjs +158 -20
  249. package/packages/workspace/bulk-columns.mjs +17 -0
  250. package/packages/workspace/bulk-review-model.mjs +11 -5
  251. package/packages/workspace/bulk-session-model.mjs +95 -20
  252. package/packages/workspace/bulk-sort.mjs +1 -0
  253. package/packages/workspace/case-views.mjs +102 -0
  254. package/packages/workspace/hostile-input.mjs +29 -8
  255. package/packages/workspace/investigation-template-model.mjs +19 -4
  256. package/packages/workspace/page-baseline.mjs +18 -15
  257. package/packages/workspace/relationship-observation-model.mjs +41 -26
  258. package/packages/workspace/watchlist-history.mjs +2 -2
  259. package/packages/workspace/watchlist-store.mjs +8 -2
  260. package/packages/workspace/website-snapshot-model.mjs +70 -30
  261. package/packages/workspace/workspace-archive-crypto.mjs +41 -61
  262. package/packages/workspace/workspace-archive.mjs +85 -21
  263. package/third-party-notices.txt +29 -2
  264. 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
@@ -51,14 +51,7 @@ only `bulk` accepts multiple targets.
51
51
 
52
52
  ## Command groups
53
53
 
54
- | Group | Common commands |
55
- | --- | --- |
56
- | Investigate | `lookup`, `bulk`, `discover`, `ct-search`, `posture`, `http`, `tls`, `compare`, `mail-headers`, `brief` |
57
- | Respond | `case-pack`, `change-packet`, `sharing-review`, `export` |
58
- | Assure | `dnssec-validate`, `mail-transport`, `domain-control`, `assurance`, `workflow-plan`, `diff`, `inspect-archive`, `verify-artifact` |
59
- | Utilities | `doctor`, `commands`, `completion`, `manual`, `registry-scaffold` |
60
-
61
- Use the installed registry for the complete inventory:
54
+ Use the installed registry to browse Investigate, Respond, Assure and Utilities:
62
55
 
63
56
  ```bash
64
57
  whoisleuth commands --group investigate
@@ -67,10 +60,6 @@ whoisleuth commands --json
67
60
  whoisleuth manual | man -l -
68
61
  ```
69
62
 
70
- `registry-scaffold` has a separate fixture contract: its `--profile` selects a
71
- fixed fixture profile and it rejects shared `--config` profiles. It creates
72
- sanitised local fixture material.
73
-
74
63
  ## Collection and output
75
64
 
76
65
  Fast Lookup is registration-first. Deep Lookup explicitly adds the applicable
@@ -86,9 +75,24 @@ or domain-only output only where declared. `--output` writes a private local
86
75
  file atomically and refuses an existing path unless `--force` is selected.
87
76
  `--strict-exit` and `--fail-on` expose selected evidence states to automation.
88
77
 
89
- Exit codes are 0 for completion, 2 for invalid input, 3 for collection or
90
- comparison failure, 4 for a partial result or unmet selected evidence policy,
91
- 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.
92
96
 
93
97
  ## Repository checkout
94
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`);
package/cli/arguments.mjs CHANGED
@@ -1,9 +1,11 @@
1
1
  import { parseCommandArguments } from "./command-argument-grammar.mjs";
2
2
  import { CliUsageError, hasUnsafeCliText } from "./errors.mjs";
3
+ import { parseArchiveContentDigest } from "./archive-content-digest.mjs";
3
4
  import { parseCliFailPolicies } from "./fail-policy.mjs";
4
5
  import { isDirectLookupTarget } from "../lib/classify.mjs";
5
- import { CLI_COMMANDS, cliMetaActionForInvocation, isCliCommand, } from "./command-reference.mjs";
6
- const MAX_CLI_ARGUMENTS = 32;
6
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACTS } from "../packages/investigation/investigation-manifest.mjs";
7
+ import { CLI_COMMANDS, CLI_CASE_OPERATIONS, cliMetaActionForInvocation, isCliCommand, } from "./command-reference.mjs";
8
+ const MAX_CLI_ARGUMENTS = MAX_INVESTIGATION_MANIFEST_ARTIFACTS + 32;
7
9
  const MAX_CLI_ARGUMENT_LENGTH = 1024;
8
10
  function boundedArgument(value) {
9
11
  if (typeof value !== 'string' || value.length > MAX_CLI_ARGUMENT_LENGTH || hasUnsafeCliText(value)) {
@@ -16,8 +18,11 @@ function terminalOptions(parsed) {
16
18
  }
17
19
  function parseInspectArchiveArguments(parsed) {
18
20
  const expectedContentDigest = parsed.optionValue('--expect-content-digest');
19
- if (expectedContentDigest !== null && !/^sha256:[a-f0-9]{64}$/u.test(expectedContentDigest)) {
20
- throw new CliUsageError('--expect-content-digest requires sha256 followed by 64 lowercase hexadecimal characters.');
21
+ try {
22
+ parseArchiveContentDigest(expectedContentDigest);
23
+ }
24
+ catch {
25
+ throw new CliUsageError('--expect-content-digest requires sha256:<64 lowercase hex> or sorted-json-v2:sha256:<64 lowercase hex>.');
21
26
  }
22
27
  return {
23
28
  action: 'inspect-archive',
@@ -43,6 +48,7 @@ function parseVerifySignatureArguments(parsed) {
43
48
  action: 'verify-signature',
44
49
  source: parsed.positionalValue('source'),
45
50
  publicKeySource: parsed.optionValue('--public-key-file'),
51
+ trustStoreSource: parsed.optionValue('--trust-store-file'),
46
52
  output: parsed.hasOption('--json') ? 'json' : 'terminal',
47
53
  ...terminalOptions(parsed),
48
54
  };
@@ -94,6 +100,7 @@ function parseLookupArguments(parsed) {
94
100
  vantageLabel: normalizedLabel(parsed, '--vantage', 80),
95
101
  ...terminalOptions(parsed),
96
102
  ...(parsed.hasOption('--browse') ? { browse: true } : {}),
103
+ ...(parsed.hasOption('--exact-url') ? { exactUrl: true } : {}),
97
104
  ...(parsed.optionValue('--save-lookup') ? { saveLookup: parsed.optionValue('--save-lookup') } : {}),
98
105
  ...(selectedFailPolicies ? { failOn: selectedFailPolicies } : {}),
99
106
  };
@@ -101,7 +108,7 @@ function parseLookupArguments(parsed) {
101
108
  function parseManifestArguments(parsed) {
102
109
  const sources = parsed.positionalValues('artefacts');
103
110
  if (sources.some((source) => !source))
104
- throw new CliUsageError('manifest requires from 1 to 16 JSON artefact files.');
111
+ throw new CliUsageError(`manifest requires from 1 to ${MAX_INVESTIGATION_MANIFEST_ARTIFACTS} artefact files.`);
105
112
  if (new Set(sources).size !== sources.length)
106
113
  throw new CliUsageError('manifest artefact files must be different.');
107
114
  const configurationDigestSha256 = parsed.optionValue('--configuration-digest');
@@ -113,6 +120,10 @@ function parseManifestArguments(parsed) {
113
120
  sources,
114
121
  workflow: parsed.optionValue('--workflow'),
115
122
  configurationDigestSha256,
123
+ ...(parsed.optionValue('--passphrase-file') ? { passphraseSource: parsed.optionValue('--passphrase-file') } : {}),
124
+ ...(parsed.hasOption('--package') ? { package: true } : {}),
125
+ ...(parsed.hasOption('--bagit') ? { bagit: true } : {}),
126
+ ...(parsed.optionValue('--folder') ? { folder: parsed.optionValue('--folder') } : {}),
116
127
  output: jsonOutput(parsed),
117
128
  ...terminalOptions(parsed),
118
129
  };
@@ -122,6 +133,7 @@ function parseBulkArguments(parsed) {
122
133
  const selectedFailPolicies = failPolicies(parsed, 'bulk');
123
134
  const output = parseOutput(parsed, [
124
135
  ['--json', 'json'], ['--jsonl', 'jsonl'], ['--csv', 'csv'], ['--domains', 'domains'],
136
+ ['--csv-with-metadata', 'csv_metadata'],
125
137
  ['--queries', 'queries'], ['--junit', 'junit'],
126
138
  ]);
127
139
  const filter = parsed.hasOption('--registered-only') ? 'registered'
@@ -172,7 +184,7 @@ function parseDiscoverScanArguments(parsed) {
172
184
  return {
173
185
  action: 'discover-scan',
174
186
  seed: parsed.positionalValue('subject'),
175
- output: parseOutput(parsed, [['--json', 'json'], ['--jsonl', 'jsonl'], ['--csv', 'csv'], ['--domains', 'domains']]),
187
+ output: parseOutput(parsed, [['--json', 'json'], ['--jsonl', 'jsonl'], ['--csv', 'csv'], ['--csv-with-metadata', 'csv_metadata'], ['--domains', 'domains']]),
176
188
  ...discoveryValues(parsed),
177
189
  deep,
178
190
  scanLimit: parsed.integerOption('--scan-limit') ?? Math.min(100, deep ? 50 : 500),
@@ -200,6 +212,7 @@ function parsePostureArguments(parsed) {
200
212
  retiredSelectorText: parsed.optionValue('--retired-selectors'),
201
213
  mailProfile: mailProfile === 'defensive-no-mail' ? 'defensive_no_mail' : mailProfile,
202
214
  ownedDomain: parsed.hasOption('--owned-domain'),
215
+ ...(parsed.hasOption('--include-inherited-dns') ? { includeInheritedDns: true } : {}),
203
216
  ...terminalOptions(parsed),
204
217
  };
205
218
  }
@@ -233,12 +246,17 @@ function parseTwoFileComparisonArguments(parsed, command) {
233
246
  return { leftSource: sources[0], rightSource: sources[1], output: jsonOutput(parsed), ...terminalOptions(parsed) };
234
247
  }
235
248
  function parseVerifyArtifactArguments(parsed) {
249
+ if (parsed.hasOption('--folder') && parsed.positionalValue('source'))
250
+ throw new CliUsageError('--folder selects its own input; do not also supply a file or stdin marker.');
236
251
  return {
237
252
  action: 'verify-artifact',
238
253
  source: parsed.positionalValue('source'),
239
254
  passphraseSource: parsed.optionValue('--passphrase-file'),
240
255
  manifestSource: parsed.optionValue('--manifest'),
241
256
  manifestEntryId: parsed.optionValue('--manifest-entry'),
257
+ ...(parsed.hasOption('--package') ? { package: true } : {}),
258
+ ...(parsed.hasOption('--bagit') ? { bagit: true } : {}),
259
+ ...(parsed.optionValue('--folder') ? { folder: parsed.optionValue('--folder') } : {}),
242
260
  output: jsonOutput(parsed),
243
261
  strictExit: parsed.hasOption('--strict-exit'),
244
262
  ...terminalOptions(parsed),
@@ -297,6 +315,12 @@ function parseWorkflowPlanArguments(parsed) {
297
315
  };
298
316
  }
299
317
  function parseWorkflowRunArguments(parsed) {
318
+ const artifactBindings = parsed.optionValues('--use-artifact').map((value) => {
319
+ const match = /^([a-z0-9]+(?:-[a-z0-9]+)*):([1-9][0-9]?)=([a-z0-9]+(?:-[a-z0-9]+)*)$/u.exec(value);
320
+ if (!match)
321
+ throw new CliUsageError('--use-artifact requires <step-id>:<input-number>=<earlier-step-id>.');
322
+ return Object.freeze({ stepId: match[1], input: Number(match[2]), sourceStepId: match[3] });
323
+ });
300
324
  const selections = parsed.optionValues('--select').map((value) => {
301
325
  const separator = value.indexOf('=');
302
326
  const stepId = separator === -1 ? '' : value.slice(0, separator);
@@ -315,7 +339,10 @@ function parseWorkflowRunArguments(parsed) {
315
339
  subject: parsed.positionalValue('subject'),
316
340
  resumeSource: parsed.optionValue('--resume'),
317
341
  selections: Object.freeze(selections),
342
+ artifactBindings: Object.freeze(artifactBindings),
343
+ confirmedReviews: Object.freeze(parsed.optionValues('--confirm-review')),
318
344
  approveNetwork: parsed.hasOption('--approve-network'),
345
+ interactive: parsed.hasOption('--interactive'),
319
346
  output: jsonOutput(parsed),
320
347
  ...terminalOptions(parsed),
321
348
  };
@@ -337,6 +364,40 @@ function uniqueSources(parsed, command) {
337
364
  throw new CliUsageError(`${command} input files must be different.`);
338
365
  return sources;
339
366
  }
367
+ function parseCaseArguments(parsed) {
368
+ const operation = parsed.positionalValue('operation');
369
+ const source = parsed.positionalValue('source');
370
+ const domain = parsed.optionValue('--domain');
371
+ const caseId = parsed.optionValue('--case-id');
372
+ const title = parsed.optionValue('--title');
373
+ const newIncident = parsed.hasOption('--new-incident');
374
+ const text = parsed.optionValue('--text');
375
+ const noteSource = parsed.optionValue('--note-file');
376
+ const inputSource = parsed.optionValue('--input');
377
+ const expectedFileDigest = parsed.optionValue('--expect-file-digest');
378
+ if (source === '-' || inputSource === '-' || noteSource === '-')
379
+ throw new CliUsageError('Case operations require selected files, not stdin.');
380
+ if (operation !== 'open' && !source)
381
+ throw new CliUsageError(`${operation} requires a Case file.`);
382
+ if (operation !== 'show' && !parsed.hasOption('--output'))
383
+ throw new CliUsageError('Case mutations require --output; nothing was changed.');
384
+ if (operation === 'open' && !domain)
385
+ throw new CliUsageError('case open requires --domain.');
386
+ if (operation !== 'open' && (title || newIncident))
387
+ throw new CliUsageError('--title and --new-incident belong to case open.');
388
+ if (newIncident && (!title || caseId))
389
+ throw new CliUsageError('--new-incident requires a distinguishing --title and cannot select an existing --case-id.');
390
+ if (operation === 'note' ? (text === null && !noteSource) : (text !== null || noteSource !== null)) {
391
+ throw new CliUsageError('Only case note accepts and requires --text or --note-file.');
392
+ }
393
+ if (['pin', 'assess', 'recheck'].includes(operation) ? !inputSource : inputSource !== null) {
394
+ throw new CliUsageError('Only case pin, assess and recheck accept and require --input.');
395
+ }
396
+ if (expectedFileDigest !== null && (!source || !/^sha256:[a-f0-9]{64}$/u.test(expectedFileDigest))) {
397
+ throw new CliUsageError('--expect-file-digest requires a source file and sha256:<64 lowercase hexadecimal characters>.');
398
+ }
399
+ return { action: 'case', operation, source, caseId, domain, title, newIncident, text, noteSource, inputSource, expectedFileDigest, output: jsonOutput(parsed), ...terminalOptions(parsed) };
400
+ }
340
401
  const CLI_PARSERS = Object.freeze({
341
402
  completion: (parsed) => ({ action: 'completion', shell: parsed.positionalValue('shell') }),
342
403
  doctor: (parsed) => ({ action: 'doctor', network: parsed.hasOption('--network'), output: jsonOutput(parsed), ...terminalOptions(parsed) }),
@@ -395,6 +456,7 @@ const CLI_PARSERS = Object.freeze({
395
456
  output: jsonOutput(parsed), strictExit: parsed.hasOption('--strict-exit'), ...terminalOptions(parsed),
396
457
  }),
397
458
  brief: (parsed) => singleInputAction('brief', parsed),
459
+ case: parseCaseArguments,
398
460
  'case-pack': (parsed) => ({
399
461
  action: 'case-pack', source: parsed.positionalValue('source'), output: jsonOutput(parsed),
400
462
  audience: parsed.optionValue('--audience'), reviewed: true, ...terminalOptions(parsed),