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-us/help/4043454

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

    .PARAMETER SearchString
        Dynamic parameter. Specify a specific search string to change the target update behaviour. The default will only download Cumulative updates for x64.

    .EXAMPLE
        Get-LatestUpdate

        Description:
        Get the latest Cumulative Update for Windows 10 x64 (Semi-Annual Channel)

    .EXAMPLE
        Get-LatestUpdate -WindowsVersion Windows10 -Architecture x86

        Description:
        Enumerate the latest Cumulative Update for Windows 10 x86 (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 -Architecture x86
    
        Description:
        Enumerate the latest Cumulative Update for Windows 10 x86 1703

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

    .EXAMPLE
        Get-LatestUpdate -WindowsVersion Windows8 -Architecture x86
    
        Description:
        Enumerate the latest Monthly Update for Windows 8.1 x86

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

    [CmdletBinding(SupportsShouldProcess = $False)]
    Param(
        [Parameter(Mandatory = $False, Position = 0, HelpMessage = "Select the OS to search for updates")]
        [ValidateSet('Windows10', 'Windows8', 'Windows7')]
        [String] $WindowsVersion = "Windows10"
    )
    DynamicParam {
        # Create dynamic parameters. Windows 10 can use -Build and -Architecture
        # Windows 8/7 use -Architecture only
        $Dictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
        If ( $WindowsVersion -eq "Windows10") {
            $args = @{
                Name         = "Build"
                Type         = [String]
                ValidateSet  = @('17134', '16299', '15063', '14393', '10586', '10240')
                Position     = 1
                HelpMessage  = "Provide a Windows 10 build number"
                DPDictionary = $Dictionary
            }
            New-DynamicParam @args
            $args = @{
                Name         = "Architecture"
                Type         = [String]
                ValidateSet  = @('x64', 'x86')
                Position     = 2
                HelpMessage  = "Processor architecture to return updates for."
                DPDictionary = $Dictionary
            }
            New-DynamicParam @args
        }
        If ( ($WindowsVersion -eq "Windows8") -or ($WindowsVersion -eq "Windows7") ) {
            $args = @{
                Name         = "Architecture"
                Type         = [String]
                ValidateSet  = @('x64', 'x86')
                Position     = 1
                HelpMessage  = "Processor architecture to return updates for."
                DPDictionary = $Dictionary
            }
            New-DynamicParam @args
        }
        #return RuntimeDefinedParameterDictionary
        Write-Output $Dictionary
    }
    Begin {
        # Get the dynamic parameters and assign to parameters
        Function _temp { [cmdletbinding()] param() }
        $BoundKeys = $PSBoundParameters.keys | Where-Object { (Get-Command _temp | Select-Object -ExpandProperty parameters).Keys -notcontains $_}
        ForEach ($param in $BoundKeys) {
            If (-not ( Get-Variable -name $param -scope 0 -ErrorAction SilentlyContinue ) ) {
                New-Variable -Name $Param -Value $PSBoundParameters.$param
                Write-Verbose "Adding variable for dynamic parameter '$param' with value '$($PSBoundParameters.$param)'"
            }
        }
        # Set values for -Build and -SearchString as required for each platform
        Switch ( $WindowsVersion ) {
            "Windows10" {
                [String] $StartKB = 'https://support.microsoft.com/app/content/api/content/asset/en-us/4000816'
                If ( $Null -eq $Build ) { [String] $Build = "17134" }
                [String] $SearchString = Switch ( $Architecture ) {
                    "x64" { 'Cumulative.*x64' }
                    "x86" { 'Cumulative.*x86' }
                    Default { 'Cumulative.*x64' }
                }
            }
            "Windows8" {
                [String] $StartKB = 'https://support.microsoft.com/app/content/api/content/asset/en-us/4010477'
                [String] $Build = "^(?!.*Preview)(?=.*Monthly).*"
                [String] $SearchString = Switch ( $Architecture ) {
                    "x64" { ".*x64" }
                    "x86" { ".*x86" }
                    Default { ".*x64" }
                }
            }
            "Windows7" {
                [String] $StartKB = 'https://support.microsoft.com/app/content/api/content/asset/en-us/4009472'
                [String] $Build = "^(?!.*Preview)(?=.*Monthly).*"
                [String] $SearchString = Switch ( $Architecture ) {
                    "x64" { ".*x64" }
                    "x86" { ".*x86" }
                    Default { ".*x64" }
                }
            }
        }
        Write-Verbose "Check updates for $Build $SearchString"
    }
    Process {
        #region Find the KB Article Number
        Write-Verbose "Downloading $StartKB to retrieve the list of updates."
        $kbID = (Invoke-WebRequest -Uri $StartKB).Content |
            ConvertFrom-Json |
            Select-Object -ExpandProperty Links |
            Where-Object level -eq 2 |
            Where-Object text -match $Build |
            # Select-LatestUpdate |
        Select-Object -First 1
        If ( $Null -eq $kbID ) { Write-Warning -Message "kbID is Null. Unable to read from the KB from the JSON." }
        #endregion

        #region get the download link from Windows Update
        $kb = $kbID.articleID
        Write-Verbose "Found ID: KB$($kbID.articleID)"
        $kbObj = Invoke-WebRequest -Uri "http://www.catalog.update.microsoft.com/Search.aspx?q=KB$($kbID.articleID)"

        # 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." }
        #endregion

        #region Parse the available KB IDs
        $availableKbIDs = $kbObj.InputFields | 
            Where-Object { $_.Type -eq 'Button' -and $_.Value -eq 'Download' } | 
            Select-Object -ExpandProperty ID
        Write-Verbose "Ids found:"
        ForEach ( $id in $availableKbIDs ) {
            "`t$($id | Out-String)" | Write-Verbose
        }
        #endregion

        #region Invoke-WebRequest on PowerShell Core doesn't return innerText
        # (Same as Invoke-WebRequest -UseBasicParsing on Windows PS)
        If ( Test-PSCore ) {
            Write-Verbose "Using outerHTML. Parsing KB notes"
            $kbIDs = $kbObj.Links | 
                Where-Object ID -match '_link' |
                Where-Object outerHTML -match $SearchString |
                ForEach-Object { $_.Id.Replace('_link', '') } |
                Where-Object { $_ -in $availableKbIDs }
        }
        Else {
            Write-Verbose "innerText found. Parsing KB notes"
            $kbIDs = $kbObj.Links | 
                Where-Object ID -match '_link' |
                Where-Object innerText -match $SearchString |
                ForEach-Object { $_.Id.Replace('_link', '') } |
                Where-Object { $_ -in $availableKbIDs }
        }
        #endregion

        #region Read KB details
        $urls = @()
        ForEach ( $kbID in $kbIDs ) {
            Write-Verbose "Download $kbID"
            $post = @{ size = 0; updateID = $kbID; uidInfo = $kbID } | ConvertTo-Json -Compress
            $postBody = @{ updateIDs = "[$post]" } 
            $urls += Invoke-WebRequest -Uri 'http://www.catalog.update.microsoft.com/DownloadDialog.aspx' -Method Post -Body $postBody |
                Select-Object -ExpandProperty Content |
                Select-String -AllMatches -Pattern "(http[s]?\://download\.windowsupdate\.com\/[^\'\""]*)" | 
                ForEach-Object { $_.matches.value }
        }
        #endregion

        #region Select the update names
        If ( Test-PSCore ) {
            # Updated for PowerShell Core
            $notes = ([regex]'(?<note>\d{4}-\d{2}.*\(KB\d{7}\))').match($kbObj.RawContent).Value
        }
        Else {
            # Original code for Windows PowerShell
            $notes = $kbObj.ParsedHtml.body.getElementsByTagName('a') | ForEach-Object InnerText | Where-Object { $_ -match $SearchString }
        }
        #endregion

        #region Build the output array
        [int] $i = 0; $output = @()
        ForEach ( $url in $urls ) {
            $item = New-Object PSObject
            $item | Add-Member -type NoteProperty -Name 'KB' -Value "KB$Kb"
            If ( $notes.Count -eq 1 ) {
                $item | Add-Member -type NoteProperty -Name 'Note' -Value $notes
            }
            Else {
                $item | Add-Member -type NoteProperty -Name 'Note' -Value $notes[$i]
            }
            $item | Add-Member -type NoteProperty -Name 'URL' -Value $url
            $output += $item
            $i = $i + 1
        }
        #endregion
    }
    End {
        # Write the URLs list to the pipeline
        Write-Output $output
    }
}