
A user reports their workstation keeps going to sleep “on its own.” Or someone claims they never left their desk, yet the session clearly disconnected. Answering these questions means digging through the Windows event log and correlating events across Security and System logs into one timeline.
This script pulls seven distinct event IDs from two different logs, merges them chronologically, and outputs a readable sequence of what happened and when.
The Events That Matter
Windows logs lock/unlock/sleep activity across two providers. You need both to get the full picture:
| Event ID | Log | Meaning |
|---|---|---|
| 4800 | Security | Session locked |
| 4801 | Security | Session unlocked |
| 4802 | Security | Screen saver started |
| 4803 | Security | Screen saver dismissed |
| 4779 | Security | User session reconnected |
| 4647 | Security | User initiated logoff |
| 42 | System | System entered sleep/hibernate |
| 107 | System | System awoken |
Events 4800 and 4801 are the most commonly referenced, but they only tell half the story. Event 42 and 107 from the System log fill in the sleep/resume gap, and 4802/4803 show when the screen saver kicked in (which often means inactivity long before the lock).
Pulling Events from Both Logs
The script uses Get-WinEvent twice (once per log) with a filter hashtable to limit the query to the relevant IDs and a time window:
$startTime = (Get-Date).AddDays(-3)
$securityEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
ID = 4800, 4801, 4802, 4803, 4779, 4647
StartTime = $startTime
}
$systemEvents = Get-WinEvent -FilterHashtable @{
LogName = 'System'
ID = 42, 107
StartTime = $startTime
}
The filter hashtable runs at the OS level. Windows filters the events before returning them, not after. This is significantly faster than piping through Where-Object, especially on logs with millions of entries. Adjust the -3 to whatever window you need.
Merging and Sorting
Both calls return System.Diagnostics.Eventing.Reader.EventLogRecord objects, which share a TimeCreated property. That makes merging straightforward:
$allEvents = ($securityEvents + $systemEvents) | Sort-Object TimeCreated
The + operator concatenates the arrays, and Sort-Object orders everything chronologically regardless of which log it came from.
Translating Event IDs to Human Language
A switch statement converts each numeric ID to a readable description:
foreach ($e in $allEvents) {
$msg = switch ($e.Id) {
4800 { "Locked" }
4801 { "Unlocked" }
4802 { "Screensaver started" }
4803 { "Screensaver dismissed" }
4779 { "Session disconnected" }
4647 { "User initiated logoff" }
42 { "System entered sleep" }
107 { "System resumed from sleep" }
default { "Unknown" }
}
[PSCustomObject]@{
Time = $e.TimeCreated
EventID = $e.Id
Source = $e.ProviderName
Message = $msg
}
}
The output is a clean timeline like:
Time EventID Source Message
---- ------- ------ -------
04/24/2026 08:31 107 Kernel-Power System resumed from sleep
04/24/2026 08:32 4801 Microsoft-Windows-Security-Auditing Unlocked
04/24/2026 12:04 4800 Microsoft-Windows-Security-Auditing Locked
04/24/2026 17:15 4800 Microsoft-Windows-Security-Auditing Locked
04/24/2026 17:45 42 Kernel-Power System entered sleep
04/24/2026 18:02 107 Kernel-Power System resumed from sleep
04/24/2026 18:03 4801 Microsoft-Windows-Security-Auditing Unlocked
The Full Script
$startTime = (Get-Date).AddDays(-3)
$securityEvents = Get-WinEvent -FilterHashtable @{
LogName = 'Security'
ID = 4800, 4801, 4802, 4803, 4779, 4647
StartTime = $startTime
}
$systemEvents = Get-WinEvent -FilterHashtable @{
LogName = 'System'
ID = 42, 107
StartTime = $startTime
}
$allEvents = ($securityEvents + $systemEvents) | Sort-Object TimeCreated
foreach ($e in $allEvents) {
$msg = switch ($e.Id) {
4800 { "Locked" }
4801 { "Unlocked" }
4802 { "Screensaver started" }
4803 { "Screensaver dismissed" }
4779 { "Session disconnected" }
4647 { "User initiated logoff" }
42 { "System entered sleep" }
107 { "System resumed from sleep" }
default { "Unknown" }
}
[PSCustomObject]@{
Time = $e.TimeCreated
EventID = $e.Id
Source = $e.ProviderName
Message = $msg
}
}
I’ve also put this script on GitHub as a gist here.
Common Patterns You’ll See
Screen saver lock, not user lock: If you see event 4802 (screensaver started) followed by 4800 (locked) with no 4801 in between, the screensaver lock timer fired (the user didn’t press Win+L).
Unexpected sleep: Event 42 with no preceding 4800 or 4647 means the system went to sleep without the user locking or logging off first. Usually points to a power plan setting or a group policy override.
RDC connection dropped: Event 4779 without a matching 4647 means a Remote Desktop session disconnected rather than the user logging off cleanly.
Running Remotely
Both Get-WinEvent calls support the -ComputerName parameter. To run against a remote machine, add it to each call:
$securityEvents = Get-WinEvent -ComputerName $targetMachine -FilterHashtable @{}
$systemEvents = Get-WinEvent -ComputerName $targetMachine -FilterHashtable @{}
You’ll need Event Log Reader permissions on the target machine, which standard domain users have by default. If you get access denied, the target might have restricted event log access via GPO.
When to Use This
- A user claims their PC “keeps locking by itself” (show them the actual timeline)
- Troubleshooting why RDP sessions keep dropping
- Investigating whether a machine actually went to sleep or the screen just timed out
- Building an activity audit for a shared workstation





