chore: commit uncommitted production changes for v16 migration

This commit is contained in:
Dave
2026-03-07 09:47:25 +01:00
parent 208212a883
commit 1fca135a83
89 changed files with 13362 additions and 635 deletions
@@ -0,0 +1,319 @@
#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
+325
View File
@@ -0,0 +1,325 @@
#Requires -Version 5.1
<#
.SYNOPSIS
Windows Update Diagnostics Script for TacticalRMM
.DESCRIPTION
Sammelt umfassende Diagnoseinformationen zum Windows Update Status.
Ausgabe erfolgt als strukturierter Text fuer TacticalRMM.
.NOTES
Version: 1.0
Datum: 2026-01-07
Fuer: MSP Documentation / TacticalRMM
#>
[CmdletBinding()]
param()
$ErrorActionPreference = "SilentlyContinue"
function Write-Section {
param([string]$Title)
Write-Output ""
Write-Output "=" * 60
Write-Output " $Title"
Write-Output "=" * 60
}
function Write-SubSection {
param([string]$Title)
Write-Output ""
Write-Output "--- $Title ---"
}
# Header
Write-Output "WINDOWS UPDATE DIAGNOSTICS"
Write-Output "Hostname: $env:COMPUTERNAME"
Write-Output "Datum: $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Output "OS: $((Get-CimInstance Win32_OperatingSystem).Caption)"
Write-Output "Build: $([System.Environment]::OSVersion.Version.Build).$((Get-ItemProperty 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion').UBR)"
# ============================================================
# 1. WINDOWS UPDATE DIENSTE
# ============================================================
Write-Section "1. WINDOWS UPDATE DIENSTE"
$services = @(
@{Name="wuauserv"; DisplayName="Windows Update"},
@{Name="bits"; DisplayName="Background Intelligent Transfer"},
@{Name="cryptsvc"; DisplayName="Cryptographic Services"},
@{Name="msiserver"; DisplayName="Windows Installer"},
@{Name="TrustedInstaller"; DisplayName="Windows Modules Installer"}
)
foreach ($svc in $services) {
$service = Get-Service -Name $svc.Name -ErrorAction SilentlyContinue
if ($service) {
$startType = (Get-CimInstance Win32_Service -Filter "Name='$($svc.Name)'").StartMode
$status = if ($service.Status -eq "Running") { "[OK]" } else { "[!!]" }
Write-Output "$status $($svc.DisplayName) ($($svc.Name)): $($service.Status) / StartType: $startType"
} else {
Write-Output "[??] $($svc.DisplayName) ($($svc.Name)): Nicht gefunden"
}
}
# ============================================================
# 2. WSUS / WINDOWS UPDATE KONFIGURATION
# ============================================================
Write-Section "2. WINDOWS UPDATE KONFIGURATION"
Write-SubSection "Registry: WindowsUpdate Policy"
$wuPolicyPath = "HKLM:\SOFTWARE\Policies\Microsoft\Windows\WindowsUpdate"
if (Test-Path $wuPolicyPath) {
$wuPolicy = Get-ItemProperty -Path $wuPolicyPath -ErrorAction SilentlyContinue
Write-Output "WUServer: $($wuPolicy.WUServer)"
Write-Output "WUStatusServer: $($wuPolicy.WUStatusServer)"
Write-Output "UseWUServer: $($wuPolicy.UseWUServer)"
Write-Output "DoNotConnectToWindowsUpdateInternetLocations: $($wuPolicy.DoNotConnectToWindowsUpdateInternetLocations)"
$auPath = "$wuPolicyPath\AU"
if (Test-Path $auPath) {
$auPolicy = Get-ItemProperty -Path $auPath -ErrorAction SilentlyContinue
Write-Output ""
Write-Output "AU Policy:"
Write-Output " NoAutoUpdate: $($auPolicy.NoAutoUpdate)"
Write-Output " AUOptions: $($auPolicy.AUOptions)"
Write-Output " ScheduledInstallDay: $($auPolicy.ScheduledInstallDay)"
Write-Output " ScheduledInstallTime: $($auPolicy.ScheduledInstallTime)"
Write-Output " UseWUServer: $($auPolicy.UseWUServer)"
}
} else {
Write-Output "Keine WSUS/GPO Konfiguration gefunden (Standard Windows Update)"
}
Write-SubSection "Registry: Windows Update Settings"
$wuSettingsPath = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate"
if (Test-Path $wuSettingsPath) {
$wuSettings = Get-ItemProperty -Path $wuSettingsPath -ErrorAction SilentlyContinue
Write-Output "AccountDomainSid: $($wuSettings.AccountDomainSid)"
Write-Output "SusClientId: $($wuSettings.SusClientId)"
}
# ============================================================
# 3. WINDOWS UPDATE HISTORIE
# ============================================================
Write-Section "3. WINDOWS UPDATE HISTORIE (letzte 20)"
try {
$Session = New-Object -ComObject Microsoft.Update.Session
$Searcher = $Session.CreateUpdateSearcher()
$HistoryCount = $Searcher.GetTotalHistoryCount()
Write-Output "Gesamte Historie-Eintraege: $HistoryCount"
Write-Output ""
if ($HistoryCount -gt 0) {
$History = $Searcher.QueryHistory(0, [Math]::Min(20, $HistoryCount))
foreach ($Update in $History) {
$ResultCode = switch ($Update.ResultCode) {
0 { "NotStarted" }
1 { "InProgress" }
2 { "Succeeded" }
3 { "SucceededWithErrors" }
4 { "Failed" }
5 { "Aborted" }
default { "Unknown" }
}
$Status = if ($Update.ResultCode -eq 2) { "[OK]" } else { "[!!]" }
$Date = $Update.Date.ToString("yyyy-MM-dd HH:mm")
$Title = $Update.Title
if ($Title.Length -gt 60) { $Title = $Title.Substring(0, 57) + "..." }
Write-Output "$Status [$Date] $ResultCode"
Write-Output " $Title"
}
}
} catch {
Write-Output "Fehler beim Abrufen der Update-Historie: $_"
}
# ============================================================
# 4. AUSSTEHENDE UPDATES
# ============================================================
Write-Section "4. AUSSTEHENDE UPDATES"
try {
$Searcher = $Session.CreateUpdateSearcher()
$SearchResult = $Searcher.Search("IsInstalled=0")
Write-Output "Ausstehende Updates: $($SearchResult.Updates.Count)"
Write-Output ""
if ($SearchResult.Updates.Count -gt 0) {
foreach ($Update in $SearchResult.Updates) {
$Severity = if ($Update.MsrcSeverity) { $Update.MsrcSeverity } else { "Unspecified" }
$Size = [math]::Round($Update.MaxDownloadSize / 1MB, 1)
Write-Output "[$Severity] $($Update.Title)"
Write-Output " KB: $($Update.KBArticleIDs -join ', ') | Groesse: ${Size}MB | Downloaded: $($Update.IsDownloaded)"
}
} else {
Write-Output "Keine ausstehenden Updates gefunden."
}
} catch {
Write-Output "Fehler beim Suchen nach Updates: $_"
}
# ============================================================
# 5. LETZTE ERFOLGREICHE INSTALLATION
# ============================================================
Write-Section "5. LETZTE ERFOLGREICHE UPDATE-INSTALLATION"
try {
$LastSuccess = $null
$History = $Searcher.QueryHistory(0, $HistoryCount)
foreach ($Update in $History) {
if ($Update.ResultCode -eq 2) {
$LastSuccess = $Update
break
}
}
if ($LastSuccess) {
$DaysAgo = [math]::Round((Get-Date) - $LastSuccess.Date).TotalDays
Write-Output "Datum: $($LastSuccess.Date.ToString('yyyy-MM-dd HH:mm:ss'))"
Write-Output "Tage her: $DaysAgo"
Write-Output "Update: $($LastSuccess.Title)"
if ($DaysAgo -gt 60) {
Write-Output ""
Write-Output "[WARNUNG] Letztes erfolgreiches Update ist mehr als 60 Tage her!"
}
} else {
Write-Output "Kein erfolgreiches Update in der Historie gefunden!"
}
} catch {
Write-Output "Fehler: $_"
}
# ============================================================
# 6. SOFTWAREDISTRIBUTION ORDNER
# ============================================================
Write-Section "6. SOFTWAREDISTRIBUTION STATUS"
$sdPath = "$env:SystemRoot\SoftwareDistribution"
if (Test-Path $sdPath) {
$sdSize = (Get-ChildItem -Path $sdPath -Recurse -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
$sdSizeMB = [math]::Round($sdSize / 1MB, 2)
Write-Output "Pfad: $sdPath"
Write-Output "Groesse: ${sdSizeMB} MB"
$downloadPath = "$sdPath\Download"
if (Test-Path $downloadPath) {
$dlSize = (Get-ChildItem -Path $downloadPath -Recurse -ErrorAction SilentlyContinue | Measure-Object -Property Length -Sum).Sum
$dlSizeMB = [math]::Round($dlSize / 1MB, 2)
Write-Output "Download-Ordner: ${dlSizeMB} MB"
}
$dataStorePath = "$sdPath\DataStore\DataStore.edb"
if (Test-Path $dataStorePath) {
$dbSize = [math]::Round((Get-Item $dataStorePath).Length / 1MB, 2)
Write-Output "DataStore.edb: ${dbSize} MB"
if ($dbSize -gt 500) {
Write-Output "[WARNUNG] DataStore.edb ist sehr gross - Reset empfohlen"
}
}
} else {
Write-Output "[FEHLER] SoftwareDistribution Ordner nicht gefunden!"
}
# ============================================================
# 7. WINDOWS UPDATE FEHLER (Event Log)
# ============================================================
Write-Section "7. WINDOWS UPDATE FEHLER (letzte 7 Tage)"
try {
$StartDate = (Get-Date).AddDays(-7)
$WUErrors = Get-WinEvent -FilterHashtable @{
LogName = 'System'
ProviderName = 'Microsoft-Windows-WindowsUpdateClient'
Level = 2,3 # Error, Warning
StartTime = $StartDate
} -MaxEvents 10 -ErrorAction SilentlyContinue
if ($WUErrors) {
Write-Output "Gefundene Fehler/Warnungen: $($WUErrors.Count)"
Write-Output ""
foreach ($Event in $WUErrors) {
$Level = if ($Event.Level -eq 2) { "[ERROR]" } else { "[WARN]" }
Write-Output "$Level [$($Event.TimeCreated.ToString('yyyy-MM-dd HH:mm'))] ID:$($Event.Id)"
Write-Output " $($Event.Message.Split("`n")[0])"
}
} else {
Write-Output "Keine Windows Update Fehler in den letzten 7 Tagen."
}
} catch {
Write-Output "Event Log konnte nicht abgefragt werden: $_"
}
# ============================================================
# 8. NETZWERK-KONNEKTIVITAET
# ============================================================
Write-Section "8. WINDOWS UPDATE KONNEKTIVITAET"
$testUrls = @(
"https://www.microsoft.com",
"https://update.microsoft.com",
"https://windowsupdate.microsoft.com"
)
foreach ($url in $testUrls) {
try {
$response = Invoke-WebRequest -Uri $url -UseBasicParsing -TimeoutSec 10 -ErrorAction Stop
Write-Output "[OK] $url (Status: $($response.StatusCode))"
} catch {
Write-Output "[!!] $url (Fehler: $($_.Exception.Message))"
}
}
# ============================================================
# 9. ZUSAMMENFASSUNG
# ============================================================
Write-Section "9. ZUSAMMENFASSUNG"
$issues = @()
# Check services
$wuService = Get-Service -Name wuauserv -ErrorAction SilentlyContinue
if ($wuService.Status -ne "Running") {
$issues += "Windows Update Dienst laeuft nicht"
}
# Check last update age
if ($LastSuccess -and $DaysAgo -gt 60) {
$issues += "Letztes Update vor mehr als $DaysAgo Tagen"
}
# Check WSUS
if ((Test-Path $wuPolicyPath) -and $wuPolicy.UseWUServer -eq 1) {
$issues += "WSUS konfiguriert: $($wuPolicy.WUServer)"
}
# Check pending updates
if ($SearchResult -and $SearchResult.Updates.Count -gt 5) {
$issues += "$($SearchResult.Updates.Count) ausstehende Updates"
}
if ($issues.Count -eq 0) {
Write-Output "Status: OK - Keine offensichtlichen Probleme gefunden"
} else {
Write-Output "Status: PROBLEME ERKANNT"
Write-Output ""
foreach ($issue in $issues) {
Write-Output " [!] $issue"
}
}
Write-Output ""
Write-Output "=" * 60
Write-Output " DIAGNOSTICS ENDE"
Write-Output "=" * 60
File diff suppressed because it is too large Load Diff