Um DefenderUpdatesOnly ergänzt

This commit is contained in:
Patrick Asmus
2026-07-07 10:14:27 +02:00
parent eacc7cdbe1
commit c236b08311
5 changed files with 239 additions and 33 deletions

View File

@@ -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).

View File

@@ -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

View File

@@ -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 <Pfad>` | *(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

View File

@@ -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 |

View File

@@ -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
}