Use powershell script to download windows patches monthly

My company concerns security, request us to deploy the newest patches on our servers in time, even we have firewall/encryption internally.

Welcome email to: [email protected]

With the number of servers increasing, there must be some servers can't be patched as expected, probably caused by SCCM/WSUS or incorrect configuration on server, plus 3rd party patch scanning software and presures from security team, support team like me have to patch missing patches one by one. This makes me have to paste the MS number in google, and find the correct KB, then download and install, turns out it's a mess.

To make life easier, I did the script to automatic download patches from MS with scheduled time, the script will grab contents from MS RSS and get MS numbers and links, it will loop MS links and grabs KBs, and download patches to local path.

Security RSS: https://technet.microsoft.com/en-us/security/rss/bulletin

Workflow: Get contents of RSS -> grab MS numbers and links -> Enum MS links and grab KBs -> filter out some KB -> grab KB details link -> grab KB download links -> Download

Step by step to analysis the script,

 

$Url = 'https://technet.microsoft.com/en-us/security/rss/bulletin'
$ExcludeProducts = 'lync|Itanium|for mac'
$IncludeProducts = 'server'

$ExcludePatches = '-IA64|Windows6\.0|-RT-|ServiceBusServer'

$PatchStoreTo = '.\'

some variables defined,

$Url is the link of RSS;

$ExcludeProducts when get the contents of MS link, use regular expression to filter out unwanted product, for me is lync and patches for Itanium cpu;

$IncludeProducts after product filter, i want to filter again for KBs for "server";

$ExcludePatches is another filter, when final get patch link, I don't want patches for Itanium (Why? because some KB doesn't have enough details, so this filter added);

$PatchStoreTo is a path to store patches.

 

$WebClient = New-Object System.Net.WebClient
$WebClient.Encoding = [System.Text.Encoding]::UTF8

Create the webclient object and set the encoding

 

do
{
    $RSSContent = $WebClient.DownloadString($Url)
}
while(
    $(if(!$?)
    {
        Write-Host 'Failed to get RSS' -ForegroundColor Red
        Start-Sleep -Seconds 600
        $true
    })
)

Get the contents of RSS, if failed, will report with red words and sleep 10 minutes to do again.

 

([xml]$RSSContent).rss.channel.Item | Sort-Object link | %{...}

Convert RSS contents to XML type, then can easily retrieve data.

 

    $MSRC_URL = $_.link
    Write-Host "Processing: [$MSRC_URL]" -ForegroundColor Yellow
    $MSRC = ([regex]::Match($MSRC_URL, '(?i)MS\d+-\d+$')).Value
    Write-Host "MS number: [$MSRC]" -ForegroundColor Green
    if(!(Test-Path -LiteralPath "$PatchStoreTo\$MSRC"))
    {
        do
        {
            New-Item -Path "$PatchStoreTo\$MSRC" -ItemType Directory | Out-Null
        }
        while(
            $(if(!$?)
            {
                Write-Host 'Failed to create MSRC folder' -ForegroundColor Red
                Start-Sleep 300
                $true
            })
        )
    }

MS link stores in $MSRC_URL, and output to screen as color yellow, then use regular expression to grab MS number and stored in $MSRC, after that create a folder named as MS number to store patches.

 

    Write-Host "Trying to capture KBs from MSRC URL" -ForegroundColor Yellow
    do
    {
        $MSContent = $null
        $MSContent = $WebClient.DownloadString($MSRC_URL)
    }
    while(
        $(if(!$?)
        {
            Write-Host 'Failed to capture MSRC content' -ForegroundColor Red
            Start-Sleep 300
            $true
        })
    )

Above codes is to grab MS link contents and store in $MSContent.

MS link is like https://technet.microsoft.com/en-us/library/security/MS14-063, MS contents are the source codes behind the web page.

 

[regex]::Matches($MSContent, '(?i)<tr>[\s\S]+?<a href="(http://www.microsoft.com/downloads/details.aspx\?FamilyID=[\w\-]+?)">[\s\S]+?\((\d{7})\)') | %{...}

The code is to grab KB information, like KB number, and KB link.

It will match contents like below screenshot, all characters in the grah will be matched by the regular expression pattern.

 

        Write-Host "KB: [$($_.Groups[2].Value)]" -NoNewline -ForegroundColor Green
        if($_.Value -imatch $ExcludeProducts)
        {
            Write-Host "   --- Excluded: [$($Matches[0])]" -ForegroundColor Red
        }
        else
        {
            if($_.Value -notmatch $IncludeProducts)
            {
                Write-Host "   --- Excluded: Not match [$IncludeProducts]" -ForegroundColor Red
                return
            }
            $KBNumber = "KB$($_.Groups[2].Value)"
       Write-Host "`nDownload URL: [$($_.Groups[1].Value)]" -ForegroundColor Gray

Above code excludes the KBs matched product names in $excludeProducts, and left KB filtered again by $IncludeProducts, final passed KBs store in $KBNumber.

 

            do
            {
                $KBContent = $null
                $KBContent = $WebClient.DownloadString($_.Groups[1].Value)
            }while(
                $(if(!$?)
                {
                    Write-Host 'Failed to capture KB content' -ForegroundColor Red
                    Start-Sleep 300
                    $true
                })
            )

Above code get contents from KB link and stores in $KBContent,

KB link looks like this in $MSContent: http://www.microsoft.com/downloads/details.aspx?familyid=8a59fc6d-cbad-4905-842b-e5aa1fc6fedf

Access KB link will automatic redirect to:http://www.microsoft.com/en-us/download/details.aspx?id=44400

Surely this is a automation behavior of web server, I don't need to do anything in the script. Anyway the KB link page doesn't contain patch link, it just ask for confimation on languages and provide us some KB details, you can find screenshot followed.

As followed, I need to analysis KB contents, and find the page called "confirmation.aspx". Actually I can see it when I move my cursor on the "Download" button.

 

            $KBConfirm = ([regex]::Match($KBContent, '(?i)href="(confirmation.aspx\?id=\d+)"')).Groups[1].Value
            $KBConfirm = "http://www.microsoft.com/en-us/download/$KBConfirm"
            Write-Host "KB confirm URL: [$KBConfirm]" -ForegroundColor Gray
            do
            {
                $KBContent = $null
                $KBContent = $WebClient.DownloadString($KBConfirm)
            }while(
                $(if(!$?)
                {
                    Write-Host 'Failed to capture KB download content' -ForegroundColor Red
                    Start-Sleep 300
                    $true
                })
            )

Codes used to grab "confirmation.aspx" page link from KB contents, you may find the "id" behind "confirmation.aspx" is the same like "details.aspx" of KB link, but just for safey, I choose to grab "confirmation.aspx" page from KB content.

 

            $KBLinks = @()
            $KBLinks = [regex]::Matches($KBContent, '(?i)<a href="(http://download.microsoft.com/download/.+?)".+?>Click here</span>') | %{
                $_.Groups[1].Value
            }
            $KBLinks = @($KBLinks | Sort-Object -Unique)
            Write-Host "The KB contains updates: [$($KBLinks.Count)]" -ForegroundColor Green

After I get the contents of "confirmation.aspx", I use regular expression to match patch links and do a unique sort for final results, now $KBLinks contains all patches belong to that KB.

Followed screenshot is the "confirmation.aspx", it contains all patches download link, I used regular expression again to grab those links.

 

            $KBLinks | %{
                $FileName = $null
                $FileName = $_.Split('/')[-1]
                if($FileName -imatch $ExcludePatches)
                {
                    Write-Host "Patch excluded: [$($Matches[0])]" -ForegroundColor Red
                    return
                }

Now I have patch links in hand, the job left is download, but I do another filter before the downloading, as i mentioned previously, sometimes KB contents don't have enough information, so in here I use another filter to remove patches i don't want by patch names.

 

                if(Test-Path -Path $FilePath)
                {
                    Write-Host 'File already exists, skip!' -ForegroundColor Gray
                }
                else
                {
                    do
                    {
                        $WebClient.DownloadFile($_, $FilePath)
                    }while(
                        $(if(!$?)
                        {
                            Write-Host 'Download file failed!' -ForegroundColor Red
                            Start-Sleep -Seconds 300
                            $true
                        })
                    )
                }

Real download codes here, if patch already exists, script will skip it.

 

Last, one screenshot when script running, and full script followed.

Full script here,

$Url = 'https://technet.microsoft.com/en-us/security/rss/bulletin'
$ExcludeProducts = 'lync|Itanium|for mac'
$IncludeProducts = 'server'

$ExcludePatches = '-IA64|Windows6\.0|-RT-|ServiceBusServer'

$PatchStoreTo = '.\'

$WebClient = New-Object System.Net.WebClient
$WebClient.Encoding = [System.Text.Encoding]::UTF8

do
{
    $RSSContent = $WebClient.DownloadString($Url)
}
while(
    $(if(!$?)
    {
        Write-Host 'Failed to get RSS' -ForegroundColor Red
        Start-Sleep -Seconds 600
        $true
    })
)

([xml]$RSSContent).rss.channel.Item | Sort-Object link | %{
    $MSRC_URL = $_.link
    Write-Host "Processing: [$MSRC_URL]" -ForegroundColor Yellow
    $MSRC = ([regex]::Match($MSRC_URL, '(?i)MS\d+-\d+$')).Value
    Write-Host "MS number: [$MSRC]" -ForegroundColor Green
    if(!(Test-Path -LiteralPath "$PatchStoreTo\$MSRC"))
    {
        do
        {
            New-Item -Path "$PatchStoreTo\$MSRC" -ItemType Directory | Out-Null
        }
        while(
            $(if(!$?)
            {
                Write-Host 'Failed to create MSRC folder' -ForegroundColor Red
                Start-Sleep 300
                $true
            })
        )
    }
    Write-Host "Trying to capture KBs from MSRC URL" -ForegroundColor Yellow
    do
    {
        $MSContent = $null
        $MSContent = $WebClient.DownloadString($MSRC_URL)
    }
    while(
        $(if(!$?)
        {
            Write-Host 'Failed to capture MSRC content' -ForegroundColor Red
            Start-Sleep 300
            $true
        })
    )
    
    [regex]::Matches($MSContent, '(?i)<tr>[\s\S]+?<a href="(https?://www.microsoft.com/downloads/details.aspx\?FamilyID=[\w\-]+?)">[\s\S]*?(\d{7})') | %{
        Write-Host "KB: [$($_.Groups[2].Value)]" -NoNewline -ForegroundColor Green
        if($_.Value -imatch $ExcludeProducts)
        {
            Write-Host "   --- Excluded: [$($Matches[0])]" -ForegroundColor Red
        }
        else
        {
            if($_.Value -notmatch $IncludeProducts)
            {
                Write-Host "   --- Excluded: Not match [$IncludeProducts]" -ForegroundColor Red
                return
            }
            $KBNumber = "KB$($_.Groups[2].Value)"
            Write-Host "`nDownload URL: [$($_.Groups[1].Value)]" -ForegroundColor Gray
<#
            if(!(Test-Path -Path "$MSRC\$KBNumber"))
            {
                do
                {
                    New-Item -Name "$MSRC\$KBNumber" -ItemType Directory | Out-Null
                }
                while(
                    $(if(!$?)
                    {
                        Write-Host 'Failed to create KB folder' -ForegroundColor Red
                        Start-Sleep 300
                        $true
                    })
                )
            }
#>
            do
            {
                $KBContent = $null
                $KBContent = $WebClient.DownloadString($_.Groups[1].Value)
            }while(
                $(if(!$?)
                {
                    Write-Host 'Failed to capture KB content' -ForegroundColor Red
                    Start-Sleep 300
                    $true
                })
            )

            $KBConfirm = ([regex]::Match($KBContent, '(?i)href="(confirmation.aspx\?id=\d+)"')).Groups[1].Value
            $KBConfirm = "http://www.microsoft.com/en-us/download/$KBConfirm"
            Write-Host "KB confirm URL: [$KBConfirm]" -ForegroundColor Gray
            do
            {
                $KBContent = $null
                $KBContent = $WebClient.DownloadString($KBConfirm)
            }while(
                $(if(!$?)
                {
                    Write-Host 'Failed to capture KB download content' -ForegroundColor Red
                    Start-Sleep 300
                    $true
                })
            )

            $KBLinks = @()
            $KBLinks = [regex]::Matches($KBContent, '(?i)<a href="(http://download.microsoft.com/download/.+?)".+?>Click here</span>') | %{
                $_.Groups[1].Value
            }
            $KBLinks = @($KBLinks | Sort-Object -Unique)
            Write-Host "The KB contains updates: [$($KBLinks.Count)]" -ForegroundColor Green
            $KBLinks | %{
                $FileName = $null
                $FileName = $_.Split('/')[-1]
                if($FileName -imatch $ExcludePatches)
                {
                    Write-Host "Patch excluded: [$($Matches[0])]" -ForegroundColor Red
                    return
                }
                $FilePath = $null
                $FilePath = "$MSRC\$FileName"
                Write-Host "Going to download file: [$FilePath]" -ForegroundColor Gray
                $FilePath = "$PatchStoreTo\$FilePath"
                if(Test-Path -Path $FilePath)
                {
                    Write-Host 'File already exists, skip!' -ForegroundColor Gray
                }
                else
                {
                    do
                    {
                        $WebClient.DownloadFile($_, $FilePath)
                    }while(
                        $(if(!$?)
                        {
                            Write-Host 'Download file failed!' -ForegroundColor Red
                            Start-Sleep -Seconds 300
                            $true
                        })
                    )
                }
            }
        }
    }
}

 

PS, about the proxy, WebClient class will use proxy settings on IE automatically, if want download patches via proxy, set IE to the right settings.

2015-03-02:Updated the regular expression for KB number capture, support for https link and fix capture for enclose.

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章