diff --git a/scripts/lib/powershell-destructive-command.js b/scripts/lib/powershell-destructive-command.js index 9f0672163..aea4e3f20 100644 --- a/scripts/lib/powershell-destructive-command.js +++ b/scripts/lib/powershell-destructive-command.js @@ -944,11 +944,11 @@ function parseStatements(input) { wordHasQuotedContent && !wordHasUnquotedContent ? wordQuoteKind : null ); segmentTokenSources.push(wordSource); - segmentInlineValueQuoteKinds.push( + segmentInlineValueQuoteKinds = [...segmentInlineValueQuoteKinds, wordInlineValueQuoteClosed && wordInlineValueQuoteKind !== 'mixed' ? wordInlineValueQuoteKind : null - ); + ]; } word = ''; wordSource = ''; @@ -1260,7 +1260,6 @@ function scanNestedPowerShell(tokens, depth, findings, analysis, scanState, upst : tokens.slice(index + 1).join(' '); const pipelinePayload = payload === '-' ? staticPipelineInput(upstreamTokens) : null; const payloadIndex = index + 1; - const hasOnePayloadToken = !inlinePayload && tokens.length === payloadIndex + 1; const inlineQuoteKind = tokens.inlineValueQuoteKinds?.[index]; if (inlinePayload && inlineQuoteKind !== "'") { const inlineSource = parameterValue(tokens.tokenSources?.[index] || token); @@ -1273,14 +1272,14 @@ function scanNestedPowerShell(tokens, depth, findings, analysis, scanState, upst payload = [expanded, ...tokens.slice(index + 1)].join(' '); } else if (inlinePayload) { payload = [inlinePayload, ...tokens.slice(index + 1)].join(' '); - } else if (hasOnePayloadToken && tokens.quoteKinds?.[payloadIndex] !== "'") { + } else if (tokens[payloadIndex] && tokens.quoteKinds?.[payloadIndex] !== "'") { const expanded = expandStaticDoubleQuotedString( - tokens.tokenSources?.[payloadIndex] ?? payload, + tokens.tokenSources?.[payloadIndex] ?? tokens[payloadIndex], scanState, findings ); if (expanded === null) return; - payload = expanded; + payload = [expanded, ...tokens.slice(payloadIndex + 1)].join(' '); } else { const payloadReference = tokens.quoteKinds?.[payloadIndex] === "'" ? null diff --git a/tests/hooks/gateguard-fact-force.test.js b/tests/hooks/gateguard-fact-force.test.js index 56af9349d..3735db1d9 100644 --- a/tests/hooks/gateguard-fact-force.test.js +++ b/tests/hooks/gateguard-fact-force.test.js @@ -2976,7 +2976,9 @@ function runTests() { "$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'; pwsh -Command $payload -Force C:/tmp/demo", 'pwsh -Command "Write-Output ready; $runtimePayload"', + 'pwsh -Command $runtimePayload -Force C:/tmp/demo', 'Write-Output "$(Remove-Item -Force C:/tmp/demo)"', '& { Remove-Item -Force C:/tmp/demo }', 'if ($true) { Remove-Item -Force C:/tmp/demo }', diff --git a/tests/hooks/governance-capture.test.js b/tests/hooks/governance-capture.test.js index bf48483a1..9ee30fed3 100644 --- a/tests/hooks/governance-capture.test.js +++ b/tests/hooks/governance-capture.test.js @@ -251,6 +251,10 @@ async function runTests() { command: 'pwsh -Command "Write-Output ready; $runtimePayload"', expectedRules: ['powershell.dynamic-execution'], }, + { + command: 'pwsh -Command $runtimePayload -Force C:/private/runtime-command-sentinel', + expectedRules: ['powershell.dynamic-execution'], + }, { command: `pwsh -EncodedCommand ${encodedPayload}`, expectedRules: ['powershell.remove-item.wildcard'], @@ -411,7 +415,7 @@ async function runTests() { 'Should not store raw command text' ); assert.ok( - !JSON.stringify(securityEvent).includes(command), + !JSON.stringify(securityEvent).includes(JSON.stringify(command).slice(1, -1)), 'Serialized governance evidence should not leak the raw command' ); } diff --git a/tests/lib/powershell-destructive-command.test.js b/tests/lib/powershell-destructive-command.test.js index 00d53c279..569cbb18d 100644 --- a/tests/lib/powershell-destructive-command.test.js +++ b/tests/lib/powershell-destructive-command.test.js @@ -470,6 +470,7 @@ test('classifies static execution primitives', () => { "$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 -Force C:/tmp/demo'; pwsh -Command:$payload", + "$payload = 'Remove-Item'; pwsh -Command $payload -Force C:/tmp/demo", "$payload = \"Remove-Item `\n-Force C:/tmp/demo\"; pwsh -Command $payload", ]) { expectRules(command, [RULES.REMOVE_FORCE]); @@ -482,6 +483,9 @@ test('classifies static execution primitives', () => { expectRules('pwsh -Command "Write-Output ready; $($runtimeValue)"', [ RULES.DYNAMIC_EXECUTION, ]); + expectRules('pwsh -Command $runtimeValue -Force C:/tmp/demo', [ + 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\""); expectSafe("$payload = 'Remove-Item -Force C:/tmp/demo'; pwsh -Command:`$payload");