From c236b08311d23c69383d94155c7ef2dc7df52287 Mon Sep 17 00:00:00 2001 From: Patrick Asmus Date: Tue, 7 Jul 2026 10:14:27 +0200 Subject: [PATCH] =?UTF-8?q?Um=20DefenderUpdatesOnly=20erg=C3=A4nzt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 32 +++++++++++++ config.example.ini | 3 ++ docs/configuration.md | 62 ++++++++++++++++++++++++- docs/task-scheduler.md | 72 ++++++++++++++++++++++++++++ windows-updater.ps1 | 103 ++++++++++++++++++++++++++++------------- 5 files changed, 239 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 4af7954..e70b8e5 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,8 @@ ## Features - **Windows Updates** automatisch installieren via PSWindowsUpdate-Modul +- **Defender-Only Modus** nur Defender-Definitionsupdates installieren (per Parameter oder Config) — ideal für häufige Ausführung alle paar Stunden +- **Kommandozeilen-Parameter** `-DefenderOnly`, `-DryRun`, `-ConfigFile` für flexible Steuerung ohne Config-Änderung - **Winget-Updates** optional (`winget upgrade --all`), automatische Erkennung der Verfügbarkeit - **Benachrichtigungen** via ntfy, E-Mail und/oder Microsoft Teams (inkl. HTML-Report und Adaptive Cards) - **Benachrichtigungs-Ereignisse** granular steuerbar (je Ereignis einzeln ein-/ausschaltbar) @@ -73,11 +75,41 @@ Danach `config.ini` mit einem Texteditor öffnen und die gewünschten Werte anpa ### 3. Script ausführen (als Administrator) ```powershell +# Alle Updates installieren (Standard) powershell.exe -ExecutionPolicy Bypass -File "C:\scripts\windows-updater\windows-updater.ps1" + +# Nur Defender-Definitionsupdates +powershell.exe -ExecutionPolicy Bypass -File "C:\scripts\windows-updater\windows-updater.ps1" -DefenderOnly + +# Dry-Run (nur prüfen, nicht installieren) +powershell.exe -ExecutionPolicy Bypass -File "C:\scripts\windows-updater\windows-updater.ps1" -DryRun + +# Alternative Config-Datei verwenden +powershell.exe -ExecutionPolicy Bypass -File "C:\scripts\windows-updater\windows-updater.ps1" -ConfigFile "D:\configs\server-config.ini" ``` ![Screen1](assets/screen1.png) +## Parameter + +| Parameter | Typ | Beschreibung | +|---|---|---| +| `-DefenderOnly` | Switch | Nur Windows Defender Definitionsupdates installieren. Überspringt Winget, Reboot und Service Watchdog | +| `-DryRun` | Switch | Nur prüfen welche Updates verfügbar sind, ohne zu installieren. Überschreibt die Config-Einstellung | +| `-ConfigFile` | String | Pfad zu einer alternativen Konfigurationsdatei (Standard: `config.ini` im Script-Verzeichnis) | + +Parameter überschreiben die entsprechenden Config-Werte. So kann dasselbe Script mit verschiedenen Parametern für verschiedene Aufgaben verwendet werden. + +### Typisches Setup: Defender häufig, volle Updates monatlich + +``` +# Alle 4 Stunden: Defender-Updates (per Aufgabenplanung) +powershell.exe -ExecutionPolicy Bypass -File "C:\scripts\windows-updater\windows-updater.ps1" -DefenderOnly + +# Einmal im Monat: Alle Updates (per Aufgabenplanung) +powershell.exe -ExecutionPolicy Bypass -File "C:\scripts\windows-updater\windows-updater.ps1" +``` + ### 4. Optional: Aufgabenplanung einrichten Für automatische Ausführung per Windows Aufgabenplanung siehe [Aufgabenplanung einrichten](docs/task-scheduler.md). diff --git a/config.example.ini b/config.example.ini index 2063dd2..2634038 100644 --- a/config.example.ini +++ b/config.example.ini @@ -11,6 +11,9 @@ LogPath = C:\logs\windows-updater LogRetentionDays = 365 # Dry-run mode: scan and report only, do not install (true/false) DryRun = false +# Update type: All (all updates), DefenderOnly (only Defender definition updates) +# Can be overridden with -DefenderOnly parameter +UpdateType = All [WindowsUpdate] # Use Microsoft Update (includes Office, .NET, drivers) instead of Windows Update only diff --git a/docs/configuration.md b/docs/configuration.md index d6e6b07..dfb713f 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -10,7 +10,8 @@ Alle Einstellungen werden in der Datei `config.ini` vorgenommen, die sich im sel |---|---|---|---| | `LogPath` | String | `C:\logs\windows-updater` | Verzeichnis für Log-Dateien (eine Datei pro Tag) | | `LogRetentionDays` | Integer | `365` | Anzahl Tage, nach denen alte Log-Dateien gelöscht werden | -| `DryRun` | Boolean | `false` | Nur prüfen welche Updates verfügbar sind, ohne zu installieren | +| `DryRun` | Boolean | `false` | Nur prüfen welche Updates verfügbar sind, ohne zu installieren. Kann per `-DryRun` Parameter überschrieben werden | +| `UpdateType` | String | `All` | Art der Updates: `All` (alle Updates), `DefenderOnly` (nur Defender-Definitionsupdates). Kann per `-DefenderOnly` Parameter überschrieben werden | ### [WindowsUpdate] @@ -109,6 +110,27 @@ Steuert, welche Ereignisse eine Benachrichtigung auslösen. Alle Ereignisse sind | `Enabled` | Boolean | `false` | Update-History-Tracking aktivieren | | `HistoryPath` | String | `C:\logs\windows-updater\update-history` | Basispfad für die History-Datei (Dateiendung wird automatisch gewählt: `.xlsx` wenn ImportExcel verfügbar, sonst `.csv`) | +## Kommandozeilen-Parameter + +Das Script unterstützt Parameter, die Config-Werte überschreiben. So kann dasselbe Script mit einer Config-Datei für verschiedene Einsatzzwecke verwendet werden. + +| Parameter | Überschreibt | Beschreibung | +|---|---|---| +| `-DefenderOnly` | `UpdateType` | Nur Windows Defender Definitionsupdates installieren | +| `-DryRun` | `DryRun` | Nur prüfen, nicht installieren | +| `-ConfigFile ` | *(Config-Pfad)* | Alternative Konfigurationsdatei verwenden | + +### DefenderOnly-Modus im Detail + +Im DefenderOnly-Modus werden ausschließlich Updates der Kategorie "Definition Updates" (Windows Defender Signaturen) installiert. Folgende Funktionen werden dabei übersprungen: + +- **Winget-Updates** — nicht relevant für Defender-Signaturen +- **Reboot-Handling** — Definitionsupdates erfordern keinen Neustart +- **Service Watchdog** — kein Neustart von Diensten nötig +- **Treiber-Ausschluss** (`ExcludeDrivers`) — nicht relevant, da nur Definitionsupdates geladen werden + +Pre-/Post-Update Hooks, Logging, History und Benachrichtigungen funktionieren normal. + ## Boolean-Werte Folgende Werte werden als `true` erkannt (Groß-/Kleinschreibung egal): @@ -129,6 +151,44 @@ DryRun = false UseMicrosoftUpdate = true ``` +### Nur Defender-Updates (per Config) + +```ini +[General] +LogPath = C:\logs\windows-updater +DryRun = false +UpdateType = DefenderOnly + +[WindowsUpdate] +UseMicrosoftUpdate = true +``` + +### Dual-Setup: Defender häufig + volle Updates monatlich + +Eine Config-Datei genügt. Die Unterscheidung erfolgt per Parameter: + +```ini +[General] +LogPath = C:\logs\windows-updater +DryRun = false +# UpdateType = All (Standard, volle Updates) + +[WindowsUpdate] +UseMicrosoftUpdate = true +ExcludeDrivers = true +RetryCount = 3 +``` + +Aufgabenplanung: + +```powershell +# Alle 4 Stunden: nur Defender-Updates +powershell.exe -ExecutionPolicy Bypass -File "C:\scripts\windows-updater\windows-updater.ps1" -DefenderOnly + +# Einmal im Monat (z.B. erster Sonntag, 03:00): alle Updates +powershell.exe -ExecutionPolicy Bypass -File "C:\scripts\windows-updater\windows-updater.ps1" +``` + ### Mit E-Mail Benachrichtigung ```ini diff --git a/docs/task-scheduler.md b/docs/task-scheduler.md index eba6d65..900eb01 100644 --- a/docs/task-scheduler.md +++ b/docs/task-scheduler.md @@ -70,6 +70,77 @@ $trigger.StartBoundary = (Get-Date "04:00").ToString("yyyy-MM-dd'T'HH:mm:ss") Den Rest wie beim täglichen Beispiel, nur den Trigger austauschen. +### Defender-Updates alle 4 Stunden + +```powershell +$action = New-ScheduledTaskAction ` + -Execute "powershell.exe" ` + -Argument "-ExecutionPolicy Bypass -NoProfile -File `"C:\scripts\windows-updater\windows-updater.ps1`" -DefenderOnly" + +$trigger = New-ScheduledTaskTrigger -Once -At "00:00" -RepetitionInterval (New-TimeSpan -Hours 4) + +$principal = New-ScheduledTaskPrincipal ` + -UserId "SYSTEM" ` + -RunLevel Highest ` + -LogonType ServiceAccount + +$settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -ExecutionTimeLimit (New-TimeSpan -Minutes 30) + +Register-ScheduledTask ` + -TaskName "Windows Updater - Defender" ` + -Action $action ` + -Trigger $trigger ` + -Principal $principal ` + -Settings $settings ` + -Description "Defender-Definitionsupdates alle 4 Stunden" +``` + +### Dual-Setup: Defender häufig + volle Updates monatlich + +Zwei Aufgaben anlegen — eine für Defender (häufig), eine für alle Updates (selten): + +```powershell +# Aufgabe 1: Defender alle 4 Stunden (wie oben) +# Aufgabe 2: Volle Updates monatlich am ersten Sonntag um 03:00 +$action = New-ScheduledTaskAction ` + -Execute "powershell.exe" ` + -Argument "-ExecutionPolicy Bypass -NoProfile -File `"C:\scripts\windows-updater\windows-updater.ps1`"" + +$trigger = New-CimInstance -CimClass ( + Get-CimClass -Namespace "Root/Microsoft/Windows/TaskScheduler" ` + -ClassName "MSFT_TaskMonthlyDOWTrigger" +) -ClientOnly -Property @{ + DaysOfWeek = [int16]1 # Sonntag + WeeksOfMonth = [int16]1 # Erste Woche + Months = [int16]4095 # Alle 12 Monate + Enabled = $true +} +$trigger.StartBoundary = (Get-Date "03:00").ToString("yyyy-MM-dd'T'HH:mm:ss") + +$principal = New-ScheduledTaskPrincipal ` + -UserId "SYSTEM" ` + -RunLevel Highest ` + -LogonType ServiceAccount + +$settings = New-ScheduledTaskSettingsSet ` + -AllowStartIfOnBatteries ` + -DontStopIfGoingOnBatteries ` + -StartWhenAvailable ` + -ExecutionTimeLimit (New-TimeSpan -Hours 2) + +Register-ScheduledTask ` + -TaskName "Windows Updater - Monthly" ` + -Action $action ` + -Trigger $trigger ` + -Principal $principal ` + -Settings $settings ` + -Description "Vollständige Windows Updates monatlich" +``` + ### Bestehende Aufgabe ändern ```powershell @@ -168,3 +239,4 @@ Get-ScheduledTaskInfo -TaskName "Windows Updater" | Select-Object LastRunTime, L | Server (nicht-kritisch) | Täglich, 03:00-05:00 mit Auto-Reboot | | Server (kritisch/Produktion) | Wöchentlich Sonntag, 03:00 mit DryRun. Manuell nach Prüfung | | Testumgebung | Täglich, 02:00 mit Auto-Reboot | +| Defender-Signaturen | Alle 4 Stunden mit `-DefenderOnly`, zusätzlich zu einem der obigen Szenarien | diff --git a/windows-updater.ps1 b/windows-updater.ps1 index cc0a0f7..5fac1c8 100644 --- a/windows-updater.ps1 +++ b/windows-updater.ps1 @@ -9,12 +9,18 @@ Author: Patrick Asmus Web: https://www.cleveradmin.de Repository: https://git.techniverse.net/scriptos/windows-updater.git License: MIT -Version: 2.7.2 -Date: 06.07.2026 -Modifications: Guard TLS 1.3 with enum check and try/catch fallback to TLS 1.2 +Version: 2.8.0 +Date: 07.07.2026 +Modifications: Add -DefenderOnly, -DryRun, -ConfigFile parameters and UpdateType config option ##################################################### #> +param( + [switch]$DefenderOnly, + [switch]$DryRun, + [string]$ConfigFile +) + # =================================================== #region Administrator Check # =================================================== @@ -53,6 +59,9 @@ $script:UseXlsx = $false $script:UpdateResults = [System.Collections.ArrayList]::new() $script:WingetResults = [System.Collections.ArrayList]::new() $script:ScriptStartTime = Get-Date +$script:DefenderOnlyMode = $DefenderOnly.IsPresent +$script:DryRunParam = $DryRun.IsPresent +$script:ConfigFilePath = $ConfigFile #endregion @@ -765,6 +774,11 @@ function Get-AvailableUpdates { $params["MicrosoftUpdate"] = $true } + if ($script:DefenderOnlyMode) { + $params["Category"] = @("Definition Updates") + Write-Log "Filtering for Defender definition updates only" + } + if (-not [string]::IsNullOrWhiteSpace($excludeKBs)) { $kbList = $excludeKBs -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } if ($kbList.Count -gt 0) { @@ -773,7 +787,7 @@ function Get-AvailableUpdates { } } - if ($excludeDrivers) { + if ($excludeDrivers -and -not $script:DefenderOnlyMode) { $params["NotCategory"] = "Drivers" Write-Log "Excluding driver updates" } @@ -804,6 +818,10 @@ function Install-PendingUpdates { $params["MicrosoftUpdate"] = $true } + if ($script:DefenderOnlyMode) { + $params["Category"] = @("Definition Updates") + } + if (-not [string]::IsNullOrWhiteSpace($excludeKBs)) { $kbList = $excludeKBs -split ',' | ForEach-Object { $_.Trim() } | Where-Object { -not [string]::IsNullOrWhiteSpace($_) } if ($kbList.Count -gt 0) { @@ -811,7 +829,7 @@ function Install-PendingUpdates { } } - if ($excludeDrivers) { + if ($excludeDrivers -and -not $script:DefenderOnlyMode) { $params["NotCategory"] = "Drivers" } @@ -1158,16 +1176,30 @@ function Invoke-ServiceWatchdog { function Start-WindowsUpdater { # --- Load Config --- - $configPath = Join-Path -Path $PSScriptRoot -ChildPath "config.ini" + if (-not [string]::IsNullOrWhiteSpace($script:ConfigFilePath)) { + $configPath = $script:ConfigFilePath + } + else { + $configPath = Join-Path -Path $PSScriptRoot -ChildPath "config.ini" + } $script:Config = Read-IniFile -Path $configPath + # --- Resolve DefenderOnly mode (parameter overrides config) --- + if (-not $script:DefenderOnlyMode) { + $configUpdateType = Get-ConfigValue -Config $script:Config -Section "General" -Key "UpdateType" -Default "All" + if ($configUpdateType -eq "DefenderOnly") { + $script:DefenderOnlyMode = $true + } + } + # --- Initialize Logging --- $logPath = Get-ConfigValue -Config $script:Config -Section "General" -Key "LogPath" -Default "C:\logs\windows-updater" $logRetention = Get-ConfigValue -Config $script:Config -Section "General" -Key "LogRetentionDays" -Default 365 -Type int Initialize-Logging -LogPath $logPath -RetentionDays $logRetention + $modeLabel = if ($script:DefenderOnlyMode) { "Defender Only" } else { "Full Update" } Write-Log "==========================================" - Write-Log "Windows Updater started" + Write-Log "Windows Updater started (Mode: $modeLabel)" Write-Log "==========================================" # --- OS Detection --- @@ -1208,6 +1240,7 @@ function Start-WindowsUpdater { # --- Scan for Updates --- $dryRun = Get-ConfigValue -Config $script:Config -Section "General" -Key "DryRun" -Default $false -Type bool + if ($script:DryRunParam) { $dryRun = $true } $availableUpdates = Get-AvailableUpdates if ($null -eq $availableUpdates) { @@ -1220,13 +1253,15 @@ function Start-WindowsUpdater { exit 1 } + $updateLabel = if ($script:DefenderOnlyMode) { "Defender" } else { "Windows" } + if (@($availableUpdates).Count -eq 0) { Write-Log "No updates available" - Send-Notification -Subject "No Updates - $env:COMPUTERNAME" ` - -Body "No Windows updates are currently available for $env:COMPUTERNAME." ` + Send-Notification -Subject "No $updateLabel Updates - $env:COMPUTERNAME" ` + -Body "No $updateLabel updates are currently available for $env:COMPUTERNAME." ` -EventName "NoUpdates" if (-not $dryRun) { - Invoke-WingetUpgrade + if (-not $script:DefenderOnlyMode) { Invoke-WingetUpgrade } Export-UpdateHistory } Invoke-UpdateHook -Phase Post | Out-Null @@ -1250,7 +1285,7 @@ function Start-WindowsUpdater { if ($dryRun) { Write-Log "DRY-RUN mode active - no updates will be installed" - $reportLines = @("Available Updates for $env:COMPUTERNAME ($updateCount):`n") + $reportLines = @("Available $updateLabel Updates for $env:COMPUTERNAME ($updateCount):`n") foreach ($u in $availableUpdates) { $sizeStr = if ($u.Size) { "{0:N2} MB" -f ($u.Size / 1MB) } else { "N/A" } $kbStr = if ($u.KB) { $u.KB } else { "N/A" } @@ -1258,7 +1293,7 @@ function Start-WindowsUpdater { } $reportBody = $reportLines -join "`n" - Send-Notification -Subject "Dry-Run Report - $env:COMPUTERNAME" -Body $reportBody -EventName "DryRun" + Send-Notification -Subject "$updateLabel Dry-Run Report - $env:COMPUTERNAME" -Body $reportBody -EventName "DryRun" Invoke-UpdateHook -Phase Post | Out-Null Write-Log "Windows Updater finished (dry-run)" @@ -1268,8 +1303,10 @@ function Start-WindowsUpdater { # --- Install Updates --- Install-PendingUpdates - # --- Winget --- - Invoke-WingetUpgrade + # --- Winget (skip in DefenderOnly mode) --- + if (-not $script:DefenderOnlyMode) { + Invoke-WingetUpgrade + } # --- Export History --- Export-UpdateHistory @@ -1279,7 +1316,7 @@ function Start-WindowsUpdater { $failCount = ($script:UpdateResults | Where-Object { $_.Result -notmatch "^Installed|^Succeeded|^Success" } | Measure-Object).Count $summaryBody = @( - "Windows Update Summary for $env:COMPUTERNAME" + "$updateLabel Update Summary for $env:COMPUTERNAME" "OS: $($script:OSInfo.Caption)" "Date: $(Get-Date -Format 'dd.MM.yyyy HH:mm:ss')" "" @@ -1295,7 +1332,7 @@ function Start-WindowsUpdater { $summaryText = $summaryBody -join "`n" $hasFailures = $failCount -gt 0 -or $script:HasErrors $priority = if ($hasFailures) { "high" } else { "default" } - $subjectPrefix = if ($hasFailures) { "Updates (with errors)" } else { "Updates OK" } + $subjectPrefix = if ($hasFailures) { "$updateLabel Updates (with errors)" } else { "$updateLabel Updates OK" } Send-Notification -Subject "$subjectPrefix - $env:COMPUTERNAME" ` -Body $summaryText -Priority $priority ` @@ -1304,22 +1341,24 @@ function Start-WindowsUpdater { # --- Post-Update Hook --- Invoke-UpdateHook -Phase Post | Out-Null - # --- Reboot Handling --- - $script:RebootPending = Test-PendingReboot - if ($script:RebootPending) { - Write-Log "System reboot is pending" - $rebootHandled = Invoke-ScheduledReboot - if (-not $rebootHandled) { - Write-Log "Auto-reboot is disabled, manual reboot required" -Level WARN - Send-Notification -Subject "Reboot Required - $env:COMPUTERNAME" ` - -Body "Windows updates have been installed on $env:COMPUTERNAME but a reboot is required. Auto-reboot is disabled." ` - -Priority "high" -EventName "RebootRequired" + # --- Reboot Handling (skip in DefenderOnly mode) --- + if (-not $script:DefenderOnlyMode) { + $script:RebootPending = Test-PendingReboot + if ($script:RebootPending) { + Write-Log "System reboot is pending" + $rebootHandled = Invoke-ScheduledReboot + if (-not $rebootHandled) { + Write-Log "Auto-reboot is disabled, manual reboot required" -Level WARN + Send-Notification -Subject "Reboot Required - $env:COMPUTERNAME" ` + -Body "Windows updates have been installed on $env:COMPUTERNAME but a reboot is required. Auto-reboot is disabled." ` + -Priority "high" -EventName "RebootRequired" + } + } + else { + Write-Log "No reboot required" + # --- Service Watchdog (only when no reboot is pending) --- + Invoke-ServiceWatchdog } - } - else { - Write-Log "No reboot required" - # --- Service Watchdog (only when no reboot is pending) --- - Invoke-ServiceWatchdog } # --- Finish --- @@ -1328,7 +1367,7 @@ function Start-WindowsUpdater { Write-Log "==========================================" if ($script:HasErrors) { exit 1 } - if ($script:RebootPending -and -not (Get-ConfigValue -Config $script:Config -Section "Reboot" -Key "Enabled" -Default $false -Type bool)) { exit 2 } + if (-not $script:DefenderOnlyMode -and $script:RebootPending -and -not (Get-ConfigValue -Config $script:Config -Section "Reboot" -Key "Enabled" -Default $false -Type bool)) { exit 2 } exit 0 }