Files
reverse-engineering-anything/src/cli/binaryDiagnosticsCommands.ts
T

492 lines
18 KiB
TypeScript

import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
import { inspectDevicetreeInputSchema } from "../domain/firmware/devicetree.js";
import { inspectPeLoadConfigInputSchema } from "../domain/native/peLoadConfig.js";
import { inspectByteEntropyInputSchema } from "../domain/native/byteEntropy.js";
import { z } from "incur";
import {
createByteEntropyService,
createRecordedCrashService,
createBinaryLayoutService,
createElfNotesService,
createAppleIpsService,
createDevicetreeService,
createKernelModuleService,
createPeLoadConfigService,
createPeResourcesService,
createPeCertificatesService,
createPeImportsService,
createNativeExportsService,
} from "../composition/binaryDiagnostics.js";
import { CLI_COMMANDS } from "../cliCommandNames.js";
import { logCliCommand } from "../cliLogging.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { inspectBinaryLayoutInputSchema } from "../domain/native/binaryLayout.js";
import { inspectRecordedCrashInputSchema } from "../domain/native/recordedCrash.js";
import { inspectPeResourcesInputSchema } from "../domain/native/peResources.js";
import { inspectPeCertificatesInputSchema } from "../domain/native/peCertificates.js";
import { inspectPeImportsInputSchema } from "../domain/native/peImports.js";
import { inspectNativeExportsInputSchema } from "../domain/native/nativeExports.js";
import { inspectElfNotesInputSchema } from "../domain/native/elfNotes.js";
import { inspectAppleIpsInputSchema } from "../domain/native/appleIps.js";
import { inspectKernelModuleInputSchema } from "../domain/native/kernelModule.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,
deliverCliAnalysisEvidence,
} from "./analysisEvidenceDelivery.js";
/** Inspect operator-selected files through the same shared workflow as MCP. */
export const registerBinaryDiagnosticsCommands = (
cli: CliInstance,
logger: Logger,
environment: Readonly<NodeJS.ProcessEnv>,
): void => {
cli.command(CLI_COMMANDS.inspectElfNotes, {
description:
"Inspect ELF PT_NOTE and SHT_NOTE identities without executing the 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,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const elf = createElfNotesService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectElfNotes,
async () =>
finishAnalysisCommand(
() =>
elf.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_entries: options.maxEntries,
},
{ signal },
),
elf,
"rea-elf-notes",
"inspect_elf_notes",
signal,
).then(projectCliAnalysisResult),
);
}),
});
cli.command(CLI_COMMANDS.inspectAppleIps, {
description:
"Inspect an Apple .ips crash report without executing a target",
args: z.object({
path: z.string().describe("Local Apple .ips crash report"),
}),
options: z.object({
maxFileBytes: inspectAppleIpsInputSchema.shape.max_file_bytes,
maxThreads: inspectAppleIpsInputSchema.shape.max_threads,
maxFrames: inspectAppleIpsInputSchema.shape.max_frames,
maxImages: inspectAppleIpsInputSchema.shape.max_images,
maxRegisters: inspectAppleIpsInputSchema.shape.max_registers,
maxIntegerTextBytes:
inspectAppleIpsInputSchema.shape.max_integer_text_bytes,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const ips = createAppleIpsService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectAppleIps,
async () =>
finishAnalysisCommand(
() =>
ips.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_threads: options.maxThreads,
max_frames: options.maxFrames,
max_images: options.maxImages,
max_registers: options.maxRegisters,
max_integer_text_bytes: options.maxIntegerTextBytes,
},
{ signal },
),
ips,
"rea-apple-ips",
"inspect_apple_ips",
signal,
).then(projectCliAnalysisResult),
);
}),
});
cli.command(CLI_COMMANDS.inspectKernelModule, {
description:
"Inspect Linux kernel module metadata without executing the module",
args: z.object({
path: localPathStringSchema.describe(
"Local ELF32/ELF64 kernel module or compressed .ko container",
),
}),
options: z.object({
maxFileBytes: inspectKernelModuleInputSchema.shape.max_file_bytes,
maxEntries: inspectKernelModuleInputSchema.shape.max_entries,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const kernelModule = createKernelModuleService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectKernelModule,
async () =>
finishAnalysisCommand(
() =>
kernelModule.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_entries: options.maxEntries,
},
{ signal },
),
kernelModule,
"rea-kernel-module",
"inspect_kernel_module",
signal,
).then(projectCliAnalysisResult),
);
}),
});
cli.command(CLI_COMMANDS.inspectPeLoadConfig, {
description:
"Inspect PE TLS callbacks and load configuration without executing the image",
args: z.object({
path: localPathStringSchema.describe("Local PE32/PE32+ image"),
}),
options: z.object({
maxFileBytes: inspectPeLoadConfigInputSchema.shape.max_file_bytes,
maxCallbacks: inspectPeLoadConfigInputSchema.shape.max_callbacks,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const elf = createPeLoadConfigService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectPeLoadConfig,
async () =>
finishAnalysisCommand(
() =>
elf.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_callbacks: options.maxCallbacks,
},
{ signal },
),
elf,
"rea-pe-load-config",
"inspect_pe_load_config",
signal,
).then(projectCliAnalysisResult),
);
}),
});
const entropy = createByteEntropyService();
cli.command(CLI_COMMANDS.inspectByteEntropy, {
description:
"Inspect file-wide and block byte entropy without executing the 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)",
),
offset: inspectByteEntropyInputSchema.shape.offset.describe(
"Window start as a decimal file byte offset (default 0)",
),
length: inspectByteEntropyInputSchema.shape.length.describe(
"Window length in bytes; omit to extend through EOF",
),
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectByteEntropy, async () => {
return await finishAnalysisCommand(
() =>
entropy.inspect(
{
path: absoluteCallerPath(args.path),
block_size: options.blockSize,
offset: options.offset,
length: options.length,
},
{ signal },
),
entropy,
"rea-byte-entropy",
"inspect_byte_entropy",
signal,
).then(projectCliAnalysisResult);
}),
),
});
cli.command(CLI_COMMANDS.inspectPeCertificates, {
description:
"Inspect PE Authenticode certificate table identities without verifying signatures",
args: z.object({
path: localPathStringSchema.describe("Local PE32/PE32+ image"),
}),
options: z.object({
maxFileBytes: inspectPeCertificatesInputSchema.shape.max_file_bytes,
maxEntries: inspectPeCertificatesInputSchema.shape.max_entries,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const pe = createPeCertificatesService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectPeCertificates,
async () =>
finishAnalysisCommand(
() =>
pe.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_entries: options.maxEntries,
},
{ signal },
),
pe,
"rea-pe-certificates",
"inspect_pe_certificates",
signal,
).then(projectCliAnalysisResult),
);
}),
});
cli.command(CLI_COMMANDS.inspectPeImports, {
description:
"Inspect PE import and delay-load table identities without executing the image",
args: z.object({ path: z.string().describe("Local PE32/PE32+ image") }),
options: z.object({
maxFileBytes: inspectPeImportsInputSchema.shape.max_file_bytes,
maxDlls: inspectPeImportsInputSchema.shape.max_dlls,
maxFunctions: inspectPeImportsInputSchema.shape.max_functions,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const pe = createPeImportsService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectPeImports,
async () =>
finishAnalysisCommand(
() =>
pe.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_dlls: options.maxDlls,
max_functions: options.maxFunctions,
},
{ signal },
),
pe,
"rea-pe-imports",
"inspect_pe_imports",
signal,
).then(projectCliAnalysisResult),
);
}),
});
cli.command(CLI_COMMANDS.inspectPeResources, {
description:
"Inspect PE resource identities, payload ranges, hashes and icon-group relationships",
args: z.object({
path: localPathStringSchema.describe("Local PE32/PE32+ image"),
}),
options: z.object({
maxFileBytes: inspectPeResourcesInputSchema.shape.max_file_bytes,
maxEntries: inspectPeResourcesInputSchema.shape.max_entries,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const pe = createPeResourcesService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectPeResources,
async () =>
finishAnalysisCommand(
() =>
pe.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_entries: options.maxEntries,
},
{ signal },
),
pe,
"rea-pe-resources",
"inspect_pe_resources",
signal,
).then(projectCliAnalysisResult),
);
}),
});
cli.command(CLI_COMMANDS.inspectDevicetree, {
description: "Inspect a flattened devicetree blob without executing it",
args: z.object({
path: localPathStringSchema.describe("Local flattened devicetree image"),
}),
options: z.object({
maxFileBytes: inspectDevicetreeInputSchema.shape.max_file_bytes.describe(
"Maximum FDT file size in bytes (default 4194304)",
),
maxNodes: inspectDevicetreeInputSchema.shape.max_nodes.describe(
"Maximum number of device-tree nodes (default 8192)",
),
maxProperties: inspectDevicetreeInputSchema.shape.max_properties.describe(
"Maximum number of device-tree properties (default 32768)",
),
maxDepth: inspectDevicetreeInputSchema.shape.max_depth.describe(
"Maximum device-tree nesting depth (default 64)",
),
...analysisEvidenceDeliveryOptions,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const devicetree = createDevicetreeService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectDevicetree,
async () =>
deliverCliAnalysisEvidence({
run: () =>
devicetree.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
max_nodes: options.maxNodes,
max_properties: options.maxProperties,
max_depth: options.maxDepth,
},
{ signal },
),
service: devicetree,
providerId: "rea-devicetree",
operation: "inspect_devicetree",
signal,
detail: options.detail,
evidenceOutput: options.evidence_output,
}),
);
}),
});
cli.command(CLI_COMMANDS.inspectNativeExports, {
description:
"Inspect a PE or ELF image's export table without executing it",
args: z.object({
path: localPathStringSchema.describe("Local PE or ELF image"),
}),
options: z.object({
maxFileBytes: inspectNativeExportsInputSchema.shape.max_file_bytes,
...analysisEvidenceDeliveryOptions,
}),
run: ({ args, options }) =>
withCommandCancellation(async (signal) => {
const exports = createNativeExportsService();
return await logCliCommand(
logger,
CLI_COMMANDS.inspectNativeExports,
async () =>
deliverCliAnalysisEvidence({
run: () =>
exports.inspect(
{
path: absoluteCallerPath(args.path),
max_file_bytes: options.maxFileBytes,
},
{ signal },
),
service: exports,
providerId: "rea-native-exports",
operation: "inspect_native_exports",
signal,
detail: options.detail,
evidenceOutput: options.evidence_output,
}),
);
}),
});
const service = createBinaryLayoutService(environment);
cli.command(CLI_COMMANDS.inspectBinaryLayout, {
description:
"Inspect binary file/linked layout without executing the selected object",
args: z.object({
path: localPathStringSchema.describe(
"Local ELF binary or relocatable object",
),
}),
options: z.object({
pwntoolsTimeoutMs:
inspectBinaryLayoutInputSchema.shape.pwntools_timeout_ms,
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectBinaryLayout, async () => {
const result = await service.inspectAndClose(
{
path: absoluteCallerPath(args.path),
...(options.pwntoolsTimeoutMs === undefined
? {}
: { pwntools_timeout_ms: options.pwntoolsTimeoutMs }),
},
{ signal },
);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
),
});
const recorded = createRecordedCrashService(environment);
cli.command(CLI_COMMANDS.inspectRecordedCrash, {
description:
"Inspect supplied core registers/signals and optional debugger mapping evidence",
args: z.object({
path: localPathStringSchema.describe(
"Local Linux ELF64 amd64 or AArch64 little-endian core",
),
}),
options: z.object({
debuggerContext: z
.boolean()
.default(false)
.describe("Add BYO core-only GDB/pwndbg mapping candidates"),
pwntoolsTimeoutMs:
inspectRecordedCrashInputSchema.shape.pwntools_timeout_ms,
}),
run: ({ args, options }) =>
withCommandCancellation((signal) =>
logCliCommand(logger, CLI_COMMANDS.inspectRecordedCrash, async () => {
const result = await recorded.inspectAndClose(
{
path: absoluteCallerPath(args.path),
include_debugger_context: options.debuggerContext,
...(options.pwntoolsTimeoutMs === undefined
? {}
: { pwntools_timeout_ms: options.pwntoolsTimeoutMs }),
},
{ signal },
);
return result.ok ? result.value : projectAnalysisError(result.error);
}),
),
});
};