Files
reverse-engineering-anything/src/cli/coreNativeCommands.ts
T
morluto 4eafd9dee5 refactor(cli): project core native commands with open_binary target options (#10736)
* refactor(cli): project core native commands with open_binary target options

A session target's opening fields may now be caller paths, admitted
resolved and passed on as the caller spelled them, and a union of text
forms such as the provider selector renders as one argv token.

nativeTarget declares a native executable opened with open_binary's
format, architecture, entry point and provider fields, plus the snapshot
where a command loads one. runDirectOnTarget runs an operation on that
target through runDirectAnalysis, with Evidence delivery when declared.

Eleven core native commands are now derived from their contracts. Their
inputs are admitted by the service's contract parse, which replaces the
CLI's copies of the byte-pattern and data-type selector checks.
inspect-native-dispatch-metadata now exposes the contract's Objective-C
class import address fields.

* fix(cli): read core native contracts from their contract groups so startup stays lazy

* refactor(cli): project procedure-references and inspect-native-api with named selector options (#10738)

* refactor(cli): project procedure-references and inspect-native-api with named selector options

A contract projection may let the caller give a positional selector with
a named option instead, for values that begin with a dash. The positional
becomes optional; equal values agree (native addresses by identity), and
different values, or neither for a required field, are refused before the
target is opened. selectCliAnalysisSelector owns that rule and returns a
Result; resolveCliAnalysisSelector remains for the hand-written core
binary commands.

procedure-references and inspect-native-api are now derived from their
contracts on the native session target, which completes the core native
family.

* fix(cli): refuse empty named and positional selectors before opening the target

* refactor(cli): project inspect, xrefs, trace, function and instructions from their contracts (#10747)

The five core binary commands are derived from their contracts on the
native session target with the snapshot option, using named selector
options for the procedure, address and query.

A selector projection may require a selector the contract lets the MCP
caller omit: xrefs' address defaults to Hopper's cursor, which a one-shot
CLI command does not have. A selector names one object, so neither
spelling may be empty, as before, even where the contract's text admits
it.

open_binary's pairing rules now refuse an unpaired --target-format,
--architecture or --entry-point for inspect before the target is opened,
naming the CLI key.

* fix(cli): read core binary contracts from their contract groups so startup stays lazy
2026-11-15 21:32:33 +08:00

322 lines
11 KiB
TypeScript

import {
addressOrProcedureGuidance,
dataTypeSelectorGuidance,
equalSelectorGuidance,
} from "./analysisSelector.js";
import type { DirectAnalysis } from "../composition/directAnalysis.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import type { Logger } from "pino";
import { nativeTarget } from "./options.js";
import type { CliInstance } from "./types.js";
import { contractNamed } from "./contractCliDescription.js";
import { ENHANCED_TOOL_CONTRACTS } from "../contracts/enhancedToolContracts.js";
import { OFFICIAL_TOOL_CONTRACTS } from "../contracts/officialToolContracts.js";
import {
registerContractCommand,
runDirectOnTarget,
} from "./contractCommand.js";
const executable = nativeTarget("Local executable path");
const nativeApplication = nativeTarget("Local native executable or app path");
/** The CLI has no Hopper document selection; it opens one target per call. */
const noDocument = {
document:
"The CLI opens one target per invocation and selects no Hopper document",
} as const;
/** Register native load-image, memory, and annotation CLI commands. */
export const registerCoreNativeCommands = (
cli: CliInstance,
logger: Logger,
runDirectAnalysis: DirectAnalysis["runDirectAnalysis"],
): void => {
const run = runDirectOnTarget(runDirectAnalysis, logger);
registerContractCommand(
cli,
logger,
contractNamed(OFFICIAL_TOOL_CONTRACTS, "annotate_native_function"),
{
command: CLI_COMMANDS.annotateNativeFunction,
description: "Edit function annotations and return refreshed analysis",
sessionTarget: executable,
positionals: ["procedure"],
fields: {
procedure: {
description:
"Function name or address; Ghidra resolves interior addresses to the containing function and edits its entry",
},
name: { description: "Analyst function name" },
comment: { description: "Regular entry comment; empty text clears it" },
inline_comment: {
name: "inline-comment",
description: "Inline entry comment; empty text clears it",
},
},
},
run,
);
registerContractCommand(
cli,
logger,
contractNamed(OFFICIAL_TOOL_CONTRACTS, "search_bytes"),
{
command: CLI_COMMANDS.searchBytes,
description:
"Find masked bytes; unbounded searches skip unloaded blocks and overlays",
sessionTarget: executable,
positionals: ["pattern"],
omit: noDocument,
fields: {
start: {
description:
"Inclusive start address; use an overlay address space to search it",
},
end: {
description:
"Inclusive end address; use an overlay address space to search it",
},
cursor: {
description: "Continuation from the same query's next_cursor",
},
limit: { description: "Maximum matches on this page" },
},
},
run,
);
registerContractCommand(
cli,
logger,
contractNamed(OFFICIAL_TOOL_CONTRACTS, "inspect_native_load_image"),
{
command: CLI_COMMANDS.inspectNativeLoadImage,
description:
"Inspect native load observations and supported snapshot verification",
sessionTarget: executable,
evidenceDelivery: true,
},
run,
);
registerProcedureSelectorCommands(cli, logger, run);
registerContractCommand(
cli,
logger,
contractNamed(OFFICIAL_TOOL_CONTRACTS, "read_bytes"),
{
command: CLI_COMMANDS.readBytes,
description:
"Read provider memory bytes; Ghidra memory may include loader fixups and differ from file bytes",
sessionTarget: executable,
positionals: ["address"],
omit: noDocument,
fields: {
address: { description: "Exact provider memory address" },
length: { description: "Requested byte count" },
},
},
run,
);
registerContractCommand(
cli,
logger,
contractNamed(OFFICIAL_TOOL_CONTRACTS, "address_to_file_offset"),
{
command: CLI_COMMANDS.addressToFileOffset,
description:
"Map one analysis address to a file offset; memory bytes may differ and ranges may not be contiguous",
sessionTarget: executable,
positionals: ["address"],
omit: noDocument,
fields: { address: { description: "Exact provider memory address" } },
},
run,
);
registerContractCommand(
cli,
logger,
contractNamed(ENHANCED_TOOL_CONTRACTS, "trace_native_values"),
{
command: CLI_COMMANDS.traceNativeValues,
description: "Trace bounded native def-use and call dependencies",
sessionTarget: nativeApplication,
positionals: ["procedure"],
evidenceDelivery: true,
fields: {
procedure: {
description: "Explicit native procedure symbol or address",
},
max_depth: { description: "Maximum call traversal depth" },
max_functions: { description: "Maximum function decompilations" },
max_call_sites: { description: "Maximum static call-site resolutions" },
max_nodes: { description: "Maximum retained graph nodes" },
max_edges: { description: "Maximum retained graph edges" },
offset: { description: "First graph node index" },
limit: { description: "Maximum nodes on this page" },
},
},
runDirectOnTarget(runDirectAnalysis, logger, {
requireSourceEvidence: true,
}),
);
registerContractCommand(
cli,
logger,
contractNamed(OFFICIAL_TOOL_CONTRACTS, "inspect_native_data_type"),
{
command: CLI_COMMANDS.inspectNativeDataType,
description: `Inspect one recovered database type or typed data object. ${dataTypeSelectorGuidance}`,
sessionTarget: nativeApplication,
omit: noDocument,
fields: {
type: {
description: `Exact database type category path. ${dataTypeSelectorGuidance}`,
},
address: {
description: `Exact native address. ${dataTypeSelectorGuidance}`,
},
},
},
run,
);
for (const [command, operation, description] of [
[
CLI_COMMANDS.inspectNativeInstruction,
"inspect_native_instruction",
"Inspect a code or data address, its containing Ghidra listing object, and typed incoming references where supported",
],
[
CLI_COMMANDS.resolveNativeCallTargets,
"resolve_native_call_targets",
"Resolve one static native call site",
],
] as const)
registerContractCommand(
cli,
logger,
contractNamed(OFFICIAL_TOOL_CONTRACTS, operation),
{
command,
description,
sessionTarget: nativeApplication,
positionals: ["address"],
omit: noDocument,
fields: { address: { description: "Exact native address" } },
},
run,
);
const evidenceTarget = "Target path used to bind the result evidence";
registerContractCommand(
cli,
logger,
contractNamed(ENHANCED_TOOL_CONTRACTS, "trace_native_ui_action"),
{
command: CLI_COMMANDS.traceNativeUiAction,
description:
"Trace a compiled UI action selector to its native handler and direct callees",
sessionTarget: nativeTarget(evidenceTarget),
positionals: ["action"],
fields: {
action: {
description:
"A unique compiled UI action selector or interface object ID",
},
max_depth: { description: "Maximum relationship depth" },
max_nodes: { description: "Maximum returned graph nodes" },
max_edges: { description: "Maximum returned graph edges" },
},
},
run,
);
registerContractCommand(
cli,
logger,
contractNamed(ENHANCED_TOOL_CONTRACTS, "inspect_native_dispatch_metadata"),
{
command: CLI_COMMANDS.inspectNativeDispatchMetadata,
description: "Inspect typed Objective-C and Swift dispatch metadata",
sessionTarget: nativeTarget(
evidenceTarget,
"Load or update the local analysis snapshot",
),
fields: {
max_records: { description: "Maximum symbol records to inspect" },
objc_class_import_addresses: {
name: "objcClassImportAddress",
description:
"Exact pointer-slot address from the selected name inventory, used to report decoded Objective-C class import bindings (repeatable)",
},
objc_class_import_addresses_complete: {
description:
"The addresses include every selected class-label row; otherwise binding coverage stays partial when the record budget truncates the scan",
},
},
},
run,
);
};
/**
* Register the native commands whose procedure is given positionally or,
* for symbols beginning with a dash, as `--procedure`.
*/
const registerProcedureSelectorCommands = (
cli: CliInstance,
logger: Logger,
run: ReturnType<typeof runDirectOnTarget>,
): void => {
registerContractCommand(
cli,
logger,
contractNamed(OFFICIAL_TOOL_CONTRACTS, "procedure_references"),
{
command: CLI_COMMANDS.procedureReferences,
description: `Inspect incoming or outgoing references for one native procedure. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
sessionTarget: nativeTarget("Local executable path", true),
positionals: ["procedure"],
selectorOptions: {
procedure: {
name: "procedure",
description: `Procedure name or provider address; use --procedure=<value> for leading dashes. ${addressOrProcedureGuidance}`,
kind: "native",
},
},
evidenceDelivery: true,
omit: noDocument,
fields: {
procedure: {
description: `Procedure name or provider address. ${addressOrProcedureGuidance}`,
},
direction: {
description: "Reference direction. Defaults to outgoing.",
},
},
},
run,
);
registerContractCommand(
cli,
logger,
contractNamed(ENHANCED_TOOL_CONTRACTS, "inspect_native_api"),
{
command: CLI_COMMANDS.inspectNativeApi,
description: `Reconstruct one native function API boundary with evidence. ${addressOrProcedureGuidance} ${equalSelectorGuidance}`,
sessionTarget: nativeTarget("App or program path", true),
positionals: ["procedure"],
selectorOptions: {
procedure: {
name: "procedure",
description: `Procedure selector; use this option for symbols beginning with a dash. ${addressOrProcedureGuidance}`,
kind: "native",
},
},
fields: {
procedure: {
name: "address",
description: `Procedure name or address. ${addressOrProcedureGuidance}`,
},
},
},
run,
);
};