Tracking User Lock, Unlock, and Sleep Events with PowerShell

Auditing External File Sharing in Microsoft 365 with PowerShell
Auditing External File Sharing in Microsoft 365 with PowerShell

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 IDLogMeaning
4800SecuritySession locked
4801SecuritySession unlocked
4802SecurityScreen saver started
4803SecurityScreen saver dismissed
4779SecurityUser session reconnected
4647SecurityUser initiated logoff
42SystemSystem entered sleep/hibernate
107SystemSystem 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
guest

0 Comments
Inline Feedbacks
View all comments