Public/eventlog/Get-ProcessCrashEvent.ps1
|
#Requires -Version 5.1 function Get-ProcessCrashEvent { <# .SYNOPSIS Report application crashes and hangs from the Windows Application log .DESCRIPTION Queries the Application event log for Application Error, Windows Error Reporting, and optionally Application Hang events over a configurable look-back window. Named EventData fields are parsed from event XML so localized message text and unstable Properties indexes do not affect the core crash details. Windows Error Reporting events enrich crash correlation through their report ID but never produce a second output row. Results are aggregated by executable name before the optional process filter is applied, then returned newest first for each machine. .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 Days Look-back window in days. Valid values are 1 through 3650. Defaults to 7. .PARAMETER MaxEvents Maximum number of Application log events read per machine. The event query is newest first and may truncate the requested window when the limit is reached. Valid values are 1 through 10000. Defaults to 200. .PARAMETER ProcessName Optional executable name filter without a path. Matching is case-insensitive and is applied after CrashCount has been calculated over all parsed crash and hang rows. .PARAMETER IncludeHang Include Application Hang event 1002 rows in addition to Application Error 1000 crash rows. Windows Error Reporting event 1001 is used only for correlation. .EXAMPLE Get-ProcessCrashEvent Returns application crash events for the local computer from the last seven days. .EXAMPLE Get-ProcessCrashEvent -ComputerName 'SRV01' -Days 30 -ProcessName 'w3wp.exe' Returns IIS worker-process crashes from SRV01 over the last 30 days. .EXAMPLE 'SRV01', 'SRV02' | Get-ProcessCrashEvent -MaxEvents 500 -IncludeHang Returns crash and hang events from multiple computers through pipeline input. .OUTPUTS PSWinOps.ProcessCrashEvent One object per Application Error 1000 crash and, when requested, Application Hang 1002 event. Windows Error Reporting 1001 events are correlated and do not emit rows. .NOTES Author: Franck SALLET Version: 1.0.0 Last Modified: 2026-09-04 Requires: PowerShell 5.1+ / Windows only Requires: Event Log Readers membership for remote or protected Application log access Requires: WinRM enabled on target machines for remote queries .LINK https://github.com/k9fr4n/PSWinOps .LINK https://learn.microsoft.com/en-us/windows/win32/eventlog/event-logging #> [CmdletBinding(SupportsShouldProcess = $false, ConfirmImpact = 'None')] [OutputType('PSWinOps.ProcessCrashEvent')] 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, 3650)] [int]$Days = 7, [Parameter(Mandatory = $false)] [ValidateRange(1, 10000)] [int]$MaxEvents = 200, [Parameter(Mandatory = $false)] [string]$ProcessName = '', [Parameter(Mandatory = $false)] [switch]$IncludeHang ) begin { Write-Verbose "[$($MyInvocation.MyCommand)] Starting process crash event query" $startTime = (Get-Date).AddDays(-$Days) $scriptBlock = { param( [datetime]$ScanStartTime, [int]$MaxEvts, [string]$ProcessNameFilter, [bool]$IncludeHangEvents ) $eventIds = @(1000, 1001) if ($IncludeHangEvents) { $eventIds += 1002 } $filter = @{ LogName = 'Application' Id = $eventIds StartTime = $ScanStartTime } $eventRecords = @() try { $eventRecords = @(Get-WinEvent -FilterHashtable $filter -MaxEvents $MaxEvts -ErrorAction Stop) } catch { if ($_.Exception.Message -notmatch 'No events were found') { throw } } if ($eventRecords.Count -eq 0) { return } $eventRecords = @($eventRecords | Sort-Object -Property TimeCreated -Descending) function Get-EventDataMap { param( [Parameter(Mandatory = $true)] [object]$Event ) $map = @{} try { $xml = [xml]$Event.ToXml() $dataNodes = @($xml.SelectNodes("//*[local-name()='Data']")) foreach ($node in $dataNodes) { $name = [string]$node.GetAttribute('Name') if ([string]::IsNullOrWhiteSpace($name)) { continue } $value = [string]$node.InnerText if (-not $map.ContainsKey($name) -or [string]::IsNullOrEmpty([string]$map[$name])) { $map[$name] = $value } } } catch { Write-Verbose "Could not parse event $($Event.Id) as XML: $_" } return $map } function Get-EventField { param( [Parameter(Mandatory = $true)] [hashtable]$Data, [Parameter(Mandatory = $true)] [string[]]$Names ) foreach ($name in $Names) { if ($Data.ContainsKey($name) -and -not [string]::IsNullOrEmpty([string]$Data[$name])) { return [string]$Data[$name] } } return '' } function Get-ExecutableName { param( [Parameter(Mandatory = $true)] [hashtable]$Data ) $value = Get-EventField -Data $Data -Names @( 'AppName', 'ProcessName', 'ApplicationName', 'ExeFileName', 'Parameter1' ) if ([string]::IsNullOrWhiteSpace($value)) { return '' } try { return [System.IO.Path]::GetFileName($value) } catch { return $value } } function Get-UserName { param( [Parameter(Mandatory = $true)] [object]$Event, [Parameter(Mandatory = $true)] [hashtable]$Data ) $userName = Get-EventField -Data $Data -Names @('UserName', 'User') if (-not [string]::IsNullOrEmpty($userName)) { return $userName } if ($null -ne $Event.UserId) { try { return $Event.UserId.Translate([System.Security.Principal.NTAccount]).Value } catch { Write-Verbose "Could not translate event user SID: $_" } } return '' } $werByReportId = @{} foreach ($eventRecord in $eventRecords) { if ([int]$eventRecord.Id -ne 1001) { continue } $data = Get-EventDataMap -Event $eventRecord $reportId = Get-EventField -Data $data -Names @('ReportId', 'ReportID', 'IntegratorReportId') if (-not [string]::IsNullOrWhiteSpace($reportId)) { $werByReportId[$reportId.ToLowerInvariant()] = [PSCustomObject]@{ ReportId = $reportId ProcessName = Get-ExecutableName -Data $data TimeCreated = $eventRecord.TimeCreated } } } $rows = [System.Collections.Generic.List[psobject]]::new() foreach ($eventRecord in $eventRecords) { $eventRecordId = [int]$eventRecord.Id if ($eventRecordId -eq 1001 -or ($eventRecordId -eq 1002 -and -not $IncludeHangEvents)) { continue } $data = Get-EventDataMap -Event $eventRecord $eventRecordType = if ($eventRecordId -eq 1002) { 'Hang' } else { 'Crash' } $processName = Get-ExecutableName -Data $data $processPath = Get-EventField -Data $data -Names @('AppPath', 'ProcessPath', 'ExePath') $faultingModule = Get-EventField -Data $data -Names @('ModuleName', 'FaultingModule', 'FaultingModuleName') $exceptionCode = Get-EventField -Data $data -Names @('ExceptionCode', 'Exception') $faultingOffset = Get-EventField -Data $data -Names @('FaultingOffset', 'Offset') $reportId = Get-EventField -Data $data -Names @('IntegratorReportId', 'ReportId', 'ReportID') if (-not [string]::IsNullOrWhiteSpace($reportId) -and $werByReportId.ContainsKey($reportId.ToLowerInvariant())) { $reportId = $werByReportId[$reportId.ToLowerInvariant()].ReportId } if ([string]::IsNullOrWhiteSpace($reportId)) { $matchingWer = @($werByReportId.Values | Where-Object { -not [string]::IsNullOrWhiteSpace($processName) -and $_.ProcessName -ieq $processName -and $null -ne $_.TimeCreated -and $null -ne $eventRecord.TimeCreated -and [math]::Abs(($_.TimeCreated - $eventRecord.TimeCreated).TotalMinutes) -le 5 } | Sort-Object -Property TimeCreated | Select-Object -First 1) if ($matchingWer.Count -gt 0) { $reportId = $matchingWer[0].ReportId } } $rows.Add([PSCustomObject]@{ EventTime = $eventRecord.TimeCreated EventId = $eventRecordId EventType = $eventRecordType ProcessName = $processName ProcessPath = $processPath FaultingModule = $faultingModule ExceptionCode = $exceptionCode FaultingOffset = $faultingOffset ReportId = $reportId UserName = Get-UserName -Event $eventRecord -Data $data Message = [string]$eventRecord.Message }) } $crashTally = @{} foreach ($row in $rows) { $key = ([string]$row.ProcessName).ToLowerInvariant() if ($crashTally.ContainsKey($key)) { $crashTally[$key]++ } else { $crashTally[$key] = 1 } } foreach ($row in ($rows | Sort-Object -Property EventTime -Descending)) { if (-not [string]::IsNullOrWhiteSpace($ProcessNameFilter) -and $row.ProcessName -ine $ProcessNameFilter) { continue } [PSCustomObject]@{ PSTypeName = 'PSWinOps.ProcessCrashEvent' ComputerName = $env:COMPUTERNAME EventTime = $row.EventTime.ToString('o') EventId = $row.EventId EventType = $row.EventType ProcessName = $row.ProcessName ProcessPath = $row.ProcessPath FaultingModule = $row.FaultingModule ExceptionCode = $row.ExceptionCode FaultingOffset = $row.FaultingOffset ReportId = $row.ReportId UserName = $row.UserName CrashCount = $crashTally[([string]$row.ProcessName).ToLowerInvariant()] Message = $row.Message Timestamp = Get-Date -Format 'o' } } } } process { foreach ($targetComputer in $ComputerName) { try { Write-Verbose "[$($MyInvocation.MyCommand)] Querying process crash events on '$targetComputer'" Invoke-RemoteOrLocal -ComputerName $targetComputer -Credential $Credential ` -ScriptBlock $scriptBlock ` -ArgumentList @($startTime, $MaxEvents, $ProcessName, [bool]$IncludeHang) } catch { Write-Error "[$($MyInvocation.MyCommand)] Failed on '$targetComputer': $_" } } } end { Write-Verbose "[$($MyInvocation.MyCommand)] Completed process crash event query" } } |