Files
rea/src/cli/binaryDiagnosticsCommands.ts
T
781760a195 feat(apple): inspect IPS crash reports (#6511)
* feat(apple): inspect IPS crash reports

Add inspect_apple_ips for caller-selected Apple .ips files. Parse the
first-line metadata object and, when bug_type is the string 309, the
crash body: process identity, exception and termination facts, threads,
usedImages, lossless integer registers, and documented classification
rules. Other bug types including 288 stackshots return no crash result.
Opened Mach-O and dSYM UUID joins remain unobserved.

Related to #4297

* fix(apple): keep IPS register literals exact

Use the arm64e fixture integer that matches 0x8527000193ed4944 instead
of an IEEE-754-rounded decimal, and count the lossless JSON example
length from the source text.

* fix(apple): budget IPS parsing and keep typed presence

Parse the IPS metadata and crash body through a projection of the
documented members. Members the report does not interpret are
syntax-checked without being materialized, and thread, frame, image and
the new max_registers budgets run before each row is parsed, so a large
selected file fails with a typed resource error instead of first
expanding every row. Fixed allowances bound JSON nesting, unlisted key
names and termination reasons.

Present values whose JSON type differs from the documented one are no
longer read as absence: they are listed in coverage.type_mismatches and
coverage becomes partial, including wrong-type or missing threads and
usedImages collections. osVersion keeps the producer dictionary or the
string form. Frame roles follow physical frame depth, so the top frame
of a non-faulting thread is current_instruction and inlined rows share
the following frame's role. SIGKILL classes compare the canonical hex
once, so 0xdeadd0cc no longer matches dead10cc. Invalid UTF-8 is refused
instead of being replaced, and JSON strings are decoded by copying
unescaped spans.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Ra7LM2NyQ7DumdssxbUq5g

* fix(apple): bound integer text expansion and reconcile coverage

* refactor(apple): retain image metadata at its owning row

* refactor(apple): validate row capacity through reconciled coverage

* test(apple): verify resource recovery at the public error boundary

* docs(apple): record complete IPS task and corpus measurements

* docs(apple): record integrated checks and stdio corpus evidence

* docs(apple): record PE integration and baseline cleanup controls

* fix(native): bound IPS string decoding and preserve invalid identities

* refactor(native): use exact optional IPS source evidence

* docs(native): verify IPS escape capacity and complete-task costs

* test(native): verify IPS through production stdio MCP

* style(tests): format IPS advertised schema bindings

* fix(native): preserve unresolved IPS references and recorded frame semantics

* docs(native): record IPS reference review evidence

* fix(apple-ips): enforce JSON and report consistency

* docs(apple-ips): record final consistency verification

---------

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: gensokyo-dev <342580529+gensokyo-dev@users.noreply.github.com>
2026-10-24 16:02:59 +08:00

364 lines
13 KiB
TypeScript

import { inspectByteEntropyInputSchema } from "../domain/native/byteEntropy.js";
import { z } from "incur";
import {
createByteEntropyService,
createRecordedCrashService,
createBinaryLayoutService,
createElfNotesService,
createAppleIpsService,
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 { 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,
),
);
}),
});
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.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);
}),
),
});
};