* feat(firmware): inspect flattened devicetree blobs Adds inspect-devicetree and inspect_devicetree for FDT v16/v17 blobs without target execution or external dependencies. Results include validated headers, reservations, a node/property tree with bounded raw previews, and derived standard values and selected phandle references. Parsing enforces file, tree, and decoded representation budgets and rejects malformed input without partial success. Summary delivery supports retained Evidence pagination, with tests covering parsing, service validation, CLI delivery, and MCP access after the input is removed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe * fix(firmware): reject conflicting devicetree phandles and bound page summaries A node whose phandle and linux,phandle disagree no longer resolves under either value; the conflict is reported as a limitation. Finalization failures after a completed read retain the file-snapshot-hash partial observation, matching the Go provider. Devicetree view pages carry fixed-size context and counts instead of repeating summary strings, so a large bootargs no longer pushes every page over the MCP response budget. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016y53PdpyMAp9RSW65fatHe --------- Co-authored-by: Matt Van Horn <455140+mvanhorn@users.noreply.github.com> Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com> Co-authored-by: morluto <76467478+morluto@users.noreply.github.com>
491 lines
18 KiB
TypeScript
491 lines
18 KiB
TypeScript
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,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
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,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
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,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
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,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
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,
|
|
);
|
|
}),
|
|
),
|
|
});
|
|
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,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
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,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
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,
|
|
),
|
|
);
|
|
}),
|
|
});
|
|
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);
|
|
}),
|
|
),
|
|
});
|
|
};
|