#Requires -RunAsAdministrator ############################################################################### # server-forensics.ps1 - Post-mortem forensics for crashed/locked Windows servers # # Collects system state, event logs, crash dumps, resource usage, and # network info into a timestamped report for root-cause analysis. # # Author: Phil Connor # Contact: contact@mylinux.work # License: MIT # Version 1.00 # # Usage: # .\server-forensics.ps1 # Full forensic collection # .\server-forensics.ps1 -Quick # Quick summary only # .\server-forensics.ps1 -Service "W3SVC" # Focus on a specific service # .\server-forensics.ps1 -Since (Get-Date).AddHours(-2) # Logs since a time # .\server-forensics.ps1 -OutputDir "C:\Temp" # Custom output directory ############################################################################### [CmdletBinding()] param( [switch]$Quick, [string]$Service, [DateTime]$Since = (Get-Date).AddHours(-4), [string]$OutputDir = "C:\Forensics" ) $ErrorActionPreference = "Continue" #------------------------------------------------------------------------------ # SETUP #------------------------------------------------------------------------------ $Timestamp = Get-Date -Format "yyyyMMdd_HHmmss" $CaseDir = Join-Path $OutputDir $Timestamp New-Item -ItemType Directory -Path $CaseDir -Force | Out-Null $ReportPath = Join-Path $CaseDir "forensics-report.txt" function Write-Log { param($msg) Write-Host "[forensics] $msg" -ForegroundColor Green } function Write-Warn { param($msg) Write-Host "[forensics] $msg" -ForegroundColor Yellow } function Write-Err { param($msg) Write-Host "[forensics] $msg" -ForegroundColor Red } function Collect { param( [string]$Label, [scriptblock]$Command ) $header = "=== $Label ===" Write-Host $header -ForegroundColor Cyan $header | Out-File -Append -FilePath $ReportPath try { $output = & $Command 2>&1 | Out-String $output | Out-File -Append -FilePath $ReportPath Write-Output $output } catch { $err = " [Error collecting: $_]" $err | Out-File -Append -FilePath $ReportPath Write-Output $err } "" | Out-File -Append -FilePath $ReportPath } Write-Log "Forensics case: $CaseDir" Write-Log "Looking back since: $Since" @" Forensics Report - $(Get-Date) Hostname: $env:COMPUTERNAME OS: $((Get-CimInstance Win32_OperatingSystem).Caption) ============================================= "@ | Out-File -FilePath $ReportPath ############################################################################### # SECTION 1: SYSTEM STATE SNAPSHOT ############################################################################### Write-Log "Collecting system state..." Collect "Uptime & Boot Time" { $os = Get-CimInstance Win32_OperatingSystem [PSCustomObject]@{ LastBootTime = $os.LastBootUpTime Uptime = (New-TimeSpan -Start $os.LastBootUpTime -End (Get-Date)).ToString() } | Format-List } Collect "Last Unexpected Shutdowns" { Get-WinEvent -FilterHashtable @{LogName='System'; Id=6008; StartTime=$Since} -MaxEvents 20 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Message -Wrap } Collect "System Startup Events" { Get-WinEvent -FilterHashtable @{LogName='System'; Id=6005,6006,6009,6013; StartTime=$Since} -MaxEvents 20 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Id, Message -Wrap } Collect "BugCheck / Blue Screen Events" { Get-WinEvent -FilterHashtable @{LogName='System'; Id=1001; ProviderName='Microsoft-Windows-WER-SystemErrorReporting'; StartTime=$Since} -MaxEvents 10 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Message -Wrap if (Test-Path "$env:SystemRoot\Minidump") { "`nMinidump files:" Get-ChildItem "$env:SystemRoot\Minidump" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 10 | Format-Table Name, LastWriteTime, Length } if (Test-Path "$env:SystemRoot\MEMORY.DMP") { $dmp = Get-Item "$env:SystemRoot\MEMORY.DMP" "`nFull memory dump: $($dmp.FullName) ($('{0:N0} MB' -f ($dmp.Length / 1MB)), $($dmp.LastWriteTime))" } } ############################################################################### # SECTION 2: RESOURCE EXHAUSTION CHECK ############################################################################### Write-Log "Checking resource exhaustion..." Collect "Memory Usage" { $os = Get-CimInstance Win32_OperatingSystem $totalGB = [math]::Round($os.TotalVisibleMemorySize / 1MB, 2) $freeGB = [math]::Round($os.FreePhysicalMemory / 1MB, 2) $usedGB = $totalGB - $freeGB $pct = [math]::Round(($usedGB / $totalGB) * 100, 1) [PSCustomObject]@{ TotalGB = $totalGB UsedGB = $usedGB FreeGB = $freeGB UsedPct = "$pct%" } | Format-List "`nPage File:" Get-CimInstance Win32_PageFileUsage | Format-Table Name, CurrentUsage, AllocatedBaseSize, PeakUsage } Collect "Disk Usage" { Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | Select-Object DeviceID, @{N='SizeGB'; E={[math]::Round($_.Size/1GB,2)}}, @{N='FreeGB'; E={[math]::Round($_.FreeSpace/1GB,2)}}, @{N='UsedPct'; E={[math]::Round((($_.Size - $_.FreeSpace) / $_.Size) * 100, 1)}} | Format-Table -AutoSize } Collect "Handle Count (Top 15)" { Get-Process | Sort-Object HandleCount -Descending | Select-Object -First 15 | Format-Table Id, ProcessName, HandleCount, @{N='WorkingSetMB';E={[math]::Round($_.WorkingSet64/1MB,1)}} -AutoSize } ############################################################################### # SECTION 3: PROCESS STATE ############################################################################### Write-Log "Collecting process info..." Collect "Top Processes by CPU Time" { Get-Process | Where-Object { $_.CPU } | Sort-Object CPU -Descending | Select-Object -First 20 | Format-Table Id, ProcessName, @{N='CPU_Sec';E={[math]::Round($_.CPU,1)}}, @{N='WorkingSetMB';E={[math]::Round($_.WorkingSet64/1MB,1)}}, @{N='Threads';E={$_.Threads.Count}}, @{N='Handles';E={$_.HandleCount}} -AutoSize } Collect "Top Processes by Memory" { Get-Process | Sort-Object WorkingSet64 -Descending | Select-Object -First 20 | Format-Table Id, ProcessName, @{N='WorkingSetMB';E={[math]::Round($_.WorkingSet64/1MB,1)}}, @{N='PrivateMB';E={[math]::Round($_.PrivateMemorySize64/1MB,1)}}, @{N='VirtualMB';E={[math]::Round($_.VirtualMemorySize64/1MB,1)}} -AutoSize } Collect "Not Responding Processes" { Get-Process | Where-Object { $_.Responding -eq $false } | Format-Table Id, ProcessName, StartTime, @{N='WorkingSetMB';E={[math]::Round($_.WorkingSet64/1MB,1)}} -AutoSize if (-not (Get-Process | Where-Object { $_.Responding -eq $false })) { " None found" } } ############################################################################### # SECTION 4: SERVICES ############################################################################### Write-Log "Checking services..." Collect "Stopped Auto-Start Services" { Get-CimInstance Win32_Service | Where-Object { $_.StartMode -eq 'Auto' -and $_.State -ne 'Running' } | Format-Table Name, DisplayName, State, StartMode, ExitCode -AutoSize } Collect "Services with Non-Zero Exit Codes" { Get-CimInstance Win32_Service | Where-Object { $_.ExitCode -ne 0 -and $_.ExitCode -ne $null } | Format-Table Name, State, ExitCode, @{N='Win32Exit';E={$_.Win32ExitCode}} -AutoSize } if ($Service) { Write-Log "Focused forensics on: $Service" Collect "Service Detail: $Service" { Get-CimInstance Win32_Service -Filter "Name='$Service'" | Format-List * } Collect "Service Event Log: $Service" { Get-WinEvent -FilterHashtable @{LogName='System'; StartTime=$Since} -MaxEvents 500 -ErrorAction SilentlyContinue | Where-Object { $_.Message -match $Service } | Select-Object -First 50 | Format-Table TimeCreated, Id, LevelDisplayName, Message -Wrap } } ############################################################################### # SECTION 5: EVENT LOGS (THE GOLD MINE) ############################################################################### Write-Log "Mining event logs..." Collect "Critical & Error Events - System Log" { Get-WinEvent -FilterHashtable @{LogName='System'; Level=1,2; StartTime=$Since} -MaxEvents 50 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Id, ProviderName, @{N='Message';E={$_.Message.Substring(0, [Math]::Min(200, $_.Message.Length))}} -Wrap } Collect "Critical & Error Events - Application Log" { Get-WinEvent -FilterHashtable @{LogName='Application'; Level=1,2; StartTime=$Since} -MaxEvents 50 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Id, ProviderName, @{N='Message';E={$_.Message.Substring(0, [Math]::Min(200, $_.Message.Length))}} -Wrap } Collect "Application Crashes (WER)" { Get-WinEvent -FilterHashtable @{LogName='Application'; ProviderName='Windows Error Reporting'; StartTime=$Since} -MaxEvents 20 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Message -Wrap } Collect "Application Hangs (Event 1002)" { Get-WinEvent -FilterHashtable @{LogName='Application'; Id=1002; StartTime=$Since} -MaxEvents 20 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Message -Wrap } if (-not $Quick) { Collect "Kernel Power Events (unexpected shutdowns)" { Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Microsoft-Windows-Kernel-Power'; StartTime=$Since} -MaxEvents 20 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Id, Message -Wrap } Collect "Disk Errors" { Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='disk','Ntfs','volmgr','vhdmp'; Level=1,2,3; StartTime=$Since} -MaxEvents 30 -ErrorAction SilentlyContinue | Format-Table TimeCreated, ProviderName, Message -Wrap } } ############################################################################### # SECTION 6: NETWORK STATE ############################################################################### Write-Log "Checking network..." Collect "Listening Ports" { Get-NetTCPConnection -State Listen -ErrorAction SilentlyContinue | Sort-Object LocalPort | Select-Object LocalAddress, LocalPort, OwningProcess, @{N='Process';E={(Get-Process -Id $_.OwningProcess -ErrorAction SilentlyContinue).ProcessName}} | Format-Table -AutoSize } Collect "Connection Count by State" { Get-NetTCPConnection -ErrorAction SilentlyContinue | Group-Object State | Sort-Object Count -Descending | Format-Table Count, Name -AutoSize } Collect "Network Adapter Errors" { Get-NetAdapterStatistics -ErrorAction SilentlyContinue | Format-Table Name, ReceivedPacketErrors, ReceivedDiscards, OutboundPacketErrors, OutboundDiscards -AutoSize } if (-not $Quick) { Collect "DNS Client Cache (last 20)" { Get-DnsClientCache -ErrorAction SilentlyContinue | Select-Object -First 20 | Format-Table Entry, RecordName, Data -AutoSize } Collect "Firewall Profile Status" { Get-NetFirewallProfile -ErrorAction SilentlyContinue | Format-Table Name, Enabled, DefaultInboundAction, DefaultOutboundAction -AutoSize } Collect "Routing Table" { Get-NetRoute -ErrorAction SilentlyContinue | Where-Object { $_.DestinationPrefix -ne 'ff00::/8' } | Select-Object -First 30 | Format-Table DestinationPrefix, NextHop, InterfaceAlias, RouteMetric -AutoSize } } ############################################################################### # SECTION 7: CRASH DUMPS & WER ############################################################################### if (-not $Quick) { Write-Log "Looking for crash dumps..." Collect "Windows Error Reports" { $werPaths = @( "$env:ProgramData\Microsoft\Windows\WER\ReportArchive", "$env:ProgramData\Microsoft\Windows\WER\ReportQueue", "$env:LOCALAPPDATA\Microsoft\Windows\WER\ReportArchive" ) foreach ($p in $werPaths) { if (Test-Path $p) { "`n--- $p ---" Get-ChildItem $p -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 10 | Format-Table Name, LastWriteTime } } } Collect "Recent Dump Files" { $dumpPaths = @( "$env:SystemRoot\Minidump", "$env:SystemRoot\LiveKernelReports", "$env:LOCALAPPDATA\CrashDumps" ) foreach ($p in $dumpPaths) { if (Test-Path $p) { "`n--- $p ---" Get-ChildItem $p -Recurse -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending | Select-Object -First 10 | Format-Table Name, LastWriteTime, @{N='SizeMB';E={[math]::Round($_.Length/1MB,2)}} } } } } ############################################################################### # SECTION 8: HARDWARE / STORAGE HEALTH ############################################################################### if (-not $Quick) { Write-Log "Checking hardware health..." Collect "Physical Disk Health" { Get-PhysicalDisk -ErrorAction SilentlyContinue | Format-Table FriendlyName, MediaType, HealthStatus, OperationalStatus, Size -AutoSize } Collect "Storage Reliability Counters" { Get-PhysicalDisk -ErrorAction SilentlyContinue | ForEach-Object { $disk = $_ $counters = Get-StorageReliabilityCounter -PhysicalDisk $disk -ErrorAction SilentlyContinue if ($counters) { "--- $($disk.FriendlyName) ---" $counters | Format-List Temperature, Wear, ReadErrorsTotal, WriteErrorsTotal, ReadErrorsCorrected, ReadErrorsUncorrected, PowerOnHours } } } Collect "WHEA Hardware Errors" { Get-WinEvent -FilterHashtable @{LogName='System'; ProviderName='Microsoft-Windows-WHEA-Logger'; StartTime=$Since} -MaxEvents 20 -ErrorAction SilentlyContinue | Format-Table TimeCreated, LevelDisplayName, Message -Wrap } Collect "Volume Status" { Get-Volume -ErrorAction SilentlyContinue | Where-Object { $_.DriveLetter } | Format-Table DriveLetter, FileSystemLabel, HealthStatus, @{N='SizeGB';E={[math]::Round($_.Size/1GB,2)}}, @{N='FreeGB';E={[math]::Round($_.SizeRemaining/1GB,2)}}, @{N='FreePct';E={[math]::Round(($_.SizeRemaining/$_.Size)*100,1)}} -AutoSize } } ############################################################################### # SECTION 9: SECURITY TRIAGE ############################################################################### if (-not $Quick) { Write-Log "Security quick-check..." Collect "Failed Login Attempts (last 24h)" { $failedLogins = Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4625; StartTime=(Get-Date).AddHours(-24)} -MaxEvents 200 -ErrorAction SilentlyContinue if ($failedLogins) { "Total failed logins: $($failedLogins.Count)" "`nBy account:" $failedLogins | ForEach-Object { $xml = [xml]$_.ToXml() $ns = @{e='http://schemas.microsoft.com/win/2004/08/events/event'} $target = ($xml | Select-Xml "//e:Data[@Name='TargetUserName']" -Namespace $ns).Node.'#text' $ip = ($xml | Select-Xml "//e:Data[@Name='IpAddress']" -Namespace $ns).Node.'#text' [PSCustomObject]@{Account=$target; SourceIP=$ip} } | Group-Object Account | Sort-Object Count -Descending | Select-Object -First 10 | Format-Table Count, Name -AutoSize "`nBy source IP:" $failedLogins | ForEach-Object { $xml = [xml]$_.ToXml() $ns = @{e='http://schemas.microsoft.com/win/2004/08/events/event'} ($xml | Select-Xml "//e:Data[@Name='IpAddress']" -Namespace $ns).Node.'#text' } | Where-Object { $_ -and $_ -ne '-' } | Group-Object | Sort-Object Count -Descending | Select-Object -First 10 | Format-Table Count, Name -AutoSize } else { " No failed logins found" } } Collect "Account Lockouts" { Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4740; StartTime=$Since} -MaxEvents 20 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Message -Wrap } Collect "Privilege Escalation Events" { Get-WinEvent -FilterHashtable @{LogName='Security'; Id=4672,4673; StartTime=$Since} -MaxEvents 20 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Id, @{N='Message';E={$_.Message.Substring(0, [Math]::Min(150, $_.Message.Length))}} -Wrap } Collect "New Services Installed" { Get-WinEvent -FilterHashtable @{LogName='System'; Id=7045; StartTime=$Since} -MaxEvents 20 -ErrorAction SilentlyContinue | Format-Table TimeCreated, Message -Wrap } Collect "Scheduled Tasks (non-Microsoft)" { Get-ScheduledTask -ErrorAction SilentlyContinue | Where-Object { $_.Author -notmatch 'Microsoft' -and $_.State -ne 'Disabled' } | Select-Object -First 20 | Format-Table TaskName, State, Author, @{N='Action';E={$_.Actions.Execute}} -AutoSize } } ############################################################################### # SECTION 10: PERFORMANCE COUNTERS SNAPSHOT ############################################################################### if (-not $Quick) { Write-Log "Collecting performance counters..." Collect "Performance Counter Snapshot" { $counters = @( '\Processor(_Total)\% Processor Time', '\Memory\Available MBytes', '\Memory\Pages/sec', '\Memory\Committed Bytes', '\PhysicalDisk(_Total)\% Disk Time', '\PhysicalDisk(_Total)\Avg. Disk Queue Length', '\PhysicalDisk(_Total)\Disk Reads/sec', '\PhysicalDisk(_Total)\Disk Writes/sec', '\Network Interface(*)\Bytes Total/sec', '\TCPv4\Connections Established', '\System\Processor Queue Length', '\System\Context Switches/sec' ) Get-Counter -Counter $counters -SampleInterval 2 -MaxSamples 3 -ErrorAction SilentlyContinue | ForEach-Object { "Sample: $($_.Timestamp)" $_.CounterSamples | Format-Table Path, @{N='Value';E={[math]::Round($_.CookedValue,2)}} -AutoSize } } } ############################################################################### # SUMMARY ############################################################################### Write-Log "Generating summary..." $summary = @() $summary += "" $summary += "=============================================" $summary += "FORENSICS SUMMARY" $summary += "=============================================" $summary += "Report generated: $(Get-Date)" $summary += "Hostname: $env:COMPUTERNAME" $os = Get-CimInstance Win32_OperatingSystem $uptime = (New-TimeSpan -Start $os.LastBootUpTime -End (Get-Date)).ToString("d\.hh\:mm\:ss") $summary += "Uptime: $uptime" $summary += "OS: $($os.Caption)" $summary += "" $summary += "🔴 CRITICAL FINDINGS:" # Unexpected shutdowns $unexpectedShutdowns = (Get-WinEvent -FilterHashtable @{LogName='System'; Id=6008; StartTime=$Since} -MaxEvents 100 -ErrorAction SilentlyContinue | Measure-Object).Count if ($unexpectedShutdowns -gt 0) { $summary += " - $unexpectedShutdowns unexpected shutdown(s)" } # BSODs $bsods = (Get-WinEvent -FilterHashtable @{LogName='System'; Id=1001; ProviderName='Microsoft-Windows-WER-SystemErrorReporting'; StartTime=$Since} -MaxEvents 100 -ErrorAction SilentlyContinue | Measure-Object).Count if ($bsods -gt 0) { $summary += " - $bsods Blue Screen / BugCheck event(s)" } # App crashes $appCrashes = (Get-WinEvent -FilterHashtable @{LogName='Application'; Id=1000; StartTime=$Since} -MaxEvents 100 -ErrorAction SilentlyContinue | Measure-Object).Count if ($appCrashes -gt 0) { $summary += " - $appCrashes application crash(es)" } # App hangs $appHangs = (Get-WinEvent -FilterHashtable @{LogName='Application'; Id=1002; StartTime=$Since} -MaxEvents 100 -ErrorAction SilentlyContinue | Measure-Object).Count if ($appHangs -gt 0) { $summary += " - $appHangs application hang(s)" } # Stopped auto-start services $stoppedSvcs = (Get-CimInstance Win32_Service | Where-Object { $_.StartMode -eq 'Auto' -and $_.State -ne 'Running' } | Measure-Object).Count if ($stoppedSvcs -gt 0) { $summary += " - $stoppedSvcs auto-start service(s) not running" } # Disk space Get-CimInstance Win32_LogicalDisk -Filter "DriveType=3" | ForEach-Object { $usedPct = [math]::Round((($_.Size - $_.FreeSpace) / $_.Size) * 100, 1) if ($usedPct -ge 90) { $summary += " - Drive $($_.DeviceID) is $usedPct% full" } } # Memory $memPct = [math]::Round((($os.TotalVisibleMemorySize - $os.FreePhysicalMemory) / $os.TotalVisibleMemorySize) * 100, 1) if ($memPct -ge 90) { $summary += " - Memory usage at $memPct%" } # Hung processes $hungProcs = (Get-Process | Where-Object { $_.Responding -eq $false } | Measure-Object).Count if ($hungProcs -gt 0) { $summary += " - $hungProcs not-responding process(es)" } # Disk health Get-PhysicalDisk -ErrorAction SilentlyContinue | Where-Object { $_.HealthStatus -ne 'Healthy' } | ForEach-Object { $summary += " - Disk '$($_.FriendlyName)' health: $($_.HealthStatus)" } $summary += "" $summary += "📁 Full report: $ReportPath" $summary += "📁 Case folder: $CaseDir" $summaryText = $summary -join "`n" $summaryText | Tee-Object -Append -FilePath $ReportPath # Export key event logs for offline analysis Write-Log "Exporting event logs..." wevtutil epl System (Join-Path $CaseDir "System.evtx") 2>$null wevtutil epl Application (Join-Path $CaseDir "Application.evtx") 2>$null wevtutil epl Security (Join-Path $CaseDir "Security.evtx") 2>$null Write-Log "✅ Forensics collection complete: $CaseDir"