mirror of
https://github.com/morluto/rea.git
synced 2026-10-11 21:39:55 +00:00
* feat(firmware): integrate Binwalk and Unblob analysis * fix(firmware): verify published files and namespace source references * test(firmware): isolate extractor dependency failure from process cleanup * fix(firmware): retain actionable report and cleanup diagnostics * test: assert retained batch output failure diagnostics
255 lines
9.0 KiB
JavaScript
255 lines
9.0 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFile } from "node:child_process";
|
|
import {
|
|
access,
|
|
mkdir,
|
|
mkdtemp,
|
|
readFile,
|
|
rm,
|
|
symlink,
|
|
} from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import { promisify } from "node:util";
|
|
import { Client } from "@modelcontextprotocol/client";
|
|
import { StdioClientTransport } from "@modelcontextprotocol/client/stdio";
|
|
import { firmwareResultSchemas } from "../dist/domain/firmwareAnalysis.js";
|
|
import { parseEvidence } from "../dist/domain/evidence.js";
|
|
import { hashFirmwareFile } from "../dist/firmware/FirmwareFiles.js";
|
|
|
|
const exec = promisify(execFile);
|
|
const repository = resolve(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const entrypoint = resolve(
|
|
process.env.REA_FIRMWARE_TEST_ENTRYPOINT ??
|
|
join(repository, "scripts/rea.mjs"),
|
|
);
|
|
if (process.platform !== "linux")
|
|
throw new Error(
|
|
"verify:firmware requires Linux; other hosts are not verified",
|
|
);
|
|
for (const variable of ["REA_BINWALK_COMMAND", "REA_UNBLOB_COMMAND"]) {
|
|
const path = process.env[variable];
|
|
if (path === undefined)
|
|
throw new Error(
|
|
`verify:firmware requires ${variable}; see docs/firmware-analysis.md`,
|
|
);
|
|
await access(path);
|
|
}
|
|
const fixtureRoot = resolve(
|
|
process.env.REA_FIRMWARE_FIXTURE_ROOT ??
|
|
join(repository, "_reference/firmware-integration/generated"),
|
|
);
|
|
await access(join(fixtureRoot, "oracle.json")).catch((cause) => {
|
|
throw new Error(
|
|
"verify:firmware requires generated fixtures; run npm run fixtures:firmware",
|
|
{ cause },
|
|
);
|
|
});
|
|
const oracle = JSON.parse(
|
|
await readFile(join(fixtureRoot, "oracle.json"), "utf8"),
|
|
);
|
|
const path = join(fixtureRoot, "firmware.bin");
|
|
assert.equal(await hashFirmwareFile(path), oracle.sha256);
|
|
const root = await mkdtemp(join(tmpdir(), "rea-real-firmware-"));
|
|
const environment = Object.fromEntries(
|
|
Object.entries(process.env).filter(([, value]) => value !== undefined),
|
|
);
|
|
const client = new Client({ name: "rea-real-firmware-verifier", version: "1" });
|
|
const cli = async (args, env = environment) => {
|
|
const response = await exec(
|
|
process.execPath,
|
|
[entrypoint, ...args, "--format", "json"],
|
|
{
|
|
env,
|
|
cwd: repository,
|
|
timeout: 150_000,
|
|
maxBuffer: 16 * 1024 * 1024,
|
|
},
|
|
);
|
|
return parseEvidence(JSON.parse(response.stdout));
|
|
};
|
|
const call = async (name, args) => {
|
|
const response = await client.callTool(
|
|
{ name, arguments: args },
|
|
{ timeout: 150_000 },
|
|
);
|
|
assert.notEqual(response.isError, true, JSON.stringify(response));
|
|
assert.ok(response.structuredContent !== undefined);
|
|
return response.structuredContent;
|
|
};
|
|
const verifyRegions = (evidence) => {
|
|
assert.equal(evidence.subject.digest.sha256, oracle.sha256);
|
|
const result = firmwareResultSchemas.inspect_firmware_regions.parse(
|
|
evidence.normalized_result,
|
|
);
|
|
const gzip = result.regions.find(
|
|
(region) => region.signature === "gzip" && region.offset === oracle.offset,
|
|
);
|
|
assert.ok(
|
|
gzip !== undefined,
|
|
"Binwalk must find the independently generated gzip offset",
|
|
);
|
|
assert.equal(gzip.reported_size, oracle.length);
|
|
assert.equal(gzip.size_basis, "provider_reported_validation_unknown");
|
|
return result.regions.map(({ provider_id, ...region }) => region);
|
|
};
|
|
const verifyFiles = async (evidence) => {
|
|
const result = firmwareResultSchemas.extract_firmware.parse(
|
|
evidence.normalized_result,
|
|
);
|
|
assert.ok(result.chunks.some((chunk) => chunk.handler === "gzip"));
|
|
for (const [suffix, expected] of Object.entries(oracle.files)) {
|
|
const file = result.files.find((candidate) =>
|
|
candidate.relative_path.endsWith(`/${suffix}`),
|
|
);
|
|
assert.ok(file !== undefined, `Missing extracted ${suffix}`);
|
|
assert.equal(file.sha256, expected.sha256);
|
|
assert.equal(file.size, expected.size);
|
|
assert.equal(await hashFirmwareFile(file.path), expected.sha256);
|
|
assert.equal(file.original_file_range, null);
|
|
assert.equal(file.runtime_address, null);
|
|
}
|
|
return result;
|
|
};
|
|
try {
|
|
const inspected = await cli(["inspect-firmware-regions", path]);
|
|
const cliRegions = verifyRegions(inspected);
|
|
const extracted = await cli([
|
|
"extract-firmware",
|
|
path,
|
|
join(root, "cli-output"),
|
|
]);
|
|
const cliFiles = await verifyFiles(extracted);
|
|
assert.equal(cliFiles.coverage, "partial");
|
|
assert.ok(
|
|
cliFiles.chunks.some(
|
|
(chunk) =>
|
|
chunk.handler === null &&
|
|
chunk.range.offset === 0 &&
|
|
chunk.range.length === 64,
|
|
),
|
|
);
|
|
await client.connect(
|
|
new StdioClientTransport({
|
|
command: process.execPath,
|
|
args: [entrypoint, "mcp"],
|
|
cwd: repository,
|
|
env: environment,
|
|
stderr: "pipe",
|
|
}),
|
|
{ timeout: 30_000 },
|
|
);
|
|
const inspectedMcp = await call("inspect_firmware_regions", { path });
|
|
const mcpEvidence = parseEvidence(inspectedMcp.evidence);
|
|
assert.deepEqual(verifyRegions(mcpEvidence), cliRegions);
|
|
const extractedMcp = await call("extract_firmware", {
|
|
path,
|
|
output_directory: join(root, "mcp-output"),
|
|
});
|
|
const mcpFiles = await verifyFiles(parseEvidence(extractedMcp.evidence));
|
|
const contentIdentity = (result) =>
|
|
result.files.map(({ relative_path, sha256, size }) => ({
|
|
relative_path,
|
|
sha256,
|
|
size,
|
|
}));
|
|
assert.deepEqual(contentIdentity(mcpFiles), contentIdentity(cliFiles));
|
|
const selected = await call("extract_firmware", {
|
|
path,
|
|
output_directory: join(root, "selected-output"),
|
|
range: { offset: oracle.offset, length: oracle.length },
|
|
});
|
|
const selectedFiles = await verifyFiles(parseEvidence(selected.evidence));
|
|
assert.equal(selectedFiles.selection.offset, oracle.offset);
|
|
assert.equal(selectedFiles.selection.length, oracle.length);
|
|
const depth = await call("extract_firmware", {
|
|
path,
|
|
output_directory: join(root, "depth-output"),
|
|
max_depth: 1,
|
|
});
|
|
const depthResult = firmwareResultSchemas.extract_firmware.parse(
|
|
depth.result,
|
|
);
|
|
assert.equal(depthResult.coverage, "partial");
|
|
assert.ok(depthResult.depth_limited_paths.length > 0);
|
|
console.log(
|
|
"PASS real Binwalk/Unblob: CLI, MCP, independent offsets/digests, range extraction, unknown chunks and depth limits",
|
|
);
|
|
if (process.env.REA_FIRMWARE_VERIFY_EXT4 === "1") {
|
|
const ext4 = join(fixtureRoot, "ext4.bin");
|
|
await access(ext4);
|
|
const response = await call("extract_firmware", {
|
|
path: ext4,
|
|
output_directory: join(root, "ext4-output"),
|
|
});
|
|
const result = firmwareResultSchemas.extract_firmware.parse(
|
|
response.result,
|
|
);
|
|
assert.ok(result.chunks.some((chunk) => chunk.handler === "extfs"));
|
|
assert.equal(result.coverage, "complete");
|
|
for (const [suffix, expected] of Object.entries(oracle.files))
|
|
assert.equal(
|
|
result.files.find((file) => file.relative_path.endsWith(`/${suffix}`))
|
|
?.sha256,
|
|
expected.sha256,
|
|
);
|
|
console.log("PASS real ext4 extraction with debugfs; no mounts");
|
|
// Preserve the process-ownership utility while withholding all extractors.
|
|
const withoutExtractors = join(root, "without-extractors");
|
|
await access("/bin/ps");
|
|
await mkdir(withoutExtractors);
|
|
await symlink("/bin/ps", join(withoutExtractors, "ps"));
|
|
const missing = await cli(
|
|
["extract-firmware", ext4, join(root, "missing-extractor-output")],
|
|
{ ...environment, PATH: withoutExtractors },
|
|
);
|
|
const partial = firmwareResultSchemas.extract_firmware.parse(
|
|
missing.normalized_result,
|
|
);
|
|
assert.equal(partial.coverage, "partial");
|
|
assert.ok(JSON.stringify(partial.diagnostics).includes("debugfs"));
|
|
console.log(
|
|
"PASS real missing-extractor diagnostics retained despite Unblob exit 1",
|
|
);
|
|
}
|
|
if (process.env.REA_FIRMWARE_VERIFY_GHIDRA === "1") {
|
|
if (process.env.GHIDRA_INSTALL_DIR === undefined)
|
|
throw new Error(
|
|
"Firmware Ghidra handoff requires GHIDRA_INSTALL_DIR and Java; see docs/ghidra.md",
|
|
);
|
|
const child = mcpFiles.files.find((file) =>
|
|
file.relative_path.endsWith("/usr/bin/probe"),
|
|
);
|
|
assert.ok(child !== undefined);
|
|
await call("open_binary", { path: child.path, provider_id: "ghidra" });
|
|
const dossier = await call("analyze_function", {
|
|
procedure: "firmware_probe",
|
|
});
|
|
const evidence = parseEvidence(dossier.evidence);
|
|
assert.equal(
|
|
evidence.subject.digest.sha256,
|
|
oracle.files["usr/bin/probe"].sha256,
|
|
);
|
|
const pseudocode = await call("procedure_pseudo_code", {
|
|
procedure: "firmware_probe",
|
|
});
|
|
assert.equal(parseEvidence(pseudocode.evidence).provider.id, "ghidra");
|
|
assert.ok(
|
|
JSON.stringify(evidence.normalized_result).includes("firmware_probe"),
|
|
);
|
|
await call("close_binary", {});
|
|
console.log(
|
|
"PASS real Ghidra handoff of the selected extracted host ELF, bound to its digest",
|
|
);
|
|
}
|
|
assert.equal(
|
|
await hashFirmwareFile(path),
|
|
oracle.sha256,
|
|
"Original firmware bytes must remain unchanged",
|
|
);
|
|
} finally {
|
|
await client.close();
|
|
await rm(root, { recursive: true, force: true });
|
|
}
|