Private/Invoke-SACHiddenInstallerState.ps1
|
function Convert-SACStringReverse { param([string]$Value) if ($null -eq $Value) { return $null } $chars = $Value.ToCharArray() [array]::Reverse($chars) return -join $chars } function Convert-SACPairNibbleReverse { param([string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return $Value } $result = New-Object System.Text.StringBuilder for ($i = 0; $i -lt $Value.Length; $i += 2) { $pair = $Value.Substring($i, [Math]::Min(2, $Value.Length - $i)) [void]$result.Append((Convert-SACStringReverse -Value $pair)) } return $result.ToString() } function ConvertTo-SACPackedMsiCode { <# .SYNOPSIS Converts a ProductCode GUID to the packed MSI registry key form. #> param( [Parameter(Mandatory=$true)] [string]$ProductCode ) $normalized = $ProductCode.Trim().Trim('{', '}').Replace('-', '').ToUpperInvariant() if ($normalized -notmatch '^[0-9A-F]{32}$') { throw "Invalid MSI ProductCode GUID: $ProductCode" } $part1 = $normalized.Substring(0, 8) $part2 = $normalized.Substring(8, 4) $part3 = $normalized.Substring(12, 4) $part4 = $normalized.Substring(16, 4) $part5 = $normalized.Substring(20, 12) return ( (Convert-SACStringReverse -Value $part1) + (Convert-SACStringReverse -Value $part2) + (Convert-SACStringReverse -Value $part3) + (Convert-SACPairNibbleReverse -Value $part4) + (Convert-SACPairNibbleReverse -Value $part5) ).ToUpperInvariant() } function ConvertFrom-SACPackedMsiCode { <# .SYNOPSIS Converts a packed MSI registry product code back to ProductCode GUID form. #> param( [Parameter(Mandatory=$true)] [string]$PackedCode ) $normalized = $PackedCode.Trim().Trim('{', '}').Replace('-', '').ToUpperInvariant() if ($normalized -notmatch '^[0-9A-F]{32}$') { throw "Invalid packed MSI product code: $PackedCode" } $part1 = Convert-SACStringReverse -Value $normalized.Substring(0, 8) $part2 = Convert-SACStringReverse -Value $normalized.Substring(8, 4) $part3 = Convert-SACStringReverse -Value $normalized.Substring(12, 4) $part4 = Convert-SACPairNibbleReverse -Value $normalized.Substring(16, 4) $part5 = Convert-SACPairNibbleReverse -Value $normalized.Substring(20, 12) return "{$part1-$part2-$part3-$part4-$part5}".ToUpperInvariant() } function Format-SACProductCode { param([string]$Value) if ([string]::IsNullOrWhiteSpace($Value)) { return $null } $match = [regex]::Match($Value, '\{?[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}\}?') if (-not $match.Success) { return $null } $guidText = $match.Value.Trim('{', '}').ToUpperInvariant() return "{$guidText}" } function Test-SACPackedMsiCode { param([string]$Value) return (-not [string]::IsNullOrWhiteSpace($Value) -and $Value.Trim() -match '^[0-9A-Fa-f]{32}$') } function Test-SACAbsoluteInstallerPath { <# .SYNOPSIS Returns true when an installer-derived path is drive-rooted, UNC-rooted, or env-var rooted. #> param([string]$PathValue) if ([string]::IsNullOrWhiteSpace($PathValue)) { return $true } $candidate = $PathValue.Trim().Trim('"') if ($candidate -match '^[A-Za-z]:[\\/]') { return $true } if ($candidate -match '^\\\\[^\\/]+[\\/][^\\/]+') { return $true } if ($candidate -match '^%[A-Za-z_][A-Za-z0-9_()]*%([\\/]|$)') { return $true } if ($candidate -match '^\$env:[A-Za-z_][A-Za-z0-9_()]*([\\/]|$)') { return $true } if ($candidate -match '^\$\{env:[^}]+\}([\\/]|$)') { return $true } return $false } function Add-SACUniqueString { param( [string[]]$Values, [string]$Value ) if ([string]::IsNullOrWhiteSpace($Value)) { return @($Values) } $trimmed = $Value.Trim() if ($Values -notcontains $trimmed) { return @($Values + $trimmed) } return @($Values) } function Add-SACContextProductCode { param( [psobject]$Context, [string]$ProductCode ) $formatted = Format-SACProductCode -Value $ProductCode if (-not $formatted) { return } $Context.ProductCodes = Add-SACUniqueString -Values $Context.ProductCodes -Value $formatted try { $Context.PackedCodes = Add-SACUniqueString -Values $Context.PackedCodes -Value (ConvertTo-SACPackedMsiCode -ProductCode $formatted) } catch { $null = $_ } } function Add-SACContextPackedCode { param( [psobject]$Context, [string]$PackedCode ) if (-not (Test-SACPackedMsiCode -Value $PackedCode)) { return } $packed = $PackedCode.Trim().ToUpperInvariant() $Context.PackedCodes = Add-SACUniqueString -Values $Context.PackedCodes -Value $packed try { $Context.ProductCodes = Add-SACUniqueString -Values $Context.ProductCodes -Value (ConvertFrom-SACPackedMsiCode -PackedCode $packed) } catch { $null = $_ } } function Add-SACContextUpgradeCode { param( [psobject]$Context, [string]$UpgradeCode, [string]$PackedUpgradeCode ) if ($UpgradeCode) { $formatted = Format-SACProductCode -Value $UpgradeCode if ($formatted) { $Context.UpgradeCodes = Add-SACUniqueString -Values $Context.UpgradeCodes -Value $formatted } } if (Test-SACPackedMsiCode -Value $PackedUpgradeCode) { $packed = $PackedUpgradeCode.Trim().ToUpperInvariant() $Context.UpgradePackedCodes = Add-SACUniqueString -Values $Context.UpgradePackedCodes -Value $packed try { $Context.UpgradeCodes = Add-SACUniqueString -Values $Context.UpgradeCodes -Value (ConvertFrom-SACPackedMsiCode -PackedCode $packed) } catch { $null = $_ } } } function Add-SACContextPackageId { param( [psobject]$Context, [string]$PackageId ) if ([string]::IsNullOrWhiteSpace($PackageId)) { return } $clean = $PackageId.Trim().Trim('"', "'", ',', ';') if ($clean.Length -lt 4) { return } $Context.PackageIds = Add-SACUniqueString -Values $Context.PackageIds -Value $clean } function Get-SACProductCodesFromText { param([string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return @() } $regexMatches = [regex]::Matches($Text, '\{?[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}\}?') $results = @() foreach ($match in $regexMatches) { $formatted = Format-SACProductCode -Value $match.Value if ($formatted -and $results -notcontains $formatted) { $results += $formatted } } return $results } function Get-SACPackageIdsFromText { param([string]$Text) if ([string]::IsNullOrWhiteSpace($Text)) { return @() } $results = @() $patterns = @( '(?i)\bUPI2?\b\s*["'']?\s*[:=]\s*["'']?([A-Za-z0-9._{}\-]+)', '(?i)\bPackage(?:Id|ID|_ID|_id)?\b\s*["'']?\s*[:=]\s*["'']?([A-Za-z0-9._{}\-]+)', '(?i)\bPackageCode\b\s*["'']?\s*[:=]\s*["'']?([A-Za-z0-9._{}\-]+)' ) foreach ($pattern in $patterns) { foreach ($match in [regex]::Matches($Text, $pattern)) { $value = $match.Groups[1].Value.Trim().Trim('"', "'", ',', ';') if ($value.Length -ge 4 -and $results -notcontains $value) { $results += $value } } } return $results } function Test-SACContextTextMatch { param( [psobject]$Context, [string[]]$Text ) $joined = if ($Text.Count -eq 1) { [string]$Text[0] } else { (@($Text) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join " " } if ([string]::IsNullOrWhiteSpace($joined)) { return $false } foreach ($code in @($Context.ProductCodes + $Context.PackedCodes + $Context.UpgradeCodes + $Context.UpgradePackedCodes + $Context.PackageIds)) { if (-not [string]::IsNullOrWhiteSpace($code) -and $joined.IndexOf($code, [StringComparison]::OrdinalIgnoreCase) -ge 0) { return $true } } # The product name is a literal, so an ordinal IndexOf beats a compiled regex here. # This runs once per candidate per context - hundreds of thousands of times on a # default cleanup - so the constant factor is the whole cost of the scan. if ($joined.IndexOf([string]$Context.Product, [StringComparison]::OrdinalIgnoreCase) -lt 0) { return $false } if ($Context.Year -eq '*') { return $true } return ($joined -match "\b$([regex]::Escape([string]$Context.Year))\b") } function Get-SACContextMatchedTerm { param( [psobject]$Context, [string[]]$Text ) $joined = (@($Text) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join " " foreach ($code in @($Context.ProductCodes + $Context.PackedCodes + $Context.UpgradeCodes + $Context.UpgradePackedCodes + $Context.PackageIds)) { if (-not [string]::IsNullOrWhiteSpace($code) -and $joined.IndexOf($code, [StringComparison]::OrdinalIgnoreCase) -ge 0) { return $code } } if ($joined -match [regex]::Escape([string]$Context.Product) -and ($Context.Year -eq '*' -or $joined -match "\b$([regex]::Escape([string]$Context.Year))\b")) { return "$($Context.Product) $($Context.Year)" } return $null } function Get-SACTargetContext { param( [string[]]$TargetProducts, [string[]]$TargetYears, [object[]]$UninstallKeys, [string[]]$KnownProductCodes, [string[]]$KnownPackedCodes ) $contexts = @() foreach ($product in $TargetProducts) { foreach ($year in $TargetYears) { $contexts += [PSCustomObject]@{ Product = [string]$product Year = [string]$year ProductCodes = @() PackedCodes = @() UpgradeCodes = @() UpgradePackedCodes = @() PackageIds = @() } } } foreach ($context in $contexts) { foreach ($app in @($UninstallKeys)) { $text = @( $app.DisplayName, $app.DisplayVersion, $app.InstallLocation, $app.InstallSource, $app.LocalPackage, $app.UninstallString, $app.QuietUninstallString, $app.PSChildName ) if (Test-SACContextTextMatch -Context $context -Text $text) { Add-SACContextProductCode -Context $context -ProductCode $app.PSChildName foreach ($code in (Get-SACProductCodesFromText -Text ($text -join " "))) { Add-SACContextProductCode -Context $context -ProductCode $code } } } foreach ($code in @($KnownProductCodes)) { Add-SACContextProductCode -Context $context -ProductCode $code } foreach ($packed in @($KnownPackedCodes)) { Add-SACContextPackedCode -Context $context -PackedCode $packed } } return $contexts } function Get-SACRegistryValue { param( [Microsoft.Win32.RegistryKey]$Key, [string[]]$Names ) if ($null -eq $Key) { return $null } foreach ($name in $Names) { try { $value = $Key.GetValue($name) if ($null -ne $value -and -not [string]::IsNullOrWhiteSpace([string]$value)) { return [string]$value } } catch { $null = $_ } } return $null } function Get-SACInstallerPathWarning { param( [psobject]$Context, [hashtable]$Values, [string]$Source, [string]$SourcePath, [string]$DisplayName ) $pathNamePattern = '^(ADSK_INSTALL_PATH|INSTALLDIR|ARPINSTALLLOCATION|InstallLocation|InstallSource|LocalPackage|TARGETDIR|ROOTDRIVE)$' $warnings = @() foreach ($key in $Values.Keys) { if ($key -notmatch $pathNamePattern) { continue } $value = [string]$Values[$key] if ([string]::IsNullOrWhiteSpace($value)) { continue } if (Test-SACAbsoluteInstallerPath -PathValue $value) { continue } $warnings += [PSCustomObject]@{ Product = $Context.Product Year = $Context.Year Source = $Source SourcePath = $SourcePath DisplayName = $DisplayName PropertyName = $key Value = $value } } return $warnings } function Get-SACTextPathWarning { param( [psobject]$Context, [string]$Text, [string]$Source, [string]$SourcePath, [string]$DisplayName ) if ([string]::IsNullOrWhiteSpace($Text)) { return @() } $warnings = @() $pattern = '(?im)\b(ADSK_INSTALL_PATH|INSTALLDIR|ARPINSTALLLOCATION)\b\s*["'']?\s*[:=]\s*["'']?([^"''\r\n,;]+)' foreach ($match in [regex]::Matches($Text, $pattern)) { $name = $match.Groups[1].Value $value = $match.Groups[2].Value.Trim() if ([string]::IsNullOrWhiteSpace($value)) { continue } if (Test-SACAbsoluteInstallerPath -PathValue $value) { continue } $warnings += [PSCustomObject]@{ Product = $Context.Product Year = $Context.Year Source = $Source SourcePath = $SourcePath DisplayName = $DisplayName PropertyName = $name Value = $value } } return $warnings } function Get-SACMsiProductState { param([string[]]$ProductCodes) $results = @() $installer = $null try { $installer = New-Object -ComObject WindowsInstaller.Installer } catch { $comError = $_.Exception.Message foreach ($code in @($ProductCodes | Select-Object -Unique)) { $packedCode = $null try { $packedCode = ConvertTo-SACPackedMsiCode -ProductCode $code } catch { $null = $_ } $results += [PSCustomObject]@{ ProductCode = $code PackedCode = $packedCode ProductState = $null IsInstalled = $false Error = $comError } } return $results } foreach ($code in @($ProductCodes | Where-Object { $_ } | Select-Object -Unique)) { $packedCode = $null try { $packedCode = ConvertTo-SACPackedMsiCode -ProductCode $code } catch { $null = $_ } try { $state = $installer.ProductState($code) $results += [PSCustomObject]@{ ProductCode = $code PackedCode = $packedCode ProductState = $state IsInstalled = ($state -eq 5) Error = $null } } catch { $results += [PSCustomObject]@{ ProductCode = $code PackedCode = $packedCode ProductState = $null IsInstalled = $false Error = $_.Exception.Message } } } return $results } function Get-SACSearchableOdisFile { param([string]$FolderPath) $extensions = @('.json', '.xml', '.txt', '.ini', '.log', '.properties', '.manifest', '.pit', '.yaml', '.yml') # Content libraries (RVT/ACAD family/template payloads) are the bulk of the volume under # ProgramData\Autodesk and never carry installer metadata, so they are skipped outright. $skipPattern = '(?i)\\(Libraries|Library|Content|Templates|Families|Materials|Samples|Lookup Tables)\\' try { return @(Get-ChildItem -LiteralPath $FolderPath -Recurse -File -ErrorAction SilentlyContinue | Where-Object { $_.Length -le 5242880 -and $_.FullName -notmatch $skipPattern -and ($extensions -contains $_.Extension.ToLowerInvariant() -or $_.Name -match '(?i)(manifest|setup|package|deployment|bundle|collection)') } | Select-Object -First 250) } catch { return @() } } function Get-SACOdisFileTextCache { <# .SYNOPSIS Reads a folder's searchable files once, for reuse across every target context. #> param([string]$FolderPath) $cache = @() foreach ($file in (Get-SACSearchableOdisFile -FolderPath $FolderPath)) { $text = Read-SACFileText -Path $file.FullName if ([string]::IsNullOrWhiteSpace($text)) { continue } $cache += [PSCustomObject]@{ Name = $file.Name; FullName = $file.FullName; Text = $text } } return @($cache) } function Read-SACFileText { param([string]$Path) try { return [System.IO.File]::ReadAllText($Path) } catch { try { $bytes = [System.IO.File]::ReadAllBytes($Path) return [System.Text.Encoding]::UTF8.GetString($bytes) } catch { return $null } } } function Get-SACOdisRemnant { param([psobject[]]$Contexts) $results = @() $warnings = @() # Quick-reject filter. Testing every file against every product/year context is # 243 regex passes per file on a default cleanup; a file that mentions no target # product and no target year cannot match any context, and two regexes settle that. $productAlternation = (@($Contexts | ForEach-Object { [regex]::Escape([string]$_.Product) } | Select-Object -Unique) -join '|') $yearAlternation = (@($Contexts | ForEach-Object { [string]$_.Year } | Where-Object { $_ -ne '*' } | ForEach-Object { [regex]::Escape($_) } | Select-Object -Unique) -join '|') $anyYearContext = @($Contexts | Where-Object { [string]$_.Year -eq '*' }).Count -gt 0 $programData = if ($env:ProgramData) { $env:ProgramData } else { 'C:\ProgramData' } $roots = @( [PSCustomObject]@{ Source = 'ODIS Metadata'; Path = (Join-Path $programData 'Autodesk\ODIS\metadata'); Action = 'Rename Folder' }, [PSCustomObject]@{ Source = 'ODIS Extract'; Path = (Join-Path $programData 'Autodesk\ODIS\extract'); Action = 'Rename Folder' } ) foreach ($root in $roots) { if (-not (Test-Path -LiteralPath $root.Path)) { continue } $folders = @(Get-ChildItem -LiteralPath $root.Path -Directory -ErrorAction SilentlyContinue) foreach ($folder in $folders) { # One folder produces at most one remnant record. Without this the file scan and # its log lines were repeated once per product/year context - 243 passes over the # same files on a default cleanup - which is what made the scan take 23 minutes # and flooded the log with identical "Queued ODIS metadata folder" lines. $fileCache = $null $folderQueued = $false foreach ($context in $Contexts) { if ($folderQueued) { break } $matched = Test-SACContextTextMatch -Context $context -Text @($folder.Name, $folder.FullName) $matchedTerm = if ($matched) { Get-SACContextMatchedTerm -Context $context -Text @($folder.Name, $folder.FullName) } else { $null } $matchedFile = $null $identifierText = $folder.Name if (-not $matched) { if ($null -eq $fileCache) { $fileCache = @(Get-SACOdisFileTextCache -FolderPath $folder.FullName | Where-Object { ($productAlternation -and $_.Text -match $productAlternation) -and ($anyYearContext -or ($yearAlternation -and $_.Text -match $yearAlternation)) }) } foreach ($file in $fileCache) { if (Test-SACContextTextMatch -Context $context -Text @($file.Name, $file.Text)) { $matched = $true $matchedFile = $file.FullName $matchedTerm = Get-SACContextMatchedTerm -Context $context -Text @($file.Name, $file.Text) $identifierText = $file.Text $warnings += Get-SACTextPathWarning -Context $context -Text $file.Text -Source $root.Source -SourcePath $file.FullName -DisplayName $folder.Name break } } } if (-not $matched) { continue } foreach ($packageId in (Get-SACPackageIdsFromText -Text $identifierText)) { Add-SACContextPackageId -Context $context -PackageId $packageId } foreach ($code in (Get-SACProductCodesFromText -Text $identifierText)) { Add-SACContextProductCode -Context $context -ProductCode $code } $folderQueued = $true $results += [PSCustomObject]@{ Product = $context.Product Year = $context.Year Source = $root.Source Action = $root.Action Path = $folder.FullName DisplayName = $folder.Name MatchedTerm = $matchedTerm MatchedFile = $matchedFile PackageIds = ($context.PackageIds -join '; ') ProductCodes = ($context.ProductCodes -join '; ') PackedCodes = ($context.PackedCodes -join '; ') UpgradeCodes = ($context.UpgradeCodes -join '; ') } } } } $installDb = Join-Path $programData 'Autodesk\ODIS\Install.db' if (Test-Path -LiteralPath $installDb) { $dbText = Read-SACFileText -Path $installDb if (-not [string]::IsNullOrWhiteSpace($dbText)) { foreach ($context in $Contexts) { if (Test-SACContextTextMatch -Context $context -Text $dbText) { $results += [PSCustomObject]@{ Product = $context.Product Year = $context.Year Source = 'ODIS Install.db' Action = 'Report Only' Path = $installDb DisplayName = 'Install.db' MatchedTerm = (Get-SACContextMatchedTerm -Context $context -Text $dbText) MatchedFile = $installDb PackageIds = ($context.PackageIds -join '; ') ProductCodes = ($context.ProductCodes -join '; ') PackedCodes = ($context.PackedCodes -join '; ') UpgradeCodes = ($context.UpgradeCodes -join '; ') } $warnings += Get-SACTextPathWarning -Context $context -Text $dbText -Source 'ODIS Install.db' -SourcePath $installDb -DisplayName 'Install.db' } } } } return [PSCustomObject]@{ Remnants = @($results) Warnings = @($warnings) } } function Get-SACCodeMatchedContext { <# .SYNOPSIS Returns the target contexts whose product/packed codes appear in a registry value. .DESCRIPTION Tests each distinct code once and fans the hit out to the contexts that own it, instead of re-testing every code for every context. #> param( [hashtable]$CodeContextMap, [string[]]$DistinctCodes, [string]$ValueName, [string]$ValueData ) if ($null -eq $CodeContextMap -or $DistinctCodes.Count -eq 0) { return @() } $matchedContexts = @() $seen = @{} foreach ($code in $DistinctCodes) { $hit = $false if (-not [string]::IsNullOrEmpty($ValueName) -and $ValueName.IndexOf($code, [StringComparison]::OrdinalIgnoreCase) -ge 0) { $hit = $true } elseif (-not [string]::IsNullOrEmpty($ValueData) -and $ValueData.IndexOf($code, [StringComparison]::OrdinalIgnoreCase) -ge 0) { $hit = $true } if (-not $hit) { continue } foreach ($context in $CodeContextMap[$code]) { $key = "$($context.Product)|$($context.Year)" if ($seen.ContainsKey($key)) { continue } $seen[$key] = $true $matchedContexts += $context } } return @($matchedContexts) } function Get-SACHiddenInstallerState { <# .SYNOPSIS Finds hidden Autodesk MSI installer-state and ODIS remnants for targeted products/years. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string[]]$TargetProducts, [Parameter(Mandatory=$true)] [string[]]$TargetYears, [object[]]$UninstallKeys = @(), [string[]]$KnownProductCodes = @(), [string[]]$KnownPackedCodes = @() ) $contexts = @(Get-SACTargetContext -TargetProducts $TargetProducts -TargetYears $TargetYears -UninstallKeys $UninstallKeys -KnownProductCodes $KnownProductCodes -KnownPackedCodes $KnownPackedCodes) $hiddenProducts = @() $componentRefs = @() $upgradeRefs = @() $relativeWarnings = @() $productRoots = @( [PSCustomObject]@{ Source = 'UserData Products'; ProviderPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products'; SubPath = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Products' }, [PSCustomObject]@{ Source = 'Classes Products'; ProviderPath = 'HKLM:\SOFTWARE\Classes\Installer\Products'; SubPath = 'SOFTWARE\Classes\Installer\Products' } ) $productBaseKey = $null try { $productBaseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) foreach ($root in $productRoots) { $productRootKey = $productBaseKey.OpenSubKey($root.SubPath) if (-not $productRootKey) { continue } try { foreach ($packedName in $productRootKey.GetSubKeyNames()) { $packedCode = $packedName.ToUpperInvariant() if (-not (Test-SACPackedMsiCode -Value $packedCode)) { continue } $productCode = $null try { $productCode = ConvertFrom-SACPackedMsiCode -PackedCode $packedCode } catch { $null = $_ } $productKeyPath = "$($root.ProviderPath)\$packedName" $installPropertiesPath = "$productKeyPath\InstallProperties" $productKey = $productRootKey.OpenSubKey($packedName) if (-not $productKey) { continue } $propsKey = $null $hadPropsKey = $false $values = @{} try { $propsKey = $productKey.OpenSubKey('InstallProperties') if ($propsKey) { $hadPropsKey = $true foreach ($name in $propsKey.GetValueNames()) { $values[$name] = [string]$propsKey.GetValue($name) } } foreach ($name in $productKey.GetValueNames()) { if (-not $values.ContainsKey($name)) { $values[$name] = [string]$productKey.GetValue($name) } } } finally { if ($propsKey) { $propsKey.Close() } $productKey.Close() } $displayName = $values['DisplayName'] if ([string]::IsNullOrWhiteSpace($displayName)) { $displayName = $values['ProductName'] } # Joined once per key, not once per key per context. $text = @(@( $displayName, $values['DisplayVersion'], $values['InstallLocation'], $values['InstallSource'], $values['LocalPackage'], $values['UninstallString'], $productCode, $packedCode ) | Where-Object { -not [string]::IsNullOrWhiteSpace($_) }) -join ' ' foreach ($context in $contexts) { if (-not (Test-SACContextTextMatch -Context $context -Text $text)) { continue } if ($productCode) { Add-SACContextProductCode -Context $context -ProductCode $productCode } Add-SACContextPackedCode -Context $context -PackedCode $packedCode $hiddenProducts += [PSCustomObject]@{ Product = $context.Product Year = $context.Year Source = $root.Source ProductCode = $productCode PackedCode = $packedCode DisplayName = $displayName DisplayVersion = $values['DisplayVersion'] InstallLocation = $values['InstallLocation'] InstallSource = $values['InstallSource'] LocalPackage = $values['LocalPackage'] UninstallString = $values['UninstallString'] RegistryPath = $productKeyPath PropertiesPath = if ($hadPropsKey) { $installPropertiesPath } else { $null } } $relativeWarnings += Get-SACInstallerPathWarning -Context $context -Values $values -Source $root.Source -SourcePath $productKeyPath -DisplayName $displayName } } } finally { $productRootKey.Close() } } $featureProviderPath = 'HKLM:\SOFTWARE\Classes\Installer\Features' $featureRootKey = $productBaseKey.OpenSubKey('SOFTWARE\Classes\Installer\Features') if ($featureRootKey) { try { foreach ($packedName in $featureRootKey.GetSubKeyNames()) { $packedCode = $packedName.ToUpperInvariant() if (-not (Test-SACPackedMsiCode -Value $packedCode)) { continue } $productCode = $null try { $productCode = ConvertFrom-SACPackedMsiCode -PackedCode $packedCode } catch { $null = $_ } foreach ($context in $contexts) { $isMatch = ($context.PackedCodes -contains $packedCode) -or ($productCode -and $context.ProductCodes -contains $productCode) if (-not $isMatch) { continue } $featureKeyPath = "$featureProviderPath\$packedName" $featureNames = @() $featureKey = $featureRootKey.OpenSubKey($packedName) if ($featureKey) { try { $featureNames = @($featureKey.GetValueNames()) } finally { $featureKey.Close() } } $hiddenProducts += [PSCustomObject]@{ Product = $context.Product Year = $context.Year Source = 'Classes Features' ProductCode = $productCode PackedCode = $packedCode DisplayName = "MSI feature registration ($($featureNames.Count) feature value(s))" DisplayVersion = $null InstallLocation = $null InstallSource = $null LocalPackage = $null UninstallString = $null RegistryPath = $featureKeyPath PropertiesPath = $null } } } } finally { $featureRootKey.Close() } } } catch { Write-SACQuietLog "Hidden MSI product/feature scan failed: $($_.Exception.Message)" } finally { if ($productBaseKey) { $productBaseKey.Close() } } foreach ($context in $contexts) { foreach ($app in @($UninstallKeys)) { $values = @{} foreach ($name in @('InstallLocation', 'InstallSource', 'LocalPackage')) { if ($null -ne $app.$name) { $values[$name] = [string]$app.$name } } if ($values.Count -gt 0 -and (Test-SACContextTextMatch -Context $context -Text @($app.DisplayName, $app.InstallLocation, $app.InstallSource, $app.LocalPackage))) { $relativeWarnings += Get-SACInstallerPathWarning -Context $context -Values $values -Source 'Add/Remove Programs' -SourcePath $app.PSPath -DisplayName $app.DisplayName } } } $allProductCodes = @() foreach ($context in $contexts) { $allProductCodes += $context.ProductCodes } $productStates = @(Get-SACMsiProductState -ProductCodes ($allProductCodes | Where-Object { $_ } | Select-Object -Unique)) $referenceContexts = @($contexts | Where-Object { @($_.ProductCodes + $_.PackedCodes).Count -gt 0 }) # The Components hive holds tens of thousands of keys. Testing every value against # every context's every code was O(keys x contexts x codes) and dominated the run # time (measured: 23 minutes on a machine with 243 contexts). Codes are indexed once # so each value is tested against the small set of *distinct* codes instead. $codeContextMap = @{} foreach ($context in $referenceContexts) { foreach ($code in @($context.ProductCodes + $context.PackedCodes)) { if ([string]::IsNullOrWhiteSpace($code)) { continue } $key = $code.Trim().ToUpperInvariant() if (-not $codeContextMap.ContainsKey($key)) { $codeContextMap[$key] = @() } $codeContextMap[$key] += $context } } $distinctCodes = @($codeContextMap.Keys) $baseKey = $null try { $baseKey = [Microsoft.Win32.RegistryKey]::OpenBaseKey([Microsoft.Win32.RegistryHive]::LocalMachine, [Microsoft.Win32.RegistryView]::Default) $componentSubPath = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UserData\S-1-5-18\Components' $componentKeyRoot = $baseKey.OpenSubKey($componentSubPath) if ($componentKeyRoot -and $referenceContexts.Count -gt 0) { # The Components hive routinely holds 75,000+ keys, so the scan is minutes long # even at one pass. Report progress: a silent multi-minute phase reads as a hang. $componentNames = @($componentKeyRoot.GetSubKeyNames()) $componentTotal = $componentNames.Count $componentSeen = 0 Write-SACMsg " Scanning $componentTotal MSI component registration(s)..." "Info" foreach ($componentName in $componentNames) { $componentSeen++ if (($componentSeen % 5000) -eq 0) { Write-Host "`r Components scanned: $componentSeen / $componentTotal" -NoNewline -ForegroundColor DarkGray } $componentKey = $componentKeyRoot.OpenSubKey($componentName) if (-not $componentKey) { continue } try { foreach ($valueName in $componentKey.GetValueNames()) { $valueData = [string]$componentKey.GetValue($valueName) foreach ($context in (Get-SACCodeMatchedContext -CodeContextMap $codeContextMap -DistinctCodes $distinctCodes -ValueName $valueName -ValueData $valueData)) { $componentRefs += [PSCustomObject]@{ Product = $context.Product Year = $context.Year ComponentKey = $componentName RegistryPath = "HKLM:\$componentSubPath\$componentName" ValueName = $valueName ValueData = $valueData MatchedTerm = Get-SACContextMatchedTerm -Context $context -Text @($valueName, $valueData) } } } } finally { $componentKey.Close() } } if ($componentTotal -ge 5000) { Write-Host "`r$(' ' * 60)`r" -NoNewline } $componentKeyRoot.Close() } $upgradeRoots = @( [PSCustomObject]@{ ProviderPath = 'HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes'; SubPath = 'SOFTWARE\Microsoft\Windows\CurrentVersion\Installer\UpgradeCodes' }, [PSCustomObject]@{ ProviderPath = 'HKLM:\SOFTWARE\Classes\Installer\UpgradeCodes'; SubPath = 'SOFTWARE\Classes\Installer\UpgradeCodes' } ) foreach ($upgradeRoot in $upgradeRoots) { $upgradeKeyRoot = $baseKey.OpenSubKey($upgradeRoot.SubPath) if (-not $upgradeKeyRoot -or $referenceContexts.Count -eq 0) { continue } try { foreach ($upgradeName in $upgradeKeyRoot.GetSubKeyNames()) { $upgradePackedCode = $upgradeName.ToUpperInvariant() $upgradeCode = $null if (Test-SACPackedMsiCode -Value $upgradePackedCode) { try { $upgradeCode = ConvertFrom-SACPackedMsiCode -PackedCode $upgradePackedCode } catch { $null = $_ } } $upgradeKey = $upgradeKeyRoot.OpenSubKey($upgradeName) if (-not $upgradeKey) { continue } try { foreach ($valueName in $upgradeKey.GetValueNames()) { $valueData = [string]$upgradeKey.GetValue($valueName) foreach ($context in (Get-SACCodeMatchedContext -CodeContextMap $codeContextMap -DistinctCodes $distinctCodes -ValueName $valueName -ValueData $valueData)) { Add-SACContextUpgradeCode -Context $context -UpgradeCode $upgradeCode -PackedUpgradeCode $upgradePackedCode $upgradeRefs += [PSCustomObject]@{ Product = $context.Product Year = $context.Year Source = $upgradeRoot.ProviderPath RegistryPath = "$($upgradeRoot.ProviderPath)\$upgradeName" UpgradeCode = $upgradeCode UpgradePackedCode = $upgradePackedCode ValueName = $valueName ValueData = $valueData MatchedTerm = Get-SACContextMatchedTerm -Context $context -Text @($valueName, $valueData) } } } } finally { $upgradeKey.Close() } } } finally { $upgradeKeyRoot.Close() } } } catch { Write-SACQuietLog "Hidden MSI component/upgrade scan failed: $($_.Exception.Message)" } finally { if ($baseKey) { $baseKey.Close() } } $odis = Get-SACOdisRemnant -Contexts $contexts $relativeWarnings += @($odis.Warnings) $uniqueHiddenCodes = @($hiddenProducts | Where-Object { $_.ProductCode } | Select-Object -ExpandProperty ProductCode -Unique) $installedStates = @($productStates | Where-Object { $_.IsInstalled }) return [PSCustomObject]@{ HiddenProducts = @($hiddenProducts | Select-Object -Unique *) ProductStates = @($productStates | Select-Object -Unique *) ComponentReferences = @($componentRefs | Select-Object -Unique *) UpgradeCodeReferences = @($upgradeRefs | Select-Object -Unique *) OdisRemnants = @($odis.Remnants | Select-Object -Unique *) RelativePathWarnings = @($relativeWarnings | Select-Object -Unique *) Contexts = @($contexts) Summary = [PSCustomObject]@{ HiddenMsiProducts = $uniqueHiddenCodes.Count ProductStateInstalled = $installedStates.Count ComponentReferences = @($componentRefs | Select-Object -Unique *).Count UpgradeCodeReferences = @($upgradeRefs | Select-Object -Unique *).Count OdisRemnants = @($odis.Remnants | Select-Object -Unique *).Count RelativePathWarnings = @($relativeWarnings | Select-Object -Unique *).Count } } } function Convert-SACRegistryPathToNative { param([string]$RegistryPath) if ([string]::IsNullOrWhiteSpace($RegistryPath)) { return $null } $path = $RegistryPath $path = $path -replace '^Microsoft\.PowerShell\.Core\\Registry::', '' $path = $path -replace '^HKLM:\\', 'HKEY_LOCAL_MACHINE\' $path = $path -replace '^HKCU:\\', 'HKEY_CURRENT_USER\' $path = $path -replace '^HKCR:\\', 'HKEY_CLASSES_ROOT\' $path = $path -replace '^HKU:\\', 'HKEY_USERS\' return $path } function Backup-SACRegistryKey { param( [string]$RegistryPath, [string]$LogDir, [string]$Prefix = 'Registry' ) if ([string]::IsNullOrWhiteSpace($LogDir) -or [string]::IsNullOrWhiteSpace($RegistryPath)) { return $null } if (-not (Test-Path -LiteralPath $RegistryPath)) { return $null } $backupDir = Join-Path $LogDir 'RegistryBackups' New-Item -ItemType Directory -Path $backupDir -Force -ErrorAction SilentlyContinue | Out-Null $safeName = ($Prefix + '_' + ($RegistryPath -replace '[\\/:*?"<>|{}\s]', '_')).Trim('_') if ($safeName.Length -gt 160) { $safeName = $safeName.Substring(0, 160) } $backupPath = Join-Path $backupDir "$safeName.reg" $nativePath = Convert-SACRegistryPathToNative -RegistryPath $RegistryPath try { & reg.exe export "$nativePath" "$backupPath" /y 2>&1 | Out-Null if (Test-Path -LiteralPath $backupPath) { Write-SACQuietLog "Backed up registry key $RegistryPath to $backupPath" return $backupPath } } catch { Write-SACQuietLog "Failed to back up registry key $($RegistryPath): $($_.Exception.Message)" } return $null } function Invoke-SACHiddenInstallerStateCleanup { <# .SYNOPSIS Removes hidden MSI installer-state references and renames targeted ODIS folders. #> [CmdletBinding()] param( [Parameter(Mandatory=$true)] [psobject]$State, [Parameter(Mandatory=$true)] [string]$LogDir ) $result = [PSCustomObject]@{ HiddenMsiProductCodes = @() ProductStateInstalledCodes = @() HiddenMsiProductKeysRemoved = 0 ComponentRefsRemoved = 0 UpgradeCodeRefsRemoved = 0 OdisFoldersRenamed = 0 } foreach ($stateItem in @($State.ProductStates | Where-Object { $_.IsInstalled })) { $result.ProductStateInstalledCodes = Add-SACUniqueString -Values $result.ProductStateInstalledCodes -Value $stateItem.ProductCode } $hiddenPaths = @($State.HiddenProducts | Where-Object { $_.RegistryPath } | Sort-Object RegistryPath -Unique) foreach ($product in $hiddenPaths) { if ($product.ProductCode) { $result.HiddenMsiProductCodes = Add-SACUniqueString -Values $result.HiddenMsiProductCodes -Value $product.ProductCode } if (-not (Test-Path -LiteralPath $product.RegistryPath)) { continue } Backup-SACRegistryKey -RegistryPath $product.RegistryPath -LogDir $LogDir -Prefix 'HiddenMsiProduct' | Out-Null try { Remove-Item -LiteralPath $product.RegistryPath -Recurse -Force -ErrorAction Stop $result.HiddenMsiProductKeysRemoved++ Write-SACMsg "Removed hidden MSI registration: $($product.DisplayName) [$($product.PackedCode)]" "Success" Write-SACQuietLog "Removed hidden MSI registry key: $($product.RegistryPath)" } catch { Write-SACQuietLog "Failed to remove hidden MSI key $($product.RegistryPath): $($_.Exception.Message)" $script:SACFailures += [PSCustomObject]@{ Component = "Hidden MSI Registry: $($product.DisplayName)"; Reason = $_.Exception.Message; Severity = 'Warning' } } } $componentRefs = @($State.ComponentReferences | Where-Object { $_.RegistryPath -and $_.ValueName } | Sort-Object RegistryPath, ValueName -Unique) foreach ($ref in $componentRefs) { if (-not (Test-Path -LiteralPath $ref.RegistryPath)) { continue } Backup-SACRegistryKey -RegistryPath $ref.RegistryPath -LogDir $LogDir -Prefix 'MsiComponentRef' | Out-Null try { Remove-ItemProperty -LiteralPath $ref.RegistryPath -Name $ref.ValueName -Force -ErrorAction Stop $result.ComponentRefsRemoved++ Write-SACMsg "Removed MSI component reference: $($ref.ValueName)" "Success" Write-SACQuietLog "Removed MSI component value $($ref.ValueName) from $($ref.RegistryPath)" } catch { Write-SACQuietLog "Failed to remove MSI component value $($ref.ValueName) from $($ref.RegistryPath): $($_.Exception.Message)" $script:SACFailures += [PSCustomObject]@{ Component = "MSI Component Ref: $($ref.ValueName)"; Reason = $_.Exception.Message; Severity = 'Warning' } } } $upgradeRefs = @($State.UpgradeCodeReferences | Where-Object { $_.RegistryPath -and $_.ValueName } | Sort-Object RegistryPath, ValueName -Unique) foreach ($ref in $upgradeRefs) { if (-not (Test-Path -LiteralPath $ref.RegistryPath)) { continue } Backup-SACRegistryKey -RegistryPath $ref.RegistryPath -LogDir $LogDir -Prefix 'MsiUpgradeRef' | Out-Null try { Remove-ItemProperty -LiteralPath $ref.RegistryPath -Name $ref.ValueName -Force -ErrorAction Stop $result.UpgradeCodeRefsRemoved++ Write-SACMsg "Removed MSI upgrade-code reference: $($ref.ValueName)" "Success" Write-SACQuietLog "Removed MSI upgrade-code value $($ref.ValueName) from $($ref.RegistryPath)" } catch { Write-SACQuietLog "Failed to remove MSI upgrade-code value $($ref.ValueName) from $($ref.RegistryPath): $($_.Exception.Message)" $script:SACFailures += [PSCustomObject]@{ Component = "MSI UpgradeCode Ref: $($ref.ValueName)"; Reason = $_.Exception.Message; Severity = 'Warning' } } } $timestamp = Get-Date -Format 'yyyyMMdd_HHmmss' $odisFolders = @($State.OdisRemnants | Where-Object { $_.Action -eq 'Rename Folder' -and $_.Path } | Sort-Object Path -Unique) foreach ($folder in $odisFolders) { if (-not (Test-Path -LiteralPath $folder.Path)) { continue } if ((Split-Path -Leaf $folder.Path) -match '\.SAC_bak_') { continue } $backupLeaf = "$(Split-Path -Leaf $folder.Path).SAC_bak_$timestamp" try { Rename-Item -LiteralPath $folder.Path -NewName $backupLeaf -Force -ErrorAction Stop $result.OdisFoldersRenamed++ Write-SACMsg "Renamed targeted ODIS folder: $($folder.Path) -> $backupLeaf" "Success" Write-SACQuietLog "Renamed targeted ODIS folder $($folder.Path) to $backupLeaf" } catch { Write-SACQuietLog "Failed to rename ODIS folder $($folder.Path): $($_.Exception.Message)" $script:SACFailures += [PSCustomObject]@{ Component = "ODIS Folder Rename: $($folder.Path)"; Reason = $_.Exception.Message; Severity = 'Warning' } } } return $result } function Get-SACHiddenInstallerStateFunctionBundle { $names = @( 'Convert-SACStringReverse', 'Convert-SACPairNibbleReverse', 'ConvertTo-SACPackedMsiCode', 'ConvertFrom-SACPackedMsiCode', 'Format-SACProductCode', 'Test-SACPackedMsiCode', 'Test-SACAbsoluteInstallerPath', 'Add-SACUniqueString', 'Add-SACContextProductCode', 'Add-SACContextPackedCode', 'Add-SACContextUpgradeCode', 'Add-SACContextPackageId', 'Get-SACProductCodesFromText', 'Get-SACPackageIdsFromText', 'Test-SACContextTextMatch', 'Get-SACContextMatchedTerm', 'Get-SACTargetContext', 'Get-SACRegistryValue', 'Get-SACInstallerPathWarning', 'Get-SACTextPathWarning', 'Get-SACMsiProductState', 'Get-SACSearchableOdisFile', 'Read-SACFileText', 'Get-SACOdisFileTextCache', 'Get-SACOdisRemnant', 'Get-SACCodeMatchedContext', 'Get-SACHiddenInstallerState' ) return (($names | ForEach-Object { $cmd = Get-Command -Name $_ -CommandType Function -ErrorAction Stop "function $($_) {`r`n$($cmd.ScriptBlock.ToString())`r`n}" }) -join "`r`n") } # SIG # Begin signature block # MII9NwYJKoZIhvcNAQcCoII9KDCCPSQCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCASCeQJ1uPQHbX2 # qB5hoC4RFf2rDnxdO5AClTwwlt7O3KCCIfwwggXMMIIDtKADAgECAhBUmNLR1FsZ # lUgTecgRwIeZMA0GCSqGSIb3DQEBDAUAMHcxCzAJBgNVBAYTAlVTMR4wHAYDVQQK # ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jvc29mdCBJZGVu # dGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAy # MDAeFw0yMDA0MTYxODM2MTZaFw00NTA0MTYxODQ0NDBaMHcxCzAJBgNVBAYTAlVT # MR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xSDBGBgNVBAMTP01pY3Jv # c29mdCBJZGVudGl0eSBWZXJpZmljYXRpb24gUm9vdCBDZXJ0aWZpY2F0ZSBBdXRo # b3JpdHkgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALORKgeD # Bmf9np3gx8C3pOZCBH8Ppttf+9Va10Wg+3cL8IDzpm1aTXlT2KCGhFdFIMeiVPvH # or+Kx24186IVxC9O40qFlkkN/76Z2BT2vCcH7kKbK/ULkgbk/WkTZaiRcvKYhOuD # PQ7k13ESSCHLDe32R0m3m/nJxxe2hE//uKya13NnSYXjhr03QNAlhtTetcJtYmrV # qXi8LW9J+eVsFBT9FMfTZRY33stuvF4pjf1imxUs1gXmuYkyM6Nix9fWUmcIxC70 # ViueC4fM7Ke0pqrrBc0ZV6U6CwQnHJFnni1iLS8evtrAIMsEGcoz+4m+mOJyoHI1 # vnnhnINv5G0Xb5DzPQCGdTiO0OBJmrvb0/gwytVXiGhNctO/bX9x2P29Da6SZEi3 # W295JrXNm5UhhNHvDzI9e1eM80UHTHzgXhgONXaLbZ7LNnSrBfjgc10yVpRnlyUK # xjU9lJfnwUSLgP3B+PR0GeUw9gb7IVc+BhyLaxWGJ0l7gpPKWeh1R+g/OPTHU3mg # trTiXFHvvV84wRPmeAyVWi7FQFkozA8kwOy6CXcjmTimthzax7ogttc32H83rwjj # O3HbbnMbfZlysOSGM1l0tRYAe1BtxoYT2v3EOYI9JACaYNq6lMAFUSw0rFCZE4e7 # swWAsk0wAly4JoNdtGNz764jlU9gKL431VulAgMBAAGjVDBSMA4GA1UdDwEB/wQE # AwIBhjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTIftJqhSobyhmYBAcnz1AQ # T2ioojAQBgkrBgEEAYI3FQEEAwIBADANBgkqhkiG9w0BAQwFAAOCAgEAr2rd5hnn # LZRDGU7L6VCVZKUDkQKL4jaAOxWiUsIWGbZqWl10QzD0m/9gdAmxIR6QFm3FJI9c # Zohj9E/MffISTEAQiwGf2qnIrvKVG8+dBetJPnSgaFvlVixlHIJ+U9pW2UYXeZJF # xBA2CFIpF8svpvJ+1Gkkih6PsHMNzBxKq7Kq7aeRYwFkIqgyuH4yKLNncy2RtNwx # AQv3Rwqm8ddK7VZgxCwIo3tAsLx0J1KH1r6I3TeKiW5niB31yV2g/rarOoDXGpc8 # FzYiQR6sTdWD5jw4vU8w6VSp07YEwzJ2YbuwGMUrGLPAgNW3lbBeUU0i/OxYqujY # lLSlLu2S3ucYfCFX3VVj979tzR/SpncocMfiWzpbCNJbTsgAlrPhgzavhgplXHT2 # 6ux6anSg8Evu75SjrFDyh+3XOjCDyft9V77l4/hByuVkrrOj7FjshZrM77nq81YY # uVxzmq/FdxeDWds3GhhyVKVB0rYjdaNDmuV3fJZ5t0GNv+zcgKCf0Xd1WF81E+Al # GmcLfc4l+gcK5GEh2NQc5QfGNpn0ltDGFf5Ozdeui53bFv0ExpK91IjmqaOqu/dk # ODtfzAzQNb50GQOmxapMomE2gj4d8yu8l13bS3g7LfU772Aj6PXsCyM2la+YZr9T # 03u4aUoqlmZpxJTG9F9urJh4iIAGXKKy7aIwggarMIIEk6ADAgECAhMzAAYgdEwI # qMbC9o3UAAAABiB0MA0GCSqGSIb3DQEBDAUAMFoxCzAJBgNVBAYTAlVTMR4wHAYD # VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29mdCBJ # RCBWZXJpZmllZCBDUyBBT0MgQ0EgMDMwHhcNMjYwOTA4MDM1NTU5WhcNMjYwOTEx # MDM1NTU5WjBtMQswCQYDVQQGEwJVUzELMAkGA1UECBMCZ2ExETAPBgNVBAcTCEJv # c3R3aWNrMR4wHAYDVQQKExVSaWNoYXJkIERhaWxlbiBHdW50ZXIxHjAcBgNVBAMT # FVJpY2hhcmQgRGFpbGVuIEd1bnRlcjCCAaIwDQYJKoZIhvcNAQEBBQADggGPADCC # AYoCggGBAMaVHw1xEKM/ypHgCoqzRLfs3ifk+jtX1alGru9yVQJl6XNgN3RQboVV # HGa4/Y4xvsfJKfcfYxr/2EuZM2dpsNmPVwAoLqRn9NrFSvM8EEIviwZjNZPFszwq # /nWUtCL1odXE6qpca2JXyYtWeGhE8XHP1LxLZ6NaAcBy1aCsliSSt45PjMqLWaw1 # 16YFWiuas9311NDYlxH5+Fz1Tqqo8SEeNioekzo0cJyDBkASuyQcf1LXy22LdNQQ # WCq6vw6bjjmS1tQZc06X6mMQYQnJdHVwqWSwzXPHtaMSXXApaiyY1SnpztOGNXF0 # Jd4u8TblLFpKmoPDCp3nmv6wHBtBwA60CVnGH0Ga3QTrqDBxUd8o7eEAhup2mOwp # U/epYC3fKOZhpq8FHnjBoxSKf6oa7LQfe53TwfdLmC5LRqROPp06FW87ifoWnPOp # I5HCNPdsokl9BoH2mBCEvywqrhUZxzZMGSvKlyGjvApE+bTNsF9N/NEzZXeQ1Eq1 # ZhybQzkyPQIDAQABo4IB1TCCAdEwDAYDVR0TAQH/BAIwADAOBgNVHQ8BAf8EBAMC # B4AwPAYDVR0lBDUwMwYKKwYBBAGCN2EBAAYIKwYBBQUHAwMGGysGAQQBgjdhoOiV # eoK/0ttMg5+ekiyCi8TIKTAdBgNVHQ4EFgQU4KW1b5S1TD1sjhK66eR2C9UMLBcw # HwYDVR0jBBgwFoAUpEMMf3ZapYXnPo0oDwwXokVpcMYwZwYDVR0fBGAwXjBcoFqg # WIZWaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWljcm9zb2Z0 # JTIwSUQlMjBWZXJpZmllZCUyMENTJTIwQU9DJTIwQ0ElMjAwMy5jcmwwdAYIKwYB # BQUHAQEEaDBmMGQGCCsGAQUFBzAChlhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20v # cGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElEJTIwVmVyaWZpZWQlMjBDUyUyMEFP # QyUyMENBJTIwMDMuY3J0MFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y # eS5odG0wDQYJKoZIhvcNAQEMBQADggIBABs65KrxSZxozLE987sSET+UmFgwaO77 # nelKKU/51CQqwtFhE8NVMc4RG3Jt2ExmVSDMxc9BJEXro0k1M5aI81qHSMU7gogF # C5Iug+/Y515Bst++G4nPiu+NsVbGlDrAPGho+NXDDg9xkCNOlga5qbl8LKmAQ/ar # p5L7BZY/qFhDFPvak0I89Y7wHEyQln8cXbysgOdKPbxbltNrDHb/JDsAU1H6Qy4P # wOcefY/z1/mPhIXMpFtMGCWtNfaSP1QfsPiYd5PdDFvJbChiVyxZA56hPBeNTRgF # y1Are1HEtvXXZePCIQSr0z6N6gdx8/b0DzRx9ABjhJzfnZaSO85fskIjWhl+/hcA # 1nhvhN2BZFX8UodOXgfu9DDJ8+1d22skEH0oyMj6WlFzGteArOEPH/s6v1diN5Wl # g3EaOvZ19eKLk92UO5dkWcw9bKiR5m/fZwo9XKTqR0bktovIGrKdp3FKTxemzq0P # a8V737OKHSingPM2zf1+1rIP+Z3jEieeCxiNkvxeadckSI9cC/sWcPKatof5tQ7f # x/kEOXbKmtSBy0lIkGTQMzd0rdg+ejHkICQBHQoNpLQXUDB8g1AQQbxp1t5y33VZ # Gkap7+f+72HnROJRTD8syZmM3O2ZP7UZgZ41gRVy3GMV0YmmAOMX+S7rZSw9wRLV # 3iXbj1569sk7MIIGqzCCBJOgAwIBAgITMwAGIHRMCKjGwvaN1AAAAAYgdDANBgkq # hkiG9w0BAQwFADBaMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSswKQYDVQQDEyJNaWNyb3NvZnQgSUQgVmVyaWZpZWQgQ1MgQU9D # IENBIDAzMB4XDTI2MDkwODAzNTU1OVoXDTI2MDkxMTAzNTU1OVowbTELMAkGA1UE # BhMCVVMxCzAJBgNVBAgTAmdhMREwDwYDVQQHEwhCb3N0d2ljazEeMBwGA1UEChMV # UmljaGFyZCBEYWlsZW4gR3VudGVyMR4wHAYDVQQDExVSaWNoYXJkIERhaWxlbiBH # dW50ZXIwggGiMA0GCSqGSIb3DQEBAQUAA4IBjwAwggGKAoIBgQDGlR8NcRCjP8qR # 4AqKs0S37N4n5Po7V9WpRq7vclUCZelzYDd0UG6FVRxmuP2OMb7HySn3H2Ma/9hL # mTNnabDZj1cAKC6kZ/TaxUrzPBBCL4sGYzWTxbM8Kv51lLQi9aHVxOqqXGtiV8mL # VnhoRPFxz9S8S2ejWgHActWgrJYkkreOT4zKi1msNdemBVormrPd9dTQ2JcR+fhc # 9U6qqPEhHjYqHpM6NHCcgwZAErskHH9S18tti3TUEFgqur8Om445ktbUGXNOl+pj # EGEJyXR1cKlksM1zx7WjEl1wKWosmNUp6c7ThjVxdCXeLvE25SxaSpqDwwqd55r+ # sBwbQcAOtAlZxh9Bmt0E66gwcVHfKO3hAIbqdpjsKVP3qWAt3yjmYaavBR54waMU # in+qGuy0H3ud08H3S5guS0akTj6dOhVvO4n6FpzzqSORwjT3bKJJfQaB9pgQhL8s # Kq4VGcc2TBkrypcho7wKRPm0zbBfTfzRM2V3kNRKtWYcm0M5Mj0CAwEAAaOCAdUw # ggHRMAwGA1UdEwEB/wQCMAAwDgYDVR0PAQH/BAQDAgeAMDwGA1UdJQQ1MDMGCisG # AQQBgjdhAQAGCCsGAQUFBwMDBhsrBgEEAYI3YaDolXqCv9LbTIOfnpIsgovEyCkw # HQYDVR0OBBYEFOCltW+UtUw9bI4SuunkdgvVDCwXMB8GA1UdIwQYMBaAFKRDDH92 # WqWF5z6NKA8MF6JFaXDGMGcGA1UdHwRgMF4wXKBaoFiGVmh0dHA6Ly93d3cubWlj # cm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUyMElEJTIwVmVyaWZpZWQl # MjBDUyUyMEFPQyUyMENBJTIwMDMuY3JsMHQGCCsGAQUFBwEBBGgwZjBkBggrBgEF # BQcwAoZYaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNy # b3NvZnQlMjBJRCUyMFZlcmlmaWVkJTIwQ1MlMjBBT0MlMjBDQSUyMDAzLmNydDBU # BgNVHSAETTBLMEkGBFUdIAAwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNy # b3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMA0GCSqGSIb3DQEB # DAUAA4ICAQAbOuSq8UmcaMyxPfO7EhE/lJhYMGju+53pSilP+dQkKsLRYRPDVTHO # ERtybdhMZlUgzMXPQSRF66NJNTOWiPNah0jFO4KIBQuSLoPv2OdeQbLfvhuJz4rv # jbFWxpQ6wDxoaPjVww4PcZAjTpYGuam5fCypgEP2q6eS+wWWP6hYQxT72pNCPPWO # 8BxMkJZ/HF28rIDnSj28W5bTawx2/yQ7AFNR+kMuD8DnHn2P89f5j4SFzKRbTBgl # rTX2kj9UH7D4mHeT3QxbyWwoYlcsWQOeoTwXjU0YBctQK3tRxLb112XjwiEEq9M+ # jeoHcfP29A80cfQAY4Sc352WkjvOX7JCI1oZfv4XANZ4b4TdgWRV/FKHTl4H7vQw # yfPtXdtrJBB9KMjI+lpRcxrXgKzhDx/7Or9XYjeVpYNxGjr2dfXii5PdlDuXZFnM # PWyokeZv32cKPVyk6kdG5LaLyBqynadxSk8Xps6tD2vFe9+zih0op4DzNs39ftay # D/md4xInngsYjZL8XmnXJEiPXAv7FnDymraH+bUO38f5BDl2yprUgctJSJBk0DM3 # dK3YPnox5CAkAR0KDaS0F1AwfINQEEG8adbect91WRpGqe/n/u9h50TiUUw/LMmZ # jNztmT+1GYGeNYEVctxjFdGJpgDjF/ku62UsPcES1d4l249eevbJOzCCBygwggUQ # oAMCAQICEzMAAAAYDeuRVamKAJgAAAAAABgwDQYJKoZIhvcNAQEMBQAwYzELMAkG # A1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjE0MDIGA1UE # AxMrTWljcm9zb2Z0IElEIFZlcmlmaWVkIENvZGUgU2lnbmluZyBQQ0EgMjAyMTAe # Fw0yNjAzMjYxODExMzJaFw0zMTAzMjYxODExMzJaMFoxCzAJBgNVBAYTAlVTMR4w # HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKzApBgNVBAMTIk1pY3Jvc29m # dCBJRCBWZXJpZmllZCBDUyBBT0MgQ0EgMDMwggIiMA0GCSqGSIb3DQEBAQUAA4IC # DwAwggIKAoICAQDIgNpgNFaiif2VWeWP5I6PnFXxJ/lB37fJR55GCvR7GLZBMkBi # jbiKVwgpBI3xM5nf484znH/qncJ+OCq6y3jgnQW+R8Zd7U+7LjlrmcskalzSQ0gh # MxEpnBW8/HHs2V8ZJzQk6HP+SDsbvsL7LdlH/eO2l4mknhDBwr0Z/Q966TvEth5b # 8kCxj1vqiV4YNthLGRqZR9u2fK/yBMWu83p6O4uo2Edg++gEew5IL7vnnnKFqmSh # /R9vPJy3WF1YcZewAUx8sXZNUnx3ZhVg59l2LpitPiwzE6FMqIsqaEvVe3MzuFd2 # a/uWDZH6VbDyUiRK78mIg1DQYA9zDEyyBFcNI+nxVSzglvL6u7PRuNqgcV3sf6EL # xw89ysQM/Z4R1hRFWXRpyOWKKAKtfBHTk0UnNiPcxmLMMYs8jeUjOidfVPjTIry/ # UVwnwxdlkK85cZfBEMYZ/DBNOwdomP459Y1n8izKkbhsa+p4lw+cQVxATBFx9ggR # 79HhryT7HDmpPLvkJvBZ4wW4CW32UT2SMyDe28nIOU3m+hfHlVeKcLBQcym5VoRD # jIcCVI7uqgGW2PNME0cfei8zCwCy6HCsssJWFS7eg/YbFhnATJcyWfMrkNuAbMfM # N8Npg8crS6jVVowyD0GG5zdgi+uQVcSK/638mA1xEYK3pnIoQgO09uuDBwIDAQAB # o4IB3DCCAdgwDgYDVR0PAQH/BAQDAgGGMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1Ud # DgQWBBSkQwx/dlqlhec+jSgPDBeiRWlwxjBUBgNVHSAETTBLMEkGBFUdIAAwQTA/ # BggrBgEFBQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2Nz # L1JlcG9zaXRvcnkuaHRtMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMBIGA1Ud # EwEB/wQIMAYBAf8CAQAwHwYDVR0jBBgwFoAU2UEpsA8PY2zvadf1zSmepEhqMOYw # cAYDVR0fBGkwZzBloGOgYYZfaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w # cy9jcmwvTWljcm9zb2Z0JTIwSUQlMjBWZXJpZmllZCUyMENvZGUlMjBTaWduaW5n # JTIwUENBJTIwMjAyMS5jcmwwfQYIKwYBBQUHAQEEcTBvMG0GCCsGAQUFBzAChmFo # dHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUy # MElEJTIwVmVyaWZpZWQlMjBDb2RlJTIwU2lnbmluZyUyMFBDQSUyMDIwMjEuY3J0 # MA0GCSqGSIb3DQEBDAUAA4ICAQBxxyBW+X6mhdRiSwD9PMMWcGUAnx5/QUwnNvZd # FGEX+4DRDIr9WCh4C87wHtw+lg1D3uzK10DstPX0LFLBFAC3vWMYX4ImXwoLhoR0 # xlN8mUdorJ3bgnpCJWuI1531Z1rCwPuUrSkBxfOIGDk3p2ECb3Ho/xHi5PRSR/OU # rWuQHwXiaXMTuXu3IRLezwVkZpFmNwYRD57R9Nx2F/yM7tzOY0Hh0hGCaYEK38/6 # FrS0SXadXWyDUCfn5XOGACRjUCnHx+JQUG0f4SHD+iblpAI0gl+ZHnVmdXXxHTZe # Ta0CYCIhFxKP2922s0g6zLmeiV13LWUmtt/UF7TrWXpMi2/0UNniaDoH7rnPGRV5 # xVX8uXy4sZii4aswzqPM7Y7+mzcranqZ8EjZk5gjLhQ3A2sZaprlOu8CaRmyfcIi # VH7zVfgAvm81MWXFziAf7my7QOvnyEFPGddq8MSfPtfRyw/Uq3uH6KpoaJNIfPYH # 6fceZSi53Rat1A9grExq3ROjhhSpTcchuBItAMNVPxoKNbUm+iR/X3XkL+9WQgin # jyHe+hXLclY8vAGXFD1p40PqMIpAYsmEJBFKW9df4//1N5oQDr/FY9IBJl/oSS97 # 9i5rtT7NZz9KvYraCPRBGs0QCy+sWvgQa0coM70QJVLeVwmSxUO/0od0w9Qry7bS # LrxGoDCCB54wggWGoAMCAQICEzMAAAAHh6M0o3uljhwAAAAAAAcwDQYJKoZIhvcN # AQEMBQAwdzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3Jh # dGlvbjFIMEYGA1UEAxM/TWljcm9zb2Z0IElkZW50aXR5IFZlcmlmaWNhdGlvbiBS # b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDIwMB4XDTIxMDQwMTIwMDUyMFoX # DTM2MDQwMTIwMTUyMFowYzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m # dCBDb3Jwb3JhdGlvbjE0MDIGA1UEAxMrTWljcm9zb2Z0IElEIFZlcmlmaWVkIENv # ZGUgU2lnbmluZyBQQ0EgMjAyMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBALLwwK8ZiCji3VR6TElsaQhVCbRS/3pK+MHrJSj3Zxd3KU3rlfL3qrZilYKJ # NqztA9OQacr1AwoNcHbKBLbsQAhBnIB34zxf52bDpIO3NJlfIaTE/xrweLoQ71lz # CHkD7A4As1Bs076Iu+mA6cQzsYYH/Cbl1icwQ6C65rU4V9NQhNUwgrx9rGQ//h89 # 0Q8JdjLLw0nV+ayQ2Fbkd242o9kH82RZsH3HEyqjAB5a8+Ae2nPIPc8sZU6ZE7iR # rRZywRmrKDp5+TcmJX9MRff241UaOBs4NmHOyke8oU1TYrkxh+YeHgfWo5tTgkoS # MoayqoDpHOLJs+qG8Tvh8SnifW2Jj3+ii11TS8/FGngEaNAWrbyfNrC69oKpRQXY # 9bGH6jn9NEJv9weFxhTwyvx9OJLXmRGbAUXN1U9nf4lXezky6Uh/cgjkVd6CGUAf # 0K+Jw+GE/5VpIVbcNr9rNE50Sbmy/4RTCEGvOq3GhjITbCa4crCzTTHgYYjHs1Nb # Oc6brH+eKpWLtr+bGecy9CrwQyx7S/BfYJ+ozst7+yZtG2wR461uckFu0t+gCwLd # N0A6cFtSRtR8bvxVFyWwTtgMMFRuBa3vmUOTnfKLsLefRaQcVTgRnzeLzdpt32cd # YKp+dhr2ogc+qM6K4CBI5/j4VFyC4QFeUP2YAidLtvpXRRo3AgMBAAGjggI1MIIC # MTAOBgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFNlB # KbAPD2Ns72nX9c0pnqRIajDmMFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUF # BwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3Np # dG9yeS5odG0wGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwDwYDVR0TAQH/BAUw # AwEB/zAfBgNVHSMEGDAWgBTIftJqhSobyhmYBAcnz1AQT2ioojCBhAYDVR0fBH0w # ezB5oHegdYZzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jcmwvTWlj # cm9zb2Z0JTIwSWRlbnRpdHklMjBWZXJpZmljYXRpb24lMjBSb290JTIwQ2VydGlm # aWNhdGUlMjBBdXRob3JpdHklMjAyMDIwLmNybDCBwwYIKwYBBQUHAQEEgbYwgbMw # gYEGCCsGAQUFBzAChnVodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2Nl # cnRzL01pY3Jvc29mdCUyMElkZW50aXR5JTIwVmVyaWZpY2F0aW9uJTIwUm9vdCUy # MENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAyMC5jcnQwLQYIKwYBBQUHMAGG # IWh0dHA6Ly9vbmVvY3NwLm1pY3Jvc29mdC5jb20vb2NzcDANBgkqhkiG9w0BAQwF # AAOCAgEAfyUqnv7Uq+rdZgrbVyNMul5skONbhls5fccPlmIbzi+OwVdPQ4H55v7V # OInnmezQEeW4LqK0wja+fBznANbXLB0KrdMCbHQpbLvG6UA/Xv2pfpVIE1CRFfNF # 4XKO8XYEa3oW8oVH+KZHgIQRIwAbyFKQ9iyj4aOWeAzwk+f9E5StNp5T8FG7/VEU # RIVWArbAzPt9ThVN3w1fAZkF7+YU9kbq1bCR2YD+MtunSQ1Rft6XG7b4e0ejRA7m # B2IoX5hNh3UEauY0byxNRG+fT2MCEhQl9g2i2fs6VOG19CNep7SquKaBjhWmirYy # ANb0RJSLWjinMLXNOAga10n8i9jqeprzSMU5ODmrMCJE12xS/NWShg/tuLjAsKP6 # SzYZ+1Ry358ZTFcx0FS/mx2vSoU8s8HRvy+rnXqyUJ9HBqS0DErVLjQwK8VtsBde # kBmdTbQVoCgPCqr+PDPB3xajYnzevs7eidBsM71PINK2BoE2UfMwxCCX3mccFgx6 # UsQeRSdVVVNSyALQe6PT12418xon2iDGE81OGCreLzDcMAZnrUAx4XQLUz6ZTl65 # yPUiOh3k7Yww94lDf+8oG2oZmDh5O1Qe38E+M3vhKwmzIeoB1dVLlz4i3IpaDcR+ # iuGjH2TdaC1ZOmBXiCRKJLj4DT2uhJ04ji+tHD6n58vhavFIrmcxghqRMIIajQIB # ATBxMFoxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp # b24xKzApBgNVBAMTIk1pY3Jvc29mdCBJRCBWZXJpZmllZCBDUyBBT0MgQ0EgMDMC # EzMABiB0TAioxsL2jdQAAAAGIHQwDQYJYIZIAWUDBAIBBQCgXjAQBgorBgEEAYI3 # AgEMMQIwADAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAvBgkqhkiG9w0BCQQx # IgQgI0LZNrPrDaHa5qFw6MWF/gxIunFEofW/lv7DYr6TrZIwDQYJKoZIhvcNAQEB # BQAEggGAAbXn20DjdIyjn6pRW6Y8OXGcJulZ/MHVQfQEymDmKoQ1pJTUgp+4uo68 # qq3GIqVBfWf4NhII97tcUa4XJI56NtkHd4j5kAbnLUllJ3YvkX1mkpNxtN6OBSr9 # 2wNCUfTos+hI/ZDtGTPV0i+/cHeuUHVYpvc5QEVaSNCNj1tBmq0RLuq9bNQN3oOI # HXywr797GYmQd9f3MwH7hYNQ/j/r+0vN3PvulvFq/P1bZWkexIFYbUumRXKm7R8i # +GaI5GdrANtR6IKw/2va8cfXD3UffGf5ESPkeRZhj/F4Sil2Akt7KfaMK64IO8Tr # YUBt8NTc/oZeW3ItnYrRw/USEgGCwhiGfdKVk9pT3kzdzf8cIA+9jJ/CvRGFhgZs # w9SqcxtAlLT+EUqqSFjAjbzLpp0LYaVVIrrIj0JH8k7Mv2WoCKlg8wIMRDn6CYwe # N/xtw97Oj2AmRVpK+rfZyts73rRx4hHhxsz02JTINwsJVq7HI/+i5PgFjQcs4694 # Yp/tGxrzoYIYETCCGA0GCisGAQQBgjcDAwExghf9MIIX+QYJKoZIhvcNAQcCoIIX # 6jCCF+YCAQMxDzANBglghkgBZQMEAgEFADCCAWIGCyqGSIb3DQEJEAEEoIIBUQSC # AU0wggFJAgEBBgorBgEEAYRZCgMBMDEwDQYJYIZIAWUDBAIBBQAEILU3FgzQr6oL # ejo28mEL4ATVcmbR8XdB1tbjh4kT9LDHAgZqhGwzOvYYEzIwMjYwOTA5MTQzODA5 # LjAwN1owBIACAfSggeGkgd4wgdsxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo # aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y # cG9yYXRpb24xJTAjBgNVBAsTHE1pY3Jvc29mdCBBbWVyaWNhIE9wZXJhdGlvbnMx # JzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjo3QTAwLTA1RTAtRDk0NzE1MDMGA1UE # AxMsTWljcm9zb2Z0IFB1YmxpYyBSU0EgVGltZSBTdGFtcGluZyBBdXRob3JpdHmg # gg8hMIIHgjCCBWqgAwIBAgITMwAAAAXlzw//Zi7JhwAAAAAABTANBgkqhkiG9w0B # AQwFADB3MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0 # aW9uMUgwRgYDVQQDEz9NaWNyb3NvZnQgSWRlbnRpdHkgVmVyaWZpY2F0aW9uIFJv # b3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMjAwHhcNMjAxMTE5MjAzMjMxWhcN # MzUxMTE5MjA0MjMxWjBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1l # c3RhbXBpbmcgQ0EgMjAyMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB # AJ5851Jj/eDFnwV9Y7UGIqMcHtfnlzPREwW9ZUZHd5HBXXBvf7KrQ5cMSqFSHGqg # 2/qJhYqOQxwuEQXG8kB41wsDJP5d0zmLYKAY8Zxv3lYkuLDsfMuIEqvGYOPURAH+ # Ybl4SJEESnt0MbPEoKdNihwM5xGv0rGofJ1qOYSTNcc55EbBT7uq3wx3mXhtVmtc # CEr5ZKTkKKE1CxZvNPWdGWJUPC6e4uRfWHIhZcgCsJ+sozf5EeH5KrlFnxpjKKTa # vwfFP6XaGZGWUG8TZaiTogRoAlqcevbiqioUz1Yt4FRK53P6ovnUfANjIgM9JDdJ # 4e0qiDRm5sOTiEQtBLGd9Vhd1MadxoGcHrRCsS5rO9yhv2fjJHrmlQ0EIXmp4DhD # BieKUGR+eZ4CNE3ctW4uvSDQVeSp9h1SaPV8UWEfyTxgGjOsRpeexIveR1MPTVf7 # gt8hY64XNPO6iyUGsEgt8c2PxF87E+CO7A28TpjNq5eLiiunhKbq0XbjkNoU5Jht # YUrlmAbpxRjb9tSreDdtACpm3rkpxp7AQndnI0Shu/fk1/rE3oWsDqMX3jjv40e8 # KN5YsJBnczyWB4JyeeFMW3JBfdeAKhzohFe8U5w9WuvcP1E8cIxLoKSDzCCBOu0h # WdjzKNu8Y5SwB1lt5dQhABYyzR3dxEO/T1K/BVF3rV69AgMBAAGjggIbMIICFzAO # BgNVHQ8BAf8EBAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFGtpKDo1 # L0hjQM972K9J6T7ZPdshMFQGA1UdIARNMEswSQYEVR0gADBBMD8GCCsGAQUFBwIB # FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9y # eS5odG0wEwYDVR0lBAwwCgYIKwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUA # YgBDAEEwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTIftJqhSobyhmYBAcn # z1AQT2ioojCBhAYDVR0fBH0wezB5oHegdYZzaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jcmwvTWljcm9zb2Z0JTIwSWRlbnRpdHklMjBWZXJpZmljYXRp # b24lMjBSb290JTIwQ2VydGlmaWNhdGUlMjBBdXRob3JpdHklMjAyMDIwLmNybDCB # lAYIKwYBBQUHAQEEgYcwgYQwgYEGCCsGAQUFBzAChnVodHRwOi8vd3d3Lm1pY3Jv # c29mdC5jb20vcGtpb3BzL2NlcnRzL01pY3Jvc29mdCUyMElkZW50aXR5JTIwVmVy # aWZpY2F0aW9uJTIwUm9vdCUyMENlcnRpZmljYXRlJTIwQXV0aG9yaXR5JTIwMjAy # MC5jcnQwDQYJKoZIhvcNAQEMBQADggIBAF+Idsd+bbVaFXXnTHho+k7h2ESZJRWl # uLE0Oa/pO+4ge/XEizXvhs0Y7+KVYyb4nHlugBesnFqBGEdC2IWmtKMyS1OWIviw # pnK3aL5JedwzbeBF7POyg6IGG/XhhJ3UqWeWTO+Czb1c2NP5zyEh89F72u9UIw+I # fvM9lzDmc2O2END7MPnrcjWdQnrLn1Ntday7JSyrDvBdmgbNnCKNZPmhzoa8PccO # iQljjTW6GePe5sGFuRHzdFt8y+bN2neF7Zu8hTO1I64XNGqst8S+w+RUdie8fXC1 # jKu3m9KGIqF4aldrYBamyh3g4nJPj/LR2CBaLyD+2BuGZCVmoNR/dSpRCxlot0i7 # 9dKOChmoONqbMI8m04uLaEHAv4qwKHQ1vBzbV/nG89LDKbRSSvijmwJwxRxLLpMQ # /u4xXxFfR4f/gksSkbJp7oqLwliDm/h+w0aJ/U5ccnYhYb7vPKNMN+SZDWycU5OD # IRfyoGl59BsXR/HpRGtiJquOYGmvA/pk5vC1lcnbeMrcWD/26ozePQ/TWfNXKBOm # kFpvPE8CH+EeGGWzqTCjdAsno2jzTeNSxlx3glDGJgcdz5D/AAxw9Sdgq/+rY7jj # gs7X6fqPTXPmaCAJKVHAP19oEjJIBwD1LyHbaEgBxFCogYSOiUIr0Xqcr1nJfiWG # 2GwYe6ZoAF1bMIIHlzCCBX+gAwIBAgITMwAAAFhlzes/odf80gAAAAAAWDANBgkq # hkiG9w0BAQwFADBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3Rh # bXBpbmcgQ0EgMjAyMDAeFw0yNTEwMjMyMDQ2NTVaFw0yNjEwMjIyMDQ2NTVaMIHb # MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVk # bW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxN # aWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRT # UyBFU046N0EwMC0wNUUwLUQ5NDcxNTAzBgNVBAMTLE1pY3Jvc29mdCBQdWJsaWMg # UlNBIFRpbWUgU3RhbXBpbmcgQXV0aG9yaXR5MIICIjANBgkqhkiG9w0BAQEFAAOC # Ag8AMIICCgKCAgEAnXg0pHaQ7PVAlln+HZZrJFcLoKbekhW1yL+QNBUgFFUsjZIK # aqqN4oIJsJM3ps0rJNSO7ndCNRuZDX2Wgur3Ak77eXrloBXqZmO6ZVXeDNRCLldW # 4A0/NfjzJ7XXkdEhjr81ghXEpR7zC+wbaNN+sPSxzLAZBeibDFP7Xws5wX0ZtIsN # 1a2+Xq5bvWp3kRMytwskTjunRgeLZL/tBp237JVdRPFAQ9jYRKpCqUBo/v1xjBLR # CV3PalKjnGfb3MN4U7jVyqifFHShcnW5CERRoBmUa6sygDzFSr8e3g93TPNLFUiv # UE0GmLfbX5ceD1Gt1FcZ6x/JLVATzk5+BWHbMxwJIVkVPTqSSMjQ6KTKdcnq3pH0 # c4AFJp/glvcpq0U9fzZIjJGGvdpishlRl77RQtUhSjxHvCn3LC/xqQQwOHSQDsGh # 6NX2D0RfsSyEtTAByAae+2w1HByTDTcmlTNLEuQLeCj1gNBdIWj0WOYyDtjjQ/8i # TWY6ey1vb9qHljIj5HgIndT5P9MYk2Vg2e7hKUZNBNbA/hsgBsuoZ+IX89WvjEN9 # abF91S4OJVuinmKsLO/MLbnl7ikuD0dN6oA0YewyDQncs12sM9HOtu72QA/TZlef # vW8r9xtMXAYoQlcGjsk8W4Uc7cfqVqbIPjdoc8ZxBzLcXcVyP4p5cyLwvkMCAwEA # AaOCAcswggHHMB0GA1UdDgQWBBRyjU3Fer4VxXJ+hjPcRJnxnRIJsDAfBgNVHSME # GDAWgBRraSg6NS9IY0DPe9ivSek+2T3bITBsBgNVHR8EZTBjMGGgX6BdhltodHRw # Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBQdWJs # aWMlMjBSU0ElMjBUaW1lc3RhbXBpbmclMjBDQSUyMDIwMjAuY3JsMHkGCCsGAQUF # BwEBBG0wazBpBggrBgEFBQcwAoZdaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br # aW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBQdWJsaWMlMjBSU0ElMjBUaW1lc3RhbXBp # bmclMjBDQSUyMDIwMjAuY3J0MAwGA1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYI # KwYBBQUHAwgwDgYDVR0PAQH/BAQDAgeAMGYGA1UdIARfMF0wUQYMKwYBBAGCN0yD # fQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lv # cHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTAIBgZngQwBBAIwDQYJKoZIhvcNAQEMBQAD # ggIBAHvrxIiVF1iHcXvxrJTCD8eOtbPUbK9x+Lz70iYehh+G0UoOMcMf04QD6tQP # TeZ5HhGETkcn0raDJ5NpfbRBuKEH31rxbZK97o12KRDNJ3Nu4ePaUIpH/TcWz8PL # VOCECywSxbEgEG20kyydGc46c591tXzpfkJDckjoYrypaerdeQLRQH9LaoTYZfdA # zMo+Dy0O1DzFJkF5YnsmAM8lt9r1NtXdFjdbFMCbV5dau64mV22s186A8Umi+l23 # 9+Ue0cbJQIykWhIlhhWhxQgoksqHz7kp2GFZAAeySTmIOQOWyXOA8JA8TISJyn3J # DOgStv583P3V0QSALT6JXDCW26FV208VGJMzkv0S22iOTZJ/oamTpk8RzD8oWT8p # fbe1q/k/bxPiXYRbzps96a5YOko7n0Vdo61DOJhL/mhk01Y348gq6vhG/VTcdGHh # 1rCkwOM05B35AZZq9AtPpfRzJinrHzzGRx+r6fD3ccYMPMMX/Nwd2irzrph172fQ # cSf1fMwvwIhmfH4GWJJ+mf1HA6uXoAOVByckguXvlj8gPi7T2ES6RU8+QssfqTNT # JKjsBheWKWv2W4ESVen2L7lCz7i79FhA+0kp0yXJnYwdzWS0ovTINULINmzVyMcS # Um5WuVf8YZ33cAud2Opr6N1+RuLZDavDvjiehlI5dH+GEy56MYIHQzCCBz8CAQEw # eDBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u # MTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0Eg # MjAyMAITMwAAAFhlzes/odf80gAAAAAAWDANBglghkgBZQMEAgEFAKCCBJwwEQYL # KoZIhvcNAQkQAg8xAgUAMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAcBgkq # hkiG9w0BCQUxDxcNMjYwOTA5MTQzODA5WjAvBgkqhkiG9w0BCQQxIgQgi0gP68vA # W4iPk1dMGHp8cRmuxN+gR1OoyCjWIG36ofswgbkGCyqGSIb3DQEJEAIvMYGpMIGm # MIGjMIGgBCDFIlS7sgfQ+wAo1cWbWz+WN69VBds58hbran919aLocTB8MGWkYzBh # MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIw # MAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0EgMjAy # MAITMwAAAFhlzes/odf80gAAAAAAWDCCA14GCyqGSIb3DQEJEAISMYIDTTCCA0mh # ggNFMIIDQTCCAikCAQEwggEJoYHhpIHeMIHbMQswCQYDVQQGEwJVUzETMBEGA1UE # CBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9z # b2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVy # YXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046N0EwMC0wNUUwLUQ5NDcx # NTAzBgNVBAMTLE1pY3Jvc29mdCBQdWJsaWMgUlNBIFRpbWUgU3RhbXBpbmcgQXV0 # aG9yaXR5oiMKAQEwBwYFKw4DAhoDFQCdZHkb26ercF2O62vCdZUfUSvEXKBnMGWk # YzBhMQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9u # MTIwMAYDVQQDEylNaWNyb3NvZnQgUHVibGljIFJTQSBUaW1lc3RhbXBpbmcgQ0Eg # MjAyMDANBgkqhkiG9w0BAQsFAAIFAO5L64kwIhgPMjAyNjA5MDkxNDI4MjVaGA8y # MDI2MDkxMDE0MjgyNVowdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA7kvriQIBADAH # AgEAAgIiMzAHAgEAAgISIDAKAgUA7k09CQIBADA2BgorBgEEAYRZCgQCMSgwJjAM # BgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEB # CwUAA4IBAQA7OJK6aYPrPIEIpO1/KgGZ4p2GfYDlHTrQkZbPs1Tx1AylYGWd1PiP # z/rid4jX9pLJD01C4BsbYbiRnSpxmrVJW0vUoX1akTfsaIBP1Mx1gT3tPSh67bbV # MhELawA283g8NUUffcm5kpfmINtX+PSjASjFWeDx47spWuBDnuGnyha8+ThyVBHl # 7bMmaFmKfntmyGtXZG0c/IaOhHCTn4cfiDyw9WgwVKFLjdn5sqaq0xgXO1lG7HSw # xNFpj3rerXNxGlrpUe1hjntQ0/+8CzvdGPuTQwZQvd60wl/5JZKo96GSpDkBC+e1 # 0Sk7F1Bxw9rKA50iTM6qlnvpY4I74Kf9MA0GCSqGSIb3DQEBAQUABIICAJd6WlWR # erl9NagpbUjElK5J6Y6hkmxx31lAKAT3W1p5Mkp2P/TI66+bu4OJEbRosHainh7Y # QQND3MYho/yTk40SxCM22o5RYq8fj0n2Ss3na7mktnYncAufJcLcvBe2sRWxj35T # moFwfoXr/ZKL2X3vv9RS2iIiJ+2e0W8J25yUYlkGeLqQVQjd8YfqkiLT/PA1DASq # t4E0ILzRBZilUXuKPHJJuY2XSiBlfRDEagPylIed41DANtEEGiDtg2wlVghup+DJ # HOKtRpyLN0QixBKW5emtFxcWiyd264W8Nz7fK0JkGTJAz9/wHeSv1A8Xsnx/rJRb # OnYmUU70YzwYciH5VA0QyI0ZjtfZk1A0jb4P1zf2tC+5nTnYyGJm8QMrLfXTOHLw # dkEY3DvrsBnyOBcJSKw5XZvR77FXWssrJsvkHFw1+/vudF0LiiGEVk0AkQOyoj7r # 4Hb5qMqG08TzE9IGwqN3FI4gMI8Y+J6Z9X/HnmcCciysoGCtb+u7xp7KNk0PBzh3 # wUhFAKXMcfPAS6CdFbta3seqKpjCjYnEo5+Ndtxb5OQG5dFx7ho62kjXgiRanw5G # d0BU9xF092M95ip1evPtG5NUZDZrpm6RThE9m6uHhsDakK9ZbhOU1W8/TWaCmrKe # slEfW3hDi9g+X/rQUTbCJelGMdEcbJfg+FXq # SIG # End signature block |