Wednesday, June 20, 2018

Introducing the Invoke-Wsus PowerShell function

A couple of weeks ago I wrote a blog post on WSUS management with PowerShell. The post came with a script that could be used to manage automatic approvals of WSUS patches and updates.

This time I have decided to transform that script in an advanced PowerShell function named Invoke-WSUS (you can find it on my Github).

This function is kind of jack-of-all-trade: depending on the used parameter set, it can do a series of actions, as shown in the description and in the help file.

NAME
    Invoke-Wsus
    
SYNOPSIS
    Invoke-Wsus is a function used to manage WSUS.
    
    
SYNTAX
    Invoke-Wsus -ShowApprovalGroups -SettingFile string [commonparameters]
    
    Invoke-Wsus -SettingFile string -WsusName string -WsusPort int32 -WsusSSL -SyncDelay int32 -CleanupDay int32 -SendMail -SMTPServer string -From string -To string> [commonparameters]
    
    Invoke-Wsus -SettingFile string -WsusName string -WsusPort int32 -WsusSSL -SyncDelay int32 -CleanupDay int32 -ShowAll [commonparameters]
    
    Invoke-Wsus -SettingFile string -WsusName string -WsusPort int32 -WsusSSL -SyncDelay int32 -ShowApprovalSchedules [-RunApprovalSchedules] [commonparameters]
    
    Invoke-Wsus -ShowWsusTargetGroups -WsusName string -WsusPort int32 -WsusSSL [commonparameters]
    
    Invoke-Wsus -WsusName string -WsusPort int32 -WsusSSL -ShowLastWsusSync [commonparameters]
    
    Invoke-Wsus -WsusName string -WsusPort int32 -WsusSSL -SyncWsusNow [commonparameters]
    
    Invoke-Wsus -ShowNextPatchTuesday [commonparameters]
    
    Invoke-Wsus -ShowNextSyncTuesday -SyncDelay int32 [commonparameters]
    
    Invoke-Wsus -ShowNextClenaupDay -CleanupDay int32 [commonparameters]
    
    
DESCRIPTION
    Invoke-Wsus is a function that is used to determine the next Patch Tuesday, to sync a
    WSUS server with Microsoft,     to approve patches based on target WSUS groups, and to
    show WSUS target groups configuration.
    

RELATED LINKS

REMARKS
    To see the examples, type: "get-help Invoke-Wsus -examples".
    For more information, type: "get-help Invoke-Wsus -detailed".
    For technical information, type: "get-help Invoke-Wsus -full

Before you can use this function you have to use the Set-ApprovalDelay script to generate a JSON setting file. This file will contain the list of the approval schedules and, for each schedule, you will have associated WSUS target computer groups and the delay in days between the synchronization of your WSUS server and the actual approval of patches.

Let's take as an example the following JSON settings:

$DelaySettings += [pscustomobject]@{

    Name          = 'Immediate'

    WsusGroup   = 'Standard'

    ApprovalDelay = 1

}

$DelaySettings += [pscustomobject]@{

    Name          = 'OneWeek'

    WsusGroup   = 'Touchy','Critical'

    ApprovalDelay = 7

}
This settings will basically say to the Invoke-Wsus function that
  • computers belonging to a WSUS target computer group whose name matches the 'Standard' word will be patched 1 day after the synchroniezation of your WSUS server with Microsoft.
  • computers belonging to a WSUS target computer group whose name matches the 'Touchy' or 'Critical' word will be patched 7 days after the synchroniezation of your WSUS server with Microsoft.

You can simply adapt these settings to your environment and re-run the Set-ApprovalDelay script to generate a new setting file for your organization.

Now that you have understood what the configuratrion file is used for, here's a couple of useful examples that show what the Invoke-Wsus function is capable of.

Example 1: Showing the approval delay per target computer group from the setting file in a readable way

Invoke-Wsus -ShowApprovalGroups -SettingFile approvaldelaysettings.json' 

Today is 06/20/2018 11:28:05

Name      WsusGroup          ApprovalDelay
----      ---------          -------------
Immediate Standard                       1
OneWeek   {Touchy, Critical}             7
Example 2: Showing the list of target computer groups defined on the WSUS server

Invoke-Wsus -ShowWsusTargetGroups -WsusName 'wsusserver' -WsusPort 8530 -WsusSSL:$false

Today is 06/20/2018 13:26:45

Name                                     Total computers
----                                     ---------------
All Computers                                        381
Mission-Critical-M1                                   37
Mission-Critical-M2                                   35
Standard                                             210
User-Touchy-M1                                        50
User-Touchy-M2                                        49
Unassigned Computers                                   0
Having this information will simplify the work of chosing the words to use in the setting file to match target computer groups.

Example 3: Showing the next Patch Tuesday (optimistically renamed by Microsoft to Update Tuesday)

Invoke-Wsus -ShowNextPatchTuesday

Today is 06/20/2018 13:30:27
Next Patch Tuesday will be in 20 days on Tuesday, July 10, 2018
I let you take the time to find out what other actions this advanced function can perform. The only thing that is really important to understand is that this Invoke-Wsus function is meant to be scheduled daily on your WSUS Server just like in the example below:

Invoke-Wsus -ShowApprovalSchedules -SettingFile 'approvaldelaysettings.json' -SyncDelay 13 -RunApprovalSchedules -WsusName 'wsusserver' -WsusPort 8530 -WsusSSL:$false
This basically tell the function to execute the automatic approval of needed patches and updates for target computer groups based on a delay between Patch Tuesday, Synchronization day and the delay specified in the setting file.

You could also add a second scheduled task that will inform you by mail of the actions of the day by using the 'Send Mail' parameter set.

Let me know how it goes for you and if you have any question or improvement to suggest, feel free to get in touch with me.

Tuesday, June 12, 2018

Gathering WSUS Security Monthly Quality Rollups with PowerShell

If you follow me, you know that I've been playing a lot with WSUS in recent days and have discovered there's an extremely simple way to extract the list of all the Microsoft Security Monthly Quality Rollups (which include cumulated security updates and non-security updates) residing on a WSUS server with the help of PowerShell.
The key cmdlet is Get-WsusUpdate. The Get-WsusUpdate cmdlet gets the Windows Server Update Services (WSUS) update object with details about existing updates.

$MSupdates  = Get-WsusUpdate
Using Get-Member we can see the returned object type:
$MSupdates  | Get-Member

   TypeName: Microsoft.UpdateServices.Commands.WsusUpdate

Name                               MemberType Definition                    
----                               ---------- ----------                    
Equals                             Method     bool Equals(System.Object obj)
GetHashCode                        Method     int GetHashCode()             
GetType                            Method     type GetType()                
ToString                           Method     string ToString()             
Approved                           Property   string Approved {get;}        
Classification                     Property   string Classification {get;}  
ComputersInstalledOrNotApplicable  Property   int ComputersInstalledOrNotApp
ComputersNeedingThisUpdate         Property   int ComputersNeedingThisUpdate
ComputersWithErrors                Property   int ComputersWithErrors {get;}
ComputersWithNoStatus              Property   int ComputersWithNoStatus {get
InstalledOrNotApplicablePercentage Property   int InstalledOrNotApplicablePe
LanguagesSupported                 Property   System.Collections.Specialized
LicenseAgreement                   Property   string LicenseAgreement {get;}
MayRequestUserInput                Property   bool MayRequestUserInput {get;
MsrcNumbers                        Property   System.Collections.Specialized
MustBeInstalledExclusively         Property   bool MustBeInstalledExclusivel
Products                           Property   System.Collections.Specialized
Removable                          Property   bool Removable {get;}         
RestartBehavior                    Property   string RestartBehavior {get;} 
Update                             Property   Microsoft.UpdateServices.Admin
UpdateId                           Property   string UpdateId {get;}        
UpdatesSupersededByThisUpdate      Property   System.Collections.Specialized
UpdatesSupersedingThisUpdate       Property   System.Collections.Specialized
The key point here is that Get-WindowsUpdate used with no parameters reports patch information of the computer where it runs, which is just a subset of all the possible patches.

If you want to retrieve the list of all the patches hosted by your WSUS server here's the syntaxt to use:

$MSupdates = Get-WsusUpdate -Verbose -Approval AnyExceptDeclined
Once you got that (it can take a while, and you'll se the WSUS database pretty busy), you can easily select the patches that are Security Monthly Quality Rollups:

$MSupdates.Update |

    ? Title -match 'Security Monthly Quality Rollup' |

    Format-Table title

Title
-----
2018-03 Security Monthly Quality Rollup for Windows Server 2012 R2...
2018-03 Security Monthly Quality Rollup for Windows Server 2012 fo...
2018-03 Security Monthly Quality Rollup for Windows 7 for x64-base...
2018-03 Security Monthly Quality Rollup for Windows Server 2008 R2...
2018-03 Security Monthly Quality Rollup for Windows 7 for x86-base...
As you can see the monthly rollups I see here is the one from last March Patch Tuesday (KB4088876).

I could think of counting all these Monthly Rollups by OS:

$MSupdates.Update |

    ? Title -match 'Security Monthly Quality Rollup' |

    Group ProductTitles | Select Name,Count | Sort -Descending
All you need to know is simply what kind of patches you want to extract from this huge list. A few examples now.

Patches whose title starts with 2018:

$MSupdates.Update |

    ? Title -match '^2018' |

    Formaat-table title, creationdate, knowledgebasearticles, producttitles, state
Patches whose title starts with 2017-12 (in this regex the caret ^ matches the position before the first character in the string):

$MSupdates.Update |

    ? Title -match '^2017-12' |

    Format-Table title, creationdate, knowledgebasearticles, producttitles, state
Patches with a certain KB number:

$MSupdates.Update |

    ? KnowledgebaseArticles -match '4088876' |

    Format-Table title, creationdate, knowledgebasearticles, producttitles, state
Stay tuned for more PowerShell and get ready for today's Patch Tuesday!

Wednesday, June 6, 2018

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 Targeting in 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.
Related Posts Plugin for WordPress, Blogger...