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.
185 lines
5.8 KiB
JavaScript
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);
|
|
}
|
|
}
|