Files
rea/scripts/verify-ghidra-bridge-sources.mjs
T
claudetrail2026-droidandKaoru0822 1b828dbb10 ci(ghidra): compile the packaged bridge source cohort (#3142)
Compile the complete packaged Java source cohort against the verified SDK in selected CI. Share release and source ownership with runtime/package consumers, fail closed on drift, and remove compiler scratch on failure.

Co-authored-by: Kaoru0822 <208281322+kaoru0822-kitauji@users.noreply.github.com>
2026-10-15 18:11:19 +00:00

183 lines
6.6 KiB
JavaScript

#!/usr/bin/env node
// Compile the packaged Ghidra bridge sources against an existing Ghidra
// installation's jars. Ghidra's script provider compiles the whole bridge
// directory when a session starts, so one unresolved symbol disables every
// operation and surfaces only after import and auto-analysis. This check
// reports the same javac diagnostics without launching Ghidra or selecting,
// importing or executing a target.
import { spawnSync } from "node:child_process";
import { mkdtemp, readdir, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join, relative, resolve } from "node:path";
import { fileURLToPath } from "node:url";
// The pinned Node toolchain strips types from these dependency-free owners;
// this preflight therefore uses runtime facts without needing npm ci or a build.
import { GHIDRA_BRIDGE_SOURCES } from "../src/ghidra/GhidraBridgeSources.ts";
import { parseGhidraJavaProperties } from "../src/ghidra/GhidraJavaProperties.ts";
import {
SUPPORTED_GHIDRA_VERSION,
SUPPORTED_GHIDRA_JAVA_MAJOR,
ghidraReleaseAccepted,
ghidraReleaseLine,
} from "../src/ghidra/GhidraReleasePolicy.ts";
const args = process.argv.slice(2);
if (
args.length > 1 ||
(args.length === 1 &&
!["--check-sdk-pin", "--verified-sdk"].includes(args[0]))
)
fail(
"Usage: verify-ghidra-bridge-sources.mjs [--check-sdk-pin|--verified-sdk]",
);
if (args[0] === "--check-sdk-pin") {
let url;
try {
url = new URL(process.env.GHIDRA_RELEASE_URL);
} catch {
fail("GHIDRA_RELEASE_URL must name the checksum-pinned official release");
}
const prefix = `/NationalSecurityAgency/ghidra/releases/download/Ghidra_${SUPPORTED_GHIDRA_VERSION}_build/ghidra_${SUPPORTED_GHIDRA_VERSION}_PUBLIC_`;
if (
url.origin !== "https://github.com" ||
url.username ||
url.password ||
url.search ||
url.hash ||
!url.pathname.startsWith(prefix) ||
!url.pathname.endsWith(".zip") ||
!/^[a-f0-9]{64}$/iu.test(process.env.GHIDRA_RELEASE_SHA256 ?? "")
)
fail(
`SDK URL and SHA256 must match verified Ghidra ${SUPPORTED_GHIDRA_VERSION}`,
);
console.log(`GHIDRA_JAVA_MAJOR=${SUPPORTED_GHIDRA_JAVA_MAJOR}`);
process.exit(0);
}
const root = resolve(dirname(fileURLToPath(import.meta.url)), "..");
const bridge = join(root, "bridge/ghidra");
const install = process.env.GHIDRA_INSTALL_DIR;
if (!install)
fail(
`Set GHIDRA_INSTALL_DIR to an existing Ghidra ${ghidraReleaseLine()} installation (verified with ${SUPPORTED_GHIDRA_VERSION}). REA does not install Ghidra.`,
);
const properties = await readFile(
join(install, "Ghidra/application.properties"),
"latin1",
).catch((cause) =>
fail(
`GHIDRA_INSTALL_DIR has no readable Ghidra/application.properties: ${cause.message}`,
),
);
const parsedProperties = parseGhidraJavaProperties(properties);
if (!parsedProperties.ok) fail(parsedProperties.error);
const version = parsedProperties.value.get("application.version");
if (!ghidraReleaseAccepted(version))
fail(
`Bridge sources require Ghidra ${ghidraReleaseLine()} (verified with ${SUPPORTED_GHIDRA_VERSION}). Observed application.version=${version === undefined ? "missing" : JSON.stringify(version)}.`,
);
if (args[0] === "--verified-sdk" && version !== SUPPORTED_GHIDRA_VERSION)
fail(
`CI SDK must be verified Ghidra ${SUPPORTED_GHIDRA_VERSION}; observed ${JSON.stringify(version)}`,
);
const sourceNames = (await readdir(bridge, { withFileTypes: true }))
.filter((entry) => entry.isFile() && entry.name.endsWith(".java"))
.map((entry) => entry.name);
for (const name of Object.values(GHIDRA_BRIDGE_SOURCES))
if (!sourceNames.includes(name))
fail(`Required Ghidra bridge source is missing: bridge/ghidra/${name}`);
const requiredNames = new Set(Object.values(GHIDRA_BRIDGE_SOURCES));
const unregistered = sourceNames.filter((name) => !requiredNames.has(name));
if (unregistered.length)
fail(
`Ghidra bridge sources absent from the manifest: ${unregistered.join(", ")}`,
);
const sources = sourceNames
.map((name) => relative(root, join(bridge, name)))
.sort();
const javac = process.env.JAVA_HOME
? join(process.env.JAVA_HOME, "bin", "javac")
: "javac";
const banner = run(javac, ["-version"]);
if (banner.error || banner.status !== 0)
fail(`javac is unavailable: ${banner.error?.message ?? output(banner)}`);
const major = Number(/^javac (\d+)/mu.exec(output(banner))?.[1]);
if (!Number.isInteger(major) || major < SUPPORTED_GHIDRA_JAVA_MAJOR)
fail(
`Ghidra ${ghidraReleaseLine()} compiles bridge scripts with Java ${SUPPORTED_GHIDRA_JAVA_MAJOR} or newer. Observed: ${output(banner) || "no version"}. REA does not install or upgrade Java.`,
);
const jars = (
await readdir(join(install, "Ghidra"), {
recursive: true,
withFileTypes: true,
})
)
.filter((entry) => entry.isFile() && entry.name.endsWith(".jar"))
.map((entry) => join(entry.parentPath, entry.name))
.sort();
if (jars.length === 0) fail(`No jars found under ${join(install, "Ghidra")}.`);
const scratch = await mkdtemp(join(tmpdir(), "rea-ghidra-bridge-"));
try {
// An argument file keeps the jar class path below Windows' command length.
const argumentFile = join(scratch, "javac.args");
await writeFile(
argumentFile,
[
"-proc:none",
"-encoding",
"UTF-8",
"--release",
String(SUPPORTED_GHIDRA_JAVA_MAJOR),
"-cp",
jars.join(process.platform === "win32" ? ";" : ":"),
"-d",
join(scratch, "classes"),
...sources,
]
.map((s) => `"${s.replaceAll("\\", "\\\\").replaceAll('"', '\\"')}"`)
.join("\n"),
);
// Sources are repository-relative, so diagnostics name bridge/ghidra/<file>:<line>.
const result = run(javac, ["-J-Xmx1g", `@${argumentFile}`]);
if (result.error || result.status !== 0) {
// Set the status instead of exiting so the scratch directory is removed.
process.stderr.write(
`${output(result) || result.error?.message || "javac failed"}\nGhidra ${version} could not compile the packaged bridge sources; the provider would fail at session start.\n`,
);
process.exitCode = 1;
} else {
console.log(
JSON.stringify({
ghidra_version: version,
javac: output(banner),
class_path_jars: jars.length,
sources,
}),
);
}
} finally {
await rm(scratch, { recursive: true, force: true });
}
function run(command, args) {
return spawnSync(command, args, {
cwd: root,
encoding: "utf8",
timeout: 300_000,
maxBuffer: 16 * 1024 * 1024,
});
}
function output(result) {
return `${result.stdout ?? ""}${result.stderr ?? ""}`.trim();
}
function fail(message) {
process.stderr.write(`${message}\n`);
process.exit(1);
}