private/Update-KbDatabase.ps1
function Update-KbDatabase { [CmdletBinding()] param( [switch]$EnableException ) begin { if ($EnableException) { $ee = $true } else { $ee = $false } $PSDefaultParameterValues["*:EnableException"] = $ee $PSDefaultParameterValues["Invoke-WebRequest:SkipHeaderValidation"] = $true $PSDefaultParameterValues["*:Confirm"] = $false # Bunch of functions are needed to help parallelization function Update-KbDb { [CmdletBinding()] param( $recent ) begin { function Get-Info ($Text, $Pattern) { if ($Pattern -match "labelTitle") { if ($Pattern -match "SupportedProducts") { # no idea what the regex below does but it's not working for SupportedProducts # do it the manual way instead $block = [regex]::Match($Text, $Pattern + '[\s\S]*?\s*(.*?)\s*<\/div>').Groups[0].Value $supported = $block -split "</span>" | Select-Object -Last 1 $supported.Trim().Replace("</div>","").Split(",").Trim() } else { # this should work... not accounting for multiple divs however? [regex]::Match($Text, $Pattern + '[\s\S]*?\s*(.*?)\s*<\/div>').Groups[1].Value } } elseif ($Pattern -match "span ") { [regex]::Match($Text, $Pattern + '(.*?)<\/span>').Groups[1].Value } else { [regex]::Match($Text, $Pattern + "\s?'?(.*?)'?;").Groups[1].Value } } function Get-SuperInfo ($Text, $Pattern) { # this works, but may also summon cthulhu $span = [regex]::match($Text, $pattern + '[\s\S]*?<div id') switch -Wildcard ($span.Value) { "*div style*" { $regex = '">\s*(.*?)\s*<\/div>' } "*a href*" { $regex = "<div[\s\S]*?'>(.*?)<\/a" } default { $regex = '"\s?>\s*(\S+?)\s*<\/div>' } } $spanMatches = [regex]::Matches($span, $regex).ForEach( { $_.Groups[1].Value }) if ($spanMatches -eq 'n/a') { $spanMatches = $null } if ($spanMatches) { foreach ($superMatch in $spanMatches) { $detailedMatches = [regex]::Matches($superMatch, '\b[kK][bB]([0-9]{6,})\b') # $null -ne $detailedMatches can throw cant index null errors, get more detailed if ($null -ne $detailedMatches.Groups) { [PSCustomObject] @{ 'KB' = $detailedMatches.Groups[1].Value 'Description' = $superMatch } | Add-Member -MemberType ScriptMethod -Name ToString -Value { $this.Description } -PassThru -Force } } } } function ConvertTo-DataTable { [OutputType([Data.DataTable])] param( # The input objects [Parameter(Mandatory, ValueFromPipeline)] [PSObject[]]$InputObject ) begin { $outputDataTable = New-Object System.Data.DataTable } process { foreach ($object in $InputObject) { $DataRow = $outputDataTable.NewRow() foreach ($property in $object.PsObject.properties) { $propName = $property.Name $propValue = $property.Value if (-not $outputDataTable.Columns.Contains($propName)) { $outputDataTable.Columns.Add(( New-Object System.Data.DataColumn -Property @{ ColumnName = $propName DataType = 'System.Object' } )) } $DataRow.Item($propName) = if ($propValue) { [PSObject]$propValue } else { [DBNull]::Value } } $outputDataTable.Rows.Add($DataRow) } } end { ,$outputDataTable } } function Get-KbItemFromWeb ($kb) { try { # long story $guids = @() $guids += [PSCustomObject]@{ Guid = $kb Title = $kb } $sb = { $post = @{ size = 0 updateID = $psitem.Guid uidInfo = $psitem.Guid } | ConvertTo-Json -Compress $parms = @{ Uri = 'https://www.catalog.update.microsoft.com/DownloadDialog.aspx' Method = "POST" Body = @{ updateIDs = "[$post]" } } Invoke-TlsWebRequest @parms | Select-Object -ExpandProperty Content } $downloaddialogs = $guids | ForEach-Object -Process $sb foreach ($downloaddialog in $downloaddialogs) { $title = Get-Info -Text $downloaddialog -Pattern 'enTitle =' $arch = Get-Info -Text $downloaddialog -Pattern 'architectures =' $longlang = Get-Info -Text $downloaddialog -Pattern 'longLanguages =' if ($Pattern -match '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$') { $updateid = "$Pattern" } else { $updateid = Get-Info -Text $downloaddialog -Pattern 'updateID =' } $ishotfix = Get-Info -Text $downloaddialog -Pattern 'isHotFix =' if ($ishotfix) { $ishotfix = "True" } else { $ishotfix = "False" } if ($longlang -eq "all") { $longlang = "All" } if ($arch -eq "") { $arch = $null } if ($arch -eq "AMD64") { $arch = "x64" } if ($title -match '64-Bit' -and $title -notmatch '32-Bit' -and -not $arch) { $arch = "x64" } if ($title -notmatch '64-Bit' -and $title -match '32-Bit' -and -not $arch) { $arch = "x86" } $detaildialog = Invoke-TlsWebRequest -Uri "https://www.catalog.update.microsoft.com/ScopedViewInline.aspx?updateid=$updateid" $description = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_desc">' $lastmodified = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_date">' $size = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_size">' $classification = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_labelClassification_Separator" class="labelTitle">' $supportedproducts = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_labelSupportedProducts_Separator" class="labelTitle">' $msrcnumber = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_labelSecurityBulliten_Separator" class="labelTitle">' $msrcseverity = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_msrcSeverity">' $kbnumbers = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_labelKBArticle_Separator" class="labelTitle">' $rebootbehavior = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_rebootBehavior">' $requestuserinput = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_userInput">' $exclusiveinstall = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_installationImpact">' $networkrequired = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_connectivity">' $uninstallnotes = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_labelUninstallNotes_Separator" class="labelTitle">' $uninstallsteps = Get-Info -Text $detaildialog -Pattern '<span id="ScopedViewHandler_labelUninstallSteps_Separator" class="labelTitle">' # Thanks @klorgas! https://github.com/potatoqualitee/kbupdate/issues/131 $supersededby = Get-SuperInfo -Text $detaildialog -Pattern '<div id="supersededbyInfo".*>' $supersedes = Get-SuperInfo -Text $detaildialog -Pattern '<div id="supersedesInfo".*>' if ($uninstallsteps -eq "n/a") { $uninstallsteps = $null } if ($msrcnumber -eq "n/a" -or $msrcnumber -eq "Unspecified") { $msrcnumber = $null } $downloaddialog = $downloaddialog.Replace('www.download.windowsupdate', 'download.windowsupdate') if ($kbnumbers -eq "n/a") { $kbnumbers = $null } $ishotfix = switch ($ishotfix) { 'Yes' { $true } 'No' { $false } default { $ishotfix } } $requestuserinput = switch ($requestuserinput) { 'Yes' { $true } 'No' { $false } default { $requestuserinput } } $exclusiveinstall = switch ($exclusiveinstall) { 'Yes' { $true } 'No' { $false } default { $exclusiveinstall } } $networkrequired = switch ($networkrequired) { 'Yes' { $true } 'No' { $false } default { $networkrequired } } if ('n/a' -eq $uninstallnotes) { $uninstallnotes = $null } if ('n/a' -eq $uninstallsteps) { $uninstallsteps = $null } # find links that contain windowsupdate.com using regex $downloaddialog = $downloaddialog.Replace('www.download.windowsupdate', 'download.windowsupdate') $links = $downloaddialog | Select-String -AllMatches -Pattern "(http[s]?\://.*download\.windowsupdate\.com\/[^\'\""]*)" | Select-Object -Unique [pscustomobject]@{ Title = $title Id = $kbnumbers Architecture = $arch Language = $longlang Hotfix = $ishotfix Description = $description LastModified = $lastmodified Size = $size Classification = $classification SupportedProducts = $supportedproducts MSRCNumber = $msrcnumber MSRCSeverity = $msrcseverity RebootBehavior = $rebootbehavior RequestsUserInput = $requestuserinput ExclusiveInstall = $exclusiveinstall NetworkRequired = $networkrequired UninstallNotes = $uninstallnotes UninstallSteps = $uninstallsteps UpdateId = $updateid Supersedes = $supersedes SupersededBy = $supersededby Link = $links.matches.value -join "|" InputObject = $kb } } } catch { throw $PSitem } } $scriptblock = { $PSDefaultParameterValues["Invoke-WebRequest:SkipHeaderValidation"] = $true Import-Module PSSQLite -Verbose:$false $update = $PSItem $guid = $update.UpdateID # Links try { # Adding 04f45522-c78e-45d8-82a1-5614e9ab8596 to kb table Write-Verbose -Message "Getting fresh web data for $guid" try { $webupdate = Get-KbItemFromWeb $guid } catch { throw "Unable to get details for $guid | $PSItem" } if ($update.PayloadFiles.File) { Write-Verbose -Message "Found $(($update.PayloadFiles.File).Count) link(s), deleting old ones from the db and adding updated links" Invoke-SQLiteQuery -DataSource $db -Query "delete from Link where UpdateId = '$guid'" foreach ($file in $update.PayloadFiles.File) { $fileid = $file.id $url = ($ds.Tables["FileLocation"].Select("Id = '$fileid'")).Url $url = $url.Replace("http://download.windowsupdate.com", "https://catalog.s.download.windowsupdate.com") $url = $url.Replace("http://www.download.windowsupdate.com", "https://catalog.s.download.windowsupdate.com") if ($url) { Invoke-SQLiteBulkCopy -DataTable ( [pscustomobject]@{ UpdateId = $guid Link = $url } | ConvertTo-DataTable) -DataSource $db -Table Link -Confirm:$false } } } elseif ($webupdate.Link) { Write-Warning "no link in xml but found in webupdate $guid" Write-Verbose -Message "Found $(($update.PayloadFiles.File).Count) link(s), deleting old ones from the db and adding updated links" Invoke-SQLiteQuery -DataSource $db -Query "delete from Link where UpdateId = '$guid'" $links = $webupdate.Link -split "\|" foreach ($link in $links) { Invoke-SQLiteBulkCopy -DataTable ( [pscustomobject]@{ UpdateId = $guid Link = $link } | ConvertTo-DataTable) -DataSource $db -Table Link -Confirm:$false } } if (-not $webupdate.UpdateId) { return } try { Write-Verbose -Message "Deleting old entries from $db" Invoke-SQLiteQuery -DataSource $db -Query "delete from Kb where UpdateId = '$guid'" Invoke-SQLiteQuery -DataSource $db -Query "delete from SupersededBy where UpdateId = '$guid'" Invoke-SQLiteQuery -DataSource $db -Query "delete from Supersedes where UpdateId = '$guid'" $kb = $webupdate | Select-Object -Property * -ExcludeProperty SupersededBy, Supersedes, Link, InputObject } catch { throw "Unable to delete db entries for $guid | $PSItem" } # Saved to DB as a full string then split by pipe in PowerShell if ($kb.SupportedProducts) { $kb.SupportedProducts = $kb.SupportedProducts -join "|" } Write-Verbose -Message "Adding $guid to kb table" $null = Add-Member -InputObject $kb -NotePropertyName DateAdded -NotePropertyValue (Get-Date) -Force try { Invoke-SQLiteBulkCopy -DataTable ($kb | ConvertTo-DataTable) -DataSource $db -Table Kb -Confirm:$false } catch { Write-Warning -Message "Failure on $guid | $PSItem" continue } try { if ($webupdate.SupersededBy) { Write-Verbose -Message "Processing $(($webupdate.SupersededBy).Count) SupersededBy matches" foreach ($item in $webupdate.SupersededBy) { if ($null -ne $item.Kb -and '' -ne $item.Kb) { if ($item.Kb) { Invoke-SQLiteBulkCopy -DataTable ([pscustomobject]@{ UpdateId = $update.UpdateId Kb = $item.Kb Description = $item.Description } | ConvertTo-DataTable) -DataSource $db -Table SupersededBy -Confirm:$false } } } } } catch { Write-Warning -Message $PSItem continue } try { Write-Verbose -Message "Processing $(($webupdate.Supersedes).Count) Supersedes matches" if ($webupdate.Supersedes) { foreach ($item in $webupdate.Supersedes) { if ($null -ne $item.Kb -and '' -ne $item.Kb) { if ($item.Kb) { Invoke-SQLiteBulkCopy -DataTable ([pscustomobject]@{ UpdateId = $update.UpdateId; Kb = $item.Kb; Description = $item.Description } | ConvertTo-DataTable) -DataSource $db -Table Supersedes -Confirm:$false } } } } } catch { Write-Warning -Message $PSItem continue } } catch { Write-Warning -Message "Failure for $guid | $PSItem" continue } } } process { $parm = @{ ImportVariables = $true ImportFunctions = $true Quiet = $true RunspaceTimeout = 180 ScriptBlock = $scriptblock ErrorAction = "Stop" } try { $recent | Invoke-Parallel @parm } catch { Write-Warning "Error: $PSItem" try { $PSItem.UpdateId | ForEach-Object -Process $scriptblock } catch { Write-Warning "Error: $PSItem" continue } } # if going one-by-one is needed for debugging # $recent | ForEach-Object -Process $scriptblock } } } process { <# KB5013944 $xml.OfflineSyncPackage.CreationDate MinimumClientVersion : 5.8.0.2678 PackageId : c837c786-be39-4f17-8ec5-ede03ad2c80a PackageVersion : 1.1 ProtocolVersion : 1.0 CreationDate : 2022-05-10T16:25:52Z SourceId : 802cb907-a558-4033-9844-bbf65cd3481e xmlns : http://schemas.microsoft.com/msus/2004/02/OfflineSync Updates : Updates FileLocations : FileLocations #> Write-ProgressHelper -StepNumber 1 -Activity "Setting up prerequisites" -Message "Getting database details" try { $null = Import-Module kbupdate-library -ErrorAction Stop } catch { $null = Set-PSRepository PSGallery -InstallationPolicy Trusted $null = Install-Module kbupdate-library -ErrorAction Stop -Scope CurrentUser $null = Import-Module kbupdate-library -ErrorAction Stop } $modpath = Split-Path (Get-Module kbupdate-library).Path $kblib = Join-PSFPath -Path $modpath -Child library $db = Join-PSFPath -Path $kblib -Child kb.sqlite $size = [int]((Get-ChildItem -Path $db).Length / 1MB) "The db is $size MB" | Write-Warning Write-ProgressHelper -StepNumber 2 -Activity "Setting up prerequisites" -Message "Saving scanfile using Save-KbScanFile" Write-PSFMessage -Level Verbose -Message "Saving scanfile" $scanfile = Save-KbScanFile $basedir = Split-Path $scanfile $cabfile = Join-PSFPath $basedir -Child package.cab Write-ProgressHelper -StepNumber 3 -Activity "Setting up prerequisites" -Message "Unpacking $scanfile" Write-PSFMessage -Level Verbose -Message "Unpacking $scanfile" $cab = New-Object Microsoft.Deployment.Compression.Cab.Cabinfo $scanfile $null = $cab.UnpackFile("package.cab", $cabfile) $xmlfile = Join-PSFPath $basedir -Child package.xml Write-ProgressHelper -StepNumber 4 -Activity "Setting up prerequisites" -Message "Importing $xmlfile" $cab = New-Object Microsoft.Deployment.Compression.Cab.Cabinfo $cabfile $null = $cab.UnpackFile("package.xml", $xmlfile) $xml = [xml](Get-Content -Path $xmlfile) $updates = $xml.OfflineSyncPackage.Updates.Update Write-ProgressHelper -StepNumber 5 -Activity "Setting up prerequisites" -Message "Loading $xmlfile into dataset" Write-PSFMessage -Level Verbose -Message "Loading $xmlfile into dataset" # This takes 30 seconds but saves time in the long-run 200ms per execution X thousands if (-not $ds) { $ds = New-Object System.Data.DataSet $null = $ds.ReadXml($xmlfile) } Write-PSFMessage -Level Verbose -Message "$($updates.Count) total items in the database" Write-ProgressHelper -StepNumber 6 -Activity "Setting up prerequisites" -Message "Searching for recent updates" # Only process from the last 3 months, this is an arbitrary amount that will cover # since the last release of the offline wsus db which usually occurs every 30 days $recent = $updates.Where({ ([datetime]($PSItem.CreationDate)) -gt ((Get-Date).AddMonths(-3)) }) Write-Warning "$($recent.Count) updates to process" Write-PSFMessage -Level Verbose -Message "Processing $($recent.Count) kbs" Write-ProgressHelper -StepNumber 7 -Activity "Setting up prerequisites" -Message "Processing $($recent.Count) kbs" Write-Progress -Activity "Setting up prerequisites" -Completed $output = Update-KbDb $recent if ($output.UpdateId) { $output.UpdateId | Write-Warning "Trying to grab $PSItem again" foreach ($object in $output) { $null = $object.UpdateId | ForEach-Object -Process $scriptblock -ErrorAction SilentlyContinue } } } end { if ($db) { $updatesfile = Resolve-Path -Path $script:ModuleRoot\build\updates.sql $null = Invoke-SQLiteQuery -DataSource $db -InputFile $updatesfile -Verbose $size = [int]((Get-ChildItem -Path $db).Length / 1MB) Write-ProgressHelper -StepNumber 1 -Activity "Compressing db" -Message "Compressing db" try { Write-PSFMessage -Level Verbose -Message "Compressing db ($size)" $null = Invoke-SqliteQuery -DataSource $db -Query "VACUUM;" -ErrorAction Stop $size = [int]((Get-ChildItem -Path $db).Length / 1MB) Write-PSFMessage -Level Verbose -Message "Done compressing db ($size)" } catch { Write-PSFMessage -Level Warning -Message "DB compression failed: $PSItem" } Write-Progress -Activity "Compressing db" -Completed "The db is $size MB" | Write-Warning Get-ChildItem -Path $db } else { Write-Warning "No db to compress" } } } # SIG # Begin signature block # MIIjZQYJKoZIhvcNAQcCoIIjVjCCI1ICAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB # gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR # AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUeKe6rLVkqG1pbuz/F2lCXBom # 5b2ggh2DMIIFGjCCBAKgAwIBAgIQAwW7hiGwoWNfv96uEgTnbTANBgkqhkiG9w0B # AQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYD # VQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEyIEFz # c3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDUxMjAwMDAwMFoXDTIzMDYw # ODEyMDAwMFowVzELMAkGA1UEBhMCVVMxETAPBgNVBAgTCFZpcmdpbmlhMQ8wDQYD # VQQHEwZWaWVubmExETAPBgNVBAoTCGRiYXRvb2xzMREwDwYDVQQDEwhkYmF0b29s # czCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALy/Y3ur47++CAG2mOa1 # 6h8WjXjSTvcldDmw4PpAvOOCKNr6xyhg/FOYVIiaeq2N9kVaa5wBawOIxVWuj/rI # aOxeYklQDugPkGUx0Ap+6KrjnnxgE6ONzQGnc1tjlka6N0KazD2WodEBWKXo/Vmk # C/cP9PJVWroCMOwlj7GtEv2IxzxikPm2ICP5KxFK5PmrA+5bzcHJEeqRonlgMn9H # zZkqHr0AU1egnfEIlH4/v6lry1t1KBF/bnDhl9g/L0icS+ychFVkx4OOO4a+qvT8 # xqvvdQjv3PQ1hbzTI3/tXOWu9XxGeeIdZjaJv16FmWKCnloSp1Xb9cVU9XhIpomz # xH0CAwEAAaOCAcUwggHBMB8GA1UdIwQYMBaAFFrEuXsqCqOl6nEDwGD5LfZldQ5Y # MB0GA1UdDgQWBBTwwKD7tgOAQ077Cdfd33qxy+OeIjAOBgNVHQ8BAf8EBAMCB4Aw # EwYDVR0lBAwwCgYIKwYBBQUHAwMwdwYDVR0fBHAwbjA1oDOgMYYvaHR0cDovL2Ny # bDMuZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwNaAzoDGGL2h0 # dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9zaGEyLWFzc3VyZWQtY3MtZzEuY3JsMEwG # A1UdIARFMEMwNwYJYIZIAYb9bAMBMCowKAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3 # LmRpZ2ljZXJ0LmNvbS9DUFMwCAYGZ4EMAQQBMIGEBggrBgEFBQcBAQR4MHYwJAYI # KwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBOBggrBgEFBQcwAoZC # aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0U0hBMkFzc3VyZWRJ # RENvZGVTaWduaW5nQ0EuY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQAD # ggEBAI/N+XCVDB/WNqQSrKY85zScHGJjsXgXByYvsitMuG5vo+ODhlh+ILv0CTPl # o2Wo75MnSSqCWR+c6xyN8pDPMPBxm2EtVmXzeKDMIudYyjxmT8PZ3hktj16wXCo8 # 2+65UOse+CHsfoMn/M9WbkQ4rSyWNPRRDodATC2i4flLyeuoIZnyMoz/4N4mWb6s # IAYZ/tNXzm6qwCfkmoMSf9tcTUCXIbVDliJcUZLlJ/SpLg2KzDu9GtnpBzg3AG3L # hwBiPMM8OLGitYjz4VU5RYox0vu1XyLf3f9fKTCxxwKy0EKntWdJk37i+DOMQlCq # Xm5B/KyNxb2utv+qLGlyw9MphEcwggUwMIIEGKADAgECAhAECRgbX9W7ZnVTQ7Vv # lVAIMA0GCSqGSIb3DQEBCwUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp # Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0Rp # Z2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0xMzEwMjIxMjAwMDBaFw0yODEw # MjIxMjAwMDBaMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx # GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNI # QTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwggEiMA0GCSqGSIb3DQEBAQUA # A4IBDwAwggEKAoIBAQD407Mcfw4Rr2d3B9MLMUkZz9D7RZmxOttE9X/lqJ3bMtdx # 6nadBS63j/qSQ8Cl+YnUNxnXtqrwnIal2CWsDnkoOn7p0WfTxvspJ8fTeyOU5JEj # lpB3gvmhhCNmElQzUHSxKCa7JGnCwlLyFGeKiUXULaGj6YgsIJWuHEqHCN8M9eJN # YBi+qsSyrnAxZjNxPqxwoqvOf+l8y5Kh5TsxHM/q8grkV7tKtel05iv+bMt+dDk2 # DZDv5LVOpKnqagqrhPOsZ061xPeM0SAlI+sIZD5SlsHyDxL0xY4PwaLoLFH3c7y9 # hbFig3NBggfkOItqcyDQD2RzPJ6fpjOp/RnfJZPRAgMBAAGjggHNMIIByTASBgNV # HRMBAf8ECDAGAQH/AgEAMA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEF # BQcDAzB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp # Z2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu # Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDCBgQYDVR0fBHoweDA6oDig # NoY0aHR0cDovL2NybDQuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNybDA6oDigNoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0 # QXNzdXJlZElEUm9vdENBLmNybDBPBgNVHSAESDBGMDgGCmCGSAGG/WwAAgQwKjAo # BggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAKBghghkgB # hv1sAzAdBgNVHQ4EFgQUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHwYDVR0jBBgwFoAU # Reuir/SSy4IxLVGLp6chnfNtyA8wDQYJKoZIhvcNAQELBQADggEBAD7sDVoks/Mi # 0RXILHwlKXaoHV0cLToaxO8wYdd+C2D9wz0PxK+L/e8q3yBVN7Dh9tGSdQ9RtG6l # jlriXiSBThCk7j9xjmMOE0ut119EefM2FAaK95xGTlz/kLEbBw6RFfu6r7VRwo0k # riTGxycqoSkoGjpxKAI8LpGjwCUR4pwUR6F6aGivm6dcIFzZcbEMj7uo+MUSaJ/P # QMtARKUT8OZkDCUIQjKyNookAv4vcn4c10lFluhZHen6dGRrsutmQ9qzsIzV6Q3d # 9gEgzpkxYz0IGhizgZtPxpMQBvwHgfqL2vmCSfdibqFT+hKUGIUukpHqaGxEMrJm # oecYpJpkUe8wggWxMIIEmaADAgECAhABJAr7HjgLihbxS3Gd9NPAMA0GCSqGSIb3 # DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAX # BgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lDZXJ0IEFzc3Vy # ZWQgSUQgUm9vdCBDQTAeFw0yMjA2MDkwMDAwMDBaFw0zMTExMDkyMzU5NTlaMGIx # CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3 # dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH # NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw # aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK # EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm # dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu # d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD # eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1 # XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld # QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS # YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm # M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT # QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx # fgommfXkaS+YHS312amyHeUbAgMBAAGjggFeMIIBWjAPBgNVHRMBAf8EBTADAQH/ # MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzAfBgNVHSMEGDAWgBRF66Kv # 9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB # BQUHAwgweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDig # NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZI # hvcNAQEMBQADggEBAJoWAqUB74H7DbRYsnitqCMZ2XM32mCeUdfL+C9AuaMffEBO # Mz6QPOeJAXWF6GJ7HVbgcbreXsY3vHlcYgBN+El6UU0GMvPF0gAqJyDqiS4VOeAs # Pvh1fCyCQWE1DyPQ7TWV0oiVKUPL4KZYEHxTjp9FySA3FMDtGbp+dznSVJbHphHf # NDP2dVJCSxydjZbVlWxHEhQkXyZB+hpGvd6w5ZFHA6wYCMvL22aJfyucZb++N06+ # LfOdSsPMzEdeyJWVrdHLuyoGIPk/cuo260VyknopexQDPPtN1khxehARigh0zWwb # BFzSipUDdlFQU9Yu90pGw64QLHFMsIe2JzdEYEQwggauMIIElqADAgECAhAHNje3 # JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAf # BgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDAeFw0yMjAzMjMwMDAwMDBa # Fw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2Vy # dCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNI # QTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIK # AoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbSg9GeTKJtoLDMg/la9hGhRBVC # X6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9/UO0hNoR8XOxs+4rgISKIhjf # 69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXnHwZljZQp09nsad/ZkIdGAHvb # REGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0VAshaG43IbtArF+y3kp9zvU5 # EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4fsbVYTXn+149zk6wsOeKlSNbw # sDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40NjgHt1biclkJg6OBGz9vae5jtb # 7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0QCirc0PO30qhHGs4xSnzyqqW # c0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvvmz3+DrhkKvp1KCRB7UK/BZxm # SVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T/jnA+bIwpUzX6ZhKWD7TA4j+ # s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk42PgpuE+9sJ0sj8eCXbsq11G # deJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5rmQzSM7TNsQIDAQABo4IBXTCC # AVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQUuhbZbU2FL3MpdpovdYxq # II+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/ # BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggr # BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVo # dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0 # LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20v # RGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjAL # BglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIBAH1ZjsCTtm+YqUQiAX5m1tgh # QuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxpwc8dB+k+YMjYC+VcW9dth/qE # ICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIlzpVpP0d3+3J0FNf/q0+KLHqr # hc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQcAp876i8dU+6WvepELJd6f8o # VInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfeKuv2nrF5mYGjVoarCkXJ38SN # oOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+jSbl3ZpHxcpzpSwJSpzd+k1Os # Ox0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJshIUDQtxMkzdwdeDrknq3lNHGS # 1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6OOmc4d0j/R0o08f56PGYX/sr # 2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDwN7+YAN8gFk8n+2BnFqFmut1V # wDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR81fZvAT6gt4y3wSJ8ADNXcL5 # 0CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2VVQrH4D6wPIOK+XW+6kvRBVK # 5xMOHds3OBqhK/bt1nz8MIIGxjCCBK6gAwIBAgIQCnpKiJ7JmUKQBmM4TYaXnTAN # BgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQs # IEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEy # NTYgVGltZVN0YW1waW5nIENBMB4XDTIyMDMyOTAwMDAwMFoXDTMzMDMxNDIzNTk1 # OVowTDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMSQwIgYD # VQQDExtEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMiAtIDIwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQC5KpYjply8X9ZJ8BWCGPQz7sxcbOPgJS7SMeQ8QK77 # q8TjeF1+XDbq9SWNQ6OB6zhj+TyIad480jBRDTEHukZu6aNLSOiJQX8Nstb5hPGY # Pgu/CoQScWyhYiYB087DbP2sO37cKhypvTDGFtjavOuy8YPRn80JxblBakVCI0Fa # +GDTZSw+fl69lqfw/LH09CjPQnkfO8eTB2ho5UQ0Ul8PUN7UWSxEdMAyRxlb4pgu # j9DKP//GZ888k5VOhOl2GJiZERTFKwygM9tNJIXogpThLwPuf4UCyYbh1RgUtwRF # 8+A4vaK9enGY7BXn/S7s0psAiqwdjTuAaP7QWZgmzuDtrn8oLsKe4AtLyAjRMruD # +iM82f/SjLv3QyPf58NaBWJ+cCzlK7I9Y+rIroEga0OJyH5fsBrdGb2fdEEKr7mO # CdN0oS+wVHbBkE+U7IZh/9sRL5IDMM4wt4sPXUSzQx0jUM2R1y+d+/zNscGnxA7E # 70A+GToC1DGpaaBJ+XXhm+ho5GoMj+vksSF7hmdYfn8f6CvkFLIW1oGhytowkGvu # b3XAsDYmsgg7/72+f2wTGN/GbaR5Sa2Lf2GHBWj31HDjQpXonrubS7LitkE956+n # GijJrWGwoEEYGU7tR5thle0+C2Fa6j56mJJRzT/JROeAiylCcvd5st2E6ifu/n16 # awIDAQABo4IBizCCAYcwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwFgYD # VR0lAQH/BAwwCgYIKwYBBQUHAwgwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZI # AYb9bAcBMB8GA1UdIwQYMBaAFLoW2W1NhS9zKXaaL3WMaiCPnshvMB0GA1UdDgQW # BBSNZLeJIf5WWESEYafqbxw2j92vDTBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8v # Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hBMjU2 # VGltZVN0YW1waW5nQ0EuY3JsMIGQBggrBgEFBQcBAQSBgzCBgDAkBggrBgEFBQcw # AYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFgGCCsGAQUFBzAChkxodHRwOi8v # Y2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRSU0E0MDk2U0hB # MjU2VGltZVN0YW1waW5nQ0EuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQANLSN0ptH1 # +OpLmT8B5PYM5K8WndmzjJeCKZxDbwEtqzi1cBG/hBmLP13lhk++kzreKjlaOU7Y # hFmlvBuYquhs79FIaRk4W8+JOR1wcNlO3yMibNXf9lnLocLqTHbKodyhK5a4m1Wp # Gmt90fUCCU+C1qVziMSYgN/uSZW3s8zFp+4O4e8eOIqf7xHJMUpYtt84fMv6XPfk # U79uCnx+196Y1SlliQ+inMBl9AEiZcfqXnSmWzWSUHz0F6aHZE8+RokWYyBry/J7 # 0DXjSnBIqbbnHWC9BCIVJXAGcqlEO2lHEdPu6cegPk8QuTA25POqaQmoi35komWU # EftuMvH1uzitzcCTEdUyeEpLNypM81zctoXAu3AwVXjWmP5UbX9xqUgaeN1Gdy4b # esAzivhKKIwSqHPPLfnTI/KeGeANlCig69saUaCVgo4oa6TOnXbeqXOqSGpZQ65f # 6vgPBkKd3wZolv4qoHRbY2beayy4eKpNcG3wLPEHFX41tOa1DKKZpdcVazUOhdbg # LMzgDCS4fFILHpl878jIxYxYaa+rPeHPzH0VrhS/inHfypex2EfqHIXgRU4SHBQp # WMxv03/LvsEOSm8gnK7ZczJZCOctkqEaEf4ymKZdK5fgi9OczG21Da5HYzhHF1tv # E9pqEG4fSbdEW7QICodaWQR2EaGndwITHDGCBUwwggVIAgEBMIGGMHIxCzAJBgNV # BAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdp # Y2VydC5jb20xMTAvBgNVBAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2Rl # IFNpZ25pbmcgQ0ECEAMFu4YhsKFjX7/erhIE520wCQYFKw4DAhoFAKB4MBgGCisG # AQQBgjcCAQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw # HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFClK # DBio8E3rPtOuSBQkGbZO+1k4MA0GCSqGSIb3DQEBAQUABIIBAEESTFMZpAwEypWn # jS5rFt7o7hysxJbIFMJjjnbJzFmxC59ZenRRDsd3FjicZzP1qEcduiYd3j7c8GPK # m8XaxgjkQ+KqPCDWkJ1YHZMTi5441x2o902uXLQOO2vH9O3ii9vKyaAnCUj8XsKE # raGffvnop2R1orVPhE3DOJTX1dgYxXWv3HzlMkp0iEBLb3lf7fSus/3B6eunrUKf # fCMGvALO2uhHYxt0lISx1cg06QrlfarCmHqIZWL9jy+C1qzhDVMm2MYyrH+ZC7TP # 9NqEv2jZP327DsT1rCCEutQX+J4/4SXg8s7cecRerb8tG3YTobdWL8iWWRxiG8/x # z4ZAnp2hggMgMIIDHAYJKoZIhvcNAQkGMYIDDTCCAwkCAQEwdzBjMQswCQYDVQQG # EwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0 # IFRydXN0ZWQgRzQgUlNBNDA5NiBTSEEyNTYgVGltZVN0YW1waW5nIENBAhAKekqI # nsmZQpAGYzhNhpedMA0GCWCGSAFlAwQCAQUAoGkwGAYJKoZIhvcNAQkDMQsGCSqG # SIb3DQEHATAcBgkqhkiG9w0BCQUxDxcNMjIwODAxMjAzMTUxWjAvBgkqhkiG9w0B # CQQxIgQgxjMS4N38dOrqtMfwMGcugmDz52N2H2Kl4NcnUD9SS8EwDQYJKoZIhvcN # AQEBBQAEggIAFq1jm24jba3DIt6dNnCtdnP2YN2EznFaY7uleF0HIXjCn8BPBqrg # BggURtfzsl2sG4ALJVZBN9gvhpGY7is3dN0IYmIRbqxVwq23Ix0V0C4Gm6ldiqZz # o1uOySEuZTe8HW9g4eF6LNINq6eQSEf3SyvG8AJcZIWoFeYEVTSn0bXISJE6dgpu # Cbzla59/uz8igo3D110oHIaIFdEBSoHydRiW8QiVLNpb3mNjF5SsNCprbWjYe+Cq # us5vefHtTeRj30bLpM94nF03lgzV6S1lFIgrRrA7r6BdZjUCi2P72kUfJKXQM+mB # dkaxd0o3zXIj4ft9lMBzRqT8kGnAAGBJsfxpg5NtZa6QvO2j6YpcRLVPKzujxmOK # 3NXnqVoviH4/9x9NZDcwRaavzwWM1hkHxojWCTswxB+WGURG+N102oRXygJ7Y6ON # ekpKj7GeRgvEr42bCtm9xIWj1zoYwwMoJna1lOg3XW9y+/q5YXug++xk3RrPUOc7 # lYt8NOsQxcrqEJONY7ukAquxI0Hiwj4o7w8DgB29Xvpv2u+dAG6zBlQ1vZ1lerwF # 8JdIqbBcZ1rTS8NWkSsBhhs1VBdUGoFWonNuE07fa5NyinHtcmGU46PF4uRXXjQn # HhmhgQJX4djYjyDDIxzH4CbEf1Rptc0mF0tvAKL9m30ZYGVUU7eCnQU= # SIG # End signature block |