WSUS management with PowerShell

I have recently discovered the work of fellow MVP Adam Marshall who wrote a fantastic script aimed at cleaning your WSUS servers and decided to adopt it. A few days later I started thinking to how I could complete his work by adding a script that could automate the management of patches throughout the year for all of my servers, so I shouldn’t have to manually sync and approve patches.

In this post I will explain how I manage the whole patch process and introduce my PowerShell script, which I named wsus-operation.ps1 (which you can find on Github). Sure, this is an on-going work, so I’ll try to keep this post updated with feedbacks from the Community and with all the things I discover over time.

The first step is to understand how to manage the patching process through the year. Hence the reason that pushed me to spend a good amount of time studying the calendar to finally draw the circular timeline you see below: it allows easy visualization of the patching process from Patch Tuesday to the day Adam’s scripts does the monthly cleanup after all the patches have been deployed.

Supposing that today it’s June, 5th, next Patch Tuesday will happen in 7 days on Tuesday, June 12, 2018: Patch Tuesday comes the second Tuesday of the month so for sure we will have to include in our script a few lines of code to calculate it correctly.

After Patch Tuesday, it’s a best practice to wait roughly a couple of weeks before synching your WSUS server, then existing Domain Group Policy (GPO) should intervene and patch servers following their criticality.

A typical way of doing is to safely sync your WSUS with Microsoft on the fourth Monday of the month (aliased to FM in the circular timeline).

The day after the sync (FM + 1 day) you could auto-approve needed patches for standard non-critical servers, then a GPO configured in ‘Auto download and schedule the install‘ mode could fire the installation every Wednesday (which is always FM + 2 days) so that those servers actually get the patches and are eventually rebooted.

For touchy and critical servers, you could approve one week after the sync (FM + 7 days) and set the corresponding GPO configured in ‘Notify for download and notify for install‘ mode, so that nothing actually happen on the servers until you are 100% sure those patches don’t impact negatively your environment (i.e. in case of a bug).

In the end you would configure AdamJ script to perform the monthly cleanup of the WSUS database on the 7h of the month following Patch Tuesday so you have plenty of time to manually patch your most critical servers.

That could be resumed to:

  • Patch Tuesday (PT)
  • Sync WSUS avec Windows Update on fourth Monday (FM) (PT + 13 days)
  • Approve for non-critical servers (FM + 1 day)
  • GPO schedule the install on non-critical servers on Wednesday (FM + 2 days)
  • Approve for touchy and critical servers (FM + 7 days)
  • GPO notify new patches to touchy and critical servers (FM + 8 days)
  • Adam’s scripts clean up old computers and superseded patches (7th of following month)

Actually, you have to know that Adam’s script runs different actions whether it’s a standard daily run, a monthly run or a quarterly run (on January, April, July and October).

Here’s the actions performed by AdamJ script which daily:

  • Declines Multiple Types of Updates Stream
  • Cleans Up WSUS Synchronization Logs
  • Cleans up Computer Object
  • Performs WSUS DB Maintenance
  • Performs WSUS Server Cleanup Wizard

The same script is also in charge of monthly:

  • Cleaning All daily tasks
  • Removing Obsolete Updates
  • Compressing Update Revisions

Furthermore, on a quarterly basis, the same script:

  • Cleans all daily and monthly tasks
  • Removes WSUS Drivers
  • Removes Declined WSUS Updates

So back to my script now. Here’s an explanation of the most relevant parts.

Everything starts with retrieving the current day and putting it in a variable which I will reuse:

$now = Get-Date

Here’s how I decided to calculate the next Patch Tuesday:

$d0 = Get-Date -Day 1 -Month $($now.Month) -Year $now.Year
switch ($d0.DayOfWeek){
        "Sunday"    {$patchTuesday0 = $d0.AddDays(9); break}
        "Monday"    {$patchTuesday0 = $d0.AddDays(8); break}
        "Tuesday"   {$patchTuesday0 = $d0.AddDays(7); break}
        "Wednesday" {$patchTuesday0 = $d0.AddDays(13); break}
        "Thursday"  {$patchTuesday0 = $d0.AddDays(12); break}
        "Friday"    {$patchTuesday0 = $d0.AddDays(11); break}
        "Saturday"  {$patchTuesday0 = $d0.AddDays(10); break}
     }
$d1 = Get-Date -Day 1 -Month $($now.Month + 1) -Year $now.Year
switch ($d1.DayOfWeek){
        "Sunday"    {$patchTuesday1 = $d1.AddDays(9); break}
        "Monday"    {$patchTuesday1 = $d1.AddDays(8); break}
        "Tuesday"   {$patchTuesday1 = $d1.AddDays(7); break}
        "Wednesday" {$patchTuesday1 = $d1.AddDays(13); break}
        "Thursday"  {$patchTuesday1 = $d1.AddDays(12); break}
        "Friday"    {$patchTuesday1 = $d1.AddDays(11); break}
        "Saturday"  {$patchTuesday1 = $d1.AddDays(10); break}
     }
if($now.date -le $patchTuesday0.date){
    $patchTuesday = $patchTuesday0}else{$patchTuesday = $patchTuesday1
    }

The very same code is used to calculate the next Fourth Monday, which is the days I suggest you sync your WSUS with Microsoft:

$d0 = Get-Date -Day 1 -Month $($now.Month) -Year $now.Year
switch ($d0.DayOfWeek){
        "Sunday"    {$FourthMonday0 = $d0.AddDays(22); break}
        "Monday"    {$FourthMonday0 = $d0.AddDays(21); break}
        "Tuesday"   {$FourthMonday0 = $d0.AddDays(20); break}
        "Wednesday" {$FourthMonday0 = $d0.AddDays(26); break}
        "Thursday"  {$FourthMonday0 = $d0.AddDays(25); break}
        "Friday"    {$FourthMonday0 = $d0.AddDays(24); break}
        "Saturday"  {$FourthMonday0 = $d0.AddDays(23); break}
     }
    
$d1 = Get-Date -Day 1 -Month $($now.Month + 1) -Year $now.Year
switch ($d1.DayOfWeek){
        "Sunday"    {$FourthMonday1 = $d1.AddDays(22); break}
        "Monday"    {$FourthMonday1 = $d1.AddDays(21); break}
        "Tuesday"   {$FourthMonday1 = $d1.AddDays(20); break}
        "Wednesday" {$FourthMonday1 = $d1.AddDays(26); break}
        "Thursday"  {$FourthMonday1 = $d1.AddDays(25); break}
        "Friday"    {$FourthMonday1 = $d1.AddDays(24); break}
        "Saturday"  {$FourthMonday1 = $d1.AddDays(23); break}
     }
if($now.date -le $FourthMonday0.date){
    $FourthMonday = $FourthMonday0}else{$FourthMonday= $FourthMonday1
    }

On top of this last portion of code I built a calculation for the days when I approve patches both for non-critical and critical servers. Here’s the code:

if($now.date -le $FourthMonday0.adddays(1).date){
    $StandardApprovalDay = $FourthMonday0.AddDays(1)}else{$StandardApprovalDay= $FourthMonday1.AddDays(1)
    }
if($now.date -le $FourthMonday0.adddays(1).date){
    $CriticalApprovalDay = $FourthMonday0.AddDays(7)}else{$CriticalApprovalDay= $FourthMonday1.AddDays(7)
    }

Then all the possible conditions have to be evaluated so that proper actions are executed. If no actions are to be executed, then we just display a message.

if($now.date -eq $PatchTuesday.date){
    "==> It's patch Tuesday!`n"
    }
else {
    "Next Patch Tuesday is in $((New-TimeSpan -Start $now.date -End $patchTuesday.date).days) days on $($patchTuesday.ToLongDateString())`n"
    
    }
if($now.date -eq $FourthMonday.date){
    (Get-WsusServer).GetSubscription().StartSynchronization()
    }
else {
    "Next Sync will happen in $((New-TimeSpan -Start $now.date -End $FourthMonday.date).days) days on $($FourthMonday.ToLongDateString())`n"
    
    }

Before we continue, a quick note about WSUS group assignment: you really should enable Client Side Targetingin your WSUS GPO so that you are assured that your servers will automatically fall in the right WSUS groups.

In my case I have configured targeting of non-critical servers in a group named ‘standard servers‘ and targeting of touchy and critical servers in groups named ‘touchy servers‘ and ‘critical servers‘.

In the following part I am using the Approve-WsusUpdate cmdlet to approve unapproved patches that are needed by servers residing in WSUS groups that match the word ‘standard‘ in their names:

if($now.date -eq $StandardApprovalDay.date){
    "==> It's the day after fourth monday of the month - approving for Standard servers`n"
    $wsus = Get-WsusServer
    $allupdates = $wsus.GetUpdates() 
    $alltargetgroups = $wsus.GetComputerTargetGroups()
    $computergroups = ($alltargetgroups | ? name -match 'Standard').name
    $computergroups | % {
        Get-WsusUpdate -Approval Unapproved -Status FailedOrNeeded | Approve-WsusUpdate -Action Install -TargetGroupName $_ –Verbose
        }
    }
else {
    "Next approval for Standard servers will happen in $((New-TimeSpan -Start $now.date -End $StandardApprovalDay.Date).days) days on $($StandardApprovalDay.ToLongDateString())`n"
    
    }

In the following part I am approving needed unapproved patches for servers residing in WSUS groups that have the words ‘touchy‘ or ‘critical‘ in their names:

if($now.date -eq $CriticalApprovalDay.date){
    "==> It's the 7th day after fourth monday of the month - approving for User-Touchy and Mission-Critical servers`n"
    $wsus = Get-WsusServer
    $allupdates = $wsus.GetUpdates() 
    $alltargetgroups = $wsus.GetComputerTargetGroups()
    $computergroups = ($alltargetgroups | ? name -match 'touchy|critical').name
    $computergroups | % {
        Get-WsusUpdate -Approval Unapproved -Status FailedOrNeeded | Approve-WsusUpdate -Action Install -TargetGroupName $_ –Verbose
        }
    }
else {
    "Next approval for User-Touchy and Mission-Critical servers will happen in $((New-TimeSpan -Start $now.date -End $CriticalApprovalDay.date).days) days on $($CriticalApprovalDay.ToLongDateString())`n"
    
    }
if($now.day -eq 7){
    "==> Today is WSUS monthly clean up day`n"
    }
else{
    "Next WSUS monthly clean up will happen in $((New-TimeSpan -Start $now.date -End $(Get-Date -Day 7 -Month $($now.Month + 1) -Year $now.Year -OutVariable datenextcleanup).Date).Days) days on $($datenextcleanup.ToLongDateString())`n"
    }

As you can see, coupling those actions with well configured group policies and with Adam’s script will make your WSUS installation agile and pretty automated.

Of course this approach can be improved and if I find better ways of doing I won’t hesitate to update the PowerShell script on GitHub as well as this post. I hope that the community will contribute to the improvement of this script based on its experience, so that this can benefit the Community and will make WSUS admins less prone to headache.

If you liked this post, feel free to share.

UPDATE June, 11th 2018: This post got a lot of feedbacks, and there is an optimization in particular that made it to the master branch on GiHub: I’d like to thank CleverTwain (reddit github) for making my script more modular. He made a few nice additions such as:

Moving parameters and general settings to the top:

$action = $false
$now = Get-Date
$comments = "Today is $($now.ToLongDateString())`n"
$WSUSServerParams = @{
    Name   = 'wsusserver'
    Port   = 8530
    UseSSL = $false
}
# Moved these to the top as others may want to tweak as necessary
$SyncDelay = 13 # How many days after Patch Tuesday should we wait before syncing WSUS
$WSUSCleanUpDay = 7 # What numerical day of the month whould the WSUS cleanup script run?
# Changed delay settings to use objects, as that is the most flexible
$DelaySettings = @()
$DelaySettings += [pscustomobject]@{
    Name          = 'Immediate'
    # Now multiple collections can share the same delay settings without adding multiple checks
    Collections   = 'Standard', 'NonCritical'
    ApprovalDelay = 1
}
$DelaySettings += [pscustomobject]@{
    Name          = 'OneWeek'
    Collections   = 'Touchy', 'Critical'
    ApprovalDelay = 7
}

Making Fourth Monday calculation dependant from Patch Tuesday through a SyncDelay variable that WSUS admins can set according to their internal policy:

$firstOfThisMonth = (Get-Date -Day 1 )
switch ( $firstOfThisMonth.DayOfWeek ) {
    "Sunday" {$thisPatchTuesday = $firstOfThisMonth.AddDays(9)}
    "Monday" {$thisPatchTuesday = $firstOfThisMonth.AddDays(8)}
    "Tuesday" {$thisPatchTuesday = $firstOfThisMonth.AddDays(7)}
    "Wednesday" {$thisPatchTuesday = $firstOfThisMonth.AddDays(13)}
    "Thursday" {$thisPatchTuesday = $firstOfThisMonth.AddDays(12)}
    "Friday" {$thisPatchTuesday = $firstOfThisMonth.AddDays(11)}
    "Saturday" {$thisPatchTuesday = $firstOfThisMonth.AddDays(10)}
}
if ($now.date -le $thisPatchTuesday.date) {
    $patchTuesday = $thisPatchTuesday
   
}
else {
    $firstOfNextMonth = (Get-Date -Day 1 -Month ((Get-Date).AddMonths(1).Month) )
    switch ( $firstOfNextMonth.DayOfWeek ) {
        "Sunday" {$patchTuesday = $firstOfNextMonth.AddDays(9); break}
        "Monday" {$patchTuesday = $firstOfNextMonth.AddDays(8); break}
        "Tuesday" {$patchTuesday = $firstOfNextMonth.AddDays(7); break}
        "Wednesday" {$patchTuesday = $firstOfNextMonth.AddDays(13); break}
        "Thursday" {$patchTuesday = $firstOfNextMonth.AddDays(12); break}
        "Friday" {$patchTuesday = $firstOfNextMonth.AddDays(11); break}
        "Saturday" {$patchTuesday = $firstOfNextMonth.AddDays(10); break}
    }
}
$SyncDay = (Get-Date -Date $patchTuesday).AddDays($SyncDelay)

Gathering all WSUS patch information once and once only:

# Getting this once now, rather than for each iteration....
$wsus = Get-WsusServer @WSUSServerParams
$allupdates = $wsus.GetUpdates()
$alltargetgroups = $wsus.GetComputerTargetGroups()
$NeededUpdates = Get-WsusUpdate -Approval Unapproved -Status FailedOrNeeded

So thanks to him for these contributions. The updated script can be found here. That is exactly what being part of a technical community should be like.

UPDATE June, 14th 2018: Added a few more date conditions so that when the script runs between approvals, it is able to handle correctly the action to do. Added also a few minor fixes. Find the updated code on GitHub.

UPDATE June, 20th 2018: Published the Invoke-Wsus advanced PowerShell function which improves and replaces the wsus-operations.ps1 script.

Leave a comment

Your email address will not be published. Required fields are marked *