Files
msp/scripts/Get-WindowsUpdateDiagnostics-JSON.ps1
T

320 lines
12 KiB
PowerShell

#Requires -Version 5.1
<#
.SYNOPSIS
Windows Update Diagnostics Script - JSON Output
.DESCRIPTION
Sammelt Windows Update Diagnoseinformationen und gibt sie als JSON aus.
Berücksichtigt TacticalRMM-verwaltete Systeme (AUOptions=1 ist dort normal).
.NOTES
Version: 2.2
Datum: 2026-01-07
Output: JSON
Changelog: Fixed connectivity check - use download.windowsupdate.com instead of SOAP endpoints
#>
$ErrorActionPreference = "SilentlyContinue"
# Detect TacticalRMM Agent
$tacticalInstalled = $false
$tacticalService = Get-Service -Name "tacticalrmm" -ErrorAction SilentlyContinue
if ($tacticalService) {
$tacticalInstalled = $true
}
# Result object
$result = @{
hostname = $env:COMPUTERNAME
timestamp = (Get-Date -Format "yyyy-MM-ddTHH:mm:ss")
managed_by = @{
tactical_rmm = $tacticalInstalled
tactical_service_status = if ($tacticalService) { $tacticalService.Status.ToString() } else { $null }
}
os = @{
caption = ""
build = ""
version = ""
}
services = @()
configuration = @{
wsus_server = $null
au_options = $null
au_options_text = ""
use_wsus = $false
policies = @{}
}
updates = @{
history_count = 0
last_success = $null
days_since_last_success = $null
pending_count = 0
pending_critical = 0
pending_important = 0
pending_driver = 0
pending_list = @()
recent_history = @()
}
storage = @{
software_distribution_mb = 0
download_folder_mb = 0
datastore_mb = 0
}
errors = @()
connectivity = @()
issues = @()
status = "OK"
}
# OS Info
try {
$osInfo = Get-CimInstance Win32_OperatingSystem
$result.os.caption = $osInfo.Caption
$result.os.version = $osInfo.Version
$ntVersion = Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' -ErrorAction SilentlyContinue
$result.os.build = "$([System.Environment]::OSVersion.Version.Build).$($ntVersion.UBR)"
} catch {}
# Services
# Note: BITS and other services start on-demand, so we check StartType not current Status
$serviceList = @(
@{Name="wuauserv"; DisplayName="Windows Update"; Critical=$true; MustRun=$false},
@{Name="bits"; DisplayName="BITS"; Critical=$false; MustRun=$false}, # Starts on demand
@{Name="cryptsvc"; DisplayName="CryptSvc"; Critical=$false; MustRun=$false},
@{Name="msiserver"; DisplayName="MSIServer"; Critical=$false; MustRun=$false},
@{Name="TrustedInstaller"; DisplayName="TrustedInstaller"; Critical=$false; MustRun=$false}
)
foreach ($svc in $serviceList) {
$service = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue
$startType = (Get-CimInstance Win32_Service -Filter "Name='$($svc.Name)'" -ErrorAction SilentlyContinue).StartMode
# Service is OK if: exists AND (running OR start type is Manual/Auto, not Disabled)
$isDisabled = ($startType -eq "Disabled")
$isOk = $service -and (-not $isDisabled)
$svcResult = @{
name = $svc.Name
display_name = $svc.DisplayName
status = if ($service) { $service.Status.ToString() } else { "NotFound" }
start_type = $startType
critical = $svc.Critical
disabled = $isDisabled
ok = $isOk
}
$result.services += $svcResult
# Only flag as issue if service is disabled or not found
if ($svc.Critical -and (-not $service -or $isDisabled)) {
$result.issues += "Service '$($svc.DisplayName)' ist deaktiviert oder nicht vorhanden"
}
}
# Configuration
$wuPolicyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
if (Test-Path $wuPolicyPath) {
$wuPolicy = Get-ItemProperty -Path $wuPolicyPath -ErrorAction SilentlyContinue
$result.configuration.wsus_server = $wuPolicy.WUServer
$result.configuration.use_wsus = ($wuPolicy.UseWUServer -eq 1)
$auPath = "$wuPolicyPath\AU"
if (Test-Path $auPath) {
$auPolicy = Get-ItemProperty -Path $auPath -ErrorAction SilentlyContinue
$result.configuration.au_options = $auPolicy.AUOptions
$result.configuration.policies = @{
no_auto_update = $auPolicy.NoAutoUpdate
au_options = $auPolicy.AUOptions
scheduled_install_day = $auPolicy.ScheduledInstallDay
scheduled_install_time = $auPolicy.ScheduledInstallTime
}
# Translate AUOptions
$auText = switch ($auPolicy.AUOptions) {
1 { "Deaktiviert (RMM-verwaltet)" }
2 { "Benachrichtigen vor Download" }
3 { "Automatisch downloaden, benachrichtigen vor Installation" }
4 { "Automatisch downloaden und installieren" }
5 { "Lokaler Admin kann Einstellung wählen" }
default { "Nicht konfiguriert" }
}
$result.configuration.au_options_text = $auText
# AUOptions=1 is EXPECTED when TacticalRMM is installed (TRMM manages updates)
# Only flag as issue if AUOptions=1 AND no RMM is installed
if ($auPolicy.AUOptions -eq 1 -and -not $tacticalInstalled) {
$result.issues += "Windows Update ist per Policy deaktiviert (AUOptions=1) ohne RMM-Verwaltung"
}
}
}
# Update History and Pending
try {
$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$result.updates.history_count = $Searcher.GetTotalHistoryCount()
# Recent history
if ($result.updates.history_count -gt 0) {
$History = $Searcher.QueryHistory(0, [Math]::Min(10, $result.updates.history_count))
foreach ($Update in $History) {
$resultCode = switch ($Update.ResultCode) {
0 { "NotStarted" }
1 { "InProgress" }
2 { "Succeeded" }
3 { "SucceededWithErrors" }
4 { "Failed" }
5 { "Aborted" }
default { "Unknown" }
}
$result.updates.recent_history += @{
date = $Update.Date.ToString("yyyy-MM-ddTHH:mm:ss")
title = $Update.Title
result = $resultCode
succeeded = ($Update.ResultCode -eq 2)
}
}
# Find last success
$allHistory = $Searcher.QueryHistory(0, $result.updates.history_count)
foreach ($Update in $allHistory) {
if ($Update.ResultCode -eq 2) {
$result.updates.last_success = $Update.Date.ToString("yyyy-MM-ddTHH:mm:ss")
$result.updates.days_since_last_success = [math]::Round(((Get-Date) - $Update.Date).TotalDays)
break
}
}
}
if (-not $result.updates.last_success) {
$result.issues += "Kein erfolgreiches Update in der Historie gefunden"
} elseif ($result.updates.days_since_last_success -gt 60) {
$result.issues += "Letztes erfolgreiches Update vor $($result.updates.days_since_last_success) Tagen"
}
# Pending updates
$SearchResult = $Searcher.Search("IsInstalled=0")
$result.updates.pending_count = $SearchResult.Updates.Count
foreach ($Update in $SearchResult.Updates) {
$severity = if ($Update.MsrcSeverity) { $Update.MsrcSeverity } else { "Unspecified" }
$isDriver = $Update.Categories | Where-Object { $_.Name -like "*Driver*" }
$pendingUpdate = @{
title = $Update.Title
kb = ($Update.KBArticleIDs -join ",")
severity = $severity
size_mb = [math]::Round($Update.MaxDownloadSize / 1MB, 1)
downloaded = $Update.IsDownloaded
is_driver = ($null -ne $isDriver)
categories = @($Update.Categories | ForEach-Object { $_.Name })
}
$result.updates.pending_list += $pendingUpdate
if ($severity -eq "Critical") { $result.updates.pending_critical++ }
elseif ($severity -eq "Important") { $result.updates.pending_important++ }
if ($isDriver) { $result.updates.pending_driver++ }
}
if ($result.updates.pending_critical -gt 0) {
$result.issues += "$($result.updates.pending_critical) kritische Updates ausstehend"
}
} catch {
$result.errors += "Update-Abfrage fehlgeschlagen: $_"
}
# Storage
$sdPath = "$env:SystemRoot\SoftwareDistribution"
if (Test-Path $sdPath) {
$sdSize = (Get-ChildItem -Path $sdPath -Recurse -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
$result.storage.software_distribution_mb = [math]::Round($sdSize / 1MB, 2)
$dlPath = "$sdPath\Download"
if (Test-Path $dlPath) {
$dlSize = (Get-ChildItem -Path $dlPath -Recurse -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
$result.storage.download_folder_mb = [math]::Round($dlSize / 1MB, 2)
}
$dbPath = "$sdPath\DataStore\DataStore.edb"
if (Test-Path $dbPath) {
$result.storage.datastore_mb = [math]::Round((Get-Item $dbPath).Length / 1MB, 2)
if ($result.storage.datastore_mb -gt 500) {
$result.issues += "DataStore.edb sehr groß ($($result.storage.datastore_mb) MB)"
}
}
}
# Event Log Errors
try {
$StartDate = (Get-Date).AddDays(-7)
$WUErrors = Get-WinEvent -FilterHashtable @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-WindowsUpdateClient'
Level = 2,3
StartTime = $StartDate
} -MaxEvents 5 -ErrorAction SilentlyContinue
foreach ($Event in $WUErrors) {
$result.errors += @{
date = $Event.TimeCreated.ToString("yyyy-MM-ddTHH:mm:ss")
level = if ($Event.Level -eq 2) { "Error" } else { "Warning" }
id = $Event.Id
message = ($Event.Message -split "`n")[0]
}
}
} catch {}
# Connectivity
# Note: update.microsoft.com and windowsupdate.microsoft.com are SOAP/WCF services
# They don't respond to HTTP GET requests - use download URLs instead
$testUrls = @(
@{url="https://www.microsoft.com"; name="Microsoft"; critical=$false},
@{url="http://download.windowsupdate.com"; name="Windows Update Download"; critical=$true},
@{url="https://www.catalog.update.microsoft.com"; name="Windows Update Catalog"; critical=$false},
@{url="https://download.microsoft.com"; name="Microsoft Download"; critical=$false}
)
foreach ($test in $testUrls) {
try {
$response = Invoke-WebRequest -Uri $test.url -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
$result.connectivity += @{
name = $test.name
url = $test.url
status = $response.StatusCode
ok = $true
}
} catch {
$result.connectivity += @{
name = $test.name
url = $test.url
status = 0
error = $_.Exception.Message
ok = $false
}
if ($test.critical) {
$result.issues += "Keine Verbindung zu $($test.name)"
}
}
}
# Final status determination
# Critical issues: connectivity problems, critical pending updates, disabled services
# Warning issues: old updates, many pending updates
if ($result.issues.Count -gt 0) {
$hasCritical = $result.issues | Where-Object {
$_ -match "kritisch|keine Verbindung|deaktiviert oder nicht vorhanden|ohne RMM"
}
$result.status = if ($hasCritical) { "CRITICAL" } else { "WARNING" }
} else {
$result.status = "OK"
}
# Add summary
$result.summary = @{
total_issues = $result.issues.Count
is_rmm_managed = $tacticalInstalled
needs_attention = ($result.status -ne "OK")
connectivity_ok = ($result.connectivity | Where-Object { -not $_.ok }).Count -eq 0
}
# Output as JSON
$result | ConvertTo-Json -Depth 5 -Compress