From ef20fe91deb8f407a17f1260d831ca65593da01c Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Fri, 9 Feb 2024 15:33:11 -0500 Subject: [PATCH 1/4] Make sudo.ps1 support multi-line commands Don't assume Length=5 (people may call sudo.ps1 or the full path) And, you know, clean it up a bit, because I can't stop myself. --- scripts/sudo.ps1 | 119 ++++++++++++++++++++++------------------------- 1 file changed, 55 insertions(+), 64 deletions(-) diff --git a/scripts/sudo.ps1 b/scripts/sudo.ps1 index 4587809..27b67c3 100644 --- a/scripts/sudo.ps1 +++ b/scripts/sudo.ps1 @@ -1,87 +1,78 @@ # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. +[CmdletBinding(DefaultParameterSetName = "Script")] +param( + [Parameter(Mandatory, Position = 0, ParameterSetName = "Script")] + [scriptblock]$ScriptBlock, -# open question - Should -NoProfile be used when invoking PowerShell -BEGIN { + [switch]$NoProfile, + + [Parameter(Mandatory, Position = 0, ParameterSetName = "Command")] + [string]$Command, + + [Parameter(Position = 1, ValueFromRemainingArguments)] + [Alias("Args")] + [PSObject[]]$ArgumentList +) +begin { if ($__SUDO_TEST -ne $true) { - $SUDOEXE = "sudo.exe" - } - else { - if ($null -eq $SUDOEXE) { - throw "variable SUDOEXE has not been set for testing" - } + $Env:SUDOEXE = "sudo.exe" + } elseif (!$Env:SUDOEXE) { + throw "Environment variable SUDOEXE has not been set for testing" } - if ([Environment]::OSVersion.Platform -ne "Win32NT") { + if ($IsLinux -or $IsMacOS) { throw "This script works only on Microsoft Windows" } - if ($null -eq (Get-Command -Type Application -Name "$SUDOEXE" -ErrorAction Ignore)) { - throw "'$SUDOEXE' cannot be found." + if (!(Get-Command -Type Application -Name $Env:SUDOEXE -ErrorAction Ignore)) { + throw "Env:SUDOEXE is set to '$Env:SUDOEXE' but it cannot be found." } - $psProcess = Get-Process -id $PID - if (($null -eq $psProcess) -or ($psProcess.Count -ne 1)) { - throw "Cannot retrieve process for '$PID'" + $thisPowerShell = (Get-Process -Id $PID).MainModule.FileName + if (!$thisPowerShell) { + throw "Cannot determine PowerShell executable path." } - $thisPowerShell = $psProcess.MainModule.FileName - if ($null -eq $thisPowerShell) { - throw "Cannot determine path to '$psProcess'" - } - - function convertToBase64EncodedString([string]$cmdLine) { - $bytes = [System.Text.Encoding]::Unicode.GetBytes($cmdLine) + function ConvertToBase64EncodedString([string]$InputObject) { + $bytes = [System.Text.Encoding]::Unicode.GetBytes($InputObject) [Convert]::ToBase64String($bytes) } - - $MI = $MyInvocation } -END { - $cmdArguments = $args - - # short-circuit if the user provided a scriptblock, then we will use it and ignore any other arguments - if ($cmdArguments.Count -eq 1 -and $cmdArguments[0] -is [scriptblock]) { - $scriptBlock = $cmdArguments[0] - $encodedCommand = convertToBase64EncodedString -cmdLine ($scriptBlock.ToString()) - if (($psversiontable.psversion.major -eq 7) -and ($__SUDO_DEBUG -eq $true)) { - Trace-Command -PSHOST -name param* -Expression { & $SUDOEXE "$thisPowerShell" -e $encodedCommand } +end { + # If the first parameter is the name of an executable, just run that without PowerShell + if ($PSCmdlet.ParameterSetName -eq "Command") { + if (@(Get-Command $Command -ErrorAction Ignore)[0].CommandType -eq "Application") { + # NOTE: this assumes that all the parameters can be just strings + if ($PSBoundParameters.Contains("Debug")) { + Trace-Command -PSHost -Name param* -Expression { & $Env:SUDOEXE $Command @ArgumentList } + } else { + & $Env:SUDOEXE $Command $ArgumentList + } + return + } else { + # In this case, we're going to need to _make_ a scriptblock out of $MyInvocation.Statement + # NOT $MyInvocation.Line because there might be more than one line in the statement + # IISReset and Jaykul apologize for the reflection, but we need to support old versions of PowerShell + $Statement = [System.Management.Automation.InvocationInfo].GetMember( + '_scriptPosition', + [System.Reflection.BindingFlags]'NonPublic,Instance' + )[0].GetValue($MyInvocation).Text. + # Strip the 'sudo' or 'sudo.ps1` or whatever off the front of the statement + $Statement = $Statement.SubString($MyInvocation.InvocationName.Length).Trim() + $EncodedCommand = ConvertToBase64EncodedString $Statement } - else { - & $SUDOEXE "$thisPowerShell" -e $encodedCommand - } - return + } else { + $EncodedCommand = ConvertToBase64EncodedString $scriptBlock } - $cmdLine = $MI.Line - $sudoOffset = $cmdLine.IndexOf($MI.InvocationName) - $cmdLineWithoutScript = $cmdLine.SubString($sudoOffset + 5) - $cmdLineAst = [System.Management.Automation.Language.Parser]::ParseInput($cmdLineWithoutScript, [ref]$null, [ref]$null) - $commandAst = $cmdLineAst.Find({$args[0] -is [System.Management.Automation.Language.CommandAst]}, $false) - $commandName = $commandAst.GetCommandName() - $isApplication = Get-Command -Type Application -Name $commandName -ErrorAction Ignore | Select-Object -First 1 - $isCmdletOrScript = Get-Command -Type Cmdlet,ExternalScript -Name $commandName -ErrorAction Ignore | Select-Object -First 1 + $switches = @("-NoLogo", "-NonInteractive") + if ($NoProfile) { $switches += "-NoProfile" } - # if the command is a native command, just invoke it - if ($null -ne $isApplication) { - if (($psversiontable.psversion.major -eq 7) -and ($__SUDO_DEBUG -eq $true)) { - trace-command -PSHOST -name param* -Expression { & $SUDOEXE $cmdArguments } - } - else { - & $SUDOEXE $cmdArguments - } - } - elseif ($null -ne $isCmdletOrScript) { - $encodedCommand = convertToBase64EncodedString($cmdLineWithoutScript) - if (($psversiontable.psversion.major -eq 7) -and ($__SUDO_DEBUG -eq $true)) { - trace-command -PSHOST -name param* -Expression { & $SUDOEXE -nologo -e $encodedCommand } - } - else { - & $SUDOEXE $thisPowerShell -nologo -e $encodedCommand - } - } - else { - throw "Cannot find '$commandName'" + if ($PSBoundParameters.Contains("Debug")) { + Trace-Command -PSHost -Name param* -Expression { & $Env:SUDOEXE $ThisPowerShell @switches -EncodedCommand $encodedCommand } + } else { + & $Env:SUDOEXE $ThisPowerShell @switches -EncodedCommand $encodedCommand } } From 2eee42f10123bce098ff5120d05ae4175c79dc80 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Tue, 13 Feb 2024 00:22:52 -0500 Subject: [PATCH 2/4] Move OS test to be first so we fail on the important things --- scripts/sudo.ps1 | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/scripts/sudo.ps1 b/scripts/sudo.ps1 index 27b67c3..144a96c 100644 --- a/scripts/sudo.ps1 +++ b/scripts/sudo.ps1 @@ -15,16 +15,16 @@ param( [PSObject[]]$ArgumentList ) begin { + if ($IsLinux -or $IsMacOS) { + throw "This script works only on Microsoft Windows" + } + if ($__SUDO_TEST -ne $true) { $Env:SUDOEXE = "sudo.exe" } elseif (!$Env:SUDOEXE) { throw "Environment variable SUDOEXE has not been set for testing" } - if ($IsLinux -or $IsMacOS) { - throw "This script works only on Microsoft Windows" - } - if (!(Get-Command -Type Application -Name $Env:SUDOEXE -ErrorAction Ignore)) { throw "Env:SUDOEXE is set to '$Env:SUDOEXE' but it cannot be found." } From 243d8763c70441becf3daa9a47ccfef2d16fccf4 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Tue, 13 Feb 2024 00:34:36 -0500 Subject: [PATCH 3/4] Add documentation comments (help) --- scripts/sudo.ps1 | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/scripts/sudo.ps1 b/scripts/sudo.ps1 index 144a96c..7fe0ce2 100644 --- a/scripts/sudo.ps1 +++ b/scripts/sudo.ps1 @@ -1,16 +1,32 @@ +<# + .SYNOPSIS + Runs a scriptblock, command or application as an elevated process using sudo.exe + .DESCRIPTION + Wraps sudo.exe to add functionality for running PowerShell scripts or commands in an elevated process. + + When running a scriptblock or PowerShell command, a new copy of PowerShell is run with sudo and an EncodedCommand. + When running a native application, the command itself is run with sudo. + + This script DOES NOT (currently) support piping input to the elevated command. +#> + # Copyright (c) Microsoft Corporation. # Licensed under the MIT License. [CmdletBinding(DefaultParameterSetName = "Script")] param( + # A scriptblock to run in an elevated process [Parameter(Mandatory, Position = 0, ParameterSetName = "Script")] [scriptblock]$ScriptBlock, + # Run PowerShell with the -NoProfile switch [switch]$NoProfile, + # A command or application to run in an elevated process [Parameter(Mandatory, Position = 0, ParameterSetName = "Command")] [string]$Command, - [Parameter(Position = 1, ValueFromRemainingArguments)] + # Arguments to pass to the command or application + [Parameter(Position = 1, ParameterSetName = "Command", ValueFromRemainingArguments)] [Alias("Args")] [PSObject[]]$ArgumentList ) From d19f0fa2efc5e334c27655a0af8f96bddb3a9362 Mon Sep 17 00:00:00 2001 From: Joel Bennett Date: Tue, 13 Feb 2024 00:56:23 -0500 Subject: [PATCH 4/4] Whitespace fix --- scripts/sudo.ps1 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/sudo.ps1 b/scripts/sudo.ps1 index 7fe0ce2..2fb31da 100644 --- a/scripts/sudo.ps1 +++ b/scripts/sudo.ps1 @@ -38,7 +38,7 @@ begin { if ($__SUDO_TEST -ne $true) { $Env:SUDOEXE = "sudo.exe" } elseif (!$Env:SUDOEXE) { - throw "Environment variable SUDOEXE has not been set for testing" + throw "Environment variable SUDOEXE has not been set for testing" } if (!(Get-Command -Type Application -Name $Env:SUDOEXE -ErrorAction Ignore)) {