Files
reverse-engineering-anything/tests/boundary/mcp/mobileApplicationGraphMcp.test.ts

209 lines
6.9 KiB
TypeScript

import { writeFile } from "node:fs/promises";
import { join } from "node:path";
import { Client, InMemoryTransport } from "@modelcontextprotocol/client";
import {
TextReader,
Uint8ArrayReader,
Uint8ArrayWriter,
ZipWriter,
} from "@zip.js/zip.js";
import { afterEach, expect, it } from "vitest";
import { createDirectAnalysis } from "../../../src/composition/directAnalysis.js";
import { parseEvidence } from "../../../src/domain/evidence.js";
import { fragmentInventoryEvidence } from "../../fixtures/artifactEvidence.js";
import { createTestBinarySession } from "../../fixtures/binarySession.js";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
import { createServer } from "../../../src/server/createServer.js";
const { runProviderAnalysis } = createDirectAnalysis({});
const resources: Array<{ close(): Promise<unknown> }> = [];
afterEach(async () => {
for (const resource of resources.splice(0)) await resource.close();
});
async function connect() {
const session = createTestBinarySession(() => {
throw new Error("mobile graph projection must not launch a provider");
});
const server = createServer({ kind: "session", session });
const client = new Client({
name: "mobile-application-graph-test",
version: "1",
});
resources.push(client, server, session);
const [a, b] = InMemoryTransport.createLinkedPair();
await server.connect(b);
await client.connect(a);
return client;
}
const writeApk = async (directory: string): Promise<string> => {
const path = join(directory, "Fixture.apk");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add(
"AndroidManifest.xml",
new Uint8ArrayReader(Uint8Array.from([3, 0, 8, 0])),
);
await writer.add(
"classes.dex",
new Uint8ArrayReader(
Uint8Array.from([0x64, 0x65, 0x78, 0x0a, 0x30, 0x33, 0x35, 0]),
),
);
await writeFile(path, await writer.close());
return path;
};
const writeIpa = async (directory: string): Promise<string> => {
const path = join(directory, "Fixture.ipa");
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add(
"Payload/Fixture.app/Info.plist",
new TextReader("<plist></plist>"),
);
await writer.add(
"Payload/Fixture.app/Fixture",
new Uint8ArrayReader(
Uint8Array.from([0xcf, 0xfa, 0xed, 0xfe, 0x0c, 0, 0, 1]),
),
);
await writeFile(path, await writer.close());
return path;
};
it("advertises and executes Android and Apple inventory projections", async () => {
const client = await connect();
const { tools } = await client.listTools();
const names = new Set(tools.map(({ name }) => name));
expect(names.has("project_android_application_graph")).toBe(true);
expect(names.has("project_apple_application_graph")).toBe(true);
const root = await createTestTempDirectory("rea-mobile-graph-mcp-");
const apkInventory = parseEvidence(
await runProviderAnalysis(await writeApk(root), "inventory_artifact", {}),
);
const apkSources = fragmentInventoryEvidence(apkInventory, 2).toReversed();
const android = await client.callTool({
name: "project_android_application_graph",
arguments: { inventory_evidence: apkSources },
});
if (android.isError === true)
throw new Error(JSON.stringify(android, null, 2));
expect(android.structuredContent).toMatchObject({
operation: "project_android_application_graph",
provider: { id: "rea-android-application" },
});
const androidEvidence = parseEvidence(android.structuredContent);
const androidBundle = await client.callTool({
name: "get_evidence_bundle",
arguments: {},
});
expect(androidBundle.structuredContent).toMatchObject({
result: {
records: expect.arrayContaining([...apkSources, androidEvidence]),
},
});
const tamperedSource = {
...apkSources[0]!,
evidence_id: apkSources[1]!.evidence_id,
};
const rejected = await client.callTool({
name: "project_android_application_graph",
arguments: { inventory_evidence: [tamperedSource] },
});
expect(rejected.isError).toBe(true);
const bundleAfterRejectedSource = await client.callTool({
name: "get_evidence_bundle",
arguments: {},
});
expect(bundleAfterRejectedSource.structuredContent).toMatchObject({
result: {
records: expect.arrayContaining([...apkSources, androidEvidence]),
},
});
expect(bundleAfterRejectedSource.structuredContent).toEqual(
androidBundle.structuredContent,
);
const ipaInventory = parseEvidence(
await runProviderAnalysis(await writeIpa(root), "inventory_artifact", {}),
);
const apple = await client.callTool({
name: "project_apple_application_graph",
arguments: { inventory_evidence: [ipaInventory] },
});
if (apple.isError === true) throw new Error(JSON.stringify(apple, null, 2));
expect(apple.structuredContent).toMatchObject({
operation: "project_apple_application_graph",
provider: { id: "rea-apple-application" },
});
});
it.each([
{
family: "Harmony",
name: "Locked.hap",
manifest: "module.json",
unread: "ets/modules.abc",
operation: "project_harmony_application_graph",
},
{
family: "Android",
name: "Locked.apk",
manifest: "AndroidManifest.xml",
unread: "classes.dex",
operation: "project_android_application_graph",
},
{
family: "Apple",
name: "Locked.ipa",
manifest: "Payload/Locked.app/Info.plist",
unread: "Payload/Locked.app/libentry.dylib",
operation: "project_apple_application_graph",
},
])(
"retains $family unavailable components and their source Evidence through MCP",
async ({ name, manifest, unread, operation }) => {
const client = await connect();
const root = await createTestTempDirectory("rea-harmony-graph-mcp-");
const path = join(root, name);
const writer = new ZipWriter(new Uint8ArrayWriter());
await writer.add(manifest, new TextReader("{}"));
await writer.add(unread, new TextReader("opaque bytecode"), {
password: "fixture-only",
});
await writeFile(path, await writer.close());
const inventory = parseEvidence(
await runProviderAnalysis(path, "inventory_artifact", {}),
);
const reply = await client.callTool({
name: operation,
arguments: { inventory_evidence: [inventory] },
});
if (reply.isError === true) throw new Error(JSON.stringify(reply));
const evidence = parseEvidence(reply.structuredContent);
expect(evidence.normalized_result).toMatchObject({
unavailable_components: [
expect.objectContaining({
path: unread,
artifact_id: null,
sha256: null,
encrypted: true,
}),
],
coverage: { status: "partial", inventory_complete: true },
});
const bundle = await client.callTool({
name: "get_evidence_bundle",
arguments: {},
});
expect(bundle.structuredContent).toMatchObject({
result: { records: expect.arrayContaining([inventory, evidence]) },
});
},
);