a1a17e81a1
Includes updated JS challenge scripts with Claude-User whitelist, same-site referer bypass, Blackbox-Exporter allowed bot, and all new exporters, cheat sheets, and automation scripts.
544 lines
22 KiB
PowerShell
544 lines
22 KiB
PowerShell
#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"
|