Public/sessions/Enter-RdpSession.ps1

#Requires -Version 5.1

function Enter-RdpSession {
    <#
.SYNOPSIS
    Establishes remote control (shadow) connection to an active RDP session
 
.DESCRIPTION
    Connects to an active RDP session on a remote computer to observe or interactively
    control the user's session. This is equivalent to the "shadow" functionality in
    Remote Desktop Services, allowing administrators to provide remote assistance or
    monitor user activity.
 
    Requires appropriate Group Policy settings and administrative permissions.
    The target user may receive a notification prompt depending on policy configuration.
 
    Two modes are supported:
    - View: Observation only (read-only access to the session)
    - Control: Full interactive control (keyboard and mouse input enabled)
 
    Supports ShouldProcess for -WhatIf and -Confirm operations.
 
.PARAMETER ComputerName
    The computer name where the target RDP session is active.
    Defaults to the local machine. Supports pipeline input by property name.
 
.PARAMETER SessionID
    The session ID to shadow. Can be retrieved using Get-ActiveRdpSession.
    Supports pipeline input by value and by property name.
 
.PARAMETER ControlMode
    Specifies the shadow mode:
    - Control: Full interactive control with keyboard and mouse input (default)
    - View: Read-only observation without interaction capability
 
.PARAMETER NoUserPrompt
    Suppresses the user consent prompt on the target session. Requires appropriate
    Group Policy configuration. If not configured, the connection may be rejected.
 
.PARAMETER Credential
    Credential to use when connecting to remote computers. If not specified,
    uses the current user's credentials.
 
.EXAMPLE
    Enter-RdpSession -SessionID 2 -ComputerName 'SRV01'
    Establishes interactive control of session 2 on SRV01 with user consent prompt.
 
.EXAMPLE
    Get-ActiveRdpSession -ComputerName 'APP01' | Where-Object { $_.UserName -eq 'DOMAIN\helpdesk' } | Enter-RdpSession -ControlMode View
    Finds helpdesk user session and connects in view-only mode.
 
.EXAMPLE
    Enter-RdpSession -SessionID 3 -ComputerName 'WEB01' -NoUserPrompt -WhatIf
    Shows what would happen if entering session 3 without user prompt.
 
.EXAMPLE
    Enter-RdpSession -SessionID 5 -ControlMode View -Credential $adminCred
    Connects to session 5 in observation mode using specified credentials.
 
.NOTES
    Author: Franck SALLET
    Version: 1.1.0
    Last Modified: 2026-03-11
    Requires: PowerShell 5.1+, Remote Desktop Services
    Permissions: Local Administrator on target machine
    Requirements: Group Policy setting "Set rules for remote control of RDS user sessions" must allow shadowing
    Note: This function initiates the shadow session. Use Ctrl+* (keypad) to exit shadow mode.
 
.LINK
    https://docs.microsoft.com/en-us/windows-server/remote/remote-desktop-services/rds-remote-control
#>

    [CmdletBinding(SupportsShouldProcess, ConfirmImpact = 'High')]
    [OutputType([PSCustomObject])]
    param(
        [Parameter(Mandatory = $false, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateRange(0, 65536)]
        [int]$SessionID,

        [Parameter(Mandatory = $false)]
        [ValidateSet('Control', 'View')]
        [string]$ControlMode = 'Control',

        [Parameter(Mandatory = $false)]
        [switch]$NoUserPrompt,

        [Parameter(Mandatory = $false)]
        [ValidateNotNull()]
        [System.Management.Automation.PSCredential]$Credential
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand)] Starting - PowerShell $($PSVersionTable.PSVersion)"

        # Shadow mode flags (from Microsoft Terminal Services documentation)
        $script:shadowModeMap = @{
            'Control' = 2  # Full control with user notification
            'View'    = 1  # View only with user notification
        }

        # If NoUserPrompt is specified, add 2 to the flag value
        # Control without prompt = 4, View without prompt = 3
        if ($NoUserPrompt) {
            Write-Verbose "[$($MyInvocation.MyCommand)] NoUserPrompt specified - user consent will be bypassed if policy allows"
        }
    }

    process {
        Write-Verbose "[$($MyInvocation.MyCommand)] Processing session ID $SessionID on $ComputerName"

        # Calculate shadow mode flag
        $shadowMode = $script:shadowModeMap[$ControlMode]
        if ($NoUserPrompt) {
            $shadowMode += 2
        }

        $actionDescription = if ($ControlMode -eq 'Control') {
            'Take interactive control of RDP session (SHADOW MODE)'
        } else {
            'Observe RDP session in view-only mode (SHADOW MODE)'
        }

        if ($PSCmdlet.ShouldProcess("$ComputerName - Session $SessionID", $actionDescription)) {
            # Build CIM session parameters
            $cimSessionParams = @{
                ComputerName = $ComputerName
                ErrorAction  = 'Stop'
            }

            if ($PSBoundParameters.ContainsKey('Credential')) {
                $cimSessionParams['Credential'] = $Credential
            }

            $cimSession = $null

            try {
                # Create CIM session
                $cimSession = New-CimSession @cimSessionParams
                Write-Verbose "[$($MyInvocation.MyCommand)] CIM session established to $ComputerName"

                # Verify target session exists using Terminal Services namespace
                $targetSession = Get-CimInstance -CimSession $cimSession `
                    -ClassName 'Win32_TSSession' `
                    -Namespace 'root\cimv2\TerminalServices' `
                    -Filter "SessionId = $SessionID" `
                    -ErrorAction SilentlyContinue

                if ($null -eq $targetSession) {
                    Write-Error "[$($MyInvocation.MyCommand)] Session ID $SessionID not found on $ComputerName"
                    return
                }

                Write-Verbose "[$($MyInvocation.MyCommand)] Target session $SessionID verified on $ComputerName"

                # Get Terminal Service instance
                $tsService = Get-CimInstance -CimSession $cimSession `
                    -ClassName 'Win32_TerminalService' `
                    -Namespace 'root\cimv2\TerminalServices' `
                    -ErrorAction Stop

                # Invoke RemoteControl method (shadow connection)
                Write-Verbose "[$($MyInvocation.MyCommand)] Initiating shadow connection with mode: $ControlMode (flag: $shadowMode)"

                $invokeParams = @{
                    InputObject = $tsService
                    MethodName  = 'RemoteControl'
                    Arguments   = @{
                        SessionId       = $SessionID
                        HotKeyVK        = 0x6A  # VK_MULTIPLY (numpad *)
                        HotkeyModifiers = 0x2   # MOD_CONTROL
                    }
                    ErrorAction = 'Stop'
                }

                $result = Invoke-CimMethod @invokeParams

                # Check return value
                $success = ($result.ReturnValue -eq 0)

                # Return code meanings for RemoteControl method
                $returnCodeMap = @{
                    0  = 'Success - Shadow session initiated'
                    1  = 'Failed - Session not found'
                    2  = 'Failed - Session not active or not accepting shadow connections'
                    5  = 'Failed - Access denied - Insufficient permissions'
                    7  = 'Failed - Invalid parameter'
                    9  = 'Failed - Shadow session already in progress'
                    10 = 'Failed - User rejected the connection request'
                    11 = 'Failed - Shadow not enabled in Group Policy'
                }

                $resultMessage = if ($returnCodeMap.ContainsKey($result.ReturnValue)) {
                    $returnCodeMap[$result.ReturnValue]
                } else {
                    "Unknown return code: $($result.ReturnValue)"
                }

                [PSCustomObject]@{
                    PSTypeName   = 'PSWinOps.RdpSessionAction'
                    ComputerName = $ComputerName
                    SessionID    = $SessionID
                    Action       = 'RemoteControl'
                    ControlMode  = $ControlMode
                    Success      = $success
                    ReturnCode   = $result.ReturnValue
                    Message      = $resultMessage
                    Timestamp    = Get-Date
                }

                if ($success) {
                    Write-Information -MessageData "[OK] Shadow session initiated for session $SessionID on $ComputerName" -InformationAction Continue
                    Write-Information -MessageData '[INFO] Press Ctrl+* (keypad asterisk) to exit shadow mode' -InformationAction Continue
                } else {
                    Write-Warning "[$($MyInvocation.MyCommand)] $resultMessage"
                }
            } catch [Microsoft.Management.Infrastructure.CimException] {
                Write-Error "[$($MyInvocation.MyCommand)] CIM error on $ComputerName - $_"
            } catch [System.UnauthorizedAccessException] {
                Write-Error "[$($MyInvocation.MyCommand)] Access denied to $ComputerName - Requires administrative permissions"
            } catch {
                Write-Error "[$($MyInvocation.MyCommand)] Failed to establish shadow connection to session $SessionID on $ComputerName - $_"
            } finally {
                if ($null -ne $cimSession) {
                    Remove-CimSession -CimSession $cimSession
                    Write-Verbose "[$($MyInvocation.MyCommand)] CIM session closed for $ComputerName"
                }
            }
        }
    }

    end {
        Write-Verbose "[$($MyInvocation.MyCommand)] Completed"
    }
}