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' } RemoteSAM = @{ GpoName = '{0} - Remote SAM Access' GpoRegSet = @{ 'System\CurrentControlSet\Control\Lsa\RestrictRemoteSAM=1' = 'O:BAG:BAD:(A;;RC;;;BA)(A;;RC;;;{0})' } RegistrySet = @{ 'System\CurrentControlSet\Control\Lsa\RestrictRemoteSAM' = 'O:BAG:BAD:(A;;RC;;;BA)(A;;RC;;;{0})' } DenyGPPermissions = [ordered]@{ '{0}-516' = 'GpoApply' } } 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' } } EntraConnectAuditing = @{ GpoName = '{0} - Advanced Audit and URA Policy for Entra Connect' GptSet = @{ 'SeServiceLogonRight' = '*{0},*S-1-5-80-0' } AuditSet = @' Machine Name,Policy Target,Subcategory,Subcategory GUID,Inclusion Setting,Exclusion Setting,Setting Value ,System,Audit Logon,{0cce9215-69ae-11d9-bed3-505054503030},Success and Failure,,3 '@ } 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]@{ '{0}-517' = 'GpoApply' '{0}-516' = 'GpoRead' 'S-1-5-11' = '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]@{ '{0}-517' = 'GpoApply' '{0}-516' = 'GpoRead' 'S-1-5-11' = '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) domainNetBiosName = [string]((Get-ADDomain -ErrorAction SilentlyContinue).NetBIOSName) } 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 Get-MDIDSA { [CmdletBinding()] param( [string] $Identity, [Parameter(Mandatory = $false)] [string] $Server ) $Server = Get-MDIDC -Server $Server $returnVal = $null $adobjectParams = @{ ldapFilter = '(&(|(samaccountname={0})(samaccountname={0}$))(|(objectClass=user)(objectClass=msDS-GroupManagedServiceAccount)))' -f $Identity }; if (-not [string]::IsNullOrEmpty($server)) { $adobjectParams.Add("Server", $Server) } try { $returnVal = Get-ADObject @adobjectParams -ErrorAction SilentlyContinue -properties * if ($returnVal -eq $null) { throw } } catch { $returnVal = $null Write-Warning ($strings['DSA_CannotFindIdentity'] -f $Identity) } return $returnVal } 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 Set-MDIGpoApplyPermission { [CmdletBinding()] param( [Parameter(Mandatory)] [guid] $Guid, [parameter(Mandatory)] [string]$Identity, [parameter(Mandatory)] [ValidateSet("Allow", "Deny")] [string]$PermissionType, [Parameter(Mandatory = $false)] [string] $Server ) $returnVal = $false $domainDn = ([adsi]'').distinguishedName.Value if ([string]::IsNullOrEmpty($server)) { $gpo = [ADSI]"LDAP://CN=`{$($Guid)`},CN=Policies,CN=System,$domainDn" } else { $gpo = [ADSI]"LDAP://$Server/CN=`{$($Guid)`},CN=Policies,CN=System,$domainDn" } if ($gpo -ne $null) { $rule = New-Object System.DirectoryServices.ActiveDirectoryAccessRule( [System.Security.Principal.NTAccount]"$($settings.domainNetBiosName)\$Identity", "ExtendedRight", $PermissionType, [Guid]"edacfd8f-ffb3-11d1-b41d-00a0c968f939" ) $acl = $gpo.ObjectSecurity $acl.AddAccessRule($rule) | Out-Null try { $gpo.CommitChanges() | Out-Null $returnVal = $true } catch { Write-Warning -Message $strings['GPO_UnableToSetPermissions'] $returnVal = $false } } return $returnVal } 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 = $false)] [string[]] $Extension, [Parameter(Mandatory = $false)] [string] $RawExtension = $null, [Parameter(Mandatory = $false)] [string] $Server ) Write-Verbose -Message $strings['GPO_SetExtension'] $return = $null $Server = Get-MDIDC -Server $Server if ([string]::IsNullOrEmpty($RawExtension)) { $extensions = $Extension | ForEach-Object { "{$_}" } $extensionGuids = '[{0}]' -f [string]::Join('', $extensions) $Replace = @{gPCMachineExtensionNames = $extensionGuids } } else { $Replace = @{gPCMachineExtensionNames = $RawExtension } } $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)] [switch] $ManualLinkRequired, [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)) { if ($ManualLinkRequired) { Write-Warning -Message ($strings['GPO_ManualLinkRequired'] -f $GPO.DisplayName) } 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 RemoteSAM helper functions function Get-MDIRemoteSAM { [CmdletBinding()] param( [string] $Identity ) $identitySid = (Get-MDIDSA -Identity $Identity).objectSid.value $settings.RemoteSAM.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 -f $identitySid [PSCustomObject]@{ Path = $path Name = $name ActualValue = $value ExpectedValue = $expected } } } function Set-MDIRemoteSAM { [CmdletBinding()] param( [string] $Identity ) $identitySid = (Get-MDIDSA -Identity $Identity).objectSid.value $settings.RemoteSAM.RegistrySet.GetEnumerator() | ForEach-Object { $name = ($_.Name -split '\\')[-1] $path = 'HKLM:\{0}' -f ($_.Name -replace $name) $value = $_.Value -f $identitySid Set-ItemProperty -Path $path -Name $name -Value $value -ErrorAction Stop } } function Test-MDIRemoteSAM { [CmdletBinding()] param( [switch] $Detailed, [string] $Identity ) Write-Verbose -Message $strings['RemoteSAM_Validate'] $remoteSAMSettings = Get-MDIRemoteSAM -Identity $Identity $status = @($ntlmAuditing | Where-Object { $_.ActualValue -match $_.ExpectedValue }).Count -eq $settings.RemoteSAM.RegistrySet.Count Write-Verbose (Get-MDIValidationMessage $status) if ($Detailed) { [PSCustomObject]([ordered]@{ Status = $status Details = $remoteSAMSettings }) } else { $status } } function Get-MDIRemoteSAMGPO { [CmdletBinding()] param( [string] $GpoNamePrefix, [string] $Identity, [Parameter(Mandatory = $false)] [string] $Server ) $Server = Get-MDIDC -Server $Server $identitySid = (Get-MDIDSA -Identity $Identity).objectSid.value $gpoName = Get-MDIGPOName -Name $settings.RemoteSAM.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 'RestrictRemoteSAM' } $RegistryValue = foreach ($opt in $options) { $valueName = ($opt.KeyName -split '\\')[-1] $path = $opt.KeyName -replace '(.*)\\(\w+)', '$1' [PSCustomObject]@{ KeyName = $path ValueName = $valueName Value = $opt.Display.DisplayString ExpectedValue = (($settings.RemoteSAM.RegistrySet.GetEnumerator() | Where-Object { ('MACHINE\{0}' -f $_.Name) -eq (Join-Path -Path $path -ChildPath $valueName) }).Value -f $identitySid) } } $gpo | Select-Object -Property *, @{N = 'RegistryValue'; E = { $RegistryValue } } } } function Set-MDIRemoteSAMGPO { [CmdletBinding()] param( [string] $Identity, [switch] $SkipGpoLink, [switch] $CreateGpoDisabled, [string] $GpoNamePrefix, [Parameter(Mandatory = $false)] [string] $Server ) $Server = Get-MDIDC -Server $Server $identitySid = (Get-MDIDSA -Identity $Identity).objectSid.value $domainSid = (Get-ADDomain).DomainSID.Value $gpoName = Get-MDIGPOName -Name $settings.RemoteSAM.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.RemoteSAM.GpoRegSet.GetEnumerator() | ForEach-Object { $value = $_.Value -f $identitySid $fileContent += '{2}MACHINE\{0}={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 $settings.RemoteSAM.DenyGPPermissions.GetEnumerator() | ForEach-Object { $adObjectParams = @{ LDAPFilter = "(objectSid=$($_.Name -f $domainSid))" Properties = "samaccountname" }; if (-not [string]::IsNullOrEmpty($server)) { $adObjectParams.Add("Server", $server) } $identitySamAccountName = (Get-ADObject @adObjectParams).samaccountname $mdiApplyParams = @{ Guid = $gpo.Id.Guid Identity = $identitySamAccountName PermissionType = "Deny" }; if (-not [string]::IsNullOrEmpty($server)) { $mdiApplyParams.Add("Server", $Server) } $gpoAclUpdate = Set-MDIGpoApplyPermission @mdiApplyParams if (-not $gpoAclUpdate) { Write-Warning $strings['GPO_UnableToSetPermissions'] } } if ($null -ne $gpoUpdated) { if (-not $SkipGpoLink) { if (-not $SkipGpoLink) { Write-Warning -Message ($strings['GPO_ManualLinkRequired'] -f $GPO.DisplayName) } } } else { Write-Warning $strings['GPO_UnableToSetExtension'] } } function Test-MDIRemoteSAMGPO { [CmdletBinding()] param( [switch] $Detailed, [string] $Identity, [string] $GpoNamePrefix, [Parameter(Mandatory = $false)] [string] $Server ) $Server = Get-MDIDC -Server $Server $gpoName = Get-MDIGPOName -Name $settings.RemoteSAM.GpoName -GpoNamePrefix $GpoNamePrefix Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName) $mdiGpoParams = @{ Identity = $Identity GpoNamePrefix = $GpoNamePrefix }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server", $Server) } $state = $false $gpo = Get-MDIRemoteSAMGPO @mdiGpoParams if ($gpo) { $gpSetOk = @($gpo.RegistryValue | Where-Object { ([string]::Compare($_.Value, $_.ExpectedValue) -eq 0) }).Count -eq $settings.RemoteSAM.RegistrySet.Count if ($gpSetOk) { $mdiGpoCheckParams = @{ GPO = $gpo ManualLinkRequired = $true }; 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) { $RegistryValue = $gpo.RegistryValue [PSCustomObject]([ordered]@{ Status = $state Details = if ($gpo) { $gpo | Select-Object DisplayName, Id, GpoStatus, @{N = 'RegistryValue'; E = { [string]($gpo.RegistryValue -join ',') } } } else { "'{0}' - {1}" -f $gpoName, $strings['GPO_NotFound'] } }) } else { $state } } #endregion #region EntraConnect Auditing helper functions function Add-MDIServiceLogonRight { [CmdletBinding()] param( [string] $Identity ) $identitySamAccountName = (Get-MDIDSA -Identity $Identity).sAMAccountName $secPol = Get-MDIServiceLogonRight $filePath = '{0}\secpol.inf' -f $env:temp $dbPath = '{0}\secpol.db' -f $env:temp ($secPol) -replace '^SeServiceLogonRight .+', "`$0,$identitySamAccountName" | Set-Content "$filePath" -Force $null = & "$($env:SystemRoot)\system32\secedit.exe" @('/import', '/cfg', "$filePath", '/db', "$dbPath", '/areas', 'USER_RIGHTS', '/overwrite', '/quiet') $null = & "$($env:SystemRoot)\system32\secedit.exe" @('/configure', '/cfg', "$filePath", '/db', "$dbPath", '/areas', 'USER_RIGHTS', '/overwrite', '/quiet') Remove-Item $filePath -ErrorAction SilentlyContinue Remove-Item $dbPath -ErrorAction SilentlyContinue } function Get-MDIServiceLogonRight { [CmdletBinding()] param() $filePath = '{0}\secpol.inf' -f $env:temp $null = & "$($env:SystemRoot)\system32\secedit.exe" @('/export', '/cfg', "$filePath") $content = Get-Content $filePath Remove-Item $filePath -ErrorAction SilentlyContinue return $content } function Get-MDIEntraConnectAuditing { [CmdletBinding()] param() $relevantGUIDs = @($settings.EntraConnectAuditing.AuditSet | ConvertFrom-Csv) | Select-Object -ExpandProperty 'Subcategory GUID' -Unique $auditResult = Get-MDIAdvAuditPolicy | Where-Object { $_.'Subcategory GUID' -in $relevantGUIDs } $secPol = ((Get-MDIServiceLogonRight) -match 'SeServiceLogonRight') return [PSCustomObject](@{ AuditSettings = $auditResult GptSettings = $secPol }) } function Test-MDIEntraConnectAuditing { [CmdletBinding()] param( [switch] $Detailed, [string] $Identity ) Write-Verbose -Message $strings['AdvancedPolicyEntra_Validate'] $identitySid = (Get-MDIDSA -Identity $Identity).objectSid.value $existingEntraConnectSettings = Get-MDIEntraConnectAuditing $result = Test-MDIAdvAuditPolicy -ExpectedAuditing @($settings.EntraConnectAuditing.AuditSet | ConvertFrom-Csv) -Detailed:$Detailed if ($Detailed) { Write-Verbose -Message (Get-MDIValidationMessage $result.Status) } else { Write-Verbose -Message (Get-MDIValidationMessage $result) } $result | Add-Member -MemberType NoteProperty -Name GptSettings -Value $existingEntraConnectSettings.GptSettings $result.Status = $result.status -and ($($result.GptSettings) -match $identitySid) return $result } function Set-MDIEntraConnectAuditing { [CmdletBinding()] param( [string] $Identity ) Write-Verbose -Message $strings['AdvancedPolicyEntra_Set'] Add-MDIServiceLogonRight -Identity $Identity $settings.EntraConnectAuditing.AuditSet | ConvertFrom-Csv | ForEach-Object { $param = @{ SubcategoryGUID = $_.'Subcategory GUID' InclusionSetting = $_.'Inclusion Setting' } Set-MDIAdvAuditPolicy @param } } function Get-MDIEntraConnectAuditingGPO { [CmdletBinding()] param( [string] $Identity, [string] $GpoNamePrefix, [Parameter(Mandatory = $false)] [string] $Server ) $Server = Get-MDIDC -Server $Server $identitySid = (Get-MDIDSA -Identity $Identity).objectSid.value $gpoName = Get-MDIGPOName -Name $settings.EntraConnectAuditing.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.EntraConnectAuditing.AuditSet | 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 } } $currentSettings = ($report.GPO.Computer.ExtensionData.Extension.UserRightsAssignment | Where-Object { $_.name -eq 'SeServiceLogonRight' }).member.sid.'#text' | sort $expectedSettings = (($settings.EntraConnectAuditing.GptSet['SeServiceLogonRight'] -split ',').trim('*') -f $identitySid) -split ' ' | sort $GptSettings = [PSCustomObject]@{ UserRightsAssignment = 'Logon as a Service' SettingValue = [string]$expectedSettings -join ',' ExpectedValue = [string]$(foreach ($gpt in $expectedSettings) { $currentSettings | Where-Object { -not [string]::IsNullOrEmpty($_) } | Where-Object { $_ -eq $gpt } }) -join ',' } } $gpo | Select-Object -Property *, @{N = 'AuditSettings'; E = { $AuditSettings } }, @{N = 'GptSettings'; E = { $GptSettings | select SettingValue, ExpectedValue } } } function Test-MDIEntraConnectAuditingGPO { [CmdletBinding()] param( [switch] $Detailed, [string] $Identity, [string] $GpoNamePrefix, [Parameter(Mandatory = $false)] [string] $Server ) $Server = Get-MDIDC -Server $Server $gpoName = Get-MDIGPOName -Name $settings.EntraConnectAuditing.GpoName -GpoNamePrefix $GpoNamePrefix Write-Verbose -Message ($strings['GPO_Validate'] -f $gpoName) $mdiGpoParams = @{ Identity = $Identity GpoNamePrefix = $GpoNamePrefix }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoParams.Add("Server", $Server) } $state = $false $gpo = Get-MDIEntraConnectAuditingGPO @mdiGpoParams if ($gpo) { $expectedSettings = @($settings.EntraConnectAuditing.AuditSet | ConvertFrom-Csv) $gpSetOk = ((@(($gpo.GptSettings | ? { $_.SettingValue -match $_.ExpectedValue })).count -eq 1)) -and (@($gpo.AuditSettings | Where-Object { $_.SettingValue -match $_.ExpectedValue }).Count -eq $expectedSettings.Count) if ($gpSetOk) { $mdiGpoCheckParams = @{ GPO = $gpo ManualLinkRequired = $true }; 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, @{N = "AuditSettings"; E = { [string]($gpo.AuditSettings -join ',') } }, @{N = "GptSettings"; E = { [string]($gpo.GptSettings -join ',') } } } else { "'{0}' - {1}" -f $gpoName, $strings['GPO_NotFound'] } }) } else { $state } } function Set-MDIEntraConnectAuditingGPO { [CmdletBinding()] param( [switch] $SkipGpoLink, [string] $Identity, [switch] $CreateGpoDisabled, [string] $GpoNamePrefix, [Parameter(Mandatory = $false)] [string] $Server ) $Server = Get-MDIDC -Server $Server $identitySid = (Get-MDIDSA -Identity $Identity).objectSid.value if ($null -eq $identitySid) { throw } $gpoName = Get-MDIGPOName -Name $settings.EntraConnectAuditing.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 [Privilege Rights] '@ $settings.EntraConnectAuditing.GptSet.GetEnumerator() | ForEach-Object { $fileContent += '{2}{0}={1}' -f $_.Name, $($_.Value -f $identitySid), [System.Environment]::NewLine } [System.Io.File]::WriteAllLines((Join-Path -Path $filePath -ChildPath 'GptTmpl.inf'), $fileContent, (New-Object System.Text.UTF8Encoding)) $auditFilePath = '{0}\Machine\Microsoft\Windows NT\Audit' -f $gpo.gPCFileSysPath try { New-Item -Path $auditFilePath -ItemType Directory -Force | Out-Null } catch {} [System.io.file]::WriteAllLines((Join-Path -Path $auditFilePath -ChildPath 'audit.csv'), $settings.EntraConnectAuditing.AuditSet, (New-Object System.Text.ASCIIEncoding)) if (-not ($CreateGpoDisabled)) { $gpo.GpoStatus = [Microsoft.GroupPolicy.GpoStatus]::UserSettingsDisabled } $gpo.MakeAclConsistent() $stringBuilderGpc = [System.Text.StringBuilder]::new() [void]$stringBuilderGpc.Append("{$($settings.gpoExtensions['Security'])}") [void]$stringBuilderGpc.Append("{$($settings.gpoExtensions['Computer Restricted Groups'])}") $stringBuilderAudit = [System.Text.StringBuilder]::new() [void]$stringBuilderAudit.Append("{$($settings.gpoExtensions['Audit Policy Configuration'])}") [void]$stringBuilderAudit.Append("{$($settings.gpoExtensions['Audit Configuration Extension'])}") $mdiGpoMachineExtensionParams = @{ Guid = $gpo.Id.Guid RawExtension = '[{0}][{1}]' -f $stringBuilderGpc.ToString(), $stringBuilderAudit.ToString() }; if (-not [string]::IsNullOrEmpty($server)) { $mdiGpoMachineExtensionParams.Add("Server", $Server) } $gpoUpdated = Set-MDIGPOMachineExtension @mdiGpoMachineExtensionParams if ($null -ne $gpoUpdated) { if (-not $SkipGpoLink) { Write-Warning -Message ($strings['GPO_ManualLinkRequired'] -f $GPO.DisplayName) } } else { Write-Warning $strings['GPO_UnableToSetExtension'] } } #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 = Get-MDIADObjectName -SidMask $_.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) { $mapping = @{}; $settings.AdvancedAuditPolicyCAs.GPPermissions.GetEnumerator() | ForEach-Object { $mapping[(Get-MDIADObjectName -SidMask $_.Key)] = $_.Key } $gpDelegationOk = @($gpo.Delegation | Where-Object { $settings.AdvancedAuditPolicyCAs.GPPermissions[$mapping[$_.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 = Get-MDIADObjectName -SidMask $_.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 = Get-MDIADObjectName -SidMask $_.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) { $mapping = @{}; $settings.CAAuditing.GPPermissions.GetEnumerator() | ForEach-Object { $mapping[(Get-MDIADObjectName -SidMask $_.Key)] = $_.Key } $gpDelegationOk = @($gpo.Delegation | Where-Object { $settings.CAAuditing.GPPermissions[$mapping[$_.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 = Get-MDIADObjectName -SidMask $_.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 } function Get-MDIADObjectName { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [ValidatePattern('^S-1-\d{1}(-\d+){1,2}$|^\{0\}-\d{3}$')] [string] $SidMask ) if ($SidMask -match '^\{0\}-\d{3}$') { $sid = $SidMask -f ((Get-ADDomain).DomainSID.Value) Get-ADObject -Filter { objectSid -eq $sid } | Select-Object -ExpandProperty Name } else { (New-Object -TypeName System.Security.Principal.SecurityIdentifier -ArgumentList $SidMask ).Translate([System.Security.Principal.NTAccount]).Value -replace '(.*)\\(.*)', '$2'; } } #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', 'EntraConnectAuditing', 'NTLMAuditing', 'ProcessorPerformance', 'RemoteSAM', 'All')] [string[]] $Configuration ) DynamicParam { $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) $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute $paramAttributes.Mandatory = $true $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] $paramAttributesCollect.Add($paramAttributes) $dynParamIdentity = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Identity", [string], $paramAttributesCollect) if ($Mode -eq 'Domain') { $paramDictionary.Add("GpoNamePrefix", $dynParam1) $paramDictionary.Add("Server", $dynParam3) } if ([bool](($($Configuration -join ',')) -match 'EntraConnectAuditing|RemoteSAM|All')) { $paramDictionary.Add("Identity", $dynParamIdentity) } return $paramDictionary } begin { foreach ($key in $PSBoundParameters.Keys) { if ($MyInvocation.MyCommand.Parameters.$key.isDynamic) { Set-Variable -Name $key -Value $PSBoundParameters.$key } } } 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 'EntraConnectAuditing') { if ($Mode -eq 'LocalMachine') { $results.Add('EntraConnectAuditing', (Test-MDIEntraConnectAuditing -Detailed -Identity $Identity)) } else { $results.Add('EntraConnectAuditing', (Test-MDIEntraConnectAuditingGPO @mdiParams -Identity $Identity)) } } 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 (Use-MDIConfigName $Configuration 'RemoteSAM') { if ($Mode -eq 'LocalMachine') { $results.Add('RemoteSAM', (Test-MDIRemoteSAM -Detailed -Identity $Identity)) } else { $results.Add('RemoteSAM', (Test-MDIRemoteSAMGPO @mdiParams -Identity $Identity)) } } 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', 'EntraConnectAuditing', 'NTLMAuditing', 'ProcessorPerformance', 'RemoteSAM', 'All')] [string[]] $Configuration ) DynamicParam { $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) $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute $paramAttributes.Mandatory = $true $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] $paramAttributesCollect.Add($paramAttributes) $dynParamIdentity = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Identity", [string], $paramAttributesCollect) if ($Mode -eq 'Domain') { $paramDictionary.Add("GpoNamePrefix", $dynParam1) $paramDictionary.Add("Server", $dynParam3) } if ([bool](($($Configuration -join ',')) -match 'EntraConnectAuditing|RemoteSAM|All')) { $paramDictionary.Add("Identity", $dynParamIdentity) } return $paramDictionary } begin { foreach ($key in $PSBoundParameters.Keys) { if ($MyInvocation.MyCommand.Parameters.$key.isDynamic) { Set-Variable -Name $key -Value $PSBoundParameters.$key } } } process { $results = if ($Mode -eq 'Domain') { $Server = Get-MDIDC -Server $Server $mdiParams = @{ Configuration = $Configuration Mode = "Domain" GpoNamePrefix = $GpoNamePrefix } if (-not [string]::IsNullOrEmpty($server)) { $mdiParams.Add("Server", $Server) } if (-not [string]::IsNullOrEmpty($Identity)) { $mdiParams.Add("Identity", $Identity) } Get-MDIConfiguration @mdiParams } 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', 'EntraConnectAuditing', 'NTLMAuditing', 'ProcessorPerformance', 'RemoteSAM', 'All')] [string[]] $Configuration, [Parameter(Mandatory = $false)] [switch] $Force ) DynamicParam { $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) $paramAttributes = New-Object -Type System.Management.Automation.ParameterAttribute $paramAttributes.Mandatory = $true $paramAttributesCollect = New-Object -Type System.Collections.ObjectModel.Collection[System.Attribute] $paramAttributesCollect.Add($paramAttributes) $dynParamIdentity = New-Object -Type System.Management.Automation.RuntimeDefinedParameter("Identity", [string], $paramAttributesCollect) if ($Mode -eq 'Domain') { $paramDictionary.Add("GpoNamePrefix", $dynParam1) $paramDictionary.Add("CreateGpoDisabled", $dynParam2) $paramDictionary.Add("SkipGpoLink", $dynParam3) $paramDictionary.Add("Server", $dynParam5) } if ([bool](($($Configuration -join ',')) -match 'EntraConnectAuditing|RemoteSAM|All')) { $paramDictionary.Add("Identity", $dynParamIdentity) } return $paramDictionary } begin { foreach ($key in $PSBoundParameters.Keys) { if ($MyInvocation.MyCommand.Parameters.$key.isDynamic) { Set-Variable -Name $key -Value $PSBoundParameters.$key } } } 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 'EntraConnectAuditing') { if ($Mode -eq 'LocalMachine') { Set-MDIEntraConnectAuditing -Identity $Identity } else { Set-MDIEntraConnectAuditingGPO @mdiParams -Identity $Identity } } 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 } } if (Use-MDIConfigName $config 'RemoteSAM') { if ($Mode -eq 'LocalMachine') { Set-MDIRemoteSAM -Identity $Identity } else { Set-MDIRemoteSAMGPO @mdiParams -Identity $Identity } } } } } 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 # KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCB/CvdVjmrvejq9 # RLCMxGpAN8GAaNkwu8PMrI5ZVroGeqCCDXYwggX0MIID3KADAgECAhMzAAAEBGx0 # Bv9XKydyAAAAAAQEMA0GCSqGSIb3DQEBCwUAMH4xCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xKDAmBgNVBAMTH01pY3Jvc29mdCBDb2RlIFNpZ25p # bmcgUENBIDIwMTEwHhcNMjQwOTEyMjAxMTE0WhcNMjUwOTExMjAxMTE0WjB0MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMR4wHAYDVQQDExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24wggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIB # AQC0KDfaY50MDqsEGdlIzDHBd6CqIMRQWW9Af1LHDDTuFjfDsvna0nEuDSYJmNyz # NB10jpbg0lhvkT1AzfX2TLITSXwS8D+mBzGCWMM/wTpciWBV/pbjSazbzoKvRrNo # DV/u9omOM2Eawyo5JJJdNkM2d8qzkQ0bRuRd4HarmGunSouyb9NY7egWN5E5lUc3 # a2AROzAdHdYpObpCOdeAY2P5XqtJkk79aROpzw16wCjdSn8qMzCBzR7rvH2WVkvF # HLIxZQET1yhPb6lRmpgBQNnzidHV2Ocxjc8wNiIDzgbDkmlx54QPfw7RwQi8p1fy # 4byhBrTjv568x8NGv3gwb0RbAgMBAAGjggFzMIIBbzAfBgNVHSUEGDAWBgorBgEE # AYI3TAgBBggrBgEFBQcDAzAdBgNVHQ4EFgQU8huhNbETDU+ZWllL4DNMPCijEU4w # RQYDVR0RBD4wPKQ6MDgxHjAcBgNVBAsTFU1pY3Jvc29mdCBDb3Jwb3JhdGlvbjEW # MBQGA1UEBRMNMjMwMDEyKzUwMjkyMzAfBgNVHSMEGDAWgBRIbmTlUAXTgqoXNzci # tW2oynUClTBUBgNVHR8ETTBLMEmgR6BFhkNodHRwOi8vd3d3Lm1pY3Jvc29mdC5j # b20vcGtpb3BzL2NybC9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3JsMGEG # CCsGAQUFBwEBBFUwUzBRBggrBgEFBQcwAoZFaHR0cDovL3d3dy5taWNyb3NvZnQu # Y29tL3BraW9wcy9jZXJ0cy9NaWNDb2RTaWdQQ0EyMDExXzIwMTEtMDctMDguY3J0 # MAwGA1UdEwEB/wQCMAAwDQYJKoZIhvcNAQELBQADggIBAIjmD9IpQVvfB1QehvpC # Ge7QeTQkKQ7j3bmDMjwSqFL4ri6ae9IFTdpywn5smmtSIyKYDn3/nHtaEn0X1NBj # L5oP0BjAy1sqxD+uy35B+V8wv5GrxhMDJP8l2QjLtH/UglSTIhLqyt8bUAqVfyfp # h4COMRvwwjTvChtCnUXXACuCXYHWalOoc0OU2oGN+mPJIJJxaNQc1sjBsMbGIWv3 # cmgSHkCEmrMv7yaidpePt6V+yPMik+eXw3IfZ5eNOiNgL1rZzgSJfTnvUqiaEQ0X # dG1HbkDv9fv6CTq6m4Ty3IzLiwGSXYxRIXTxT4TYs5VxHy2uFjFXWVSL0J2ARTYL # E4Oyl1wXDF1PX4bxg1yDMfKPHcE1Ijic5lx1KdK1SkaEJdto4hd++05J9Bf9TAmi # u6EK6C9Oe5vRadroJCK26uCUI4zIjL/qG7mswW+qT0CW0gnR9JHkXCWNbo8ccMk1 # sJatmRoSAifbgzaYbUz8+lv+IXy5GFuAmLnNbGjacB3IMGpa+lbFgih57/fIhamq # 5VhxgaEmn/UjWyr+cPiAFWuTVIpfsOjbEAww75wURNM1Imp9NJKye1O24EspEHmb # DmqCUcq7NqkOKIG4PVm3hDDED/WQpzJDkvu4FrIbvyTGVU01vKsg4UfcdiZ0fQ+/ # V0hf8yrtq9CkB8iIuk5bBxuPMIIHejCCBWKgAwIBAgIKYQ6Q0gAAAAAAAzANBgkq # 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 # Z25pbmcgUENBIDIwMTECEzMAAAQEbHQG/1crJ3IAAAAABAQwDQYJYIZIAWUDBAIB # BQCgga4wGQYJKoZIhvcNAQkDMQwGCisGAQQBgjcCAQQwHAYKKwYBBAGCNwIBCzEO # MAwGCisGAQQBgjcCARUwLwYJKoZIhvcNAQkEMSIEIGlgy9NeUeNfVnHRUVN5kQfF # jo6dhmMTODePazFiWXmpMEIGCisGAQQBgjcCAQwxNDAyoBSAEgBNAGkAYwByAG8A # cwBvAGYAdKEagBhodHRwOi8vd3d3Lm1pY3Jvc29mdC5jb20wDQYJKoZIhvcNAQEB # BQAEggEALPU1kUY5B2r8VoQn1UNuwxDsjnhxYesOuG7WXO5b1aAg7Z6cxSVBV0DF # IkkSF0YOteeu4WQ2bLnOk663MQZ1DWlv3RnJXCL47Ru2e6BuO89DD+61p+ekMvBr # JWMVgChWiZN4CeaiwuaGiaQl/L27Q2ZGwFauOoyjhcqsIwMaz6P42nnrHFFzwhMi # tMNp4RwzLfy5pjd48+Yo3SY6jGKRjtHN9ilCpPMS8eouryQLbak6PUaU3RANE0tL # KcWCvFuRBsPnKcuFfufH+f7pTYwYhTqwjNUtHCxCCwt8Wf12fj2PhEBh/y55NKRd # rWGbvERmUnOYSUu+pDOavVaEQMqC7qGCF5QwgheQBgorBgEEAYI3AwMBMYIXgDCC # F3wGCSqGSIb3DQEHAqCCF20wghdpAgEDMQ8wDQYJYIZIAWUDBAIBBQAwggFSBgsq # hkiG9w0BCRABBKCCAUEEggE9MIIBOQIBAQYKKwYBBAGEWQoDATAxMA0GCWCGSAFl # AwQCAQUABCAWx3pcbVgwAP+XWRLSYc/LJZTP44ReO2ccju8ikBQPogIGZxqKybJx # GBMyMDI0MTEwNTA5Mzg0NS41OTNaMASAAgH0oIHRpIHOMIHLMQswCQYDVQQGEwJV # UzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UE # ChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1l # cmljYSBPcGVyYXRpb25zMScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTYwMC0w # NUUwLUQ5NDcxJTAjBgNVBAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2Wg # ghHqMIIHIDCCBQigAwIBAgITMwAAAe+JP1ahWMyo2gABAAAB7zANBgkqhkiG9w0B # AQsFADB8MQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UE # BxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYD # VQQDEx1NaWNyb3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMDAeFw0yMzEyMDYxODQ1 # NDhaFw0yNTAzMDUxODQ1NDhaMIHLMQswCQYDVQQGEwJVUzETMBEGA1UECBMKV2Fz # aGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9uZDEeMBwGA1UEChMVTWljcm9zb2Z0IENv # cnBvcmF0aW9uMSUwIwYDVQQLExxNaWNyb3NvZnQgQW1lcmljYSBPcGVyYXRpb25z # MScwJQYDVQQLEx5uU2hpZWxkIFRTUyBFU046OTYwMC0wNUUwLUQ5NDcxJTAjBgNV # BAMTHE1pY3Jvc29mdCBUaW1lLVN0YW1wIFNlcnZpY2UwggIiMA0GCSqGSIb3DQEB # AQUAA4ICDwAwggIKAoICAQCjC1jinwzgHwhOakZqy17oE4BIBKsm5kX4DUmCBWI0 # lFVpEiK5mZ2Kh59soL4ns52phFMQYGG5kypCipungwP9Nob4VGVE6aoMo5hZ9Nyt # XR5ZRgb9Z8NR6EmLKICRhD4sojPMg/RnGRTcdf7/TYvyM10jLjmLyKEegMHfvIwP # mM+AP7hzQLfExDdqCJ2u64Gd5XlnrFOku5U9jLOKk1y70c+Twt04/RLqruv1fGP8 # LmYmtHvrB4TcBsADXSmcFjh0VgQkX4zXFwqnIG8rgY+zDqJYQNZP8O1Yo4kSckHT # 43XC0oM40ye2+9l/rTYiDFM3nlZe2jhtOkGCO6GqiTp50xI9ITpJXi0vEek8AejT # 4PKMEO2bPxU63p63uZbjdN5L+lgIcCNMCNI0SIopS4gaVR4Sy/IoDv1vDWpe+I28 # /Ky8jWTeed0O3HxPJMZqX4QB3I6DnwZrHiKn6oE38tgBTCCAKvEoYOTg7r2lF0Iu # bt/3+VPvKtTCUbZPFOG8jZt9q6AFodlvQntiolYIYtqSrLyXAQIlXGhZ4gNcv4dv # 1YAilnbWA9CsnYh+OKEFr/4w4M69lI+yaoZ3L/t/UfXpT/+yc7hS/FolcmrGFJTB # YlS4nE1cuKblwZ/UOG26SLhDONWXGZDKMJKN53oOLSSk4ldR0HlsbT4heLlWlOEl # JQIDAQABo4IBSTCCAUUwHQYDVR0OBBYEFO1MWqKFwrCbtrw9P8A63bAVSJzLMB8G # A1UdIwQYMBaAFJ+nFV0AXmJdg/Tl0mWnG1M1GelyMF8GA1UdHwRYMFYwVKBSoFCG # Tmh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY3JsL01pY3Jvc29mdCUy # MFRpbWUtU3RhbXAlMjBQQ0ElMjAyMDEwKDEpLmNybDBsBggrBgEFBQcBAQRgMF4w # XAYIKwYBBQUHMAKGUGh0dHA6Ly93d3cubWljcm9zb2Z0LmNvbS9wa2lvcHMvY2Vy # dHMvTWljcm9zb2Z0JTIwVGltZS1TdGFtcCUyMFBDQSUyMDIwMTAoMSkuY3J0MAwG # A1UdEwEB/wQCMAAwFgYDVR0lAQH/BAwwCgYIKwYBBQUHAwgwDgYDVR0PAQH/BAQD # AgeAMA0GCSqGSIb3DQEBCwUAA4ICAQAYGZa3aCDudbk9EVdkP8xcQGZuIAIPRx9K # 1CA7uRzBt80fC0aWkuYYhQMvHHJRHUobSM4Uw3zN7fHEN8hhaBDb9NRaGnFWdtHx # mJ9eMz6Jpn6KiIyi9U5Og7QCTZMl17n2w4eddq5vtk4rRWOVvpiDBGJARKiXWB9u # 2ix0WH2EMFGHqjIhjWUXhPgR4C6NKFNXHvWvXecJ2WXrJnvvQGXAfNJGETJZGpR4 # 1nUN3ijfiCSjFDxamGPsy5iYu904Hv9uuSXYd5m0Jxf2WNJSXkPGlNhrO27pPxgT # 111myAR61S3S2hc572zN9yoJEObE98Vy5KEM3ZX53cLefN81F1C9p/cAKkE6u9V6 # ryyl/qSgxu1UqeOZCtG/iaHSKMoxM7Mq4SMFsPT/8ieOdwClYpcw0CjZe5KBx2xL # a4B1neFib8J8/gSosjMdF3nHiyHx1YedZDtxSSgegeJsi0fbUgdzsVMJYvqVw52W # qQNu0GRC79ZuVreUVKdCJmUMBHBpTp6VFopL0Jf4Srgg+zRD9iwbc9uZrn+89odp # InbznYrnPKHiO26qe1ekNwl/d7ro2ItP/lghz0DoD7kEGeikKJWHdto7eVJoJhkr # UcanTuUH08g+NYwG6S+PjBSB/NyNF6bHa/xR+ceAYhcjx0iBiv90Mn0JiGfnA2/h # Lj5evhTcAjCCB3EwggVZoAMCAQICEzMAAAAVxedrngKbSZkAAAAAABUwDQYJKoZI # 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 # MCUGA1UECxMeblNoaWVsZCBUU1MgRVNOOjk2MDAtMDVFMC1EOTQ3MSUwIwYDVQQD # ExxNaWNyb3NvZnQgVGltZS1TdGFtcCBTZXJ2aWNloiMKAQEwBwYFKw4DAhoDFQBL # cI81gxbea1Ex2mFbXx7ck+0g/6CBgzCBgKR+MHwxCzAJBgNVBAYTAlVTMRMwEQYD # VQQIEwpXYXNoaW5ndG9uMRAwDgYDVQQHEwdSZWRtb25kMR4wHAYDVQQKExVNaWNy # b3NvZnQgQ29ycG9yYXRpb24xJjAkBgNVBAMTHU1pY3Jvc29mdCBUaW1lLVN0YW1w # IFBDQSAyMDEwMA0GCSqGSIb3DQEBCwUAAgUA6tQxrTAiGA8yMDI0MTEwNTA1NTQ1 # M1oYDzIwMjQxMTA2MDU1NDUzWjB0MDoGCisGAQQBhFkKBAExLDAqMAoCBQDq1DGt # AgEAMAcCAQACAgJcMAcCAQACAhNIMAoCBQDq1YMtAgEAMDYGCisGAQQBhFkKBAIx # KDAmMAwGCisGAQQBhFkKAwKgCjAIAgEAAgMHoSChCjAIAgEAAgMBhqAwDQYJKoZI # hvcNAQELBQADggEBAFhKgS6mNBAXD5U+6mnJ4GTPbLspyBTiJshcUGkERWX5RMAs # oZWg28+tcw+U41NquZxy1lZL81I/fM2u6QeT6M+/PRH2/29qFI6K1TA6QIKdLjIE # DsuVwPOKrDkqPEsF3LYppbZPD1x0unICEFMyb911mnC0LD6+FEkPiffeCNFf5mDG # nMrC30LUAOXaglFYy2IXSyXtpy4LnelKRUBDachJlJ9XLu0m3xTQ7jufl07H6iQ4 # /vYymQNVKXWNbdHap9S36phSWdX5C6B0em4Vqu72MV6T0pllZ/bElxgcjMf9dJGT # 6T6PuRB7q1iSJIHYPaBmvMkjGfye7VeRKRVZs8wxggQNMIIECQIBATCBkzB8MQsw # CQYDVQQGEwJVUzETMBEGA1UECBMKV2FzaGluZ3RvbjEQMA4GA1UEBxMHUmVkbW9u # ZDEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMSYwJAYDVQQDEx1NaWNy # b3NvZnQgVGltZS1TdGFtcCBQQ0EgMjAxMAITMwAAAe+JP1ahWMyo2gABAAAB7zAN # BglghkgBZQMEAgEFAKCCAUowGgYJKoZIhvcNAQkDMQ0GCyqGSIb3DQEJEAEEMC8G # CSqGSIb3DQEJBDEiBCDGIwfjqCjTmbPL+C4H06jnTQYSjic/2SjScs6C3qpC0TCB # +gYLKoZIhvcNAQkQAi8xgeowgecwgeQwgb0EIPBhKEW4Fo3wUz09NQx2a0DbcdsX # 8jovM5LizHmnyX+jMIGYMIGApH4wfDELMAkGA1UEBhMCVVMxEzARBgNVBAgTCldh # c2hpbmd0b24xEDAOBgNVBAcTB1JlZG1vbmQxHjAcBgNVBAoTFU1pY3Jvc29mdCBD # b3Jwb3JhdGlvbjEmMCQGA1UEAxMdTWljcm9zb2Z0IFRpbWUtU3RhbXAgUENBIDIw # MTACEzMAAAHviT9WoVjMqNoAAQAAAe8wIgQgi0d0nSrjTe+41q03EMA9lpqZ8vPc # qjp0wK6znzIh0uMwDQYJKoZIhvcNAQELBQAEggIAOVCVLgIA74UabSLMfM9xR9SH # TZWiiVZ6CpCfGUd+HG865/RLcA9W/cp8lvZ+SS3H1872MoDYElFfhv+mZqVYpHTm # +rtxrFP6Po+5JdzgjygEKPEkc5dFHJ3jKvobumSJN5s51rRt5LBhBtaztDYVMdfX # ttxANU/19WpRSr9Uvc3sFwJLW4sud7PdtKOIgJF/89oShkHFNy/PNa4e5pqI4vvB # bzTVktxAUcUjrz0yZv1Y1SbKrZuqEAwgUDpaWQEHBCUPjIL7ztwPfwEw6SJtXEHZ # guB34YE2ctwSJ3eihzMhpi4e8MhgzOSlSlzQInLymTEnibna5Mf3S4NaCi25OHf9 # S8Jdh3kmHaC0fI2BWA/U/ZIlXgxpEIeqWFltWyYn8Xhswue5v70T3wS6XcIus31c # C3YBD0v4yNjG1E0hb8GbvB4TP8EQwQCBkqy+hy1TTX3qRCDx2X9XXAOIz8vjaBkI # llfHNaGCzZe6n4SUbipgNkP2VNe0G8MY8AmIw3Ed/lm7z1U5BkJMD//PsO6cGqB7 # fnEJoprz+Oii43kntA9ZDE3ZNjqEyi4rTNKer9BGXnZ4XOcSOUylFko1qRDxNvIv # ar/W0faWZl4rUwhqrYL8le7hWhIAhgkjJj17hvs1qxVbG4w5vsoZZbN7MW8hV5Yo # ecDPS96Sbah6CdvA+94= # SIG # End signature block |