Public/system/Get-RebootHistory.ps1
|
#Requires -Version 5.1 function Get-RebootHistory { <# .SYNOPSIS Reconstructs reboot and shutdown history from the Windows System event log .DESCRIPTION Correlates Windows System event log entries (1074, 1076, 6005, 6006, 6008 and Kernel-Power 41) to rebuild each reboot or shutdown for one or more computers. Each event is classified as Planned, Unexpected, Crash, PowerLoss or Unknown, with its cause, initiator and comment, and the downtime duration between a clean stop and the following boot. Local and remote targets are dispatched through Invoke-RemoteOrLocal. .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 Number of reboot/shutdown events to return, newest first. Valid range is 1 to 10000. Defaults to 50. .PARAMETER After Only return events at or after this datetime. When omitted, no lower bound is applied to the query. .PARAMETER Before Only return events at or before this datetime. When omitted, no upper bound is applied to the query. .EXAMPLE Get-RebootHistory Returns the 50 most recent reboot and shutdown records for the local machine. .EXAMPLE Get-RebootHistory -ComputerName 'SRV01' -MaxEvents 20 Returns the 20 most recent reboot records for SRV01 via WinRM. .EXAMPLE 'SRV01', 'SRV02' | Get-RebootHistory -After (Get-Date).AddDays(-30) Returns all reboots in the last 30 days for SRV01 and SRV02 via pipeline. .OUTPUTS PSWinOps.RebootHistory One object per boot event, enriched with shutdown type, cause, initiator, comment and downtime duration. .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-06-23 Requires: PowerShell 5.1+ / Windows only Requires: WinRM enabled on target machines for remote queries .LINK https://github.com/k9fr4n/PSWinOps #> [CmdletBinding()] [OutputType('PSWinOps.RebootHistory')] 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)] [datetime]$After, [Parameter(Mandatory = $false)] [datetime]$Before ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting reboot history query" $hasAfter = $PSBoundParameters.ContainsKey('After') $hasBefore = $PSBoundParameters.ContainsKey('Before') $afterVal = if ($hasAfter) { $After } else { [datetime]::MinValue } $beforeVal = if ($hasBefore) { $Before } else { [datetime]::MaxValue } $scriptBlock = { param( [int]$MaxEvts, [datetime]$AfterDate, [datetime]$BeforeDate, [bool]$HasAfter, [bool]$HasBefore ) # Build filter for System log events (1074, 1076, 6005, 6006, 6008) $sysFilter = @{ LogName = 'System' Id = @(1074, 1076, 6005, 6006, 6008) } if ($HasAfter) { $sysFilter['StartTime'] = $AfterDate } if ($HasBefore) { $sysFilter['EndTime'] = $BeforeDate } # Build filter for Kernel-Power 41 (crash / power loss) $kpFilter = @{ LogName = 'System' ProviderName = 'Microsoft-Windows-Kernel-Power' Id = 41 } if ($HasAfter) { $kpFilter['StartTime'] = $AfterDate } if ($HasBefore) { $kpFilter['EndTime'] = $BeforeDate } $sysEvents = @(Get-WinEvent -FilterHashtable $sysFilter -ErrorAction SilentlyContinue) $kpEvents = @(Get-WinEvent -FilterHashtable $kpFilter -ErrorAction SilentlyContinue) $allEvents = ($sysEvents + $kpEvents) | Sort-Object TimeCreated $bootEvents = @($allEvents | Where-Object { $_.Id -eq 6005 }) $cleanStops = @($allEvents | Where-Object { $_.Id -eq 6006 }) $dirtyStops = @($allEvents | Where-Object { $_.Id -eq 6008 }) $crashEvents = @($allEvents | Where-Object { $_.Id -eq 41 }) $plannedShuts = @($allEvents | Where-Object { $_.Id -eq 1074 }) $reasonRecs = @($allEvents | Where-Object { $_.Id -eq 1076 }) $results = [System.Collections.Generic.List[psobject]]::new() foreach ($boot in ($bootEvents | Sort-Object TimeCreated -Descending)) { if ($results.Count -ge $MaxEvts) { break } $bootTime = $boot.TimeCreated $shutdownTime = $null $downtimeMinutes = $null $type = 'Unknown' $cause = '' $initiator = '' $comment = '' $eventId = 6005 # Determine the search window lower bound: after the previous boot $prevBoot = $bootEvents | Where-Object { $_.TimeCreated -lt $bootTime } | Sort-Object TimeCreated -Descending | Select-Object -First 1 $windowStart = if ($null -ne $prevBoot) { $prevBoot.TimeCreated } else { $bootTime.AddDays(-365) } # Check for 6008 (dirty shutdown marker logged at boot time) $dirty6008 = $dirtyStops | Where-Object { [math]::Abs(($_.TimeCreated - $bootTime).TotalMinutes) -le 5 } | Sort-Object TimeCreated | Select-Object -First 1 # Look for a Kernel-Power 41 just before this boot $precedingCrash = $crashEvents | Where-Object { $_.TimeCreated -lt $bootTime -and $_.TimeCreated -gt $windowStart } | Sort-Object TimeCreated -Descending | Select-Object -First 1 # Look for a clean shutdown (6006) just before this boot $precedingClean = $cleanStops | Where-Object { $_.TimeCreated -lt $bootTime -and $_.TimeCreated -gt $windowStart } | Sort-Object TimeCreated -Descending | Select-Object -First 1 if ($null -ne $dirty6008) { # Unexpected shutdown recorded at boot time by 6008 $type = 'Unexpected' $eventId = 6008 # 6008 Properties[0] = time string, Properties[1] = date string try { if ($dirty6008.Properties.Count -ge 2) { $timeStr = [string]$dirty6008.Properties[0].Value $dateStr = [string]$dirty6008.Properties[1].Value $parsed = [datetime]::Parse("$dateStr $timeStr") $shutdownTime = $parsed $downtimeMinutes = [math]::Round(($bootTime - $shutdownTime).TotalMinutes, 2) } } catch { Write-Verbose "Could not parse 6008 shutdown time: $_" } # Look for 1076 (operator-supplied reason recorded after this boot) $reason1076 = $reasonRecs | Where-Object { $_.TimeCreated -ge $bootTime -and $_.TimeCreated -le $bootTime.AddHours(2) } | Sort-Object TimeCreated | Select-Object -First 1 if ($null -ne $reason1076) { try { $cause = if ($reason1076.Properties.Count -ge 2) { [string]$reason1076.Properties[1].Value } else { '' } $initiator = if ($reason1076.Properties.Count -ge 5) { [string]$reason1076.Properties[4].Value } else { '' } $comment = if ($reason1076.Properties.Count -ge 7) { [string]$reason1076.Properties[6].Value } else { '' } } catch { Write-Verbose "Could not parse 1076 properties: $_" } } } elseif ($null -ne $precedingCrash) { # Kernel-Power 41: crash (BugcheckCode != 0) or power loss (== 0) $shutdownTime = $precedingCrash.TimeCreated $downtimeMinutes = [math]::Round(($bootTime - $shutdownTime).TotalMinutes, 2) $eventId = 41 try { $bugcheck = [uint32]$precedingCrash.Properties[0].Value if ($bugcheck -ne 0) { $type = 'Crash' $cause = 'BugcheckCode: 0x{0:X8}' -f $bugcheck } else { $type = 'PowerLoss' } } catch { $type = 'Crash' } } elseif ($null -ne $precedingClean) { # Clean shutdown (6006), classified as Planned $shutdownTime = $precedingClean.TimeCreated $downtimeMinutes = [math]::Round(($bootTime - $shutdownTime).TotalMinutes, 2) $type = 'Planned' $eventId = 6006 # Enrich from 1074 near the 6006 time $plan1074 = $plannedShuts | Where-Object { $_.TimeCreated -le $shutdownTime.AddMinutes(2) -and $_.TimeCreated -ge $shutdownTime.AddMinutes(-10) } | Sort-Object TimeCreated -Descending | Select-Object -First 1 if ($null -ne $plan1074) { $eventId = 1074 try { $initiator = if ($plan1074.Properties.Count -ge 7) { [string]$plan1074.Properties[6].Value } else { '' } $cause = if ($plan1074.Properties.Count -ge 3) { [string]$plan1074.Properties[2].Value } else { '' } $comment = if ($plan1074.Properties.Count -ge 9) { [string]$plan1074.Properties[8].Value } else { '' } } catch { Write-Verbose "Could not parse 1074 properties: $_" } } } $results.Add([PSCustomObject]@{ PSTypeName = 'PSWinOps.RebootHistory' ComputerName = $env:COMPUTERNAME ShutdownTime = if ($null -ne $shutdownTime) { $shutdownTime.ToString('yyyy-MM-dd HH:mm:ss') } else { $null } BootTime = $bootTime.ToString('yyyy-MM-dd HH:mm:ss') DowntimeMinutes = $downtimeMinutes Type = $type Cause = $cause Initiator = $initiator Comment = $comment EventId = $eventId Timestamp = (Get-Date -Format 'yyyy-MM-dd HH:mm:ss') }) } $results } } process { foreach ($targetComputer in $ComputerName) { try { Write-Verbose "[$($MyInvocation.MyCommand)] Querying reboot history on '$targetComputer'" Invoke-RemoteOrLocal -ComputerName $targetComputer -Credential $Credential ` -ScriptBlock $scriptBlock ` -ArgumentList @($MaxEvents, $afterVal, $beforeVal, $hasAfter, $hasBefore) } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_" } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed reboot history query" } } |