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 -ErrorAction SilentlyContinue).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: $AzStackHciEnvironmentLogFile") -ConsoleOut:(-not $PassThru)
    Log-Info -Message ("Report location: $AzStackHciEnvironmentReport") -ConsoleOut:(-not $PassThru)
    Log-Info -Message ("Use -Passthru parameter to return results as a PSObject.") -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, Title, Description))
        {
            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
    }
}

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
    )

    try {
        $source = 'AzStackHciEnvironmentChecker/Telemetry'
        if ($Result.HealthCheckSource) {
            $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

    }
    catch {
        Log-Info "Failed to write result to telemetry channel. Error: $($_.Exception.message)" -Type Warning
    }
}

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"
                }
            }
        }

        if ($Summary.HelpLinks)
        {
            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."
        }
        [array]$success = $result | Select-Object -ExpandProperty AdditionalData | Where-Object Status -EQ 'Succeeded'
        [array]$HelpLinks = $result | Where-Object Status -NE 'Succeeded' | Select-Object -ExpandProperty Remediation | Sort-Object | Get-Unique
        [array]$nonSuccess = $result | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'Succeeded'
        [array]$nonSuccessCritical = $result | Where-Object Severity -EQ Critical | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'Succeeded'
        [array]$nonSuccessWarning = $result | Where-Object Severity -EQ Warning | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'Succeeded'
        [array]$nonSuccessInformational = $result | Where-Object Severity -EQ Informational | Select-Object -ExpandProperty AdditionalData | Where-Object Status -NE 'Succeeded'

        $successPercentage = if ($success.count -gt 0)
        {
            [Math]::Round(($success.Count / $result.AdditionalData.Resource.count) * 100)
        }
        else
        {
            0
        }

        $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
# MIInqgYJKoZIhvcNAQcCoIInmzCCJ5cCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCDX0CWBtRiN6V3r
# cFy6Ze3N3sVhjHFEOCVW4Vnq1wB6faCCDYEwggX/MIID56ADAgECAhMzAAACzI61
# lqa90clOAAAAAALMMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjIwNTEyMjA0NjAxWhcNMjMwNTExMjA0NjAxWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQCiTbHs68bADvNud97NzcdP0zh0mRr4VpDv68KobjQFybVAuVgiINf9aG2zQtWK
# No6+2X2Ix65KGcBXuZyEi0oBUAAGnIe5O5q/Y0Ij0WwDyMWaVad2Te4r1Eic3HWH
# UfiiNjF0ETHKg3qa7DCyUqwsR9q5SaXuHlYCwM+m59Nl3jKnYnKLLfzhl13wImV9
# DF8N76ANkRyK6BYoc9I6hHF2MCTQYWbQ4fXgzKhgzj4zeabWgfu+ZJCiFLkogvc0
# RVb0x3DtyxMbl/3e45Eu+sn/x6EVwbJZVvtQYcmdGF1yAYht+JnNmWwAxL8MgHMz
# xEcoY1Q1JtstiY3+u3ulGMvhAgMBAAGjggF+MIIBejAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQUiLhHjTKWzIqVIp+sM2rOHH11rfQw
# UAYDVR0RBEkwR6RFMEMxKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1
# ZXJ0byBSaWNvMRYwFAYDVQQFEw0yMzAwMTIrNDcwNTI5MB8GA1UdIwQYMBaAFEhu
# ZOVQBdOCqhc3NyK1bajKdQKVMFQGA1UdHwRNMEswSaBHoEWGQ2h0dHA6Ly93d3cu
# bWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY0NvZFNpZ1BDQTIwMTFfMjAxMS0w
# Ny0wOC5jcmwwYQYIKwYBBQUHAQEEVTBTMFEGCCsGAQUFBzAChkVodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01pY0NvZFNpZ1BDQTIwMTFfMjAx
# MS0wNy0wOC5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAgEAeA8D
# sOAHS53MTIHYu8bbXrO6yQtRD6JfyMWeXaLu3Nc8PDnFc1efYq/F3MGx/aiwNbcs
# J2MU7BKNWTP5JQVBA2GNIeR3mScXqnOsv1XqXPvZeISDVWLaBQzceItdIwgo6B13
# vxlkkSYMvB0Dr3Yw7/W9U4Wk5K/RDOnIGvmKqKi3AwyxlV1mpefy729FKaWT7edB
# d3I4+hldMY8sdfDPjWRtJzjMjXZs41OUOwtHccPazjjC7KndzvZHx/0VWL8n0NT/
# 404vftnXKifMZkS4p2sB3oK+6kCcsyWsgS/3eYGw1Fe4MOnin1RhgrW1rHPODJTG
# AUOmW4wc3Q6KKr2zve7sMDZe9tfylonPwhk971rX8qGw6LkrGFv31IJeJSe/aUbG
# dUDPkbrABbVvPElgoj5eP3REqx5jdfkQw7tOdWkhn0jDUh2uQen9Atj3RkJyHuR0
# GUsJVMWFJdkIO/gFwzoOGlHNsmxvpANV86/1qgb1oZXdrURpzJp53MsDaBY/pxOc
# J0Cvg6uWs3kQWgKk5aBzvsX95BzdItHTpVMtVPW4q41XEvbFmUP1n6oL5rdNdrTM
# j/HXMRk1KCksax1Vxo3qv+13cCsZAaQNaIAvt5LvkshZkDZIP//0Hnq7NnWeYR3z
# 4oFiw9N2n3bb9baQWuWPswG0Dq9YT9kb+Cs4qIIwggd6MIIFYqADAgECAgphDpDS
# AAAAAAADMA0GCSqGSIb3DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMK
# V2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0
# IENvcnBvcmF0aW9uMTIwMAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0
# ZSBBdXRob3JpdHkgMjAxMTAeFw0xMTA3MDgyMDU5MDlaFw0yNjA3MDgyMTA5MDla
# MH4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdS
# ZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMT
# H01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMTEwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCr8PpyEBwurdhuqoIQTTS68rZYIZ9CGypr6VpQqrgG
# OBoESbp/wwwe3TdrxhLYC/A4wpkGsMg51QEUMULTiQ15ZId+lGAkbK+eSZzpaF7S
# 35tTsgosw6/ZqSuuegmv15ZZymAaBelmdugyUiYSL+erCFDPs0S3XdjELgN1q2jz
# y23zOlyhFvRGuuA4ZKxuZDV4pqBjDy3TQJP4494HDdVceaVJKecNvqATd76UPe/7
# 4ytaEB9NViiienLgEjq3SV7Y7e1DkYPZe7J7hhvZPrGMXeiJT4Qa8qEvWeSQOy2u
# M1jFtz7+MtOzAz2xsq+SOH7SnYAs9U5WkSE1JcM5bmR/U7qcD60ZI4TL9LoDho33
# X/DQUr+MlIe8wCF0JV8YKLbMJyg4JZg5SjbPfLGSrhwjp6lm7GEfauEoSZ1fiOIl
# XdMhSz5SxLVXPyQD8NF6Wy/VI+NwXQ9RRnez+ADhvKwCgl/bwBWzvRvUVUvnOaEP
# 6SNJvBi4RHxF5MHDcnrgcuck379GmcXvwhxX24ON7E1JMKerjt/sW5+v/N2wZuLB
# l4F77dbtS+dJKacTKKanfWeA5opieF+yL4TXV5xcv3coKPHtbcMojyyPQDdPweGF
# RInECUzF1KVDL3SV9274eCBYLBNdYJWaPk8zhNqwiBfenk70lrC8RqBsmNLg1oiM
# CwIDAQABo4IB7TCCAekwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFEhuZOVQ
# BdOCqhc3NyK1bajKdQKVMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMAsGA1Ud
# DwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFHItOgIxkEO5FAVO
# 4eqnxzHRI4k0MFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6Ly9jcmwubWljcm9zb2Z0
# LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcmwwXgYIKwYBBQUHAQEEUjBQME4GCCsGAQUFBzAChkJodHRwOi8vd3d3Lm1p
# Y3Jvc29mdC5jb20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dDIwMTFfMjAxMV8wM18y
# Mi5jcnQwgZ8GA1UdIASBlzCBlDCBkQYJKwYBBAGCNy4DMIGDMD8GCCsGAQUFBwIB
# FjNodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2RvY3MvcHJpbWFyeWNw
# cy5odG0wQAYIKwYBBQUHAgIwNB4yIB0ATABlAGcAYQBsAF8AcABvAGwAaQBjAHkA
# XwBzAHQAYQB0AGUAbQBlAG4AdAAuIB0wDQYJKoZIhvcNAQELBQADggIBAGfyhqWY
# 4FR5Gi7T2HRnIpsLlhHhY5KZQpZ90nkMkMFlXy4sPvjDctFtg/6+P+gKyju/R6mj
# 82nbY78iNaWXXWWEkH2LRlBV2AySfNIaSxzzPEKLUtCw/WvjPgcuKZvmPRul1LUd
# d5Q54ulkyUQ9eHoj8xN9ppB0g430yyYCRirCihC7pKkFDJvtaPpoLpWgKj8qa1hJ
# Yx8JaW5amJbkg/TAj/NGK978O9C9Ne9uJa7lryft0N3zDq+ZKJeYTQ49C/IIidYf
# wzIY4vDFLc5bnrRJOQrGCsLGra7lstnbFYhRRVg4MnEnGn+x9Cf43iw6IGmYslmJ
# aG5vp7d0w0AFBqYBKig+gj8TTWYLwLNN9eGPfxxvFX1Fp3blQCplo8NdUmKGwx1j
# NpeG39rz+PIWoZon4c2ll9DuXWNB41sHnIc+BncG0QaxdR8UvmFhtfDcxhsEvt9B
# xw4o7t5lL+yX9qFcltgA1qFGvVnzl6UJS0gQmYAf0AApxbGbpT9Fdx41xtKiop96
# eiL6SJUfq/tHI4D1nvi/a7dLl+LrdXga7Oo3mXkYS//WsyNodeav+vyL6wuA6mk7
# r/ww7QRMjt/fdW1jkT3RnVZOT7+AVyKheBEyIXrvQQqxP/uozKRdwaGIm1dxVk5I
# RcBCyZt2WwqASGv9eZ/BvW1taslScxMNelDNMYIZfzCCGXsCAQEwgZUwfjELMAkG
# A1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQx
# HjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9z
# b2Z0IENvZGUgU2lnbmluZyBQQ0EgMjAxMQITMwAAAsyOtZamvdHJTgAAAAACzDAN
# BglghkgBZQMEAgEFAKCBrjAZBgkqhkiG9w0BCQMxDAYKKwYBBAGCNwIBBDAcBgor
# BgEEAYI3AgELMQ4wDAYKKwYBBAGCNwIBFTAvBgkqhkiG9w0BCQQxIgQg63weksxA
# 4+W06Tcn+h9YRobl/AU0lgY65kLidGBvu5MwQgYKKwYBBAGCNwIBDDE0MDKgFIAS
# AE0AaQBjAHIAbwBzAG8AZgB0oRqAGGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbTAN
# BgkqhkiG9w0BAQEFAASCAQAAXV3H3twvAsA+d5Q1lZDjMvhyysNZ5qZ9sP4HJ8hj
# Yeoen3rhQue/a6r/VtVQafIExCu/qGtbzqMbfHpzXolLk2tNNDp8SVxdKKzWNALQ
# sUY0TH17VBLWQJ6ZAYv8R5uUHQM7ptxG3268/s+GGklp9RlXTESrklj8D7/Xx+Np
# 9d0XMbElNsYimGCP4w8qi07pp0ITt38h1tipQRC840I/YLQlfkYx2odxASRnrJ3S
# ypNdyQOyJu+yRsu1GoyAwBTly3HF1P+zUqnyJLlaGgwSwffmgipzCQObWLp4g5ga
# Et0+U5eDI7vzf2TDcSZ81nnnTEEGNkiR3zkfGxH8TuzRoYIXCTCCFwUGCisGAQQB
# gjcDAwExghb1MIIW8QYJKoZIhvcNAQcCoIIW4jCCFt4CAQMxDzANBglghkgBZQME
# AgEFADCCAVUGCyqGSIb3DQEJEAEEoIIBRASCAUAwggE8AgEBBgorBgEEAYRZCgMB
# MDEwDQYJYIZIAWUDBAIBBQAEIJacpMJTRl/vlbR4r9LYxFnsbbICUkv35YeSdtWF
# B1JpAgZjYr0lWzoYEzIwMjIxMTE1MTA0NzM1Ljc1NlowBIACAfSggdSkgdEwgc4x
# CzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRt
# b25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1p
# Y3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMg
# VFNTIEVTTjpGN0E2LUUyNTEtMTUwQTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUt
# U3RhbXAgU2VydmljZaCCEVwwggcQMIIE+KADAgECAhMzAAABpQDeCMRAB3FOAAEA
# AAGlMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNo
# aW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29y
# cG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEw
# MB4XDTIyMDMwMjE4NTExOVoXDTIzMDUxMTE4NTExOVowgc4xCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVy
# YXRpb25zIFB1ZXJ0byBSaWNvMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpGN0E2
# LUUyNTEtMTUwQTElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vydmlj
# ZTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALqxhuggaki8wuoOMMd7
# rsEQnmAhtV8iU1Y0itsHq30TdCXJDmvZjaZ8yvOHYFVhTyD1b5JGJtwZjWz1fglC
# qsx5qBxP1Wx1CZnsQ1tiRsRWQc12VkETmkY8x46MgHsGyAmhPPpsgRnklGai7HqQ
# FB31x/Qjkx7rbAlr6PblB4tOmaR1nKxl4VIgstDwfneKaoEEw4iN/xTdztZjwyGi
# Y5hNp6beetkcizgJFO3/yRHYh0gtk+bREhrmIgbarrrgbz7MsnA7tlKvGcO9iHc6
# +2symrAVy3CzQ4IMNPFcTTx8wTZ+kpv6lFs1eG8xlfsu2NDWKshrMlKH2JpYzWAW
# 1fCOD5irXsE4LOvixZQvbneQE6+iGfIQwabj+fRdouAU2AiE+iaNsIDapdKab8WL
# xz6VPRbEL+M6MFkcsoiuKHHoshCp7JhmZ9iM0yrEx2XebOha/XQ342KsRGs2h02g
# pX6wByyT8eD3MJVIxSRm4MLIilvWcpd9N3rooawbLU6gdk7goKWS69+w2jtouXCE
# Yt6IPfZq8ldi0L/CwYbtv7mbHmIZ9Oc0JEJc6b9gcVDfoPiemMKcz15BLepyx7np
# Q2MiDKIscOqKhXuZI+PZerNOHhi/vsy2/Fj9lB6kJrMYSfV0F2frvBSBXMB7xjv8
# pgqX5QXUe8nTxb4UfJ0cDAvBAgMBAAGjggE2MIIBMjAdBgNVHQ4EFgQUX6aPAwCX
# rq6tcO773FkXS2ipGt8wHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIw
# XwYDVR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9jcmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3Js
# MGwGCCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3Nv
# ZnQuY29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENB
# JTIwMjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADATBgNVHSUEDDAKBggrBgEFBQcD
# CDANBgkqhkiG9w0BAQsFAAOCAgEAlpsLF+UwMKKER2p0WJno4G6GGGnfg3qjDdaH
# c5uvXYtG6KmHrqAf/YqHkmNotSr6ZEEnlGCJYR7W3uJ+5bpvj03wFqGefvQsKIR2
# +q6TrzozvP4NsodWTT5SVp/C6TEDGuLC9mOQKA4tyL40HTW7txb0cAdfgnyHFoI/
# BsZo/FaXezQ8hO4xUjhDpyNNeJ6WYvX5NC+Hv9nmTyzjqyEg/L2cXAOmxEWvfPAQ
# 1lfxvrtUwG75jGeUaewkhwtzanCnP3l6YjwJFKB6n7/TXtrfik1xY1kgev1JwQ5a
# UdPxwSdDmGE4XTN2s6pPOi8IO199Of6AEvh41eDxRz+11VUcpuGn7tJUeSTUSHsv
# zQ8ECOj5w77Mv55/F8hWu07egnG8SrWj5+TFxNPCpx/AFNvzz+odTRTZd4LWuomc
# MHUmLFiUGOAdetF6SofHG5EcFn0DTD1apBZzCP8xsGQcZgwVqo7ov23/uIJlMCLA
# yTYZV9ITCP09ciUJbKBVCQNrGEnQ/XLFO9mysyyDRrvHhU5uGPdXz4Jt2/ZN7JQY
# RuVNSuCpNwoK0Jr1s6ciDvHEeLyiczxoIe9GH3SyfbHx6v/phI+iE3DWo1TCK75E
# L6pt6k5i36/kn2uSVXdTH44ZVkh3/ihV3vEws78uGlvsiMcrKBgpo3HdcjDHiHoW
# sUf4GIwwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3
# DQEBCwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4G
# A1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIw
# MAYDVQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAx
# MDAeFw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVT
# MRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQK
# ExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1l
# LVN0YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA
# 5OGmTOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/
# XE/HZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1
# hlDcwUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7
# M62AW36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3K
# Ni1wjjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy
# 1cCGMFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF80
# 3RKJ1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQc
# NIIP8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahha
# YQFzymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkL
# iWHzNgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV
# 2xo3xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIG
# CSsGAQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUp
# zxD/LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBT
# MFEGDCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jv
# c29mdC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYI
# KwYBBQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGG
# MA8GA1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186a
# GMQwVgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3Br
# aS9jcmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsG
# AQUFBwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcN
# AQELBQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1
# OdfCcTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYA
# A7AFvonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbz
# aN9l9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6L
# GYnn8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3m
# Sj5mO0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0
# SCyxTkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxko
# JLo4S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFm
# PWn9y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC482
# 2rpM+Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7
# vzhwRNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYICzzCC
# AjgCAQEwgfyhgdSkgdEwgc4xCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5n
# dG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9y
# YXRpb24xKTAnBgNVBAsTIE1pY3Jvc29mdCBPcGVyYXRpb25zIFB1ZXJ0byBSaWNv
# MSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpGN0E2LUUyNTEtMTUwQTElMCMGA1UE
# AxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIaAxUA
# s8lw20WzmxDKiN1Lhh7mZWXutKiggYMwgYCkfjB8MQswCQYDVQQGEwJVUzETMBEG
# A1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWlj
# cm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1TdGFt
# cCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIFAOcdteQwIhgPMjAyMjExMTUxMDUz
# MjRaGA8yMDIyMTExNjEwNTMyNFowdDA6BgorBgEEAYRZCgQBMSwwKjAKAgUA5x21
# 5AIBADAHAgEAAgInOTAHAgEAAgIRiTAKAgUA5x8HZAIBADA2BgorBgEEAYRZCgQC
# MSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYagMA0GCSqG
# SIb3DQEBBQUAA4GBAALJjbd86JwVlaAgnL5m9UVh6CSIGB0oZ+OYmDyMeg/fGR61
# bCIgYpsMNRW1X+DMkeD/Jsz7p0XwZNZocde3OwmjDPEphR+vmpOVzTr6Db4A71QQ
# rDuSQGeWzD3wTI+LfBsURi44NM6iXkmowAwciy5fs0lRMeGqTjFQra/+jhUSMYIE
# DTCCBAkCAQEwgZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAGl
# AN4IxEAHcU4AAQAAAaUwDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzEN
# BgsqhkiG9w0BCRABBDAvBgkqhkiG9w0BCQQxIgQgAicRYxJJG2vPdyUuOWQWE5c1
# srfKjlcOkmCohTKOt/0wgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCC4Cjhx
# fmYEsaCt2AU83Khh+6JHlyk3B70vfMHMlBLcXDCBmDCBgKR+MHwxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBU
# aW1lLVN0YW1wIFBDQSAyMDEwAhMzAAABpQDeCMRAB3FOAAEAAAGlMCIEIFJJPk5c
# OQcAkFdhBB9KntdU+0N9rWA3h0JPseLwkJ1/MA0GCSqGSIb3DQEBCwUABIICAK20
# DqTn4qUrPGBP6uBkqMtzZJrMI183NlppOiM7HsB/ESdXiBrAIO3dewkOeed49cNB
# GQW3zMbiLQde9k3Va9+f+KtpE4ca4FHhiQviJwek5CsDbGwfhv8G7uc453sqxHYl
# k/l2QYdRgJkw/HRD044eniEBG1nPBtjehVZwDYhOSGjXw8ulz6D2IDOcTZESUemT
# 8WuCbCR8jALhGTIjxUeBFyKFSx5+vfuszwc94YgoUWAwtdVrET011Rl9LbnjicbU
# UdYyfUJsJfX72AXYC0PUhk4KAJeMJmK76Q895LpGFieY/7Tvi1ZDBQgHPDW1HGhs
# MI3WZpjlQwBD8eg879GB+1immVXLquQN86/iUFqnKDreG8bnk7NsgdiP+9Ptjw7U
# erpYEcYQyo6TkRniCcz03lYjQuuqiNzV9WhRC2A+ewZDN7heIh/vXNOBYJz/6Tis
# qIqfYUuj5z3tDXI2NiICIoXrMxqnfgxRDnpNyUB0h+VKnaYZKjwUW+O66Njvv+oB
# AfqI69Cbw3nBjGJvT9Xv5vnhEWd9U1VKv3qJl3yO9tUAqb12sXnm1oFPxiDGxYFL
# 1IlZbYjwVmSt4/XlFruyEacwf95G1IgknIPUM6Dha8LsM5ijkj0WYDx5K/4/Dcu7
# HSIMie7vy4qLBRzkp4rzK+mkJqZS7wH7C7VIvkJ5
# SIG # End signature block