Skip to content

Instantly share code, notes, and snippets.

@thomasswilliams
Last active November 28, 2023 22:19
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 3 You must be signed in to fork a gist
  • Save thomasswilliams/473f02c52e7036e84486dd8515dff7d0 to your computer and use it in GitHub Desktop.
Save thomasswilliams/473f02c52e7036e84486dd8515dff7d0 to your computer and use it in GitHub Desktop.
PowerShell script to list remote desktop logon, logoff, disconnect events from the Terminal Services event log for a passed computer, collection of computers, or computer name(s) from prompt
<#
.SYNOPSIS
Get remote desktop sessions for a specified computer or computers, from the Terminal
Services event log. Adapted from https://serverfault.com/a/687079/78216, https://ss64.com/ps/get-winevent.html
Requires appropriate permission on computers to call Get-WinEvent remotely, PowerShell Active Directory module.
If using Windows Firewall, may require enabling "Remote Event Log Management" rule (from
https://social.technet.microsoft.com/Forums/en-US/69fba4e0-a248-4d1c-9e0d-80071a2a446d/getwinevent-the-rpc-server-is-unavailable)
By Thomas Williams <https://github.com/thomasswilliams>
.DESCRIPTION
Pass one or more computer names, or run without parameters to be prompted for computer name(s).
.PARAMETER computers
Computer or computers to get Remote Desktop history for.
.EXAMPLE
$DebugPreference = "Continue"; & '.\remote-desktop-history.ps1'; $DebugPreference = "SilentlyContinue"
Runs with debug statements, prompts for a computer, and turns debug statements off at the end.
.EXAMPLE
.\remote-desktop-history.ps1 "PC1"
Gets Remote Desktop history for computer "PC1".
.EXAMPLE
.\remote-desktop-history.ps1 "PC1", "PC2", "SERVER1"
Gets Remote Desktop history for computers "PC1", "PC2", "SERVER1".
#>
[CmdletBinding()]
Param(
[Parameter(Mandatory = $false,
Position = 0,
ValueFromPipeline = $true,
ValueFromPipelineByPropertyName = $true,
HelpMessage = "Computer or computers to get Remote Desktop history for.")]
[String[]]$computers
)
Begin {
# Active Directory module needed for Get-ADUser
Import-Module ActiveDirectory -Verbose:$false
}
Process {
# error on coding violations
Set-StrictMode -Version Latest
Function getDisplayNameFromUser($userName) {
<#
.SYNOPSIS
Get the user object from AD using default settings, and return the display name.
Expects Windows login without domain.
#>
Try {
# note getting user object may error, if so return empty string
# expand the name property otherwise will get object e.g. "@{ name=xxx }"
Return Get-ADUser $userName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty name
} Catch {
Return [string]::Empty
}
}
# if passed computer name is empty, prompt user
If ([string]::IsNullOrEmpty($computers)) {
# get user input (string)
[string]$input_from_user = Read-Host "Remote Desktop history
Enter computer name (blank to quit)"
# if user input contains comma or space characters, split into computer collection
If ($input_from_user.indexOf(",") -gt -1) {
$computers = $input_from_user -Split ","
} ElseIf ($input_from_user.indexOf(" ") -gt -1) {
$computers = $input_from_user -Split " "
} Else {
# set computers collection to user input
$computers = $input_from_user
}
}
# check for cancel here
If ([string]::IsNullOrEmpty($computers)) {
Write-Output "No computer entered, quitting..."
Exit
}
# set up filter for Get-WinEvent
$filter = @{
# specified event log:
# "The following is a list of Remote Desktop Services events that can appear in the event log
# of a computer that is running a Remote Desktop Services role service, such as Remote Desktop
# Session Host or Remote Desktop Gateway. The events can be viewed by using Event Viewer."
# see docs at https://technet.microsoft.com/en-au/library/ff404144%28v=ws.10%29.aspx?f=255&MSPPError=-2147217396
# "...includes both RDP logins as well as regular console logins too" as per https://gallery.technet.microsoft.com/scriptcenter/Remote-Desktop-Connection-3fe225cd
LogName = "Microsoft-Windows-TerminalServices-LocalSessionManager/Operational"
# last 180 days only
# note event log may not retain events back this far, depending on max size and overwrite settings
StartTime = (Get-Date).AddDays(-180)
# limit to events:
# 21 = logon
# 23 = logoff
# 24 = disconnected
# 25 = reconnection
ID = 21, 23, 24, 25
}
# loop through computers collection
ForEach ($computer in $computers) {
# reset timer
$TimerStart = Get-Date
# if the computer is online
If (Test-Connection $computer -Count 1 -Quiet -ErrorAction SilentlyContinue) {
Write-Debug "$computer is online, about to get events from event log..."
Try {
# empty results collection for starters
$Results = @()
# get events collection from log for computer, for specified filter
# this may fail if the user running this script does not have permissions e.g. is a non-admin user
# see https://social.technet.microsoft.com/Forums/lync/en-US/b72162d1-2c86-4d1a-9727-ec7269814cc4/getwinevent-with-nonadministrative-user?forum=winserverpowershell for potential workaround (not tested)
$Events = Get-WinEvent -ComputerName $computer -FilterHashtable $filter
"Got $($Events.Count ?? 0) events from $computer in " + [math]::Round((New-Timespan -Start $TimerStart -End $(Get-Date)).TotalSeconds, 2) + " seconds" | ForEach-Object { Write-Debug $_ }
# reset progress counter
$progress_counter = 0
# pre-calculate event count variable to avoid recalculating in loop
$Events_count = $Events.Count
# loop through each event in returned events
ForEach ($Event in $Events) {
# increment progress counter
$progress_counter++
# output progress
Write-Progress -Activity "Processing $($Events_count) events from $($computer)..." -Status " " -PercentComplete ($progress_counter/$Events_count * 100)
# convert to XML to better access some nested properties
$EventXml = [xml]$Event.ToXML()
# if the XML has nested property for source IP address, get that, else empty string
# some events do not have a source IP address e.g. logoff
$source_ip = [string]::Empty
If (Get-Member -InputObject $EventXml.Event.UserData.EventXML -Name Address -MemberType Properties) {
$source_ip = $EventXml.Event.UserData.EventXML.Address
}
# put together array of properties from event log, and extract nested
# properties like user and IP
$Result = @{
Computer = $computer
# remove seconds & format event time to unambiguous month format d/MMM/YYYY (because, Aussie here)
# can change date & time format - set to "G" for instance for general date long time using local regional settings
Time = $Event.TimeCreated.toString("d/MMM/yyyy h:mmtt")
"Event ID" = $Event.Id
# get just the first line of the event message
"Desc" = ($Event.Message -split [environment]::NewLine)[0]
# (optional) remove leading domain from username
Username = [string]$EventXml.Event.UserData.EventXML.User.Replace("DOMAIN\", [string]::Empty)
# get display name from AD for the user, pass to "getDisplayNameFromUser" function, remove leading domain
DisplayName = getDisplayNameFromUser([string]$EventXml.Event.UserData.EventXML.User.Replace("DOMAIN\", [string]::Empty))
"Source IP" = $source_ip
}
# include this event in the results collection
# add "event description" property determined by event ID
$Results += (New-Object PSObject -Property $Result) |
Select-Object Computer, Time, Username, DisplayName, "Source IP",
@{ Name = "Event description"; Expression = {
If ($Event.Id -eq "21") { "Logon" }
If ($Event.Id -eq "23") { "Logoff" }
If ($Event.Id -eq "24") { "Disconnected" }
If ($Event.Id -eq "25") { "Reconnection" }
}
}
}
# output subset of results to console, formatted as a table with width adjusted
$Results | Select-Object -First 40 | Format-Table -Property * -AutoSize
} Catch {
$msg = "Error getting events on $computer" + ": " + $error[0].ToString()
Write-Error $msg
}
} Else {
Write-Warning "The computer $computer is not contactable, skipping..."
}
}
# return successfully
# environment error code will equal zero by default
Write-Debug "Done!"
Exit
}
@itzCozza
Copy link

itzCozza commented Aug 4, 2023

script works great, however If anyone gets the error unexpected Token '??' this is due to running an old powershell version, either update or change out line

old code

"Got $($Events.Count ?? 0) events from $computer in " + [math]::Round((New-Timespan -Start $TimerStart -End $(Get-Date)).TotalSeconds, 2) + " seconds" | ForEach-Object { Write-Debug $_ }

new code

$eventsCount = if ($Events.Count) { $Events.Count } else { 0 }
"Got $eventsCount events from $computer in " + [math]::Round((New-Timespan -Start $TimerStart -End $(Get-Date)).TotalSeconds, 2) + " seconds" | ForEach-Object { Write-Debug $_ }

@itzCozza
Copy link

itzCozza commented Aug 4, 2023

complete code that fixs the unexpected token and includes export to csv, change the C:\temp\file.csv on line 103

[CmdletBinding()]
Param(
  [Parameter(Mandatory = $false,
             Position = 0,
             ValueFromPipeline = $true,
             ValueFromPipelineByPropertyName = $true,
             HelpMessage = "Computer or computers to get Remote Desktop history for.")]
  [String[]]$computers
)

Import-Module ActiveDirectory -Verbose:$false

Function getDisplayNameFromUser($userName) {
  Try {
    Return Get-ADUser $userName -ErrorAction SilentlyContinue | Select-Object -ExpandProperty name
  } Catch {
    Return [string]::Empty
  }
}

If ([string]::IsNullOrEmpty($computers)) {
  [string]$input_from_user = Read-Host "Remote Desktop history
Enter computer name (blank to quit)"
  If ($input_from_user.indexOf(",") -gt -1) {
    $computers = $input_from_user -Split ","
  } ElseIf ($input_from_user.indexOf(" ") -gt -1) {
    $computers = $input_from_user -Split " "
  } Else {
    $computers = $input_from_user
  }
}

If ([string]::IsNullOrEmpty($computers)) {
  Write-Output "No computer entered, quitting..."
  Exit
}

$filter = @{
  LogName = "Microsoft-Windows-TerminalServices-LocalSessionManager/Operational"
  StartTime = (Get-Date).AddDays(-180)
  ID = 21, 23, 24, 25
}

$AllResults = @()

ForEach ($computer in $computers) {
  $TimerStart = Get-Date

  If (Test-Connection $computer -Count 1 -Quiet -ErrorAction SilentlyContinue) {
    Try {
      $Results = @()
      $Events = Get-WinEvent -ComputerName $computer -FilterHashtable $filter

      $eventsCount = if ($Events.Count) { $Events.Count } else { 0 }
        "Got $eventsCount events from $computer in " + [math]::Round((New-Timespan -Start $TimerStart -End $(Get-Date)).TotalSeconds, 2) + " seconds" | ForEach-Object { Write-Debug $_ }


      $progress_counter = 0
      $Events_count = $Events.Count

      ForEach ($Event in $Events) {
        $progress_counter++
        Write-Progress -Activity "Processing $($Events_count) events from $($computer)..." -Status " " -PercentComplete ($progress_counter/$Events_count * 100)

        $EventXml = [xml]$Event.ToXML()
        $source_ip = [string]::Empty
        If (Get-Member -InputObject $EventXml.Event.UserData.EventXML -Name Address -MemberType Properties) {
          $source_ip = $EventXml.Event.UserData.EventXML.Address
        }

        $Result = @{
          Computer = $computer
          Time = $Event.TimeCreated.toString("d/MMM/yyyy h:mmtt")
          "Event ID" = $Event.Id
          "Desc" = ($Event.Message -split [environment]::NewLine)[0]
          Username = [string]$EventXml.Event.UserData.EventXML.User.Replace("DOMAIN\", [string]::Empty)
          DisplayName = getDisplayNameFromUser([string]$EventXml.Event.UserData.EventXML.User.Replace("DOMAIN\", [string]::Empty))
          "Source IP" = $source_ip
        }

        $Results += (New-Object PSObject -Property $Result) |
          Select-Object Computer, Time, Username, DisplayName, "Source IP",
            @{ Name = "Event description"; Expression = {
                If ($Event.Id -eq "21") { "Logon" }
                If ($Event.Id -eq "23") { "Logoff" }
                If ($Event.Id -eq "24") { "Disconnected" }
                If ($Event.Id -eq "25") { "Reconnection" }
              }
            }
      }

      $AllResults += $Results

    } Catch {
      $msg = "Error getting events on $computer" + ": " + $error[0].ToString()
      Write-Error $msg
    }
  } Else {
    Write-Warning "The computer $computer is not contactable, skipping..."
  }
}

$OutputFilePath = "C:\temp\file.csv"
$AllResults | Export-Csv -Path $OutputFilePath -NoTypeInformation
Write-Host "Results exported to: $OutputFilePath"

Write-Debug "Done!"
Exit

@pbabbar1122
Copy link

is it not supposed to log the problematic list of computers in the output file as well?
the ones which doesnt have matching events or the systems unreachable.?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment