mirror of
https://github.com/morluto/rea.git
synced 2026-10-12 02:19:24 +00:00
296 lines
9.9 KiB
JavaScript
296 lines
9.9 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import { mkdir, mkdtemp, readdir, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import { promisify } from "node:util";
|
|
import { fileURLToPath } from "node:url";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
|
|
import { parseEvidence } from "../../dist/domain/evidence.js";
|
|
import { analysisProfileSchema } from "../../dist/domain/analysisProfile.js";
|
|
import { nativeValueTraceSchema } from "../../dist/domain/native/nativeValueTrace.js";
|
|
import { verifyGhidraBoundaries } from "./real-ghidra-boundaries.mjs";
|
|
|
|
const defaultEntrypoint = fileURLToPath(new URL("../rea.mjs", import.meta.url));
|
|
const environment = () => ({
|
|
PATH: process.env.PATH ?? "/usr/bin:/bin",
|
|
REA_LOG_LEVEL: "silent",
|
|
REA_ANALYSIS_PROVIDER: "ghidra",
|
|
HOPPER_LAUNCHER_PATH: "/rea-unconfigured-deep-provider/hopper",
|
|
...(process.env.GHIDRA_INSTALL_DIR === undefined
|
|
? {}
|
|
: { GHIDRA_INSTALL_DIR: process.env.GHIDRA_INSTALL_DIR }),
|
|
...(process.env.JAVA_HOME === undefined
|
|
? {}
|
|
: { JAVA_HOME: process.env.JAVA_HOME }),
|
|
...(process.env.GHIDRA_HEADLESS_MAXMEM === undefined
|
|
? {}
|
|
: { GHIDRA_HEADLESS_MAXMEM: process.env.GHIDRA_HEADLESS_MAXMEM }),
|
|
...(process.env.GHIDRA_MAXMEM === undefined
|
|
? {}
|
|
: { GHIDRA_MAXMEM: process.env.GHIDRA_MAXMEM }),
|
|
...(process.env._JAVA_OPTIONS === undefined
|
|
? {}
|
|
: { _JAVA_OPTIONS: process.env._JAVA_OPTIONS }),
|
|
});
|
|
|
|
const verifyGraph = (input, target, procedure, globalAddress) => {
|
|
const graph = nativeValueTraceSchema.parse(input);
|
|
assert.equal(graph.target_sha256, target.sha256);
|
|
assert.equal(graph.seed, procedure);
|
|
const nodes = new Map(graph.nodes.map((node) => [node.id, node]));
|
|
for (const kind of ["argument-binding", "parameter-use", "return-binding"]) {
|
|
const edges = graph.edges.filter((edge) => edge.kind === kind);
|
|
assert.ok(edges.length > 0, `Real native dependency graph lacks ${kind}`);
|
|
for (const edge of edges) {
|
|
const source = nodes.get(edge.source);
|
|
const destination = nodes.get(edge.target);
|
|
assert.ok(source, `Missing source node ${edge.source}`);
|
|
assert.ok(destination, `Missing target node ${edge.target}`);
|
|
assert.equal(edge.status, "derived");
|
|
if (kind === "argument-binding") {
|
|
assert.match(source.operation?.opcode ?? "", /^CALL(?:IND)?$/u);
|
|
assert.equal(destination.kind, "parameter");
|
|
assert.equal(edge.input_index, destination.parameter.ordinal + 1);
|
|
} else if (kind === "parameter-use") {
|
|
assert.equal(source.kind, "parameter");
|
|
assert.ok(destination.operation.inputs[edge.input_index]);
|
|
} else {
|
|
assert.equal(source.operation?.opcode, "RETURN");
|
|
assert.match(destination.operation?.opcode ?? "", /^CALL(?:IND)?$/u);
|
|
assert.equal(edge.input_index, 1);
|
|
}
|
|
}
|
|
}
|
|
assert.ok(
|
|
graph.nodes.some((node) =>
|
|
node.operation?.inputs.some((value) => value.location === globalAddress),
|
|
),
|
|
"Source-owned global operand is absent from the real dependency graph",
|
|
);
|
|
for (const node of graph.nodes)
|
|
assert.match(node.evidence_id, /^ev_[a-f0-9]{64}$/u);
|
|
for (const procedure of graph.procedures) {
|
|
assert.equal(procedure.provider, "ghidra");
|
|
assert.match(procedure.analysis_profile_digest, /^[a-f0-9]{64}$/u);
|
|
}
|
|
return graph;
|
|
};
|
|
|
|
/** Verify real Ghidra dependency composition through production CLI and stdio MCP. */
|
|
export async function verifyNativeValueE2e(
|
|
target,
|
|
procedure,
|
|
globalAddress,
|
|
{ entrypoint = defaultEntrypoint } = {},
|
|
) {
|
|
const workspace = await mkdtemp(join(tmpdir(), "rea-native-values-e2e-"));
|
|
const runtime = join(
|
|
workspace,
|
|
`runtime${process.platform === "win32" ? "" : " with spaces"}-${"x".repeat(100)}`,
|
|
);
|
|
await mkdir(runtime);
|
|
const env = { ...environment(), TMPDIR: runtime };
|
|
try {
|
|
return await verifyNativeValueAdapters(
|
|
target,
|
|
procedure,
|
|
globalAddress,
|
|
entrypoint,
|
|
env,
|
|
);
|
|
} finally {
|
|
await rm(workspace, { recursive: true, force: true });
|
|
}
|
|
}
|
|
|
|
async function verifyNativeValueAdapters(
|
|
target,
|
|
procedure,
|
|
globalAddress,
|
|
entrypoint,
|
|
env,
|
|
) {
|
|
const { stdout } = await promisify(execFile)(
|
|
process.execPath,
|
|
[
|
|
entrypoint,
|
|
"trace-native-values",
|
|
target.path,
|
|
procedure,
|
|
"--provider",
|
|
"ghidra",
|
|
"--max-depth",
|
|
"2",
|
|
"--max-functions",
|
|
"8",
|
|
"--limit",
|
|
"5000",
|
|
"--json",
|
|
],
|
|
{ env, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
|
|
);
|
|
const evidence = parseEvidence(JSON.parse(stdout));
|
|
assert.equal(evidence.operation, "trace_native_values");
|
|
assert.equal(evidence.provider.id, "rea-workflow");
|
|
const workflowProfile = analysisProfileSchema.parse(
|
|
evidence.analysis_profile,
|
|
);
|
|
assert.equal(workflowProfile.provider.id, "rea-workflow");
|
|
const upstreamProfile = analysisProfileSchema.parse(
|
|
workflowProfile.parameters.upstream_analysis_profile,
|
|
);
|
|
assert.equal(upstreamProfile.provider.id, "ghidra");
|
|
const cliGraph = verifyGraph(
|
|
evidence.normalized_result,
|
|
target,
|
|
procedure,
|
|
globalAddress,
|
|
);
|
|
for (const procedure of cliGraph.procedures)
|
|
assert.equal(procedure.analysis_profile_digest, upstreamProfile.digest);
|
|
const transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [entrypoint, "mcp"],
|
|
env,
|
|
stderr: "pipe",
|
|
});
|
|
const client = new Client({ name: "native-values-real-e2e", version: "1" });
|
|
let boundaries;
|
|
try {
|
|
await client.connect(transport);
|
|
const opened = await client.callTool(
|
|
{
|
|
name: "open_binary",
|
|
arguments: { path: target.path, provider_id: "ghidra" },
|
|
},
|
|
{ timeout: 180000 },
|
|
);
|
|
assert.notEqual(opened.isError, true, JSON.stringify(opened));
|
|
const inventory = await client.callTool({
|
|
name: "binary_session",
|
|
arguments: {},
|
|
});
|
|
assert.notEqual(inventory.isError, true, JSON.stringify(inventory));
|
|
assert.ok(
|
|
inventory.structuredContent?.result?.tool_availability.some(
|
|
(tool) => tool.name === "trace_native_values" && tool.available,
|
|
),
|
|
"Native value tracing is unavailable in real provider discovery",
|
|
);
|
|
const result = await client.callTool(
|
|
{
|
|
name: "trace_native_values",
|
|
arguments: { procedure, max_depth: 2, max_functions: 8, limit: 5000 },
|
|
},
|
|
{ timeout: 180000 },
|
|
);
|
|
assert.notEqual(result.isError, true, JSON.stringify(result));
|
|
const mcpEvidence = parseEvidence(result.structuredContent);
|
|
assert.equal(mcpEvidence.operation, "trace_native_values");
|
|
assert.equal(mcpEvidence.provider.id, "rea-workflow");
|
|
assert.deepEqual(mcpEvidence.analysis_profile, workflowProfile);
|
|
const mcpGraph = verifyGraph(
|
|
result.structuredContent?.normalized_result,
|
|
target,
|
|
procedure,
|
|
globalAddress,
|
|
);
|
|
assert.deepEqual(mcpEvidence.normalized_result, mcpGraph);
|
|
assert.deepEqual(mcpGraph, cliGraph);
|
|
const analyzed = await client.callTool(
|
|
{ name: "analyze_function", arguments: { procedure } },
|
|
{ timeout: 180000 },
|
|
);
|
|
assert.notEqual(analyzed.isError, true, JSON.stringify(analyzed));
|
|
const functionEvidence = parseEvidence(analyzed.structuredContent);
|
|
assert.equal(functionEvidence.operation, "analyze_function");
|
|
assert.equal(functionEvidence.provider.id, "ghidra");
|
|
const functionProfile = analysisProfileSchema.parse(
|
|
functionEvidence.analysis_profile,
|
|
);
|
|
assert.deepEqual(functionProfile, upstreamProfile);
|
|
assert.deepEqual(
|
|
functionEvidence.normalized_result,
|
|
analyzed.structuredContent?.normalized_result,
|
|
);
|
|
const comparison = await client.callTool(
|
|
{
|
|
name: "compare_functions",
|
|
arguments: {
|
|
left: analyzed.structuredContent,
|
|
right: analyzed.structuredContent,
|
|
},
|
|
},
|
|
{ timeout: 180000 },
|
|
);
|
|
assert.notEqual(comparison.isError, true, JSON.stringify(comparison));
|
|
const comparisonEvidence = parseEvidence(comparison.structuredContent);
|
|
assert.equal(comparisonEvidence.normalized_result.status, "unchanged");
|
|
assert.deepEqual(
|
|
comparisonEvidence.normalized_result,
|
|
comparison.structuredContent?.normalized_result,
|
|
);
|
|
assert.ok(
|
|
comparisonEvidence.evidence_links.includes(functionEvidence.evidence_id),
|
|
);
|
|
const bundle = await client.callTool({
|
|
name: "get_evidence_bundle",
|
|
arguments: {},
|
|
});
|
|
assert.notEqual(bundle.isError, true, JSON.stringify(bundle));
|
|
for (const evidence of [
|
|
mcpEvidence,
|
|
functionEvidence,
|
|
comparisonEvidence,
|
|
]) {
|
|
assert.deepEqual(
|
|
bundle.structuredContent?.result?.records.find(
|
|
(record) => record.evidence_id === evidence.evidence_id,
|
|
),
|
|
evidence,
|
|
);
|
|
}
|
|
boundaries = await verifyGhidraBoundaries(client, {
|
|
target,
|
|
procedure,
|
|
entrypoint,
|
|
env,
|
|
});
|
|
const closed = await client.callTool({
|
|
name: "close_binary",
|
|
arguments: {},
|
|
});
|
|
assert.notEqual(closed.isError, true, JSON.stringify(closed));
|
|
const status = await client.callTool({
|
|
name: "binary_session",
|
|
arguments: {},
|
|
});
|
|
assert.equal(status.structuredContent?.result?.open, false);
|
|
assert.deepEqual(
|
|
(await readdir(env.TMPDIR)).filter((name) =>
|
|
name.startsWith("rea-ghidra-"),
|
|
),
|
|
[],
|
|
"Ghidra left runtime files after close",
|
|
);
|
|
} finally {
|
|
await client.close();
|
|
await transport.close();
|
|
}
|
|
return {
|
|
mocked: false,
|
|
cli: true,
|
|
stdio_mcp: true,
|
|
inline_evidence: true,
|
|
direct_function_comparison: true,
|
|
boundaries,
|
|
nodes: cliGraph.total_nodes,
|
|
edges: cliGraph.total_edges,
|
|
decompilations: cliGraph.decompilations,
|
|
unknowns: cliGraph.unknowns.length,
|
|
};
|
|
}
|