import { describe, expect, test } from 'bun:test'; import { mkdtempSync, rmSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { analyzeRm } from '../src/core/rules-rm.ts'; import { assertAllowed, assertBlocked, withEnv } from './helpers.ts'; describe('rm -rf blocked', () => { test('rm -rf blocked', () => { assertBlocked('rm -rf /some/path', 'rm -rf'); }); test('rm -Rf blocked', () => { assertBlocked('rm -Rf /some/path', 'rm -rf'); }); test('rm -R -f blocked', () => { assertBlocked('rm -R -f /some/path', 'rm -rf'); }); test('rm -rf ~/projects blocked', () => { assertBlocked('rm -rf ~/projects', 'rm -rf'); }); test('rm -fr blocked', () => { assertBlocked('rm -fr /some/path', 'rm -rf'); }); test('true & rm -rf blocked', () => { assertBlocked('true & rm -rf /some/path', 'rm -rf'); }); test('rm -rf /tmp/../Users/some/path blocked', () => { assertBlocked('rm -rf /tmp/../Users/some/path', 'rm -rf'); }); test('/bin/rm -rf blocked', () => { assertBlocked('/bin/rm -rf /some/path', 'rm -rf'); }); test('busybox rm -rf blocked', () => { assertBlocked('busybox rm -rf /some/path', 'rm -rf'); }); test('busybox rm -R -f blocked', () => { assertBlocked('busybox rm -R -f /some/path', 'rm -rf'); }); test("bash -c 'rm -rf /some/path' blocked", () => { assertBlocked("bash -c 'rm -rf /some/path'", 'rm -rf'); }); test('python -c rm -rf blocked', () => { assertBlocked('python -c \'import os; os.system("rm -rf /some/path")\'', 'dangerous'); }); test('echo $(rm -rf /some/path) blocked', () => { assertBlocked('echo $(rm -rf /some/path)', 'rm -rf'); }); test('TMPDIR=/Users rm -rf $TMPDIR/test-dir blocked', () => { assertBlocked('TMPDIR=/Users rm -rf $TMPDIR/test-dir', 'rm -rf'); }); test('rm -rf / blocked (root)', () => { assertBlocked('rm -rf /', 'extremely dangerous'); }); test('rm -rf ~ blocked (home)', () => { assertBlocked('rm -rf ~', 'extremely dangerous'); }); test('rm -rf -- / blocked', () => { assertBlocked('rm -rf -- /', 'extremely dangerous'); }); test('rm -rf $TMPDIR/../escape blocked', () => { assertBlocked('rm -rf $TMPDIR/../escape', 'rm -rf'); }); test('rm -rf `pwd`/escape blocked', () => { assertBlocked('rm -rf `pwd`/escape', 'rm -rf'); }); test('rm -rf ~someone/escape blocked', () => { assertBlocked('rm -rf ~someone/escape', 'rm -rf'); }); }); describe('rm -rf allowed', () => { test('rm -rf /tmp/test-dir allowed', () => { assertAllowed('rm -rf /tmp/test-dir'); }); test('rm -rf /var/tmp/test-dir allowed', () => { assertAllowed('rm -rf /var/tmp/test-dir'); }); test('rm -rf $TMPDIR/test-dir allowed', () => { assertAllowed('rm -rf $TMPDIR/test-dir'); }); test('rm -rf ${TMPDIR}/test-dir allowed', () => { assertAllowed('rm -rf ${TMPDIR}/test-dir'); }); test('rm -rf "$TMPDIR/test-dir" allowed', () => { assertAllowed('rm -rf "$TMPDIR/test-dir"'); }); test('rm -rf $TMPDIR allowed', () => { assertAllowed('rm -rf $TMPDIR'); }); test('rm -rf /tmp allowed', () => { assertAllowed('rm -rf /tmp'); }); test('rm -r without force allowed', () => { assertAllowed('rm -r /some/path'); }); test('rm -R without force allowed', () => { assertAllowed('rm -R /some/path'); }); test('rm -f without recursive allowed', () => { assertAllowed('rm -f /some/path'); }); test('/bin/rm -rf /tmp/test-dir allowed', () => { assertAllowed('/bin/rm -rf /tmp/test-dir'); }); test('busybox rm -rf /tmp/test-dir allowed', () => { assertAllowed('busybox rm -rf /tmp/test-dir'); }); }); describe('rm -rf cwd-aware', () => { let tmpDir: string; const setup = () => { tmpDir = mkdtempSync(join(tmpdir(), 'safety-net-test-')); }; const cleanup = () => { if (tmpDir) { rmSync(tmpDir, { recursive: true, force: true }); } }; test('rm -rf relative path in home cwd blocked', () => { setup(); try { withEnv({ HOME: tmpDir }, () => { assertBlocked('rm -rf build', 'rm -rf', tmpDir); }); } finally { cleanup(); } }); test('rm -rf relative path in subdir of home allowed', () => { setup(); try { const repo = join(tmpDir, 'repo'); require('node:fs').mkdirSync(repo); withEnv({ HOME: tmpDir }, () => { assertAllowed('rm -rf build', repo); }); } finally { cleanup(); } }); test('rm -rf relative path allowed', () => { setup(); try { assertAllowed('rm -rf build', tmpDir); } finally { cleanup(); } }); test('rm -rf ./dist allowed', () => { setup(); try { assertAllowed('rm -rf ./dist', tmpDir); } finally { cleanup(); } }); test('rm -rf ../other blocked', () => { setup(); try { assertBlocked('rm -rf ../other', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('rm -rf /other/path blocked', () => { setup(); try { assertBlocked('rm -rf /other/path', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('rm -rf absolute inside cwd allowed', () => { setup(); try { const inside = join(tmpDir, 'dist'); assertAllowed(`rm -rf ${inside}`, tmpDir); } finally { cleanup(); } }); test('rm -rf . blocked', () => { setup(); try { assertBlocked('rm -rf .', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('rm -rf cwd itself blocked', () => { setup(); try { assertBlocked(`rm -rf ${tmpDir}`, 'rm -rf', tmpDir); } finally { cleanup(); } }); test('cd .. && rm -rf build blocked', () => { setup(); try { assertBlocked('cd .. && rm -rf build', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('paranoid rm blocks within cwd', () => { setup(); try { withEnv({ SAFETY_NET_PARANOID_RM: '1' }, () => { assertBlocked('rm -rf build', 'SAFETY_NET_PARANOID', tmpDir); }); } finally { cleanup(); } }); test('global paranoid blocks within cwd', () => { setup(); try { withEnv({ SAFETY_NET_PARANOID: '1' }, () => { assertBlocked('rm -rf build', 'SAFETY_NET_PARANOID', tmpDir); }); } finally { cleanup(); } }); test('rm -rf after builtin cd bypasses cwd allowlist blocked', () => { setup(); try { assertBlocked('builtin cd .. && rm -rf build', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('rm -rf after command substitution cd bypasses cwd allowlist blocked', () => { setup(); try { assertBlocked('$( cd ..; rm -rf build )', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('rm -rf after grouped cd bypasses cwd allowlist blocked', () => { setup(); try { assertBlocked('{ cd ..; rm -rf build; }', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('rm -rf after safe command substitution cd bypasses cwd allowlist blocked', () => { setup(); try { assertBlocked('$( cd ..; echo ok ) && rm -rf build', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('rm -rf after safe grouped cd bypasses cwd allowlist blocked', () => { setup(); try { assertBlocked('{ cd ..; echo ok; } && rm -rf build', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('rm -rf after subshell cd bypasses cwd allowlist blocked', () => { setup(); try { assertBlocked('( cd ..; rm -rf build )', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('rm -rf strict mode allows within cwd', () => { setup(); try { assertAllowed('rm -rf build', tmpDir); } finally { cleanup(); } }); test('command substitution rm rf blocked', () => { setup(); try { assertBlocked('echo $(rm -rf /some/path)', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('TMPDIR assignment not trusted blocked', () => { setup(); try { assertBlocked('TMPDIR=/Users rm -rf $TMPDIR/test-dir', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('TMPDIR= empty assignment blocked (expands to /)', () => { setup(); try { assertBlocked('TMPDIR= rm -rf $TMPDIR/test-dir', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('TMPDIR=/tmp-malicious blocked (not a real temp path)', () => { setup(); try { assertBlocked('TMPDIR=/tmp-malicious rm -rf $TMPDIR/test-dir', 'rm -rf', tmpDir); } finally { cleanup(); } }); test('TMPDIR=/tmp/subdir allowed (subpath of /tmp)', () => { setup(); try { assertAllowed('TMPDIR=/tmp/subdir rm -rf $TMPDIR/test-dir', tmpDir); } finally { cleanup(); } }); test('TMPDIR=/var/tmp-malicious blocked (not a real temp path)', () => { setup(); try { assertBlocked('TMPDIR=/var/tmp-malicious rm -rf $TMPDIR/test-dir', 'rm -rf', tmpDir); } finally { cleanup(); } }); }); describe('analyzeRm (unit)', () => { test('does not treat flags after -- as rm -rf', () => { expect(analyzeRm(['rm', '--', '-rf', '/'], { cwd: '/tmp' })).toBeNull(); }); test('blocks $HOME targets', () => { expect(analyzeRm(['rm', '-rf', '$HOME/*'], { cwd: '/tmp' })).toContain('extremely dangerous'); }); test('blocks ${HOME} targets', () => { expect(analyzeRm(['rm', '-rf', '${HOME}/*'], { cwd: '/tmp' })).toContain('extremely dangerous'); }); test('treats ${TMPDIR} paths as temp when allowed', () => { expect( analyzeRm(['rm', '-rf', '${TMPDIR}/test'], { cwd: '/tmp', allowTmpdirVar: true, }), ).toBeNull(); }); test('does not trust ${TMPDIR} when disallowed', () => { expect( analyzeRm(['rm', '-rf', '${TMPDIR}/test'], { cwd: '/tmp', allowTmpdirVar: false, }), ).toContain('rm -rf outside cwd'); }); test('handles non-string cwd defensively', () => { const badCwd = 1 as unknown as string; expect(analyzeRm(['rm', '-rf', 'foo'], { cwd: badCwd })).toContain('rm -rf outside cwd'); }); test('handles absolute-path checks defensively', () => { const badCwd = 1 as unknown as string; expect(analyzeRm(['rm', '-rf', '/abs'], { cwd: badCwd })).toContain('rm -rf outside cwd'); }); test('blocks tilde-prefixed paths (not cwd-relative)', () => { expect(analyzeRm(['rm', '-rf', '~/somewhere'], { cwd: '/tmp' })).toContain( 'rm -rf outside cwd', ); }); test('blocks ../ paths', () => { expect(analyzeRm(['rm', '-rf', '../escape'], { cwd: '/tmp' })).toContain('rm -rf outside cwd'); }); test('allows nested relative paths within cwd', () => { const cwd = mkdtempSync(join(tmpdir(), 'safety-net-rm-unit-')); try { expect( analyzeRm(['rm', '-rf', 'subdir/file'], { cwd, originalCwd: cwd, }), ).toBeNull(); } finally { rmSync(cwd, { recursive: true, force: true }); } }); });