mirror of
https://github.com/morluto/rea
synced 2026-10-28 06:46:47 -04:00
fix(cli): validate byte queries before opening targets (#7723)
This commit is contained in:
@@ -1,6 +1,13 @@
|
||||
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
|
||||
import { nativeDispatchMetadataInputSchema } from "../domain/native/objcSwiftMetadata.js";
|
||||
import { z } from "incur";
|
||||
import {
|
||||
bytePatternFields,
|
||||
validateBytePattern,
|
||||
MAX_BYTE_SEARCH_MATCHES,
|
||||
} from "../domain/native/bytePattern.js";
|
||||
import { analysisInputErrorFromIssues } from "../domain/inputIssueProjection.js";
|
||||
|
||||
import {
|
||||
addressOrProcedureGuidance,
|
||||
equalSelectorGuidance,
|
||||
@@ -25,6 +32,10 @@ import {
|
||||
} from "./analysisEvidenceDelivery.js";
|
||||
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
||||
|
||||
const bytePatternInputSchema = z
|
||||
.object(bytePatternFields)
|
||||
.superRefine(validateBytePattern);
|
||||
|
||||
/** Register native load-image, memory, and annotation CLI commands. */
|
||||
export const registerCoreNativeCommands = (
|
||||
cli: CliInstance,
|
||||
@@ -36,10 +47,10 @@ export const registerCoreNativeCommands = (
|
||||
description: "Find masked byte patterns in initialized provider memory",
|
||||
args: z.object({
|
||||
path: z.string().describe("Local executable path"),
|
||||
pattern: z.string().describe("Hexadecimal byte pairs with ?? wildcards"),
|
||||
pattern: bytePatternFields.pattern,
|
||||
}),
|
||||
options: z.object({
|
||||
mask: z.string().optional().describe("Equal-length hexadecimal bit mask"),
|
||||
mask: bytePatternFields.mask,
|
||||
start: z.string().optional().describe("Inclusive start address"),
|
||||
end: z.string().optional().describe("Inclusive end address"),
|
||||
cursor: z
|
||||
@@ -50,27 +61,43 @@ export const registerCoreNativeCommands = (
|
||||
.number()
|
||||
.int()
|
||||
.min(1)
|
||||
.max(1000)
|
||||
.max(MAX_BYTE_SEARCH_MATCHES)
|
||||
.default(100)
|
||||
.describe("Maximum matches on this page"),
|
||||
...nativeOpenOptionFields,
|
||||
}),
|
||||
run: ({ args, options }) =>
|
||||
logCliCommand(logger, CLI_COMMANDS.searchBytes, () =>
|
||||
runDirectAnalysis(
|
||||
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: args.pattern,
|
||||
pattern: parsed.data.pattern,
|
||||
...(parsed.data.mask === undefined
|
||||
? {}
|
||||
: { mask: parsed.data.mask }),
|
||||
limit: options.limit,
|
||||
...(options.mask === undefined ? {} : { mask: options.mask }),
|
||||
...(options.start === undefined ? {} : { start: options.start }),
|
||||
...(options.end === undefined ? {} : { end: options.end }),
|
||||
...(options.cursor === undefined ? {} : { cursor: options.cursor }),
|
||||
},
|
||||
directAnalysisOptionsFromCli(logger, options),
|
||||
).then(projectCliAnalysisResult),
|
||||
),
|
||||
).then(projectCliAnalysisResult);
|
||||
}),
|
||||
});
|
||||
cli.command(CLI_COMMANDS.inspectNativeLoadImage, {
|
||||
description:
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
import { writeFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { expect } from "vitest";
|
||||
import { cliTest } from "../support/cli/cliFixture.js";
|
||||
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
||||
|
||||
cliTest.for([
|
||||
{ pattern: "??", mask: undefined, field: "pattern" },
|
||||
{ pattern: "ZZ", mask: undefined, field: "pattern" },
|
||||
{ pattern: "", mask: undefined, field: "pattern" },
|
||||
{ pattern: "AB", mask: "FFFF", field: "mask" },
|
||||
])(
|
||||
"reports query correction before target refusal ($pattern/$mask)",
|
||||
async ({ pattern, mask, field }, { cli }) => {
|
||||
const root = await createTestTempDirectory("rea-byte-admission-");
|
||||
const text = join(root, "text.bin");
|
||||
await writeFile(text, "hello world");
|
||||
for (const path of [text, join(root, "missing")]) {
|
||||
const result = await cli.run({
|
||||
arguments: [
|
||||
"search-bytes",
|
||||
path,
|
||||
pattern,
|
||||
...(mask === undefined ? [] : ["--mask", mask]),
|
||||
"--json",
|
||||
],
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
issues: [
|
||||
expect.objectContaining({ path: [field], reason: "invalid_value" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
cliTest("keeps target admission for a valid byte query", async ({ cli }) => {
|
||||
const root = await createTestTempDirectory("rea-byte-valid-");
|
||||
const path = join(root, "text.bin");
|
||||
await writeFile(path, "hello world");
|
||||
const result = await cli.run({
|
||||
arguments: ["search-bytes", path, "AB", "--json"],
|
||||
});
|
||||
expect(result.exitCode).toBe(1);
|
||||
expect(result.json).toMatchObject({
|
||||
code: "target_unavailable",
|
||||
category: "unsupported_target",
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,106 @@
|
||||
import { Cli } from "incur";
|
||||
import pino from "pino";
|
||||
import { expect, it, vi } from "vitest";
|
||||
import { registerCoreNativeCommands } from "../../../src/cli/coreNativeCommands.js";
|
||||
|
||||
const registry = () => {
|
||||
const run = vi.fn(async () => ({
|
||||
ok: false as const,
|
||||
error: { code: "invalid_request", message: "Admitted query control" },
|
||||
}));
|
||||
const cli = Cli.create("byte-admission", { sync: false });
|
||||
registerCoreNativeCommands(cli, pino({ enabled: false }), run);
|
||||
return { cli, run };
|
||||
};
|
||||
|
||||
it.each([
|
||||
{ pattern: "??", mask: undefined, field: "pattern" },
|
||||
{ pattern: "ZZ", mask: undefined, field: "pattern" },
|
||||
{ pattern: "", mask: undefined, field: "pattern" },
|
||||
{ pattern: "AB", mask: "FFFF", field: "mask" },
|
||||
{ pattern: "AB", mask: "00", field: "pattern" },
|
||||
])(
|
||||
"rejects $pattern/$mask before any target access",
|
||||
async ({ pattern, mask, field }) => {
|
||||
const { cli, run } = registry();
|
||||
let stdout = "";
|
||||
await cli.serve(
|
||||
[
|
||||
"search-bytes",
|
||||
"/missing-target",
|
||||
pattern,
|
||||
...(mask === undefined ? [] : ["--mask", mask]),
|
||||
"--json",
|
||||
],
|
||||
{
|
||||
stdout: (text) => {
|
||||
stdout += text;
|
||||
},
|
||||
exit: () => undefined,
|
||||
},
|
||||
);
|
||||
expect(run).not.toHaveBeenCalled();
|
||||
expect(JSON.parse(stdout)).toMatchObject({
|
||||
code: "invalid_request",
|
||||
category: "invalid_input",
|
||||
details: {
|
||||
operation: "search_bytes",
|
||||
issues: [
|
||||
expect.objectContaining({ path: [field], reason: "invalid_value" }),
|
||||
],
|
||||
},
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it("forwards admitted original strings and caller-selected search/provider controls", async () => {
|
||||
const { cli, run } = registry();
|
||||
await cli.serve(
|
||||
[
|
||||
"search-bytes",
|
||||
"/selected",
|
||||
"ab ?? CD",
|
||||
"--mask",
|
||||
"FF 00 ff",
|
||||
"--start",
|
||||
"0x10",
|
||||
"--end",
|
||||
"0x20",
|
||||
"--cursor",
|
||||
"next",
|
||||
"--limit",
|
||||
"7",
|
||||
"--provider",
|
||||
"ghidra",
|
||||
"--json",
|
||||
],
|
||||
{ stdout: () => undefined, exit: () => undefined },
|
||||
);
|
||||
expect(run).toHaveBeenCalledExactlyOnceWith(
|
||||
"/selected",
|
||||
"search_bytes",
|
||||
{
|
||||
pattern: "ab ?? CD",
|
||||
mask: "FF 00 ff",
|
||||
start: "0x10",
|
||||
end: "0x20",
|
||||
cursor: "next",
|
||||
limit: 7,
|
||||
},
|
||||
expect.objectContaining({ providerId: "ghidra" }),
|
||||
);
|
||||
});
|
||||
|
||||
it("keeps an omitted mask omitted", async () => {
|
||||
const { cli, run } = registry();
|
||||
await cli.serve(["search-bytes", "/selected", "AB", "--json"], {
|
||||
stdout: () => undefined,
|
||||
exit: () => undefined,
|
||||
});
|
||||
expect(run).toHaveBeenCalledExactlyOnceWith(
|
||||
"/selected",
|
||||
"search_bytes",
|
||||
{ pattern: "AB", limit: 100 },
|
||||
expect.any(Object),
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user