mirror of
https://github.com/morluto/rea
synced 2026-10-27 22:45:19 -04:00
404 lines
16 KiB
TypeScript
404 lines
16 KiB
TypeScript
import {
|
|
chmod,
|
|
mkdir,
|
|
readdir,
|
|
realpath,
|
|
symlink,
|
|
unlink,
|
|
writeFile,
|
|
} from "node:fs/promises";
|
|
import { join } from "node:path";
|
|
import { expect, it, onTestFinished, vi } from "vitest";
|
|
|
|
import { FirmwareProvider } from "../../../src/firmware/FirmwareProvider.js";
|
|
import { FirmwareAnalysisService } from "../../../src/application/firmware/FirmwareAnalysisService.js";
|
|
import { FirmwareFileAccess } from "../../../src/firmware/FirmwareFiles.js";
|
|
import { WakaruProvider } from "../../../src/javascript/recovery/WakaruProvider.js";
|
|
import { JavaScriptRecoveryService } from "../../../src/application/javascript/JavaScriptRecoveryService.js";
|
|
import {
|
|
spawnOwnedProviderProcess,
|
|
type ProviderProcessSupervisor,
|
|
} from "../../../src/process/ProviderProcess.js";
|
|
import { cleanupOwnedProcessGroup } from "../../../src/process/ProcessOwnership.js";
|
|
import { observeProcessStartIdentity } from "../../../src/process/ProcessOwnershipObservation.js";
|
|
import { AnalysisError } from "../../../src/domain/analysisErrorBase.js";
|
|
import { fileURLToPath } from "node:url";
|
|
import { preparePcapNativeBound } from "../../../src/pcap/PcapNativeBound.js";
|
|
import { runPcapCommand } from "../../../src/pcap/PcapCommand.js";
|
|
import { ArtifactAnalysisResources } from "../../../src/process/ArtifactAnalysisResources.js";
|
|
import { PacketCaptureService } from "../../../src/application/captures/PacketCaptureService.js";
|
|
import { SignalRecordingService } from "../../../src/application/signals/SignalRecordingService.js";
|
|
import { TsharkCaptureProvider } from "../../../src/captures/TsharkCaptureProvider.js";
|
|
import { SigrokRecordingProvider } from "../../../src/signals/SigrokRecordingProvider.js";
|
|
import { readStableArtifact } from "../../../src/artifacts/readStableArtifact.js";
|
|
import { PrivateRuntimeRoot } from "../../../src/process/PrivateRuntimeRoot.js";
|
|
import type { LinuxResourceLimits } from "../../../src/process/LinuxResourceLimits.js";
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
|
|
// Real children and filesystem, with simulated Linux admission/limits. These
|
|
// scripts do not establish native TShark, sigrok or prlimit behavior.
|
|
it.skipIf(process.platform === "win32").each(["tshark", "sigrok"] as const)(
|
|
"executes selected %s and limiter wrappers, retaining identity failure output after success",
|
|
async (kind) => {
|
|
const directory = await createTestTempDirectory("rea-selected-producer-");
|
|
const bin = join(directory, "bin"),
|
|
libexec = join(directory, "libexec");
|
|
await mkdir(bin);
|
|
await mkdir(libexec);
|
|
const command = join(bin, kind),
|
|
limiter = join(bin, "prlimit");
|
|
const commandSource = join(libexec, "producer"),
|
|
limiterSource = join(libexec, "limiter");
|
|
const replacement = join(libexec, "replacement");
|
|
const script =
|
|
'#!/bin/sh\nselected="${0%/*}/${0##*/}"\nIFS= read -r role < "$selected.role" || exit 19\nif [ "$role" = limiter ]; then\n if [ -f "$selected.retarget" ]; then\n IFS= read -r target < "$selected.retarget"\n /bin/ln -sf "$target" "$0"\n fi\n while [ "$1" != -- ]; do shift; done\n shift\n exec "$@"\nfi\nif [ "$1" = --version ]; then\n if [ "$role" = tshark ]; then printf "TShark (Wireshark) 4.6.9\\n"; else printf "sigrok-cli 0.7.2\\nlibsigrokdecode 0.5.3\\n"; fi\nelse\n if [ "$role" = tshark ]; then printf "P\\tInternet Protocol\\tip\\n"; else printf "Supported protocol decoders:\\n uart UART\\n"; fi\nfi\n';
|
|
for (const path of [commandSource, limiterSource, replacement]) {
|
|
await writeFile(path, script);
|
|
await chmod(path, 0o755);
|
|
}
|
|
await symlink(commandSource, command);
|
|
await symlink(limiterSource, limiter);
|
|
await writeFile(`${command}.role`, `${kind}\n`);
|
|
await writeFile(`${limiter}.role`, "limiter\n");
|
|
const environment = {
|
|
...process.env,
|
|
...(kind === "tshark"
|
|
? { REA_TSHARK_COMMAND: command, REA_TSHARK_PRLIMIT_COMMAND: limiter }
|
|
: { REA_SIGROK_COMMAND: command, REA_SIGROK_PRLIMIT_COMMAND: limiter }),
|
|
};
|
|
const runtime = () =>
|
|
PrivateRuntimeRoot.create({ parent: directory, prefix: "workspace-" });
|
|
const limits = async (configured: LinuxResourceLimits) => configured;
|
|
const provider =
|
|
kind === "tshark"
|
|
? new TsharkCaptureProvider(
|
|
environment,
|
|
undefined,
|
|
runtime,
|
|
"linux",
|
|
readStableArtifact,
|
|
limits,
|
|
)
|
|
: new SigrokRecordingProvider(
|
|
environment,
|
|
undefined,
|
|
runtime,
|
|
"linux",
|
|
readStableArtifact,
|
|
limits,
|
|
);
|
|
onTestFinished(() => provider.close());
|
|
const inspect =
|
|
provider instanceof TsharkCaptureProvider
|
|
? () => new PacketCaptureService(provider).inspectFields({})
|
|
: () => new SignalRecordingService(provider).inspectDecoders({});
|
|
const first = await inspect();
|
|
if (!first.ok) throw first.error;
|
|
expect(first.value.normalized_result).toMatchObject({
|
|
tool: { configured_path: command, path: await realpath(commandSource) },
|
|
resource_limiter: {
|
|
configured_path: limiter,
|
|
path: await realpath(limiterSource),
|
|
arguments: expect.arrayContaining([command]),
|
|
},
|
|
});
|
|
// Identical bytes at another physical path must not pass the current
|
|
// invocation's identity verification, even though the producer succeeds.
|
|
await writeFile(`${limiter}.retarget`, `${replacement}\n`);
|
|
const changed = await inspect();
|
|
if (changed.ok) throw new Error("Retargeted limiter was accepted");
|
|
expect(changed.error).toMatchObject({
|
|
_tag: "AnalysisArtifactChangedError",
|
|
path: limiter,
|
|
capturedOutput: {
|
|
stdout:
|
|
kind === "tshark"
|
|
? "TShark (Wireshark) 4.6.9\n"
|
|
: "sigrok-cli 0.7.2\nlibsigrokdecode 0.5.3\n",
|
|
},
|
|
});
|
|
await unlink(`${limiter}.retarget`);
|
|
const fresh = await inspect();
|
|
if (!fresh.ok) throw fresh.error;
|
|
expect(fresh.value.normalized_result).toMatchObject({
|
|
resource_limiter: { path: await realpath(replacement) },
|
|
});
|
|
expect(first.value.normalized_result).toMatchObject({
|
|
resource_limiter: { path: await realpath(limiterSource) },
|
|
});
|
|
expect(
|
|
(await readdir(directory)).filter((name) =>
|
|
name.startsWith("workspace-"),
|
|
),
|
|
).toEqual([]);
|
|
},
|
|
);
|
|
|
|
const selectedEngineLauncher = (kind: "firmware" | "wakaru"): string => {
|
|
const producer = fileURLToPath(
|
|
new URL(
|
|
kind === "firmware"
|
|
? "../../fixtures/firmware/producer.mjs"
|
|
: "../../fixtures/javascript-recovery/producer.mjs",
|
|
import.meta.url,
|
|
),
|
|
);
|
|
const invocation = [
|
|
process.execPath,
|
|
producer,
|
|
...(kind === "firmware" ? ["binwalk"] : []),
|
|
]
|
|
.map((argument) => `'${argument.replaceAll("'", "'\\''")}'`)
|
|
.join(" ");
|
|
return `#!/bin/sh\nIFS= read -r role < "$0.role" || exit 19\nif [ -f "$0.retarget" ]; then\n IFS= read -r target < "$0.retarget"\n /bin/ln -sf "$target" "$0"\nfi\nif [ "$role" = limiter ]; then\n while [ "$1" != -- ]; do shift; done\n shift\n exec "$@"\nfi\nexec ${invocation} "$@"\n`;
|
|
};
|
|
|
|
// Exercise shared CLI/MCP workflows with real children and filesystem. Only
|
|
// Linux/x64 admission and /proc limits are simulated; the launcher restores the
|
|
// actual host before entering real process supervision.
|
|
it.skipIf(process.platform === "win32").each(["firmware", "wakaru"] as const)(
|
|
"executes selected %s wrappers and retains completed output when either alias changes",
|
|
async (kind) => {
|
|
const directory = await createTestTempDirectory("rea-selected-command-");
|
|
const bin = join(directory, "bin"),
|
|
libexec = join(directory, "libexec");
|
|
await mkdir(bin);
|
|
await mkdir(libexec);
|
|
const command = join(bin, "engine"),
|
|
limiter = join(bin, "prlimit");
|
|
const commandSource = join(libexec, "engine"),
|
|
limiterSource = join(libexec, "limiter");
|
|
const replacement = join(libexec, "replacement");
|
|
const script = selectedEngineLauncher(kind);
|
|
for (const path of [commandSource, limiterSource, replacement])
|
|
await writeFile(path, script, { mode: 0o755 });
|
|
await symlink(commandSource, command);
|
|
await symlink(limiterSource, limiter);
|
|
await writeFile(`${command}.role`, `${kind}\n`);
|
|
await writeFile(`${limiter}.role`, "limiter\n");
|
|
const input = join(
|
|
directory,
|
|
kind === "firmware" ? "input.bin" : "input.js",
|
|
);
|
|
await writeFile(
|
|
input,
|
|
kind === "firmware" ? "fixture-input-data" : "module.exports = 42;",
|
|
);
|
|
const platform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
const arch = Object.getOwnPropertyDescriptor(process, "arch");
|
|
if (platform === undefined || arch === undefined)
|
|
throw new Error("Missing host descriptors");
|
|
const restoreHost = () => {
|
|
Object.defineProperty(process, "platform", platform);
|
|
Object.defineProperty(process, "arch", arch);
|
|
};
|
|
const launches: { cwd?: string; pid?: number }[] = [];
|
|
const launcher: typeof spawnOwnedProviderProcess = async (options) => {
|
|
restoreHost();
|
|
const child = await spawnOwnedProviderProcess(options);
|
|
launches.push({
|
|
...(options.cwd === undefined ? {} : { cwd: options.cwd }),
|
|
...(child.process.pid === undefined ? {} : { pid: child.process.pid }),
|
|
});
|
|
return child;
|
|
};
|
|
const fileSizeCeiling = vi
|
|
.spyOn(FirmwareFileAccess.prototype, "fileSizeCeiling")
|
|
.mockResolvedValue(Number.MAX_SAFE_INTEGER);
|
|
onTestFinished(() => fileSizeCeiling.mockRestore());
|
|
const service =
|
|
kind === "firmware"
|
|
? new FirmwareAnalysisService(
|
|
new FirmwareProvider(
|
|
{
|
|
...process.env,
|
|
REA_BINWALK_COMMAND: command,
|
|
REA_FIRMWARE_PRLIMIT_COMMAND: limiter,
|
|
},
|
|
launcher,
|
|
),
|
|
)
|
|
: new JavaScriptRecoveryService(
|
|
new WakaruProvider(
|
|
{
|
|
...process.env,
|
|
REA_WAKARU_COMMAND: command,
|
|
REA_JAVASCRIPT_PRLIMIT_COMMAND: limiter,
|
|
},
|
|
launcher,
|
|
),
|
|
);
|
|
onTestFinished(async () => {
|
|
restoreHost();
|
|
await service.close();
|
|
});
|
|
let sequence = 0;
|
|
const execute = async () => {
|
|
Object.defineProperty(process, "platform", {
|
|
...platform,
|
|
value: "linux",
|
|
});
|
|
Object.defineProperty(process, "arch", { ...arch, value: "x64" });
|
|
try {
|
|
return service instanceof FirmwareAnalysisService
|
|
? await service.execute("inspect_firmware_regions", { path: input })
|
|
: await service.recover({
|
|
path: input,
|
|
output_directory: join(directory, `output-${sequence++}`),
|
|
});
|
|
} finally {
|
|
restoreHost();
|
|
}
|
|
};
|
|
const first = await execute();
|
|
if (!first.ok) throw first.error;
|
|
expect(first.value.normalized_result).toMatchObject({
|
|
engine:
|
|
kind === "firmware"
|
|
? { executable_path: await realpath(commandSource) }
|
|
: { executable: { path: await realpath(commandSource) } },
|
|
});
|
|
expect(first.value.raw_result).toMatchObject({
|
|
[kind === "firmware" ? "version_execution" : "version"]: {
|
|
command,
|
|
resource_limiter: limiter,
|
|
},
|
|
});
|
|
for (const [selectedPath, target] of [
|
|
[command, replacement],
|
|
[limiter, replacement],
|
|
[command, command],
|
|
[limiter, limiter],
|
|
] as const) {
|
|
await writeFile(`${selectedPath}.retarget`, `${target}\n`);
|
|
const changed = await execute();
|
|
if (changed.ok) throw new Error("Retargeted executable was accepted");
|
|
expect(changed.error).toMatchObject({
|
|
_tag: "AnalysisArtifactChangedError",
|
|
path: selectedPath,
|
|
capturedOutput: {
|
|
stdout: kind === "firmware" ? "binwalk 3.1.0\n" : "wakaru 1.14.0\n",
|
|
},
|
|
});
|
|
await unlink(`${selectedPath}.retarget`);
|
|
await unlink(selectedPath);
|
|
await symlink(
|
|
selectedPath === command ? commandSource : limiterSource,
|
|
selectedPath,
|
|
);
|
|
}
|
|
for (const launch of launches) {
|
|
if (launch.pid !== undefined)
|
|
expect(await observeProcessStartIdentity(launch.pid)).toBeUndefined();
|
|
if (launch.cwd !== undefined)
|
|
await expect(readdir(launch.cwd)).rejects.toMatchObject({
|
|
code: "ENOENT",
|
|
});
|
|
}
|
|
},
|
|
);
|
|
|
|
it.skipIf(process.platform === "win32")(
|
|
"executes the selected PCAP limiter and retains completed output when its alias changes",
|
|
async () => {
|
|
const directory = await createTestTempDirectory(
|
|
"rea-selected-pcap-limiter-",
|
|
);
|
|
const selected = join(directory, "prlimit"),
|
|
physical = join(directory, "driver"),
|
|
replacement = join(directory, "replacement");
|
|
const script =
|
|
'#!/bin/sh\n[ -f "$0.role" ] || exit 19\nif [ -f "$0.retarget" ]; then\n IFS= read -r target < "$0.retarget"\n /bin/ln -sf "$target" "$0"\nfi\nwhile [ "$1" != -- ]; do shift; done\nshift\nexec "$@"\n';
|
|
await writeFile(physical, script, { mode: 0o755 });
|
|
await writeFile(replacement, script, { mode: 0o755 });
|
|
await symlink(physical, selected);
|
|
await writeFile(`${selected}.role`, "limiter");
|
|
const resources = new ArtifactAnalysisResources(
|
|
"pcap",
|
|
"inspect_pcap_capture",
|
|
readStableArtifact,
|
|
async (configured) => configured,
|
|
{ message: "busy", details: {} },
|
|
);
|
|
onTestFinished(() => resources.close());
|
|
const platform = Object.getOwnPropertyDescriptor(process, "platform");
|
|
if (platform === undefined) throw new Error("Missing host descriptor");
|
|
let prepared: Awaited<ReturnType<typeof preparePcapNativeBound>>;
|
|
try {
|
|
Object.defineProperty(process, "platform", {
|
|
...platform,
|
|
value: "linux",
|
|
});
|
|
prepared = await preparePcapNativeBound(
|
|
{ REA_TSHARK_PRLIMIT_COMMAND: selected },
|
|
resources,
|
|
);
|
|
} finally {
|
|
Object.defineProperty(process, "platform", platform);
|
|
}
|
|
let refuseCleanup = false;
|
|
let retained: ProviderProcessSupervisor | undefined;
|
|
onTestFinished(async () => {
|
|
refuseCleanup = false;
|
|
await retained?.stop();
|
|
});
|
|
const execute = () =>
|
|
runPcapCommand({
|
|
operation: "inspect_pcap_capture",
|
|
command: "/bin/echo",
|
|
arguments: ["selected limiter"],
|
|
limits: { timeoutMs: 10_000, maxOutputBytes: 1024 },
|
|
environment: process.env,
|
|
signal: undefined,
|
|
nativeBound: prepared.nativeBound,
|
|
launcher: async (options) => {
|
|
const child = await spawnOwnedProviderProcess(options);
|
|
return {
|
|
...child,
|
|
cleanup: () =>
|
|
refuseCleanup
|
|
? Promise.resolve({
|
|
cleaned: false,
|
|
reason: "controlled pending command cleanup",
|
|
})
|
|
: cleanupOwnedProcessGroup(child.ownership),
|
|
};
|
|
},
|
|
retain: (owner) => {
|
|
retained = owner;
|
|
},
|
|
});
|
|
await expect(execute()).resolves.toMatchObject({
|
|
stdout: "selected limiter\n",
|
|
});
|
|
await writeFile(`${selected}.retarget`, `${replacement}\n`);
|
|
await expect(execute()).rejects.toMatchObject({
|
|
_tag: "AnalysisArtifactChangedError",
|
|
path: selected,
|
|
capturedOutput: { stdout: "selected limiter\n" },
|
|
});
|
|
await unlink(selected);
|
|
await symlink(physical, selected);
|
|
await writeFile(`${selected}.retarget`, `${selected}\n`);
|
|
refuseCleanup = true;
|
|
const failed: unknown = await execute().catch((cause: unknown) => cause);
|
|
expect(failed).toBeInstanceOf(AnalysisError);
|
|
if (!(failed instanceof AnalysisError))
|
|
throw new Error("Expected typed cleanup failure");
|
|
expect(failed).toMatchObject({
|
|
cleanupIncomplete: true,
|
|
cleanup: { reason: "controlled pending command cleanup" },
|
|
capturedOutput: { stdout: "selected limiter\n" },
|
|
});
|
|
expect(failed.cleanupPrimary).toMatchObject({
|
|
_tag: "AnalysisArtifactChangedError",
|
|
path: selected,
|
|
});
|
|
refuseCleanup = false;
|
|
await expect(retained?.stop()).resolves.toMatchObject({
|
|
status: "verified-cleanup",
|
|
});
|
|
expect(failed.resolveCleanup().cleanupIncomplete).toBe(false);
|
|
},
|
|
);
|