helpers/config-drift/Get-ConfigDriftDashboard.ps1
|
#requires -Version 5.1 <# .SYNOPSIS Audits network and server configurations for drift and renders a dashboard. .DESCRIPTION Collects configurations over SSH (or reads them from files), compares each against its stored baseline, evaluates policy rules, checks peer-group consistency, and produces a WhatsUpGoldPS dashboard plus a JSON data file. Baselines live in a local folder with a JSON index. No Git or other external tool is required. .PARAMETER Target Device hostnames or IP addresses to collect from over SSH. .PARAMETER ConfigPath Existing configuration files to audit instead of connecting over SSH. The file base name is used as the device key. .PARAMETER StorePath Baseline store folder. Defaults to config-baselines beside this script. .PARAMETER Profile Vendor profile controlling the collection command and volatile-line filters. .PARAMETER Rule Policy rules as hashtables with Id and MustMatch or MustNotMatch. .PARAMETER UseGolden Compare against the most recent approved revision instead of the newest one. .PARAMETER UpdateBaseline Store the collected configuration as a new revision. .PARAMETER ApproveBaseline Store and mark the collected configuration as the golden revision. .PARAMETER ComparePeers Add cross-device consistency checks across all audited targets. .EXAMPLE .\Get-ConfigDriftDashboard.ps1 -ConfigPath .\configs\*.txt -Profile cisco-ios -ApproveBaseline .EXAMPLE .\Get-ConfigDriftDashboard.ps1 -Target sw1,sw2 -Username admin -Profile cisco-ios -ComparePeers .NOTES Author : jason@wug.ninja Requires: PowerShell 5.1+, ConfigDriftHelpers.ps1 in the same directory. .LINK https://github.com/jayyx2/WhatsUpGoldPS #> [CmdletBinding(DefaultParameterSetName = 'Ssh')] param( [Parameter(ParameterSetName = 'Ssh')][string[]]$Target, [Parameter(ParameterSetName = 'Wug', Mandatory = $true)][string]$FromWUGGroup, [Parameter(ParameterSetName = 'Wug')][string]$WUGSearch, [Parameter(ParameterSetName = 'Ssh')] [Parameter(ParameterSetName = 'Wug')] [string]$Username, [Parameter(ParameterSetName = 'Ssh')] [Parameter(ParameterSetName = 'Wug')] [string]$KeyFile, [Parameter(ParameterSetName = 'Ssh')] [Parameter(ParameterSetName = 'Wug')] [string]$KeyPassphrase, [Parameter(ParameterSetName = 'Ssh')] [Parameter(ParameterSetName = 'Wug')] [string]$VaultCredential, [Parameter(ParameterSetName = 'Ssh')] [Parameter(ParameterSetName = 'Wug')] [int]$Port = 22, [Parameter(ParameterSetName = 'Ssh')] [Parameter(ParameterSetName = 'Wug')] [string[]]$Command, [Parameter(ParameterSetName = 'Ssh')] [Parameter(ParameterSetName = 'Wug')] [string[]]$SetupCommand, [Parameter(ParameterSetName = 'Ssh')] [Parameter(ParameterSetName = 'Wug')] [switch]$Shell, [Parameter(ParameterSetName = 'Ssh')] [Parameter(ParameterSetName = 'Wug')] [object[]]$PreStep, [Parameter(ParameterSetName = 'Ssh')] [Parameter(ParameterSetName = 'Wug')] [switch]$UseEnable, [Parameter(ParameterSetName = 'File', Mandatory = $true)][string[]]$ConfigPath, [string]$StorePath, [ValidateSet('cisco-ios', 'cisco-nxos', 'cisco-asa', 'linux', 'generic')] [string]$Profile = 'generic', [string[]]$IgnorePattern = @(), [hashtable[]]$IgnoreBlock = @(), [string[]]$IgnoreSection = @(), [string[]]$IncludePattern = @(), [hashtable[]]$IncludeBlock = @(), [string[]]$IncludeSection = @(), [ValidateSet('cisco-hardening', 'cisco-snmp', 'linux-ssh')] [string[]]$PolicyPack, [hashtable[]]$Rule, [switch]$UseGolden, [switch]$UpdateBaseline, [switch]$ApproveBaseline, [switch]$ComparePeers, [ValidateRange(1, 100)][int]$ConsensusPercent = 80, [int]$TimeoutSeconds = 60, [string]$OutputPath, [string]$JsonPath, [string]$DiffPath, [switch]$ShowDiff, [switch]$PublishToWUG, [switch]$NoLaunch ) $helpersPath = Join-Path $PSScriptRoot 'ConfigDriftHelpers.ps1' if (-not (Test-Path -LiteralPath $helpersPath)) { throw "ConfigDriftHelpers.ps1 not found at $helpersPath. Ensure it is in the same directory." } . $helpersPath if (-not $StorePath) { $StorePath = Join-Path $PSScriptRoot 'config-baselines' } if (-not $OutputPath) { $OutputPath = Join-Path $env:TEMP 'Config-Drift-Dashboard.html' } if (-not $JsonPath) { $JsonPath = Join-Path $env:TEMP 'config_drift.json' } $effectiveRules = @() foreach ($pack in @($PolicyPack)) { if ($pack) { $effectiveRules += @(Get-ConfigPolicyPack -Name $pack) } } if ($Rule) { $effectiveRules += @($Rule) } Initialize-ConfigBaselineStore -StorePath $StorePath | Out-Null $collected = [System.Collections.Generic.List[object]]::new() if ($PSCmdlet.ParameterSetName -eq 'File') { $files = @() foreach ($pattern in $ConfigPath) { $files += @(Get-ChildItem -Path $pattern -File -ErrorAction SilentlyContinue) } if ($files.Count -eq 0) { throw "No configuration files matched: $($ConfigPath -join ', ')" } foreach ($file in $files) { $collected.Add([pscustomobject]@{ Target = $file.BaseName Config = (Get-Content -LiteralPath $file.FullName -Raw) Error = $null }) } } else { $deviceMap = @{} if ($PSCmdlet.ParameterSetName -eq 'Wug') { $wugSplat = @{ GroupName = $FromWUGGroup } if ($WUGSearch) { $wugSplat['SearchValue'] = $WUGSearch } $wugTargets = @(Get-ConfigDriftTarget @wugSplat) if ($wugTargets.Count -eq 0) { throw "No devices found in WUG group '$FromWUGGroup'." } $Target = @($wugTargets | ForEach-Object { $_.Target }) foreach ($wugTarget in $wugTargets) { $deviceMap[$wugTarget.Target] = $wugTarget.DeviceId } Write-Host "Resolved $($Target.Count) device(s) from WUG group '$FromWUGGroup'." } if (-not $Target -or $Target.Count -eq 0) { $targetInput = Read-Host -Prompt 'Enter device(s) - hostname or IP (comma-separated)' $Target = @($targetInput -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne '' }) } if (-not $Target -or $Target.Count -eq 0) { throw 'At least one target must be specified.' } $securePassword = $null $enablePassword = $null if ($VaultCredential) { $vaultEntry = Get-ConfigDriftVaultCredential -Name $VaultCredential if (-not $Username -and $vaultEntry.Username) { $Username = $vaultEntry.Username } if (-not $KeyFile -and $vaultEntry.KeyFile) { $KeyFile = $vaultEntry.KeyFile } if (-not $KeyPassphrase -and $vaultEntry.KeyPassphrase) { $KeyPassphrase = $vaultEntry.KeyPassphrase } if ($vaultEntry.SecurePassword) { $securePassword = $vaultEntry.SecurePassword } } if (-not $Username) { $Username = Read-Host -Prompt 'Enter SSH username' } if (-not $Username) { throw 'An SSH username is required.' } if (-not $securePassword -and -not $KeyFile) { $securePassword = Read-Host -Prompt "Enter SSH password for $Username" -AsSecureString } if ($UseEnable) { $enablePassword = Read-Host -Prompt 'Enter enable password' -AsSecureString } $index = 0 foreach ($item in $Target) { $index++ Write-Progress -Activity 'Collecting configurations' -Status $item -PercentComplete (($index / $Target.Count) * 100) $splat = @{ Target = $item Username = $Username Port = $Port TimeoutSeconds = $TimeoutSeconds Profile = $Profile } if ($KeyFile) { $splat['KeyFile'] = $KeyFile } if ($KeyPassphrase) { $splat['KeyPassphrase'] = $KeyPassphrase } if ($securePassword) { $splat['SecurePassword'] = $securePassword } if ($Command) { $splat['Command'] = $Command } if ($PSBoundParameters.ContainsKey('SetupCommand')) { $splat['SetupCommand'] = $SetupCommand } if ($Shell) { $splat['Shell'] = $true } if ($PreStep) { $splat['PreStep'] = $PreStep } if ($enablePassword) { $splat['EnablePassword'] = $enablePassword } $deviceId = $null if ($deviceMap.ContainsKey($item)) { $deviceId = $deviceMap[$item] } try { $collected.Add([pscustomobject]@{ Target = $item; Config = (Get-ConfigViaSsh @splat); Error = $null; DeviceId = $deviceId }) } catch { Write-Warning "Collection failed for ${item}: $($_.Exception.Message)" $collected.Add([pscustomobject]@{ Target = $item; Config = $null; Error = $_.Exception.Message; DeviceId = $deviceId }) } } Write-Progress -Activity 'Collecting configurations' -Completed } $results = [System.Collections.Generic.List[object]]::new() $allChecks = [System.Collections.Generic.List[object]]::new() $peerMap = @{} foreach ($item in $collected) { if ($null -eq $item.Config) { $allChecks.Add([pscustomobject]@{ Target = $item.Target Category = 'Collection' Check = 'Configuration retrieval' Status = 'Fail' Value = 'error' Detail = $item.Error }) continue } $auditSplat = @{ Target = $item.Target Config = $item.Config StorePath = $StorePath Profile = $Profile IgnorePattern = $IgnorePattern IgnoreBlock = $IgnoreBlock IgnoreSection = $IgnoreSection IncludePattern = $IncludePattern IncludeBlock = $IncludeBlock IncludeSection = $IncludeSection } if ($effectiveRules.Count -gt 0) { $auditSplat['Rule'] = $effectiveRules } if ($UseGolden) { $auditSplat['UseGolden'] = $true } if ($UpdateBaseline) { $auditSplat['UpdateBaseline'] = $true } if ($ApproveBaseline) { $auditSplat['ApproveBaseline'] = $true } $audit = Invoke-ConfigDriftAudit @auditSplat if ($item.PSObject.Properties['DeviceId'] -and $item.DeviceId) { $audit | Add-Member -NotePropertyName 'DeviceId' -NotePropertyValue $item.DeviceId -Force } $results.Add($audit) foreach ($check in @($audit.Checks)) { $allChecks.Add($check) } $peerMap[$item.Target] = $audit.Lines } if ($ComparePeers -and $peerMap.Keys.Count -ge 2) { foreach ($check in @(Compare-ConfigPeerGroup -ConfigMap $peerMap -ConsensusPercent $ConsensusPercent -IgnorePattern $IgnorePattern)) { $allChecks.Add($check) } } if ($allChecks.Count -eq 0) { throw 'No configurations were audited.' } $results | Select-Object Target, CollectedAt, Hash, BaselineHash, DriftDetected, AddedLines, RemovedLines, LineCount, Diff | ConvertTo-Json -Depth 6 | Set-Content -LiteralPath $JsonPath -Encoding UTF8 if ($DiffPath) { $diffText = foreach ($audit in $results) { if (-not $audit.DriftDetected) { continue } "=== $($audit.Target) (+$($audit.AddedLines)/-$($audit.RemovedLines)) ===" Format-ConfigDiff -Diff $audit.Diff '' } ($diffText -join "`n") | Set-Content -LiteralPath $DiffPath -Encoding UTF8 } $repoRoot = Split-Path (Split-Path $PSScriptRoot -Parent) -Parent $exporter = Join-Path $repoRoot 'helpers\reports\Export-DynamicDashboardHtml.ps1' $templatePath = Join-Path $repoRoot 'helpers\reports\Dynamic-Dashboard-Template.html' if (-not (Test-Path -LiteralPath $exporter)) { throw "Dashboard generator not found: $exporter" } . $exporter Set-StrictMode -Off $allChecks | Export-DynamicDashboardHtml -OutputPath $OutputPath -ReportTitle 'Configuration Drift Audit' ` -CardField @('Status', 'Category', 'Target') -StatusField 'Status' ` -ExportPrefix 'config_drift' -TemplatePath $templatePath | Out-Null Write-Host "Baseline store: $StorePath" Write-Host "JSON data : $JsonPath" if ($DiffPath) { Write-Host "Diff text : $DiffPath" } Write-Host "HTML report : $OutputPath" foreach ($audit in $results) { $state = 'clean' if ($audit.DriftDetected) { $state = "DRIFT +$($audit.AddedLines)/-$($audit.RemovedLines)" } Write-Host ("{0,-24} {1,-22} lines={2}" -f $audit.Target, $state, $audit.LineCount) } if ($ShowDiff) { foreach ($audit in $results) { if (-not $audit.DriftDetected) { continue } Write-Host '' Write-Host "=== $($audit.Target) ===" -ForegroundColor Cyan foreach ($row in $audit.Diff) { if ($row.Operation -eq 'Added') { Write-Host "+ $($row.Text)" -ForegroundColor Green } elseif ($row.Operation -eq 'Removed') { Write-Host "- $($row.Text)" -ForegroundColor Red } } } } if ($PublishToWUG) { Write-Host '' $publishResults = @($results | Publish-ConfigDriftToWUG) Write-Host "Published $($publishResults.Count) result(s) to WhatsUp Gold as ConfigDrift.* attributes." } if (-not $NoLaunch) { Start-Process $OutputPath } # SIG # Begin signature block # MIIr+wYJKoZIhvcNAQcCoIIr7DCCK+gCAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCAQT9Mo5v6sd7bB # 6FhIYYd9CRWjdngp/UvcqXBRxvRCaKCCJQ0wggVvMIIEV6ADAgECAhBI/JO0YFWU # jTanyYqJ1pQWMA0GCSqGSIb3DQEBDAUAMHsxCzAJBgNVBAYTAkdCMRswGQYDVQQI # DBJHcmVhdGVyIE1hbmNoZXN0ZXIxEDAOBgNVBAcMB1NhbGZvcmQxGjAYBgNVBAoM # EUNvbW9kbyBDQSBMaW1pdGVkMSEwHwYDVQQDDBhBQUEgQ2VydGlmaWNhdGUgU2Vy # dmljZXMwHhcNMjEwNTI1MDAwMDAwWhcNMjgxMjMxMjM1OTU5WjBWMQswCQYDVQQG # EwJHQjEYMBYGA1UEChMPU2VjdGlnbyBMaW1pdGVkMS0wKwYDVQQDEyRTZWN0aWdv # IFB1YmxpYyBDb2RlIFNpZ25pbmcgUm9vdCBSNDYwggIiMA0GCSqGSIb3DQEBAQUA # A4ICDwAwggIKAoICAQCN55QSIgQkdC7/FiMCkoq2rjaFrEfUI5ErPtx94jGgUW+s # hJHjUoq14pbe0IdjJImK/+8Skzt9u7aKvb0Ffyeba2XTpQxpsbxJOZrxbW6q5KCD # J9qaDStQ6Utbs7hkNqR+Sj2pcaths3OzPAsM79szV+W+NDfjlxtd/R8SPYIDdub7 # P2bSlDFp+m2zNKzBenjcklDyZMeqLQSrw2rq4C+np9xu1+j/2iGrQL+57g2extme # me/G3h+pDHazJyCh1rr9gOcB0u/rgimVcI3/uxXP/tEPNqIuTzKQdEZrRzUTdwUz # T2MuuC3hv2WnBGsY2HH6zAjybYmZELGt2z4s5KoYsMYHAXVn3m3pY2MeNn9pib6q # RT5uWl+PoVvLnTCGMOgDs0DGDQ84zWeoU4j6uDBl+m/H5x2xg3RpPqzEaDux5mcz # mrYI4IAFSEDu9oJkRqj1c7AGlfJsZZ+/VVscnFcax3hGfHCqlBuCF6yH6bbJDoEc # QNYWFyn8XJwYK+pF9e+91WdPKF4F7pBMeufG9ND8+s0+MkYTIDaKBOq3qgdGnA2T # OglmmVhcKaO5DKYwODzQRjY1fJy67sPV+Qp2+n4FG0DKkjXp1XrRtX8ArqmQqsV/ # AZwQsRb8zG4Y3G9i/qZQp7h7uJ0VP/4gDHXIIloTlRmQAOka1cKG8eOO7F/05QID # AQABo4IBEjCCAQ4wHwYDVR0jBBgwFoAUoBEKIz6W8Qfs4q8p74Klf9AwpLQwHQYD # VR0OBBYEFDLrkpr/NZZILyhAQnAgNpFcF4XmMA4GA1UdDwEB/wQEAwIBhjAPBgNV # HRMBAf8EBTADAQH/MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBsGA1UdIAQUMBIwBgYE # VR0gADAIBgZngQwBBAEwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybC5jb21v # ZG9jYS5jb20vQUFBQ2VydGlmaWNhdGVTZXJ2aWNlcy5jcmwwNAYIKwYBBQUHAQEE # KDAmMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5jb21vZG9jYS5jb20wDQYJKoZI # hvcNAQEMBQADggEBABK/oe+LdJqYRLhpRrWrJAoMpIpnuDqBv0WKfVIHqI0fTiGF # OaNrXi0ghr8QuK55O1PNtPvYRL4G2VxjZ9RAFodEhnIq1jIV9RKDwvnhXRFAZ/ZC # J3LFI+ICOBpMIOLbAffNRk8monxmwFE2tokCVMf8WPtsAO7+mKYulaEMUykfb9gZ # pk+e96wJ6l2CxouvgKe9gUhShDHaMuwV5KZMPWw5c9QLhTkg4IUaaOGnSDip0TYl # d8GNGRbFiExmfS9jzpjoad+sPKhdnckcW67Y8y90z7h+9teDnRGWYpquRRPaf9xH # +9/DUp/mBlXpnYzyOmJRvOwkDynUWICE5EV7WtgwggWNMIIEdaADAgECAhAOmxiO # +dAt5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYD # VQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAi # BgNVBAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAw # MDBaFw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdp # Q2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERp # Z2lDZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCC # AgoCggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsb # hA3EMB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iT # cMKyunWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGb # NOsFxl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclP # XuU15zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCr # VYJBMtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFP # ObURWBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTv # kpI6nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWM # cCxBYKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls # 5Q5SUUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBR # a2+xq4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6 # MIIBNjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qY # rhwPTzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8E # BAMCAYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k # aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0 # LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDig # NoY0aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9v # dENBLmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCg # v0NcVec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQT # SnovLbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh # 65ZyoUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSw # uKFWjuyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAO # QGPFmCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjD # TZ9ztwGpn1eqXijiuZQwggYaMIIEAqADAgECAhBiHW0MUgGeO5B5FSCJIRwKMA0G # CSqGSIb3DQEBDAUAMFYxCzAJBgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExp # bWl0ZWQxLTArBgNVBAMTJFNlY3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBSb290 # IFI0NjAeFw0yMTAzMjIwMDAwMDBaFw0zNjAzMjEyMzU5NTlaMFQxCzAJBgNVBAYT # AkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNlY3RpZ28g # UHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwggGiMA0GCSqGSIb3DQEBAQUAA4IB # jwAwggGKAoIBgQCbK51T+jU/jmAGQ2rAz/V/9shTUxjIztNsfvxYB5UXeWUzCxEe # AEZGbEN4QMgCsJLZUKhWThj/yPqy0iSZhXkZ6Pg2A2NVDgFigOMYzB2OKhdqfWGV # oYW3haT29PSTahYkwmMv0b/83nbeECbiMXhSOtbam+/36F09fy1tsB8je/RV0mIk # 8XL/tfCK6cPuYHE215wzrK0h1SWHTxPbPuYkRdkP05ZwmRmTnAO5/arnY83jeNzh # P06ShdnRqtZlV59+8yv+KIhE5ILMqgOZYAENHNX9SJDm+qxp4VqpB3MV/h53yl41 # aHU5pledi9lCBbH9JeIkNFICiVHNkRmq4TpxtwfvjsUedyz8rNyfQJy/aOs5b4s+ # ac7IH60B+Ja7TVM+EKv1WuTGwcLmoU3FpOFMbmPj8pz44MPZ1f9+YEQIQty/NQd/ # 2yGgW+ufflcZ/ZE9o1M7a5Jnqf2i2/uMSWymR8r2oQBMdlyh2n5HirY4jKnFH/9g # Rvd+QOfdRrJZb1sCAwEAAaOCAWQwggFgMB8GA1UdIwQYMBaAFDLrkpr/NZZILyhA # QnAgNpFcF4XmMB0GA1UdDgQWBBQPKssghyi47G9IritUpimqF6TNDDAOBgNVHQ8B # Af8EBAMCAYYwEgYDVR0TAQH/BAgwBgEB/wIBADATBgNVHSUEDDAKBggrBgEFBQcD # AzAbBgNVHSAEFDASMAYGBFUdIAAwCAYGZ4EMAQQBMEsGA1UdHwREMEIwQKA+oDyG # Omh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVTaWduaW5n # Um9vdFI0Ni5jcmwwewYIKwYBBQUHAQEEbzBtMEYGCCsGAQUFBzAChjpodHRwOi8v # Y3J0LnNlY3RpZ28uY29tL1NlY3RpZ29QdWJsaWNDb2RlU2lnbmluZ1Jvb3RSNDYu # cDdjMCMGCCsGAQUFBzABhhdodHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG # 9w0BAQwFAAOCAgEABv+C4XdjNm57oRUgmxP/BP6YdURhw1aVcdGRP4Wh60BAscjW # 4HL9hcpkOTz5jUug2oeunbYAowbFC2AKK+cMcXIBD0ZdOaWTsyNyBBsMLHqafvIh # rCymlaS98+QpoBCyKppP0OcxYEdU0hpsaqBBIZOtBajjcw5+w/KeFvPYfLF/ldYp # mlG+vd0xqlqd099iChnyIMvY5HexjO2AmtsbpVn0OhNcWbWDRF/3sBp6fWXhz7Dc # ML4iTAWS+MVXeNLj1lJziVKEoroGs9Mlizg0bUMbOalOhOfCipnx8CaLZeVme5yE # Lg09Jlo8BMe80jO37PU8ejfkP9/uPak7VLwELKxAMcJszkyeiaerlphwoKx1uHRz # NyE6bxuSKcutisqmKL5OTunAvtONEoteSiabkPVSZ2z76mKnzAfZxCl/3dq3dUNw # 4rg3sTCggkHSRqTqlLMS7gjrhTqBmzu1L90Y1KWN/Y5JKdGvspbOrTfOXyXvmPL6 # E52z1NZJ6ctuMFBQZH3pwWvqURR8AgQdULUvrxjUYbHHj95Ejza63zdrEcxWLDX6 # xWls/GDnVNueKjWUH3fTv1Y8Wdho698YADR7TNx8X8z2Bev6SivBBOHY+uqiirZt # g0y9ShQoPzmCcn63Syatatvx157YK9hlcPmVoa1oDE5/L9Uo2bC5a4CH2RwwggY+ # MIIEpqADAgECAhAHnODk0RR/hc05c892LTfrMA0GCSqGSIb3DQEBDAUAMFQxCzAJ # BgNVBAYTAkdCMRgwFgYDVQQKEw9TZWN0aWdvIExpbWl0ZWQxKzApBgNVBAMTIlNl # Y3RpZ28gUHVibGljIENvZGUgU2lnbmluZyBDQSBSMzYwHhcNMjYwMjA5MDAwMDAw # WhcNMjkwNDIxMjM1OTU5WjBVMQswCQYDVQQGEwJVUzEUMBIGA1UECAwLQ29ubmVj # dGljdXQxFzAVBgNVBAoMDkphc29uIEFsYmVyaW5vMRcwFQYDVQQDDA5KYXNvbiBB # bGJlcmlubzCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAPN6aN4B1yYW # kI5b5TBj3I0VV/peETrHb6EY4BHGxt8Ap+eT+WpEpJyEtRYPxEmNJL3A38Bkg7mw # zPE3/1NK570ZBCuBjSAn4mSDIgIuXZnvyBO9W1OQs5d67MlJLUAEufl18tOr3ST1 # DeO9gSjQSAE5Nql0QDxPnm93OZBon+Fz3CmE+z3MwAe2h4KdtRAnCqwM+/V7iBdb # w+JOxolpx+7RVjGyProTENIG3pe/hKvPb501lf8uBAADLdjZr5ip8vIWbf857Yw1 # Bu10nVI7HW3eE8Cl5//d1ribHlzTzQLfttW+k+DaFsKZBBL56l4YAlIVRsrOiE1k # dHYYx6IGrEA809R7+TZA9DzGqyFiv9qmJAbL4fDwetDeyIq+Oztz1LvEdy8Rcd0J # BY+J4S0eDEFIA3X0N8VcLeAwabKb9AjulKXwUeqCJLvN79CJ90UTZb2+I+tamj0d # n+IKMEsJ4v4Ggx72sxFr9+6XziodtTg5Luf2xd6+PhhamOxF2px9LObhBLLEMyRs # CHZIzVZOFKu9BpHQH7ufGB+Sa80Tli0/6LEyn9+bMYWi2ttn6lLOPThXMiQaooRU # q6q2u3+F4SaPlxVFLI7OJVMhar6nW6joBvELTJPmANSMjDSRFDfHRCdGbZsL/keE # LJNy+jZctF6VvxQEjFM8/bazu6qYhrA7AgMBAAGjggGJMIIBhTAfBgNVHSMEGDAW # gBQPKssghyi47G9IritUpimqF6TNDDAdBgNVHQ4EFgQU6YF0o0D5AVhKHbVocr8G # aSIBibAwDgYDVR0PAQH/BAQDAgeAMAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYI # KwYBBQUHAwMwSgYDVR0gBEMwQTA1BgwrBgEEAbIxAQIBAwIwJTAjBggrBgEFBQcC # ARYXaHR0cHM6Ly9zZWN0aWdvLmNvbS9DUFMwCAYGZ4EMAQQBMEkGA1UdHwRCMEAw # PqA8oDqGOGh0dHA6Ly9jcmwuc2VjdGlnby5jb20vU2VjdGlnb1B1YmxpY0NvZGVT # aWduaW5nQ0FSMzYuY3JsMHkGCCsGAQUFBwEBBG0wazBEBggrBgEFBQcwAoY4aHR0 # cDovL2NydC5zZWN0aWdvLmNvbS9TZWN0aWdvUHVibGljQ29kZVNpZ25pbmdDQVIz # Ni5jcnQwIwYIKwYBBQUHMAGGF2h0dHA6Ly9vY3NwLnNlY3RpZ28uY29tMA0GCSqG # SIb3DQEBDAUAA4IBgQAEIsm4xnOd/tZMVrKwi3doAXvCwOA/RYQnFJD7R/bSQRu3 # wXEK4o9SIefye18B/q4fhBkhNAJuEvTQAGfqbbpxow03J5PrDTp1WPCWbXKX8Oz9 # vGWJFyJxRGftkdzZ57JE00synEMS8XCwLO9P32MyR9Z9URrpiLPJ9rQjfHMb1BUd # vaNayomm7aWLAnD+X7jm6o8sNT5An1cwEAob7obWDM6sX93wphwJNBJAstH9Ozs6 # LwISOX6sKS7CKm9N3Kp8hOUue0ZHAtZdFl6o5u12wy+zzieGEI50fKnN77FfNKFO # WKlS6OJwlArcbFegB5K89LcE5iNSmaM3VMB2ADV1FEcjGSHw4lTg1Wx+WMAMdl/7 # nbvfFxJ9uu5tNiT54B0s+lZO/HztwXYQUczdsFon3pjsNrsk9ZlalBi5SHkIu+F6 # g7tWiEv3rtVApmJRnLkUr2Xq2a4nbslUCt4jKs5UX4V1nSX8OM++AXoyVGO+iTj7 # z+pl6XE9Gw/Td6WKKKswgga0MIIEnKADAgECAhANx6xXBf8hmS5AQyIMOkmGMA0G # CSqGSIb3DQEBCwUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJ # bmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0 # IFRydXN0ZWQgUm9vdCBHNDAeFw0yNTA1MDcwMDAwMDBaFw0zODAxMTQyMzU5NTla # MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjFBMD8GA1UE # AxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1lU3RhbXBpbmcgUlNBNDA5NiBTSEEy # NTYgMjAyNSBDQTEwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC0eDHT # CphBcr48RsAcrHXbo0ZodLRRF51NrY0NlLWZloMsVO1DahGPNRcybEKq+RuwOnPh # of6pvF4uGjwjqNjfEvUi6wuim5bap+0lgloM2zX4kftn5B1IpYzTqpyFQ/4Bt0mA # xAHeHYNnQxqXmRinvuNgxVBdJkf77S2uPoCj7GH8BLuxBG5AvftBdsOECS1UkxBv # MgEdgkFiDNYiOTx4OtiFcMSkqTtF2hfQz3zQSku2Ws3IfDReb6e3mmdglTcaarps # 0wjUjsZvkgFkriK9tUKJm/s80FiocSk1VYLZlDwFt+cVFBURJg6zMUjZa/zbCclF # 83bRVFLeGkuAhHiGPMvSGmhgaTzVyhYn4p0+8y9oHRaQT/aofEnS5xLrfxnGpTXi # UOeSLsJygoLPp66bkDX1ZlAeSpQl92QOMeRxykvq6gbylsXQskBBBnGy3tW/AMOM # CZIVNSaz7BX8VtYGqLt9MmeOreGPRdtBx3yGOP+rx3rKWDEJlIqLXvJWnY0v5ydP # pOjL6s36czwzsucuoKs7Yk/ehb//Wx+5kMqIMRvUBDx6z1ev+7psNOdgJMoiwOrU # G2ZdSoQbU2rMkpLiQ6bGRinZbI4OLu9BMIFm1UUl9VnePs6BaaeEWvjJSjNm2qA+ # sdFUeEY0qVjPKOWug/G6X5uAiynM7Bu2ayBjUwIDAQABo4IBXTCCAVkwEgYDVR0T # AQH/BAgwBgEB/wIBADAdBgNVHQ4EFgQU729TSunkBnx6yuKQVvYv1Ensy04wHwYD # VR0jBBgwFoAU7NfjgtJxXWRM3y5nP+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMG # A1UdJQQMMAoGCCsGAQUFBwMIMHcGCCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYY # aHR0cDovL29jc3AuZGlnaWNlcnQuY29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2Fj # ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNV # HR8EPDA6MDigNqA0hjJodHRwOi8vY3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRU # cnVzdGVkUm9vdEc0LmNybDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1s # BwEwDQYJKoZIhvcNAQELBQADggIBABfO+xaAHP4HPRF2cTC9vgvItTSmf83Qh8WI # GjB/T8ObXAZz8OjuhUxjaaFdleMM0lBryPTQM2qEJPe36zwbSI/mS83afsl3YTj+ # IQhQE7jU/kXjjytJgnn0hvrV6hqWGd3rLAUt6vJy9lMDPjTLxLgXf9r5nWMQwr8M # yb9rEVKChHyfpzee5kH0F8HABBgr0UdqirZ7bowe9Vj2AIMD8liyrukZ2iA/wdG2 # th9y1IsA0QF8dTXqvcnTmpfeQh35k5zOCPmSNq1UH410ANVko43+Cdmu4y81hjaj # V/gxdEkMx1NKU4uHQcKfZxAvBAKqMVuqte69M9J6A47OvgRaPs+2ykgcGV00TYr2 # Lr3ty9qIijanrUR3anzEwlvzZiiyfTPjLbnFRsjsYg39OlV8cipDoq7+qNNjqFze # GxcytL5TTLL4ZaoBdqbhOhZ3ZRDUphPvSRmMThi0vw9vODRzW6AxnJll38F0cuJG # 7uEBYTptMSbhdhGQDpOXgpIUsWTjd6xpR6oaQf/DJbg3s6KCLPAlZ66RzIg9sC+N # Jpud/v4+7RWsWCiKi9EOLLHfMR2ZyJ/+xhCx9yHbxtl5TPau1j/1MIDpMPx0LckT # etiSuEtQvLsNz3Qbp7wGWqbIiOWCnb5WqxL3/BAPvIXKUjPSxyZsq8WhbaM2tszW # kPZPubdcMIIG7TCCBNWgAwIBAgIQCE/cM09+RU7bww+P+ZIYNTANBgkqhkiG9w0B # AQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGlnaUNlcnQsIEluYy4xQTA/ # BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgVGltZVN0YW1waW5nIFJTQTQwOTYg # U0hBMjU2IDIwMjUgQ0ExMB4XDTI2MDgwNTAwMDAwMFoXDTM3MTEwNDIzNTk1OVow # YzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQD # EzJEaWdpQ2VydCBTSEEyNTYgUlNBNDA5NiBUaW1lc3RhbXAgUmVzcG9uZGVyIDIw # MjYgMTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALZ7pvLJ/s1K+NSb # TGWz/TjGMPh8CQ6RucZCLv5anHzWJjF/NWJrFIhy24fcpKXlgRiky4WAawDfU3YP # 0BMxt9l3Dm5oCG5Z69AqEN1kgHg2epx+l+lZBcmJCcN0ASURML5uFIS80sZsDwO3 # BSkUxDjLJhBI+qiZP3aixAC/qEGLjsBNlLol9VZ7pfGEXiMlneJIC5/YKuizVzNF # KZZEeoy/0B8Zm+nzKBgSWG52lCO1w+nCg6XpCtklTJXeIg283hw7TmmsZXR+SMbj # brEOvZ3fP2VxIgeR28Y90ZStd3F9VuA5RVynb/whITPAo9b75Zr4Ta6Mj3URm26Q # ZYMn/FnbuTegcoRcFEZ9FOqM5T6MTdtr/n74lIT/ug0eeOzmZ6QTFg33otX+bFRs # IolvykE1jive4PuESaT8zzVeFWDAMDtozNgLctkGD1ZjkEyZtJrLl5ya0m5doH/S # cpaZCZVl6pNUOCybMc/kxC6EAmSJY24L0yYKD1Nkddsnb/ItVKi/2nXpQNMu1PT5 # prW83vV8d67WowuUs0HdY4H8AMLGvdL/WHEj3ZnqMqAQQP9u3Ai9t+5eQ02GDwy0 # ODjdzi0xlp70W+ow63/0++YDEX1M0iwgUHwbrJvfpklkZQvw3+kv3vUPItdwrocz # k9icflf55W1zOEKAcJVAIXpcMCU9AgMBAAGjggGVMIIBkTAMBgNVHRMBAf8EAjAA # MB0GA1UdDgQWBBQUyWOKMC7USvtulPPm40B+9ezN4jAfBgNVHSMEGDAWgBTvb1NK # 6eQGfHrK4pBW9i/USezLTjAOBgNVHQ8BAf8EBAMCB4AwFgYDVR0lAQH/BAwwCgYI # KwYBBQUHAwgwgZUGCCsGAQUFBwEBBIGIMIGFMCQGCCsGAQUFBzABhhhodHRwOi8v # b2NzcC5kaWdpY2VydC5jb20wXQYIKwYBBQUHMAKGUWh0dHA6Ly9jYWNlcnRzLmRp # Z2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFRpbWVTdGFtcGluZ1JTQTQwOTZT # SEEyNTYyMDI1Q0ExLmNydDBfBgNVHR8EWDBWMFSgUqBQhk5odHRwOi8vY3JsMy5k # aWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRUaW1lU3RhbXBpbmdSU0E0MDk2 # U0hBMjU2MjAyNUNBMS5jcmwwIAYDVR0gBBkwFzAIBgZngQwBBAIwCwYJYIZIAYb9 # bAcBMA0GCSqGSIb3DQEBCwUAA4ICAQCNxTphHp1SCt+ZrAmAfn0oQLFr0mLywSLa # DXQIENoyKqxrFbJblzCVP/pkXmwXOdrOpWygLzlT12os5ipDCy35RBCg2UMeApEt # rfGhz45F4Wt4WGdNdIbRWt3YTYJmpR+b7lr4d7Uwn+H600u4D7RnOGf8Wj4UNgAd # ZkfHhHv1mx9EVh71SJelcEN/oORSjXzdjfw1iZH9d8Nh/thn6hH23d+VsPAr6GAY # yzSA02nXD1nYLI7Ijmiv+xLCiYC41DSFYL3GhTiy0PxpawPtGRyaBVGzq+UiTfM8 # pD7KVyF5aQyWP4KhVGUUTnmm/RlYJoW3TiXA/+t0YcT2oRVBm3JETjajHug2AL+v # 5jhtKVnd3D0rbHXEu27o+Q8p4sEWPMqKDB+qbceb6T/6WcwTwXmQ9lOCLLYcsQeS # WmvKqzpAec9etE14jOQAzLKWdE3w/TCaKtLRaRT7LCkRYVnhA2D73FLje1O5b3HR # 5eHs0NzU/+xX7NbEdcofy0W3Wdwd1XOqtlpg/JgwtKfZM5dqO94lbUveOiJBI+xZ # EbGRsMNbXmMREUTgu+Oca7Y73MPWcslIx2VhkSKSXjDbD6rgg39H5Mh7QfieAIjW # agkJNt68Yfim6cjEzVSiLSeZfdkr5dtFPTW6jATlWJdYeeDRGCyatf8R1hSjzSvd # N8yWQPT9gzGCBkQwggZAAgEBMGgwVDELMAkGA1UEBhMCR0IxGDAWBgNVBAoTD1Nl # Y3RpZ28gTGltaXRlZDErMCkGA1UEAxMiU2VjdGlnbyBQdWJsaWMgQ29kZSBTaWdu # aW5nIENBIFIzNgIQB5zg5NEUf4XNOXPPdi036zANBglghkgBZQMEAgEFAKCBhDAY # BgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEMBgorBgEEAYI3 # AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqGSIb3DQEJBDEi # BCA3GcJ2nJRwD31/OBsQjXVMsksMBJx5fIL+O2SnGdkt3TANBgkqhkiG9w0BAQEF # AASCAgDnKA33Jb94vihEs/bVdYPpVUVD4cQaWKydf2v5mfOWjQi+QTCpsO+avD51 # 9H3FauYut7U6qR4FYUyxLsiiKvWF1wz4cGbpnvX0lN+ah8AH7ie/tIBukKmwR9xd # REDFIllEPDyq7NXgCoMY3WUfQCy5fT7Sb5QDTtM7l/OMPTu4RHCFe435RI6UZm2J # dUFh961HLYMb0x4P0IBUvNF8nGX/cauREd35ucKs6Ldyd5sTjOg3Z48XA4tNBSbI # EcWgPsDmlJP8vin6zJAZg1BToTgzfJQKUN+WSm5/udqXNNJzclaE0gyucYHrBldx # lafyRmMeRNau++0wDYws2D+lnwh1bOqPTftVf4m5J/juQPZfOzOtGL0JflouyfPw # MrAPrQdhzdiKwj4dvutHQJSsuhV6BZ0DRhQPP8c1+N1B+FWq2VJgS/Pj3qqouYno # SgRjA29UMv7bLcIoaxe2hRMpqtlrIC/+fwqWcHkAYK1rJVL/sNeCgo1iBI8yIwth # rjhmzDlLPvp0mUhvf4NdZw58CAGxB4ttsXnmZ9oQZR1ec3DgY6aoy93EHwSuMxJ9 # a3/w2ocoKcj0X1G7HythfkF2l0GFWvv8u9clrEsHeenJtsdb1OwVBF52qDAGvzfQ # 8tKhgWJk1zT5pZaWPnYR6f9yL0nfl9dQmmITNPpQIqhkvdRVPaGCAyYwggMiBgkq # hkiG9w0BCQYxggMTMIIDDwIBATB9MGkxCzAJBgNVBAYTAlVTMRcwFQYDVQQKEw5E # aWdpQ2VydCwgSW5jLjFBMD8GA1UEAxM4RGlnaUNlcnQgVHJ1c3RlZCBHNCBUaW1l # U3RhbXBpbmcgUlNBNDA5NiBTSEEyNTYgMjAyNSBDQTECEAhP3DNPfkVO28MPj/mS # GDUwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwG # CSqGSIb3DQEJBTEPFw0yNjA5MjkxOTM3MzhaMC8GCSqGSIb3DQEJBDEiBCAvYcOe # k4n7asZWDqeffXMi83J9f88RHNkRBKmVcgLH6jANBgkqhkiG9w0BAQEFAASCAgCH # hb2BD5RslatYn+tM5ymllUG40qhEHGyEgpoe4TJSaDIzNC4Do1cRi0CXqWxQpg69 # hnLnlWubeHvXblYrrAKQEFUDJu9DvrLuraNRizSXao97BSWG9EiGfiil+Cs0VSk2 # IyMFWrLfz6XXU4p+f8y29UVIOIYNvsFQwjFOq0lFEF9ZQfKLtnsYiNT9SuebW3T0 # rGXHuO2ImO+I9dv5jU6/eQvuQRJGlfVxUyraW/yeu7xpDUpgnTZQcU5AIFLYitxg # gbSkY+OUPVvj4fSWWUT3ID9hgRXk5L3WENALK14SfCoVPQWRlUHkgGePDSeQB3Sb # ouk+Zt4jDTQXivcnDgwjNr2np8v5PW4su2NI4ekLqLlGGXBbcKqWUPeT+6mqFFGK # 2cdI/9CSpxmZX22rOagsz3FD94hkFRzGb+RYbgN+agiI3UUneJpGM8MJssSp2+m8 # XsK0J+aSw8/5+ZFwB3epLLt7lmzMmM6y8S58rzciCVK9Z7cE3dKny/tQEoQHRh6t # QckBNysyPqOqAzGNw/kdtS2TOBcvh9iHDKhtmSY36A8HyaszPDM0517CboP05DcF # OP08nDGKucjkA304TP37kTEOEjzyeZbyCIMLvFa12GcaRG4qvDFZ7M5/AIRbVCXb # WdgxBhPxaJ3aa0FgE28mf4cD0gnCNcqwGD+7XBAc8w== # SIG # End signature block |