Repository navigation
Expand file tree
/
Copy pathfridaCommands.ts
More file actions
292 lines (287 loc) · 10.1 KB
/
Copy pathfridaCommands.ts
File metadata and controls
292 lines (287 loc) · 10.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
import { projectCliAnalysisResult } from "./projectCliAnalysisResult.js";
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 "../domain/fridaRemoteAddress.js";
import type { Logger } from "pino";
import type { CliInstance } from "./types.js";
import { withCommandCancellation } from "./commandCancellation.js";
import { finishAnalysisCommand } from "./finishAnalysisCommand.js";
/** Script sources stay optional in the options schema; exactly one is required. */
const exactlyOneScriptSource =
"Provide exactly one of --script or --script-file.";
/** 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,
).then(projectCliAnalysisResult),
),
),
});
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,
).then(projectCliAnalysisResult);
}),
),
});
cli.command(CLI_COMMANDS.instrumentWithFrida, {
description: `Run one Frida script against a selected process and detach. ${exactlyOneScriptSource}`,
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. ${exactlyOneScriptSource}`),
scriptFile: z
.string()
.min(1)
.optional()
.describe(
`Path to JavaScript source file. ${exactlyOneScriptSource}`,
),
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,
).then(projectCliAnalysisResult);
}),
),
});
};