// git-clone-sandbox.test.mjs — Tests for sandboxed git clone + fs-utils tmppath // Zero external dependencies: node:test + node:assert only. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { spawnSync } from 'node:child_process'; import { existsSync, rmSync, readFileSync, realpathSync } from 'node:fs'; import { join } from 'node:path'; import { tmpdir } from 'node:os'; import { fileURLToPath } from 'node:url'; const __dirname = fileURLToPath(new URL('.', import.meta.url)); const LIB_DIR = join(__dirname, '..', '..', 'scanners', 'lib'); const GIT_CLONE = join(LIB_DIR, 'git-clone.mjs'); const FS_UTILS = join(LIB_DIR, 'fs-utils.mjs'); // --------------------------------------------------------------------------- // Import sandbox exports for unit testing // --------------------------------------------------------------------------- const { GIT_SANDBOX_CONFIG, GIT_SANDBOX_ENV, buildSandboxProfile, buildBwrapArgs, buildSandboxedClone, MAX_CLONE_SIZE_MB, } = await import('../../scanners/lib/git-clone.mjs'); // --------------------------------------------------------------------------- // GIT_SANDBOX_CONFIG // --------------------------------------------------------------------------- describe('GIT_SANDBOX_CONFIG', () => { it('disables hooks', () => { const idx = GIT_SANDBOX_CONFIG.indexOf('core.hooksPath=/dev/null'); assert.ok(idx > 0, 'core.hooksPath=/dev/null must be in config flags'); }); it('disables symlinks', () => { assert.ok(GIT_SANDBOX_CONFIG.includes('core.symlinks=false')); }); it('disables fsmonitor', () => { assert.ok(GIT_SANDBOX_CONFIG.includes('core.fsmonitor=false')); }); it('disables LFS filter drivers', () => { assert.ok(GIT_SANDBOX_CONFIG.includes('filter.lfs.process=')); assert.ok(GIT_SANDBOX_CONFIG.includes('filter.lfs.smudge=')); assert.ok(GIT_SANDBOX_CONFIG.includes('filter.lfs.clean=')); }); it('blocks local file protocol', () => { assert.ok(GIT_SANDBOX_CONFIG.includes('protocol.file.allow=never')); }); it('enables fsck on transfer', () => { assert.ok(GIT_SANDBOX_CONFIG.includes('transfer.fsckObjects=true')); }); it('has 8 -c flag pairs (16 elements)', () => { const cCount = GIT_SANDBOX_CONFIG.filter(f => f === '-c').length; assert.equal(cCount, 8, 'Should have exactly 8 -c flags'); }); }); // --------------------------------------------------------------------------- // GIT_SANDBOX_ENV // --------------------------------------------------------------------------- describe('GIT_SANDBOX_ENV', () => { it('sets GIT_CONFIG_NOSYSTEM', () => { assert.equal(GIT_SANDBOX_ENV.GIT_CONFIG_NOSYSTEM, '1'); }); it('sets GIT_CONFIG_GLOBAL to /dev/null', () => { assert.equal(GIT_SANDBOX_ENV.GIT_CONFIG_GLOBAL, '/dev/null'); }); it('sets GIT_ATTR_NOSYSTEM', () => { assert.equal(GIT_SANDBOX_ENV.GIT_ATTR_NOSYSTEM, '1'); }); it('sets GIT_TERMINAL_PROMPT to 0', () => { assert.equal(GIT_SANDBOX_ENV.GIT_TERMINAL_PROMPT, '0'); }); it('preserves existing PATH', () => { assert.ok(GIT_SANDBOX_ENV.PATH, 'PATH must be preserved from process.env'); }); }); // --------------------------------------------------------------------------- // buildSandboxProfile // --------------------------------------------------------------------------- describe('buildSandboxProfile', () => { it('returns a profile string on macOS', () => { if (process.platform !== 'darwin') return; // Use tmpdir() which always exists — realpathSync needs an existing path const profile = buildSandboxProfile(tmpdir()); assert.ok(profile !== null, 'Should return a profile on macOS'); assert.ok(profile.includes('(version 1)'), 'Profile must start with version'); assert.ok(profile.includes('(deny file-write*)'), 'Must deny writes by default'); }); it('includes the resolved real path in the profile', () => { if (process.platform !== 'darwin') return; const realPath = realpathSync(tmpdir()); const profile = buildSandboxProfile(tmpdir()); assert.ok(profile.includes(realPath), `Profile must contain resolved path: ${realPath}`); }); it('allows /dev/null and /dev/tty writes', () => { if (process.platform !== 'darwin') return; const profile = buildSandboxProfile(tmpdir()); assert.ok(profile.includes('/dev/null'), 'Must allow /dev/null'); assert.ok(profile.includes('/dev/tty'), 'Must allow /dev/tty'); }); }); // --------------------------------------------------------------------------- // buildBwrapArgs // --------------------------------------------------------------------------- describe('buildBwrapArgs', () => { it('returns null on non-Linux platforms', () => { if (process.platform === 'linux') return; const result = buildBwrapArgs('/tmp/test', ['git', 'clone']); assert.equal(result, null, 'Should return null on non-Linux'); }); it('on Linux: returns args array if bwrap is available', () => { if (process.platform !== 'linux') return; const check = spawnSync('which', ['bwrap'], { encoding: 'utf8' }); if (check.status !== 0) return; // bwrap not installed, skip const result = buildBwrapArgs('/tmp/test-bwrap', ['git', 'clone']); if (result === null) return; // bwrap installed but fails (Ubuntu 24.04+) assert.ok(Array.isArray(result), 'Should return an array'); assert.ok(result.includes('--ro-bind'), 'Should include --ro-bind'); assert.ok(result.includes('--unshare-all'), 'Should include --unshare-all'); assert.ok(result.includes('/tmp/test-bwrap'), 'Should include the allowed write path'); }); }); // --------------------------------------------------------------------------- // buildSandboxedClone // --------------------------------------------------------------------------- describe('buildSandboxedClone', () => { it('returns cmd, args, and sandbox properties', () => { const result = buildSandboxedClone(tmpdir(), ['clone', '--depth', '1', 'url', tmpdir()]); assert.ok(result.cmd, 'Must have cmd'); assert.ok(Array.isArray(result.args), 'args must be an array'); assert.ok('sandbox' in result, 'Must have sandbox property'); }); it('uses sandbox-exec on macOS', () => { if (process.platform !== 'darwin') return; const result = buildSandboxedClone(tmpdir(), ['clone', '--depth', '1', 'url', tmpdir()]); assert.equal(result.sandbox, 'sandbox-exec'); assert.equal(result.cmd, 'sandbox-exec'); }); it('includes git config flags in args regardless of platform', () => { const result = buildSandboxedClone(tmpdir(), ['clone', '--depth', '1', 'url', tmpdir()]); const argsStr = result.args.join(' '); assert.ok(argsStr.includes('core.hooksPath=/dev/null'), 'Must include hooksPath'); assert.ok(argsStr.includes('core.symlinks=false'), 'Must include symlinks=false'); }); it('falls back gracefully with sandbox=null when no OS sandbox', () => { // This test verifies the structure — on macOS/Linux with sandbox available, // it will have a sandbox. The key assertion is structural. const result = buildSandboxedClone(tmpdir(), ['clone', 'url', tmpdir()]); if (result.sandbox === null) { assert.equal(result.cmd, 'git', 'Fallback must use git directly'); } }); }); // --------------------------------------------------------------------------- // MAX_CLONE_SIZE_MB // --------------------------------------------------------------------------- describe('MAX_CLONE_SIZE_MB', () => { it('is 100', () => { assert.equal(MAX_CLONE_SIZE_MB, 100); }); }); // --------------------------------------------------------------------------- // fs-utils tmppath uniqueness // --------------------------------------------------------------------------- describe('fs-utils tmppath', () => { it('generates unique paths for the same base name', () => { const paths = new Set(); for (let i = 0; i < 5; i++) { const result = spawnSync('node', [FS_UTILS, 'tmppath', 'content-extract.json'], { encoding: 'utf8', }); assert.equal(result.status, 0, `tmppath should exit 0, got: ${result.stderr}`); paths.add(result.stdout.trim()); } assert.equal(paths.size, 5, 'All 5 paths should be unique'); }); it('preserves file extension', () => { const result = spawnSync('node', [FS_UTILS, 'tmppath', 'test-file.json'], { encoding: 'utf8', }); assert.ok(result.stdout.trim().endsWith('.json'), 'Should preserve .json extension'); }); it('preserves base name prefix', () => { const result = spawnSync('node', [FS_UTILS, 'tmppath', 'my-evidence.json'], { encoding: 'utf8', }); assert.ok(result.stdout.trim().includes('my-evidence-'), 'Should contain base name prefix'); }); it('paths are under tmpdir', () => { const result = spawnSync('node', [FS_UTILS, 'tmppath', 'test.json'], { encoding: 'utf8', }); const path = result.stdout.trim(); assert.ok(path.startsWith(tmpdir()), `Path should be under tmpdir: ${path}`); }); }); // --------------------------------------------------------------------------- // git-clone CLI: validate // --------------------------------------------------------------------------- describe('git-clone validate', () => { it('accepts valid HTTPS GitHub URL', () => { const result = spawnSync('node', [GIT_CLONE, 'validate', 'https://github.com/org/repo'], { encoding: 'utf8', }); assert.equal(result.status, 0); }); it('accepts valid SSH GitHub URL', () => { const result = spawnSync('node', [GIT_CLONE, 'validate', 'git@github.com:org/repo.git'], { encoding: 'utf8', }); assert.equal(result.status, 0); }); it('rejects non-GitHub URL', () => { const result = spawnSync('node', [GIT_CLONE, 'validate', 'https://evil.com/repo'], { encoding: 'utf8', }); assert.equal(result.status, 1); }); it('rejects URL with tree path', () => { const result = spawnSync('node', [GIT_CLONE, 'validate', 'https://github.com/org/repo/tree/main/dir'], { encoding: 'utf8', }); assert.equal(result.status, 1); }); }); // --------------------------------------------------------------------------- // git-clone CLI: cleanup safety // --------------------------------------------------------------------------- describe('git-clone cleanup', () => { it('refuses to remove paths outside tmpdir', () => { const result = spawnSync('node', [GIT_CLONE, 'cleanup', '/home/user/important'], { encoding: 'utf8', }); assert.equal(result.status, 1); assert.ok(result.stderr.includes('refusing to remove')); }); it('handles non-existent tmpdir path gracefully', () => { const fakePath = join(tmpdir(), 'llm-sec-nonexistent-test-' + Date.now()); const result = spawnSync('node', [GIT_CLONE, 'cleanup', fakePath], { encoding: 'utf8', }); assert.equal(result.status, 0, 'Should exit 0 for non-existent path in tmpdir'); }); });