AzStackHci.EnvironmentChecker.Reporting.psm1
<#
.SYNOPSIS Common Reporting functions across all modules/scenarios .DESCRIPTION Logging, Reporting .INPUTS Inputs (if any) .OUTPUTS Output (if any) .NOTES General notes #> function Set-AzStackHciOutputPath { param ($Path) if ([string]::IsNullOrEmpty($Path)) { $Path = Join-Path -Path $HOME -ChildPath ".AzStackHci" } $Global:AzStackHciEnvironmentLogFile = Join-Path -Path $Path -ChildPath 'AzStackHciEnvironmentChecker.log' $Global:AzStackHciEnvironmentReport = Join-Path -Path $Path -ChildPath 'AzStackHciEnvironmentReport.json' $Global:AzStackHciEnvironmentReportXml = Join-Path -Path $Path -ChildPath 'AzStackHciEnvironmentReport.xml' } Import-LocalizedData -BindingVariable lTxt -FileName AzStackHci.EnvironmentChecker.Strings.psd1 function Get-AzStackHciEnvProgress { <# .SYNOPSIS Look for existing progress or create new progress. .DESCRIPTION Finds either the latest progress XML file or creates a new progress XML file .EXAMPLE PS C:\> <example usage> Explanation of what the example does .INPUTS Clean switch, in case the user wants to start fresh Path to search for progress run. .OUTPUTS PSCustomObject of progress. .NOTES #> param ([switch]$clean, $path = $PSScriptRoot) $latestReport = Get-Item -Path $Global:AzStackHciEnvironmentReportXml -ErrorAction SilentlyContinue if (-not $clean -and $latestReport) { $report = Import-Clixml $latestReport.FullName Log-Info -Message ('Found existing report: {0}' -f $report.FilePath) -Type Info } else { $hash = @{ FilePath = $Global:AzStackHciEnvironmentReportXml Version = $MyInvocation.MyCommand.Module.Version.ToString() Jobs = @{} } $report = New-Object PSObject -Property $hash Log-Info -Message ('Creating new report {0}' -f $report.FilePath) -Type Info } $report } function Write-AzStackHciEnvProgress { <# .SYNOPSIS Write report output to JSON .DESCRIPTION After all processing, take results object and convert to JSON report. Any file already existing will be overwritten. .EXAMPLE Write-AzStackHciEnvProgress -report $report Writes $report to JSON file .INPUTS [psobject] .OUTPUTS XML file on disk (path on disk is expected to be embedded in psobject) .NOTES General notes #> param ([psobject]$report) try { $report | Export-Clixml -Depth 10 -Path $report.FilePath -Force Log-Info -Message ('AzStackHCI progress written: {0}' -f $report.FilePath) -Type Info } Catch { Log-Info -Message ('Writing XML progress to disk error {0}' -f $_.exception.message) -Type Error throw $_.exception } } function Add-AzStackHciEnvJob { <# .SYNOPSIS Adds a 'Job' to the progress object. .DESCRIPTION If a user runs the tool multiple time to check different assets e.g. Certificates on one execution and Registration details on the next execution Those executions are added to the progress for tracking purposes. Execution/Job details include: start time, parameters, parameterset (indicating what is being checked, certificates or Azure Accounts), Placeholders for EndTime and Duration (later filled in by Close-AzStackHciEnvJob) .EXAMPLE Add-AzStackHciEnvJob -report $report Adds execution job to progress object ($report) .INPUTS Report - psobject - containing all progress to date .OUTPUTS Report - psobject - updated with execution job log. .NOTES General notes #> param ($report) $allJobs = @{} $alljobs = $report.Jobs # Index for jobs must be a string for json conversion later if ($alljobs.Count) { $jobCount = ($alljobs.Count++).tostring() } else { $jobCount = '0' } # Record current job $currentJob = @{ Index = $jobCount StartTime = (Get-Date -f 'yyyy/MM/dd HH:mm:ss') PSBoundParameters = (Get-PSCallStack).Command Operations = (Get-PSCallStack).Arguments EndTime = $null Duration = $null } Log-Info -Message ('Adding current job to progress: {0}' -f $currentJob) -Type Info # Add current job $allJobs += @{"$jobcount" = $currentJob } $report.Jobs = $allJobs $report } function Close-AzStackHciEnvJob { <# .SYNOPSIS Writes endtime and duration for jobs .DESCRIPTION Find latest job entry and update time and calculates duration calls function to update xml on disk and updates and returns report object .EXAMPLE Close-AzStackHciEnvJob -report $report .INPUTS Report - psobject - containing all progress to date .OUTPUTS Report - psobject - updated with finished execution job log. .NOTES General notes #> param ($report) try { $latestJob = $report.jobs.Keys -match '[0-9]' | ForEach-Object { [int]$_ } | Sort-Object -Descending | Select-Object -First 1 $report.jobs["$latestJob"].EndTime = (Get-Date -f 'yyyy/MM/dd HH:mm:ss') $duration = (([dateTime]$report.jobs["$latestJob"].EndTime) - ([dateTime]$report.jobs["$latestJob"].StartTime)).TotalSeconds $report.jobs["$latestJob"].Duration = $duration Log-Info -Message ('Updating current job to progress with endTime: {0} and duration {1}' -f $report.jobs["$latestJob"].EndTime, $duration) -Type Info } Catch { Log-Info -Message ('Updating current job to progress failed with exception: {0}' -f $_.exception) -Type Error throw } Write-AzStackHciEnvProgress -report $report $report } function Write-AzStackHciEnvReport { <# .SYNOPSIS Writes progress to disk in JSON format .DESCRIPTION Write progress object to disk in JSON format, overwriting as neccessary. The resulting blob is intended to be a portable record of what has been checked including the results of that check .EXAMPLE Write-AzStackHciEnvReport -report $report .INPUTS Report - psobject - containing all progress to date .OUTPUTS JSON - file - named AzStackEnvReport.json .NOTES General notes #> param ([psobject]$report) try { ConvertTo-Json -InputObject $report -Depth 8 -WarningAction SilentlyContinue | Out-File $AzStackHciEnvironmentReport -Force -Encoding UTF8 Log-Info -Message ('JSON report written to {0}' -f $AzStackHciEnvironmentReport) -Type Info } catch { Log-Info -Message ('Writing JSON report failed:' -f $_.exception.message) -Type Error throw $_.exception } } function Log-Info { <# .SYNOPSIS Write verbose logging to disk .DESCRIPTION Formats and writes verbose logging to disk under scriptroot. Log type (or severity) is essentially cosmetic to the verbose log file, no action should be inferred, such as termination of the script. .EXAMPLE Write-AzStackHciEnvironmentLog -Message ('Script messaging include data {0}' -f $data) -Type 'Info|Warning|Error' -Function 'FunctionName' .INPUTS Message - a string of the body of the log entry Type - a cosmetic type or severity for the message, must be info, warning or error Function - ideally the name of the function or the script writing the log entry. .OUTPUTS Appends Log entry to AzStackHciEnvironmentChecker.log under the script root. .NOTES General notes #> [cmdletbinding()] param( [string] $Message, [ValidateSet('Info', 'Warning', 'Error', 'Success')] [string] $Type = 'Info', [ValidateNotNullOrEmpty()] [string]$Function = ((Get-PSCallStack)[0].Command), [switch]$ConsoleOut, [switch]$Telemetry ) $Message = RunMask $Message if ($ConsoleOut) { #if ($PSEdition -eq 'desktop') if ($true) { switch -wildcard ($function) { '*-AzStackHciEnvironment*' { $foregroundcolor = 'DarkYellow' } default { $foregroundcolor = "White" } } switch ($Type) { 'Success' { $foregroundcolor = 'Green' } 'Warning' { $foregroundcolor = 'Yellow' } 'Error' { $foregroundcolor = 'Red' } default { $foregroundcolor = "White" } } Write-Host $message -ForegroundColor $foregroundcolor } else { Write-Host $message } } else { Write-Verbose $message } if (-not [string]::IsNullOrEmpty($message)) { # Log to ETW if ($Telemetry) { $source = "AzStackHciEnvironmentChecker/Telemetry" $EventId = 17201 } else { $source = "AzStackHciEnvironmentChecker/Operational" $EventId = 17203 } $logName = 'AzStackHciEnvironmentChecker' $EventType = switch ($Type) { "Error" { "Error" } "Warning" { "Warning" } "Success" { "Information" } "Info" { "Information" } Default { "Information" } } Write-ETWLog -Source $Source -logName $logName -Message $Message -EventType $EventType -EventId $EventId # Log to file $entry = "[{0}] [{1}] [{2}] {3}" -f ([datetime]::now).tostring(), $type, $function, ($Message -replace "`n|`t", "") if (-not (Test-Path $AzStackHciEnvironmentLogFile)) { New-Item -Path $AzStackHciEnvironmentLogFile -Force | Out-Null } $entry | Out-File -FilePath $AzStackHciEnvironmentLogFile -Append -Force -Encoding UTF8 } } function RunMask { [cmdletbinding()] [OutputType([string])] Param ( [Parameter(ValueFromPipeline = $True)] [string] $in ) Begin {} Process { try { <#$in | Get-PIIMask | Get-GuidMask#> $in | Get-GuidMask } catch { $_.exception } } End {} } function Get-PIIMask { [cmdletbinding()] [OutputType([string])] Param ( [Parameter(ValueFromPipeline = $True)] [string] $in ) Begin { $pii = $($ENV:USERDNSDOMAIN), $($ENV:COMPUTERNAME), $($ENV:USERNAME), $($ENV:USERDOMAIN) | ForEach-Object { if ($null -ne $PSITEM) { $PSITEM } } $r = $pii -join '|' } Process { try { return [regex]::replace($in, $r, "[*redacted*]") } catch { $_.exception } } End {} } function Get-GuidMask { [OutputType([string])] [CmdletBinding()] param ( [Parameter(ValueFromPipeline = $True)] [String] $guid ) Begin { $r = [regex]::new("(-([a-fA-F0-9]{4}-){3})") } Process { try { return [regex]::replace($guid, $r, "-xxxx-xxxx-xxxx-") } catch { $_.exception } } End {} } function Write-AzStackHciHeader { <# .SYNOPSIS Write invocation and system information into log and writes cmdlet name and version to screen. #> param ( [Parameter()] [System.Management.Automation.InvocationInfo] $invocation, [psobject] $params, [switch] $PassThru ) try { $paramToString = ($params.GetEnumerator() | ForEach-Object { "$($_.Key)=$($_.Value)" }) -join ';' $cmdLetName = Get-CmdletName (Get-PSCallStack) $cmdletVersion = (Get-Command $cmdletName).version.tostring() Log-Info -Message '' Log-Info -Message ('{0} v{1} started.' -f ` $cmdLetName, $cmdletVersion) ` -ConsoleOut:(-not $PassThru) Log-Info -Telemetry -Message ('{0} started version: {1} with parameters: {2}' ` -f $invocation.MyCommand.Name, (Get-Module AzStackHci.EnvironmentChecker).Version.ToString(), $paramToString) Log-Info -Message ('OSVersion: {0} PSVersion: {1} PSEdition: {2} Security Protocol: {3}' -f ` [environment]::OSVersion.Version.tostring(), $PSVersionTable.PSVersion.tostring(), $PSEdition, [Net.ServicePointManager]::SecurityProtocol) } catch { if (-not $PassThru) { Log-Info ("Unable to write header to screen. Error: {0}" -f $_.exception.message) } } } function Write-AzStackHciFooter { <# .SYNOPSIS Writes report, log and cmdlet to screen. #> param ( [Parameter()] [System.Management.Automation.InvocationInfo] $invocation, [switch] $failed, [switch] $PassThru ) Log-Info -Message ("`nLog location (contains PII): $AzStackHciEnvironmentLogFile") -ConsoleOut:(-not $PassThru) Log-Info -Message ("Report location (contains PII): $AzStackHciEnvironmentReport") -ConsoleOut:(-not $PassThru) if ($failed) { Log-Info -Message ("{0} failed" -f (Get-CmdletName (Get-PSCallStack))) -ConsoleOut:(-not $PassThru) -Type Error -Telemetry } else { Log-Info -Message ("{0} completed" -f (Get-CmdletName (Get-PSCallStack))) -ConsoleOut:(-not $PassThru) -Telemetry } } function Get-CmdletName { param ($callstack) try { $functionCalled = (Get-PSCallStack)[-2].Command if (!$functionCalled) { "Hci Validation" } $functionCalled } catch { throw "Hci Validation" } } function Write-AzStackHciResult { <# .SYNOPSIS Displays results to screen .DESCRIPTION Displays test results to screen, highlighting failed tests. #> param ( [Parameter()] [string] $Title, [Parameter()] [psobject] $result, $seperator = ' -> ', [switch] $Expand, [switch] $ShowFailedOnly ) try { if (-not $result) { throw "Results missing. Ensure tests ran successfully." } Log-Info ("`n{0}:" -f $Title) -ConsoleOut foreach ($r in ($result | Sort-Object Status)) { if ($r.status -ne 'Succeeded' -or $Expand) { Write-StatusSymbol -Status $r.Status -Severity $r.Severity Write-Host " " -NoNewline Write-Host @expandDownSymbol Write-Host " " -NoNewline if ($r.status -ne 'Succeeded') { switch ($r.Severity) { Critical { Write-Host @needsRemediation } Warning { Write-Host @needsAttention } Informational { Write-Host @forInformation } Default { Write-Host @Critical } } } Write-Host " " -NoNewline Write-Host ($r.TargetResourceType + " - " + $r.Title + " " + $r.Description) foreach ($detail in ($r.AdditionalData | Sort-Object Status -Descending)) { if ($ShowFailedOnly -and $detail.Status -eq 'Succeeded') { continue } else { Write-Host " " -NoNewline Write-StatusSymbol -Status $detail.Status -Severity $r.Severity Write-Host " " -NoNewline Write-Host " " -NoNewline Write-Host ("{0}{1}{2}" -f $detail.Source, $seperator, $detail.Resource) } } if ($detail.Status -ne 'Succeeded') { Write-Host " " -NoNewline Write-Host @helpSymbol Write-Host (" Help URL: {0}" -f $r.Remediation) Write-Host "" } } else { if (-not $ShowFailedOnly) { Write-Host @expandOutSymbol Write-Host " " -NoNewline Write-Host @greenTickSymbol Write-Host " " -NoNewline Write-Host @isHealthy Write-Host " " -NoNewline Write-Host ($r.TargetResourceType + " " + $r.Title + " " + $r.Description) } } } } catch { Log-Info "Unable to write results. Error: $($_.exception.message)" -Type Warning -ConsoleOut } } function Write-ETWLog { [CmdletBinding()] param ( [Parameter()] [string] $source = 'AzStackHciEnvironmentChecker/Diagnostic', [Parameter()] [string] $logName = 'AzStackHciEnvironmentChecker', [Parameter(Mandatory = $true)] [string] $Message, [Parameter()] [string] $EventId = 0, [Parameter()] [string] $EventType = 'Information' ) try { # Silently check for log first try { $eventLog = Get-EventLog -LogName AzStackHciEnvironmentChecker -Source $Source -ErrorAction SilentlyContinue } catch {} # Try to create the log if (-not $eventLog) { New-AzStackHciEnvironmentCheckerLog } Write-EventLog -LogName $LogName -Source $Source -EntryType $EventType -Message $Message -EventId $EventId } catch { throw "Creating event log failed. Error $($_.exception.message)" } } function Write-ETWResult { <# .SYNOPSIS Write result to telemetry channel #> [CmdletBinding()] param ( [Parameter()] [psobject] $Result ) $source = 'AzStackHciEnvironmentChecker/Telemetry' $Result.HealthCheckSource = (Get-PSCallStack).Command | Where-Object {$_ -like 'Invoke-*'} $Message = $Result | ConvertTo-Json -Depth 5 $EventId = 17205 $EventType = if ($Result.Status -ne 'Succeeded') { 'Warning' } else { 'Information' } Write-ETWLog -Source $Source -EventType $EventType -Message $Message -EventId $EventId } function Get-AzStackHciEnvironmentCheckerEvents { <# .SYNOPSIS Retrieve AzStackHCI Environment Checker events from event log .EXAMPLE Get-AzStackHciEnvironmentCheckerEvents -Verbose Retrieve AzStackHCI Environment Checker events from event log .EXAMPLE $results = Get-AzStackHciEnvironmentCheckerEvents | ? EventId -eq 17205 | Select -last 1 | Select -expand Message | Convertfrom-Json Write-AzStackHciResult -result $results Get last result and write to screen #> [CmdletBinding()] param ( [Parameter()] [ValidateSet('Operational', 'Diagnostic', 'Telemetry')] [string] $Source ) try { $sourceFilter = switch ($source) { Operational { "AzStackHciEnvironmentChecker/Operational" } Diagnostic { "AzStackHciEnvironmentChecker/Diagnostic" } Telemetry { "AzStackHciEnvironmentChecker/Telemetry" } Default { "*" } } try { Get-EventLog -LogName AzStackHciEnvironmentChecker -Source $SourceFilter } catch {} } catch { throw "Failed to retrieve AzStackHCI environment checker logs. Error: $($_.exception.message)" } } function New-AzStackHciEnvironmentCheckerLog { try { $scriptBlock = { $logName = 'AzStackHciEnvironmentChecker' $sources = @('AzStackHciEnvironmentChecker/Operational', 'AzStackHciEnvironmentChecker/Diagnostic', 'AzStackHciEnvironmentChecker/Telemetry') foreach ($source in $sources) { New-EventLog -LogName $logName -Source $Source -ErrorAction SilentlyContinue Write-EventLog -Message ('Initializing log provider {0}' -f $source) -EventId 0 -EntryType Information -Source $source -LogName $logName -ErrorAction Stop } } if (Test-Elevation) { Invoke-Command -ScriptBlock $scriptBlock } else { $psProcess = if (Join-Path -Path $PSHOME -ChildPath powershell.exe -Resolve -ErrorAction SilentlyContinue) { Join-Path -Path $PSHOME -ChildPath powershell.exe } elseif (Join-Path -Path $PSHOME -ChildPath pwsh.exe -Resolve -ErrorAction SilentlyContinue) { Join-Path -Path $PSHOME -ChildPath pwsh.exe } else { throw "Cannot find powershell process. Please run powershell elevated and run the following command: 'New-EventLog -LogName $logName -Source $sourceName'" } Write-Warning "We need to run an elevated process to register our event log. `nPlease continue and accept the UAC prompt to continue. `nAlternatively, run: `nNew-EventLog -LogName $logName -Source $source `nmanually and restart this command." if (Grant-UACConcent) { Start-Process $psProcess -Verb Runas -ArgumentList "-command (Invoke-Command -ScriptBlock {$scriptBlock})" } else { throw "Unable to elevate and register event log provider." } } } catch { throw "Failed to create Environment Checker log. Error: $($_.Exception.Message)" } } function Remove-AzStackHciEnvironmentCheckerEventLog { <# .SYNOPSIS Remove AzStackHCI Environment Checker event log .EXAMPLE Remove-AzStackHciEnvironmentCheckerEventLog -Verbose Remove AzStackHCI Environment Checker event log #> [cmdletbinding()] param() Remove-EventLog -LogName "AzStackHciEnvironmentChecker" } function Grant-UACConcent { $concentAnswered = $false $concent = $false while ($false -eq $concentAnswered) { $promptResponse = Read-Host -Prompt "Register the event log. (Y/N)" if ($promptResponse -imatch '^y$|^yes$') { $concentAnswered = $true $concent = $true } elseif ($promptResponse -imatch '^n$|^no$') { $concentAnswered = $true $concent = $false } else { Write-Warning "Unexpected response" } } return $concent } function Write-Summary { param ($result, $property1, $property2, $property3, $seperator = '->') try { $summary = Get-Summary @PSBoundParameters # Write percentage Write-Host "`nSummary" Write-Host $lTxt.Summary if (-not ([string]::IsNullOrEmpty($summary.FailedResourceCritical))) { Write-Host " " -NoNewline Write-StatusSymbol -status 'Failed' -Severity Critical Write-Host (" {0} Critical Issue(s)" -f @($summary.FailedResourceCritical).Count) } if (-not ([string]::IsNullOrEmpty($summary.FailedResourceWarning))) { Write-Host " " -NoNewline Write-StatusSymbol -status 'Failed' -Severity Warning Write-Host (" {0} Warning Issue(s)" -f @($summary.FailedResourceWarning).Count) } if (-not ([string]::IsNullOrEmpty($summary.FailedResourceInformational))) { Write-Host " " -NoNewline Write-StatusSymbol -status 'Failed' -Severity Informational Write-Host (" {0} Informational Issue(s)" -f @($summary.FailedResourceInformational).Count) } if ($Summary.successCount -gt 0) { Write-Host " " -NoNewline Write-StatusSymbol -status 'Succeeded' Write-Host (" {0} successes" -f ($Summary.successCount)) } <#Write-Host @expandDownSymbol Write-Host " " -NoNewline switch ($Severity) { 'Critical' { Write-Host @redCrossSymbol } 'Warning' { Write-Host @warningSymbol } Default { Write-Host @redCrossSymbol } }#> #Write-Host (" {0} / {1} ({2}%)" -f $summary.SuccessCount, $Result.AdditionalData.Resource.Count, $summary.SuccessPercentage) # Write issues by severity foreach ($severity in 'Critical', 'Warning', 'Informational') { $SeverityProp = "FailedResource{0}" -f $severity $failedResources = $summary.$SeverityProp | Sort-Object | Get-Unique if ($failedResources -gt 0) { Write-Host "" Write-Severity -severity $Severity Write-Host "" #Write-Host "`n$Severity Issues:" $failedResources | Sort-Object | Get-Unique | ForEach-Object { Write-Host " " -NoNewline switch ($Severity) { 'Critical' { Write-Host @redCrossSymbol } 'Warning' { Write-Host @warningSymbol } Default { Write-Host @redCrossSymbol } } Write-Host " $PSITEM" } } } Write-Host "`nRemediation: " $Summary.HelpLinks | ForEach-Object { Write-Host " " -NoNewline Write-Host @helpSymbol Write-Host " $PSITEM" } if (-not $summary.FailedResourceCritical -and -not $summary.FailedResourceWarning -and -not $summary.FailedResourceInformational) { Write-Host "`nSummary" Write-Host @expandOutSymbol Write-Host " " -NoNewline Write-Host @greenTickSymbol Write-Host (" {0} / {1} ({2}%) resources test successfully." -f $summary.SuccessCount, $Result.AdditionalData.Resource.Count, $summary.SuccessPercentage) } } catch { Log-Info -Message "Summary failed. $($_.Exception.Message)" -ConsoleOut -Type Warning } } function Get-Summary { param ($result, $property1, $property2, $property3, $seperator = '->') try { if (-not $result) { throw "Unable to write summary. Check tests run successfully." } $success = $result | Select-Object -ExpandProperty AdditionalData | Where-Object Status -EQ 'Succeeded' $HelpLinks = $result | Where-Object Status -NE 'Succeeded' | Select-Object -ExpandProperty Remediation | Sort-Object | Get-Unique $nonSuccess = $result | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'Succeeded' $nonSuccessCritical = $result | Where-Object Severity -EQ Critical | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'Succeeded' $nonSuccessWarning = $result | Where-Object Severity -EQ Warning | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'Succeeded' $nonSuccessInformational = $result | Where-Object Severity -EQ Informational | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'Succeeded' $successPercentage = [Math]::Round(($success.Count / $result.AdditionalData.Resource.count) * 100) $sourceDestsb = { if ([string]::IsNullOrEmpty($_.$property2) -and [string]::IsNullOrEmpty($_.$property3)) { "{0}" -f $_.$property1 } elseif ([string]::IsNullOrEmpty($_.$property3)) { "{0}{1}{2}" -f $_.$property1, $seperator, $_.$property2 } else { "{0}{1}{2}({3})" -f $_.$property1, $seperator, $_.$property2, $_.$property3 } } $FailedResourceCritical = $nonSuccessCritical | Select-Object @{ label = 'SourceDest'; Expression = $sourceDestsb } -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SourceDest | Sort-Object | Get-Unique $FailedResourceWarning = $nonSuccessWarning | Select-Object @{ label = 'SourceDest'; Expression = $sourceDestsb } -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SourceDest | Sort-Object | Get-Unique $FailedResourceInformational = $nonSuccessInformational | Select-Object @{ label = 'SourceDest'; Expression = $sourceDestsb } -ErrorAction SilentlyContinue | Select-Object -ExpandProperty SourceDest | Sort-Object | Get-Unique $summary = New-Object -Type PsObject -Property @{ successCount = $success.Count nonSuccessCount = $nonSuccess.Count successPercentage = $successPercentage HelpLinks = $HelpLinks FailedResourceCritical = $FailedResourceCritical FailedResourceWarning = $FailedResourceWarning FailedResourceInformational = $FailedResourceInformational } return $summary } catch { throw "Unable to calculate summary. Error $($_.exception.message)" } } # Symbols $global:greenTickSymbol = @{ Object = [Char]0x2713 #8730 ForegroundColor = 'Green' NoNewLine = $true } $global:redCrossSymbol = @{ Object = [Char]0x2622 #0x00D7 ForegroundColor = 'Red' NoNewLine = $true } $global:WarningSymbol = @{ Object = [char]0x26A0 ForegroundColor = 'Yellow' NoNewLine = $true } $global:bulletSymbol = @{ Object = [Char]0x25BA NoNewLine = $true } # Text $global:needsAttention = @{ object = $lTxt.NeedsAttention; ForegroundColor = 'Yellow' NoNewLine = $true } $global:needsRemediation = @{ object = $lTxt.NeedsRemediation; ForegroundColor = 'Red' NoNewLine = $true } $global:ForInformation = @{ object = $lTxt.ForInformation; NoNewLine = $true } $global:expandDownSymbol = @{ object = [Char]0x25BC # expand down NoNewLine = $true } $global:expandOutSymbol = @{ object = [Char]0x25BA # expand out NoNewLine = $true } $global:helpSymbol = @{ object = [char]0x270E #0x263C # sunshine NoNewLine = $true #ForegroundColor = 'Yellow' } $global:Critical = @{ object = $lTxt.Critical; ForegroundColor = 'Red' NoNewLine = $true } $global:Warning = @{ object = $lTxt.Warning; ForegroundColor = 'Yellow' NoNewLine = $true } $global:Information = @{ object = $lTxt.Informational; NoNewLine = $true } $global:isHealthy = @{ object = $lTxt.Healthy NoNewLine = $true } function Write-StatusSymbol { param ($status, $severity) switch ($status) { "Succeeded" { Write-Host @greenTickSymbol } "Failed" { switch ($Severity) { 'Critical' { Write-Host @redCrossSymbol } 'Warning' { Write-Host @warningSymbol } Default { Write-Host @redCrossSymbol } } } Default { Write-Host @bulletSymbol } } } function Write-Severity { param ($severity) switch ($severity) { 'Critical' { Write-Host @needsRemediation } 'Warning' { Write-Host @needsAttention } 'Informational' { Write-Host @ForInformation } Default { Write-Host @Critical } } } # SIG # Begin signature block # MIInsQYJKoZIhvcNAQcCoIInojCCJ54CAQExDzANBglghkgBZQMEAgEFADB5Bgor # BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDx8PxTz/6FPDIY # 14lssq4vA4ZUkHqc54B/defVU6hjHqCCDYUwggYDMIID66ADAgECAhMzAAACzfNk # v/jUTF1RAAAAAALNMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjIwNTEyMjA0NjAyWhcNMjMwNTExMjA0NjAyWjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQDrIzsY62MmKrzergm7Ucnu+DuSHdgzRZVCIGi9CalFrhwtiK+3FIDzlOYbs/zz # HwuLC3hir55wVgHoaC4liQwQ60wVyR17EZPa4BQ28C5ARlxqftdp3H8RrXWbVyvQ # aUnBQVZM73XDyGV1oUPZGHGWtgdqtBUd60VjnFPICSf8pnFiit6hvSxH5IVWI0iO # nfqdXYoPWUtVUMmVqW1yBX0NtbQlSHIU6hlPvo9/uqKvkjFUFA2LbC9AWQbJmH+1 # uM0l4nDSKfCqccvdI5l3zjEk9yUSUmh1IQhDFn+5SL2JmnCF0jZEZ4f5HE7ykDP+ # oiA3Q+fhKCseg+0aEHi+DRPZAgMBAAGjggGCMIIBfjAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU0WymH4CP7s1+yQktEwbcLQuR9Zww # VAYDVR0RBE0wS6RJMEcxLTArBgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJh # dGlvbnMgTGltaXRlZDEWMBQGA1UEBRMNMjMwMDEyKzQ3MDUzMDAfBgNVHSMEGDAW # gBRIbmTlUAXTgqoXNzcitW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8v # d3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIw # MTEtMDctMDguY3JsMGEGCCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDov # L3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDEx # XzIwMTEtMDctMDguY3J0MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIB # AE7LSuuNObCBWYuttxJAgilXJ92GpyV/fTiyXHZ/9LbzXs/MfKnPwRydlmA2ak0r # GWLDFh89zAWHFI8t9JLwpd/VRoVE3+WyzTIskdbBnHbf1yjo/+0tpHlnroFJdcDS # MIsH+T7z3ClY+6WnjSTetpg1Y/pLOLXZpZjYeXQiFwo9G5lzUcSd8YVQNPQAGICl # 2JRSaCNlzAdIFCF5PNKoXbJtEqDcPZ8oDrM9KdO7TqUE5VqeBe6DggY1sZYnQD+/ # LWlz5D0wCriNgGQ/TWWexMwwnEqlIwfkIcNFxo0QND/6Ya9DTAUykk2SKGSPt0kL # tHxNEn2GJvcNtfohVY/b0tuyF05eXE3cdtYZbeGoU1xQixPZAlTdtLmeFNly82uB # VbybAZ4Ut18F//UrugVQ9UUdK1uYmc+2SdRQQCccKwXGOuYgZ1ULW2u5PyfWxzo4 # BR++53OB/tZXQpz4OkgBZeqs9YaYLFfKRlQHVtmQghFHzB5v/WFonxDVlvPxy2go # a0u9Z+ZlIpvooZRvm6OtXxdAjMBcWBAsnBRr/Oj5s356EDdf2l/sLwLFYE61t+ME # iNYdy0pXL6gN3DxTVf2qjJxXFkFfjjTisndudHsguEMk8mEtnvwo9fOSKT6oRHhM # 9sZ4HTg/TTMjUljmN3mBYWAWI5ExdC1inuog0xrKmOWVMIIHejCCBWKgAwIBAgIK # YQ6Q0gAAAAAAAzANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNV # BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv # c29mdCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlm # aWNhdGUgQXV0aG9yaXR5IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEw # OTA5WjB+MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYD # VQQDEx9NaWNyb3NvZnQgQ29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG # 9w0BAQEFAAOCAg8AMIICCgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+la # UKq4BjgaBEm6f8MMHt03a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc # 6Whe0t+bU7IKLMOv2akrrnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4D # dato88tt8zpcoRb0RrrgOGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+ # lD3v++MrWhAfTVYoonpy4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nk # kDstrjNYxbc+/jLTswM9sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6 # A4aN91/w0FK/jJSHvMAhdCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmd # X4jiJV3TIUs+UsS1Vz8kA/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL # 5zmhD+kjSbwYuER8ReTBw3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zd # sGbiwZeBe+3W7UvnSSmnEyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3 # T8HhhUSJxAlMxdSlQy90lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS # 4NaIjAsCAwEAAaOCAe0wggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRI # bmTlUAXTgqoXNzcitW2oynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTAL # BgNVHQ8EBAMCAYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBD # uRQFTuHqp8cx0SOJNDBaBgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jv # c29mdC5jb20vcGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3JsMF4GCCsGAQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3 # dy5taWNyb3NvZnQuY29tL3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFf # MDNfMjIuY3J0MIGfBgNVHSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEF # BQcCARYzaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1h # cnljcHMuaHRtMEAGCCsGAQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkA # YwB5AF8AcwB0AGEAdABlAG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn # 8oalmOBUeRou09h0ZyKbC5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7 # v0epo/Np22O/IjWll11lhJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0b # pdS1HXeUOeLpZMlEPXh6I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/ # KmtYSWMfCWluWpiW5IP0wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvy # CInWH8MyGOLwxS3OW560STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBp # mLJZiWhub6e3dMNABQamASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJi # hsMdYzaXht/a8/jyFqGaJ+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYb # BL7fQccOKO7eZS/sl/ahXJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbS # oqKfenoi+kiVH6v7RyOA9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sL # gOppO6/8MO0ETI7f33VtY5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtX # cVZOSEXAQsmbdlsKgEhr/Xmfwb1tbWrJUnMTDXpQzTGCGYIwghl+AgEBMIGVMH4x # CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt # b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01p # Y3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTECEzMAAALN82S/+NRMXVEAAAAA # As0wDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQw # HAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIJnv # 28uuVK3ot2GbFw2YuGI6Aeh0e9yPVAPFECiNyz/VMEIGCisGAQQBgjcCAQwxNDAy # oBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20wDQYJKoZIhvcNAQEBBQAEggEAsAJPMq+Pm6Y4LvlW1GRY2tybXsgypL1zHsMu # b2I/fy2xz5ED+igTTIK/RzfyWJHRW6jrhf7pDyrnli3GLecJRaPX6fJw3NjsNsf3 # CDZz5wxzAnEcVXzxb83SiihBrquvn4yPvYdkP82h1IhKU5Uq0WY7wZfjnjcbkOzq # Q5/eJU1DdRjDQxcHuXyqDHafTmUG0A5ezu0FDllwDSCRXfm3iyy3/q6mAFn+61lg # 57RIyDGeTcV3dQlCWyJ2Z85MJ8MPEOEDmDPHGBtJ46zzBEtWVfbZaZ7/ux0GVAv4 # jwTMMsQEkwC7rCkQUmBwkMgeteuHRQl6P5E+lw5NteSkJT51TaGCFwwwghcIBgor # BgEEAYI3AwMBMYIW+DCCFvQGCSqGSIb3DQEHAqCCFuUwghbhAgEDMQ8wDQYJYIZI # AWUDBAIBBQAwggFVBgsqhkiG9w0BCRABBKCCAUQEggFAMIIBPAIBAQYKKwYBBAGE # WQoDATAxMA0GCWCGSAFlAwQCAQUABCCQCwg7cd55BeDxDPWNwRDCraTyoiO5zDae # BQKbx0ogeAIGYvuhQcvTGBMyMDIyMDgzMTExMDAzNC4wOTlaMASAAgH0oIHUpIHR # MIHOMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH # UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQL # EyBNaWNyb3NvZnQgT3BlcmF0aW9ucyBQdWVydG8gUmljbzEmMCQGA1UECxMdVGhh # bGVzIFRTUyBFU046NjBCQy1FMzgzLTI2MzUxJTAjBgNVBAMTHE1pY3Jvc29mdCBU # aW1lLVN0YW1wIFNlcnZpY2WgghFfMIIHEDCCBPigAwIBAgITMwAAAaZZRYM5TZ7r # SwABAAABpjANBgkqhkiG9w0BAQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMK # V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0 # IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0Eg # MjAxMDAeFw0yMjAzMDIxODUxMjFaFw0yMzA1MTExODUxMjFaMIHOMQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSkwJwYDVQQLEyBNaWNyb3NvZnQg # T3BlcmF0aW9ucyBQdWVydG8gUmljbzEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046 # NjBCQy1FMzgzLTI2MzUxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNl # cnZpY2UwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDZmL97UiPnyfzU # CZ48+ybwp3Pl5tKyqHvWCg+DLzGArpe3oHa0/87+bxW0IIzUO+Ou9nzcHms7ZEeu # VfMtvbuTy9rH9NafrnIXtGbuLUooPhrEOmUJfbYz0QGP9yEwRw3iGMr6vFp3vfuz # aDy4cQ0junbV+2ArkOM3Ez90hOjLweG+TYoIXbb6GVWmJNZV6Y1E33ZiqF9QAatb # CW1C0p0otEHeL75d5mfY8cL/XUf55WT+tpa2WGauyz7Rw+gZZnJQeT0/PQ50ptbI # 2mZxR6yszrJquRpZi+UhboAgmTqCs9d9xSXkGhTHFwWUgkIzQAVgWxyEQhNcrBxx # vNw3aJ0ZpwvBDpWHkcE1s/0As+qtK4jiG2MgvwNgYFBKbvf/RMpq07MjK9v80vBn # RMm0OVu39Fq3K5igf2OtvoOk5nzkvDbVPi9YxqCjRukOUZXycGbvCf0PXZeDschy # rsu/PsJuh7Be7gIs6bFoet1FGqCvzhkIgRtzSfpHn+XlqZ72uGSX4QJ6mEwGQ9bh # 4H/FX0I55dAQdmF8yvVmk6nXvHfvKgsVSq+YSWL2zvl9/tpOTwoq1Cv0m6K3l/sV # IVWkBIVQ2KpWrcj7bSO2diK5ITM8Bb3PqdEHsjIjZqNnAWXo8fInAznFIncMpg1G # KhjxOzAPL7Slt33nkkmCbAhJLlDv7wIDAQABo4IBNjCCATIwHQYDVR0OBBYEFDpU # ITv8xpaivfVJDS/xrvwK8jfYMB8GA1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1 # GelyMF8GA1UdHwRYMFYwVKBSoFCGTmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9w # a2lvcHMvY3JsL01pY3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEp # LmNybDBsBggrBgEFBQcBAQRgMF4wXAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWlj # cm9zb2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUy # MFBDQSUyMDIwMTAoMSkuY3J0MAwGA1UdEwEB/wQCMAAwEwYDVR0lBAwwCgYIKwYB # BQUHAwgwDQYJKoZIhvcNAQELBQADggIBAIDA8Vg06Rqi5xaD4Zv4g38BxhfMa9jW # 6yZfHoBINk4UybE39MARPmULJ2H60ZlwW3urAly1Te9Kj7iPjhGzeTDmouwbntf+ # I+VU5Fqrh+RmXlWrdjfnQ+5UlFqdHVPI/rgYQS+RhUpqA1VZvs1thkdo7jyNb9ue # ACU29peOfGp5ZCYxr5mJ9gbUUtd4f8A0e4a0GiOwYHch1gFefhxI+VIayK677cCY # or0mlBAN6iumSv62SEL/7jkQ5DjcPtqRxyBNUl5v1iJYa1UthyKIH69yY6r2YqJ+ # iyUg++NY/MVQy4gpcAG7KR6FRY8bcQXDI6j8emlgiUvL40qE54ZFeDzueZqrDO0P # F0ERkIQO8OMzUDibvZA+MRXWKT1Jizf3WiHBBJaHwYxs/rBHdQeMqqiJN7thuFco # E1xZrYS/HIUqO6/hiL06lioUgP7Gp0uDd4woAgntxU0ibKeIOZ8Gry71gLc3DiL0 # kaKxpgHjdJtsIMwSveU/6oKxhg10qLNSTQ1kVQZz9KrMNUKKuRtA/Icb0D7N1+Ny # gb9RiZdMKOa3AvvTjFsSZQet4LU6ELANQhK2KGCzGbVMyS++I8GZP4K6RxEISIQd # 7J3gvMMxiibn7e2Dvx1gqbsHQoSI8p05wYfshRjHYN8EayGznMP4ipl2aKTE0DDn # JiHiMCQHswOwMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJmQAAAAAAFTANBgkq # hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x # EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv # bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 # IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1WjB8MQswCQYDVQQG # EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG # A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQg # VGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC # ggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjKNVf2AX9sSuDivbk+ # F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhgfWpSg0S3po5GawcU # 88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJprx2rrPY2vjUmZNqY # O7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/dvI2k45GPsjksUZzp # cGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka97aSueik3rMvrg0Xn # Rm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKRHh09/SDPc31BmkZ1 # zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9ituqBJR6L8FA6PRc6ZN # N3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyOArxCaC4Q6oRRRuLR # vWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItboKaDIV1fMHSRlJTY # uVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6bMURHXLvjflSxIUX # k8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6tAgMBAAGjggHdMIIB # 2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQWBBQqp1L+ZMSavoKR # PEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXAYDVR0g # BFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYzaHR0cDovL3d3dy5t # aWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnkuaHRtMBMGA1UdJQQM # MAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1UdDwQE # AwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2VsuP6KJcYmjRPZSQ # W9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwubWljcm9zb2Z0LmNv # bS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNybDBa # BggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93d3cubWljcm9zb2Z0 # LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3J0MA0GCSqG # SIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/qXBS2Pk5HZHixBpOX # PTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6U03dmLq2HnjYNi6c # qYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVtI1TkeFN1JFe53Z/z # jj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis9/kpicO8F7BUhUKz # /AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTpkbKpW99Jo3QMvOyR # gNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0sHrYUP4KWN1APMdU # bZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138eW0QBjloZkWsNn6Qo # 3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJsWkBRH58oWFsc/4K # u+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7Fx0ViY1w/ue10Cga # iQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0dFtq0Z4+7X6gMTN9 # vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQtB1VM1izoXBm8qGC # AtIwggI7AgEBMIH8oYHUpIHRMIHOMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSkwJwYDVQQLEyBNaWNyb3NvZnQgT3BlcmF0aW9ucyBQdWVydG8g # UmljbzEmMCQGA1UECxMdVGhhbGVzIFRTUyBFU046NjBCQy1FMzgzLTI2MzUxJTAj # BgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2WiIwoBATAHBgUrDgMC # GgMVAGp0M62VvUwfd1Xuz2uFD2qNn3ytoIGDMIGApH4wfDELMAkGA1UEBhMCVVMx # EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT # FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUt # U3RhbXAgUENBIDIwMTAwDQYJKoZIhvcNAQEFBQACBQDmuTz8MCIYDzIwMjIwODMx # MDU1MDUyWhgPMjAyMjA5MDEwNTUwNTJaMHcwPQYKKwYBBAGEWQoEATEvMC0wCgIF # AOa5PPwCAQAwCgIBAAICJd8CAf8wBwIBAAICESMwCgIFAOa6jnwCAQAwNgYKKwYB # BAGEWQoEAjEoMCYwDAYKKwYBBAGEWQoDAqAKMAgCAQACAwehIKEKMAgCAQACAwGG # oDANBgkqhkiG9w0BAQUFAAOBgQAynUGdhUv2+xZqhLB2/rL/Fq7y80num2UZfE6f # f2loFEhumdarTjsj/FUews6IwkeSTwnr0mlegjE9zPi7nPN3ypYemHPAd4PwUzvs # IpOEJ0O5S6G/UpLuu2+4neis/088cUVtnP/niIyv2LuD+8hg4fxzJit8h7hwyeDr # 5eR0VjGCBA0wggQJAgEBMIGTMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo # aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y # cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw # AhMzAAABpllFgzlNnutLAAEAAAGmMA0GCWCGSAFlAwQCAQUAoIIBSjAaBgkqhkiG # 9w0BCQMxDQYLKoZIhvcNAQkQAQQwLwYJKoZIhvcNAQkEMSIEII++XX6MepH+9kt5 # kdv/2VaGFJZujHQQ2xEVST/khCp4MIH6BgsqhkiG9w0BCRACLzGB6jCB5zCB5DCB # vQQggwsZi8M/dH1r4TCmyUwEGirdw6F3ogIX6fEw/bYEqw0wgZgwgYCkfjB8MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy # b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAaZZRYM5TZ7rSwABAAABpjAi # BCBHDZboXFSwAFH0+LRbLDCHXGHjNu8UTi6Xq+wtpT4wQTANBgkqhkiG9w0BAQsF # AASCAgDXa7ufmZOrma2dih7eCZg7Wf5fPO4kYsHYaQSPU751b+LYoA3rUunShC1U # a8LGu32r9arG56CGPaFbJvM0a0mppTHZSFQ340Zw+cDfuOJhJpNO/fznHUPfMxmI # hkYYWKFGUKGWqL1lYm7+GnxJX0CwvfUqeQyHBKnbB+1vMqQwl+B17m7Er6lQlP4g # J8yL/SRd6W9M/GFKZKglJCSyaeNCOHxE/8Qe7VNwF/N9LverSITvIFiZhiPWMGGq # ryXjCE9fvLkZzI19EMMl5twYS7Jf/BBsFCZzZjfsikyzDO1W0TAqfl+38M6FjyCs # LQ5l0JF2ufxnKINCi3WLRN+SbPlYfv+OMCHQgg0m7rRxwXYzKsNXCT+mrvgBv34x # K5KoRq3/xzI+nGIHn+36Bmky7mSBZWmRJjZ+JR5V6LiOeofJmdQo3ldzZVRgpnpk # MSAjv1P63ZdLCIZtuUDQGN8t3Rd7YY9n2C6FCIeOPEp75rrtZyQ2Qa4knoSF6qli # Z0fLKJyMo7yq/xw8WOhjstYJJYXuXwK7Y4VikN54224VItWIhV0ULJrbE4XWuKcO # aVdCUMJ0axQY5IHDzR1AvRkIOq2IGWK0vfLKzTJI5XPXQWSS8yre53eb9p0pkGXH # tufK377A8U8HXMH1xnbe6GyYbKOQ0RpSw6kxeoX0oVWMMhkiVA== # SIG # End signature block |