mirror of
https://github.com/morluto/rea
synced 2026-10-27 14:42:18 -04:00
289 lines
9.8 KiB
TypeScript
289 lines
9.8 KiB
TypeScript
import { z } from "incur";
|
|
|
|
import { FridaInstrumentationService } from "../application/frida/FridaInstrumentationService.js";
|
|
import type {
|
|
FridaInstrumentationPort,
|
|
FridaRemoteConnection,
|
|
} from "../application/frida/FridaInstrumentationPort.js";
|
|
import { FridaInstrumentationManager } from "../frida/FridaInstrumentationManager.js";
|
|
import { CLI_COMMANDS } from "../cliCommandNames.js";
|
|
import { logCliCommand } from "../cliLogging.js";
|
|
import { projectAnalysisError } from "../domain/analysisErrorProjection.js";
|
|
import { AnalysisInputError } from "../domain/analysisErrorCore.js";
|
|
import { ok } from "../domain/result.js";
|
|
import { FRIDA_KEEPALIVE_INTERVAL_DESCRIPTION } from "../contracts/fridaToolContracts.js";
|
|
import { isValidFridaRemoteAddress } from "../contracts/fridaRemoteAddress.js";
|
|
import type { Logger } from "pino";
|
|
import type { CliInstance } from "./types.js";
|
|
import { withCommandCancellation } from "./commandCancellation.js";
|
|
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
|
|
|
|
/** Register Frida discovery and one-shot instrumentation CLI operations. */
|
|
export const registerFridaCommands = (
|
|
cli: CliInstance,
|
|
logger: Logger,
|
|
provider: FridaInstrumentationPort = new FridaInstrumentationManager(),
|
|
): void => {
|
|
const service = new FridaInstrumentationService(provider);
|
|
// CLI calls own a disposable provider; MCP retains its shared provider until
|
|
// server shutdown. Await cleanup here, including failed startup resources.
|
|
const cleanup = { close: () => provider.closeAll() };
|
|
const remoteOptions = z.object({
|
|
remoteAddress: z
|
|
.string()
|
|
.min(1)
|
|
.max(2048)
|
|
.refine(isValidFridaRemoteAddress)
|
|
.optional()
|
|
.describe("Remote Frida host address, optionally with a port"),
|
|
token: z.string().min(1).optional().describe("Remote authentication token"),
|
|
certificate: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Remote TLS certificate"),
|
|
origin: z.string().min(1).optional().describe("Remote connection origin"),
|
|
keepaliveInterval: z
|
|
.number()
|
|
.int()
|
|
.positive()
|
|
.optional()
|
|
.describe(FRIDA_KEEPALIVE_INTERVAL_DESCRIPTION),
|
|
});
|
|
const remoteFrom = (
|
|
options: z.infer<typeof remoteOptions>,
|
|
): FridaRemoteConnection | undefined =>
|
|
options.remoteAddress === undefined
|
|
? undefined
|
|
: {
|
|
address: options.remoteAddress,
|
|
...(options.token === undefined ? {} : { token: options.token }),
|
|
...(options.certificate === undefined
|
|
? {}
|
|
: { certificate: options.certificate }),
|
|
...(options.origin === undefined ? {} : { origin: options.origin }),
|
|
...(options.keepaliveInterval === undefined
|
|
? {}
|
|
: { keepaliveInterval: options.keepaliveInterval }),
|
|
};
|
|
|
|
const remoteAddressRequired = (
|
|
options: z.infer<typeof remoteOptions>,
|
|
): boolean =>
|
|
options.remoteAddress !== undefined ||
|
|
(options.token === undefined &&
|
|
options.certificate === undefined &&
|
|
options.origin === undefined &&
|
|
options.keepaliveInterval === undefined);
|
|
|
|
cli.command(CLI_COMMANDS.listFridaDevices, {
|
|
description: "List local Frida devices and an optional remote endpoint",
|
|
args: z.object({}),
|
|
options: remoteOptions.refine(
|
|
remoteAddressRequired,
|
|
"Remote credentials and connection options require remoteAddress",
|
|
),
|
|
run: ({ options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.listFridaDevices, () =>
|
|
finishAnalysisCommand(
|
|
() => service.listDevices(remoteFrom(options)),
|
|
cleanup,
|
|
"frida",
|
|
"list_frida_devices",
|
|
signal,
|
|
),
|
|
),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.listFridaProcesses, {
|
|
description: "List processes visible through a Frida device",
|
|
args: z.object({}),
|
|
options: remoteOptions
|
|
.extend({
|
|
deviceId: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Frida device identifier"),
|
|
})
|
|
.refine(
|
|
(options) =>
|
|
options.remoteAddress === undefined || options.deviceId === undefined,
|
|
"Choose either deviceId or remoteAddress, not both",
|
|
)
|
|
.refine(
|
|
remoteAddressRequired,
|
|
"Remote credentials and connection options require remoteAddress",
|
|
),
|
|
run: ({ options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.listFridaProcesses, () => {
|
|
const remote = remoteFrom(options);
|
|
return finishAnalysisCommand(
|
|
() =>
|
|
service.listProcesses(
|
|
remote === undefined
|
|
? { deviceId: options.deviceId ?? "local" }
|
|
: { remote },
|
|
),
|
|
cleanup,
|
|
"frida",
|
|
"list_frida_processes",
|
|
signal,
|
|
);
|
|
}),
|
|
),
|
|
});
|
|
cli.command(CLI_COMMANDS.instrumentWithFrida, {
|
|
description:
|
|
"Run one Frida script against a selected process and detach; requires exactly one of --script or --script-file",
|
|
args: z.object({
|
|
mode: z
|
|
.enum(["attach", "spawn"])
|
|
.describe("Attach to a PID or spawn a program"),
|
|
target: z
|
|
.string()
|
|
.min(1)
|
|
.describe("Process ID for attach or program path for spawn"),
|
|
}),
|
|
options: remoteOptions
|
|
.extend({
|
|
deviceId: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Frida device identifier"),
|
|
script: z
|
|
.string()
|
|
.optional()
|
|
.describe(
|
|
"Inline JavaScript to load; supply exactly one of --script or --script-file",
|
|
),
|
|
scriptFile: z
|
|
.string()
|
|
.min(1)
|
|
.optional()
|
|
.describe("Path to JavaScript source file"),
|
|
argument: z
|
|
.array(z.string())
|
|
.optional()
|
|
.describe("Arguments passed to a spawned target"),
|
|
durationMs: z
|
|
.number()
|
|
.int()
|
|
.min(0)
|
|
.max(60_000)
|
|
.default(1_000)
|
|
.describe("Instrumentation duration in milliseconds"),
|
|
})
|
|
.refine(
|
|
(options) =>
|
|
options.remoteAddress === undefined || options.deviceId === undefined,
|
|
"Choose either deviceId or remoteAddress, not both",
|
|
)
|
|
.refine(
|
|
remoteAddressRequired,
|
|
"Remote credentials and connection options require remoteAddress",
|
|
),
|
|
run: ({ args, options }) =>
|
|
withCommandCancellation((signal) =>
|
|
logCliCommand(logger, CLI_COMMANDS.instrumentWithFrida, async () => {
|
|
if (args.mode === "attach" && options.argument !== undefined)
|
|
return projectAnalysisError(
|
|
new AnalysisInputError(
|
|
CLI_COMMANDS.instrumentWithFrida,
|
|
undefined,
|
|
[
|
|
{
|
|
path: ["argument"],
|
|
reason: "invalid_value",
|
|
message:
|
|
"--argument is only valid in spawn mode; remove it when attaching to a process.",
|
|
},
|
|
],
|
|
),
|
|
);
|
|
if (
|
|
(options.script === undefined) ===
|
|
(options.scriptFile === undefined)
|
|
)
|
|
return projectAnalysisError(
|
|
new AnalysisInputError(
|
|
CLI_COMMANDS.instrumentWithFrida,
|
|
undefined,
|
|
[
|
|
{
|
|
path: ["script"],
|
|
reason: "invalid_value",
|
|
message:
|
|
"Provide exactly one of --script or --script-file.",
|
|
},
|
|
],
|
|
),
|
|
);
|
|
const remote = remoteFrom(options);
|
|
const selected =
|
|
remote === undefined
|
|
? { deviceId: options.deviceId ?? "local" }
|
|
: { remote };
|
|
const targetInput =
|
|
args.mode === "attach"
|
|
? /^[1-9][0-9]*$/u.test(args.target) &&
|
|
Number.isSafeInteger(Number(args.target)) &&
|
|
Number(args.target) > 0
|
|
? {
|
|
...selected,
|
|
mode: "attach" as const,
|
|
pid: Number(args.target),
|
|
}
|
|
: undefined
|
|
: {
|
|
...selected,
|
|
mode: "spawn" as const,
|
|
program: args.target,
|
|
...(options.argument === undefined
|
|
? {}
|
|
: { argv: options.argument }),
|
|
};
|
|
if (targetInput === undefined)
|
|
return projectAnalysisError(
|
|
new AnalysisInputError(
|
|
CLI_COMMANDS.instrumentWithFrida,
|
|
undefined,
|
|
[
|
|
{
|
|
path: ["target"],
|
|
reason: "invalid_value",
|
|
message: "attach mode needs a positive integer process ID.",
|
|
},
|
|
],
|
|
),
|
|
);
|
|
return finishAnalysisCommand(
|
|
async () => {
|
|
const result = await service.instrument(
|
|
{
|
|
...targetInput,
|
|
source:
|
|
options.scriptFile === undefined
|
|
? { sourceKind: "inline", source: options.script ?? "" }
|
|
: { sourceKind: "file", path: options.scriptFile },
|
|
durationMs: options.durationMs,
|
|
},
|
|
signal,
|
|
);
|
|
if (!result.ok) return result;
|
|
if (result.value.cleanupError !== null) process.exitCode = 1;
|
|
return ok(result.value.evidence);
|
|
},
|
|
cleanup,
|
|
"frida",
|
|
"instrument_with_frida",
|
|
signal,
|
|
);
|
|
}),
|
|
),
|
|
});
|
|
};
|