From 9d7779d36541628cd516bbef5d3d6f2cd7bbf8a5 Mon Sep 17 00:00:00 2001 From: morluto <76467478+morluto@users.noreply.github.com> Date: Sat, 24 Oct 2026 07:28:09 +0800 Subject: [PATCH] fix(cli): validate selected local paths before resolution Reuse the nonempty NUL-free local path contract before normalization in artifact commands, auxiliary inputs, and output destinations. Empty selections now fail at parsing instead of selecting the working directory; explicit relative paths preserve their target. Public CLI checks cover primary and auxiliary empty paths, exact relative-file digest, explicit current-directory selection, symlink parent semantics, and environment export inputs. Correct the desktop fixture to assert the existing CLI validation envelope. --- docs/cli.md | 5 + src/cli/analysisEvidenceDelivery.ts | 5 +- src/cli/apkSignatureCommands.ts | 5 +- src/cli/binaryDiagnosticsCommands.ts | 29 ++-- src/cli/captureCommands.ts | 5 +- src/cli/coreBinaryCommands.ts | 8 +- src/cli/decompileDirectoryCommand.ts | 10 +- src/cli/environmentCommands.ts | 12 +- src/cli/evmCommands.ts | 3 +- src/cli/goCommands.ts | 7 +- src/cli/gpuCommands.ts | 7 +- src/cli/il2cppCommands.ts | 13 +- src/cli/modelCommands.ts | 5 +- src/cli/pythonPycCommands.ts | 3 +- src/cli/rustCommands.ts | 7 +- src/cli/signalCommands.ts | 5 +- src/cli/sqliteCommands.ts | 3 +- src/cli/webNetworkCaptureCommands.ts | 8 +- .../acceptance/localPathAdmissionCli.test.ts | 141 ++++++++++++++++++ .../cli/desktopApplicationCli.test.ts | 6 +- 20 files changed, 229 insertions(+), 58 deletions(-) create mode 100644 tests/acceptance/localPathAdmissionCli.test.ts diff --git a/docs/cli.md b/docs/cli.md index 3eeac08b7c..1e1b58ba86 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -39,6 +39,11 @@ pass an absolute path. REA reads and authenticates the file, then projects it like retained Evidence, so a large file is bounded by the file reader rather than charged as inline input. +Local file inspection commands reject empty paths and NUL bytes before resolving +relative inputs, auxiliary files or output destinations. An empty string never +selects the working directory; use an explicit `.` when selecting that directory +is supported by the command. + For example, a 334 MB Obsidian application Evidence file yields a summary view of about 10 KB. Replace the view with `{"kind": "page", "collection": "modules", "offset": 0, "limit": 32}` for a diff --git a/src/cli/analysisEvidenceDelivery.ts b/src/cli/analysisEvidenceDelivery.ts index a661b277da..d4906ed7ee 100644 --- a/src/cli/analysisEvidenceDelivery.ts +++ b/src/cli/analysisEvidenceDelivery.ts @@ -10,6 +10,7 @@ import { AnalysisInputError } from "../domain/analysisErrorCore.js"; import { createEvidenceBundle } from "../domain/evidenceBundle.js"; import { publishEvidenceDelivery } from "../application/EvidenceBundleCommands.js"; import { inspectAnalysisViewValidated } from "../application/analysisView/AnalysisViewService.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; @@ -18,9 +19,7 @@ export const analysisEvidenceDeliveryOptions = { .enum(["complete", "summary"]) .default("complete") .describe("Deliver complete Evidence, or a summary with --evidence_output"), - evidence_output: z - .string() - .min(1) + evidence_output: localPathStringSchema .optional() .describe( "Save complete Evidence as a bundle; required for summary delivery. Never overwrites.", diff --git a/src/cli/apkSignatureCommands.ts b/src/cli/apkSignatureCommands.ts index 0b3896d927..d1894e42ee 100644 --- a/src/cli/apkSignatureCommands.ts +++ b/src/cli/apkSignatureCommands.ts @@ -8,6 +8,7 @@ import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js"; import { safeParseJson } from "../domain/safeJson.js"; import { logCliCommand } from "../cliLogging.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import type { CliInstance } from "./types.js"; import { withCommandCancellation } from "./commandCancellation.js"; @@ -25,7 +26,9 @@ export const registerApkSignatureCommands = ( cli.command(apkSignatureModule.cliCommand, { description: "Inspect declared APK signing schemes, signers and content digests", - args: z.object({ path: z.string().describe("Selected local APK/ZIP/AAB") }), + args: z.object({ + path: localPathStringSchema.describe("Selected local APK/ZIP/AAB"), + }), options: z.object({ ...analysisEvidenceDeliveryOptions, view: z diff --git a/src/cli/binaryDiagnosticsCommands.ts b/src/cli/binaryDiagnosticsCommands.ts index d5cf0e831d..91f0f6803d 100644 --- a/src/cli/binaryDiagnosticsCommands.ts +++ b/src/cli/binaryDiagnosticsCommands.ts @@ -22,6 +22,7 @@ import { inspectNativeExportsInputSchema } from "../domain/native/nativeExports. import { inspectElfNotesInputSchema } from "../domain/native/elfNotes.js"; import { projectAnalysisError } from "../domain/analysisErrorProjection.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { analysisEvidenceDeliveryOptions, @@ -37,7 +38,9 @@ export const registerBinaryDiagnosticsCommands = ( cli.command(CLI_COMMANDS.inspectElfNotes, { description: "Inspect ELF PT_NOTE and SHT_NOTE identities without executing the image", - args: z.object({ path: z.string().describe("Local ELF32/ELF64 image") }), + args: z.object({ + path: localPathStringSchema.describe("Local ELF32/ELF64 image"), + }), options: z.object({ maxFileBytes: inspectElfNotesInputSchema.shape.max_file_bytes, maxEntries: inspectElfNotesInputSchema.shape.max_entries, @@ -71,7 +74,7 @@ export const registerBinaryDiagnosticsCommands = ( cli.command(CLI_COMMANDS.inspectByteEntropy, { description: "Inspect file-wide and block byte entropy without executing the file", - args: z.object({ path: z.string().describe("Local file") }), + args: z.object({ path: localPathStringSchema.describe("Local file") }), options: z.object({ blockSize: inspectByteEntropyInputSchema.shape.block_size.describe( "Power-of-two block size from 256 to 65536 bytes (default 4096)", @@ -108,7 +111,9 @@ export const registerBinaryDiagnosticsCommands = ( cli.command(CLI_COMMANDS.inspectPeCertificates, { description: "Inspect PE Authenticode certificate table identities without verifying signatures", - args: z.object({ path: z.string().describe("Local PE32/PE32+ image") }), + args: z.object({ + path: localPathStringSchema.describe("Local PE32/PE32+ image"), + }), options: z.object({ maxFileBytes: inspectPeCertificatesInputSchema.shape.max_file_bytes, maxEntries: inspectPeCertificatesInputSchema.shape.max_entries, @@ -141,7 +146,9 @@ export const registerBinaryDiagnosticsCommands = ( cli.command(CLI_COMMANDS.inspectPeResources, { description: "Inspect PE resource identities, payload ranges, hashes and icon-group relationships", - args: z.object({ path: z.string().describe("Local PE32/PE32+ image") }), + args: z.object({ + path: localPathStringSchema.describe("Local PE32/PE32+ image"), + }), options: z.object({ maxFileBytes: inspectPeResourcesInputSchema.shape.max_file_bytes, maxEntries: inspectPeResourcesInputSchema.shape.max_entries, @@ -174,7 +181,9 @@ export const registerBinaryDiagnosticsCommands = ( cli.command(CLI_COMMANDS.inspectNativeExports, { description: "Inspect a PE or ELF image's export table without executing it", - args: z.object({ path: z.string().describe("Local PE or ELF image") }), + args: z.object({ + path: localPathStringSchema.describe("Local PE or ELF image"), + }), options: z.object({ maxFileBytes: inspectNativeExportsInputSchema.shape.max_file_bytes, ...analysisEvidenceDeliveryOptions, @@ -210,7 +219,9 @@ export const registerBinaryDiagnosticsCommands = ( description: "Inspect binary file/linked layout without executing the selected object", args: z.object({ - path: z.string().describe("Local ELF binary or relocatable object"), + path: localPathStringSchema.describe( + "Local ELF binary or relocatable object", + ), }), options: z.object({ pwntoolsTimeoutMs: @@ -237,9 +248,9 @@ export const registerBinaryDiagnosticsCommands = ( description: "Inspect supplied core registers/signals and optional debugger mapping evidence", args: z.object({ - path: z - .string() - .describe("Local Linux ELF64 amd64 or AArch64 little-endian core"), + path: localPathStringSchema.describe( + "Local Linux ELF64 amd64 or AArch64 little-endian core", + ), }), options: z.object({ debuggerContext: z diff --git a/src/cli/captureCommands.ts b/src/cli/captureCommands.ts index 66a4f27259..8ad700a415 100644 --- a/src/cli/captureCommands.ts +++ b/src/cli/captureCommands.ts @@ -13,6 +13,7 @@ import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; import { withCommandCancellation } from "./commandCancellation.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; export const registerCaptureCommands = ( cli: CliInstance, @@ -68,7 +69,9 @@ export const registerCaptureCommands = ( cli.command(CLI_COMMANDS.inspectPacketCapture, { description: "Inspect an existing packet capture with caller-supplied TShark", - args: z.object({ path: z.string().describe("Local packet capture") }), + args: z.object({ + path: localPathStringSchema.describe("Local packet capture"), + }), options: z .object({ ...packetCaptureOptionsSchema.shape, diff --git a/src/cli/coreBinaryCommands.ts b/src/cli/coreBinaryCommands.ts index c7be5c0cbf..e40e603534 100644 --- a/src/cli/coreBinaryCommands.ts +++ b/src/cli/coreBinaryCommands.ts @@ -22,6 +22,7 @@ import { runCliJavaScriptApplicationAnalysis } from "./javascriptApplicationAnal import { withCommandCancellation } from "./commandCancellation.js"; import { javascriptApplicationOptions } from "../cliObservationOptions.js"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { registerDecompileDirectoryCommand } from "./decompileDirectoryCommand.js"; @@ -84,10 +85,9 @@ const registerOverviewCommands = ( cli.command(CLI_COMMANDS.analyze, { description: "Get an overview of an app", args: z.object({ - path: z - .string() - .min(1) - .describe("App, program, or analysis database path"), + path: localPathStringSchema.describe( + "App, program, or analysis database path", + ), }), options: overviewOptions.extend({ integrityPolicy: laneIntegrityPolicy, diff --git a/src/cli/decompileDirectoryCommand.ts b/src/cli/decompileDirectoryCommand.ts index 2cf651f1cf..4e049512b2 100644 --- a/src/cli/decompileDirectoryCommand.ts +++ b/src/cli/decompileDirectoryCommand.ts @@ -9,6 +9,7 @@ import { nativeOpenOptionFields, } from "./options.js"; import type { CliInstance } from "./types.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import type { JsonValue } from "../domain/jsonValue.js"; @@ -25,12 +26,9 @@ export const registerDecompileDirectoryCommand = ( path: z.string().describe("App or program path"), }), options: z.object({ - output: z - .string() - .min(1) - .describe( - "Output directory. A relative path is resolved from the working directory. The parent must exist.", - ), + output: localPathStringSchema.describe( + "Output directory. A relative path is resolved from the working directory. The parent must exist.", + ), layout: z .enum(["address", "dwarf"]) .default("address") diff --git a/src/cli/environmentCommands.ts b/src/cli/environmentCommands.ts index ea3bbce52d..7b29c9c87b 100644 --- a/src/cli/environmentCommands.ts +++ b/src/cli/environmentCommands.ts @@ -1,4 +1,5 @@ import { basename } from "node:path"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import type { EnvironmentExportResult } from "../application/environment/environmentExport.js"; import type { EnvironmentSource } from "../application/environment/environmentSource.js"; @@ -393,16 +394,13 @@ const registerExportCommand = (cli: CliInstance, logger: Logger): void => { "Export an environment manifest and its capture Evidence as an agent environment bundle folder the agentenv framework runs with agent-env run", args: z.object({ manifest: manifestArgument, - directory: z - .string() - .describe( - "Bundle directory to create; must be absent or empty, nothing is overwritten", - ), + directory: localPathStringSchema.describe( + "Bundle directory to create; must be absent or empty, nothing is overwritten", + ), }), options: z.object({ evidence: evidenceOption, - packageTarball: z - .string() + packageTarball: localPathStringSchema .optional() .describe( "Packed rea-agents tarball (npm pack) copied into the env folder and installed by the Dockerfile instead of the registry package pinned to this version", diff --git a/src/cli/evmCommands.ts b/src/cli/evmCommands.ts index f5ac166cb6..e57970bf38 100644 --- a/src/cli/evmCommands.ts +++ b/src/cli/evmCommands.ts @@ -4,6 +4,7 @@ import { logCliCommand } from "../cliLogging.js"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; import { withCommandCancellation } from "./commandCancellation.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; import { inspectEvmInterfaceInputSchema } from "../domain/evm/evmInterface.js"; @@ -18,7 +19,7 @@ export const registerEvmCommands = ( cli.command(evmInterfaceModule.cliCommand, { description: "Infer interface candidates from explicit local EVM bytecode", args: z.object({ - path: z.string().describe("Local bytecode carrier"), + path: localPathStringSchema.describe("Local bytecode carrier"), encoding: z .enum(["raw", "hex"]) .describe("Selected carrier representation"), diff --git a/src/cli/goCommands.ts b/src/cli/goCommands.ts index a5adab8ae3..5536464afe 100644 --- a/src/cli/goCommands.ts +++ b/src/cli/goCommands.ts @@ -9,6 +9,7 @@ import { analysisEvidenceDeliveryOptions, deliverCliAnalysisEvidence, } from "./analysisEvidenceDelivery.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { machoSliceArchitectureSchema } from "../domain/apple/machoTargetSelection.js"; @@ -18,9 +19,9 @@ export const registerGoCommands = (cli: CliInstance, logger: Logger): void => { description: "Inspect embedded Go compiler, modules and build settings without target execution", args: z.object({ - path: z - .string() - .describe("Local ELF, PE, thin Mach-O or universal Mach-O image"), + path: localPathStringSchema.describe( + "Local ELF, PE, thin Mach-O or universal Mach-O image", + ), }), options: z.object({ ...analysisEvidenceDeliveryOptions, diff --git a/src/cli/gpuCommands.ts b/src/cli/gpuCommands.ts index 039bdfe99c..c5cb889dcf 100644 --- a/src/cli/gpuCommands.ts +++ b/src/cli/gpuCommands.ts @@ -6,6 +6,7 @@ import { safeParseJson } from "../domain/safeJson.js"; import { z } from "incur"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { createGpuCodeObjectsService } from "../composition/gpu.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; @@ -22,9 +23,9 @@ export const registerGpuCommands = (cli: CliInstance, logger: Logger): void => { description: "Inspect embedded CUDA/HIP targets and selected static kernel metadata", args: z.object({ - path: z - .string() - .describe("Local ELF/PE, CUDA fatbin, CUBIN, PTX or HIP bundle"), + path: localPathStringSchema.describe( + "Local ELF/PE, CUDA fatbin, CUBIN, PTX or HIP bundle", + ), }), options: z.object({ ...analysisEvidenceDeliveryOptions, diff --git a/src/cli/il2cppCommands.ts b/src/cli/il2cppCommands.ts index 2c88d21303..2af53da95f 100644 --- a/src/cli/il2cppCommands.ts +++ b/src/cli/il2cppCommands.ts @@ -1,3 +1,4 @@ +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { z } from "incur"; import { AnalysisInputError } from "../domain/analysisErrorCore.js"; @@ -22,14 +23,13 @@ export const registerIl2cppCommands = ( description: "Inventory IL2CPP global-metadata.dat names, owners and string literals, optionally linking method addresses from the native binary", args: z.object({ - path: z - .string() - .describe("Local global-metadata.dat (version 24, 27, 29, 31 or 39)"), + path: localPathStringSchema.describe( + "Local global-metadata.dat (version 24, 27, 29, 31 or 39)", + ), }), options: z.object({ ...analysisEvidenceDeliveryOptions, - binary: z - .string() + binary: localPathStringSchema .optional() .describe( "Matching native binary (Mach-O GameAssembly.dylib, PE GameAssembly.dll or ELF libil2cpp.so) to link method addresses; read only", @@ -38,8 +38,7 @@ export const registerIl2cppCommands = ( .enum(["arm64", "amd64"]) .optional() .describe("Slice of a universal Mach-O binary"), - ghidraSeeds: z - .string() + ghidraSeeds: localPathStringSchema .optional() .describe( "Also write REA_GHIDRA_SEED_FILE function seeds named Type$$Method for every linked method; needs --binary", diff --git a/src/cli/modelCommands.ts b/src/cli/modelCommands.ts index 0df6e94408..c120e88ea9 100644 --- a/src/cli/modelCommands.ts +++ b/src/cli/modelCommands.ts @@ -6,6 +6,7 @@ import { safeParseJson } from "../domain/safeJson.js"; import { z } from "incur"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { createModelArtifactService } from "../composition/model.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; @@ -25,7 +26,9 @@ export const registerModelCommands = ( description: "Inspect GGUF or safetensors headers without loading model weights", args: z.object({ - path: z.string().describe("Local model file, detected by content"), + path: localPathStringSchema.describe( + "Local model file, detected by content", + ), }), options: z.object({ ...analysisEvidenceDeliveryOptions, diff --git a/src/cli/pythonPycCommands.ts b/src/cli/pythonPycCommands.ts index f408eee3aa..c58f083d2a 100644 --- a/src/cli/pythonPycCommands.ts +++ b/src/cli/pythonPycCommands.ts @@ -1,4 +1,5 @@ import { z } from "incur"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { createPythonPycService } from "../composition/pythonPyc.js"; import { CLI_COMMANDS } from "../cliCommandNames.js"; @@ -18,7 +19,7 @@ export const registerPythonPycCommands = ( description: "Inspect a supported CPython 3.7–3.13 pyc header and marshal code objects without executing bytecode", args: z.object({ - path: z.string().describe("Local CPython .pyc file"), + path: localPathStringSchema.describe("Local CPython .pyc file"), }), options: z.object({ maxFileBytes: inspectPythonPycInputSchema.shape.max_file_bytes, diff --git a/src/cli/rustCommands.ts b/src/cli/rustCommands.ts index 62be1ab645..6840bb62b9 100644 --- a/src/cli/rustCommands.ts +++ b/src/cli/rustCommands.ts @@ -14,6 +14,7 @@ import { safeParseJson } from "../domain/safeJson.js"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; import { withCommandCancellation } from "./commandCancellation.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { machoSliceArchitectureSchema } from "../domain/apple/machoTargetSelection.js"; @@ -26,9 +27,9 @@ export const registerRustCommands = ( description: "Inspect an embedded cargo-auditable Rust dependency list without target execution", args: z.object({ - path: z - .string() - .describe("Local ELF, PE, thin Mach-O or universal Mach-O image"), + path: localPathStringSchema.describe( + "Local ELF, PE, thin Mach-O or universal Mach-O image", + ), }), options: z.object({ ...analysisEvidenceDeliveryOptions, diff --git a/src/cli/signalCommands.ts b/src/cli/signalCommands.ts index 2d48b7bd9d..1673d5734d 100644 --- a/src/cli/signalCommands.ts +++ b/src/cli/signalCommands.ts @@ -13,6 +13,7 @@ import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; import { withCommandCancellation } from "./commandCancellation.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; export const registerSignalCommands = ( cli: CliInstance, @@ -70,7 +71,9 @@ export const registerSignalCommands = ( cli.command(CLI_COMMANDS.decodeSignalRecording, { description: "Decode an existing signal recording with caller-supplied sigrok", - args: z.object({ path: z.string().describe("Local signal recording") }), + args: z.object({ + path: localPathStringSchema.describe("Local signal recording"), + }), options: z .object({ ...signalRecordingOptionsSchema.shape, diff --git a/src/cli/sqliteCommands.ts b/src/cli/sqliteCommands.ts index f1b8e464f0..2e69ba1aec 100644 --- a/src/cli/sqliteCommands.ts +++ b/src/cli/sqliteCommands.ts @@ -5,6 +5,7 @@ import { logCliCommand } from "../cliLogging.js"; import type { Logger } from "pino"; import type { CliInstance } from "./types.js"; import { withCommandCancellation } from "./commandCancellation.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { finishAnalysisCommand } from "./finishAnalysisCommand.js"; import { inspectSqliteDatabaseInputSchema } from "../domain/sqlite/sqliteDatabase.js"; @@ -20,7 +21,7 @@ export const registerSqliteCommands = ( description: "Inspect SQLite schema and optional ordinary-table rows from a local snapshot", args: z.object({ - path: z.string().describe("Local SQLite database snapshot"), + path: localPathStringSchema.describe("Local SQLite database snapshot"), }), options: z.object({ workerTimeoutMs: inspectSqliteDatabaseInputSchema.shape.worker_timeout_ms, diff --git a/src/cli/webNetworkCaptureCommands.ts b/src/cli/webNetworkCaptureCommands.ts index 659d9c023f..9f3ee1dc01 100644 --- a/src/cli/webNetworkCaptureCommands.ts +++ b/src/cli/webNetworkCaptureCommands.ts @@ -14,6 +14,7 @@ import { analysisEvidenceDeliveryOptions, deliverCliAnalysisEvidence, } from "./analysisEvidenceDelivery.js"; +import { localPathStringSchema } from "../domain/localPath.js"; import { absoluteCallerPath } from "../filesystem/callerPath.js"; import { explicitSensitiveValueOptions } from "./explicitSensitiveArguments.js"; @@ -27,7 +28,7 @@ export const registerWebNetworkCaptureCommands = ( description: "Inspect retained HAR, native mitmproxy, Fiddler SAZ or aioquic qlog records without fetching recorded URLs", args: z.object({ - capturePath: z.string().describe("Absolute local capture path"), + capturePath: localPathStringSchema.describe("Local capture path"), format: z .enum(WEB_NETWORK_CAPTURE_FORMATS) .describe("Producer capture format"), @@ -42,10 +43,9 @@ export const registerWebNetworkCaptureCommands = ( .array(z.string()) .optional() .describe("Exact body pointers; repeat to select several"), - protobufSchema: z - .string() + protobufSchema: localPathStringSchema .optional() - .describe("Absolute local compiled FileDescriptorSet path"), + .describe("Local compiled FileDescriptorSet path"), protobufMessage: z .string() .optional() diff --git a/tests/acceptance/localPathAdmissionCli.test.ts b/tests/acceptance/localPathAdmissionCli.test.ts new file mode 100644 index 0000000000..fcbfb6b468 --- /dev/null +++ b/tests/acceptance/localPathAdmissionCli.test.ts @@ -0,0 +1,141 @@ +import { createHash } from "node:crypto"; +import { writeFile } from "node:fs/promises"; +import { join } from "node:path"; +import { expect } from "vitest"; + +import { parseEvidence } from "../../src/domain/evidence.js"; +import { pythonPycFixture } from "../../src/python/PythonPyc.fixture.js"; +import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js"; +import { cliTest, runFileCli } from "../support/cli/cliFixture.js"; + +const fileCommands = [ + "inspect-sqlite-database", + "inspect-python-pyc", + "inspect-apk-signature", + "inspect-packet-capture", + "decode-signal-recording", + "inspect-model-artifact", + "inspect-elf-notes", + "inspect-byte-entropy", + "inspect-pe-resources", + "inspect-pe-certificates", + "inspect-native-exports", + "inspect-binary-layout", + "inspect-recorded-crash", + "inspect-go-binary", + "inspect-rust-binary", + "inspect-gpu-code-objects", + "inspect-il2cpp-metadata", +] as const; + +cliTest( + "rejects empty file selections before they can become the working directory", + async ({ cli }) => { + const cwd = await createTestTempDirectory("rea-empty-local-path-"); + const selections = [ + ...fileCommands.map((command) => ({ + arguments: [command, ""], + field: "path", + })), + { arguments: ["inspect-evm-interface", "", "hex"], field: "path" }, + { + arguments: ["inspect-web-network-capture", "", "har"], + field: "capturePath", + }, + ]; + for (const selection of selections) { + const result = await runFileCli(cli, { + arguments: [...selection.arguments, "--json"], + cwd, + }); + expect(result.exitCode, selection.arguments[0]).toBe(1); + expect(result.json, selection.arguments[0]).toMatchObject({ + code: "VALIDATION_ERROR", + fieldErrors: expect.arrayContaining([ + expect.objectContaining({ + path: selection.field, + code: "too_small", + missing: false, + }), + ]), + }); + } + }, +); + +cliTest( + "rejects empty auxiliary selections and output destinations before reading inputs", + async ({ cli }) => { + const cwd = await createTestTempDirectory("rea-empty-auxiliary-path-"); + const selections = [ + { + arguments: ["inspect-il2cpp-metadata", "unread", "--binary", ""], + field: "binary", + }, + { + arguments: ["inspect-il2cpp-metadata", "unread", "--ghidra-seeds", ""], + field: "ghidraSeeds", + }, + { + arguments: [ + "inspect-web-network-capture", + "unread", + "har", + "--protobuf-schema", + "", + ], + field: "protobufSchema", + }, + { + arguments: ["decompile-all", "unread", "--output", ""], + field: "output", + }, + { arguments: ["environment-export", "unread", ""], field: "directory" }, + { + arguments: [ + "environment-export", + "unread", + "output", + "--package-tarball", + "", + ], + field: "packageTarball", + }, + ]; + for (const selection of selections) { + const result = await runFileCli(cli, { + arguments: [...selection.arguments, "--json"], + cwd, + }); + expect(result.exitCode, selection.field).toBe(1); + expect(result.json, selection.field).toMatchObject({ + code: "VALIDATION_ERROR", + fieldErrors: expect.arrayContaining([ + expect.objectContaining({ path: selection.field, code: "too_small" }), + ]), + }); + } + }, +); + +cliTest( + "still inspects an explicit relative file with its exact digest", + async ({ cli }) => { + const cwd = await createTestTempDirectory("rea-relative-local-path-"); + const bytes = pythonPycFixture(); + const path = join(cwd, "selected.pyc"); + await writeFile(path, bytes); + const result = await runFileCli(cli, { + arguments: ["inspect-python-pyc", "./selected.pyc", "--json"], + cwd, + }); + expect(result.exitCode, result.stderr).toBe(0); + expect(parseEvidence(result.json)).toMatchObject({ + operation: "inspect_python_pyc", + subject: { + local_path: path, + digest: { sha256: createHash("sha256").update(bytes).digest("hex") }, + }, + }); + }, +); diff --git a/tests/boundary/cli/desktopApplicationCli.test.ts b/tests/boundary/cli/desktopApplicationCli.test.ts index 95d26ec561..92020943af 100644 --- a/tests/boundary/cli/desktopApplicationCli.test.ts +++ b/tests/boundary/cli/desktopApplicationCli.test.ts @@ -18,8 +18,10 @@ cliTest( }); expect(empty.exitCode).not.toBe(0); expect(empty.json).toMatchObject({ - code: "invalid_request", - category: "invalid_input", + code: "VALIDATION_ERROR", + fieldErrors: expect.arrayContaining([ + expect.objectContaining({ path: "path", code: "too_small" }), + ]), }); const explicit = await cli.run({ arguments: ["inspect-desktop-application", ".", "--json"],