Finding PowerShell Last Logon by User Logon Event ID

Published:3 December 2021 - 6 min. read

There are many fancy tools out there to monitor user login activity. What if I told you, you didn’t need to spend any money by building a PowerShell last logon and history script? You can find the last logon date and even the user logon event ID with the Windows event log and a little PowerShell!

Not a reader? Watch this related video tutorial!
Not seeing the video? Make sure your ad blocker is disabled.

In this article, you’re going to learn how to build a user activity PowerShell script. This script will pull information from the Windows event log for a local computer and provide a detailed report on user login activity like finding PowerShell last logon.

Meet AD and Windows Server auditing, security, and compliance needs with ManageEngine ADAudit Plus. Download Free Trail!

Prerequisites

If you’re in an AD environment be sure you:

  • are on a domain-joined Windows 10 PC
  • are logged in with an account that can read domain controller event logs
  • have permission to modify domain GPOs

Audit policies to enable login auditing will be set via GPO in this article. But you can use local policies instead.

Enabling Audit Policies

To ensure the event log on the computer records user logins like recording the PowerShell last logon, you must first enable some audit policies. In this article, you’ll learn how to set these policies via GPO. But if you don’t have AD, you can also set these same policies via local policy.

To report on the time users have been logged in, you’ll first need to enable three advanced audit policies.

  • Audit Logoff – When a user is logged off.
  • Audit Logon – When a user authenticates to Windows
  • Audit Other Logon/Logoff Events – Computer lock, unlocks, RDP connects and disconnects

Enabling all of these audit policies ensures you capture all possible activity start and stop times.

When you enable these audit policies on a local PC, the following user logon time event IDs (and logoff IDs) will begin to be recorded in the Windows event logs to enable finding via PowerShell last logon events. Each of these events represents a user activity start and stop time.

  • Logon – 4624
  • Logoff – 4647
  • Startup – 6005
  • RDP Session Reconnect – 4778
  • RDP Session Disconnect – 4779
  • Locked – 4800
  • Unlocked – 4801

You can see an example below of modifying the Default Domain Policy GPO. You’d modify this GPO if enabling these policies on all domain-joined PCs. You may also create your own auditing policy GPO and assign it to various OUs as well.

Enabling the Audit Logoff, Audit Logon and Audit Other Logon/Logoff Events policies
Enabling the Audit Logoff, Audit Logon and Audit Other Logon/Logoff Events policies

Understanding User Logon Sessions and PowerShell Last Logon

Once all of the appropriate events are being generated, you’ve now got to define user login sessions. I’m calling a user session as the total time between when the user begins working and stops; that’s it. To build an accurate report, the script must match up the start and end times to understand these PowerShell last logon sessions.

The concept of a logon session is important because there might be more than one user logging onto a computer. To match up start/stop times with a particular user account, you can use the Logon ID field for each event.

To figure out the start and stop times of a login session, the script finds a session start time and looks back through the event log for the next session stop time with the same Logon ID. Once that event is found (the stop event), the script then knows the user’s total session time.

You can see an example of an event viewer user logon event id (and logoff) with the same Logon ID below.

PowerShell Last Logon : Login event ID in event view
PowerShell Last Logon : Login event ID in event view
Login event ID in event view
Login event ID in event view

In this example, the LAB\Administrator account had logged in (ID 4624) on 8/27/2015 at 5:28PM with a Logon ID of 0x146FF6. By searching earlier in the event log, a session end event (ID 4634) was found with the same Logon ID at 5:30PM on the same day. By now knowing the start time and stop time for this particular login session, you can then deduce that the LAB\Administrator account had been logged on for three minutes or so.

Meet AD and Windows Server auditing, security, and compliance needs with ManageEngine ADAudit Plus. Download Free Trail!

The User Login History Script

Once the policies are enabled and you understand the concept of a login session, you can then start writing some PowerShell to find PowerShell last logon events.

Rather than going over this script line by line, it is provided in its entirety below. You can also download it from this GitHub repo.

In summary, the script below:

  1. Defines all of the important start and stop event ID necessary for PowerShell last logon events.
  2. Creates an XPath query to find appropriate events.
  3. Queries each computer using XPath event log query.
  4. Finds the start event IDs and attempts to match them up to stop event IDs.
  5. Outputs start/end times with other information.

Note: This script may need some tweaks to work 100% correctly. Please issue a GitHub pull request if you notice problems and would like to fix them.

<#	
.SYNOPSIS
	This script finds all PowerShell last logon, logoff and total active session times of all users on all computers specified. For this script
	to function as expected, the advanced AD policies; Audit Logon, Audit Logoff and Audit Other Logon/Logoff Events must be
    enabled and targeted to the appropriate computers via GPO or local policy.
.EXAMPLE
	
.PARAMETER ComputerName
	An array of computer names to search for events on. If this is not provided, the script will search the local computer.
.INPUTS
	None. You cannot pipe objects to Get-ActiveDirectoryUserActivity.ps1.
.OUTPUTS
	None. If successful, this script does not output anything.
#>
[CmdletBinding()]
param
(
    [Parameter()]
    [ValidateNotNullOrEmpty()]
    [string[]]$ComputerName = $Env:COMPUTERNAME
)

try {
	
    #region Defie all of the events to indicate session start or top
    $sessionEvents = @(
        @{ 'Label' = 'Logon'; 'EventType' = 'SessionStart'; 'LogName' = 'Security'; 'ID' = 4624 } ## Advanced Audit Policy --> Audit Logon
        @{ 'Label' = 'Logoff'; 'EventType' = 'SessionStop'; 'LogName' = 'Security'; 'ID' = 4647 } ## Advanced Audit Policy --> Audit Logoff
        @{ 'Label' = 'Startup'; 'EventType' = 'SessionStop'; 'LogName' = 'System'; 'ID' = 6005 }
        @{ 'Label' = 'RdpSessionReconnect'; 'EventType' = 'SessionStart'; 'LogName' = 'Security'; 'ID' = 4778 } ## Advanced Audit Policy --> Audit Other Logon/Logoff Events
        @{ 'Label' = 'RdpSessionDisconnect'; 'EventType' = 'SessionStop'; 'LogName' = 'Security'; 'ID' = 4779 } ## Advanced Audit Policy --> Audit Other Logon/Logoff Events
        @{ 'Label' = 'Locked'; 'EventType' = 'SessionStop'; 'LogName' = 'Security'; 'ID' = 4800 } ## Advanced Audit Policy --> Audit Other Logon/Logoff Events
        @{ 'Label' = 'Unlocked'; 'EventType' = 'SessionStart'; 'LogName' = 'Security'; 'ID' = 4801 } ## Advanced Audit Policy --> Audit Other Logon/Logoff Events
    )
    
    ## All of the IDs that designate when user activity starts
    $sessionStartIds = ($sessionEvents | where { $_.EventType -eq 'SessionStart' }).ID
    ## All of the IDs that designate when user activity stops
    $sessionStopIds = ($sessionEvents | where { $_.EventType -eq 'SessionStop' }).ID
    #endregion
	
    ## Define all of the log names we'll be querying
    $logNames = ($sessionEvents.LogName | select -Unique)
    ## Grab all of the interesting IDs we'll be looking for
    $ids = $sessionEvents.Id
		
    ## Build the insane XPath query for the security event log in order to query PowerShell last logon events and others as fast as possible
    $logonXPath = "Event[System[EventID=4624]] and Event[EventData[Data[@Name='TargetDomainName'] != 'Window Manager']] and Event[EventData[Data[@Name='TargetDomainName'] != 'NT AUTHORITY']] and (Event[EventData[Data[@Name='LogonType'] = '2']] or Event[EventData[Data[@Name='LogonType'] = '10']])"
    $otherXpath = 'Event[System[({0})]]' -f "EventID=$(($ids.where({ $_ -ne '4624' })) -join ' or EventID=')"
    $xPath = '({0}) or ({1})' -f $logonXPath, $otherXpath

    foreach ($computer in $ComputerName) {
        ## Query each computer's event logs using the Xpath filter
        $events = Get-WinEvent -ComputerName $computer -LogName $logNames -FilterXPath $xPath
        Write-Verbose -Message "Found [$($events.Count)] events to look through"

        ## Set up the output object
        $output = [ordered]@{
            'ComputerName'          = $computer
            'Username'              = $null
            'StartTime'             = $null
            'StartAction'           = $null
            'StopTime'              = $null
            'StopAction'            = $null
            'Session Active (Days)' = $null
            'Session Active (Min)'  = $null
        }
        
        ## Need current users because if no stop time, they're still probably logged in
        $getGimInstanceParams = @{
            ClassName = 'Win32_ComputerSystem'
        }
        if ($computer -ne $Env:COMPUTERNAME) {
            $getGimInstanceParams.ComputerName = $computer
        }
        $loggedInUsers = Get-CimInstance @getGimInstanceParams | Select-Object -ExpandProperty UserName | foreach { $_.split('\')[1] }
            
        ## Find all user start activity events and begin parsing
        $events.where({ $_.Id -in $sessionStartIds }).foreach({
                try {
                    $logonEvtId = $_.Id
                    $output.StartAction = $sessionEvents.where({ $_.ID -eq $logonEvtId }).Label
                    $xEvt = [xml]$_.ToXml()

                    ## Figure out the login session ID
                    $output.Username = ($xEvt.Event.EventData.Data | where { $_.Name -eq 'TargetUserName' }).'#text'
                    $logonId = ($xEvt.Event.EventData.Data | where { $_.Name -eq 'TargetLogonId' }).'#text'
                    if (-not $logonId) {
                        $logonId = ($xEvt.Event.EventData.Data | where { $_.Name -eq 'LogonId' }).'#text'
                    }
                    $output.StartTime = $_.TimeCreated
        
                    Write-Verbose -Message "New session start event found: event ID [$($logonEvtId)] username [$($output.Username)] logonID [$($logonId)] time [$($output.StartTime)]"
                    ## Try to match up the user activity end event with the start event we're processing
                    if (-not ($sessionEndEvent = $Events.where({ ## If a user activity end event could not be found, assume the user is still logged on
                                    $_.TimeCreated -gt $output.StartTime -and
                                    $_.ID -in $sessionStopIds -and
                                    (([xml]$_.ToXml()).Event.EventData.Data | where { $_.Name -eq 'TargetLogonId' }).'#text' -eq $logonId
                                })) | select -last 1) {
                        if ($output.UserName -in $loggedInUsers) {
                            $output.StopTime = Get-Date
                            $output.StopAction = 'Still logged in'
                        } else {
                            throw "Could not find a session end event for logon ID [$($logonId)]."
                        }
                    } else {
                        ## Capture the user activity end time
                        $output.StopTime = $sessionEndEvent.TimeCreated
                        Write-Verbose -Message "Session stop ID is [$($sessionEndEvent.Id)]"
                        $output.StopAction = $sessionEvents.where({ $_.ID -eq $sessionEndEvent.Id }).Label
                    }

                    $sessionTimespan = New-TimeSpan -Start $output.StartTime -End $output.StopTime
                    $output.'Session Active (Days)' = [math]::Round($sessionTimespan.TotalDays, 2)
                    $output.'Session Active (Min)'  = [math]::Round($sessionTimespan.TotalMinutes, 2)
                    
                    [pscustomobject]$output
                } catch {
                    Write-Warning -Message $_.Exception.Message
                }
            })
    }
} catch {
    $PSCmdlet.ThrowTerminatingError($_)
}

Hate ads? Want to support the writer? Get many of our tutorials packaged as an ATA Guidebook.

Explore ATA Guidebooks

Looks like you're offline!