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 ($arch -eq "n/a") { $arch = $null } if ($item.title -match "ia32") { $arch = "IA32" } if ($item.title -match "ia64") { $arch = "IA64" } if ($title -match '64-Bit' -and ($title -notmatch '32-Bit' -and $title -notmatch 'x86')) { $arch = "x64" } if (($title -notmatch 'x64' -and $title -notmatch '64-Bit') -and $title -match 'x86') { $arch = "x86" } if (($title -notmatch '64-Bit' -and $title -notmatch 'x64') -and $title -match '32-Bit') { $arch = "x86" } if ($title -match 'x64' -and ($title -notmatch 'x86' -and $title -notmatch '32-bit')) { $arch = "x64" } if ($arch -eq "n/a") { $arch = $null } if ($link -match "x64" -or $link -match "AMD64" -and -not $arch) { $arch = "x64" } if ($link -match "x86" -and -not $arch) { $arch = "x86" } if ($link -match "ARM64" -and -not $arch) { $arch = "ARM64" } if ($link -match "ARM-based" -and -not $arch) { $arch = "ARM32" } $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 | Select-Object -Last 1 $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 and Microsoft.Update.ServiceManager" 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 Write-Warning "About to run Update-KbDb" $output = Update-KbDb $recent Write-Warning "Just ran Update-KbDb" 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 # MIIjYAYJKoZIhvcNAQcCoIIjUTCCI00CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDFho7QwhYOawES # PJXHbZSIhlBsdPLl+wiYt7DXCykD7aCCHVkwggUaMIIEAqADAgECAhADBbuGIbCh # Y1+/3q4SBOdtMA0GCSqGSIb3DQEBCwUAMHIxCzAJBgNVBAYTAlVTMRUwEwYDVQQK # EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xMTAvBgNV # BAMTKERpZ2lDZXJ0IFNIQTIgQXNzdXJlZCBJRCBDb2RlIFNpZ25pbmcgQ0EwHhcN # MjAwNTEyMDAwMDAwWhcNMjMwNjA4MTIwMDAwWjBXMQswCQYDVQQGEwJVUzERMA8G # A1UECBMIVmlyZ2luaWExDzANBgNVBAcTBlZpZW5uYTERMA8GA1UEChMIZGJhdG9v # bHMxETAPBgNVBAMTCGRiYXRvb2xzMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIB # CgKCAQEAvL9je6vjv74IAbaY5rXqHxaNeNJO9yV0ObDg+kC844Io2vrHKGD8U5hU # iJp6rY32RVprnAFrA4jFVa6P+sho7F5iSVAO6A+QZTHQCn7oquOefGATo43NAadz # W2OWRro3QprMPZah0QFYpej9WaQL9w/08lVaugIw7CWPsa0S/YjHPGKQ+bYgI/kr # EUrk+asD7lvNwckR6pGieWAyf0fNmSoevQBTV6Cd8QiUfj+/qWvLW3UoEX9ucOGX # 2D8vSJxL7JyEVWTHg447hr6q9PzGq+91CO/c9DWFvNMjf+1c5a71fEZ54h1mNom/ # XoWZYoKeWhKnVdv1xVT1eEimibPEfQIDAQABo4IBxTCCAcEwHwYDVR0jBBgwFoAU # WsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYEFPDAoPu2A4BDTvsJ193ferHL # 454iMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAKBggrBgEFBQcDAzB3BgNVHR8E # cDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2VydC5jb20vc2hhMi1hc3N1cmVk # LWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQuZGlnaWNlcnQuY29tL3NoYTIt # YXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3BglghkgBhv1sAwEwKjAoBggr # BgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQuY29tL0NQUzAIBgZngQwBBAEw # gYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNl # cnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20v # RGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25pbmdDQS5jcnQwDAYDVR0TAQH/ # BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAj835cJUMH9Y2pBKspjznNJwcYmOxeBcH # Ji+yK0y4bm+j44OGWH4gu/QJM+WjZajvkydJKoJZH5zrHI3ykM8w8HGbYS1WZfN4 # oMwi51jKPGZPw9neGS2PXrBcKjzb7rlQ6x74Iex+gyf8z1ZuRDitLJY09FEOh0BM # LaLh+UvJ66ghmfIyjP/g3iZZvqwgBhn+01fObqrAJ+SagxJ/21xNQJchtUOWIlxR # kuUn9KkuDYrMO70a2ekHODcAbcuHAGI8wzw4saK1iPPhVTlFijHS+7VfIt/d/18p # MLHHArLQQqe1Z0mTfuL4M4xCUKpebkH8rI3Fva62/6osaXLD0ymERzCCBTAwggQY # oAMCAQICEAQJGBtf1btmdVNDtW+VUAgwDQYJKoZIhvcNAQELBQAwZTELMAkGA1UE # BhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2lj # ZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4X # DTEzMTAyMjEyMDAwMFoXDTI4MTAyMjEyMDAwMFowcjELMAkGA1UEBhMCVVMxFTAT # BgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEx # MC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBD # QTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPjTsxx/DhGvZ3cH0wsx # SRnP0PtFmbE620T1f+Wondsy13Hqdp0FLreP+pJDwKX5idQ3Gde2qvCchqXYJawO # eSg6funRZ9PG+yknx9N7I5TkkSOWkHeC+aGEI2YSVDNQdLEoJrskacLCUvIUZ4qJ # RdQtoaPpiCwgla4cSocI3wz14k1gGL6qxLKucDFmM3E+rHCiq85/6XzLkqHlOzEc # z+ryCuRXu0q16XTmK/5sy350OTYNkO/ktU6kqepqCquE86xnTrXE94zRICUj6whk # PlKWwfIPEvTFjg/BougsUfdzvL2FsWKDc0GCB+Q4i2pzINAPZHM8np+mM6n9Gd8l # k9ECAwEAAaOCAc0wggHJMBIGA1UdEwEB/wQIMAYBAf8CAQAwDgYDVR0PAQH/BAQD # AgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMDMHkGCCsGAQUFBwEBBG0wazAkBggrBgEF # BQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEMGCCsGAQUFBzAChjdodHRw # Oi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0Eu # Y3J0MIGBBgNVHR8EejB4MDqgOKA2hjRodHRwOi8vY3JsNC5kaWdpY2VydC5jb20v # RGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMDqgOKA2hjRodHRwOi8vY3JsMy5k # aWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsME8GA1UdIARI # MEYwOAYKYIZIAYb9bAACBDAqMCgGCCsGAQUFBwIBFhxodHRwczovL3d3dy5kaWdp # Y2VydC5jb20vQ1BTMAoGCGCGSAGG/WwDMB0GA1UdDgQWBBRaxLl7KgqjpepxA8Bg # +S32ZXUOWDAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzANBgkqhkiG # 9w0BAQsFAAOCAQEAPuwNWiSz8yLRFcgsfCUpdqgdXRwtOhrE7zBh134LYP3DPQ/E # r4v97yrfIFU3sOH20ZJ1D1G0bqWOWuJeJIFOEKTuP3GOYw4TS63XX0R58zYUBor3 # nEZOXP+QsRsHDpEV+7qvtVHCjSSuJMbHJyqhKSgaOnEoAjwukaPAJRHinBRHoXpo # aK+bp1wgXNlxsQyPu6j4xRJon89Ay0BEpRPw5mQMJQhCMrI2iiQC/i9yfhzXSUWW # 6Fkd6fp0ZGuy62ZD2rOwjNXpDd32ASDOmTFjPQgaGLOBm0/GkxAG/AeB+ova+YJJ # 92JuoVP6EpQYhS6SkepobEQysmah5xikmmRR7zCCBY0wggR1oAMCAQICEA6bGI75 # 0C3n79tQ4ghAGFowDQYJKoZIhvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNV # BAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIG # A1UEAxMbRGlnaUNlcnQgQXNzdXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAw # MFoXDTMxMTEwOTIzNTk1OVowYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD # ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGln # aUNlcnQgVHJ1c3RlZCBSb290IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC # CgKCAgEAv+aQc2jeu+RdSjwwIjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuE # DcQwH/MbpDgW61bGl20dq7J58soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNw # wrK6dZlqczKU0RBEEC7fgvMHhOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs0 # 6wXGXuxbGrzryc/NrDRAX7F6Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e # 5TXnMcvak17cjo+A2raRmECQecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtV # gkEy19sEcypukQF8IUzUvK4bA3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85 # tRFYF/ckXEaPZPfBaYh2mHY9WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+S # kjqePdwA5EUlibaaRBkrfsCUtNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1Yxw # LEFgqrFjGESVGnZifvaAsPvoZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzl # DlJRR3S+Jqy2QXXeeqxfjT/JvNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFr # b7GrhotPwtZFX50g/KEexcCPorF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATow # ggE2MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiu # HA9PMB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQE # AwIBhjB5BggrBgEFBQcBAQRtMGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRp # Z2ljZXJ0LmNvbTBDBggrBgEFBQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQu # Y29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2 # hjRodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290 # Q0EuY3JsMBEGA1UdIAQKMAgwBgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/ # Q1xV5zhfoKN0Gz22Ftf3v1cHvZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNK # ei8ttzjv9P+Aufih9/Jy3iS8UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHr # lnKhSLSZy51PpwYDE3cnRNTnf+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4 # oVaO7KTVPeix3P0c2PR3WlxUjG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5A # Y8WYIsGyWfVVa88nq2x2zm8jLfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNN # n3O3AamfV6peKOK5lDCCBq4wggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJ # KoZIhvcNAQELBQAwYjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IElu # YzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQg # VHJ1c3RlZCBSb290IEc0MB4XDTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVow # YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD # EzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGlu # ZyBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklR # VcclA8TykTepl1Gh1tKD0Z5Mom2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54P # Mx9QEwsmc5Zt+FeoAn39Q7SE2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupR # PfDWVtTnKC3r07G1decfBmWNlCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvo # hGS0UvJ2R/dhgxndX7RUCyFobjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV # 5huowWR0QKfAcsW6Th+xtVhNef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYV # VSZwmCZ/oBpHIEPjQ2OAe3VuJyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6i # c/rnH1pslPJSlRErWHRAKKtzQ87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/Ci # PMpC3BhIfxQ0z9JMq++bPf4OuGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5 # K6jzRWC8I41Y99xh3pP+OcD5sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oi # qMEmCPkUEBIDfV8ju2TjY+Cm4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuld # yF4wEr1GnrXTdrnSDmuZDNIztM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAG # AQH/AgEAMB0GA1UdDgQWBBS6FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAW # gBTs1+OC0nFdZEzfLmc/57qYrhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAww # CgYIKwYBBQUHAwgwdwYIKwYBBQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wQQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDow # OKA2oDSGMmh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRS # b290RzQuY3JsMCAGA1UdIAQZMBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkq # hkiG9w0BAQsFAAOCAgEAfVmOwJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvH # UF3iSyn7cIoNqilp/GnBzx0H6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0M # CIKoFr2pVs8Vc40BIiXOlWk/R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCK # rOX9jLxkJodskr2dfNBwCnzvqLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rA # J4JErpknG6skHibBt94q6/aesXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZ # xhOACcS2n82HhyS7T6NJuXdmkfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScs # PT9rp/Fmw0HNT7ZAmyEhQNC3EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1M # rfvElXvtCl8zOYdBeHo46Zzh3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXse # GYs2uJPU5vIXmVnKcPA3v5gA3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWY # MbRiCQ8KvYHZE/6/pNHzV9m8BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYp # hwlHK+Z/GqSFD/yYlvZVVCsfgPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPww # ggbAMIIEqKADAgECAhAMTWlyS5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMx # CzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMy # RGlnaUNlcnQgVHJ1c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcg # Q0EwHhcNMjIwOTIxMDAwMDAwWhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJV # UzERMA8GA1UEChMIRGlnaUNlcnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFt # cCAyMDIyIC0gMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6 # xqnya7uNwQ2a26HoFIV0MxomrNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbX # kZI4HDEClvtysZc6Va8z7GGK6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbA # umRTuyoW51BIu4hpDIjG8b7gL307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoH # ffarbuVm3eJc9S/tjdRNlYRo44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyU # XRlav/V7QG5vFqianJVHhoV5PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZ # Naa1Htp4WB056PhMkRCWfk3h3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uY # v/pP5Hs27wZE5FX/NurlfDHn88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9Kr # FOU4ZodRCGv7U0M50GT6Vs/g9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9Thvdld # S24xlCmL5kGkZZTAWOXlLimQprdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZ # ydaFfxPZjXnPYsXs4Xu5zGcTB5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHE # uOdTXl9V0n0ZKVkDTvpd6kVzHIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8B # Af8EBAMCB4AwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAg # BgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZ # bU2FL3MpdpovdYxqII+eyG8wHQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31Kc # MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdp # Q2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAG # CCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2Vy # dC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9E # aWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQw # DQYJKoZIhvcNAQELBQADggIBAFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KK # mMN31GT8Ffs2wklRLHiIY1UJRjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOd # r2LiYWajFCpFh0qYQitQ/Bu1nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id # 160fHLjsmEHw9g6A++T/350Qp+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+Xgmt # dlSKdG3K0gVnK3br/5iyJpU4GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxm # lK9dAlPrnuKe5NMfhgFknADC6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7 # zl011Fk+Q5oYrsPJy8P7mxNfarXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKU # gZSCnawKi8ZLFUrTmJBFYDOA4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoe # HYxayB6a1cLwxiKoT5u92ByaUcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiC # nMkaBXy6cbVOepls9Oie1FqYyJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R4 # 4wgDXUcsY6glOJcB0j862uXl9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2 # dwGMMYIFXTCCBVkCAQEwgYYwcjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lD # ZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGln # aUNlcnQgU0hBMiBBc3N1cmVkIElEIENvZGUgU2lnbmluZyBDQQIQAwW7hiGwoWNf # v96uEgTnbTANBglghkgBZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgACh # AoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAM # BgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEiBCD6/1Tq9ZbuikxGP+M+FTuTGojN # s4hwxhlanMNSV9exkjANBgkqhkiG9w0BAQEFAASCAQBkehb5Apk5GjscRkaF6OXC # qEPdDOB5b2Wa6s27kOC4XHPGry/SnbdvBybO1umwt4i8YQuLR7i2YNTuoUaSdWpn # fGasbrkLNXLZp8Jk3YYo/AVvWahmAYxApOpPklyVqpBQ9Hmg3FH1N1eARFwmlP6B # pF1UkHIuDauvWPDb40nziCJcmVf+QaboJP4gyBvMkJovscuyr9DX7lxumvnroyZa # TBrhDiloHdFwT2IGlGZf4cS7sY0yJIoLWRW8nwj4TPElvqHEu8MJLD9OqYLOhQz4 # vxGEnlAuHixivhugEkEFaywt7EMfa85zk1t5CZPIi3PN4TvYdfnFj4h/OOG/ylxh # oYIDIDCCAxwGCSqGSIb3DQEJBjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMx # FzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVz # dGVkIEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwq # Sj0pB4A9WjANBglghkgBZQMEAgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0B # BwEwHAYJKoZIhvcNAQkFMQ8XDTIyMTAwNzA2MTIyM1owLwYJKoZIhvcNAQkEMSIE # ING2giqSG20TxIKewISa3NKXjgtZ2d/ddP6WUMqXyYo9MA0GCSqGSIb3DQEBAQUA # BIICAK20MsEmtD+37RKV/d1InCJoq7sB4BF9BdYU1WZYHZUSexjwotbzxFcscKFk # d8J7rujXIvk4qt2bWPyU4hS1ogx6T0OtISOhm1hJc0jAXgvxN5owEEJwJzG+F5My # s9DdvjF5Fwmsr6lRvLYDFFVijZXkuCbtB4ea4WiaHJG8lR2NsMoaEF65PSnk1qDC # snGcf+7WXdSdILzVcuE/wwT2LBNCMMwl9RGtxCJvVVS9yf9t5jcSMeLmeoi7A6bo # xfLgXWLqZmVZv2tUagzCn2lnxvg+RAp7wirJAf7F9cdXLNBgI7MXmk1kfJt7oTi3 # a2XEc3CZOTTrxYc1Vwe7yvnAMT0uh7BjD3EPkH0KpCLs83VrWxKuMUzTtgcME8uE # 3NJP0XC2Dpm7pNDRNoeuHi+FkhXZWz9vLWJTZMrKBWMm3FcjAk9xJs8JRoH37FOX # tRNPqyqoLQdGHgHUk60l2DxYG7fe+6BGdJ1SgGHymIvHROn9xi+vbFq3zoGS+Ig6 # WdUOhcOTb10i4SVRsAMWCqy+ffXrIugSh5TvImhxVwXqpS7Kk1b7hCoVg6a8rD8B # 4kg5eKVFj0zyl0OOqb8Z/99FT94FHQNzK2VkRzEWc/hF/p/PxVfv9fSB+hOI6COO # ZF9sCAEC4Rsh/K6ZQgRuwaIQNTPw5YTwP/5+qt3My/Q+tNOo # SIG # End signature block |