mirror of
https://github.com/morluto/rea.git
synced 2026-11-11 01:13:39 +00:00
* fix(cli): admit native data type selectors before opening a target * fix(cli): advertise the native data type selector choice * test(cli): reject invalid native data type selectors before target access * test(cli): cover native data type selector discovery * fix(cli): reject an empty native data type target before resolution inspect-native-data-type still declared its positional target as a plain string after #7956 moved its sibling local-file commands to localPathStringSchema, so an empty target passed validation and resolved to the working directory. Use the shared schema and cover the command in the empty-target admission table. * refactor(cli): share native data type selector guidance owner * test(cli): bind native data type selector checks to owning contracts
598 lines
21 KiB
TypeScript
598 lines
21 KiB
TypeScript
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
|
import { nativeDispatchMetadataInputSchema } from "../domain/native/objcSwiftMetadata.js";
|
|
import { nativeInvestigationTraceLimitsSchema } from "../domain/native/nativeInvestigationGraph.js";
|
|
import { nativeValueTraceInputSchema } from "../domain/native/nativeValueTrace.js";
|
|
import { nativeDataTypeInputSchema } from "../domain/native/nativeDataType.js";
|
|
import { z } from "incur";
|
|
import {
|
|
bytePatternFields,
|
|
validateBytePattern,
|
|
MAX_BYTE_SEARCH_MATCHES,
|
|
} from "../domain/native/bytePattern.js";
|
|
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
|
import { localPathStringSchema } from "../domain/localPath.js";
|
|
|
|
import {
|
|
addressOrProcedureGuidance,
|
|
dataTypeSelectorGuidance,
|
|
equalSelectorGuidance,
|
|
resolveCliAnalysisSelector,
|
|
} from "./analysisSelector.js";
|
|
|
|
import type { DirectAnalysis } from "../composition/directAnalysis.js";
|
|
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
|
import { logCliCommand } from "../cliLogging.js";
|
|
import type { Logger } from "pino";
|
|
import {
|
|
cliDecimalField,
|
|
directAnalysisOptionsFromCli,
|
|
nativeOpenOptionFields,
|
|
snapshotSelectionOption,
|
|
} from "./options.js";
|
|
import type { CliInstance } from "./types.js";
|
|
import { withCommandCancellation } from "./commandCancellation.js";
|
|
import {
|
|
analysisEvidenceDeliveryOptions,
|
|
cliEvidenceDeliveryInputError,
|
|
publishCliAnalysisEvidence,
|
|
} from "./analysisEvidenceDelivery.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import { AnalysisOutputError } from "../domain/analysisErrorCore.js";
|
|
|
|
const bytePatternInputSchema = z
|
|
.object(bytePatternFields)
|
|
.superRefine(validateBytePattern);
|
|
|
|
/** Register native load-image, memory, and annotation CLI commands. */
|
|
export const registerCoreNativeCommands = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
|
): void => {
|
|
registerAnnotationCommand(cli, logger, runDirectAnalysis);
|
|
cli.command(CLI_COMMANDS.searchBytes, {
|
|
description:
|
|
"Find masked bytes; unbounded searches skip unloaded blocks and overlays",
|
|
args: z.object({
|
|
path: localPathStringSchema.describe("Local executable path"),
|
|
pattern: bytePatternFields.pattern,
|
|
}),
|
|
options: z.object({
|
|
mask: bytePatternFields.mask,
|
|
start: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Inclusive start address; use an overlay address space to search it",
|
|
),
|
|
end: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Inclusive end address; use an overlay address space to search it",
|
|
),
|
|
cursor: z
|
|
.string()
|
|
.optional()
|
|
.describe("Continuation from the same query's next_cursor"),
|
|
limit: cliDecimalField(
|
|
z
|
|
.number()
|
|
.int()
|
|
.min(1)
|
|
.max(MAX_BYTE_SEARCH_MATCHES)
|
|
.default(100)
|
|
.describe("Maximum matches on this page"),
|
|
),
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, CLI_COMMANDS.searchBytes, async () => {
|
|
const selection = {
|
|
pattern: args.pattern,
|
|
...(options.mask === undefined ? {} : { mask: options.mask }),
|
|
};
|
|
const parsed = bytePatternInputSchema.safeParse(selection);
|
|
if (!parsed.success)
|
|
return projectAnalysisError(
|
|
analysisInputErrorFromIssues(
|
|
"search_bytes",
|
|
parsed.error.issues,
|
|
selection,
|
|
{ cause: parsed.error },
|
|
),
|
|
);
|
|
return runDirectAnalysis(
|
|
args.path,
|
|
"search_bytes",
|
|
{
|
|
pattern: parsed.data.pattern,
|
|
...(parsed.data.mask === undefined
|
|
? {}
|
|
: { mask: parsed.data.mask }),
|
|
limit: options.limit,
|
|
...(options.start === undefined ? {} : { start: options.start }),
|
|
...(options.end === undefined ? {} : { end: options.end }),
|
|
...(options.cursor === undefined ? {} : { cursor: options.cursor }),
|
|
},
|
|
directAnalysisOptionsFromCli(logger, options),
|
|
).then(projectCliAnalysisResult);
|
|
}),
|
|
});
|
|
cli.command(CLI_COMMANDS.inspectNativeLoadImage, {
|
|
description:
|
|
"Inspect native load observations and supported snapshot verification",
|
|
args: z.object({
|
|
path: localPathStringSchema.describe("Local executable path"),
|
|
}),
|
|
options: z.object({
|
|
...analysisEvidenceDeliveryOptions,
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.inspectNativeLoadImage, async () => {
|
|
const delivery = {
|
|
providerId: "auto",
|
|
operation: "inspect_native_load_image",
|
|
signal,
|
|
detail: options.detail,
|
|
evidenceOutput: options.evidence_output,
|
|
};
|
|
const invalid = cliEvidenceDeliveryInputError(delivery);
|
|
if (invalid !== undefined) return projectAnalysisError(invalid);
|
|
const complete = await runDirectAnalysis(
|
|
args.path,
|
|
"inspect_native_load_image",
|
|
{},
|
|
{ ...directAnalysisOptionsFromCli(logger, options), signal },
|
|
);
|
|
return publishCliAnalysisEvidence(complete, delivery);
|
|
}),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.procedureReferences, {
|
|
description: `Inspect incoming or outgoing references for one native procedure. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
|
|
args: z.object({
|
|
path: localPathStringSchema.describe("Local executable path"),
|
|
procedure: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe(
|
|
`Procedure name or provider address. ${addressOrProcedureGuidance}`,
|
|
),
|
|
}),
|
|
options: z.object({
|
|
procedure: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe(
|
|
`Procedure name or provider address; use --procedure=<value> for leading dashes. ${addressOrProcedureGuidance}`,
|
|
),
|
|
direction: z
|
|
.enum(["incoming", "outgoing"])
|
|
.default("outgoing")
|
|
.describe("Reference direction. Defaults to outgoing."),
|
|
snapshot: snapshotSelectionOption,
|
|
...analysisEvidenceDeliveryOptions,
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.procedureReferences, async () => {
|
|
const delivery = {
|
|
providerId: "auto",
|
|
operation: "procedure_references",
|
|
signal,
|
|
detail: options.detail,
|
|
evidenceOutput: options.evidence_output,
|
|
};
|
|
const invalid = cliEvidenceDeliveryInputError(delivery);
|
|
if (invalid !== undefined) return projectAnalysisError(invalid);
|
|
const result = await runDirectAnalysis(
|
|
args.path,
|
|
"procedure_references",
|
|
{
|
|
procedure: resolveCliAnalysisSelector(
|
|
args.procedure,
|
|
options.procedure,
|
|
"procedure",
|
|
"procedure_references",
|
|
),
|
|
direction: options.direction,
|
|
},
|
|
{
|
|
...directAnalysisOptionsFromCli(logger, options),
|
|
signal,
|
|
includeEvidenceSources: options.evidence_output !== undefined,
|
|
},
|
|
);
|
|
return publishCliAnalysisEvidence(result, delivery);
|
|
}),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.readBytes, {
|
|
description:
|
|
"Read provider memory bytes; Ghidra memory may include loader fixups and differ from file bytes",
|
|
args: z.object({
|
|
path: localPathStringSchema.describe("Local executable path"),
|
|
address: z.string().describe("Exact provider memory address"),
|
|
}),
|
|
options: z.object({
|
|
length: cliDecimalField(
|
|
z.number().int().min(1).default(256).describe("Requested byte count"),
|
|
),
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, CLI_COMMANDS.readBytes, () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"read_bytes",
|
|
{ address: args.address, length: options.length },
|
|
directAnalysisOptionsFromCli(logger, options),
|
|
).then(projectCliAnalysisResult),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.addressToFileOffset, {
|
|
description:
|
|
"Map one analysis address to a file offset; memory bytes may differ and ranges may not be contiguous",
|
|
args: z.object({
|
|
path: localPathStringSchema.describe("Local executable path"),
|
|
address: z.string().describe("Exact provider memory address"),
|
|
}),
|
|
options: z.object({
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, CLI_COMMANDS.addressToFileOffset, () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"address_to_file_offset",
|
|
{ address: args.address },
|
|
directAnalysisOptionsFromCli(logger, options),
|
|
).then(projectCliAnalysisResult),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.traceNativeValues, {
|
|
description: "Trace bounded native def-use and call dependencies",
|
|
args: z.object({
|
|
path: localPathStringSchema.describe(
|
|
"Local native executable or app path",
|
|
),
|
|
procedure: nativeValueTraceInputSchema.shape.procedure.describe(
|
|
"Explicit native procedure symbol or address",
|
|
),
|
|
}),
|
|
options: z.object({
|
|
maxDepth: cliDecimalField(
|
|
nativeValueTraceInputSchema.shape.max_depth,
|
|
).describe("Maximum call traversal depth"),
|
|
maxFunctions: cliDecimalField(
|
|
nativeValueTraceInputSchema.shape.max_functions,
|
|
).describe("Maximum function decompilations"),
|
|
maxCallSites: cliDecimalField(
|
|
nativeValueTraceInputSchema.shape.max_call_sites,
|
|
).describe("Maximum static call-site resolutions"),
|
|
maxNodes: cliDecimalField(
|
|
nativeValueTraceInputSchema.shape.max_nodes,
|
|
).describe("Maximum retained graph nodes"),
|
|
maxEdges: cliDecimalField(
|
|
nativeValueTraceInputSchema.shape.max_edges,
|
|
).describe("Maximum retained graph edges"),
|
|
offset: cliDecimalField(
|
|
nativeValueTraceInputSchema.shape.offset,
|
|
).describe("First graph node index"),
|
|
limit: cliDecimalField(nativeValueTraceInputSchema.shape.limit).describe(
|
|
"Maximum nodes on this page",
|
|
),
|
|
...analysisEvidenceDeliveryOptions,
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.traceNativeValues, async () => {
|
|
const delivery = {
|
|
providerId: "auto",
|
|
operation: "trace_native_values",
|
|
signal,
|
|
detail: options.detail,
|
|
evidenceOutput: options.evidence_output,
|
|
};
|
|
const invalid = cliEvidenceDeliveryInputError(delivery);
|
|
if (invalid !== undefined) return projectAnalysisError(invalid);
|
|
const complete = await runDirectAnalysis(
|
|
args.path,
|
|
"trace_native_values",
|
|
{
|
|
procedure: args.procedure,
|
|
max_depth: options.maxDepth,
|
|
max_functions: options.maxFunctions,
|
|
max_call_sites: options.maxCallSites,
|
|
max_nodes: options.maxNodes,
|
|
max_edges: options.maxEdges,
|
|
offset: options.offset,
|
|
limit: options.limit,
|
|
},
|
|
{
|
|
...directAnalysisOptionsFromCli(logger, options),
|
|
signal,
|
|
includeEvidenceSources: options.evidence_output !== undefined,
|
|
},
|
|
);
|
|
if (!complete.ok || options.evidence_output === undefined)
|
|
return publishCliAnalysisEvidence(complete, delivery);
|
|
if (complete.sourceEvidence === undefined)
|
|
return projectAnalysisError(
|
|
new AnalysisOutputError(
|
|
"trace_native_values",
|
|
"The CLI session did not return its source Evidence.",
|
|
).retainPartialObservation(complete.value),
|
|
);
|
|
return publishCliAnalysisEvidence(complete, delivery);
|
|
}),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.inspectNativeDataType, {
|
|
description: `Inspect one recovered database type or typed data object. ${dataTypeSelectorGuidance}`,
|
|
args: z.object({
|
|
path: localPathStringSchema.describe(
|
|
"Local native executable or app path",
|
|
),
|
|
}),
|
|
options: z.object({
|
|
type: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
`Exact database type category path. ${dataTypeSelectorGuidance}`,
|
|
),
|
|
address: z
|
|
.string()
|
|
.optional()
|
|
.describe(`Exact native address. ${dataTypeSelectorGuidance}`),
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, CLI_COMMANDS.inspectNativeDataType, async () => {
|
|
const input = {
|
|
...(options.type === undefined ? {} : { type: options.type }),
|
|
...(options.address === undefined
|
|
? {}
|
|
: { address: options.address }),
|
|
};
|
|
// Admit the canonical selector choice before opening the target.
|
|
const selected = nativeDataTypeInputSchema.safeParse(input);
|
|
if (!selected.success)
|
|
return projectAnalysisError(
|
|
analysisInputErrorFromIssues(
|
|
"inspect_native_data_type",
|
|
selected.error.issues,
|
|
input,
|
|
{ cause: selected.error },
|
|
),
|
|
);
|
|
return runDirectAnalysis(
|
|
args.path,
|
|
"inspect_native_data_type",
|
|
input,
|
|
directAnalysisOptionsFromCli(logger, options),
|
|
).then(projectCliAnalysisResult);
|
|
}),
|
|
});
|
|
for (const [command, operation] of [
|
|
[CLI_COMMANDS.inspectNativeInstruction, "inspect_native_instruction"],
|
|
[CLI_COMMANDS.resolveNativeCallTargets, "resolve_native_call_targets"],
|
|
] as const) {
|
|
cli.command(command, {
|
|
description:
|
|
operation === "inspect_native_instruction"
|
|
? "Inspect a code or data address, its containing Ghidra listing object, and typed incoming references where supported"
|
|
: "Resolve one static native call site",
|
|
args: z.object({
|
|
path: localPathStringSchema.describe(
|
|
"Local native executable or app path",
|
|
),
|
|
address: z.string().describe("Exact native address"),
|
|
}),
|
|
options: z.object({
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, command, () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
operation,
|
|
{ address: args.address },
|
|
directAnalysisOptionsFromCli(logger, options),
|
|
).then(projectCliAnalysisResult),
|
|
),
|
|
});
|
|
}
|
|
registerNativeApiCommand(cli, logger, runDirectAnalysis);
|
|
registerNativeUiActionCommand(cli, logger, runDirectAnalysis);
|
|
registerNativeDispatchMetadataCommand(cli, logger, runDirectAnalysis);
|
|
};
|
|
|
|
const registerNativeDispatchMetadataCommand = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.inspectNativeDispatchMetadata, {
|
|
description: "Inspect typed Objective-C and Swift dispatch metadata",
|
|
args: z.object({
|
|
path: localPathStringSchema.describe(
|
|
"Target path used to bind the result evidence",
|
|
),
|
|
}),
|
|
options: z.object({
|
|
maxRecords: cliDecimalField(
|
|
nativeDispatchMetadataInputSchema.shape.max_records,
|
|
).describe("Maximum symbol records to inspect"),
|
|
maxInputBytes: cliDecimalField(
|
|
nativeDispatchMetadataInputSchema.shape.max_input_bytes,
|
|
),
|
|
snapshot: snapshotSelectionOption.describe(
|
|
"Load or update the local analysis snapshot",
|
|
),
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: async ({ args, options }) =>
|
|
logCliCommand(logger, "inspect-native-dispatch-metadata", () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"inspect_native_dispatch_metadata",
|
|
{
|
|
max_records: options.maxRecords,
|
|
...(options.maxInputBytes === undefined
|
|
? {}
|
|
: { max_input_bytes: options.maxInputBytes }),
|
|
},
|
|
directAnalysisOptionsFromCli(logger, options),
|
|
).then(projectCliAnalysisResult),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerNativeUiActionCommand = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.traceNativeUiAction, {
|
|
description:
|
|
"Trace a compiled UI action selector to its native handler and direct callees",
|
|
args: z.object({
|
|
path: localPathStringSchema.describe(
|
|
"Target path used to bind the result evidence",
|
|
),
|
|
action: z
|
|
.string()
|
|
.min(1)
|
|
.describe(
|
|
"A unique compiled UI action selector or interface object ID",
|
|
),
|
|
}),
|
|
options: z.object({
|
|
maxDepth: cliDecimalField(
|
|
nativeInvestigationTraceLimitsSchema.shape.max_depth,
|
|
).describe("Maximum relationship depth"),
|
|
maxNodes: cliDecimalField(
|
|
nativeInvestigationTraceLimitsSchema.shape.max_nodes,
|
|
).describe("Maximum returned graph nodes"),
|
|
maxEdges: cliDecimalField(
|
|
nativeInvestigationTraceLimitsSchema.shape.max_edges,
|
|
).describe("Maximum returned graph edges"),
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: async ({ args, options }) =>
|
|
logCliCommand(logger, "trace-native-ui-action", () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"trace_native_ui_action",
|
|
{
|
|
action: args.action,
|
|
max_depth: options.maxDepth,
|
|
max_nodes: options.maxNodes,
|
|
max_edges: options.maxEdges,
|
|
},
|
|
directAnalysisOptionsFromCli(logger, options),
|
|
).then(projectCliAnalysisResult),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerNativeApiCommand = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.inspectNativeApi, {
|
|
description: `Reconstruct one native function API boundary with evidence. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
|
|
args: z.object({
|
|
path: localPathStringSchema.describe("App or program path"),
|
|
address: z
|
|
.string()
|
|
.optional()
|
|
.describe(`Procedure name or address. ${addressOrProcedureGuidance}`),
|
|
}),
|
|
options: z.object({
|
|
procedure: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
`Procedure selector; use this option for symbols beginning with a dash. ${addressOrProcedureGuidance}`,
|
|
),
|
|
snapshot: snapshotSelectionOption,
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, "inspect-native-api", () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"inspect_native_api",
|
|
{
|
|
procedure: resolveCliAnalysisSelector(
|
|
args.address,
|
|
options.procedure,
|
|
"procedure",
|
|
"inspect_native_api",
|
|
),
|
|
},
|
|
directAnalysisOptionsFromCli(logger, options),
|
|
).then(projectCliAnalysisResult),
|
|
),
|
|
});
|
|
};
|
|
|
|
const registerAnnotationCommand = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
|
|
): void => {
|
|
cli.command(CLI_COMMANDS.annotateNativeFunction, {
|
|
description: "Edit function annotations and return refreshed analysis",
|
|
args: z.object({
|
|
path: localPathStringSchema.describe("Local executable path"),
|
|
procedure: z
|
|
.string()
|
|
.describe(
|
|
"Function name or address; Ghidra resolves interior addresses to the containing function and edits its entry",
|
|
),
|
|
}),
|
|
options: z.object({
|
|
name: z.string().min(1).optional().describe("Analyst function name"),
|
|
comment: z
|
|
.string()
|
|
.optional()
|
|
.describe("Regular entry comment; empty text clears it"),
|
|
"inline-comment": z
|
|
.string()
|
|
.optional()
|
|
.describe("Inline entry comment; empty text clears it"),
|
|
...nativeOpenOptionFields,
|
|
}),
|
|
run: ({ args, options }) =>
|
|
logCliCommand(logger, CLI_COMMANDS.annotateNativeFunction, () =>
|
|
runDirectAnalysis(
|
|
args.path,
|
|
"annotate_native_function",
|
|
{
|
|
procedure: args.procedure,
|
|
...(options.name === undefined ? {} : { name: options.name }),
|
|
...(options.comment === undefined
|
|
? {}
|
|
: { comment: options.comment }),
|
|
...(options["inline-comment"] === undefined
|
|
? {}
|
|
: { inline_comment: options["inline-comment"] }),
|
|
},
|
|
directAnalysisOptionsFromCli(logger, options),
|
|
).then(projectCliAnalysisResult),
|
|
),
|
|
});
|
|
};
|