#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"
