@slicedearth/whoisleuth-cli 1.45.0 → 2.0.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 (268) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +74 -124
  4. package/SECURITY.md +6 -3
  5. package/bin/whoisleuth.mjs +7 -4
  6. package/cli/archive-inspect.mjs +28 -10
  7. package/cli/arguments.mjs +450 -173
  8. package/cli/artifact-structure.mjs +2507 -0
  9. package/cli/artifact-verify.mjs +286 -64
  10. package/cli/bounded-json.mjs +1 -0
  11. package/cli/bulk-checkpoint.mjs +35 -23
  12. package/cli/bulk-command-runner.mjs +2 -5
  13. package/cli/bulk.mjs +9 -2
  14. package/cli/case-pack.mjs +435 -46
  15. package/cli/ci-report.mjs +6 -1
  16. package/cli/collection-preflight.mjs +2 -2
  17. package/cli/command-catalogue.mjs +12 -3
  18. package/cli/command-reference.mjs +948 -106
  19. package/cli/completion.mjs +504 -218
  20. package/cli/config-profile.mjs +13 -3
  21. package/cli/ct-event-intake.mjs +32 -2
  22. package/cli/discover.mjs +7 -1
  23. package/cli/discovery-observation-snapshot.mjs +24 -28
  24. package/cli/discovery-scan-command-runner.mjs +13 -14
  25. package/cli/discovery-scan.mjs +17 -3
  26. package/cli/discovery-snapshot.mjs +15 -7
  27. package/cli/discovery-workflow.mjs +2 -3
  28. package/cli/doctor.mjs +2 -2
  29. package/cli/domain-control-monitor.mjs +215 -36
  30. package/cli/domain-control-observations.mjs +34 -17
  31. package/cli/errors.mjs +8 -3
  32. package/cli/evidence-command-runner.mjs +17 -11
  33. package/cli/evidence-signing.mjs +68 -50
  34. package/cli/export-evidence.mjs +11 -4
  35. package/cli/external-observation-mapping.mjs +3 -3
  36. package/cli/formatters/html.mjs +3 -3
  37. package/cli/formatters/json.mjs +20 -32
  38. package/cli/formatters/terminal.mjs +586 -22
  39. package/cli/input.mjs +62 -0
  40. package/cli/interactive-launcher.mjs +226 -0
  41. package/cli/interchange-report.mjs +226 -0
  42. package/cli/investigation-manifest.mjs +16 -17
  43. package/cli/investigation-plan.mjs +167 -16
  44. package/cli/investigation-run.mjs +80 -24
  45. package/cli/local-mmdb-review.mjs +45 -14
  46. package/cli/lookalike-calibration.mjs +3 -1
  47. package/cli/lookup-brief.mjs +13 -8
  48. package/cli/lookup-browser.mjs +844 -0
  49. package/cli/lookup-command-runner.mjs +96 -33
  50. package/cli/lookup-diff.mjs +11 -2
  51. package/cli/lookup-plan.mjs +1 -0
  52. package/cli/mail-review.mjs +206 -94
  53. package/cli/manual.mjs +5 -5
  54. package/cli/network-command-runner.mjs +78 -0
  55. package/cli/offline-evidence-review.mjs +45 -14
  56. package/cli/open-asset-model-bridge.mjs +1 -1
  57. package/cli/output-file.mjs +70 -14
  58. package/cli/page-compare.mjs +18 -10
  59. package/cli/progress-events.mjs +2 -1
  60. package/cli/progress.mjs +3 -3
  61. package/cli/registry-cohort.mjs +343 -61
  62. package/cli/registry-doctor.mjs +67 -28
  63. package/cli/registry-support.mjs +2 -1
  64. package/cli/retained-artifact-diff.mjs +375 -0
  65. package/cli/risk-calibration.mjs +89 -40
  66. package/cli/runner.mjs +322 -246
  67. package/cli/saved-lookup.mjs +439 -49
  68. package/cli/sharing-review.mjs +18 -7
  69. package/cli/source-reliability.mjs +75 -36
  70. package/cli/terminal-presentation.mjs +43 -18
  71. package/docs/cli-reference.md +125 -1334
  72. package/docs/cli.md +87 -236
  73. package/frontend/src/lib/analysis/domain-control-manifest-core.js +3 -0
  74. package/frontend/src/lib/analysis/domain-control-records.js +3 -0
  75. package/lib/application-version.mjs +3 -0
  76. package/lib/availability.mjs +72 -52
  77. package/lib/bounded-contract-normalizers.mjs +2 -4
  78. package/lib/bounded-file.mjs +91 -4
  79. package/lib/bounded-json.mjs +287 -0
  80. package/lib/browser-library-profile.mjs +1 -1
  81. package/lib/certificate-extension-profile.mjs +2 -1
  82. package/lib/classify.mjs +56 -5
  83. package/lib/client-behavior-profile.mjs +1 -1
  84. package/lib/credential-surface-profile.mjs +1 -1
  85. package/lib/cryptographic-assurance.mjs +173 -0
  86. package/lib/ct-query.mjs +3 -3
  87. package/lib/ct-search.mjs +120 -64
  88. package/lib/distributed-operation-budget.mjs +4 -2
  89. package/lib/dns-delegation-health.mjs +9 -6
  90. package/lib/dns-intelligence.mjs +4 -3
  91. package/lib/dns-resolver-selection.mjs +6 -4
  92. package/lib/dnssec-chain-validation.mjs +1052 -0
  93. package/lib/dnssec-evidence-validation.mjs +23 -10
  94. package/lib/domain-assurance.mjs +12 -12
  95. package/lib/domain-change-packet.mjs +7 -5
  96. package/lib/domain-control-flight-recorder.mjs +442 -78
  97. package/lib/domain-control-manifest.mjs +314 -171
  98. package/lib/domain-posture-analysis.mjs +5 -2
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/encrypted-dns-contract.mjs +2 -3
  101. package/lib/evidence-export.mjs +1243 -88
  102. package/lib/evidence-report-markdown.mjs +3 -0
  103. package/lib/evidence-report.mjs +5 -0
  104. package/lib/external-intelligence-risk.mjs +26 -10
  105. package/lib/favicon.mjs +8 -5
  106. package/lib/feature-policy.mjs +1 -1
  107. package/lib/homepage-metadata-contract.mjs +290 -0
  108. package/lib/hostname.mjs +1 -1
  109. package/lib/html-signals.mjs +89 -5
  110. package/lib/http-evidence-bounds.mjs +3 -0
  111. package/lib/http-intelligence.mjs +332 -6
  112. package/lib/interchange-fidelity-registry.mjs +124 -0
  113. package/lib/local-geoip-evidence.mjs +2 -1
  114. package/lib/lookup-cache.mjs +6 -4
  115. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  116. package/lib/lookup.mjs +27 -4
  117. package/lib/observed-network-context.mjs +1 -1
  118. package/lib/outbound-identity.mjs +2 -3
  119. package/lib/page-fingerprints.mjs +2 -2
  120. package/lib/page-role-profile.mjs +1 -1
  121. package/lib/perceptual-hash.mjs +12 -4
  122. package/lib/portable-generator.mjs +29 -0
  123. package/lib/provider-policy-admission.mjs +11 -4
  124. package/lib/rdap-bootstrap.mjs +76 -16
  125. package/lib/rdap-client.mjs +20 -12
  126. package/lib/rdap-normalization.mjs +324 -80
  127. package/lib/rdap-registrar.mjs +5 -1
  128. package/lib/rdap-transport.mjs +2 -2
  129. package/lib/rdap-validation.mjs +65 -14
  130. package/lib/rdap.mjs +2 -2
  131. package/lib/registrable-domain.mjs +36 -0
  132. package/lib/registry-capabilities.mjs +3 -0
  133. package/lib/registry-capability-catalogue.mjs +12 -2
  134. package/lib/registry-comparison.mjs +28 -11
  135. package/lib/registry-insights.mjs +7 -0
  136. package/lib/response-policy.mjs +27 -4
  137. package/lib/risk-calibration-summary.mjs +340 -0
  138. package/lib/risk-scoring.mjs +4 -3
  139. package/lib/rpki-evidence.mjs +9 -8
  140. package/lib/safe-fetch.mjs +121 -50
  141. package/lib/security-txt.mjs +27 -4
  142. package/lib/service-binding-dns.mjs +6 -1
  143. package/lib/smtp-transport-review.mjs +787 -0
  144. package/lib/sslbl-certificates.generated.mjs +136 -134
  145. package/lib/sslbl-intelligence.mjs +6 -8
  146. package/lib/static-html-analysis.mjs +301 -4
  147. package/lib/structured-data-identity.mjs +51 -19
  148. package/lib/threat-intelligence-fixtures.mjs +2 -4
  149. package/lib/threat-intelligence-manifests.mjs +2 -7
  150. package/lib/threat-intelligence-runtime.mjs +5 -15
  151. package/lib/threat-intelligence-types.mjs +21 -1
  152. package/lib/threatfox-intelligence.mjs +7 -3
  153. package/lib/tls-intelligence.mjs +15 -9
  154. package/lib/tlsa-evidence.mjs +37 -7
  155. package/lib/urlhaus-intelligence.mjs +4 -2
  156. package/lib/urlscan-intelligence.mjs +6 -5
  157. package/lib/website-security-posture.mjs +1 -1
  158. package/lib/website-technology.mjs +1 -1
  159. package/lib/whois-authority.mjs +43 -8
  160. package/lib/whois-chain.mjs +25 -3
  161. package/lib/whois-normalization.mjs +3 -1
  162. package/lib/whois-transport.mjs +12 -3
  163. package/lib/zone-intent-review.mjs +1 -1
  164. package/package.json +12 -4
  165. package/packages/cases/case-brand-profile-references.mjs +85 -0
  166. package/{frontend/src/lib/analysis/case-evidence-model.js → packages/cases/case-evidence-model.mjs} +43 -14
  167. package/packages/cases/case-investigation-branch-model.mjs +132 -0
  168. package/packages/cases/case-migration-model.mjs +476 -0
  169. package/{frontend/src/lib/analysis/case-model.js → packages/cases/case-model.mjs} +3 -3
  170. package/{frontend/src/lib/analysis/case-record-contracts.js → packages/cases/case-record-contracts.mjs} +3 -19
  171. package/{frontend/src/lib/analysis/case-record-core.js → packages/cases/case-record-core.mjs} +58 -31
  172. package/packages/cases/case-record-model.mjs +9 -0
  173. package/{frontend/src/lib/analysis/case-record-operations.js → packages/cases/case-record-operations.mjs} +106 -33
  174. package/{frontend/src/lib/analysis/case-report.js → packages/cases/case-report.mjs} +143 -16
  175. package/packages/cases/case-response-model.mjs +1604 -0
  176. package/packages/cases/case-response-packet.mjs +1312 -0
  177. package/{frontend/src/lib/analysis/case-storage-model.js → packages/cases/case-storage-model.mjs} +2 -2
  178. package/{frontend/src/lib/analysis/http-summary.js → packages/cases/http-summary.mjs} +1 -1
  179. package/packages/cases/opaque-reference-id.mjs +9 -0
  180. package/packages/comparison/comparison-ledger-bulk.mjs +366 -0
  181. package/packages/comparison/comparison-ledger-contract.mjs +241 -0
  182. package/packages/comparison/comparison-ledger-json.mjs +80 -0
  183. package/packages/comparison/comparison-ledger-serialization.mjs +52 -0
  184. package/packages/comparison/favicon-similarity.mjs +78 -0
  185. package/{frontend/src/lib/analysis/page-similarity.js → packages/comparison/page-similarity.mjs} +4 -3
  186. package/{frontend/src/lib/analysis/relationship-evidence.js → packages/comparison/relationship-evidence.mjs} +6 -7
  187. package/packages/contracts/analyst-review-state-contract.mjs +61 -0
  188. package/packages/contracts/analyst-review-state.mjs +50 -0
  189. package/packages/contracts/case-portability.mjs +1310 -0
  190. package/packages/contracts/cli-command-catalogue.mjs +2 -0
  191. package/packages/contracts/cli-lookup.mjs +394 -0
  192. package/packages/contracts/domain-control-flight-recorder.mjs +461 -0
  193. package/packages/contracts/domain-control-manifest.mjs +1027 -0
  194. package/packages/contracts/domain-control-monitor.mjs +310 -0
  195. package/packages/contracts/domain-control-review.mjs +828 -0
  196. package/packages/contracts/domain-name.mjs +4 -0
  197. package/packages/contracts/external-observation-interchange.mjs +224 -0
  198. package/packages/contracts/extracted-domain-lifecycle.mjs +218 -0
  199. package/packages/contracts/investigation-portability.mjs +331 -0
  200. package/packages/contracts/offline-comparison.mjs +160 -0
  201. package/packages/contracts/risk-calibration.mjs +1288 -0
  202. package/packages/contracts/schema-compatibility.mjs +71 -0
  203. package/packages/contracts/schema-lifecycle.mjs +1702 -0
  204. package/packages/contracts/sslbl-snapshot.mjs +17 -0
  205. package/packages/contracts/workspace-portability.mjs +1347 -0
  206. package/packages/evidence/artifact-integrity.mjs +65 -0
  207. package/packages/evidence/decision-fact.mjs +735 -0
  208. package/packages/evidence/domain-control-runtime.mjs +547 -0
  209. package/packages/evidence/domain-name.mjs +41 -0
  210. package/packages/evidence/observation.mjs +384 -0
  211. package/{frontend/src/lib/analysis/external-findings-import.js → packages/interchange/external-findings-import.mjs} +86 -20
  212. package/packages/interchange/retained-text.mjs +8 -0
  213. package/{frontend/src/lib/analysis/acquisition-decision-packet.js → packages/investigation/acquisition-decision-packet.mjs} +11 -8
  214. package/{frontend/src/lib/analysis/bulk-domain-comparison.js → packages/investigation/bulk-domain-comparison.mjs} +28 -9
  215. package/{frontend/src/lib/analysis/bulk-mail-exposure.js → packages/investigation/bulk-mail-exposure.mjs} +67 -18
  216. package/{frontend/src/lib/analysis/bulk-review-export.js → packages/investigation/bulk-review-export.mjs} +10 -9
  217. package/packages/investigation/bulk-review-policy.mjs +1 -0
  218. package/{frontend/src/lib/analysis/investigation-capsule.js → packages/investigation/investigation-capsule.mjs} +50 -18
  219. package/packages/investigation/lookup-artefact-inputs.mjs +1 -0
  220. package/{frontend/src/lib/analysis/lookup-asset-graph.js → packages/investigation/lookup-asset-graph.mjs} +4 -2
  221. package/packages/investigation/lookup-claim-passport.mjs +232 -0
  222. package/packages/investigation/lookup-claim-readiness.mjs +287 -0
  223. package/packages/investigation/lookup-investigation-brief.mjs +172 -0
  224. package/packages/monitoring/analyst-review-state.mjs +680 -0
  225. package/{frontend/src/lib/analysis/brand-profile-model.js → packages/workspace/brand-profile-model.mjs} +97 -50
  226. package/{frontend/src/lib/analysis/bulk-review-model.js → packages/workspace/bulk-review-model.mjs} +15 -15
  227. package/{frontend/src/lib/analysis/bulk-session-model.js → packages/workspace/bulk-session-model.mjs} +179 -39
  228. package/{frontend/src/lib/analysis/bulk-sort.js → packages/workspace/bulk-sort.mjs} +12 -5
  229. package/{frontend/src/lib/analysis/campaign-model.js → packages/workspace/campaign-model.mjs} +20 -22
  230. package/{frontend/src/lib/analysis/detection-rule-model.js → packages/workspace/detection-rule-model.mjs} +15 -17
  231. package/packages/workspace/hostile-input.mjs +172 -0
  232. package/{frontend/src/lib/analysis/investigation-guide.js → packages/workspace/investigation-guide.mjs} +51 -46
  233. package/{frontend/src/lib/analysis/investigation-template-model.js → packages/workspace/investigation-template-model.mjs} +15 -15
  234. package/{frontend/src/lib/analysis/page-baseline.js → packages/workspace/page-baseline.mjs} +6 -13
  235. package/{frontend/src/lib/analysis/relationship-observation-model.js → packages/workspace/relationship-observation-model.mjs} +13 -17
  236. package/{frontend/src/lib/analysis/shortlist-model.js → packages/workspace/shortlist-model.mjs} +20 -18
  237. package/{frontend/src/lib/analysis/watchlist-history.js → packages/workspace/watchlist-history.mjs} +9 -16
  238. package/{frontend/src/lib/analysis/watchlist-store.js → packages/workspace/watchlist-store.mjs} +24 -17
  239. package/{frontend/src/lib/analysis/website-snapshot-model.js → packages/workspace/website-snapshot-model.mjs} +18 -12
  240. package/{frontend/src/lib/analysis/workspace-archive-crypto.js → packages/workspace/workspace-archive-crypto.mjs} +49 -19
  241. package/{frontend/src/lib/analysis/workspace-archive.js → packages/workspace/workspace-archive.mjs} +195 -84
  242. package/third-party-notices.txt +112 -5834
  243. package/frontend/src/lib/analysis/artifact-integrity.js +0 -21
  244. package/frontend/src/lib/analysis/bulk-retry-plan.js +0 -79
  245. package/frontend/src/lib/analysis/bulk-source-coverage.js +0 -76
  246. package/frontend/src/lib/analysis/case-migration-model.js +0 -253
  247. package/frontend/src/lib/analysis/case-record-model.js +0 -7
  248. package/frontend/src/lib/analysis/case-response-model.js +0 -669
  249. package/frontend/src/lib/analysis/case-response-packet.js +0 -573
  250. package/frontend/src/lib/analysis/lookup-decision-support.js +0 -487
  251. package/frontend/src/lib/analysis/lookup-investigation-brief.js +0 -153
  252. package/frontend/src/lib/analysis/lookup-presentation.js +0 -58
  253. package/frontend/src/lib/analysis/lookup-response.js +0 -2
  254. package/frontend/src/lib/analysis/lookup-source-refresh.js +0 -277
  255. package/frontend/src/lib/analysis/lookup-summary-model.js +0 -398
  256. package/frontend/src/lib/analysis/scoring.js +0 -55
  257. package/frontend/src/lib/analysis/utils.js +0 -232
  258. package/frontend/src/lib/analysis/visualization-bounds.js +0 -26
  259. package/frontend/src/lib/analysis/visualization-force-graph.js +0 -352
  260. package/frontend/src/lib/analysis/visualization-matrix.js +0 -88
  261. package/frontend/src/lib/analysis/visualization-models.js +0 -369
  262. package/frontend/src/lib/analysis/visualization-time-series.js +0 -144
  263. package/lib/bounded-json-response.mjs +0 -157
  264. package/lib/lookup-response-contract.mjs +0 -346
  265. package/lib/observation.mjs +0 -114
  266. /package/{frontend/src/lib/analysis/acquisition-due-diligence.js → packages/investigation/acquisition-due-diligence.mjs} +0 -0
  267. /package/{frontend/src/lib/analysis/evidence-coverage-ledger.js → packages/investigation/evidence-coverage-ledger.mjs} +0 -0
  268. /package/{frontend/src/lib/analysis/dns-record-normalization.js → packages/workspace/dns-record-normalization.mjs} +0 -0
package/cli/runner.mjs CHANGED
@@ -1,15 +1,16 @@
1
1
  import { Buffer } from 'node:buffer';
2
- import { createReadStream } from 'node:fs';
3
2
  import { createRequire } from 'node:module';
4
3
  import { abortable } from "../lib/abort.mjs";
4
+ import { scanBoundedJson } from "../lib/bounded-json.mjs";
5
5
  import { REGISTRY_CAPABILITIES_VERSION, registryCapabilityFor } from "../lib/registry-capabilities.mjs";
6
6
  import { explainRiskScore, explainRiskScoreV6, RISK_MODEL_VERSION, RISK_REVIEW_THRESHOLD } from "../lib/risk-scoring.mjs";
7
- import { CLI_COMMANDS, parseCliArguments } from "./arguments.mjs";
8
- import { buildCliCommandCatalogue, formatCliCommandCatalogue } from "./command-catalogue.mjs";
9
- import { COMMAND_COLLECTION, COMMAND_DETAILS, COMMAND_USAGE, HELP, commandHelp, } from "./command-reference.mjs";
7
+ import { buildRiskCalibrationSummaryReport } from "../lib/risk-calibration-summary.mjs";
8
+ import { parseCliArguments } from "./arguments.mjs";
9
+ import { buildCliCommandCatalogue, formatCliCommandCatalogue, selectCliCommands } from "./command-catalogue.mjs";
10
+ import { COMMAND_COLLECTION, COMMAND_DETAILS, COMMAND_USAGE, HELP, CLI_COMMANDS, CLI_COMMAND_REGISTRY, commandDefinition, commandHelp, } from "./command-reference.mjs";
10
11
  import { buildShellCompletion } from "./completion.mjs";
11
12
  import { buildDoctorReport, formatDoctorReport } from "./doctor.mjs";
12
- import { MAX_COMPARE_INPUT_BYTES, compareLookupDocument, parseCliLookupDocument, readCompareInputBounded, } from "./compare.mjs";
13
+ import { MAX_COMPARE_INPUT_BYTES, compareLookupDocument, parseCliLookupDocument, } from "./compare.mjs";
13
14
  import { boundedCliErrorMessage, CliUsageError } from "./errors.mjs";
14
15
  import { evidenceCommandFailureLabel, isEvidenceCommand, runEvidenceCommand, } from "./evidence-command-runner.mjs";
15
16
  import { buildCliEvidenceExport, formatCliEvidenceExport } from "./export-evidence.mjs";
@@ -18,7 +19,7 @@ import { formatLookupEvidenceHtml } from "./formatters/html.mjs";
18
19
  import { buildCliCompareDocument, formatJsonDocument, } from "./formatters/json.mjs";
19
20
  import { formatLookupEvidenceMarkdown } from "./formatters/markdown.mjs";
20
21
  import { formatTerminalCompare, formatTerminalRegistrySupport, formatTerminalRiskCalibration, } from "./formatters/terminal.mjs";
21
- import { buildCliLookupDiff, formatCliLookupDiff } from "./lookup-diff.mjs";
22
+ import { MAX_RETAINED_ARTIFACT_DIFF_BYTES, buildCliRetainedArtifactDiff, formatCliRetainedArtifactDiff, } from "./retained-artifact-diff.mjs";
22
23
  import { MAX_LOOKUP_RECONCILIATION_INPUT_BYTES, buildCliLookupReconciliation, formatCliLookupReconciliation, } from "./lookup-reconcile.mjs";
23
24
  import { MAX_SHARING_REVIEW_BYTES, buildSharingReview, formatSharingReview, } from "./sharing-review.mjs";
24
25
  import { MAX_LOOKUP_TIMELINE_INPUT_BYTES, buildCliLookupTimeline, formatCliLookupTimeline, } from "./lookup-timeline.mjs";
@@ -26,61 +27,48 @@ import { buildCliPageComparison, formatCliPageComparison } from "./page-compare.
26
27
  import { MAX_MAIL_REVIEW_INPUT_BYTES, buildCliMailReview, formatCliMailReview, } from "./mail-review.mjs";
27
28
  import { MAX_OFFLINE_EVIDENCE_INPUT_BYTES, buildOfflineEvidenceReview, buildOfflineEvidenceReviewWithLocalResources, formatOfflineEvidenceReview, } from "./offline-evidence-review.mjs";
28
29
  import { DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA, DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, buildDomainControlManifest, formatDomainControlResult, reviewDomainControlManifest, } from "../lib/domain-control-manifest.mjs";
29
- import { DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, buildDomainControlFlightRecorder, formatDomainControlFlightRecorder, } from "../lib/domain-control-flight-recorder.mjs";
30
- import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, buildCliDomainControlReview, formatCliDomainControlReview, } from "./domain-control-observations.mjs";
30
+ import { serializeDomainControlManifest } from "../packages/evidence/domain-control-runtime.mjs";
31
+ import { MAX_DOMAIN_CONTROL_REVIEW_COMMAND_BYTES } from "../packages/contracts/domain-control-review.mjs";
32
+ import { DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA, buildDomainControlFlightRecorder, formatDomainControlFlightRecorder, serializeDomainControlFlightRecorder, } from "../lib/domain-control-flight-recorder.mjs";
33
+ import { CLI_DOMAIN_CONTROL_REVIEW_INPUT_SCHEMA, MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH, MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS, MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES, buildCliDomainControlReview, formatCliDomainControlReview, } from "./domain-control-observations.mjs";
31
34
  import { buildCliLookupBrief, formatCliLookupBrief } from "./lookup-brief.mjs";
32
35
  import { buildCliCasePack, formatCliCasePack } from "./case-pack.mjs";
33
36
  import { formatDomainControlMonitor, runDomainControlMonitor } from "./domain-control-monitor.mjs";
34
37
  import { MAX_ASSURANCE_INPUT_BYTES, buildDomainAssurance, formatDomainAssurance, } from "../lib/domain-assurance.mjs";
35
38
  import { MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, buildDomainChangePacket, formatDomainChangePacket, } from "../lib/domain-change-packet.mjs";
36
39
  import { buildCliManual } from "./manual.mjs";
37
- import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, buildInvestigationManifest, formatInvestigationManifest, } from "./investigation-manifest.mjs";
40
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, buildInvestigationManifest, formatInvestigationManifest, } from "./investigation-manifest.mjs";
38
41
  import { MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, mapExternalObservations, formatExternalObservationMapping, } from "./external-observation-mapping.mjs";
39
42
  import { buildOpenAssetModelBridge, formatOpenAssetModelBridge, } from "./open-asset-model-bridge.mjs";
40
43
  import { MAX_CT_EVENT_INPUT_BYTES, buildCtEventFindings, formatCtEventFindings, } from "./ct-event-intake.mjs";
41
- import { buildInvestigationPlan, formatInvestigationPlan } from "./investigation-plan.mjs";
42
- import { formatInvestigationRun, runInvestigationRecipe } from "./investigation-run.mjs";
44
+ import { buildInvestigationPlan, buildWorkflowRecipeCatalogue, formatInvestigationPlan, formatWorkflowRecipeCatalogue, } from "./investigation-plan.mjs";
45
+ import { MAX_INVESTIGATION_RUN_BYTES, formatInvestigationRun, runInvestigationRecipe } from "./investigation-run.mjs";
46
+ import { readCliTextInput } from "./input.mjs";
43
47
  import { evaluateCliFailPolicies, formatFailPolicyNotice } from "./fail-policy.mjs";
44
48
  import { formatCliJunit } from "./ci-report.mjs";
45
- import { createBufferedOutput, writePrivateFile } from "./output-file.mjs";
49
+ import { cleanupPendingOutputFiles, createBufferedOutput, writePrivateFile } from "./output-file.mjs";
50
+ import { canLaunchInteractiveCli, launchInteractiveCli, } from "./interactive-launcher.mjs";
46
51
  import { createTerminalProgress } from "./progress.mjs";
47
52
  import { buildRegistrySupportDocument } from "./registry-support.mjs";
48
53
  import { buildRegistryDoctorReport, formatRegistryDoctorReport } from "./registry-doctor.mjs";
49
54
  import { MAX_REGISTRY_COHORT_INPUT_BYTES, buildRegistryCohortReport, formatRegistryCohortReport, } from "./registry-cohort.mjs";
50
55
  import { buildRegistryFixtureScaffold } from "./registry-fixture-scaffold.mjs";
51
- import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, formatOfflineArtifactVerification, verifyOfflineArtifact, } from "./artifact-verify.mjs";
56
+ import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, formatOfflineArtifactVerification, isCompleteOfflineArtifactVerification, verifyOfflineArtifact, } from "./artifact-verify.mjs";
57
+ import { buildInterchangeFidelityReport, formatInterchangeFidelityReport, } from "./interchange-report.mjs";
52
58
  import { MAX_SOURCE_RELIABILITY_INPUT_BYTES, buildSourceReliabilityReport, formatSourceReliabilityReport, } from "./source-reliability.mjs";
53
- import { MAX_RISK_CALIBRATION_INPUT_BYTES, buildRiskCalibrationReport, parseRiskCalibrationDataset, readRiskCalibrationInputBounded, } from "./risk-calibration.mjs";
59
+ import { MAX_RISK_CALIBRATION_INPUT_BYTES, buildRiskCalibrationReport, parseRiskCalibrationDataset, serializeRiskCalibrationReport, } from "./risk-calibration.mjs";
54
60
  import { MAX_LOOKALIKE_CALIBRATION_BYTES, buildLookalikeCalibration, formatLookalikeCalibration, } from "./lookalike-calibration.mjs";
55
- import { MAX_SAVED_LOOKUP_INPUT_BYTES, readSavedLookupInputBounded } from "./saved-lookup.mjs";
61
+ import { MAX_SAVED_LOOKUP_INPUT_BYTES } from "./saved-lookup.mjs";
56
62
  import { presentTerminalOutput, terminalPresentation, } from "./terminal-presentation.mjs";
57
63
  const require = createRequire(import.meta.url);
58
64
  const { version: VERSION } = require('../package.json');
59
65
  const MAX_STDIN_BYTES = 4096;
60
66
  async function readStdinBounded(stream, limit = MAX_STDIN_BYTES, signal) {
61
- if (!stream || stream.isTTY)
62
- return '';
63
- const abort = () => stream.destroy?.(new DOMException('Aborted', 'AbortError'));
64
- if (signal?.aborted)
65
- throw new DOMException('Aborted', 'AbortError');
66
- signal?.addEventListener('abort', abort, { once: true });
67
- const chunks = [];
68
- let total = 0;
69
- try {
70
- for await (const chunk of stream) {
71
- if (signal?.aborted)
72
- throw new DOMException('Aborted', 'AbortError');
73
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
74
- total += buffer.length;
75
- if (total > limit)
76
- throw new CliUsageError(`Standard input is limited to ${limit} bytes.`);
77
- chunks.push(buffer);
78
- }
79
- }
80
- finally {
81
- signal?.removeEventListener('abort', abort);
82
- }
83
- const text = Buffer.concat(chunks).toString('utf8').trim();
67
+ const text = (await readCliTextInput(null, stream, {
68
+ maximumBytes: limit,
69
+ label: 'Standard input',
70
+ ...(signal ? { signal } : {}),
71
+ })).trim();
84
72
  const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
85
73
  if (lines.length > 1)
86
74
  throw new CliUsageError('Single-value commands accept one stdin line. Use the bulk command for multiple inputs.');
@@ -90,10 +78,13 @@ function write(stream, value) {
90
78
  if (stream && typeof stream.write === 'function')
91
79
  stream.write(value);
92
80
  }
93
- function formatForTerminal(value, stream, color, environment) {
94
- return presentTerminalOutput(value, terminalPresentation(stream, color, environment));
81
+ function formatForTerminal(value, stream, color, environment, palette) {
82
+ return presentTerminalOutput(value, terminalPresentation(stream, color, environment, palette));
95
83
  }
96
84
  function isCancellation(error, signal) {
85
+ if (error instanceof AggregateError) {
86
+ return error.errors.length > 0 && error.errors.every((item) => isCancellation(item));
87
+ }
97
88
  return signal?.aborted === true
98
89
  || Boolean(error && typeof error === 'object' && 'name' in error && error.name === 'AbortError');
99
90
  }
@@ -107,6 +98,16 @@ function usageEventReason(error) {
107
98
  return 'missing_input';
108
99
  return 'invalid_input';
109
100
  }
101
+ const INLINE_CLI_COMMANDS = Object.freeze([
102
+ 'completion', 'doctor', 'commands', 'manual', 'manifest', 'map-observations',
103
+ 'oam-export', 'ct-intake', 'registry-support', 'registry-doctor',
104
+ 'registry-cohort', 'registry-scaffold', 'risk-calibrate', 'lookalike-calibrate',
105
+ 'verify-artifact', 'interchange-report', 'source-report', 'compare',
106
+ 'page-compare', 'mail-review', 'review-evidence', 'brief', 'case-pack',
107
+ 'domain-control', 'monitor-once', 'assurance', 'change-packet',
108
+ 'sharing-review', 'workflow-plan', 'workflow-run', 'diff', 'reconcile',
109
+ 'timeline', 'export',
110
+ ]);
110
111
  async function runParsedCli(args, dependencies = {}) {
111
112
  const stdout = dependencies.stdout || process.stdout;
112
113
  const stderr = dependencies.stderr || process.stderr;
@@ -115,7 +116,8 @@ async function runParsedCli(args, dependencies = {}) {
115
116
  const eventProgress = { current: null };
116
117
  let failureLabel = 'Lookup';
117
118
  try {
118
- const terminal = (value, color = true) => formatForTerminal(value, stdout, color, environment);
119
+ const palette = args.palette || 'auto';
120
+ const terminal = (value, color = true) => formatForTerminal(value, stdout, color, environment, palette);
119
121
  const beginProgress = (message) => {
120
122
  const terminalOutput = 'output' in args
121
123
  && args.output === 'terminal'
@@ -127,6 +129,7 @@ async function runParsedCli(args, dependencies = {}) {
127
129
  enabled,
128
130
  color: terminalOutput ? args.color : false,
129
131
  environment,
132
+ palette,
130
133
  ...(dependencies.nowMs ? { now: dependencies.nowMs } : {}),
131
134
  });
132
135
  progress.start(message);
@@ -148,6 +151,29 @@ async function runParsedCli(args, dependencies = {}) {
148
151
  const readSingleInput = async () => (dependencies.readStdin
149
152
  ? await dependencies.readStdin()
150
153
  : await readStdinBounded(dependencies.stdin || process.stdin, MAX_STDIN_BYTES, dependencies.signal));
154
+ const readInput = async (source, maximumBytes, label) => (readCliTextInput(source, dependencies.stdin || process.stdin, {
155
+ maximumBytes,
156
+ label,
157
+ ...(dependencies.signal ? { signal: dependencies.signal } : {}),
158
+ }));
159
+ const readPassphraseSource = async (source) => {
160
+ let passphraseText;
161
+ try {
162
+ passphraseText = dependencies.readPassphraseFile
163
+ ? await dependencies.readPassphraseFile(source)
164
+ : await readInput(source, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, 'Passphrase file');
165
+ }
166
+ catch (error) {
167
+ if (error instanceof CliUsageError)
168
+ throw error;
169
+ throw new CliUsageError(`Could not read passphrase file: ${boundedCliErrorMessage(error, 'File could not be read')}`);
170
+ }
171
+ const passphrase = passphraseText.replace(/\r?\n$/u, '');
172
+ if (!passphrase || /[\r\n\u0000]/u.test(passphrase)) {
173
+ throw new CliUsageError('Passphrase file must contain exactly one non-empty UTF-8 line.');
174
+ }
175
+ return passphrase;
176
+ };
151
177
  const commandContext = Object.freeze({
152
178
  stdout,
153
179
  stderr,
@@ -155,6 +181,7 @@ async function runParsedCli(args, dependencies = {}) {
155
181
  writeStdout: (value) => write(stdout, value),
156
182
  writeStderr: (value) => write(stderr, value),
157
183
  readSingleInput,
184
+ readInput,
158
185
  now: () => dependencies.now ? dependencies.now() : new Date().toISOString(),
159
186
  beginProgress,
160
187
  endProgress,
@@ -171,13 +198,90 @@ async function runParsedCli(args, dependencies = {}) {
171
198
  write(stdout, `${VERSION}\n`);
172
199
  return EXIT_CODES.SUCCESS;
173
200
  }
201
+ const handlerOwner = commandDefinition(args.action).execution.handlerOwner;
202
+ if (handlerOwner === 'bulk') {
203
+ if (args.action !== 'bulk')
204
+ throw new Error('Bulk command registry ownership is inconsistent.');
205
+ failureLabel = 'Bulk lookup';
206
+ const { runBulkCommand } = await import("./bulk-command-runner.mjs");
207
+ return await runBulkCommand(args, dependencies, commandContext);
208
+ }
209
+ if (handlerOwner === 'discovery') {
210
+ if (args.action !== 'discover')
211
+ throw new Error('Discovery command registry ownership is inconsistent.');
212
+ failureLabel = 'Candidate generation';
213
+ const { runDiscoveryCommand } = await import("./discovery-command-runner.mjs");
214
+ return await runDiscoveryCommand(args, dependencies, commandContext);
215
+ }
216
+ if (handlerOwner === 'discovery_scan') {
217
+ if (args.action !== 'discover-scan')
218
+ throw new Error('Discovery-scan command registry ownership is inconsistent.');
219
+ failureLabel = 'Candidate scan';
220
+ const { runDiscoveryScanCommand } = await import("./discovery-scan-command-runner.mjs");
221
+ return await runDiscoveryScanCommand(args, dependencies, commandContext);
222
+ }
223
+ if (handlerOwner === 'evidence') {
224
+ if (!isEvidenceCommand(args))
225
+ throw new Error('Evidence command registry ownership is inconsistent.');
226
+ failureLabel = evidenceCommandFailureLabel(args.action);
227
+ const evidenceStdout = args.action !== 'sign-artifact' && args.output === 'terminal'
228
+ ? { write: (value) => write(stdout, terminal(value, args.color)) }
229
+ : stdout;
230
+ return await runEvidenceCommand(args, {
231
+ stdout: evidenceStdout,
232
+ stdin: dependencies.stdin || process.stdin,
233
+ readArtifactInput: dependencies.readArtifactInput,
234
+ readPassphraseFile: dependencies.readPassphraseFile,
235
+ readPrivateKeyFile: dependencies.readPrivateKeyFile,
236
+ readPublicKeyFile: dependencies.readPublicKeyFile,
237
+ now: dependencies.now,
238
+ signal: dependencies.signal,
239
+ });
240
+ }
241
+ if (handlerOwner === 'network') {
242
+ if (args.action !== 'ct-search'
243
+ && args.action !== 'posture'
244
+ && args.action !== 'http'
245
+ && args.action !== 'tls'
246
+ && args.action !== 'dnssec-validate'
247
+ && args.action !== 'mail-transport') {
248
+ throw new Error('Network command registry ownership is inconsistent.');
249
+ }
250
+ failureLabel = args.action === 'ct-search'
251
+ ? 'Certificate Transparency search'
252
+ : args.action === 'posture'
253
+ ? 'Domain posture audit'
254
+ : args.action === 'http'
255
+ ? 'HTTP probe'
256
+ : args.action === 'tls'
257
+ ? 'TLS evidence collection'
258
+ : args.action === 'dnssec-validate'
259
+ ? 'DNSSEC chain validation'
260
+ : 'Mail transport review';
261
+ const { runNetworkCommand } = await import("./network-command-runner.mjs");
262
+ return await runNetworkCommand(args, dependencies, commandContext);
263
+ }
264
+ if (handlerOwner === 'lookup') {
265
+ if (args.action !== 'lookup')
266
+ throw new Error('Lookup command registry ownership is inconsistent.');
267
+ const { runLookupCommand } = await import("./lookup-command-runner.mjs");
268
+ return await runLookupCommand(args, dependencies, commandContext);
269
+ }
270
+ if (handlerOwner !== 'inline' || !INLINE_CLI_COMMANDS.includes(args.action)) {
271
+ throw new Error('No CLI execution route is registered for the parsed command.');
272
+ }
174
273
  if (args.action === 'completion') {
175
274
  write(stdout, buildShellCompletion(args.shell));
176
275
  return EXIT_CODES.SUCCESS;
177
276
  }
178
277
  if (args.action === 'commands') {
278
+ const selectedCommands = selectCliCommands(CLI_COMMAND_REGISTRY, {
279
+ common: args.common,
280
+ group: args.group,
281
+ mode: args.mode,
282
+ });
179
283
  const catalogue = buildCliCommandCatalogue({
180
- commands: CLI_COMMANDS,
284
+ commands: selectedCommands,
181
285
  collections: COMMAND_COLLECTION,
182
286
  details: COMMAND_DETAILS,
183
287
  usage: COMMAND_USAGE,
@@ -202,11 +306,16 @@ async function runParsedCli(args, dependencies = {}) {
202
306
  if (args.action === 'manifest') {
203
307
  failureLabel = 'Investigation manifest';
204
308
  const artifacts = [];
309
+ let totalBytes = 0;
205
310
  try {
206
311
  for (const source of args.sources) {
207
312
  const content = dependencies.readDiffInput
208
313
  ? await dependencies.readDiffInput(source)
209
- : await readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), { limit: MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, label: 'Manifest artefact input' });
314
+ : await readInput(source, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, 'Manifest artefact input');
315
+ totalBytes += Buffer.byteLength(content, 'utf8');
316
+ if (totalBytes > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES) {
317
+ throw new CliUsageError(`Manifest artefacts exceed the ${MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES}-byte combined limit.`);
318
+ }
210
319
  artifacts.push({ content });
211
320
  }
212
321
  }
@@ -239,12 +348,7 @@ async function runParsedCli(args, dependencies = {}) {
239
348
  try {
240
349
  input = dependencies.readArtifactInput
241
350
  ? await dependencies.readArtifactInput(args.source)
242
- : await readSavedLookupInputBounded(args.source
243
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
244
- : dependencies.stdin || process.stdin, {
245
- limit: MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES,
246
- label: mapping ? 'External observation mapping input' : 'Open Asset Model bridge input',
247
- });
351
+ : await readInput(args.source, MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, mapping ? 'External observation mapping input' : 'Open Asset Model bridge input');
248
352
  }
249
353
  catch (error) {
250
354
  if (error instanceof CliUsageError)
@@ -255,10 +359,11 @@ async function runParsedCli(args, dependencies = {}) {
255
359
  throw new CliUsageError(`${args.action} requires one versioned JSON file or a document on stdin.`);
256
360
  let parsed;
257
361
  try {
362
+ scanBoundedJson(input);
258
363
  parsed = JSON.parse(input);
259
364
  }
260
365
  catch {
261
- throw new CliUsageError(`${mapping ? 'External observation mapping' : 'Open Asset Model bridge'} input is not valid JSON.`);
366
+ throw new CliUsageError(`${mapping ? 'External observation mapping' : 'Open Asset Model bridge'} input is not valid bounded JSON without duplicate keys.`);
262
367
  }
263
368
  let document;
264
369
  try {
@@ -283,7 +388,7 @@ async function runParsedCli(args, dependencies = {}) {
283
388
  version: VERSION,
284
389
  generatedAt: dependencies.now ? dependencies.now() : new Date().toISOString(),
285
390
  network: args.network,
286
- presentation: terminalPresentation(stdout, args.color, environment),
391
+ presentation: terminalPresentation(stdout, args.color, environment, palette),
287
392
  ...(dependencies.resolvePublicAddresses ? { resolveAddresses: dependencies.resolvePublicAddresses } : {}),
288
393
  ...(dependencies.safeFetch ? { fetchHttps: dependencies.safeFetch } : {}),
289
394
  ...(dependencies.whoisQuery ? { queryWhois: dependencies.whoisQuery } : {}),
@@ -321,12 +426,7 @@ async function runParsedCli(args, dependencies = {}) {
321
426
  try {
322
427
  input = dependencies.readCompareInput
323
428
  ? await dependencies.readCompareInput(args.source)
324
- : await readSavedLookupInputBounded(args.source
325
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
326
- : dependencies.stdin || process.stdin, {
327
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
328
- label: 'Registry doctor input',
329
- });
429
+ : await readInput(args.source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Registry doctor input');
330
430
  }
331
431
  catch (error) {
332
432
  if (error instanceof CliUsageError)
@@ -348,12 +448,7 @@ async function runParsedCli(args, dependencies = {}) {
348
448
  try {
349
449
  input = dependencies.readCompareInput
350
450
  ? await dependencies.readCompareInput(args.source)
351
- : await readSavedLookupInputBounded(args.source
352
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
353
- : dependencies.stdin || process.stdin, {
354
- limit: MAX_REGISTRY_COHORT_INPUT_BYTES,
355
- label: 'Registry cohort input',
356
- });
451
+ : await readInput(args.source, MAX_REGISTRY_COHORT_INPUT_BYTES, 'Registry cohort input');
357
452
  }
358
453
  catch (error) {
359
454
  if (error instanceof CliUsageError)
@@ -385,9 +480,7 @@ async function runParsedCli(args, dependencies = {}) {
385
480
  try {
386
481
  input = dependencies.readRiskCalibrationInput
387
482
  ? await dependencies.readRiskCalibrationInput(args.source)
388
- : await readRiskCalibrationInputBounded(args.source
389
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
390
- : dependencies.stdin || process.stdin, MAX_RISK_CALIBRATION_INPUT_BYTES);
483
+ : await readInput(args.source, MAX_RISK_CALIBRATION_INPUT_BYTES, 'Risk calibration input');
391
484
  }
392
485
  catch (error) {
393
486
  if (error instanceof CliUsageError)
@@ -407,7 +500,11 @@ async function runParsedCli(args, dependencies = {}) {
407
500
  } : {}),
408
501
  });
409
502
  if (!args.quiet)
410
- write(stdout, args.output === 'json' ? formatJsonDocument(report) : terminal(formatTerminalRiskCalibration(report), args.color));
503
+ write(stdout, args.output === 'summary_json'
504
+ ? serializeRiskCalibrationReport(buildRiskCalibrationSummaryReport(report))
505
+ : args.output === 'json'
506
+ ? serializeRiskCalibrationReport(report)
507
+ : terminal(formatTerminalRiskCalibration(report), args.color));
411
508
  return EXIT_CODES.SUCCESS;
412
509
  }
413
510
  if (args.action === 'lookalike-calibrate') {
@@ -416,12 +513,7 @@ async function runParsedCli(args, dependencies = {}) {
416
513
  try {
417
514
  input = dependencies.readRiskCalibrationInput
418
515
  ? await dependencies.readRiskCalibrationInput(args.source)
419
- : await readSavedLookupInputBounded(args.source
420
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
421
- : dependencies.stdin || process.stdin, {
422
- limit: MAX_LOOKALIKE_CALIBRATION_BYTES,
423
- label: 'Lookalike calibration input',
424
- });
516
+ : await readInput(args.source, MAX_LOOKALIKE_CALIBRATION_BYTES, 'Lookalike calibration input');
425
517
  }
426
518
  catch (error) {
427
519
  if (error instanceof CliUsageError)
@@ -449,12 +541,7 @@ async function runParsedCli(args, dependencies = {}) {
449
541
  try {
450
542
  input = dependencies.readArtifactInput
451
543
  ? await dependencies.readArtifactInput(args.source)
452
- : await readSavedLookupInputBounded(args.source
453
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
454
- : dependencies.stdin || process.stdin, {
455
- limit: MAX_OFFLINE_ARTIFACT_BYTES,
456
- label: 'Artefact input',
457
- });
544
+ : await readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Artefact input');
458
545
  }
459
546
  catch (error) {
460
547
  if (error instanceof CliUsageError)
@@ -463,49 +550,59 @@ async function runParsedCli(args, dependencies = {}) {
463
550
  }
464
551
  if (!input.trim())
465
552
  throw new CliUsageError('verify-artifact requires one JSON file or an artefact on stdin.');
466
- let passphrase = null;
467
- if (args.passphraseSource) {
468
- let passphraseText;
553
+ const passphrase = args.passphraseSource ? await readPassphraseSource(args.passphraseSource) : null;
554
+ let manifest = null;
555
+ if (args.manifestSource && args.manifestEntryId) {
556
+ let raw;
469
557
  try {
470
- passphraseText = dependencies.readPassphraseFile
471
- ? await dependencies.readPassphraseFile(args.passphraseSource)
472
- : await readSavedLookupInputBounded(createReadStream(args.passphraseSource, { highWaterMark: MAX_OFFLINE_PASSPHRASE_FILE_BYTES }), {
473
- limit: MAX_OFFLINE_PASSPHRASE_FILE_BYTES,
474
- label: 'Passphrase file',
475
- });
558
+ raw = dependencies.readArtifactInput
559
+ ? await dependencies.readArtifactInput(args.manifestSource)
560
+ : await readInput(args.manifestSource, MAX_OFFLINE_ARTIFACT_BYTES, 'Investigation manifest input');
476
561
  }
477
562
  catch (error) {
478
563
  if (error instanceof CliUsageError)
479
564
  throw error;
480
- throw new CliUsageError(`Could not read passphrase file: ${boundedCliErrorMessage(error, 'File could not be read')}`);
481
- }
482
- passphrase = passphraseText.replace(/\r?\n$/u, '');
483
- if (!passphrase || /[\r\n\u0000]/u.test(passphrase)) {
484
- throw new CliUsageError('Passphrase file must contain exactly one non-empty UTF-8 line.');
565
+ throw new CliUsageError(`Could not read investigation manifest input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
485
566
  }
567
+ if (!raw.trim())
568
+ throw new CliUsageError('The investigation manifest input is empty.');
569
+ manifest = Object.freeze({ raw, entryId: args.manifestEntryId });
486
570
  }
487
- const report = await verifyOfflineArtifact(input, { passphrase });
571
+ const report = await verifyOfflineArtifact(input, { passphrase, manifest });
488
572
  if (!args.quiet) {
489
573
  write(stdout, args.output === 'json'
490
574
  ? formatJsonDocument(report)
491
575
  : terminal(formatOfflineArtifactVerification(report), args.color));
492
576
  }
493
- return EXIT_CODES.SUCCESS;
577
+ return args.strictExit && !isCompleteOfflineArtifactVerification(report)
578
+ ? EXIT_CODES.PARTIAL_FAILURE
579
+ : EXIT_CODES.SUCCESS;
494
580
  }
495
- if (isEvidenceCommand(args)) {
496
- failureLabel = evidenceCommandFailureLabel(args.action);
497
- const evidenceStdout = args.action !== 'sign-artifact' && args.output === 'terminal'
498
- ? { write: (value) => write(stdout, terminal(value, args.color)) }
499
- : stdout;
500
- return await runEvidenceCommand(args, {
501
- stdout: evidenceStdout,
502
- stdin: dependencies.stdin || process.stdin,
503
- readArtifactInput: dependencies.readArtifactInput,
504
- readPassphraseFile: dependencies.readPassphraseFile,
505
- readPrivateKeyFile: dependencies.readPrivateKeyFile,
506
- readPublicKeyFile: dependencies.readPublicKeyFile,
507
- now: dependencies.now,
581
+ if (args.action === 'interchange-report') {
582
+ failureLabel = 'Interchange fidelity report';
583
+ let input;
584
+ try {
585
+ input = dependencies.readArtifactInput
586
+ ? await dependencies.readArtifactInput(args.source)
587
+ : await readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Interchange input');
588
+ }
589
+ catch (error) {
590
+ if (error instanceof CliUsageError)
591
+ throw error;
592
+ throw new CliUsageError(`Could not read interchange input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
593
+ }
594
+ if (!input.trim())
595
+ throw new CliUsageError('interchange-report requires one JSON file or an artefact on stdin.');
596
+ const passphrase = args.passphraseSource ? await readPassphraseSource(args.passphraseSource) : null;
597
+ const report = await buildInterchangeFidelityReport(input, {
598
+ generatedAt: commandContext.now(),
599
+ passphrase,
508
600
  });
601
+ if (!args.quiet)
602
+ write(stdout, args.output === 'json'
603
+ ? formatJsonDocument(report)
604
+ : terminal(formatInterchangeFidelityReport(report), args.color));
605
+ return EXIT_CODES.SUCCESS;
509
606
  }
510
607
  if (args.action === 'source-report') {
511
608
  failureLabel = 'Source reliability report';
@@ -513,12 +610,7 @@ async function runParsedCli(args, dependencies = {}) {
513
610
  try {
514
611
  input = dependencies.readSourceReliabilityInput
515
612
  ? await dependencies.readSourceReliabilityInput(args.source)
516
- : await readSavedLookupInputBounded(args.source
517
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
518
- : dependencies.stdin || process.stdin, {
519
- limit: MAX_SOURCE_RELIABILITY_INPUT_BYTES,
520
- label: 'Source reliability input',
521
- });
613
+ : await readInput(args.source, MAX_SOURCE_RELIABILITY_INPUT_BYTES, 'Source reliability input');
522
614
  }
523
615
  catch (error) {
524
616
  if (error instanceof CliUsageError)
@@ -541,9 +633,7 @@ async function runParsedCli(args, dependencies = {}) {
541
633
  try {
542
634
  input = dependencies.readCompareInput
543
635
  ? await dependencies.readCompareInput(args.source)
544
- : await readCompareInputBounded(args.source
545
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
546
- : dependencies.stdin || process.stdin, MAX_COMPARE_INPUT_BYTES);
636
+ : await readInput(args.source, MAX_COMPARE_INPUT_BYTES, 'Comparison input');
547
637
  }
548
638
  catch (error) {
549
639
  if (error instanceof CliUsageError)
@@ -564,10 +654,8 @@ async function runParsedCli(args, dependencies = {}) {
564
654
  }
565
655
  if (args.action === 'page-compare') {
566
656
  failureLabel = 'Static page comparison';
567
- const readDiffInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
568
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
569
- label: 'Page comparison input',
570
- })));
657
+ const readDiffInput = dependencies.readDiffInput
658
+ || ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Page comparison input'));
571
659
  let leftInput;
572
660
  let rightInput;
573
661
  try {
@@ -594,12 +682,7 @@ async function runParsedCli(args, dependencies = {}) {
594
682
  try {
595
683
  input = dependencies.readMailReviewInput
596
684
  ? await dependencies.readMailReviewInput(args.source)
597
- : await readSavedLookupInputBounded(args.source
598
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
599
- : dependencies.stdin || process.stdin, {
600
- limit: MAX_MAIL_REVIEW_INPUT_BYTES,
601
- label: 'Mail review input',
602
- });
685
+ : await readInput(args.source, MAX_MAIL_REVIEW_INPUT_BYTES, 'Mail review input');
603
686
  }
604
687
  catch (error) {
605
688
  if (error instanceof CliUsageError)
@@ -619,12 +702,7 @@ async function runParsedCli(args, dependencies = {}) {
619
702
  try {
620
703
  input = dependencies.readArtifactInput
621
704
  ? await dependencies.readArtifactInput(args.source)
622
- : await readSavedLookupInputBounded(args.source
623
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
624
- : dependencies.stdin || process.stdin, {
625
- limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
626
- label: 'Offline evidence input',
627
- });
705
+ : await readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Offline evidence input');
628
706
  }
629
707
  catch (error) {
630
708
  if (error instanceof CliUsageError)
@@ -662,12 +740,7 @@ async function runParsedCli(args, dependencies = {}) {
662
740
  try {
663
741
  input = dependencies.readArtifactInput
664
742
  ? await dependencies.readArtifactInput(args.source)
665
- : await readSavedLookupInputBounded(args.source
666
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
667
- : dependencies.stdin || process.stdin, {
668
- limit: isBrief ? MAX_SAVED_LOOKUP_INPUT_BYTES : 4 * 1024 * 1024,
669
- label: isBrief ? 'Lookup brief input' : 'Case-pack input',
670
- });
743
+ : await readInput(args.source, isBrief ? MAX_SAVED_LOOKUP_INPUT_BYTES : 4 * 1024 * 1024, isBrief ? 'Lookup brief input' : 'Case-pack input');
671
744
  }
672
745
  catch (error) {
673
746
  if (error instanceof CliUsageError)
@@ -698,12 +771,7 @@ async function runParsedCli(args, dependencies = {}) {
698
771
  try {
699
772
  input = dependencies.readArtifactInput
700
773
  ? await dependencies.readArtifactInput(args.source)
701
- : await readSavedLookupInputBounded(args.source
702
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
703
- : dependencies.stdin || process.stdin, {
704
- limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
705
- label: 'Domain control input',
706
- });
774
+ : await readInput(args.source, MAX_DOMAIN_CONTROL_REVIEW_COMMAND_BYTES, 'Domain control input');
707
775
  }
708
776
  catch (error) {
709
777
  if (error instanceof CliUsageError)
@@ -712,11 +780,21 @@ async function runParsedCli(args, dependencies = {}) {
712
780
  }
713
781
  if (!input.trim())
714
782
  throw new CliUsageError('domain-control requires one JSON file or a document on stdin.');
783
+ const normalizedDomainControlInput = input.replace(/^\uFEFF/u, '');
715
784
  let parsed;
716
785
  try {
717
- parsed = JSON.parse(input);
786
+ scanBoundedJson(normalizedDomainControlInput, {
787
+ maximumDepth: MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH,
788
+ maximumKeys: MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS,
789
+ maximumValues: MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES,
790
+ });
791
+ parsed = JSON.parse(normalizedDomainControlInput);
718
792
  }
719
- catch {
793
+ catch (cause) {
794
+ const detail = cause instanceof Error ? cause.message : '';
795
+ if (detail.startsWith('Artefact JSON ')) {
796
+ throw new CliUsageError(`Domain control input ${detail.slice('Artefact JSON '.length)}`);
797
+ }
720
798
  throw new CliUsageError('Domain control input is not valid JSON.');
721
799
  }
722
800
  const schema = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
@@ -741,7 +819,11 @@ async function runParsedCli(args, dependencies = {}) {
741
819
  : formatDomainControlResult(document);
742
820
  if (!args.quiet)
743
821
  write(stdout, args.output === 'json'
744
- ? formatJsonDocument(document)
822
+ ? schema === DOMAIN_CONTROL_MANIFEST_INPUT_SCHEMA
823
+ ? serializeDomainControlManifest(document)
824
+ : schema === DOMAIN_CONTROL_FLIGHT_RECORDER_INPUT_SCHEMA
825
+ ? serializeDomainControlFlightRecorder(document)
826
+ : formatJsonDocument(document)
745
827
  : terminal(terminalDocument, args.color));
746
828
  return EXIT_CODES.SUCCESS;
747
829
  }
@@ -752,19 +834,11 @@ async function runParsedCli(args, dependencies = {}) {
752
834
  try {
753
835
  manifestInput = dependencies.readArtifactInput
754
836
  ? await dependencies.readArtifactInput(args.source)
755
- : await readSavedLookupInputBounded(args.source
756
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
757
- : dependencies.stdin || process.stdin, {
758
- limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
759
- label: 'Domain-control manifest input',
760
- });
837
+ : await readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Domain-control manifest input');
761
838
  if (args.previousSource) {
762
839
  previousInput = dependencies.readDiffInput
763
840
  ? await dependencies.readDiffInput(args.previousSource)
764
- : await readSavedLookupInputBounded(createReadStream(args.previousSource, { highWaterMark: 64 * 1024 }), {
765
- limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
766
- label: 'Prior monitor snapshot',
767
- });
841
+ : await readInput(args.previousSource, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Prior monitor snapshot');
768
842
  }
769
843
  }
770
844
  catch (error) {
@@ -807,12 +881,7 @@ async function runParsedCli(args, dependencies = {}) {
807
881
  try {
808
882
  input = dependencies.readArtifactInput
809
883
  ? await dependencies.readArtifactInput(args.source)
810
- : await readSavedLookupInputBounded(args.source
811
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
812
- : dependencies.stdin || process.stdin, {
813
- limit: MAX_ASSURANCE_INPUT_BYTES,
814
- label: 'Domain assurance input',
815
- });
884
+ : await readInput(args.source, MAX_ASSURANCE_INPUT_BYTES, 'Domain assurance input');
816
885
  }
817
886
  catch (error) {
818
887
  if (error instanceof CliUsageError)
@@ -823,10 +892,11 @@ async function runParsedCli(args, dependencies = {}) {
823
892
  throw new CliUsageError('assurance requires one versioned JSON file or a document on stdin.');
824
893
  let parsed;
825
894
  try {
895
+ scanBoundedJson(input);
826
896
  parsed = JSON.parse(input);
827
897
  }
828
898
  catch {
829
- throw new CliUsageError('Domain assurance input is not valid JSON.');
899
+ throw new CliUsageError('Domain assurance input is not valid bounded JSON without duplicate keys.');
830
900
  }
831
901
  let document;
832
902
  try {
@@ -847,12 +917,7 @@ async function runParsedCli(args, dependencies = {}) {
847
917
  try {
848
918
  input = dependencies.readArtifactInput
849
919
  ? await dependencies.readArtifactInput(args.source)
850
- : await readSavedLookupInputBounded(args.source
851
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
852
- : dependencies.stdin || process.stdin, {
853
- limit: MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES,
854
- label: 'Domain change packet input',
855
- });
920
+ : await readInput(args.source, MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, 'Domain change packet input');
856
921
  }
857
922
  catch (error) {
858
923
  if (error instanceof CliUsageError)
@@ -863,10 +928,11 @@ async function runParsedCli(args, dependencies = {}) {
863
928
  throw new CliUsageError('change-packet requires one versioned JSON file or a document on stdin.');
864
929
  let parsed;
865
930
  try {
931
+ scanBoundedJson(input);
866
932
  parsed = JSON.parse(input);
867
933
  }
868
934
  catch {
869
- throw new CliUsageError('Domain change packet input is not valid JSON.');
935
+ throw new CliUsageError('Domain change packet input is not valid bounded JSON without duplicate keys.');
870
936
  }
871
937
  let document;
872
938
  try {
@@ -887,12 +953,7 @@ async function runParsedCli(args, dependencies = {}) {
887
953
  try {
888
954
  input = dependencies.readArtifactInput
889
955
  ? await dependencies.readArtifactInput(args.source)
890
- : await readSavedLookupInputBounded(args.source
891
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
892
- : dependencies.stdin || process.stdin, {
893
- limit: MAX_SHARING_REVIEW_BYTES,
894
- label: 'Sharing review input',
895
- });
956
+ : await readInput(args.source, MAX_SHARING_REVIEW_BYTES, 'Sharing review input');
896
957
  }
897
958
  catch (error) {
898
959
  if (error instanceof CliUsageError)
@@ -923,6 +984,14 @@ async function runParsedCli(args, dependencies = {}) {
923
984
  }
924
985
  if (args.action === 'workflow-plan') {
925
986
  failureLabel = 'Investigation plan';
987
+ if ('discovery' in args) {
988
+ const catalogue = buildWorkflowRecipeCatalogue(args.discovery === 'explain' ? args.recipe : null);
989
+ if (!args.quiet)
990
+ write(stdout, args.output === 'json'
991
+ ? formatJsonDocument(catalogue)
992
+ : terminal(formatWorkflowRecipeCatalogue(catalogue), args.color));
993
+ return EXIT_CODES.SUCCESS;
994
+ }
926
995
  const document = buildInvestigationPlan(args.recipe, args.subject, commandContext.now());
927
996
  if (!args.quiet)
928
997
  write(stdout, args.output === 'json'
@@ -937,10 +1006,7 @@ async function runParsedCli(args, dependencies = {}) {
937
1006
  try {
938
1007
  resumeInput = dependencies.readDiffInput
939
1008
  ? await dependencies.readDiffInput(args.resumeSource)
940
- : await readSavedLookupInputBounded(createReadStream(args.resumeSource, { highWaterMark: 64 * 1024 }), {
941
- limit: 24 * 1024 * 1024,
942
- label: 'Investigation resume state',
943
- });
1009
+ : await readInput(args.resumeSource, MAX_INVESTIGATION_RUN_BYTES, 'Investigation resume state');
944
1010
  }
945
1011
  catch (error) {
946
1012
  if (error instanceof CliUsageError)
@@ -952,6 +1018,7 @@ async function runParsedCli(args, dependencies = {}) {
952
1018
  approveNetwork: args.approveNetwork,
953
1019
  resumeInput,
954
1020
  generatedAt: commandContext.now(),
1021
+ ...(dependencies.signal ? { signal: dependencies.signal } : {}),
955
1022
  execute: async (command, stepArguments) => {
956
1023
  const stepStdout = createBufferedOutput();
957
1024
  const stepStderr = createBufferedOutput();
@@ -970,11 +1037,9 @@ async function runParsedCli(args, dependencies = {}) {
970
1037
  return document.state === 'step_failed' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
971
1038
  }
972
1039
  if (args.action === 'diff') {
973
- failureLabel = 'Lookup evidence diff';
974
- const readDiffInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
975
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
976
- label: 'Lookup diff input',
977
- })));
1040
+ failureLabel = 'Retained artifact diff';
1041
+ const readDiffInput = dependencies.readDiffInput
1042
+ || ((source) => readInput(source, MAX_RETAINED_ARTIFACT_DIFF_BYTES, 'Retained diff input'));
978
1043
  let leftInput;
979
1044
  let rightInput;
980
1045
  try {
@@ -986,21 +1051,19 @@ async function runParsedCli(args, dependencies = {}) {
986
1051
  catch (error) {
987
1052
  if (error instanceof CliUsageError)
988
1053
  throw error;
989
- throw new CliUsageError(`Could not read Lookup diff input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
1054
+ throw new CliUsageError(`Could not read retained diff input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
990
1055
  }
991
- const document = buildCliLookupDiff(leftInput, rightInput, dependencies.now ? dependencies.now() : new Date().toISOString());
1056
+ const document = buildCliRetainedArtifactDiff(leftInput, rightInput, { leftSessionId: args.leftSessionId, rightSessionId: args.rightSessionId }, dependencies.now ? dependencies.now() : new Date().toISOString());
992
1057
  if (!args.quiet)
993
1058
  write(stdout, args.output === 'json'
994
1059
  ? formatJsonDocument(document)
995
- : terminal(formatCliLookupDiff(document), args.color));
1060
+ : terminal(formatCliRetainedArtifactDiff(document), args.color));
996
1061
  return EXIT_CODES.SUCCESS;
997
1062
  }
998
1063
  if (args.action === 'reconcile') {
999
1064
  failureLabel = 'Lookup observation reconciliation';
1000
- const readReconciliationInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
1001
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
1002
- label: 'Lookup reconciliation input',
1003
- })));
1065
+ const readReconciliationInput = dependencies.readDiffInput
1066
+ || ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Lookup reconciliation input'));
1004
1067
  const inputs = [];
1005
1068
  let totalBytes = 0;
1006
1069
  try {
@@ -1027,10 +1090,8 @@ async function runParsedCli(args, dependencies = {}) {
1027
1090
  }
1028
1091
  if (args.action === 'timeline') {
1029
1092
  failureLabel = 'Lookup observation timeline';
1030
- const readTimelineInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
1031
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
1032
- label: 'Lookup timeline input',
1033
- })));
1093
+ const readTimelineInput = dependencies.readDiffInput
1094
+ || ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Lookup timeline input'));
1034
1095
  const inputs = [];
1035
1096
  let totalBytes = 0;
1036
1097
  try {
@@ -1061,12 +1122,7 @@ async function runParsedCli(args, dependencies = {}) {
1061
1122
  try {
1062
1123
  input = dependencies.readExportInput
1063
1124
  ? await dependencies.readExportInput(args.source)
1064
- : await readSavedLookupInputBounded(args.source
1065
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
1066
- : dependencies.stdin || process.stdin, {
1067
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
1068
- label: 'Evidence export input',
1069
- });
1125
+ : await readInput(args.source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Evidence export input');
1070
1126
  }
1071
1127
  catch (error) {
1072
1128
  if (error instanceof CliUsageError)
@@ -1080,40 +1136,20 @@ async function runParsedCli(args, dependencies = {}) {
1080
1136
  const now = dependencies.now ? dependencies.now() : new Date().toISOString();
1081
1137
  const document = buildCliEvidenceExport(input, evidenceModule, now);
1082
1138
  const output = args.format === 'markdown'
1083
- ? formatLookupEvidenceMarkdown(document)
1139
+ ? formatLookupEvidenceMarkdown(document, { includeAttribution: args.includeAttribution })
1084
1140
  : args.format === 'html'
1085
- ? formatLookupEvidenceHtml(document)
1141
+ ? formatLookupEvidenceHtml(document, { includeAttribution: args.includeAttribution })
1086
1142
  : formatCliEvidenceExport(document, args.compact);
1087
1143
  write(stdout, output);
1088
1144
  return EXIT_CODES.SUCCESS;
1089
1145
  }
1090
- if (args.action === 'bulk') {
1091
- failureLabel = 'Bulk lookup';
1092
- const { runBulkCommand } = await import("./bulk-command-runner.mjs");
1093
- return await runBulkCommand(args, dependencies, commandContext);
1094
- }
1095
- if (args.action === 'discover') {
1096
- failureLabel = 'Candidate generation';
1097
- const { runDiscoveryCommand } = await import("./discovery-command-runner.mjs");
1098
- return await runDiscoveryCommand(args, dependencies, commandContext);
1099
- }
1100
- if (args.action === 'discover-scan') {
1101
- failureLabel = 'Candidate scan';
1102
- const { runDiscoveryScanCommand } = await import("./discovery-scan-command-runner.mjs");
1103
- return await runDiscoveryScanCommand(args, dependencies, commandContext);
1104
- }
1105
1146
  if (args.action === 'ct-intake') {
1106
1147
  failureLabel = 'Certificate event intake';
1107
1148
  let input;
1108
1149
  try {
1109
1150
  input = dependencies.readArtifactInput
1110
1151
  ? await dependencies.readArtifactInput(args.source)
1111
- : await readSavedLookupInputBounded(args.source
1112
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
1113
- : dependencies.stdin || process.stdin, {
1114
- limit: MAX_CT_EVENT_INPUT_BYTES,
1115
- label: 'Certificate event input',
1116
- });
1152
+ : await readInput(args.source, MAX_CT_EVENT_INPUT_BYTES, 'Certificate event input');
1117
1153
  }
1118
1154
  catch (error) {
1119
1155
  if (error instanceof CliUsageError)
@@ -1124,10 +1160,11 @@ async function runParsedCli(args, dependencies = {}) {
1124
1160
  throw new CliUsageError('ct-intake requires one versioned JSON file or a document on stdin.');
1125
1161
  let parsed;
1126
1162
  try {
1163
+ scanBoundedJson(input);
1127
1164
  parsed = JSON.parse(input);
1128
1165
  }
1129
1166
  catch {
1130
- throw new CliUsageError('Certificate event input is not valid JSON.');
1167
+ throw new CliUsageError('Certificate event input is not valid bounded JSON without duplicate keys.');
1131
1168
  }
1132
1169
  let document;
1133
1170
  try {
@@ -1142,22 +1179,7 @@ async function runParsedCli(args, dependencies = {}) {
1142
1179
  : terminal(formatCtEventFindings(document), args.color));
1143
1180
  return EXIT_CODES.SUCCESS;
1144
1181
  }
1145
- if (args.action === 'ct-search'
1146
- || args.action === 'posture'
1147
- || args.action === 'http'
1148
- || args.action === 'tls') {
1149
- failureLabel = args.action === 'ct-search'
1150
- ? 'Certificate Transparency search'
1151
- : args.action === 'posture'
1152
- ? 'Domain posture audit'
1153
- : args.action === 'http'
1154
- ? 'HTTP probe'
1155
- : 'TLS intelligence';
1156
- const { runNetworkCommand } = await import("./network-command-runner.mjs");
1157
- return await runNetworkCommand(args, dependencies, commandContext);
1158
- }
1159
- const { runLookupCommand } = await import("./lookup-command-runner.mjs");
1160
- return await runLookupCommand(args, dependencies, commandContext);
1182
+ throw new Error('No CLI execution route is registered for the parsed command.');
1161
1183
  }
1162
1184
  catch (error) {
1163
1185
  progress?.stop();
@@ -1185,8 +1207,41 @@ async function runParsedCli(args, dependencies = {}) {
1185
1207
  return EXIT_CODES.LOOKUP_FAILED;
1186
1208
  }
1187
1209
  }
1188
- async function runCli(argv, dependencies = {}) {
1210
+ async function runCliCommand(argv, dependencies = {}) {
1211
+ const stdout = dependencies.stdout || process.stdout;
1189
1212
  const stderr = dependencies.stderr || process.stderr;
1213
+ const environment = dependencies.environment || process.env;
1214
+ if (Array.isArray(argv) && argv.length === 0) {
1215
+ const input = (dependencies.stdin || process.stdin);
1216
+ const output = stdout;
1217
+ const supportsInteractiveLaunch = dependencies.canLaunchInteractiveCli || canLaunchInteractiveCli;
1218
+ if (supportsInteractiveLaunch(input, output, environment)) {
1219
+ const launch = dependencies.launchInteractiveCli || launchInteractiveCli;
1220
+ try {
1221
+ const launchedArgv = await launch({
1222
+ input,
1223
+ output,
1224
+ environment,
1225
+ ...(dependencies.signal ? { signal: dependencies.signal } : {}),
1226
+ });
1227
+ if (launchedArgv === null)
1228
+ return EXIT_CODES.SUCCESS;
1229
+ argv = launchedArgv;
1230
+ }
1231
+ catch (error) {
1232
+ if (isCancellation(error, dependencies.signal)) {
1233
+ write(stderr, 'Cancelled by analyst.\n');
1234
+ return EXIT_CODES.CANCELLED;
1235
+ }
1236
+ if (error instanceof CliUsageError) {
1237
+ write(stderr, `Usage error: ${boundedCliErrorMessage(error, 'Invalid interactive selection')}\n`);
1238
+ return EXIT_CODES.USAGE;
1239
+ }
1240
+ write(stderr, `CLI startup failed: ${boundedCliErrorMessage(error, 'Interactive launch failed')}\n`);
1241
+ return EXIT_CODES.INTERNAL_ERROR;
1242
+ }
1243
+ }
1244
+ }
1190
1245
  let args;
1191
1246
  try {
1192
1247
  args = parseCliArguments(argv);
@@ -1218,4 +1273,25 @@ async function runCli(argv, dependencies = {}) {
1218
1273
  return EXIT_CODES.LOOKUP_FAILED;
1219
1274
  }
1220
1275
  }
1221
- export { HELP, MAX_STDIN_BYTES, VERSION, readStdinBounded, runCli };
1276
+ async function runCli(argv, dependencies = {}) {
1277
+ const stderr = dependencies.stderr || process.stderr;
1278
+ try {
1279
+ return await runCliCommand(argv, dependencies);
1280
+ }
1281
+ finally {
1282
+ const cleanup = dependencies.cleanupPendingOutputFiles || cleanupPendingOutputFiles;
1283
+ try {
1284
+ const report = await cleanup();
1285
+ if (report.retainedPublished > 0) {
1286
+ write(stderr, `Output cleanup warning: Published output is intact, but ${report.retainedPublished} linked temporary output ${report.retainedPublished === 1 ? 'file remains' : 'files remain'} in the selected output directory.\n`);
1287
+ }
1288
+ if (report.retainedUnpublished > 0) {
1289
+ write(stderr, `Output cleanup warning: ${report.retainedUnpublished} unpublished temporary output ${report.retainedUnpublished === 1 ? 'file remains' : 'files remain'} in the selected output directory.\n`);
1290
+ }
1291
+ }
1292
+ catch {
1293
+ write(stderr, 'Output cleanup warning: Temporary output cleanup could not be verified.\n');
1294
+ }
1295
+ }
1296
+ }
1297
+ export { HELP, INLINE_CLI_COMMANDS, MAX_STDIN_BYTES, VERSION, readStdinBounded, runCli };