@slicedearth/whoisleuth-cli 2.4.0 → 2.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (300) hide show
  1. package/NOTICE +2 -2
  2. package/cli/argument-values.mjs +55 -0
  3. package/cli/arguments.mjs +24 -470
  4. package/cli/artifact-validation/case-response.mjs +21 -6
  5. package/cli/artifact-validation/lookup-evidence.mjs +3 -3
  6. package/cli/artifact-validation/signed-review.mjs +59 -13
  7. package/cli/artifact-verify.mjs +2 -0
  8. package/cli/assurance-arguments.mjs +56 -0
  9. package/cli/assurance-command-definitions.mjs +179 -0
  10. package/cli/assurance-command-runner.mjs +1 -1
  11. package/cli/bounded-json.mjs +1 -1
  12. package/cli/bulk-checkpoint.mjs +1 -1
  13. package/cli/bulk-command-runner.mjs +2 -2
  14. package/cli/bulk.mjs +2 -3
  15. package/cli/case-command.mjs +20 -3
  16. package/cli/case-pack.mjs +12 -2
  17. package/cli/ci-report.mjs +6 -3
  18. package/cli/collection-arguments.mjs +123 -0
  19. package/cli/collection-command-definitions.mjs +330 -0
  20. package/cli/collection-preflight.mjs +8 -1
  21. package/cli/command-argument-grammar.mjs +8 -210
  22. package/cli/command-definition.mjs +329 -0
  23. package/cli/command-reference.mjs +55 -1388
  24. package/cli/command-seeds.mjs +18 -0
  25. package/cli/compare.mjs +1 -1
  26. package/cli/config-profile.mjs +1 -1
  27. package/cli/ct-event-intake.mjs +4 -4
  28. package/cli/discover.mjs +2 -2
  29. package/cli/discovery-observation-snapshot.mjs +2 -2
  30. package/cli/discovery-scan-command-runner.mjs +3 -3
  31. package/cli/discovery-scan.mjs +1 -1
  32. package/cli/discovery-snapshot.mjs +2 -2
  33. package/cli/discovery-workflow.mjs +4 -4
  34. package/cli/domain-control-monitor.mjs +6 -6
  35. package/cli/domain-control-observations.mjs +2 -1
  36. package/cli/errors.mjs +5 -6
  37. package/cli/evidence-arguments.mjs +46 -0
  38. package/cli/evidence-command-definitions.mjs +69 -0
  39. package/cli/evidence-command-runner.mjs +2 -3
  40. package/cli/evidence-signing.mjs +3 -2
  41. package/cli/external-observation-mapping.mjs +2 -2
  42. package/cli/formatters/terminal-command-formats.mjs +6 -0
  43. package/cli/formatters/terminal-lookup.mjs +6 -0
  44. package/cli/history-arguments.mjs +39 -0
  45. package/cli/history-command-definitions.mjs +88 -0
  46. package/cli/indicator-set-command.mjs +33 -0
  47. package/cli/inline-command-families.mjs +6 -47
  48. package/cli/inline-command-runner.mjs +4 -31
  49. package/cli/input.mjs +2 -2
  50. package/cli/intake-command.mjs +107 -0
  51. package/cli/interchange-report.mjs +4 -2
  52. package/cli/investigation-artifacts.mjs +1 -1
  53. package/cli/investigation-plan.mjs +32 -154
  54. package/cli/investigation-recipes.mjs +177 -0
  55. package/cli/investigation-run.mjs +78 -12
  56. package/cli/local-mmdb-review.mjs +197 -17
  57. package/cli/lookalike-calibration.mjs +1 -1
  58. package/cli/lookup-brief.mjs +2 -2
  59. package/cli/lookup-command-runner.mjs +18 -35
  60. package/cli/lookup-diff.mjs +10 -9
  61. package/cli/lookup-plan.mjs +10 -14
  62. package/cli/lookup-reconcile.mjs +6 -5
  63. package/cli/lookup-timeline.mjs +7 -6
  64. package/cli/mail-header-review.mjs +24 -247
  65. package/cli/mail-review.mjs +9 -7
  66. package/cli/network-arguments.mjs +51 -0
  67. package/cli/network-command-definitions.mjs +130 -0
  68. package/cli/network-command-runner.mjs +43 -31
  69. package/cli/offline-evidence-review.mjs +39 -6
  70. package/cli/posture.mjs +1 -1
  71. package/cli/registry-fixture-scaffold.mjs +1 -1
  72. package/cli/retained-artifact-diff.mjs +8 -7
  73. package/cli/review-arguments.mjs +131 -0
  74. package/cli/review-command-definitions.mjs +273 -0
  75. package/cli/review-command-runner.mjs +68 -158
  76. package/cli/risk-calibration.mjs +8 -8
  77. package/cli/runner.mjs +20 -32
  78. package/cli/saved-lookup.mjs +2 -2
  79. package/cli/selected-input-worker.mjs +63 -0
  80. package/cli/sharing-review.mjs +5 -64
  81. package/cli/source-reliability.mjs +2 -2
  82. package/cli/support-arguments.mjs +49 -0
  83. package/cli/support-command-definitions.mjs +209 -0
  84. package/cli/support-command-runner.mjs +2 -2
  85. package/cli/workflow-arguments.mjs +86 -0
  86. package/cli/workflow-command-definitions.mjs +78 -0
  87. package/cli/workflow-command-runner.mjs +8 -1
  88. package/docs/cli-reference.md +27 -0
  89. package/docs/cli.md +223 -2
  90. package/docs/contextual-reviews.md +155 -0
  91. package/lib/analyst-taxonomy.mjs +2 -40
  92. package/lib/application-version.mjs +1 -1
  93. package/lib/availability.mjs +13 -789
  94. package/lib/bounded-contract-normalizers.mjs +4 -0
  95. package/lib/bounded-json.mjs +2 -330
  96. package/lib/browser-library-profile.mjs +62 -84
  97. package/lib/candidate-provenance-bounds.mjs +2 -0
  98. package/lib/classify.mjs +1 -1
  99. package/lib/ct-query.mjs +2 -27
  100. package/lib/ct-response-bounds.mjs +2 -9
  101. package/lib/ct-search.mjs +83 -47
  102. package/lib/dmarc-discovery.mjs +62 -0
  103. package/lib/dns-convergence-review.mjs +21 -8
  104. package/lib/dns-delegation-health.mjs +12 -8
  105. package/lib/dns-inheritance-review.mjs +53 -63
  106. package/lib/dns-intelligence.mjs +9 -7
  107. package/lib/dns-query-coverage.mjs +40 -0
  108. package/lib/domain-assurance.mjs +1 -1
  109. package/lib/domain-change-packet.mjs +12 -13
  110. package/lib/domain-change-review.mjs +32 -14
  111. package/lib/domain-control-flight-recorder.mjs +103 -27
  112. package/lib/domain-control-manifest.mjs +25 -17
  113. package/lib/domain-posture-analysis.mjs +22 -5
  114. package/lib/domain-posture.mjs +16 -15
  115. package/lib/domain-registration.mjs +392 -0
  116. package/lib/evidence-export-privacy.mjs +22 -15
  117. package/lib/evidence-export.mjs +3 -3
  118. package/lib/evidence-report.mjs +4 -1
  119. package/lib/external-intelligence-risk.mjs +2 -124
  120. package/lib/generated/cisa-kev-catalog.mjs +41 -3
  121. package/lib/generated/registrar-standing-catalogue.mjs +18 -10
  122. package/lib/generated/retire-browser-catalog.mjs +332 -19
  123. package/lib/generated/{unicode-confusables-17.mjs → unicode-confusables.mjs} +33 -33
  124. package/lib/html-signals.mjs +7 -2
  125. package/lib/http-evidence-bounds.mjs +2 -8
  126. package/lib/http-intelligence.mjs +13 -1
  127. package/lib/idn-confusable-policy.mjs +6 -4
  128. package/lib/idn-confusables.mjs +1 -1
  129. package/lib/interchange-fidelity-registry.mjs +10 -0
  130. package/lib/lookup-contract-primitives.mjs +1 -1
  131. package/lib/lookup-diagnostics.mjs +1 -23
  132. package/lib/lookup-network-evidence-bounds.mjs +1 -10
  133. package/lib/lookup-progress.mjs +50 -1
  134. package/lib/lookup-response-contract.mjs +69 -40
  135. package/lib/lookup-response.mjs +5 -15
  136. package/lib/lookup-source-progress.mjs +8 -22
  137. package/lib/lookup.mjs +1 -1
  138. package/lib/opportunity-scoring.mjs +2 -107
  139. package/lib/outbound-identity.mjs +1 -1
  140. package/lib/page-fingerprints.mjs +18 -1
  141. package/lib/perceptual-hash-comparison.mjs +2 -34
  142. package/lib/perceptual-hash.mjs +36 -10
  143. package/lib/portable-generator.mjs +2 -29
  144. package/lib/project-metadata.mjs +2 -8
  145. package/lib/rdap-client.mjs +11 -7
  146. package/lib/registrable-domain.mjs +2 -56
  147. package/lib/registrar-standing-contract.mjs +1 -1
  148. package/lib/registrar-standing.mjs +9 -13
  149. package/lib/registry-capabilities.mjs +2 -2
  150. package/lib/registry-capability-catalogue.mjs +27 -19
  151. package/lib/risk-calibration-summary.mjs +2 -352
  152. package/lib/risk-scoring.mjs +2 -345
  153. package/lib/scoring-evidence-quality.mjs +2 -107
  154. package/lib/semantic-version.mjs +2 -54
  155. package/lib/service-binding-dns.mjs +27 -0
  156. package/lib/smtp-transport-review.mjs +48 -19
  157. package/lib/sslbl-certificates.generated.mjs +141 -139
  158. package/lib/static-html-analysis.mjs +1 -1
  159. package/lib/structured-data-identity.mjs +1 -1
  160. package/lib/threat-intelligence-manifests.mjs +1 -1
  161. package/lib/threat-intelligence-runtime.mjs +1 -1
  162. package/lib/threat-intelligence-targets.mjs +1 -1
  163. package/lib/threat-intelligence-types.mjs +2 -75
  164. package/lib/typosquat-generator.mjs +11 -2
  165. package/lib/website-enrichment.mjs +388 -0
  166. package/lib/whois-authority.mjs +43 -5
  167. package/lib/whois-normalization.mjs +3 -1
  168. package/lib/whois-parser.mjs +5 -3
  169. package/package.json +13 -14
  170. package/packages/analysis/analyst-taxonomy.mjs +40 -0
  171. package/packages/analysis/bounded-json.mjs +330 -0
  172. package/packages/analysis/candidate-provenance-bounds.mjs +4 -0
  173. package/packages/analysis/cli-argument-grammar.mjs +216 -0
  174. package/packages/analysis/ct-query.mjs +27 -0
  175. package/packages/analysis/ct-response-bounds.mjs +9 -0
  176. package/packages/analysis/external-intelligence-risk.mjs +124 -0
  177. package/packages/analysis/http-evidence-bounds.mjs +8 -0
  178. package/packages/analysis/lookup-network-evidence-bounds.mjs +10 -0
  179. package/packages/analysis/opportunity-scoring.mjs +107 -0
  180. package/packages/analysis/perceptual-hash-comparison.mjs +34 -0
  181. package/packages/analysis/portable-generator.mjs +29 -0
  182. package/packages/analysis/project-metadata.mjs +8 -0
  183. package/packages/analysis/registrable-domain.mjs +56 -0
  184. package/packages/analysis/risk-calibration-summary.mjs +352 -0
  185. package/packages/analysis/risk-scoring.mjs +345 -0
  186. package/packages/analysis/scoring-evidence-quality.mjs +107 -0
  187. package/packages/analysis/semantic-version.mjs +54 -0
  188. package/packages/analysis/threat-intelligence-types.mjs +75 -0
  189. package/packages/cases/case-evidence-links.mjs +135 -0
  190. package/packages/cases/case-evidence-model.mjs +78 -12
  191. package/packages/cases/case-evidence-quality.mjs +5 -0
  192. package/packages/cases/case-export-input.mjs +7 -5
  193. package/packages/cases/case-incident-context.mjs +52 -0
  194. package/packages/cases/case-markdown.mjs +11 -0
  195. package/packages/cases/case-migration-model.mjs +40 -24
  196. package/packages/cases/case-recheck-model.mjs +90 -32
  197. package/packages/cases/case-record-contracts.mjs +1 -1
  198. package/packages/cases/case-record-core.mjs +1 -1
  199. package/packages/cases/case-record-operations.mjs +40 -112
  200. package/packages/cases/case-record-projection.mjs +8 -1
  201. package/packages/cases/case-record-version-input.mjs +19 -0
  202. package/packages/cases/case-report.mjs +63 -27
  203. package/packages/cases/case-requested-evidence.mjs +146 -0
  204. package/packages/cases/case-response-actions.mjs +103 -1
  205. package/packages/cases/case-response-model.mjs +9 -1
  206. package/packages/cases/case-response-outcomes.mjs +29 -8
  207. package/packages/cases/case-response-packet-render.mjs +13 -9
  208. package/packages/cases/case-response-packet.mjs +25 -11
  209. package/packages/cases/case-response-records.mjs +0 -1
  210. package/packages/cases/case-response-review-inputs.mjs +14 -3
  211. package/packages/cases/case-storage-model.mjs +1 -1
  212. package/packages/cases/case-workflow-metadata.mjs +225 -0
  213. package/packages/cases/case-workflow-migration.mjs +43 -0
  214. package/packages/cases/http-summary.mjs +6 -1
  215. package/packages/comparison/favicon-similarity.mjs +1 -1
  216. package/packages/comparison/page-similarity.mjs +1 -1
  217. package/packages/comparison/relationship-evidence.mjs +21 -9
  218. package/packages/comparison/relationship-provenance.mjs +7 -1
  219. package/packages/contracts/analyst-interchange.mjs +28 -10
  220. package/packages/contracts/case-portability.mjs +118 -25
  221. package/packages/contracts/cli-command-semantics.mjs +2 -0
  222. package/packages/contracts/cli-grammar.mjs +6 -0
  223. package/packages/contracts/cli-lookup-brief.mjs +2 -0
  224. package/packages/contracts/context-review.mjs +24 -0
  225. package/packages/contracts/document-intake.mjs +11 -0
  226. package/packages/contracts/domain-control-flight-recorder.mjs +30 -14
  227. package/packages/contracts/domain-control-manifest.mjs +1 -1
  228. package/packages/contracts/domain-control-monitor.mjs +13 -2
  229. package/packages/contracts/domain-control-review.mjs +36 -15
  230. package/packages/contracts/har-review.mjs +2 -0
  231. package/packages/contracts/identity-events.mjs +4 -0
  232. package/packages/contracts/investigation-run.mjs +2 -0
  233. package/packages/contracts/ip-address.mjs +10 -0
  234. package/packages/contracts/mail-authentication.mjs +5 -0
  235. package/packages/contracts/message-intake.mjs +31 -0
  236. package/packages/contracts/offline-comparison.mjs +4 -1
  237. package/packages/contracts/passive-mail.mjs +10 -0
  238. package/packages/contracts/web-capture.mjs +2 -2
  239. package/packages/contracts/workspace-portability.mjs +63 -17
  240. package/packages/evidence/collection-quality.mjs +67 -0
  241. package/packages/evidence/dns-query-outcome.mjs +72 -0
  242. package/packages/evidence/email-recipient.mjs +18 -0
  243. package/packages/evidence/lookup-target.mjs +5 -1
  244. package/packages/evidence/observation.mjs +2 -0
  245. package/packages/evidence/png-pixels.mjs +117 -0
  246. package/packages/evidence/public-address-policy.mjs +83 -0
  247. package/packages/interchange/defensive-indicator-export.mjs +264 -0
  248. package/packages/interchange/external-findings-import.mjs +4 -12
  249. package/packages/interchange/managed-indicator-export.mjs +35 -0
  250. package/packages/interchange/managed-indicator-set.mjs +210 -0
  251. package/packages/interchange/misp-indicator-export.mjs +93 -0
  252. package/packages/interchange/sharing-policy.mjs +56 -0
  253. package/packages/interchange/stix-indicator-export.mjs +129 -0
  254. package/packages/interchange/web-capture-import.mjs +22 -15
  255. package/packages/investigation/acquisition-decision-packet.mjs +1 -1
  256. package/packages/investigation/bulk-mail-exposure.mjs +7 -13
  257. package/packages/investigation/capture-coverage.mjs +57 -0
  258. package/packages/investigation/connector-provenance-review.mjs +100 -0
  259. package/packages/investigation/context-review.mjs +35 -0
  260. package/packages/investigation/document-intake.mjs +81 -0
  261. package/packages/investigation/docx-intake.mjs +133 -0
  262. package/packages/investigation/domain-history-review.mjs +100 -0
  263. package/packages/investigation/domain-transition-review.mjs +47 -0
  264. package/packages/investigation/evidence-coverage-ledger.mjs +9 -1
  265. package/packages/investigation/har-intake.mjs +68 -0
  266. package/packages/investigation/identity-event-intake.mjs +131 -0
  267. package/packages/investigation/identity-incident-review.mjs +39 -0
  268. package/packages/investigation/incident-sequence-review.mjs +58 -0
  269. package/packages/investigation/intake-report.mjs +19 -0
  270. package/packages/investigation/investigation-manifest.mjs +2 -2
  271. package/packages/investigation/link-intake.mjs +179 -0
  272. package/packages/investigation/lookup-asset-graph.mjs +1 -1
  273. package/packages/investigation/mail-authentication-review.mjs +222 -0
  274. package/packages/investigation/mail-header-identity.mjs +209 -0
  275. package/packages/investigation/message-intake.mjs +195 -0
  276. package/packages/investigation/page-behaviour.mjs +113 -0
  277. package/packages/investigation/pdf-intake.mjs +155 -0
  278. package/packages/investigation/platform-continuity-review.mjs +60 -0
  279. package/packages/investigation/qr-intake.mjs +56 -0
  280. package/packages/investigation/requested-action-hints.mjs +11 -0
  281. package/packages/investigation/selected-input-review.mjs +17 -0
  282. package/packages/investigation/storefront-review.mjs +70 -0
  283. package/packages/workspace/brand-profile-model.mjs +2 -2
  284. package/packages/workspace/bulk-review-model.mjs +28 -26
  285. package/packages/workspace/bulk-session-model.mjs +23 -13
  286. package/packages/workspace/campaign-model.mjs +1 -1
  287. package/packages/workspace/detection-rule-model.mjs +1 -1
  288. package/packages/workspace/hostile-input.mjs +1 -1
  289. package/packages/workspace/investigation-guide.mjs +2 -2
  290. package/packages/workspace/investigation-template-model.mjs +26 -2
  291. package/packages/workspace/page-baseline.mjs +2 -2
  292. package/packages/workspace/relationship-observation-model.mjs +17 -13
  293. package/packages/workspace/shortlist-model.mjs +14 -5
  294. package/packages/workspace/watchlist-history.mjs +28 -12
  295. package/packages/workspace/watchlist-store.mjs +17 -5
  296. package/packages/workspace/website-snapshot-model.mjs +28 -3
  297. package/packages/workspace/workspace-archive-crypto.mjs +1 -1
  298. package/packages/workspace/workspace-archive.mjs +5 -4
  299. package/third-party-notices.txt +549 -16
  300. package/lib/hostname.mjs +0 -2
@@ -0,0 +1,51 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import { commandDefaultText } from "./command-reference.mjs";
3
+ import { NETWORK_COMMAND_DEFINITIONS } from "./network-command-definitions.mjs";
4
+ import { terminalOptions, jsonOutput, parseOutput, singleInputAction, } from "./argument-values.mjs";
5
+ function parsePostureArguments(parsed) {
6
+ const mailProfile = parsed.optionValue('--mail-profile') ?? commandDefaultText('posture', '--mail-profile');
7
+ return {
8
+ action: 'posture',
9
+ domain: parsed.positionalValue('domain'),
10
+ output: parseOutput(parsed, NETWORK_COMMAND_DEFINITIONS.posture),
11
+ selectorText: parsed.optionValue('--selectors'),
12
+ retiredSelectorText: parsed.optionValue('--retired-selectors'),
13
+ mailProfile: mailProfile === 'defensive-no-mail'
14
+ ? 'defensive_no_mail'
15
+ : mailProfile,
16
+ ownedDomain: parsed.hasOption('--owned-domain'),
17
+ ...(parsed.hasOption('--include-inherited-dns') ? { includeInheritedDns: true } : {}),
18
+ ...terminalOptions(parsed),
19
+ };
20
+ }
21
+ function parseDnssecValidateArguments(parsed) {
22
+ return {
23
+ action: 'dnssec-validate',
24
+ target: parsed.positionalValue('domain'),
25
+ resolver: parsed.optionValue('--resolver'),
26
+ trustAnchorSource: parsed.optionValue('--trust-anchor'),
27
+ ownedOrAuthorized: true,
28
+ output: jsonOutput(parsed),
29
+ ...terminalOptions(parsed),
30
+ };
31
+ }
32
+ function parseMailTransportArguments(parsed) {
33
+ return {
34
+ action: 'mail-transport',
35
+ source: parsed.positionalValue('source'),
36
+ resolver: parsed.optionValue('--resolver'),
37
+ trustAnchorSource: parsed.optionValue('--trust-anchor'),
38
+ ownedOrAuthorized: true,
39
+ activeProbeAcknowledged: true,
40
+ output: jsonOutput(parsed),
41
+ ...terminalOptions(parsed),
42
+ };
43
+ }
44
+ export const NETWORK_ARGUMENT_PARSERS = Object.freeze({
45
+ 'ct-search': (parsed) => singleInputAction('ct-search', parsed, 'keyword'),
46
+ posture: parsePostureArguments,
47
+ http: (parsed) => singleInputAction('http', parsed, 'domain'),
48
+ tls: (parsed) => singleInputAction('tls', parsed, 'hostname'),
49
+ 'dnssec-validate': parseDnssecValidateArguments,
50
+ 'mail-transport': parseMailTransportArguments,
51
+ });
@@ -0,0 +1,130 @@
1
+ import { positional, OPTIONAL_FILE_POSITIONAL, constraint, EMPTY_CONSTRAINTS, commandSeed } from "./command-definition.mjs";
2
+ export const NETWORK_COMMAND_DEFINITIONS = Object.freeze({
3
+ "ct-search": commandSeed({
4
+ reference: {
5
+ description: 'Search certificate-transparency observations for one bounded keyword.',
6
+ example: 'whoisleuth ct-search "example brand" --json',
7
+ boundary: 'Certificate observations do not prove website activity, registration ownership, or malicious intent.',
8
+ },
9
+ collection: { scope: 'Accepts one bounded search keyword and queries the fixed certificate-transparency source.' },
10
+ summary: 'Search certificate observations',
11
+ options: ['--json', '--quiet', '--no-color'],
12
+ positionals: Object.freeze([positional('keyword', 'text', 0, 1, [], 'argv_or_stdin')]),
13
+ constraints: EMPTY_CONSTRAINTS,
14
+ handlerOwner: 'network',
15
+ networkEffect: 'always_network',
16
+ common: false,
17
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.ct-search']),
18
+ primaryArtefacts: Object.freeze([]),
19
+ planSupport: false,
20
+ additionalOutputFormats: Object.freeze([]),
21
+ bootstrapProfile: 'allowed',
22
+ }),
23
+ posture: commandSeed({
24
+ reference: {
25
+ description: 'Review bounded DNS mail, delegation, and domain-control posture.',
26
+ example: 'whoisleuth posture example.test --mail-profile standard --json',
27
+ boundary: 'Missing or failed DNS observations remain inconclusive. --include-inherited-dns adds inherited policy and reporting-boundary reviews within 32 additional TXT queries and ten seconds, plus a direct parent-delegation sample. No message is sent and receiver enforcement is not inferred.',
28
+ },
29
+ collection: { scope: 'Accepts one domain and performs bounded RDAP, DNS, and conditional MTA-STS HTTPS requests. --include-inherited-dns separately adds ancestor and reporting-destination DMARC questions and direct DNS/TCP to sampled parent servers.' },
30
+ summary: 'Review DNS and mail posture',
31
+ options: ['--selectors', '--retired-selectors', '--mail-profile', '--include-inherited-dns', '--json', '--sarif', '--owned-domain', '--quiet', '--no-color'],
32
+ positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
33
+ constraints: Object.freeze([
34
+ constraint({ kind: 'mutually_exclusive', options: ['--json', '--sarif'] }),
35
+ constraint({ kind: 'requires_all', option: '--sarif', requiredOptions: ['--owned-domain'] }),
36
+ ]),
37
+ handlerOwner: 'network',
38
+ networkEffect: 'always_network',
39
+ common: false,
40
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.posture']),
41
+ primaryArtefacts: Object.freeze([]),
42
+ planSupport: false,
43
+ additionalOutputFormats: Object.freeze([]),
44
+ bootstrapProfile: 'allowed',
45
+ }),
46
+ http: commandSeed({
47
+ reference: {
48
+ description: 'Inspect one homepage request, redirects, and bounded response metadata.',
49
+ example: 'whoisleuth http example.test --json',
50
+ boundary: 'Requests use the shared public-address and redirect guards. Fixed content-coding and cache-policy metadata describes only the selected response, excludes raw header values, and does not prove caching, transfer savings, performance, privacy, or safety. This is not a rendered browser or vulnerability scan.',
51
+ },
52
+ collection: { scope: 'Accepts one domain and follows only the bounded SSRF-guarded homepage redirect workflow.' },
53
+ summary: 'Inspect one homepage request',
54
+ options: ['--json', '--quiet', '--no-color'],
55
+ positionals: Object.freeze([positional('domain', 'text', 0, 1, [], 'argv_or_stdin')]),
56
+ constraints: EMPTY_CONSTRAINTS,
57
+ handlerOwner: 'network',
58
+ networkEffect: 'always_network',
59
+ common: false,
60
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.http']),
61
+ primaryArtefacts: Object.freeze([]),
62
+ planSupport: false,
63
+ additionalOutputFormats: Object.freeze([]),
64
+ bootstrapProfile: 'allowed',
65
+ }),
66
+ tls: commandSeed({
67
+ reference: {
68
+ description: 'Inspect one hostname certificate through a bounded TLS connection.',
69
+ example: 'whoisleuth tls example.test --json',
70
+ boundary: 'One observed connection is point-in-time evidence and does not establish every address, edge, or historical certificate.',
71
+ },
72
+ collection: { scope: 'Accepts one public hostname and opens one bounded certificate connection.' },
73
+ summary: 'Inspect one TLS connection',
74
+ options: ['--json', '--quiet', '--no-color'],
75
+ positionals: Object.freeze([positional('hostname', 'text', 0, 1, [], 'argv_or_stdin')]),
76
+ constraints: EMPTY_CONSTRAINTS,
77
+ handlerOwner: 'network',
78
+ networkEffect: 'always_network',
79
+ common: false,
80
+ schemaIdentifiers: Object.freeze(['whoisleuth.cli.tls']),
81
+ primaryArtefacts: Object.freeze([]),
82
+ planSupport: false,
83
+ additionalOutputFormats: Object.freeze([]),
84
+ bootstrapProfile: 'allowed',
85
+ }),
86
+ "dnssec-validate": commandSeed({
87
+ reference: {
88
+ description: 'Cryptographically validate one authorised DNSSEC chain from a supplied trust anchor through one selected public resolver.',
89
+ example: 'whoisleuth dnssec-validate example.test --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --json',
90
+ boundary: 'This isolated action is never invoked by Lookup, Bulk, monitoring, or recipes. It caps DNS queries, aliases, delegations, bytes, and duration; transport and validation failures remain separate, and secure is not a general safety verdict.',
91
+ },
92
+ collection: { scope: 'Accepts one authorised domain, one public resolver IP, and one local trust-anchor file; DNS-over-TCP validation is capped at 32 queries and 15 seconds.' },
93
+ summary: 'Validate an authorised DNSSEC chain',
94
+ options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--json', '--quiet', '--no-color'],
95
+ positionals: Object.freeze([positional('domain', 'text', 1, 1)]),
96
+ constraints: Object.freeze([
97
+ constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized'] }),
98
+ ]),
99
+ handlerOwner: 'network',
100
+ networkEffect: 'always_network',
101
+ common: false,
102
+ schemaIdentifiers: Object.freeze(['whoisleuth.dnssec-chain-validation', 'whoisleuth.dnssec-trust-anchor']),
103
+ primaryArtefacts: Object.freeze([]),
104
+ planSupport: false,
105
+ additionalOutputFormats: Object.freeze([]),
106
+ bootstrapProfile: 'allowed',
107
+ }),
108
+ "mail-transport": commandSeed({
109
+ reference: {
110
+ description: 'Review selected authorised MX endpoints, DNSSEC-qualified TLSA evidence, SMTP capabilities, and optional STARTTLS certificates.',
111
+ example: 'whoisleuth mail-transport selected-mx.json --resolver "$PUBLIC_RESOLVER_IP" --trust-anchor anchor.json --owned-or-authorized --active-probe --json',
112
+ boundary: 'This isolated action probes at most three selected MX hosts sequentially, reports selection, public revalidation, connection, and address authentication separately, sends only EHLO and optional STARTTLS, never retries, and performs no authentication, relay, recipient, mailbox, catch-all, or message test. If a DANE-TA TLSA usage 2 association is published, active collection retains only the leaf certificate and leaves that comparison partial without certificate-path construction and trust-anchor path validation. SMTP relay PKIX-TA usage 0 and PKIX-EE usage 1 records remain unsupported and cannot complete SMTP DANE assurance; a separate usage 3 match remains eligible.',
113
+ },
114
+ collection: { scope: 'Accepts at most three selected authorised MX hosts, uses one public resolver, and performs sequential bounded SMTP connections with no retries.' },
115
+ summary: 'Review selected authorised SMTP transports',
116
+ options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe', '--json', '--quiet', '--no-color'],
117
+ positionals: OPTIONAL_FILE_POSITIONAL,
118
+ constraints: Object.freeze([
119
+ constraint({ kind: 'required', options: ['--resolver', '--trust-anchor', '--owned-or-authorized', '--active-probe'] }),
120
+ ]),
121
+ handlerOwner: 'network',
122
+ networkEffect: 'always_network',
123
+ common: false,
124
+ schemaIdentifiers: Object.freeze(['whoisleuth.mail-transport.input', 'whoisleuth.cli.mail-transport-review']),
125
+ primaryArtefacts: Object.freeze([]),
126
+ planSupport: false,
127
+ additionalOutputFormats: Object.freeze([]),
128
+ bootstrapProfile: 'allowed',
129
+ }),
130
+ });
@@ -1,6 +1,7 @@
1
+ import { runDiscriminatedCommandHandler, } from "./discriminated-command-handlers.mjs";
1
2
  import { fetchHomepage } from "../lib/availability.mjs";
2
3
  import { searchCertificateTransparency } from "../lib/ct-search.mjs";
3
- import { checkDomainPosture, normalizeAuditDomain, normalizeDkimSelectors } from "../lib/domain-posture.mjs";
4
+ import { checkDomainPosture, normalizeAuditDomain, normalizeDkimSelectors, } from "../lib/domain-posture.mjs";
4
5
  import { collectTlsIntelligence, normalizeTlsHostname } from "../lib/tls-intelligence.mjs";
5
6
  import { MAX_DNSSEC_TRUST_ANCHOR_BYTES, formatDnssecChainReport, validateDnssecChain, } from "../lib/dnssec-chain-validation.mjs";
6
7
  import { MAX_MAIL_TRANSPORT_INPUT_BYTES, collectMailTransportReview, formatMailTransportReview, } from "../lib/smtp-transport-review.mjs";
@@ -11,9 +12,10 @@ import { buildHttpProbeResult } from "./http.mjs";
11
12
  import { normalizePostureSelectors } from "./posture.mjs";
12
13
  import EXIT_CODES from "./exit-codes.mjs";
13
14
  import { buildPostureSarif } from "./ci-report.mjs";
14
- async function runNetworkCommand(args, dependencies, context) {
15
- if (args.action === 'ct-search') {
16
- const keyword = args.keyword || await context.readSingleInput();
15
+ const networkCommandHandlers = {
16
+ 'ct-search': async (args, dependencies, context) => {
17
+ context.setFailureLabel('Certificate Transparency search');
18
+ const keyword = args.keyword || (await context.readSingleInput());
17
19
  if (!keyword)
18
20
  throw new CliUsageError('ct-search requires one keyword as an argument or on stdin.');
19
21
  const search = dependencies.searchCertificateTransparency || searchCertificateTransparency;
@@ -25,9 +27,10 @@ async function runNetworkCommand(args, dependencies, context) {
25
27
  : context.terminal(formatTerminalCtSearch(document), args.color));
26
28
  }
27
29
  return EXIT_CODES.SUCCESS;
28
- }
29
- if (args.action === 'posture') {
30
- const requestedDomain = args.domain || await context.readSingleInput();
30
+ },
31
+ posture: async (args, dependencies, context) => {
32
+ context.setFailureLabel('Domain posture audit');
33
+ const requestedDomain = args.domain || (await context.readSingleInput());
31
34
  if (!requestedDomain)
32
35
  throw new CliUsageError('posture requires one domain as an argument or on stdin.');
33
36
  const normalizeDomain = dependencies.normalizeAuditDomain || normalizeAuditDomain;
@@ -56,9 +59,10 @@ async function runNetworkCommand(args, dependencies, context) {
56
59
  : context.terminal(formatTerminalPosture(document), args.color));
57
60
  }
58
61
  return EXIT_CODES.SUCCESS;
59
- }
60
- if (args.action === 'http') {
61
- const requestedDomain = args.domain || await context.readSingleInput();
62
+ },
63
+ http: async (args, dependencies, context) => {
64
+ context.setFailureLabel('HTTP probe');
65
+ const requestedDomain = args.domain || (await context.readSingleInput());
62
66
  if (!requestedDomain)
63
67
  throw new CliUsageError('http requires one domain as an argument or on stdin.');
64
68
  const normalizeDomain = dependencies.normalizeAuditDomain || normalizeAuditDomain;
@@ -74,8 +78,9 @@ async function runNetworkCommand(args, dependencies, context) {
74
78
  : context.terminal(formatTerminalHttp(document), args.color));
75
79
  }
76
80
  return EXIT_CODES.SUCCESS;
77
- }
78
- if (args.action === 'dnssec-validate') {
81
+ },
82
+ 'dnssec-validate': async (args, dependencies, context) => {
83
+ context.setFailureLabel('DNSSEC chain validation');
79
84
  let anchorInput;
80
85
  try {
81
86
  anchorInput = dependencies.readTrustAnchorInput
@@ -103,8 +108,9 @@ async function runNetworkCommand(args, dependencies, context) {
103
108
  return report.state === 'secure' || report.state === 'insecure'
104
109
  ? EXIT_CODES.SUCCESS
105
110
  : EXIT_CODES.PARTIAL_FAILURE;
106
- }
107
- if (args.action === 'mail-transport') {
111
+ },
112
+ 'mail-transport': async (args, dependencies, context) => {
113
+ context.setFailureLabel('Mail transport review');
108
114
  let input;
109
115
  let anchorInput;
110
116
  try {
@@ -150,22 +156,28 @@ async function runNetworkCommand(args, dependencies, context) {
150
156
  : context.terminal(formatMailTransportReview(review), args.color));
151
157
  }
152
158
  return review.runState === 'complete' ? EXIT_CODES.SUCCESS : EXIT_CODES.PARTIAL_FAILURE;
153
- }
154
- const requestedHostname = args.hostname || await context.readSingleInput();
155
- if (!requestedHostname)
156
- throw new CliUsageError('tls requires one hostname as an argument or on stdin.');
157
- const normalizeHostname = dependencies.normalizeTlsHostname || normalizeTlsHostname;
158
- const hostname = normalizeHostname(requestedHostname);
159
- if (!hostname)
160
- throw new CliUsageError('tls requires a valid DNS hostname, not an IP address.');
161
- const collect = dependencies.collectTlsIntelligence || collectTlsIntelligence;
162
- const result = await context.withProgress('Inspecting the current TLS connection', () => collect(hostname));
163
- const document = buildCliTlsDocument(requestedHostname, result, context.now());
164
- if (!args.quiet) {
165
- context.writeStdout(args.output === 'json'
166
- ? formatJsonDocument(document)
167
- : context.terminal(formatTerminalTls(document), args.color));
168
- }
169
- return EXIT_CODES.SUCCESS;
159
+ },
160
+ tls: async (args, dependencies, context) => {
161
+ context.setFailureLabel('TLS evidence collection');
162
+ const requestedHostname = args.hostname || (await context.readSingleInput());
163
+ if (!requestedHostname)
164
+ throw new CliUsageError('tls requires one hostname as an argument or on stdin.');
165
+ const normalizeHostname = dependencies.normalizeTlsHostname || normalizeTlsHostname;
166
+ const hostname = normalizeHostname(requestedHostname);
167
+ if (!hostname)
168
+ throw new CliUsageError('tls requires a valid DNS hostname, not an IP address.');
169
+ const collect = dependencies.collectTlsIntelligence || collectTlsIntelligence;
170
+ const result = await context.withProgress('Inspecting the current TLS connection', () => collect(hostname));
171
+ const document = buildCliTlsDocument(requestedHostname, result, context.now());
172
+ if (!args.quiet) {
173
+ context.writeStdout(args.output === 'json'
174
+ ? formatJsonDocument(document)
175
+ : context.terminal(formatTerminalTls(document), args.color));
176
+ }
177
+ return EXIT_CODES.SUCCESS;
178
+ },
179
+ };
180
+ async function runNetworkCommand(args, dependencies, context) {
181
+ return runDiscriminatedCommandHandler(networkCommandHandlers, args, dependencies, context);
170
182
  }
171
183
  export { runNetworkCommand };
@@ -1,5 +1,7 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import { scanBoundedJson } from "../lib/bounded-json.mjs";
2
+ import { scanBoundedJson } from "../packages/analysis/bounded-json.mjs";
3
+ import { CONTEXT_INPUT_SCHEMAS, reviewContextInput } from "../packages/investigation/context-review.mjs";
4
+ import { CONTEXT_REVIEW_KINDS } from "../packages/contracts/context-review.mjs";
3
5
  import { normalizeEncryptedDnsAdapter, planEncryptedDnsQuery, } from "../lib/encrypted-dns-contract.mjs";
4
6
  import { validateDnssecEvidence } from "../lib/dnssec-evidence-validation.mjs";
5
7
  import { reviewDomainChange } from "../lib/domain-change-review.mjs";
@@ -15,7 +17,7 @@ import { reviewDnsConvergence } from "../lib/dns-convergence-review.mjs";
15
17
  import { compareTrustStoreEvidence } from "../lib/trust-store-comparison.mjs";
16
18
  import { CliUsageError } from "./errors.mjs";
17
19
  import { safeTerminalValue } from "./formatters/terminal.mjs";
18
- import { LOCAL_MMDB_QUERY_SCHEMA, reviewLocalMmdb } from "./local-mmdb-review.mjs";
20
+ import { LOCAL_MMDB_QUERY_SCHEMA, LOCAL_MMDB_QUERY_VERSION, LOCAL_MMDB_REVIEW_SCHEMA, LOCAL_MMDB_REVIEW_VERSION, reviewLocalMmdb } from "./local-mmdb-review.mjs";
19
21
  const OFFLINE_EVIDENCE_REVIEW_SCHEMA = 'whoisleuth.cli.offline-evidence-review';
20
22
  const OFFLINE_EVIDENCE_REVIEW_VERSION = 1;
21
23
  const MAX_OFFLINE_EVIDENCE_INPUT_BYTES = 16 * 1024 * 1024;
@@ -43,16 +45,24 @@ function parseInput(value) {
43
45
  throw new CliUsageError('Offline evidence review requires one valid bounded JSON document without duplicate keys.');
44
46
  }
45
47
  const document = record(parsed);
46
- if (document.version !== 1 || typeof document.schema !== 'string') {
48
+ if (typeof document.schema !== 'string' || document.version !== 1
49
+ && !(document.schema === LOCAL_MMDB_QUERY_SCHEMA && document.version === LOCAL_MMDB_QUERY_VERSION)) {
47
50
  throw new CliUsageError('Offline evidence review requires a supported versioned input schema.');
48
51
  }
49
52
  return document;
50
53
  }
51
54
  function buildOfflineEvidenceReview(value, generatedAt = new Date().toISOString()) {
52
55
  const input = parseInput(value);
56
+ if (input.schema === LOCAL_MMDB_QUERY_SCHEMA)
57
+ throw new CliUsageError('Local MMDB review requires --mmdb <database-file>.');
53
58
  let kind;
54
59
  let result;
55
- if (input.schema === RDAP_SEARCH_INPUT_SCHEMA) {
60
+ if (CONTEXT_INPUT_SCHEMAS.includes(input.schema)) {
61
+ const review = reviewContextInput(input, generatedAt);
62
+ kind = review.kind;
63
+ result = review;
64
+ }
65
+ else if (input.schema === RDAP_SEARCH_INPUT_SCHEMA) {
56
66
  kind = 'rdap_search';
57
67
  const request = record(input.request);
58
68
  const help = normalizeRdapSearchHelp(input.help);
@@ -163,7 +173,7 @@ async function buildOfflineEvidenceReviewWithLocalResources(value, generatedAt =
163
173
  version: OFFLINE_EVIDENCE_REVIEW_VERSION,
164
174
  generatedAt,
165
175
  kind: 'geoip',
166
- result: await reviewLocalMmdb(input, options.mmdbPath),
176
+ result: await reviewLocalMmdb(input, options.mmdbPath, generatedAt),
167
177
  limitations: Object.freeze([
168
178
  'The review is local and uses only the supplied query metadata and analyst-supplied database. It does not transmit the address or refresh the database.',
169
179
  ]),
@@ -171,6 +181,9 @@ async function buildOfflineEvidenceReviewWithLocalResources(value, generatedAt =
171
181
  }
172
182
  function formatOfflineEvidenceReview(document) {
173
183
  const result = record(document.result);
184
+ if (document.kind === 'geoip' && result.schema === LOCAL_MMDB_REVIEW_SCHEMA && result.version !== LOCAL_MMDB_REVIEW_VERSION) {
185
+ throw new CliUsageError('Offline evidence review does not support this MMDB result version.');
186
+ }
174
187
  const gate = record(result.gate);
175
188
  const counts = record(result.counts);
176
189
  const count = (value) => typeof value === 'number' && Number.isSafeInteger(value) && value >= 0 ? value : 0;
@@ -191,7 +204,16 @@ function formatOfflineEvidenceReview(document) {
191
204
  `Kind ${document.kind.replaceAll('_', ' ')}`,
192
205
  `State ${state.replaceAll('_', ' ')}`,
193
206
  ];
194
- if (document.kind === 'rdap_search') {
207
+ if (CONTEXT_REVIEW_KINDS.includes(document.kind)) {
208
+ lines.push(safeTerminalValue(result.summary, ''));
209
+ for (const item of Array.isArray(result.observations) ? result.observations : []) {
210
+ const row = record(item);
211
+ lines.push('', `${safeTerminalValue(row.label, 'Observation')} [${safeTerminalValue(row.state, 'unknown')}]`, ` ${safeTerminalValue(row.detail, '')}`, ` Source: ${safeTerminalValue(row.source, '')} · ${safeTerminalValue(row.observedAt, 'time unknown')}`);
212
+ }
213
+ for (const step of Array.isArray(result.nextSteps) ? result.nextSteps : [])
214
+ lines.push(`Next: ${safeTerminalValue(step, '')}`);
215
+ }
216
+ else if (document.kind === 'rdap_search') {
195
217
  const help = record(result.help);
196
218
  const plan = record(result.plan);
197
219
  const responseInspection = record(result.responseInspection);
@@ -200,6 +222,17 @@ function formatOfflineEvidenceReview(document) {
200
222
  if (result.responseInspection !== null)
201
223
  lines.push(`Result ${String(responseInspection.state ?? 'invalid').replaceAll('_', ' ')}`);
202
224
  }
225
+ else if (document.kind === 'geoip' && result.schema === LOCAL_MMDB_REVIEW_SCHEMA && result.version === LOCAL_MMDB_REVIEW_VERSION) {
226
+ const database = record(result.database), metadata = record(database.metadata), binaryFormat = record(metadata.binaryFormat);
227
+ const freshness = record(result.freshness), policy = record(freshness.policy), source = record(result.source), match = record(result.match);
228
+ lines.push(`Outcome ${safeTerminalValue(result.reason, 'unknown').replaceAll('_', ' ')}`, `Coverage ${safeTerminalValue(result.completeness, 'unavailable')}`, `Database SHA-256 ${safeTerminalValue(database.sha256, 'unavailable')}`, `Database bytes ${safeTerminalValue(database.byteLength, 'unavailable')}`, `Intrinsic type ${safeTerminalValue(metadata.databaseType, 'unavailable')}`, `Built ${safeTerminalValue(metadata.builtAt, 'unavailable')}`, `Binary format ${safeTerminalValue(binaryFormat.major, '?')}.${safeTerminalValue(binaryFormat.minor, '?')} · IP version ${safeTerminalValue(metadata.ipVersion, '?')}`, `Freshness ${safeTerminalValue(freshness.state, 'unknown')} · checked ${safeTerminalValue(freshness.checkedAt, 'unknown')}`, `Age policy ${safeTerminalValue(policy.maxAgeDays, '?')} days · ${safeTerminalValue(policy.rationale, '')}`, `Declared source ${safeTerminalValue(source.label, 'unknown')} · version ${safeTerminalValue(source.version, 'unknown')} · licence ${safeTerminalValue(source.license, 'unknown')}`);
229
+ if (result.match !== null) {
230
+ lines.push(`Matched network ${safeTerminalValue(match.network, 'unknown')}`, `Country ${safeTerminalValue(match.countryCode, 'unknown')} · region ${safeTerminalValue(match.region, 'unknown')} · city ${safeTerminalValue(match.city, 'unknown')}`, `ASN ${safeTerminalValue(match.asn, 'unknown')} · network label ${safeTerminalValue(match.asName, 'unknown')}`);
231
+ }
232
+ }
233
+ else if (document.kind === 'geoip') {
234
+ lines.push('Intrinsic MMDB freshness is not assessed by this result. A version-2 MMDB query can assess a selected local database against an explicit age policy.');
235
+ }
203
236
  else if (document.kind === 'zone_intent') {
204
237
  const desired = record(result.desired);
205
238
  lines.push(`Desired ${listLength(desired.records)}`);
package/cli/posture.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { CliUsageError } from "./arguments.mjs";
1
+ import { CliUsageError } from "./errors.mjs";
2
2
  const MAX_POSTURE_SELECTORS = 10;
3
3
  function normalizePostureSelectors(raw, normalizeSelectors) {
4
4
  if (raw === null || raw === undefined || raw === '')
@@ -14,7 +14,7 @@ export function buildRegistryFixtureScaffold(profileIdInput, suffixInput, scenar
14
14
  throw new TypeError('Scenario must be registered, not_found, or inconclusive.');
15
15
  const matchingProfiles = registryCompatibilityMatrix().filter((item) => item.id === profileId);
16
16
  if (!matchingProfiles.length)
17
- throw new TypeError('The capability profile is not present in the registry catalogue.');
17
+ throw new TypeError('The capability profile is not present in the registry catalogue. Run registry-support <suffix> --json to find its profile ID.');
18
18
  const profile = matchingProfiles.find((item) => item.suffixes.includes(suffix));
19
19
  if (!profile)
20
20
  throw new TypeError(`The capability profile does not cover .${suffix}.`);
@@ -9,6 +9,7 @@ import { parseBoundedJsonObject } from "./bounded-json.mjs";
9
9
  import { CliUsageError } from "./errors.mjs";
10
10
  import { buildCliLookupDiff, formatCliLookupDiff } from "./lookup-diff.mjs";
11
11
  import { SAVED_LOOKUP_SCHEMA } from "./saved-lookup.mjs";
12
+ import { safeTerminalValue } from "./formatters/terminal-shared.mjs";
12
13
  import { CLI_COMPARISON_LEDGER_SCHEMA, CLI_COMPARISON_LEDGER_VERSION, MAX_RETAINED_ARTIFACT_DIFF_BYTES, } from "../packages/contracts/offline-comparison.mjs";
13
14
  export { CLI_COMPARISON_LEDGER_SCHEMA, CLI_COMPARISON_LEDGER_VERSION, MAX_RETAINED_ARTIFACT_DIFF_BYTES };
14
15
  const BULK_ROOT_KEYS = new Set(['schema', 'version', 'generatedAt', 'sessions', 'limitations']);
@@ -354,22 +355,22 @@ export function formatCliRetainedArtifactDiff(document) {
354
355
  return formatCliLookupDiff(document);
355
356
  const lines = [
356
357
  'Retained artifact comparison ledger',
357
- `Family ${document.artifactFamily.replaceAll('_', ' ')}`,
358
- `Left ${document.left.label} · ${document.left.id}`,
359
- `Right ${document.right.label} · ${document.right.id}`,
358
+ `Family ${safeTerminalValue(document.artifactFamily).replaceAll('_', ' ')}`,
359
+ `Left ${safeTerminalValue(document.left.label)} · ${safeTerminalValue(document.left.id)}`,
360
+ `Right ${safeTerminalValue(document.right.label)} · ${safeTerminalValue(document.right.id)}`,
360
361
  `Candidates ${document.index.counts.retained}`,
361
362
  `Exact rows ${document.details.rows.length} of ${document.details.totalRows}`,
362
363
  '',
363
364
  ];
364
365
  for (const row of document.details.rows) {
365
- lines.push(`${row.entityId} · ${row.field} [${row.state.replaceAll('_', ' ')}]`);
366
- lines.push(` Earlier: ${row.earlier.value ?? 'not retained'} · ${row.earlier.sourceState}`);
367
- lines.push(` Later: ${row.later.value ?? 'not retained'} · ${row.later.sourceState}`);
366
+ lines.push(`${safeTerminalValue(row.entityId)} · ${safeTerminalValue(row.field)} [${safeTerminalValue(row.state).replaceAll('_', ' ')}]`);
367
+ lines.push(` Earlier: ${safeTerminalValue(row.earlier.value, 'not retained')} · ${safeTerminalValue(row.earlier.sourceState)}`);
368
+ lines.push(` Later: ${safeTerminalValue(row.later.value, 'not retained')} · ${safeTerminalValue(row.later.sourceState)}`);
368
369
  }
369
370
  if (document.details.omissions.detailRows)
370
371
  lines.push(`Omitted exact rows: ${document.details.omissions.detailRows}`);
371
372
  lines.push('', 'Limitations:');
372
373
  for (const limitation of document.limitations)
373
- lines.push(` - ${limitation}`);
374
+ lines.push(` - ${safeTerminalValue(limitation)}`);
374
375
  return `${lines.join('\n')}\n`;
375
376
  }
@@ -0,0 +1,131 @@
1
+ import {} from "./command-argument-grammar.mjs";
2
+ import { CliUsageError } from "./errors.mjs";
3
+ import { CLI_CASE_OPERATIONS, CLI_INDICATOR_OPERATIONS } from "./command-reference.mjs";
4
+ import { terminalOptions, jsonOutput, parseTwoFileComparisonArguments, singleInputAction, } from "./argument-values.mjs";
5
+ function parseVerifyArtifactArguments(parsed) {
6
+ if (parsed.hasOption('--folder') && parsed.positionalValue('source'))
7
+ throw new CliUsageError('--folder selects its own input; do not also supply a file or stdin marker.');
8
+ return {
9
+ action: 'verify-artifact',
10
+ source: parsed.positionalValue('source'),
11
+ passphraseSource: parsed.optionValue('--passphrase-file'),
12
+ manifestSource: parsed.optionValue('--manifest'),
13
+ manifestEntryId: parsed.optionValue('--manifest-entry'),
14
+ ...(parsed.hasOption('--package') ? { package: true } : {}),
15
+ ...(parsed.hasOption('--bagit') ? { bagit: true } : {}),
16
+ ...(parsed.optionValue('--folder') ? { folder: parsed.optionValue('--folder') } : {}),
17
+ output: jsonOutput(parsed),
18
+ strictExit: parsed.hasOption('--strict-exit'),
19
+ ...terminalOptions(parsed),
20
+ };
21
+ }
22
+ function parseCaseArguments(parsed) {
23
+ const operation = parsed.positionalValue('operation');
24
+ const source = parsed.positionalValue('source');
25
+ const domain = parsed.optionValue('--domain');
26
+ const caseId = parsed.optionValue('--case-id');
27
+ const title = parsed.optionValue('--title');
28
+ const newIncident = parsed.hasOption('--new-incident');
29
+ const text = parsed.optionValue('--text');
30
+ const noteSource = parsed.optionValue('--note-file');
31
+ const inputSource = parsed.optionValue('--input');
32
+ const expectedFileDigest = parsed.optionValue('--expect-file-digest');
33
+ if (source === '-' || inputSource === '-' || noteSource === '-')
34
+ throw new CliUsageError('Case operations require selected files, not stdin.');
35
+ if (operation !== 'open' && !source)
36
+ throw new CliUsageError(`${operation} requires a Case file.`);
37
+ if (operation !== 'show' && !parsed.hasOption('--output'))
38
+ throw new CliUsageError('Case mutations require --output; nothing was changed.');
39
+ if (operation === 'open' && !domain)
40
+ throw new CliUsageError('case open requires --domain.');
41
+ if (operation !== 'open' && (title || newIncident))
42
+ throw new CliUsageError('--title and --new-incident belong to case open.');
43
+ if (newIncident && (!title || caseId))
44
+ throw new CliUsageError('--new-incident requires a distinguishing --title and cannot select an existing --case-id.');
45
+ if (operation === 'note' ? text === null && !noteSource : text !== null || noteSource !== null) {
46
+ throw new CliUsageError('Only case note accepts and requires --text or --note-file.');
47
+ }
48
+ if (['pin', 'link', 'withdraw-link', 'assess', 'recheck'].includes(operation)
49
+ ? !inputSource
50
+ : inputSource !== null) {
51
+ throw new CliUsageError('Only case pin, link, withdraw-link, assess and recheck accept and require --input.');
52
+ }
53
+ if (expectedFileDigest !== null && (!source || !/^sha256:[a-f0-9]{64}$/u.test(expectedFileDigest))) {
54
+ throw new CliUsageError('--expect-file-digest requires a source file and sha256:<64 lowercase hexadecimal characters>.');
55
+ }
56
+ return {
57
+ action: 'case',
58
+ operation,
59
+ source,
60
+ caseId,
61
+ domain,
62
+ title,
63
+ newIncident,
64
+ text,
65
+ noteSource,
66
+ inputSource,
67
+ expectedFileDigest,
68
+ output: jsonOutput(parsed),
69
+ ...terminalOptions(parsed),
70
+ };
71
+ }
72
+ export const REVIEW_ARGUMENT_PARSERS = Object.freeze({
73
+ 'verify-artifact': parseVerifyArtifactArguments,
74
+ 'interchange-report': (parsed) => ({
75
+ action: 'interchange-report',
76
+ source: parsed.positionalValue('source'),
77
+ passphraseSource: parsed.optionValue('--passphrase-file'),
78
+ output: jsonOutput(parsed),
79
+ ...terminalOptions(parsed),
80
+ }),
81
+ 'source-report': (parsed) => singleInputAction('source-report', parsed),
82
+ compare: (parsed) => singleInputAction('compare', parsed),
83
+ 'page-compare': (parsed) => ({
84
+ action: 'page-compare',
85
+ ...parseTwoFileComparisonArguments(parsed, 'page-compare'),
86
+ }),
87
+ 'mail-review': (parsed) => singleInputAction('mail-review', parsed),
88
+ 'mail-headers': (parsed) => ({
89
+ ...singleInputAction('mail-headers', parsed),
90
+ ...(parsed.hasOption('--trusted-auth-header')
91
+ ? { trustedAuthHeaders: parsed.optionValues('--trusted-auth-header') }
92
+ : {}),
93
+ }),
94
+ intake: (parsed) => ({
95
+ action: 'intake',
96
+ kind: parsed.positionalValue('kind'),
97
+ reportedActions: parsed.optionValues('--reported-action'),
98
+ ...(parsed.hasOption('--trusted-auth-header')
99
+ ? { trustedAuthHeaders: parsed.optionValues('--trusted-auth-header') }
100
+ : {}),
101
+ source: parsed.positionalValue('source'),
102
+ output: jsonOutput(parsed),
103
+ strictExit: parsed.hasOption('--strict-exit'),
104
+ ...terminalOptions(parsed),
105
+ }),
106
+ 'review-evidence': (parsed) => ({
107
+ action: 'review-evidence',
108
+ source: parsed.positionalValue('source'),
109
+ mmdbSource: parsed.optionValue('--mmdb'),
110
+ output: jsonOutput(parsed),
111
+ strictExit: parsed.hasOption('--strict-exit'),
112
+ ...terminalOptions(parsed),
113
+ }),
114
+ brief: (parsed) => singleInputAction('brief', parsed),
115
+ case: parseCaseArguments,
116
+ 'indicator-set': (parsed) => ({
117
+ action: 'indicator-set',
118
+ operation: parsed.positionalValue('operation'),
119
+ source: parsed.positionalValue('source'),
120
+ output: jsonOutput(parsed),
121
+ ...terminalOptions(parsed),
122
+ }),
123
+ 'case-pack': (parsed) => ({
124
+ action: 'case-pack',
125
+ source: parsed.positionalValue('source'),
126
+ output: jsonOutput(parsed),
127
+ audience: parsed.optionValue('--audience'),
128
+ reviewed: true,
129
+ ...terminalOptions(parsed),
130
+ }),
131
+ });