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!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.
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.
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:
- Defines all of the important start and stop event ID necessary for PowerShell last logon events.
- Creates an XPath query to find appropriate events.
- Queries each computer using XPath event log query.
- Finds the start event IDs and attempts to match them up to stop event IDs.
- 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($_)
}