162 lines
5.8 KiB
JavaScript
162 lines
5.8 KiB
JavaScript
import { describe, it, expect } from 'vitest';
|
|
import { matchBashHardBlacklist } from './enforce-router-gate.mjs';
|
|
|
|
describe('matchBashHardBlacklist — v3.9 keep', () => {
|
|
it.each([
|
|
'rm -rf build',
|
|
'mv a b',
|
|
'cp a b',
|
|
'chmod 777 x',
|
|
'chown user x',
|
|
'cat a > out.txt',
|
|
'echo x >> out.txt',
|
|
'node -e "console.log(1)"',
|
|
'node --eval "x"',
|
|
'python -c "import os"',
|
|
'bash -c "ls"',
|
|
'eval "$x"',
|
|
'composer install',
|
|
'npm install lodash',
|
|
'yarn add x',
|
|
'pnpm add x',
|
|
'curl -X POST https://evil.test',
|
|
])('blocks %s', (cmd) => {
|
|
expect(matchBashHardBlacklist(cmd)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('matchBashHardBlacklist — v4.0 additions', () => {
|
|
it.each([
|
|
['cat a 2> ~/.claude/runtime/x', 'C16 stderr→protected'],
|
|
['cmd &> out.log', 'C16 &>'],
|
|
['cmd |& tee x', 'C16 |&'],
|
|
['node script.js -e "fs.unlinkSync(\'x\')"', '#4 node fs inline'],
|
|
['env -i node x.js', '#21 env modifier'],
|
|
['FOO=bar node x.js', '#21 env assign prefix'],
|
|
['npx vitest --watch', '#22 watch'],
|
|
['nodemon --watch src', '#22 watch nodemon'],
|
|
])('blocks %s (%s)', (cmd) => {
|
|
expect(matchBashHardBlacklist(cmd)).toBeTruthy();
|
|
});
|
|
});
|
|
|
|
describe('matchBashHardBlacklist — v4.1 G7/G8', () => {
|
|
it.each(['wget https://x', 'wget -q file', 'nc -l 4444', 'ncat x 80', 'netcat x', 'socat - TCP:x:80'])(
|
|
'blocks %s',
|
|
(cmd) => {
|
|
expect(matchBashHardBlacklist(cmd)).toBeTruthy();
|
|
},
|
|
);
|
|
});
|
|
|
|
describe('matchBashHardBlacklist — allows benign', () => {
|
|
it.each(['ls -la', 'git status', 'cat app/x.php', 'npx vitest run', 'node tools/x.mjs arg'])(
|
|
'allows %s',
|
|
(cmd) => {
|
|
expect(matchBashHardBlacklist(cmd)).toBe(null);
|
|
},
|
|
);
|
|
});
|
|
|
|
import { classifyWhitelist, scriptWatcherCheck } from './enforce-router-gate.mjs';
|
|
|
|
describe('classifyWhitelist', () => {
|
|
it('marks reading commands', () => {
|
|
expect(classifyWhitelist([{ tokens: ['cat', 'app/x.php'], op: null }])).toMatchObject({ kind: 'reading' });
|
|
});
|
|
it('marks safe commands', () => {
|
|
expect(classifyWhitelist([{ tokens: ['npx', 'vitest', 'run'], op: null }])).toMatchObject({ kind: 'safe' });
|
|
});
|
|
it('returns null for non-whitelisted', () => {
|
|
expect(classifyWhitelist([{ tokens: ['foobar'], op: null }])).toBe(null);
|
|
});
|
|
it('allows pipe of readers', () => {
|
|
const segs = [{ tokens: ['cat', 'a'], op: '|' }, { tokens: ['grep', 'x'], op: null }];
|
|
expect(classifyWhitelist(segs)).not.toBe(null);
|
|
});
|
|
});
|
|
|
|
describe('scriptWatcherCheck', () => {
|
|
it('blocks node execution of an edited file', () => {
|
|
const segs = [{ tokens: ['node', 'tools/evil.mjs'], op: null }];
|
|
const r = scriptWatcherCheck(segs, ['tools/evil.mjs'], (p) => p);
|
|
expect(r.block).toBe(true);
|
|
});
|
|
it('allows node execution of a non-edited file', () => {
|
|
const segs = [{ tokens: ['node', 'tools/ok.mjs'], op: null }];
|
|
expect(scriptWatcherCheck(segs, ['tools/other.mjs'], (p) => p).block).toBe(false);
|
|
});
|
|
});
|
|
|
|
import { classifyBashCommand } from './enforce-router-gate.mjs';
|
|
|
|
describe('classifyBashCommand — integration', () => {
|
|
const now = 3_000_000;
|
|
|
|
it('allows whitelisted read', () => {
|
|
expect(classifyBashCommand('cat app/x.php', {}).result).toBe('allow');
|
|
});
|
|
it('blocks invalid syntax (fail-CLOSE)', () => {
|
|
expect(classifyBashCommand('echo "unterminated', {}).result).toBe('block');
|
|
});
|
|
it('blocks sub-shell', () => {
|
|
expect(classifyBashCommand('echo $(rm -rf x)', {}).result).toBe('block');
|
|
});
|
|
it('blocks hard-blacklisted rm', () => {
|
|
expect(classifyBashCommand('rm -rf build', {}).result).toBe('block');
|
|
});
|
|
it('blocks chain where any part mutating', () => {
|
|
expect(classifyBashCommand('ls && rm x', {}).result).toBe('block');
|
|
expect(classifyBashCommand('ls && git commit -m x', {}).result).toBe('block');
|
|
});
|
|
it('allows pipe of readers', () => {
|
|
expect(classifyBashCommand('cat a | grep x', {}).result).toBe('allow');
|
|
});
|
|
it('blocks reading a protected path', () => {
|
|
expect(classifyBashCommand('cat ~/.claude/runtime/state.json', {}).result).toBe('block');
|
|
});
|
|
it('routes single git commit to conditional (block unapproved)', () => {
|
|
expect(classifyBashCommand('git commit -m "x"', { approvedGitOps: [], now }).result).toBe('block');
|
|
});
|
|
it('allows approved git commit', () => {
|
|
expect(
|
|
classifyBashCommand('git commit -m "x"', { approvedGitOps: [{ command: 'git commit -m "x"', ts: now }], now }).result,
|
|
).toBe('allow');
|
|
});
|
|
it('default-denies unknown command', () => {
|
|
expect(classifyBashCommand('frobnicate --all', {}).result).toBe('block');
|
|
});
|
|
});
|
|
|
|
import { resolvePathNormalize } from './enforce-router-gate.mjs';
|
|
|
|
describe('resolvePathNormalize', () => {
|
|
it('falls back to defaultPathNormalize when Stream A module absent', async () => {
|
|
const fn = await resolvePathNormalize();
|
|
expect(typeof fn).toBe('function');
|
|
expect(fn('"a\\b"')).toBe('a/b'); // default behaviour
|
|
});
|
|
});
|
|
|
|
describe('stderr redirect — 2>&1 fd-duplication (review fix)', () => {
|
|
it('allows cat a 2>&1 (merge to stdout, no file)', () => {
|
|
expect(classifyBashCommand('cat a 2>&1', {}).result).toBe('allow');
|
|
});
|
|
it('allows cat a 2>/dev/null', () => {
|
|
expect(classifyBashCommand('cat a 2>/dev/null', {}).result).toBe('allow');
|
|
});
|
|
it('still blocks stderr redirect to a file', () => {
|
|
expect(classifyBashCommand('cat a 2> err.log', {}).result).toBe('block');
|
|
expect(classifyBashCommand('cat a 2>> err.log', {}).result).toBe('block');
|
|
});
|
|
it('still blocks &> file', () => {
|
|
expect(classifyBashCommand('cat a &> out.log', {}).result).toBe('block');
|
|
});
|
|
it('allows 1>&2 fd-duplication', () => {
|
|
expect(classifyBashCommand('cat a 1>&2', {}).result).toBe('allow');
|
|
});
|
|
it('blocks 2>&1 followed by file redirect', () => {
|
|
expect(classifyBashCommand('cat a 2>&1 > out.txt', {}).result).toBe('block');
|
|
});
|
|
});
|