WSUS
Auto Approve / Decline Updates
function Manage-WsusUpdates {
<#
.SYNOPSIS
WSUS güncellemelerini filtreleyip Approve/Decline eder. Declined olanları gerçek anlamda “undecline + approve” yapar.
#>
[CmdletBinding()]
param (
[int]$Days = 30,
[string[]]$Products,
[ValidateSet('Exact','Like','Regex')]
[string]$ProductMatchMode = 'Like',
[string[]]$Classifications,
[string[]]$Titles,
[string]$TargetGroupName = "All Computers",
[string]$WsusServer = "localhost",
[bool]$UseSsl = $false,
[int]$Port = 8530,
[string]$RequiredFilter, # "gt 0", "ge 1", "eq 0" ...
[switch]$IncludeSubgroupsRequired, # Required hesaplamasında alt grupları dahil et
[bool]$AutoUndeclineBeforeApprove = $true, # declined ise önce gerçek undecline et, sonra approve
[switch]$AllowDecline, # cutoff öncesi uygunları decline et
[switch]$SkipExpired, # expired güncellemeleri atla
[switch]$SkipSuperseded, # superseded güncellemeleri atla
[switch]$DryRun
)
# Assembly (PS 5.1)
$null = [Reflection.Assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration")
# Connect
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($WsusServer, $UseSsl, $Port)
$cutoffDate = (Get-Date).AddDays(-$Days)
# Target group
$targetGroup = $wsus.GetComputerTargetGroups() | Where-Object { $_.Name -eq $TargetGroupName }
if (-not $targetGroup) { Write-Error "Target WSUS group not found: $TargetGroupName"; return }
# Universe
$allUpdates = $wsus.GetUpdates()
# Counters
$scanned = 0; $passProduct = 0; $passClass = 0; $passTitle = 0; $passRequired = 0
$approvedCount = 0; $declinedCount = 0; $skippedByRequired = 0
$skippedExpired = 0; $skippedSuperseded = 0; $undeclineFailures = 0
foreach ($update in $allUpdates) {
$scanned++
# Optional skips: expired / superseded
if ($SkipExpired.IsPresent -and $update.IsExpired) { $skippedExpired++; continue }
if ($SkipSuperseded.IsPresent -and $update.IsSuperseded) { $skippedSuperseded++; continue }
# Product match
$matchesProduct = $true
if ($Products -and $Products.Count -gt 0) {
$matchesProduct = $false
foreach ($prodTitle in $update.ProductTitles) {
foreach ($filter in $Products) {
switch ($ProductMatchMode) {
'Exact' { if ($prodTitle -eq $filter) { $matchesProduct = $true } }
'Like' { if ($prodTitle -like ("*" + $filter + "*")) { $matchesProduct = $true } }
'Regex' { if ($prodTitle -match $filter) { $matchesProduct = $true } }
}
if ($matchesProduct) { break }
}
if ($matchesProduct) { break }
}
}
if (-not $matchesProduct) { continue } else { $passProduct++ }
# Classification match
$matchesClass = $true
if ($Classifications -and $Classifications.Count -gt 0) {
$matchesClass = ($Classifications -contains $update.UpdateClassificationTitle)
}
if (-not $matchesClass) { continue } else { $passClass++ }
# Title match
$matchesTitle = $true
if ($Titles -and $Titles.Count -gt 0) {
$matchesTitle = $false
foreach ($tf in $Titles) { if ($update.Title -like ("*" + $tf + "*")) { $matchesTitle = $true; break } }
}
if (-not $matchesTitle) { continue } else { $passTitle++ }
# Required (Needed) filter
$matchesRequired = $true
if ($RequiredFilter -and $RequiredFilter.Trim().Length -gt 0) {
$op = $null; $thrText = $null
$parts = $RequiredFilter -split '\s+', 2
if ($parts.Length -ne 2) {
Write-Warning "Invalid RequiredFilter format: '$RequiredFilter'. Use: 'gt 0', 'ge 1', 'eq 0'..."
$matchesRequired = $false
} else { $op,$thrText = $parts[0],$parts[1] }
$requiredCount = 0
if ($matchesRequired) {
$summary = $null
try {
$summary = $update.GetSummaryForComputerTargetGroup($targetGroup, [bool]$IncludeSubgroupsRequired.IsPresent)
} catch {
try {
$sumAll = $update.GetSummaryPerComputerTargetGroup()
if ($sumAll) {
$summary = $sumAll | Where-Object { $_.ComputerTargetGroupId -eq $targetGroup.Id } | Select-Object -First 1
}
} catch { $summary = $null }
}
$requiredCount = if ($summary) { [int]$summary.NotInstalledCount } else { 0 }
$thr = 0
if (-not [int]::TryParse($thrText, [ref]$thr)) {
Write-Warning "Invalid RequiredFilter threshold: '$thrText' (must be integer)."
$matchesRequired = $false
} else {
switch ($op.ToLower()) {
'eq' { $matchesRequired = ($requiredCount -eq $thr) }
'ne' { $matchesRequired = ($requiredCount -ne $thr) }
'gt' { $matchesRequired = ($requiredCount -gt $thr) }
'ge' { $matchesRequired = ($requiredCount -ge $thr) }
'lt' { $matchesRequired = ($requiredCount -lt $thr) }
'le' { $matchesRequired = ($requiredCount -le $thr) }
default { Write-Warning "Invalid operator '$op' (eq,ne,gt,ge,lt,le)"; $matchesRequired = $false }
}
}
}
if (-not $matchesRequired) { $skippedByRequired++; continue } else { $passRequired++ }
}
# Approve / Decline
$creationDate = $update.CreationDate
$isDeclined = $update.IsDeclined
if ($creationDate -ge $cutoffDate) {
# --- APPROVE path ---
if ($isDeclined -and $AutoUndeclineBeforeApprove) {
if ($DryRun) {
Write-Host "[DRY-RUN] Would UNDECLINE: $($update.Title)"
$isDeclined = $false # DryRun’da devam edelim
} else {
$undeclined = $false
# A) İç arayüze cast ederek SetDeclined(false) çağır
try {
$intfType = [type]'Microsoft.UpdateServices.Internal.BaseApi.IUpdate'
if ($intfType) {
$iu = $update -as $intfType
if ($iu) {
$iu.SetDeclined($false)
$update.Refresh()
if (-not $update.IsDeclined) { $undeclined = $true }
}
}
} catch { }
# B) NonPublic reflection ile SetDeclined(false)
if (-not $undeclined) {
try {
$flags = [System.Reflection.BindingFlags]'Instance,NonPublic'
$m = $update.GetType().GetMethod('SetDeclined', $flags, $null, @([bool]), $null)
if ($m) {
[void]$m.Invoke($update, @($false))
$update.Refresh()
if (-not $update.IsDeclined) { $undeclined = $true }
}
} catch { }
}
# C) (son çare) Approve(NotApproved) + Refresh — bazı ortamlarda flag’i temizliyor
if (-not $undeclined) {
try {
$update.Approve([Microsoft.UpdateServices.Administration.UpdateApprovalAction]::NotApproved, $targetGroup) | Out-Null
$update.Refresh()
if (-not $update.IsDeclined) { $undeclined = $true }
} catch { }
}
if ($undeclined) {
Write-Host "Undeclined: $($update.Title)"
$isDeclined = $false
} else {
Write-Warning "Failed to undecline: $($update.Title). Skipping approve."
$undeclineFailures++
continue
}
}
}
# Onay zaten var mı?
$alreadyApproved = $false
foreach ($appr in $update.GetUpdateApprovals()) {
if (($appr.ComputerTargetGroupId -eq $targetGroup.Id) -and
($appr.Action -eq [Microsoft.UpdateServices.Administration.UpdateApprovalAction]::Install)) {
$alreadyApproved = $true; break
}
}
if (-not $alreadyApproved) {
if ($DryRun) {
Write-Host "[DRY-RUN] Would APPROVE: $($update.Title)"
} else {
try {
$update.Approve([Microsoft.UpdateServices.Administration.UpdateApprovalAction]::Install, $targetGroup) | Out-Null
Write-Host "Approved: $($update.Title)"
$approvedCount++
} catch {
Write-Warning "Error while approving: $($update.Title) - $($_.Exception.Message)"
}
}
} else {
Write-Host "Already approved: $($update.Title)"
}
}
elseif ($creationDate -lt $cutoffDate -and -not $isDeclined -and $AllowDecline.IsPresent) {
# --- DECLINE path ---
if ($DryRun) {
Write-Host "[DRY-RUN] Would DECLINE: $($update.Title)"
} else {
try { $update.Decline(); Write-Host "Declined: $($update.Title)"; $declinedCount++ }
catch { Write-Warning "Error while declining: $($update.Title) - $($_.Exception.Message)" }
}
}
}
# Özet
if ($DryRun) { Write-Host "`nSimulation completed. No updates were actually approved or declined." }
Write-Host ("Scanned: {0} | Pass(Product): {1} | Pass(Class): {2} | Pass(Title): {3} | Pass(Required): {4} | Skipped(Expired): {5} | Skipped(Superseded): {6} | UndeclineFailed: {7}" -f `
$scanned, $passProduct, $passClass, $passTitle, $passRequired, $skippedExpired, $skippedSuperseded, $undeclineFailures)
if (-not $DryRun) {
Write-Host "Operation completed. Total approved: $approvedCount | Total declined: $declinedCount | Skipped by RequiredFilter: $skippedByRequired"
}
}
1) Genel Bakış
2) Sistem Gereksinimleri & Önkoşullar
3) Parametreler (Tam Referans)
Parametre
Tip
Varsayılan
Açıklama
4) RequiredFilter Sözdizimi
5) Onay/Ret (Approve/Decline) Davranışı
6) “Declined → Undecline → Approve” Mantığı
7) Çıktılar & Sayaçlar
8) Kullanım Senaryoları (Örnekler)
8.1) Son 30 gün & Required > 0 olanları approve et (decline yok)
8.2) 1 yıl geriye bak, Tools/Documents ürünleri, alt gruplar dahil, approve et
8.3) SSL’li WSUS, belirli grup, “Cumulative” başlıklı güvenlik güncellemelerini onayla
8.4) Eski güncellemeleri decline et (ör. 120 günden eski & hiç ihtiyaç yok)
8.5) DryRun ile güvenli prova
8.6) Undecline + Approve akışı açık (varsayılan) — pratik örnek
9) En İyi Uygulamalar
10) Sık Karşılaşılan Sorunlar & Çözümler
10.1) “0 approved | 0 declined” (ama filtreye uyan güncellemeler var)
10.2) “Failed to undecline… Skipping approve.”
10.3) “Argument type cannot be System.Void.”
10.4) “Cannot validate argument on parameter 'ProductMatchMode'. The argument "False"…”
"False"…”11) Güvenlik & İdempotans
12) Hızlı Referans (Kısa Özet)
13) Sürüm Notu (Bu belgeye göre)
14) Önerilen Komut Şablonları
WSUS Export & Import PowerShell Script
Parametreler
Parametre
Tür
Açıklama
Usage
Örnek 1: WSUS verisini ve içeriğini dışa aktar (Export)
Örnek 2: WSUS verisini ve içeriğini yedekten geri yükle (Import)
Örnek 3: Özel bir yedekleme klasörü ile kullan
Örnek 4: Özel WSUS kurulum dizinleri ile kullanım
Oluşan Dosyalar ve Klasörler
Dosya/Klasör
Açıklama
İpuçları
Last updated