173 lines
6.0 KiB
TypeScript
173 lines
6.0 KiB
TypeScript
import { assertEquals, assertRejects, assertStringIncludes } from "@std/assert";
|
|
import { join } from "@std/path";
|
|
import {
|
|
assertBundleIsSecretFree,
|
|
redactText,
|
|
safeUrl,
|
|
writePrivateJson,
|
|
} from "../src/artifacts.ts";
|
|
import {
|
|
PROCESS_LOG_BYTE_LIMIT,
|
|
startOwnedProcesses,
|
|
stopOwnedProcesses,
|
|
} from "../src/processes.ts";
|
|
|
|
Deno.test("redaction removes common credentials and query values", () => {
|
|
const redacted = redactText(
|
|
"Authorization: Bearer abc.def cookie=session-value token=secret-value",
|
|
["abc.def"],
|
|
);
|
|
assertStringIncludes(redacted, "[REDACTED]");
|
|
assertEquals(redacted.includes("abc.def"), false);
|
|
assertEquals(redacted.includes("session-value"), false);
|
|
assertEquals(
|
|
safeUrl("https://user:pass@example.test/path?token=secret#fragment"),
|
|
"https://example.test/path?token=%5BREDACTED%5D",
|
|
);
|
|
assertRejects(
|
|
async () => assertBundleIsSecretFree('{"authorization":"Bearer abc"}'),
|
|
Error,
|
|
"forbidden secret marker",
|
|
);
|
|
});
|
|
|
|
Deno.test("private JSON state uses owner-only permissions", async () => {
|
|
const directory = await Deno.makeTempDir();
|
|
try {
|
|
const path = join(directory, "state", "owner.json");
|
|
await writePrivateJson(path, { cookies: [], origins: [] });
|
|
assertEquals(JSON.parse(await Deno.readTextFile(path)), { cookies: [], origins: [] });
|
|
if (Deno.build.os !== "windows") assertEquals((await Deno.stat(path)).mode! & 0o777, 0o600);
|
|
} finally {
|
|
await Deno.remove(directory, { recursive: true });
|
|
}
|
|
});
|
|
|
|
Deno.test("owned process is terminated and its logs are redacted", async () => {
|
|
const directory = await Deno.makeTempDir();
|
|
const scenario = join(directory, "scenario.json");
|
|
await Deno.writeTextFile(scenario, "{}");
|
|
try {
|
|
const processes = await startOwnedProcesses(
|
|
[{
|
|
id: "fixture",
|
|
command: Deno.execPath(),
|
|
args: ["eval", 'console.log("authorization: secret-value"); setInterval(() => {}, 1000)'],
|
|
}],
|
|
scenario,
|
|
join(directory, "logs"),
|
|
["secret-value"],
|
|
);
|
|
assertEquals(processes.length, 1);
|
|
const logPath = join(directory, "logs", "fixture.stdout.log");
|
|
for (let attempt = 0; attempt < 20; attempt++) {
|
|
try {
|
|
if ((await Deno.readTextFile(logPath)).length > 0) break;
|
|
} catch {
|
|
// The output pump creates the file asynchronously.
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
const diagnostics = await stopOwnedProcesses(processes);
|
|
assertEquals(diagnostics, []);
|
|
const log = await Deno.readTextFile(logPath);
|
|
assertEquals(log.includes("secret-value"), false);
|
|
assertStringIncludes(log, "[REDACTED]");
|
|
const metadata = JSON.parse(await Deno.readTextFile(`${logPath}.meta.json`));
|
|
assertEquals(metadata.truncated, false);
|
|
} finally {
|
|
await Deno.remove(directory, { recursive: true });
|
|
}
|
|
});
|
|
|
|
Deno.test("owned process logs stop at the byte limit and record truncation", async () => {
|
|
const directory = await Deno.makeTempDir();
|
|
const scenario = join(directory, "scenario.json");
|
|
await Deno.writeTextFile(scenario, "{}");
|
|
try {
|
|
const processes = await startOwnedProcesses(
|
|
[{
|
|
id: "large-output",
|
|
command: Deno.execPath(),
|
|
args: [
|
|
"eval",
|
|
`console.log("x".repeat(${
|
|
PROCESS_LOG_BYTE_LIMIT + 32_768
|
|
})); setInterval(() => {}, 1000)`,
|
|
],
|
|
}],
|
|
scenario,
|
|
join(directory, "logs"),
|
|
[],
|
|
);
|
|
const logPath = join(directory, "logs", "large-output.stdout.log");
|
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
try {
|
|
if ((await Deno.stat(logPath)).size >= PROCESS_LOG_BYTE_LIMIT) break;
|
|
} catch {
|
|
// The output pump creates the file asynchronously.
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
assertEquals(await stopOwnedProcesses(processes), []);
|
|
assertEquals((await Deno.stat(logPath)).size, PROCESS_LOG_BYTE_LIMIT);
|
|
const metadata = JSON.parse(await Deno.readTextFile(`${logPath}.meta.json`));
|
|
assertEquals(metadata.byteLimit, PROCESS_LOG_BYTE_LIMIT);
|
|
assertEquals(metadata.truncated, true);
|
|
assertEquals(metadata.bytesWritten, PROCESS_LOG_BYTE_LIMIT);
|
|
} finally {
|
|
await Deno.remove(directory, { recursive: true });
|
|
}
|
|
});
|
|
|
|
Deno.test("forced cleanup terminates a TERM-resistant descendant", async () => {
|
|
if (Deno.build.os === "windows") return;
|
|
const directory = await Deno.makeTempDir();
|
|
const scenario = join(directory, "scenario.json");
|
|
const childPidPath = join(directory, "child.pid");
|
|
await Deno.writeTextFile(scenario, "{}");
|
|
try {
|
|
const childProgram = 'Deno.addSignalListener("SIGTERM", () => {}); setInterval(() => {}, 1000)';
|
|
const parentProgram = `
|
|
const child = new Deno.Command(Deno.execPath(), {
|
|
args: ["eval", ${JSON.stringify(childProgram)}],
|
|
stdout: "null",
|
|
stderr: "null"
|
|
}).spawn();
|
|
Deno.writeTextFileSync(Deno.args[0], String(child.pid));
|
|
Deno.addSignalListener("SIGTERM", () => {});
|
|
setInterval(() => {}, 1000);
|
|
`;
|
|
const processes = await startOwnedProcesses(
|
|
[{
|
|
id: "process-tree",
|
|
command: Deno.execPath(),
|
|
args: ["eval", parentProgram, childPidPath],
|
|
}],
|
|
scenario,
|
|
join(directory, "logs"),
|
|
[],
|
|
);
|
|
let childPid = 0;
|
|
for (let attempt = 0; attempt < 100; attempt++) {
|
|
try {
|
|
childPid = Number(await Deno.readTextFile(childPidPath));
|
|
if (childPid > 0) break;
|
|
} catch {
|
|
// The fixture publishes its descendant PID after spawn.
|
|
}
|
|
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
}
|
|
assertEquals(childPid > 0, true);
|
|
assertEquals(await stopOwnedProcesses(processes), []);
|
|
const status = await new Deno.Command("ps", {
|
|
args: ["-p", String(childPid), "-o", "pid="],
|
|
stdout: "piped",
|
|
stderr: "null",
|
|
}).output();
|
|
assertEquals(new TextDecoder().decode(status.stdout).trim(), "");
|
|
} finally {
|
|
await Deno.remove(directory, { recursive: true });
|
|
}
|
|
});
|