DefenderForIdentity.psm1

<#
Copyright (c) Microsoft Corporation.
MIT License
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#>


#requires -Version 4.0
#requires -Modules ActiveDirectory, GroupPolicy

#region General settings

$script:settings = @{

    gpoNamePrefix                  = 'Microsoft Defender for Identity'

    gpoExtensions                  = @{
        'Core GPO Engine'                                = '00000000-0000-0000-0000-000000000000'
        'Tool Extension GUID (Computer Policy Settings)' = '0F6B957D-509E-11D1-A7CC-0000F87571E3'
        'Security'                                       = '827D319E-6EAC-11D2-A4EA-00C04F79F83A'
        'Computer Restricted Groups'                     = '803E14A0-B4FB-11D0-A0D0-00A0C90F574B'
        'Preference Tool CSE GUID Registry'              = 'BEE07A6A-EC9F-4659-B8C9-0B1937907C83'
        'Preference CSE GUID Registry'                   = 'B087BE9D-ED37-454F-AF9C-04291E351182'
        'Audit Configuration Extension'                  = '0F3F3735-573D-9804-99E4-AB2A69BA5FD4'
        'Audit Policy Configuration'                     = 'F3CCC681-B74C-4060-9F26-CD84525DCA2A'
    }

    ProcessorPerformance           = @{
        GpoName    = '{0} - Processor Performance'
        SchemeGuid = '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c'
        Key        = 'HKEY_LOCAL_MACHINE\Software\Policies\Microsoft\Power\PowerSettings'
        ValueName  = 'ActivePowerScheme'
    }

    NTLMAuditing                   = @{
        GpoName     = '{0} - NTLM Auditing for DCs'
        RegistrySet = @{
            'System\CurrentControlSet\Control\Lsa\MSV1_0\AuditReceivingNTLMTraffic'   = '2'
            'System\CurrentControlSet\Control\Lsa\MSV1_0\RestrictSendingNTLMTraffic'  = '1|2'
            'System\CurrentControlSet\Services\Netlogon\Parameters\AuditNTLMInDomain' = '7'
        }
    }

    CAAuditing                     = @{
        GpoName       = '{0} - Auditing for CAs'
        GpoVal        = @{ 'AuditFilter' = 127 }
        GpoReg        = 'System\CurrentControlSet\Services\CertSvc\Configuration\%DomainName%-%ComputerName%-CA'
        RegPathActive = 'System\CurrentControlSet\Services\CertSvc\Configuration\Active'
        RegistrySet   = @{
            'System\CurrentControlSet\Services\CertSvc\Configuration\{0}\AuditFilter' = 127
        }
        GPPermissions = [ordered]@{
            'Cert Publishers'     = 'GpoApply'
            'Domain Controllers'  = 'GpoRead'
            'Authenticated Users' = 'GpoRead'
        }
    }

    AdvancedAuditPolicyDCs         = @{
        GpoName        = '{0} - Advanced Audit Policy for DCs'
        PolicySettings = @'
Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting,Setting Value
,System,Security System Extension,{0CCE9211-69AE-11D9-BED3-505054503030},Success and Failure,,3
,System,Distribution Group Management,{0CCE9238-69AE-11D9-BED3-505054503030},Success and Failure,,3
,System,Security Group Management,{0CCE9237-69AE-11D9-BED3-505054503030},Success and Failure,,3
,System,Computer Account Management,{0CCE9236-69AE-11D9-BED3-505054503030},Success and Failure,,3
,System,User Account Management,{0CCE9235-69AE-11D9-BED3-505054503030},Success and Failure,,3
,System,Directory Service Access,{0CCE923B-69AE-11D9-BED3-505054503030},Success and Failure,,3
,System,Directory Service Changes,{0CCE923C-69AE-11D9-BED3-505054503030},Success and Failure,,3
,System,Credential Validation,{0CCE923F-69AE-11D9-BED3-505054503030},Success and Failure,,3
'@

    }

    AdvancedAuditPolicyCAs         = @{
        GpoName        = '{0} - Advanced Audit Policy for CAs'
        PolicySettings = @'
Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting,Setting Value
,System,Audit Certification Services,{0cce9221-69ae-11d9-bed3-505054503030},Success and Failure,,3
'@

        GPPermissions  = [ordered]@{
            'Cert Publishers'     = 'GpoApply'
            'Domain Controllers'  = 'GpoRead'
            'Authenticated Users' = 'GpoRead'
        }
    }

    ObjectAuditing                 = @{
        Path     = 'AD:\{0}'
        Auditing = @'
SecurityIdentifier,AccessMask,AuditFlagsValue,InheritedObjectAceType,Description,InheritanceType,PropagationFlags
S-1-1-0,852331,1,bf967aba-0de6-11d0-a285-00aa003049e2,Descendant User Objects,2,2
S-1-1-0,852331,1,bf967a9c-0de6-11d0-a285-00aa003049e2,Descendant Group Objects,2,2
S-1-1-0,852331,1,bf967a86-0de6-11d0-a285-00aa003049e2,Descendant Computer Objects,2,2
S-1-1-0,852331,1,ce206244-5827-4a86-ba1c-1c0c386c1b64,Descendant msDS-ManagedServiceAccount Objects,2,2
S-1-1-0,852075,1,7b8b558a-93a5-4af7-adca-c017e67f1057,Descendant msDS-GroupManagedServiceAccount Objects,2,2
'@
 | ConvertFrom-Csv
    }

    ConfigurationContainerAuditing = @{
        Validate = 'LDAP://CN=Microsoft Exchange,CN=Services,CN=Configuration,{0}'
        Path     = 'AD:\CN=Configuration,{0}'
        Auditing = @'
SecurityIdentifier,AccessMask,AuditFlagsValue,AceFlagsValue,InheritedObjectAceType,InheritanceType,PropagationFlags
S-1-1-0,32,3,194,00000000-0000-0000-0000-000000000000,1,0
'@
 | ConvertFrom-Csv
    }

    AdfsAuditing                   = @{
        Validate = 'LDAP://CN=ADFS,CN=Microsoft,CN=Program Data,{0}'
        Path     = 'AD:\CN=ADFS,CN=Microsoft,CN=Program Data,{0}'
        Auditing = @'
SecurityIdentifier,AccessMask,AuditFlagsValue,AceFlagsValue,InheritedObjectAceType,InheritanceType,PropagationFlags
S-1-1-0,48,3,194,00000000-0000-0000-0000-000000000000,1,0
'@
 | ConvertFrom-Csv
    }

    SensitiveGroups                = @{
        'Administrators'              = 'S-1-5-32-544'
        'Account Operators'           = 'S-1-5-32-548'
        'Backup Operators'            = 'S-1-5-32-551'
        'Domain Admins'               = '{0}-512'
        'Domain Controllers'          = '{0}-516'
        'Enterprise Admins'           = '{0}-519'
        'Group Policy Creator Owners' = '{0}-520'
        'Print Operators'             = 'S-1-5-32-550'
        'Replicators'                 = 'S-1-5-32-552'
        'Schema Admins'               = '{0}-518'
        'Server Operators'            = 'S-1-5-32-549'
        'Cert Publishers'             = '{0}-517'
    }

}

if (Test-Path -Path (Join-Path -Path $PSScriptRoot -ChildPath ($PSUICulture))) {
    Import-LocalizedData -BindingVariable strings
} else {
    Import-LocalizedData -BindingVariable strings -UICulture en-US
}

#endregion

#region General helper functions

function Get-MDIValidationMessage {
    param($Result)
    if ($Result) {
        $strings['Validation_Passed']
    } else {
        $strings['Validation_Failed']
    }
}

function Resolve-MDIPath {
    param(
        [parameter(Mandatory)] $Path
    )
    $return = Resolve-Path -Path $Path -ErrorAction SilentlyContinue -ErrorVariable resolveError
    if ($return.Path) { $return.Path }
    else { $resolveError[0].TargetObject }
}

function Format-Json {
    param(
        [Parameter(Mandatory, ValueFromPipeline)] [String] $json
    )
    $indent = 0;
    ($json -Split '\n' | ForEach-Object {
        if ($_ -match '[\}\]]') {
            $indent--
        }
        $line = (' ' * $indent * 2) + $_.TrimStart().Replace(': ', ': ')
        if ($_ -match '[\{\[]') {
            $indent++
        }
        $line
    }) -join "`n"
}

function Test-MDICAServer {
    [CmdletBinding()]
    param()
    [bool](Get-Service CertSvc -ErrorAction SilentlyContinue)
}

#endregion

#region Sensor service helper functions

function Get-MDISensorBinPath {
    [CmdletBinding()]
    param()
    $wmiParams = @{
        Namespace   = 'root\cimv2'
        ClassName   = 'Win32_Service'
        Property    = 'PathName'
        Filter      = 'Name="AATPSensor"'
        ErrorAction = 'Stop'
    }
    Write-Verbose -Message $strings['Sensor_LocateConfigurationFile']
    try {
        $return = (Get-CimInstance @wmiParams | Select-Object -ExpandProperty PathName) -replace '"|Microsoft\.Tri\.Sensor\.exe', ''
    } catch {
        $return = $null
    }
    if ([string]::IsNullOrEmpty($return)) {
        Write-Warning $strings['Sensor_ServiceNotFound']
    }
    $return
}

function Stop-MDISensor {
    [CmdletBinding()]
    param()
    Stop-Service -Name AATPSensorUpdater -Force
}

function Start-MDISensor {
    [CmdletBinding()]
    param()
    Start-Service -Name AATPSensorUpdater
}

#endregion

#region Sensor configuration helper functions

function Get-MDISensorConfiguration {
    [CmdletBinding()]
    param()
    $sensorBinPath = Get-MDISensorBinPath
    if ($null -eq $sensorBinPath) {
        $sensorConfiguration = $null
    } else {
        Write-Verbose -Message $strings['Sensor_ReadConfigurationFile']
        $sensorConfigurationPath = Join-Path -Path $sensorBinPath -ChildPath 'SensorConfiguration.json'
        $sensorConfiguration = Get-Content -Path $sensorConfigurationPath -Raw | ConvertFrom-Json
    }

    if ($null -ne $sensorConfiguration.SensorProxyConfiguration) {
        $SensorProxyConfiguration = [PSCustomObject]@{
            IsProxyEnabled               = -not [string]::IsNullOrEmpty($sensorConfiguration.SensorProxyConfiguration.Url)
            IsAuthenticationProxyEnabled = -not [string]::IsNullOrEmpty($sensorConfiguration.SensorProxyConfiguration.UserName)
            Url                          = $sensorConfiguration.SensorProxyConfiguration.Url
            UserName                     = $sensorConfiguration.SensorProxyConfiguration.UserName
            EncryptedUserPasswordData    = $sensorConfiguration.SensorProxyConfiguration.EncryptedUserPasswordData.EncryptedBytes
            CertificateThumbprint        = $sensorConfiguration.SensorProxyConfiguration.EncryptedUserPasswordData.CertificateThumbprint

        }
        $sensorConfiguration.SensorProxyConfiguration = $SensorProxyConfiguration
    }
    $sensorConfiguration
}

function Get-MDIEncryptedPassword {
    param(
        [Parameter(Mandatory = $true)] [string] $CertificateThumbprint,
        [Parameter(Mandatory = $true)] [PSCredential] $Credential
    )
    $store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList @(
        [System.Security.Cryptography.X509Certificates.StoreName]::My,
        [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine
    )
    $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)

    $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2] $store.Certificates.Find(
        [System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $CertificateThumbprint, $false)[0]

    $rsaPublicKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)
    $bytes = [System.Text.Encoding]::Unicode.GetBytes(
        $Credential.GetNetworkCredential().Password
    )
    $encrypted = $rsaPublicKey.Encrypt($bytes, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA256)
    $encryptedPassword = [System.Convert]::ToBase64String($encrypted)

    $store.Close()
    $encryptedPassword
}

function Get-MDIDecryptedPassword {
    param(
        [Parameter(Mandatory = $true)] [string] $CertificateThumbprint,
        [Parameter(Mandatory = $true)] [string] $EncryptedString
    )
    $store = New-Object -TypeName System.Security.Cryptography.X509Certificates.X509Store -ArgumentList @(
        [System.Security.Cryptography.X509Certificates.StoreName]::My,
        [System.Security.Cryptography.X509Certificates.StoreLocation]::LocalMachine
    )
    $store.Open([System.Security.Cryptography.X509Certificates.OpenFlags]::ReadOnly)

    $cert = [System.Security.Cryptography.X509Certificates.X509Certificate2] $store.Certificates.Find(
        [System.Security.Cryptography.X509Certificates.X509FindType]::FindByThumbprint, $CertificateThumbprint, $false)[0]

    $rsaPublicKey = [System.Security.Cryptography.X509Certificates.RSACertificateExtensions]::GetRSAPrivateKey($cert)

    $encrypted = [System.Convert]::FromBase64String($EncryptedString)
    $bytes = $rsaPublicKey.Decrypt($encrypted, [System.Security.Cryptography.RSAEncryptionPadding]::OaepSHA256)
    $decryptedPassword = [System.Text.Encoding]::Unicode.GetString($bytes)

    $store.Close()
    $decryptedPassword
}

function Get-MDISensorProxyConfiguration {
    [CmdletBinding()]
    param()
    $sensorConfiguration = Get-MDISensorConfiguration
    if ($null -eq $sensorConfiguration) {
        $proxyConfiguration = $null
    } else {
        $proxyConfiguration = $sensorConfiguration.SensorProxyConfiguration
    }
    $proxyConfiguration
}

function Set-MDISensorProxyConfiguration {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param (
        [Parameter(Mandatory = $false)] [string] $ProxyUrl,
        [Parameter(Mandatory = $false)] [PSCredential] $ProxyCredential
    )
    $operation = if ([string]::IsNullOrEmpty($ProxyUrl)) { 'Clear' } else { 'Set' }
    if ($PSCmdlet.ShouldProcess($strings['Sensor_ProxyConfigurationAction'], $operation)) {
        $sensorConfiguration = Get-MDISensorConfiguration
        if ($null -eq $sensorConfiguration) {
            Write-Error $strings['Sensor_ErrorReadingSensorConfiguration'] -ErrorAction Stop
        }
        if ([string]::IsNullOrEmpty($ProxyUrl)) {
            $sensorConfiguration.SensorProxyConfiguration = $null
        } else {
            if ($ProxyCredential) {
                $thumbprint = $sensorConfiguration.SecretManagerConfigurationCertificateThumbprint
                $sensorConfiguration.SensorProxyConfiguration = [PSCustomObject]@{
                    '$type'                   = 'SensorProxyConfiguration'
                    Url                       = $ProxyUrl
                    UserName                  = $ProxyCredential.UserName
                    EncryptedUserPasswordData = [PSCustomObject]@{
                        '$type'               = 'EncryptedData'
                        EncryptedBytes        = Get-MDIEncryptedPassword -CertificateThumbprint $thumbprint -Credential $ProxyCredential
                        SecretVersion         = $null
                        CertificateThumbprint = $sensorConfiguration.SecretManagerConfigurationCertificateThumbprint
                    }
                }
            } else {
                $sensorConfiguration.SensorProxyConfiguration = [PSCustomObject]@{
                    '$type' = 'SensorProxyConfiguration'
                    Url     = $ProxyUrl
                }
            }
        }
        Stop-MDISensor
        Write-Verbose -Message $strings['Sensor_WriteSensorConfigurationFile']
        $sensorConfiguration | ConvertTo-Json | Format-Json |
            Set-Content -Path (Join-Path -Path (Get-MDISensorBinPath) -ChildPath 'SensorConfiguration.json')
        Start-MDISensor
    }
}

function Clear-MDISensorProxyConfiguration {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param()
    if ($PSCmdlet.ShouldProcess($strings['Sensor_ProxyConfigurationAction'], 'Clear')) {
        Set-MDISensorProxyConfiguration -ProxyUrl $null
    }
}

#endregion

#region GPO helper functions

function Get-MDIGPOName {
    param(
        [Parameter(Mandatory)] [string] $Name,
        [Parameter(Mandatory = $false)] [string] $GpoNamePrefix
    )
    if ([string]::IsNullOrEmpty($GpoNamePrefix)) {
        $Name -f $script:settings['gpoNamePrefix']
    } else {
        $Name -f $GpoNamePrefix
    }
}

function New-MDIGPO {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Name,
        [Parameter(Mandatory = $false)] [switch] $CreateGpoDisabled
    )
    Write-Verbose -Message ($strings['GPO_Create'] -f $Name)
    $gpo = New-GPO -Name $Name
    if ($gpo) {
        Start-Sleep -Milliseconds 500
        $gPCFileSysPath = (Get-ADObject -Identity $gpo.Path -Properties gPCFileSysPath).gPCFileSysPath
        $maxWaitTime = (Get-Date).AddSeconds(3)
        do {
            Start-Sleep -Milliseconds 500
        } while (-not ((Test-Path -Path $gPCFileSysPath) -or ($maxWaitTime -lt (Get-Date))))
        if ($CreateGpoDisabled) {
            $gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::AllSettingsDisabled
        } else {
            $gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled
        }
        $gpo | Add-Member -MemberType NoteProperty -Name gPCFileSysPath -Value $gPCFileSysPath -PassThru -Force
    }
}

function Get-MDIGPO {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Name
    )
    $gpo = Get-GPO -Name $gpoName -ErrorAction SilentlyContinue
    if ($null -eq $gpo) {
        Write-Verbose -Message ("'{0}' - {1}" -f $gpoName, $strings['GPO_NotFound'])
    }
    $gpo
}

function Get-MDIGPOLink {
    param(
        [guid] $Guid
    )
    Write-Verbose -Message $strings['GPO_GetLinks']
    $xml = [xml](Get-GPOReport -Guid $Guid -ReportType Xml)
    @($xml.GPO.LinksTo)
}

function Test-MDIGPOLink {
    [CmdletBinding()]
    param(
        [guid] $Guid
    )
    $return = $false
    $enabledLinks = @(Get-MDIGPOLink -Guid $Guid | Where-Object { $_.Enabled -eq 'true' })
    if ($enabledLinks.Count -lt 1) {
        Write-Verbose -Message ($strings['GPO_NotLinkedOrEnabled'])
    } else {
        $return = $true
        $enabledLinks | ForEach-Object {
            Write-Verbose -Message ($strings['GPO_LinkedAndEnabled'] -f $_.SOMPath)
        }
    }
    $return
}

function Set-MDIGPOLink {
    param(
        [Parameter(Mandatory)] [guid] $Guid,
        [Parameter(Mandatory)] [string] $Target,
        [Microsoft.GroupPolicy.EnableLink] $LinkEnabled = [Microsoft.GroupPolicy.EnableLink]::Yes,
        [Microsoft.GroupPolicy.EnforceLink] $Enforced = [Microsoft.GroupPolicy.EnforceLink]::Yes
    )
    Write-Verbose -Message $strings['GPO_SetLink']
    $gpLink = @{
        Guid        = $Guid
        LinkEnabled = $LinkEnabled
        Enforced    = $Enforced
        Target      = $Target
    }
    $link = New-GPLink @gpLink -ErrorAction SilentlyContinue
    if ($null -eq $link) {
        $link = Set-GPLink @gpLink -ErrorAction SilentlyContinue
    }
    if ($null -eq $link) {
        throw $strings['GPO_UnableToUpdateLink']
    }
}

function Get-MDIGPOMachineVersion {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [guid] $Guid
    )
    (Get-GPO -Guid $Guid).Computer | Select-Object -Property *Version
}

function Set-MDIGPOMachineVersion {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [guid] $Guid,
        [Parameter(Mandatory)] [int] $Version,
        [Parameter(Mandatory = $false)] [ValidateSet('Sysvol', 'DS', 'All')] [string] $Mode = 'Sysvol'
    )
    Write-Verbose -Message $strings['GPO_UpdateVersion']
    if ($Mode -match 'ALL|DS') {
        $gpoAdObjectPath = 'CN={0},{1}' -f "{$Guid}", (Get-MDIAdPath 'CN=Policies,CN=System,{0}')
        Set-ADObject -Identity $gpoAdObjectPath -Replace @{versionNumber = $Version } | Out-Null
    }
    if ($Mode -match 'ALL|Sysvol') {
        $filePath = '\\{0}\SYSVOL\{0}\Policies\{1}\GPT.INI' -f $env:USERDNSDOMAIN, "{$guid}"
        $newContent = ((Get-Content $filePath) -join [environment]::NewLine) -replace 'Version=\d+', ('Version={0}' -f $version)
        Set-Content -Path $filePath -Encoding ASCII -Value $newContent
    }
}

function Get-MDIGPOMachineExtension {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [guid] $Guid
    )
    Write-Verbose -Message $strings['GPO_GetExtension']
    $gpoAdObjectPath = 'CN={0},{1}' -f "{$Guid}", (Get-MDIAdPath 'CN=Policies,CN=System,{0}')
    Get-ADObject -Identity $gpoAdObjectPath -Properties gPCMachineExtensionNames, VersionNumber
}

function Set-MDIGPOMachineExtension {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [guid] $Guid,
        [Parameter(Mandatory)] [string[]] $Extension
    )
    Write-Verbose -Message $strings['GPO_SetExtension']
    $return = $null

    $extensions = $Extension | ForEach-Object { "{$_}" }
    $extensionGuids = '[{0}]' -f [string]::Join('', $extensions, 0, 2)
    $Replace = @{gPCMachineExtensionNames = $extensionGuids }

    $gpoAdObjectPath = 'CN={0},{1}' -f "{$Guid}", (Get-MDIAdPath 'CN=Policies,CN=System,{0}')
    $gpoUpdated = Set-ADObject -Identity $gpoAdObjectPath -Replace $Replace -PassThru

    if ($gpoUpdated) {
        try {
            $gpoComputerDSVersion = (Get-MDIGPOMachineVersion -Guid $Guid).DSVersion
            if ($gpoComputerDSVersion -lt 2) { $gpoComputerDSVersion = 3 } else { $gpoComputerDSVersion++ }
            Set-MDIGPOMachineVersion -Guid $Guid -Version $gpoComputerDSVersion -Mode All
            $return = $gpoComputerDSVersion
        } catch {
            Write-Verbose -Message $_.Exception.Message
        }
    }
    $return
}

function Test-MDIGPOEnabledAndLink {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] $GPO
    )
    $state = $false
    if (-not ($GPO.GpoStatus -ne [Microsoft.GroupPolicy.GpoStatus]::AllSettingsDisabled)) {
        Write-Verbose -Message $strings['GPO_SettingsDisabled']
    } else {
        if (-not (Test-MDIGPOLink -Guid $GPO.Id.Guid)) {
            Write-Verbose -Message $strings['GPO_LinkNotFound']
        } else {
            $state = $true
        }
    }
    $state
}

#endregion

#region Processor Performance helper functions

function Get-MDIProcessorPerformance {
    & "$($env:SystemRoot)\system32\powercfg.exe" @('/GETACTIVESCHEME')
}

function Test-MDIProcessorPerformance {
    [CmdletBinding()]
    param(
        [switch] $Detailed
    )
    Write-Verbose -Message $strings['ProcessorPerformance_Validate']
    $result = $false
    $activeScheme = Get-MDIProcessorPerformance
    if ($activeScheme -match ':\s+(?<guid>[a-fA-F0-9]{8}[-]?([a-fA-F0-9]{4}[-]?){3}[a-fA-F0-9]{12})\s+\((?<name>.*)\)') {
        $result = $Matches.guid -eq '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c'
    }
    Write-Verbose -Message (Get-MDIValidationMessage $result)
    if ($Detailed) {
        [PSCustomObject]([ordered]@{
                Status  = $result
                Details = $activeScheme
            })
    } else {
        $result
    }
}

function Set-MDIProcessorPerformance {
    & "$($env:SystemRoot)\system32\powercfg.exe" @('/SETACTIVE', '8c5e7fda-e8bf-4a96-9a85-a6e23a8c635c')
}

function Get-MDIProcessorPerformanceGPO {
    [CmdletBinding()]
    param(
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.ProcessorPerformance.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpo = Get-MDIGPO -Name $gpoName
    if ($gpo) {
        $gpo | Select-Object -Property *,
        @{N = 'GPRegistryValue'; E = { Get-GPRegistryValue -Guid $gpo.Id.Guid -Key $settings.ProcessorPerformance.Key } }
    }
}

function Test-MDIProcessorPerformanceGPO {
    [CmdletBinding()]
    param(
        [switch] $Detailed,
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.ProcessorPerformance.GpoName -GpoNamePrefix $GpoNamePrefix
    Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName)

    $state = $false
    $gpo = Get-MDIProcessorPerformanceGPO -GpoNamePrefix $GpoNamePrefix

    if ($gpo) {
        $gpSetOk = $gpo.GPRegistryValue.ValueName -eq $settings.ProcessorPerformance.ValueName -and
        $gpo.GPRegistryValue.Value -eq $settings.ProcessorPerformance.SchemeGuid -and
        $gpo.GPRegistryValue.PolicyState -eq [Microsoft.GroupPolicy.PolicyState]::Set

        if ($gpSetOk) {
            $state = Test-MDIGPOEnabledAndLink -GPO $gpo
        } else {
            Write-Verbose -Message $strings['GPO_SettingsMismatch']
        }
    }
    Write-Verbose -Message (Get-MDIValidationMessage $state)
    if ($Detailed) {
        [PSCustomObject]([ordered]@{
                Status  = $state
                Details = if ($gpo) { $gpo | Select-Object DisplayName, Id, GpoStatus, GPRegistryValue }
                else { "'{0}' - {1}" -f $gpoName, $strings['GPO_NotFound'] }
            })
    } else {
        $state
    }
}

function Set-MDIProcessorPerformanceGPO {
    [CmdletBinding()]
    param(
        [switch] $SkipGpoLink,
        [switch] $CreateGpoDisabled,
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.ProcessorPerformance.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpo = Get-MDIProcessorPerformanceGPO -GpoNamePrefix $GpoNamePrefix
    if ($null -eq $gpo) {
        $gpo = New-MDIGPO -Name $gpoName -CreateGpoDisabled:$CreateGpoDisabled
    }
    if ($gpo) {
        $gppParams = @{
            Guid      = $gpo.Id.Guid
            Type      = 'String'
            Key       = $settings.ProcessorPerformance.Key
            ValueName = $settings.ProcessorPerformance.ValueName
            Value     = $settings.ProcessorPerformance.SchemeGuid
        }
        $gpoUpdated = Set-GPRegistryValue @gppParams
        if (-not ($CreateGpoDisabled)) { $gpoUpdated.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled }
        $gpoUpdated.MakeAclConsistent()

        if (-not $SkipGpoLink) {
            $gpLinkParams = @{
                Guid        = $gpo.Id.Guid
                LinkEnabled = [Microsoft.GroupPolicy.EnableLink]::Yes
                Enforced    = [Microsoft.GroupPolicy.EnforceLink]::Yes
                Target      = 'OU=Domain Controllers,{0}' -f ([adsi]'').distinguishedName.Value
            }
            Set-MDIGPOLink @gpLinkParams
        }
    } else {
        throw $strings['GPO_UnableToUpdate']
    }
}

#endregion

#region Directory Services Auditing helper functions

function Get-MDIAdPath {
    param(
        [Parameter(Mandatory)] $Path
    )
    $DefaultNamingContext = ([adsi]('LDAP://{0}/RootDSE' -f $env:USERDNSDOMAIN)).defaultNamingContext.Value
    $Path -f $DefaultNamingContext
}

function Get-MDISAcl {
    param(
        [Parameter(Mandatory)] $Path
    )
    $acls = Get-Acl -Path $Path -Audit -ErrorAction Stop
    if ($acls) {
        foreach ($acl in $acls.Audit) {
            [PSCustomObject]@{
                Account                = $acl.IdentityReference.Value
                SecurityIdentifier     = $acl.IdentityReference.Translate([System.Security.Principal.SecurityIdentifier]).Value
                AccessMask             = [int]$acl.ActiveDirectoryRights
                AccessMaskDetails      = $acl.ActiveDirectoryRights
                AuditFlags             = $acl.AuditFlags
                AuditFlagsValue        = [int]$acl.AuditFlags
                InheritedObjectAceType = $acl.InheritedObjectType
                InheritanceType        = [int]$acl.InheritanceType
                PropagationFlags       = [int]$acl.PropagationFlags
            }
        }
    }
}

function Set-MDISAcl {
    param(
        [Parameter(Mandatory)] $Auditing
    )
    $Path = Get-MDIAdPath -Path $Auditing.Path
    $acls = Get-Acl -Path $Path -Audit -ErrorAction SilentlyContinue
    if ($acls) {
        Write-Verbose -Message ('Setting System Access Control Lists')
        foreach ($audit in $Auditing.Auditing) {
            $account = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @(
                    $audit.SecurityIdentifier)).Translate([System.Security.Principal.NTAccount]).Value
            $argumentList = @(
                [Security.Principal.NTAccount] $account,
                [System.DirectoryServices.ActiveDirectoryRights] $audit.AccessMask,
                [System.Security.AccessControl.AuditFlags] $audit.AuditFlagsValue,
                [guid]::Empty.Guid.ToString(),
                [System.DirectoryServices.ActiveDirectorySecurityInheritance] $audit.InheritanceType,
                [guid] $audit.InheritedObjectAceType
            )
            $rule = New-Object -TypeName System.DirectoryServices.ActiveDirectoryAuditRule -ArgumentList $argumentList
            $acls.AddAuditRule($rule)
        }
        Set-Acl -Path $Path -AclObject $acls
    }
}

function Get-MDIDomainObjectAuditing {
    try {
        Get-MDISAcl -Path (Get-MDIAdPath -Path $settings.ObjectAuditing.Path)
    } catch [System.Management.Automation.ActionPreferenceStopException] {
        if ('ObjectNotFound' -eq $_.Exception.ErrorRecord.CategoryInfo.Category) {
            Write-Warning $_.Exception.Message
        } else {
            throw $_
        }
    }
}

function Get-MDIAdfsAuditing {
    try {
        Get-MDISAcl -Path (Get-MDIAdPath -Path $settings.AdfsAuditing.Path)
    } catch [System.Management.Automation.ActionPreferenceStopException] {
        if ('ObjectNotFound' -eq $_.Exception.ErrorRecord.CategoryInfo.Category) {
            Write-Warning $_.Exception.Message
        } else {
            throw $_
        }
    }
}

function Get-MDIConfigurationContainerAuditing {
    try {
        Get-MDISAcl -Path (Get-MDIAdPath -Path $settings.ConfigurationContainerAuditing.Path)
    } catch [System.Management.Automation.ActionPreferenceStopException] {
        if ('ObjectNotFound' -eq $_.Exception.ErrorRecord.CategoryInfo.Category) {
            Write-Warning $_.Exception.Message
        } else {
            throw $_
        }
    }
}

function Test-MDIAuditing {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Path,
        [Parameter(Mandatory)] [object[]] $ExpectedAuditing,
        [switch] $Detailed
    )
    try {
        $AppliedAuditing = Get-MDISAcl -Path (Get-MDIAdPath -Path $Path)
        $isAuditingOk = @(foreach ($applied in $AppliedAuditing) {
                $ExpectedAuditing | Where-Object { ($_.SecurityIdentifier -eq $applied.SecurityIdentifier) -and
                ($_.AuditFlagsValue -eq $applied.AuditFlagsValue) -and
                ($_.InheritedObjectAceType -eq $applied.InheritedObjectAceType) -and
                ($_.InheritanceType -eq $applied.InheritanceType) -and
                ($_.PropagationFlags -eq $applied.PropagationFlags) -and
                (([System.DirectoryServices.ActiveDirectoryRights]$applied.AccessMask).HasFlag(([System.DirectoryServices.ActiveDirectoryRights]($_.AccessMask)))) }
            }).Count -ge $ExpectedAuditing.Count

    } catch [System.Management.Automation.ActionPreferenceStopException] {
        if ('ObjectNotFound' -eq $_.Exception.ErrorRecord.CategoryInfo.Category) {
            $isAuditingOk = $true
        } else {
            $isAuditingOk = $false
        }
    }
    if ($Detailed) {
        [PSCustomObject]([ordered]@{
                Status  = $isAuditingOk
                Details = $AppliedAuditing
            })
    } else {
        $isAuditingOk
    }
}

function Test-MDIDomainObjectAuditing {
    [CmdletBinding()]
    param(
        [switch] $Detailed
    )
    Write-Verbose -Message $strings['DomainObject_ValidateAuditing']
    $result = Test-MDIAuditing -Path $settings.ObjectAuditing.Path -ExpectedAuditing $settings.ObjectAuditing.Auditing -Detailed:$Detailed
    if ($Detailed) {
        Write-Verbose -Message (Get-MDIValidationMessage $result.Status)
    } else {
        Write-Verbose -Message (Get-MDIValidationMessage $result)
    }
    $result
}

function Test-MDIAdfsAuditing {
    [CmdletBinding()]
    param(
        [switch] $Detailed
    )
    $result = [PSCustomObject]@{
        Status  = $true
        Details = $strings['ADFS_ContainerNotFound']
    }
    Write-Verbose -Message $strings['ADFS_ValidateAuditing']
    if ([System.DirectoryServices.DirectoryEntry]::Exists((Get-MDIAdPath -Path $settings.AdfsAuditing.Validate))) {
        $result = Test-MDIAuditing -Path $settings.AdfsAuditing.Path -ExpectedAuditing $settings.AdfsAuditing.Auditing -Detailed:$Detailed
    } elseif (-not $Detailed) {
        $result = $true
    }
    if ($Detailed) {
        Write-Verbose -Message (Get-MDIValidationMessage $result.Status)
    } else {
        Write-Verbose -Message (Get-MDIValidationMessage $result)
    }
    $result
}

function Test-MDIConfigurationContainerAuditing {
    [CmdletBinding()]
    param(
        [switch] $Detailed
    )
    $result = [PSCustomObject]@{
        Status  = $true
        Details = $strings['Exchange_ContainerNotFound']
    }
    Write-Verbose -Message $strings['Exchange_ValidateAuditing']
    if ([System.DirectoryServices.DirectoryEntry]::Exists((Get-MDIAdPath -Path $settings.ConfigurationContainerAuditing.Validate))) {
        $result = Test-MDIAuditing -Path $settings.ConfigurationContainerAuditing.Path -ExpectedAuditing $settings.ConfigurationContainerAuditing.Auditing -Detailed:$Detailed
    } elseif (-not $Detailed) {
        $result = $true
    }
    if ($Detailed) {
        Write-Verbose -Message (Get-MDIValidationMessage $result.Status)
    } else {
        Write-Verbose -Message (Get-MDIValidationMessage $result)
    }
    $result
}

function Set-MDIDomainObjectAuditing {
    Set-MDISAcl -Auditing $settings.ObjectAuditing
}

function Set-MDIAdfsAuditing {
    if ([System.DirectoryServices.DirectoryEntry]::Exists((Get-MDIAdPath -Path $settings.AdfsAuditing.Validate))) {
        Set-MDISAcl -Auditing $settings.AdfsAuditing
    } else {
        Write-Warning $strings['ADFS_ContainerNotFound']
    }
}

function Set-MDIConfigurationContainerAuditing {
    [CmdletBinding()]
    param(
        [switch] $Force
    )
    if ($Force -or [System.DirectoryServices.DirectoryEntry]::Exists((Get-MDIAdPath -Path $settings.ConfigurationContainerAuditing.Validate))) {
        Set-MDISAcl -Auditing $settings.ConfigurationContainerAuditing
    } else {
        Write-Warning $strings['Exchange_ContainerNotFound']
    }
}

#endregion

#region NTLM Auditing helper functions

function Get-MDINTLMAuditing {
    [CmdletBinding()]
    param()
    $settings.NTLMAuditing.RegistrySet.GetEnumerator() | ForEach-Object {
        $name = ($_.Name -split '\\')[-1]
        $path = 'HKLM:\{0}' -f ($_.Name -replace $name)
        $value = Get-ItemProperty -Path $path -Name $name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $name
        $expected = $_.Value
        [PSCustomObject]@{
            Path          = $path
            Name          = $name
            ActualValue   = $value
            ExpectedValue = $expected
        }
    }
}

function Test-MDINTLMAuditing {
    [CmdletBinding()]
    param(
        [switch] $Detailed
    )
    Write-Verbose -Message $strings['NTLM_ValidateAuditing']
    $ntlmAuditing = Get-MDINTLMAuditing
    $status = @($ntlmAuditing | Where-Object { $_.ActualValue -match $_.ExpectedValue }).Count -eq $settings.NTLMAuditing.RegistrySet.Count
    Write-Verbose (Get-MDIValidationMessage $status)
    if ($Detailed) {
        [PSCustomObject]([ordered]@{
                Status  = $status
                Details = $ntlmAuditing
            })
    } else {
        $status
    }
}

function Set-MDINTLMAuditing {
    [CmdletBinding()]
    param()
    $settings.NTLMAuditing.RegistrySet.GetEnumerator() | ForEach-Object {
        $name = ($_.Name -split '\\')[-1]
        $path = 'HKLM:\{0}' -f ($_.Name -replace $name)
        $value = ($_.Value -split '\|')[0]
        Set-ItemProperty -Path $path -Name $name -Value $value -ErrorAction Stop
    }
}

function Get-MDINTLMAuditingGPO {
    [CmdletBinding()]
    param(
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.NTLMAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpo = Get-MDIGPO -Name $gpoName
    if ($gpo) {
        $report = [xml](Get-GPOReport -Guid $gpo.Id -ReportType Xml)
        $options = $report.GPO.Computer.ExtensionData.Extension.SecurityOptions | Where-Object { $_.KeyName -Match 'AuditReceivingNTLMTraffic|RestrictSendingNTLMTraffic|AuditNTLMInDomain' }
        $RegistryValue = foreach ($opt in $options) {
            $valueName = ($opt.KeyName -split '\\')[-1]
            $path = $opt.KeyName -replace '(.*)\\(\w+)', '$1'
            [PSCustomObject]@{
                KeyName       = $path
                valueName     = $valueName
                Value         = $opt.SettingNumber
                valueDisplay  = $opt.Display.DisplayString
                ExpectedValue = ($settings.NTLMAuditing.RegistrySet.GetEnumerator() |
                        Where-Object { ('MACHINE\{0}' -f $_.Name) -eq (Join-Path -Path $path -ChildPath $valueName) }).Value
            }
        }
        $gpo | Select-Object -Property *, @{N = 'RegistryValue'; E = { $RegistryValue } }
    }
}

function Test-MDINTLMAuditingGPO {
    [CmdletBinding()]
    param(
        [switch] $Detailed,
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.NTLMAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName)

    $state = $false
    $gpo = Get-MDINTLMAuditingGPO -GpoNamePrefix $GpoNamePrefix

    if ($gpo) {
        $gpSetOk = @($gpo.RegistryValue | Where-Object {
                $_.Value -match $_.ExpectedValue
            }).Count -eq $settings.NTLMAuditing.RegistrySet.Count

        if ($gpSetOk) {
            $state = Test-MDIGPOEnabledAndLink -GPO $gpo
        } else {
            Write-Verbose -Message $strings['GPO_SettingsMismatch']
        }
    }
    Write-Verbose -Message (Get-MDIValidationMessage $state)
    if ($Detailed) {
        [PSCustomObject]([ordered]@{
                Status  = $state
                Details = if ($gpo) { $gpo | Select-Object DisplayName, Id, GpoStatus, RegistryValue }
                else { "'{0}' - {1}" -f $gpoName, $strings['GPO_NotFound'] }
            })
    } else {
        $state
    }
}

function Set-MDINTLMAuditingGPO {
    [CmdletBinding()]
    param(
        [switch] $SkipGpoLink,
        [switch] $CreateGpoDisabled,
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.NTLMAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpo = Get-MDIGPO -Name $gpoName
    if ($null -eq $gpo) {
        $gpo = New-MDIGPO -Name $gpoName -CreateGpoDisabled:$CreateGpoDisabled
    }

    $filePath = '{0}\Machine\Microsoft\Windows NT\SecEdit' -f $gpo.gPCFileSysPath
    if (-not (Test-Path $filePath)) { New-Item -Path $filePath -ItemType Directory -Force | Out-Null }

    $fileContent = @'
[Unicode]
Unicode=yes
[Version]
signature="$CHICAGO$"
Revision=1
[Registry Values]
'@


    $settings.NTLMAuditing.RegistrySet.GetEnumerator() | ForEach-Object {
        $value = ($_.Value -split '\|')[0]
        $fileContent += '{2}MACHINE\{0}=4,{1}' -f $_.Name, $Value, [System.Environment]::NewLine
    }
    Set-Content -Path (Join-Path -Path $filePath -ChildPath 'GptTmpl.inf') -Encoding Unicode -Value $fileContent

    if (-not ($CreateGpoDisabled)) { $gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled }
    $gpo.MakeAclConsistent()
    $gpoUpdated = Set-MDIGPOMachineExtension -Guid $gpo.Id.Guid -Extension @(
        $settings.gpoExtensions['Security'], $settings.gpoExtensions['Computer Restricted Groups'])

    if ($null -ne $gpoUpdated) {
        if (-not $SkipGpoLink) {
            $gpLinkParams = @{
                Guid        = $gpo.Id.Guid
                LinkEnabled = [Microsoft.GroupPolicy.EnableLink]::Yes
                Enforced    = [Microsoft.GroupPolicy.EnforceLink]::Yes
                Target      = 'OU=Domain Controllers,{0}' -f ([adsi]'').distinguishedName.Value
            }
            Set-MDIGPOLink @gpLinkParams
        }
    } else {
        Write-Warning $strings['GPO_UnableToSetExtension']
    }
}

#endregion

#region Advanced Auditing Policy helper functions

function Get-MDIAdvAuditPolicy {
    [CmdletBinding()]
    param()
    & "$($env:SystemRoot)\system32\auditpol.exe" @('/get', '/category:*', '/r') | ConvertFrom-Csv |
        Select-Object *, @{N = 'Setting Value'; E = {
                $setting = 0
                if ($_.'Inclusion Setting' -match 'Success') { $setting += 1 }
                if ($_.'Inclusion Setting' -match 'Failure') { $setting += 2 }
                if ($_.'Exclusion Setting' -match 'Success') { $setting += 4 }
                if ($_.'Exclusion Setting' -match 'Failure') { $setting += 8 }
                $setting
            }
        }
}

function Test-MDIAdvAuditPolicy {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)]
        [object[]] $ExpectedAuditing,
        [switch] $Detailed
    )
    $AppliedAuditing = Get-MDIAdvAuditPolicy
    $status = @(foreach ($applied in $AppliedAuditing) {
            $ExpectedAuditing | Where-Object {
            ($applied.'Policy Target') -eq ($_.'Policy Target') -and
            ($applied.'Subcategory GUID').ToUpper() -eq ($_.'Subcategory Guid').ToUpper() -and
            ($applied.'Setting Value') -eq ($_.'Setting Value')
            }
        }).Count -ge $ExpectedAuditing.Count
    if ($Detailed) {
        [PSCustomObject]([ordered]@{
                Status  = $status
                Details = $AppliedAuditing
            })
    } else {
        $status
    }
}

function Set-MDIAdvAuditPolicy {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] $SubcategoryGUID,
        [string] $InclusionSetting
    )
    if ($SubcategoryGUID -notmatch '^{.*}$') { $SubcategoryGUID = "{$SubcategoryGUID}" }
    $success = if ($InclusionSetting -match 'Success') { 'enable' } else { 'disable' }
    $failure = if ($InclusionSetting -match 'Failure') { 'enable' } else { 'disable' }
    $null = & "$($env:SystemRoot)\system32\auditpol.exe" @('/set', "/subcategory:$SubcategoryGUID", "/success:$success", "/failure:$failure")
}

#endregion

#region Advanced Auditing Policy for DCs Settings

function Get-MDIAdvancedAuditPolicyDCsGPO {
    [CmdletBinding()]
    param(
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyDCs.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpo = Get-MDIGPO -Name $gpoName
    if ($gpo) {
        $report = [xml](Get-GPOReport -Guid $gpo.Id -ReportType Xml)
        $currentSettings = $report.GPO.Computer.ExtensionData.Extension.AuditSetting
        $expectedSettings = $settings.AdvancedAuditPolicyDCs.PolicySettings | ConvertFrom-Csv
        $AuditSettings = foreach ($audit in $expectedSettings) {
            [PSCustomObject]@{
                PolicyTarget    = $audit.'Policy Target'
                SubcategoryName = $audit.'Subcategory'
                SubcategoryGuid = $audit.'Subcategory GUID'
                SettingValue    = $audit.'Setting Value'
                ExpectedValue   = ($currentSettings | Where-Object { -not [string]::IsNullOrEmpty($_) } | Where-Object {
                    ($_.SubcategoryGuid).ToUpper() -eq ($audit.'Subcategory GUID').ToUpper() -and
                        $_.PolicyTarget -eq $audit.'Policy Target' }).SettingValue
            }
        }
        $gpo | Select-Object -Property *, @{N = 'AuditSettings'; E = { $AuditSettings } }
    }
}

function Test-MDIAdvancedAuditPolicyDCsGPO {
    [CmdletBinding()]
    param(
        [switch] $Detailed,
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyDCs.GpoName -GpoNamePrefix $GpoNamePrefix
    Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName)

    $state = $false
    $gpo = Get-MDIAdvancedAuditPolicyDCsGPO -GpoNamePrefix $GpoNamePrefix

    if ($gpo) {
        $expectedSettings = @($settings.AdvancedAuditPolicyDCs.PolicySettings | ConvertFrom-Csv)
        $gpSetOk = @($gpo.AuditSettings | Where-Object {
                $_.SettingValue -match $_.ExpectedValue
            }).Count -eq $expectedSettings.Count

        if ($gpSetOk) {
            $state = Test-MDIGPOEnabledAndLink -GPO $gpo
        } else {
            Write-Verbose -Message $strings['GPO_SettingsMismatch']
        }
    }
    Write-Verbose -Message (Get-MDIValidationMessage $state)
    if ($Detailed) {
        [PSCustomObject]([ordered]@{
                Status  = $state
                Details = if ($gpo) { $gpo | Select-Object DisplayName, Id, GpoStatus, AuditSettings }
                else { "'{0}' - {1}" -f $gpoName, $strings['GPO_NotFound'] }
            })
    } else {
        $state
    }
}

function Set-MDIAdvancedAuditPolicyDCsGPO {
    [CmdletBinding()]
    param(
        [switch] $SkipGpoLink,
        [switch] $CreateGpoDisabled,
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyDCs.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpo = Get-MDIGPO -Name $gpoName
    if ($null -eq $gpo) {
        $gpo = New-MDIGPO -Name $gpoName -CreateGpoDisabled:$CreateGpoDisabled
    }

    $filePath = '{0}\Machine\Microsoft\Windows NT\Audit' -f $gpo.gPCFileSysPath
    if (-not (Test-Path $filePath)) { New-Item -Path $filePath -ItemType Directory -Force | Out-Null }
    Set-Content -Path (Join-Path -Path $filePath -ChildPath 'audit.csv') -Encoding ASCII -Value $settings.AdvancedAuditPolicyDCs.PolicySettings

    if (-not ($CreateGpoDisabled)) { $gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled }
    $gpo.MakeAclConsistent()
    $gpoUpdated = Set-MDIGPOMachineExtension -Guid $gpo.Id.Guid -Extension @(
        $settings.gpoExtensions['Audit Policy Configuration'], $settings.gpoExtensions['Audit Configuration Extension'])

    if ($null -ne $gpoUpdated) {
        if (-not $SkipGpoLink) {
            $gpLinkParams = @{
                Guid        = $gpo.Id.Guid
                LinkEnabled = [Microsoft.GroupPolicy.EnableLink]::Yes
                Enforced    = [Microsoft.GroupPolicy.EnforceLink]::Yes
                Target      = 'OU=Domain Controllers,{0}' -f ([adsi]'').distinguishedName.Value
            }
            Set-MDIGPOLink @gpLinkParams
        }
    } else {
        Write-Warning $strings['GPO_UnableToSetExtension']
    }
}

function Get-MDIAdvancedAuditPolicyDCs {
    [CmdletBinding()]
    param()
    $relevantGUIDs = @($settings.AdvancedAuditPolicyDCs.PolicySettings | ConvertFrom-Csv) | Select-Object -ExpandProperty 'Subcategory GUID' -Unique
    Get-MDIAdvAuditPolicy | Where-Object { $_.'Subcategory GUID' -in $relevantGUIDs }
}

function Test-MDIAdvancedAuditPolicyDCs {
    [CmdletBinding()]
    param(
        [switch] $Detailed
    )
    Write-Verbose -Message $strings['AdvancedPolicyDCs_Validate']
    $result = Test-MDIAdvAuditPolicy -ExpectedAuditing @($settings.AdvancedAuditPolicyDCs.PolicySettings | ConvertFrom-Csv) -Detailed:$Detailed
    if ($Detailed) {
        Write-Verbose -Message (Get-MDIValidationMessage $result.Status)
    } else {
        Write-Verbose -Message (Get-MDIValidationMessage $result)
    }
    $result
}

function Set-MDIAdvancedAuditPolicyDCs {
    Write-Verbose -Message $strings['AdvancedPolicyDCs_Set']
    $settings.AdvancedAuditPolicyDCs.PolicySettings | ConvertFrom-Csv | ForEach-Object {
        $param = @{
            SubcategoryGUID  = $_.'Subcategory GUID'
            InclusionSetting = $_.'Inclusion Setting'
        }
        Set-MDIAdvAuditPolicy @param
    }
}

#endregion

#region Advanced Auditing Policy for CAs Settings

function Get-MDIAdvancedAuditPolicyCAsGPO {
    [CmdletBinding()]
    param(
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyCAs.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpo = Get-MDIGPO -Name $gpoName
    if ($gpo) {
        $report = [xml](Get-GPOReport -Guid $gpo.Id -ReportType Xml)
        $currentSettings = $report.GPO.Computer.ExtensionData.Extension.AuditSetting
        $expectedSettings = $settings.AdvancedAuditPolicyCAs.PolicySettings | ConvertFrom-Csv
        $AuditSettings = foreach ($audit in $expectedSettings) {
            [PSCustomObject]@{
                PolicyTarget    = $audit.'Policy Target'
                SubcategoryName = $audit.'Subcategory'
                SubcategoryGuid = $audit.'Subcategory GUID'
                SettingValue    = $audit.'Setting Value'
                ExpectedValue   = ($currentSettings | Where-Object {
                        $_.SubcategoryName -eq ($audit.Subcategory) -and
                        ($_.SubcategoryGuid).ToUpper() -eq ($audit.'Subcategory GUID').ToUpper() -and
                        $_.PolicyTarget -eq $audit.'Policy Target' }).SettingValue
            }
        }
        $delegation = $settings.AdvancedAuditPolicyCAs.GPPermissions.GetEnumerator() | ForEach-Object {
            Get-GPPermission -Guid $gpo.Id.Guid -TargetType Group -TargetName $_.Key
        }
        $gpo = $gpo | Select-Object -Property *, @{N = 'AuditSettings'; E = { $AuditSettings } }, @{N = 'Delegation'; E = { $delegation } }
    }
    $gpo
}

function Test-MDIAdvancedAuditPolicyCAsGPO {
    [CmdletBinding()]
    param(
        [switch] $Detailed,
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyCAs.GpoName -GpoNamePrefix $GpoNamePrefix
    Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName)

    $state = $false
    $gpo = Get-MDIAdvancedAuditPolicyCAsGPO -GpoNamePrefix $GpoNamePrefix

    if ($gpo) {
        $expectedSettings = @($settings.AdvancedAuditPolicyCAs.PolicySettings | ConvertFrom-Csv)
        $gpSetOk = @($gpo.AuditSettings | Where-Object {
                $_.SettingValue -match $_.ExpectedValue
            }).Count -eq $expectedSettings.Count

        if ($gpSetOk) {
            $gpDelegationOk = @($gpo.Delegation | Where-Object {
                    $settings.AdvancedAuditPolicyCAs.GPPermissions[$_.Trustee.Name] -eq $_.Permission
                }).Count -eq $settings.AdvancedAuditPolicyCAs.GPPermissions.Count

            if (-not $gpDelegationOk) {
                Write-Verbose -Message $strings['GPO_DelegationMismatch']
            } else {
                $state = Test-MDIGPOEnabledAndLink -GPO $gpo
            }

        } else {
            Write-Verbose -Message $strings['GPO_SettingsMismatch']
        }
    }
    Write-Verbose -Message (Get-MDIValidationMessage $state)
    if ($Detailed) {
        [PSCustomObject]([ordered]@{
                Status  = $state
                Details = if ($gpo) { $gpo | Select-Object DisplayName, Id, GpoStatus, AuditSettings }
                else { "'{0}' - {1}" -f $gpoName, $strings['GPO_NotFound'] }
            })
    } else {
        $state
    }
}

function Set-MDIAdvancedAuditPolicyCAsGPO {
    [CmdletBinding()]
    param(
        [switch] $SkipGpoLink,
        [switch] $CreateGpoDisabled,
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyCAs.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpo = Get-MDIGPO -Name $gpoName
    if ($null -eq $gpo) {
        $gpo = New-MDIGPO -Name $gpoName -CreateGpoDisabled:$CreateGpoDisabled
    }

    $filePath = '{0}\Machine\Microsoft\Windows NT\Audit' -f $gpo.gPCFileSysPath
    if (-not (Test-Path $filePath)) { New-Item -Path $filePath -ItemType Directory -Force | Out-Null }
    Set-Content -Path (Join-Path -Path $filePath -ChildPath 'audit.csv') -Encoding ASCII -Value $settings.AdvancedAuditPolicyCAs.PolicySettings

    if (-not ($CreateGpoDisabled)) { $gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled }
    $gpo.MakeAclConsistent()
    $gpoUpdated = Set-MDIGPOMachineExtension -Guid $gpo.Id.Guid -Extension @(
        $settings.gpoExtensions['Audit Policy Configuration'], $settings.gpoExtensions['Audit Configuration Extension'])

    Write-Verbose -Message $strings['GPO_SetDelegation']
    $settings.AdvancedAuditPolicyCAs.GPPermissions.GetEnumerator() | ForEach-Object {
        Set-GPPermission -Guid $gpo.Id.Guid -TargetType Group -TargetName $_.Key -PermissionLevel $_.Value -Replace | Out-Null
    }

    if ($null -ne $gpoUpdated) {
        if (-not $SkipGpoLink) {
            $gpLinkParams = @{
                Guid        = $gpo.Id.Guid
                LinkEnabled = [Microsoft.GroupPolicy.EnableLink]::Yes
                Enforced    = [Microsoft.GroupPolicy.EnforceLink]::Yes
                Target      = ([adsi]'').distinguishedName.Value
            }
            Set-MDIGPOLink @gpLinkParams
        }
    } else {
        Write-Warning $strings['GPO_UnableToSetExtension']
    }
}

function Get-MDIAdvancedAuditPolicyCAs {
    [CmdletBinding()]
    param()
    $relevantGUIDs = @($settings.AdvancedAuditPolicyCAs.PolicySettings | ConvertFrom-Csv) | Select-Object -ExpandProperty 'Subcategory GUID' -Unique
    Get-MDIAdvAuditPolicy | Where-Object { $_.'Subcategory GUID' -in $relevantGUIDs }
}

function Test-MDIAdvancedAuditPolicyCAs {
    [CmdletBinding()]
    param(
        [switch] $Detailed
    )
    Write-Verbose -Message $strings['AdvancedPolicyCAs_Validate']
    if (Test-MDICAServer) {
        $result = Test-MDIAdvAuditPolicy -ExpectedAuditing @($settings.AdvancedAuditPolicyCAs.PolicySettings | ConvertFrom-Csv) -Detailed:$Detailed
    } else {
        Write-Verbose -Message $strings['CAAuditing_NotCAServer']
        $result = [PSCustomObject]([ordered]@{
                Status  = $true
                Details = $strings['CAAuditing_NotCAServer']
            })
    }

    if ($Detailed) {
        Write-Verbose -Message (Get-MDIValidationMessage $result.Status)
    } else {
        Write-Verbose -Message (Get-MDIValidationMessage $result)
    }
    $result
}

function Set-MDIAdvancedAuditPolicyCAs {
    Write-Verbose -Message $strings['AdvancedPolicyCAs_Set']
    $settings.AdvancedAuditPolicyCAs.PolicySettings | ConvertFrom-Csv | ForEach-Object {
        $param = @{
            SubcategoryGUID  = $_.'Subcategory GUID'
            InclusionSetting = $_.'Inclusion Setting'
        }
        Set-MDIAdvAuditPolicy @param
    }
}

#endregion

#region CA Audit configuration helper functions

function Get-MDICAAuditing {
    [CmdletBinding()]
    param()
    $certSvcConfigPath = $settings.CAAuditing.RegPathActive
    $name = ($certSvcConfigPath -split '\\')[-1]
    $activePath = 'HKLM:\{0}' -f ($certSvcConfigPath -replace $name)
    $activeValue = Get-ItemProperty -Path $activePath -Name $name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $name

    $settings.CAAuditing.RegistrySet.GetEnumerator() | ForEach-Object {
        $name = ($_.Name -split '\\')[-1]
        $path = 'HKLM:\{0}' -f (($_.Name -replace $name) -f $activeValue)
        $value = Get-ItemProperty -Path $path -Name $name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $name
        $expected = $_.Value
        [PSCustomObject]@{
            Path          = $path
            Name          = $name
            ActualValue   = $value
            ExpectedValue = $expected
        }
    }
}

function Test-MDICAAuditing {
    [CmdletBinding()]
    param(
        [switch] $Detailed
    )
    Write-Verbose -Message $strings['CAAuditing_Validate']
    if (Test-MDICAServer) {
        $caAuditing = Get-MDICAAuditing
        $caAuditingOk = @($caAuditing | Where-Object { $_.ActualValue -match $_.ExpectedValue }).Count -eq $settings.CAAuditing.RegistrySet.Count
    } else {
        Write-Verbose -Message $strings['CAAuditing_NotCAServer']
        $caAuditing = $strings['CAAuditing_NotCAServer']
        $caAuditingOk = $true
    }
    Write-Verbose -Message (Get-MDIValidationMessage $caAuditingOk)

    if ($Detailed) {
        [PSCustomObject]([ordered]@{
                Status  = $caAuditingOk
                Details = $caAuditing
            })
    } else {
        $caAuditingOk
    }
}

function Set-MDICAAuditing {
    [CmdletBinding()]
    param(
        [switch] $SkipServiceRestart
    )
    if (Get-Service CertSvc -ErrorAction SilentlyContinue) {
        $certSvcConfigPath = $settings.CAAuditing.RegPathActive
        $name = ($certSvcConfigPath -split '\\')[-1]
        $activePath = 'HKLM:\{0}' -f ($certSvcConfigPath -replace $name)
        $activeValue = Get-ItemProperty -Path $activePath -Name $name -ErrorAction SilentlyContinue | Select-Object -ExpandProperty $name

        $settings.CAAuditing.RegistrySet.GetEnumerator() | ForEach-Object {
            $name = ($_.Name -split '\\')[-1]
            $path = 'HKLM:\{0}' -f (($_.Name -replace $name) -f $activeValue)
            $value = ($_.Value -split '\|')[0]
            Write-Verbose -Message ('Setting {0}{1} to {2}' -f $path, $name, $value)
            Set-ItemProperty -Path $path -Name $name -Value $value -ErrorAction Stop
        }
        if (-not $SkipServiceRestart) { Restart-Service -Name CertSvc -Force -Verbose:$VerbosePreference }
    } else {
        Write-Warning $strings['CAAuditing_NotCAServer']
    }
}

function Get-MDICAAuditingGPO {
    [CmdletBinding()]
    param(
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.CAAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpo = Get-MDIGPO -Name $gpoName
    if ($gpo) {
        $params = @{
            Guid      = $gpo.Id
            Context   = 'Computer'
            Key       = 'HKEY_LOCAL_MACHINE\{0}' -f $settings.CAAuditing.GpoReg
            ValueName = ($settings.CAAuditing.GpoVal).Keys[0]
        }; $GPPrefRegistryValue = Get-GPPrefRegistryValue @params -ErrorAction SilentlyContinue
        $delegation = $settings.CAAuditing.GPPermissions.GetEnumerator() | ForEach-Object {
            Get-GPPermission -Guid $gpo.Id.Guid -TargetType Group -TargetName $_.Key
        }
        $gpo = $gpo | Select-Object -Property *, @{N = 'GPPrefRegistryValue'; E = { $GPPrefRegistryValue } }, @{N = 'Delegation'; E = { $delegation } }
    }
    $gpo
}

function Test-MDICAAuditingGPO {
    [CmdletBinding()]
    param(
        [switch] $Detailed,
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.CAAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName)

    $state = $false
    $gpo = Get-MDICAAuditingGPO -GpoNamePrefix $GpoNamePrefix
    $gpSetOk = @()

    if ($gpo -and $gpo.GPPrefRegistryValue) {
        $settings.CAAuditing.GpoVal.GetEnumerator() | ForEach-Object {
            $expected = [PSCustomObject]@{
                DisabledDirectly = $false
                Type             = 'DWord'
                Action           = 'Update'
                Hive             = 'LocalMachine'
                FullKeyPath      = 'HKEY_LOCAL_MACHINE\{0}' -f $settings.CAAuditing.GpoReg
                ValueName        = $_.Key
                Value            = $_.Value
            }
            $properties = $expected | Get-Member -MemberType NoteProperty | Select-Object -ExpandProperty Name
            $applied = $gpo.GPPrefRegistryValue | Select-Object -Property $properties
            $gpSetOk += ($null -ne (Compare-Object -ReferenceObject $applied -DifferenceObject $expected -Property $properties -IncludeEqual -ExcludeDifferent))
        }

        if (($gpSetOk -eq $false).Count -eq 0) {
            $gpDelegationOk = @($gpo.Delegation | Where-Object {
                    $settings.CAAuditing.GPPermissions[$_.Trustee.Name] -eq $_.Permission
                }).Count -eq $settings.CAAuditing.GPPermissions.Count

            if (-not $gpDelegationOk) {
                Write-Verbose -Message $strings['GPO_DelegationMismatch']
            } else {
                $state = Test-MDIGPOEnabledAndLink -GPO $gpo
            }

        } else {
            Write-Verbose -Message $strings['GPO_SettingsMismatch']
        }
    }
    Write-Verbose -Message (Get-MDIValidationMessage $state)
    if ($Detailed) {
        [PSCustomObject]([ordered]@{
                Status  = $state
                Details = if ($gpo) { $gpo | Select-Object DisplayName, Id, GpoStatus, GPPrefRegistryValue }
                else { "'{0}' - {1}" -f $gpoName, $strings['GPO_NotFound'] }
            })
    } else {
        $state
    }
}

function Set-MDICAAuditingGPO {
    [CmdletBinding()]
    param(
        [switch] $SkipGpoLink,
        [switch] $CreateGpoDisabled,
        [string] $GpoNamePrefix
    )
    $gpoName = Get-MDIGPOName -Name $settings.CAAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpo = Get-MDIGPO -Name $gpoName
    if ($null -eq $gpo) {
        $gpo = New-MDIGPO -Name $gpoName -CreateGpoDisabled:$CreateGpoDisabled
    }

    $settings.CAAuditing.GpoVal.GetEnumerator() | ForEach-Object {
        $params = @{
            Guid      = $gpo.Id
            Context   = 'Computer'
            Key       = 'HKEY_LOCAL_MACHINE\{0}' -f $settings.CAAuditing.GpoReg
            ValueName = $_.Name
            Order     = -1
        }; if (Get-GPPrefRegistryValue @params -ErrorAction SilentlyContinue) { $gpo = Remove-GPPrefRegistryValue @params }

        $params += @{
            Value  = [int]$_.Value
            Type   = 'DWord'
            Action = 'Update'
        }
        Set-GPPrefRegistryValue @params | Out-Null
    }

    if (-not ($CreateGpoDisabled)) { $gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled }
    $gpo.MakeAclConsistent()
    $gpoUpdated = Set-MDIGPOMachineExtension -Guid $gpo.Id.Guid -Extension @(
        $settings.gpoExtensions['Preference CSE GUID Registry'], $settings.gpoExtensions['Preference Tool CSE GUID Registry'])

    Write-Verbose -Message $strings['GPO_SetDelegation']
    $settings.CAAuditing.GPPermissions.GetEnumerator() | ForEach-Object {
        Set-GPPermission -Guid $gpo.Id.Guid -TargetType Group -TargetName $_.Key -PermissionLevel $_.Value -Replace | Out-Null
    }

    if ($null -ne $gpoUpdated) {
        if (-not $SkipGpoLink) {
            $gpLinkParams = @{
                Guid        = $gpo.Id.Guid
                LinkEnabled = [Microsoft.GroupPolicy.EnableLink]::Yes
                Enforced    = [Microsoft.GroupPolicy.EnforceLink]::Yes
                Target      = ([adsi]'').distinguishedName.Value
            }
            Set-MDIGPOLink @gpLinkParams
        }
    } else {
        Write-Warning $strings['GPO_UnableToSetExtension']
    }
}

#endregion

#region Domain helper functions

function Get-MDIDomainSchemaVersion {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)] [string] $Domain = $env:USERDNSDOMAIN
    )
    $schemaVersions = @{
        13 = 'Windows 2000 Server'
        30 = 'Windows Server 2003'
        31 = 'Windows Server 2003 R2'
        44 = 'Windows Server 2008'
        47 = 'Windows Server 2008 R2'
        56 = 'Windows Server 2012'
        69 = 'Windows Server 2012 R2'
        87 = 'Windows Server 2016'
        88 = 'Windows Server 2019 / 2022'
        90 = 'Windows Server vNext'
    }

    Write-Verbose -Message 'Getting AD Schema Version'
    $schema = New-Object -TypeName System.DirectoryServices.DirectoryEntry -ArgumentList (
        'LDAP://{0}' -f ([adsi]'LDAP://rootDSE').Properties['schemaNamingContext'].Value
    )
    $schemaVersion = $schema.Properties['objectVersion'].Value

    $return = @{
        schemaVersion = $schemaVersion
        details       = $schemaVersions[$schemaVersion]
    }
    $return
}

#endregion

#region DSA helper functions

function Get-MDIDeletedObjectsContainerPermission {
    [CmdletBinding()]
    param ()
    $deletedObjectsDN = 'CN=Deleted Objects,{0}' -f ([adsi]'').distinguishedName.Value
    $output = & "$($env:SystemRoot)\system32\dsacls.exe" @($deletedObjectsDN)
    ($output -join [System.Environment]::NewLine) -split '(?=Allow\s)' | Where-Object { $_ -match 'Allow' } | ForEach-Object {
        if ($_ -match 'Allow\s(?<Identity>(NT AUTHORITY\\\w+)|([^\s]+))\s+(?<Permissions>.*(?:\n\s+.*)*)') {
            [PSCustomObject]@{
                Identity    = $Matches.Identity
                Permissions = $Matches.Permissions -split '\s{2,}' | ForEach-Object { $_.Trim() }
            }
        }
    }
}

function Test-MDIDSA {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)] [string] $Identity,
        [switch] $Detailed
    )
    $return = @()
    $account = try {
        Get-ADUser -Identity $Identity -Properties msDS-PrincipalName
    } catch { try { Get-ADServiceAccount -Identity $Identity -Properties msDS-PrincipalName } catch { $null } }

    if ($null -eq $account) {
        $false
        Write-Error $strings['DSA_CannotFindIdentity'] -ErrorAction Stop
    } else {

        Write-Verbose -Message $strings['DSA_TestGroupMembership']
        $memberOf = @{}
        $filter = '(&(objectCategory=group)(objectClass=group)(member:1.2.840.113556.1.4.1941:={0}))' -f $account.DistinguishedName
        $searcher = [adsisearcher]$filter
        'objectSid', 'distinguishedName', 'msDS-PrincipalName' | ForEach-Object { [void]($searcher.PropertiesToLoad.Add($_)) }
        $searcher.FindAll() | ForEach-Object {
            $memberOf.Add($_.Properties['distinguishedname'][0],
                [PSCustomObject]@{
                    'objectSid'          = (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList @($_.Properties['objectSid'][0], 0)).Value
                    'msDS-PrincipalName' = $_.Properties['msDS-PrincipalName'][0]
                })
        }

        $domainSid = (Get-ADDomain).DomainSID.Value
        $sensitiveGroups = @{}
        $settings.SensitiveGroups.GetEnumerator() | ForEach-Object {
            $sensitiveGroups.Add(($_.Value -f $domainSid), $_.Key)
        }

        $sensitiveGroupsMembership = @(
            $memberOf.GetEnumerator() | Where-Object {
                $sensitiveGroups.ContainsKey($_.Value.objectSid)
            } | Select-Object -ExpandProperty Name
        )
        $return += [PSCustomObject][ordered]@{
            Test    = 'SensitiveGroupsMembership'
            Status  = $sensitiveGroupsMembership.Count -eq 0
            Details = $sensitiveGroupsMembership
        }

        Write-Verbose -Message $strings['DSA_TestDelegation']
        $sidsToCheck = @($account.SID.Value)
        $sidsToCheck += ($memberOf.GetEnumerator() | Where-Object {
                $sensitiveGroupsMembership -notcontains $_.Key }).Value.Value

        $filter = '(|(objectClass=domain)(objectClass=organizationalUnit)(objectClass=group))'
        $searcher = [adsisearcher]$filter
        $searcher.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
        $delegatedObjects = $searcher.FindAll() | ForEach-Object {
            $de = $_.GetDirectoryEntry()
            $permissions = $de.PsBase.ObjectSecurity.GetAccessRules($true, $false, [System.Security.Principal.SecurityIdentifier])
            if ($permissions | Where-Object { ($_.AccessControlType -eq 'Allow') -and ($sidsToCheck -contains $_.IdentityReference.Value) }) {
                $de.distinguishedName.Value
            }
        }
        $return += [PSCustomObject][ordered]@{
            Test    = 'ExplicitDelegation'
            Status  = $delegatedObjects.Count -eq 0
            Details = @($delegatedObjects | Select-Object -Unique)
        }

        Write-Verbose -Message $strings['DSA_TestDeletedObjectsAccess']
        $appliedAsExpected = $false
        $msDSPrincipalNamesToCheck = @($memberOf.GetEnumerator() | ForEach-Object { $_.Value.'msDS-PrincipalName' })
        $msDSPrincipalNamesToCheck += $account.'msDS-PrincipalName'
        $expectedDsacls = @('SPECIAL ACCESS', 'LIST CONTENTS', 'READ PROPERTY')
        $appliedDsacls = Get-MDIDeletedObjectsContainerPermission
        if ([string]::IsNullOrEmpty($appliedDsacls)) {
            Write-Warning -Message $strings['DSA_CannotReadDeletedObjectsContainer']
        } else {
            $dsaDsacls = $appliedDsacls | Where-Object { $msDSPrincipalNamesToCheck -contains $_.Identity } | Select-Object -ExpandProperty Permissions
            if ($null -eq $dsaDsacls) {
                $dsaDsacls = 'NONE'
            } else {
                $dsaDsacls = $dsaDsacls | Select-Object -Unique
                $appliedAsExpected = (Compare-Object -ReferenceObject $dsaDsacls -DifferenceObject $expectedDsacls -IncludeEqual -ExcludeDifferent).Count -eq $expectedDsacls.Count
            }
        }
        $return += [PSCustomObject][ordered]@{
            Test    = 'DeletedObjectsContainerPermission'
            Status  = $appliedAsExpected
            Details = $dsaDsacls
        }

        if ($account.ObjectClass -eq 'user') {
            Write-Verbose -Message $strings['DSA_TestManager']
            $filter = '(|(managedBy={0})(manager={0}))' -f ($account.DistinguishedName -replace '\s', '\20')
            $searcher = [adsisearcher]$filter
            $searcher.SearchScope = [System.DirectoryServices.SearchScope]::Subtree
            $managerOf = $searcher.FindAll()
            $return += [PSCustomObject][ordered]@{
                Test    = 'ManagerOf'
                Status  = $managerOf.Count -eq 0
                Details = @($managerOf | ForEach-Object { $_.Properties['distinguishedname'] })
            }
        } else {
            Write-Warning $strings['DSA_SkipGmsaTests']
        }

        $overallStatus = ($return.Status -eq $false).Count -eq 0
        if (-not $Detailed) { $overallStatus }
        else { $return }
        Write-Verbose -Message (Get-MDIValidationMessage $overallStatus)
    }
}

#endregion

#region Connectivity helper functions

function Test-MDISensorApiConnection {
    [CmdletBinding(DefaultParameterSetName = 'UseCurrentConfiguration')]
    param(
        [Parameter(Mandatory = $true, ParameterSetName = 'BypassConfiguration')]
        [switch] $BypassConfiguration,

        [Parameter(Mandatory = $true, ParameterSetName = 'BypassConfiguration')]
        [string] $SensorApiUrl,

        [Parameter(Mandatory = $false, ParameterSetName = 'BypassConfiguration')]
        [string] $ProxyUrl,

        [Parameter(Mandatory = $false, ParameterSetName = 'BypassConfiguration')]
        [PSCredential] $ProxyCredential
    )

    $sensorApiPath = 'tri/sensor/api/ping'
    $protocol = @{
        80  = 'http'
        443 = 'https'
    }

    if ($PSCmdlet.ParameterSetName -eq 'BypassConfiguration') {

        $params = @{ URI = $SensorApiUrl }
        if ($ProxyUrl) { $params.Add('Proxy', $ProxyUrl) }
        if ($ProxyCredential) { $params.Add('ProxyCredential', $ProxyCredential) }

    } else {
        $sensorConfiguration = Get-MDISensorConfiguration
        if ([string]::IsNullOrEmpty($sensorConfiguration)) {
            Write-Error $strings['Sensor_ErrorReadingSensorConfiguration'] -ErrorAction Stop
        } else {
            $params = @{
                URI = '{0}://{1}' -f $protocol[$sensorConfiguration.WorkspaceApplicationSensorApiWebClientConfigurationServiceEndpoint.Port],
                $sensorConfiguration.WorkspaceApplicationSensorApiWebClientConfigurationServiceEndpoint.Address
            }
            if ($sensorConfiguration.SensorProxyConfiguration.IsProxyEnabled) {
                $params.Add('Proxy', $sensorConfiguration.SensorProxyConfiguration.Url)

                if ($sensorConfiguration.SensorProxyConfiguration.IsAuthenticationProxyEnabled) {
                    $decryptParams = @{
                        CertificateThumbprint = $sensorConfiguration.SensorProxyConfiguration.CertificateThumbprint
                        EncryptedString       = $sensorConfiguration.SensorProxyConfiguration.EncryptedUserPasswordData
                    }
                    $passwd = Get-MDIDecryptedPassword @decryptParams
                    $proxyCredential = New-Object -TypeName System.Management.Automation.PSCredential -ArgumentList @(
                        $sensorConfiguration.SensorProxyConfiguration.UserName,
                        ($passwd | ConvertTo-SecureString -AsPlainText -Force)
                    )
                    $params.Add('ProxyCredential', $proxyCredential)
                }
            }
        }
    }
    try {
        if ($params.URI -notmatch "$sensorApiPath`$") {
            $params.URI = '{0}/{1}' -f $params.URI, $sensorApiPath
        }
        $response = Invoke-WebRequest @params
        (200 -eq $response.StatusCode)
    } catch {
        Write-Verbose -Message $_.Exception.Message
        $false
    }
}

#endregion

#region Post deployment configuration helper functions

function Use-MDIConfigName {
    param(
        [Parameter(Mandatory)] [string[]] $Configuration,
        [Parameter(Mandatory)] [string[]] $ActionItem
    )
    $ActionItem += 'All'
    @(Compare-Object -ReferenceObject $Configuration -DifferenceObject $ActionItem -ExcludeDifferent -IncludeEqual).Count -gt 0
}

function Get-MDIConfiguration {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)] [ValidateSet('Domain', 'LocalMachine')] [string] $Mode,
        [Parameter(Mandatory = $true)] [ValidateSet('AdfsAuditing', 'AdvancedAuditPolicyCAs', 'AdvancedAuditPolicyDCs',
            'CAAuditing', 'ConfigurationContainerAuditing', 'DomainObjectAuditing', 'NTLMAuditing', 'ProcessorPerformance', 'All')] [string[]] $Configuration,
        [Parameter(Mandatory = $false)] [string] $GpoNamePrefix
    )

    $results = @{}
    if (Use-MDIConfigName $Configuration 'AdfsAuditing') {
        $results.Add('AdfsAuditing', (Test-MDIAdfsAuditing -Detailed))
    }
    if (Use-MDIConfigName $Configuration 'AdvancedAuditPolicyCAs') {
        if ($Mode -eq 'LocalMachine') {
            $results.Add('AdvancedAuditPolicyCAs', (Test-MDIAdvancedAuditPolicyCAs -Detailed))
        } else {
            $results.Add('AdvancedAuditPolicyCAs', (Test-MDIAdvancedAuditPolicyCAsGPO -Detailed -GpoNamePrefix $GpoNamePrefix))
        }
    }
    if (Use-MDIConfigName $Configuration 'AdvancedAuditPolicyDCs') {
        if ($Mode -eq 'LocalMachine') {
            $results.Add('AdvancedAuditPolicyDCs', (Test-MDIAdvancedAuditPolicyDCs -Detailed))
        } else {
            $results.Add('AdvancedAuditPolicyDCs', (Test-MDIAdvancedAuditPolicyDCsGPO -Detailed -GpoNamePrefix $GpoNamePrefix))
        }
    }
    if (Use-MDIConfigName $Configuration 'CAAuditing') {
        if ($Mode -eq 'LocalMachine') {
            $results.Add('CAAuditing', (Test-MDICAAuditing -Detailed))
        } else {
            $results.Add('CAAuditing', (Test-MDICAAuditingGPO -Detailed -GpoNamePrefix $GpoNamePrefix))
        }
    }
    if (Use-MDIConfigName $Configuration 'ConfigurationContainerAuditing') {
        $results.Add('ConfigurationContainerAuditing', (Test-MDIConfigurationContainerAuditing -Detailed))
    }
    if (Use-MDIConfigName $Configuration 'DomainObjectAuditing') {
        $results.Add('DomainObjectAuditing', (Test-MDIDomainObjectAuditing -Detailed))
    }
    if (Use-MDIConfigName $Configuration 'NTLMAuditing') {
        if ($Mode -eq 'LocalMachine') {
            $results.Add('NTLMAuditing', (Test-MDINTLMAuditing -Detailed))
        } else {
            $results.Add('NTLMAuditing', (Test-MDINTLMAuditingGPO -Detailed -GpoNamePrefix $GpoNamePrefix))
        }
    }
    if (Use-MDIConfigName $Configuration 'ProcessorPerformance') {
        if ($Mode -eq 'LocalMachine') {
            $results.Add('ProcessorPerformance', (Test-MDIProcessorPerformance -Detailed))
        } else {
            $results.Add('ProcessorPerformance', (Test-MDIProcessorPerformanceGPO -Detailed -GpoNamePrefix $GpoNamePrefix))
        }
    }

    if ($Configuration -contains 'All') {
        $Configuration += $results.GetEnumerator() | Select-Object -ExpandProperty Name
    }
    $Configuration | Select-Object -Unique | Where-Object { $_ -ne 'All' } | ForEach-Object {
        [PSCustomObject]@{
            Configuration = $_
            Mode          = $Mode
            Status        = $results[$_].Status
            Details       = $results[$_].Details
        }
    }
}

function Test-MDIConfiguration {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory = $true)] [ValidateSet('Domain', 'LocalMachine')] [string] $Mode,
        [Parameter(Mandatory = $true)] [ValidateSet('AdfsAuditing', 'AdvancedAuditPolicyCAs', 'AdvancedAuditPolicyDCs',
            'CAAuditing', 'ConfigurationContainerAuditing', 'DomainObjectAuditing', 'NTLMAuditing', 'ProcessorPerformance', 'All')] [string[]] $Configuration,
        [Parameter(Mandatory = $false)] [string] $GpoNamePrefix
    )

    $results = if ($Mode -eq 'Domain') {
        Get-MDIConfiguration -Configuration $Configuration -Mode Domain -GpoNamePrefix $GpoNamePrefix
    } else {
        Get-MDIConfiguration -Configuration $Configuration -Mode LocalMachine
    }

    if ('All' -eq $Configuration) {
        @($results | Where-Object { $_.Status -eq $false }).Count -eq 0
    } else {
        $results.Status
    }
}

function Set-MDIConfiguration {
    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateSet('Domain', 'LocalMachine')] [string] $Mode,
        [Parameter(Mandatory = $true, ValueFromPipelineByPropertyName = $true)] [ValidateSet('AdfsAuditing', 'AdvancedAuditPolicyCAs', 'AdvancedAuditPolicyDCs',
            'CAAuditing', 'ConfigurationContainerAuditing', 'DomainObjectAuditing', 'NTLMAuditing', 'ProcessorPerformance', 'All')] [string[]] $Configuration,
        [Parameter(Mandatory = $false)] [string] $GpoNamePrefix,
        [Parameter(Mandatory = $false)] [switch] $CreateGpoDisabled,
        [Parameter(Mandatory = $false)] [switch] $SkipGpoLink,
        [Parameter(Mandatory = $false)] [switch] $Force
    )

    Process {
        foreach ($config in $Configuration) {

            Write-Verbose ($strings['Configuration_Set'] -f $config)
            if (Use-MDIConfigName $config 'AdfsAuditing') { Set-MDIAdfsAuditing }

            if (Use-MDIConfigName $config 'AdvancedAuditPolicyCAs') {
                if ($Mode -eq 'LocalMachine') {
                    Set-MDIAdvancedAuditPolicyCAs
                } else {
                    Set-MDIAdvancedAuditPolicyCAsGPO -CreateGpoDisabled:$CreateGpoDisabled -SkipGpoLink:$SkipGpoLink -GpoNamePrefix $GpoNamePrefix
                }
            }

            if (Use-MDIConfigName $config 'AdvancedAuditPolicyDCs') {
                if ($Mode -eq 'LocalMachine') {
                    Set-MDIAdvancedAuditPolicyDCs
                } else {
                    Set-MDIAdvancedAuditPolicyDCsGPO -CreateGpoDisabled:$CreateGpoDisabled -SkipGpoLink:$SkipGpoLink -GpoNamePrefix $GpoNamePrefix
                }
            }

            if (Use-MDIConfigName $config 'CAAuditing') {
                if ($Mode -eq 'LocalMachine') {
                    Set-MDICAAuditing
                } else {
                    Set-MDICAAuditingGPO -CreateGpoDisabled:$CreateGpoDisabled -SkipGpoLink:$SkipGpoLink -GpoNamePrefix $GpoNamePrefix
                }
            }

            if (Use-MDIConfigName $config 'ConfigurationContainerAuditing') { Set-MDIConfigurationContainerAuditing -Force:$Force }

            if (Use-MDIConfigName $config 'DomainObjectAuditing') { Set-MDIDomainObjectAuditing }

            if (Use-MDIConfigName $config 'NTLMAuditing') {
                if ($Mode -eq 'LocalMachine') {
                    Set-MDINTLMAuditing
                } else {
                    Set-MDINTLMAuditingGPO -CreateGpoDisabled:$CreateGpoDisabled -SkipGpoLink:$SkipGpoLink -GpoNamePrefix $GpoNamePrefix
                }
            }

            if (Use-MDIConfigName $config 'ProcessorPerformance') {
                if ($Mode -eq 'LocalMachine') {
                    Set-MDIProcessorPerformance
                } else {
                    Set-MDIProcessorPerformanceGPO -CreateGpoDisabled:$CreateGpoDisabled -SkipGpoLink:$SkipGpoLink -GpoNamePrefix $GpoNamePrefix
                }
            }
        }
    }
}

function New-MDIConfigurationReport {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Path,
        [Parameter(Mandatory = $false)] [ValidateSet('Domain', 'LocalMachine')] [string] $Mode = 'Domain',
        [Parameter(Mandatory = $false)] [string] $GpoNamePrefix,
        [switch] $OpenHtmlReport
    )
    if (-not(Test-Path -Path $Path)) { [void](New-Item -Path $Path -ItemType Directory -Force -ErrorAction SilentlyContinue) }

    $reportTarget = if ($Mode -eq 'Domain') { $env:USERDNSDOMAIN } else { '{0}.{1}' -f $env:COMPUTERNAME, $env:USERDNSDOMAIN }
    $configurations = Get-MDIConfiguration -Configuration All -Mode $Mode -GpoNamePrefix $GpoNamePrefix

    $jsonReportFile = Resolve-MDIPath -Path (
        Join-Path -Path $Path -ChildPath ('MDI-configuration-report-{0}.json' -f $reportTarget))
    $htmlReportFile = Resolve-MDIPath -Path (
        Join-Path -Path $Path -ChildPath ('MDI-configuration-report-{0}.html' -f $reportTarget))

    $css = @'
<style>
body { font-family: Arial, sans-serif, 'Open Sans'; }
table { border-collapse: collapse; }
td, th { border: 1px solid #aeb0b5; padding: 5px; text-align: left; vertical-align: middle; }
tr:nth-child(even) { background-color: #f2f2f2; }
th { padding: 8px; text-align: left; background-color: #e4e2e0; color: #212121; }
.red {background-color: #cd2026; color: #ffffff; }
.green {background-color: #4aa564; color: #212121; }
ul { list-style: none; padding-left: 0.5em;}
</style>
'@

    $colors = @{$true = 'green'; $false = 'red' }
    $status = @{$true = $strings['DomainReport_StatusPass']; $false = $strings['DomainReport_StatusFail'] }
    $tblHeader = '<tr><th>{0}</th><th>{1}</th><th>{2}</th></tr>' -f $strings['DomainReport_Configuration'],
    $strings['DomainReport_Status'], $strings['DomainReport_CommandToFix']
    $tblContent = @($configurations | Sort-Object Configuration | ForEach-Object {
            $gpoPrefixIfUsed = if ([string]::IsNullOrEmpty($GpoNamePrefix)) { '' } else { " -GpoNamePrefix $GpoNamePrefix" }
            "<tr><td><a href='https://aka.ms/mdi/{0}'>{0}</a></td><td class='{1}'>{2}</td><td>{3}{0}{4}</td></tr>" -f `
                $_.Configuration, $colors[$_.Status], $status[$_.Status], 'Set-MDIConfiguration -Mode Domain -Configuration ', $gpoPrefixIfUsed
        }) -join [environment]::NewLine

    $htmlContent = @'
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>{0}</head><body>
<h2>{1}</h2>
{2}
<br/><br/>
<table>
{3}
{4}
</table>
<br/>
<hr>
<ul>
<li>{5}</li>
</ul>
<hr>
<br/>{6} <a href='{7}'>{7}</a><br/>
<br/>{8}
'@
 -f $css, ($strings['DomainReport_Title'] -f $reportTarget), $strings['DomainReport_Subtitle'],
    $tblHeader, $tblContent, $strings['DomainReport_NoteMessage'], $strings['DomainReport_DetailsMessage'],
    $jsonReportFile, ($strings['DomainReport_CreatedBy'] -f "<a href='https://aka.ms/mdi/psmodule'>DefenderForIdentity</a>")

    Write-Verbose ('{0}: {1}' -f $strings['DomainReport_JsonMessage'], $jsonReportFile)
    $configurations | ConvertTo-Json -Depth 5 | Format-Json | Out-File -FilePath $jsonReportFile -Force -Encoding utf8

    Write-Verbose ('{0}: {1}' -f $strings['DomainReport_HtmlMessage'], $htmlReportFile)
    $htmlContent | Out-File -FilePath $htmlReportFile -Force -Encoding utf8

    $reportPath = (Resolve-Path -Path $htmlReportFile).Path
    if ($OpenHtmlReport) { Invoke-Item -Path $reportPath }
}

#endregion
# SIG # Begin signature block
# MIInwgYJKoZIhvcNAQcCoIInszCCJ68CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCa0by0+BLeQfLB
# ShK29MiB+JJ3IBsqOIPE1R1OgmvWqqCCDXYwggX0MIID3KADAgECAhMzAAADrzBA
# DkyjTQVBAAAAAAOvMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p
# bmcgUENBIDIwMTEwHhcNMjMxMTE2MTkwOTAwWhcNMjQxMTE0MTkwOTAwWjB0MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB
# AQDOS8s1ra6f0YGtg0OhEaQa/t3Q+q1MEHhWJhqQVuO5amYXQpy8MDPNoJYk+FWA
# hePP5LxwcSge5aen+f5Q6WNPd6EDxGzotvVpNi5ve0H97S3F7C/axDfKxyNh21MG
# 0W8Sb0vxi/vorcLHOL9i+t2D6yvvDzLlEefUCbQV/zGCBjXGlYJcUj6RAzXyeNAN
# xSpKXAGd7Fh+ocGHPPphcD9LQTOJgG7Y7aYztHqBLJiQQ4eAgZNU4ac6+8LnEGAL
# go1ydC5BJEuJQjYKbNTy959HrKSu7LO3Ws0w8jw6pYdC1IMpdTkk2puTgY2PDNzB
# tLM4evG7FYer3WX+8t1UMYNTAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE
# AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQURxxxNPIEPGSO8kqz+bgCAQWGXsEw
# RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW
# MBQGA1UEBRMNMjMwMDEyKzUwMTgyNjAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci
# tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG
# CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0
# MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAISxFt/zR2frTFPB45Yd
# mhZpB2nNJoOoi+qlgcTlnO4QwlYN1w/vYwbDy/oFJolD5r6FMJd0RGcgEM8q9TgQ
# 2OC7gQEmhweVJ7yuKJlQBH7P7Pg5RiqgV3cSonJ+OM4kFHbP3gPLiyzssSQdRuPY
# 1mIWoGg9i7Y4ZC8ST7WhpSyc0pns2XsUe1XsIjaUcGu7zd7gg97eCUiLRdVklPmp
# XobH9CEAWakRUGNICYN2AgjhRTC4j3KJfqMkU04R6Toyh4/Toswm1uoDcGr5laYn
# TfcX3u5WnJqJLhuPe8Uj9kGAOcyo0O1mNwDa+LhFEzB6CB32+wfJMumfr6degvLT
# e8x55urQLeTjimBQgS49BSUkhFN7ois3cZyNpnrMca5AZaC7pLI72vuqSsSlLalG
# OcZmPHZGYJqZ0BacN274OZ80Q8B11iNokns9Od348bMb5Z4fihxaBWebl8kWEi2O
# PvQImOAeq3nt7UWJBzJYLAGEpfasaA3ZQgIcEXdD+uwo6ymMzDY6UamFOfYqYWXk
# ntxDGu7ngD2ugKUuccYKJJRiiz+LAUcj90BVcSHRLQop9N8zoALr/1sJuwPrVAtx
# HNEgSW+AKBqIxYWM4Ev32l6agSUAezLMbq5f3d8x9qzT031jMDT+sUAoCw0M5wVt
# CUQcqINPuYjbS1WgJyZIiEkBMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq
# hkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5
# IDIwMTEwHhcNMTEwNzA4MjA1OTA5WhcNMjYwNzA4MjEwOTA5WjB+MQswCQYDVQQG
# EwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwG
# A1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSgwJgYDVQQDEx9NaWNyb3NvZnQg
# Q29kZSBTaWduaW5nIFBDQSAyMDExMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIIC
# CgKCAgEAq/D6chAcLq3YbqqCEE00uvK2WCGfQhsqa+laUKq4BjgaBEm6f8MMHt03
# a8YS2AvwOMKZBrDIOdUBFDFC04kNeWSHfpRgJGyvnkmc6Whe0t+bU7IKLMOv2akr
# rnoJr9eWWcpgGgXpZnboMlImEi/nqwhQz7NEt13YxC4Ddato88tt8zpcoRb0Rrrg
# OGSsbmQ1eKagYw8t00CT+OPeBw3VXHmlSSnnDb6gE3e+lD3v++MrWhAfTVYoonpy
# 4BI6t0le2O3tQ5GD2Xuye4Yb2T6xjF3oiU+EGvKhL1nkkDstrjNYxbc+/jLTswM9
# sbKvkjh+0p2ALPVOVpEhNSXDOW5kf1O6nA+tGSOEy/S6A4aN91/w0FK/jJSHvMAh
# dCVfGCi2zCcoOCWYOUo2z3yxkq4cI6epZuxhH2rhKEmdX4jiJV3TIUs+UsS1Vz8k
# A/DRelsv1SPjcF0PUUZ3s/gA4bysAoJf28AVs70b1FVL5zmhD+kjSbwYuER8ReTB
# w3J64HLnJN+/RpnF78IcV9uDjexNSTCnq47f7Fufr/zdsGbiwZeBe+3W7UvnSSmn
# Eyimp31ngOaKYnhfsi+E11ecXL93KCjx7W3DKI8sj0A3T8HhhUSJxAlMxdSlQy90
# lfdu+HggWCwTXWCVmj5PM4TasIgX3p5O9JawvEagbJjS4NaIjAsCAwEAAaOCAe0w
# ggHpMBAGCSsGAQQBgjcVAQQDAgEAMB0GA1UdDgQWBBRIbmTlUAXTgqoXNzcitW2o
# ynUClTAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMCAYYwDwYD
# VR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBRyLToCMZBDuRQFTuHqp8cx0SOJNDBa
# BgNVHR8EUzBRME+gTaBLhklodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20vcGtpL2Ny
# bC9wcm9kdWN0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3JsMF4GCCsG
# AQUFBwEBBFIwUDBOBggrBgEFBQcwAoZCaHR0cDovL3d3dy5taWNyb3NvZnQuY29t
# L3BraS9jZXJ0cy9NaWNSb29DZXJBdXQyMDExXzIwMTFfMDNfMjIuY3J0MIGfBgNV
# HSAEgZcwgZQwgZEGCSsGAQQBgjcuAzCBgzA/BggrBgEFBQcCARYzaHR0cDovL3d3
# dy5taWNyb3NvZnQuY29tL3BraW9wcy9kb2NzL3ByaW1hcnljcHMuaHRtMEAGCCsG
# AQUFBwICMDQeMiAdAEwAZQBnAGEAbABfAHAAbwBsAGkAYwB5AF8AcwB0AGEAdABl
# AG0AZQBuAHQALiAdMA0GCSqGSIb3DQEBCwUAA4ICAQBn8oalmOBUeRou09h0ZyKb
# C5YR4WOSmUKWfdJ5DJDBZV8uLD74w3LRbYP+vj/oCso7v0epo/Np22O/IjWll11l
# hJB9i0ZQVdgMknzSGksc8zxCi1LQsP1r4z4HLimb5j0bpdS1HXeUOeLpZMlEPXh6
# I/MTfaaQdION9MsmAkYqwooQu6SpBQyb7Wj6aC6VoCo/KmtYSWMfCWluWpiW5IP0
# wI/zRive/DvQvTXvbiWu5a8n7dDd8w6vmSiXmE0OPQvyCInWH8MyGOLwxS3OW560
# STkKxgrCxq2u5bLZ2xWIUUVYODJxJxp/sfQn+N4sOiBpmLJZiWhub6e3dMNABQam
# ASooPoI/E01mC8CzTfXhj38cbxV9Rad25UAqZaPDXVJihsMdYzaXht/a8/jyFqGa
# J+HNpZfQ7l1jQeNbB5yHPgZ3BtEGsXUfFL5hYbXw3MYbBL7fQccOKO7eZS/sl/ah
# XJbYANahRr1Z85elCUtIEJmAH9AAKcWxm6U/RXceNcbSoqKfenoi+kiVH6v7RyOA
# 9Z74v2u3S5fi63V4GuzqN5l5GEv/1rMjaHXmr/r8i+sLgOppO6/8MO0ETI7f33Vt
# Y5E90Z1WTk+/gFcioXgRMiF670EKsT/7qMykXcGhiJtXcVZOSEXAQsmbdlsKgEhr
# /Xmfwb1tbWrJUnMTDXpQzTGCGaIwghmeAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEINCWB+Ip8dUfgSroJkCwRkPk
# rsC07J+gw7qh2Clg4CzeMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEAg0TtQQ5HDnyG6z+tz0ehqP4diSk/8/WfWUde4MvdoLxxLo2Zr2PcVHxI
# pNAuh+Z74E9WPt2OZA+jDDYjt7ThdQvh4vqni21Th4aTyCeTbrCasIekEMLVW0tb
# 82Q/FcAyySAktvYtl81E36/7ampw1rRk1FwOC7LtAIli4tULtRn23DhJlaSjCx5P
# +sEMjpSiAvzSnkBF6/8R11XZJikwtu34SPGXeL783Q9KsDbnG1EoUY2lDBP+LuMV
# e4yr9H83iQoqCX9/Q89iZeUUdM8UMjkTNtUIHBPuQr2i6/JGI4trzYKAaPZFKPJG
# VIQvLDRMgGHjSWR0iBd9ZykFoAwNZqGCFywwghcoBgorBgEEAYI3AwMBMYIXGDCC
# FxQGCSqGSIb3DQEHAqCCFwUwghcBAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFZBgsq
# hkiG9w0BCRABBKCCAUgEggFEMIIBQAIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCA97exOaQX+q/wrHHHiO3oaOO0ITW5DmiSwcTjhtHcxNAIGZYM0bihF
# GBMyMDI0MDExNjE2Mjc1OS43NThaMASAAgH0oIHYpIHVMIHSMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMS0wKwYDVQQLEyRNaWNyb3NvZnQgSXJl
# bGFuZCBPcGVyYXRpb25zIExpbWl0ZWQxJjAkBgNVBAsTHVRoYWxlcyBUU1MgRVNO
# OkZDNDEtNEJENC1EMjIwMSUwIwYDVQQDExxNaWNyb3NvZnQgVGltZS1TdGFtcCBT
# ZXJ2aWNloIIRezCCBycwggUPoAMCAQICEzMAAAHimZmV8dzjIOsAAQAAAeIwDQYJ
# KoZIhvcNAQELBQAwfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hpbmd0b24x
# EDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlv
# bjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIwMTAwHhcNMjMx
# MDEyMTkwNzI1WhcNMjUwMTEwMTkwNzI1WjCB0jELMAkGA1UEBhMCVVMxEzARBgNV
# BAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jv
# c29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxhbmQgT3Bl
# cmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpGQzQxLTRC
# RDQtRDIyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2VydmljZTCC
# AiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBALVjtZhV+kFmb8cKQpg2mzis
# DlRI978Gb2amGvbAmCd04JVGeTe/QGzM8KbQrMDol7DC7jS03JkcrPsWi9WpVwsI
# ckRQ8AkX1idBG9HhyCspAavfuvz55khl7brPQx7H99UJbsE3wMmpmJasPWpgF05z
# ZlvpWQDULDcIYyl5lXI4HVZ5N6MSxWO8zwWr4r9xkMmUXs7ICxDJr5a39SSePAJR
# IyznaIc0WzZ6MFcTRzLLNyPBE4KrVv1LFd96FNxAzwnetSePg88EmRezr2T3HTFE
# lneJXyQYd6YQ7eCIc7yllWoY03CEg9ghorp9qUKcBUfFcS4XElf3GSERnlzJsK7s
# /ZGPU4daHT2jWGoYha2QCOmkgjOmBFCqQFFwFmsPrZj4eQszYxq4c4HqPnUu4hT4
# aqpvUZ3qIOXbdyU42pNL93cn0rPTTleOUsOQbgvlRdthFCBepxfb6nbsp3fcZaPB
# fTbtXVa8nLQuMCBqyfsebuqnbwj+lHQfqKpivpyd7KCWACoj78XUwYqy1HyYnStT
# me4T9vK6u2O/KThfROeJHiSg44ymFj+34IcFEhPogaKvNNsTVm4QbqphCyknrwBy
# qorBCLH6bllRtJMJwmu7GRdTQsIx2HMKqphEtpSm1z3ufASdPrgPhsQIRFkHZGui
# hL1Jjj4Lu3CbAmha0lOrAgMBAAGjggFJMIIBRTAdBgNVHQ4EFgQURIQOEdq+7Qds
# lptJiCRNpXgJ2gUwHwYDVR0jBBgwFoAUn6cVXQBeYl2D9OXSZacbUzUZ6XIwXwYD
# VR0fBFgwVjBUoFKgUIZOaHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9j
# cmwvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3JsMGwG
# CCsGAQUFBwEBBGAwXjBcBggrBgEFBQcwAoZQaHR0cDovL3d3dy5taWNyb3NvZnQu
# Y29tL3BraW9wcy9jZXJ0cy9NaWNyb3NvZnQlMjBUaW1lLVN0YW1wJTIwUENBJTIw
# MjAxMCgxKS5jcnQwDAYDVR0TAQH/BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcD
# CDAOBgNVHQ8BAf8EBAMCB4AwDQYJKoZIhvcNAQELBQADggIBAORURDGrVRTbnulf
# sg2cTsyyh7YXvhVU7NZMkITAQYsFEPVgvSviCylr5ap3ka76Yz0t/6lxuczI6w7t
# Xq8n4WxUUgcj5wAhnNorhnD8ljYqbck37fggYK3+wEwLhP1PGC5tvXK0xYomU1nU
# +lXOy9ZRnShI/HZdFrw2srgtsbWow9OMuADS5lg7okrXa2daCOGnxuaD1IO+65E7
# qv2O0W0sGj7AWdOjNdpexPrspL2KEcOMeJVmkk/O0ganhFzzHAnWjtNWneU11WQ6
# Bxv8OpN1fY9wzQoiycgvOOJM93od55EGeXxfF8bofLVlUE3zIikoSed+8s61NDP+
# x9RMya2mwK/Ys1xdvDlZTHndIKssfmu3vu/a+BFf2uIoycVTvBQpv/drRJD68eo4
# 01mkCRFkmy/+BmQlRrx2rapqAu5k0Nev+iUdBUKmX/iOaKZ75vuQg7hCiBA5xIm5
# ZIXDSlX47wwFar3/BgTwntMq9ra6QRAeS/o/uYWkmvqvE8Aq38QmKgTiBnWSS/uV
# PcaHEyArnyFh5G+qeCGmL44MfEnFEhxc3saPmXhe6MhSgCIGJUZDA7336nQD8fn4
# y6534Lel+LuT5F5bFt0mLwd+H5GxGzObZmm/c3pEWtHv1ug7dS/Dfrcd1sn2E4gk
# 4W1L1jdRBbK9xwkMmwY+CHZeMSvBMIIHcTCCBVmgAwIBAgITMwAAABXF52ueAptJ
# mQAAAAAAFTANBgkqhkiG9w0BAQsFADCBiDELMAkGA1UEBhMCVVMxEzARBgNVBAgT
# Cldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29m
# dCBDb3Jwb3JhdGlvbjEyMDAGA1UEAxMpTWljcm9zb2Z0IFJvb3QgQ2VydGlmaWNh
# dGUgQXV0aG9yaXR5IDIwMTAwHhcNMjEwOTMwMTgyMjI1WhcNMzAwOTMwMTgzMjI1
# WjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDCCAiIwDQYJKoZIhvcNAQEB
# BQADggIPADCCAgoCggIBAOThpkzntHIhC3miy9ckeb0O1YLT/e6cBwfSqWxOdcjK
# NVf2AX9sSuDivbk+F2Az/1xPx2b3lVNxWuJ+Slr+uDZnhUYjDLWNE893MsAQGOhg
# fWpSg0S3po5GawcU88V29YZQ3MFEyHFcUTE3oAo4bo3t1w/YJlN8OWECesSq/XJp
# rx2rrPY2vjUmZNqYO7oaezOtgFt+jBAcnVL+tuhiJdxqD89d9P6OU8/W7IVWTe/d
# vI2k45GPsjksUZzpcGkNyjYtcI4xyDUoveO0hyTD4MmPfrVUj9z6BVWYbWg7mka9
# 7aSueik3rMvrg0XnRm7KMtXAhjBcTyziYrLNueKNiOSWrAFKu75xqRdbZ2De+JKR
# Hh09/SDPc31BmkZ1zcRfNN0Sidb9pSB9fvzZnkXftnIv231fgLrbqn427DZM9itu
# qBJR6L8FA6PRc6ZNN3SUHDSCD/AQ8rdHGO2n6Jl8P0zbr17C89XYcz1DTsEzOUyO
# ArxCaC4Q6oRRRuLRvWoYWmEBc8pnol7XKHYC4jMYctenIPDC+hIK12NvDMk2ZItb
# oKaDIV1fMHSRlJTYuVD5C4lh8zYGNRiER9vcG9H9stQcxWv2XFJRXRLbJbqvUAV6
# bMURHXLvjflSxIUXk8A8FdsaN8cIFRg/eKtFtvUeh17aj54WcmnGrnu3tz5q4i6t
# AgMBAAGjggHdMIIB2TASBgkrBgEEAYI3FQEEBQIDAQABMCMGCSsGAQQBgjcVAgQW
# BBQqp1L+ZMSavoKRPEY1Kc8Q/y8E7jAdBgNVHQ4EFgQUn6cVXQBeYl2D9OXSZacb
# UzUZ6XIwXAYDVR0gBFUwUzBRBgwrBgEEAYI3TIN9AQEwQTA/BggrBgEFBQcCARYz
# aHR0cDovL3d3dy5taWNyb3NvZnQuY29tL3BraW9wcy9Eb2NzL1JlcG9zaXRvcnku
# aHRtMBMGA1UdJQQMMAoGCCsGAQUFBwMIMBkGCSsGAQQBgjcUAgQMHgoAUwB1AGIA
# QwBBMAsGA1UdDwQEAwIBhjAPBgNVHRMBAf8EBTADAQH/MB8GA1UdIwQYMBaAFNX2
# VsuP6KJcYmjRPZSQW9fOmhjEMFYGA1UdHwRPME0wS6BJoEeGRWh0dHA6Ly9jcmwu
# bWljcm9zb2Z0LmNvbS9wa2kvY3JsL3Byb2R1Y3RzL01pY1Jvb0NlckF1dF8yMDEw
# LTA2LTIzLmNybDBaBggrBgEFBQcBAQROMEwwSgYIKwYBBQUHMAKGPmh0dHA6Ly93
# d3cubWljcm9zb2Z0LmNvbS9wa2kvY2VydHMvTWljUm9vQ2VyQXV0XzIwMTAtMDYt
# MjMuY3J0MA0GCSqGSIb3DQEBCwUAA4ICAQCdVX38Kq3hLB9nATEkW+Geckv8qW/q
# XBS2Pk5HZHixBpOXPTEztTnXwnE2P9pkbHzQdTltuw8x5MKP+2zRoZQYIu7pZmc6
# U03dmLq2HnjYNi6cqYJWAAOwBb6J6Gngugnue99qb74py27YP0h1AdkY3m2CDPVt
# I1TkeFN1JFe53Z/zjj3G82jfZfakVqr3lbYoVSfQJL1AoL8ZthISEV09J+BAljis
# 9/kpicO8F7BUhUKz/AyeixmJ5/ALaoHCgRlCGVJ1ijbCHcNhcy4sa3tuPywJeBTp
# kbKpW99Jo3QMvOyRgNI95ko+ZjtPu4b6MhrZlvSP9pEB9s7GdP32THJvEKt1MMU0
# sHrYUP4KWN1APMdUbZ1jdEgssU5HLcEUBHG/ZPkkvnNtyo4JvbMBV0lUZNlz138e
# W0QBjloZkWsNn6Qo3GcZKCS6OEuabvshVGtqRRFHqfG3rsjoiV5PndLQTHa1V1QJ
# sWkBRH58oWFsc/4Ku+xBZj1p/cvBQUl+fpO+y/g75LcVv7TOPqUxUYS8vwLBgqJ7
# Fx0ViY1w/ue10CgaiQuPNtq6TPmb/wrpNPgkNWcr4A245oyZ1uEi6vAnQj0llOZ0
# dFtq0Z4+7X6gMTN9vMvpe784cETRkPHIqzqKOghif9lwY1NNje6CbaUFEMFxBmoQ
# tB1VM1izoXBm8qGCAtcwggJAAgEBMIIBAKGB2KSB1TCB0jELMAkGA1UEBhMCVVMx
# EzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoT
# FU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEtMCsGA1UECxMkTWljcm9zb2Z0IElyZWxh
# bmQgT3BlcmF0aW9ucyBMaW1pdGVkMSYwJAYDVQQLEx1UaGFsZXMgVFNTIEVTTjpG
# QzQxLTRCRDQtRDIyMDElMCMGA1UEAxMcTWljcm9zb2Z0IFRpbWUtU3RhbXAgU2Vy
# dmljZaIjCgEBMAcGBSsOAwIaAxUAFpuZafp0bnpJdIhfiB1d8pTohm+ggYMwgYCk
# fjB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMH
# UmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQD
# Ex1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDANBgkqhkiG9w0BAQUFAAIF
# AOlQoEIwIhgPMjAyNDAxMTYxNDI3MTRaGA8yMDI0MDExNzE0MjcxNFowdzA9Bgor
# BgEEAYRZCgQBMS8wLTAKAgUA6VCgQgIBADAKAgEAAgIKLAIB/zAHAgEAAgIRyDAK
# AgUA6VHxwgIBADA2BgorBgEEAYRZCgQCMSgwJjAMBgorBgEEAYRZCgMCoAowCAIB
# AAIDB6EgoQowCAIBAAIDAYagMA0GCSqGSIb3DQEBBQUAA4GBAIBj2sMdZAA/09lo
# V3xOBfIdoW9QMJK5wCyKpO60cg3tVApa1jRDYSAxVB0t2L8w5Tkipzjvulxeqbwu
# R96pBGWzY9BhXrYt0hMJbpUPH5ZCjKrNCRgvtKy0ULFDynD6R25sMxDrRsxF6Kwk
# o43UOuft5pZ0EM9hJ3awZRxlvjflMYIEDTCCBAkCAQEwgZMwfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTACEzMAAAHimZmV8dzjIOsAAQAAAeIwDQYJYIZIAWUD
# BAIBBQCgggFKMBoGCSqGSIb3DQEJAzENBgsqhkiG9w0BCRABBDAvBgkqhkiG9w0B
# CQQxIgQgEYgQ2jMSCMcTNgZoZNYhyn0XEJWjb58YEtWxBC54GyswgfoGCyqGSIb3
# DQEJEAIvMYHqMIHnMIHkMIG9BCAriSpKEP0muMbBUETODoL4d5LU6I/bjucIZkOJ
# CI9//zCBmDCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9u
# MRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRp
# b24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1wIFBDQSAyMDEwAhMzAAAB
# 4pmZlfHc4yDrAAEAAAHiMCIEIAaqhv1DLSsM/NeHlcOS4Olpo7LvVZMWkAvWmOTb
# aw50MA0GCSqGSIb3DQEBCwUABIICAHd/iL9+cVOc4YJcKLJx836AHpyoH+VX0IAW
# BSGI1Pu/P1RMFWq/Pxcb3FrpUJmuVJCaDoEGSt+WhLl7WLUOGk8aBtcqPIjPNaUe
# TEeTPiAZOYvDvvLvRk5+qP54S1DPz2T5fxJY3kdzUeQ3bsF1jV5mFBpQDShrCnXj
# riuIajReflIKsv6t4WuayfvlZvB5kUjKrvE9z6zFWkFUuOSBIsFhK1flHXSQqd0G
# JaJMInulSIq9/8IOGezGxr1KPu/4aVbcrPHO1b96EKkCAZDUsO+3y3rIB7jTkIlY
# Hs7VnwuWe1Y+62PFpW1JVYpqHi7yLSSgOThlYeE5otRWkaNUH3CuwSTyyXnezlwy
# pyDT45haLSdADtsB7nQs/8DvkHI0o1hDf+FZ2pPPdWQ92iVJhIWgyfgZQLkCDlaX
# nP3Yu8DwAFodzpXHfWW1TvOQp1Jlc5y5QfMA5YDJdoqjwR7IMazRPv8sQDsZI18Y
# UrOxaRrmqoaxKrT+7ZL3cRIQsrcxM+E3jn/aWjt0i/B5J6DKhx945+z+calc9OZq
# pUv/CRju1WZKzvFc21D1A179qRgrNsUFEvI0j3evUGLVsZ44dyphuf+e+iI+UGst
# oAwD5IG99QuQTirt8wMPbCaFjbADKSillcfg2gsM+unFbV0H8g561Hn0oo1GkO/H
# tyUKUW5J
# SIG # End signature block