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

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){



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"



    "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.

Monday, February 12, 2018

How to use PowerShell to solve WSUS error 0x80244022

Recently I have started seeing my WSUS clients returning error 0x80244022 during the classic Windows Update checks.

At first I tought there was some kind of bug on my clients, then, once I checked my WSUS server, I came across an error message on the patch management console which stated I had to perform a Node Reset for the WSUS service to get back online.

After a bit of digging, I discovered that the WSUS pool is configured with a hardcoded Private Memory Limit set to 1843200 (which is 1.8 GB). Further analysis made me realize that each time that memory limit is reached, the IIS pool simply stops, breaking my WSUS service.

Needless to say, I decicded to try and solve this issue using my favorite tool: PowerShell.

So I produced a quick and dirty PowerShell script that increases that private memory limit to 8 GB, which should be enough for this kind of service.

Here's the code:
Import-Module WebAdministration
$NewPrivateMemoryLimit = 8388608
$ApplicationPoolsPath = "/system.applicationHost/applicationPools"
$ApplicationPools = Get-WebConfiguration $applicationPoolsPath
    foreach ($AppPool in $ApplicationPools.Collection) {
     if ($AppPool.name -eq 'WsusPool') {
      $AppPoolPath = "$ApplicationPoolsPath/add[@name='$($AppPool.Name)']"
      $CurrentPrivateMemoryLimit = (Get-WebConfiguration "$AppPoolPath/recycling/periodicRestart/@privateMemory").Value
            "Private Memory Limit for $($AppPool.name) is currently set to: $($CurrentPrivateMemoryLimit/1000) MB"
            Set-WebConfiguration "$AppPoolPath/recycling/periodicRestart/@privateMemory" -Value $NewPrivateMemoryLimit
            "New Private Memory Limit for $($AppPool.name) is: $($NewPrivateMemoryLimit/1000) MB"
            Restart-WebAppPool -Name $($AppPool.name)
            "Restarted the $($AppPool.name) Application Pool to apply changes"
Once you run this piece of code on your WSUS server, the application pool gets restarted and your WSUS will have happy access to an increased memory space:

If you look at the following Resource Monitor screenshot you will see that the actual used memory becomes in my case a bit more of 2 GB, which is larger than the default 1.8 GB value but smaller than the 8 GB I set, so I'm fine:

Stay tuned for more PowerShell.

Friday, February 2, 2018

PowerShell oneliner to list all the installed .NET versions on a remote computer

Not so long ago I described how to retrieve the installed .NET version in a single line of PowerShell code through a registry query. This time I want to show you how the same can be achieved on a remote system by modifying a bit our approach. The big difference is that for a remote instance you have to access the HKEY_LOCAL_MACHINE base key through the RegistryKey.OpenRemoteBaseKey method.

The key path is the same as the one we saw on the locally run oneliner:
HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP

The regular expression is moved at the beginning of the oneliner, so that I can cycle through every item and open one subkey at the time.
"v1.1.4322","v2.0.50727","v3.0","v3.5","v4\Full"|%{([Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey('LocalMachine', $Computer)).OpenSubKey("SOFTWARE\Microsoft\NET Framework Setup\NDP\$_").GetValue('version')}
As you can see with a bit of PowerShell gymnastics, you can get pretty concise lines of code that do powerful things.
Stay tuned for more PowerShell.
Related Posts Plugin for WordPress, Blogger...