Public/sessions/Get-ActiveRdpSession.ps1
|
function Get-ActiveRdpSession { <# .SYNOPSIS Retrieves currently active and disconnected RDP sessions from local or remote computers .DESCRIPTION Queries the Terminal Services session information on one or more computers to retrieve all active, disconnected, and idle RDP sessions. Returns structured objects with session ID, user, state, logon time, and idle duration. Uses the Win32_LogonSession WMI class via CIM to retrieve session information, providing more reliable cross-version compatibility than legacy query commands. .PARAMETER ComputerName One or more computer names to query. Defaults to the local machine. Supports pipeline input by value and by property name. .PARAMETER Credential Credential to use when querying remote computers. If not specified, uses the current user's credentials. .PARAMETER IncludeSystemSessions Include system sessions (Session 0, Services, Console) in the output. By default, only user sessions are returned. .EXAMPLE Get-ActiveRdpSession Retrieves all active user sessions from the local computer. .EXAMPLE Get-ActiveRdpSession -ComputerName 'SRV01', 'SRV02' -Credential $cred Retrieves active sessions from multiple remote servers using specified credentials. .EXAMPLE 'WEB01', 'APP01' | Get-ActiveRdpSession | Where-Object { $_.State -eq 'Disconnected' } Pipeline example: finds all disconnected sessions on multiple servers. .EXAMPLE Get-ADComputer -Filter "OperatingSystem -like '*Server*'" | Get-ActiveRdpSession | Where-Object { $_.IdleTime -gt (New-TimeSpan -Hours 4) } Retrieves sessions idle for more than 4 hours across all domain servers. .NOTES Author: Franck SALLET Version: 1.1.0 Last Modified: 2026-03-11 Requires: PowerShell 5.1+ Permissions: Remote Desktop Users group or local Administrator on target machines .LINK https://docs.microsoft.com/en-us/windows/win32/termserv/win32-logonsession #> [CmdletBinding()] [OutputType([PSCustomObject])] param( [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)] [ValidateNotNullOrEmpty()] [Alias('CN', 'Name', 'DNSHostName')] [string[]]$ComputerName = $env:COMPUTERNAME, [Parameter(Mandatory = $false)] [ValidateNotNull()] [System.Management.Automation.PSCredential]$Credential, [Parameter(Mandatory = $false)] [switch]$IncludeSystemSessions ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting - PowerShell $($PSVersionTable.PSVersion)" # Session state mapping $script:stateMap = @{ 0 = 'Active' 1 = 'Connected' 2 = 'ConnectQuery' 3 = 'Shadow' 4 = 'Disconnected' 5 = 'Idle' 6 = 'Listen' 7 = 'Reset' 8 = 'Down' 9 = 'Init' } } process { foreach ($computer in $ComputerName) { Write-Verbose "[$($MyInvocation.MyCommand)] Processing: $computer" # Determine whether the target is the local machine. # Local queries skip New-CimSession entirely, which avoids type identity # mismatches between runspaces and removes an unnecessary network hop. $isLocal = ($computer -eq $env:COMPUTERNAME) -or ($computer -eq 'localhost') -or ($computer -eq '.') $cimSession = $null try { # Only create a CIM session for remote computers if (-not $isLocal) { $cimSessionParams = @{ ComputerName = $computer ErrorAction = 'Stop' } if ($PSBoundParameters.ContainsKey('Credential')) { $cimSessionParams['Credential'] = $Credential } $cimSession = New-CimSession @cimSessionParams Write-Verbose "[$($MyInvocation.MyCommand)] CIM session established to $computer" } # Build Get-CimInstance parameters — omit CimSession for local queries $cimParams = @{ ClassName = 'Win32_LogonSession' ErrorAction = 'Stop' } if ($null -ne $cimSession) { $cimParams['CimSession'] = $cimSession } # Filter to RemoteInteractive (LogonType 10 = RDP) sessions only $logonSessions = Get-CimInstance @cimParams | Where-Object { $_.LogonType -eq 10 } if ($null -eq $logonSessions -or @($logonSessions).Count -eq 0) { Write-Verbose "[$($MyInvocation.MyCommand)] No RDP sessions found on $computer" continue } Write-Verbose "[$($MyInvocation.MyCommand)] Retrieved $(@($logonSessions).Count) session(s) from $computer" foreach ($session in $logonSessions) { # Resolve the associated user via Win32_LoggedOnUser. # We query Win32_LoggedOnUser filtered by LogonId, then read the # Domain and Name properties directly — avoiding Get-CimAssociatedInstance # which requires a typed CimInstance object and fails across Pester runspaces. $userName = 'UNKNOWN' try { $logonId = $session.LogonId $wqlFilter = "Dependent = 'Win32_LogonSession.LogonId=""$logonId""'" $assocParams = @{ ClassName = 'Win32_LoggedOnUser' Filter = $wqlFilter ErrorAction = 'Stop' } if ($null -ne $cimSession) { $assocParams['CimSession'] = $cimSession } $association = Get-CimInstance @assocParams | Select-Object -First 1 # Real CimInstance exposes Antecedent as a typed reference object # with Domain and Name properties directly accessible. if ($association -and $association.Antecedent) { $ref = $association.Antecedent if ($ref.Domain -and $ref.Name) { $userName = "$($ref.Domain)\$($ref.Name)" } } } catch { Write-Verbose "[$($MyInvocation.MyCommand)] Could not resolve user for session $($session.LogonId) on $computer - $_" } $startTime = $session.StartTime $idleTime = if ($startTime) { (Get-Date) - $startTime } else { $null } # Emit one structured object per session [PSCustomObject]@{ PSTypeName = 'PSWinOps.ActiveRdpSession' ComputerName = $computer SessionID = $session.LogonId UserName = $userName LogonTime = $startTime IdleTime = $idleTime LogonType = 'RemoteInteractive' AuthPackage = $session.AuthenticationPackage } } } catch [Microsoft.Management.Infrastructure.CimException] { Write-Error "[$($MyInvocation.MyCommand)] CIM error on $computer - $_" } catch [System.UnauthorizedAccessException] { Write-Error "[$($MyInvocation.MyCommand)] Access denied to $computer - Requires administrative permissions" } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed to query $computer - $_" } finally { if ($null -ne $cimSession) { Remove-CimSession -CimSession $cimSession Write-Verbose "[$($MyInvocation.MyCommand)] CIM session closed for $computer" } } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed" } } |