mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-17 23:28:04 +02:00
716 lines
29 KiB
JavaScript
716 lines
29 KiB
JavaScript
'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 `\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]);
|
||
expectSafe("$payload = 'Remove-Item -Force C:/tmp/demo'; pwsh -Command '$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);
|
||
}
|