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'
    }
    pdcE                           = [string]((Get-ADDomain -ErrorAction SilentlyContinue).PDCEmulator)
}

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

function New-MDIPassword {
    $guid = (New-Guid).guid.split('-')
    $guid[0] += [char](Get-Random -min 65 -max 90)
    $guid[1] += [char](Get-Random -min 65 -max 90)
    ConvertTo-SecureString ($guid -join '-') -AsPlainText -Force
}

#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 Service account configuration functions

function New-MDIDSA {
    [CmdletBinding(DefaultParameterSetName = "gmsaAccount")]
    Param(
        [parameter(Mandatory = $true, ParameterSetName = "gmsaAccount", Position = 1)]
        [parameter(Mandatory = $true, ParameterSetName = "standardAccount", Position = 1)]
        [ValidateLength(1, 16)]
        [string]$Identity,
        [parameter(Mandatory = $true, ParameterSetName = "gmsaAccount")]
        [ValidateLength(1, 28)]
        [string]$GmsaGroupName,
        [parameter(Mandatory = $false, ParameterSetName = "gmsaAccount")]
        [parameter(Mandatory = $false, ParameterSetName = "standardAccount")]
        [string]$BaseDn,
        [parameter(Mandatory = $false, ParameterSetName = "standardAccount")]
        [switch]$ForceStandardAccount,
        [parameter(Mandatory = $false, ParameterSetName = "gmsaAccount")]
        [parameter(Mandatory = $false, ParameterSetName = "standardAccount")]
        [string]$Server
    )

    $Server = Get-MDIDC -Server $Server
    $domain = Get-ADDomain -Server $Server
    $returnVal = $false
    if ($Identity -match '.*\$') {
        $Identity = $Identity.replace('$', '')
    }
    try {
        $adObjectParams = @{
            LDAPFilter = "(objectSid=$(($domain).domainSid)-519)"
        }; if (-not [string]::IsNullOrEmpty($server)) { $adObjectParams.Add("Server",$server) }
        $id = [Security.Principal.WindowsIdentity]::GetCurrent()
        $groups = $id.Groups | ForEach-Object { $_.Translate([Security.Principal.NTAccount]) }
        if ([bool]$(($domain).parentdomain)) {
            $forestSid = (Get-ADDomain -Server $($domain.parentdomain)).domainsid.value
            $eaGroupName = '{0}\{1}' -f (Get-ADDomain -Server $($domain.parentdomain)).netbiosname,
                (Get-ADObject -LDAPFilter "(objectSid=$forestSid-519)" -Server $($domain.parentdomain)).name
        } else {
            $eaGroupName = '{0}\{1}' -f ($domain.netbiosname), (Get-ADObject @adObjectParams).name
        }
    } catch {
        Write-Warning -Message $strings['DSA_EnterpriseAdminGroupNotFound']
    }

    if ([string]::IsNullOrEmpty($baseDn)) {
        $baseDn = "{0},{1}" -f "CN=Users", $($domain.DistinguishedName)
        $adObjectParams.LDAPFilter = "(distinguishedName=$baseDn)"
        try {
            $bdnCheck = Get-ADObject @adObjectParams
        } catch {
            $baseDn = $($domain.distinguishedname)
        }
    }
    if ($forceStandardAccount) {
        $adUserParams = @{
            Identity = $Identity
        }; if (-not [string]::IsNullOrEmpty($server)) { $adUserParams.Add("Server",$server) }
        try {
            Get-Aduser @adUserParams -ErrorAction silentlycontinue
        } catch {
            try {
                $securePassword = New-MDIPassword
                $adUserParams.Add("Name", $Identity)
                $adUserParams.Add("AccountPassword", $securePassword)
                $adUserParams.Add("SamAccountName", $Identity)
                $adUserParams.Add("Path", $baseDn)
                $adUserParams.Add("Description", "This account runs the MDI service")
                $adUserParams.Add("Enabled", $true)
                $adUserParams.Remove("Identity")
                $serviceAccount = New-ADUser @adUserParams -PassThru
                $returnVal = $true
            } catch {
                Write-Error $strings['DSA_CannotCreateAccount']
            }
        }
    } else {
        if ($eaGroupName) {
            if ($eaGroupName -in $groups) {
                if (-not (Get-KdsRootKey)) {
                    try {
                        Add-KdsRootKey -EffectiveTime ((Get-Date).AddHours(-10))
                        $strings['DSA_CreatedKDSRootKey']
                    } catch {
                        throw $strings['DSA_CannotCreateKDSRootKey']
                    }
                } else {
                    Write-Verbose $strings['DSA_FoundKDSRootKey']
                }
            }
        }

        $domainDCs = "$(($domain.domainsid).value)-516"
        try {
            $adObjectParams.LDAPFilter = "(objectSid=$domainDCs)"
            $domainDCsGroupName = (Get-ADGroup @adObjectParams).Name
        } catch {
            Write-Warning $strings['DSA_CannotFindDomainControllersGroup']
        }
        $gmsaGroupParams = @{
            Name = $GmsaGroupName
            SamAccountName = $GmsaGroupName
            Path = $baseDn
            GroupScope = "Universal"
            Description = $strings['DSA_GroupDescription'] -f $Identity
        }; if (-not [string]::IsNullOrEmpty($server)) { $gmsaGroupParams.Add("Server",$server) }
        try {
            $groupExists = [bool](Get-ADGroup -Identity $GmsaGroupName -ErrorAction SilentlyContinue)
        } catch {
            $groupExists = $false
        }
        if (-not $groupExists) {
            try {
                New-ADGroup @gmsaGroupParams
                Write-Verbose $strings['DSA_CreatedGMSAGroup']
            } catch {
                Write-Error $strings['DSA_CannotCreateGMSAGroup']
            }
        }
        $newAdServiceAccountParams = @{
            Name = $Identity
            DNSHostName = $($domain.DNSRoot)
            PrincipalsAllowedToRetrieveManagedPassword = [string[]]$GmsaGroupName
            SamAccountName = $gmsaAccountName
        }; if (-not [string]::IsNullOrEmpty($server)) { $newAdServiceAccountParams.Add("Server",$server) }
        if (-not [string]::IsNullOrEmpty($domainDCsGroupName)) {
            $newAdServiceAccountParams.PrincipalsAllowedToRetrieveManagedPassword += $domainDCsGroupName
        }
        try {
            $serviceAccount = New-ADServiceAccount @newAdServiceAccountParams -PassThru
            $returnVal = $true
        } catch {
            try {
                $newAdServiceAccountParams.Add("Path",$baseDn)
                $serviceAccount = New-ADServiceAccount @newAdServiceAccountParams -PassThru
                $returnVal = $true
            } catch {
                throw
            }
            write-error $strings['DSA_CannotCreateGMSAAccount']
        }
    }
    Set-MDIDeletedObjectsContainerPermission -Identity ("{0}\{1}" -f $domain.netbiosname, $serviceAccount.SamAccountName) -Server $Server | out-null
    return $returnVal
}

#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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $returnVal = $null
    Write-Verbose -Message ($strings['GPO_Create'] -f $Name)
    $Server = Get-MDIDC -Server $Server
    $maxWaitTime = (Get-Date).AddSeconds(3)
    $successGpo = $false
    $gpoParams = @{
        Name = $Name
    }; if (-not [string]::IsNullOrEmpty($server)) { $gpoParams.Add("Server",$server) }
    try {
        $gpoCreate = New-GPO @gpoParams -ErrorAction silentlycontinue
        $successGpo = $true
    } catch {
        $successGpo = $false
    }

    if ($successGpo) {
        do {
            Start-Sleep -Milliseconds 500
            $gpo = Get-MDIGPO @gpoParams
        } while (-not (($gpo) -or ($maxWaitTime -lt (Get-Date))))

        if ($gpo) {
            $gPCFileSysPath = $gpo.gPCFileSysPath
            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
            }
            $returnVal = $gpo | Add-Member -MemberType NoteProperty -Name gPCFileSysPath -Value $gPCFileSysPath -PassThru -Force
        } else {
            $returnVal = $null
        }
    } else {
        $returnVal = $null
    }
    return $returnVal
}

function Get-MDIGPO {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [string] $Name,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $maxWaitTime = (Get-Date).AddSeconds(3)
    $gpoParams = @{
        Name = $Name
    }; if (-not [string]::IsNullOrEmpty($server)) { $gpoParams.Add("Server",$server) }
    try {
        $gpo = Get-Gpo @gpoParams -ErrorAction SilentlyContinue
    } catch {
        $gpo = $null
    }

    if ($null -eq $gpo) {
        Write-Verbose -Message ("'{0}' - {1}" -f $Name, $strings['GPO_NotFound'])
    } else {
        Start-Sleep -Milliseconds 500
        if ($Server) {
            try {
                $gPCFileSysPath = (Get-ADObject -Identity $gpo.Path -Properties gPCFileSysPath -Server $Server).gPCFileSysPath
            } catch {
                do {
                    try {
                        Start-Sleep -Milliseconds 500
                        $gPCFileSysPath = (Get-ADObject -Identity $gpo.Path -Properties gPCFileSysPath -Server $Server).gPCFileSysPath
                    } catch {
                    }
                } while (!(($gPCFileSysPath) -or ($maxWaitTime -lt (Get-Date))))
            }

            $gPCArray = $gPCFileSysPath.split('\')
            $gPCArray[2] = $Server
            $gPCFileSysPath = $gPCArray -join '\'
        } else {
            $gPCFileSysPath = (Get-ADObject -Identity $gpo.Path -Properties gPCFileSysPath).gPCFileSysPath
        }
        do {
            Start-Sleep -Milliseconds 500
        } while (-not ((Test-Path -Path $gPCFileSysPath) -or ($maxWaitTime -lt (Get-Date))))
        $gpo | Add-Member -MemberType NoteProperty -Name gPCFileSysPath -Value $gPCFileSysPath -Force
    }
    return $gpo
}

function Get-MDIGPOLink {
    param(
        [guid] $Guid,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    Write-Verbose -Message $strings['GPO_GetLinks']
    $Server = Get-MDIDC -Server $Server
    $gpoReportParams = @{
        Guid = $Guid
        ReportType = "Xml"
    }; if (-not [string]::IsNullOrEmpty($server)) { $gpoReportParams.Add("Server",$server) }
    $xml = [xml](Get-GPOReport @gpoReportParams)
    @($xml.GPO.LinksTo)
}

function Test-MDIGPOLink {
    [CmdletBinding()]
    param(
        [guid] $Guid,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $return = $false
    $gpoParams = @{
        Guid = $Guid
    }; if (-not [string]::IsNullOrEmpty($server)) { $gpoParams.Add("Server",$server) }
    $enabledLinks = @(Get-MDIGPOLink @gpoParams | 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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    Write-Verbose -Message $strings['GPO_SetLink']
    $gpLink = @{
        Guid        = $Guid
        LinkEnabled = $LinkEnabled
        Enforced    = $Enforced
        Target      = $Target
    }; if (-not [string]::IsNullOrEmpty($server)) { $gpLink.Add("Server",$server) }
    $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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoParams = @{
        Guid = $Guid
    }; if (-not [string]::IsNullOrEmpty($server)) { $gpoParams.Add("Server",$server) }
    (Get-GPO @gpoParams).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',
        [Parameter(Mandatory = $false)] [string] $Server
    )
    Write-Verbose -Message $strings['GPO_UpdateVersion']
    $Server = Get-MDIDC -Server $Server
    if ($Mode -match 'ALL|DS') {
        $Replace =  @{versionNumber = $Version }
        $gpoAdObjectPath = 'CN={0},{1}' -f "{$Guid}", (Get-MDIAdPath 'CN=Policies,CN=System,{0}')
        $adObjectParams = @{
            Identity = $gpoAdObjectPath
            Replace = $Replace
        }; if (-not [string]::IsNullOrEmpty($server)) { $adObjectParams.Add("Server",$server) }
        Set-ADObject @adObjectParams | Out-Null
    }
    if ($Mode -match 'ALL|Sysvol') {
        if ($Server) {
            $filePath = '\\{0}\SYSVOL\{1}\Policies\{2}\GPT.INI' -f $Server, $env:USERDNSDOMAIN, "{$guid}"
        } else {
            $filePath = '\\{0}\SYSVOL\{0}\Policies\{1}\GPT.INI' -f $env:USERDNSDOMAIN, "{$guid}"
        }
        $newContent = (([system.io.file]::ReadAllLines($filePath)) -join [environment]::NewLine) -replace 'Version=\d+', ('Version={0}' -f $version)
        [System.io.file]::WriteAllLines($filePath, $newContent, (New-Object System.Text.ASCIIEncoding))
    }
}

function Get-MDIGPOMachineExtension {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [guid] $Guid,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    Write-Verbose -Message $strings['GPO_GetExtension']
    $Server = Get-MDIDC -Server $Server
    $gpoAdObjectPath = 'CN={0},{1}' -f "{$Guid}", (Get-MDIAdPath 'CN=Policies,CN=System,{0}')
    $adObjectParams = @{
        Identity = $gpoAdObjectPath
        Properties = @("gPCMachineExtensionNames", "VersionNumber")
    }; if (-not [string]::IsNullOrEmpty($server)) { $adObjectParams.Add("Server",$server) }
    Get-ADObject @adObjectParams
}

function Set-MDIGPOMachineExtension {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] [guid] $Guid,
        [Parameter(Mandatory)] [string[]] $Extension,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    Write-Verbose -Message $strings['GPO_SetExtension']
    $return = $null
    $Server = Get-MDIDC -Server $Server
    $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}')
    $adObjectParams = @{
        Identity = $gpoAdObjectPath
        Replace = $Replace
    }; if (-not [string]::IsNullOrEmpty($server)) { $adObjectParams.Add("Server",$server) }
    try {
        $gpoUpdated = Set-ADObject @adObjectParams -PassThru
    } catch {
        Write-Verbose -Message $strings['GPO_UnableToSetExtension']
    }
    if ($gpoUpdated) {
        try {
            $gpoVersionParams = @{
                Guid = $Guid
            }; if (-not [string]::IsNullOrEmpty($server)) { $gpoVersionParams.Add("Server",$server) }
            $gpoComputerDSVersion = (Get-MDIGPOMachineVersion @gpoVersionParams).DSVersion
            if ($gpoComputerDSVersion -lt 2) { $gpoComputerDSVersion = 3 } else { $gpoComputerDSVersion++ }
            $setGpoMachineVersionParams = @{
                Guid = $Guid
                Version = $gpoComputerDSVersion
                Mode = "All"
            }; if (-not [string]::IsNullOrEmpty($server)) { $setGpoMachineVersionParams.Add("Server",$server) }
            Set-MDIGPOMachineVersion @setGpoMachineVersionParams
            $return = $gpoComputerDSVersion
        } catch {
            Write-Verbose -Message $strings['GPO_UnableToSetExtension']
        }
    }
    $return
}

function Test-MDIGPOEnabledAndLink {
    [CmdletBinding()]
    param(
        [Parameter(Mandatory)] $GPO,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $state = $false
    $testMdiGpoLinkParams = @{
        Guid = $GPO.Id.Guid
    }; if (-not [string]::IsNullOrEmpty($server)) { $testMdiGpoLinkParams.Add("Server",$Server) }
    if (-not ($GPO.GpoStatus -ne [Microsoft.GroupPolicy.GpoStatus]::AllSettingsDisabled)) {
        Write-Verbose -Message $strings['GPO_SettingsDisabled']
    } else {
        if (-not (Test-MDIGPOLink @testMdiGpoLinkParams)) {
            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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.ProcessorPerformance.GpoName -GpoNamePrefix $GpoNamePrefix
    $mdiGpoParams = @{
        Name = $gpoName
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDIGPO @mdiGpoParams
    $gpRegValParams = @{
        Guid = $gpo.Id.Guid
        Key = $settings.ProcessorPerformance.Key
    }; if (-not [string]::IsNullOrEmpty($server)) { $gpRegValParams.Add("Server",$Server) }
    if ($gpo) {
        $gpo | Select-Object -Property *,
        @{N = 'GPRegistryValue'; E = { Get-GPRegistryValue @gpRegValParams } }
    }
}

function Test-MDIProcessorPerformanceGPO {
    [CmdletBinding()]
    param(
        [switch] $Detailed,
        [string] $GpoNamePrefix,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.ProcessorPerformance.GpoName -GpoNamePrefix $GpoNamePrefix
    Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName)
    $processorPerfGpoParams = @{
        GpoNamePrefix = $GpoNamePrefix
    }; if (-not [string]::IsNullOrEmpty($server)) { $processorPerfGpoParams.Add("Server",$Server) }
    $state = $false
    $gpo = Get-MDIProcessorPerformanceGPO @processorPerfGpoParams
    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
        $mdiGpoTestLinkParams = @{
            GPO = $gpo
        }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoTestLinkParams.Add("Server",$Server) }
        if ($gpSetOk) {
            $state = Test-MDIGPOEnabledAndLink @mdiGpoTestLinkParams
        } 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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.ProcessorPerformance.GpoName -GpoNamePrefix $GpoNamePrefix
    $gpoParams = @{
        GpoNamePrefix = $GpoNamePrefix
    }; if (-not [string]::IsNullOrEmpty($server)) { $gpoParams.Add("Server",$Server) }

    $gpo = Get-MDIProcessorPerformanceGPO @gpoParams
    if ($null -eq $gpo) {
        $gpoParams.Add("CreateGpoDisabled",$CreateGpoDisabled)
        $gpoParams.Add("Name",$gpoName)
        $gpoParams.Remove("GpoNamePrefix")
        $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
        }; if (-not [string]::IsNullOrEmpty($server)) { $gppParams.Add("Server",$Server) }
        $gpoUpdated = Set-GPRegistryValue @gppParams
        if (-not ($CreateGpoDisabled)) { $gpoUpdated.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled }
        Start-Sleep -Milliseconds 500
        $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
            }; if (-not [string]::IsNullOrEmpty($server)) { $gpLinkParams.Add("Server",$Server) }
            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']
    }
}

function Set-MDIDeletedObjectsContainerPermission {
    [CmdletBinding()]
    Param(
        [parameter(Mandatory = $True, Position = 1)]
        [string]$Identity,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $returnVal = $false
    $Server = Get-MDIDC -Server $Server
    try {
        $domain = (Get-ADDomain)
    } catch {
        throw
    }
    try {
        $parameters = @{
            ScriptBlock  = {
                Param ($param1, $param2)
                $deletedObjectsDN = "\\$server\CN=Deleted Objects,{0}" -f $param1
                $params = @("$deletedObjectsDN", '/takeOwnership')
                C:\Windows\System32\dsacls.exe $params
                $params = @("$deletedObjectsDN", '/G', "$($param2):LCRP")
                C:\Windows\System32\dsacls.exe $params
            }
            ArgumentList = $($domain.distinguishedName), $Identity
        }
        $command = "Invoke-command @parameters"
        $dsaclCheck = Invoke-Expression $command
        $returnVal = $true
    } catch {
        Write-Error $strings['DeletedObjectsPermissions_StatusFail']
    }
    return $returnVal
}

#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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.NTLMAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    $mdiGpoParams = @{
        Name = $gpoName
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDIGPO @mdiGpoParams
    if ($gpo) {
        $gpoReportParams = @{
            Guid = $gpo.Id
            ReportType = "Xml"
        }; if (-not [string]::IsNullOrEmpty($server)) { $gpoReportParams.Add("Server",$Server) }
        $report = [xml](Get-GPOReport @gpoReportParams)

        $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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.NTLMAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName)
    $mdiGpoParams = @{
        GpoNamePrefix = $GpoNamePrefix
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $state = $false
    $gpo = Get-MDINTLMAuditingGPO @mdiGpoParams

    if ($gpo) {
        $gpSetOk = @($gpo.RegistryValue | Where-Object {
                $_.Value -match $_.ExpectedValue
            }).Count -eq $settings.NTLMAuditing.RegistrySet.Count
        if ($gpSetOk) {
            $mdiGpoCheckParams = @{
                GPO = $gpo
            }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoCheckParams.Add("Server",$Server) }
            $state = Test-MDIGPOEnabledAndLink @mdiGpoCheckParams
        } 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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.NTLMAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    $mdiGpoParams = @{
        Name = $gpoName
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDIGPO @mdiGpoParams
    if ($null -eq $gpo) {
        $mdiGpoParams.Add("CreateGpoDisabled",$CreateGpoDisabled)
        $gpo = New-MDIGPO @mdiGpoParams
    }

    $filePath = '{0}\Machine\Microsoft\Windows NT\SecEdit' -f $gpo.gPCFileSysPath
    try {
        New-Item -Path $filePath -ItemType Directory -Force | Out-Null
    } catch {}

    $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
    }
    [System.Io.File]::WriteAllLines((Join-Path -Path $filePath -ChildPath 'GptTmpl.inf'), $fileContent, (New-Object System.Text.UnicodeEncoding))
    if (-not ($CreateGpoDisabled)) { $gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled }
    $gpo.MakeAclConsistent()
    $mdiGpoMachineExtensionParams = @{
        Guid = $gpo.Id.Guid
        Extension = @($settings.gpoExtensions['Security'], $settings.gpoExtensions['Computer Restricted Groups'])
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoMachineExtensionParams.Add("Server",$Server) }
    $gpoUpdated = Set-MDIGPOMachineExtension @mdiGpoMachineExtensionParams

    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
            }
            if ($Server) {
                Start-Sleep -Milliseconds 500
                $gpLinkParams.Add("Server", $Server)
            }
            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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyDCs.GpoName -GpoNamePrefix $GpoNamePrefix
    $mdiGpoParams = @{
        Name = $gpoName
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDIGPO @mdiGpoParams
    if ($gpo) {
        $gpoReportParams = @{
            Guid = $gpo.Id
            ReportType = "Xml"
        }; if (-not [string]::IsNullOrEmpty($server)) { $gpoReportParams.Add("Server",$Server) }
        $report = [xml](Get-GPOReport @gpoReportParams)
        $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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyDCs.GpoName -GpoNamePrefix $GpoNamePrefix
    Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName)
    $state = $false
    $mdiGpoParams = @{
        GpoNamePrefix = $GpoNamePrefix
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDIAdvancedAuditPolicyDCsGPO @mdiGpoParams
    if ($gpo) {
        $expectedSettings = @($settings.AdvancedAuditPolicyDCs.PolicySettings | ConvertFrom-Csv)
        $gpSetOk = @($gpo.AuditSettings | Where-Object {
                $_.SettingValue -match $_.ExpectedValue
            }).Count -eq $expectedSettings.Count

        if ($gpSetOk) {
            $mdiGpoCheckParams = @{
                GPO = $gpo
            }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoCheckParams.Add("Server",$Server) }
            $state = Test-MDIGPOEnabledAndLink @mdiGpoCheckParams
        } 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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyDCs.GpoName -GpoNamePrefix $GpoNamePrefix
    $mdiGpoParams = @{
        Name = $gpoName
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDIGPO @mdiGpoParams
    if ($null -eq $gpo) {
        $mdiGpoParams.Add("CreateGpoDisabled",$CreateGpoDisabled)
        $gpo = New-MDIGPO @mdiGpoParams
    }

    $filePath = '{0}\Machine\Microsoft\Windows NT\Audit' -f $gpo.gPCFileSysPath
    try {
        New-Item -Path $filePath -ItemType Directory -Force | Out-Null
    } catch {}
    [System.io.file]::WriteAllLines((Join-Path -Path $filePath -ChildPath 'audit.csv'), $settings.AdvancedAuditPolicyDCs.PolicySettings, (New-Object System.Text.ASCIIEncoding))

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

    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
            }
            if ($Server) {
                Start-Sleep -Milliseconds 500
                $gpLinkParams.Add("Server", $Server)
            }
            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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyCAs.GpoName -GpoNamePrefix $GpoNamePrefix
    $mdiGpoParams = @{
        Name = $gpoName
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDIGPO @mdiGpoParams
    if ($gpo) {
        $gpoReportParams = @{
            Guid = $gpo.Id
            ReportType = "Xml"
        }; if (-not [string]::IsNullOrEmpty($server)) { $gpoReportParams.Add("Server",$Server) }
        $report = [xml](Get-GPOReport @gpoReportParams)
        $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 {
            $mdiGpPermissionParams = @{
                Guid = $gpo.Id.Guid
                TargetType = "Group"
                TargetName = $_.Key
            }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpPermissionParams.Add("Server",$Server) }
            Get-GPPermission @mdiGpPermissionParams
        }
        $gpo = $gpo | Select-Object -Property *, @{N = 'AuditSettings'; E = { $AuditSettings } }, @{N = 'Delegation'; E = { $delegation } }
    }
    $gpo
}

function Test-MDIAdvancedAuditPolicyCAsGPO {
    [CmdletBinding()]
    param(
        [switch] $Detailed,
        [string] $GpoNamePrefix,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyCAs.GpoName -GpoNamePrefix $GpoNamePrefix
    Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName)
    $mdiGpoParams = @{
        GpoNamePrefix = $GpoNamePrefix
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $state = $false
    $gpo = Get-MDIAdvancedAuditPolicyCAsGPO @mdiGpoParams

    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 {
                $mdiGpoCheckParams = @{
                    GPO = $gpo
                }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoCheckParams.Add("Server",$Server) }
                $state = Test-MDIGPOEnabledAndLink @mdiGpoCheckParams
            }

        } 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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.AdvancedAuditPolicyCAs.GpoName -GpoNamePrefix $GpoNamePrefix
    $mdiGpoParams = @{
        Name = $gpoName
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDIGPO @mdiGpoParams
    if ($null -eq $gpo) {
        $mdiGpoParams.Add("CreateGpoDisabled",$CreateGpoDisabled)
        $gpo = New-MDIGPO @mdiGpoParams
    }
    if ($gpo) {
        $filePath = '{0}\Machine\Microsoft\Windows NT\Audit' -f $gpo.gPCFileSysPath
        try {
            Test-Path $filePath | Out-Null
        } catch {
            Start-Sleep 3
        }
        try {
            New-Item -Path $filePath -ItemType Directory -Force | Out-Null
            Start-Sleep -Milliseconds 500
        } catch {}
        [System.io.file]::WriteAllLines((Join-Path -Path $filePath -ChildPath 'audit.csv'), $settings.AdvancedAuditPolicyCAs.PolicySettings, (New-Object System.Text.ASCIIEncoding))

        if (-not ($CreateGpoDisabled)) { $gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled }
        $gpo.MakeAclConsistent()
        $mdiGpoMachineExtensionParams = @{
            Guid = $gpo.Id.Guid
            Extension = @($settings.gpoExtensions['Audit Policy Configuration'], $settings.gpoExtensions['Audit Configuration Extension'])
        }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoMachineExtensionParams.Add("Server",$Server) }
        $gpoUpdated = Set-MDIGPOMachineExtension @mdiGpoMachineExtensionParams
        Write-Verbose -Message $strings['GPO_SetDelegation']
        $settings.AdvancedAuditPolicyCAs.GPPermissions.GetEnumerator() | ForEach-Object {
            $TargetName = $($_.Key)
            $PermissionLevel = $($_.Value)
            $mdiGpPermissionParams = @{
                Guid = $gpo.Id.Guid
                TargetType = "Group"
                TargetName = "$TargetName"
                PermissionLevel = "$PermissionLevel"
                Replace = $true
            }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpPermissionParams.Add("Server",$Server) }
            Start-Sleep -Milliseconds 500
            Set-GPPermission @mdiGpPermissionParams | 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
                }
                if ($Server) {
                    Start-Sleep -Milliseconds 500
                    $gpLinkParams.Add("Server", $Server)
                }
                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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.CAAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    $mdiGpoParams = @{
        Name = $gpoName
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDIGPO @mdiGpoParams

    if ($gpo) {
        $params = @{
            Guid      = $gpo.Id
            Context   = 'Computer'
            Key       = 'HKEY_LOCAL_MACHINE\{0}' -f $settings.CAAuditing.GpoReg
            ValueName = ($settings.CAAuditing.GpoVal).Keys[0]
        }; if ($Server) { $params.Add("Server", $Server) }
        $GPPrefRegistryValue = Get-GPPrefRegistryValue @params -ErrorAction SilentlyContinue
        $delegation = $settings.CAAuditing.GPPermissions.GetEnumerator() | ForEach-Object {
            $mdiGpPermissionParams = @{
                Guid = $gpo.Id.Guid
                TargetType = "Group"
                TargetName = $_.Key
            }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpPermissionParams.Add("Server",$Server) }
            Get-GPPermission @mdiGpPermissionParams
        }
        $gpo = $gpo | Select-Object -Property *, @{N = 'GPPrefRegistryValue'; E = { $GPPrefRegistryValue } }, @{N = 'Delegation'; E = { $delegation } }
    }
    $gpo
}

function Test-MDICAAuditingGPO {
    [CmdletBinding()]
    param(
        [switch] $Detailed,
        [string] $GpoNamePrefix,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $gpoName = Get-MDIGPOName -Name $settings.CAAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName)
    $state = $false
    $mdiGpoParams = @{
        GpoNamePrefix = $GpoNamePrefix
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDICAAuditingGPO @mdiGpoParams
    $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 {
                $mdiGpoParams.Add("GPO", $gpo)
                $mdiGpoParams.Remove("GpoNamePrefix")
                $state = Test-MDIGPOEnabledAndLink @mdiGpoParams
            }

        } 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,
        [Parameter(Mandatory = $false)] [string] $Server
    )

    $gpoName = Get-MDIGPOName -Name $settings.CAAuditing.GpoName -GpoNamePrefix $GpoNamePrefix
    $Server = Get-MDIDC -Server $Server
    $mdiGpoParams = @{
        Name = $gpoName
    }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server",$Server) }
    $gpo = Get-MDIGPO @mdiGpoParams

    if ($null -eq $gpo) {
        $mdiGpoParams.Add("CreateGpoDisabled",$CreateGpoDisabled)
        $gpo = New-MDIGPO @mdiGpoParams
    }

    $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 ($Server) {
            Start-Sleep -Milliseconds 500
            $params.Add("Server", $Server)
        }
        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 {
        $TargetName = $($_.Key)
        $PermissionLevel = $($_.Value)
        $mdiGpPermissionParams = @{
                Guid = $gpo.Id.Guid
                TargetType = "Group"
                TargetName = "$TargetName"
                PermissionLevel = "$PermissionLevel"
                Replace = $true
        }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpPermissionParams.Add("Server",$Server) }
        Start-Sleep -Milliseconds 500
        Set-GPPermission @mdiGpPermissionParams | 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
            }
            if ($Server) {
                Start-Sleep -Milliseconds 500
                $gpLinkParams.Add("Server", $Server)
            }
            Set-MDIGPOLink @gpLinkParams
        }
    } else {
        Write-Warning $strings['GPO_UnableToSetExtension']
    }
}

#endregion

#region Domain helper functions

function Get-MDIDC {
    [CmdletBinding()]
    param (
        [Parameter(Mandatory = $false)] [string] $Server
    )
    & {
        $VerbosePreference = 'SilentlyContinue'
        $returnVal = $null
        if ($Server) {
            try {
                $returnVal = ((Resolve-DnsName $Server -Verbose:$false -ErrorAction Silentlycontinue) | Where-Object { $_.type -eq 'A' })[0].Name
            } catch {
                $returnVal = $Server
            }
            try {
                $socket = New-Object -TypeName System.Net.Sockets.TcpClient -ArgumentList ($returnVal, 9389)
                if (-not $socket.Connected) {
                    throw
                }
                $socket.Close()
            } catch {
                $returnVal = $settings.pdcE
            }
        } else {
            if (-not [string]::IsNullOrEmpty($settings.pdcE)) {
                $returnVal = $settings.pdcE
            }
        }
        return $returnVal
    }
}

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,
        [Parameter(Mandatory = $false)] [string] $Server
    )
    $Server = Get-MDIDC -Server $Server
    $return = @()
    $adAccountParams = @{
        Identity = $Identity
        Properties = "msDS-PrincipalName"
    }; if (-not [string]::IsNullOrEmpty($server)) { $adAccountParams.Add("Server",$Server) }
    $account = try {
        Get-ADUser @adAccountParams
    } catch {
        try {
            Get-ADServiceAccount @adAccountParams
        } catch { $null }
    }

    if ($null -eq $account) {
        $return += [PSCustomObject][ordered]@{
            Test    = 'AccountExists'
            Status  = $false
            Details = $strings['ServiceAccount_NotFound']
        }
    } 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']
            Write-Verbose -Message $strings['DSA_TestPasswordRetrieval']
            try {
                $pwdCheck = Test-ADServiceAccount -Identity $($account.samaccountname)
                $return += [PSCustomObject][ordered]@{
                    Test    = 'PasswordRetrieval'
                    Status  = $pwdCheck
                    Details = $null
                }
            } catch {

            }
        }

    }
    $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
            }
            $params.Add("UseBasicParsing", $true)
            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
    )
    DynamicParam {
        if ($Mode -eq 'Domain') {
            $paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
            $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
            $paramAttributes.Mandatory = $false
            $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $paramAttributesCollect.Add($paramAttributes)
            $dynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("GpoNamePrefix", [string], $paramAttributesCollect)
            $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
            $paramAttributes.Mandatory = $false
            $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $paramAttributesCollect.Add($paramAttributes)
            $dynParam3 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Server", [string], $paramAttributesCollect)
            $paramDictionary.Add("GpoNamePrefix", $dynParam1)
            $paramDictionary.Add("Server", $dynParam3)
            return $paramDictionary
        }
    }
    begin {
        if ($Mode -eq 'Domain') {
            $GpoNamePrefix = $PSBoundParameters['GpoNamePrefix']
            $Server = $PSBoundParameters['Server']
        }
    }
    process {
        $mdiParams = @{
            Detailed = $true
            GpoNamePrefix = $GpoNamePrefix
        }; if (-not [string]::IsNullOrEmpty($server)) { $mdiParams.Add("Server",$Server) }
        $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 @mdiParams))
            }
        }
        if (Use-MDIConfigName $Configuration 'AdvancedAuditPolicyDCs') {
            if ($Mode -eq 'LocalMachine') {
                $results.Add('AdvancedAuditPolicyDCs', (Test-MDIAdvancedAuditPolicyDCs -Detailed))
            } else {
                $results.Add('AdvancedAuditPolicyDCs', (Test-MDIAdvancedAuditPolicyDCsGPO @mdiParams))
            }
        }
        if (Use-MDIConfigName $Configuration 'CAAuditing') {
            if ($Mode -eq 'LocalMachine') {
                $results.Add('CAAuditing', (Test-MDICAAuditing -Detailed))
            } else {
                $results.Add('CAAuditing', (Test-MDICAAuditingGPO @mdiParams))
            }
        }
        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 @mdiParams))
            }
        }
        if (Use-MDIConfigName $Configuration 'ProcessorPerformance') {
            if ($Mode -eq 'LocalMachine') {
                $results.Add('ProcessorPerformance', (Test-MDIProcessorPerformance -Detailed))
            } else {
                $results.Add('ProcessorPerformance', (Test-MDIProcessorPerformanceGPO @mdiParams))
            }
        }
        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
    )
    DynamicParam {
        if ($Mode -eq 'Domain') {
            $paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
            $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
            $paramAttributes.Mandatory = $false
            $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $paramAttributesCollect.Add($paramAttributes)
            $dynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("GpoNamePrefix", [string], $paramAttributesCollect)
            $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
            $paramAttributes.Mandatory = $false
            $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $paramAttributesCollect.Add($paramAttributes)
            $dynParam3 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Server", [string], $paramAttributesCollect)
            $paramDictionary.Add("GpoNamePrefix", $dynParam1)
            $paramDictionary.Add("Server", $dynParam3)
            return $paramDictionary
        }
    }
    begin {
        if ($Mode -eq 'Domain') {
            $GpoNamePrefix = $PSBoundParameters['GpoNamePrefix']
            $ServiceAccountName = $PSBoundParameters['ServiceAccountName']
            $Server = $PSBoundParameters['Server']
        }
    }
    process {
        $results = if ($Mode -eq 'Domain') {
            $Server = Get-MDIDC -Server $Server
            $mdiParms = @{
                Configuration = $Configuration
                Mode = "Domain"
                GpoNamePrefix = $GpoNamePrefix
            }; if (-not [string]::IsNullOrEmpty($server)) { $mdiParms.Add("Server",$Server) }
            Get-MDIConfiguration @mdiParms
        } 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)] [switch] $Force
    )
    DynamicParam {
        if ($Mode -eq 'Domain') {
            $paramDictionary = New-Object -Type System.Management.Automation.RuntimeDefinedParameterDictionary
            $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
            $paramAttributes.Mandatory = $false
            $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $paramAttributesCollect.Add($paramAttributes)
            $dynParam1 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("GpoNamePrefix", [string], $paramAttributesCollect)
            $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
            $paramAttributes.Mandatory = $false
            $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $paramAttributesCollect.Add($paramAttributes)
            $dynParam2 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("CreateGpoDisabled", [switch], $paramAttributesCollect)
            $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
            $paramAttributes.Mandatory = $false
            $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $paramAttributesCollect.Add($paramAttributes)
            $dynParam3 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("SkipGpoLink", [switch], $paramAttributesCollect)
            $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute
            $paramAttributes.Mandatory = $false
            $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute]
            $paramAttributesCollect.Add($paramAttributes)
            $dynParam5 = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Server", [string], $paramAttributesCollect)
            $paramDictionary.Add("GpoNamePrefix", $dynParam1)
            $paramDictionary.Add("CreateGpoDisabled", $dynParam2)
            $paramDictionary.Add("SkipGpoLink", $dynParam3)
            $paramDictionary.Add("Server", $dynParam5)

            return $paramDictionary
        }
    }
    begin {
        if ($Mode -eq 'Domain') {
            $GpoNamePrefix = $PSBoundParameters['GpoNamePrefix']
            $CreateGpoDisabled = $PSBoundParameters['CreateGpoDisabled']
            $SkipGpoLink = $PSBoundParameters['SkipGpoLink']
            $ServiceAccountName = $PSBoundParameters['ServiceAccountName']
            $Server = $PSBoundParameters['Server']
        }
    }
    Process {
        $Server = Get-MDIDC -Server $Server
        foreach ($config in $Configuration) {
            $mdiParams = @{
                CreateGpoDisabled = $CreateGpoDisabled
                SkipGpoLink = $SkipGpoLink
                GpoNamePrefix = $GpoNamePrefix
            }; if (-not [string]::IsNullOrEmpty($server)) { $mdiParams.Add("Server",$Server) }
            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 @mdiParams
                }
            }

            if (Use-MDIConfigName $config 'AdvancedAuditPolicyDCs') {
                if ($Mode -eq 'LocalMachine') {
                    Set-MDIAdvancedAuditPolicyDCs
                } else {
                    Set-MDIAdvancedAuditPolicyDCsGPO @mdiParams
                }
            }

            if (Use-MDIConfigName $config 'CAAuditing') {
                if ($Mode -eq 'LocalMachine') {
                    Set-MDICAAuditing
                } else {
                    Set-MDICAAuditingGPO @mdiParams
                }
            }

            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 @mdiParams
                }
            }

            if (Use-MDIConfigName $config 'ProcessorPerformance') {
                if ($Mode -eq 'LocalMachine') {
                    Set-MDIProcessorPerformance
                } else {
                    Set-MDIProcessorPerformanceGPO @mdiParams
                }
            }
        }
    }
}

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
# MIIoKgYJKoZIhvcNAQcCoIIoGzCCKBcCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCD7T+bs0spUwLVc
# oH2dX68rhf33kjfYYqE//0xCIWrUSaCCDXYwggX0MIID3KADAgECAhMzAAADrzBA
# 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
# /Xmfwb1tbWrJUnMTDXpQzTGCGgowghoGAgEBMIGVMH4xCzAJBgNVBAYTAlVTMRMw
# EQYDVQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVN
# aWNyb3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNp
# Z25pbmcgUENBIDIwMTECEzMAAAOvMEAOTKNNBUEAAAAAA68wDQYJYIZIAWUDBAIB
# BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO
# MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIMFBhby3JVdm3XsiCxQu283v
# Ta+39XGjvjkwIEy3WUj8MEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A
# cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB
# BQAEggEALc3b1p3k9qWeMwmkmuyiK+Ta6Qc2kE633UqQDsbpim62lYJjDA7YnouU
# w/FHJq/fpe/QKDnQiHUbz8U+xLyHvYrAG2566+pOeZdmDZyg7wfCFRcVfofSK3gV
# 0XonORdH0lYOMzfnqdgKx3wlPQexu905i6iwEpP0Ary3Dgye2PUFi29ir8GK1JGH
# iggCtY+vfP5JO8H9t6C1MHZlCYC5VlbsQQNCsneg0rAVXVBgTGde6a24G3jYnkFZ
# 1uscIUEMg3xaF1GiX54uXY8O/uhs4KDvacciuR4N9UU0IhbZ9bZLODqWi+JTJckQ
# JD6RJ42+3kalNu8DRwreHaM9Ewz6iqGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC
# F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq
# hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl
# AwQCAQUABCANr5UcgKk8Hy/uuspJvK5s4LbAe73vSYe3cDumiRuvHwIGZr4GLjei
# GBMyMDI0MDgyMTEwMDAxNC41NjJaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV
# UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE
# ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l
# cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODYwMy0w
# NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg
# ghHqMIIHIDCCBQigAwIBAgITMwAAAfGzRfUn6MAW1gABAAAB8TANBgkqhkiG9w0B
# AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE
# BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD
# VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzEyMDYxODQ1
# NTVaFw0yNTAzMDUxODQ1NTVaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz
# aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv
# cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z
# MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046ODYwMy0wNUUwLUQ5NDcxJTAjBgNV
# BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB
# AQUAA4ICDwAwggIKAoICAQCxulCZttIf8X97rW9/J+Q4Vg9PiugB1ya1/DRxxLW2
# hwy4QgtU3j5fV75ZKa6XTTQhW5ClkGl6gp1nd5VBsx4Jb+oU4PsMA2foe8gP9bQN
# PVxIHMJu6TYcrrn39Hddet2xkdqUhzzySXaPFqFMk2VifEfj+HR6JheNs2LLzm8F
# DJm+pBddPDLag/R+APIWHyftq9itwM0WP5Z0dfQyI4WlVeUS+votsPbWm+RKsH4F
# QNhzb0t/D4iutcfCK3/LK+xLmS6dmAh7AMKuEUl8i2kdWBDRcc+JWa21SCefx5SP
# hJEFgYhdGPAop3G1l8T33cqrbLtcFJqww4TQiYiCkdysCcnIF0ZqSNAHcfI9SAv3
# gfkyxqQNJJ3sTsg5GPRF95mqgbfQbkFnU17iYbRIPJqwgSLhyB833ZDgmzxbKmJm
# dDabbzS0yGhngHa6+gwVaOUqcHf9w6kwxMo+OqG3QZIcwd5wHECs5rAJZ6PIyFM7
# Ad2hRUFHRTi353I7V4xEgYGuZb6qFx6Pf44i7AjXbptUolDcVzYEdgLQSWiuFajS
# 6Xg3k7Cy8TiM5HPUK9LZInloTxuULSxJmJ7nTjUjOj5xwRmC7x2S/mxql8nvHSCN
# 1OED2/wECOot6MEe9bL3nzoKwO8TNlEStq5scd25GA0gMQO+qNXV/xTDOBTJ8zBc
# GQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFLy2xe59sCE0SjycqE5Erb4YrS1gMB8G
# A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG
# Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy
# MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w
# XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy
# dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG
# A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD
# AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQDhSEjSBFSCbJyl3U/QmFMW2eLPBknnlsfI
# D/7gTMvANEnhq08I9HHbbqiwqDEHSvARvKtL7j0znICYBbMrVSmvgDxU8jAGqMyi
# LoM80788So3+T6IZV//UZRJqBl4oM3bCIQgFGo0VTeQ6RzYL+t1zCUXmmpPmM4xc
# ScVFATXj5Tx7By4ShWUC7Vhm7picDiU5igGjuivRhxPvbpflbh/bsiE5tx5cuOJE
# JSG+uWcqByR7TC4cGvuavHSjk1iRXT/QjaOEeJoOnfesbOdvJrJdbm+leYLRI67N
# 3cd8B/suU21tRdgwOnTk2hOuZKs/kLwaX6NsAbUy9pKsDmTyoWnGmyTWBPiTb2rp
# 5ogo8Y8hMU1YQs7rHR5hqilEq88jF+9H8Kccb/1ismJTGnBnRMv68Ud2l5LFhOZ4
# nRtl4lHri+N1L8EBg7aE8EvPe8Ca9gz8sh2F4COTYd1PHce1ugLvvWW1+aOSpd8N
# nwEid4zgD79ZQxisJqyO4lMWMzAgEeFhUm40FshtzXudAsX5LoCil4rLbHfwYtGO
# pw9DVX3jXAV90tG9iRbcqjtt3vhW9T+L3fAZlMeraWfh7eUmPltMU8lEQOMelo/1
# ehkIGO7YZOHxUqeKpmF9QaW8LXTT090AHZ4k6g+tdpZFfCMotyG+E4XqN6ZWtKEB
# QiE3xL27BDCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI
# hvcNAQELBQAwgYgxCzAJBgNVBAYTAlVTMRMwEQYDVQQIEwpXYXNoaW5ndG9uMRAw
# DgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNyb3NvZnQgQ29ycG9yYXRpb24x
# MjAwBgNVBAMTKU1pY3Jvc29mdCBSb290IENlcnRpZmljYXRlIEF1dGhvcml0eSAy
# MDEwMB4XDTIxMDkzMDE4MjIyNVoXDTMwMDkzMDE4MzIyNVowfDELMAkGA1UEBhMC
# VVMxEzARBgNVBAgTCldhc2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNV
# BAoTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRp
# bWUtU3RhbXAgUENBIDIwMTAwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC
# AQDk4aZM57RyIQt5osvXJHm9DtWC0/3unAcH0qlsTnXIyjVX9gF/bErg4r25Phdg
# M/9cT8dm95VTcVrifkpa/rg2Z4VGIwy1jRPPdzLAEBjoYH1qUoNEt6aORmsHFPPF
# dvWGUNzBRMhxXFExN6AKOG6N7dcP2CZTfDlhAnrEqv1yaa8dq6z2Nr41JmTamDu6
# GnszrYBbfowQHJ1S/rboYiXcag/PXfT+jlPP1uyFVk3v3byNpOORj7I5LFGc6XBp
# Dco2LXCOMcg1KL3jtIckw+DJj361VI/c+gVVmG1oO5pGve2krnopN6zL64NF50Zu
# yjLVwIYwXE8s4mKyzbnijYjklqwBSru+cakXW2dg3viSkR4dPf0gz3N9QZpGdc3E
# XzTdEonW/aUgfX782Z5F37ZyL9t9X4C626p+Nuw2TPYrbqgSUei/BQOj0XOmTTd0
# lBw0gg/wEPK3Rxjtp+iZfD9M269ewvPV2HM9Q07BMzlMjgK8QmguEOqEUUbi0b1q
# GFphAXPKZ6Je1yh2AuIzGHLXpyDwwvoSCtdjbwzJNmSLW6CmgyFdXzB0kZSU2LlQ
# +QuJYfM2BjUYhEfb3BvR/bLUHMVr9lxSUV0S2yW6r1AFemzFER1y7435UsSFF5PA
# PBXbGjfHCBUYP3irRbb1Hode2o+eFnJpxq57t7c+auIurQIDAQABo4IB3TCCAdkw
# EgYJKwYBBAGCNxUBBAUCAwEAATAjBgkrBgEEAYI3FQIEFgQUKqdS/mTEmr6CkTxG
# NSnPEP8vBO4wHQYDVR0OBBYEFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMFwGA1UdIARV
# MFMwUQYMKwYBBAGCN0yDfQEBMEEwPwYIKwYBBQUHAgEWM2h0dHA6Ly93d3cubWlj
# cm9zb2Z0LmNvbS9wa2lvcHMvRG9jcy9SZXBvc2l0b3J5Lmh0bTATBgNVHSUEDDAK
# BggrBgEFBQcDCDAZBgkrBgEEAYI3FAIEDB4KAFMAdQBiAEMAQTALBgNVHQ8EBAMC
# AYYwDwYDVR0TAQH/BAUwAwEB/zAfBgNVHSMEGDAWgBTV9lbLj+iiXGJo0T2UkFvX
# zpoYxDBWBgNVHR8ETzBNMEugSaBHhkVodHRwOi8vY3JsLm1pY3Jvc29mdC5jb20v
# cGtpL2NybC9wcm9kdWN0cy9NaWNSb29DZXJBdXRfMjAxMC0wNi0yMy5jcmwwWgYI
# KwYBBQUHAQEETjBMMEoGCCsGAQUFBzAChj5odHRwOi8vd3d3Lm1pY3Jvc29mdC5j
# b20vcGtpL2NlcnRzL01pY1Jvb0NlckF1dF8yMDEwLTA2LTIzLmNydDANBgkqhkiG
# 9w0BAQsFAAOCAgEAnVV9/Cqt4SwfZwExJFvhnnJL/Klv6lwUtj5OR2R4sQaTlz0x
# M7U518JxNj/aZGx80HU5bbsPMeTCj/ts0aGUGCLu6WZnOlNN3Zi6th542DYunKmC
# VgADsAW+iehp4LoJ7nvfam++Kctu2D9IdQHZGN5tggz1bSNU5HhTdSRXud2f8449
# xvNo32X2pFaq95W2KFUn0CS9QKC/GbYSEhFdPSfgQJY4rPf5KYnDvBewVIVCs/wM
# nosZiefwC2qBwoEZQhlSdYo2wh3DYXMuLGt7bj8sCXgU6ZGyqVvfSaN0DLzskYDS
# PeZKPmY7T7uG+jIa2Zb0j/aRAfbOxnT99kxybxCrdTDFNLB62FD+CljdQDzHVG2d
# Y3RILLFORy3BFARxv2T5JL5zbcqOCb2zAVdJVGTZc9d/HltEAY5aGZFrDZ+kKNxn
# GSgkujhLmm77IVRrakURR6nxt67I6IleT53S0Ex2tVdUCbFpAUR+fKFhbHP+Crvs
# QWY9af3LwUFJfn6Tvsv4O+S3Fb+0zj6lMVGEvL8CwYKiexcdFYmNcP7ntdAoGokL
# jzbaukz5m/8K6TT4JDVnK+ANuOaMmdbhIurwJ0I9JZTmdHRbatGePu1+oDEzfbzL
# 6Xu/OHBE0ZDxyKs6ijoIYn/ZcGNTTY3ugm2lBRDBcQZqELQdVTNYs6FwZvKhggNN
# MIICNQIBATCB+aGB0aSBzjCByzELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldhc2hp
# bmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBDb3Jw
# b3JhdGlvbjElMCMGA1UECxMcTWljcm9zb2Z0IEFtZXJpY2EgT3BlcmF0aW9uczEn
# MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjg2MDMtMDVFMC1EOTQ3MSUwIwYDVQQD
# ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQD7
# n7Bk4gsM2tbU/i+M3BtRnLj096CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD
# VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy
# b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w
# IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6m/EZjAiGA8yMDI0MDgyMTAxNDE1
# OFoYDzIwMjQwODIyMDE0MTU4WjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDqb8Rm
# AgEAMAcCAQACAgEAMAcCAQACAhNFMAoCBQDqcRXmAgEAMDYGCisGAQQBhFkKBAIx
# KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI
# hvcNAQELBQADggEBAHHlVXwxyY+ytRLFYIWhed3D1JpIkqIgBQC3iBpCvkatnYY5
# 5kZXx9GtC72fxGAVksjojTxC/qvj0XB2HF14VZjFoYWpmLbbHApCrEe/E/OgEeu6
# ji/Of3yo/18E579hz4vDITgF9hp8xyCba9Kz8bnEL/WxQnN5Lr9Z8uyw3MdS7fTW
# 79aAuK1JdKqL19drqNrGVUxQeRWsmzGn0HLVgEEgjQKuavY12KqKOqfDhwKnJT2Q
# wuH9AkFUs8PWtfkQLfNwcNelv3+8npVigVsN3q2Zi1rmBmVJpwBBu3mI3KYxQh7v
# BB2uGoBES9zBBKuwt6IXKzDA4n84404a2hfvHdExggQNMIIECQIBATCBkzB8MQsw
# CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u
# ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy
# b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAfGzRfUn6MAW1gABAAAB8TAN
# BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G
# CSqGSIb3DQEJBDEiBCDFFLUKYex4MV0+CfjtM5qwZ6gev83dzMtdaydALZZrHDCB
# +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EINV3/T5hS7ijwao466RosB7wwEib
# t0a1P5EqIwEj9hF4MIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh
# c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD
# b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw
# MTACEzMAAAHxs0X1J+jAFtYAAQAAAfEwIgQgobu/SzQa7l//nPELEA7xxCIIlQyS
# gK5YH9ocrSQ/nu4wDQYJKoZIhvcNAQELBQAEggIAWJ3hKP06OC4Uliev9iTty7ML
# wM/3OtE8TBucMaBw7+OMll/wOteOZJTilE/luHBVD5ru+MuJZWa8xM5GCJ+8KUei
# +VLA1QylvhSkWK8rlDQb4rPW+0ciL2OmIkLaGhjcHZi91aZA2TREeibvdj60T8YB
# EOSZVbf+nXx7UhRFYBzQ1gpGed9hYdjtpgNTnsrQsYrKCfhq8/RHbSDiQpywG9KB
# /+P/mEjY4KoPlQkycouF9KxQgrns1hOWaptd4xPakLUthEgh6rzDpq/YFOEf+5Ny
# +uWxaPgacNI/Rb3Df5B+vhcujuYQWRlvoXoFslYj2XZZ2RIcBmUMpvYqrHK6Pypc
# Zf4THQgPKddU/TJup2L1bAnrcT2hVeFJZz8yfhQTRXGs/1ki+tvzKfqhMo/ZmnCS
# Ak3PagNvNW+NYkHZDVAvTcL7kMXR1k2krClWRfIJ+QS3IPPNRYjKPFRrF0FdkT1O
# aqCJKB4vXqfBH4Hcr41R3+XqNy7d1S9wS2/T0DrvTXk0SNyjKDNwDoHQOG9xkve7
# sK91ZQ7JVAjjI2EoZ1xqJ++uGy/VeNLxBjMiXYp6z+CyJ45lCfddoOt2CyWqfHBy
# zjP76vr5dA7mASaOpt1MBr05gEgdclJcdpDo+lwy15/OHGJu0QeJmGLJp/FHhk6t
# bSdRZ7mpeU3LkCtgTdc=
# SIG # End signature block