@slicedearth/whoisleuth-cli 1.43.0 → 1.47.4

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 (153) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +67 -10
  4. package/bin/whoisleuth.mjs +22 -9
  5. package/cli/arguments.mjs +256 -9
  6. package/cli/artifact-structure.mjs +1833 -0
  7. package/cli/artifact-verify.mjs +242 -49
  8. package/cli/bounded-json.mjs +1 -0
  9. package/cli/bulk-checkpoint.mjs +67 -31
  10. package/cli/bulk-command-runner.mjs +16 -6
  11. package/cli/bulk.mjs +18 -7
  12. package/cli/case-pack.mjs +819 -34
  13. package/cli/command-reference.mjs +48 -24
  14. package/cli/completion.mjs +50 -11
  15. package/cli/config-profile.mjs +15 -12
  16. package/cli/ct-event-intake.mjs +32 -2
  17. package/cli/discovery-observation-snapshot.mjs +8 -14
  18. package/cli/discovery-scan-command-runner.mjs +18 -6
  19. package/cli/discovery-snapshot.mjs +8 -15
  20. package/cli/discovery-workflow.mjs +2 -3
  21. package/cli/doctor.mjs +2 -2
  22. package/cli/domain-control-monitor.mjs +21 -15
  23. package/cli/domain-control-observations.mjs +25 -2
  24. package/cli/errors.mjs +7 -1
  25. package/cli/evidence-command-runner.mjs +17 -11
  26. package/cli/evidence-signing.mjs +73 -45
  27. package/cli/export-evidence.mjs +11 -4
  28. package/cli/formatters/html.mjs +3 -3
  29. package/cli/formatters/json.mjs +12 -4
  30. package/cli/formatters/terminal.mjs +4 -1
  31. package/cli/http.mjs +2 -1
  32. package/cli/input.mjs +57 -0
  33. package/cli/interchange-report.mjs +226 -0
  34. package/cli/investigation-manifest.mjs +16 -17
  35. package/cli/investigation-run.mjs +71 -23
  36. package/cli/local-mmdb-review.mjs +45 -14
  37. package/cli/lookup-command-runner.mjs +2 -2
  38. package/cli/lookup-diff.mjs +9 -0
  39. package/cli/lookup-reconcile.mjs +1 -1
  40. package/cli/mail-review.mjs +5 -5
  41. package/cli/manual.mjs +1 -1
  42. package/cli/network-command-runner.mjs +78 -0
  43. package/cli/offline-evidence-review.mjs +28 -8
  44. package/cli/page-compare.mjs +8 -5
  45. package/cli/registry-cohort.mjs +385 -61
  46. package/cli/registry-fixture-scaffold.mjs +4 -3
  47. package/cli/registry-support.mjs +2 -1
  48. package/cli/retained-artifact-diff.mjs +376 -0
  49. package/cli/risk-calibration.mjs +7 -5
  50. package/cli/runner.mjs +150 -177
  51. package/cli/saved-lookup.mjs +26 -3
  52. package/cli/sharing-review.mjs +12 -6
  53. package/docs/cli-reference.md +467 -74
  54. package/docs/cli.md +25 -5
  55. package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
  56. package/frontend/src/lib/analysis/acquisition-due-diligence.js +1 -1
  57. package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
  58. package/frontend/src/lib/analysis/brand-profile-model.js +33 -7
  59. package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
  60. package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
  61. package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
  62. package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
  63. package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
  64. package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
  65. package/frontend/src/lib/analysis/bulk-sort.js +4 -0
  66. package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
  67. package/frontend/src/lib/analysis/case-evidence-model.js +9 -1
  68. package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
  69. package/frontend/src/lib/analysis/case-migration-model.js +83 -31
  70. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  71. package/frontend/src/lib/analysis/case-record-core.js +1 -1
  72. package/frontend/src/lib/analysis/case-record-model.js +2 -0
  73. package/frontend/src/lib/analysis/case-record-operations.js +43 -13
  74. package/frontend/src/lib/analysis/case-report.js +39 -6
  75. package/frontend/src/lib/analysis/case-response-model.js +66 -7
  76. package/frontend/src/lib/analysis/case-response-packet.js +15 -5
  77. package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
  78. package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
  79. package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
  80. package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
  81. package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
  82. package/frontend/src/lib/analysis/domain-control-records.js +88 -0
  83. package/frontend/src/lib/analysis/external-findings-import.js +70 -6
  84. package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
  85. package/frontend/src/lib/analysis/investigation-guide.js +52 -10
  86. package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
  87. package/frontend/src/lib/analysis/lookup-asset-graph.js +8 -8
  88. package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
  89. package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
  90. package/frontend/src/lib/analysis/lookup-decision-support.js +63 -8
  91. package/frontend/src/lib/analysis/lookup-presentation.js +70 -14
  92. package/frontend/src/lib/analysis/lookup-source-refresh.js +36 -63
  93. package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
  94. package/frontend/src/lib/analysis/utils.js +27 -4
  95. package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
  96. package/frontend/src/lib/analysis/visualization-models.js +63 -94
  97. package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
  98. package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
  99. package/frontend/src/lib/analysis/workspace-archive-crypto.js +1 -4
  100. package/frontend/src/lib/analysis/workspace-archive.js +105 -19
  101. package/lib/application-version.mjs +3 -0
  102. package/lib/availability.mjs +14 -12
  103. package/lib/bounded-file.mjs +82 -0
  104. package/lib/bounded-json-response.mjs +170 -0
  105. package/lib/bounded-json.mjs +249 -0
  106. package/lib/browser-library-profile.mjs +167 -38
  107. package/lib/certificate-extension-profile.mjs +2 -1
  108. package/lib/cryptographic-assurance.mjs +155 -0
  109. package/lib/ct-search.mjs +87 -47
  110. package/lib/dns-intelligence.mjs +3 -2
  111. package/lib/dns-resolver-selection.mjs +6 -4
  112. package/lib/dnssec-chain-validation.mjs +1053 -0
  113. package/lib/dnssec-evidence-validation.mjs +10 -3
  114. package/lib/domain-change-packet.mjs +7 -5
  115. package/lib/domain-control-manifest.mjs +17 -129
  116. package/lib/domain-posture.mjs +7 -4
  117. package/lib/evidence-export.mjs +751 -78
  118. package/lib/evidence-report-markdown.mjs +3 -0
  119. package/lib/evidence-report.mjs +5 -0
  120. package/lib/favicon.mjs +8 -5
  121. package/lib/http-evidence-bounds.mjs +3 -0
  122. package/lib/http-intelligence.mjs +2 -5
  123. package/lib/interchange-fidelity-registry.mjs +122 -0
  124. package/lib/local-geoip-evidence.mjs +2 -1
  125. package/lib/lookup-cache.mjs +5 -1
  126. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  127. package/lib/lookup-response-contract.mjs +691 -3
  128. package/lib/lookup.mjs +2 -2
  129. package/lib/observation.mjs +7 -6
  130. package/lib/outbound-identity.mjs +2 -3
  131. package/lib/perceptual-hash.mjs +12 -4
  132. package/lib/portable-generator.mjs +21 -0
  133. package/lib/provider-policy-admission.mjs +9 -4
  134. package/lib/rdap-normalization.mjs +18 -5
  135. package/lib/rdap-registrar.mjs +5 -1
  136. package/lib/rdap-transport.mjs +6 -6
  137. package/lib/registry-capabilities.mjs +3 -0
  138. package/lib/registry-capability-catalogue.mjs +12 -2
  139. package/lib/registry-comparison.mjs +28 -11
  140. package/lib/risk-calibration-summary.mjs +338 -0
  141. package/lib/rpki-evidence.mjs +9 -8
  142. package/lib/safe-fetch.mjs +96 -42
  143. package/lib/security-txt.mjs +26 -3
  144. package/lib/service-binding-dns.mjs +6 -1
  145. package/lib/smtp-transport-review.mjs +784 -0
  146. package/lib/sslbl-certificates.generated.mjs +136 -134
  147. package/lib/threat-intelligence-runtime.mjs +4 -13
  148. package/lib/threat-intelligence-types.mjs +18 -1
  149. package/lib/tls-intelligence.mjs +11 -5
  150. package/lib/tlsa-evidence.mjs +37 -7
  151. package/lib/website-technology.mjs +2 -6
  152. package/package.json +1 -1
  153. package/third-party-notices.txt +95 -5817
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 { buildRiskCalibrationSummaryReport } from "../lib/risk-calibration-summary.mjs";
7
8
  import { CLI_COMMANDS, parseCliArguments } from "./arguments.mjs";
8
9
  import { buildCliCommandCatalogue, formatCliCommandCatalogue } from "./command-catalogue.mjs";
9
10
  import { COMMAND_COLLECTION, COMMAND_DETAILS, COMMAND_USAGE, HELP, 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";
@@ -27,19 +28,20 @@ import { MAX_MAIL_REVIEW_INPUT_BYTES, buildCliMailReview, formatCliMailReview, }
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
30
  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";
31
+ 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
32
  import { buildCliLookupBrief, formatCliLookupBrief } from "./lookup-brief.mjs";
32
33
  import { buildCliCasePack, formatCliCasePack } from "./case-pack.mjs";
33
34
  import { formatDomainControlMonitor, runDomainControlMonitor } from "./domain-control-monitor.mjs";
34
35
  import { MAX_ASSURANCE_INPUT_BYTES, buildDomainAssurance, formatDomainAssurance, } from "../lib/domain-assurance.mjs";
35
36
  import { MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, buildDomainChangePacket, formatDomainChangePacket, } from "../lib/domain-change-packet.mjs";
36
37
  import { buildCliManual } from "./manual.mjs";
37
- import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, buildInvestigationManifest, formatInvestigationManifest, } from "./investigation-manifest.mjs";
38
+ import { MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES, buildInvestigationManifest, formatInvestigationManifest, } from "./investigation-manifest.mjs";
38
39
  import { MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, mapExternalObservations, formatExternalObservationMapping, } from "./external-observation-mapping.mjs";
39
40
  import { buildOpenAssetModelBridge, formatOpenAssetModelBridge, } from "./open-asset-model-bridge.mjs";
40
41
  import { MAX_CT_EVENT_INPUT_BYTES, buildCtEventFindings, formatCtEventFindings, } from "./ct-event-intake.mjs";
41
42
  import { buildInvestigationPlan, formatInvestigationPlan } from "./investigation-plan.mjs";
42
- import { formatInvestigationRun, runInvestigationRecipe } from "./investigation-run.mjs";
43
+ import { MAX_INVESTIGATION_RUN_BYTES, formatInvestigationRun, runInvestigationRecipe } from "./investigation-run.mjs";
44
+ import { readCliTextInput } from "./input.mjs";
43
45
  import { evaluateCliFailPolicies, formatFailPolicyNotice } from "./fail-policy.mjs";
44
46
  import { formatCliJunit } from "./ci-report.mjs";
45
47
  import { createBufferedOutput, writePrivateFile } from "./output-file.mjs";
@@ -48,28 +50,22 @@ import { buildRegistrySupportDocument } from "./registry-support.mjs";
48
50
  import { buildRegistryDoctorReport, formatRegistryDoctorReport } from "./registry-doctor.mjs";
49
51
  import { MAX_REGISTRY_COHORT_INPUT_BYTES, buildRegistryCohortReport, formatRegistryCohortReport, } from "./registry-cohort.mjs";
50
52
  import { buildRegistryFixtureScaffold } from "./registry-fixture-scaffold.mjs";
51
- import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, formatOfflineArtifactVerification, verifyOfflineArtifact, } from "./artifact-verify.mjs";
53
+ import { MAX_OFFLINE_ARTIFACT_BYTES, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, formatOfflineArtifactVerification, isCompleteOfflineArtifactVerification, verifyOfflineArtifact, } from "./artifact-verify.mjs";
54
+ import { buildInterchangeFidelityReport, formatInterchangeFidelityReport, } from "./interchange-report.mjs";
52
55
  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";
56
+ import { MAX_RISK_CALIBRATION_INPUT_BYTES, buildRiskCalibrationReport, parseRiskCalibrationDataset, } from "./risk-calibration.mjs";
54
57
  import { MAX_LOOKALIKE_CALIBRATION_BYTES, buildLookalikeCalibration, formatLookalikeCalibration, } from "./lookalike-calibration.mjs";
55
- import { MAX_SAVED_LOOKUP_INPUT_BYTES, readSavedLookupInputBounded } from "./saved-lookup.mjs";
58
+ import { MAX_SAVED_LOOKUP_INPUT_BYTES } from "./saved-lookup.mjs";
56
59
  import { presentTerminalOutput, terminalPresentation, } from "./terminal-presentation.mjs";
57
60
  const require = createRequire(import.meta.url);
58
61
  const { version: VERSION } = require('../package.json');
59
62
  const MAX_STDIN_BYTES = 4096;
60
- async function readStdinBounded(stream, limit = MAX_STDIN_BYTES) {
61
- if (!stream || stream.isTTY)
62
- return '';
63
- const chunks = [];
64
- let total = 0;
65
- for await (const chunk of stream) {
66
- const buffer = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
67
- total += buffer.length;
68
- if (total > limit)
69
- throw new CliUsageError(`Standard input is limited to ${limit} bytes.`);
70
- chunks.push(buffer);
71
- }
72
- const text = Buffer.concat(chunks).toString('utf8').trim();
63
+ async function readStdinBounded(stream, limit = MAX_STDIN_BYTES, signal) {
64
+ const text = (await readCliTextInput(null, stream, {
65
+ maximumBytes: limit,
66
+ label: 'Standard input',
67
+ ...(signal ? { signal } : {}),
68
+ })).trim();
73
69
  const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
74
70
  if (lines.length > 1)
75
71
  throw new CliUsageError('Single-value commands accept one stdin line. Use the bulk command for multiple inputs.');
@@ -136,7 +132,30 @@ async function runParsedCli(args, dependencies = {}) {
136
132
  };
137
133
  const readSingleInput = async () => (dependencies.readStdin
138
134
  ? await dependencies.readStdin()
139
- : await readStdinBounded(dependencies.stdin || process.stdin));
135
+ : await readStdinBounded(dependencies.stdin || process.stdin, MAX_STDIN_BYTES, dependencies.signal));
136
+ const readInput = async (source, maximumBytes, label) => (readCliTextInput(source, dependencies.stdin || process.stdin, {
137
+ maximumBytes,
138
+ label,
139
+ ...(dependencies.signal ? { signal: dependencies.signal } : {}),
140
+ }));
141
+ const readPassphraseSource = async (source) => {
142
+ let passphraseText;
143
+ try {
144
+ passphraseText = dependencies.readPassphraseFile
145
+ ? await dependencies.readPassphraseFile(source)
146
+ : await readInput(source, MAX_OFFLINE_PASSPHRASE_FILE_BYTES, 'Passphrase file');
147
+ }
148
+ catch (error) {
149
+ if (error instanceof CliUsageError)
150
+ throw error;
151
+ throw new CliUsageError(`Could not read passphrase file: ${boundedCliErrorMessage(error, 'File could not be read')}`);
152
+ }
153
+ const passphrase = passphraseText.replace(/\r?\n$/u, '');
154
+ if (!passphrase || /[\r\n\u0000]/u.test(passphrase)) {
155
+ throw new CliUsageError('Passphrase file must contain exactly one non-empty UTF-8 line.');
156
+ }
157
+ return passphrase;
158
+ };
140
159
  const commandContext = Object.freeze({
141
160
  stdout,
142
161
  stderr,
@@ -144,6 +163,7 @@ async function runParsedCli(args, dependencies = {}) {
144
163
  writeStdout: (value) => write(stdout, value),
145
164
  writeStderr: (value) => write(stderr, value),
146
165
  readSingleInput,
166
+ readInput,
147
167
  now: () => dependencies.now ? dependencies.now() : new Date().toISOString(),
148
168
  beginProgress,
149
169
  endProgress,
@@ -191,11 +211,16 @@ async function runParsedCli(args, dependencies = {}) {
191
211
  if (args.action === 'manifest') {
192
212
  failureLabel = 'Investigation manifest';
193
213
  const artifacts = [];
214
+ let totalBytes = 0;
194
215
  try {
195
216
  for (const source of args.sources) {
196
217
  const content = dependencies.readDiffInput
197
218
  ? await dependencies.readDiffInput(source)
198
- : await readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), { limit: MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, label: 'Manifest artefact input' });
219
+ : await readInput(source, MAX_INVESTIGATION_MANIFEST_ARTIFACT_BYTES, 'Manifest artefact input');
220
+ totalBytes += Buffer.byteLength(content, 'utf8');
221
+ if (totalBytes > MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES) {
222
+ throw new CliUsageError(`Manifest artefacts exceed the ${MAX_INVESTIGATION_MANIFEST_TOTAL_BYTES}-byte combined limit.`);
223
+ }
199
224
  artifacts.push({ content });
200
225
  }
201
226
  }
@@ -228,12 +253,7 @@ async function runParsedCli(args, dependencies = {}) {
228
253
  try {
229
254
  input = dependencies.readArtifactInput
230
255
  ? await dependencies.readArtifactInput(args.source)
231
- : await readSavedLookupInputBounded(args.source
232
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
233
- : dependencies.stdin || process.stdin, {
234
- limit: MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES,
235
- label: mapping ? 'External observation mapping input' : 'Open Asset Model bridge input',
236
- });
256
+ : await readInput(args.source, MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, mapping ? 'External observation mapping input' : 'Open Asset Model bridge input');
237
257
  }
238
258
  catch (error) {
239
259
  if (error instanceof CliUsageError)
@@ -289,7 +309,7 @@ async function runParsedCli(args, dependencies = {}) {
289
309
  }
290
310
  if (args.action === 'registry-support') {
291
311
  failureLabel = 'Registry support';
292
- const readInput = dependencies.readStdin || (() => readStdinBounded(dependencies.stdin || process.stdin));
312
+ const readInput = dependencies.readStdin || (() => readStdinBounded(dependencies.stdin || process.stdin, MAX_STDIN_BYTES, dependencies.signal));
293
313
  const requestedInput = args.target || await readInput();
294
314
  if (!requestedInput)
295
315
  throw new CliUsageError('registry-support requires one domain or suffix as an argument or on stdin.');
@@ -310,12 +330,7 @@ async function runParsedCli(args, dependencies = {}) {
310
330
  try {
311
331
  input = dependencies.readCompareInput
312
332
  ? await dependencies.readCompareInput(args.source)
313
- : await readSavedLookupInputBounded(args.source
314
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
315
- : dependencies.stdin || process.stdin, {
316
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
317
- label: 'Registry doctor input',
318
- });
333
+ : await readInput(args.source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Registry doctor input');
319
334
  }
320
335
  catch (error) {
321
336
  if (error instanceof CliUsageError)
@@ -337,12 +352,7 @@ async function runParsedCli(args, dependencies = {}) {
337
352
  try {
338
353
  input = dependencies.readCompareInput
339
354
  ? await dependencies.readCompareInput(args.source)
340
- : await readSavedLookupInputBounded(args.source
341
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
342
- : dependencies.stdin || process.stdin, {
343
- limit: MAX_REGISTRY_COHORT_INPUT_BYTES,
344
- label: 'Registry cohort input',
345
- });
355
+ : await readInput(args.source, MAX_REGISTRY_COHORT_INPUT_BYTES, 'Registry cohort input');
346
356
  }
347
357
  catch (error) {
348
358
  if (error instanceof CliUsageError)
@@ -374,9 +384,7 @@ async function runParsedCli(args, dependencies = {}) {
374
384
  try {
375
385
  input = dependencies.readRiskCalibrationInput
376
386
  ? await dependencies.readRiskCalibrationInput(args.source)
377
- : await readRiskCalibrationInputBounded(args.source
378
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
379
- : dependencies.stdin || process.stdin, MAX_RISK_CALIBRATION_INPUT_BYTES);
387
+ : await readInput(args.source, MAX_RISK_CALIBRATION_INPUT_BYTES, 'Risk calibration input');
380
388
  }
381
389
  catch (error) {
382
390
  if (error instanceof CliUsageError)
@@ -396,7 +404,11 @@ async function runParsedCli(args, dependencies = {}) {
396
404
  } : {}),
397
405
  });
398
406
  if (!args.quiet)
399
- write(stdout, args.output === 'json' ? formatJsonDocument(report) : terminal(formatTerminalRiskCalibration(report), args.color));
407
+ write(stdout, args.output === 'summary_json'
408
+ ? formatJsonDocument(buildRiskCalibrationSummaryReport(report))
409
+ : args.output === 'json'
410
+ ? formatJsonDocument(report)
411
+ : terminal(formatTerminalRiskCalibration(report), args.color));
400
412
  return EXIT_CODES.SUCCESS;
401
413
  }
402
414
  if (args.action === 'lookalike-calibrate') {
@@ -405,12 +417,7 @@ async function runParsedCli(args, dependencies = {}) {
405
417
  try {
406
418
  input = dependencies.readRiskCalibrationInput
407
419
  ? await dependencies.readRiskCalibrationInput(args.source)
408
- : await readSavedLookupInputBounded(args.source
409
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
410
- : dependencies.stdin || process.stdin, {
411
- limit: MAX_LOOKALIKE_CALIBRATION_BYTES,
412
- label: 'Lookalike calibration input',
413
- });
420
+ : await readInput(args.source, MAX_LOOKALIKE_CALIBRATION_BYTES, 'Lookalike calibration input');
414
421
  }
415
422
  catch (error) {
416
423
  if (error instanceof CliUsageError)
@@ -438,12 +445,7 @@ async function runParsedCli(args, dependencies = {}) {
438
445
  try {
439
446
  input = dependencies.readArtifactInput
440
447
  ? await dependencies.readArtifactInput(args.source)
441
- : await readSavedLookupInputBounded(args.source
442
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
443
- : dependencies.stdin || process.stdin, {
444
- limit: MAX_OFFLINE_ARTIFACT_BYTES,
445
- label: 'Artefact input',
446
- });
448
+ : await readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Artefact input');
447
449
  }
448
450
  catch (error) {
449
451
  if (error instanceof CliUsageError)
@@ -452,33 +454,58 @@ async function runParsedCli(args, dependencies = {}) {
452
454
  }
453
455
  if (!input.trim())
454
456
  throw new CliUsageError('verify-artifact requires one JSON file or an artefact on stdin.');
455
- let passphrase = null;
456
- if (args.passphraseSource) {
457
- let passphraseText;
457
+ const passphrase = args.passphraseSource ? await readPassphraseSource(args.passphraseSource) : null;
458
+ let manifest = null;
459
+ if (args.manifestSource && args.manifestEntryId) {
460
+ let raw;
458
461
  try {
459
- passphraseText = dependencies.readPassphraseFile
460
- ? await dependencies.readPassphraseFile(args.passphraseSource)
461
- : await readSavedLookupInputBounded(createReadStream(args.passphraseSource, { highWaterMark: MAX_OFFLINE_PASSPHRASE_FILE_BYTES }), {
462
- limit: MAX_OFFLINE_PASSPHRASE_FILE_BYTES,
463
- label: 'Passphrase file',
464
- });
462
+ raw = dependencies.readArtifactInput
463
+ ? await dependencies.readArtifactInput(args.manifestSource)
464
+ : await readInput(args.manifestSource, MAX_OFFLINE_ARTIFACT_BYTES, 'Investigation manifest input');
465
465
  }
466
466
  catch (error) {
467
467
  if (error instanceof CliUsageError)
468
468
  throw error;
469
- throw new CliUsageError(`Could not read passphrase file: ${boundedCliErrorMessage(error, 'File could not be read')}`);
470
- }
471
- passphrase = passphraseText.replace(/\r?\n$/u, '');
472
- if (!passphrase || /[\r\n\u0000]/u.test(passphrase)) {
473
- throw new CliUsageError('Passphrase file must contain exactly one non-empty UTF-8 line.');
469
+ throw new CliUsageError(`Could not read investigation manifest input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
474
470
  }
471
+ if (!raw.trim())
472
+ throw new CliUsageError('The investigation manifest input is empty.');
473
+ manifest = Object.freeze({ raw, entryId: args.manifestEntryId });
475
474
  }
476
- const report = await verifyOfflineArtifact(input, { passphrase });
475
+ const report = await verifyOfflineArtifact(input, { passphrase, manifest });
477
476
  if (!args.quiet) {
478
477
  write(stdout, args.output === 'json'
479
478
  ? formatJsonDocument(report)
480
479
  : terminal(formatOfflineArtifactVerification(report), args.color));
481
480
  }
481
+ return args.strictExit && !isCompleteOfflineArtifactVerification(report)
482
+ ? EXIT_CODES.PARTIAL_FAILURE
483
+ : EXIT_CODES.SUCCESS;
484
+ }
485
+ if (args.action === 'interchange-report') {
486
+ failureLabel = 'Interchange fidelity report';
487
+ let input;
488
+ try {
489
+ input = dependencies.readArtifactInput
490
+ ? await dependencies.readArtifactInput(args.source)
491
+ : await readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Interchange input');
492
+ }
493
+ catch (error) {
494
+ if (error instanceof CliUsageError)
495
+ throw error;
496
+ throw new CliUsageError(`Could not read interchange input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
497
+ }
498
+ if (!input.trim())
499
+ throw new CliUsageError('interchange-report requires one JSON file or an artefact on stdin.');
500
+ const passphrase = args.passphraseSource ? await readPassphraseSource(args.passphraseSource) : null;
501
+ const report = await buildInterchangeFidelityReport(input, {
502
+ generatedAt: commandContext.now(),
503
+ passphrase,
504
+ });
505
+ if (!args.quiet)
506
+ write(stdout, args.output === 'json'
507
+ ? formatJsonDocument(report)
508
+ : terminal(formatInterchangeFidelityReport(report), args.color));
482
509
  return EXIT_CODES.SUCCESS;
483
510
  }
484
511
  if (isEvidenceCommand(args)) {
@@ -494,6 +521,7 @@ async function runParsedCli(args, dependencies = {}) {
494
521
  readPrivateKeyFile: dependencies.readPrivateKeyFile,
495
522
  readPublicKeyFile: dependencies.readPublicKeyFile,
496
523
  now: dependencies.now,
524
+ signal: dependencies.signal,
497
525
  });
498
526
  }
499
527
  if (args.action === 'source-report') {
@@ -502,12 +530,7 @@ async function runParsedCli(args, dependencies = {}) {
502
530
  try {
503
531
  input = dependencies.readSourceReliabilityInput
504
532
  ? await dependencies.readSourceReliabilityInput(args.source)
505
- : await readSavedLookupInputBounded(args.source
506
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
507
- : dependencies.stdin || process.stdin, {
508
- limit: MAX_SOURCE_RELIABILITY_INPUT_BYTES,
509
- label: 'Source reliability input',
510
- });
533
+ : await readInput(args.source, MAX_SOURCE_RELIABILITY_INPUT_BYTES, 'Source reliability input');
511
534
  }
512
535
  catch (error) {
513
536
  if (error instanceof CliUsageError)
@@ -530,9 +553,7 @@ async function runParsedCli(args, dependencies = {}) {
530
553
  try {
531
554
  input = dependencies.readCompareInput
532
555
  ? await dependencies.readCompareInput(args.source)
533
- : await readCompareInputBounded(args.source
534
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
535
- : dependencies.stdin || process.stdin, MAX_COMPARE_INPUT_BYTES);
556
+ : await readInput(args.source, MAX_COMPARE_INPUT_BYTES, 'Comparison input');
536
557
  }
537
558
  catch (error) {
538
559
  if (error instanceof CliUsageError)
@@ -553,10 +574,8 @@ async function runParsedCli(args, dependencies = {}) {
553
574
  }
554
575
  if (args.action === 'page-compare') {
555
576
  failureLabel = 'Static page comparison';
556
- const readDiffInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
557
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
558
- label: 'Page comparison input',
559
- })));
577
+ const readDiffInput = dependencies.readDiffInput
578
+ || ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Page comparison input'));
560
579
  let leftInput;
561
580
  let rightInput;
562
581
  try {
@@ -583,12 +602,7 @@ async function runParsedCli(args, dependencies = {}) {
583
602
  try {
584
603
  input = dependencies.readMailReviewInput
585
604
  ? await dependencies.readMailReviewInput(args.source)
586
- : await readSavedLookupInputBounded(args.source
587
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
588
- : dependencies.stdin || process.stdin, {
589
- limit: MAX_MAIL_REVIEW_INPUT_BYTES,
590
- label: 'Mail review input',
591
- });
605
+ : await readInput(args.source, MAX_MAIL_REVIEW_INPUT_BYTES, 'Mail review input');
592
606
  }
593
607
  catch (error) {
594
608
  if (error instanceof CliUsageError)
@@ -608,12 +622,7 @@ async function runParsedCli(args, dependencies = {}) {
608
622
  try {
609
623
  input = dependencies.readArtifactInput
610
624
  ? await dependencies.readArtifactInput(args.source)
611
- : await readSavedLookupInputBounded(args.source
612
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
613
- : dependencies.stdin || process.stdin, {
614
- limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
615
- label: 'Offline evidence input',
616
- });
625
+ : await readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Offline evidence input');
617
626
  }
618
627
  catch (error) {
619
628
  if (error instanceof CliUsageError)
@@ -651,12 +660,7 @@ async function runParsedCli(args, dependencies = {}) {
651
660
  try {
652
661
  input = dependencies.readArtifactInput
653
662
  ? await dependencies.readArtifactInput(args.source)
654
- : await readSavedLookupInputBounded(args.source
655
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
656
- : dependencies.stdin || process.stdin, {
657
- limit: isBrief ? MAX_SAVED_LOOKUP_INPUT_BYTES : 4 * 1024 * 1024,
658
- label: isBrief ? 'Lookup brief input' : 'Case-pack input',
659
- });
663
+ : await readInput(args.source, isBrief ? MAX_SAVED_LOOKUP_INPUT_BYTES : 4 * 1024 * 1024, isBrief ? 'Lookup brief input' : 'Case-pack input');
660
664
  }
661
665
  catch (error) {
662
666
  if (error instanceof CliUsageError)
@@ -687,12 +691,7 @@ async function runParsedCli(args, dependencies = {}) {
687
691
  try {
688
692
  input = dependencies.readArtifactInput
689
693
  ? await dependencies.readArtifactInput(args.source)
690
- : await readSavedLookupInputBounded(args.source
691
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
692
- : dependencies.stdin || process.stdin, {
693
- limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
694
- label: 'Domain control input',
695
- });
694
+ : await readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Domain control input');
696
695
  }
697
696
  catch (error) {
698
697
  if (error instanceof CliUsageError)
@@ -701,11 +700,21 @@ async function runParsedCli(args, dependencies = {}) {
701
700
  }
702
701
  if (!input.trim())
703
702
  throw new CliUsageError('domain-control requires one JSON file or a document on stdin.');
703
+ const normalizedDomainControlInput = input.replace(/^\uFEFF/u, '');
704
704
  let parsed;
705
705
  try {
706
- parsed = JSON.parse(input);
706
+ scanBoundedJson(normalizedDomainControlInput, {
707
+ maximumDepth: MAX_DOMAIN_CONTROL_REVIEW_JSON_DEPTH,
708
+ maximumKeys: MAX_DOMAIN_CONTROL_REVIEW_JSON_KEYS,
709
+ maximumValues: MAX_DOMAIN_CONTROL_REVIEW_JSON_VALUES,
710
+ });
711
+ parsed = JSON.parse(normalizedDomainControlInput);
707
712
  }
708
- catch {
713
+ catch (cause) {
714
+ const detail = cause instanceof Error ? cause.message : '';
715
+ if (detail.startsWith('Artefact JSON ')) {
716
+ throw new CliUsageError(`Domain control input ${detail.slice('Artefact JSON '.length)}`);
717
+ }
709
718
  throw new CliUsageError('Domain control input is not valid JSON.');
710
719
  }
711
720
  const schema = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
@@ -741,19 +750,11 @@ async function runParsedCli(args, dependencies = {}) {
741
750
  try {
742
751
  manifestInput = dependencies.readArtifactInput
743
752
  ? await dependencies.readArtifactInput(args.source)
744
- : await readSavedLookupInputBounded(args.source
745
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
746
- : dependencies.stdin || process.stdin, {
747
- limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
748
- label: 'Domain-control manifest input',
749
- });
753
+ : await readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Domain-control manifest input');
750
754
  if (args.previousSource) {
751
755
  previousInput = dependencies.readDiffInput
752
756
  ? await dependencies.readDiffInput(args.previousSource)
753
- : await readSavedLookupInputBounded(createReadStream(args.previousSource, { highWaterMark: 64 * 1024 }), {
754
- limit: MAX_OFFLINE_EVIDENCE_INPUT_BYTES,
755
- label: 'Prior monitor snapshot',
756
- });
757
+ : await readInput(args.previousSource, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Prior monitor snapshot');
757
758
  }
758
759
  }
759
760
  catch (error) {
@@ -796,12 +797,7 @@ async function runParsedCli(args, dependencies = {}) {
796
797
  try {
797
798
  input = dependencies.readArtifactInput
798
799
  ? await dependencies.readArtifactInput(args.source)
799
- : await readSavedLookupInputBounded(args.source
800
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
801
- : dependencies.stdin || process.stdin, {
802
- limit: MAX_ASSURANCE_INPUT_BYTES,
803
- label: 'Domain assurance input',
804
- });
800
+ : await readInput(args.source, MAX_ASSURANCE_INPUT_BYTES, 'Domain assurance input');
805
801
  }
806
802
  catch (error) {
807
803
  if (error instanceof CliUsageError)
@@ -836,12 +832,7 @@ async function runParsedCli(args, dependencies = {}) {
836
832
  try {
837
833
  input = dependencies.readArtifactInput
838
834
  ? await dependencies.readArtifactInput(args.source)
839
- : await readSavedLookupInputBounded(args.source
840
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
841
- : dependencies.stdin || process.stdin, {
842
- limit: MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES,
843
- label: 'Domain change packet input',
844
- });
835
+ : await readInput(args.source, MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, 'Domain change packet input');
845
836
  }
846
837
  catch (error) {
847
838
  if (error instanceof CliUsageError)
@@ -876,12 +867,7 @@ async function runParsedCli(args, dependencies = {}) {
876
867
  try {
877
868
  input = dependencies.readArtifactInput
878
869
  ? await dependencies.readArtifactInput(args.source)
879
- : await readSavedLookupInputBounded(args.source
880
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
881
- : dependencies.stdin || process.stdin, {
882
- limit: MAX_SHARING_REVIEW_BYTES,
883
- label: 'Sharing review input',
884
- });
870
+ : await readInput(args.source, MAX_SHARING_REVIEW_BYTES, 'Sharing review input');
885
871
  }
886
872
  catch (error) {
887
873
  if (error instanceof CliUsageError)
@@ -926,10 +912,7 @@ async function runParsedCli(args, dependencies = {}) {
926
912
  try {
927
913
  resumeInput = dependencies.readDiffInput
928
914
  ? await dependencies.readDiffInput(args.resumeSource)
929
- : await readSavedLookupInputBounded(createReadStream(args.resumeSource, { highWaterMark: 64 * 1024 }), {
930
- limit: 24 * 1024 * 1024,
931
- label: 'Investigation resume state',
932
- });
915
+ : await readInput(args.resumeSource, MAX_INVESTIGATION_RUN_BYTES, 'Investigation resume state');
933
916
  }
934
917
  catch (error) {
935
918
  if (error instanceof CliUsageError)
@@ -959,11 +942,9 @@ async function runParsedCli(args, dependencies = {}) {
959
942
  return document.state === 'step_failed' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
960
943
  }
961
944
  if (args.action === 'diff') {
962
- failureLabel = 'Lookup evidence diff';
963
- const readDiffInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
964
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
965
- label: 'Lookup diff input',
966
- })));
945
+ failureLabel = 'Retained artifact diff';
946
+ const readDiffInput = dependencies.readDiffInput
947
+ || ((source) => readInput(source, MAX_RETAINED_ARTIFACT_DIFF_BYTES, 'Retained diff input'));
967
948
  let leftInput;
968
949
  let rightInput;
969
950
  try {
@@ -975,21 +956,19 @@ async function runParsedCli(args, dependencies = {}) {
975
956
  catch (error) {
976
957
  if (error instanceof CliUsageError)
977
958
  throw error;
978
- throw new CliUsageError(`Could not read Lookup diff input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
959
+ throw new CliUsageError(`Could not read retained diff input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
979
960
  }
980
- const document = buildCliLookupDiff(leftInput, rightInput, dependencies.now ? dependencies.now() : new Date().toISOString());
961
+ const document = buildCliRetainedArtifactDiff(leftInput, rightInput, { leftSessionId: args.leftSessionId, rightSessionId: args.rightSessionId }, dependencies.now ? dependencies.now() : new Date().toISOString());
981
962
  if (!args.quiet)
982
963
  write(stdout, args.output === 'json'
983
964
  ? formatJsonDocument(document)
984
- : terminal(formatCliLookupDiff(document), args.color));
965
+ : terminal(formatCliRetainedArtifactDiff(document), args.color));
985
966
  return EXIT_CODES.SUCCESS;
986
967
  }
987
968
  if (args.action === 'reconcile') {
988
969
  failureLabel = 'Lookup observation reconciliation';
989
- const readReconciliationInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
990
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
991
- label: 'Lookup reconciliation input',
992
- })));
970
+ const readReconciliationInput = dependencies.readDiffInput
971
+ || ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Lookup reconciliation input'));
993
972
  const inputs = [];
994
973
  let totalBytes = 0;
995
974
  try {
@@ -1016,10 +995,8 @@ async function runParsedCli(args, dependencies = {}) {
1016
995
  }
1017
996
  if (args.action === 'timeline') {
1018
997
  failureLabel = 'Lookup observation timeline';
1019
- const readTimelineInput = dependencies.readDiffInput || (async (source) => (readSavedLookupInputBounded(createReadStream(source, { highWaterMark: 64 * 1024 }), {
1020
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
1021
- label: 'Lookup timeline input',
1022
- })));
998
+ const readTimelineInput = dependencies.readDiffInput
999
+ || ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Lookup timeline input'));
1023
1000
  const inputs = [];
1024
1001
  let totalBytes = 0;
1025
1002
  try {
@@ -1050,12 +1027,7 @@ async function runParsedCli(args, dependencies = {}) {
1050
1027
  try {
1051
1028
  input = dependencies.readExportInput
1052
1029
  ? await dependencies.readExportInput(args.source)
1053
- : await readSavedLookupInputBounded(args.source
1054
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
1055
- : dependencies.stdin || process.stdin, {
1056
- limit: MAX_SAVED_LOOKUP_INPUT_BYTES,
1057
- label: 'Evidence export input',
1058
- });
1030
+ : await readInput(args.source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Evidence export input');
1059
1031
  }
1060
1032
  catch (error) {
1061
1033
  if (error instanceof CliUsageError)
@@ -1069,9 +1041,9 @@ async function runParsedCli(args, dependencies = {}) {
1069
1041
  const now = dependencies.now ? dependencies.now() : new Date().toISOString();
1070
1042
  const document = buildCliEvidenceExport(input, evidenceModule, now);
1071
1043
  const output = args.format === 'markdown'
1072
- ? formatLookupEvidenceMarkdown(document)
1044
+ ? formatLookupEvidenceMarkdown(document, { includeAttribution: args.includeAttribution })
1073
1045
  : args.format === 'html'
1074
- ? formatLookupEvidenceHtml(document)
1046
+ ? formatLookupEvidenceHtml(document, { includeAttribution: args.includeAttribution })
1075
1047
  : formatCliEvidenceExport(document, args.compact);
1076
1048
  write(stdout, output);
1077
1049
  return EXIT_CODES.SUCCESS;
@@ -1097,12 +1069,7 @@ async function runParsedCli(args, dependencies = {}) {
1097
1069
  try {
1098
1070
  input = dependencies.readArtifactInput
1099
1071
  ? await dependencies.readArtifactInput(args.source)
1100
- : await readSavedLookupInputBounded(args.source
1101
- ? createReadStream(args.source, { highWaterMark: 64 * 1024 })
1102
- : dependencies.stdin || process.stdin, {
1103
- limit: MAX_CT_EVENT_INPUT_BYTES,
1104
- label: 'Certificate event input',
1105
- });
1072
+ : await readInput(args.source, MAX_CT_EVENT_INPUT_BYTES, 'Certificate event input');
1106
1073
  }
1107
1074
  catch (error) {
1108
1075
  if (error instanceof CliUsageError)
@@ -1134,14 +1101,20 @@ async function runParsedCli(args, dependencies = {}) {
1134
1101
  if (args.action === 'ct-search'
1135
1102
  || args.action === 'posture'
1136
1103
  || args.action === 'http'
1137
- || args.action === 'tls') {
1104
+ || args.action === 'tls'
1105
+ || args.action === 'dnssec-validate'
1106
+ || args.action === 'mail-transport') {
1138
1107
  failureLabel = args.action === 'ct-search'
1139
1108
  ? 'Certificate Transparency search'
1140
1109
  : args.action === 'posture'
1141
1110
  ? 'Domain posture audit'
1142
1111
  : args.action === 'http'
1143
1112
  ? 'HTTP probe'
1144
- : 'TLS intelligence';
1113
+ : args.action === 'tls'
1114
+ ? 'TLS intelligence'
1115
+ : args.action === 'dnssec-validate'
1116
+ ? 'DNSSEC chain validation'
1117
+ : 'Mail transport review';
1145
1118
  const { runNetworkCommand } = await import("./network-command-runner.mjs");
1146
1119
  return await runNetworkCommand(args, dependencies, commandContext);
1147
1120
  }