diff --git a/scripts/verify/firmware/analysis.mjs b/scripts/verify/firmware/analysis.mjs index b90dba14b7..cbbd242014 100644 --- a/scripts/verify/firmware/analysis.mjs +++ b/scripts/verify/firmware/analysis.mjs @@ -9,7 +9,9 @@ import { Client } from "@modelcontextprotocol/client"; import { StdioClientTransport } from "@modelcontextprotocol/client/stdio"; import { firmwareResultSchemas } from "../../../dist/domain/firmware/firmwareAnalysis.js"; import { parseEvidence } from "../../../dist/domain/evidence.js"; -import { hashFirmwareFile } from "../../../dist/firmware/FirmwareFiles.js"; +import { ArtifactResourceScope } from "../../../dist/artifacts/ArtifactResourceScope.js"; +import { hashVerifierFile } from "../../lib/verifier-file-resources.mjs"; +import { closeVerifierResources } from "../../lib/verifier-cleanup.mjs"; const execFileAsync = promisify(execFile); const { join, resolve, dirname } = nodePath; @@ -79,7 +81,7 @@ const oracle = JSON.parse( await fs.readFile(join(FIXTURE_ROOT, "oracle.json"), "utf8"), ); const firmwarePath = join(FIXTURE_ROOT, "firmware.bin"); -assert.equal(await hashFirmwareFile(firmwarePath), oracle.sha256); +const fileResources = new ArtifactResourceScope(); const workDir = await fs.mkdtemp(join(tmpdir(), "rea-real-firmware-")); const baseEnv = Object.fromEntries( @@ -160,7 +162,10 @@ async function verifyFiles(evidence) { assert.ok(entry !== undefined, `Missing extracted ${suffix}`); assert.equal(entry.sha256, expected.sha256); assert.equal(entry.size, expected.size); - assert.equal(await hashFirmwareFile(entry.path), expected.sha256); + assert.equal( + (await hashVerifierFile(entry.path, fileResources)).sha256, + expected.sha256, + ); assert.equal(entry.original_file_range, null); assert.equal(entry.runtime_address, null); } @@ -371,7 +376,12 @@ async function stageGhidra(mcpFiles) { // Entry point // --------------------------------------------------------------------------- +let primaryError; try { + assert.equal( + (await hashVerifierFile(firmwarePath, fileResources)).sha256, + oracle.sha256, + ); const cli = await stageCli(); await connectMcp(); const mcpFiles = await stageMcp(cli); @@ -381,11 +391,24 @@ try { await stageGhidra(mcpFiles); assert.equal( - await hashFirmwareFile(firmwarePath), + (await hashVerifierFile(firmwarePath, fileResources)).sha256, oracle.sha256, "Original firmware bytes must remain unchanged", ); +} catch (cause) { + primaryError = cause; + throw cause; } finally { - await mcpClient.close(); - await fs.rm(workDir, { recursive: true, force: true }); + await closeVerifierResources( + [ + async () => { + await closeVerifierResources([ + () => mcpClient.close(), + () => fileResources.close(), + ]); + await fs.rm(workDir, { recursive: true, force: true }); + }, + ], + primaryError, + ); } diff --git a/tests/boundary/process/verifierFileResources.test.ts b/tests/boundary/process/verifierFileResources.test.ts index a87824e275..f6d45f1abf 100644 --- a/tests/boundary/process/verifierFileResources.test.ts +++ b/tests/boundary/process/verifierFileResources.test.ts @@ -134,6 +134,25 @@ it("Ghidra verifier probes await the shared owned inspection contract", async () expect(violations).toEqual([]); }); +it("firmware verifier loads its current hash API before reporting unavailable prerequisites", () => { + const result = spawnSync( + process.execPath, + ["scripts/verify/firmware/analysis.mjs"], + { + encoding: "utf8", + timeout: 10_000, + env: { PATH: process.env.PATH ?? "" }, + }, + ); + expect(result.status).toBe(1); + expect(result.stderr).toContain( + process.platform === "linux" + ? "requires REA_BINWALK_COMMAND" + : "requires Linux", + ); + expect(result.stderr).not.toContain("does not provide an export"); +}); + it.each(["late-close", "retry-close", "retained-close", "opaque-cancellation"])( "owns Ghidra verifier prerequisite finalization: %s", async (mode) => {