Files
ECC/tests/lib/powershell-destructive-command.test.js
T

725 lines
30 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
'use strict';
const assert = require('assert');
const {
classifyPowerShellDestructiveCommand,
} = require('../../scripts/lib/powershell-destructive-command');
const RULES = Object.freeze({
REMOVE_RECURSE: 'powershell.remove-item.recurse',
REMOVE_FORCE: 'powershell.remove-item.force',
REMOVE_WILDCARD: 'powershell.remove-item.wildcard',
REMOVE_SPLAT: 'powershell.remove-item.splat',
PIPELINE_RECURSE: 'powershell.remove-item.pipeline-recurse',
CLEAR_CONTENT: 'powershell.clear-content',
CLEAR_DISK: 'powershell.clear-disk',
FORMAT_VOLUME: 'powershell.format-volume',
DOTNET_DIRECTORY_DELETE: 'powershell.dotnet.directory-delete',
DOTNET_FILE_DELETE: 'powershell.dotnet.file-delete',
CMD_RECURSIVE_DELETE: 'powershell.cmd.recursive-delete',
DYNAMIC_EXECUTION: 'powershell.dynamic-execution',
SCAN_DEPTH_EXCEEDED: 'powershell.scan-depth-exceeded',
});
console.log('=== Testing powershell-destructive-command.js ===\n');
let passed = 0;
let failed = 0;
function test(name, fn) {
try {
fn();
console.log(` PASS ${name}`);
passed += 1;
} catch (error) {
console.log(` FAIL ${name}`);
console.log(` ${error.message}`);
failed += 1;
}
}
function classify(command) {
const findings = classifyPowerShellDestructiveCommand(command);
assert.ok(Array.isArray(findings), 'classifier must return an array');
assert.ok(
findings.every(ruleId => typeof ruleId === 'string' && ruleId.length > 0),
'every finding must be a non-empty rule-id string'
);
assert.strictEqual(
new Set(findings).size,
findings.length,
`findings must be unique: ${JSON.stringify(findings)}`
);
return findings;
}
function expectRules(command, expected) {
const actual = classify(command);
assert.deepStrictEqual(
[...actual].sort(),
[...expected].sort(),
`unexpected findings for ${JSON.stringify(command)}`
);
}
function expectSafe(command) {
expectRules(command, []);
}
console.log('Remove-Item forms:');
test('classifies recursive and force parameters independently', () => {
expectRules('Remove-Item -Recurse -Force C:/tmp/demo', [
RULES.REMOVE_RECURSE,
RULES.REMOVE_FORCE,
]);
expectRules('Remove-Item -Recurse C:/tmp/demo', [RULES.REMOVE_RECURSE]);
expectRules('Remove-Item -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
});
test('classifies PowerShell parameter abbreviations case-insensitively', () => {
expectRules('REMOVE-ITEM -Rec -Fo C:/tmp/demo', [
RULES.REMOVE_RECURSE,
RULES.REMOVE_FORCE,
]);
});
test('normalizes every PowerShell command-parameter dash character', () => {
expectRules('Remove-Item Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectRules('Remove-Item —Recurse C:/tmp/demo', [RULES.REMOVE_RECURSE]);
expectRules('Remove-Item ―Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
});
test('normalizes PowerShell backtick obfuscation after finding executable ranges', () => {
expectRules('Rem`ove-Item -Rec`urse C:/tmp/demo', [RULES.REMOVE_RECURSE]);
});
test('classifies recursive Remove-Item aliases', () => {
for (const alias of ['ri', 'rm', 'rmdir', 'rd', 'del', 'erase']) {
expectRules(`${alias} -Recurse C:/tmp/demo`, [RULES.REMOVE_RECURSE]);
}
expectRules('rp -Force HKCU:/Software/Demo -Name setting', [RULES.REMOVE_FORCE]);
expectRules('Remove-ItemProperty -Force HKCU:/Software/Demo -Name setting', [
RULES.REMOVE_FORCE,
]);
});
test('classifies wildcard targets, including quoted provider paths', () => {
expectRules('Remove-Item C:/build/*', [RULES.REMOVE_WILDCARD]);
expectRules('Remove-Item "C:/build/file?.tmp"', [RULES.REMOVE_WILDCARD]);
});
test('classifies splatted Remove-Item parameters', () => {
expectRules('Remove-Item @deleteParams', [RULES.REMOVE_SPLAT]);
});
test('returns deterministic, unique rule IDs when a rule matches repeatedly', () => {
const command = 'Remove-Item -Force C:/one; Remove-Item -Force C:/two';
const first = classify(command);
const second = classify(command);
assert.deepStrictEqual(first, second);
assert.deepStrictEqual(first, [RULES.REMOVE_FORCE]);
});
console.log('\nAdditional destructive APIs:');
test('classifies Clear-Content, Clear-Disk, and Format-Volume', () => {
expectRules('Clear-Content C:/tmp/log.txt', [RULES.CLEAR_CONTENT]);
expectRules('Clear-Disk -Number 2 -RemoveData -Confirm:$false', [RULES.CLEAR_DISK]);
expectRules('Format-Volume -DriveLetter D -Force', [RULES.FORMAT_VOLUME]);
});
test('classifies .NET directory and file deletion', () => {
expectRules("[System.IO.Directory]::Delete('C:/tmp/demo', $true)", [
RULES.DOTNET_DIRECTORY_DELETE,
]);
expectRules("[IO.File]::Delete('C:/tmp/demo.txt')", [
RULES.DOTNET_FILE_DELETE,
]);
expectRules("[IO.Fi`le]::Delete('C:/tmp/demo.txt')", [
RULES.DOTNET_FILE_DELETE,
]);
});
test('classifies recursive cmd.exe deletion reached through PowerShell', () => {
expectRules('cmd /c rd /s /q C:/tmp/demo', [RULES.CMD_RECURSIVE_DELETE]);
expectRules('cmd.exe /c del /s /q C:/tmp/demo/*', [
RULES.CMD_RECURSIVE_DELETE,
]);
expectRules('cmd /c "rd /s /q C:/tmp/demo"', [RULES.CMD_RECURSIVE_DELETE]);
expectRules('cmd /c @rd /s /q C:/tmp/demo', [RULES.CMD_RECURSIVE_DELETE]);
expectRules('cmd /c --% rd /s /q C:/tmp/demo', [RULES.CMD_RECURSIVE_DELETE]);
expectRules('cmd /c if exist C:/tmp/demo rd /s /q C:/tmp/demo', [
RULES.CMD_RECURSIVE_DELETE,
]);
expectRules('cmd /c "(rd /s /q C:/tmp/demo)"', [RULES.CMD_RECURSIVE_DELETE]);
expectRules('cmd /c (rd /s /q C:/tmp/demo)', [RULES.CMD_RECURSIVE_DELETE]);
expectRules('cmd /c if /i "x"=="x" rd /s /q C:/tmp/demo', [
RULES.CMD_RECURSIVE_DELETE,
]);
expectRules('cmd /c for %i in (1) do rd /s /q C:/tmp/demo', [
RULES.CMD_RECURSIVE_DELETE,
]);
expectRules('cmd /c call rd /s /q C:/tmp/demo', [RULES.CMD_RECURSIVE_DELETE]);
expectRules('cmd /c start /wait rd /s /q C:/tmp/demo', [
RULES.CMD_RECURSIVE_DELETE,
]);
expectRules('cmd /c if exist C:/never echo safe else rd /s /q C:/tmp/demo', [
RULES.CMD_RECURSIVE_DELETE,
]);
for (const command of [
'cmd /c if exist C:/never echo safe else if exist C:/never echo safe else rd /s /q C:/tmp/demo',
'cmd /c for %i in (1) do if exist C:/never echo safe else rd /s /q C:/tmp/demo',
'cmd /c call call rd /s /q C:/tmp/demo',
'cmd /c start "job" /wait cmd /c rd /s /q C:/tmp/demo',
'cmd /c >nul rd /s /q C:/tmp/demo',
'cmd /c if /i "x" EQU "x" rd /s /q C:/tmp/demo',
'cmd /c if 1 NEQ 2 rd /s /q C:/tmp/demo',
'cmd /c if /i "x" EQU "x" if 1 NEQ 2 rd /s /q C:/tmp/demo',
]) {
expectRules(command, [RULES.CMD_RECURSIVE_DELETE]);
}
});
test('classifies pipeline recursion evidence upstream of Remove-Item', () => {
expectRules('Get-ChildItem C:/tmp -Recurse | Remove-Item', [
RULES.PIPELINE_RECURSE,
]);
});
console.log('\nNested shell payloads:');
test('classifies powershell and pwsh command payloads recursively', () => {
expectRules(
'powershell -Command "Remove-Item -Recurse C:/tmp/demo"',
[RULES.REMOVE_RECURSE]
);
expectRules(
"pwsh -c 'Remove-Item -Force C:/tmp/demo'",
[RULES.REMOVE_FORCE]
);
expectRules(
'cmd /c pwsh -Command "Remove-Item -Force C:/tmp/demo"',
[RULES.REMOVE_FORCE]
);
expectRules(
"'Remove-Item -Force C:/tmp/demo' | pwsh -Command -",
[RULES.REMOVE_FORCE]
);
expectRules(
"Write-Output 'Remove-Item -Force C:/tmp/demo' | pwsh -Command -",
[RULES.REMOVE_FORCE]
);
expectRules("@('Remove-Item -Force C:/tmp/demo') | pwsh -Command -", [
RULES.REMOVE_FORCE,
]);
expectRules("@'\nRemove-Item -Force C:/tmp/demo\n'@ | pwsh -Command -", [
RULES.REMOVE_FORCE,
]);
expectRules("@'\nRemove-Item -Force C:/tmp/demo\n'@ | pwsh -NoProfile -Command -", [
RULES.REMOVE_FORCE,
]);
expectRules(
"Write-Output \"[IO.File]::Delete('C:/tmp/demo')\" | pwsh -Command -",
[RULES.DOTNET_FILE_DELETE]
);
expectRules('pwsh -CommandWithArgs "Remove-Item -Force C:/tmp/demo"', [
RULES.REMOVE_FORCE,
]);
expectRules('pwsh -cwa "Remove-Item -Force C:/tmp/demo"', [RULES.REMOVE_FORCE]);
expectRules(
"Start-Process pwsh -ArgumentList '-NoProfile -Command \"Remove-Item -Force C:/tmp/demo\"'",
[RULES.REMOVE_FORCE]
);
for (const command of [
"Start-Process pwsh -ArgumentList '-NoProfile','-Command','Remove-Item -Force C:/tmp/demo'",
"Start-Process -FilePath pwsh -ArgumentList '-NoProfile', '-Command', 'Remove-Item -Force C:/tmp/demo'",
"saps pwsh -ArgumentList '-NoProfile','-c','Remove-Item -Force C:/tmp/demo'",
"Start-Process pwsh -ArgumentList @('-NoProfile','-Command','Remove-Item -Force C:/tmp/demo')",
"Start-Process pwsh '-Command \"Remove-Item -Force C:/tmp/demo\"'",
"Start-Process pwsh -Args '-Command \"Remove-Item -Force C:/tmp/demo\"'",
"Start-Process -FilePath:pwsh -ArgumentList '-Command \"Remove-Item -Force C:/tmp/demo\"'",
"Start-Process pwsh -ArgumentList:'-Command \"Remove-Item -Force C:/tmp/demo\"'",
"Start-Process -Fi:pwsh -Arg:'-Command \"Remove-Item -Force C:/tmp/demo\"'",
"Start-Process -ArgumentList '-Command \"Remove-Item -Force C:/tmp/demo\"' -FilePath pwsh",
"Start-Process -WindowStyle Hidden pwsh -ArgumentList '-Command \"Remove-Item -Force C:/tmp/demo\"'",
"Start-Process -WorkingDirectory C:/tmp pwsh -ArgumentList '-Command \"Remove-Item -Force C:/tmp/demo\"'",
"Start-Process pwsh '-NoProfile','-Command','Remove-Item -Force C:/tmp/demo'",
"Start-Process pwsh -ArgumentList @('-NoProfile',('-Command'),('Remove-Item -Force C:/tmp/demo'))",
]) {
expectRules(command, [RULES.REMOVE_FORCE]);
}
expectRules("Start-Process cmd -ArgumentList '/c rd /s /q C:/tmp/demo'", [
RULES.CMD_RECURSIVE_DELETE,
]);
expectRules(
"$params=@{FilePath='pwsh';ArgumentList='-Command \"Remove-Item -Force C:/tmp/demo\"'}; Start-Process @params",
[RULES.DYNAMIC_EXECUTION]
);
expectRules(
"$global:params=@{FilePath='pwsh';ArgumentList='-Command \"Remove-Item -Force C:/tmp/demo\"'}; Start-Process @global:params",
[RULES.DYNAMIC_EXECUTION]
);
expectRules(
"$shell='pwsh'; 'Remove-Item -Force C:/tmp/demo' | & $shell -Command -",
[RULES.REMOVE_FORCE]
);
expectRules("@'\nRemove-Item -Force C:/tmp/demo\n'@ | & pwsh -Command -", [
RULES.REMOVE_FORCE,
]);
});
test('classifies UTF-16LE EncodedCommand payloads', () => {
const payload = Buffer.from(
'Remove-Item C:/tmp/demo/*',
'utf16le'
).toString('base64');
expectRules(`pwsh -EncodedCommand ${payload}`, [RULES.REMOVE_WILDCARD]);
});
test('ignores an invalid EncodedCommand payload without throwing', () => {
assert.doesNotThrow(() => classify('pwsh -EncodedCommand %%%not-base64%%%'));
expectSafe('pwsh -EncodedCommand %%%not-base64%%%');
});
test('bounds deeply nested encoded commands and reports conservative evidence', () => {
let command = 'Remove-Item -Recurse C:/tmp/demo';
for (let depth = 0; depth < 8; depth += 1) {
const payload = Buffer.from(command, 'utf16le').toString('base64');
command = `pwsh -EncodedCommand ${payload}`;
}
expectRules(command, [RULES.SCAN_DEPTH_EXCEEDED]);
});
test('classifies destructive commands in executable PowerShell containers', () => {
const commands = [
'& { Remove-Item -Force C:/tmp/demo }',
'if ($true) { Remove-Item -Force C:/tmp/demo }',
'ForEach-Object { Remove-Item -Force C:/tmp/demo }',
'@(Remove-Item -Force C:/tmp/demo)',
'(Remove-Item -Force C:/tmp/demo)',
'pwsh -Command "& { Remove-Item -Force C:/tmp/demo }"',
'pwsh -Command { Remove-Item -Force C:/tmp/demo }',
'switch ($x) { default { Remove-Item -Force C:/tmp/demo } }',
"switch ($x) { 'match' { Remove-Item -Force C:/tmp/demo } }",
'& ({ Remove-Item -Force C:/tmp/demo })',
'& $( { Remove-Item -Force C:/tmp/demo } )',
'Invoke-Command -ScriptBlock ({ Remove-Item -Force C:/tmp/demo })',
'ForEach-Object -Process ({ Remove-Item -Force C:/tmp/demo })',
'function cleanup { Remove-Item -Force C:/tmp/demo }; cleanup',
];
for (const command of commands) expectRules(command, [RULES.REMOVE_FORCE]);
});
test('preserves executable context through spacing and nested grouping', () => {
const commands = [
`&${' '.repeat(300)}{ Remove-Item -Force C:/tmp/demo }`,
'& (({ Remove-Item -Force C:/tmp/demo }))',
'pwsh -Command (({ Remove-Item -Force C:/tmp/demo }))',
'{ Remove-Item -Force C:/tmp/demo }.Invoke()',
'{ Remove-Item -Force C:/tmp/demo }.InvokeReturnAsIs()',
'{ Remove-Item -Force C:/tmp/demo }.Inv`oke()',
"{ Remove-Item -Force C:/tmp/demo }.'Invoke'()",
'{ Remove-Item -Force C:/tmp/demo }.InvokeWithContext($null, $null, @())',
'{ Remove-Item -Force C:/tmp/demo } `\n.Invoke()',
'{ Remove-Item -Force C:/tmp/demo }.GetNewClosure().Invoke()',
'{ Remove-Item -Force C:/tmp/demo }.GetNewClosure().GetNewClosure().Invoke()',
"{ Remove-Item -Force C:/tmp/demo }.'GetNewClosure'().Invoke()",
];
for (const command of commands) expectRules(command, [RULES.REMOVE_FORCE]);
});
test('classifies invoked functions and filters across executable containers', () => {
const commands = [
'function cleanup { Remove-Item -Force C:/tmp/demo }; if ($true) { cleanup }',
'function cleanup { Remove-Item -Force C:/tmp/demo }; $(cleanup)',
'filter cleanup { Remove-Item -Force C:/tmp/demo }; 1 | cleanup',
'1 | foreach { Remove-Item -Force C:/tmp/demo }',
'1 | where { Remove-Item -Force C:/tmp/demo; $true }',
'1 | Microsoft.PowerShell.Core\\ForEach-Object { Remove-Item -Force C:/tmp/demo }',
];
for (const command of commands) expectRules(command, [RULES.REMOVE_FORCE]);
});
test('classifies invoked static script-block variables but leaves assignments inert', () => {
expectSafe('$cleanup = { Remove-Item -Force C:/tmp/demo }');
expectRules('$cleanup = { Remove-Item -Force C:/tmp/demo }; & $cleanup', [
RULES.REMOVE_FORCE,
]);
expectRules('$cleanup = { Remove-Item -Force C:/tmp/demo }; $cleanup.Invoke()', [
RULES.REMOVE_FORCE,
]);
expectRules('${cleanup} = { Remove-Item -Force C:/tmp/demo }; & ${cleanup}', [
RULES.REMOVE_FORCE,
]);
for (const command of [
'$cleanup = { Remove-Item -Force C:/tmp/demo }; Invoke-Command -ScriptBlock $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; 1 | ForEach-Object -Process $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; Start-Job -ScriptBlock $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; Measure-Command -Expression $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; Register-EngineEvent x -Action $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; Invoke-Command $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; 1 | ForEach-Object $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; Start-Job $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; Measure-Command $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; $cleanup.GetNewClosure().Invoke()',
'${cleanup} = { Remove-Item -Force C:/tmp/demo }; ${cleanup}.GetNewClosure().Invoke()',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; icm -ScriptBlock $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; sajb -ScriptBlock $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; Trace-Command demo -Expression $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; Invoke-Command -NoNewScope $cleanup',
'$cleanup = { Remove-Item -Force C:/tmp/demo }; 1 | ForEach-Object -Begin {} $cleanup',
]) {
expectRules(command, [RULES.REMOVE_FORCE]);
}
});
test('classifies static command results reached through the call operator', () => {
expectRules("& ('Remove-Item') -Force C:/tmp/demo", [RULES.REMOVE_FORCE]);
expectRules("& $('Remove-Item') -Force C:/tmp/demo", [RULES.REMOVE_FORCE]);
expectRules("& (('Remove-Item')) -Force C:/tmp/demo", [RULES.REMOVE_FORCE]);
expectRules("& $(( 'Remove-Item')) -Force C:/tmp/demo", [RULES.REMOVE_FORCE]);
});
test('classifies assignments, hashtables, and multiline executable blocks', () => {
const commands = [
'$x = Remove-Item -Force C:/tmp/demo',
'$h = @{ x = $(Remove-Item -Force C:/tmp/demo) }',
'if ($true)\n{ Remove-Item -Force C:/tmp/demo }',
'switch ($x)\n{ default { Remove-Item -Force C:/tmp/demo } }',
'function cleanup\n{ Remove-Item -Force C:/tmp/demo }; cleanup',
'if ($true) `\n{ Remove-Item -Force C:/tmp/demo }',
];
for (const command of commands) expectRules(command, [RULES.REMOVE_FORCE]);
expectRules('$null = Clear-Disk -Number 2 -RemoveData -Confirm:$false', [
RULES.CLEAR_DISK,
]);
});
test('classifies compact, scoped, indexed, property, and return execution', () => {
const commands = [
'$result=Remove-Item -Force C:/tmp/demo',
'[object]$result=Remove-Item -Force C:/tmp/demo',
'$script:x = Remove-Item -Force C:/tmp/demo',
'${x} = Remove-Item -Force C:/tmp/demo',
'$x[0] = Remove-Item -Force C:/tmp/demo',
'$x.Value = Remove-Item -Force C:/tmp/demo',
'$x,$y = Remove-Item -Force C:/tmp/demo',
'return Remove-Item -Force C:/tmp/demo',
'$script:cleanup = { Remove-Item -Force C:/tmp/demo }; & $script:cleanup',
];
for (const command of commands) expectRules(command, [RULES.REMOVE_FORCE]);
});
test('classifies named function blocks and sibling consumer blocks', () => {
const commands = [
'function cleanup { begin { Remove-Item -Force C:/tmp/demo } }; cleanup',
'function cleanup { process { Remove-Item -Force C:/tmp/demo } }; 1 | cleanup',
'workflow cleanup { Remove-Item -Force C:/tmp/demo }; cleanup',
'1 | ForEach-Object { Write-Output safe } { Remove-Item -Force C:/tmp/demo }',
'Trace-Command demo -Expression { Remove-Item -Force C:/tmp/demo }',
'Register-EngineEvent demo -Action { Remove-Item -Force C:/tmp/demo }',
'Register-EngineEvent demo -Action:{ Remove-Item -Force C:/tmp/demo }',
'class Cleanup { static [void] Run() { Remove-Item -Force C:/tmp/demo } }; [Cleanup]::Run()',
'class Cleanup { Cleanup() { Remove-Item -Force C:/tmp/demo } }; [Cleanup]::new()',
'class Cleanup { Cleanup() { Remove-Item -Force C:/tmp/demo } }; New-Object -TypeName Cleanup',
'class Cleanup { Cleanup() { Remove-Item -Force C:/tmp/demo } }; New-Object Cleanup',
'class Cleanup { Cleanup() { Remove-Item -Force C:/tmp/demo } }; New-Object ([Cleanup])',
"class Cleanup { Cleanup() { Remove-Item -Force C:/tmp/demo } }; New-Object ('Cleanup')",
'class Cleanup { Cleanup() { Remove-Item -Force C:/tmp/demo } }; [Activator]::CreateInstance([Cleanup])',
"$type = 'Cleanup'; class Cleanup { Cleanup() { Remove-Item -Force C:/tmp/demo } }; New-Object $type",
];
for (const command of commands) expectRules(command, [RULES.REMOVE_FORCE]);
});
test('classifies static execution primitives', () => {
expectRules("iex 'Remove-Item -Force C:/tmp/demo'", [RULES.REMOVE_FORCE]);
expectRules("Invoke-Expression 'Remove-Item -Force C:/tmp/demo'", [
RULES.REMOVE_FORCE,
]);
expectRules("& ([scriptblock]::Create('Remove-Item -Force C:/tmp/demo'))", [
RULES.REMOVE_FORCE,
]);
expectRules("Invoke-Expression @'\nRemove-Item -Force C:/tmp/demo\n'@", [
RULES.REMOVE_FORCE,
]);
expectRules("[scriptblock]::Create(@'\nRemove-Item -Force C:/tmp/demo\n'@).Invoke()", [
RULES.REMOVE_FORCE,
]);
expectRules("& (@'\nRemove-Item\n'@) -Force C:/tmp/demo", [RULES.REMOVE_FORCE]);
for (const command of [
"$cmd = 'Remove-Item -Force C:/tmp/demo'; Invoke-Expression $cmd",
"$cmd='Remove-Item -Force C:/tmp/demo'; iex $cmd",
"$cmd = 'Remove-Item -Force C:/tmp/demo'; & ([scriptblock]::Create($cmd))",
"$name = 'Remove-Item'; & $name -Force C:/tmp/demo",
"$args = '-Command \"Remove-Item -Force C:/tmp/demo\"'; Start-Process pwsh -ArgumentList $args",
"$payload = 'Remove-Item -Force C:/tmp/demo'; pwsh -Command $payload",
"$payload = 'Remove-Item -Force C:/tmp/demo'; pwsh -Command \"$payload\"",
"$payload = 'Remove-Item -Force C:/tmp/demo'; pwsh -Command \"Write-Output ready; $payload\"",
"$payload = 'Remove-Item -Force C:/tmp/demo'; pwsh -Command \"Write-Output ready; $($payload)\"",
"$payload = \"Remove-Item `\n-Force C:/tmp/demo\"; pwsh -Command $payload",
]) {
expectRules(command, [RULES.REMOVE_FORCE]);
}
expectRules('Invoke-Expression $runtimeValue', [RULES.DYNAMIC_EXECUTION]);
expectRules('pwsh -Command $runtimeValue', [RULES.DYNAMIC_EXECUTION]);
expectRules('pwsh -Command "Write-Output ready; $runtimeValue"', [
RULES.DYNAMIC_EXECUTION,
]);
expectRules('pwsh -Command "Write-Output ready; $($runtimeValue)"', [
RULES.DYNAMIC_EXECUTION,
]);
expectSafe("$payload = 'Remove-Item -Force C:/tmp/demo'; pwsh -Command '$payload'");
expectSafe("$payload = 'Remove-Item -Force C:/tmp/demo'; pwsh -Command \"Write-Output `$payload\"");
expectRules('Start-Process pwsh -ArgumentList $runtimeArgs', [RULES.DYNAMIC_EXECUTION]);
expectRules("$cmd='Remove-'; $cmd+='Item'; & $cmd -Force C:/tmp/demo", [
RULES.DYNAMIC_EXECUTION,
]);
expectRules(
'$verb=\'Remove\'; $cmd="${verb}-Item"; & $cmd -Force C:/tmp/demo',
[RULES.DYNAMIC_EXECUTION]
);
expectRules('& (Get-Command Remove-Item) -Force C:/tmp/demo', [
RULES.DYNAMIC_EXECUTION,
]);
expectRules("iex ('Remove-'+'Item -Force C:/tmp/demo')", [
RULES.DYNAMIC_EXECUTION,
]);
expectRules("iex ('{0}-Item -Force C:/tmp/demo' -f 'Remove')", [
RULES.DYNAMIC_EXECUTION,
]);
expectRules(
'$cleanup={ Remove-Item -Force C:/tmp/demo }; Invoke-Command -ScriptBlock (Get-Variable cleanup -ValueOnly)',
[RULES.DYNAMIC_EXECUTION]
);
expectRules('Set-Alias zap Remove-Item; zap -Force C:/tmp/demo', [
RULES.REMOVE_FORCE,
]);
expectRules('New-Alias -Name zap -Value Remove-Item; zap -Force C:/tmp/demo', [
RULES.REMOVE_FORCE,
]);
expectRules(
"$ExecutionContext.InvokeCommand.InvokeScript('Remove-Item -Force C:/tmp/demo')",
[RULES.REMOVE_FORCE]
);
expectRules(
"${ExecutionContext}.InvokeCommand.InvokeScript('Remove-Item -Force C:/tmp/demo')",
[RULES.REMOVE_FORCE]
);
expectRules(
"$ExecutionContext.InvokeCommand.InvokeScript(\"Write-Output safe; `\nRemove-Item -Force C:/tmp/demo\")",
[RULES.REMOVE_FORCE]
);
});
test('scans malformed InvokeScript string arguments in bounded time', () => {
const command = `$ExecutionContext.InvokeCommand.InvokeScript("${'`!'.repeat(10000)}`;
const assignment = '$payload = "' + '`!'.repeat(10000);
const startedAt = Date.now();
expectRules(command, [RULES.DYNAMIC_EXECUTION]);
expectSafe(assignment);
assert.ok(Date.now() - startedAt < 4000, 'malformed string scan should remain below hook timeout');
});
test('classifies command names composed from static subexpression output', () => {
expectRules('Remove-$(Write-Output Item) -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectRules('Clear-$(echo Disk) -Number 2', [RULES.CLEAR_DISK]);
expectRules('Format-$(echo Volume) -DriveLetter D', [RULES.FORMAT_VOLUME]);
expectRules('r$(echo m) -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
});
test('classifies executable containers inside EncodedCommand payloads', () => {
const payload = Buffer.from(
'& { Remove-Item -Force C:/tmp/demo }',
'utf16le'
).toString('base64');
expectRules(`pwsh -EncodedCommand ${payload}`, [RULES.REMOVE_FORCE]);
});
console.log('\nPowerShell subexpressions:');
test('classifies destructive commands in unquoted subexpressions', () => {
expectRules('Write-Output $(Remove-Item -Force C:/tmp/demo)', [
RULES.REMOVE_FORCE,
]);
});
test('classifies destructive commands in double-quoted subexpressions', () => {
expectRules('Write-Output "$(Remove-Item -Recurse C:/tmp/demo)"', [
RULES.REMOVE_RECURSE,
]);
});
test('classifies recursively nested subexpressions', () => {
expectRules(
'Write-Output "$(Write-Output $(Remove-Item -Force C:/tmp/demo))"',
[RULES.REMOVE_FORCE]
);
});
test('classifies sibling subexpressions without duplicating rule IDs', () => {
expectRules(
'Write-Output $(Remove-Item -Force C:/one) $(Remove-Item -Force C:/two)',
[RULES.REMOVE_FORCE]
);
});
test('keeps quoted delimiters inside subexpressions from splitting commands', () => {
expectRules(
'Write-Output $(Write-Output "safe;|&"; Remove-Item -Force "C:/tmp/a;b/*")',
[RULES.REMOVE_FORCE, RULES.REMOVE_WILDCARD]
);
});
test('keeps a quoted closing parenthesis inside a subexpression body', () => {
expectRules(
'Write-Output $(Write-Output ")"; Remove-Item -Force C:/tmp/demo)',
[RULES.REMOVE_FORCE]
);
});
test('keeps double-quoted apostrophes from suppressing executable subexpressions', () => {
expectRules(
'Write-Output "it\'s $(Remove-Item -Force C:/tmp/demo)"',
[RULES.REMOVE_FORCE]
);
});
test('treats subexpression text inside single quotes as literal', () => {
expectSafe("Write-Output '$(Remove-Item -Force C:/tmp/demo)'");
});
test('treats a backtick-escaped subexpression inside double quotes as literal', () => {
expectSafe('Write-Output "`$(Remove-Item -Force C:/tmp/demo)"');
});
test('respects literal and expandable PowerShell here-strings', () => {
expectSafe("@'\nliteral's Remove-Item -Force C:/tmp/demo\n'@");
expectSafe('Write-Output "@\'\nRemove-Item -Force C:/tmp/demo\n\'@"');
expectRules('@"\n$(Remove-Item -Force C:/tmp/demo)\n"@', [
RULES.REMOVE_FORCE,
]);
expectRules('@"\n" # $(Remove-Item -Force C:/tmp/demo)\n"@', [
RULES.REMOVE_FORCE,
]);
expectSafe('@"\n" # literal Remove-Item -Force C:/tmp/demo\n"@');
});
test('normalizes PowerShell smart quotes before lexical analysis', () => {
expectRules('& Remove-Item -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectRules('& Remove-Item -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectRules('& Remove-Item -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectRules('& „Remove-Item„ -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectSafe('Write-Output $(Remove-Item -Force C:/tmp/demo)');
expectSafe('Write-Output $(Remove-Item -Force C:/tmp/demo)');
});
test('does not treat a backslash as an escape for an executable subexpression', () => {
expectRules('Write-Output \\$(Remove-Item -Force C:/tmp/demo)', [
RULES.REMOVE_FORCE,
]);
});
test('handles backtick line continuations before destructive parameters', () => {
expectRules('Remove-Item `\n-Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectRules('Remove-Item `\r\n-Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
});
test('ignores comment syntax without letting it poison following parser state', () => {
expectRules('# (\nRemove-Item -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectRules('<# ( #>\nRemove-Item -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectRules('<# ignored <# #> Remove-Item -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectSafe('Write-Output safe # ; Remove-Item -Force C:/tmp/demo');
expectSafe('Write-Output safe# | Remove-Item -Force C:/tmp/demo');
expectSafe("# [IO.File]::Delete('C:/tmp/demo')");
expectRules('# @"\nRemove-Item -Force C:/tmp/demo\n"@', [RULES.REMOVE_FORCE]);
expectRules('${a#b}=1; Remove-Item -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
expectRules('${a<#b}=1; Remove-Item -Force C:/tmp/demo', [RULES.REMOVE_FORCE]);
});
test('scans large comments and unmatched openers in bounded time', () => {
const started = Date.now();
expectRules(`<# ${'$('.repeat(40000)} #>\nRemove-Item -Force C:/tmp/demo`, [
RULES.REMOVE_FORCE,
]);
expectSafe('$('.repeat(40000));
expectSafe('()'.repeat(10000));
assert.ok(Date.now() - started < 2000, 'large malformed input should remain bounded');
});
test('resolves long invoked-function chains within the hook time budget', () => {
const definitions = [];
for (let index = 0; index < 20001; index += 1) {
const body = index === 20000
? 'Remove-Item -Force C:/tmp/demo'
: `f${index + 1}`;
definitions.push(`function f${index} { ${body} }`);
}
const started = Date.now();
expectRules(`${definitions.join('; ')}; f0`, [RULES.REMOVE_FORCE]);
assert.ok(Date.now() - started < 4000, 'function resolution should remain below hook timeout');
});
test('scans many sibling executable containers within the hook time budget', () => {
const command = Array.from(
{ length: 40000 },
(_, index) => index === 39999
? '$(Remove-Item -Force C:/tmp/demo)'
: '$(Write-Output safe)'
).join(' ');
const started = Date.now();
expectRules(command, [RULES.REMOVE_FORCE]);
assert.ok(Date.now() - started < 4000, 'sibling containers should remain bounded');
});
console.log('\nBenign controls:');
test('allows plain non-recursive, non-forced, non-wildcard Remove-Item', () => {
expectSafe('Remove-Item C:/tmp/notes.txt');
});
test('allows benign PowerShell and non-recursive cmd commands', () => {
expectSafe('Get-ChildItem C:/tmp');
expectSafe('Get-Date');
expectSafe('cmd /c del C:/tmp/notes.txt');
expectSafe('cmd /c echo rd /s /q C:/tmp/demo');
expectSafe('cmd /c "echo safe ^& rd /s /q C:/tmp/demo"');
expectSafe("function cleanup { Remove-Item -Force C:/tmp/demo }; 'cleanup'");
expectSafe('Write-Output safe`nRemove-Item -Force C:/tmp/demo');
});
test('allows explicitly false destructive switches and inert script blocks', () => {
expectSafe('Remove-Item -Force:$false C:/tmp/demo');
expectSafe('Remove-Item -Force:$null C:/tmp/demo');
expectSafe('Remove-Item -Recurse:$false C:/tmp/demo');
expectSafe("Remove-Item '-Force'");
expectSafe("Remove-Item -LiteralPath 'C:/tmp/file*.txt'");
expectSafe('{ Remove-Item -Force C:/tmp/demo }');
expectSafe('function cleanup { Remove-Item -Force C:/tmp/demo }');
});
test('treats backticks literally inside single-quoted strings', () => {
expectRules("Write-Output 'safe`'; Remove-Item -Force C:/tmp/demo", [
RULES.REMOVE_FORCE,
]);
});
test('handles empty and non-string commands', () => {
expectSafe('');
expectSafe(null);
expectSafe(undefined);
});
test('handles a trailing backtick without throwing or inventing a finding', () => {
assert.doesNotThrow(() => classify('Write-Output safe`'));
expectSafe('Write-Output safe`');
});
console.log(`\nResults: Passed: ${passed}, Failed: ${failed}`);
if (failed > 0) {
process.exit(1);
}