mirror of
https://github.com/affaan-m/ECC.git
synced 2026-09-29 04:55:12 +02:00
fix: address PowerShell review bypasses
This commit is contained in:
@@ -133,6 +133,13 @@ function summarizeCommand(command) {
|
||||
};
|
||||
}
|
||||
|
||||
if (trimmed.startsWith("'") || trimmed.startsWith('"')) {
|
||||
return {
|
||||
commandName: null,
|
||||
commandFingerprint: fingerprintCommand(trimmed),
|
||||
};
|
||||
}
|
||||
|
||||
const firstToken = trimmed.split(/\s+/)[0] || '';
|
||||
// Static method invocations can attach their arguments to the first token,
|
||||
// for example `[IO.File]::Delete('private-path')`. Keep the operation name
|
||||
|
||||
@@ -28,6 +28,7 @@ const RULE_IDS = Object.freeze({
|
||||
const DELETE_COMMANDS = new Set([
|
||||
'remove-item',
|
||||
'remove-itemproperty',
|
||||
'rp',
|
||||
'ri',
|
||||
'rm',
|
||||
'rmdir',
|
||||
@@ -507,6 +508,33 @@ function staticStringResult(body) {
|
||||
return quote === "'" ? content.replace(/''/g, "'") : content.replace(/`(.)/gs, '$1');
|
||||
}
|
||||
|
||||
function leadingStaticStringResult(source) {
|
||||
const input = String(source || '');
|
||||
let index = 0;
|
||||
while (/\s/.test(input[index] || '')) index += 1;
|
||||
const quote = input[index];
|
||||
if (quote !== "'" && quote !== '"') return null;
|
||||
index += 1;
|
||||
let value = '';
|
||||
while (index < input.length) {
|
||||
const char = input[index];
|
||||
if (quote === "'" && char === "'" && input[index + 1] === "'") {
|
||||
value += "'";
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
if (quote === '"' && char === '`' && index + 1 < input.length) {
|
||||
value += input[index + 1];
|
||||
index += 2;
|
||||
continue;
|
||||
}
|
||||
if (char === quote) return value;
|
||||
value += char;
|
||||
index += 1;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function staticScalarResult(body, depth = 0) {
|
||||
if (depth > MAX_SCAN_DEPTH) return null;
|
||||
const value = String(body || '').trim();
|
||||
@@ -1083,8 +1111,19 @@ function scanNestedPowerShell(tokens, depth, findings, analysis, scanState, upst
|
||||
}
|
||||
|
||||
if (isCommandFlag(token)) {
|
||||
const payload = tokens.slice(index + 1).join(' ');
|
||||
let payload = tokens.slice(index + 1).join(' ');
|
||||
const pipelinePayload = payload === '-' ? staticPipelineInput(upstreamTokens) : null;
|
||||
const payloadReference = tokens.quotedTokens?.[index + 1] === true
|
||||
? null
|
||||
: variableReference(payload);
|
||||
if (payloadReference) {
|
||||
const staticValue = scanState?.staticScalars.get(payloadReference);
|
||||
if (staticValue === undefined) {
|
||||
findings.add(RULE_IDS.DYNAMIC_EXECUTION);
|
||||
return;
|
||||
}
|
||||
payload = staticValue;
|
||||
}
|
||||
if (pipelinePayload || (payload && payload !== '-')) {
|
||||
addNestedScan(
|
||||
pipelinePayload || payload,
|
||||
@@ -1558,8 +1597,7 @@ function scanInvokeScriptCalls(source, unquoted, depth, findings, analysis, stat
|
||||
const pattern = /\$executioncontext\.invokecommand\.invokescript\s*\(/gi;
|
||||
while (pattern.exec(unquoted) !== null) {
|
||||
const argumentSource = source.slice(pattern.lastIndex);
|
||||
const literal = argumentSource.match(/^\s*(?:'(?:''|[^'])*'|"(?:`[\s\S]|[^"])*")/);
|
||||
const payload = literal ? staticStringResult(literal[0].trim()) : null;
|
||||
const payload = leadingStaticStringResult(argumentSource);
|
||||
if (payload === null) {
|
||||
findings.add(RULE_IDS.DYNAMIC_EXECUTION);
|
||||
} else {
|
||||
|
||||
@@ -2964,8 +2964,10 @@ function runTests() {
|
||||
test('denies direct and nested destructive PowerShell commands', () => {
|
||||
const commands = [
|
||||
'Remove-Item -Recurse C:/tmp/demo',
|
||||
'rp -Force HKCU:/Software/Demo -Name setting',
|
||||
'Clear-Disk -Number 2 -RemoveData -Confirm:$false',
|
||||
'pwsh -Command "Remove-Item -Force C:/tmp/demo"',
|
||||
"$payload='Remove-Item -Force C:/tmp/demo'; pwsh -Command $payload",
|
||||
'Write-Output "$(Remove-Item -Force C:/tmp/demo)"',
|
||||
'& { Remove-Item -Force C:/tmp/demo }',
|
||||
'if ($true) { Remove-Item -Force C:/tmp/demo }',
|
||||
|
||||
@@ -325,7 +325,7 @@ async function runTests() {
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (await test('PowerShell governance normalizes tool casing and redacts assignment prefixes', async () => {
|
||||
const command = "$password='governance-secret-sentinel'; Remove-Item -Force C:/tmp/demo";
|
||||
const command = "$label='governance-private-marker'; Remove-Item -Force C:/tmp/demo";
|
||||
for (const toolName of ['PowerShell', 'powershell', 'POWERSHELL']) {
|
||||
const events = analyzeForGovernanceEvents({
|
||||
tool_name: toolName,
|
||||
@@ -337,10 +337,24 @@ async function runTests() {
|
||||
assert.ok(approvalEvent, `${toolName} should raise approval_requested`);
|
||||
assert.strictEqual(approvalEvent.payload.toolName, 'PowerShell');
|
||||
assert.strictEqual(approvalEvent.payload.commandName, null);
|
||||
assert.ok(!JSON.stringify(events).includes('governance-secret-sentinel'));
|
||||
assert.ok(!JSON.stringify(events).includes('governance-private-marker'));
|
||||
}
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (await test('PowerShell governance redacts quoted expression prefixes', async () => {
|
||||
const command = "'quoted-private-marker' ; Remove-Item -Force C:/tmp/demo";
|
||||
const events = analyzeForGovernanceEvents({
|
||||
tool_name: 'PowerShell',
|
||||
tool_input: { command },
|
||||
}, {
|
||||
hookPhase: 'pre',
|
||||
});
|
||||
const approvalEvent = events.find(event => event.eventType === 'approval_requested');
|
||||
assert.ok(approvalEvent, 'quoted prefix should still raise approval_requested');
|
||||
assert.strictEqual(approvalEvent.payload.commandName, null);
|
||||
assert.ok(!JSON.stringify(events).includes('quoted-private-marker'));
|
||||
})) passed += 1; else failed += 1;
|
||||
|
||||
if (await test('PowerShell elevation events are captured without raw command leakage', async () => {
|
||||
const commands = [
|
||||
'Start-Process -Verb RunAs cmd -ArgumentList elevation-command-sentinel',
|
||||
|
||||
@@ -98,6 +98,7 @@ 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,
|
||||
]);
|
||||
@@ -455,10 +456,13 @@ test('classifies static execution primitives', () => {
|
||||
"$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",
|
||||
]) {
|
||||
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,
|
||||
@@ -492,6 +496,13 @@ test('classifies static execution primitives', () => {
|
||||
);
|
||||
});
|
||||
|
||||
test('scans malformed InvokeScript string arguments in bounded time', () => {
|
||||
const command = `$ExecutionContext.InvokeCommand.InvokeScript("${'`!'.repeat(10000)}`;
|
||||
const startedAt = Date.now();
|
||||
expectRules(command, [RULES.DYNAMIC_EXECUTION]);
|
||||
assert.ok(Date.now() - startedAt < 1000, 'malformed string scan should remain bounded');
|
||||
});
|
||||
|
||||
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]);
|
||||
|
||||
Reference in New Issue
Block a user