UMN-Common.psm1

####### UMN-Common Module ####

###
# Copyright 2017 University of Minnesota, Office of Information Technology

# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.

# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.

# You should have received a copy of the GNU General Public License
# along with Foobar. If not, see <http://www.gnu.org/licenses/>.
###

function Convert-ColumnIndexToA1Notation {
    <#
    .SYNOPSIS
        Short description
    .DESCRIPTION
        Long description
    .EXAMPLE
        Example
    .NOTES
        General notes
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory=$true)]
        [int]$ColumnIndex
    )
    process {
        while ($ColumnIndex -gt 0) {
            $Temp = ($ColumnIndex -1) % 26
            #$Letter =
        }
    }
} #END Convert-ColumnIndexToA1Notation

<#
    .Synopsis
    convert text or byte array to URL friendly BAse64
    .DESCRIPTION
    convert text or byte array to URL friendly BAse64
    .EXAMPLE
    ConvertTo-Base64URL -text $headerJSON
    .EXAMPLE
        ConvertTo-Base64URL -Bytes $rsa.SignData($toSign,"SHA256")
#>

function ConvertTo-Base64URL
{
    param
    (
        [Parameter(ParameterSetName='String')]
        [string]$text,

        [Parameter(ParameterSetName='Bytes')]
        [System.Byte[]]$Bytes
    )

    if($Bytes){$base = $Bytes}
    else{$base =  [System.Text.Encoding]::UTF8.GetBytes($text)}
    $base64Url = [System.Convert]::ToBase64String($base)
    $base64Url = $base64Url.Split('=')[0]
    $base64Url = $base64Url.Replace('+', '-')
    $base64Url = $base64Url.Replace('/', '_')
    $base64Url
}

function ConvertTo-OrderedDictionary {
    <#
        .SYNOPSIS
        Converts a hashtable or array to an ordered dictionary.

        .DESCRIPTION
        Takes in a hashtable or array and then returns an ordered dictionary.

        .PARAMETER Object
        Object to convert to an ordered dictionary

        .NOTES
        Name: ConvertTo-OrderedDictionary
        Author: Jeff Bolduan
        LASTEDIT: 3/11/2016

        .EXAMPLE
        @{"Item1" = "Value1"; "Item2" = "Value2"; "Item3" = "Value3"; "Item4" = "Value4"} | ConvertTo-OrderedDictionary

        Will return the following:
        Name Value
        ---- -----
        Item1 Value1
        Item2 Value2
        Item3 Value3
        Item4 Value4

        .EXAMPLE
        ConvertTo-OrderedDictionary -Object @{"Item1" = "Value1"; "Item2" = "Value2"; "Item3" = "Value3"; "Item4" = "Value4"}

        Will return the following:
        Name Value
        ---- -----
        Item1 Value1
        Item2 Value2
        Item3 Value3
        Item4 Value4
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true, ValueFromPipeline=$true)]
        [ValidateNotNullOrEmpty()]
        $Object
    )
    Begin {
        $Dictionary = [ordered]@{}
    }
    Process {
        if($Object -is [System.Collections.Hashtable]) {
            foreach($Key in ($Object.Keys | sort)) {
                $Dictionary.Add($Key, $Object[$Key])
            }
        } elseif($Object -is [System.Array]) {
            for($i = 0; $ -lt $Object.Count; $i++) {
                $Dictionary.Add($i, $Object[$i])
            }
        } else {
            throw [System.IO.InvalidDataException]
        }
    }
    End {
        return $Dictionary
    }
} #END ConvertTo-OrderedDictionary

<#
    .Synopsis
    zip up module for DSC pull Server - can not use 7zip
    .DESCRIPTION
    zip up module for DSC pull Server - can not use 7zip
    .EXAMPLE
    CreateZipFromPSModulePath -ListModuleNames cChoco -Destination $dest
#>

function CreateZipFromPSModulePath
{
    param(

        [Parameter(Mandatory)]
        [string[]]$ListModuleNames,

        [Parameter(Mandatory)]
        [string]$Destination
    )

    foreach ($module in $ListModuleNames)
    {
        $allVersions = Get-Module -Name $module -ListAvailable -Verbose
        # Package all versions of the module
        foreach ($moduleVersion in $allVersions)
        {
            $name   = $moduleVersion.Name
            $source = "$Destination\$name"
            # Create package zip
            $path    = $moduleVersion.ModuleBase
            $version = $moduleVersion.Version.ToString()
            Compress-Archive -Path "$path\*" -DestinationPath "$source.zip" -Verbose -Force
            $newName = "$Destination\$name" + "_" + "$version" + ".zip"
            # Rename the module folder to contain the version info.
            if (Test-Path $newName)
            {
                Remove-Item $newName -Recurse -Force
            }
            Rename-Item -Path "$source.zip" -NewName $newName -Force
        }
    }

}

function Get-ARP {
    <#
        .SYNOPSIS
            This function is designed to return all ARP entries

        .DESCRIPTION
            This function returns an object containing all arp entries and details for each sub item property. On 64-bit
            powershell sessions there's dynamic paramters to specify the the 32-bit registry or 64-bit registry only

        .NOTES
            Name: Get-ARP
            Author: Aaron Miller
            LASTEDIT: 05/08/2013

        .EXAMPLE
            $ARP = Get-ARP
            This returns all arp entries into a variable for processing later.

    #>

    [CmdletBinding(DefaultParameterSetName='none')]
    Param ()

    DynamicParam {
        if ([IntPtr]::size -eq 8) {
            $att1 = new-object -Type System.Management.Automation.ParameterAttribute -Property @{ParameterSetName="x64ARP"}
            $attC1 = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $attC1.Add($att1)
            $dynParam1 = new-object -Type System.Management.Automation.RuntimeDefinedParameter("x64ARP", [switch], $attC1)

            $att2 = new-object -Type System.Management.Automation.ParameterAttribute -Property @{ParameterSetName="x86ARP"}
            $attC2 = new-object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $attC2.Add($att2)
            $dynParam2 = new-object -Type System.Management.Automation.RuntimeDefinedParameter("x86ARP", [switch], $attC2)

            $paramDictionary = new-object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
            $paramDictionary.Add("x64ARP", $dynParam1)
            $paramDictionary.Add("x86ARP", $dynParam2)
            return $paramDictionary
        }
    }

    Begin {
        $Primary = "HKLM:\SOFTWARE\Microsoft\Windows\CurrentVersion\Uninstall"
        $Wow = "HKLM:\SOFTWARE\Wow6432Node\Microsoft\Windows\CurrentVersion\Uninstall"
        $toProcess = @()
        switch ($PsCmdlet.ParameterSetName) {
            "x64ARP" {$toProcess+=$Primary}
            "x86ARP" {$toProcess+=$Wow}
            default {$toProcess+=$Primary;if ([IntPtr]::size -eq 8) {$toProcess+=$Wow}}
        }
    }

    End {Return [array]($toProcess | ForEach-Object {Get-ChildItem $_} | ForEach-Object {Get-ItemProperty $_.pspath})}
} #END Get-ARP

function Get-ExceptionsList {
    <#
        .SYNOPSIS
            Get's all exceptions available on current machine.

        .DESCRIPTION
            Goes through all the assemblies on the current computer and gets every exception then outputs them to the console.

        .NOTES
            Name: Get-ExceptionsList
            Author: Jeff Bolduan
            LASTEDIT: 3/11/2016
    #>

    [CmdletBinding()]
    param()

    # Get all current assemblies
    $CurrentDomainAssemblies = [appdomain]::CurrentDomain.GetAssemblies()

    # Loop through assemblies and output any members which contain exception in the name
    foreach($Assembly in $CurrentDomainAssemblies) {
        try {
            $Assembly.GetExportedTypes() | Where-Object {
                $_.Fullname -match 'Exception'
            }
        } catch {

        }
    }
} #END Get-ExceptionsList

function Get-RandomString {
    <#
        .SYNOPSIS
            Returns a random string of a given length.

        .DESCRIPTION
            Takes in a minimum and maximum lenth and then builds a string of that size.

        .PARAMETER LengthMin
            Integer for the minimum length of the string

        .PARAMETER LengthMax
            Integer for the maximum length of the string

        .NOTES
            Name: Get-RandomString
            Author: Jeff Bolduan
            LASTEDIT: 3/11/2016

        .EXAMPLE
            Get-RandomString -LengthMin 5 -LengthMax 10

            Will return a random string composed of [a-z][A-Z][0-9] and dash, underscore and period. It's length will be between 5 and 10.
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$LengthMin,

        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [string]$LengthMax,

        [Parameter(Mandatory=$false)]
        [ValidateNotNullOrEmpty()]
        [string]$ValidCharacters = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_."
    )

    $PossibleCharacters = $ValidCharacters.ToCharArray()

    $Result = ""

    if($LengthMin -eq $LengthMax) {
        $Length = $LengthMin
    } else {
        $Length = Get-Random -Minimum $LengthMin -Maximum $LengthMax
    }

    #Write-Verbose -Message "Length: $Length"
    for($i = 0; $i -lt $Length; $i++) {
        $Result += $PossibleCharacters | Get-Random
    }

    return $Result
} #END Get-RandomString

#region Get-UsersIDM
    function Get-UsersIDM
    {
        <#
            .Synopsis
                Fetch list of users from IDM
            .DESCRIPTION
                Fetch list of users from IDM
            .EXAMPLE
                $users = Get-UsersIDM -ldapCredential $ldapCredential -ldapServer $ldapServer -ldapSearchString "(Role=*.cur*)"
            .EXAMPLE
                $users = Get-UsersIDM -ldapCredential $ldapCredential -ldapServer $ldapServer -ldapSearchString "(&(Role=*.staff.*)(cn=mrEd))"
        #>


        [CmdletBinding()]
        Param
        (
            [System.Net.NetworkCredential]$ldapCredential,

            [Parameter(Mandatory)]
            [string]$ldapServer,

            [Parameter(Mandatory)]
            [string]$ldapSearchString,

            [string]$searchDN
        )
        #Load the assemblies needed for ldap lookups
        $null = [System.Reflection.Assembly]::LoadWithPartialName("System.DirectoryServices.Protocols")
        $null = [System.Reflection.Assembly]::LoadWithPartialName("System.Net")

        #setup the ldap connection
        $ldapConnection = New-Object System.DirectoryServices.Protocols.LdapConnection((New-Object System.DirectoryServices.Protocols.LdapDirectoryIdentifier($ldapServer,636)), $ldapCredential)
        $ldapConnection.AuthType = [System.DirectoryServices.Protocols.AuthType]::Basic
        $ldapConnection.SessionOptions.ProtocolVersion = 3
        #cert validation fails, so this will never validate the cert and just connect things
        $ldapConnection.SessionOptions.VerifyServerCertificate = { return $true; }
        $ldapConnection.SessionOptions.SecureSocketLayer = $true

        $ldapConnection.Bind()

        #build the ldap query
        $ldapSearch = New-Object System.DirectoryServices.Protocols.SearchRequest
        $ldapSearch.Filter = $ldapSearchString
        $ldapSearch.Scope = "Subtree"
        $ldapSearch.DistinguishedName = $searchDN

        #execute query for Students...30 minute timeout...generally takes about 12 minutes
        $ldapResponse = $ldapConnection.SendRequest($ldapSearch, (New-Object System.TimeSpan(0,30,0))) -as [System.DirectoryServices.Protocols.SearchResponse]
        $null = $ldapConnection.Dispose()
        return ($ldapResponse)
    }
#endregion

#region Get-WebReqErrorDetails
function Get-WebReqErrorDetails {
    <#
        .SYNOPSIS
            Returns JSON Responsbody data from an Error thrown by Invoke-Webrequest or Invoke-RestMethod

        .DESCRIPTION
            Returns JSON Responsbody data from an Error thrown by Invoke-Webrequest or Invoke-RestMethod

        .PARAMETER err
            Error thrown by Invoke-Webrequest or Invoke-RestMethod

        .NOTES
            Author: Travis Sobeck
    #>

    [CmdletBinding()]
    param
    (
        [Parameter(Mandatory)]
        [System.Management.Automation.ErrorRecord]$err
    )

    $reader = New-Object System.IO.StreamReader($err.Exception.Response.GetResponseStream())
    $reader.BaseStream.Position = 0
    $reader.DiscardBufferedData()
    return ($reader.ReadToEnd() | ConvertFrom-Json)
}
#endregion
function Out-RecursiveHash {
    <#
        .SYNOPSIS
        Outputs a hashtable recursively

        .DESCRIPTION
        Takes in a hashtable and then writes the values stored within to output.

        .PARAMETER hash
        Hashtable to be outputted recursively

        .NOTES
        Name: Out-RecursiveHash
        Author: Jeff Bolduan
        LASTEDIT: 3/11/2016

        .EXAMPLE
        $hashtable = @{ "Item1" = @{ "SubItem1" = "Value" }; "Item2" = "Value2" }
        Out-RecursiveHash -Hash $hashtable

        This will output:
            SubItem1 : Value
            Item2 : Value2
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [ValidateNotNullOrEmpty()]
        [hashtable]$Hash
    )
    $Return = ""

    # Loop through each of the hashtable keys and output the key pair unless it's a hashtable then recursive call
    foreach($key in $hash.keys) {
        if($hash[$key] -is [HashTable]) {
            $Return += (Out-RecursiveHash $hash[$key])
        } else {
            $Return += "$key : $($hash[$key])`n"
        }
    }

    return $Return
} #END Out-RecursiveHash

#region Send-SplunkHEC
    function Send-SplunkHEC
    {
        <#
            .Synopsis
                Send event to Splunk HTTP Event Collector
            .DESCRIPTION
                Send event to Splunk HTTP Event Collector
            .PARAMETER uri
                URI for HEC endpoint
            .PARAMETER header
                contains auth token
            .PARAMETER host
                Part of Splunk Metadata for event. Device data being sent from
            .PARAMETER source
                Part of Splunk Metadata for event. Source
            .PARAMETER sourceType
                Part of Splunk Metadata for event. SourceType
            .PARAMETER Retries
                Set how many retries will be attempted if invoking fails
            .PARAMETER SecondsDelay
                Set how many seconds to wait between retries
            .PARAMETER metadata
                Part of Splunk Metadata for event. Combination of host,source,sourcetype in performatted hashtable, will be comverted to JSON
            .PARAMETER eventData
                Event Data in hastable or pscustomeobject, will be comverted to JSON
            .PARAMETER JsonDepth
                Optional, specifies the Depth parameter to pass to ConvertTo-JSON, defaults to 100
        #>

        [CmdletBinding()]
        Param
        (
            # Param1 help description
            [Parameter(Mandatory)]
            [string]$uri,

            [Parameter(Mandatory)]
            [Collections.Hashtable]$header,

            [Parameter(Mandatory,ParameterSetName='Components')]
            [String]$source,

            [Parameter(Mandatory,ParameterSetName='Components')]
            [String]$sourceType,

            [Alias("Host")]
            [Parameter(Mandatory,ParameterSetName='Components')]
            [String]$EventHost,

            [Parameter(Mandatory,ParameterSetName='hashtable')]
            [Collections.Hashtable]$metadata,

            # This can be [Management.Automation.PSCustomObject] or [Collections.Hashtable]
            [Parameter(Mandatory)]
            $eventData,

            [int]$Retries = 5,

            [int]$SecondsDelay = 10,

            [int]$JsonDepth = 100
        )

        Begin{
            $retryCount = 0
            $completed = $false
            $response = $null
        }
        Process
        {
            if ($metadata){$bodySplunk = $metadata.Clone()}
            else {$bodySplunk = @{'host' = $EventHost;'source' = $source;'sourcetype' = $sourcetype}}
            #Splunk takes time in Unix Epoch format, so first get the current date,
            #convert it to UTC (what Epoch is based on) then format it to seconds since January 1 1970.
            #Without converting it to UTC the date would be offset by a number of hours equal to your timezone's offset from UTC
            $bodySplunk['time'] = (Get-Date).toUniversalTime() | Get-Date -UFormat %s
            $internalEventData = $eventData | ConvertTo-Json | ConvertFrom-Json
            Add-Member -InputObject $internalEventData -Name "SplunkHECRetry" -Value $retryCount -MemberType NoteProperty
            $bodySplunk['event'] = $internalEventData
            while (-not $completed) {
                try {
                    $response = Invoke-RestMethod -Uri $uri -Headers $header -UseBasicParsing -Body ($bodySplunk | ConvertTo-Json -Depth $JsonDepth) -Method Post
                    if ($response.text -ne 'Success' -or $response.code -ne 0){throw "Failed to submit to Splunk HEC $($response)"}
                    $completed = $true
                }
                catch {
                    if ($retrycount -ge $Retries) {
                        throw
                    }
                    else {
                        Start-Sleep $SecondsDelay
                        $retrycount++
                        $bodySplunk.event.SplunkHECRetry = $retryCount
                    }
                }
            }
        }
        End{return $true}
    }
#endregion


#region Get-CurrentEpochTime
function Get-CurrentEpochTime
{
    <#
        .Synopsis
            Get current Epoch Time (seconds from 00:00:00 1 January 1970 UTC) as string with 1/100,000 of a second precision
        .DESCRIPTION
            Get current Epoch Time (seconds from 00:00:00 1 January 1970 UTC) as string with 1/100,000 of a second precision
    #>

    [OutputType([string])]
    [CmdletBinding()]
    Param
    (
    )

    Begin{}
    Process
    {
        (Get-Date).toUniversalTime() | Get-Date -UFormat %s
    }
    End{}
}
#endregion

function Set-ModuleLatestVersion
{
    <#
        .Synopsis
        installs latest version of a module and deletes the old one
        .DESCRIPTION
        The problem with Update-module is it leave the old one behind, this cleans that up
        .EXAMPLE
        Set-ModuleLatestVersion -module xPSDesiredStateConfiguration
    #>

    [CmdletBinding()]
    Param
    (
        # Param1 help description
        [Parameter(Mandatory=$true,
                   ValueFromPipelineByPropertyName=$true,
                   Position=0)]
        [string]$module
    )

    Begin
    {
    }
    Process
    {
        $currentMod = get-module -ListAvailable $module
        if ($currentMod.count -gt 1){throw "Multiple version of module installed, clear out old $($currentMod.Version)"}
        $currentVersion = $currentMod.Version.ToString()
        Update-Module $module -Force
        if((get-module -ListAvailable $module).count -gt 1){Uninstall-Module -Name $module -RequiredVersion $currentVersion;get-module -ListAvailable $module}
        else {Write-Warning "Current version was latest version"}
    }
    End
    {
    }
}

function Test-RegistryValue {
    <#
        .SYNOPSIS
            This function takes in a registry path, a name and then determines whether the registry value exists.

        .NOTES
            Name: Test-RegistryValue
            Author: Jeff Bolduan
            LASTEDIT: 09/01/2016

        .EXAMPLE
            Test-RegistryValue -Path HKLM:\Foo\Bar -Value FooBar
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)]
        [string]$Path,

        [Parameter(Mandatory=$true)]
        [string]$Name,

        [Parameter(Mandatory=$false)]
        [switch]$PassThru
    )

    if(Test-Path -LiteralPath $Path) {
        $Key = Get-Item -LiteralPath $Path
        if($Key.GetValue($Value, $null) -ne $null) {
            if($PassThru) {
                Get-ItemProperty -LiteralPath $Path -Name $Name
            } else {
                $true
            }
        } else {
            $false
        }
    } else {
        return $false
    }
} #END Test-RegistryValue

function Write-Log {
    <#
        .SYNOPSIS
        This function is used to pass messages to a ScriptLog. It can also be leveraged for other purposes if more complex logging is required.

        .DESCRIPTION
        Write-Log function is setup to write to a log file in a format that can easily be read using CMTrace.exe. Variables are setup to adjust the output.

        .PARAMETER Message
        The message you want to pass to the log.

        .PARAMETER Path
        The full path to the script log that you want to write to.

        .PARAMETER Severity
        Manual indicator (highlighting) that the message being written to the log is of concern. 1 - No Concern (Default), 2 - Warning (yellow), 3 - Error (red).

        .PARAMETER Component
        Provide a non null string to explain what is being worked on.

        .PARAMETER Context
        Provide a non null string to explain why.

        .PARAMETER Thread
        Provide a optional thread number.

        .PARAMETER Source
        What was the root cause or action.

        .PARAMETER Console
        Adjusts whether output is also directed to the console window.

        .NOTES
        Name: Write-Log
        Author: Aaron Miller
        LASTEDIT: 01/23/2013 10:09:00

        .EXAMPLE
        Write-Log -Message $exceptionMsg -Path $ScriptLog -Severity 3
        Writes the content of $exceptionMsg to the file at $ScriptLog and marks it as an error highlighted in red
    #>


    PARAM(
        [Parameter(Mandatory=$true)][string]$Message,
        [Parameter(Mandatory=$false)][string]$Path = "$env:TEMP\CMTrace.Log",
        [Parameter(Mandatory=$false)][int]$Severity = 1,
        [Parameter(Mandatory=$false)][string]$Component = " ",
        [Parameter(Mandatory=$false)][string]$Context = " ",
        [Parameter(Mandatory=$false)][string]$Thread = "1",
        [Parameter(Mandatory=$false)][string]$Source = "",
        [Parameter(Mandatory=$false)][switch]$Console
    )

    # Setup the log message

        $time = Get-Date -Format "HH:mm:ss.fff"
        $date = Get-Date -Format "MM-dd-yyyy"
        $LogMsg = '<![LOG['+$Message+']LOG]!><time="'+$time+'+000" date="'+$date+'" component="'+$Component+'" context="'+$Context+'" type="'+$Severity+'" thread="'+$Thread+'" file="'+$Source+'">'

    # Write out the log file using the ComObject Scripting.FilesystemObject

        $ForAppending = 8
        $oFSO = New-Object -ComObject scripting.filesystemobject
        $oFile = $oFSO.OpenTextFile($Path, $ForAppending, $true)
        $oFile.WriteLine($LogMsg)
        $oFile.Close()
        Remove-Variable oFSO
        Remove-Variable oFile

    # Write to the console if $Console is set to True

        if ($Console -eq $true) {Write-Host $Message}

} #END Write-Log

# SIG # Begin signature block
# MIIfBwYJKoZIhvcNAQcCoIIe+DCCHvQCAQExCzAJBgUrDgMCGgUAMGkGCisGAQQB
# gjcCAQSgWzBZMDQGCisGAQQBgjcCAR4wJgIDAQAABBAfzDtgWUsITrck0sYpfvNR
# AgEAAgEAAgEAAgEAAgEAMCEwCQYFKw4DAhoFAAQUOFCuA8/9mI4/9H2vJ+3Iq1PS
# G+mgghoTMIIEhDCCA2ygAwIBAgIQQhrylAmEGR9SCkvGJCanSzANBgkqhkiG9w0B
# AQUFADBvMQswCQYDVQQGEwJTRTEUMBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNV
# BAsTHUFkZFRydXN0IEV4dGVybmFsIFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRU
# cnVzdCBFeHRlcm5hbCBDQSBSb290MB4XDTA1MDYwNzA4MDkxMFoXDTIwMDUzMDEw
# NDgzOFowgZUxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJVVDEXMBUGA1UEBxMOU2Fs
# dCBMYWtlIENpdHkxHjAcBgNVBAoTFVRoZSBVU0VSVFJVU1QgTmV0d29yazEhMB8G
# A1UECxMYaHR0cDovL3d3dy51c2VydHJ1c3QuY29tMR0wGwYDVQQDExRVVE4tVVNF
# UkZpcnN0LU9iamVjdDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAM6q
# gT+jo2F4qjEAVZURnicPHxzfOpuCaDDASmEd8S8O+r5596Uj71VRloTN2+O5bj4x
# 2AogZ8f02b+U60cEPgLOKqJdhwQJ9jCdGIqXsqoc/EHSoTbL+z2RuufZcDX65OeQ
# w5ujm9M89RKZd7G3CeBo5hy485RjiGpq/gt2yb70IuRnuasaXnfBhQfdDWy/7gbH
# d2pBnqcP1/vulBe3/IW+pKvEHDHd17bR5PDv3xaPslKT16HUiaEHLr/hARJCHhrh
# 2JU022R5KP+6LhHC5ehbkkj7RwvCbNqtMoNB86XlQXD9ZZBt+vpRxPm9lisZBCzT
# bafc8H9vg2XiaquHhnUCAwEAAaOB9DCB8TAfBgNVHSMEGDAWgBStvZh6NLQm9/rE
# JlTvA73gJMtUGjAdBgNVHQ4EFgQU2u1kdBScFDyr3ZmpvVsoTYs8ydgwDgYDVR0P
# AQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wEQYDVR0gBAowCDAGBgRVHSAAMEQG
# A1UdHwQ9MDswOaA3oDWGM2h0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9BZGRUcnVz
# dEV4dGVybmFsQ0FSb290LmNybDA1BggrBgEFBQcBAQQpMCcwJQYIKwYBBQUHMAGG
# GWh0dHA6Ly9vY3NwLnVzZXJ0cnVzdC5jb20wDQYJKoZIhvcNAQEFBQADggEBAE1C
# L6bBiusHgJBYRoz4GTlmKjxaLG3P1NmHVY15CxKIe0CP1cf4S41VFmOtt1fcOyu9
# 08FPHgOHS0Sb4+JARSbzJkkraoTxVHrUQtr802q7Zn7Knurpu9wHx8OSToM8gUmf
# ktUyCepJLqERcZo20sVOaLbLDhslFq9s3l122B9ysZMmhhfbGN6vRenf+5ivFBjt
# pF72iZRF8FUESt3/J90GSkD2tLzx5A+ZArv9XQ4uKMG+O18aP5cQhLwWPtijnGMd
# ZstcX9o+8w8KCTUi29vAPwD55g1dZ9H9oB4DK9lA977Mh2ZUgKajuPUZYtXSJrGY
# Ju6ay0SnRVqBlRUa9VEwggTmMIIDzqADAgECAhBiXE2QjNVC+6supXM/8VQZMA0G
# CSqGSIb3DQEBBQUAMIGVMQswCQYDVQQGEwJVUzELMAkGA1UECBMCVVQxFzAVBgNV
# BAcTDlNhbHQgTGFrZSBDaXR5MR4wHAYDVQQKExVUaGUgVVNFUlRSVVNUIE5ldHdv
# cmsxITAfBgNVBAsTGGh0dHA6Ly93d3cudXNlcnRydXN0LmNvbTEdMBsGA1UEAxMU
# VVROLVVTRVJGaXJzdC1PYmplY3QwHhcNMTEwNDI3MDAwMDAwWhcNMjAwNTMwMTA0
# ODM4WjB6MQswCQYDVQQGEwJHQjEbMBkGA1UECBMSR3JlYXRlciBNYW5jaGVzdGVy
# MRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFDT01PRE8gQ0EgTGltaXRlZDEg
# MB4GA1UEAxMXQ09NT0RPIFRpbWUgU3RhbXBpbmcgQ0EwggEiMA0GCSqGSIb3DQEB
# AQUAA4IBDwAwggEKAoIBAQCqgvGEqVvYcbXSXSvt9BMgDPmb6dGPdF5u7uspSNjI
# vizrCmFgzL2SjXzddLsKnmhOqnUkcyeuN/MagqVtuMgJRkx+oYPp4gNgpCEQJ0Ca
# WeFtrz6CryFpWW1jzM6x9haaeYOXOh0Mr8l90U7Yw0ahpZiqYM5V1BIR8zsLbMaI
# upUu76BGRTl8rOnjrehXl1/++8IJjf6OmqU/WUb8xy1dhIfwb1gmw/BC/FXeZb5n
# OGOzEbGhJe2pm75I30x3wKoZC7b9So8seVWx/llaWm1VixxD9rFVcimJTUA/vn9J
# AV08m1wI+8ridRUFk50IYv+6Dduq+LW/EDLKcuoIJs0ZAgMBAAGjggFKMIIBRjAf
# BgNVHSMEGDAWgBTa7WR0FJwUPKvdmam9WyhNizzJ2DAdBgNVHQ4EFgQUZCKGtkqJ
# yQQP0ARYkiuzbj0eJ2wwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8C
# AQAwEwYDVR0lBAwwCgYIKwYBBQUHAwgwEQYDVR0gBAowCDAGBgRVHSAAMEIGA1Ud
# HwQ7MDkwN6A1oDOGMWh0dHA6Ly9jcmwudXNlcnRydXN0LmNvbS9VVE4tVVNFUkZp
# cnN0LU9iamVjdC5jcmwwdAYIKwYBBQUHAQEEaDBmMD0GCCsGAQUFBzAChjFodHRw
# Oi8vY3J0LnVzZXJ0cnVzdC5jb20vVVROQWRkVHJ1c3RPYmplY3RfQ0EuY3J0MCUG
# CCsGAQUFBzABhhlodHRwOi8vb2NzcC51c2VydHJ1c3QuY29tMA0GCSqGSIb3DQEB
# BQUAA4IBAQARyT3hBeg7ZazJdDEDt9qDOMaSuv3N+Ntjm30ekKSYyNlYaDS18Ash
# U55ZRv1jhd/+R6pw5D9eCJUoXxTx/SKucOS38bC2Vp+xZ7hog16oYNuYOfbcSV4T
# p5BnS+Nu5+vwQ8fQL33/llqnA9abVKAj06XCoI75T9GyBiH+IV0njKCv2bBS7vzI
# 7bec8ckmONalMu1Il5RePeA9NbSwyVivx1j/YnQWkmRB2sqo64sDvcFOrh+RMrjh
# JDt77RRoCYaWKMk7yWwowiVp9UphreAn+FOndRWwUTGw8UH/PlomHmB+4uNqOZrE
# 6u4/5rITP1UDBE0LkHLU6/u8h5BRsjgZMIIE/jCCA+agAwIBAgIQK3PbdGMRTFpb
# MkryMFdySTANBgkqhkiG9w0BAQUFADB6MQswCQYDVQQGEwJHQjEbMBkGA1UECBMS
# R3JlYXRlciBNYW5jaGVzdGVyMRAwDgYDVQQHEwdTYWxmb3JkMRowGAYDVQQKExFD
# T01PRE8gQ0EgTGltaXRlZDEgMB4GA1UEAxMXQ09NT0RPIFRpbWUgU3RhbXBpbmcg
# Q0EwHhcNMTkwNTAyMDAwMDAwWhcNMjAwNTMwMTA0ODM4WjCBgzELMAkGA1UEBhMC
# R0IxGzAZBgNVBAgMEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBwwHU2FsZm9y
# ZDEYMBYGA1UECgwPU2VjdGlnbyBMaW1pdGVkMSswKQYDVQQDDCJTZWN0aWdvIFNI
# QS0xIFRpbWUgU3RhbXBpbmcgU2lnbmVyMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
# MIIBCgKCAQEAv1I2gjrcdDcNeNV/FlAZZu26GpnRYziaDGayQNungFC/aS42Lwpn
# P0ChSopjNZvQGcx0qhcZkSu1VSAZ+8AaOm3KOZuC8rqVoRrYNMe4iXtwiHBRZmns
# d/7GlHJ6zyWB7TSCmt8IFTcxtG2uHL8Y1Q3P/rXhxPuxR3Hp+u5jkezx7M5ZBBF8
# rgtgU+oq874vAg/QTF0xEy8eaQ+Fm0WWwo0Si2euH69pqwaWgQDfkXyVHOaeGWTf
# dshgRC9J449/YGpFORNEIaW6+5H6QUDtTQK0S3/f4uA9uKrzGthBg49/M+1BBuJ9
# nj9ThI0o2t12xr33jh44zcDLYCQD3npMqwIDAQABo4IBdDCCAXAwHwYDVR0jBBgw
# FoAUZCKGtkqJyQQP0ARYkiuzbj0eJ2wwHQYDVR0OBBYEFK7u2WC6XvUsARL9jo2y
# VXI1Rm/xMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMBYGA1UdJQEB/wQM
# MAoGCCsGAQUFBwMIMEAGA1UdIAQ5MDcwNQYMKwYBBAGyMQECAQMIMCUwIwYIKwYB
# BQUHAgEWF2h0dHBzOi8vc2VjdGlnby5jb20vQ1BTMEIGA1UdHwQ7MDkwN6A1oDOG
# MWh0dHA6Ly9jcmwuc2VjdGlnby5jb20vQ09NT0RPVGltZVN0YW1waW5nQ0FfMi5j
# cmwwcgYIKwYBBQUHAQEEZjBkMD0GCCsGAQUFBzAChjFodHRwOi8vY3J0LnNlY3Rp
# Z28uY29tL0NPTU9ET1RpbWVTdGFtcGluZ0NBXzIuY3J0MCMGCCsGAQUFBzABhhdo
# dHRwOi8vb2NzcC5zZWN0aWdvLmNvbTANBgkqhkiG9w0BAQUFAAOCAQEAen+pStKw
# pBwdDZ0tXMauWt2PRR3wnlyQ9l6scP7T2c3kGaQKQ3VgaoOkw5mEIDG61v5MzxP4
# EPdUCX7q3NIuedcHTFS3tcmdsvDyHiQU0JzHyGeqC2K3tPEG5OfkIUsZMpk0uRlh
# dwozkGdswIhKkvWhQwHzrqJvyZW9ljj3g/etfCgf8zjfjiHIcWhTLcuuquIwF4Mi
# KRi14YyJ6274fji7kE+5Xwc0EmuX1eY7kb4AFyFu4m38UnnvgSW6zxPQ+90rzYG2
# V4lO8N3zC0o0yoX/CLmWX+sRE+DhxQOtVxzhXZIGvhvIPD+lIJ9p0GnBxcLJPufF
# cvfqG5bilK+GLjCCBawwggSUoAMCAQICEHJNXiAT1cKRQFXzfFSJVHEwDQYJKoZI
# hvcNAQELBQAwfDELMAkGA1UEBhMCVVMxCzAJBgNVBAgTAk1JMRIwEAYDVQQHEwlB
# bm4gQXJib3IxEjAQBgNVBAoTCUludGVybmV0MjERMA8GA1UECxMISW5Db21tb24x
# JTAjBgNVBAMTHEluQ29tbW9uIFJTQSBDb2RlIFNpZ25pbmcgQ0EwHhcNMTcxMjE0
# MDAwMDAwWhcNMjAxMjEzMjM1OTU5WjCByzELMAkGA1UEBhMCVVMxDjAMBgNVBBEM
# BTU1NDU1MRIwEAYDVQQIDAlNaW5uZXNvdGExFDASBgNVBAcMC01pbm5lYXBvbGlz
# MRgwFgYDVQQJDA8xMDAgVW5pb24gU3QgU0UxIDAeBgNVBAoMF1VuaXZlcnNpdHkg
# b2YgTWlubmVzb3RhMSQwIgYDVQQLDBtDb21wdXRlciBhbmQgRGV2aWNlIFN1cHBv
# cnQxIDAeBgNVBAMMF1VuaXZlcnNpdHkgb2YgTWlubmVzb3RhMIIBIjANBgkqhkiG
# 9w0BAQEFAAOCAQ8AMIIBCgKCAQEAwk6kLE9u+tWv0JUkIJSn5pWfa09g6cqFLucC
# XomNj9NYj8t+JfPna3gC6LHv3OQAUDHOoC5H+8N3ea7qVGYIiwPRHzXOGqG/tVai
# U5s5hG3vBhfRX8W1/2g4/hpgeXUzrxYn/2c5SOGGy0MU1ZJyUSFEdsjXHEV7HXK4
# qmFGV9RJxtiLZH1qUldCglxcj7zw0QnUdG6oAxpwTCeVp057/WXbnIR8a0gPse+y
# /new5+CBUGTAvrw6K2BrJQVsdIIVn/q+BbcZxh9PpeZfTtsi6lgkvy0bUWtl5sSp
# d75+hvw4Sl3HAaWZtoWN7LPmbDbbVRO2Arv4doh4Chod4wJ5xQIDAQABo4IB2DCC
# AdQwHwYDVR0jBBgwFoAUrjUjF///Bj2cUOCMJGUzHnAQiKIwHQYDVR0OBBYEFF4L
# EhElVUvT8n5txOJSNAczooSAMA4GA1UdDwEB/wQEAwIHgDAMBgNVHRMBAf8EAjAA
# MBMGA1UdJQQMMAoGCCsGAQUFBwMDMBEGCWCGSAGG+EIBAQQEAwIEEDBmBgNVHSAE
# XzBdMFsGDCsGAQQBriMBBAMCATBLMEkGCCsGAQUFBwIBFj1odHRwczovL3d3dy5p
# bmNvbW1vbi5vcmcvY2VydC9yZXBvc2l0b3J5L2Nwc19jb2RlX3NpZ25pbmcucGRm
# MEkGA1UdHwRCMEAwPqA8oDqGOGh0dHA6Ly9jcmwuaW5jb21tb24tcnNhLm9yZy9J
# bkNvbW1vblJTQUNvZGVTaWduaW5nQ0EuY3JsMH4GCCsGAQUFBwEBBHIwcDBEBggr
# BgEFBQcwAoY4aHR0cDovL2NydC5pbmNvbW1vbi1yc2Eub3JnL0luQ29tbW9uUlNB
# Q29kZVNpZ25pbmdDQS5jcnQwKAYIKwYBBQUHMAGGHGh0dHA6Ly9vY3NwLmluY29t
# bW9uLXJzYS5vcmcwGQYDVR0RBBIwEIEOb2l0bXB0QHVtbi5lZHUwDQYJKoZIhvcN
# AQELBQADggEBAENRlesMKmBaZ0g68lttYEMtaPiz+DaNpOlXBs1gH66aghB1aP6i
# iRJcFVasGLUVFncdG1xbw503LTrBUc5PECMVDVF7KKCfHA1OeFV9vOWyvdVgbe3p
# aDy1sj4CADO2D0gnxcGiZoFhEZiBkTvSsj4S3GXZEvoFHJxJLw2kvdLnzy0gH/b/
# b/yblwA1fKXw4locUpDM6qTwM7SiKgkQ5W7/280EYu8BI6c8rpiJmqM1tZLcpswu
# avB00T52Y+ZZmz3tMMVgFHn9pFFltYr3s3bEek7I6pU8unISbiyQzxqhIUKaBi8h
# y8LgoY5UnGjX5jHsIvINzms+JX5Ity02sL0wggXrMIID06ADAgECAhBl4eLj1d5Q
# RYXzJiSABeLUMA0GCSqGSIb3DQEBDQUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UE
# CBMKTmV3IEplcnNleTEUMBIGA1UEBxMLSmVyc2V5IENpdHkxHjAcBgNVBAoTFVRo
# ZSBVU0VSVFJVU1QgTmV0d29yazEuMCwGA1UEAxMlVVNFUlRydXN0IFJTQSBDZXJ0
# aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNDA5MTkwMDAwMDBaFw0yNDA5MTgyMzU5
# NTlaMHwxCzAJBgNVBAYTAlVTMQswCQYDVQQIEwJNSTESMBAGA1UEBxMJQW5uIEFy
# Ym9yMRIwEAYDVQQKEwlJbnRlcm5ldDIxETAPBgNVBAsTCEluQ29tbW9uMSUwIwYD
# VQQDExxJbkNvbW1vbiBSU0EgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0B
# AQEFAAOCAQ8AMIIBCgKCAQEAwKAvix56u2p1rPg+3KO6OSLK86N25L99MCfmutOY
# MlYjXAaGlw2A6O2igTXrC/Zefqk+aHP9ndRnec6q6mi3GdscdjpZh11emcehsrip
# hHMMzKuHRhxqx+85Jb6n3dosNXA2HSIuIDvd4xwOPzSf5X3+VYBbBnyCV4RV8zj7
# 8gw2qblessWBRyN9EoGgwAEoPgP5OJejrQLyAmj91QGr9dVRTVDTFyJG5XMY4Drk
# N3dRyJ59UopPgNwmucBMyvxR+hAJEXpXKnPE4CEqbMJUvRw+g/hbqSzx+tt4z9mJ
# mm2j/w2nP35MViPWCb7hpR2LB8W/499Yqu+kr4LLBfgKCQIDAQABo4IBWjCCAVYw
# HwYDVR0jBBgwFoAUU3m/WqorSs9UgOHYm8Cd8rIDZsswHQYDVR0OBBYEFK41Ixf/
# /wY9nFDgjCRlMx5wEIiiMA4GA1UdDwEB/wQEAwIBhjASBgNVHRMBAf8ECDAGAQH/
# AgEAMBMGA1UdJQQMMAoGCCsGAQUFBwMDMBEGA1UdIAQKMAgwBgYEVR0gADBQBgNV
# HR8ESTBHMEWgQ6BBhj9odHRwOi8vY3JsLnVzZXJ0cnVzdC5jb20vVVNFUlRydXN0
# UlNBQ2VydGlmaWNhdGlvbkF1dGhvcml0eS5jcmwwdgYIKwYBBQUHAQEEajBoMD8G
# CCsGAQUFBzAChjNodHRwOi8vY3J0LnVzZXJ0cnVzdC5jb20vVVNFUlRydXN0UlNB
# QWRkVHJ1c3RDQS5jcnQwJQYIKwYBBQUHMAGGGWh0dHA6Ly9vY3NwLnVzZXJ0cnVz
# dC5jb20wDQYJKoZIhvcNAQENBQADggIBAEYstn9qTiVmvZxqpqrQnr0Prk41/PA4
# J8HHnQTJgjTbhuET98GWjTBEE9I17Xn3V1yTphJXbat5l8EmZN/JXMvDNqJtkyOh
# 26owAmvquMCF1pKiQWyuDDllxR9MECp6xF4wnH1Mcs4WeLOrQPy+C5kWE5gg/7K6
# c9G1VNwLkl/po9ORPljxKKeFhPg9+Ti3JzHIxW7LdyljffccWiuNFR51/BJHAZIq
# UDw3LsrdYWzgg4x06tgMvOEf0nITelpFTxqVvMtJhnOfZbpdXZQ5o1TspxfTEVOQ
# Asp05HUNCXyhznlVLr0JaNkM7edgk59zmdTbSGdMq8Ztuu6VyrivOlMSPWmay5Mj
# vwTzuNorbwBv0DL+7cyZBp7NYZou+DoGd1lFZN0jU5IsQKgm3+00pnnJ67crdFwf
# z/8bq3MhTiKOWEb04FT3OZVp+jzvaChHWLQ8gbCORgClaZq1H3aqI7JeRkWEEEp6
# Tv4WAVsr/i7LoXU72gOb8CAzPFqwI4Excdrxp0I4OXbECHlDqU4sTInqwlMwofmx
# eO4u94196qIqJQl+8Sykl06VktqMux84Iw3ZQLH08J8LaJ+WDUycc4OjY61I7FGx
# CDkbSQf3npXeRFm0IBn8GiW+TRDk6J2XJFLWEtVZmhboFlBLoUlqHUCKu0QOhU/+
# AEOqnY98j2zRMYIEXjCCBFoCAQEwgZAwfDELMAkGA1UEBhMCVVMxCzAJBgNVBAgT
# Ak1JMRIwEAYDVQQHEwlBbm4gQXJib3IxEjAQBgNVBAoTCUludGVybmV0MjERMA8G
# A1UECxMISW5Db21tb24xJTAjBgNVBAMTHEluQ29tbW9uIFJTQSBDb2RlIFNpZ25p
# bmcgQ0ECEHJNXiAT1cKRQFXzfFSJVHEwCQYFKw4DAhoFAKB4MBgGCisGAQQBgjcC
# AQwxCjAIoAKAAKECgAAwGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYB
# BAGCNwIBCzEOMAwGCisGAQQBgjcCARUwIwYJKoZIhvcNAQkEMRYEFDz4MmLAv9aD
# 1Y5wVeuupxeILJjTMA0GCSqGSIb3DQEBAQUABIIBADKD8OswOiBJMTiJ4p3KUbw4
# 2At7ozGsUF8sIQ8KsoLlkVCpIat546SYxriXG+9PAvwqXmYRjLYEa8oKSP5xR3VC
# mgkXEBiOFFiGGS8dE4wtnhml3o4LVyoBSlWjBkZUMxME8FEnxZG1z8vTwnNtH8GW
# 7HOUz14WgqAENKpPKJH2YF2iIWpKRKP+pAy2O2e/ITg10JmVVgoRjePItkm02Xsy
# KhjQRt39gmXJK7ahj4pNXKgZa3Yc9g/n+PJG4ZlOX+D5IuBmDeCveCMkGTVqnXpD
# wnHzIG3sGkbNXUKtTS9dYEDZvHN4qG3EAovsTqRnuBJUh5qY6H96DqeiGYi/e1Ch
# ggIoMIICJAYJKoZIhvcNAQkGMYICFTCCAhECAQEwgY4wejELMAkGA1UEBhMCR0Ix
# GzAZBgNVBAgTEkdyZWF0ZXIgTWFuY2hlc3RlcjEQMA4GA1UEBxMHU2FsZm9yZDEa
# MBgGA1UEChMRQ09NT0RPIENBIExpbWl0ZWQxIDAeBgNVBAMTF0NPTU9ETyBUaW1l
# IFN0YW1waW5nIENBAhArc9t0YxFMWlsySvIwV3JJMAkGBSsOAwIaBQCgXTAYBgkq
# hkiG9w0BCQMxCwYJKoZIhvcNAQcBMBwGCSqGSIb3DQEJBTEPFw0xOTEyMTAyMDMw
# NTFaMCMGCSqGSIb3DQEJBDEWBBTQQiSpZPVNGAWl7xWbEl0hmTQKVTANBgkqhkiG
# 9w0BAQEFAASCAQA52neglyvtVNudIiedFuDT/hZWxQCGtPwMvQpGfdwojywa9UEc
# Bio+DMHQMyNYvtfuP5bDIrMYilLPnk9pNgNUvkMYY2S7ugTwjZMh1W+dOzAYTJ6z
# K9WIVTOobziJqRM+OFIcCqNUW5ZRVs9L4R18+RUM8r6J46xs4aZ0uxHKw4vfy73I
# BbZ8a7BOKMf8OI9CcVbMj9pdIXV/b0Yv4mU381xLIcpJPbREQ+WIFyUsYszq1z9a
# mTzFyiw+96TowO6BBchm0K95aCwa9HTgy/qRucmv+BV+d3whi7MR1PZhtvGrM1kC
# cKZLqmQZPKh3P4nTzY4aFF3qhlU3xvyd77Z+
# SIG # End signature block