mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
209 lines
6.9 KiB
TypeScript
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]) },
|
|
});
|
|
},
|
|
);
|