Files
rea/tests/boundary/firmware/firmwareReportFiles.test.ts
T

410 lines
14 KiB
TypeScript

import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
import { join } from "node:path";
import { expect, it, vi } from "vitest";
import {
inventoryFirmwareOutput,
FirmwareFileAccess,
} from "../../../src/firmware/FirmwareFiles.js";
import { readFirmwareCommandReport } from "../../../src/firmware/FirmwareCommand.js";
import { FIRMWARE_LIMITS } from "../../../src/firmware/FirmwareRelease.js";
import { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
import { projectAnalysisError } from "../../../src/domain/analysisErrorProjection.js";
import { SafeOutputTree } from "../../../src/artifacts/SafeOutputTree.js";
import {
normalizeBinwalkReport,
normalizeUnblobReport,
} from "../../../src/firmware/FirmwareReports.js";
import {
firmwareResultSchemas,
firmwareRequestSchema,
} from "../../../src/domain/firmware/firmwareAnalysis.js";
import { createHash } from "node:crypto";
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
it.each([
["invalid leading byte", [0xff]],
["incomplete sequence", [0xe2, 0x82]],
["overlong sequence", [0xc0, 0xaf]],
["encoded surrogate", [0xed, 0xa0, 0x80]],
])(
"rejects complete JSON containing %s without repairing producer bytes",
async (_name, bytes) => {
const root = await createTestTempDirectory("rea-firmware-report-");
const path = join(root, "report.json");
await writeFile(
path,
Buffer.concat([
Buffer.from('{"description":"'),
Buffer.from(bytes),
Buffer.from('"}'),
]),
);
await expect(
new FirmwareFileAccess().report(path, "inspect_firmware_regions"),
).rejects.toMatchObject({
_tag: "AnalysisOutputError",
reason: "Tool report is not valid complete JSON",
});
},
);
it.each([
["missing", "Report was not created", async () => {}],
["empty", "Report is empty", (path: string) => writeFile(path, "")],
[
"invalid_json",
"Tool report is not valid complete JSON",
(path: string) => writeFile(path, '[{"Analysis":'),
],
[
"oversized",
`Report exceeded ${FIRMWARE_LIMITS.reportBytes} bytes`,
(path: string) =>
writeFile(path, Buffer.alloc(FIRMWARE_LIMITS.reportBytes + 1, 0x20)),
],
[
"not_regular_file",
"Report is not a regular file",
(path: string) => mkdir(path),
],
] as const)(
"keeps report state %s after a successful exit distinct from an empty scan",
async (state, reason, prepare) => {
const root = await createTestTempDirectory("rea-firmware-report-");
const path = join(root, "report.json");
await prepare(path);
const failure: unknown = await readFirmwareCommandReport({
path,
engine: "binwalk",
operation: "inspect_firmware_regions",
execution: {
stdout: { text: "", bytes: 0, observedBytes: 0 },
stderr: { text: "", bytes: 0, observedBytes: 0 },
exit_code: 0,
signal: null,
command: "/opt/binwalk",
args: ["--quiet", "--log", path, "/work/firmware.bin"],
resource_limiter: "/usr/bin/prlimit",
},
files: new FirmwareFileAccess(),
}).catch((cause: unknown) => cause);
if (!(failure instanceof AnalysisError))
throw new Error("Expected a typed report failure");
const projected = projectAnalysisError(failure);
expect(projected).toMatchObject({
code: "unreadable_output",
retryable: false,
message: expect.stringContaining("not an empty or complete result"),
details: {
reason: `binwalk exited 0 without a usable report: ${reason}`,
diagnostics: {
report_state: state,
report_path: path,
command: "/opt/binwalk",
args: ["--quiet", "--log", path, "/work/firmware.bin"],
resource_limiter: "/usr/bin/prlimit",
exit_code: 0,
signal: null,
stdout_bytes: 0,
stderr_bytes: 0,
},
},
});
// Generic doctor advice is not presented as a repair for a missing report.
expect(projected.message).not.toContain("rea doctor");
// Recorded arguments name a removed private workspace; recovery must not
// present them as a replayable command.
expect(projected.remediation.action).toContain("diagnostics.report_state");
expect(projected.remediation.action).toContain("cannot be replayed as-is");
if (state === "oversized") {
expect(projected.remediation.action).toContain("no runtime override");
expect(projected.remediation.action).toContain(
"Repeating the unchanged request fails the same way",
);
expect(projected.remediation.action).not.toContain(
"Rerun the same REA request",
);
expect(projected.message).toContain("no runtime override");
expect(projected.message).not.toContain("Rerun the same REA request");
} else {
expect(projected.remediation.action).toContain(
"replacing every private path",
);
expect(projected.remediation.action).toContain(
"Rerun the same REA request",
);
expect(projected.remediation.action).not.toContain(
"Repeating the unchanged request fails the same way",
);
}
},
);
it("preserves valid Unicode and a literal replacement character in producer JSON", async () => {
const root = await createTestTempDirectory("rea-firmware-report-");
const path = join(root, "report.json");
const report = { description: "固件 🧩 \ufffd", path: "/输入/firmware.bin" };
await writeFile(path, JSON.stringify(report));
expect(
await new FirmwareFileAccess().report(path, "inspect_firmware_regions"),
).toEqual(report);
});
it("orders extracted firmware paths by code point independently of host locale", async () => {
const root = await createTestTempDirectory("rea-firmware-inventory-");
await mkdir(join(root, "Lib"));
for (const path of ["a.bin", "z.bin", "\u00e4.bin", "B.bin", "Lib/x.bin"])
await writeFile(join(root, path), "");
const entries = await inventoryFirmwareOutput(
root,
1024,
16,
"inspect_firmware_regions",
);
// Locale collation would interleave case and place "ä" by language rules.
expect(entries.map(({ relativePath }) => relativePath)).toEqual([
"B.bin",
"Lib/x.bin",
"a.bin",
"z.bin",
"\u00e4.bin",
]);
});
it.skipIf(process.platform !== "linux")(
"refuses raw filenames that cannot round-trip through UTF-8 instead of omitting their bytes",
async () => {
const root = await createTestTempDirectory("rea-firmware-raw-name-");
await writeFile(
Buffer.concat([
Buffer.from(`${root}/bad`),
Buffer.from([0xff]),
Buffer.from(".bin"),
]),
"123",
);
await writeFile(join(root, "good.bin"), "1234");
await expect(
inventoryFirmwareOutput(root, 4, 10, "extract_firmware"),
).rejects.toMatchObject({
_tag: "AnalysisOutputError",
diagnostics: { raw_name_hex: "626164ff2e62696e" },
});
},
);
it.each([
{ bytes: 1, files: 10, expected: "max_output_bytes" },
{ bytes: 10, files: 1, expected: "max_output_files" },
])(
"reports the selected $expected staging budget as a deterministic resource refusal",
async ({ bytes, files, expected }) => {
const root = await createTestTempDirectory("rea-firmware-staging-");
await writeFile(join(root, "a"), "1");
await writeFile(join(root, "b"), "2");
await expect(
inventoryFirmwareOutput(root, bytes, files, "extract_firmware"),
).rejects.toMatchObject({
_tag: "AnalysisResourceConstraintError",
resource: "capacity",
reportedLimits: { [expected]: 1 },
retryable: false,
remediationAction: expect.stringContaining(expected),
});
},
);
it("attributes missing sources to the selected path without relabeling snapshot destination failure", async () => {
const root = await createTestTempDirectory("rea-firmware-source-");
const files = new FirmwareFileAccess();
const request = (path: string) =>
firmwareRequestSchema.parse({
operation: "inspect_firmware_regions",
input: { path },
});
await expect(
files.snapshot(
request(join(root, "absent")),
root,
new AbortController().signal,
),
).rejects.toMatchObject({
_tag: "AnalysisInputError",
issues: [{ path: ["path"], reason: "invalid_value" }],
});
const source = join(root, "source");
await writeFile(source, "firmware");
await expect(
files.snapshot(
request(source),
join(root, "missing-workspace"),
new AbortController().signal,
),
).rejects.toMatchObject({ code: "ENOENT" });
await files.close();
});
it("does not activate an abort-bound source during awaited destination setup", async () => {
const root = await createTestTempDirectory("rea-firmware-publish-abort-");
const source = join(root, "source");
const bytes = Buffer.from("firmware");
await writeFile(source, bytes);
const files = new FirmwareFileAccess();
const tree = await SafeOutputTree.create(join(root, "published"));
const controller = new AbortController();
const reason = { stop: "during destination setup" };
const write = tree.write.bind(tree);
const spy = vi.spyOn(tree, "write").mockImplementation(async (...args) => {
controller.abort(reason);
// Yield a turn while the source has no consumer, reproducing the handoff.
await new Promise<void>((resolve) => setImmediate(resolve));
return write(...args);
});
try {
await expect(
files.publish(
{
path: source,
relativePath: "nested/file",
kind: "file",
size: bytes.length,
linkTarget: null,
},
createHash("sha256").update(bytes).digest("hex"),
tree,
controller.signal,
),
).rejects.toMatchObject({ reason: "cancelled", cause: reason });
} finally {
spy.mockRestore();
await tree.rollback();
await files.close();
}
await expect(access(join(root, "published"))).rejects.toMatchObject({
code: "ENOENT",
});
expect(Buffer.compare(await readFile(source), bytes)).toBe(0);
});
it.skipIf(process.platform === "win32" || process.getuid?.() === 0)(
"keeps denied source access distinct from invalid paths and producer failures",
async () => {
const root = await createTestTempDirectory("rea-firmware-source-denial-");
const source = join(root, "input");
await writeFile(source, "firmware", { mode: 0o000 });
const files = new FirmwareFileAccess();
try {
await expect(
files.snapshot(
firmwareRequestSchema.parse({
operation: "inspect_firmware_regions",
input: { path: source },
}),
root,
new AbortController().signal,
),
).rejects.toMatchObject({
_tag: "AnalysisAccessDeniedError",
path: source,
systemCode: "EACCES",
});
} finally {
await chmod(source, 0o600);
await files.close();
}
},
);
it("retains unfamiliar Unblob report members from JSON files through public diagnostics", async () => {
const root = await createTestTempDirectory("rea-firmware-report-metadata-");
const inputPath = join(root, "input.bin");
const reportPath = join(root, "report.json");
const sha256 = "a".repeat(64);
const unfamiliar: unknown = JSON.parse(
'{"__typename__":"FutureReport","__proto__":{"source":"local","empty":null},"constructor":false,"prototype":0,"nested":{"__proto__":"retained"}}',
);
const record = {
task: { path: inputPath, depth: 0, blob_id: "", is_multi_file: false },
reports: [
{ __typename__: "HashReport", sha256 },
{
__typename__: "StatReport",
path: inputPath,
size: 4,
is_dir: false,
is_file: true,
is_link: false,
link_target: null,
},
unfamiliar,
],
subtasks: [],
};
await writeFile(reportPath, JSON.stringify([record]));
const files = new FirmwareFileAccess();
try {
const raw = await files.report(reportPath, "extract_firmware");
const normalized = normalizeUnblobReport(raw, {
inputPath,
outputRoot: join(root, "output"),
sha256,
length: 4,
offset: 0,
maxDepth: 3,
});
const diagnostics =
firmwareResultSchemas.extract_firmware.shape.diagnostics.parse(
normalized.diagnostics,
);
expect(JSON.stringify(diagnostics)).toBe(
JSON.stringify([{ input_path: "$input", report: unfamiliar }]),
);
expect(normalized.coverage).toBe("partial");
for (const malformed of [{}, { __typename__: null }, { __typename__: 1 }]) {
expect(() =>
normalizeUnblobReport(
[{ ...record, reports: [...record.reports.slice(0, 2), malformed] }],
{
inputPath,
outputRoot: join(root, "output"),
sha256,
length: 4,
offset: 0,
maxDepth: 3,
},
),
).toThrow();
}
} finally {
await files.close();
}
});
it("rejects Binwalk extraction activity under every own JSON member", async () => {
const root = await createTestTempDirectory(
"rea-firmware-report-extractions-",
);
const reportPath = join(root, "report.json");
const inputPath = join(root, "input.bin");
const files = new FirmwareFileAccess();
try {
for (const extractions of [
"{}",
'{"__proto__":null}',
'{"ordinary":false}',
]) {
await writeFile(
reportPath,
`[{"Analysis":{"file_path":${JSON.stringify(inputPath)},"file_map":[],"extractions":${extractions}}}]`,
);
const raw = await files.report(reportPath, "inspect_firmware_regions");
const normalize = () => normalizeBinwalkReport(raw, inputPath, 4);
if (extractions === "{}") expect(normalize()).toEqual([]);
else expect(normalize).toThrow("Expected one inspection-only report");
}
} finally {
await files.close();
}
});