mirror of
https://github.com/morluto/rea
synced 2026-10-25 22:08:11 -04:00
277 lines
8.8 KiB
JavaScript
277 lines
8.8 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createHash } from "node:crypto";
|
|
import { createWriteStream } from "node:fs";
|
|
import { mkdir, mkdtemp, open, readFile, writeFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join, resolve } from "node:path";
|
|
import { finished } from "node:stream/promises";
|
|
import { parseArgs } from "node:util";
|
|
import { pathToFileURL } from "node:url";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
let parseEvidence, createEvidenceBundle;
|
|
import { createRetainedQueryCaller } from "./retained-query-calls.mjs";
|
|
import { finalizeRetainedQueryReceipt } from "./retained-query-finalization.mjs";
|
|
const { values } = parseArgs({
|
|
options: Object.fromEntries(
|
|
["runtime", "target", "output", "ghidra", "java", "profile"].map((key) => [
|
|
key,
|
|
{ type: "string" },
|
|
]),
|
|
),
|
|
});
|
|
for (const key of [
|
|
"runtime",
|
|
"target",
|
|
"output",
|
|
"ghidra",
|
|
"java",
|
|
"profile",
|
|
]) {
|
|
assert.ok(values[key], `Missing --${key}`);
|
|
values[key] = resolve(values[key]);
|
|
}
|
|
const target = values.target,
|
|
output = values.output,
|
|
repo = values.runtime;
|
|
await mkdir(output);
|
|
const report = {
|
|
target,
|
|
profile_scope:
|
|
"Ghidra-reported function identities and defined strings under explicit disabled-analyzer profile; no claim of default-analysis or function-body completeness",
|
|
calls: [],
|
|
},
|
|
transcript = [];
|
|
let account,
|
|
transport,
|
|
serverLog,
|
|
serverLogFinished,
|
|
client,
|
|
call,
|
|
opened = false;
|
|
const start = performance.now();
|
|
try {
|
|
({ parseEvidence } = await import(
|
|
pathToFileURL(join(repo, "dist/domain/evidence.js")).href
|
|
));
|
|
({ createEvidenceBundle } = await import(
|
|
pathToFileURL(join(repo, "dist/domain/evidenceBundle.js")).href
|
|
));
|
|
report.sha256 = createHash("sha256")
|
|
.update(await readFile(target))
|
|
.digest("hex");
|
|
const disabled = JSON.parse(await readFile(values.profile, "utf8")).names;
|
|
report.disabled_analyzers = disabled;
|
|
account = await mkdtemp(join(tmpdir(), "rea-large-native-inventory-"));
|
|
transport = new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [join(repo, "scripts/rea.mjs"), "mcp"],
|
|
cwd: repo,
|
|
env: {
|
|
HOME: account,
|
|
PATH: process.env.PATH,
|
|
NODE_OPTIONS: "--max-old-space-size=3072",
|
|
GHIDRA_INSTALL_DIR: values.ghidra,
|
|
JAVA_HOME: values.java,
|
|
GHIDRA_HEADLESS_MAXMEM: "3072M",
|
|
REA_GHIDRA_STARTUP_TIMEOUT_MS: "600000",
|
|
REA_GHIDRA_DISABLED_ANALYZERS: disabled.join(","),
|
|
REA_MCP_INPUT_SCHEMA_PROFILE: "compact",
|
|
REA_LOG_LEVEL: "silent",
|
|
},
|
|
stderr: "pipe",
|
|
});
|
|
serverLog = createWriteStream(join(output, "server.log"));
|
|
serverLogFinished = finished(serverLog);
|
|
serverLogFinished.catch(() => {});
|
|
transport.stderr?.pipe(serverLog);
|
|
client = new Client({
|
|
name: "large-native-inventory-verifier",
|
|
version: "1",
|
|
});
|
|
call = createRetainedQueryCaller(client, (receipt, event) => {
|
|
report.calls.push(receipt);
|
|
transcript.push(event);
|
|
});
|
|
await client.connect(transport);
|
|
const catalog = await client.listTools();
|
|
await writeFile(join(output, "catalog.json"), JSON.stringify(catalog));
|
|
const defs = new Map(catalog.tools.map((t) => [t.name, t]));
|
|
const original = client.callTool.bind(client);
|
|
client.callTool = (input, options) =>
|
|
original(input, {
|
|
...options,
|
|
timeout: 900000,
|
|
toolDefinition: defs.get(input.name),
|
|
});
|
|
const raw = async (name, args, phase) => {
|
|
const r = await call(name, args, phase);
|
|
if (r.isError) {
|
|
const e = JSON.parse(r.content.find((c) => c.type === "text").text).error;
|
|
throw new Error(
|
|
name +
|
|
": " +
|
|
e.code +
|
|
": " +
|
|
(e.details?.diagnostics?.failure_message ?? e.message),
|
|
);
|
|
}
|
|
return r;
|
|
};
|
|
const good = async (name, args, phase) =>
|
|
parseEvidence((await raw(name, args, phase)).structuredContent);
|
|
await raw("open_binary", { path: target, provider_id: "ghidra" }, "open");
|
|
opened = true;
|
|
const procedures = await good(
|
|
"list_procedures",
|
|
{ detail: "summary" },
|
|
"functions",
|
|
);
|
|
report.procedures = procedures.normalized_result;
|
|
const full = await call("list_strings", {}, "complete-strings");
|
|
report.complete_strings_refused = full.isError === true;
|
|
if (full.isError) {
|
|
const error = JSON.parse(
|
|
full.content.find((c) => c.type === "text").text,
|
|
).error;
|
|
assert.equal(error.code, "resource_constraint");
|
|
assert.equal(error.details.reported_limits.boundary, "mcp-response");
|
|
report.overflow_evidence_id =
|
|
error.details.reported_limits.evidence_reference.evidence_id;
|
|
}
|
|
const parentId =
|
|
report.overflow_evidence_id ??
|
|
parseEvidence(full.structuredContent).evidence_id;
|
|
const strings = await good(
|
|
"inspect_analysis_view",
|
|
{
|
|
source: { kind: "retained-evidence", evidence_id: parentId },
|
|
view: { kind: "summary" },
|
|
},
|
|
"summary-strings",
|
|
);
|
|
const summary = strings.normalized_result;
|
|
report.strings = summary;
|
|
if (report.overflow_evidence_id)
|
|
assert.equal(summary.parent_evidence_id, report.overflow_evidence_id);
|
|
const selected = [];
|
|
const total = summary.summary.row_count;
|
|
for (const offset of [...new Set([0, Math.max(0, total - 3)])]) {
|
|
const e = await good(
|
|
"inspect_analysis_view",
|
|
{
|
|
source: {
|
|
kind: "retained-evidence",
|
|
evidence_id: summary.parent_evidence_id,
|
|
},
|
|
view: { kind: "native-inventory", offset, limit: 3, fields: ["value"] },
|
|
},
|
|
"selected-strings",
|
|
);
|
|
assert.equal(e.normalized_result.coverage.total, total);
|
|
selected.push(...e.normalized_result.rows);
|
|
}
|
|
report.byte_oracles = [];
|
|
const file = await open(target, "r");
|
|
try {
|
|
for (const row of selected) {
|
|
const mapping = await good(
|
|
"address_to_file_offset",
|
|
{ address: row.record.address },
|
|
"string-file-coordinate",
|
|
);
|
|
const offset = mapping.normalized_result.file_offset;
|
|
assert.ok(Number.isSafeInteger(offset));
|
|
const expected = Buffer.from(row.record.value, "utf8"),
|
|
bytes = Buffer.alloc(expected.length);
|
|
const read = await file.read(bytes, 0, bytes.length, offset);
|
|
assert.equal(read.bytesRead, bytes.length);
|
|
const encoding =
|
|
row.record.string?.encoding ?? row.record.decoding?.encoding;
|
|
if (["utf-8", "UTF-8", "ascii", "ASCII", "US-ASCII"].includes(encoding))
|
|
assert.deepEqual(bytes, expected);
|
|
report.byte_oracles.push({
|
|
source_index: row.source_index,
|
|
address: row.record.address,
|
|
file_offset: offset,
|
|
encoding,
|
|
utf8_bytes_match: bytes.equals(expected),
|
|
bytes_sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
});
|
|
}
|
|
} finally {
|
|
await file.close();
|
|
}
|
|
await raw(
|
|
"export_evidence_bundle",
|
|
{ path: join(output, "complete-evidence.json") },
|
|
"export",
|
|
);
|
|
await raw("close_binary", {}, "close");
|
|
opened = false;
|
|
const bundle = JSON.parse(
|
|
await readFile(join(output, "complete-evidence.json"), "utf8"),
|
|
);
|
|
const source = parseEvidence(
|
|
bundle.records.find((e) => e.evidence_id === summary.parent_evidence_id),
|
|
);
|
|
assert.equal(source.subject.digest.sha256, report.sha256);
|
|
assert.equal(source.normalized_result.length, total);
|
|
for (const row of selected)
|
|
assert.equal(
|
|
row.record.value,
|
|
source.normalized_result[row.source_index].value,
|
|
);
|
|
const fn = parseEvidence(
|
|
bundle.records.find(
|
|
(e) => e.evidence_id === report.procedures.parent_evidence_id,
|
|
),
|
|
);
|
|
assert.equal(fn.subject.digest.sha256, report.sha256);
|
|
report.procedure_count = fn.normalized_result.length;
|
|
assert.deepEqual(
|
|
fn.analysis_profile.parameters.analysis_options.disabled_analyzers,
|
|
disabled,
|
|
);
|
|
assert.equal(
|
|
fn.evidence_links.length,
|
|
0,
|
|
"This capture expects a self-contained inventory; retain its full source closure otherwise.",
|
|
);
|
|
await writeFile(
|
|
join(output, "functions.evidence.json"),
|
|
JSON.stringify(createEvidenceBundle([fn])),
|
|
{ flag: "wx" },
|
|
);
|
|
report.profile_digest = fn.analysis_profile.digest;
|
|
report.source_string_count = source.normalized_result.length;
|
|
report.status = "passed";
|
|
} catch (cause) {
|
|
report.status = "failed";
|
|
report.failure = String(cause?.stack ?? cause);
|
|
process.exitCode = 1;
|
|
} finally {
|
|
if (opened)
|
|
try {
|
|
const r = await call("close_binary", {}, "cleanup");
|
|
assert.notEqual(r.isError, true);
|
|
} catch (cause) {
|
|
report.close_failure = String(cause);
|
|
process.exitCode = 1;
|
|
}
|
|
report.elapsed_ms = performance.now() - start;
|
|
if (
|
|
!(await finalizeRetainedQueryReceipt({
|
|
report,
|
|
client,
|
|
transport,
|
|
account,
|
|
serverLog,
|
|
serverLogFinished,
|
|
output,
|
|
transcript,
|
|
}))
|
|
)
|
|
process.exitCode = 1;
|
|
}
|