Files
qiming/qimingclaw/crates/agent-electron-client/tests/sandbox-integration/macos-seatbelt.integration.test.ts

146 lines
6.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* macOS Seatbelt 沙箱集成测试
*
* 使用与生产完全一致的 seatbelt profile 格式(无 network*
* 验证危险操作被正确拦截。
*
* 运行条件:
* - platform === darwin
* - /usr/bin/sandbox-exec 存在
*/
import { existsSync, mkdirSync } from "fs";
import { execSync } from "child_process";
import { describe, it, expect, beforeAll } from "vitest";
import { runInSeatbelt, expectBlocked, expectAllowed } from "./shared-integration-utils";
const testOnMacOS = process.platform === "darwin" && existsSync("/usr/bin/sandbox-exec") ? describe : describe.skip;
const WORKSPACE_DIR = "/tmp/test-seatbelt-workspace";
const WORKSPACE_DIR_REALPATH = "/private/tmp/test-seatbelt-workspace";
// 确保 workspace 目录存在(宿主上)
beforeAll(() => {
try {
mkdirSync(WORKSPACE_DIR, { recursive: true });
} catch {}
});
const SEATBELT_PROFILE_NO_NETWORK = `(version 1)
(deny default)
(allow file-read*)
(allow process-exec (regex #"^/usr/bin/"))
(allow process-exec (regex #"^/bin/"))
(allow process-exec (regex #"^/usr/lib/"))
(allow process-fork)
(allow signal (target self))
(allow sysctl-read)
(allow mach-lookup)
(allow ipc-posix*)
(allow file-lock)
(allow file-write* (subpath "${WORKSPACE_DIR}"))
(allow file-write* (subpath "${WORKSPACE_DIR_REALPATH}"))
(allow file-write* (literal "/dev/null"))
(allow file-write* (literal "/dev/dtracehelper"))
(allow file-write* (literal "/dev/urandom"))
`;
const SEATBELT_PROFILE_WITH_NETWORK = SEATBELT_PROFILE_NO_NETWORK + "(allow network*)\n";
testOnMacOS("macOS seatbelt integration tests", () => {
// ==================== 文件写入测试 ====================
describe("file write operations", () => {
it("should BLOCK writing to /etc/hosts", async () => {
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "echo test >> /etc/hosts");
expectBlocked(result);
});
it("should BLOCK writing to /etc/passwd", async () => {
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "echo test >> /etc/passwd");
expectBlocked(result);
});
it("should BLOCK writing to /usr/bin/", async () => {
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "touch /usr/bin/evil-binary");
expectBlocked(result);
});
it("should BLOCK writing to ~/Documents/", async () => {
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "touch ~/Documents/exfil.txt");
expectBlocked(result);
});
it("should BLOCK writing to /tmp/ (not in writablePaths)", async () => {
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "touch /tmp/attacker-file.txt");
expectBlocked(result);
});
it("should ALLOW writing inside workspace", async () => {
// Workspace dir is pre-created by beforeAll on host.
// Only the write operation runs inside sandbox (mkdir already succeeded on host).
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "echo safe > /tmp/test-seatbelt-workspace/safe.txt");
expectAllowed(result);
});
});
// ==================== 文件删除测试 ====================
describe("file delete operations", () => {
it("should BLOCK deleting /etc/resolv.conf", async () => {
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "rm /etc/resolv.conf 2>&1 || true");
expectBlocked(result);
});
it("should BLOCK deleting /usr/bin/ls", async () => {
// Note: On SIP-protected systems, /usr/bin/ls can't be deleted anyway (SIP, not sandbox)
// We still expect blocked for other reasons
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "rm /usr/bin/ls 2>&1 || true");
// Just verify some error occurred (either sandbox or system)
const blocked = result.exitCode !== 0 || /permission|denied|not allowed|no such file/i.test(result.stderr + result.stdout);
expect(blocked).toBe(true);
});
});
// ==================== 提权测试 ====================
describe("privilege escalation", () => {
it("should BLOCK sudo id - sandbox denies exec", async () => {
// Without || true, exit code reflects sandbox failure
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "sudo id 2>&1");
// sandbox denies exec of sudo → "Operation not permitted"
const blocked = result.exitCode !== 0 ||
/permission|denied|not allowed|operation not permitted/i.test(result.stderr + result.stdout);
expect(blocked).toBe(true);
});
});
// ==================== 系统命令测试 ====================
describe("system commands", () => {
it("should BLOCK shutdown - permission denied by sandbox", async () => {
// Without || true, the actual exit code will be captured
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "/sbin/shutdown -h now 2>&1");
// sandbox blocks exec of /sbin/shutdown → "Operation not permitted"
const blocked = result.exitCode !== 0 ||
/permission|denied|not allowed|operation not permitted/i.test(result.stderr + result.stdout);
expect(blocked).toBe(true);
});
});
// ==================== 网络测试 ====================
describe("network access", () => {
it("should BLOCK network access (no network* profile)", async () => {
// Use curl without -s to see error messages; no || true so exitCode reflects curl
const result = await runInSeatbelt(SEATBELT_PROFILE_NO_NETWORK, "curl --max-time 5 http://example.com 2>&1");
// curl exit 6 = Could not resolve host (DNS blocked by sandbox)
const blocked = result.exitCode !== 0 ||
/could not resolve|failed|connection|network|permission/i.test(result.stderr + result.stdout);
expect(blocked).toBe(true);
});
it("should ALLOW network access (with network* profile)", async () => {
// With network* allowed, curl error would be non-permission (e.g., exit 6 = DNS fail)
const result = await runInSeatbelt(SEATBELT_PROFILE_WITH_NETWORK, "curl --max-time 10 http://example.com 2>&1");
// If exit is non-zero, it should NOT be due to permission denied by sandbox
if (result.exitCode !== 0) {
expect(/permission|denied.*network|operation not permitted/i.test(result.stderr + result.stdout)).toBe(false);
}
});
});
});