classes/OSConfig.ps1

# Copyright (c) Microsoft Corporation. All rights reserved.

function Get-ServerType() {
    try {
        $Value = Get-ItemPropertyValue -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Name "SysvolReady"
        if (-not [String]::IsNullOrWhiteSpace($Value)) {
            return "Domain Controller"
        }
    } catch {
        # Ignored.
    }

    try {
        $Value = Get-ItemPropertyValue -LiteralPath "HKLM:\SYSTEM\CurrentControlSet\Services\Tcpip\Parameters" -Name "Domain"
        if (-not [String]::IsNullOrWhiteSpace($Value)) {
            return "Member Server"
        }
    } catch {
        # Ignored.
    }

    return "Workgroup Member"
}

function Get-EnvironmentType {
    $Properties = Get-ItemProperty -Path "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion"

    if ($Properties.InstallationType -match "Server") {
        if ($Properties.EditionId -eq "ServerAzureStackHCICor") {
            $InstallationType = "AzureLocal"

            if ($Properties.DisplayVersion -eq "24H2") {
                $Version = "24H2"
            } elseif ($Properties.DisplayVersion -eq "23H2") {
                $Version = "23H2"
            } else {
                return
            }
        } else {
            $InstallationType = "WindowsServer"

            # We need to have the necessary update for Windows Server 2022 and Windows Server 23H2
            # but not for Windows Server 2025. We can use the UBR (Update Build Revision) to
            # determine the version.

            if ($Properties.DisplayVersion -eq "24H2") {
                $Version = "2025"
            } elseif (($Properties.DisplayVersion -eq "23H2") -and ($Properties.UBR -ge 1369)) {
                $Version = "2022"
            } elseif (($Properties.DisplayVersion -eq "21H2") -and ($Properties.UBR -ge 3091)) {
                $Version = "2022"
            } else {
                return
            }
        }
    } else {
        return
    }

    $Role = (Get-ServerType) -replace " ", ""

    "$InstallationType\$Version\$Role"
}

function ConvertFrom-Expression($Expression) {
    if ($Expression -isnot [String] -or [String]::IsNullOrWhiteSpace($Expression)) {
        throw [InvalidValueException]::new($Strings.ErrorInvalidExpression -f $Expression)
    }

    # JSON object format
    if ($Expression.TrimStart().StartsWith('{')) {
        try {
            $JsonObj = ConvertFrom-Json -InputObject $Expression
            $Result = @()
            foreach ($Property in $JsonObj.PSObject.Properties) {
                $Result += [PSCustomObject]@{ Name = $Property.Name; Value = $Property.Value }
            }
            if ($Result.Count -gt 0) {
                return , $Result
            }
        } catch {
            # Not valid JSON, fall through to semicolon-delimited parsing
        }
    }

    # Semicolon-delimited format
    $InQuotes = $False
    $ProcessedExpression = $Expression.Clone()

    for ($i = 0; $i -lt $Expression.Length; $i++) {
        if ($Expression[$i] -eq '"') {
            $InQuotes = -not $InQuotes
            continue
        }

        if ($InQuotes) {
            continue
        }

        if ($Expression[$i] -eq ";") {
            $ProcessedExpression = $ProcessedExpression.Remove($i, 1).Insert($i, "`n")
        }
    }

    try {
        $Result = @($ProcessedExpression | ConvertFrom-Csv -Header @("Name", "Value") -Delimiter ":")

        if (($Result.Count -eq 1) -and (-not $Result.Value)) {
            $Result[0].Value = $Result[0].Name
            $Result[0].Name = "*"
        }

        , $Result
    } catch {
        throw [InvalidValueException]::new($Strings.ErrorInvalidExpression -f $Expression)
    }
}

function Find-Rule($Name, $Rules) {
    # First pass: exact or wildcard match
    foreach ($Rule in $Rules) {
        $InclusionRule = -not $Rule.Name.StartsWith("!")
        $RuleName = if ($InclusionRule) { $Rule.Name } else { $Rule.Name.Substring(1) }

        if ($Name -like $RuleName) {
            return $(if ($InclusionRule) { $Rule } else { $null })
        }
    }

    # Second pass: segment-boundary suffix match
    foreach ($Rule in $Rules) {
        $InclusionRule = -not $Rule.Name.StartsWith("!")
        $RuleName = if ($InclusionRule) { $Rule.Name } else { $Rule.Name.Substring(1) }

        if ($Name -like "*\$RuleName") {
            return $(if ($InclusionRule) { $Rule } else { $null })
        }
    }
}

class OSConfigReason {
    [DscProperty()]
    [String] $Code

    [DscProperty()]
    [String] $Phrase

    OSConfigReason() { }

    OSConfigReason([String] $RuleId, [String] $Severity, [Bool] $IsCompliant, [String] $Reason) {
        $Status = if ($IsCompliant) { 'BaselineSettingCompliant' } else { 'BaselineSettingNotCompliant' }

        if ($RuleId) {
            $Status = "$Status`:$RuleId"
        }

        $this.Code = $Status

        if (-not [String]::IsNullOrWhiteSpace($Severity)) {
            $this.Phrase = "[$Severity] $Reason"
        } else {
            $this.Phrase = $Reason
        }
    }
}

[DscResource()]
class OSConfig {
    [DscProperty()]
    [String] $RuleId

    [DscProperty()]
    [String] $Severity

    [DscProperty()]
    [String] $CorrelationGroup

    [DscProperty(Key)]
    [String] $Name

    [DscProperty(Key)]
    [String] $Type

    [DscProperty(Key)]
    [String] $Properties

    [DscProperty()]
    [String] $Value

    [DscProperty()]
    [String] $ValueType

    [DscProperty()]
    [String] $ValueName = 'value'

    [DscProperty()]
    [String] $Schema

    [DscProperty()]
    [Bool] $ExtendSchema

    [DscProperty()]
    [String] $Expression

    [DscProperty()]
    [String] $Template

    [DscProperty()]
    [String] $RoleFilter

    [DscProperty()]
    [String] $VersionFilter

    [DscProperty(NotConfigurable)]
    [OSConfigReason[]] $Reasons

    hidden [Bool] $IsCompliant = $True

    [OSConfig] Get() {
        $CurrentState = [OSConfig]::new()

        $CurrentState.RuleId = $this.RuleId
        $CurrentState.Severity = $this.Severity
        $CurrentState.Name = $this.Name
        $CurrentState.Type = $this.Type
        $CurrentState.Properties = $this.Properties
        $CurrentState.Value = $this.Value
        $CurrentState.Schema = $this.Schema
        $CurrentState.ExtendSchema = $this.ExtendSchema
        $CurrentState.ValueType = $this.ValueType
        $CurrentState.RoleFilter = $this.RoleFilter
        $CurrentState.VersionFilter = $this.VersionFilter
        $CurrentState.Reasons = @()

        $env:OSCONFIG_LOG_DIR = "$PSScriptRoot\logs"
        $env:DMOSCONFIG_AUTHORITY = $Script:Constants.Authority.Cloud

        try {
            $ErrorActionPreference = 'Stop'

            if (-not $this.IsApplicable()) {
                $CurrentState.IsCompliant = $True
                $CurrentState.Reasons += [OSConfigReason]::new($this.RuleId, $null, $CurrentState.IsCompliant, 'Not applicable')
                return $CurrentState
            }

            $ActualValue = $this.GetActualValue()

            $ResourceProperties = @{
                'resource' = @{
                    'name'       = $this.Name
                    'type'       = $this.Type
                    'properties' = $this.Properties | ConvertFrom-Json
                }
                'template' = $this.GetTemplate($ActualValue)
            }

            $ResourceSchema = $this.GetSchema($ActualValue)
            $ResourceExpression = $this.Expression

            if ($ResourceExpression) {
                $ResourceProperties['expression'] = $ResourceExpression
            } elseif ($ResourceSchema) {
                $ResourceProperties['schema'] = $ResourceSchema
            }

            $Resource = @{
                Name       = $this.Name
                Type       = 'Microsoft.OSConfig/Test'
                Properties = $ResourceProperties
            }

            $Output = Invoke-Native exec resource --correlation-id $(Get-CorrelationId) --correlation-group $this.CorrelationGroup --mode get --name $Resource.Name --type $Resource.Type --properties (ConvertTo-Json -InputObject $Resource.Properties -Compress -Depth 32)

            $CurrentState.IsCompliant = $Output.Properties.Compliance.Status -eq 'compliant'
            $CurrentState.Reasons += [OSConfigReason]::new($this.RuleId, $CurrentState.Severity, $CurrentState.IsCompliant, $Output.Properties.Compliance.Reason)
        } catch {
            $CurrentState.IsCompliant = $False
            $CurrentState.Reasons += [OSConfigReason]::new($this.RuleId, $CurrentState.Severity, $CurrentState.IsCompliant, "$_")
            Write-Verbose "Error: $_"
        }

        return $CurrentState
    }

    [Bool] Test() {
        try {
            return $this.Get().IsCompliant
        } catch {
            Write-Verbose "Error: $_"
        }

        return $False
    }

    [Void] Set() {
        try {
            $env:OSCONFIG_LOG_DIR = "$PSScriptRoot\logs"
            $env:DMOSCONFIG_AUTHORITY = $Script:Constants.Authority.Cloud

            if (-not $this.CorrelationGroup) {
                throw "No correlation group specified."
            }

            $ResourceProperties = $this.Properties | ConvertFrom-Json

            if ($this.ValueName) {
                $ResourceProperties | Add-Member -MemberType NoteProperty -Name $this.ValueName -Value $this.GetActualValue()
            }

            Invoke-Native exec resource --correlation-id $(Get-CorrelationId) --correlation-group $this.CorrelationGroup --mode set --name $this.Name --type $this.type --properties $ResourceProperties
        } catch {
            Write-Verbose "Error: $_"
        }
    }

    [Bool] IsApplicable() {
        if (-not [String]::IsNullOrWhiteSpace($this.RoleFilter)) {
            $CurrentRole = Get-ServerType
            $AllowedRoles = $this.RoleFilter -split ',' | ForEach-Object { $_.Trim() }

            if ($AllowedRoles -notcontains $CurrentRole) {
                return $False
            }
        }

        if (-not [String]::IsNullOrWhiteSpace($this.VersionFilter)) {
            $CurrentVersion = Get-WindowsServerVersion

            if (-not $CurrentVersion) {
                return $False
            }

            $AllowedVersions = $this.VersionFilter -split ',' | ForEach-Object { $_.Trim() }

            if ($AllowedVersions -notcontains $CurrentVersion) {
                return $False
            }
        }

        if (-not [String]::IsNullOrWhiteSpace($this.Value)) {
            $Rules = ConvertFrom-Expression -Expression $this.Value
            $IsEnvironmentValue = $Rules -and ($Rules.Name -match '^(WindowsServer|AzureLocal)\\.*\\.*$')

            if ($IsEnvironmentValue) {
                $EnvironmentType = Get-EnvironmentType
                $Rule = Find-Rule -Name $EnvironmentType -Rules $Rules

                if (-not $Rule) {
                    return $False
                }
            }
        }

        # If there is no schema or expression, there must be a value to generate the compliance reasoning.
        # Omitting the schema and expression will provide a default compliance reasoning based on the value.
        if (-not $this.Schema -and -not $this.Expression -and -not $this.Value) {
            if ($this.ValueType -ne 'string[]') {
                throw "No value, schema, or expression specified to evaluate compliance."
            }
        }

        return $True
    }

    [PSCustomObject] GetDefaultSchema([PSCustomObject] $ActualValue) {
        if ($null -eq $ActualValue) {
            return @{ 'type' = 'null' }
        }

        switch ($this.ValueType) {
            'string' {
                return @{ 'type' = 'string'; 'const' = $ActualValue }
            }
            'string[]' {
                $ArraySchema = @{
                    'type'        = 'array'
                    'items'       = @{
                        'type' = 'string'
                        'enum' = $ActualValue
                    }
                    'minItems'    = $ActualValue.Count
                    'maxItems'    = $ActualValue.Count
                    'uniqueItems' = $True
                }
                if ($ActualValue.Count -eq 0) {
                    return @{ 'anyOf' = @( @{ 'type' = 'null' }, $ArraySchema ) }
                }
                return $ArraySchema
            }
            'integer' {
                return @{ 'type' = 'integer'; 'const' = $ActualValue }
            }
            'boolean' {
                return @{ 'type' = 'boolean'; 'const' = $ActualValue }
            }
        }

        return @{ 'const' = $ActualValue }
    }

    [PSCustomObject] GetSchema([PSCustomObject] $ActualValue) {
        $DefaultSchema = $this.GetDefaultSchema($ActualValue)
        $CustomSchema = if ($this.Schema) { ConvertFrom-Json -InputObject $this.Schema }

        if ($this.ExtendSchema -and $CustomSchema) {
            return @{
                'allOf' = @(
                    $DefaultSchema,
                    $CustomSchema
                )
            }
        } elseif ($CustomSchema) {
            return $CustomSchema
        } else {
            return $DefaultSchema
        }
    }

    [String] GetTemplate([PSCustomObject] $ActualValue) {
        if ($this.Template) {
            return $this.Template
        }

        if ($null -eq $ActualValue) {
            return "The value {value} must be (null)"
        }

        return "The value {value} must be $(ConvertTo-Json -InputObject $ActualValue -Compress)."
    }

    [PSCustomObject] GetActualValue() {
        $Rules = if (-not [String]::IsNullOrWhiteSpace($this.Value)) {
            ConvertFrom-Expression -Expression $this.Value
        }

        $EnvironmentType = Get-EnvironmentType
        $Rule = Find-Rule -Name $EnvironmentType -Rules $Rules

        $StringValue = if ($null -ne $Rule) {
            $Rule.Value
        } elseif ($this.Value -or ($this.ValueType -eq 'string[]')) {
            $this.Value
        } else {
            (ConvertFrom-Json -InputObject $this.Properties).$($this.ValueName)
        }

        if ($null -eq $StringValue) {
            return $null
        }

        try {
            switch ($this.ValueType) {
                'string' {
                    return $StringValue
                }
                'integer' {
                    return [Int64]::Parse($StringValue)
                }
                'boolean' {
                    if ([Int32]::TryParse($StringValue, [ref]$null)) {
                        return [Boolean]::Parse(([Int32]$StringValue -ne 0).ToString())
                    } else {
                        return [Boolean]::Parse($StringValue)
                    }
                }
                'string[]' {
                    if ([String]::IsNullOrWhiteSpace($StringValue)) {
                        return @()
                    } else {
                        return @($StringValue -split ',' | ForEach-Object { $_.Trim() })
                    }
                }
            }
        } catch {
            throw "Unable to convert value '$StringValue' to type '$($this.ValueType)'"
        }

        return $StringValue
    }
}

# SIG # Begin signature block
# MIIncQYJKoZIhvcNAQcCoIInYjCCJ14CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCW0Unie2w01Uic
# u6S9kMy3Tk+lxkDzCDffaYRaAtuMtaCCDMkwggYEMIID7KADAgECAhMzAAACHPrN
# xZvoL37EAAAAAAIcMA0GCSqGSIb3DQEBCwUAMFcxCzAJBgNVBAYTAlVTMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBD
# b2RlIFNpZ25pbmcgUENBIDIwMjQwHhcNMjYwNDE2MTg1OTQxWhcNMjcwNDE1MTg1
# OTQxWjB0MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYD
# VQQDExVNaWNyb3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IB
# DwAwggEKAoIBAQDVsZfgOKmM31HPfoWOoNEiw0SlCiIxUMC0I9NMWbucKOw/e9lP
# oAoehQVu6SG65V4EPzrYsnBnFPNoi4/HoOdjhz1qkrEt4I6tEcxXU6oOeY9zGveC
# /3iBeuhLYxM3M/PkcUoebF+Nednm8OkdSPoDu8imViHPQq/8CQUu0WRR4rE+dMRf
# rpVqfmNi2qWCX94T4MsepijGVkwE//tJg0ryAiYdHT34LSnlG/RSBZmQRGWZ5g8j
# qnKjRParSqMft1gvjuUTVgtWNZfgcLFSK5Wa0myrq8OPcgTGGsRgun+tnSS+IxDT
# xVsAPH1OzvPjwomguByhUe/OcvUN0D5Wmp7xAgMBAAGjggGqMIIBpjAOBgNVHQ8B
# Af8EBAMCB4AwHwYDVR0lBBgwFgYKKwYBBAGCN0wIAQYIKwYBBQUHAwMwHQYDVR0O
# BBYEFNoH7a2YDjOSwpkp6DHcmUS7J+0yMFQGA1UdEQRNMEukSTBHMS0wKwYDVQQL
# EyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxFjAUBgNVBAUT
# DTIzMDAxMis1MDc1NjkwHwYDVR0jBBgwFoAUf1k/VCHarU/vBeXmo9ctBpQSCDEw
# YAYDVR0fBFkwVzBVoFOgUYZPaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9w
# cy9jcmwvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmclMjBQQ0ElMjAyMDI0LmNy
# bDBtBggrBgEFBQcBAQRhMF8wXQYIKwYBBQUHMAKGUWh0dHA6Ly93d3cubWljcm9z
# b2Z0LmNvbS9wa2lvcHMvY2VydHMvTWljcm9zb2Z0JTIwQ29kZSUyMFNpZ25pbmcl
# MjBQQ0ElMjAyMDI0LmNydDAMBgNVHRMBAf8EAjAAMA0GCSqGSIb3DQEBCwUAA4IC
# AQAUnEqhaRXe0T3hIJjvdQErEkrA/7bByjn6t5IArODkkRjzkYwtKMc2yYj2quaN
# rLutWw2YZcngKPy1b71YyDJQTy4NDRwaSh9Tw5thrk3NmcPrAHia5vtcBJ1CgtKK
# 7mQbIcQ22d/N3813ayCDDFewu1+jsZmX+r/aTEqaOM4TVxVtRSkuCy8nAXKuChOK
# Li/zA4XuH8iEYqIsj2YoNaeSxVmeGiERXpKdo3dDmYi0kO5w2D8VS4c3+9h6gElY
# BaAAg/dYErBg27qT3vv0zRDJhJufvCNylA8S7/+8H5E/PV5cng6na9VV/w9OV3qu
# uND6zdGa2EX38Glp50F9AIQk3p2xXmcvorDeM4XJ7UlWYBi6g80J1SSOQnInCYFE
# msfUNn3+1AaTJKSJL83quKArTac2pKhu0Yzzzrzo6HrsRiQKzpnRBb1/dMa6P3hz
# 75XbMRBctNsFhZC07WCmjExdLg2eHW5uV0TY8D5+6wozJf7vF3+WHkYPO85Z+BC6
# U4FkNbYNycZ9cE4j1tXRdyDCfml6c0HWPHjNVDObrv9lKt3qUqFpX38VCqVCyNOO
# 1UcXfQiVjJw32U2WUKZjt/neJKHEBsm9kFsLuWzkQ53+qcaSaytmsCnk2gOglrlD
# 5d3kKyvvAw+rzm0lT8K38P6PLxfZQHhu4W8dV7Av8N2ZmDCCBr0wggSloAMCAQIC
# EzMAAAA5O7Y3Gb8GHWcAAAAAADkwDQYJKoZIhvcNAQEMBQAwgYgxCzAJBgNVBAYT
# AlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYD
# VQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xMjAwBgNVBAMTKU1pY3Jvc29mdCBS
# b290IENlcnRpZmljYXRlIEF1dGhvcml0eSAyMDExMB4XDTI0MDgwODIwNTQxOFoX
# DTM2MDMyMjIyMTMwNFowVzELMAkGA1UEBhMCVVMxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEoMCYGA1UEAxMfTWljcm9zb2Z0IENvZGUgU2lnbmluZyBQ
# Q0EgMjAyNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBANgBnB7jOMeq
# lRYHNa265v4IY9fH8TKhemHfPINe1gpLaV3dhg324WwH06LcHbpnsBukCDNitryo
# 0dtS/EW6I/yEL/bLSY8hKpbfQuWusBPr9qazYcDxCW/qnjb5JsI1s8bNOg3bVATv
# QVL4tcf03aTycsz8QeCdM0l/yHRObJ9QqazM1r6VPEOJ7LL+uEEb73w6QCuhs89a
# 1uv1zerOYMnsneRRwCbpyW11IcggU0cRKDDq1pjVJzIbIF6+oiXXbReOsgeI8zu1
# FyQfK0fVkaya8SmVHQ/tOf23mZ4W9k0Ri22QW9p3UgSC5OUDktKxxcCmGL6tXLfO
# GSWHIIV4YrTJTT6PNty5REojHJuZHArkF9VnHTERWoTjAzfI3kP+5b4alUdhgAZ7
# ttOu1bVnXfHaqPYl2rPs20ji03LOVWsh/radgE17es5hL+t6lV0eVHrVhsssROWJ
# uz2MXMCt7iw7lFPG9LXKGjsmonn2gotGdHIuEg5JnJMJVmixd5LRlkmgYRZKzhxS
# CwyoGIq0PhaA7Y+VPct5pCHkijcIIDm0nlkK+0KyepolcqGm0T/GYQRMhHJlGOOm
# VQop36wUVUYklUy++vDWeEgEo4s7hxN6mIbf2MSIQ/iIfMZgJxC69oukMUXCrOC3
# SkE/xIkgpfl22MM1itkZ35nNXkMolU1lAgMBAAGjggFOMIIBSjAOBgNVHQ8BAf8E
# BAMCAYYwEAYJKwYBBAGCNxUBBAMCAQAwHQYDVR0OBBYEFH9ZP1Qh2q1P7wXl5qPX
# LQaUEggxMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIAQwBBMA8GA1UdEwEB/wQFMAMB
# Af8wHwYDVR0jBBgwFoAUci06AjGQQ7kUBU7h6qfHMdEjiTQwWgYDVR0fBFMwUTBP
# oE2gS4ZJaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9jcmwvcHJvZHVjdHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNybDBeBggrBgEFBQcBAQRSMFAw
# TgYIKwYBBQUHMAKGQmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMv
# TWljUm9vQ2VyQXV0MjAxMV8yMDExXzAzXzIyLmNydDANBgkqhkiG9w0BAQwFAAOC
# AgEAFJQfOChP7onn6fLIMKrSlN1WYKwDFgAddymOUO3FrM8d7B/W/iQ6DxXsDn7D
# 5W4wMwYeLystcEqfkjz4NURRgazyMu5yRzQh4LqjA4tStTcJh1opExo7nn5PuPBY
# nbu0+THSuVHTe0VTTPVhily/piFrDo3axQ9P4C+Ol5yet+2gTfekICS5xS+cYfSI
# vgn0JksVBVMYVI5QFu/qhnLhsEFEUzG8fvv0hjgkO+lkpV9ty6GkN4vdnd7ya6Q6
# aR9y34aiM1qmxaxBi6OUnyNl6fkuun/diTFnYDLTppOkr/mg5WSfCiDVMNCxtj4w
# PKC5OmHm1DQIt/MNokbbH3UGsFP1QbzsLocuSqLCvH09Io3fDPTmscR9Y75G4qX7
# RTX8AdBPo0I6OEojf39zuFZt0qOHm65YWQE69cZM2ueE1MB05dNNgHK9gTE7zKvK
# /fg8B2qjW88MT/WF5V5uvZGtqa9FSL2RazArA+rDPuf6JGYz4HpgMZHB4S6szWSK
# YBv0VisCzfxgeU+dquXW9bd0auYlOB58DPcOYKdc3Se94g+xL4pcEhbB54JOgAkw
# YTu/9dLeH2pDqeJZAABVDWRQCaXfO5LgyKwKCLYXpigrZYCjUSBcr+Ve8PFWMhVT
# Ql0v4q8J/AUmQN5W4n101cY2L4A7GTQG1h32HHAvfQESWP0xghn+MIIZ+gIBATBu
# MFcxCzAJBgNVBAYTAlVTMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# KDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25pbmcgUENBIDIwMjQCEzMAAAIc
# +s3Fm+gvfsQAAAAAAhwwDQYJYIZIAWUDBAIBBQCgga4wGQYJKoZIhvcNAQkDMQwG
# CisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEOMAwGCisGAQQBgjcCARUwLwYJKoZI
# hvcNAQkEMSIEIMKmrEJYMcHafNe23YuJDuOyzWPqSQML9/7NiP2FvADdMEIGCisG
# AQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8AcwBvAGYAdKEagBhodHRwOi8vd3d3
# Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEBBQAEggEAnA4wVZ/d5Y5w07dnbwIA
# mwAJvOVd6BuuqkdKuHJmTgnedNZdqfIplIsV95mCV85Qxv/xAaHE9SBgOqAqpJwi
# MTuiJUfsUWlYr7DHxNgtYnkna5rNlFmzR1EDgcDy5TIIeUQ9f6HjpUnoUvsPN1s7
# rZ4l7hrCTTZxaI5w2xQiTBVrhve57HMXIIIn9uthNrDyZeqjcvl59i6XCdNI3evP
# 3PczlA82M8L6YzIuOcwK5Ka85phMjwU5YpSsr3MkMk4xTdD+PZb4udN+e7m76Jku
# A2DHbhKcg8pTa/oB/9asXLi/vlJ6TBS0K9h1lKy9HXJYUSmJqpF5RLMUmnMDfN0U
# 9KGCF7AwghesBgorBgEEAYI3AwMBMYIXnDCCF5gGCSqGSIb3DQEHAqCCF4kwgheF
# AgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFaBgsqhkiG9w0BCRABBKCCAUkEggFFMIIB
# QQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFlAwQCAQUABCDuHGVeHuXHr8ATr3C3
# wuDx1QVMUItRfOX5myw3EtHIhAIGajqmCFhEGBMyMDI2MDYyNTAzMjYzMy42Njla
# MASAAgH0oIHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0
# ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjozMjFBLTA1RTAtRDk0NzElMCMG
# A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaCCEf4wggcoMIIFEKAD
# AgECAhMzAAACGqmgHQagD0OqAAEAAAIaMA0GCSqGSIb3DQEBCwUAMHwxCzAJBgNV
# BAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4w
# HAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29m
# dCBUaW1lLVN0YW1wIFBDQSAyMDEwMB4XDTI1MDgxNDE4NDgyOFoXDTI2MTExMzE4
# NDgyOFowgdMxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYD
# VQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24xLTAr
# BgNVBAsTJE1pY3Jvc29mdCBJcmVsYW5kIE9wZXJhdGlvbnMgTGltaXRlZDEnMCUG
# A1UECxMeblNoaWVsZCBUU1MgRVNOOjMyMUEtMDVFMC1EOTQ3MSUwIwYDVQQDExxN
# aWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNlMIICIjANBgkqhkiG9w0BAQEFAAOC
# Ag8AMIICCgKCAgEAmYEAwSTz79q2V3ZWzQ5Ev7RKgadQtMBy7+V3XQ8R0NL8R9mu
# pxcqJQ/KPeZGJTER+9Qq/t7HOQfBbDy6e0TepvBFV/RY3w+LOPMKn0Uoh2/8IvdS
# bJ8qAWRVoz2S9VrJzZpB8/f5rQcRETgX/t8N66D2JlEXv4fZQB7XzcJMXr1puhuX
# bOt9RYEyN1Q3Z7YjRkhfBsRc+SD/C9F4iwZqfQgo82GG4wguIhjJU7+XMfrv4vxA
# FNVg3mn1PoMWGZWio+e14+PGYPVLKlad+0IhdHK5AgPyXKkqAhEZpYhYYVEItHOO
# vqrwukxVAJXMvWA3GatWkRZn33WDJVtghCW6XPLi1cDKiGE5UcXZSV4OjQIUB8vp
# 2LUMRXud5I49FIBcE9nT00z8A+EekrPM+OAk07aDfwZbdmZ56j7ub5fNDLf8yIb8
# QxZ8Mr4RwWy/czBuV5rkWQQ+msjJ5AKtYZxJdnaZehUgUNArU/u36SH1eXKMQGRX
# r/xeKFGI8vvv5Jl1knZ8UqEQr9PxDbis7OXp2WSMK5lLGdYVH8VownYF3sbOiRkx
# 5Q5GaEyTehOQp2SfdbsJZlg0SXmHphGnoW1/gQ/5P6BgSq4PAWIZaDJj6AvLLCdb
# URgR5apNQQed2zYUgUbjACA/TomA8Ll7Arrv2oZGiUO5Vdi4xxtA3BRTQTUCAwEA
# AaOCAUkwggFFMB0GA1UdDgQWBBTwqyIJ3QMoPasDcGdGovbaY8IlNjAfBgNVHSME
# GDAWgBSfpxVdAF5iXYP05dJlpxtTNRnpcjBfBgNVHR8EWDBWMFSgUqBQhk5odHRw
# Oi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NybC9NaWNyb3NvZnQlMjBUaW1l
# LVN0YW1wJTIwUENBJTIwMjAxMCgxKS5jcmwwbAYIKwYBBQUHAQEEYDBeMFwGCCsG
# AQUFBzAChlBodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20vcGtpb3BzL2NlcnRzL01p
# Y3Jvc29mdCUyMFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNydDAMBgNVHRMB
# Af8EAjAAMBYGA1UdJQEB/wQMMAoGCCsGAQUFBwMIMA4GA1UdDwEB/wQEAwIHgDAN
# BgkqhkiG9w0BAQsFAAOCAgEA1a72WFq7B6bJT3VOJ21nnToPJ9O/q51bw1bhPfQy
# 67uy+f8x8akipzNL2k5b6mtxuPbZGpBqpBKguDwQmxVpX8cGmafeo3wGr4a8Yk6S
# y09tEh/Nwwlsyq7BRrJNn6bGOB8iG4OTy+pmMUh7FejNPRgvgeo/OPytm4NNrMMg
# 98UVlrZxGNOYsifpRJFg5jE/Yu6lqFa1lTm9cHuPYxWa2oEwC0sEAsTFb69iKpN0
# sO19xBZCr0h5ClU9Pgo6ekiJb7QJoDzrDoPQHwbNA87Cto7TLuphj0m9l/I70gLj
# Eq53SHjuURzwpmNxdm18Qg+rlkaMC6Y2KukOfJ7oCSu9vcNGQM+inl9gsNgirZ6y
# Jk9VsXEsoTtoR7fMNU6Py6ufJQGMTmq6ZCq2eIGOXWMBb79ZF6tiKTa4qami3US0
# mTY41J129XmAglVy+ujSZkHu2lHJDRHs7FjnIXZVUE5pl6yUIl23jG50fRTLQcSt
# dwY/LvJUgEHCIzjvlLTqLt6JVR5bcs5aN4Dh0YPG95B9iDMZrq4rli5SnGNWev5L
# LsDY1fbrK6uVpD+psvSLsNpht27QcHRsYdAMALXM+HNsz2LZ8xiOfwt6rOsVWXoi
# HV86/TeMy5TZFUl7qB59INoMSJgDRladVXeT9fwOuirFIoqgjKGk3vO2bELrYMN0
# QVwwggdxMIIFWaADAgECAhMzAAAAFcXna54Cm0mZAAAAAAAVMA0GCSqGSIb3DQEB
# CwUAMIGIMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTIwMAYD
# VQQDEylNaWNyb3NvZnQgUm9vdCBDZXJ0aWZpY2F0ZSBBdXRob3JpdHkgMjAxMDAe
# Fw0yMTA5MzAxODIyMjVaFw0zMDA5MzAxODMyMjVaMHwxCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0
# YW1wIFBDQSAyMDEwMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5OGm
# TOe0ciELeaLL1yR5vQ7VgtP97pwHB9KpbE51yMo1V/YBf2xK4OK9uT4XYDP/XE/H
# ZveVU3Fa4n5KWv64NmeFRiMMtY0Tz3cywBAY6GB9alKDRLemjkZrBxTzxXb1hlDc
# wUTIcVxRMTegCjhuje3XD9gmU3w5YQJ6xKr9cmmvHaus9ja+NSZk2pg7uhp7M62A
# W36MEBydUv626GIl3GoPz130/o5Tz9bshVZN7928jaTjkY+yOSxRnOlwaQ3KNi1w
# jjHINSi947SHJMPgyY9+tVSP3PoFVZhtaDuaRr3tpK56KTesy+uDRedGbsoy1cCG
# MFxPLOJiss254o2I5JasAUq7vnGpF1tnYN74kpEeHT39IM9zfUGaRnXNxF803RKJ
# 1v2lIH1+/NmeRd+2ci/bfV+AutuqfjbsNkz2K26oElHovwUDo9Fzpk03dJQcNIIP
# 8BDyt0cY7afomXw/TNuvXsLz1dhzPUNOwTM5TI4CvEJoLhDqhFFG4tG9ahhaYQFz
# ymeiXtcodgLiMxhy16cg8ML6EgrXY28MyTZki1ugpoMhXV8wdJGUlNi5UPkLiWHz
# NgY1GIRH29wb0f2y1BzFa/ZcUlFdEtsluq9QBXpsxREdcu+N+VLEhReTwDwV2xo3
# xwgVGD94q0W29R6HXtqPnhZyacaue7e3PmriLq0CAwEAAaOCAd0wggHZMBIGCSsG
# AQQBgjcVAQQFAgMBAAEwIwYJKwYBBAGCNxUCBBYEFCqnUv5kxJq+gpE8RjUpzxD/
# LwTuMB0GA1UdDgQWBBSfpxVdAF5iXYP05dJlpxtTNRnpcjBcBgNVHSAEVTBTMFEG
# DCsGAQQBgjdMg30BATBBMD8GCCsGAQUFBwIBFjNodHRwOi8vd3d3Lm1pY3Jvc29m
# dC5jb20vcGtpb3BzL0RvY3MvUmVwb3NpdG9yeS5odG0wEwYDVR0lBAwwCgYIKwYB
# BQUHAwgwGQYJKwYBBAGCNxQCBAweCgBTAHUAYgBDAEEwCwYDVR0PBAQDAgGGMA8G
# A1UdEwEB/wQFMAMBAf8wHwYDVR0jBBgwFoAU1fZWy4/oolxiaNE9lJBb186aGMQw
# VgYDVR0fBE8wTTBLoEmgR4ZFaHR0cDovL2NybC5taWNyb3NvZnQuY29tL3BraS9j
# cmwvcHJvZHVjdHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYtMjMuY3JsMFoGCCsGAQUF
# BwEBBE4wTDBKBggrBgEFBQcwAoY+aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3Br
# aS9jZXJ0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcnQwDQYJKoZIhvcNAQEL
# BQADggIBAJ1VffwqreEsH2cBMSRb4Z5yS/ypb+pcFLY+TkdkeLEGk5c9MTO1OdfC
# cTY/2mRsfNB1OW27DzHkwo/7bNGhlBgi7ulmZzpTTd2YurYeeNg2LpypglYAA7AF
# vonoaeC6Ce5732pvvinLbtg/SHUB2RjebYIM9W0jVOR4U3UkV7ndn/OOPcbzaN9l
# 9qRWqveVtihVJ9AkvUCgvxm2EhIRXT0n4ECWOKz3+SmJw7wXsFSFQrP8DJ6LGYnn
# 8AtqgcKBGUIZUnWKNsIdw2FzLixre24/LAl4FOmRsqlb30mjdAy87JGA0j3mSj5m
# O0+7hvoyGtmW9I/2kQH2zsZ0/fZMcm8Qq3UwxTSwethQ/gpY3UA8x1RtnWN0SCyx
# TkctwRQEcb9k+SS+c23Kjgm9swFXSVRk2XPXfx5bRAGOWhmRaw2fpCjcZxkoJLo4
# S5pu+yFUa2pFEUep8beuyOiJXk+d0tBMdrVXVAmxaQFEfnyhYWxz/gq77EFmPWn9
# y8FBSX5+k77L+DvktxW/tM4+pTFRhLy/AsGConsXHRWJjXD+57XQKBqJC4822rpM
# +Zv/Cuk0+CQ1ZyvgDbjmjJnW4SLq8CdCPSWU5nR0W2rRnj7tfqAxM328y+l7vzhw
# RNGQ8cirOoo6CGJ/2XBjU02N7oJtpQUQwXEGahC0HVUzWLOhcGbyoYIDWTCCAkEC
# AQEwggEBoYHZpIHWMIHTMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3Rv
# bjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0
# aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJlbGFuZCBPcGVyYXRpb25zIExpbWl0
# ZWQxJzAlBgNVBAsTHm5TaGllbGQgVFNTIEVTTjozMjFBLTA1RTAtRDk0NzElMCMG
# A1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZaIjCgEBMAcGBSsOAwIa
# AxUA8YrutmKpSrubCaAYsU4pt1Ft8DaggYMwgYCkfjB8MQswCQYDVQQGEwJVUzET
# MBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMV
# TWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNyb3NvZnQgVGltZS1T
# dGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQsFAAIFAO3mdegwIhgPMjAyNjA2MjQx
# NTI3MzZaGA8yMDI2MDYyNTE1MjczNlowdzA9BgorBgEEAYRZCgQBMS8wLTAKAgUA
# 7eZ16AIBADAKAgEAAgIfCwIB/zAHAgEAAgIS2DAKAgUA7efHaAIBADA2BgorBgEE
# AYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIBAAIDB6EgoQowCAIBAAIDAYag
# MA0GCSqGSIb3DQEBCwUAA4IBAQClCgqU2Z5HeG2YCqYEbQOuaKq+7kbunC9jhaLP
# uneW9iMBT8737XbPF2sHWyWRXbTbqEO9DQpwKHgwXI3DyUfEKO/TnbgysypKnuFj
# 8uNvbOH/qjt/dIqpu5GLobRDC0GbP4mrXQOqwsMgym7h/c5qNZJO6Na8eZE9FSTE
# DVRK7TTs1Tkq8evBnXVYQs1aAGnrMgXE/mE5NTFxI5F5oOm9ZRTjz4ZYCJQzHBaF
# 7KcT+vo9l1wBV6ZFO4YAzVTD6jEcnt96oaIfQ5DfeKVew/JKiqFfsJIkVP6yu/mP
# Wz1rp49rWfuh5O3tGKO65PrqlbNSRuKNTPiAbMw5vsFL5V7bMYIEDTCCBAkCAQEw
# gZMwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcT
# B1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UE
# AxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTACEzMAAAIaqaAdBqAPQ6oA
# AQAAAhowDQYJYIZIAWUDBAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0B
# CRABBDAvBgkqhkiG9w0BCQQxIgQgy9hZpqPRKWN/AB7syztvz1uN1gzhX7hdO1aL
# Zcp4vYYwgfoGCyqGSIb3DQEJEAIvMYHqMIHnMIHkMIG9BCCdeiHHrbtpKcwB20do
# VU89WHIOH8S7w37uaHcDmemK+zCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwAhMzAAACGqmgHQagD0OqAAEAAAIaMCIEIN9Jwwnp9eY3LCbzLXB3
# BRUe663gd3iy0a/oaGMRJYsSMA0GCSqGSIb3DQEBCwUABIICAAU6OIVx1tipDXiM
# dNnoYHjEARCua/nbXA+t80pAfXpuamAmRkltYfnfaRf24Yn36vzjXwoy1Vo8+NHU
# soLLDfA6moJZb+AWJSoLn92Tx67F9UGCvJJZvqdwEBjIbJ7FPpwIrYZzfnNGO9Q0
# XM6gHyRGfH9rcf2G0Yy9TXtIMEXFTsHsvuCFvr//zlR4Ehh4o5GLKGoaDXaQY3nm
# 0iW1PmIRrgYRiHIhedHglp4fBNwrR6l9bb8qGLnSC6gn44DVEnxkNTYt30Xz6X0w
# HvOz0JSVJ9HJBozHKVu5kam8JP9yjlyZkV0eyQG5x01pWvuK5DP3LAL2cFgKoiB/
# kq0VauTdxQykZtbag5kgCYrZt+2SjWU13WtcaWc8qvQZKj1XUbTZIqO5za5PETgo
# JQ5ASLynwPXku3yf1tRgxZsAq9n90cY2BeZ8dgX4gWrC8yi5e8ZJiwPWsAuLUkyv
# tENddIjmwkbJF0O4cUHt32rqtwd+Vbzso5Twwe55CgFYfI2e4VALLJD56HLFPHpn
# XbocyOphSeRpbDOE6Y0OuxqSrsafW1Vs1H+69G6vqVt59V9Yf6Eqh/+2s9zWEOaz
# zFGRzxYHRtx+AK+ODLOuWVkIWHRgzVi9pYMn3WDXLY4o2qx3nvkrjJnZqQfE3qPC
# SUlaRchmb9SlP8GlWtvicV7kY/fL
# SIG # End signature block