@slicedearth/whoisleuth-cli 1.45.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 (133) hide show
  1. package/DISCLOSURE +7 -3
  2. package/NOTICE +5 -4
  3. package/README.md +59 -6
  4. package/bin/whoisleuth.mjs +5 -3
  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 +2 -3
  10. package/cli/bulk-command-runner.mjs +2 -5
  11. package/cli/case-pack.mjs +819 -34
  12. package/cli/command-reference.mjs +48 -24
  13. package/cli/completion.mjs +16 -8
  14. package/cli/ct-event-intake.mjs +32 -2
  15. package/cli/discovery-scan-command-runner.mjs +4 -5
  16. package/cli/discovery-workflow.mjs +2 -3
  17. package/cli/doctor.mjs +2 -2
  18. package/cli/domain-control-monitor.mjs +21 -15
  19. package/cli/domain-control-observations.mjs +25 -2
  20. package/cli/evidence-command-runner.mjs +17 -11
  21. package/cli/evidence-signing.mjs +73 -45
  22. package/cli/export-evidence.mjs +11 -4
  23. package/cli/formatters/html.mjs +3 -3
  24. package/cli/formatters/terminal.mjs +3 -0
  25. package/cli/input.mjs +57 -0
  26. package/cli/interchange-report.mjs +226 -0
  27. package/cli/investigation-manifest.mjs +16 -17
  28. package/cli/investigation-run.mjs +71 -23
  29. package/cli/local-mmdb-review.mjs +45 -14
  30. package/cli/lookup-command-runner.mjs +2 -2
  31. package/cli/lookup-diff.mjs +9 -0
  32. package/cli/network-command-runner.mjs +78 -0
  33. package/cli/offline-evidence-review.mjs +28 -8
  34. package/cli/page-compare.mjs +8 -5
  35. package/cli/registry-cohort.mjs +385 -61
  36. package/cli/registry-support.mjs +2 -1
  37. package/cli/retained-artifact-diff.mjs +376 -0
  38. package/cli/risk-calibration.mjs +7 -5
  39. package/cli/runner.mjs +147 -185
  40. package/cli/saved-lookup.mjs +26 -3
  41. package/cli/sharing-review.mjs +12 -6
  42. package/docs/cli-reference.md +444 -70
  43. package/docs/cli.md +24 -5
  44. package/frontend/src/lib/analysis/acquisition-decision-packet.js +4 -4
  45. package/frontend/src/lib/analysis/artifact-integrity.js +42 -0
  46. package/frontend/src/lib/analysis/brand-profile-model.js +29 -5
  47. package/frontend/src/lib/analysis/bulk-domain-comparison.js +25 -5
  48. package/frontend/src/lib/analysis/bulk-mail-exposure.js +66 -15
  49. package/frontend/src/lib/analysis/bulk-retry-plan.js +1 -1
  50. package/frontend/src/lib/analysis/bulk-review-export.js +7 -6
  51. package/frontend/src/lib/analysis/bulk-review-model.js +3 -2
  52. package/frontend/src/lib/analysis/bulk-session-model.js +192 -26
  53. package/frontend/src/lib/analysis/bulk-sort.js +4 -0
  54. package/frontend/src/lib/analysis/case-brand-profile-references.js +85 -0
  55. package/frontend/src/lib/analysis/case-evidence-model.js +8 -0
  56. package/frontend/src/lib/analysis/case-investigation-branch-model.js +134 -0
  57. package/frontend/src/lib/analysis/case-migration-model.js +83 -31
  58. package/frontend/src/lib/analysis/case-record-contracts.js +4 -2
  59. package/frontend/src/lib/analysis/case-record-model.js +2 -0
  60. package/frontend/src/lib/analysis/case-record-operations.js +42 -12
  61. package/frontend/src/lib/analysis/case-report.js +39 -6
  62. package/frontend/src/lib/analysis/case-response-model.js +66 -7
  63. package/frontend/src/lib/analysis/case-response-packet.js +15 -5
  64. package/frontend/src/lib/analysis/comparison-ledger-bulk.js +366 -0
  65. package/frontend/src/lib/analysis/comparison-ledger-contract.js +241 -0
  66. package/frontend/src/lib/analysis/comparison-ledger-json.js +79 -0
  67. package/frontend/src/lib/analysis/comparison-ledger-serialization.js +51 -0
  68. package/frontend/src/lib/analysis/domain-control-manifest-core.js +145 -0
  69. package/frontend/src/lib/analysis/domain-control-records.js +88 -0
  70. package/frontend/src/lib/analysis/external-findings-import.js +70 -6
  71. package/frontend/src/lib/analysis/investigation-capsule.js +47 -13
  72. package/frontend/src/lib/analysis/investigation-guide.js +52 -10
  73. package/frontend/src/lib/analysis/investigation-template-model.js +9 -6
  74. package/frontend/src/lib/analysis/lookup-claim-passport.js +236 -0
  75. package/frontend/src/lib/analysis/lookup-claim-readiness.js +280 -0
  76. package/frontend/src/lib/analysis/lookup-decision-support.js +61 -6
  77. package/frontend/src/lib/analysis/lookup-presentation.js +62 -0
  78. package/frontend/src/lib/analysis/opaque-reference-id.js +9 -0
  79. package/frontend/src/lib/analysis/utils.js +26 -3
  80. package/frontend/src/lib/analysis/visualization-matrix.js +71 -28
  81. package/frontend/src/lib/analysis/visualization-models.js +63 -94
  82. package/frontend/src/lib/analysis/visualization-time-series.js +53 -12
  83. package/frontend/src/lib/analysis/website-snapshot-model.js +6 -0
  84. package/frontend/src/lib/analysis/workspace-archive.js +104 -14
  85. package/lib/application-version.mjs +3 -0
  86. package/lib/availability.mjs +14 -12
  87. package/lib/bounded-file.mjs +13 -0
  88. package/lib/bounded-json-response.mjs +17 -4
  89. package/lib/bounded-json.mjs +249 -0
  90. package/lib/certificate-extension-profile.mjs +2 -1
  91. package/lib/cryptographic-assurance.mjs +155 -0
  92. package/lib/ct-search.mjs +87 -47
  93. package/lib/dns-intelligence.mjs +3 -2
  94. package/lib/dns-resolver-selection.mjs +6 -4
  95. package/lib/dnssec-chain-validation.mjs +1053 -0
  96. package/lib/dnssec-evidence-validation.mjs +10 -3
  97. package/lib/domain-change-packet.mjs +7 -5
  98. package/lib/domain-control-manifest.mjs +17 -129
  99. package/lib/domain-posture.mjs +7 -4
  100. package/lib/evidence-export.mjs +751 -78
  101. package/lib/evidence-report-markdown.mjs +3 -0
  102. package/lib/evidence-report.mjs +5 -0
  103. package/lib/favicon.mjs +8 -5
  104. package/lib/http-evidence-bounds.mjs +3 -0
  105. package/lib/http-intelligence.mjs +2 -5
  106. package/lib/interchange-fidelity-registry.mjs +122 -0
  107. package/lib/local-geoip-evidence.mjs +2 -1
  108. package/lib/lookup-network-evidence-bounds.mjs +9 -0
  109. package/lib/lookup-response-contract.mjs +691 -3
  110. package/lib/lookup.mjs +2 -2
  111. package/lib/observation.mjs +7 -6
  112. package/lib/outbound-identity.mjs +2 -3
  113. package/lib/perceptual-hash.mjs +12 -4
  114. package/lib/portable-generator.mjs +21 -0
  115. package/lib/provider-policy-admission.mjs +9 -4
  116. package/lib/rdap-normalization.mjs +18 -5
  117. package/lib/rdap-registrar.mjs +5 -1
  118. package/lib/registry-capabilities.mjs +3 -0
  119. package/lib/registry-capability-catalogue.mjs +12 -2
  120. package/lib/registry-comparison.mjs +28 -11
  121. package/lib/risk-calibration-summary.mjs +338 -0
  122. package/lib/rpki-evidence.mjs +9 -8
  123. package/lib/safe-fetch.mjs +59 -10
  124. package/lib/security-txt.mjs +26 -3
  125. package/lib/service-binding-dns.mjs +6 -1
  126. package/lib/smtp-transport-review.mjs +784 -0
  127. package/lib/sslbl-certificates.generated.mjs +136 -134
  128. package/lib/threat-intelligence-runtime.mjs +3 -11
  129. package/lib/threat-intelligence-types.mjs +18 -1
  130. package/lib/tls-intelligence.mjs +11 -5
  131. package/lib/tlsa-evidence.mjs +37 -7
  132. package/package.json +1 -1
  133. 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,39 +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
63
  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();
64
+ const text = (await readCliTextInput(null, stream, {
65
+ maximumBytes: limit,
66
+ label: 'Standard input',
67
+ ...(signal ? { signal } : {}),
68
+ })).trim();
84
69
  const lines = text.split(/\r?\n/).map((line) => line.trim()).filter(Boolean);
85
70
  if (lines.length > 1)
86
71
  throw new CliUsageError('Single-value commands accept one stdin line. Use the bulk command for multiple inputs.');
@@ -148,6 +133,29 @@ async function runParsedCli(args, dependencies = {}) {
148
133
  const readSingleInput = async () => (dependencies.readStdin
149
134
  ? await dependencies.readStdin()
150
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
+ };
151
159
  const commandContext = Object.freeze({
152
160
  stdout,
153
161
  stderr,
@@ -155,6 +163,7 @@ async function runParsedCli(args, dependencies = {}) {
155
163
  writeStdout: (value) => write(stdout, value),
156
164
  writeStderr: (value) => write(stderr, value),
157
165
  readSingleInput,
166
+ readInput,
158
167
  now: () => dependencies.now ? dependencies.now() : new Date().toISOString(),
159
168
  beginProgress,
160
169
  endProgress,
@@ -202,11 +211,16 @@ async function runParsedCli(args, dependencies = {}) {
202
211
  if (args.action === 'manifest') {
203
212
  failureLabel = 'Investigation manifest';
204
213
  const artifacts = [];
214
+ let totalBytes = 0;
205
215
  try {
206
216
  for (const source of args.sources) {
207
217
  const content = dependencies.readDiffInput
208
218
  ? await dependencies.readDiffInput(source)
209
- : 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
+ }
210
224
  artifacts.push({ content });
211
225
  }
212
226
  }
@@ -239,12 +253,7 @@ async function runParsedCli(args, dependencies = {}) {
239
253
  try {
240
254
  input = dependencies.readArtifactInput
241
255
  ? 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
- });
256
+ : await readInput(args.source, MAX_EXTERNAL_OBSERVATION_MAPPING_BYTES, mapping ? 'External observation mapping input' : 'Open Asset Model bridge input');
248
257
  }
249
258
  catch (error) {
250
259
  if (error instanceof CliUsageError)
@@ -321,12 +330,7 @@ async function runParsedCli(args, dependencies = {}) {
321
330
  try {
322
331
  input = dependencies.readCompareInput
323
332
  ? 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
- });
333
+ : await readInput(args.source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Registry doctor input');
330
334
  }
331
335
  catch (error) {
332
336
  if (error instanceof CliUsageError)
@@ -348,12 +352,7 @@ async function runParsedCli(args, dependencies = {}) {
348
352
  try {
349
353
  input = dependencies.readCompareInput
350
354
  ? 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
- });
355
+ : await readInput(args.source, MAX_REGISTRY_COHORT_INPUT_BYTES, 'Registry cohort input');
357
356
  }
358
357
  catch (error) {
359
358
  if (error instanceof CliUsageError)
@@ -385,9 +384,7 @@ async function runParsedCli(args, dependencies = {}) {
385
384
  try {
386
385
  input = dependencies.readRiskCalibrationInput
387
386
  ? 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);
387
+ : await readInput(args.source, MAX_RISK_CALIBRATION_INPUT_BYTES, 'Risk calibration input');
391
388
  }
392
389
  catch (error) {
393
390
  if (error instanceof CliUsageError)
@@ -407,7 +404,11 @@ async function runParsedCli(args, dependencies = {}) {
407
404
  } : {}),
408
405
  });
409
406
  if (!args.quiet)
410
- 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));
411
412
  return EXIT_CODES.SUCCESS;
412
413
  }
413
414
  if (args.action === 'lookalike-calibrate') {
@@ -416,12 +417,7 @@ async function runParsedCli(args, dependencies = {}) {
416
417
  try {
417
418
  input = dependencies.readRiskCalibrationInput
418
419
  ? 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
- });
420
+ : await readInput(args.source, MAX_LOOKALIKE_CALIBRATION_BYTES, 'Lookalike calibration input');
425
421
  }
426
422
  catch (error) {
427
423
  if (error instanceof CliUsageError)
@@ -449,12 +445,7 @@ async function runParsedCli(args, dependencies = {}) {
449
445
  try {
450
446
  input = dependencies.readArtifactInput
451
447
  ? 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
- });
448
+ : await readInput(args.source, MAX_OFFLINE_ARTIFACT_BYTES, 'Artefact input');
458
449
  }
459
450
  catch (error) {
460
451
  if (error instanceof CliUsageError)
@@ -463,33 +454,58 @@ async function runParsedCli(args, dependencies = {}) {
463
454
  }
464
455
  if (!input.trim())
465
456
  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;
457
+ const passphrase = args.passphraseSource ? await readPassphraseSource(args.passphraseSource) : null;
458
+ let manifest = null;
459
+ if (args.manifestSource && args.manifestEntryId) {
460
+ let raw;
469
461
  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
- });
462
+ raw = dependencies.readArtifactInput
463
+ ? await dependencies.readArtifactInput(args.manifestSource)
464
+ : await readInput(args.manifestSource, MAX_OFFLINE_ARTIFACT_BYTES, 'Investigation manifest input');
476
465
  }
477
466
  catch (error) {
478
467
  if (error instanceof CliUsageError)
479
468
  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.');
469
+ throw new CliUsageError(`Could not read investigation manifest input: ${boundedCliErrorMessage(error, 'Input could not be read')}`);
485
470
  }
471
+ if (!raw.trim())
472
+ throw new CliUsageError('The investigation manifest input is empty.');
473
+ manifest = Object.freeze({ raw, entryId: args.manifestEntryId });
486
474
  }
487
- const report = await verifyOfflineArtifact(input, { passphrase });
475
+ const report = await verifyOfflineArtifact(input, { passphrase, manifest });
488
476
  if (!args.quiet) {
489
477
  write(stdout, args.output === 'json'
490
478
  ? formatJsonDocument(report)
491
479
  : terminal(formatOfflineArtifactVerification(report), args.color));
492
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));
493
509
  return EXIT_CODES.SUCCESS;
494
510
  }
495
511
  if (isEvidenceCommand(args)) {
@@ -505,6 +521,7 @@ async function runParsedCli(args, dependencies = {}) {
505
521
  readPrivateKeyFile: dependencies.readPrivateKeyFile,
506
522
  readPublicKeyFile: dependencies.readPublicKeyFile,
507
523
  now: dependencies.now,
524
+ signal: dependencies.signal,
508
525
  });
509
526
  }
510
527
  if (args.action === 'source-report') {
@@ -513,12 +530,7 @@ async function runParsedCli(args, dependencies = {}) {
513
530
  try {
514
531
  input = dependencies.readSourceReliabilityInput
515
532
  ? 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
- });
533
+ : await readInput(args.source, MAX_SOURCE_RELIABILITY_INPUT_BYTES, 'Source reliability input');
522
534
  }
523
535
  catch (error) {
524
536
  if (error instanceof CliUsageError)
@@ -541,9 +553,7 @@ async function runParsedCli(args, dependencies = {}) {
541
553
  try {
542
554
  input = dependencies.readCompareInput
543
555
  ? 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);
556
+ : await readInput(args.source, MAX_COMPARE_INPUT_BYTES, 'Comparison input');
547
557
  }
548
558
  catch (error) {
549
559
  if (error instanceof CliUsageError)
@@ -564,10 +574,8 @@ async function runParsedCli(args, dependencies = {}) {
564
574
  }
565
575
  if (args.action === 'page-compare') {
566
576
  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
- })));
577
+ const readDiffInput = dependencies.readDiffInput
578
+ || ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Page comparison input'));
571
579
  let leftInput;
572
580
  let rightInput;
573
581
  try {
@@ -594,12 +602,7 @@ async function runParsedCli(args, dependencies = {}) {
594
602
  try {
595
603
  input = dependencies.readMailReviewInput
596
604
  ? 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
- });
605
+ : await readInput(args.source, MAX_MAIL_REVIEW_INPUT_BYTES, 'Mail review input');
603
606
  }
604
607
  catch (error) {
605
608
  if (error instanceof CliUsageError)
@@ -619,12 +622,7 @@ async function runParsedCli(args, dependencies = {}) {
619
622
  try {
620
623
  input = dependencies.readArtifactInput
621
624
  ? 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
- });
625
+ : await readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Offline evidence input');
628
626
  }
629
627
  catch (error) {
630
628
  if (error instanceof CliUsageError)
@@ -662,12 +660,7 @@ async function runParsedCli(args, dependencies = {}) {
662
660
  try {
663
661
  input = dependencies.readArtifactInput
664
662
  ? 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
- });
663
+ : await readInput(args.source, isBrief ? MAX_SAVED_LOOKUP_INPUT_BYTES : 4 * 1024 * 1024, isBrief ? 'Lookup brief input' : 'Case-pack input');
671
664
  }
672
665
  catch (error) {
673
666
  if (error instanceof CliUsageError)
@@ -698,12 +691,7 @@ async function runParsedCli(args, dependencies = {}) {
698
691
  try {
699
692
  input = dependencies.readArtifactInput
700
693
  ? 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
- });
694
+ : await readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Domain control input');
707
695
  }
708
696
  catch (error) {
709
697
  if (error instanceof CliUsageError)
@@ -712,11 +700,21 @@ async function runParsedCli(args, dependencies = {}) {
712
700
  }
713
701
  if (!input.trim())
714
702
  throw new CliUsageError('domain-control requires one JSON file or a document on stdin.');
703
+ const normalizedDomainControlInput = input.replace(/^\uFEFF/u, '');
715
704
  let parsed;
716
705
  try {
717
- 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);
718
712
  }
719
- 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
+ }
720
718
  throw new CliUsageError('Domain control input is not valid JSON.');
721
719
  }
722
720
  const schema = parsed && typeof parsed === 'object' && !Array.isArray(parsed)
@@ -752,19 +750,11 @@ async function runParsedCli(args, dependencies = {}) {
752
750
  try {
753
751
  manifestInput = dependencies.readArtifactInput
754
752
  ? 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
- });
753
+ : await readInput(args.source, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Domain-control manifest input');
761
754
  if (args.previousSource) {
762
755
  previousInput = dependencies.readDiffInput
763
756
  ? 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
- });
757
+ : await readInput(args.previousSource, MAX_OFFLINE_EVIDENCE_INPUT_BYTES, 'Prior monitor snapshot');
768
758
  }
769
759
  }
770
760
  catch (error) {
@@ -807,12 +797,7 @@ async function runParsedCli(args, dependencies = {}) {
807
797
  try {
808
798
  input = dependencies.readArtifactInput
809
799
  ? 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
- });
800
+ : await readInput(args.source, MAX_ASSURANCE_INPUT_BYTES, 'Domain assurance input');
816
801
  }
817
802
  catch (error) {
818
803
  if (error instanceof CliUsageError)
@@ -847,12 +832,7 @@ async function runParsedCli(args, dependencies = {}) {
847
832
  try {
848
833
  input = dependencies.readArtifactInput
849
834
  ? 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
- });
835
+ : await readInput(args.source, MAX_DOMAIN_CHANGE_PACKET_INPUT_BYTES, 'Domain change packet input');
856
836
  }
857
837
  catch (error) {
858
838
  if (error instanceof CliUsageError)
@@ -887,12 +867,7 @@ async function runParsedCli(args, dependencies = {}) {
887
867
  try {
888
868
  input = dependencies.readArtifactInput
889
869
  ? 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
- });
870
+ : await readInput(args.source, MAX_SHARING_REVIEW_BYTES, 'Sharing review input');
896
871
  }
897
872
  catch (error) {
898
873
  if (error instanceof CliUsageError)
@@ -937,10 +912,7 @@ async function runParsedCli(args, dependencies = {}) {
937
912
  try {
938
913
  resumeInput = dependencies.readDiffInput
939
914
  ? 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
- });
915
+ : await readInput(args.resumeSource, MAX_INVESTIGATION_RUN_BYTES, 'Investigation resume state');
944
916
  }
945
917
  catch (error) {
946
918
  if (error instanceof CliUsageError)
@@ -970,11 +942,9 @@ async function runParsedCli(args, dependencies = {}) {
970
942
  return document.state === 'step_failed' ? EXIT_CODES.PARTIAL_FAILURE : EXIT_CODES.SUCCESS;
971
943
  }
972
944
  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
- })));
945
+ failureLabel = 'Retained artifact diff';
946
+ const readDiffInput = dependencies.readDiffInput
947
+ || ((source) => readInput(source, MAX_RETAINED_ARTIFACT_DIFF_BYTES, 'Retained diff input'));
978
948
  let leftInput;
979
949
  let rightInput;
980
950
  try {
@@ -986,21 +956,19 @@ async function runParsedCli(args, dependencies = {}) {
986
956
  catch (error) {
987
957
  if (error instanceof CliUsageError)
988
958
  throw error;
989
- 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')}`);
990
960
  }
991
- 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());
992
962
  if (!args.quiet)
993
963
  write(stdout, args.output === 'json'
994
964
  ? formatJsonDocument(document)
995
- : terminal(formatCliLookupDiff(document), args.color));
965
+ : terminal(formatCliRetainedArtifactDiff(document), args.color));
996
966
  return EXIT_CODES.SUCCESS;
997
967
  }
998
968
  if (args.action === 'reconcile') {
999
969
  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
- })));
970
+ const readReconciliationInput = dependencies.readDiffInput
971
+ || ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Lookup reconciliation input'));
1004
972
  const inputs = [];
1005
973
  let totalBytes = 0;
1006
974
  try {
@@ -1027,10 +995,8 @@ async function runParsedCli(args, dependencies = {}) {
1027
995
  }
1028
996
  if (args.action === 'timeline') {
1029
997
  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
- })));
998
+ const readTimelineInput = dependencies.readDiffInput
999
+ || ((source) => readInput(source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Lookup timeline input'));
1034
1000
  const inputs = [];
1035
1001
  let totalBytes = 0;
1036
1002
  try {
@@ -1061,12 +1027,7 @@ async function runParsedCli(args, dependencies = {}) {
1061
1027
  try {
1062
1028
  input = dependencies.readExportInput
1063
1029
  ? 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
- });
1030
+ : await readInput(args.source, MAX_SAVED_LOOKUP_INPUT_BYTES, 'Evidence export input');
1070
1031
  }
1071
1032
  catch (error) {
1072
1033
  if (error instanceof CliUsageError)
@@ -1080,9 +1041,9 @@ async function runParsedCli(args, dependencies = {}) {
1080
1041
  const now = dependencies.now ? dependencies.now() : new Date().toISOString();
1081
1042
  const document = buildCliEvidenceExport(input, evidenceModule, now);
1082
1043
  const output = args.format === 'markdown'
1083
- ? formatLookupEvidenceMarkdown(document)
1044
+ ? formatLookupEvidenceMarkdown(document, { includeAttribution: args.includeAttribution })
1084
1045
  : args.format === 'html'
1085
- ? formatLookupEvidenceHtml(document)
1046
+ ? formatLookupEvidenceHtml(document, { includeAttribution: args.includeAttribution })
1086
1047
  : formatCliEvidenceExport(document, args.compact);
1087
1048
  write(stdout, output);
1088
1049
  return EXIT_CODES.SUCCESS;
@@ -1108,12 +1069,7 @@ async function runParsedCli(args, dependencies = {}) {
1108
1069
  try {
1109
1070
  input = dependencies.readArtifactInput
1110
1071
  ? 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
- });
1072
+ : await readInput(args.source, MAX_CT_EVENT_INPUT_BYTES, 'Certificate event input');
1117
1073
  }
1118
1074
  catch (error) {
1119
1075
  if (error instanceof CliUsageError)
@@ -1145,14 +1101,20 @@ async function runParsedCli(args, dependencies = {}) {
1145
1101
  if (args.action === 'ct-search'
1146
1102
  || args.action === 'posture'
1147
1103
  || args.action === 'http'
1148
- || args.action === 'tls') {
1104
+ || args.action === 'tls'
1105
+ || args.action === 'dnssec-validate'
1106
+ || args.action === 'mail-transport') {
1149
1107
  failureLabel = args.action === 'ct-search'
1150
1108
  ? 'Certificate Transparency search'
1151
1109
  : args.action === 'posture'
1152
1110
  ? 'Domain posture audit'
1153
1111
  : args.action === 'http'
1154
1112
  ? 'HTTP probe'
1155
- : 'TLS intelligence';
1113
+ : args.action === 'tls'
1114
+ ? 'TLS intelligence'
1115
+ : args.action === 'dnssec-validate'
1116
+ ? 'DNSSEC chain validation'
1117
+ : 'Mail transport review';
1156
1118
  const { runNetworkCommand } = await import("./network-command-runner.mjs");
1157
1119
  return await runNetworkCommand(args, dependencies, commandContext);
1158
1120
  }