fix(cli): reject empty native and managed targets before resolution (#7956)

Local file commands in the core binary, native, managed and native-tool
CLI groups declared their positional target as a plain string. An empty
argument therefore reached path admission, where it resolved to the
working directory, as docs/cli.md says it never should.

Declare those targets with the shared nonempty, NUL-free local path
schema already used by analyze, so the CLI rejects them as a
VALIDATION_ERROR naming the field before any analysis runner is called.
Explicit "." is unchanged.

Fixes #7436
Fixes #7404
This commit is contained in:
RRFZ authored and GitHub committed 2026-10-29 08:31:12 +08:00
1 parent 4671947c97
commit 6db024dc96
5 files changed
+174 -26

No files matched your search

+9 -7
View File
@@ -174,7 +174,9 @@ const registerOverviewCommands = (
cli.command(CLI_COMMANDS.inspect, {
description: "Inspect an app overview with evidence",
args: z.object({
path: z.string().describe("App, program, or analysis database path"),
path: localPathStringSchema.describe(
"App, program, or analysis database path",
),
}),
options: overviewOptions,
run: ({ args, options }) =>
@@ -197,7 +199,7 @@ const registerDecompileCommand = (
cli.command(CLI_COMMANDS.decompile, {
description: `Read one part of an app as code. ${decompileSelectorGuidance} Equal positional and --procedure values are accepted; conflicts fail.`,
args: z.object({
path: z.string().describe("App or program path"),
path: localPathStringSchema.describe("App or program path"),
address: z
.string()
.min(1)
@@ -290,7 +292,7 @@ const registerXrefsCommand = (
cli.command(CLI_COMMANDS.xrefs, {
description: `List bounded references to an analyzed address. ${namedAddressGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
path: localPathStringSchema.describe("App or program path"),
address: z
.string()
.min(1)
@@ -335,7 +337,7 @@ const registerTraceCommand = (
cli.command(CLI_COMMANDS.trace, {
description: `Trace a literal feature through analyzed references. ${querySelectorGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
path: localPathStringSchema.describe("App or program path"),
query: z
.string()
.min(1)
@@ -385,7 +387,7 @@ const registerFunctionCommand = (
cli.command(CLI_COMMANDS.function, {
description: `Analyze one complete function with evidence. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
path: localPathStringSchema.describe("App or program path"),
address: z
.string()
.min(1)
@@ -444,7 +446,7 @@ const registerInstructionsCommand = (
cli.command(CLI_COMMANDS.instructions, {
description: `Read every raw instruction without decompiling. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
path: localPathStringSchema.describe("App or program path"),
address: z
.string()
.min(1)
@@ -489,7 +491,7 @@ const registerSearchCommand = (
cli.command(CLI_COMMANDS.search, {
description: `Search every analyzed string or procedure name. ${patternSelectorGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
path: localPathStringSchema.describe("App or program path"),
pattern: z
.string()
.min(1)
+21 -10
View File
@@ -8,6 +8,7 @@ import {
MAX_BYTE_SEARCH_MATCHES,
} from "../domain/native/bytePattern.js";
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
import { localPathStringSchema } from "../domain/localPath.js";
import {
addressOrProcedureGuidance,
@@ -47,7 +48,7 @@ export const registerCoreNativeCommands = (
cli.command(CLI_COMMANDS.searchBytes, {
description: "Find masked byte patterns in initialized provider memory",
args: z.object({
path: z.string().describe("Local executable path"),
path: localPathStringSchema.describe("Local executable path"),
pattern: bytePatternFields.pattern,
}),
options: z.object({
@@ -103,7 +104,9 @@ export const registerCoreNativeCommands = (
cli.command(CLI_COMMANDS.inspectNativeLoadImage, {
description:
"Inspect native load observations and supported snapshot verification",
args: z.object({ path: z.string().describe("Local executable path") }),
args: z.object({
path: localPathStringSchema.describe("Local executable path"),
}),
options: z.object({
...analysisEvidenceDeliveryOptions,
...nativeOpenOptionFields,
@@ -133,7 +136,7 @@ export const registerCoreNativeCommands = (
cli.command(CLI_COMMANDS.readBytes, {
description: "Read exact provider memory bytes at one analysis address",
args: z.object({
path: z.string().describe("Local executable path"),
path: localPathStringSchema.describe("Local executable path"),
address: z.string().describe("Exact provider memory address"),
}),
options: z.object({
@@ -158,7 +161,7 @@ export const registerCoreNativeCommands = (
cli.command(CLI_COMMANDS.addressToFileOffset, {
description: "Resolve an analysis address to its original file byte offset",
args: z.object({
path: z.string().describe("Local executable path"),
path: localPathStringSchema.describe("Local executable path"),
address: z.string().describe("Exact provider memory address"),
}),
options: z.object({
@@ -177,7 +180,9 @@ export const registerCoreNativeCommands = (
cli.command(CLI_COMMANDS.traceNativeValues, {
description: "Trace bounded native def-use and call dependencies",
args: z.object({
path: z.string().describe("Local native executable or app path"),
path: localPathStringSchema.describe(
"Local native executable or app path",
),
procedure: nativeValueTraceInputSchema.shape.procedure.describe(
"Explicit native procedure symbol or address",
),
@@ -287,7 +292,9 @@ export const registerCoreNativeCommands = (
? "Inspect one native instruction with decoded operand facts"
: "Resolve one static native call site",
args: z.object({
path: z.string().describe("Local native executable or app path"),
path: localPathStringSchema.describe(
"Local native executable or app path",
),
address: z.string().describe("Exact native address"),
}),
options: z.object({
@@ -317,7 +324,9 @@ const registerNativeDispatchMetadataCommand = (
cli.command(CLI_COMMANDS.inspectNativeDispatchMetadata, {
description: "Inspect typed Objective-C and Swift dispatch metadata",
args: z.object({
path: z.string().describe("Target path used to bind the result evidence"),
path: localPathStringSchema.describe(
"Target path used to bind the result evidence",
),
}),
options: z.object({
maxRecords: nativeDispatchMetadataInputSchema.shape.max_records.describe(
@@ -355,7 +364,9 @@ const registerNativeUiActionCommand = (
description:
"Trace a compiled UI action selector to its native handler and direct callees",
args: z.object({
path: z.string().describe("Target path used to bind the result evidence"),
path: localPathStringSchema.describe(
"Target path used to bind the result evidence",
),
action: z
.string()
.min(1)
@@ -412,7 +423,7 @@ const registerNativeApiCommand = (
cli.command(CLI_COMMANDS.inspectNativeApi, {
description: `Reconstruct one native function API boundary with evidence. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
args: z.object({
path: z.string().describe("App or program path"),
path: localPathStringSchema.describe("App or program path"),
address: z
.string()
.optional()
@@ -455,7 +466,7 @@ const registerAnnotationCommand = (
cli.command(CLI_COMMANDS.annotateNativeFunction, {
description: "Edit function annotations and return refreshed analysis",
args: z.object({
path: z.string().describe("Local executable path"),
path: localPathStringSchema.describe("Local executable path"),
procedure: z
.string()
.describe("Exact function entry address or unique name"),
+15 -5
View File
@@ -41,7 +41,9 @@ const registerInspectManagedArtifact = (
cli.command(CLI_COMMANDS.inspectManagedArtifact, {
description: "Inspect PE/CLI identity without loading target code",
args: z.object({
path: z.string().describe("Managed PE executable or assembly path"),
path: localPathStringSchema.describe(
"Managed PE executable or assembly path",
),
}),
run: ({ args }) =>
logCliCommand(logger, "inspect-managed-artifact", () =>
@@ -64,7 +66,9 @@ const registerInspectManagedMembers = (
description:
"Inspect PE/CLI metadata members, signatures, CIL anchors, and ldstr string literals without loading target code",
args: z.object({
path: z.string().describe("Managed PE executable or assembly path"),
path: localPathStringSchema.describe(
"Managed PE executable or assembly path",
),
}),
options: z.object({
type: managedMembersInputSchema.shape.type_names.describe(
@@ -111,7 +115,9 @@ const registerInspectManagedNativeBoundaries = (
description:
"Inspect PE/CLI PInvoke and native implementation boundary declarations without loading target code",
args: z.object({
path: z.string().describe("Managed PE executable or assembly path"),
path: localPathStringSchema.describe(
"Managed PE executable or assembly path",
),
}),
run: ({ args }) =>
logCliCommand(logger, "inspect-managed-native-boundaries", () =>
@@ -126,8 +132,12 @@ const registerInspectManagedNativeBoundaries = (
};
const compareManagedMemberArgs = z.object({
leftPath: z.string().describe("Baseline managed PE executable or assembly"),
rightPath: z.string().describe("Candidate managed PE executable or assembly"),
leftPath: localPathStringSchema.describe(
"Baseline managed PE executable or assembly",
),
rightPath: localPathStringSchema.describe(
"Candidate managed PE executable or assembly",
),
});
const registerCompareManagedMembers = (
+11 -4
View File
@@ -24,6 +24,7 @@ import { PRODUCT_IDENTITY } from "../identity.js";
import { isCliOperationFailure, logCliCommand } from "../cliLogging.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { swiftSymbolsSchema } from "../contracts/native/nativeToolContracts.js";
import { localPathStringSchema } from "../domain/localPath.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { registerNativeCallCommands } from "./nativeCallCommands.js";
@@ -96,7 +97,7 @@ const registerNativeUiCommands = (
runProviderAnalysis: DirectAnalysis["runProviderAnalysis"],
): void => {
const pathArgument = z.object({
path: z.string().describe("Local native executable or app path"),
path: localPathStringSchema.describe("Local native executable or app path"),
});
cli.command(CLI_COMMANDS.observeNativeUi, {
description:
@@ -198,7 +199,9 @@ const registerNativeCommands = (
] as const) {
cli.command(command, {
description: `Run ${tool} without launching Hopper`,
args: z.object({ path: z.string().describe("Mach-O or app path") }),
args: z.object({
path: localPathStringSchema.describe("Mach-O or app path"),
}),
options: z.object({
nativeCommandTimeoutMs: nativeCommandTimeoutField,
}),
@@ -217,7 +220,9 @@ const registerNativeCommands = (
}
cli.command(CLI_COMMANDS.inspectPlist, {
description: "Parse app plist metadata without launching Hopper",
args: z.object({ path: z.string().describe("App or Mach-O path") }),
args: z.object({
path: localPathStringSchema.describe("App or Mach-O path"),
}),
options: z.object({
relativePath: z
.string()
@@ -247,7 +252,9 @@ const registerNativeCommands = (
cli.command(CLI_COMMANDS.demangleSwift, {
description: "Demangle Swift symbols without Hopper",
args: z.object({
path: z.string().describe("Artifact path used for evidence identity"),
path: localPathStringSchema.describe(
"Artifact path used for evidence identity",
),
symbols: swiftSymbolsSchema.describe("Swift mangled symbols to demangle"),
}),
options: z.object({