rea/scripts/verify-real-ghidra-assertio...

350 lines
11 KiB
JavaScript

import { access } from "node:fs/promises";
import { parseBinaryTarget } from "../dist/domain/binaryTarget.js";
import { SUPPORTED_GHIDRA_VERSION } from "../dist/ghidra/GhidraInstallation.js";
import { GHIDRA_SESSION_CAPABILITIES } from "../dist/ghidra/GhidraSessionValues.js";
export function assertLocalProcedureInfo(info, expectedAddress) {
if (
info.entrypoint !== expectedAddress ||
info.basicblock_count < 1 ||
info.length < 1 ||
typeof info.signature !== "string" ||
info.signature.length === 0 ||
info.classification.external ||
info.classification.provenance !== "ghidra-function-manager"
)
throw new Error(
`Ghidra procedure metadata drifted: ${JSON.stringify(info)}`,
);
}
export function assertProviderText(assembly, pseudocode, expectedAddress) {
if (
typeof assembly !== "string" ||
!assembly.includes(`${expectedAddress}:`) ||
assembly.trim().length === 0
)
throw new Error("Ghidra assembly rendering lost its address-bearing lines");
if (typeof pseudocode !== "string" || pseudocode.trim().length === 0)
throw new Error("Ghidra local function pseudocode is unavailable");
}
export function assertReferenceMetadata(observed) {
if (
observed.instructions_scanned < 1 ||
observed.instruction_scan_truncated ||
observed.references.items.length === 0 ||
!observed.references.items.every(
({ kind }) =>
kind.available && kind.provenance === "ghidra-reference-manager",
) ||
!observed.references.items.some(({ kind }) => kind.call) ||
!observed.references.items.some(({ kind }) => kind.data)
)
throw new Error(
`Ghidra typed reference metadata drifted: ${JSON.stringify(observed)}`,
);
}
export function assertDossier(
dossier,
{
address,
expectedCallees = [],
expectedString = null,
requireAssembly = false,
requireMultiBlock = false,
},
) {
assertDossierProcedure(dossier, address);
assertDossierReferences(dossier);
assertDossierCallees(dossier, expectedCallees);
assertDossierStrings(dossier, expectedString);
assertDossierAssembly(dossier, requireAssembly);
assertDossierMultiBlock(dossier, requireMultiBlock);
}
function assertDossierProcedure(dossier, address) {
if (
dossier.procedure.address !== address ||
dossier.procedure.classification === null ||
dossier.procedure.classification.provenance !== "ghidra-function-manager" ||
dossier.procedure.classification.external ||
dossier.pseudocode.text.trim().length === 0 ||
dossier.pseudocode.returned_chars !== [...dossier.pseudocode.text].length ||
dossier.instruction_scan.scanned < 1 ||
dossier.instruction_scan.truncated ||
!dossier.limitations.some((value) => /indirect|computed/u.test(value)) ||
!dossier.limitations.some((value) => /Ghidra|Hopper|provider/u.test(value))
)
throw new Error(
`Ghidra dossier truthfulness drifted: ${JSON.stringify(dossier)}`,
);
}
function assertDossierReferences(dossier) {
if (
![
...dossier.incoming_references.items,
...dossier.outgoing_references.items,
].every(
({ kind }) =>
kind.available && kind.provenance === "ghidra-reference-manager",
)
)
throw new Error("Ghidra dossier lost typed reference provenance");
}
function assertDossierCallees(dossier, expectedCallees) {
for (const expected of expectedCallees)
if (!dossier.callees.items.some(({ address: value }) => value === expected))
throw new Error(`Ghidra dossier missed callee ${expected}`);
}
function assertDossierStrings(dossier, expectedString) {
if (
expectedString !== null &&
!dossier.referenced_strings.items.some(
({ value }) => value === expectedString,
)
)
throw new Error(
`Ghidra dossier missed referenced string ${expectedString}`,
);
}
function assertDossierAssembly(dossier, requireAssembly) {
if (
requireAssembly &&
(dossier.assembly.items.length === 0 || dossier.assembly.truncated)
)
throw new Error("Ghidra dossier assembly was unavailable or truncated");
}
function assertDossierMultiBlock(dossier, requireMultiBlock) {
if (
requireMultiBlock &&
(dossier.basic_blocks.items.length < 2 ||
!dossier.basic_blocks.items.some(
({ successors }) => successors.length > 0,
))
)
throw new Error(
"Ghidra dossier CFG missed the multi-block branch structure",
);
}
export function dossierSummary(dossier) {
return {
address: dossier.procedure.address,
pseudocode_chars: dossier.pseudocode.total_chars,
instructions: dossier.instruction_scan.scanned,
callees: dossier.callees.returned,
outgoing_references: dossier.outgoing_references.returned,
referenced_strings: dossier.referenced_strings.returned,
basic_blocks: dossier.basic_blocks.returned,
};
}
export async function assertSession(
session,
expectedDigest,
expectedTargetSha256,
) {
if (
session.provider.id !== "ghidra" ||
session.provider.version !== SUPPORTED_GHIDRA_VERSION ||
session.profile_digest !== expectedDigest ||
session.target.sha256 !== expectedTargetSha256 ||
session.read_only !== true ||
session.analysis_complete !== true ||
session.analysis_timed_out !== false ||
session.capabilities.join(",") !== GHIDRA_SESSION_CAPABILITIES.join(",") ||
!/^0x[0-9a-f]+$/u.test(session.target.image_base) ||
session.target.default_address_space.length === 0
)
throw new Error(
`Ghidra session commitment drifted: ${JSON.stringify(session)}`,
);
}
export function assertRuntimeCoordinates(coordinates) {
for (const name of ["runtime_root", "endpoint_path", "project_root"])
if (typeof coordinates[name] !== "string")
throw new Error(`Ghidra verifier did not observe ${name}`);
if (typeof coordinates.process_id !== "number")
throw new Error("Ghidra verifier did not observe its process ID");
}
export async function assertCleanup(coordinates) {
for (const name of ["runtime_root", "endpoint_path", "project_root"]) {
const path = coordinates[name];
if (typeof path === "string" && (await exists(path)))
throw new Error(`Ghidra cleanup left ${name}: ${path}`);
}
const pid = coordinates.process_id;
if (typeof pid === "number" && processExists(pid))
throw new Error(`Ghidra cleanup left process ${String(pid)}`);
}
export function assertDebugFixture(observed) {
if (
!Array.isArray(observed.documents) ||
observed.documents.length !== 1 ||
!observed.documents[0].includes("debug")
)
throw new Error("Ghidra did not expose exactly one debug Program identity");
const entry = findValue(observed.procedures, "rea_ghidra_inventory_entry");
const leaf = findValue(observed.procedures, "rea_ghidra_inventory_leaf");
const external = observed.procedures.find(
(item) => item.procedure.external && symbolTail(item.value) === "puts",
);
if (entry === undefined || leaf === undefined || external === undefined)
throw new Error(
"Ghidra procedure inventory missed local or external functions",
);
if (
!observed.procedures.some(
(item) => item.procedure.thunk && item.procedure.thunk_target !== null,
)
)
throw new Error(
"Ghidra procedure inventory did not distinguish a resolved thunk",
);
const entryName = findValue(observed.names, "rea_ghidra_inventory_entry");
if (entryName === undefined || !entryName.symbol.primary)
throw new Error("Ghidra symbol inventory missed the primary entry symbol");
if (!observed.names.some((item) => item.symbol.external))
throw new Error("Ghidra symbol inventory missed external symbols");
assertStrings(observed.strings);
assertSegments(observed.segments, observed.session.target.image_base);
}
export function assertStrippedFixture(observed) {
if (
observed.procedures.length === 0 ||
observed.names.length === 0 ||
findValue(observed.procedures, "rea_ghidra_inventory_entry") !== undefined
)
throw new Error(
"Stripped Ghidra fixture did not preserve stripped semantics",
);
assertStrings(observed.strings);
assertSegments(observed.segments, observed.session.target.image_base);
}
export function assertCrossFixture(observed) {
if (
observed.procedures.length === 0 ||
observed.names.length === 0 ||
observed.probes === null ||
typeof observed.probes.entry !== "string" ||
observed.probes.string_owner.referenced_strings < 1 ||
observed.probes.branch.basic_blocks < 2
)
throw new Error(
`Cross-format Ghidra semantics drifted: ${JSON.stringify(summary(observed))}`,
);
assertSegments(observed.segments, observed.session.target.image_base);
}
export async function assertMalformedFixture(path) {
const parsed = await parseBinaryTarget(path);
if (parsed.ok || parsed.error._tag !== "BinaryTargetError")
throw new Error(
"Malformed executable was not rejected before provider start",
);
}
function assertStrings(strings) {
for (const expected of [
"REA_GHIDRA_INVENTORY_ENTRY",
"REA_GHIDRA_LEAF_VALUE",
]) {
const item = strings.find((candidate) => candidate.value === expected);
if (
item === undefined ||
item.value_truncated ||
typeof item.string.encoding !== "string" ||
item.string.byte_length < expected.length
)
throw new Error(`Ghidra string inventory missed ${expected}`);
}
}
function assertSegments(segments, imageBase) {
if (
!Array.isArray(segments) ||
segments.length === 0 ||
!segments.some((item) => item.executable) ||
!segments.some((item) => item.writable) ||
!segments.every(
(item) =>
item.permissions.available === true &&
item.permissions.source === "ghidra-memory-block" &&
item.image_base === imageBase,
)
)
throw new Error("Ghidra memory-block permissions or image base drifted");
}
export function findValue(items, expected) {
return items.find((item) => symbolTail(item.value) === expected);
}
export function requireProcedure(items, expected) {
const observed = findValue(items, expected);
if (observed === undefined)
throw new Error(`Ghidra procedure probe is unavailable: ${expected}`);
return observed;
}
export function matchesSymbol(value, expected) {
const tail = symbolTail(value);
return tail === expected || tail === `_${expected}`;
}
export function symbolTail(value) {
if (typeof value !== "string") return undefined;
return value.split("::").at(-1);
}
export function summary(observed) {
return {
variant: observed.variant,
target_path: observed.target.path,
target_sha256: observed.target.sha256,
analysis_profile_digest: observed.profile.digest,
language_id: observed.session.target.language_id,
compiler_spec_id: observed.session.target.compiler_spec_id,
image_base: observed.session.target.image_base,
procedures: observed.procedures.length,
names: observed.names.length,
strings: observed.strings.length,
memory_blocks: observed.segments.length,
};
}
async function exists(path) {
try {
await access(path);
return true;
} catch {
return false;
}
}
function processExists(pid) {
try {
process.kill(pid, 0);
return true;
} catch (cause) {
return !(
cause instanceof Error &&
"code" in cause &&
cause.code === "ESRCH"
);
}
}