利用powershell script每个月定期从microsoft download网站上抓补丁

This artical will be published in English also: http://www.cnblogs.com/LarryAtCNBlog/p/4026695.html

本人所在的公司对于安全性要求较高,除了平时各种内网加密外网firewall之外,对于server所使用的OS也要求更新到最新的security级别的补丁。

但是样本数量一多就总有些是打不上补丁的,这可能由于各种各样如update配置错误,SCCM/WSUS抽风,加上第3方扫描补丁软件的2X机制和security team的压力,不得不把缺失的补丁一个个打上。这样的话就导致了经常性的要把KB或MS号贴在google里,然后找到链接,再找补丁下载。机器一多就乱的要死,重复下载就是经常发生的事~

于是为了让生活轻松一些就做了这样一个script,从security RSS里抓出MS号,然后从MS的链接里抓出KB号,再抓出补丁下载链接把其下载到本地。所以只需要每月schedule一次或多次运行就可以把所有补丁放在一个固定的共享中了。microsoft每个月第二个星期二release当月的补丁(米国的周二,亚太大概就是周三)。

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

流程:读取RSS内容 -> 脚本抓出MS号和链接 -> 循环读MS号链接内容,取出所有KB -> 使用一些条件filter掉不要的KB,比如我只管server,我就不想下载非server的补丁 -> 脚本抓出KB的下载链接 -> 再从KB下载链接内容中抓取具体的下载路径 -> 下载补丁到本地

下面一步步分解该脚本

 

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

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

$PatchStoreTo = '.\'

在上面的几行中,定义了几个变量,

$Url 当然就是RSS的链接;

$ExcludeProducts 就是抓出MS号网页内容之后根据提供的正则表达式过滤掉不想要的product,比如lync,安腾cpu相关的补丁;

$IncludeProducts 就是经过上面exclude过滤后留下来的KB再过滤一次,而这次就是滤出包涵server信息的KB;

$ExcludePatches 是另一个过滤,在取到具体的补丁下载链接后对补丁的名字进行匹配,过滤掉安腾补丁之类的安装包(因为有些KB信息里没有直接写明安腾cpu,所以加了该过滤从文件名来判断);

$PatchStoreTo 就是把补丁存在哪个地方,当然,要有写权限才行。

 

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

以上建立Webclient类并指定编码

 

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

上面就是从RSS链接中取到RSS的内容,如果不成功的话就等10分钟再试一次。

 

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

把RSS内容转换为xml对象,然后就可以方便的从xml中读节点数据了

 

    $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号的链接放在了$MSRC_URL变量中,然后用黄字输出到屏幕上,利用正则匹配到MS号存在$MSRC中,同样输出到屏幕。后面write-host这种输出信息的就不描述了。随后是创建以MS号为名称的文件夹,方便之后用来存放补丁文件。

 

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

上面的代码则是抓取MS号的链接内容存在$MSContent中。

MS链接例如 https://technet.microsoft.com/en-us/library/security/MS14-063,MSContent就是该网页的后台代码。

 

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

上面就是从MS的网页内容中抓到具体的KB信息,如KB的链接,KB号。

它匹配到的内容如下,图片中所有的内容都会被匹配出来。

 

        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

上面的内容以KB的内容排除了$excludeProducts中描述的产品名称,然后通过的KB又会经过$IncludeProducts的过滤,最终都通过的话,KB号存放于$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
                })
            )

以上代码则是从已抓出的KB链接中取KB的网页内容放于$KBContent中。

KB链接在MSContent中长这样:http://www.microsoft.com/downloads/details.aspx?familyid=8a59fc6d-cbad-4905-842b-e5aa1fc6fedf

但是访问它后会跳转成:http://www.microsoft.com/en-us/download/details.aspx?id=44400

当然,这是Web server自动的,不需要我们手动在代码中做什么,该网页并不包涵补丁下载信息,它只是让我们确认一下语言,还有告诉我们KB具体信息而已,截图如下。

因此,我们还要接着抓该网页后台的字符串信息,找到下载链接confirmation.aspx,当把鼠标放于"Download"按钮上时,可以在状态栏看到confirmation.aspx的链接。

 

            $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
                })
            )

以上是从$KBContent中抓到confirmation.aspx链接,并会抓出该confirmation.aspx中的内容,其实confirmation.aspx后面跟的id好像和KB details.aspx后面的id是一样的,但是为了保险一点,我还是选择了从网页内容中抓confirmation.aspx的信息。

 

            $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

在抓到KB的confirmation.aspx内容之后,然后从内容中用正则匹配具体的下载链接,最后做一个排序并去除重复的条目,完成后$KBLinks中就包涵了该KB中所有补丁的下载链接。

当在浏览器中打开confirmation.aspx后其实会自动弹出下载,但这是游览器的行为,代码是不会自动下载的,但是我们可以看到一个"click here"就是下载链接了。

要做的也就是分析它后面的网页代码了,依然还是用正则。

 

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

既然有了补丁的具体下载链接,剩下的就是下载了,但在下载之前又对补丁的名称做了一次过滤,之前也提到了KB的信息有时候是不完整的,因此要做这里的过滤。

 

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

上面就是下载补丁的代码了,当然,如果补丁已经存在,脚本不会重复下载。

 

以上就是脚本的分析和介绍了,最后贴张运行图还有完整的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 = '.\'

$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
                        })
                    )
                }
            }
        }
    }
}

 

附,关于proxy,WebClient类会自动使用IE里所设置的proxy,所以如果要用proxy的话,把IE设置好就行了。

2015-03-02:抓取KB号的正则表达式更新,因为加入了原本未考虑到的https链接及后台html括号位置的改变。

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