mirror of
https://github.com/morluto/rea.git
synced 2026-11-07 05:56:19 -10:00
fix(frida): retain selected discovery endpoint on failure (#7892)
This commit is contained in:
1 parent
78a350e0b6
commit
e03ff93913
8 files changed
+209
-5
No files matched your search
@@ -1203,6 +1203,9 @@ it("redacts remote credentials and maps a missing optional binding", async () =>
|
||||
if (!failed.ok) {
|
||||
expect(failed.error.message).not.toContain(secret);
|
||||
expect(failed.error.message).not.toContain("synthetic-url-token");
|
||||
expect(JSON.stringify(projectAnalysisError(failed.error))).not.toContain(
|
||||
"synthetic-url-token",
|
||||
);
|
||||
}
|
||||
|
||||
const missing = new FridaInstrumentationManager({
|
||||
@@ -1216,6 +1219,80 @@ it("redacts remote credentials and maps a missing optional binding", async () =>
|
||||
expect(unavailable.error.message).toContain("Cannot find package 'frida'");
|
||||
});
|
||||
|
||||
it("retains the selected device or credential-free remote address on discovery and startup failures", async () => {
|
||||
const bindings = new FakeDeviceManager();
|
||||
bindings.getDeviceById = async () => {
|
||||
throw new Error("Device not found");
|
||||
};
|
||||
bindings.addRemoteDevice = async () => {
|
||||
throw new Error("Unable to connect to remote frida-server");
|
||||
};
|
||||
const manager = new FridaInstrumentationManager({ deviceManager: bindings });
|
||||
const device = await manager.listProcesses({ deviceId: "missing-device" });
|
||||
expect(device.ok).toBe(false);
|
||||
if (!device.ok) {
|
||||
expect(projectAnalysisError(device.error)).toMatchObject({
|
||||
code: "provider_timeout",
|
||||
details: { diagnostics: { device_id: "missing-device" } },
|
||||
});
|
||||
}
|
||||
const remote = { address: "127.0.0.1:27042", token: "secret-token" };
|
||||
for (const result of [
|
||||
await manager.listProcesses({ remote }),
|
||||
await manager.listDevices(remote),
|
||||
await manager.startSession({ mode: "attach", remote, pid: 512 }),
|
||||
]) {
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) continue;
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected).toMatchObject({
|
||||
code: "capability_unavailable",
|
||||
details: { diagnostics: { remote_address: remote.address } },
|
||||
});
|
||||
expect(JSON.stringify(projected)).not.toContain(remote.token);
|
||||
expect(projected.remediation.action).toContain("selected Frida server");
|
||||
}
|
||||
});
|
||||
|
||||
it("redacts credentials from inherited typed failure diagnostics", async () => {
|
||||
const bindings = new FakeDeviceManager();
|
||||
bindings.addRemoteDevice = async () => {
|
||||
throw new AnalysisTimeoutError("frida_device_lookup", 1000, {
|
||||
diagnostics: { detail: "token=synthetic-secret-token" },
|
||||
});
|
||||
};
|
||||
const manager = new FridaInstrumentationManager({ deviceManager: bindings });
|
||||
const result = await manager.listProcesses({
|
||||
remote: { address: "127.0.0.1:27042", token: "synthetic-secret-token" },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected).toMatchObject({
|
||||
code: "provider_timeout",
|
||||
details: { diagnostics: { remote_address: "127.0.0.1:27042" } },
|
||||
});
|
||||
expect(JSON.stringify(projected)).not.toContain("synthetic-secret-token");
|
||||
});
|
||||
|
||||
it("does not describe a connected device's enumeration failure as a connection failure", async () => {
|
||||
const bindings = new FakeDeviceManager();
|
||||
bindings.device.enumerateProcesses = async () => {
|
||||
throw new Error("process enumeration denied");
|
||||
};
|
||||
const manager = new FridaInstrumentationManager({ deviceManager: bindings });
|
||||
const result = await manager.listProcesses({
|
||||
remote: { address: "127.0.0.1:27042" },
|
||||
});
|
||||
expect(result.ok).toBe(false);
|
||||
if (result.ok) return;
|
||||
const projected = projectAnalysisError(result.error);
|
||||
expect(projected.details?.diagnostics).toMatchObject({
|
||||
remote_address: "127.0.0.1:27042",
|
||||
});
|
||||
expect(projected.remediation.action).not.toContain("selected Frida server");
|
||||
});
|
||||
|
||||
it("clears current binding failure after recovery", async () => {
|
||||
const bindings = new FakeDeviceManager();
|
||||
let attempts = 0;
|
||||
|
||||
@@ -13,7 +13,7 @@ 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 { isValidFridaRemoteAddress } from "../domain/fridaRemoteAddress.js";
|
||||
import type { Logger } from "pino";
|
||||
import type { CliInstance } from "./types.js";
|
||||
import { withCommandCancellation } from "./commandCancellation.js";
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, expect, it } from "vitest";
|
||||
|
||||
import { TOOL_CONTRACTS } from "./toolContracts.js";
|
||||
import { FRIDA_KEEPALIVE_INTERVAL_DESCRIPTION } from "./fridaToolContracts.js";
|
||||
import { isValidFridaRemoteAddress } from "./fridaRemoteAddress.js";
|
||||
import { isValidFridaRemoteAddress } from "../domain/fridaRemoteAddress.js";
|
||||
|
||||
const contract = (name: string) => {
|
||||
const found = TOOL_CONTRACTS.find((candidate) => candidate.name === name);
|
||||
|
||||
@@ -5,7 +5,7 @@ import type { ToolContract } from "./toolContractTypes.js";
|
||||
import { evidenceResultOf } from "./toolOutputSchemaPrimitives.js";
|
||||
import { analysisDetailOutputSchemaOf } from "./analysisDetail.js";
|
||||
import { toolContractMetadata } from "./toolEffects.js";
|
||||
import { isValidFridaRemoteAddress } from "./fridaRemoteAddress.js";
|
||||
import { isValidFridaRemoteAddress } from "../domain/fridaRemoteAddress.js";
|
||||
import {
|
||||
fridaSessionObservationSchema,
|
||||
fridaSessionStatusInputSchema,
|
||||
|
||||
File renamed without changes.
@@ -32,7 +32,9 @@ import {
|
||||
AnalysisInputError,
|
||||
AnalysisTimeoutError,
|
||||
} from "../domain/analysisErrorCore.js";
|
||||
import type { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { AnalysisError } from "../domain/analysisErrorBase.js";
|
||||
import { isValidFridaRemoteAddress } from "../domain/fridaRemoteAddress.js";
|
||||
import { redactExplicitFailure } from "../domain/explicitSensitiveFailure.js";
|
||||
import { ProviderCleanupError } from "../domain/providerCleanupError.js";
|
||||
import { AnalysisCleanupLease } from "../artifacts/AnalysisCleanupLease.js";
|
||||
import type { JsonValue } from "../domain/jsonValue.js";
|
||||
@@ -215,6 +217,7 @@ export class FridaInstrumentationManager implements FridaInstrumentationPort {
|
||||
cause,
|
||||
remote,
|
||||
cleanupError,
|
||||
remote === undefined ? undefined : { remote },
|
||||
);
|
||||
const reference =
|
||||
remote === undefined
|
||||
@@ -285,6 +288,7 @@ export class FridaInstrumentationManager implements FridaInstrumentationPort {
|
||||
cause,
|
||||
selector.remote,
|
||||
cleanupError,
|
||||
selector,
|
||||
);
|
||||
const reference =
|
||||
selector.remote === undefined
|
||||
@@ -395,6 +399,7 @@ export class FridaInstrumentationManager implements FridaInstrumentationPort {
|
||||
cause,
|
||||
input.remote,
|
||||
cleanupFailures.join("; ") || undefined,
|
||||
input,
|
||||
);
|
||||
if (pendingCleanup !== undefined && failure.cleanupIncomplete)
|
||||
this.#retainCleanupFailure(pendingCleanup, failure);
|
||||
@@ -1597,12 +1602,39 @@ const providerError = (
|
||||
cause: unknown,
|
||||
remote?: FridaRemoteConnection,
|
||||
cleanupReason?: string,
|
||||
selector?: FridaDeviceSelector,
|
||||
): AnalysisError => {
|
||||
const secrets = [remote?.token, remote?.certificate].filter(
|
||||
(secret): secret is string => secret !== undefined && secret.length > 0,
|
||||
);
|
||||
const selection =
|
||||
selector?.remote === undefined
|
||||
? selector?.deviceId
|
||||
: isValidFridaRemoteAddress(selector.remote.address)
|
||||
? selector.remote.address
|
||||
: undefined;
|
||||
const selectionKey =
|
||||
selector?.remote === undefined ? "device_id" : "remote_address";
|
||||
const diagnostics = {
|
||||
...(cause instanceof AnalysisError
|
||||
? redactExplicitFailure(cause, secrets).diagnostics
|
||||
: {}),
|
||||
...(selection === undefined
|
||||
? {}
|
||||
: {
|
||||
[selectionKey]: boundedRedactedString(selection, 4096, secrets).value,
|
||||
}),
|
||||
};
|
||||
if (cause instanceof AnalysisInputError)
|
||||
return new AnalysisInputError(operation, { cause }, cause.issues);
|
||||
return new AnalysisInputError(
|
||||
operation,
|
||||
{ cause, diagnostics },
|
||||
cause.issues,
|
||||
);
|
||||
if (cause instanceof AnalysisTimeoutError)
|
||||
return new AnalysisTimeoutError(operation, cause.timeoutMs, {
|
||||
cause,
|
||||
diagnostics,
|
||||
...(cause.userMessage === undefined
|
||||
? {}
|
||||
: { userMessage: cause.userMessage }),
|
||||
@@ -1621,6 +1653,7 @@ const providerError = (
|
||||
if (cause instanceof AnalysisCancelledError)
|
||||
return new AnalysisCancelledError(operation, {
|
||||
cause: cause.cause,
|
||||
diagnostics,
|
||||
...(cleanupReason === undefined
|
||||
? {}
|
||||
: {
|
||||
@@ -1633,6 +1666,15 @@ const providerError = (
|
||||
const reason = sanitizeError(cause, remote);
|
||||
return new AnalysisCapabilityUnavailableError("frida", operation, reason, {
|
||||
cause: remote === undefined ? cause : new Error(reason),
|
||||
diagnostics,
|
||||
...(selector?.remote === undefined ||
|
||||
cleanupReason !== undefined ||
|
||||
reason !== "Unable to connect to remote frida-server"
|
||||
? {}
|
||||
: {
|
||||
remediationAction:
|
||||
"Confirm the selected Frida server is reachable and its connection options are correct, then retry.",
|
||||
}),
|
||||
...(cleanupReason === undefined
|
||||
? {}
|
||||
: {
|
||||
|
||||
@@ -11,6 +11,53 @@ import type { FridaInstrumentationPort } from "../../../src/application/frida/Fr
|
||||
import { AnalysisCapabilityUnavailableError } from "../../../src/domain/analysisErrorCore.js";
|
||||
import { FridaInstrumentationManager } from "../../../src/frida/FridaInstrumentationManager.js";
|
||||
|
||||
it.each([
|
||||
{
|
||||
option: "--device-id",
|
||||
value: "missing-device",
|
||||
error: "Device not found",
|
||||
field: "device_id",
|
||||
code: "provider_timeout",
|
||||
},
|
||||
{
|
||||
option: "--remote-address",
|
||||
value: "127.0.0.1:27042",
|
||||
error: "Unable to connect to remote frida-server",
|
||||
field: "remote_address",
|
||||
code: "capability_unavailable",
|
||||
},
|
||||
])(
|
||||
"retains selected Frida endpoint in CLI discovery failures: $field",
|
||||
async ({ option, value, error, field, code }) => {
|
||||
const bindings = {
|
||||
getDeviceById: async () => {
|
||||
throw new Error(error);
|
||||
},
|
||||
addRemoteDevice: async () => {
|
||||
throw new Error(error);
|
||||
},
|
||||
} as unknown as DeviceManager;
|
||||
const provider = new FridaInstrumentationManager({
|
||||
deviceManager: bindings,
|
||||
});
|
||||
let stdout = "";
|
||||
await createCli({}, undefined, provider).serve(
|
||||
["list-frida-processes", option, value, "--json"],
|
||||
{
|
||||
env: {},
|
||||
exit: () => {},
|
||||
stdout: (text) => {
|
||||
stdout += text;
|
||||
},
|
||||
},
|
||||
);
|
||||
expect(JSON.parse(stdout)).toMatchObject({
|
||||
code,
|
||||
details: { diagnostics: { [field]: value } },
|
||||
});
|
||||
},
|
||||
);
|
||||
|
||||
it.each([
|
||||
{ devices: [], cleanupError: null },
|
||||
{
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
|
||||
import { Ajv2020 } from "ajv/dist/2020.js";
|
||||
import type { AnySchema } from "ajv";
|
||||
import type { DeviceManager } from "frida";
|
||||
import { expect, it, onTestFinished } from "vitest";
|
||||
|
||||
import type { FridaInstrumentationPort } from "../../../src/application/frida/FridaInstrumentationPort.js";
|
||||
@@ -15,6 +16,43 @@ import { evidenceBundleSummarySchema } from "../../../src/domain/evidenceRecordS
|
||||
import { createEvidence, parseEvidence } from "../../../src/domain/evidence.js";
|
||||
import { parseMcpEvidenceBundle } from "../../fixtures/mcpEvidenceBundle.js";
|
||||
|
||||
it("retains the remote address, but not authentication, in discovery failures", async () => {
|
||||
const session = createTestBinarySession(() => {
|
||||
throw new Error("unexpected binary provider");
|
||||
});
|
||||
const bindings = {
|
||||
addRemoteDevice: async () => {
|
||||
throw new Error("connection refused: synthetic-secret-token");
|
||||
},
|
||||
} as unknown as DeviceManager;
|
||||
const provider = new FridaInstrumentationManager({ deviceManager: bindings });
|
||||
const server = createServer(
|
||||
{ kind: "session", session },
|
||||
{ fridaInstrumentation: provider },
|
||||
);
|
||||
const client = await connectFridaTestClient(
|
||||
server,
|
||||
session,
|
||||
"frida-discovery-failure",
|
||||
);
|
||||
const result = await client.callTool({
|
||||
name: "list_frida_processes",
|
||||
arguments: {
|
||||
remote: { address: "127.0.0.1:27042", token: "synthetic-secret-token" },
|
||||
},
|
||||
});
|
||||
expect(result.isError).toBe(true);
|
||||
const first = result.content[0];
|
||||
if (first?.type !== "text") throw new Error("Expected text failure");
|
||||
expect(JSON.parse(first.text)).toMatchObject({
|
||||
error: {
|
||||
code: "capability_unavailable",
|
||||
details: { diagnostics: { remote_address: "127.0.0.1:27042" } },
|
||||
},
|
||||
});
|
||||
expect(JSON.stringify(result)).not.toContain("synthetic-secret-token");
|
||||
});
|
||||
|
||||
const createFridaTestProvider = (
|
||||
overrides: Partial<FridaInstrumentationPort> = {},
|
||||
): FridaInstrumentationPort => ({
|
||||
|
||||
Reference in new issue
Block a user