mirror of
https://github.com/morluto/rea
synced 2026-10-26 14:25:49 -04:00
200 lines
7.4 KiB
TypeScript
200 lines
7.4 KiB
TypeScript
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
import { createHash } from "node:crypto";
|
|
import { join } from "node:path";
|
|
|
|
import { expect, onTestFinished } from "vitest";
|
|
|
|
import { artifactExtractionResultSchema } from "../../src/domain/artifactGraph.js";
|
|
import { parseEvidence } from "../../src/domain/evidence.js";
|
|
import { createTestTempDirectory } from "../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../support/cli/cliFixture.js";
|
|
import {
|
|
cArchiveExecutable,
|
|
pyzArchive,
|
|
PYTHON_39_MAGIC,
|
|
} from "../../src/artifacts/pyinstaller/CArchive.fixture.js";
|
|
|
|
// APFS rejects the raw FF basename with EILSEQ; match the filename-byte lane.
|
|
cliTest.skipIf(process.platform !== "linux").for([false, true])(
|
|
"preserves extraction traversal uncertainty through CLI and stdio MCP (opaque name: %s)",
|
|
async (withOpaqueName, { cli }) => {
|
|
const root = await createTestTempDirectory("rea-extraction-coverage-");
|
|
const source = join(root, "Fixture.app");
|
|
await mkdir(join(source, "Contents", "MacOS"), { recursive: true });
|
|
const macho = Buffer.alloc(32);
|
|
macho.writeUInt32LE(0xfeedfacf, 0);
|
|
macho.writeUInt32LE(0x0100000c, 4);
|
|
macho.writeUInt32LE(2, 12);
|
|
const files = new Map([
|
|
[
|
|
"Contents/Info.plist",
|
|
Buffer.from(
|
|
"<plist><dict><key>CFBundlePackageType</key><string>APPL</string><key>CFBundleExecutable</key><string>fixture</string></dict></plist>",
|
|
),
|
|
],
|
|
["Contents/MacOS/fixture", macho],
|
|
["Contents/good.txt", Buffer.from("retained bytes\n")],
|
|
]);
|
|
for (const [path, bytes] of files)
|
|
await writeFile(join(source, path), bytes);
|
|
const opaque = Buffer.concat([
|
|
Buffer.from(join(source, "Contents", "opaque-")),
|
|
Buffer.from([0xff]),
|
|
Buffer.from(".txt"),
|
|
]);
|
|
if (withOpaqueName) await writeFile(opaque, "omitted bytes\n");
|
|
|
|
const cliResult = await cli.run({
|
|
arguments: ["extract-artifact", source, "--json"],
|
|
});
|
|
expect(cliResult.exitCode, cliResult.stdout + cliResult.stderr).toBe(0);
|
|
const cliEvidence = parseEvidence(cliResult.json);
|
|
const cliExtraction = artifactExtractionResultSchema.parse(
|
|
cliEvidence.normalized_result,
|
|
);
|
|
onTestFinished(() =>
|
|
rm(cliExtraction.output_root, { recursive: true, force: true }),
|
|
);
|
|
const call = await cli.sharedMcpCall?.();
|
|
if (call === undefined) throw new Error("Expected a stdio MCP session");
|
|
const opened = await call("open_binary", { path: source });
|
|
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
|
|
const reply = await call("extract_artifact", {});
|
|
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
|
const mcpEvidence = parseEvidence(reply.structuredContent);
|
|
const mcpExtraction = artifactExtractionResultSchema.parse(
|
|
mcpEvidence.normalized_result,
|
|
);
|
|
onTestFinished(() =>
|
|
rm(mcpExtraction.output_root, { recursive: true, force: true }),
|
|
);
|
|
for (const { evidence, result } of [
|
|
{ evidence: cliEvidence, result: cliExtraction },
|
|
{ evidence: mcpEvidence, result: mcpExtraction },
|
|
]) {
|
|
expect(
|
|
result.limitations.some((limitation) =>
|
|
limitation.includes("nested archive contents remain"),
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
result.artifacts.map(({ relative_path }) => relative_path),
|
|
).toEqual([...files.keys()]);
|
|
for (const [path, bytes] of files)
|
|
expect(await readFile(join(result.output_root, path))).toEqual(bytes);
|
|
for (const limitations of [result.limitations, evidence.limitations]) {
|
|
expect(
|
|
limitations.some((value) => value.includes("not valid UTF-8")),
|
|
).toBe(withOpaqueName);
|
|
expect(
|
|
limitations.some((value) =>
|
|
value.includes("source traversal coverage is unknown"),
|
|
),
|
|
).toBe(withOpaqueName);
|
|
expect(
|
|
limitations.some((value) => value.startsWith("All regular files")),
|
|
).toBe(!withOpaqueName);
|
|
}
|
|
}
|
|
if (withOpaqueName)
|
|
expect(await readFile(opaque, "utf8")).toBe("omitted bytes\n");
|
|
},
|
|
);
|
|
|
|
cliTest.for([false, true])(
|
|
"retains PYZ traversal coverage through CLI and stdio MCP extraction (opaque: %s)",
|
|
async (opaque, { cli }) => {
|
|
const root = await createTestTempDirectory("rea-extraction-pyz-coverage-");
|
|
const source = join(root, "fixture.exe");
|
|
const pyz = pyzArchive({
|
|
magic: opaque ? Buffer.alloc(4) : PYTHON_39_MAGIC,
|
|
modules: [],
|
|
});
|
|
const healthy = Buffer.from("retained sibling bytes\n");
|
|
await writeFile(
|
|
source,
|
|
cArchiveExecutable({
|
|
carrier: "pe",
|
|
layout: "2.1",
|
|
pythonVersion: 309,
|
|
members: [
|
|
{ name: "PYZ.pyz", typeCode: "z", payload: pyz, compress: true },
|
|
{
|
|
name: "good.txt",
|
|
typeCode: "x",
|
|
payload: healthy,
|
|
compress: false,
|
|
},
|
|
],
|
|
}),
|
|
);
|
|
const cliResult = await cli.run({
|
|
arguments: ["extract-artifact", source, "--json"],
|
|
});
|
|
expect(cliResult.exitCode, cliResult.stdout + cliResult.stderr).toBe(0);
|
|
const cliEvidence = parseEvidence(cliResult.json);
|
|
const cliExtraction = artifactExtractionResultSchema.parse(
|
|
cliEvidence.normalized_result,
|
|
);
|
|
onTestFinished(() =>
|
|
rm(cliExtraction.output_root, { recursive: true, force: true }),
|
|
);
|
|
const call = await cli.sharedMcpCall?.();
|
|
if (call === undefined) throw new Error("Expected a stdio MCP session");
|
|
const opened = await call("open_binary", { path: source });
|
|
expect(opened.isError, JSON.stringify(opened)).not.toBe(true);
|
|
const reply = await call("extract_artifact", {});
|
|
expect(reply.isError, JSON.stringify(reply)).not.toBe(true);
|
|
const mcpEvidence = parseEvidence(reply.structuredContent);
|
|
const mcpExtraction = artifactExtractionResultSchema.parse(
|
|
mcpEvidence.normalized_result,
|
|
);
|
|
onTestFinished(() =>
|
|
rm(mcpExtraction.output_root, { recursive: true, force: true }),
|
|
);
|
|
const files = opaque
|
|
? ([
|
|
["PYZ.pyz", pyz],
|
|
["good.txt", healthy],
|
|
] as const)
|
|
: ([["good.txt", healthy]] as const);
|
|
for (const { evidence, result } of [
|
|
{ evidence: cliEvidence, result: cliExtraction },
|
|
{ evidence: mcpEvidence, result: mcpExtraction },
|
|
]) {
|
|
expect(
|
|
result.limitations.some((limitation) =>
|
|
limitation.includes("nested archive contents remain"),
|
|
),
|
|
).toBe(false);
|
|
expect(
|
|
result.artifacts.map(({ relative_path }) => relative_path),
|
|
).toEqual(files.map(([path]) => path));
|
|
for (const [path, bytes] of files) {
|
|
expect(await readFile(join(result.output_root, path))).toEqual(bytes);
|
|
expect(
|
|
result.artifacts.find((file) => file.relative_path === path),
|
|
).toMatchObject({
|
|
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
bytes_written: bytes.length,
|
|
});
|
|
}
|
|
for (const limitations of [result.limitations, evidence.limitations]) {
|
|
expect(
|
|
limitations.some((value) =>
|
|
value.includes("PYZ archives remain opaque"),
|
|
),
|
|
).toBe(opaque);
|
|
expect(
|
|
limitations.some((value) =>
|
|
value.includes("source traversal coverage is unknown"),
|
|
),
|
|
).toBe(opaque);
|
|
expect(
|
|
limitations.some((value) => value.startsWith("All regular files")),
|
|
).toBe(!opaque);
|
|
}
|
|
}
|
|
},
|
|
);
|