-
Notifications
You must be signed in to change notification settings - Fork 131
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Make sudo.ps1 support multi-line commands #39
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change | ||||
---|---|---|---|---|---|---|
@@ -1,87 +1,94 @@ | ||||||
<# | ||||||
.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, | ||||||
|
||||||
# open question - Should -NoProfile be used when invoking PowerShell | ||||||
BEGIN { | ||||||
if ($__SUDO_TEST -ne $true) { | ||||||
$SUDOEXE = "sudo.exe" | ||||||
} | ||||||
else { | ||||||
if ($null -eq $SUDOEXE) { | ||||||
throw "variable SUDOEXE has not been set for testing" | ||||||
} | ||||||
} | ||||||
# Run PowerShell with the -NoProfile switch | ||||||
[switch]$NoProfile, | ||||||
|
||||||
if ([Environment]::OSVersion.Platform -ne "Win32NT") { | ||||||
# A command or application to run in an elevated process | ||||||
[Parameter(Mandatory, Position = 0, ParameterSetName = "Command")] | ||||||
[string]$Command, | ||||||
|
||||||
# Arguments to pass to the command or application | ||||||
[Parameter(Position = 1, ParameterSetName = "Command", ValueFromRemainingArguments)] | ||||||
[Alias("Args")] | ||||||
[PSObject[]]$ArgumentList | ||||||
) | ||||||
begin { | ||||||
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 ($__SUDO_TEST -ne $true) { | ||||||
$Env:SUDOEXE = "sudo.exe" | ||||||
} elseif (!$Env:SUDOEXE) { | ||||||
throw "Environment variable SUDOEXE has not been set for testing" | ||||||
} | ||||||
|
||||||
$psProcess = Get-Process -id $PID | ||||||
if (($null -eq $psProcess) -or ($psProcess.Count -ne 1)) { | ||||||
throw "Cannot retrieve process for '$PID'" | ||||||
if (!(Get-Command -Type Application -Name $Env:SUDOEXE -ErrorAction Ignore)) { | ||||||
throw "Env:SUDOEXE is set to '$Env:SUDOEXE' but it cannot be found." | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe "that cannot be found." or keep "it" There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah, I think this: Is better than: |
||||||
} | ||||||
|
||||||
$thisPowerShell = $psProcess.MainModule.FileName | ||||||
if ($null -eq $thisPowerShell) { | ||||||
throw "Cannot determine path to '$psProcess'" | ||||||
$thisPowerShell = (Get-Process -Id $PID).MainModule.FileName | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I want to point out that I dramatically simplified this, just because there isn't really any way this can go wrong. $PID is a constant in PowerShell and can't changed, so this is always going to return the full path to the executable. |
||||||
if (!$thisPowerShell) { | ||||||
throw "Cannot determine PowerShell executable path." | ||||||
} | ||||||
|
||||||
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 } | ||||||
} | ||||||
else { | ||||||
& $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")) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should be |
||||||
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. | ||||||
Comment on lines
+74
to
+77
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the trailing |
||||||
# Strip the 'sudo' or 'sudo.ps1` or whatever off the front of the statement | ||||||
$Statement = $Statement.SubString($MyInvocation.InvocationName.Length).Trim() | ||||||
$EncodedCommand = ConvertToBase64EncodedString $Statement | ||||||
} | ||||||
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")) { | ||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this should also be |
||||||
Trace-Command -PSHost -Name param* -Expression { & $Env:SUDOEXE $ThisPowerShell @switches -EncodedCommand $encodedCommand } | ||||||
} else { | ||||||
& $Env:SUDOEXE $ThisPowerShell @switches -EncodedCommand $encodedCommand | ||||||
} | ||||||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This seems overly complicated. One must set two variables: SUDOEXE and __SUDO_TEST, to actually be able to use an alternate executable for testing? I'm not sure why this was here...
I would prefer to just have a parameter
SudoPath
that you can set to whatever you want for testing purposes, but that defaults to justsudo.exe
and assumes that sudo.exe is in the PATH.NOTE: I changed
SUDOEXE
to an environment variable. I feel like if we're using a mock sudo, you need to be able to set the environment stuff once (i.e. in your pipeline, or in your machine scope), instead of needing to set multiple flags for different wrapper scripts.I'd rather remove these, but if we want to keep them, should
__SUDO_TEST
also be an environment variable?