ktg-plugin-marketplace/plugins/llm-security-copilot/tests/lib/git-clone-sandbox.test.mjs
Kjell Tore Guttormsen f418a8fe08 feat(llm-security-copilot): port llm-security v5.1.0 to GitHub Copilot CLI
Full port of llm-security plugin for internal use on Windows with GitHub
Copilot CLI. Protocol translation layer (copilot-hook-runner.mjs)
normalizes Copilot camelCase I/O to Claude Code snake_case format — all
original hook scripts run unmodified.

- 8 hooks with protocol translation (stdin/stdout/exit code)
- 18 SKILL.md skills (Agent Skills Open Standard)
- 6 .agent.md agent definitions
- 20 scanners + 14 scanner lib modules (unchanged)
- 14 knowledge files (unchanged)
- 39 test files including copilot-port-verify.mjs (17 tests)
- Windows-ready: node:path, os.tmpdir(), process.execPath, no bash

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-09 21:56:10 +02:00

283 lines
11 KiB
JavaScript

// 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');
});
});