Files
rea/scripts/lib/ghidra-reference-snapshot-e2e.mjs
T
morluto 84a17d5519 fix(ghidra): preserve function references and snapshot boundaries
Collect references from the complete owned function body and exact entry,
including instruction-interior destinations and embedded-data sources.
Bind the corrected semantics to the analysis profile so legacy snapshots
cannot replay incomplete results. Preserve snapshot mismatch reasons and
recovery advice through CLI and MCP without disturbing live sessions.

Return exact entry candidates for ambiguous function names. Expand the
real-provider regression workflow and replace redundant parser assertions
with malformed-output coverage through the pinned MCP client.

Verified with Ghidra 12.1.4 on macOS arm64: full host-native lane, real
CLI/MCP boundary probes, 30 focused tests, and typecheck/lint.
2026-10-09 03:19:47 +08:00

185 lines
5.8 KiB
JavaScript

import assert from "node:assert/strict";
import { execFile } from "node:child_process";
import { randomUUID } from "node:crypto";
import { writeFile, rm } from "node:fs/promises";
import { join } from "node:path";
import { promisify } from "node:util";
import { parseBinaryTarget } from "../../dist/application/BinaryTargetResolver.js";
import { createAnalysisProfile } from "../../dist/domain/analysisProfile.js";
import {
createAnalysisSnapshotEntry,
parseAnalysisSnapshot,
serializeAnalysisSnapshot,
snapshotBinding,
snapshotTarget,
} from "../../dist/domain/analysisSnapshot.js";
import { createEvidence } from "../../dist/domain/evidence.js";
import { createEvidenceBundle } from "../../dist/domain/evidenceBundle.js";
import { requireMcpResult } from "./mcp-verifier-results.mjs";
/** Reject a valid older snapshot whose instruction-only collector omitted a real edge. */
export async function verifyLegacyGhidraReferenceSnapshot(
client,
{ target, procedure, omittedEdge, entrypoint, env },
) {
const bundle = requireMcpResult(
await client.callTool({
name: "get_evidence_bundle",
arguments: {},
}),
"get_evidence_bundle",
);
const observed = bundle.records.find(
(record) =>
record.operation === "analyze_function" &&
record.parameters.procedure === procedure,
);
assert.ok(observed);
const { function_references: coverage, ...legacyParameters } =
observed.analysis_profile.parameters;
assert.equal(
typeof coverage,
"string",
"Reference semantics must participate in snapshot identity",
);
const legacyProfile = createAnalysisProfile(
observed.provider,
legacyParameters,
);
assert.notEqual(legacyProfile.digest, observed.analysis_profile.digest);
const parsed = await parseBinaryTarget(
target.path,
process.cwd(),
process.arch,
);
if (!parsed.ok) throw parsed.error;
const nativeTarget = parsed.value;
const result = structuredClone(observed.normalized_result);
// Reconstruct the observed pre-fix omission; all other data
// comes from the real provider and the file has a fully valid Evidence binding.
result.incoming_references = result.incoming_references.filter(
(edge) =>
edge.source_address !== omittedEdge.source_address ||
edge.target_address !== omittedEdge.target_address,
);
assert.equal(
result.incoming_references.length,
observed.normalized_result.incoming_references.length - 1,
);
const evidence = createEvidence(nativeTarget, observed.provider, {
operation: observed.operation,
parameters: observed.parameters,
result,
rawResult: result,
analysisProfile: legacyProfile,
limitations: observed.limitations,
locations: observed.locations,
});
const binding = snapshotBinding(legacyProfile);
const identity = snapshotTarget(nativeTarget);
const snapshot = parseAnalysisSnapshot({
target: identity,
binding,
entries: [
createAnalysisSnapshotEntry({
target: identity,
binding,
operation: observed.operation,
parameters: observed.parameters,
execution: {
result,
rawResult: result,
provider: observed.provider,
analysisProfile: legacyProfile,
limitations: observed.limitations,
locations: observed.locations,
subject: nativeTarget,
},
}),
],
evidence_bundle: createEvidenceBundle([evidence]),
});
assert.equal(
snapshot.entries.length,
1,
"The legacy query must remain valid and replayable under its own profile",
);
const path = join(env.TMPDIR, `legacy-reference-${randomUUID()}.json`);
await writeFile(path, serializeAnalysisSnapshot(snapshot), { flag: "wx" });
try {
const session = requireMcpResult(
await client.callTool({ name: "binary_session", arguments: {} }),
"binary_session",
);
const reply = await client.callTool({
name: "open_binary",
arguments: {
path: target.path,
provider_id: "ghidra",
snapshot_path: path,
},
});
assert.equal(reply.isError, true);
const mcpRejection = reply.structuredContent.error;
assert.equal(mcpRejection.code, "evidence_integrity_mismatch");
assert.match(mcpRejection.details.reason, /profile_mismatch/u);
assert.match(
mcpRejection.remediation.action,
/without this snapshot.*fresh snapshot/u,
);
assert.deepEqual(
requireMcpResult(
await client.callTool({ name: "binary_session", arguments: {} }),
"binary_session",
),
session,
"A rejected snapshot must preserve the selected session",
);
const retained = requireMcpResult(
await client.callTool({
name: "analyze_function",
arguments: { procedure },
}),
"analyze_function",
);
assert.ok(
retained.incoming_references.some(
(edge) =>
edge.source_address === omittedEdge.source_address &&
edge.target_address === omittedEdge.target_address,
),
"A rejected legacy snapshot must not replace complete live analysis",
);
await assert.rejects(
promisify(execFile)(
process.execPath,
[
entrypoint,
"function",
target.path,
procedure,
"--provider",
"ghidra",
"--snapshot",
path,
"--json",
],
{ env, timeout: 240000, maxBuffer: 72 * 1024 * 1024 },
),
(error) => {
assert.equal(error.code, 1);
const rejection = JSON.parse(error.stdout);
assert.equal(rejection.code, "evidence_integrity_mismatch");
assert.equal(rejection.details.reason, mcpRejection.details.reason);
assert.equal(
rejection.remediation.action,
mcpRejection.remediation.action,
);
return true;
},
);
} finally {
await rm(path);
}
}