Public/Invoke-BAShSudoCommand.ps1

# Invoke-BAShSudoCommand
function Invoke-BAShSudoCommand {

    <#
    .SYNOPSIS
    Runs a Bourne Again Shell sudo command on a remote computer.

    .DESCRIPTION
    The Invoke-SudoCommand function runs sudo commands via secure shell on a remote computer and returns any
    output.

    .EXAMPLE
    PS C:\> Invoke-SudoCommand -Name LinuxServer -Port 22 -Command "sudo whoami" -Credential (Get-Credential)
    Description
    -----------
    This runs the command "whoami" with sudo on the computer 'LinuxServer' via secure shell on port 22.

    .PARAMETER Name
    Specifies the computer on which the sudo command runs. Use an IP address or a DNS name of the remote computer.

    .PARAMETER Port
    Specifies the network port on the remote computer that is used for a secure shell session. To connect to a
    remote computer, it must be listening on the port that the connection uses. The default port is 22.

    .PARAMETER Command
    Specifies the command to run with sudo on the remote computer. Type in the command as you would do normally.

    .PARAMETER Credential
    Specifies a user account credential for the secure shell session connection, and has sudo permissions on the
    remote computer. Either pass a PSCredential object or respond to the prompt.

    .PARAMETER Timeout
    Specifies the timeout period for the sudo command to complete on the remote computer. The default is five
    seconds.

    .INPUTS
    System.String, System.Int16, System.Int32, pscredential

    .OUTPUTS
    System.String
    #>


    [CmdLetBinding()]
    param (

        # Remote computer name
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [ValidateNotNullOrEmpty()]
        [Alias("ComputerName", "Computer", "Server")]
        [string] $Name,

        # Secure shell port number
        [ValidateNotNullOrEmpty()]
        [Alias("SSHPort")]
        [int32] $Port = 22,

        # Sudo command to invoke on the remote computer
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [ValidateScript( { 
        
                if ($PSItem -like "sudo*") { Write-Output -InputObject $true }
                else { throw "'$PSItem' does not start with the word 'sudo'." }
            }
        )]
        [Alias("SudoCommand", "SudoCmd", "Cmd")]
        [string] $Command,

        # Credential for secure shell authentication
        [Parameter(Mandatory)]
        [ValidateNotNullOrEmpty()]
        [Alias("Cred")]
        [pscredential] $Credential,

        # Command Timeout
        [ValidateNotNullOrEmpty()]
        [Alias("Wait")]
        [int16] $Timeout = 5
    )

    begin {

        # Error handling
        Set-StrictMode -Version "Latest"
        Get-CallerPreference -Cmdlet $PSCmdlet -SessionState $ExecutionContext.SessionState
        $CallerEA = $ErrorActionPreference
        $ErrorActionPreference = "Stop"

        # Shorten the paramater variables
        $Cred = $Credential
        $Cmd = $Command
    }

    process {

        try {

            #TODO Create type names for xml output formatting in module

            # Create a secure shell session with computer
            Write-Debug -Message "Creating a secure shell session with '$Name' on '$Port'"
            Write-Verbose -Message "Creating a secure shell session"
            $Sesh = New-SSHSession -ComputerName $Name -Port $Port -Credential $Cred -AcceptKey
            if (-not $Sesh) { throw "A secure shell could not be created with '$Name' on '$Port'." }

            # Write debug information about the session
            $SeshCxnInfo = $Sesh.Session.ConnectionInfo
            Write-Debug -Message "Session information -"
            Write-Debug -Message "Identifier: $($Sesh.SessionId)"
            Write-Debug -Message "Host: $($Sesh.Host)"
            Write-Debug -Message "Port: $($SeshCxnInfo.Port)"
            Write-Debug -Message "Username: $($SeshCxnInfo.Username)"
            Write-Debug -Message "Encoding: $($SeshCxnInfo.Encoding)"
            Write-Debug -Message "Key exchange: $($SeshCxnInfo.CurrentKeyExchangeAlgorithm)"
            Write-Debug -Message "Server encryption: $($SeshCxnInfo.CurrentServerEncryption)"
            Write-Debug -Message "Client encryption: $($SeshCxnInfo.CurrentClientEncryption)"
            Write-Debug -Message "Server HMAC: $($SeshCxnInfo.CurrentServerHmacAlgorithm)"
            Write-Debug -Message "Client HMAC: $($SeshCxnInfo.CurrentClientHmacAlgorithm)"
            Write-Debug -Message "Host key: $($SeshCxnInfo.CurrentHostKeyAlgorithm)"
            Write-Debug -Message "Server compression: $($SeshCxnInfo.CurrentServerCompressionAlgorithm)"
            Write-Debug -Message "Client compression: $($SeshCxnInfo.CurrentClientCompressionAlgorithm)"
            Write-Debug -Message "Server version $($SeshCxnInfo.ServerVersion)"
            Write-Debug -Message "Client version: $($SeshCxnInfo.ClientVersion)"
            
            # Create a secure shell stream within the established session
            Write-Debug -Message "Creating a secure shell stream within the established session"
            Write-Verbose -Message "Creating a secure shell stream within the established session"
            $Stream = New-SSHShellStream -SSHSession $Sesh

            # Check that the user account has sudo permissions
            Write-Debug -Message "Checking that the user account '$($Cred.UserName)' has sudo permissions"
            Write-Verbose -Message "Checking that the user account has sudo permissions"
            $Params = @{

                ShellStream  = $Stream
                ExpectString = "[sudo] password for $($Cred.UserName):"
                SecureAction = $Cred.Password
            }
            $Invoke = Invoke-SSHStreamExpectSecureAction -Command "sudo whoami" @Params
            Start-Sleep -Seconds 1
            $Out = $Stream.Read()
            if (($Invoke -ne $true) -or ($Out -notmatch "^\s{2}\nroot\s\n\[$($Cred.UserName)@")) {

                throw "The user account '$($Cred.UserName)' does not have sudo permissions on '$Name'."
            }

            # Check that there is not a zero timeout set on sudo
            Write-Debug -Message "Checking that there is not a zero timeout set on sudo"
            Write-Verbose -Message "Checking that there is not a zero timeout set on sudo"
            $Invoke = Invoke-SSHStreamExpectSecureAction -Command "sudo whoami" -TimeOut 2 @Params
            Start-Sleep -Seconds 1
            $Out = $Stream.Read()
            if (($Invoke -eq $true) -or ($Out -notmatch "^sudo whoami\s\nroot\s\n\[$($Cred.UserName)@")) {

                throw "There is a zero timeout set on sudo on computer '$Name'."
            }

            # Invoke the command using the established stream
            Write-Debug -Message "Invoking the sudo command '$Cmd' within the established stream"
            Write-Verbose -Message "Invoking the sudo command within the established stream"
            do { $Stream.Read() | Out-Null } while ($Stream.DataAvailable) # Clear the stream in preparation
            $Stream.WriteLine($Cmd) # Send the command
            $Stream.ReadLine() | Out-Null # Clear the command from the stream
            Start-Sleep -Seconds 1 # Allow the command time to execute

            # Wait for the command to complete
            Write-Debug -Message "Waiting for the stream to return data - timeout is set to '$Timeout' second(s)"
            Write-Verbose -Message "Waiting for the command to complete"
            $Span = New-TimeSpan -Seconds $Timeout # Set the timeout for the command to complete
            $Out = $Stream.Expect([regex] "(\]\W $|\]\W\snohup: appending output to ``nohup.out'\s$)", $Span)

            # Timeout period reached
            if (-not $Out) { throw "The command '$Cmd' failed to complete within '$Timeout' seconds on '$Name'." }

            # Parse the data and output
            if ($Out.Length -eq 0) {
            
                Write-Debug -Message "No data to return"
                Write-Verbose -Message "No data to return"
                return
            }
            Write-Debug -Message "Parsing the data:`n$Out`n"
            Write-Verbose -Message "Parsing the data and outputting"
            Write-Output -InputObject $Out.SubString(0, $Out.LastIndexOf("[")).Trim()
        }
        catch { Write-Error -ErrorRecord $PSItem -EA $CallerEA }
        finally {
        
            # Remove the secure shell session
            Write-Debug -Message "Removing the secure shell session"
            Write-Verbose -Message "Removing the secure shell session"
            Remove-SSHSession -SessionId 0 | Out-Null
        }
    }
    end { }
}