Lister les bulletins de sécurité MS publiés entre deux dates / List MS security bulletins published during a date range

Go to English version

Lors de l’un de mes onsites suite à un IIS RaaS+, je me suis trouvé confronté à un serveur qui n’avait pas été patché depuis le 02 Avril 2012 (!). Je ne m’attarderai pas ici sur les risques encourus et la difficulté (pour ne pas dire impossibilité)  de la remédiation. Mais une question m’est venue à l’esprit : Combien de bulletins de sécurité Microsoft a publié depuis cette date ?

En cherchant un peu j’ai vu que la page https://technet.microsoft.com/en-us/security/bulletins me donnait les informations dont j’avais besoin. En poussant un peu plus loin l’analyse à l’aide d’une trace réseau j’ai vu que le résultat était transmis en JSON via une URI du type https://technet.microsoft.com/security/bulletin/services/GetBulletins?searchText=&sortField=0&sortOrder=1&currentPage=1&bulletinsPerPage=15&locale=en-us. Il ne restait plus qu’à manipuler les données JSON en PowerShell (nativement avec la cmdlet ConvertFrom-Json). Je vous propose donc ici la version de mon script.

 

Ce script est aussi disponible dans le TechNet Script Center : https://gallery.technet.microsoft.com/List-MS-security-bulletins-2b545eb2

Aller à la version française

During one of my onsites following an IIS RaaS+, I faced to a server that had not been patched since April 2, 2012 (!). I will not dwell here on the risks involved and the difficulty (or the impossibility) of the remediation. But one question came to my mind: How many security bulletins Microsoft has published since that date?

Looking for a bit, I saw that the https://technet.microsoft.com/en-us/security/bulletins page gave me the information I needed. By doing a deeper the analysis with a network trace I saw that the result was transmitted in JSON via a URI of the type https://technet.microsoft.com/security/bulletin/services/GetBulletins?searchText=&sortField=0&sortOrder=1&currentPage=1&bulletinsPerPage=15&locale=en-us. . All that was left to do was to manipulate the JSON data in PowerShell (natively with the ConvertFrom-Json cmdlet). So here I propose the version of my script.

 

The script file is also available at the TechNet Script Center repository, at: https://gallery.technet.microsoft.com/List-MS-security-bulletins-2b545eb2

 

 #requires -version 3
function Get-MSSecurityBulletin
{
    [CmdletBinding(PositionalBinding = $false)]
    Param(
        [parameter(Mandatory = $false)]
        [alias('From', 'Start')]
        [datetime]$StartDate,
        
        [parameter(Mandatory = $false)]
        [alias('To', 'End')]
        [datetime]$EndDate
        
        <#
                #Code replaced by a dynamic parameter
                [parameter(Mandatory=$false)]
                [ValidateScript({New-Object -TypeName[System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::AllCultures).Name -contains $_})]
                [String]$Locale
        #>
    )
    #Dynamic parameter to fill the list of known locales
    DynamicParam {
        # Create the dictionary 
        $RuntimeParameterDictionary  = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameterDictionary

        # Create the collection of attributes
        $AttributeCollection = New-Object -TypeName System.Collections.ObjectModel.Collection[System.Attribute]

        # Create and set the parameters' attributes
        $Attributes = New-Object -TypeName System.Management.Automation.ParameterAttribute
        $Attributes.Mandatory = $false
        $Attributes.ParameterSetName = 'Locale'
        
        # Add the attributes to the attributes collection
        $AttributeCollection.Add($Attributes)
        
        # Generate and set the ValidateSet 
        $ValidateSet = [System.Globalization.CultureInfo]::GetCultures([System.Globalization.CultureTypes]::AllCultures).Name | Where-Object -FilterScript {
            $_ -match '\w+-\w+'
        }
        $ValidateSetAttribute = New-Object -TypeName System.Management.Automation.ValidateSetAttribute -ArgumentList ($ValidateSet)
        
        # Add the ValidateSet to the attributes collection
        $AttributeCollection.Add($ValidateSetAttribute)
        
        # Create and return the dynamic parameter
        $Locale = New-Object -TypeName System.Management.Automation.RuntimeDefinedParameter -ArgumentList ('Locale', [string], $AttributeCollection)        
        $RuntimeParameterDictionary.Add('Locale', $Locale)
        return $RuntimeParameterDictionary 
    }
    
    begin
    {
        #Getting the dynamic paramater 
        $Locale = $PSBoundParameters.Locale
    }
    process
    {
        #If the results have more than one result page
        $HasMore = $True
        #Offset for paging
        $Offset = 0
        #Current result page
        $CurrentPage = 1
        #Bulletin number per page
        $BulletinsPerPage = 1000
        #Array storing the hotfix list
        $Bulletins = @()
        #If the locale parameter has nt been specified we will use the "en-Us" locale
        if (!$Locale)
        {
            $Locale = 'en-us'
        }
        Write-Verbose -Message "`$Locale : $($Locale)"
        $CultureInfo = New-Object -TypeName System.Globalization.CultureInfo -ArgumentList ($Locale)
        Write-Verbose -Message "`$CultureInfo : $CultureInfo"
        
        While ($HasMore)
        {
            #Getting the URI 
            $CurrentBulletin = Invoke-WebRequest -Uri "https://technet.microsoft.com/security/bulletin/services/GetBulletins?searchText=&sortField=0&sortOrder=1&currentPage=$CurrentPage&bulletinsPerPage=$BulletinsPerPage&locale=$Locale" -UseBasicParsing
            #UTF-8 conversion
            $CurrentBulletin = [system.Text.Encoding]::UTF8.GetString($CurrentBulletin.RawContentStream.ToArray())
            #JSON -> Object conversion
            $CurrentBulletin = $CurrentBulletin | ConvertFrom-Json 
            #Getting only bulletin data
            $Bulletins += $CurrentBulletin.b
            $CurrentPage++
            $Offset += $CurrentBulletin.b.Count
            $HasMore = $Offset -lt $CurrentBulletin.l
            Write-Verbose -Message "`$CurrentPage : $CurrentPage"
            Write-Verbose -Message "`$Offset : $Offset"
            Write-Verbose -Message "`$HasMore : $HasMore"
        }
        #if a start date has been specified we need to filter
        if ($StartDate)
        {
            Write-Verbose -Message "`$StartDate : $StartDate"
            $Bulletins = $Bulletins | Where-Object -FilterScript {
                ([datetime]::ParseExact($_.d,$CultureInfo.DateTimeFormat.ShortDatePattern,[System.Globalization.CultureInfo]::InvariantCulture) -ge $StartDate)
            }
        }
        else
        {
            #Just to know the date of the first hotfix
            Write-Verbose -Message "Starting from $(($Bulletins | Select-Object -Property @{
                Name       = 'Date'
                Expression = { [datetime]::ParseExact($_.d,$CultureInfo.DateTimeFormat.ShortDatePattern,[System.Globalization.CultureInfo]::InvariantCulture) }
            } -Unique | Sort-Object -Property Date | Select-Object -First 1).Date)"
        }
        #if an end date has been specified we need to filter
        if ($EndDate)
        {
            Write-Verbose -Message "`$EndDate : $EndDate"
            $Bulletins = $Bulletins | Where-Object -FilterScript {
                ([datetime]::ParseExact($_.d,$CultureInfo.DateTimeFormat.ShortDatePattern,[System.Globalization.CultureInfo]::InvariantCulture) -le $EndDate)
            }
        }
        else
        {
            #Just to know the date of the last hotfix
            Write-Verbose -Message "Ending to $(($Bulletins | Select-Object -Property @{
                Name       = 'Date'
                Expression = { [datetime]::ParseExact($_.d,$CultureInfo.DateTimeFormat.ShortDatePattern,[System.Globalization.CultureInfo]::InvariantCulture) }
            } -Unique | Sort-Object -Property Date -Descending | Select-Object -First 1).Date)"
        }
        return $Bulletins
    }
    end
    {
    }
}

Clear-Host
# Getting the this script path
$CurrentScript = $MyInvocation.MyCommand.Path
# Getting the directory of this script
$CurrentDir = Split-Path -Path $CurrentScript -Parent

# Building a CSV file with the same base name that with script
$CSVFile = $CurrentScript.replace((Get-Item -Path $CurrentScript).Extension, '.csv')

#$Bulletins = Get-MSSecurityBulletin -StartDate '01/01/2008' -EndDate '12/31/2017' -Verbose 
#$Bulletins = Get-MSSecurityBulletin -StartDate '04/02/2012' -Locale fr-fr -Verbose 
#$Bulletins = Get-MSSecurityBulletin -StartDate '04/02/2012' -Verbose 
#The From/Start/StartDate and To/End/Endate are expressed by using your regional settings
#In this example I'm configured in en-US but I want to get the results by using the fr-FR locale
$Bulletins = Get-MSSecurityBulletin -From '04/02/2012' -Locale fr-FR -Verbose 
#Displaying the number of hotfixes
Write-Host -Object "Security bulletins Count : $($Bulletins.Count)"
#Exporting to a CSV file
Write-Host -Object "The security bulletins have been exported to $CSVFile"
$Bulletins | Export-Csv -Path $CSVFile -Force -NoTypeInformation -Encoding UTF8
#Security bulletins By Rating
Write-Host -Object 'Security bulletins By Rating : '
$Bulletins | Group-Object -Property Rating -NoElement | Format-Table

Laurent.