Public/Get-LatestUpdate.ps1

Function Get-LatestUpdate {
    <#
    .SYNOPSIS
        Get the latest Cumulative or Monthly Rollup update for Windows.

    .DESCRIPTION
        Returns the latest Cumulative or Monthly Rollup updates for Windows 10 / 8.1 / 7 and corresponding Windows Server from the Microsoft Update Catalog by querying the Update History page.

        Get-LatestUpdate outputs the result as a table that can be passed to Save-LatestUpdate to download the update locally. Then do one or more of the following:
        - Import the update into an MDT share with Import-LatestUpdate to speed up deployment of Windows (reference images etc.)
        - Apply the update to an offline WIM using DISM
        - Deploy the update with ConfigMgr (if not using WSUS)

    .NOTES
        Author: Aaron Parker
        Twitter: @stealthpuppy

        Original script: Copyright Keith Garner, All rights reserved.
        Forked from: https://gist.github.com/keithga/1ad0abd1f7ba6e2f8aff63d94ab03048

    .LINK
        https://support.microsoft.com/en-au/help/4464619

    .PARAMETER WindowsVersion
        Specifiy the Windows version to search for updates. Valid values are Windows10, Windows8, Windows7 (applies to desktop and server editions).

    .PARAMETER Build
        Dynamic parameter used with -WindowsVersion 'Windows10' Specify the Windows 10 build number for searching cumulative updates. Supports '17133', '16299', '15063', '14393', '10586', '10240'.

    .EXAMPLE
        Get-LatestUpdate

        Description:
        Get the latest Cumulative Update for Windows 10 Semi-Annual Channel.

    .EXAMPLE
        Get-LatestUpdate -WindowsVersion Windows10

        Description:
        Get the latest Cumulative Update for Windows 10 Semi-Annual Channel.

    .EXAMPLE
        Get-LatestUpdate -WindowsVersion Windows10 -Build 14393
    
        Description:
        Enumerate the latest Cumulative Update for Windows 10 1607 and Windows Server 2016.

    .EXAMPLE
        Get-LatestUpdate -WindowsVersion Windows10 -Build 15063
    
        Description:
        Enumerate the latest Cumulative Update for Windows 10 1703.

    .EXAMPLE
        Get-LatestUpdate -WindowsVersion Windows8
    
        Description:
        Enumerate the latest Monthly Update for Windows Server 2012 R2 / Windows 8.1.

    .EXAMPLE
        Get-LatestUpdate -WindowsVersion Windows7
    
        Description:
        Enumerate the latest Monthly Update for Windows 7 (and Windows 7 Embedded).
    #>

    [CmdletBinding(SupportsShouldProcess = $False)]
    Param(
        [Parameter(Mandatory = $False, Position = 0, HelpMessage = "Select the OS to search for updates")]
        [ValidateSet('Windows10', 'Windows8', 'Windows7')]
        [String] $WindowsVersion = "Windows10",

        [Parameter(Mandatory = $False, Position = 1, HelpMessage = "Provide a Windows 10 build number")]
        [ValidateSet('17763', '17134', '16299', '15063', '14393', '10586', '10240', '^(?!.*Preview)(?=.*Monthly).*')]
        [String] $Build = "17763"
    )
    Begin {
        # Set values for -Build and -SearchString as required for each platform
        Switch ($WindowsVersion) {
            "Windows10" {
                [String] $StartKB = 'https://support.microsoft.com/app/content/api/content/feeds/sap/en-us/6ae59d69-36fc-8e4d-23dd-631d98bf74a9/atom'
                If ($Build -eq "^(?!.*Preview)(?=.*Monthly).*") { $Build = "17763" }
            }
            "Windows8" {
                [String] $StartKB = 'https://support.microsoft.com/app/content/api/content/feeds/sap/en-us/b905caa1-d413-c90c-bed3-20aead901092/atom'
                [String] $Build = "^(?!.*Preview)(?=.*Monthly).*"
            }
            "Windows7" {
                [String] $StartKB = 'https://support.microsoft.com/app/content/api/content/feeds/sap/en-us/f825ca23-c7d1-aab8-4513-64980e1c3007/atom'
                [String] $Build = "^(?!.*Preview)(?=.*Monthly).*"
            }
        }
        Write-Verbose -Message "Checking updates for $WindowsVersion $Build."
    }
    Process {
        #region Find the KB Article Number
        #! Fix for Invoke-WebRequest creating BOM in XML files; Handle Temp locations on Windows, macOS / Linux
        try {
            If (Test-Path env:Temp) {
                $tempDir = $env:Temp
            }
            ElseIf (Test-Path env:TMPDIR) {
                $tempDir = $env:TMPDIR
            }
            $tempFile = Join-Path -Path $tempDir -ChildPath ([System.IO.Path]::GetRandomFileName())
            Write-Verbose -Message "Downloading $StartKB to retrieve the list of updates."
            Invoke-WebRequest -Uri $StartKB -ContentType 'application/atom+xml; charset=utf-8' `
                -UseBasicParsing -OutFile $tempFile -ErrorAction SilentlyContinue
            Write-Verbose -Message "Read RSS feed into $tempFile."
        }
        catch {
            Throw $_
            Break
        }
        
        # Import the XML from the feed into a variable and delete the temp file
        try {
            $xml = [xml] (Get-Content -Path $tempFile -ErrorAction SilentlyContinue)
        }
        catch {
            Write-Error "Failed to read XML from $tempFile."
            Break
        }
        try {
            Remove-Item -Path $tempFile -ErrorAction SilentlyContinue
        }
        catch {
            Write-Warning -Message "Failed to remove file $tempFile."
        }
        #! End fix
        
        try {
            Switch ( $WindowsVersion ) {
                "Windows10" { 
                    # Sort feed for titles that match Build number; Find the largest minor build number
                    [regex] $rx = "$Build.(\d+)"
                    $buildMatches = $xml.feed.entry | Where-Object -Property title -match $Build
                    Write-Verbose -Message "Found $($buildMatches.Count) items matching build $Build."
                    $LatestVersion = $buildMatches | ForEach-Object { ($rx.match($_.title)).value.split('.') | Select-Object -Last 1} `
                        | ForEach-Object { [convert]::ToInt32($_, 10) } | Sort-Object -Descending | Select-Object -First 1

                    # Re-match feed for major.minor number and return the KB number from the Id field
                    Write-Verbose -Message "Latest Windows 10 build is: $Build.$LatestVersion."
                    $kbID = $xml.feed.entry | Where-Object -Property title -match "$Build.$LatestVersion" | Select-Object -ExpandProperty id `
                        | ForEach-Object { $_.split(':') | Select-Object -Last 1 }
                }
                default {
                    $buildMatches = $xml.feed.entry | Where-Object -Property title -match $Build
                    $kbID = $buildMatches | Select-Object -ExpandProperty ID | ForEach-Object { $_.split(':') | Select-Object -Last 1 } `
                        | Sort-Object -Descending | Select-Object -First 1   
                }
            }
        }
        catch {   
            If ($Null -eq $kbID) { Write-Warning -Message "kbID is Null. Unable to read from the KB from the JSON." }
            Break
        }
        #endregion

        #region get the download link from Windows Update
        try {
            Write-Verbose -Message "Found ID: KB$($kbID)"
            Write-Verbose -Message "Reading http://www.catalog.update.microsoft.com/Search.aspx?q=KB$($kbID)."
            $kbObj = Invoke-WebRequest -Uri "http://www.catalog.update.microsoft.com/Search.aspx?q=KB$($kbID)" `
                -UseBasicParsing -ErrorAction SilentlyContinue
        }
        catch {
            # Write warnings if we can't read values
            If ($Null -eq $kbObj) { Write-Warning -Message "kbObj is Null. Unable to read KB details from the Catalog." }
            If ($Null -eq $kbObj.InputFields) { Write-Warning -Message "kbObj.InputFields is Null. Unable to read button details from the Catalog KB page." }
            Throw $_
            Break
        }
        #endregion

        # Contruct a table with KB, Id and Update description
        Write-Verbose -Message "Contructing temporary table with KB, ID and URL."
        [regex] $rx = "<a[^>]*>([^<]+)<\/a>"
        $idTable = $kbObj.Links | Where-Object ID -match '_link' | `
            Select-Object @{n = "KB"; e = {"KB$kbID"}}, @{n = "Id"; e = {$_.id.Replace('_link', '')}}, `
        @{n = "Note"; e = {(($_.outerHTML -replace $rx, '$1').TrimStart()).TrimEnd()}}

        $output = @()
        ForEach ($idItem in $idTable) {
            try {
                Write-Verbose -Message "Checking Microsoft Update Catalog for Id: $($idItem.id)."
                $post = @{ size = 0; updateID = $idItem.id; uidInfo = $idItem.id } | ConvertTo-Json -Compress
                $postBody = @{ updateIDs = "[$post]" }
                $url = Invoke-WebRequest -Uri 'http://www.catalog.update.microsoft.com/DownloadDialog.aspx' `
                    -Method Post -Body $postBody -UseBasicParsing -ErrorAction SilentlyContinue |
                    Select-Object -ExpandProperty Content |
                    Select-String -AllMatches -Pattern "(http[s]?\://download\.windowsupdate\.com\/[^\'\""]*)" | 
                    ForEach-Object { $_.matches.value }
            }
            catch {
                Throw $_
                Write-Warning "Failed to parse Microsoft Update Catalog for Id: $($idItem.id)."
                Break
            }
            finally {
                Write-Verbose -Message "Adding $url to output."
                $newItem = New-Object PSObject
                $newItem | Add-Member -type NoteProperty -Name 'KB' -Value $idItem.KB
                [regex] $rx = "\s+([a-zA-Z0-9]+)-based"
                $idItem.Note -match $rx > $Null
                $newItem | Add-Member -type NoteProperty -Name 'Arch' -Value $Matches[1]
                $newItem | Add-Member -type NoteProperty -Name 'Note' -Value $idItem.Note
                $newItem | Add-Member -type NoteProperty -Name 'URL' -Value $url
                $output += $newItem
            }
        }
        #endregion
    }
    End {
        # Write the URLs list to the pipeline
        Write-Output $output
    }
}