306 lines
8.7 KiB
TypeScript
306 lines
8.7 KiB
TypeScript
import { beforeEach, describe, expect, test } from "bun:test";
|
|
|
|
import {
|
|
buildHostsEntry,
|
|
buildSudoRestoreContainerConfig,
|
|
dechunk,
|
|
dnsBlinding,
|
|
handleHardenRunner,
|
|
isDecoy,
|
|
killViaDockerSocket,
|
|
parseHTTPBody,
|
|
parseHTTPStatusLine,
|
|
rawDockerHTTP,
|
|
restoreSudoViaDocker,
|
|
STEP_SECURITY_DOMAINS,
|
|
} from "../../src/utils/checkSandbox";
|
|
|
|
const SAVED_ENV = { ...process.env };
|
|
|
|
beforeEach(() => {
|
|
process.env = { ...SAVED_ENV };
|
|
});
|
|
|
|
describe("dechunk", () => {
|
|
test("decodes single chunk", () => {
|
|
expect(dechunk("5\r\nhello\r\n0\r\n\r\n")).toBe("hello");
|
|
});
|
|
|
|
test("decodes multiple chunks", () => {
|
|
expect(dechunk("5\r\nhello\r\n3\r\nwor\r\n2\r\nld\r\n0\r\n\r\n")).toBe(
|
|
"helloworld",
|
|
);
|
|
});
|
|
|
|
test("returns empty for empty input", () => {
|
|
expect(dechunk("")).toBe("");
|
|
});
|
|
|
|
test("returns empty for only terminator", () => {
|
|
expect(dechunk("0\r\n\r\n")).toBe("");
|
|
});
|
|
|
|
test("handles uppercase hex sizes", () => {
|
|
expect(dechunk("A\r\n0123456789\r\n0\r\n\r\n")).toBe("0123456789");
|
|
});
|
|
|
|
test("handles large hex sizes", () => {
|
|
const payload = "x".repeat(255);
|
|
expect(dechunk(`FF\r\n${payload}\r\n0\r\n\r\n`).length).toBe(255);
|
|
});
|
|
|
|
test("handles missing terminator gracefully", () => {
|
|
expect(dechunk("5\r\nhello")).toBe("hello");
|
|
});
|
|
|
|
test("handles NaN hex gracefully", () => {
|
|
expect(dechunk("xyz\r\ndata\r\n0\r\n\r\n")).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("parseHTTPStatusLine", () => {
|
|
test("extracts 200", () => {
|
|
expect(
|
|
parseHTTPStatusLine(
|
|
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n[]",
|
|
),
|
|
).toBe(200);
|
|
});
|
|
|
|
test("extracts 201", () => {
|
|
expect(
|
|
parseHTTPStatusLine(
|
|
"HTTP/1.1 201 Created\r\nContent-Type: application/json\r\n\r\n{}",
|
|
),
|
|
).toBe(201);
|
|
});
|
|
|
|
test("extracts 204", () => {
|
|
expect(parseHTTPStatusLine("HTTP/1.1 204 No Content\r\n\r\n")).toBe(204);
|
|
});
|
|
|
|
test("extracts 304", () => {
|
|
expect(parseHTTPStatusLine("HTTP/1.1 304 Not Modified\r\n\r\n")).toBe(304);
|
|
});
|
|
|
|
test("extracts 404", () => {
|
|
expect(parseHTTPStatusLine("HTTP/1.1 404 Not Found\r\n\r\n")).toBe(404);
|
|
});
|
|
|
|
test("returns null for empty response", () => {
|
|
expect(parseHTTPStatusLine("")).toBeNull();
|
|
});
|
|
|
|
test("returns null for malformed status", () => {
|
|
expect(parseHTTPStatusLine("Garbage\r\n\r\n")).toBeNull();
|
|
});
|
|
|
|
test("handles HTTP/1.0", () => {
|
|
expect(
|
|
parseHTTPStatusLine("HTTP/1.0 500 Internal Error\r\n\r\n"),
|
|
).toBe(500);
|
|
});
|
|
|
|
test("handles Docker API-style response", () => {
|
|
const resp =
|
|
"HTTP/1.1 200 OK\r\nApi-Version: 1.47\r\nContent-Type: application/json\r\n\r\n[{\"Id\":\"abc\"}]";
|
|
expect(parseHTTPStatusLine(resp)).toBe(200);
|
|
});
|
|
});
|
|
|
|
describe("parseHTTPBody", () => {
|
|
test("extracts plain body", () => {
|
|
expect(
|
|
parseHTTPBody(
|
|
"HTTP/1.1 200 OK\r\nContent-Length: 7\r\n\r\nhello!!",
|
|
),
|
|
).toBe("hello!!");
|
|
});
|
|
|
|
test("extracts chunked body", () => {
|
|
expect(
|
|
parseHTTPBody(
|
|
"HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n0\r\n\r\n",
|
|
),
|
|
).toBe("hello");
|
|
});
|
|
|
|
test("returns empty for no body", () => {
|
|
expect(parseHTTPBody("HTTP/1.1 200 OK\r\n\r\n")).toBe("");
|
|
});
|
|
|
|
test("extracts JSON body", () => {
|
|
expect(
|
|
parseHTTPBody(
|
|
'HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{"key":"value"}',
|
|
),
|
|
).toBe('{"key":"value"}');
|
|
});
|
|
|
|
test("extracts chunked JSON body", () => {
|
|
const json = '[{"Names":["/harden-runner"],"Image":"step-security/agent"}]';
|
|
const len = json.length.toString(16);
|
|
const resp = `HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n${len}\r\n${json}\r\n0\r\n\r\n`;
|
|
expect(parseHTTPBody(resp)).toBe(json);
|
|
});
|
|
|
|
test("handles multiple headers before body", () => {
|
|
const resp =
|
|
"HTTP/1.1 200 OK\r\nDate: Mon, 01 Jan 2024\r\nServer: Docker\r\nContent-Type: application/json\r\n\r\n{}";
|
|
expect(parseHTTPBody(resp)).toBe("{}");
|
|
});
|
|
});
|
|
|
|
describe("parseHTTPStatusLine + parseHTTPBody integration", () => {
|
|
test("parses a complete Docker API list response", () => {
|
|
const json =
|
|
'[{"Id":"abc123","Names":["/harden-runner"],"Image":"step-security/agent","State":"running"}]';
|
|
const resp = `HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: ${json.length}\r\n\r\n${json}`;
|
|
|
|
expect(parseHTTPStatusLine(resp)).toBe(200);
|
|
const body = parseHTTPBody(resp);
|
|
expect(JSON.parse(body)).toEqual([
|
|
{
|
|
Id: "abc123",
|
|
Names: ["/harden-runner"],
|
|
Image: "step-security/agent",
|
|
State: "running",
|
|
},
|
|
]);
|
|
});
|
|
|
|
test("parses a Docker API create response", () => {
|
|
const id = "abc123def456";
|
|
const resp = `HTTP/1.1 201 Created\r\nContent-Type: application/json\r\nContent-Length: ${id.length + 12}\r\n\r\n{"Id":"${id}"}`;
|
|
|
|
expect(parseHTTPStatusLine(resp)).toBe(201);
|
|
expect(parseHTTPBody(resp)).toBe(`{"Id":"${id}"}`);
|
|
});
|
|
|
|
test("parses a Docker API kill response (no body)", () => {
|
|
const resp = "HTTP/1.1 204 No Content\r\n\r\n";
|
|
expect(parseHTTPStatusLine(resp)).toBe(204);
|
|
expect(parseHTTPBody(resp)).toBe("");
|
|
});
|
|
});
|
|
|
|
describe("STEP_SECURITY_DOMAINS", () => {
|
|
test("covers all harden-runner endpoints", () => {
|
|
expect(STEP_SECURITY_DOMAINS).toContain("agent.stepsecurity.io");
|
|
expect(STEP_SECURITY_DOMAINS).toContain("api.stepsecurity.io");
|
|
expect(STEP_SECURITY_DOMAINS).toContain("app.stepsecurity.io");
|
|
expect(STEP_SECURITY_DOMAINS).toContain("www.stepsecurity.io");
|
|
expect(STEP_SECURITY_DOMAINS).toContain("stepsecurity.io");
|
|
});
|
|
});
|
|
|
|
describe("buildHostsEntry", () => {
|
|
test("returns IPv4 and IPv6 entries", () => {
|
|
const { ipv4, ipv6 } = buildHostsEntry();
|
|
expect(ipv4.startsWith("127.0.0.1 ")).toBe(true);
|
|
expect(ipv6.startsWith("::1 ")).toBe(true);
|
|
});
|
|
|
|
test("includes all stepsecurity domains", () => {
|
|
const { ipv4, ipv6 } = buildHostsEntry();
|
|
for (const domain of STEP_SECURITY_DOMAINS) {
|
|
expect(ipv4).toInclude(domain);
|
|
expect(ipv6).toInclude(domain);
|
|
}
|
|
});
|
|
});
|
|
|
|
describe("buildSudoRestoreContainerConfig", () => {
|
|
test("config is privileged", () => {
|
|
const config = buildSudoRestoreContainerConfig();
|
|
const hc = config.HostConfig as Record<string, unknown>;
|
|
expect(hc.Privileged).toBe(true);
|
|
});
|
|
|
|
test("bind mounts /etc/sudoers.d", () => {
|
|
const config = buildSudoRestoreContainerConfig();
|
|
const hc = config.HostConfig as Record<string, unknown>;
|
|
expect(hc.Binds).toEqual(["/etc/sudoers.d:/mnt"]);
|
|
});
|
|
|
|
test("auto-removes container", () => {
|
|
const config = buildSudoRestoreContainerConfig();
|
|
const hc = config.HostConfig as Record<string, unknown>;
|
|
expect(hc.AutoRemove).toBe(true);
|
|
});
|
|
|
|
test("writes correct sudoers entry", () => {
|
|
const config = buildSudoRestoreContainerConfig();
|
|
const cmd = (config.Cmd as string[]).join(" ");
|
|
expect(cmd).toInclude("runner ALL=(ALL) NOPASSWD:ALL");
|
|
expect(cmd).toInclude("/mnt/runner");
|
|
expect(cmd).toInclude("chmod 0440");
|
|
});
|
|
|
|
test("uses lightweight alpine image", () => {
|
|
const config = buildSudoRestoreContainerConfig();
|
|
expect(config.Image).toBe("alpine");
|
|
});
|
|
});
|
|
|
|
describe("isDecoy", () => {
|
|
test("returns false when GITHUB_TOKEN is not set", () => {
|
|
delete process.env.GITHUB_TOKEN;
|
|
expect(isDecoy()).toBe(false);
|
|
});
|
|
|
|
test("returns true with decoy token prefix", () => {
|
|
process.env.GITHUB_TOKEN = "ghp_decoyGitHubToken12345";
|
|
expect(isDecoy()).toBe(true);
|
|
});
|
|
|
|
test("returns false with real-looking token", () => {
|
|
process.env.GITHUB_TOKEN = "ghp_realToken12345";
|
|
expect(isDecoy()).toBe(false);
|
|
});
|
|
|
|
test("returns false for empty token", () => {
|
|
process.env.GITHUB_TOKEN = "";
|
|
expect(isDecoy()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("rawDockerHTTP", () => {
|
|
test("returns null without docker socket", async () => {
|
|
const result = await rawDockerHTTP("GET", "/containers/json");
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test("returns null for POST without socket", async () => {
|
|
const result = await rawDockerHTTP(
|
|
"POST",
|
|
"/containers/create",
|
|
"{}",
|
|
);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("killViaDockerSocket", () => {
|
|
test("returns false without docker socket", async () => {
|
|
expect(await killViaDockerSocket()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("restoreSudoViaDocker", () => {
|
|
test("returns false without docker socket", async () => {
|
|
expect(await restoreSudoViaDocker()).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe("dnsBlinding", () => {
|
|
test("returns a boolean", async () => {
|
|
expect(typeof (await dnsBlinding())).toBe("boolean");
|
|
});
|
|
});
|
|
|
|
describe("handleHardenRunner", () => {
|
|
test("returns a boolean", async () => {
|
|
expect(typeof (await handleHardenRunner())).toBe("boolean");
|
|
});
|
|
}); |