Skip to content

Windows Guest 診斷 SOP

在 KubeVirt 環境中,維運團隊無法直接登入使用者 VM,當 user 回報異常時,請依下列 SOP 引導使用者執行診斷腳本,收集後回傳分析。


腳本說明

腳本 用途 使用時機
windows-diag.ps1 全面收集系統狀態(Event Log、行程、網路、磁碟、安全性等) 任何異常,或 user 當下仍有問題時

腳本輸出結構:

vm_diag_<ComputerName>_<timestamp>.zip
├── 01_system/        # 系統資訊、硬體規格、Hotfix 清單
├── 02_event_logs/    # System / Application / Security / Defender 等事件日誌
├── 03_processes/     # 行程、服務、排程任務、啟動項目
├── 04_network/       # 網路設定、連線、防火牆規則
├── 05_storage/       # 磁碟使用量、健康狀態、Shadow Copy
├── 06_security/      # 本地帳號、群組、登入 Session
├── 07_software/      # 安裝軟體清單、QEMU-GA 狀態
└── 08_crash_reports/ # WER 崩潰報告、Minidump 清單


使用者執行步驟

Step 1:複製腳本

將下方 「腳本原始碼」 區段的完整內容複製,貼到記事本,存成 windows-diag.ps1(放桌面即可)。

Step 2:以系統管理員身分執行

按下 Win + X,選「Windows PowerShell (系統管理員)」,執行:

# 預設收集(Event Log 範圍:過去 7 天)
powershell -ExecutionPolicy Bypass -File "$env:USERPROFILE\Desktop\windows-diag.ps1"

若知道事發時間,指定時間段(推薦):

powershell -ExecutionPolicy Bypass -File "$env:USERPROFILE\Desktop\windows-diag.ps1" -StartTime "2026-09-04 09:00" -EndTime "2026-09-04 13:00"

Step 3:回傳壓縮檔

腳本完成後會顯示輸出路徑,請使用者將 .zip 傳回維運團隊:

C:\Users\<User>\AppData\Local\Temp\vm_diag_<ComputerName>_<timestamp>.zip


關鍵 Event ID 速查

Event ID Log 意義
2004 System 虛擬記憶體嚴重不足,直接列出當時前 3 名元兇 🔴
41 System Kernel-Power:非預期關機/重啟 🔴
6008 System 上次關機是非預期的 🔴
1001 Application 藍屏 (BSOD) 🔴
1002 Application 程式無回應 (Hang) 🟠
1000 Application 程式崩潰 (Crash) 🟠
7034 System 服務意外終止 🟠
1074 System 主動重啟記錄 🟡

[!IMPORTANT] QEMU Guest Agent 狀態:請在 07_software/qemu_guest_agent.txt 確認 QEMU-GA 是否正常執行。 若未運行,Prometheus 上的 memory_available 指標不可信,容易產生假性記憶體告警。


腳本原始碼

複製以下完整內容,存成 windows-diag.ps1 後執行。

# windows-diag.ps1
# General-purpose Windows Guest Diagnostic Collector
# Collects: System Info, Event Logs, Processes, Network, Storage, Security, Software, Crash Reports
# Output: <OutputPath>\vm_diag_<ComputerName>_<timestamp>.zip
# Usage: powershell -ExecutionPolicy Bypass -File windows-diag.ps1
#        powershell -ExecutionPolicy Bypass -File windows-diag.ps1 -StartTime "2026-09-04 09:00" -EndTime "2026-09-04 13:00"
#        powershell -ExecutionPolicy Bypass -File windows-diag.ps1 -StartTime "2026-09-04 09:00" -EndTime "2026-09-04 13:00" -OutputPath "C:\Diag"
#
# Notes:
#   - Run as Administrator for full data collection (script warns but continues if not admin)
#   - No external dependencies: pure PowerShell built-in cmdlets only
#   - ErrorAction SilentlyContinue is used throughout to avoid stopping on permission errors
#   - Event logs are filtered by StartTime/EndTime parameters

param(
    [string]$StartTime  = "",   # "yyyy-MM-dd HH:mm", default: 7 days ago
    [string]$EndTime    = "",   # "yyyy-MM-dd HH:mm", default: now
    [string]$OutputPath = ""    # Output base directory, default: $env:TEMP
)

Set-StrictMode -Off
$ErrorActionPreference = "SilentlyContinue"

# Detect OS type for compatibility branching
$OSInfo       = Get-CimInstance Win32_OperatingSystem
$OSCaption    = $OSInfo.Caption
$OSBuild      = [int]$OSInfo.BuildNumber
$IsServerOS   = $OSCaption -match "Server"
$IsWin11      = (-not $IsServerOS) -and $OSBuild -ge 22000
$IsWin2019    = $OSCaption -match "2019"
$IsWin2022    = $OSCaption -match "2022"
$IsWin2025    = $OSCaption -match "2025"

# ---------------------------------------------------------------------------
# 0. INITIALIZATION
# ---------------------------------------------------------------------------

$ScriptVersion  = "1.0.0"
$Timestamp      = Get-Date -Format "yyyyMMdd_HHmmss"
$ComputerName   = $env:COMPUTERNAME
$DiagRoot       = "vm_diag_${ComputerName}_${Timestamp}"
$PermissionErrors = [System.Collections.Generic.List[string]]::new()

# Resolve output path: default to same directory as the script
if ([string]::IsNullOrWhiteSpace($OutputPath)) { $OutputPath = $PSScriptRoot }
$DiagPath = Join-Path $OutputPath $DiagRoot

# Parse time range
$Now = Get-Date
if ([string]::IsNullOrWhiteSpace($StartTime)) {
    $dtStart = $Now.AddDays(-7)
} else {
    $dtStart = [datetime]::ParseExact($StartTime, "yyyy-MM-dd HH:mm", $null)
}
if ([string]::IsNullOrWhiteSpace($EndTime)) {
    $dtEnd = $Now
} else {
    $dtEnd = [datetime]::ParseExact($EndTime, "yyyy-MM-dd HH:mm", $null)
}

# Admin check
$IsAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole(
    [Security.Principal.WindowsBuiltInRole]::Administrator)

Write-Host ""
Write-Host "========================================================" -ForegroundColor Cyan
Write-Host "  Windows Guest Diagnostic Collector v$ScriptVersion" -ForegroundColor Cyan
Write-Host "========================================================" -ForegroundColor Cyan
Write-Host "  Computer  : $ComputerName"
Write-Host "  StartTime : $($dtStart.ToString('yyyy-MM-dd HH:mm:ss'))"
Write-Host "  EndTime   : $($dtEnd.ToString('yyyy-MM-dd HH:mm:ss'))"
Write-Host "  OutputPath: $OutputPath"
Write-Host "  Admin     : $IsAdmin"
if (-not $IsAdmin) {
    Write-Host "  [WARNING] Not running as Administrator. Some data may be incomplete." -ForegroundColor Yellow
}
Write-Host ""

# Create directory tree
$Sections = @(
    "01_system", "02_event_logs", "03_processes",
    "04_network", "05_storage",   "06_security",
    "07_software","08_crash_reports"
)
foreach ($s in $Sections) {
    New-Item -ItemType Directory -Path (Join-Path $DiagPath $s) -Force | Out-Null
}

# Helper: write a file header banner
function Write-FileHeader {
    param([string]$FilePath, [string]$Title, [string]$Description, [string]$AnalysisTip = "")
    $banner = @"
================================================================================
  $Title
================================================================================
  Description : $Description
  Analysis Tip: $AnalysisTip
  Collected At: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')
  Computer    : $ComputerName
================================================================================

"@
    Set-Content -Path $FilePath -Value $banner -Encoding UTF8
}

# Helper: append output or an error note
function Invoke-CollectBlock {
    param([string]$FilePath, [scriptblock]$Block)
    try {
        $result = & $Block 2>&1
        Add-Content -Path $FilePath -Value $result -Encoding UTF8
    } catch {
        $msg = "[COLLECTION ERROR] $($_.Exception.Message)"
        Add-Content -Path $FilePath -Value $msg -Encoding UTF8
        $PermissionErrors.Add("$FilePath : $($_.Exception.Message)")
    }
}

# Helper: collect a named event log
function Get-FilteredWinEvent {
    param([string]$LogName, [string]$FilePath, [int[]]$EventIds = @())
    try {
        $filter = @{ LogName = $LogName; StartTime = $dtStart; EndTime = $dtEnd }
        if ($EventIds.Count -gt 0) { $filter["Id"] = $EventIds }
        $events = Get-WinEvent -FilterHashtable $filter -ErrorAction Stop |
            Select-Object TimeCreated, Id, LevelDisplayName, ProviderName, Message
        if ($events) {
            foreach ($e in $events) {
                $line = "[{0}] [{1}] [{2}] [{3}] {4}" -f `
                    $e.TimeCreated.ToString("yyyy-MM-dd HH:mm:ss"), $e.Id,
                    $e.LevelDisplayName, $e.ProviderName,
                    ($e.Message -replace "`r`n|`n", " " | ForEach-Object { $_.Substring(0, [Math]::Min($_.Length,300)) })
                Add-Content -Path $FilePath -Value $line -Encoding UTF8
            }
            Add-Content -Path $FilePath -Value "`n[Total events: $($events.Count)]" -Encoding UTF8
        } else {
            Add-Content -Path $FilePath -Value "[No events found in the specified time range]" -Encoding UTF8
        }
    } catch {
        $msg = "[LOG ERROR] LogName='$LogName' : $($_.Exception.Message)"
        Add-Content -Path $FilePath -Value $msg -Encoding UTF8
        $PermissionErrors.Add("$FilePath ($LogName): $($_.Exception.Message)")
    }
}

# ===========================================================================
# SECTION 1/8 -- SYSTEM
# ===========================================================================
Write-Host "[1/8] Collecting system information..." -ForegroundColor Green

# 1a. system_overview.txt
$f = Join-Path $DiagPath "01_system\system_overview.txt"
Write-FileHeader $f "System Overview" `
    "OS version, timezone, uptime, boot time, basic system identifiers." `
    "Check OS version for patch baseline, boot time to confirm recent reboots."
Invoke-CollectBlock $f {
    "--- systeminfo ---"
    systeminfo 2>&1
    ""
    "--- OS Details (CIM) ---"
    Get-CimInstance Win32_OperatingSystem | Format-List Caption, Version, BuildNumber,
        OSArchitecture, InstallDate, LastBootUpTime, LocalDateTime, SystemDirectory,
        TotalVisibleMemorySize, FreePhysicalMemory, TotalVirtualMemorySize, FreeVirtualMemory
    ""
    "--- Uptime ---"
    $os = Get-CimInstance Win32_OperatingSystem
    $uptime = $os.LocalDateTime - $os.LastBootUpTime
    "Boot Time  : $($os.LastBootUpTime)"
    "Uptime     : $($uptime.Days)d $($uptime.Hours)h $($uptime.Minutes)m $($uptime.Seconds)s"
    ""
    "--- Timezone ---"
    Get-TimeZone | Format-List
    [System.TimeZoneInfo]::Local
}

# 1b. hardware.txt
$f = Join-Path $DiagPath "01_system\hardware.txt"
Write-FileHeader $f "Hardware Information" `
    "CPU, RAM DIMMs, BIOS, baseboard/motherboard details." `
    "Verify CPU core count vs. expected VM spec. Check BIOS version for known firmware CVEs."
Invoke-CollectBlock $f {
    "--- CPU ---"
    Get-CimInstance Win32_Processor | Format-List Name, Manufacturer, MaxClockSpeed,
        NumberOfCores, NumberOfLogicalProcessors, L2CacheSize, L3CacheSize, SocketDesignation, Status
    ""
    "--- Physical Memory (DIMMs) ---"
    Get-CimInstance Win32_PhysicalMemory | Format-List BankLabel, DeviceLocator, Capacity,
        Speed, MemoryType, Manufacturer, PartNumber
    ""
    "--- BIOS ---"
    Get-CimInstance Win32_BIOS | Format-List Manufacturer, Name, Version, SMBIOSBIOSVersion,
        ReleaseDate, SerialNumber
    ""
    "--- Baseboard / Motherboard ---"
    Get-CimInstance Win32_BaseBoard | Format-List Manufacturer, Product, SerialNumber, Version
    ""
    "--- System Enclosure ---"
    Get-CimInstance Win32_SystemEnclosure | Format-List Manufacturer, SerialNumber, SMBIOSAssetTag, ChassisTypes
    ""
    "--- Computer System ---"
    Get-CimInstance Win32_ComputerSystem | Format-List Manufacturer, Model, SystemType,
        TotalPhysicalMemory, NumberOfProcessors, NumberOfLogicalProcessors, DomainRole, Domain, Workgroup
}

# 1c. hotfixes.txt
$f = Join-Path $DiagPath "01_system\hotfixes.txt"
Write-FileHeader $f "Installed Hotfixes / Windows Updates" `
    "All installed Windows updates, patches, and hotfixes." `
    "Look for missing critical patches. Sort by InstalledOn to find recent changes."
Invoke-CollectBlock $f {
    Get-HotFix | Sort-Object InstalledOn -Descending | Format-Table HotFixID, Description, InstalledBy, InstalledOn -AutoSize
}

# 1d. environment_vars.txt
$f = Join-Path $DiagPath "01_system\environment_vars.txt"
Write-FileHeader $f "Environment Variables" `
    "System-level and current-user environment variables." `
    "Check PATH for unexpected entries. Look for credentials accidentally stored in env vars."
Invoke-CollectBlock $f {
    "--- System Environment Variables ---"
    [System.Environment]::GetEnvironmentVariables("Machine").GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Name) = $($_.Value)" }
    ""
    "--- User Environment Variables ---"
    [System.Environment]::GetEnvironmentVariables("User").GetEnumerator() | Sort-Object Name | ForEach-Object { "$($_.Name) = $($_.Value)" }
    ""
    "--- Process Environment Variables (current session) ---"
    Get-ChildItem Env: | Sort-Object Name | Format-Table Name, Value -AutoSize
}

# 1e. boot_config.txt
$f = Join-Path $DiagPath "01_system\boot_config.txt"
Write-FileHeader $f "Boot Configuration (BCD)" `
    "Boot Configuration Data entries via bcdedit." `
    "Check boot order, safe mode flags, kernel debugger settings, or unexpected boot entries."
Invoke-CollectBlock $f {
    bcdedit /enum ALL 2>&1
}

# ===========================================================================
# SECTION 2/8 -- EVENT LOGS
# ===========================================================================
Write-Host "[2/8] Collecting event logs (range: $($dtStart.ToString('yyyy-MM-dd HH:mm')) to $($dtEnd.ToString('yyyy-MM-dd HH:mm')))..." -ForegroundColor Green

# 2a. system_events.txt
$f = Join-Path $DiagPath "02_event_logs\system_events.txt"
Write-FileHeader $f "System Event Log" `
    "System log: all severity levels, filtered to the specified time range." `
    "Filter for Level=Error/Critical first. Look for disk errors (Disk/NTFS), service crashes (SCM), BSODs (BugCheck)."
Get-FilteredWinEvent -LogName "System" -FilePath $f

# 2b. application_events.txt
$f = Join-Path $DiagPath "02_event_logs\application_events.txt"
Write-FileHeader $f "Application Event Log" `
    "Application log: all severity levels, filtered to the specified time range." `
    "Look for application crashes (.NET runtime errors, faulting module paths), hang events."
Get-FilteredWinEvent -LogName "Application" -FilePath $f

# 2c. security_events.txt
$f = Join-Path $DiagPath "02_event_logs\security_events.txt"
Write-FileHeader $f "Security Event Log (Key IDs)" `
    "Security log filtered to incident-relevant event IDs only (4624=Logon, 4625=FailedLogon, 4648=ExplicitCreds, 4672=PrivilegeUse, 4720=UserCreated, 4726=UserDeleted, 1102=AuditLogCleared)." `
    "4625 failures in bursts = brute force. 4720/4726 = account manipulation. 1102 = log tampering."
Get-FilteredWinEvent -LogName "Security" -FilePath $f -EventIds @(4624,4625,4648,4672,4720,4726,1102)

# 2d. defender_events.txt
$f = Join-Path $DiagPath "02_event_logs\defender_events.txt"
Write-FileHeader $f "Windows Defender Operational Log" `
    "Defender detections, scan results, real-time protection events." `
    "IDs 1116/1117 = malware detected/blocked. ID 5001 = real-time protection disabled (high severity)."
Get-FilteredWinEvent -LogName "Microsoft-Windows-Windows Defender/Operational" -FilePath $f

# 2e. powershell_events.txt
$f = Join-Path $DiagPath "02_event_logs\powershell_events.txt"
Write-FileHeader $f "PowerShell Logging (Script Block & Operational)" `
    "PowerShell script block execution logs and operational events." `
    "ID 4104 = script block executed (look for encoded/obfuscated commands). ID 400/800 = pipeline execution."
Get-FilteredWinEvent -LogName "Microsoft-Windows-PowerShell/Operational" -FilePath $f
Add-Content -Path $f -Value "`n--- PowerShell Admin Log ---`n" -Encoding UTF8
Get-FilteredWinEvent -LogName "Microsoft-Windows-PowerShell/Admin" -FilePath $f

# 2f. task_scheduler_events.txt
$f = Join-Path $DiagPath "02_event_logs\task_scheduler_events.txt"
Write-FileHeader $f "Task Scheduler Operational Log" `
    "Scheduled task execution start/stop/failure events." `
    "ID 106 = task registered. ID 201 = task completed. ID 101/103 = task failed to start. Watch for unknown tasks."
Get-FilteredWinEvent -LogName "Microsoft-Windows-TaskScheduler/Operational" -FilePath $f

# 2g. windows_update_events.txt
$f = Join-Path $DiagPath "02_event_logs\windows_update_events.txt"
Write-FileHeader $f "Windows Update Client Log" `
    "Windows Update installation, download, and failure events." `
    "ID 19 = update installed successfully. ID 20 = install failed (check error code). Look for recent patch activity."
Get-FilteredWinEvent -LogName "Microsoft-Windows-WindowsUpdateClient/Operational" -FilePath $f

# ===========================================================================
# SECTION 3/8 -- PROCESSES
# ===========================================================================
Write-Host "[3/8] Collecting process and service information..." -ForegroundColor Green

# 3a. running_processes.txt
$f = Join-Path $DiagPath "03_processes\running_processes.txt"
Write-FileHeader $f "Running Processes" `
    "All running processes with PID, parent PID, owner, command line, path, and memory." `
    "Check for processes with unusual paths (Temp, AppData), unsigned binaries, or high memory/CPU consumers."
Invoke-CollectBlock $f {
    $procs = Get-CimInstance Win32_Process | Sort-Object ProcessId
    foreach ($p in $procs) {
        $ownerInfo = $p.GetOwner()
        $owner = $ownerInfo.User
        $domain = $ownerInfo.Domain
        [PSCustomObject]@{
            PID         = $p.ProcessId
            PPID        = $p.ParentProcessId
            Name        = $p.Name
            Owner       = if ($owner) { "$domain\$owner" } else { "N/A" }
            "WS(MB)"    = [math]::Round($p.WorkingSetSize / 1MB, 1)
            CommandLine = $p.CommandLine
            Path        = $p.ExecutablePath
        } | Format-List
    }
}

# 3b. process_tree.txt
$f = Join-Path $DiagPath "03_processes\process_tree.txt"
Write-FileHeader $f "Process Tree (Parent-Child)" `
    "Parent-child process relationships showing process hierarchy." `
    "Suspicious: cmd.exe or powershell.exe spawned by Office apps, browsers, or services indicates potential exploitation."
Invoke-CollectBlock $f {
    $all = Get-CimInstance Win32_Process | Sort-Object ProcessId
    $lookup = @{}
    foreach ($p in $all) { $lookup[$p.ProcessId] = $p }

    function Show-Tree {
        param($pid, $indent = "")
        $p = $lookup[$pid]
        if (-not $p) { return }
        $ownerInfo = $p.GetOwner()
        $owner = $ownerInfo.User
        "{0}{1} (PID:{2} Owner:{3})" -f $indent, $p.Name, $p.ProcessId, $owner
        foreach ($child in ($all | Where-Object { $_.ParentProcessId -eq $pid -and $_.ProcessId -ne $pid })) {
            Show-Tree -pid $child.ProcessId -indent ("  " + $indent)
        }
    }

    $roots = $all | Where-Object { -not $lookup.ContainsKey($_.ParentProcessId) -or $_.ParentProcessId -eq 0 }
    foreach ($r in $roots) { Show-Tree -pid $r.ProcessId }
}

# 3c. services.txt
$f = Join-Path $DiagPath "03_processes\services.txt"
Write-FileHeader $f "Windows Services" `
    "All services with status, start type, binary path, and service account." `
    "Look for services running from Temp/AppData, non-standard accounts, or unexpected Auto-start services."
Invoke-CollectBlock $f {
    Get-CimInstance Win32_Service | Sort-Object State, Name |
        Select-Object Name, DisplayName, State, StartMode, PathName, StartName, Description |
        Format-List
}

# 3d. scheduled_tasks.txt
$f = Join-Path $DiagPath "03_processes\scheduled_tasks.txt"
Write-FileHeader $f "Scheduled Tasks" `
    "All scheduled tasks with state, triggers, actions, last/next run time, and author." `
    "Flag tasks not created by Microsoft, tasks running from unusual paths, or recently modified tasks."
Invoke-CollectBlock $f {
    Get-ScheduledTask | ForEach-Object {
        $info = Get-ScheduledTaskInfo -TaskName $_.TaskName -TaskPath $_.TaskPath -ErrorAction SilentlyContinue
        [PSCustomObject]@{
            TaskPath    = $_.TaskPath + $_.TaskName
            State       = $_.State
            Author      = $_.Principal.UserId
            RunAs       = $_.Principal.RunLevel
            LastRunTime = $info.LastRunTime
            LastResult  = $info.LastTaskResult
            NextRunTime = $info.NextRunTime
            Actions     = ($_.Actions | ForEach-Object { "$($_.Execute) $($_.Arguments)" }) -join " | "
            Triggers    = ($_.Triggers | ForEach-Object { $_.CimClass.CimClassName }) -join ", "
        } | Format-List
    }
}

# 3e. startup_items.txt
$f = Join-Path $DiagPath "03_processes\startup_items.txt"
Write-FileHeader $f "Startup Items" `
    "Registry Run keys and Startup folder entries for all users." `
    "Compare against known-good baseline. Malware commonly persists via HKCU\Run or Startup folders."
Invoke-CollectBlock $f {
    $runKeys = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce",
        "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
        "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\RunOnce",
        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run"
    )
    foreach ($key in $runKeys) {
        "--- $key ---"
        $props = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue
        if ($props) {
            $props.PSObject.Properties | Where-Object { $_.Name -notlike "PS*" } |
                ForEach-Object { "  $($_.Name) = $($_.Value)" }
        } else { "  (empty or not found)" }
        ""
    }
    ""
    "--- Startup Folders ---"
    $startupPaths = @(
        "$env:ProgramData\Microsoft\Windows\Start Menu\Programs\Startup",
        "$env:APPDATA\Microsoft\Windows\Start Menu\Programs\Startup"
    )
    foreach ($sp in $startupPaths) {
        "  Path: $sp"
        if (Test-Path $sp) {
            Get-ChildItem $sp -ErrorAction SilentlyContinue | Format-Table Name, LastWriteTime, Length -AutoSize
        } else { "  (not found)" }
    }
}

# ===========================================================================
# SECTION 4/8 -- NETWORK
# ===========================================================================
Write-Host "[4/8] Collecting network information..." -ForegroundColor Green

# 4a. network_config.txt
$f = Join-Path $DiagPath "04_network\network_config.txt"
Write-FileHeader $f "Network Configuration" `
    "All network adapters: IP, MAC, DNS, DHCP, gateway settings (equivalent to ipconfig /all)." `
    "Verify IPs match expected allocation. Check for unexpected adapters or manual DNS servers."
Invoke-CollectBlock $f {
    "--- ipconfig /all ---"
    ipconfig /all 2>&1
    ""
    "--- Network Adapters (CIM) ---"
    Get-CimInstance Win32_NetworkAdapterConfiguration | Where-Object { $_.IPEnabled } |
        Format-List Description, MACAddress, IPAddress, IPSubnet, DefaultIPGateway,
            DNSServerSearchOrder, DHCPEnabled, DHCPServer, DHCPLeaseObtained, DHCPLeaseExpires
    ""
    "--- All Network Adapters ---"
    Get-NetAdapter | Format-Table Name, Status, MacAddress, LinkSpeed, MediaType, InterfaceDescription -AutoSize
}

# 4b. active_connections.txt
$f = Join-Path $DiagPath "04_network\active_connections.txt"
Write-FileHeader $f "Active Network Connections" `
    "All TCP/UDP connections with local/remote address, state, PID, and process name." `
    "Look for ESTABLISHED connections to unknown external IPs, listeners on unexpected ports, or processes making unusual connections."
Invoke-CollectBlock $f {
    "--- TCP Connections ---"
    $procMap = @{}
    Get-Process | ForEach-Object { $procMap[$_.Id] = $_.Name }
    Get-NetTCPConnection | Sort-Object State, LocalPort |
        Select-Object LocalAddress, LocalPort, RemoteAddress, RemotePort, State,
            @{N="PID";E={$_.OwningProcess}},
            @{N="Process";E={$procMap[$_.OwningProcess]}} |
        Format-Table -AutoSize
    ""
    "--- UDP Endpoints ---"
    Get-NetUDPEndpoint | Sort-Object LocalPort |
        Select-Object LocalAddress, LocalPort,
            @{N="PID";E={$_.OwningProcess}},
            @{N="Process";E={$procMap[$_.OwningProcess]}} |
        Format-Table -AutoSize
    ""
    "--- Raw netstat output ---"
    netstat -ano 2>&1
}

# 4c. arp_table.txt
$f = Join-Path $DiagPath "04_network\arp_table.txt"
Write-FileHeader $f "ARP Cache" `
    "Current ARP table mapping IP addresses to MAC addresses." `
    "Look for duplicate MACs (ARP poisoning) or unexpected entries on the local subnet."
Invoke-CollectBlock $f {
    "--- arp -a ---"
    arp -a 2>&1
    ""
    "--- Get-NetNeighbor ---"
    Get-NetNeighbor | Format-Table InterfaceAlias, IPAddress, LinkLayerAddress, State -AutoSize
}

# 4d. routing_table.txt
$f = Join-Path $DiagPath "04_network\routing_table.txt"
Write-FileHeader $f "Routing Table" `
    "IP routing table showing all routes (equivalent to route print)." `
    "Verify default gateway. Check for unexpected static routes that may redirect traffic."
Invoke-CollectBlock $f {
    "--- route print ---"
    route print 2>&1
    ""
    "--- Get-NetRoute ---"
    Get-NetRoute | Sort-Object RouteMetric | Format-Table DestinationPrefix, NextHop, RouteMetric, InterfaceAlias -AutoSize
}

# 4e. dns_cache.txt
$f = Join-Path $DiagPath "04_network\dns_cache.txt"
Write-FileHeader $f "DNS Cache" `
    "Local DNS resolver cache (equivalent to ipconfig /displaydns)." `
    "Unusual or malicious domain lookups may appear here after an incident even if connections are closed."
Invoke-CollectBlock $f {
    "--- ipconfig /displaydns ---"
    ipconfig /displaydns 2>&1
    ""
    "--- Get-DnsClientCache ---"
    Get-DnsClientCache | Format-Table Entry, RecordName, RecordType, TimeToLive, Data -AutoSize
}

# 4f. firewall_status.txt
$f = Join-Path $DiagPath "04_network\firewall_status.txt"
Write-FileHeader $f "Windows Firewall Status and Rules" `
    "Firewall profile settings and all inbound/outbound rules." `
    "Check if firewall is disabled (major risk). Look for rules allowing broad inbound access from Any."
Invoke-CollectBlock $f {
    "--- Firewall Profile Status ---"
    Get-NetFirewallProfile | Format-Table Name, Enabled, DefaultInboundAction, DefaultOutboundAction, LogFileName -AutoSize
    ""
    "--- Enabled Firewall Rules ---"
    Get-NetFirewallRule | Where-Object { $_.Enabled -eq "True" } |
        Sort-Object Direction, Action |
        Select-Object DisplayName, Direction, Action, Profile,
            @{N="LocalPort"; E={ ($_ | Get-NetFirewallPortFilter -ErrorAction SilentlyContinue).LocalPort }},
            @{N="RemoteAddress"; E={ ($_ | Get-NetFirewallAddressFilter -ErrorAction SilentlyContinue).RemoteAddress }} |
        Format-Table -AutoSize
    ""
    "--- All Firewall Rules (summary) ---"
    Get-NetFirewallRule | Format-Table DisplayName, Direction, Action, Enabled, Profile -AutoSize
}

# 4g. hosts_file.txt
$f = Join-Path $DiagPath "04_network\hosts_file.txt"
Write-FileHeader $f "Hosts File" `
    "Contents of C:\Windows\System32\drivers\etc\hosts." `
    "Malware commonly hijacks hosts file to redirect domains (e.g., AV update URLs, banking sites)."
Invoke-CollectBlock $f {
    $hostsPath = "$env:SystemRoot\System32\drivers\etc\hosts"
    "Path: $hostsPath"
    ""
    Get-Content $hostsPath -ErrorAction SilentlyContinue
}

# ===========================================================================
# SECTION 5/8 -- STORAGE
# ===========================================================================
Write-Host "[5/8] Collecting storage information..." -ForegroundColor Green

# 5a. disk_usage.txt
$f = Join-Path $DiagPath "05_storage\disk_usage.txt"
Write-FileHeader $f "Disk / Volume Usage" `
    "All logical drives and volumes with size, free space, and filesystem type." `
    "Flag volumes below 10% free (may cause performance issues or log truncation)."
Invoke-CollectBlock $f {
    "--- Logical Disks ---"
    Get-CimInstance Win32_LogicalDisk | Format-Table DeviceID, DriveType, FileSystem,
        @{N="Size(GB)";E={[math]::Round($_.Size/1GB,2)}},
        @{N="FreeGB";E={[math]::Round($_.FreeSpace/1GB,2)}},
        @{N="Free%";E={if($_.Size -gt 0){[math]::Round(($_.FreeSpace/$_.Size)*100,1)}else{0}}},
        VolumeName -AutoSize
    ""
    "--- Volumes (Get-Volume) ---"
    Get-Volume | Format-Table DriveLetter, FriendlyName, FileSystem, HealthStatus, OperationalStatus,
        @{N="Size(GB)";E={[math]::Round($_.Size/1GB,2)}},
        @{N="Free(GB)";E={[math]::Round($_.SizeRemaining/1GB,2)}} -AutoSize
    ""
    "--- Disk Partitions ---"
    Get-Partition | Format-Table DiskNumber, PartitionNumber, DriveLetter,
        @{N="Size(GB)";E={[math]::Round($_.Size/1GB,2)}}, Type, IsActive, IsSystem -AutoSize
}

# 5b. disk_health.txt
$f = Join-Path $DiagPath "05_storage\disk_health.txt"
Write-FileHeader $f "Physical Disk Health" `
    "Physical disk SMART status, reliability data, and disk model/serial information." `
    "HealthStatus 'Unhealthy' or 'Warning' requires immediate attention. Check PredictFailure."
Invoke-CollectBlock $f {
    "--- Physical Disks ---"
    Get-PhysicalDisk | Format-List FriendlyName, MediaType, OperationalStatus, HealthStatus,
        @{N="Size(GB)";E={[math]::Round($_.Size/1GB,2)}}, BusType, FirmwareVersion, SerialNumber, UniqueId
    ""
    "--- Disk Reliability ---"
    Get-Disk | ForEach-Object {
        $disk = $_
        "Disk $($disk.Number): $($disk.FriendlyName)"
        "  OperationalStatus: $($disk.OperationalStatus)  HealthStatus: $($disk.HealthStatus)"
        $rel = $disk | Get-StorageReliabilityCounter -ErrorAction SilentlyContinue
        if ($rel) {
            "  ReadErrorsTotal : $($rel.ReadErrorsTotal)"
            "  WriteErrorsTotal: $($rel.WriteErrorsTotal)"
            "  Temperature (C) : $($rel.Temperature)"
            "  Wear (%)        : $($rel.Wear)"
        } else {
            "  (StorageReliabilityCounter not available for this disk)"
        }
        ""
    }
    "--- Win32_DiskDrive ---"
    Get-CimInstance Win32_DiskDrive | Format-List Name, Model, Size, MediaType, SerialNumber,
        Status, PredictFailure, Partitions, BytesPerSector
}

# 5c. volume_shadows.txt
$f = Join-Path $DiagPath "05_storage\volume_shadows.txt"
Write-FileHeader $f "Volume Shadow Copies (VSS)" `
    "List of all VSS shadow copies with creation time and volume." `
    "Ransomware commonly deletes shadow copies. Absence of shadows on critical volumes is a red flag."
Invoke-CollectBlock $f {
    "--- vssadmin list shadows ---"
    vssadmin list shadows 2>&1
    ""
    "--- Win32_ShadowCopy ---"
    Get-CimInstance Win32_ShadowCopy | Sort-Object InstallDate -Descending |
        Format-Table ID, VolumeName, InstallDate, SetID -AutoSize
}

# 5d. pagefile.txt
$f = Join-Path $DiagPath "05_storage\pagefile.txt"
Write-FileHeader $f "Pagefile Configuration" `
    "Pagefile (virtual memory) location, configured size, and current usage." `
    "Pagefile at 100% usage indicates memory pressure. System-managed vs. manual size config."
Invoke-CollectBlock $f {
    "--- Pagefile Configuration ---"
    Get-CimInstance Win32_PageFileSetting | Format-List Name, InitialSize, MaximumSize
    ""
    "--- Pagefile Current Usage ---"
    Get-CimInstance Win32_PageFileUsage | Format-List Name, CurrentUsage, PeakUsage, AllocatedBaseSize
    ""
    "--- Virtual Memory Summary ---"
    $os = Get-CimInstance Win32_OperatingSystem
    "TotalVirtualMemorySize : $([math]::Round($os.TotalVirtualMemorySize / 1MB, 2)) GB"
    "FreeVirtualMemory      : $([math]::Round($os.FreeVirtualMemory / 1MB, 2)) GB"
    "TotalVisibleMemorySize : $([math]::Round($os.TotalVisibleMemorySize / 1MB, 2)) GB"
    "FreePhysicalMemory     : $([math]::Round($os.FreePhysicalMemory / 1MB, 2)) GB"
}

# ===========================================================================
# SECTION 6/8 -- SECURITY
# ===========================================================================
Write-Host "[6/8] Collecting security information..." -ForegroundColor Green

# 6a. local_users.txt
$f = Join-Path $DiagPath "06_security\local_users.txt"
Write-FileHeader $f "Local User Accounts" `
    "All local user accounts with enabled status, last logon, and password info." `
    "Look for unexpected accounts, accounts with 'PasswordNeverExpires', or recently created accounts."
Invoke-CollectBlock $f {
    "--- Get-LocalUser ---"
    Get-LocalUser | Sort-Object Name | Format-List Name, Enabled, FullName, Description,
        LastLogon, PasswordLastSet, PasswordExpires, PasswordNeverExpires,
        UserMayChangePassword, AccountExpires, SID
    ""
    "--- net user (extended detail) ---"
    $users = Get-LocalUser | Select-Object -ExpandProperty Name
    foreach ($u in $users) {
        "--- net user $u ---"
        net user $u 2>&1
        ""
    }
}

# 6b. local_groups.txt
$f = Join-Path $DiagPath "06_security\local_groups.txt"
Write-FileHeader $f "Local Groups and Members" `
    "All local groups and their members." `
    "Administrators group is critical. Verify all members are expected. Look for unexpected accounts in privileged groups."
Invoke-CollectBlock $f {
    $groups = Get-LocalGroup | Sort-Object Name
    foreach ($g in $groups) {
        "=== $($g.Name) ==="
        $g | Format-List Name, Description, SID
        "Members:"
        Get-LocalGroupMember -Group $g.Name -ErrorAction SilentlyContinue |
            Format-Table Name, ObjectClass, PrincipalSource -AutoSize
        ""
    }
}

# 6c. current_sessions.txt
$f = Join-Path $DiagPath "06_security\current_sessions.txt"
Write-FileHeader $f "Current Logon Sessions" `
    "Currently logged-on users and active logon sessions." `
    "Check for unexpected interactive sessions or elevated sessions that should not be active."
Invoke-CollectBlock $f {
    "--- quser (active sessions) ---"
    quser 2>&1
    ""
    "--- Win32_LogonSession ---"
    Get-CimInstance Win32_LogonSession | Sort-Object StartTime -Descending |
        Format-Table LogonId, LogonType, StartTime, AuthenticationPackage -AutoSize
    ""
    "--- Win32_ComputerSystem (logged on users) ---"
    Get-CimInstance Win32_ComputerSystem | Select-Object UserName, Domain | Format-List
    ""
    "--- whoami /all ---"
    whoami /all 2>&1
}

# 6d. audit_policy.txt
$f = Join-Path $DiagPath "06_security\audit_policy.txt"
Write-FileHeader $f "Audit Policy" `
    "Current audit policy settings for all categories (via auditpol)." `
    "Verify Account Logon, Logon/Logoff, and Privilege Use audit both Success and Failure."
Invoke-CollectBlock $f {
    "--- auditpol /get /category:* ---"
    auditpol /get /category:* 2>&1
}

# 6e. antivirus_status.txt
$f = Join-Path $DiagPath "06_security\antivirus_status.txt"
Write-FileHeader $f "Antivirus / Security Product Status" `
    "Installed AV and security products via WMI SecurityCenter2." `
    "productState encodes enabled/disabled and up-to-date status. Multiple conflicting AVs can cause coverage gaps."
Invoke-CollectBlock $f {
    "--- AntiVirusProduct / FirewallProduct (SecurityCenter2) ---"
    if ($IsServerOS) {
        "SecurityCenter2 WMI namespace is not available on Windows Server editions."
        "Use Windows Defender status below instead."
    } else {
        $avProducts = Get-CimInstance -Namespace "root/SecurityCenter2" -ClassName AntiVirusProduct -ErrorAction SilentlyContinue
        if ($avProducts) {
            $avProducts | ForEach-Object {
                $state = $_.productState
                $rtEnabled   = ($state -band 0x1000) -ne 0
                $defUpToDate = ($state -band 0x0010) -eq 0
                [PSCustomObject]@{
                    DisplayName         = $_.displayName
                    ProductState        = "0x{0:X6}" -f $state
                    RealtimeEnabled     = $rtEnabled
                    DefinitionsUpToDate = $defUpToDate
                    PathToExe           = $_.pathToSignedProductExe
                    Timestamp           = $_.timestamp
                } | Format-List
            }
        } else {
            "No AntiVirus products found via SecurityCenter2."
        }
        ""
        "--- FirewallProduct (SecurityCenter2) ---"
        Get-CimInstance -Namespace "root/SecurityCenter2" -ClassName FirewallProduct -ErrorAction SilentlyContinue |
            Format-List displayName, productState, pathToSignedProductExe
    }
    ""
    "--- Windows Defender Status (Get-MpComputerStatus) ---"
    $mpStatus = Get-MpComputerStatus -ErrorAction SilentlyContinue
    if ($mpStatus) {
        $mpStatus | Format-List AMRunningMode, AMServiceEnabled, AntivirusEnabled,
            AntispywareEnabled, RealTimeProtectionEnabled, NISEnabled,
            AntivirusSignatureLastUpdated, AntispywareSignatureLastUpdated,
            QuickScanAge, FullScanAge, LastFullScanSource, TamperProtectionSource
    } else { "Windows Defender status not available." }
    ""
    "--- Windows Defender Recent Threat Detections ---"
    Get-MpThreatDetection -ErrorAction SilentlyContinue | Sort-Object InitialDetectionTime -Descending |
        Select-Object -First 50 | Format-Table InitialDetectionTime, ThreatName, ActionSuccess, Resources -AutoSize
}

# ===========================================================================
# SECTION 7/8 -- SOFTWARE
# ===========================================================================
Write-Host "[7/8] Collecting software information..." -ForegroundColor Green

# 7a. installed_apps.txt
$f = Join-Path $DiagPath "07_software\installed_apps.txt"
Write-FileHeader $f "Installed Applications" `
    "All installed software from both 64-bit and 32-bit registry hives." `
    "Sort by InstallDate for recently installed software. Look for unexpected or unsigned software."
Invoke-CollectBlock $f {
    $uninstallPaths = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*",
        "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*"
    )
    $apps = foreach ($path in $uninstallPaths) {
        Get-ItemProperty $path -ErrorAction SilentlyContinue |
            Where-Object { $_.DisplayName } |
            Select-Object DisplayName, DisplayVersion, Publisher, InstallDate,
                InstallLocation, UninstallString,
                @{N="Hive";E={ if ($path -like "*WOW*") {"32-bit"} elseif ($path -like "*HKCU*") {"User"} else {"64-bit"} }}
    }
    "--- Summary Table ---"
    $apps | Sort-Object DisplayName | Format-Table DisplayName, DisplayVersion, Publisher, InstallDate, Hive -AutoSize
    ""
    "--- Full Details (sorted by InstallDate desc) ---"
    $apps | Sort-Object InstallDate -Descending | Format-List
}

# 7b. running_browsers.txt
$f = Join-Path $DiagPath "07_software\running_browsers.txt"
Write-FileHeader $f "Browser Processes" `
    "Detected browser processes and their versions." `
    "Outdated browsers are common attack vectors. Browsers running unexpected child processes may indicate exploitation."
Invoke-CollectBlock $f {
    $browserNames = @("chrome","msedge","firefox","iexplore","opera","brave","vivaldi","safari")
    $found = $false
    foreach ($name in $browserNames) {
        $procs = Get-Process -Name $name -ErrorAction SilentlyContinue
        if ($procs) {
            $found = $true
            foreach ($p in $procs) {
                [PSCustomObject]@{
                    Browser   = $p.Name
                    PID       = $p.Id
                    Path      = $p.Path
                    Version   = if ($p.Path) { (Get-Item $p.Path -ErrorAction SilentlyContinue).VersionInfo.ProductVersion } else { "N/A" }
                    "CPU(s)"  = [math]::Round($p.TotalProcessorTime.TotalSeconds, 2)
                    "Mem(MB)" = [math]::Round($p.WorkingSet64 / 1MB, 1)
                } | Format-List
            }
        }
    }
    if (-not $found) { "No browser processes currently running." }
}

# 7c. qemu_guest_agent.txt
$f = Join-Path $DiagPath "07_software\qemu_guest_agent.txt"
Write-FileHeader $f "QEMU Guest Agent (KubeVirt)" `
    "Status and configuration of the QEMU Guest Agent service (qemu-ga), relevant for KubeVirt VMs." `
    "Agent must be running for KubeVirt VM lifecycle operations (shutdown, snapshot quiesce) to work correctly."
Invoke-CollectBlock $f {
    "--- QEMU-GA Service Status ---"
    $svc = Get-Service -Name "QEMU-GA" -ErrorAction SilentlyContinue
    if ($svc) {
        $svc | Format-List Name, DisplayName, Status, StartType
    } else {
        "QEMU-GA service not found. This may not be a KubeVirt/QEMU VM, or the agent is not installed."
    }
    ""
    "--- QEMU-GA Process ---"
    Get-Process -Name "qemu-ga" -ErrorAction SilentlyContinue | Format-List Name, Id, Path, CPU, WorkingSet
    ""
    "--- QEMU-GA via WMI (any qemu service) ---"
    Get-CimInstance Win32_Service | Where-Object { $_.Name -like "*qemu*" -or $_.DisplayName -like "*qemu*" } |
        Format-List Name, DisplayName, State, PathName, StartName
    ""
    "--- VirtIO Drivers ---"
    Get-CimInstance Win32_PnPEntity | Where-Object { $_.Name -like "*VirtIO*" -or $_.Manufacturer -like "*Red Hat*" } |
        Format-Table Name, Status, Manufacturer, DeviceID -AutoSize
}

# ===========================================================================
# SECTION 8/8 -- CRASH REPORTS
# ===========================================================================
Write-Host "[8/8] Collecting crash reports and WER data..." -ForegroundColor Green

# 8a. wer_reports.txt
$f = Join-Path $DiagPath "08_crash_reports\wer_reports.txt"
Write-FileHeader $f "Windows Error Reporting (WER) Reports" `
    "WER report directories and Report.wer summaries for application crashes." `
    "Each report contains crash details including faulting module and exception code. Critical for diagnosing app crashes."
Invoke-CollectBlock $f {
    $werPaths = @(
        "$env:ProgramData\Microsoft\Windows\WER\ReportQueue",
        "$env:ProgramData\Microsoft\Windows\WER\ReportArchive",
        "$env:LOCALAPPDATA\Microsoft\Windows\WER\ReportQueue",
        "$env:LOCALAPPDATA\Microsoft\Windows\WER\ReportArchive"
    )
    foreach ($werPath in $werPaths) {
        "=== $werPath ==="
        if (Test-Path $werPath) {
            $reports = Get-ChildItem $werPath -Directory -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending
            if ($reports) {
                "Found $($reports.Count) report(s):"
                foreach ($report in $reports) {
                    "  [$($report.LastWriteTime.ToString('yyyy-MM-dd HH:mm:ss'))] $($report.Name)"
                    $werFile = Join-Path $report.FullName "Report.wer"
                    if (Test-Path $werFile) {
                        "  --- Report.wer ---"
                        Get-Content $werFile -ErrorAction SilentlyContinue | ForEach-Object { "    $_" }
                    }
                    ""
                }
            } else { "  (no reports found)" }
        } else { "  (path not found)" }
        ""
    }
}

# 8b. minidumps.txt
$f = Join-Path $DiagPath "08_crash_reports\minidumps.txt"
Write-FileHeader $f "Minidump Files" `
    "List of BSOD/kernel minidump files in C:\Windows\Minidump with timestamps." `
    "Each .dmp file corresponds to a Blue Screen of Death. Use WinDbg or !analyze to read them. Recent dumps indicate instability."
Invoke-CollectBlock $f {
    "--- C:\Windows\Minidump ---"
    $dumpPath = "$env:SystemRoot\Minidump"
    if (Test-Path $dumpPath) {
        $dumps = Get-ChildItem $dumpPath -Filter "*.dmp" -ErrorAction SilentlyContinue | Sort-Object LastWriteTime -Descending
        if ($dumps) {
            "Found $($dumps.Count) minidump file(s):"
            $dumps | Format-Table Name, LastWriteTime, @{N="Size(KB)";E={[math]::Round($_.Length/1KB,1)}} -AutoSize
        } else { "No .dmp files found in $dumpPath" }
    } else { "Minidump directory not found: $dumpPath" }
    ""
    "--- Memory.dmp (full kernel dump) ---"
    $fullDump = "$env:SystemRoot\Memory.dmp"
    if (Test-Path $fullDump) {
        $d = Get-Item $fullDump
        "Found: $($d.FullName) | Size: $([math]::Round($d.Length/1GB,2)) GB | Modified: $($d.LastWriteTime)"
    } else { "No Memory.dmp found at $env:SystemRoot\Memory.dmp" }
    ""
    "--- Crash Dump Settings (registry) ---"
    $crashKey = "HKLM:\SYSTEM\CurrentControlSet\Control\CrashControl"
    Get-ItemProperty -Path $crashKey -ErrorAction SilentlyContinue |
        Select-Object CrashDumpEnabled, DumpFile, MiniDumpDir, AutoReboot | Format-List
}

# ===========================================================================
# SUMMARY_FOR_AI.txt  -- Condensed report optimized for AI-assisted analysis
# ===========================================================================
Write-Host "" 
Write-Host "Generating SUMMARY_FOR_AI.txt..." -ForegroundColor Cyan
$aiFile = Join-Path $DiagPath "SUMMARY_FOR_AI.txt"

$aiLines = [System.Collections.Generic.List[string]]::new()
function AI { param([string]$line = "") $aiLines.Add($line) }

AI "================================================================================"
AI "  WINDOWS GUEST DIAGNOSTIC SUMMARY -- FOR AI-ASSISTED ANALYSIS"
AI "================================================================================"
AI "  Computer    : $ComputerName"
AI "  OS          : $OSCaption"
AI "  Build       : $OSBuild  |  Type: $(if($IsServerOS){'Server'}else{'Desktop'})"
AI "  Collected   : $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')  [Timezone: $(([System.TimeZoneInfo]::Local).DisplayName)]"
AI "  Time Range  : $($dtStart.ToString('yyyy-MM-dd HH:mm')) -> $($dtEnd.ToString('yyyy-MM-dd HH:mm'))"
AI "  Run As Admin: $IsAdmin"
AI "================================================================================"
AI ""
AI "------------------------------------------------------------------------------"
AI "  HOW TO USE"
AI "------------------------------------------------------------------------------"
AI "  1. Share this file with an AI assistant (ChatGPT, Claude, Gemini, etc.)"
AI "  2. Use the prompt template in the next section"
AI "  3. If the AI needs more detail, share the specific .txt file from the ZIP"
AI ""
AI "------------------------------------------------------------------------------"
AI "  PROMPT TEMPLATE (copy and paste to AI, then attach this file)"
AI "------------------------------------------------------------------------------"
AI ""
AI "You are an expert Windows systems administrator performing incident analysis."
AI "The attached file is a diagnostic summary from a Windows VM (running in a"
AI "KubeVirt virtualization environment). Please analyze and provide:"
AI "  1. Overall system health assessment"
AI "  2. Root cause of reported issues (if any)"
AI "  3. Security concerns"
AI "  4. Recommended next steps"
AI ""
AI "Incident context: [USER: describe the symptoms here, e.g. 'high CPU around 09:30']"
AI ""
AI "If you need more detail on any area, ask me to share the specific log file"
AI "from the diagnostic ZIP package (e.g. '02_event_logs/system_events.txt')."
AI ""
AI "------------------------------------------------------------------------------"
AI "  [1] SYSTEM IDENTITY"
AI "------------------------------------------------------------------------------"
try {
    $osObj  = Get-CimInstance Win32_OperatingSystem
    $uptime = $osObj.LocalDateTime - $osObj.LastBootUpTime
    $cs     = Get-CimInstance Win32_ComputerSystem
    $cpu    = Get-CimInstance Win32_Processor | Select-Object -First 1
    $totalRAM = [math]::Round($osObj.TotalVisibleMemorySize / 1MB, 1)
    $freeRAM  = [math]::Round($osObj.FreePhysicalMemory  / 1MB, 1)
    $usedRAMpct = [math]::Round((($osObj.TotalVisibleMemorySize - $osObj.FreePhysicalMemory) / $osObj.TotalVisibleMemorySize) * 100, 1)
    AI "  Hostname    : $ComputerName"
    AI "  OS          : $($osObj.Caption) Build $($osObj.BuildNumber)"
    AI "  Last Boot   : $($osObj.LastBootUpTime.ToString('yyyy-MM-dd HH:mm:ss'))"
    AI "  Uptime      : $($uptime.Days)d $($uptime.Hours)h $($uptime.Minutes)m"
    AI "  CPU         : $($cpu.Name) | Cores: $($cpu.NumberOfCores) | Logical: $($cpu.NumberOfLogicalProcessors)"
    AI "  RAM Total   : $totalRAM GB  |  Free: $freeRAM GB  |  Used: $usedRAMpct%"
    AI "  Domain      : $($cs.Domain)  |  Model: $($cs.Model)"
    $tz = [System.TimeZoneInfo]::Local
    AI "  TimeZone    : $($tz.DisplayName)  (UTC$($tz.BaseUtcOffset.Hours.ToString('+0;-#')))"
} catch { AI "  [ERROR collecting system identity: $_]" }
AI ""

AI "------------------------------------------------------------------------------"
AI "  [2] [!]  AUTO-DETECTED ALERTS"
AI "------------------------------------------------------------------------------"
$alertCount = 0

# Disk space alerts
try {
    Get-CimInstance Win32_LogicalDisk | Where-Object { $_.Size -gt 0 -and $_.DriveType -eq 3 } | ForEach-Object {
        $freePct = [math]::Round(($_.FreeSpace / $_.Size) * 100, 1)
        $freeGB  = [math]::Round($_.FreeSpace / 1GB, 1)
        if ($freePct -lt 10) {
            AI "  [DISK CRITICAL] $($_.DeviceID) is $([math]::Round(100-$freePct,1))% full -- only $freeGB GB remaining!"
            $alertCount++
        } elseif ($freePct -lt 20) {
            AI "  [DISK WARNING]  $($_.DeviceID) is $([math]::Round(100-$freePct,1))% full -- $freeGB GB remaining"
            $alertCount++
        }
    }
} catch {}

# RAM usage alert
try {
    $osObj2 = Get-CimInstance Win32_OperatingSystem
    $ramUsed = [math]::Round((($osObj2.TotalVisibleMemorySize - $osObj2.FreePhysicalMemory) / $osObj2.TotalVisibleMemorySize) * 100, 1)
    if ($ramUsed -gt 90) { AI "  [RAM CRITICAL]  Physical memory usage is $ramUsed%"; $alertCount++ }
    elseif ($ramUsed -gt 80) { AI "  [RAM WARNING]   Physical memory usage is $ramUsed%"; $alertCount++ }
} catch {}

# Pagefile usage
try {
    $pf = Get-CimInstance Win32_PageFileUsage | Select-Object -First 1
    if ($pf -and $pf.AllocatedBaseSize -gt 0) {
        $pfPct = [math]::Round(($pf.CurrentUsage / $pf.AllocatedBaseSize) * 100, 1)
        if ($pfPct -gt 80) { AI "  [PAGEFILE WARN] Pagefile usage is $pfPct% ($($pf.CurrentUsage) MB / $($pf.AllocatedBaseSize) MB)"; $alertCount++ }
    }
} catch {}

# QEMU-GA status
try {
    $qemuSvc = Get-Service -Name "QEMU-GA" -ErrorAction SilentlyContinue
    if (-not $qemuSvc) {
        AI "  [QEMU-GA MISSING]  QEMU Guest Agent is NOT installed -- KubeVirt memory metrics UNRELIABLE"
        $alertCount++
    } elseif ($qemuSvc.Status -ne "Running") {
        AI "  [QEMU-GA STOPPED]  QEMU Guest Agent is installed but NOT running (Status: $($qemuSvc.Status))"
        $alertCount++
    }
} catch {}

# Auto-start services that are stopped
try {
    $stoppedSvcs = Get-CimInstance Win32_Service | Where-Object {
        $_.StartMode -eq "Auto" -and $_.State -ne "Running" -and
        $_.Name -notin @("MapsBroker","NetTcpPortSharing","RemoteRegistry","shpamsvc","tzautoupdate","XblAuthManager","XblGameSave","XboxGipSvc","XboxNetApiSvc")
    }
    foreach ($svc in $stoppedSvcs) {
        AI "  [SVC STOPPED]   Auto-start service not running: '$($svc.Name)' ($($svc.DisplayName))"
        $alertCount++
    }
} catch {}

# WER crash reports in range
try {
    $werDirs = @(
        "$env:ProgramData\Microsoft\Windows\WER\ReportQueue",
        "$env:ProgramData\Microsoft\Windows\WER\ReportArchive",
        "$env:LOCALAPPDATA\Microsoft\Windows\WER\ReportQueue",
        "$env:LOCALAPPDATA\Microsoft\Windows\WER\ReportArchive"
    )
    $crashCount = 0
    foreach ($dir in $werDirs) {
        if (Test-Path $dir) {
            $crashCount += (Get-ChildItem $dir -Directory -ErrorAction SilentlyContinue | Where-Object {
                $_.CreationTime -ge $dtStart -and $_.CreationTime -le $dtEnd }).Count
        }
    }
    if ($crashCount -gt 0) { AI "  [CRASH REPORTS] $crashCount WER crash report(s) found in the specified time range"; $alertCount++ }
} catch {}

# Minidumps in range
try {
    $dumpPath = "$env:SystemRoot\Minidump"
    if (Test-Path $dumpPath) {
        $recentDumps = (Get-ChildItem $dumpPath -Filter "*.dmp" -ErrorAction SilentlyContinue | Where-Object { $_.LastWriteTime -ge $dtStart -and $_.LastWriteTime -le $dtEnd }).Count
        if ($recentDumps -gt 0) { AI "  [BSOD DETECTED] $recentDumps minidump file(s) in time range -- system experienced BSOD(s)"; $alertCount++ }
    }
} catch {}

# Defender real-time protection
try {
    $mp = Get-MpComputerStatus -ErrorAction SilentlyContinue
    if ($mp -and -not $mp.RealTimeProtectionEnabled) { AI "  [SECURITY] Windows Defender real-time protection is DISABLED"; $alertCount++ }
    if ($mp -and $mp.AntivirusSignatureAge -gt 7) { AI "  [SECURITY] Defender signatures are $($mp.AntivirusSignatureAge) days old (>7 days)"; $alertCount++ }
} catch {}

if ($alertCount -eq 0) { AI "  No critical alerts auto-detected." }
AI ""

AI "------------------------------------------------------------------------------"
AI "  [3] CRITICAL EVENTS IN TIME RANGE (Level >= Warning)"
AI "  Range: $($dtStart.ToString('yyyy-MM-dd HH:mm')) -> $($dtEnd.ToString('yyyy-MM-dd HH:mm'))"
AI "------------------------------------------------------------------------------"
try {
    # Priority Event IDs (OOM, BSOD, Crash, Hang, Kernel)
    $priorityIds = @(41, 6008, 2004, 1001, 1002, 1000, 7034, 7031, 7036, 1074)
    $priorityEvts = @()
    foreach ($log in @("System","Application")) {
        $evts = Get-WinEvent -FilterHashtable @{LogName=$log; StartTime=$dtStart; EndTime=$dtEnd; Level=1,2,3} -ErrorAction SilentlyContinue
        if ($evts) { $priorityEvts += $evts }
    }
    # Include priority IDs even if they're not Error/Critical
    foreach ($log in @("System","Application")) {
        $evts = Get-WinEvent -FilterHashtable @{LogName=$log; StartTime=$dtStart; EndTime=$dtEnd; Id=$priorityIds} -ErrorAction SilentlyContinue
        if ($evts) { $priorityEvts += $evts }
    }
    $priorityEvts = $priorityEvts | Sort-Object TimeCreated -Descending | Select-Object -Unique TimeCreated, Id, LevelDisplayName, LogName, ProviderName, Message
    if ($priorityEvts) {
        AI "  Total: $($priorityEvts.Count) event(s)"
        AI ""
        foreach ($e in ($priorityEvts | Select-Object -First 50)) {
            $shortMsg = ($e.Message -replace "`r`n|`n", " ").Substring(0, [Math]::Min(($e.Message -replace "`r`n|`n", " ").Length, 250))
            AI "  [$($e.TimeCreated.ToString('yyyy-MM-dd HH:mm:ss'))] [ID:$($e.Id)] [$($e.LevelDisplayName)] [$($e.LogName)] $shortMsg"
        }
        if ($priorityEvts.Count -gt 50) { AI "  ... ($(($priorityEvts.Count - 50)) more events -- see 02_event_logs/ for full list)" }
    } else {
        AI "  No Warning/Error/Critical events found in the specified time range."
    }
} catch { AI "  [ERROR collecting events: $_]" }
AI ""

AI "------------------------------------------------------------------------------"
AI "  [4] TOP 15 PROCESSES BY MEMORY (Working Set)"
AI "------------------------------------------------------------------------------"
try {
    $procs = Get-CimInstance Win32_Process | Sort-Object WorkingSetSize -Descending | Select-Object -First 15
    AI "  {'Rank',-4} {'PID',-7} {'Name',-30} {'WS(MB)',-10} {'CPU(s)',-10} Owner"
    AI "  $('-'*80)"
    $rank = 1
    foreach ($p in $procs) {
        $owner = $p.GetOwner()
        $ownerStr = if ($owner.User) { "$($owner.Domain)\$($owner.User)" } else { "N/A" }
        $cpuSec = try { [math]::Round($p.UserModeTime / 1e7 + $p.KernelModeTime / 1e7, 1) } catch { "N/A" }
        $wsMB = [math]::Round($p.WorkingSetSize / 1MB, 1)
        AI "  $('{0,-4}' -f $rank) $('{0,-7}' -f $p.ProcessId) $('{0,-30}' -f $p.Name) $('{0,-10}' -f $wsMB) $('{0,-10}' -f $cpuSec) $ownerStr"
        $rank++
    }
} catch { AI "  [ERROR: $_]" }
AI ""

AI "------------------------------------------------------------------------------"
AI "  [5] TOP 15 PROCESSES BY CPU TIME"
AI "------------------------------------------------------------------------------"
try {
    $cpuProcs = Get-CimInstance Win32_Process |
        Select-Object ProcessId, Name, WorkingSetSize,
            @{N="TotalCPUsec";E={[math]::Round(($_.UserModeTime + $_.KernelModeTime)/1e7,1)}} |
        Sort-Object TotalCPUsec -Descending | Select-Object -First 15
    AI "  {'Rank',-4} {'PID',-7} {'Name',-30} {'CPU(s)',-12} WS(MB)"
    AI "  $('-'*70)"
    $rank = 1
    foreach ($p in $cpuProcs) {
        AI "  $('{0,-4}' -f $rank) $('{0,-7}' -f $p.ProcessId) $('{0,-30}' -f $p.Name) $('{0,-12}' -f $p.TotalCPUsec) $([math]::Round($p.WorkingSetSize/1MB,1))"
        $rank++
    }
} catch { AI "  [ERROR: $_]" }
AI ""

AI "------------------------------------------------------------------------------"
AI "  [6] STORAGE STATUS"
AI "------------------------------------------------------------------------------"
try {
    Get-CimInstance Win32_LogicalDisk | Where-Object { $_.DriveType -eq 3 } | ForEach-Object {
        $freePct = if ($_.Size -gt 0) { [math]::Round(($_.FreeSpace / $_.Size) * 100, 1) } else { 0 }
        $freeGB  = [math]::Round($_.FreeSpace / 1GB, 1)
        $totalGB = [math]::Round($_.Size / 1GB, 1)
        $flag = if ($freePct -lt 10) { " <-- CRITICAL" } elseif ($freePct -lt 20) { " <-- WARNING" } else { "" }
        AI "  $($_.DeviceID)  Total: $totalGB GB  Free: $freeGB GB ($freePct%)$flag"
    }
} catch { AI "  [ERROR: $_]" }
try {
    $pfUsage = Get-CimInstance Win32_PageFileUsage | Select-Object -First 1
    if ($pfUsage) {
        AI "  Pagefile: $($pfUsage.CurrentUsage) MB used / $($pfUsage.AllocatedBaseSize) MB allocated (Peak: $($pfUsage.PeakUsage) MB)"
    }
} catch {}
AI ""

AI "------------------------------------------------------------------------------"
AI "  [7] SERVICES -- Auto-start but STOPPED"
AI "------------------------------------------------------------------------------"
try {
    $knownOk = @("MapsBroker","NetTcpPortSharing","RemoteRegistry","shpamsvc","tzautoupdate",
                 "XblAuthManager","XblGameSave","XboxGipSvc","XboxNetApiSvc","wisvc",
                 "WbioSrvc","PimIndexMaintenanceSvc","OneSyncSvc")
    $stopped = Get-CimInstance Win32_Service | Where-Object {
        $_.StartMode -eq "Auto" -and $_.State -ne "Running" -and $_.Name -notin $knownOk
    }
    if ($stopped) {
        foreach ($s in $stopped) {
            AI "  [STOPPED] $($s.Name.PadRight(35)) $($s.DisplayName)"
        }
    } else { AI "  All expected auto-start services are running." }
} catch { AI "  [ERROR: $_]" }
AI ""

AI "------------------------------------------------------------------------------"
AI "  [8] WER CRASH REPORTS (in time range)"
AI "------------------------------------------------------------------------------"
try {
    $werDirs = @(
        "$env:ProgramData\Microsoft\Windows\WER\ReportQueue",
        "$env:ProgramData\Microsoft\Windows\WER\ReportArchive",
        "$env:LOCALAPPDATA\Microsoft\Windows\WER\ReportQueue",
        "$env:LOCALAPPDATA\Microsoft\Windows\WER\ReportArchive"
    )
    $allCrashes = @()
    foreach ($dir in $werDirs) {
        if (Test-Path $dir) {
            $allCrashes += Get-ChildItem $dir -Directory -ErrorAction SilentlyContinue |
                Where-Object { $_.CreationTime -ge $dtStart -and $_.CreationTime -le $dtEnd }
        }
    }
    if ($allCrashes) {
        foreach ($r in ($allCrashes | Sort-Object CreationTime -Descending | Select-Object -First 20)) {
            $werFile = Join-Path $r.FullName "Report.wer"
            $appName = if (Test-Path $werFile) {
                (Get-Content $werFile -ErrorAction SilentlyContinue | Select-String "AppName") -replace ".*AppName.*?=\s*",""
            } else { "(no Report.wer)" }
            AI "  [$($r.CreationTime.ToString('yyyy-MM-dd HH:mm:ss'))] $($r.Name)  App: $appName"
        }
    } else { AI "  No crash reports in the specified time range." }
} catch { AI "  [ERROR: $_]" }
AI ""

AI "------------------------------------------------------------------------------"
AI "  [9] STARTUP ITEMS (non-Microsoft / non-Windows)"
AI "------------------------------------------------------------------------------"
try {
    $runKeys = @(
        "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run",
        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Run",
        "HKCU:\SOFTWARE\Microsoft\Windows\CurrentVersion\Run"
    )
    $startupFound = $false
    foreach ($key in $runKeys) {
        $props = Get-ItemProperty -Path $key -ErrorAction SilentlyContinue
        if ($props) {
            $props.PSObject.Properties | Where-Object { $_.Name -notlike "PS*" } | ForEach-Object {
                $val = $_.Value
                $isMicrosoft = $val -match "Microsoft|Windows\\System32|Windows\\SysWOW64"
                if (-not $isMicrosoft) {
                    AI "  [$key]"
                    AI "    Name : $($_.Name)"
                    AI "    Value: $val"
                    AI ""
                    $startupFound = $true
                }
            }
        }
    }
    if (-not $startupFound) { AI "  No non-Microsoft startup items found in registry Run keys." }
} catch { AI "  [ERROR: $_]" }
AI ""

AI "------------------------------------------------------------------------------"
AI "  [10] ACTIVE CONNECTIONS (ESTABLISHED, non-loopback)"
AI "------------------------------------------------------------------------------"
try {
    $procMap2 = @{}
    Get-Process | ForEach-Object { $procMap2[$_.Id] = $_.Name }
    $conns = Get-NetTCPConnection -State Established -ErrorAction SilentlyContinue |
        Where-Object { $_.RemoteAddress -ne "127.0.0.1" -and $_.RemoteAddress -ne "::1" } |
        Sort-Object RemoteAddress
    if ($conns) {
        AI "  {'LocalAddr:Port',-25} {'RemoteAddr:Port',-25} {'PID',-7} Process"
        AI "  $('-'*75)"
        foreach ($c in ($conns | Select-Object -First 40)) {
            $local  = "$($c.LocalAddress):$($c.LocalPort)"
            $remote = "$($c.RemoteAddress):$($c.RemotePort)"
            $pname  = $procMap2[$c.OwningProcess]
            AI "  $('{0,-25}' -f $local) $('{0,-25}' -f $remote) $('{0,-7}' -f $c.OwningProcess) $pname"
        }
        if ($conns.Count -gt 40) { AI "  ... ($($conns.Count - 40) more -- see 04_network/active_connections.txt)" }
    } else { AI "  No established non-loopback TCP connections." }
} catch { AI "  [ERROR: $_]" }
AI ""

AI "------------------------------------------------------------------------------"
AI "  [11] SECURITY SUMMARY (Event Counts in Time Range)"
AI "------------------------------------------------------------------------------"
try {
    $secIds = @(
        @{Id=4625; Label="Failed Logons (brute force indicator if >10)"},
        @{Id=4720; Label="User Accounts Created"},
        @{Id=4726; Label="User Accounts Deleted"},
        @{Id=4672; Label="Special Privilege Logons (admin-level)"},
        @{Id=1102; Label="Audit Log Cleared (tampering indicator)"}
    )
    foreach ($item in $secIds) {
        try {
            $cnt = (Get-WinEvent -FilterHashtable @{LogName="Security"; Id=$item.Id; StartTime=$dtStart; EndTime=$dtEnd} -ErrorAction Stop).Count
            $flag = ""
            if ($item.Id -eq 4625 -and $cnt -gt 10) { $flag = " <-- SUSPICIOUS" }
            if ($item.Id -eq 1102 -and $cnt -gt 0)  { $flag = " <-- ALERT: LOG TAMPERING" }
            if ($item.Id -eq 4720 -and $cnt -gt 0)  { $flag = " <-- REVIEW REQUIRED" }
            AI "  ID $($item.Id): $cnt event(s)  -- $($item.Label)$flag"
        } catch {
            AI "  ID $($item.Id): (access denied or log unavailable) -- $($item.Label)"
        }
    }
} catch { AI "  [ERROR collecting security summary: $_]" }
AI ""

AI "------------------------------------------------------------------------------"
AI "  [12] INSTALLED SOFTWARE (Last 30 Days)"
AI "------------------------------------------------------------------------------"
try {
    $cutoff = (Get-Date).AddDays(-30).ToString("yyyyMMdd")
    $recentApps = @()
    foreach ($path in @("HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall\*",
                        "HKLM:\SOFTWARE\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*")) {
        $recentApps += Get-ItemProperty $path -ErrorAction SilentlyContinue |
            Where-Object { $_.DisplayName -and $_.InstallDate -ge $cutoff } |
            Select-Object DisplayName, DisplayVersion, Publisher, InstallDate
    }
    if ($recentApps) {
        $recentApps | Sort-Object InstallDate -Descending | ForEach-Object {
            AI "  [$($_.InstallDate)] $($_.DisplayName) $($_.DisplayVersion)  by $($_.Publisher)"
        }
    } else { AI "  No software installed in the past 30 days." }
} catch { AI "  [ERROR: $_]" }
AI ""

AI "------------------------------------------------------------------------------"
AI "  END OF SUMMARY -- See individual files in ZIP for full details"
AI "------------------------------------------------------------------------------"

$aiLines | Set-Content -Path $aiFile -Encoding UTF8
Write-Host "  -> SUMMARY_FOR_AI.txt written ($($aiLines.Count) lines)" -ForegroundColor Green

# ===========================================================================
# COMPRESS OUTPUT
# ===========================================================================
Write-Host ""
Write-Host "Compressing output to ZIP..." -ForegroundColor Cyan
$zipPath = Join-Path $OutputPath "${DiagRoot}.zip"

try {
    Add-Type -AssemblyName System.IO.Compression.FileSystem
    [System.IO.Compression.ZipFile]::CreateFromDirectory($DiagPath, $zipPath)
    $zipSizeMB = [math]::Round((Get-Item $zipPath).Length / 1MB, 2)
    Write-Host "ZIP created: $zipPath ($zipSizeMB MB)" -ForegroundColor Green
} catch {
    Write-Host "[ERROR] Failed to create ZIP: $($_.Exception.Message)" -ForegroundColor Red
    Write-Host "Raw output directory is still available at: $DiagPath" -ForegroundColor Yellow
    $zipPath = "(ZIP creation failed -- see raw folder: $DiagPath)"
}

# ===========================================================================
# SUMMARY
# ===========================================================================
Write-Host ""
Write-Host "========================================================" -ForegroundColor Cyan
Write-Host "  Collection Summary" -ForegroundColor Cyan
Write-Host "========================================================" -ForegroundColor Cyan
Write-Host "  Computer   : $ComputerName"
Write-Host "  Time Range : $($dtStart.ToString('yyyy-MM-dd HH:mm')) -> $($dtEnd.ToString('yyyy-MM-dd HH:mm'))"
Write-Host "  Output ZIP : $zipPath"
Write-Host "  Raw Folder : $DiagPath"
Write-Host ""

$fileCount = (Get-ChildItem $DiagPath -Recurse -File -ErrorAction SilentlyContinue).Count
Write-Host "  Files collected: $fileCount"
Write-Host ""

if ($PermissionErrors.Count -gt 0) {
    Write-Host "  Permission / Collection Errors ($($PermissionErrors.Count)):" -ForegroundColor Yellow
    foreach ($err in $PermissionErrors) {
        Write-Host "    - $err" -ForegroundColor Yellow
    }
} else {
    Write-Host "  No permission errors encountered." -ForegroundColor Green
}

Write-Host ""
Write-Host "  Sections collected:" -ForegroundColor Cyan
Write-Host "    [1/8] 01_system          - systeminfo, hardware, hotfixes, env vars, BCD"
Write-Host "    [2/8] 02_event_logs      - System, Application, Security, Defender, PS, TaskSched, WU"
Write-Host "    [3/8] 03_processes       - Processes, tree, services, scheduled tasks, startup items"
Write-Host "    [4/8] 04_network         - IP config, connections, ARP, routes, DNS, firewall, hosts"
Write-Host "    [5/8] 05_storage         - Disk usage, health, shadow copies, pagefile"
Write-Host "    [6/8] 06_security        - Users, groups, sessions, audit policy, AV status"
Write-Host "    [7/8] 07_software        - Installed apps, browsers, QEMU Guest Agent"
Write-Host "    [8/8] 08_crash_reports   - WER reports, minidumps"
Write-Host ""
Write-Host "Done." -ForegroundColor Green

維運分析重點

收到 ZIP 後,依以下順序檢查:

  1. 01_system/system_overview.txt — 確認時區,對齊 Prometheus 告警時間
  2. 02_event_logs/system_events.txt — 搜尋 Event ID 2004 / 41 / 6008(記憶體耗盡 / 非預期關機)
  3. 02_event_logs/application_events.txt — 搜尋 Event ID 1002 / 1000(Hang / Crash)
  4. 03_processes/services.txt — 確認是否有服務在事發前後崩潰重啟
  5. 03_processes/scheduled_tasks.txt — 確認事發時間是否有排程任務觸發(備份、掃毒、更新)
  6. 08_crash_reports/wer_reports.txt — 確認是否有程式在事發時段產生 Crash Report