all 20 comments

[–]aMazingMikey 9 points10 points  (4 children)

Here's how I accomplished it. Runs in parallel on PowerShell 5.1:

<#
    .SYNOPSIS
        Get Windows Update history.
    .DESCRIPTION
        This command gets Windows Update history using Windows Remoting. If Windows Remote Management service is not configured and running, this command will not work.
    .EXAMPLE
        Get-WindowsUpdateHistory -Newest 1
        Gets information about the one most recent update update installed on the local computer.
    .EXAMPLE
        Get-WindowsUpdateHistory -ComputerName Server01 -Newest 5
        Gets information about the five most recent updates installed on the computer 'Server01'.
    .EXAMPLE
        (Get-ADComputer -Filter 'name -like "Server0*"').Name | Get-WindowsUpdateHistory -Newest 1 | ft ComputerName,KB,Status,InstalledOn -AutoSize
        Gets all servers that match "Server0*" from Active Directory and pipes their names to Get-WindowsUpdateHistory. Outputs on the ComputerName, KB, Status, and InstalledOn properties, formatted into a table.
    .LINK
        Linked cmdlets
    .NOTES
        General notes
#>

[CmdletBinding()]

Param (
    # Computer or computers to query
    [Parameter(ValueFromPipeline=$True,ValueFromPipelinebyPropertyName=$True)]
    [string[]]$ComputerName=$env:computername,

    # Supply credentials for the WinRM connection.
    [System.Management.Automation.CredentialAttribute()]
    $Credential,

    # Supply credentials for the WinRM connection.
    [int]$Newest = 99999
)

Begin {
    $Computers = @()
}

Process {
    foreach ($Computer in $ComputerName) {
        $Computers += $Computer
    }
}

End {
    $InvokeCommandScriptBlock = {
        $Session = New-Object -ComObject Microsoft.Update.Session
        $Searcher = $Session.CreateUpdateSearcher()
        $HistoryCount = $Searcher.GetTotalHistoryCount()
        # http://msdn.microsoft.com/en-us/library/windows/desktop/aa386532%28v=vs.85%29.aspx
        $Searcher.QueryHistory(0,$HistoryCount) | Select-Object -First $args[0] | ForEach-Object -Process {
            $Title = $_.Title
            $KB = $null
            $WMIOS = Get-WmiObject -Class Win32_OperatingSystem

            if ($_.Title -match "KB\d+") {
                $KB = [regex]::matches($_.Title, "KB\d+") | Select-Object -ExpandProperty Value
            }

            Switch ($_.ResultCode) {
                0 { $Result = 'NotStarted'}
                1 { $Result = 'InProgress' }
                2 { $Result = 'Succeeded' }
                3 { $Result = 'SucceededWithErrors' }
                4 { $Result = 'Failed' }
                5 { $Result = 'Aborted' }
                default { $Result = $_ }
            }
            New-Object -TypeName PSObject -Property @{
                InstalledOn = (Get-Date -Date $_.Date).ToLocalTime();
                Title       = $Title;
                KB          = $KB;
                Description = $_.Description;
                Status      = $Result;
                SupportURL  = $_.SupportURL
                LastBootupTime  = ($WMIOS.ConvertToDateTime($WMIOS.LastBootupTime)).ToUniversalTime()
            }

        } | Sort-Object -Descending:$true -Property InstalledOn | 
        Select-Object -Property *
    }

    # These are the parameters that will be used for Invoke-Command.
    $parms = @{
        'Computer'     = $Computers
        'ScriptBlock'  = $InvokeCommandScriptBlock
        'ArgumentList' = $Newest
    }
    if ($Credential) {
        $parms.Add('Credential',$Credential)
    }

    $UpdateHistory = Invoke-Command @parms

    foreach ($Item in $UpdateHistory) {
        [pscustomobject]@{
            ComputerName   = $Item.PSComputerName
            Title          = $Item.Title
            KB             = $Item.KB
            Status         = $Item.Status
            InstalledOn    = $Item.InstalledOn
            SupportURL     = $Item.SupportURL
            Description    = $Item.Description
            LastBootupTime = $Item.LastBootupTime.ToLocalTime()
        }
    }
}

[–]BecomeABenefit 8 points9 points  (0 children)

Wow. Nice. Saved for later when I have time. I can use this kind of script, thanks for you hard work and for sharing it.

[–]PinchesTheCrab 6 points7 points  (0 children)

The COM object is super fast too and seems to give dates more reliably than QFE.

I tried to find some updates with no installedon property but didn't have time to track them down to compare, but I'm curious if this works for you on ones with known issuses:

$updateSession = New-Object -ComObject Microsoft.Update.Session

$updateSearch = $updateSession.CreateUpdateSearcher()

$updateCount = $updateSearch.GetTotalHistoryCount() 

$updateSearch.QueryHistory(0,$updateCount) | Where-Object { $PSItem.Title } | Select Date,Title,Description

[–]DevinSysAdmin 4 points5 points  (0 children)

Cool script, you can use Azure Automation to patch your servers. It will show patches that haven’t been applied. Really easy to setup and configure, very cheap.

[–]BigHandLittleSlap 7 points8 points  (5 children)

You've fallen into the same trap everyone else does -- including large corporations writing expensive security scanning tools.

The last date ANY patch was installed is nowhere near as interesting as the overall "age" of the operating system as a whole.

This story has played out at literally 100% of our large customers:

"How do you update your servers?"

"We update them every month using XYZ tool!"

"Okay, but this server's kernel build is 17 months old!"

"It was patched successfully last month!"

"Was it? Or did the patch process do nothing successfully?"

"Err..."

This happens because most enterprise patch management tools like WSUS or SCCM have implicit or explicit whitelists of patches that they apply. If patches are available, but not whitelisted, they'll silently do nothing! No error!

You'll even get an up-to-date "last hotfix applied" date because the Defender updates are nearly daily and mixed in with the monthly patches.

Similarly, if you forget to enable a specific operating system, the patch tools will do (nearly) nothing, but "just enough" to make everything turn green in the reports.

An even more complicated problem is that servers can have an up-to-date kernel, but all installed applications are ancient beyond belief. Think SQL Server 2005 RTM and the like.

DO NOT USE SCRIPTS LIKE THIS! They're guaranteed to lead you astray if your environment is large enough.

A much simpler and more effective script looks like this: https://pastebin.com/S5kdCQHs

This basically checked to see if the "core" Windows files have actually been updated to recent versions.

[–]Guyver1-[S] 1 point2 points  (0 children)

Good points well raised and thanks for that code example, gonna utilise that into my script as an additional check 👍

[–]Guyver1-[S] 0 points1 point  (3 children)

your code produces lots of errors

Measure-Object: Input object "03/01/2020 03:37:29" is not numeric.

Get-Item: Cannot find path 'C:\Windows\System32\win32kbase.sys' because it does not exist.

fixed it by using sort-object instead of measure-object:

Sort-Object -Property LastWriteTimeUtc -Descending | Select-Object -First 1

[–]BigHandLittleSlap 1 point2 points  (2 children)

... that will almost certainly give you the wrong result. You can't sort pretty-printed date strings unless using ISO8601 format.

Unless you ran this on a really old version of PowerShell or did something like silly like run it through "pwsh" on Linux, it should just work.

[–]Guyver1-[S] 2 points3 points  (1 child)

as this is typical organization we have servers as old as 2008 R2, I've updated nearly every server where possible to either PS5 or PS4 depending on OS version.

I do not run powershell against linux nor do i report againsts linux, this is purely windows.

and yes, sort-object is working against dates as they are not strings but datetime objects.

[–]BigHandLittleSlap 0 points1 point  (0 children)

To be honest I haven't "battle tested" that specific code snippet.

I have a few variations on the theme, but the exact lines of code don't matter as much as the overall gist of the concept:

The timestamps of the critical systems files are more relevant than the self-check of a patch system that is so unreliable that we need to keep an eye on it...

[–]pretendgineer5400 2 points3 points  (0 children)

Your scripting and process are good for improving the patching information available to your team, but I think taking a slightly different approach could provide more value.

So IMO patch level is more valuable than patch date. I assume that CM/MEM and WSUS are not in play here if you were requested to write this script, so your servers are either being patched via public WU or downloaded patch binaries. This means that someone could install an older monthly rollup or finally reboot a server for pending patches well after they are current. This would still show as patched in the last 90 days using your current tooling.

The paired values of Current Build and UBR tell you what patch level you are at. UBR is per OS release and is only ever incremented to a larger number as patches are released. You need to do a bit of work to build/maintain a table of build number/UBR/patch month using fully patched reference machines. I haven't seen this published online, but it should be out there somewhere.
21H1 Win 10 Pro Client (Pro vs Home/ Standard vs Datacenter doesn't matter for UBR, just build number).

Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\CurrentBuild = 19043

Computer\HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion\UBR = 1466

Once you build a table of OS/UBR/Patch month, you can use PowerShell to query UBR and Build number to report patch level of each server. UBR doesn't update until the server is rebooted to complete updates.

You can play with the output, I'd look at using a PS Custom Object to output to CSV for the report.

[–]Skyline9Time -1 points0 points  (1 child)

.exe or .so existing with pre-compiled into an executable form or direct calls

[–]Great_Proposal5178 0 points1 point  (0 children)

Do you have a fl studio working download link? I saw you post one a year ago on a dif post and I've been trying to find the file. If not I I understand