Auto-commit: 2025-10-31 08:55:43

This commit is contained in:
David Wuibaille
2025-10-31 08:55:43 +01:00
parent 9bb5ad24bb
commit 24c0c6509f
33 changed files with 13144 additions and 0 deletions

8
.gitignore vendored
View File

@@ -4,3 +4,11 @@ config*.json
.env.*
*.key
*.pem
# Ignore local config.json
config.json
# Ignore log files
*.log
# Ignore CSV exports
*.csv

View File

@@ -0,0 +1,42 @@
# Connect to WSUS Integrated Database (WID)
Connect locally to the WSUS Windows Internal Database (WID) via the named pipe to run maintenance or reporting against the `SUSDB` database. WID does not allow remote connections—run tools **on the WSUS server** with administrative privileges.
## Prerequisites
- Run on the WSUS server, **elevated** (Run as Administrator).
- One of: SSMS, `sqlcmd`, or the PowerShell `SqlServer` module.
- Pipe (WID on Server 2012+): `\\.\pipe\MICROSOFT##WID\tsql\query`
Database: `SUSDB`
## SSMS (GUI)
1. Start SSMS “Run as administrator”.
2. Server type: Database Engine
Server name: `\\.\pipe\MICROSOFT##WID\tsql\query`
Authentication: Windows Authentication
3. Connect and open a new query window.
Quick smoke test:
```sql
SELECT @@VERSION AS SqlEngineVersion, DB_NAME() AS CurrentDatabase;
SELECT TOP (5) name, create_date FROM sys.tables ORDER BY create_date DESC;
```
## sqlcmd (CLI)
```bat
:: Run locally on the WSUS server (elevated)
sqlcmd -S np:\\.\pipe\MICROSOFT##WID\tsql\query -d SUSDB -E -Q "SELECT TOP (1) GETDATE() AS ConnectedAt;"
```
## PowerShell (Invoke-Sqlcmd)
```PowerShell
Import-Module SqlServer
$Instance = "\\.\pipe\MICROSOFT##WID\tsql\query"
$Database = "SUSDB"
# One-liner test
Invoke-Sqlcmd -ServerInstance $Instance -Database $Database -Query "SELECT TOP (1) GETDATE() AS ConnectedAt;"
# Run a maintenance script
$FileSql = ".\Maintenance.sql" # e.g., reindex/cleanup queries
Invoke-Sqlcmd -ServerInstance $Instance -Database $Database -InputFile $FileSql
```

37
Dashboard/readme.md Normal file
View File

@@ -0,0 +1,37 @@
# WSUS HTML Report (PowerShell)
Generates a **single-page HTML dashboard** for WSUS: target-group inventory, KB deployment status (timeline + donuts), per-KB computer states, and a table of computers with failed updates.
## Features
- Computers by **Target Group** (table + pie).
- KB status per group: **Installed vs Missing** and **approval age timeline**.
- Per-KB list of affected computers (hidden sections you can expand).
- **Failed** updates: computers in error.
## Requirements
- Run on the **WSUS server** as Administrator.
- Module: `PSWriteHTML`
```powershell
Install-Module PSWriteHTML -Scope CurrentUser
```
- WSUS API available (either the `UpdateServices` module or
`Microsoft.UpdateServices.Administration.dll` at
`"%ProgramFiles%\Update Services\Tools\Microsoft.UpdateServices.Administration.dll"`).
- The script uses `-Online` (CDN assets). Remove that flag if your server has no internet.
## Quick start
```powershell
# 1) Save the script as: C:\Scripts\Wsus-Report.ps1
# 2) Run it (elevated)
powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Wsus-Report.ps1
# Output:
# C:\exploit\report\default.htm
```
## Customize
- Output path: `$outFile` (default `C:\exploit\report\default.htm`).
- Exclusions for titles (ARM64, SSU, Flash, etc.) in the KB section.
- Table/chart IDs if you embed multiple reports.
## Full article
https://blog.wuibaille.fr/2024/10/creating-a-wsus-dashboard/

231
Dashboard/report.ps1 Normal file
View File

@@ -0,0 +1,231 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Stop'
# ****************************** MODULE IMPORT & INITIALIZATION ******************************
# HTML reporting module
Import-Module -Name PSWriteHTML -ErrorAction Stop
Write-Host "Starting WSUS report generation..."
# Load WSUS admin assembly explicitly (no deprecated LoadWithPartialName)
$wsusDll = "$env:ProgramFiles\Update Services\Tools\Microsoft.UpdateServices.Administration.dll"
if (Test-Path $wsusDll) { Add-Type -Path $wsusDll } else { throw "WSUS Admin DLL not found: $wsusDll" }
# Connect to local WSUS and pull data
$wsusServer = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
$updates = $wsusServer.GetUpdates()
$computers = $wsusServer.GetComputerTargets()
$targetGroups = $wsusServer.GetComputerTargetGroups()
# Map groupId -> groupName for quick lookups
$groupHashTable = @{}
$targetGroups | ForEach-Object { $groupHashTable[$_.ID] = $_.Name }
Write-Host "Retrieved WSUS data: Updates, Computers, and Target Groups."
# Ensure output folder exists
$outFile = "C:\exploit\report\default.htm"
$outDir = Split-Path $outFile -Parent
if (-not (Test-Path $outDir)) { New-Item -Path $outDir -ItemType Directory -Force | Out-Null }
# ****************************** REPORT GENERATION START ******************************
New-HTML -TitleText 'WSUS Report' {
# ****************************** COMPUTERS BY TARGET GROUP ******************************
# Build a simple dataset: group name + computer count (skip 'All Computers')
$DataTable = New-Object System.Collections.Generic.List[Object]
foreach ($group in $targetGroups) {
$computersCount = $group.GetComputerTargets().Count
if ($computersCount -gt 0 -and $group.Name -ne 'All Computers') {
$DataTable.Add([PSCustomObject]@{
TargetName = $group.Name
Total = $computersCount
})
}
}
Write-Host "Populated DataTable with computer counts per Target Group."
New-HTMLSection -HeaderText 'Computers By TargetGroup' {
# Table
New-HTMLPanel {
New-HTMLTable -DataTable $DataTable -HideFooter -DataTableID 'IDtargetGroup' {
# Bind events to the correct table ID
New-TableEvent -ID 'IDtargetGroup' -SourceColumnID 0 -TargetColumnId 0
}
}
# Pie chart per group
New-HTMLPanel {
New-HTMLChart {
New-ChartToolbar -Download
foreach ($row in $DataTable) { New-ChartPie -Name $row.TargetName -Value $row.Total }
}
}
}
# ****************************** COMPUTER LIST (HIDDEN) ******************************
$ListeComputersWSUS = @()
foreach ($computer in $computers) {
foreach ($tg in $computer.GetComputerTargetGroups()) {
if ($tg.Name -ne 'All Computers') {
$ListeComputersWSUS += [PSCustomObject]@{
TargetName = $tg.Name
ComputerName = $computer.FullDomainName
}
}
}
}
New-HTMLSection -HeaderText 'Computers' -Invisible {
New-HTMLTable -DataTable $ListeComputersWSUS -DataTableID 'AllComputertargetGroup' -HideFooter
}
# ****************************** DEPLOYMENT STATUS OF KB UPDATES ******************************
$DateNow = Get-Date
$ListeKB = @()
$MaxDaysReport = 90 # (kept for future filtering if needed)
foreach ($update in $updates) {
foreach ($approval in $update.GetUpdateApprovals()) {
foreach ($tg in $targetGroups) {
if ($tg.Id -eq $approval.ComputerTargetGroupId) {
$DateKB = $approval.GoLiveTime
$LastChange = New-TimeSpan -Start $DateKB -End $DateNow
$ActionType = $approval.Action
if ($ActionType -eq 'Install') {
$KBTitre = $update.Title
$installKB = 0; $MissKB = 0
# Summaries per group for this update
foreach ($sum in $update.GetSummaryPerComputerTargetGroup()) {
$groupName = $groupHashTable[$sum.ComputerTargetGroupId]
if ($tg.Name -eq $groupName) {
$installKB = $sum.InstalledCount + $sum.InstalledPendingRebootCount
$MissKB = $sum.UnknownCount + $sum.NotInstalledCount + $sum.DownloadedCount + $sum.FailedCount
}
}
# Keep only meaningful KBs (exclude ARM64, SSU, Flash, specific KBs)
if (($installKB -ne 0 -or $MissKB -ne 0) -and
($KBTitre -notmatch 'ARM64') -and
($KBTitre -notmatch 'Servicing Stack Update') -and
($KBTitre -notmatch 'Flash Player') -and
($KBTitre -notlike '*KB4470788*') -and
($KBTitre -notlike '*KB4499728*')) {
$ListeKB += [PSCustomObject]@{
KBTitre = $KBTitre
KBGroup = $tg.Name
KBChang = $LastChange.Days
ActionType = $ActionType
installKB = $installKB
MissKB = $MissKB
}
}
}
}
}
}
}
# Sort by age then title; get unique titles for chart grouping
$ListeKB = $ListeKB | Sort-Object KBChang, KBTitre
$groupedByTitle = @{}
foreach ($o in $ListeKB) {
if (-not $groupedByTitle.ContainsKey($o.KBTitre)) { $groupedByTitle[$o.KBTitre] = @() }
$groupedByTitle[$o.KBTitre] += $o
}
foreach ($Title in $groupedByTitle.Keys) {
# Timeline per group (how many days since approval)
New-HTMLSection -HeaderText $Title {
New-HTMLChart -Title $Title -TitleAlignment center {
foreach ($o in $groupedByTitle[$Title]) {
New-ChartTimeline -DateFrom (Get-Date).AddDays(-$o.KBChang - 1) -DateTo (Get-Date) -Name $o.KBGroup
}
}
}
# Donut charts (Installed vs Missing) per group hidden
New-HTMLSection -HeaderText 'Computers' -Invisible {
foreach ($o in $groupedByTitle[$Title]) {
New-HTMLPanel {
New-HTMLChart -Title $o.KBGroup {
New-ChartLegend -Name "Installed","Missing" -Color Green,Red
New-ChartDonut -Name "Installed" -Value $o.installKB
New-ChartDonut -Name "Missing" -Value $o.MissKB
}
}
}
}
# ------------------------- KB and Computers status table (hidden) -------------------------
$updateScope2 = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope2.ApprovedStates = [Microsoft.UpdateServices.Administration.ApprovedStates]::LatestRevisionApproved
$updateScope2.UpdateApprovalActions = [Microsoft.UpdateServices.Administration.UpdateApprovalActions]::Install
$updateScope2.UpdateSources = [Microsoft.UpdateServices.Administration.UpdateSources]::MicrosoftUpdate
$updateScope2.ExcludedInstallationStates = @('NotApplicable','Installed','InstalledPendingReboot')
$allComputers = $wsusServer.GetComputerTargets()
$resultsComputerStatus = @()
foreach ($pc in $allComputers) {
$updatelist = $pc.GetUpdateInstallationInfoPerUpdate($updateScope2)
foreach ($ui in $updatelist) {
$updInfo = $ui.GetUpdate()
$approvalGroup = $ui.GetUpdateApprovalTargetGroup().Name
if ($approvalGroup -ne 'All Computers' -and $updInfo.IsApproved) {
$resultsComputerStatus += [pscustomobject][Ordered]@{
ComputerName = $pc.FullDomainName
Status = $ui.UpdateInstallationState
ApprovalTargetGroup= $approvalGroup
Approved = $updInfo.IsApproved
Title = $updInfo.Title
}
}
}
}
New-HTMLSection -HeaderText 'Computers' -Invisible {
foreach ($o in $groupedByTitle[$Title]) {
New-HTMLPanel {
$specific = $resultsComputerStatus |
Where-Object { $_.Title -like "*$Title*" -and $_.ApprovalTargetGroup -like "*$($o.KBGroup)*" } |
Select-Object ComputerName, Status
New-HTMLTable -DataTable $specific -HideFooter -HideButtons
}
}
}
}
# ****************************** COMPUTERS IN ERROR ******************************
$computersInError = @()
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$updateScope.IncludedInstallationStates = 'Failed'
$computerScope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
$computerScope.IncludedInstallationStates = 'Failed'
$computersInError = $wsusServer.GetComputerTargets($computerScope) | ForEach-Object {
$compName = $_.FullDomainName
$failedTitles = ($_.GetUpdateInstallationInfoPerUpdate($updateScope) | ForEach-Object {
$_.GetUpdate().Title
}) -join ', '
if ($failedTitles) {
[pscustomobject]@{
Computername = $compName
TargetGroups = ($_.GetComputerTargetGroups() | Select-Object -ExpandProperty Name) -join ', '
Updates = $failedTitles
}
}
} | Sort-Object Computername
New-HTMLSection -HeaderText 'Computers in ERROR' {
New-HTMLTable -DataTable $computersInError -DataTableID 'NewIDtoSearchInChartERR' -HideFooter
}
# ****************************** FOOTER ******************************
New-HTMLFooter {
New-HTMLText -Text ("Report generated (GMT): {0:u}" -f (Get-Date).ToUniversalTime()) -Color Blue -Alignment center
}
} -FilePath $outFile -Online
Write-Host "WSUS report has been generated successfully at $outFile"

123
auto-approval/approval.ps1 Normal file
View File

@@ -0,0 +1,123 @@
$SyncApprovals = @(
# Sync TESTERS -> PILOT
@{
"Source" = "Pilot" # Source group: TESTERS
"Target" = "Global1" # Target group: PILOT
"MinDays" = 5 # Minimum number of days before synchronization
},
# Sync PILOT -> PROD
@{
"Source" = "Global1" # Source group: PILOT
"Target" = "Global2" # Target group: PROD
"MinDays" = 5 # Minimum number of days before synchronization
}
)
$logFolder = "C:\logs"
$maxLogs = 60
# Ensure the log folder exists (create if missing)
If (-not (Test-Path $logFolder)) {
New-Item -Path $logFolder -ItemType Directory
Write-Host "The directory $logFolder has been created."
} else {
Write-Host "The directory $logFolder already exists."
}
# Limit the number of log files (keep the most recent $maxLogs)
$logFiles = Get-ChildItem -Path $logFolder -Filter "WSUS-ManageApprovals*.log" | Sort-Object LastWriteTime -Descending
if ($logFiles.Count -gt $maxLogs) {
$logFiles | Select-Object -Skip $maxLogs | Remove-Item -Force
}
# Build current log file path
$logFile = Join-Path $logFolder ("WSUS-ManageApprovals" + (Get-Date -format "yyyyMMdd-HHmmss") + ".log")
# Connect to the WSUS server
Try {
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | Out-Null
$wsusServer = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
$subscription = $wsusServer.GetSubscription()
$wsusServerConfig = $wsusServer.GetConfiguration()
$targetGroups = $wsusServer.GetComputerTargetGroups()
$updates = $wsusServer.GetUpdates()
} Catch {
Write-Output "Error connecting to the WSUS server: $_" | Add-Content -Path $logFile
Exit
}
# Start a WSUS synchronization (non-blocking)
Try {
$subscription.StartSynchronization()
Write-Output "Synchronization successfully started." | Add-Content -Path $logFile
} Catch {
Write-Output "Error during WSUS synchronization: $_" | Add-Content -Path $logFile
Exit
}
# Filter working updates (exclude declined) and accept EULAs if needed
$workingUpdates = $updates | Where-Object { -not $_.IsDeclined }
Foreach ($update in $workingUpdates) {
# Accept license agreement if required
If ($update.RequiresLicenseAgreementAcceptance) {
$update.AcceptLicenseAgreement()
Write-Output "License accepted for: $($update.Title)" | Add-Content -Path $logFile
}
}
# Approve updates to target groups after MinDays since source approval GoLiveTime
Write-Output "********** Approve KB **********" | Add-Content -Path $logFile
Foreach ($update in $workingUpdates) {
$approvals = $update.GetUpdateApprovals()
Foreach ($syncApproval in $SyncApprovals) {
$sourceGroup = $targetGroups | Where-Object { $_.Name -eq $syncApproval.Source }
# Ensure source group exists and action is Install
If ($sourceGroup) {
$sourceApproval = $approvals | Where-Object { $_.ComputerTargetGroupId -eq $sourceGroup.ID }
If ($sourceApproval -and $($sourceApproval.Action) -eq "Install") {
# Check if enough days have passed since GoLiveTime
$LastChangeKB = (New-TimeSpan -Start $sourceApproval.GoLiveTime -End (Get-Date)).Days
If ($LastChangeKB -ge $syncApproval.MinDays) {
$targetGroup = $targetGroups | Where-Object { $_.Name -eq $syncApproval.Target }
$targetApproval = $approvals | Where-Object { $_.ComputerTargetGroupId -eq $targetGroup.ID }
# Approve the update if it's not already approved for installation
If ($($targetApproval.Action) -ne "Install") {
Write-Output "Approving: $($syncApproval.Target) => $($update.Title) - $LastChangeKB days" | Add-Content -Path $logFile
$update.Approve("Install", $targetGroup) | Out-Null
}
}
}
} else {
Write-Output "Source group $($syncApproval.Source) not found." | Add-Content -Path $logFile
}
}
}
# Decline superseded updates if a superseding one is installed for the expected target groups
Write-Output "********** Disable Superseded Updates **********" | Add-Content -Path $logFile
$workingUpdates = $workingUpdates | Where-Object { $_.IsSuperseded }
Foreach ($update in $workingUpdates) {
Write-Output "$($update.Title)" | Add-Content -Path $logFile
# Find updates that supersede the current update
Foreach ($Supersede in $update.GetRelatedUpdates([Microsoft.UpdateServices.Administration.UpdateRelationship]::UpdatesThatSupersedeThisUpdate)) {
Write-Output "----- Replaced By: $($Supersede.Title)" | Add-Content -Path $logFile
$approvals = $Supersede.GetUpdateApprovals()
# If a superseding update is approved for Install in Global2, decline the superseded one
Foreach ($approval in $approvals) {
Foreach ($targetGroup in $targetGroups) {
If (($targetGroup.Id -eq $approval.ComputerTargetGroupId) -and ($($approval.Action) -eq "Install")) {
Write-Output "---------- $($targetGroup.Name)" | Add-Content -Path $logFile
if ($($targetGroup.Name) -eq "Global2") { # fixed: target group is Global2
Write-Output "---------- Declining: $($update.Title)" | Add-Content -Path $logFile
$update.Decline()
}
}
}
}
}
}

31
auto-approval/readme.md Normal file
View File

@@ -0,0 +1,31 @@
# WSUS Automate Patch Assignment to Groups
PowerShell script to automatically **promote WSUS approvals** across groups after a delay, accept EULAs, and decline superseded updates when a newer update is approved in production.
## What it does
- Sync approvals after X days:
- `Pilot → Global1`
- `Global1 → Global2`
- Accept license agreements when required.
- Decline superseded updates if a superseding update is approved for **Global2**.
- Write rotating logs to `C:\Logs` (keeps the latest 60 files).
## Requirements
- Run on the WSUS server with **Administrator** privileges.
- WSUS installed (PowerShell `UpdateServices` module or the WSUS Admin DLL available).
- Existing WSUS groups: `Pilot`, `Global1`, `Global2` (adjust names if different).
## Configuration
Edit the top of the script:
- `\$SyncApprovals` define source/target groups and `MinDays`.
- `\$logFolder`, `\$maxLogs` logging folder and retention.
- Superseded-decline rule currently targets `Global2`.
## Quick start
```powershell
# Run the script (no parameters)
.\Wsus-ManageApprovals.ps1
```
## Full documentation
https://blog.wuibaille.fr/2024/10/automate-assign-patch-to-group/

View File

@@ -0,0 +1,32 @@
#Requires -RunAsAdministrator
[CmdletBinding()]
param(
[string]$ServerName = 'localhost',
[int]$Port = 8530,
[switch]$UseSsl
)
# Load the WSUS admin assembly explicitly if the module isn't present
$wsusDll = "$env:ProgramFiles\Update Services\Tools\Microsoft.UpdateServices.Administration.dll"
if (-not (Get-Module -ListAvailable UpdateServices)) {
if (Test-Path $wsusDll) { Add-Type -Path $wsusDll } else { throw "WSUS Admin DLL not found: $wsusDll" }
}
try {
$wsusServer = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer($ServerName, [bool]$UseSsl, $Port)
$cleanupInterface = $wsusServer.GetCleanupManager()
$cleanupScope = New-Object Microsoft.UpdateServices.Administration.CleanupScope
$cleanupScope.DeclineSupersededUpdates = $true
$cleanupScope.DeclineExpiredUpdates = $true
$cleanupScope.CleanupObsoleteComputers = $true
$cleanupScope.CleanupObsoleteUpdates = $true
$cleanupScope.CompressUpdates = $true
$cleanupScope.CleanupUnneededContentFiles = $true
$cleanupInterface.PerformCleanup($cleanupScope)
Write-Host "WSUS cleanup completed."
}
catch {
Write-Error "WSUS cleanup failed: $($_.Exception.Message)"
exit 1
}

28
cleanup-server/readme.md Normal file
View File

@@ -0,0 +1,28 @@
# WSUS Cleanup (PowerShell)
Powershell scripts to run a safe, repeatable **WSUS** cleanup:
- Decline superseded/expired updates
- Remove obsolete updates/computers
- Compress updates
- Delete unneeded content files
Works with the supported WSUS cmdlets (`UpdateServices` module). A legacy .NET fallback is included.
---
## Requirements
- Run on the WSUS server in an **elevated** PowerShell session.
- WSUS PowerShell module: `UpdateServices` (installed with WSUS/RSAT).
- Port **8530** (HTTP) or **8531** (HTTPS).
- Expect long runtimes on large servers; schedule outside business hours.
## Quick start
```powershell
# Standard full cleanup (HTTP 8530)
.\Wsus-Cleanup.ps1 -Verbose
# HTTPS on 8531
.\Wsus-Cleanup.ps1 -UseSsl -Port 8531 -Verbose
```

View File

@@ -0,0 +1,10 @@
# Generate a Windows Update Log
## Procedure
1) Generate the Windows Update log file
Use the `Get-WindowsUpdateLog` cmdlet in PowerShell to merge the Windows Update ETL files into a single log file.
2) Locate the generated log file
`Get-WindowsUpdateLog` creates `WindowsUpdate.log` on your Desktop by default.

View File

@@ -0,0 +1,72 @@
# MSU Prerequisite Check and Install Script
$LogPath = "C:\Windows\Temp\MSU_Install.log"
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $LogPath -Value "$timestamp $Message"
Write-Host $Message
}
# Check 1: Reboot pending
$RebootRegKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
if (Test-Path $RebootRegKey) {
Write-Log "Pending reboot detected."
exit 101
} else {
Write-Log "No pending reboot."
}
# Check 3: Free space on C:
$minFreeGB = 5
$drive = Get-PSDrive -Name C
if ($null -eq $drive -or ($drive.Free/1GB) -lt $minFreeGB) {
Write-Log "Insufficient free space on C:. Required: ${minFreeGB}GB"
exit 103
} else {
Write-Log ("Sufficient free space on C:. Free: {0:N2}GB" -f ($drive.Free/1GB))
}
# MSU download info
$MSUFile = "windows10.0-kb5060531-x64.msu"
$MSUUrl = "https://catalog.s.download.windowsupdate.com/c/msdownload/update/software/secu/2025/06/windows10.0-kb5060531-x64_83789c3b9350e10e207370622c4ef54dd685ee02.msu"
$ScriptDir = "c:\windows\temp"
$MSUPath = Join-Path $ScriptDir $MSUFile
# Always delete the MSU file before download
if (Test-Path $MSUPath) {
Write-Log "Deleting existing MSU file: $MSUPath"
Remove-Item -Path $MSUPath -Force -ErrorAction SilentlyContinue
}
try {
Write-Log "Downloading MSU from $MSUUrl using System.Net.WebClient"
$sw = [System.Diagnostics.Stopwatch]::StartNew()
$wc = New-Object System.Net.WebClient
$wc.DownloadFile($MSUUrl, $MSUPath)
$sw.Stop()
$downloadTime = "{0:N2}" -f $sw.Elapsed.TotalSeconds
if (-not (Test-Path $MSUPath)) {
Write-Log "Failed to download MSU (file not found after download)"
exit 104
} else {
Write-Log "Download completed: $MSUPath"
Write-Log "Download time: $downloadTime seconds"
}
} catch {
Write-Log "Failed to download MSU with WebClient: $_"
exit 104
}
Write-Log "Starting installation of $MSUPath"
# Install the MSU silently, no restart
$process = Start-Process -FilePath "wusa.exe" -ArgumentList "`"$MSUPath`" /quiet /norestart" -Wait -PassThru
$exitCode = $process.ExitCode
Write-Log "wusa.exe exit code: $exitCode"
# Return the exit code as the script's own exit code
exit $exitCode

View File

@@ -0,0 +1,250 @@
#requires -version 5.1
<#
MSU Prereq + Install + WS logging + STRICT selection by FULL BUILD (major.UBR)
- Mapping is downloaded from https://nas.wuibaille.fr/WS/patchs/msu.json
- Supports multiple packages per build (e.g., SSU then LCU)
- JSON needs only: { "<build>": { "packages": [ { "url": "...", "type": "LCU", "order": 20 }, ... ] } }
- No defaults: if build key missing -> exit 111
- If JSON cannot be downloaded and no cache -> exit 112
- If cached JSON invalid -> exit 113
#>
# -------- Settings --------
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue'
$ComputerName = $env:COMPUTERNAME
$LogPath = "C:\Windows\Temp\MSU_Install.log"
$ApiUrl = "https://nas.wuibaille.fr/WS/message.php"
$SendLogsToWS = $true
# Remote JSON map + local cache
$MsuMapUrl = "https://nas.wuibaille.fr/WS/patchs/msu.json"
$MsuMapCache = "C:\Windows\Temp\msu.json"
# Optional: force a build key like '17763.7786'
$OverrideBuildFull = $null
# -------- Helpers --------
function Write-Log {
param([Parameter(Mandatory)][string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$fullMessage = "[$ComputerName] $Message"
Add-Content -Path $LogPath -Value "$timestamp $fullMessage" -Encoding UTF8
Write-Host $fullMessage
if ($SendLogsToWS) {
try {
$body = @{ message = $fullMessage } | ConvertTo-Json -Depth 2
$null = Invoke-RestMethod -Uri $ApiUrl -Method POST -Body $body -ContentType "application/json" -TimeoutSec 10 -ErrorAction Stop
} catch {
Write-Warning ("[$ComputerName] Webservice log failed: {0}" -f $_.Exception.Message)
}
}
}
function Get-BuildFull {
# Returns PSCustomObject with BuildMajor, UBR, BuildFull="major.UBR"
$cvPath = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$cv = Get-ItemProperty -LiteralPath $cvPath
$buildStr = [string]$cv.CurrentBuild
if ([string]::IsNullOrWhiteSpace($buildStr)) { $buildStr = [string]$cv.CurrentBuildNumber }
$buildMajor = [int]$buildStr
$ubr = 0; try { $ubr = [int]$cv.UBR } catch {}
[PSCustomObject]@{
BuildMajor = $buildMajor
UBR = $ubr
BuildFull = ('{0}.{1}' -f $buildMajor, $ubr)
}
}
function Get-MSUMapFromJson {
# Downloads JSON (and caches it). On failure, tries cache; else exits.
Write-Log ("Fetching MSU map: {0}" -f $MsuMapUrl)
try {
$json = Invoke-RestMethod -Uri $MsuMapUrl -Method GET -TimeoutSec 15 -ErrorAction Stop
try { ($json | ConvertTo-Json -Depth 10) | Out-File -FilePath $MsuMapCache -Encoding UTF8 -Force } catch {}
return $json
} catch {
Write-Log ("Online map fetch failed: {0}" -f $_.Exception.Message)
if (Test-Path -LiteralPath $MsuMapCache) {
Write-Log ("Using cached map: {0}" -f $MsuMapCache)
try {
$raw = Get-Content -LiteralPath $MsuMapCache -Raw -ErrorAction Stop
$json = $raw | ConvertFrom-Json -ErrorAction Stop
return $json
} catch {
Write-Log ("Cached map is invalid: {0}. Exiting 113." -f $_.Exception.Message)
exit 113
}
} else {
Write-Log "No cached map available. Exiting 112."
exit 112
}
}
}
function Ensure-PackagesArray {
param(
[Parameter(Mandatory)]$Entry,
[Parameter(Mandatory)][string]$KeyForErrors
)
<#
Accepts:
- { "packages": [ { "url": "...", "type": "LCU", "order": 20 }, ... ] }
- { "url": "..." } (single package)
- "https://..." (single package as string)
Returns: array of package objects with at least .url
#>
if ($Entry -is [string]) {
if ($Entry -notmatch '^https?://') { throw "Invalid entry for build '$KeyForErrors': expected URL string." }
return @(@{ url = $Entry; type = $null; order = $null })
}
$props = @($Entry.PSObject.Properties.Name)
if ($props -contains 'packages') {
$arr = @($Entry.packages)
if (-not $arr -or $arr.Count -eq 0) { throw "Entry for build '$KeyForErrors' has empty 'packages'." }
foreach ($p in $arr) {
if (-not $p.url) { throw "One package in build '$KeyForErrors' is missing 'url'." }
}
return $arr
}
if ($props -contains 'url') {
return @(@{ url = [string]$Entry.url; type = $Entry.type; order = $Entry.order })
}
throw "Invalid entry format for build '$KeyForErrors'. Expected 'packages' array or 'url' string/property."
}
function Sort-Packages {
param([Parameter(Mandatory)][array]$Packages)
# Priority: explicit 'order' asc, else by 'type' (SSU=10, LCU=20, NET=30, other=50)
$prio = @{ 'SSU' = 10; 'LCU' = 20; 'NET' = 30 }
return $Packages | Sort-Object `
@{ Expression = { if ($_.order -ne $null -and "$($_.order)" -match '^\d+$') { [int]$_.order } else { 999 } } },
@{ Expression = { $t = ("" + $_.type).ToUpper(); if ($prio.ContainsKey($t)) { $prio[$t] } else { 50 } } }
}
function Resolve-PackagesForBuild {
param(
[Parameter(Mandatory)]$BuildInfo,
[Parameter(Mandatory)]$JsonMap,
[string]$OverrideBuildFull
)
$key = if ([string]::IsNullOrWhiteSpace($OverrideBuildFull)) { $BuildInfo.BuildFull } else { $OverrideBuildFull }
# Convert root object to hashtable keyed by build strings
$map = @{}
foreach ($p in $JsonMap.PSObject.Properties) { $map[$p.Name] = $p.Value }
if (-not $map.ContainsKey($key)) { throw ("No MSU mapping defined for full build '{0}'." -f $key) }
$entry = $map[$key]
$packages = Ensure-PackagesArray -Entry $entry -KeyForErrors $key
$packages = Sort-Packages -Packages $packages
return @{ Key=$key; Packages=$packages }
}
function Test-IsAdmin {
$wi = [Security.Principal.WindowsIdentity]::GetCurrent()
$wp = [Security.Principal.WindowsPrincipal]::new($wi)
return $wp.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Filename-FromUrl {
param([Parameter(Mandatory)][string]$Url)
try {
$u = [Uri]$Url
$name = [IO.Path]::GetFileName($u.AbsolutePath)
if ([string]::IsNullOrWhiteSpace($name)) { $name = "update.msu" }
} catch { $name = "update.msu" }
if ($name -notmatch '\.msu$') { $name = "$name.msu" }
return $name
}
# -------- Script start --------
if (-not (Test-IsAdmin)) { Write-Log "Warning: Script is not running elevated." }
# Build detection
$b = Get-BuildFull
Write-Log ("Detected OS: Build={0}" -f $b.BuildFull)
# Load mapping + resolve package list
$msuJson = Get-MSUMapFromJson
try {
$sel = Resolve-PackagesForBuild -BuildInfo $b -JsonMap $msuJson -OverrideBuildFull $OverrideBuildFull
Write-Log ("Selected {0} package(s) for Build={1}" -f $sel.Packages.Count, $sel.Key)
} catch {
Write-Log ("MSU selection error: {0}. Exiting 111." -f $_.Exception.Message)
exit 111
}
# Checks
try {
$RebootRegKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
if (Test-Path $RebootRegKey) { Write-Log "Pending reboot detected. Exiting 101."; exit 101 }
} catch { Write-Log ("Error while checking free space: {0}" -f $_.Exception.Message); exit 199 }
try {
$minFreeGB = 5
$drive = Get-PSDrive -Name C -ErrorAction Stop
$freeGB = [math]::Round(($drive.Free/1GB),2)
if ($freeGB -lt $minFreeGB) { Write-Log ("Insufficient free space: required {0}GB, available {1}GB. Exiting 103." -f $minFreeGB, $freeGB); exit 103 }
} catch { Write-Log ("Error while checking free space: {0}" -f $_.Exception.Message); exit 198 }
# Download + install each package in order
$ScriptDir = "C:\Windows\Temp"
if (-not (Test-Path -LiteralPath $ScriptDir)) { New-Item -ItemType Directory -Path $ScriptDir -Force | Out-Null }
$anyNeedReboot = $false
$anyInstalled = $false
foreach ($pkg in $sel.Packages) {
$url = [string]$pkg.url
if ([string]::IsNullOrWhiteSpace($url)) { Write-Log "Package without URL encountered. Aborting (bad JSON)."; exit 113 }
$type = ("" + $pkg.type)
$fileName = Filename-FromUrl -Url $url
$destPath = Join-Path $ScriptDir $fileName
Write-Log ("Downloading {0}{1} from {2}" -f (if ($type) { "[$type] " } else { "" }), $fileName, $url)
try {
$wc = New-Object System.Net.WebClient
try {
$wc.Headers['User-Agent'] = ("MSU-Installer/1.0 ({0})" -f $ComputerName)
if (Test-Path -LiteralPath $destPath) { Remove-Item -LiteralPath $destPath -Force -ErrorAction Stop }
$wc.DownloadFile($url, $destPath)
} finally { if ($wc) { $wc.Dispose() } }
if (-not (Test-Path -LiteralPath $destPath)) { Write-Log "Download failed: file not found after download. Exiting 104."; exit 104 }
} catch {
Write-Log ("Failed to download package {0}: {1}. Exiting 104." -f $fileName, $_.Exception.Message)
exit 104
}
Write-Log ("Installing {0}{1}" -f (if ($type) { "[$type] " } else { "" }), $fileName)
try {
$proc = Start-Process -FilePath "wusa.exe" -ArgumentList "`"$destPath`" /quiet /norestart" -Wait -PassThru
$code = $proc.ExitCode
Write-Log ("wusa.exe exit code: {0} for {1}" -f $code, $fileName)
if ($code -eq 3010) { $anyNeedReboot = $true; $anyInstalled = $true }
elseif ($code -eq 0) { $anyInstalled = $true }
elseif ($code -eq 2359302) { } # already installed, continue
else {
Write-Log ("Installation failed for {0} with code {1}. Aborting." -f $fileName, $code)
exit $code
}
} catch {
Write-Log ("Installation failed for {0}: {1}" -f $fileName, $_.Exception.Message)
exit 195
}
}
# Overall exit code policy
if ($anyNeedReboot) { exit 3010 }
elseif ($anyInstalled) { exit 0 }
else { exit 2359302 }

View File

@@ -0,0 +1,329 @@
#requires -version 5.1
<#
Auto-install latest OS cumulative updates using PSWindowsUpdate (no JSON)
- Forces Windows Update public service (-WindowsUpdate)
- Installs SSU (if present) first, then latest non-Preview LCU
- ASCII-only logs; messages sanitized before sending to web service
#>
# -------- Settings --------
$ErrorActionPreference = 'Stop'
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
$ProgressPreference = 'SilentlyContinue'
$ComputerName = $env:COMPUTERNAME
$LogPath = "C:\Windows\Temp\MSU_Install.log"
$ApiUrl = "https://nas.wuibaille.fr/WS/message.php"
$SendLogsToWS = $true
# Optional WS retry tunables (defaults: 6 tries, 1s delay)
$script:WsMaxRetries = 6
$script:WsRetryDelaySec = 1
# Module install policy
$AllowInstallPSWindowsUpdate = $true
# -------- Utilities --------
function Sanitize-Ascii {
param([string]$Text)
if ($null -eq $Text) { return "" }
$t = $Text
$t = $t -replace [char]0x2013, "-" # en dash
$t = $t -replace [char]0x2014, "-" # em dash
$t = $t -replace [char]0x2026, "..." # ellipsis
$t = $t -replace [char]0x2018, "'" # left single quote
$t = $t -replace [char]0x2019, "'" # right single quote
$t = $t -replace [char]0x201C, '"' # left double quote
$t = $t -replace [char]0x201D, '"' # right double quote
$t = $t -replace [char]0x00A0, " " # nbsp
$t = $t -replace [char]0x2022, "-" # bullet
$t = [System.Text.RegularExpressions.Regex]::Replace($t, '[^\x09\x0A\x0D\x20-\x7E]', '')
return $t
}
# -------- Logging --------
function Write-Log {
param([Parameter(Mandatory)][string]$Message)
$msg = Sanitize-Ascii $Message
$full = "[$env:COMPUTERNAME] $msg"
$ts = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $LogPath -Value "$ts $full" -Encoding UTF8
Write-Host $full
if (-not $SendLogsToWS) { return }
$payload = @{ message = $full } | ConvertTo-Json -Compress
$bytes = [System.Text.Encoding]::UTF8.GetBytes($payload)
$max = if ($script:WsMaxRetries) { [int]$script:WsMaxRetries } else { 6 }
$delay = if ($script:WsRetryDelaySec) { [int]$script:WsRetryDelaySec } else { 1 }
$ok = $false
$lastErr = $null
for ($attempt = 1; $attempt -le $max -and -not $ok; $attempt++) {
try {
$null = Invoke-RestMethod -Uri $ApiUrl -Method POST -Body $bytes `
-ContentType 'application/json; charset=utf-8' -TimeoutSec 10 -ErrorAction Stop
$ok = $true
} catch {
$lastErr = $_.Exception.Message
if ($attempt -lt $max) { Start-Sleep -Seconds $delay }
}
}
if (-not $ok) {
Write-Warning "[$env:COMPUTERNAME] Webservice log failed after $max attempts: $lastErr"
}
}
# -------- Prereqs --------
function Test-IsAdmin {
$wi = [Security.Principal.WindowsIdentity]::GetCurrent()
$wp = [Security.Principal.WindowsPrincipal]::new($wi)
return $wp.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}
function Get-BuildFull {
$cv = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
$buildStr = [string]$cv.CurrentBuild
if ([string]::IsNullOrWhiteSpace($buildStr)) { $buildStr = [string]$cv.CurrentBuildNumber }
$major = [int]$buildStr
$ubr = 0; try { $ubr = [int]$cv.UBR } catch {}
[PSCustomObject]@{ BuildMajor=$major; UBR=$ubr; BuildFull=("{0}.{1}" -f $major,$ubr) }
}
# -------- PSWindowsUpdate bootstrap (silent) --------
function Ensure-PSWindowsUpdate {
try {
Import-Module PSWindowsUpdate -ErrorAction Stop
$m = Get-Module PSWindowsUpdate
Write-Log ("PSWindowsUpdate module loaded (v{0})." -f $m.Version)
return $true
} catch {}
if (-not $AllowInstallPSWindowsUpdate) {
Write-Log "PSWindowsUpdate module not found and auto-install is disabled. Exiting 120."
exit 120
}
$scope = if (Test-IsAdmin) { 'AllUsers' } else { 'CurrentUser' }
try {
$nuget = Get-PackageProvider -Name NuGet -ListAvailable -ErrorAction SilentlyContinue |
Sort-Object Version -Descending | Select-Object -First 1
if (-not $nuget -or ([Version]$nuget.Version -lt [Version]"2.8.5.201")) {
Write-Log "Installing NuGet provider silently..."
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force -Scope $scope -ErrorAction Stop
}
$repo = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
if (-not $repo) {
Write-Log "Registering PSGallery repository..."
try { Register-PSRepository -Default -ErrorAction Stop } catch {}
$repo = Get-PSRepository -Name PSGallery -ErrorAction SilentlyContinue
}
if ($repo -and $repo.InstallationPolicy -ne 'Trusted') {
Write-Log "Setting PSGallery as Trusted..."
Set-PSRepository -Name PSGallery -InstallationPolicy Trusted -ErrorAction SilentlyContinue
}
try {
$psget = Get-Module -ListAvailable PowerShellGet | Sort-Object Version -Descending | Select-Object -First 1
if (-not $psget -or ([Version]$psget.Version -lt [Version]"2.2.5")) {
Write-Log "Updating PowerShellGet silently..."
Install-Module PowerShellGet -Scope $scope -Force -AllowClobber -ErrorAction Stop
}
} catch {
Write-Log ("PowerShellGet update failed (continuing): {0}" -f $_.Exception.Message)
}
Write-Log "Installing PSWindowsUpdate silently..."
Install-Module PSWindowsUpdate -Scope $scope -Force -AllowClobber -ErrorAction Stop
Import-Module PSWindowsUpdate -ErrorAction Stop
$m = Get-Module PSWindowsUpdate
Write-Log ("PSWindowsUpdate installed and loaded (v{0})." -f $m.Version)
return $true
} catch {
Write-Log ("Failed to install/load PSWindowsUpdate: {0}. Exiting 120." -f $_.Exception.Message)
exit 120
}
}
# -------- Discovery & Install using PSWindowsUpdate --------
function Get-AvailableOSUpdates {
# Returns: @{ SSU = <update or $null>; LCU = <update or $null>; Raw = <array> }
try {
$raw = Get-WindowsUpdate -WindowsUpdate -UpdateType Software -IgnoreReboot -ErrorAction Stop
} catch {
Write-Log ("Windows Update query failed: {0}" -f $_.Exception.Message)
Write-Log "This system may be WSUS-managed or blocked by policy/network. Exiting 121."
exit 121
}
if (-not $raw) {
return [PSCustomObject]@{ SSU=$null; LCU=$null; Raw=@() }
}
$filtered = $raw | Where-Object {
$_.Title -and
($_.Title -notmatch 'Preview') -and
($_.Title -notmatch 'for\s*\.NET') -and
($_.Categories -notcontains 'Drivers') -and
($_.Categories -notcontains 'Definition Updates')
}
$norm = {
param($s)
if (-not $s) { return '' }
$t = Sanitize-Ascii $s
return $t.ToLowerInvariant()
}
$ssu = $filtered | Where-Object {
$t = & $norm $_.Title
($t -match 'servicing stack update' -or $t -match 'pile de maintenance')
} | Sort-Object -Property LastDeploymentChangeTime -Descending | Select-Object -First 1
$lcu = $filtered | Where-Object {
$t = & $norm $_.Title
($t -match 'cumulative update' -or $t -match 'mise a jour cumulative')
} | Sort-Object -Property LastDeploymentChangeTime -Descending | Select-Object -First 1
[PSCustomObject]@{ SSU=$ssu; LCU=$lcu; Raw=$filtered }
}
function Install-OneWU {
param([Parameter(Mandatory)]$UpdateObject)
$updateId = $UpdateObject.UpdateID
$title = Sanitize-Ascii $UpdateObject.Title
$kb = $UpdateObject.KB
$kbPrefix = if ($kb) { "$kb " } else { "" }
Write-Log ("Installing: {0}{1}" -f $kbPrefix, $title)
try {
if ($updateId) {
$res = Install-WindowsUpdate -WindowsUpdate -UpdateID $updateId -AcceptAll -IgnoreReboot -Confirm:$false -ErrorAction Stop
} elseif ($kb) {
$res = Install-WindowsUpdate -WindowsUpdate -KBArticleID $kb -AcceptAll -IgnoreReboot -Confirm:$false -ErrorAction Stop
} else {
$res = Install-WindowsUpdate -WindowsUpdate -AcceptAll -IgnoreReboot -Confirm:$false -ErrorAction Stop |
Where-Object { (Sanitize-Ascii $_.Title) -eq $title }
}
} catch {
Write-Log ("Install failed for {0}{1}: {2}" -f $kbPrefix, $title, $_.Exception.Message)
return [PSCustomObject]@{
Title = $title; KB = $kb; Result = 'Failed'; HResult = $null; RebootRequired = $false
}
}
# Build a clean result array without inline 'if'
$out = @()
foreach ($o in @($res)) {
$rTitle = Sanitize-Ascii $o.Title
$rKB = $null
if ($o.PSObject.Properties['KB'] -and $o.KB) { $rKB = $o.KB }
elseif ($o.PSObject.Properties['KBArticleID'] -and $o.KBArticleID) { $rKB = $o.KBArticleID }
$rResult = $null
if ($o.PSObject.Properties['Result'] -and $o.Result) { $rResult = $o.Result } else { $rResult = $o.ResultCode }
$rHResult = $null
if ($o.PSObject.Properties['HResult']) { $rHResult = $o.HResult }
$rReboot = $false
if ($o.PSObject.Properties['RebootRequired']) { $rReboot = [bool]$o.RebootRequired }
$out += [PSCustomObject]@{
Title = $rTitle
KB = $rKB
Result = $rResult
HResult = $rHResult
RebootRequired = $rReboot
}
}
return ,$out # ensure array even if single item
}
# =================== Main ===================
if (-not (Test-IsAdmin)) { Write-Log "Warning: script is not running elevated." }
try {
$RebootRegKey = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\WindowsUpdate\Auto Update\RebootRequired'
if (Test-Path $RebootRegKey) { Write-Log "Pending reboot detected. Exiting 101."; exit 101 }
} catch { Write-Log ("Error while checking pending reboot: {0}" -f $_.Exception.Message); exit 199 }
try {
$minFreeGB = 5
$drive = Get-PSDrive -Name C -ErrorAction Stop
$freeGB = [math]::Round(($drive.Free/1GB),2)
if ($freeGB -lt $minFreeGB) { Write-Log ("Insufficient free space: required {0}GB, available {1}GB. Exiting 103." -f $minFreeGB, $freeGB); exit 103 }
} catch { Write-Log ("Error while checking free space: {0}" -f $_.Exception.Message); exit 198 }
$build = Get-BuildFull
Write-Log ("Detected OS: Build={0}" -f $build.BuildFull)
Ensure-PSWindowsUpdate | Out-Null
$updates = Get-AvailableOSUpdates
# Plan (pre-calc strings to avoid inline if in -f)
if ($updates.SSU) {
$kbPrefix = if ($updates.SSU.KB) { "$($updates.SSU.KB) " } else { "" }
$title = Sanitize-Ascii $updates.SSU.Title
Write-Log ("Plan: SSU -> {0}{1}" -f $kbPrefix, $title)
}
if ($updates.LCU) {
$kbPrefix = if ($updates.LCU.KB) { "$($updates.LCU.KB) " } else { "" }
$title = Sanitize-Ascii $updates.LCU.Title
Write-Log ("Plan: LCU -> {0}{1}" -f $kbPrefix, $title)
}
if (-not $updates.SSU -and -not $updates.LCU) {
Write-Log "No OS SSU/LCU updates available (system appears up-to-date)."
exit 0
}
$needReboot = $false
$installed = $false
$failedAny = $false
if ($updates.SSU) {
$r = Install-OneWU -UpdateObject $updates.SSU
foreach ($i in @($r)) {
$kbPrefix = if ($i.KB) { "$($i.KB) " } else { "" }
$hrSuf = if ($i.HResult) { " (HResult=$($i.HResult))" } else { "" }
$rbSuf = if ($i.RebootRequired) { " [RebootRequired]" } else { "" }
Write-Log ("Result: {0}{1} => {2}{3}{4}" -f $kbPrefix, $i.Title, $i.Result, $hrSuf, $rbSuf)
if ($i.Result -match 'Succeeded') { $installed = $true }
elseif ($i.Result -match 'Failed|Aborted|Error') { $failedAny = $true }
if ($i.RebootRequired) { $needReboot = $true }
}
}
if ($updates.LCU) {
$r = Install-OneWU -UpdateObject $updates.LCU
foreach ($i in @($r)) {
$kbPrefix = if ($i.KB) { "$($i.KB) " } else { "" }
$hrSuf = if ($i.HResult) { " (HResult=$($i.HResult))" } else { "" }
$rbSuf = if ($i.RebootRequired) { " [RebootRequired]" } else { "" }
Write-Log ("Result: {0}{1} => {2}{3}{4}" -f $kbPrefix, $i.Title, $i.Result, $hrSuf, $rbSuf)
if ($i.Result -match 'Succeeded') { $installed = $true }
elseif ($i.Result -match 'Failed|Aborted|Error') { $failedAny = $true }
if ($i.RebootRequired) { $needReboot = $true }
}
}
if ($failedAny) { Write-Log "One or more updates failed."; exit 195 }
if ($needReboot) { exit 3010 }
if ($installed) { exit 0 }
exit 0

View File

@@ -0,0 +1,41 @@
# MSU Direct Download & Silent Install (PowerShell)
Download a specific **MSU** from Microsoft Update Catalog, run **pre-checks**, install with `wusa.exe /quiet /norestart`, and return the installers exit code. Logs to `C:\Windows\Temp\MSU_Install.log`.
## What it checks
- **Pending reboot** → exits **101**.
- **Free space on C:** ≥ **5 GB** (default) → exits **103**.
- **Download** success from the Catalog URL → exits **104** on failure.
- Otherwise runs `wusa` and returns its native **exit code** (`0` OK, `3010` reboot required).
## Quick start
1. Edit the variables at the top of the script:
- `$MSUUrl` (direct Catalog URL)
- `$MSUFile` (e.g. `windows10.0-kb5060531-x64.msu`)
- Optional: `$minFreeGB`
2. Run as Administrator:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Install-MSU.ps1
```
## Exit codes
- **0** = Installed successfully
- **3010** = Installed, reboot required (from `wusa`)
- **101** = Pending reboot detected
- **103** = Not enough free space on C:
- **104** = Download failed
- **others** = Native `wusa.exe` codes
## Requirements
- Windows 10/11 or Server, **elevated** PowerShell.
- Outbound access to the Microsoft Update Catalog URL.
- `wusa.exe` available (built-in).
## Notes
- The script deletes any existing MSU at the target path before downloading.
- If proxy/TLS blocks `WebClient`, switch to `Invoke-WebRequest` and enforce TLS 1.2:
```powershell
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Invoke-WebRequest -Uri $MSUUrl -OutFile $MSUPath -UseBasicParsing
```

View File

@@ -0,0 +1,73 @@
# Initializations
[reflection.assembly]::LoadWithPartialName("Microsoft.UpdateServices.Administration") | out-null
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
$computerScope = New-Object Microsoft.UpdateServices.Administration.ComputerTargetScope
$updateScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
$summariesComputerFailed = $wsus.GetSummariesPerComputerTarget($updateScope,$computerScope) | Where-Object FailedCount -NE 0 | Sort-Object FailedCount, UnknownCount, NotInstalledCount -Descending
$computers = Get-WsusComputer
$computersErrorEvents = $wsus.GetUpdateEventHistory([System.DateTime]::Today.AddDays(-7), [System.DateTime]::Today) | Where-Object ComputerId -ne [Guid]::Empty | Where-Object IsError -eq $true
function Get-TimeStamp {
return "[{0:MM/dd/yy} {0:HH:mm:ss}]" -f (Get-Date)
}
$csvFile = "$PSScriptRoot\WSUSFailedComputer_" + "{0:MMddyy}" -f (Get-Date) + ".csv"
Get-WsusComputer -ComputerUpdateStatus Failed | Export-Csv -Path $csvFile -NoTypeInformation
$ComputersErrorToday = Get-WsusComputer -ComputerUpdateStatus Failed
$csvList = (Get-ChildItem -Path $PSScriptRoot -Filter *.csv).FullName
$AllCsv = $csvList | Import-Csv
$AllCsv = $AllCsv | Sort-Object "FullDomainName"
$Ordinateur = @()
$CountError = 1
$OldComputer = "XXXX"
Foreach ($csv In $AllCsv) {
$FullDomainName = $csv.FullDomainName
$ExitError = $true
Foreach ($ComputerErrorToday In $ComputersErrorToday) {
$FullNameComputerErrorToday = $ComputerErrorToday.FullDomainName
if ($FullNameComputerErrorToday -eq $FullDomainName) { $ExitError = $false }
}
If (-not $ExitError) {
If ($OldComputer -eq $FullDomainName) {
$CountError += 1
} Else {
if ($OldComputer -ne "XXXX") {
$CountError = $CountError.ToString("0000")
$Ordinateur += "$CountError;$OldComputer"
}
$CountError = 1
$OldComputer = $FullDomainName
}
}
}
$Ordinateur = $Ordinateur | Sort-Object -Descending
$TotalAffiche = 10
$NbAffiche = 0
Foreach ($Computererror In $Ordinateur) {
If ($NbAffiche -lt $TotalAffiche) {
Write-Host $Computererror
$NbAffiche += 1
$NomOrdinateur = $Computererror.Substring(5)
ForEach ($computerFailed In $summariesComputerFailed) {
$computer = $computers | Where-Object Id -eq $computerFailed.ComputerTargetId
$Computername = $computer.FullDomainName
$ComputerIP = $computer.IPAddress
if ($Computername -eq $NomOrdinateur) {
$computerUpdatesFailed = ($wsus.GetComputerTargets($computerScope) | Where-Object Id -EQ $computerFailed.ComputerTargetId).GetUpdateInstallationInfoPerUpdate($updateScope) | Where-Object UpdateInstallationState -EQ "Failed"
ForEach ($update In $computerUpdatesFailed) {
$outputText = $wsus.GetUpdate($update.UpdateId).Title
Write-Host "------------ $outputText"
}
}
}
}
}

View File

@@ -0,0 +1,27 @@
# WSUS Failed Computers (Display.ps1)
Generate a CSV of client machines that had Windows Update installation errors over a recent time window, by querying **WSUS event history (errors only)**. Faster and more accurate than stitching multiple daily exports. If the `UpdateServices` module isnt present, the script uses the WSUS Admin .NET DLL directly.
---
## Requirements
- Run on the **WSUS server**, in an **elevated** 64-bit Windows PowerShell (5.1).
- WSUS PowerShell module `UpdateServices` (installed with WSUS/RSAT) **or** the Admin DLL:
`C:\Program Files\Update Services\Tools\Microsoft.UpdateServices.Administration.dll`
- Local access to WSUS (HTTP 8530 or HTTPS 8531).
---
## Install
1. Copy the script here: `DisplayToErrorComputer/Display.ps1`
2. Unblock if downloaded from the internet:
```powershell
Unblock-File .\Display.ps1
```
## How to run
Standard run (last 7 days, top 100):
```powershell
.\Display.ps1 -Days 7 -Top 100 -Verbose
```

View File

@@ -0,0 +1,61 @@
#Requires -RunAsAdministrator
$ErrorActionPreference = 'Stop'
# ---- Parameters ----
$Days = 7 # time window for error events
$Top = 10 # top N computers by error count
$OutCsv = "$PSScriptRoot\WSUS_FailedComputers_{0:yyyyMMdd}.csv" -f (Get-Date)
# ---- Connect to WSUS (module if available, else DLL) ----
if (Get-Module -ListAvailable UpdateServices) {
Import-Module UpdateServices -ErrorAction Stop
$wsus = Get-WsusServer
} else {
$dll = "$env:ProgramFiles\Update Services\Tools\Microsoft.UpdateServices.Administration.dll"
if (-not (Test-Path $dll)) { throw "WSUS Admin DLL not found: $dll" }
Add-Type -Path $dll
$wsus = [Microsoft.UpdateServices.Administration.AdminProxy]::GetUpdateServer()
}
# ---- Pull error events in the time window ----
$from = (Get-Date).Date.AddDays(-[int]$Days)
$to = (Get-Date).Date
$events = $wsus.GetUpdateEventHistory($from, $to) |
Where-Object { $_.IsError -and $_.ComputerId -ne [Guid]::Empty }
# ---- Aggregate per computer and take Top N ----
$allComputers = $wsus.GetComputerTargets()
$compById = @{}; foreach ($c in $allComputers) { $compById[$c.Id] = $c }
$topByCount = $events | Group-Object ComputerId |
Sort-Object Count -Descending | Select-Object -First $Top
$result = foreach ($g in $topByCount) {
$c = $compById[$g.Name]
$lastEvt = $events | Where-Object { $_.ComputerId -eq $c.Id } |
Sort-Object CreationDate -Descending | Select-Object -First 1
[pscustomobject]@{
ComputerName = $c.FullDomainName
IPAddress = $c.IPAddress
Errors = $g.Count
LastError = $lastEvt.CreationDate
}
}
# ---- Output: table + CSV ----
$result | Format-Table -AutoSize
$result | Export-Csv -Path $OutCsv -NoTypeInformation -Encoding UTF8
Write-Host "CSV: $OutCsv"
# ---- For each top computer, list failed updates (titles) ----
$updScope = New-Object Microsoft.UpdateServices.Administration.UpdateScope
foreach ($item in $result) {
$pc = $allComputers | Where-Object { $_.FullDomainName -eq $item.ComputerName }
if (-not $pc) { continue }
$failed = $pc.GetUpdateInstallationInfoPerUpdate($updScope) |
Where-Object UpdateInstallationState -EQ 'Failed'
if ($failed) {
Write-Host "`n$($item.ComputerName) — failed updates:"
foreach ($u in $failed) { ($wsus.GetUpdate($u.UpdateId).Title) | ForEach-Object { Write-Host " - $_" } }
}
}

View File

@@ -0,0 +1,40 @@
# WSUS Display Top Error Computers (PowerShell)
Find the **top N computers** with the most Windows Update failures in the last **N days** from WSUS, list their failed updates, and export results to CSV.
## What the script does
- Reads WSUS **event history** over a time window.
- Aggregates errors **per computer** and shows the **Top N**.
- Prints failed **KB titles per machine**.
- Exports a CSV: `WSUS_FailedComputers_YYYYMMDD.csv`.
## Requirements
- Run **on the WSUS server**, in an **elevated** 64-bit PowerShell.
- WSUS API available:
- `UpdateServices` module **or**
- `"%ProgramFiles%\Update Services\Tools\Microsoft.UpdateServices.Administration.dll"`.
## Usage
Save as `Top-ErrorComputers.ps1`, then run:
```powershell
powershell.exe -NoProfile -ExecutionPolicy Bypass -File .\Top-ErrorComputers.ps1
```
## Configuration
Edit the variables at the top of the script:
- `$Days` time window (default: `7`)
- `$Top` number of computers to display (default: `10`)
- `$OutCsv` export path
## Output
- Console table: `ComputerName`, `IPAddress`, `Errors`, `LastError`
- CSV file at `$OutCsv`
- For each “top” computer, a list of **failed update titles** is printed below the table.
## Optional: schedule (Task Scheduler)
```powershell
schtasks /Create /TN "WSUS Top Error Computers" ^
/TR "powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Top-ErrorComputers.ps1" ^
/SC DAILY /ST 02:00 /RU SYSTEM /RL HIGHEST /F
```

View File

@@ -0,0 +1,68 @@
# Script to stop update-related services, rename folders, and restart services
$LogPath = "C:\Windows\Temp\WUA_Reset.log"
$Services = @("wuauserv", "cryptSvc", "bits", "msiserver")
$SoftwareDistribution = "C:\Windows\SoftwareDistribution"
$SoftwareDistributionOld = "C:\Windows\SoftwareDistribution.old"
$Catroot2 = "C:\Windows\System32\catroot2"
$Catroot2Old = "C:\Windows\System32\catroot2.old"
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $LogPath -Value "$timestamp $Message"
}
# Stop services
foreach ($svc in $Services) {
try {
Write-Log "Stopping service: $svc"
Stop-Service -Name $svc -Force -ErrorAction Stop
} catch {
Write-Log "Failed to stop service $svc: $_"
}
}
Start-Sleep -Seconds 3
# Rename SoftwareDistribution folder
if (Test-Path $SoftwareDistribution) {
try {
if (Test-Path $SoftwareDistributionOld) {
Remove-Item -Path $SoftwareDistributionOld -Recurse -Force
Write-Log "Removed existing $SoftwareDistributionOld"
}
Rename-Item -Path $SoftwareDistribution -NewName "SoftwareDistribution.old"
Write-Log "Renamed $SoftwareDistribution to $SoftwareDistributionOld"
} catch {
Write-Log "Failed to rename $SoftwareDistribution: $_"
}
}
# Rename catroot2 folder
if (Test-Path $Catroot2) {
try {
if (Test-Path $Catroot2Old) {
Remove-Item -Path $Catroot2Old -Recurse -Force
Write-Log "Removed existing $Catroot2Old"
}
Rename-Item -Path $Catroot2 -NewName "catroot2.old"
Write-Log "Renamed $Catroot2 to $Catroot2Old"
} catch {
Write-Log "Failed to rename $Catroot2: $_"
}
}
Start-Sleep -Seconds 2
# Start services
foreach ($svc in $Services) {
try {
Write-Log "Starting service: $svc"
Start-Service -Name $svc -ErrorAction Stop
} catch {
Write-Log "Failed to start service $svc: $_"
}
}
Write-Log "Script completed."

View File

@@ -0,0 +1,51 @@
$global:LogPath = "C:\Windows\Temp\Wuauserv_Cleanup.log"
$ServiceName = "wuauserv"
function Write-Log {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
Add-Content -Path $global:LogPath -Value "$timestamp $Message"
write-host $Message
}
function Clean-WUClientFolders {
Write-Log "Cleaning up Windows Update client folders"
$windir = $env:WINDIR
$WUDownload = Join-Path $windir "SoftwareDistribution\Download"
$WUDataStore = Join-Path $windir "SoftwareDistribution\Datastore"
if (Test-Path $WUDownload) {
try {
Remove-Item $WUDownload -Recurse -Force
Write-Log "Deleted $WUDownload"
} catch {
Write-Log ("Failed to delete $WUDownload")
}
}
if (Test-Path $WUDataStore) {
try {
Remove-Item $WUDataStore -Recurse -Force
Write-Log "Deleted $WUDataStore"
} catch {
Write-Log ("Failed to delete $WUDataStore")
}
}
}
try {
Write-Log "Stopping $ServiceName"
Stop-Service -Name $ServiceName -Force -ErrorAction Stop
Start-Sleep -Seconds 3
} catch {
Write-Log ("Failed to stop $ServiceName")
}
Clean-WUClientFolders
try {
Write-Log "Starting $ServiceName"
Start-Service -Name $ServiceName -ErrorAction Stop
Write-Log "$ServiceName started"
} catch {
Write-Log ("Failed to start $ServiceName")
}

View File

@@ -0,0 +1,43 @@
# Reset Windows Update Components (PowerShell)
Two maintained scripts to fix stuck Windows Update on Windows 10/11 and Server:
- **cleanlight.ps1** — quick cache cleanup (fast, minimal).
- **cleanFull.ps1** — full component reset (deeper, reversible).
## What each script does
- `cleanlight.ps1`
- Stops `wuauserv`
- Deletes `%WINDIR%\SoftwareDistribution\Download` and `...\Datastore`
- Restarts `wuauserv`
- Log: `C:\Windows\Temp\Wuauserv_Cleanup.log`
- `cleanFull.ps1`
- Stops `wuauserv`, `bits`, `cryptSvc`, `msiserver`
- Renames `%WINDIR%\SoftwareDistribution``SoftwareDistribution.old`
- Renames `%WINDIR%\System32\catroot2``catroot2.old`
- Restarts services
- Log: `C:\Windows\Temp\WUA_Reset.log`
> Note: Both approaches reset the **local** Windows Update history view. The first scan afterward can be longer.
## Requirements
- Run **as Administrator** on the affected device.
- Use **64-bit** PowerShell path.
- Close the Windows Update UI before running.
## Usage
```powershell
# Light cleanup
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\cleanlight.ps1
# Full reset
C:\Windows\System32\WindowsPowerShell\v1.0\powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\cleanFull.ps1
```
## After running
- Trigger a scan: `USOClient StartScan` (Win10/11) or wait for the next schedule.
- Optionally delete `*.old` folders after a few days if disk space matters.
- Check logs (paths above) if issues persist; see also `C:\Windows\WindowsUpdate.log` and `C:\Windows\Logs\CBS\CBS.log`.

View File

@@ -0,0 +1,118 @@
USE SUSDB;
GO
SET NOCOUNT ON;
-- Rebuild or reorganize indexes based on their fragmentation levels
DECLARE @work_to_do TABLE (
objectid int
, indexid int
, pagedensity float
, fragmentation float
, numrows int
)
DECLARE @objectid int;
DECLARE @indexid int;
DECLARE @schemaname nvarchar(130);
DECLARE @objectname nvarchar(130);
DECLARE @indexname nvarchar(130);
DECLARE @numrows int
DECLARE @density float;
DECLARE @fragmentation float;
DECLARE @command nvarchar(4000);
DECLARE @fillfactorset bit
DECLARE @numpages int
-- Select indexes that need to be defragmented based on the following
-- * Page density is low
-- * External fragmentation is high in relation to index size
PRINT 'Estimating fragmentation: Begin. ' + convert(nvarchar, getdate(), 121)
INSERT @work_to_do
SELECT
f.object_id
, index_id
, avg_page_space_used_in_percent
, avg_fragmentation_in_percent
, record_count
FROM
sys.dm_db_index_physical_stats (DB_ID(), NULL, NULL , NULL, 'SAMPLED') AS f
WHERE
(f.avg_page_space_used_in_percent < 85.0 and f.avg_page_space_used_in_percent/100.0 * page_count < page_count - 1)
or (f.page_count > 50 and f.avg_fragmentation_in_percent > 15.0)
or (f.page_count > 10 and f.avg_fragmentation_in_percent > 80.0)
PRINT 'Number of indexes to rebuild: ' + cast(@@ROWCOUNT as nvarchar(20))
PRINT 'Estimating fragmentation: End. ' + convert(nvarchar, getdate(), 121)
SELECT @numpages = sum(ps.used_page_count)
FROM
@work_to_do AS fi
INNER JOIN sys.indexes AS i ON fi.objectid = i.object_id and fi.indexid = i.index_id
INNER JOIN sys.dm_db_partition_stats AS ps on i.object_id = ps.object_id and i.index_id = ps.index_id
-- Declare the cursor for the list of indexes to be processed.
DECLARE curIndexes CURSOR FOR SELECT * FROM @work_to_do
-- Open the cursor.
OPEN curIndexes
-- Loop through the indexes
WHILE (1=1)
BEGIN
FETCH NEXT FROM curIndexes
INTO @objectid, @indexid, @density, @fragmentation, @numrows;
IF @@FETCH_STATUS < 0 BREAK;
SELECT
@objectname = QUOTENAME(o.name)
, @schemaname = QUOTENAME(s.name)
FROM
sys.objects AS o
INNER JOIN sys.schemas as s ON s.schema_id = o.schema_id
WHERE
o.object_id = @objectid;
SELECT
@indexname = QUOTENAME(name)
, @fillfactorset = CASE fill_factor WHEN 0 THEN 0 ELSE 1 END
FROM
sys.indexes
WHERE
object_id = @objectid AND index_id = @indexid;
IF ((@density BETWEEN 75.0 AND 85.0) AND @fillfactorset = 1) OR (@fragmentation < 30.0)
SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REORGANIZE';
ELSE IF @numrows >= 5000 AND @fillfactorset = 0
SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD WITH (FILLFACTOR = 90)';
ELSE
SET @command = N'ALTER INDEX ' + @indexname + N' ON ' + @schemaname + N'.' + @objectname + N' REBUILD';
PRINT convert(nvarchar, getdate(), 121) + N' Executing: ' + @command;
EXEC (@command);
PRINT convert(nvarchar, getdate(), 121) + N' Done.';
END
-- Close and deallocate the cursor.
CLOSE curIndexes;
DEALLOCATE curIndexes;
IF EXISTS (SELECT * FROM @work_to_do)
BEGIN
PRINT 'Estimated number of pages in fragmented indexes: ' + cast(@numpages as nvarchar(20))
SELECT @numpages = @numpages - sum(ps.used_page_count)
FROM
@work_to_do AS fi
INNER JOIN sys.indexes AS i ON fi.objectid = i.object_id and fi.indexid = i.index_id
INNER JOIN sys.dm_db_partition_stats AS ps on i.object_id = ps.object_id and i.index_id = ps.index_id
PRINT 'Estimated number of pages freed: ' + cast(@numpages as nvarchar(20))
END
GO
--Update all statistics
PRINT 'Updating all statistics.' + convert(nvarchar, getdate(), 121)
EXEC sp_updatestats
PRINT 'Done updating statistics.' + convert(nvarchar, getdate(), 121)
GO

38
sql-maintenance/readme.md Normal file
View File

@@ -0,0 +1,38 @@
# WSUS SUSDB Maintenance (Reindex + Update Stats)
Keep **SUSDB** fast: rebuild/reorganize fragmented indexes, then run `sp_updatestats`. Run off-hours; back up first.
## Requirements
- Admin on the WSUS server.
- Either **SqlServer** PowerShell module (`Install-Module SqlServer`) or **sqlcmd** tools.
- WID instance uses the named pipe: `np:\\.\pipe\MICROSOFT##WID\tsql\query`.
## Quick start
1) Save the SQL script as: `C:\Scripts\WSUSDBMaintenance.sql` (same as in this repo/article).
2) Run one of the following:
```powershell
# PowerShell (SqlServer module) — SQL Server instance
Invoke-Sqlcmd -ServerInstance 'localhost' -Database 'SUSDB' `
-InputFile C:\Scripts\WSUSDBMaintenance.sql -AbortOnError
# PowerShell (SqlServer module) — WID (local only)
Invoke-Sqlcmd -ServerInstance 'np:\\.\pipe\MICROSOFT##WID\tsql\query' `
-Database 'SUSDB' -InputFile C:\Scripts\WSUSDBMaintenance.sql -AbortOnError
```
```powershell
# sqlcmd CLI — SQL Server
sqlcmd -S localhost -d SUSDB -i C:\Scripts\WSUSDBMaintenance.sql -b
# sqlcmd CLI — WID (local only)
sqlcmd -S np:\\.\pipe\MICROSOFT##WID\tsql\query -d SUSDB -i C:\Scripts\WSUSDBMaintenance.sql -b
```
## Schedule (optional)
```powershell
schtasks /Create /TN "WSUS DB Maintenance" ^
/TR "powershell.exe -NoProfile -Command Invoke-Sqlcmd -ServerInstance 'np:\\.\pipe\MICROSOFT##WID\tsql\query' -Database SUSDB -InputFile C:\Scripts\WSUSDBMaintenance.sql -AbortOnError" ^
/SC MONTHLY /D 1 /ST 02:00 /RU "SYSTEM" /RL HIGHEST /F
```

View File

@@ -0,0 +1,616 @@
<?xml version="1.0" encoding="utf-8" ?>
<Configuration>
<ViewDefinitions>
<View>
<Name>PSWindowsUpdate</Name>
<ViewSelectedBy>
<TypeName>PSWindowsUpdate.WindowsUpdate</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>12</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>10</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>9</Width>
</TableColumnHeader>
<TableColumnHeader>
<Alignment>Right</Alignment>
<Width>6</Width>
</TableColumnHeader>
<TableColumnHeader/>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>ComputerName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Status</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>KB</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Size</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Title</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>PSWindowsUpdate</Name>
<ViewSelectedBy>
<TypeName>PSWindowsUpdate.OfflineMSU</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>1</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>10</Width>
</TableColumnHeader>
<TableColumnHeader/>
<TableColumnHeader>
<Width>11</Width>
</TableColumnHeader>
<TableColumnHeader>
<Alignment>Right</Alignment>
<Width>8</Width>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>X</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Result</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Title</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>LastUpdated</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Size</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>PSWindowsUpdate</Name>
<ViewSelectedBy>
<TypeName>PSWindowsUpdate.WindowsUpdateJob</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>1</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>12</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>10</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>9</Width>
</TableColumnHeader>
<TableColumnHeader>
<Alignment>Right</Alignment>
<Width>6</Width>
</TableColumnHeader>
<TableColumnHeader/>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>X</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>ComputerName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Result</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>KB</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Size</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Title</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>PSWindowsUpdate</Name>
<ViewSelectedBy>
<TypeName>PSWindowsUpdate.History</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>12</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>14</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>10</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>19</Width>
</TableColumnHeader>
<TableColumnHeader/>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>ComputerName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Operationname</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Result</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Date</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Title</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>PSWindowsUpdate</Name>
<ViewSelectedBy>
<TypeName>PSWindowsUpdate.ServiceManager</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>36</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>9</Width>
</TableColumnHeader>
<TableColumnHeader>
<Label>IsDefault</Label>
<Width>9</Width>
</TableColumnHeader>
<TableColumnHeader/>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>ServiceID</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>IsManaged</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>IsDefaultAUService</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>PSWindowsUpdate</Name>
<ViewSelectedBy>
<TypeName>PSWindowsUpdate.AgentInfo</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>12</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>15</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>15</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>10</Width>
</TableColumnHeader>
<TableColumnHeader/>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>ComputerName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>PSWindowsUpdate</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>PSWUModuleDll</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>ApiVersion</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>WuapiDllVersion</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>PSWindowsUpdate</Name>
<ViewSelectedBy>
<TypeName>PSWindowsUpdate.InstallerStatus</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>12</Width>
</TableColumnHeader>
<TableColumnHeader/>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>ComputerName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>IsBusy</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>PSWindowsUpdate</Name>
<ViewSelectedBy>
<TypeName>PSWindowsUpdate.LastResults</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>12</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>10</Width>
</TableColumnHeader>
<TableColumnHeader/>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>ComputerName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>LastSearchSuccessDate</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>LastInstallationSuccessDate</PropertyName>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
<View>
<Name>PSWindowsUpdate</Name>
<ViewSelectedBy>
<TypeName>PSWindowsUpdate.WUJob</TypeName>
</ViewSelectedBy>
<TableControl>
<TableHeaders>
<TableColumnHeader>
<Width>12</Width>
</TableColumnHeader>
<TableColumnHeader>
<Width>20</Width>
</TableColumnHeader>
<TableColumnHeader>
<Label>Action</Label>
</TableColumnHeader>
</TableHeaders>
<TableRowEntries>
<TableRowEntry>
<TableColumnItems>
<TableColumnItem>
<PropertyName>ComputerName</PropertyName>
</TableColumnItem>
<TableColumnItem>
<PropertyName>Name</PropertyName>
</TableColumnItem>
<TableColumnItem>
<ScriptBlock>
$_.Definition.Actions[1].Arguments -replace '-Command "|"$'
</ScriptBlock>
</TableColumnItem>
</TableColumnItems>
</TableRowEntry>
</TableRowEntries>
</TableControl>
</View>
</ViewDefinitions>
</Configuration>
<!-- SIG # Begin signature block -->
<!-- MIIuSAYJKoZIhvcNAQcCoIIuOTCCLjUCAQExDzANBglghkgBZQMEAgEFADB5Bgor -->
<!-- BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG -->
<!-- KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDKpaJQFFx7VN+7 -->
<!-- 59g3yyjxDqbSN9hAueD6BbACoSGELqCCJnowggXJMIIEsaADAgECAhAbtY8lKt8j -->
<!-- AEkoya49fu0nMA0GCSqGSIb3DQEBDAUAMH4xCzAJBgNVBAYTAlBMMSIwIAYDVQQK -->
<!-- ExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEuMScwJQYDVQQLEx5DZXJ0dW0gQ2Vy -->
<!-- dGlmaWNhdGlvbiBBdXRob3JpdHkxIjAgBgNVBAMTGUNlcnR1bSBUcnVzdGVkIE5l -->
<!-- dHdvcmsgQ0EwHhcNMjEwNTMxMDY0MzA2WhcNMjkwOTE3MDY0MzA2WjCBgDELMAkG -->
<!-- A1UEBhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAl -->
<!-- BgNVBAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMb -->
<!-- Q2VydHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMIICIjANBgkqhkiG9w0BAQEFAAOC -->
<!-- Ag8AMIICCgKCAgEAvfl4+ObVgAxknYYblmRnPyI6HnUBfe/7XGeMycxca6mR5rlC -->
<!-- 5SBLm9qbe7mZXdmbgEvXhEArJ9PoujC7Pgkap0mV7ytAJMKXx6fumyXvqAoAl4Va -->
<!-- qp3cKcniNQfrcE1K1sGzVrihQTib0fsxf4/gX+GxPw+OFklg1waNGPmqJhCrKtPQ -->
<!-- 0WeNG0a+RzDVLnLRxWPa52N5RH5LYySJhi40PylMUosqp8DikSiJucBb+R3Z5yet -->
<!-- /5oCl8HGUJKbAiy9qbk0WQq/hEr/3/6zn+vZnuCYI+yma3cWKtvMrTscpIfcRnNe -->
<!-- GWJoRVfkkIJCu0LW8GHgwaM9ZqNd9BjuiMmNF0UpmTJ1AjHuKSbIawLmtWJFfzcV -->
<!-- WiNoidQ+3k4nsPBADLxNF8tNorMe0AZa3faTz1d1mfX6hhpneLO/lv403L3nUlbl -->
<!-- s+V1e9dBkQXcXWnjlQ1DufyDljmVe2yAWk8TcsbXfSl6RLpSpCrVQUYJIP4ioLZb -->
<!-- MI28iQzV13D4h1L92u+sUS4Hs07+0AnacO+Y+lbmbdu1V0vc5SwlFcieLnhO+Nqc -->
<!-- noYsylfzGuXIkosagpZ6w7xQEmnYDlpGizrrJvojybawgb5CAKT41v4wLsfSRvbl -->
<!-- jnX98sy50IdbzAYQYLuDNbdeZ95H7JlI8aShFf6tjGKOOVVPORa5sWOd/7cCAwEA -->
<!-- AaOCAT4wggE6MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLahVDkCw6A/joq8 -->
<!-- +tT4HKbROg79MB8GA1UdIwQYMBaAFAh2zcsH/yT2xc3tu5C84oQ3RnX3MA4GA1Ud -->
<!-- DwEB/wQEAwIBBjAvBgNVHR8EKDAmMCSgIqAghh5odHRwOi8vY3JsLmNlcnR1bS5w -->
<!-- bC9jdG5jYS5jcmwwawYIKwYBBQUHAQEEXzBdMCgGCCsGAQUFBzABhhxodHRwOi8v -->
<!-- c3ViY2Eub2NzcC1jZXJ0dW0uY29tMDEGCCsGAQUFBzAChiVodHRwOi8vcmVwb3Np -->
<!-- dG9yeS5jZXJ0dW0ucGwvY3RuY2EuY2VyMDkGA1UdIAQyMDAwLgYEVR0gADAmMCQG -->
<!-- CCsGAQUFBwIBFhhodHRwOi8vd3d3LmNlcnR1bS5wbC9DUFMwDQYJKoZIhvcNAQEM -->
<!-- BQADggEBAFHCoVgWIhCL/IYx1MIy01z4S6Ivaj5N+KsIHu3V6PrnCA3st8YeDrJ1 -->
<!-- BXqxC/rXdGoABh+kzqrya33YEcARCNQOTWHFOqj6seHjmOriY/1B9ZN9DbxdkjuR -->
<!-- mmW60F9MvkyNaAMQFtXx0ASKhTP5N+dbLiZpQjy6zbzUeulNndrnQ/tjUoCFBMQl -->
<!-- lVXwfqefAcVbKPjgzoZwpic7Ofs4LphTZSJ1Ldf23SIikZbr3WjtP6MZl9M7JYjs -->
<!-- NhI9qX7OAo0FmpKnJ25FspxihjcNpDOO16hO0EoXQ0zF8ads0h5YbBRRfopUofbv -->
<!-- n3l6XYGaFpAP4bvxSgD5+d2+7arszgowggXSMIIDuqADAgECAhAh1tBKTyUPyTI3 -->
<!-- /KpeEo3pMA0GCSqGSIb3DQEBDQUAMIGAMQswCQYDVQQGEwJQTDEiMCAGA1UEChMZ -->
<!-- VW5pemV0byBUZWNobm9sb2dpZXMgUy5BLjEnMCUGA1UECxMeQ2VydHVtIENlcnRp -->
<!-- ZmljYXRpb24gQXV0aG9yaXR5MSQwIgYDVQQDExtDZXJ0dW0gVHJ1c3RlZCBOZXR3 -->
<!-- b3JrIENBIDIwIhgPMjAxMTEwMDYwODM5NTZaGA8yMDQ2MTAwNjA4Mzk1NlowgYAx -->
<!-- CzAJBgNVBAYTAlBMMSIwIAYDVQQKExlVbml6ZXRvIFRlY2hub2xvZ2llcyBTLkEu -->
<!-- MScwJQYDVQQLEx5DZXJ0dW0gQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkxJDAiBgNV -->
<!-- BAMTG0NlcnR1bSBUcnVzdGVkIE5ldHdvcmsgQ0EgMjCCAiIwDQYJKoZIhvcNAQEB -->
<!-- BQADggIPADCCAgoCggIBAL35ePjm1YAMZJ2GG5ZkZz8iOh51AX3v+1xnjMnMXGup -->
<!-- kea5QuUgS5vam3u5mV3Zm4BL14RAKyfT6Lowuz4JGqdJle8rQCTCl8en7psl76gK -->
<!-- AJeFWqqd3CnJ4jUH63BNStbBs1a4oUE4m9H7MX+P4F/hsT8PjhZJYNcGjRj5qiYQ -->
<!-- qyrT0NFnjRtGvkcw1S5y0cVj2udjeUR+S2MkiYYuND8pTFKLKqfA4pEoibnAW/kd -->
<!-- 2ecnrf+aApfBxlCSmwIsvam5NFkKv4RK/9/+s5/r2Z7gmCPspmt3FirbzK07HKSH -->
<!-- 3EZzXhliaEVX5JCCQrtC1vBh4MGjPWajXfQY7ojJjRdFKZkydQIx7ikmyGsC5rVi -->
<!-- RX83FVojaInUPt5OJ7DwQAy8TRfLTaKzHtAGWt32k89XdZn1+oYaZ3izv5b+NNy9 -->
<!-- 51JW5bPldXvXQZEF3F1p45UNQ7n8g5Y5lXtsgFpPE3LG130pekS6UqQq1UFGCSD+ -->
<!-- IqC2WzCNvIkM1ddw+IdS/drvrFEuB7NO/tAJ2nDvmPpW5m3btVdL3OUsJRXIni54 -->
<!-- TvjanJ6GLMpX8xrlyJKLGoKWesO8UBJp2A5aRos66yb6I8m2sIG+QgCk+Nb+MC7H -->
<!-- 0kb25Y51/fLMudCHW8wGEGC7gzW3XmfeR+yZSPGkoRX+rYxijjlVTzkWubFjnf+3 -->
<!-- AgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFLahVDkCw6A/joq8 -->
<!-- +tT4HKbROg79MA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQ0FAAOCAgEAcaUO -->
<!-- zuTpvz841YlaxAJh+0zFFBcti09TaxAX/GWExxBJkN7bxyaTiCZvcNYCXjmg94+r -->
<!-- lrWlE1yBFG0OgYIRG4pOxk+l3WIeRN8JWfRbdws36YsgxvgKTi5YHOsz0M+GYMna -->
<!-- +4AvnkxghHg9IWTW+0EfGA/nyXVxvb1c3jSHPkGwDva51j8JE5YUL96aHVq5Vs41 -->
<!-- OrBfcE1e4ynxIyhyWbarwoxmJhx3LCZ2NYsop2mg+Tv1I92FEHTJkANWkeevukfU -->
<!-- EpcRIuOiSZRs57eUS7otpNozi0ymRP9aPMYdZNi1MeSmPHqoVwvb7WEay/HOc3dj -->
<!-- pIdvTFE41uRfx5+2gSrkhUh5WF47+NsCgmfBOdvDdEs9Nh75KZOIaFuoRBkh8Kfo -->
<!-- gQ0s6JM2tDeyyrAbJnqaJR+amoCeSyo/+6Oa/nMyccKexnLhimgn8eQPtMRMpWGT -->
<!-- +JcQByowJam5yHG472jMLX714H4Pgqhvtrpsg0N3zYqSF6GeW3gWPUXiM3Ld4WbK -->
<!-- mdPJxSb9DWgERq622ZuMvhm+scbyGeNcAsos2G9KB9nJNdpAdfLEpxlvnkIQmHXm -->
<!-- lYtgvO3FEteKztWYXFaWA8XudwY1/8/k7j8TYe7b2i2F8M2unbIYCUXDkqFyF/xH -->
<!-- tqALLPHE3kNoCGpfO/B2Y/vMBiymxuIOtbm+JI8wggaVMIIEfaADAgECAhEA8WQl -->
<!-- jAm24nviDjJgjkv0qDANBgkqhkiG9w0BAQwFADBWMQswCQYDVQQGEwJQTDEhMB8G -->
<!-- A1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBTLkEuMSQwIgYDVQQDExtDZXJ0dW0g -->
<!-- VGltZXN0YW1waW5nIDIwMjEgQ0EwHhcNMjEwNTE5MDU0MjQ2WhcNMzIwNTE4MDU0 -->
<!-- MjQ2WjBQMQswCQYDVQQGEwJQTDEhMB8GA1UECgwYQXNzZWNvIERhdGEgU3lzdGVt -->
<!-- cyBTLkEuMR4wHAYDVQQDDBVDZXJ0dW0gVGltZXN0YW1wIDIwMjEwggIiMA0GCSqG -->
<!-- SIb3DQEBAQUAA4ICDwAwggIKAoICAQDVYb6AAL3dhGPuEmWYHXhUi0b6xpEWGro9 -->
<!-- Hny+NBj26L94gmI8kONVYdu2Cz9Bftkiyvk4+3MFDrkovZZQ8WDcmGXltX4xAwPA -->
<!-- cjXEbXgEZ0exEP5Ae2bkwKlTiyUXCaq0D9JEaK5t4Kq7rH7rndKd5kX7KARcMFWE -->
<!-- N+ikV1cgGlKgqmJSTk0Bvbgbc67oolIhtohcEktZZFut5VJxTJ1OKsRR3FUmN+4Q -->
<!-- rAk0RIv4dw2Z4sWilqbdBaBS/5hqLt58sptiORkxnijr33VnviLP2+wbWyQM5k/A -->
<!-- grKj8lk6A5C8V/dShj6l/TqqRMykGAKOmi6CcvGbUDibPKkjlxlALd4mHLFujWoE -->
<!-- 91GicKUKfVkLsFqplb/dPPXQjw2TCmZbAegDQlsAppndi9UUZxHvPcryyy0Eyh1y -->
<!-- 4Gn7Xv1vEwnwBisZjB72My8kzUQ0gjxP26vhBkvF2Cic16nVAHxyGOPm0Y0v7lFm -->
<!-- cSyYVWg1J56YZb+QAJZCL7BJ9CBSJpAXNGxcNURN0baABlZTHn3bbBPOBhOSY9vb -->
<!-- GwL34nOmTFpRG5mP6HQVXc/EO9cj856a9aueDGyz2hclMIZijGEa5rwacGtPw1Hz -->
<!-- WpgNAOI24ChDBRQ8YmD23IN1rmLlzCMsRZ9wFYIvNDtMJVMSQgC0+XQBFPOe69kP -->
<!-- wxgPNN4CCwIDAQABo4IBYjCCAV4wDAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUxUcS -->
<!-- TnJXtkQUa4hxGhSsMbk/uggwHwYDVR0jBBgwFoAUvlQCL79AbHNDzqwJJU6eQ0Qa -->
<!-- 7uAwDgYDVR0PAQH/BAQDAgeAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMDMGA1Ud -->
<!-- HwQsMCowKKAmoCSGImh0dHA6Ly9jcmwuY2VydHVtLnBsL2N0c2NhMjAyMS5jcmww -->
<!-- bwYIKwYBBQUHAQEEYzBhMCgGCCsGAQUFBzABhhxodHRwOi8vc3ViY2Eub2NzcC1j -->
<!-- ZXJ0dW0uY29tMDUGCCsGAQUFBzAChilodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0u -->
<!-- cGwvY3RzY2EyMDIxLmNlcjBABgNVHSAEOTA3MDUGCyqEaAGG9ncCBQELMCYwJAYI -->
<!-- KwYBBQUHAgEWGGh0dHA6Ly93d3cuY2VydHVtLnBsL0NQUzANBgkqhkiG9w0BAQwF -->
<!-- AAOCAgEAN3PMMLfCX4nmqnSsHU2rZhE/dkqrdSYLvI3U9i49hxs+i+9oo5mJl4ur -->
<!-- PLZJ0xIz6B7CHFBNW9dFwgahnFMXiT7QnPuZ5CAwfL/9CfsAL3XdnS0AWll+7ISo -->
<!-- mRo8d51bfpHHt3P3jx9C6Imh1A73JSp90Cq0NqPqnEflrVxYX+sYa2SO9vGsRMYs -->
<!-- hU7uzE1V5cYWWoFUMaDHpwQuH4DNXiZO6D7f8QGWnXNHXu6S3SlaYDG4Yox7SIW1 -->
<!-- tQv0jskmF1vdNfoxVAymQGRdNLsGzAXn6OPAUiw1xQ6M1qpjK4UnKTUiFJfvgDXb -->
<!-- T1cvrYsJrybB/41so+DsAt0yjKxbpS5iP7SpxyHsnch0VcI54sIf0K66f4LJGocB -->
<!-- pDTKbU1AOq3OvHbVqI7Vwqs+TGCu7TKqrTL2NQTRDAxHkso7FtH841R2A2lvYSFD -->
<!-- fGx87B1NvPWYU3mY/GRsmQx+RgA8Pl/7Nvp7ZAY+AU8mDVr2KXrFP4unpswVBQlH -->
<!-- xtIOxz6jeyfdLIG2oFJll3ipcASHav/obYEt/F1GRlJ+mFIQtKDadxUBmfhRlgIg -->
<!-- YvEEtuJGERHuxfMD26jLmixu8STPGRRco+R5Bdgu+qFbnymKfuXO4sR96JYqaOOx -->
<!-- ilcN/xr7ms13iS7wqANpd2txKZjPy3wdWniVQcuL7yCXD2uEc20wgga5MIIEoaAD -->
<!-- AgECAhEAmaOACiZVO2Wr3G6EprPqOTANBgkqhkiG9w0BAQwFADCBgDELMAkGA1UE -->
<!-- BhMCUEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNV -->
<!-- BAsTHkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2Vy -->
<!-- dHVtIFRydXN0ZWQgTmV0d29yayBDQSAyMB4XDTIxMDUxOTA1MzIxOFoXDTM2MDUx -->
<!-- ODA1MzIxOFowVjELMAkGA1UEBhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5 -->
<!-- c3RlbXMgUy5BLjEkMCIGA1UEAxMbQ2VydHVtIENvZGUgU2lnbmluZyAyMDIxIENB -->
<!-- MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAnSPPBDAjO8FGLOczcz5j -->
<!-- XXp1ur5cTbq96y34vuTmflN4mSAfgLKTvggv24/rWiVGzGxT9YEASVMw1Aj8ewTS -->
<!-- 4IndU8s7VS5+djSoMcbvIKck6+hI1shsylP4JyLvmxwLHtSworV9wmjhNd627h27 -->
<!-- a8RdrT1PH9ud0IF+njvMk2xqbNTIPsnWtw3E7DmDoUmDQiYi/ucJ42fcHqBkbbxY -->
<!-- DB7SYOouu9Tj1yHIohzuC8KNqfcYf7Z4/iZgkBJ+UFNDcc6zokZ2uJIxWgPWXMEm -->
<!-- hu1gMXgv8aGUsRdaCtVD2bSlbfsq7BiqljjaCun+RJgTgFRCtsuAEw0pG9+FA+yQ -->
<!-- N9n/kZtMLK+Wo837Q4QOZgYqVWQ4x6cM7/G0yswg1ElLlJj6NYKLw9EcBXE7TF3H -->
<!-- ybZtYvj9lDV2nT8mFSkcSkAExzd4prHwYjUXTeZIlVXqj+eaYqoMTpMrfh5MCAOI -->
<!-- G5knN4Q/JHuurfTI5XDYO962WZayx7ACFf5ydJpoEowSP07YaBiQ8nXpDkNrUA9g -->
<!-- 7qf/rCkKbWpQ5boufUnq1UiYPIAHlezf4muJqxqIns/kqld6JVX8cixbd6PzkDpw -->
<!-- Zo4SlADaCi2JSplKShBSND36E/ENVv8urPS0yOnpG4tIoBGxVCARPCg1BnyMJ4rB -->
<!-- JAcOSnAWd18Jx5n858JSqPECAwEAAaOCAVUwggFRMA8GA1UdEwEB/wQFMAMBAf8w -->
<!-- HQYDVR0OBBYEFN10XUwA23ufoHTKsW73PMAywHDNMB8GA1UdIwQYMBaAFLahVDkC -->
<!-- w6A/joq8+tT4HKbROg79MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEF -->
<!-- BQcDAzAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vY3JsLmNlcnR1bS5wbC9jdG5j -->
<!-- YTIuY3JsMGwGCCsGAQUFBwEBBGAwXjAoBggrBgEFBQcwAYYcaHR0cDovL3N1YmNh -->
<!-- Lm9jc3AtY2VydHVtLmNvbTAyBggrBgEFBQcwAoYmaHR0cDovL3JlcG9zaXRvcnku -->
<!-- Y2VydHVtLnBsL2N0bmNhMi5jZXIwOQYDVR0gBDIwMDAuBgRVHSAAMCYwJAYIKwYB -->
<!-- BQUHAgEWGGh0dHA6Ly93d3cuY2VydHVtLnBsL0NQUzANBgkqhkiG9w0BAQwFAAOC -->
<!-- AgEAdYhYD+WPUCiaU58Q7EP89DttyZqGYn2XRDhJkL6P+/T0IPZyxfxiXumYlARM -->
<!-- gwRzLRUStJl490L94C9LGF3vjzzH8Jq3iR74BRlkO18J3zIdmCKQa5LyZ48IfICJ -->
<!-- TZVJeChDUyuQy6rGDxLUUAsO0eqeLNhLVsgw6/zOfImNlARKn1FP7o0fTbj8ipNG -->
<!-- xHBIutiRsWrhWM2f8pXdd3x2mbJCKKtl2s42g9KUJHEIiLni9ByoqIUul4GblLQi -->
<!-- gO0ugh7bWRLDm0CdY9rNLqyA3ahe8WlxVWkxyrQLjH8ItI17RdySaYayX3PhRSC4 -->
<!-- Am1/7mATwZWwSD+B7eMcZNhpn8zJ+6MTyE6YoEBSRVrs0zFFIHUR08Wk0ikSf+lI -->
<!-- e5Iv6RY3/bFAEloMU+vUBfSouCReZwSLo8WdrDlPXtR0gicDnytO7eZ5827NS2x7 -->
<!-- gCBibESYkOh1/w1tVxTpV2Na3PR7nxYVlPu1JPoRZCbH86gc96UTvuWiOruWmyOE -->
<!-- MLOGGniR+x+zPF/2DaGgK2W1eEJfo2qyrBNPvF7wuAyQfiFXLwvWHamoYtPZo0LH -->
<!-- uH8X3n9C+xN4YaNjt2ywzOr+tKyEVAotnyU9vyEVOaIYMk3IeBrmFnn0gbKeTTyY -->
<!-- eEEUz/Qwt4HOUBCrW602NCmvO1nm+/80nLy5r0AZvCQxaQ4wgga5MIIEoaADAgEC -->
<!-- AhEA5/9pxzs1zkuRJth0fGilhzANBgkqhkiG9w0BAQwFADCBgDELMAkGA1UEBhMC -->
<!-- UEwxIjAgBgNVBAoTGVVuaXpldG8gVGVjaG5vbG9naWVzIFMuQS4xJzAlBgNVBAsT -->
<!-- HkNlcnR1bSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTEkMCIGA1UEAxMbQ2VydHVt -->
<!-- IFRydXN0ZWQgTmV0d29yayBDQSAyMB4XDTIxMDUxOTA1MzIwN1oXDTM2MDUxODA1 -->
<!-- MzIwN1owVjELMAkGA1UEBhMCUEwxITAfBgNVBAoTGEFzc2VjbyBEYXRhIFN5c3Rl -->
<!-- bXMgUy5BLjEkMCIGA1UEAxMbQ2VydHVtIFRpbWVzdGFtcGluZyAyMDIxIENBMIIC -->
<!-- IjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA6RIfBDXtuV16xaaVQb6KZX9O -->
<!-- d9FtJXXTZo7b+GEof3+3g0ChWiKnO7R4+6MfrvLyLCWZa6GpFHjEt4t0/GiUQvnk -->
<!-- LOBRdBqr5DOvlmTvJJs2X8ZmWgWJjC7PBZLYBWAs8sJl3kNXxBMX5XntjqWx1ZOu -->
<!-- uXl0R4x+zGGSMzZ45dpvB8vLpQfZkfMC/1tL9KYyjU+htLH68dZJPtzhqLBVG+8l -->
<!-- jZ1ZFilOKksS79epCeqFSeAUm2eMTGpOiS3gfLM6yvb8Bg6bxg5yglDGC9zbr4sB -->
<!-- 9ceIGRtCQF1N8dqTgM/dSViiUgJkcv5dLNJeWxGCqJYPgzKlYZTgDXfGIeZpEFmj -->
<!-- BLwURP5ABsyKoFocMzdjrCiFbTvJn+bD1kq78qZUgAQGGtd6zGJ88H4NPJ5Y2R4I -->
<!-- argiWAmv8RyvWnHr/VA+2PrrK9eXe5q7M88YRdSTq9TKbqdnITUgZcjjm4ZUjteq -->
<!-- 8K331a4P0s2in0p3UubMEYa/G5w6jSWPUzchGLwWKYBfeSu6dIOC4LkeAPvmdZxS -->
<!-- B1lWOb9HzVWZoM8Q/blaP4LWt6JxjkI9yQsYGMdCqwl7uMnPUIlcExS1mzXRxUow -->
<!-- Qref/EPaS7kYVaHHQrp4XB7nTEtQhkP0Z9Puz/n8zIFnUSnxDof4Yy650PAXSYmK -->
<!-- 2TcbyDoTNmmt8xAxzcMCAwEAAaOCAVUwggFRMA8GA1UdEwEB/wQFMAMBAf8wHQYD -->
<!-- VR0OBBYEFL5UAi+/QGxzQ86sCSVOnkNEGu7gMB8GA1UdIwQYMBaAFLahVDkCw6A/ -->
<!-- joq8+tT4HKbROg79MA4GA1UdDwEB/wQEAwIBBjATBgNVHSUEDDAKBggrBgEFBQcD -->
<!-- CDAwBgNVHR8EKTAnMCWgI6Ahhh9odHRwOi8vY3JsLmNlcnR1bS5wbC9jdG5jYTIu -->
<!-- Y3JsMGwGCCsGAQUFBwEBBGAwXjAoBggrBgEFBQcwAYYcaHR0cDovL3N1YmNhLm9j -->
<!-- c3AtY2VydHVtLmNvbTAyBggrBgEFBQcwAoYmaHR0cDovL3JlcG9zaXRvcnkuY2Vy -->
<!-- dHVtLnBsL2N0bmNhMi5jZXIwOQYDVR0gBDIwMDAuBgRVHSAAMCYwJAYIKwYBBQUH -->
<!-- AgEWGGh0dHA6Ly93d3cuY2VydHVtLnBsL0NQUzANBgkqhkiG9w0BAQwFAAOCAgEA -->
<!-- uJNZd8lMFf2UBwigp3qgLPBBk58BFCS3Q6aJDf3TISoytK0eal/JyCB88aUEd0wM -->
<!-- NiEcNVMbK9j5Yht2whaknUE1G32k6uld7wcxHmw67vUBY6pSp8QhdodY4SzRRaZW -->
<!-- zyYlviUpyU4dXyhKhHSncYJfa1U75cXxCe3sTp9uTBm3f8Bj8LkpjMUSVTtMJ6oE -->
<!-- u5JqCYzRfc6nnoRUgwz/GVZFoOBGdrSEtDN7mZgcka/tS5MI47fALVvN5lZ2U8k7 -->
<!-- Dm/hTX8CWOw0uBZloZEW4HB0Xra3qE4qzzq/6M8gyoU/DE0k3+i7bYOrOk/7tPJg -->
<!-- 1sOhytOGUQ30PbG++0FfJioDuOFhj99b151SqFlSaRQYz74y/P2XJP+cF19oqozm -->
<!-- i0rRTkfyEJIvhIZ+M5XIFZttmVQgTxfpfJwMFFEoQrSrklOxpmSygppsUDJEoliC -->
<!-- 05vBLVQ+gMZyYaKvBJ4YxBMlKH5ZHkRdloRYlUDplk8GUa+OCMVhpDSQurU6K1ua -->
<!-- 5dmZftnvSSz2H96UrQDzA6DyiI1V3ejVtvn2azVAXg6NnjmuRZ+wa7Pxy0H3+V4K -->
<!-- 4rOTHlG3VYA6xfLsTunCz72T6Ot4+tkrDYOeaU1pPX1CBfYj6EW2+ELq46GP8KCN -->
<!-- UQDirWLU4nOmgCat7vN0SD6RlwUiSsMeCiQDmZwgwrUwggbAMIIEqKADAgECAhA/ -->
<!-- LwafXGuInpplxVMGdJYVMA0GCSqGSIb3DQEBCwUAMFYxCzAJBgNVBAYTAlBMMSEw -->
<!-- HwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1 -->
<!-- bSBDb2RlIFNpZ25pbmcgMjAyMSBDQTAeFw0yMjA0MTQwOTIxMzhaFw0yNTA0MTMw -->
<!-- OTIxMzdaMGYxCzAJBgNVBAYTAlBMMREwDwYDVQQHDAhXYXJzemF3YTEhMB8GA1UE -->
<!-- CgwYUG93ZXJDbG91ZHMgTWljaGFsIEdhamRhMSEwHwYDVQQDDBhQb3dlckNsb3Vk -->
<!-- cyBNaWNoYWwgR2FqZGEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQCe -->
<!-- t+xkW4ECVL1DSj9xcwGDe3yTrXncKuO/UsHQjDG3wTKEMZJukC8gHXnPKlrtg9H3 -->
<!-- KtdG9L55A+x55VCl5Kpay4ATrxAUKds5B6Zu22aoe3tepXlaQk8PO+auvtT+r0cz -->
<!-- JvTA7/w7Fvm5V9tJBTOyRX0ZsGypifov8sZFghL9pVQBqc+aAWx/Fsg8s1JSggx5 -->
<!-- iOSQlLvxxLb97mhpLDFxobgqndbjCquLMEV5cqY6+ODhVJBAEH4425UkMEVkX50g -->
<!-- 42PFoHee/ZA4+5aMx8Rf9u1pA6yVqvcHoJwXjuou2r/x6ss+b9ScUyvUIcjXokyA -->
<!-- tTtyrUOwE2jWDvznhbPmlthn2lzss6yAaTTeryTgcV0f8nhs2QFoa8TZMFi/fNjL -->
<!-- 8orWspq/0Z1HwNNkD1Mh2B6JnD/unOy3fj77MBhMnWzSVTHH+ZYDGwYDsgmUbf8i -->
<!-- D6MvEGzLwiyP83iL6rvcskSsKNocPB1LypTP+TvuGGIdmQheNfj1Y2ms66O/sO27 -->
<!-- +9JA4jWxNFFQQ6ZTITTl7fff91bozeKjI+CE7xbAtODm+Gsi17YrhgeUVk1YpVOK -->
<!-- jBhnXjjMJ9Mh+CVWfxArEs/A0ZZHzXeACYmxaMso8hXU5FSCUTMt4MjxPFamz59e -->
<!-- 0f98qKLkVfFioQqU/D/J2u4/+UFJegzZg93b6iD+1QIDAQABo4IBeDCCAXQwDAYD -->
<!-- VR0TAQH/BAIwADA9BgNVHR8ENjA0MDKgMKAuhixodHRwOi8vY2NzY2EyMDIxLmNy -->
<!-- bC5jZXJ0dW0ucGwvY2NzY2EyMDIxLmNybDBzBggrBgEFBQcBAQRnMGUwLAYIKwYB -->
<!-- BQUHMAGGIGh0dHA6Ly9jY3NjYTIwMjEub2NzcC1jZXJ0dW0uY29tMDUGCCsGAQUF -->
<!-- BzAChilodHRwOi8vcmVwb3NpdG9yeS5jZXJ0dW0ucGwvY2NzY2EyMDIxLmNlcjAf -->
<!-- BgNVHSMEGDAWgBTddF1MANt7n6B0yrFu9zzAMsBwzTAdBgNVHQ4EFgQUdcAoCYdl -->
<!-- DBKMfqix4T3EIEAB5TEwSwYDVR0gBEQwQjAIBgZngQwBBAEwNgYLKoRoAYb2dwIF -->
<!-- AQQwJzAlBggrBgEFBQcCARYZaHR0cHM6Ly93d3cuY2VydHVtLnBsL0NQUzATBgNV -->
<!-- HSUEDDAKBggrBgEFBQcDAzAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQAD -->
<!-- ggIBABXxKsBFLjKAz9CvVM2Qv6+4MyUAJDT3Ws2i+9qHUsdwGkrdXvIO7z+FXxhP -->
<!-- I04XwcZNNcjhyQY6MIOxDWIVcJ3QhO0LScuZCeO43ZSLMrkNyXBbaHCyWIGU1QlD -->
<!-- m3Ah8uo0HMXcEDAmaUB1c2Ak4ARfTunTlSP+4cd7c3MvmZ4tOQcHKTKFd0RAFpa9 -->
<!-- ynEd/DoAXrACdDPAc3clEzPfbw5jJGwbs9Nysk5SyiNKCQtMAnYnqGRfRkLjb73t -->
<!-- NPH1M7UnUqnEVAqs+t64a7QCIUSDSIITGEB4bhlFUIuwAhekZj7yPIrPwv2TY8q7 -->
<!-- ClyN4bOTSphh/JGTBlW9dgSPF5bQCeW7s3jAggLnz4xg3eojZuSPhXYdL5Ax0Yya -->
<!-- ZoXq3K2CvP0/SYZh0xTp75nNI60tV9DsopI/ymkBwfYFS77Kmelohst2BAjCRmJN -->
<!-- LDZJK66vUXlarANhjgL5B/WUyxIuPNaf6PC9AzWz1GHxNbeNUCZgjD9wlpILDH5g -->
<!-- sYdimAhTvgftUyMKvyL42rKVpGZW9L8/v9/+vrkf+6fxufHxgksscAH0Qqofx++Y -->
<!-- lq4ZQ2PzprI9YBNLkmwuSQhf+IdT/jwu2cYUN9buOZJo39RtFsVLQMGfFuliN3SA -->
<!-- VtfFN+2f4wsRtp3q7BllwC4ke4KdtkedLNUFPUj10aBGkSacMYIHJDCCByACAQEw -->
<!-- ajBWMQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVtcyBT -->
<!-- LkEuMSQwIgYDVQQDExtDZXJ0dW0gQ29kZSBTaWduaW5nIDIwMjEgQ0ECED8vBp9c -->
<!-- a4iemmXFUwZ0lhUwDQYJYIZIAWUDBAIBBQCggYQwGAYKKwYBBAGCNwIBDDEKMAig -->
<!-- AoAAoQKAADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgorBgEEAYI3AgEL -->
<!-- MQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQgtijQ815FRBTwG3ETOLZq -->
<!-- m3ClpfuhrSrIjiwdF3NJbLIwDQYJKoZIhvcNAQEBBQAEggIAAAhkBgKkZO5r67jR -->
<!-- tAUWwspQC0fuYz2+WduY+zk5aFwSqkX4HB0gC3CEB82fzZPlS85T87PCJEo0z0lw -->
<!-- H6RJDcQZUhyRm3X77Uu6deaOu/hPrXWQ7VnwH6zl2wUJNr4/m4Gil1K27CCsr7cv -->
<!-- vCMKZb/n2aJp2kFJWlWvrcyWSlRS3iG51aciZG5dLUJZVRxmXcRufZMOzokwh8qm -->
<!-- xUFXawjWPmlA2Vqujc1ojIyaf6lLD6bBBAWVn3DtMsEJU2ZjH76cRyHF9ikJqGqq -->
<!-- 8haT+nZ2n59u+qQn9krHRToRuIFfpeLLieUKE9iUw9r2w8rm9c70P9VWgFIsuU8o -->
<!-- v+Lr6a8n3NWpY2iZQXypPRSDr/BxEETaW28V6GYNMgYZBqmP2Zgm+o2UVLCjG00c -->
<!-- LjLqKd0gFB3kbMeNeuZHVZVDzaCxMXrNN/EuLrhgJP3lKu8gnPXHi5pUs31Rt/XE -->
<!-- 3eEbiXP686fWT+Qh382rgDpoPWVrNlRrfsZrI3mahCIoMsOoKyTquTNXZ6RTuJUL -->
<!-- hJBGqkoS4YOFuvhoOjeRuxSj3Gz9qZwmBlC05p6zdjcWxEvS8bLnjv55NGfN8baB -->
<!-- JWQmvuc+6r89mzM1FLSQow+wQWaTEfBzQFme/WT4CeZTLmSllQgjZmAvxa92dl8h -->
<!-- YcA4jg0u2Kgz00eV48uIGXqS7RKhggQEMIIEAAYJKoZIhvcNAQkGMYID8TCCA+0C -->
<!-- AQEwazBWMQswCQYDVQQGEwJQTDEhMB8GA1UEChMYQXNzZWNvIERhdGEgU3lzdGVt -->
<!-- cyBTLkEuMSQwIgYDVQQDExtDZXJ0dW0gVGltZXN0YW1waW5nIDIwMjEgQ0ECEQDx -->
<!-- ZCWMCbbie+IOMmCOS/SoMA0GCWCGSAFlAwQCAgUAoIIBVzAaBgkqhkiG9w0BCQMx -->
<!-- DQYLKoZIhvcNAQkQAQQwHAYJKoZIhvcNAQkFMQ8XDTIyMDQxNDE2MjgwM1owNwYL -->
<!-- KoZIhvcNAQkQAi8xKDAmMCQwIgQgG1m/6OV3K6z2Q7t5rLSOgVh4TyHFVK4TR206 -->
<!-- Gj4FxdMwPwYJKoZIhvcNAQkEMTIEME5v0tKwiudEv1n1KYov3yE2QsVCGP8D7+aB -->
<!-- KIZy/jHXtb9ztg4DSQShYNtQ6qtKuTCBoAYLKoZIhvcNAQkQAgwxgZAwgY0wgYow -->
<!-- gYcEFNMRxpUxG4znP9W1Uxis31mK4ZsTMG8wWqRYMFYxCzAJBgNVBAYTAlBMMSEw -->
<!-- HwYDVQQKExhBc3NlY28gRGF0YSBTeXN0ZW1zIFMuQS4xJDAiBgNVBAMTG0NlcnR1 -->
<!-- bSBUaW1lc3RhbXBpbmcgMjAyMSBDQQIRAPFkJYwJtuJ74g4yYI5L9KgwDQYJKoZI -->
<!-- hvcNAQEBBQAEggIAh75u2Q63COfUNq/KdldXpX7Qqnn996b17Up5ji/ZZiHNkFma -->
<!-- Utf+0g7sg6QFYmdqQC7LWQf4kuskeSLrJLhtNf/Ps+FB6uU4/ADCrSaNP7hxOcal -->
<!-- hLtwo5N32OSoNksh4+/OPLNGGeXaY9BFv/fMzJd/2m5PdfvuZuXjzo9LkJZIZFIX -->
<!-- 4aGw2JQ5z5P/SSjUDAWxwQk+dS2bn7i2apedDFbyJehz4TZvhNaLBR8xxDiDHZS2 -->
<!-- KMtSKBiwO+4Z+J4KKri8LgiYy/g/KT0muVH8ZBqrE7tRffaBphREeXthG/T165fD -->
<!-- 49iFlXtn1lRogrgUL69QqrjOhx4ZXoTyW6EveDchIXRiS8dHom6iGgxp55PSyU9t -->
<!-- TFkoUuMIJ7BRvoePYFiqSKmKvQ26U3QukUN0zePCjaKeOw6ta+ATbJW0fhR+eEO5 -->
<!-- Awfj4nl0rVwo1O0b+DLzQSvGuDffVMuD1RBeghwZwrPRB2WInxpK27bek01D4T13 -->
<!-- /VjmfbEdbVXCMXCt8kViO9OwFwFwMnTPCBDg5hlVELW+U9M8RLj63AQB7GjmAtlS -->
<!-- T8ZYX+vy5GTisBTCuS6gzK5CuQnQPczkxqbHHZbX1Jam/1isBbvz1IVk8MXp675g -->
<!-- fCa+wIFEEexj2jUqjQGcdcHZNsV6sdaMw+StiHB57nCf40GE8hZRJMPnCX0= -->
<!-- SIG # End signature block -->

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,34 @@
# Uninstall a Windows Update by KB (PSWindowsUpdate)
Minimal helper and examples to remove an installed Windows update by its **KB** number using the **PSWindowsUpdate** module.
## Requirements
- Run PowerShell **as Administrator**.
- Module: `PSWindowsUpdate` (install if needed: `Install-Module PSWindowsUpdate -Scope CurrentUser`).
- A reboot may be required after removal.
## Quick commands (no wrapper)
```powershell
# List installed updates matching the KB
Get-WindowsUpdate -IsInstalled -KBArticleID KB5028952
# Uninstall (no auto-restart)
Remove-WindowsUpdate -KBArticleID KB5028952 -NoRestart -Confirm:$false
```
## Script usage
If you saved the wrapper as `Remove-KB.ps1`:
```powershell
# Example
.\Remove-KB.ps1 -KB KB5028952 -NoRestart
```
## Notes
- Some updates (especially **Servicing Stack Updates**) cannot be uninstalled.
- If removal via PSWindowsUpdate fails for a cumulative update, find the exact package name and try DISM:
```powershell
DISM /Online /Get-Packages
DISM /Online /Remove-Package /PackageName:<ExactName> /Quiet /NoRestart
```
- Logs: `C:\Windows\Logs\WindowsUpdate\windowsupdate.log` and `C:\Windows\Logs\CBS\CBS.log`.

View File

@@ -0,0 +1,36 @@
#Requires -RunAsAdministrator
#Requires -Modules PSWindowsUpdate
param(
[Parameter(Mandatory)]
[ValidatePattern('^(?i:KB)?\d+$')]
[string]$KB,
[switch]$NoRestart
)
# Normalize input to "KBxxxxxxx"
if ($KB -match '^\d+$') { $KB = "KB$KB" }
try {
Import-Module PSWindowsUpdate -ErrorAction Stop
# Query only installed updates matching the KB
$installed = Get-WindowsUpdate -IsInstalled -KBArticleID $KB -ErrorAction SilentlyContinue
if (-not $installed) {
Write-Host "Update $KB not found on this system."
return
}
Write-Host "Found installed update: $KB"
$removeParams = @{ KBArticleID = $KB; Confirm = $false }
if ($NoRestart) { $removeParams['NoRestart'] = $true }
Write-Host "Uninstalling $KB ..."
Remove-WindowsUpdate @removeParams -ErrorAction Stop
Write-Host "Uninstall command issued for $KB."
if (-not $NoRestart) { Write-Host "A restart may be required." }
}
catch {
Write-Error "Failed to uninstall $KB. $($_.Exception.Message)"
}

View File

@@ -0,0 +1,19 @@
# Remove Windows Updates by KB (PowerShell)
Uninstalls all installed Windows packages whose name matches a given **KB** via DISM PowerShell cmdlets.
## Usage
```powershell
# List installed packages and filter by KB
Get-WindowsPackage -Online | Where-Object { $_.PackageName -match "KB5032189" } | Select-Object PackageName, State, InstallTime
```
```powershell
# Uninstall all packages matching the KB
Remove-Package -KB "KB4589210"
```
## Notes & limitations
MSU files: Remove-WindowsPackage removes packages in the image (.cab/package identities), not .msu directly
SSUs cant be uninstalled: Servicing Stack Updates modify the update stack and are not removable.
After ResetBase: If you ran DISM /Online /Cleanup-Image /StartComponentCleanup /ResetBase, existing update packages can no longer be uninstalled.

View File

@@ -0,0 +1,33 @@
function Remove-Package {
param(
[string]$KB
)
# Find matching package(s)
$packages = Get-WindowsPackage -Online | Where-Object { $_.PackageName -match $KB }
if($packages.Count -eq 0) {
Write-Host "No packages found matching $KB."
return
}
# Display found packages
Write-Host "Packages found matching $KB"
$packages | ForEach-Object { Write-Host $_.PackageName }
# Uninstall packages
foreach($package in $packages) {
try {
Write-Host "Uninstalling $($package.PackageName)..."
Remove-WindowsPackage -Online -PackageName $package.PackageName -NoRestart -ErrorAction Stop
Write-Host "$($package.PackageName) uninstalled successfully."
} catch {
Write-Error "Failed to uninstall $($package.PackageName). Error: $_"
}
}
Write-Host "Please restart your computer."
}
# Example usage
Remove-Package -KB "KB4589210"