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.
This commit is contained in:
morluto committed 2026-10-24 07:28:09 +08:00
1 parent 231e458246
commit 9d7779d365
20 files changed
+229 -58

No files matched your search

+2 -3
View File
@@ -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.",
+4 -1
View File
@@ -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
+20 -9
View File
@@ -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
+4 -1
View File
@@ -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,
+4 -4
View File
@@ -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,
+4 -6
View File
@@ -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")
+5 -7
View File
@@ -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",
+2 -1
View File
@@ -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"),
+4 -3
View File
@@ -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,
+4 -3
View File
@@ -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,
+6 -7
View File
@@ -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",
+4 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+4 -3
View File
@@ -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,
+4 -1
View File
@@ -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,
+2 -1
View File
@@ -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,
+4 -4
View File
@@ -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()