Public/eventlog/Get-UnexpectedShutdown.ps1

#Requires -Version 5.1

function Get-UnexpectedShutdown {
    <#
    .SYNOPSIS
        Reports shutdown and restart events with cause, expectedness and initiator
 
    .DESCRIPTION
        Correlates Windows System event log entries (6008 dirty shutdown, Kernel-Power
        41 unexpected, 1074 planned shutdown/restart, 6006 clean shutdown, 1076 reason
        supplied) to report each shutdown or restart with its cause, whether it was
        expected or unexpected, the initiating process/user and the reason code.
        Local and remote targets are dispatched through Invoke-RemoteOrLocal; machines
        with no matching events return nothing.
 
    .PARAMETER ComputerName
        One or more computer names to target. Defaults to the local computer.
        Accepts pipeline input by value and by property name.
 
    .PARAMETER Credential
        Optional PSCredential for authenticating to remote machines. Ignored for
        local machine queries.
 
    .PARAMETER MaxEvents
        Maximum number of shutdown/restart rows returned per machine, newest first.
        Valid range is 1 to 10000. Defaults to 50.
 
    .PARAMETER Days
        Look-back window in days used to bound the event log scan. StartTime is
        computed as (Get-Date).AddDays(-Days). Valid range is 1 to 3650. Defaults to 30.
 
    .EXAMPLE
        Get-UnexpectedShutdown
 
        Returns the most recent shutdown/restart events for the local machine over
        the last 30 days.
 
    .EXAMPLE
        Get-UnexpectedShutdown -ComputerName SRV01 -Days 90
 
        Returns shutdown/restart events from SRV01 over the last 90 days via WinRM.
 
    .EXAMPLE
        'SRV01','SRV02' | Get-UnexpectedShutdown -MaxEvents 20
 
        Returns up to 20 shutdown/restart events per machine for SRV01 and SRV02 via
        pipeline.
 
    .OUTPUTS
        PSWinOps.UnexpectedShutdown
        One object per shutdown/restart-related event (6008, 41, 1074, 6006, 1076),
        newest first, with cause, expectedness, initiator, reason code and comment.
 
    .NOTES
        Author: Franck SALLET
        Version: 1.0.0
        Last Modified: 2026-07-05
        Requires: PowerShell 5.1+ / Windows only
        Requires: WinRM enabled on target machines for remote queries
 
    .LINK
        https://github.com/k9fr4n/PSWinOps
 
    .LINK
        Source of truth: Windows System event log (provider EventLog for 6008/6006/1074/1076; provider Microsoft-Windows-Kernel-Power for 41)
    #>

    [CmdletBinding(SupportsShouldProcess = $false, ConfirmImpact = 'None')]
    [OutputType('PSWinOps.UnexpectedShutdown')]
    param(
        [Parameter(Mandatory = $false, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
        [ValidateNotNullOrEmpty()]
        [Alias('CN', 'Name', 'DNSHostName')]
        [string[]]$ComputerName = $env:COMPUTERNAME,

        [Parameter(Mandatory = $false)]
        [System.Management.Automation.PSCredential]$Credential,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 10000)]
        [int]$MaxEvents = 50,

        [Parameter(Mandatory = $false)]
        [ValidateRange(1, 3650)]
        [int]$Days = 30
    )

    begin {
        Write-Verbose "[$($MyInvocation.MyCommand)] Starting unexpected shutdown query"

        $startTime = (Get-Date).AddDays(-$Days)

        $scriptBlock = {
            param(
                [int]$MaxEvts,
                [datetime]$ScanStartTime
            )

            # Build filter for System log events (6008, 1074, 6006, 1076)
            $sysFilter = @{
                LogName   = 'System'
                Id        = @(6008, 1074, 6006, 1076)
                StartTime = $ScanStartTime
            }

            # Build filter for Kernel-Power 41 (crash / power loss)
            $kpFilter = @{
                LogName      = 'System'
                ProviderName = 'Microsoft-Windows-Kernel-Power'
                Id           = 41
                StartTime    = $ScanStartTime
            }

            $sysEvents = @(Get-WinEvent -FilterHashtable $sysFilter -ErrorAction SilentlyContinue)
            $kpEvents  = @(Get-WinEvent -FilterHashtable $kpFilter  -ErrorAction SilentlyContinue)
            $allEvents = @(($sysEvents + $kpEvents) | Sort-Object TimeCreated -Descending)

            $results = [System.Collections.Generic.List[psobject]]::new()

            foreach ($evt in $allEvents) {
                if ($results.Count -ge $MaxEvts) { break }

                $shutdownType = 'Unexpected'
                $isExpected   = $false
                $cause        = ''
                $reasonCode   = ''
                $initiator    = ''
                $comment      = ''

                switch ($evt.Id) {
                    6008 {
                        # Dirty/unexpected shutdown; no per-event cause available beyond the marker itself.
                        $shutdownType = 'Unexpected'
                        $isExpected   = $false
                    }
                    41 {
                        # Kernel-Power 41: BugcheckCode 0 => PowerLoss, non-zero => Crash
                        $isExpected = $false
                        try {
                            $bugcheck = [uint32]$evt.Properties[0].Value
                            if ($bugcheck -ne 0) {
                                $shutdownType = 'Crash'
                                $cause        = 'BugcheckCode: 0x{0:X8}' -f $bugcheck
                            } else {
                                $shutdownType = 'PowerLoss'
                            }
                        } catch {
                            $shutdownType = 'Crash'
                            Write-Verbose "Could not parse Kernel-Power 41 BugcheckCode: $_"
                        }
                    }
                    1074 {
                        # Planned shutdown/restart: reason text, reason code, initiator, comment
                        $shutdownType = 'Planned'
                        $isExpected   = $true
                        try {
                            $cause      = if ($evt.Properties.Count -ge 3) { [string]$evt.Properties[2].Value } else { '' }
                            $reasonCode = if ($evt.Properties.Count -ge 5) { [string]$evt.Properties[4].Value } else { '' }
                            $initiator  = if ($evt.Properties.Count -ge 7) { [string]$evt.Properties[6].Value } else { '' }
                            $comment    = if ($evt.Properties.Count -ge 9) { [string]$evt.Properties[8].Value } else { '' }
                        } catch { Write-Verbose "Could not parse 1074 properties: $_" }
                    }
                    6006 {
                        # Clean, service-controlled shutdown
                        $shutdownType = 'Clean'
                        $isExpected   = $true
                    }
                    1076 {
                        # Operator-supplied reason after a prior unexpected shutdown
                        $shutdownType = 'ReasonSupplied'
                        $isExpected   = $false
                        try {
                            $cause     = if ($evt.Properties.Count -ge 2) { [string]$evt.Properties[1].Value } else { '' }
                            $initiator = if ($evt.Properties.Count -ge 5) { [string]$evt.Properties[4].Value } else { '' }
                        } catch { Write-Verbose "Could not parse 1076 properties: $_" }
                    }
                }

                $results.Add([PSCustomObject]@{
                    PSTypeName   = 'PSWinOps.UnexpectedShutdown'
                    ComputerName = $env:COMPUTERNAME
                    EventTime    = $evt.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss')
                    EventId      = $evt.Id
                    ShutdownType = $shutdownType
                    IsExpected   = $isExpected
                    Cause        = $cause
                    ReasonCode   = $reasonCode
                    Initiator    = $initiator
                    Comment      = $comment
                    Timestamp    = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
                })
            }

            $results
        }
    }

    process {
        foreach ($targetComputer in $ComputerName) {
            try {
                Write-Verbose "[$($MyInvocation.MyCommand)] Querying unexpected shutdowns on '$targetComputer'"
                Invoke-RemoteOrLocal -ComputerName $targetComputer -Credential $Credential `
                    -ScriptBlock $scriptBlock `
                    -ArgumentList @($MaxEvents, $startTime)
            } catch {
                Write-Error "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_"
            }
        }
    }

    end {
        Write-Verbose "[$($MyInvocation.MyCommand)] Completed unexpected shutdown query"
    }
}