ADEssentials.psm1

function Get-WinADDFSHealth {
    [cmdletBinding()]
    param([string[]] $Domains,
        [Array] $DomainControllers,
        [int] $EventDays = 1)
    $Today = (Get-Date)
    $Yesterday = (Get-Date -Hour 0 -Second 0 -Minute 0 -Millisecond 0).AddDays(-$EventDays)
    if (-not $Domains) {
        $Forest = Get-ADForest
        $Domains = $Forest.Domains
    }
    [Array] $Table = foreach ($Domain in $Domains) {
        Write-Verbose "Get-WinADDFSHealth - Processing $Domain"
        if (-not $DomainControllers) { $DomainControllers = Get-ADDomainController -Filter * -Server $Domain } else { $DomainControllers = foreach ($_ in $DomainControllers) { Get-ADDomainController -Identity $_ -Server $Domain } }
        [Array]$GPOs = @(Get-GPO -All -Domain $Domain)
        try {
            $CentralRepository = Get-ChildItem -Path "\\$Domain\SYSVOL\$Domain\policies\PolicyDefinitions" -ErrorAction Stop
            $CentralRepositoryDomain = if ($CentralRepository) { $true } else { $false }
        } catch { $CentralRepositoryDomain = $false }
        foreach ($DC in $DomainControllers) {
            Write-Verbose "Get-WinADDFSHealth - Processing $DC for $Domain"
            $DCName = $DC.Name
            $Hostname = $DC.Hostname
            $DN = $DC.ComputerObjectDN
            $LocalSettings = "CN=DFSR-LocalSettings,$DN"
            $Subscriber = "CN=Domain System Volume,$LocalSettings"
            $Subscription = "CN=SYSVOL Subscription,$Subscriber"
            $ReplicationStatus = @{'0' = 'Uninitialized'
                '1' = 'Initialized'
                '2' = 'Initial synchronization'
                '3' = 'Auto recovery'
                '4' = 'Normal'
                '5' = 'In error state'
                '6' = 'Disabled'
                '7' = 'Unknown'
            }
            $DomainSummary = [ordered] @{"DomainController" = $DCName
                "Domain" = $Domain
                "Status" = $false
                "ReplicationState" = 'Unknown'
                "IsPDC" = $DC.OperationMasterRoles -contains 'PDCEmulator'
                "GroupPolicyCount" = $GPOs.Count
                "SYSVOLCount" = 0
                CentralRepository = $CentralRepositoryDomain
                CentralRepositoryDC = $false
                'IdenticalCount' = $false
                "Availability" = $false
                "MemberReference" = $false
                "DFSErrors" = 0
                "DFSEvents" = $null
                "DFSLocalSetting" = $false
                "DomainSystemVolume" = $false
                "SYSVOLSubscription" = $false
                "StopReplicationOnAutoRecovery" = $false
            }
            $DFSReplicatedFolderInfo = Get-CimData -NameSpace "root\microsoftdfs" -Class 'dfsrreplicatedfolderinfo' -ComputerName $Hostname
            $DomainSummary['ReplicationState'] = $ReplicationStatus["$($DFSReplicatedFolderInfo.State)"]
            try {
                $CentralRepositoryDC = Get-ChildItem -Path "\\$Hostname\SYSVOL\$Domain\policies\PolicyDefinitions" -ErrorAction Stop
                $DomainSummary['CentralRepositoryDC'] = if ($CentralRepositoryDC) { $true } else { $false }
            } catch { $DomainSummary['CentralRepositoryDC'] = $false }
            try {
                $MemberReference = (Get-ADObject $Subscriber -Properties msDFSR-MemberReference -Server $Domain -ErrorAction Stop).'msDFSR-MemberReference' -like "CN=$DCName,*"
                $DomainSummary['MemberReference'] = if ($MemberReference) { $true } else { $false }
            } catch { $DomainSummary['MemberReference'] = $false }
            try {
                $DFSLocalSetting = Get-ADObject $LocalSettings -Server $Domain -ErrorAction Stop
                $DomainSummary['DFSLocalSetting'] = if ($DFSLocalSetting) { $true } else { $false }
            } catch { $DomainSummary['DFSLocalSetting'] = $false }
            try {
                $DomainSystemVolume = Get-ADObject $Subscriber -Server $Domain -ErrorAction Stop
                $DomainSummary['DomainSystemVolume'] = if ($DomainSystemVolume) { $true } else { $false }
            } catch { $DomainSummary['DomainSystemVolume'] = $false }
            try {
                $SysVolSubscription = Get-ADObject $Subscription -Server $Domain -ErrorAction Stop
                $DomainSummary['SYSVOLSubscription'] = if ($SysVolSubscription) { $true } else { $false }
            } catch { $DomainSummary['SYSVOLSubscription'] = $false }
            try {
                [Array] $SYSVOL = Get-ChildItem -Path "\\$Hostname\SYSVOL\$Domain\Policies" -ErrorAction Stop
                $DomainSummary['SysvolCount'] = $SYSVOL.Count
            } catch { $DomainSummary['SysvolCount'] = 0 }
            if (Test-Connection $Hostname -ErrorAction SilentlyContinue) { $DomainSummary['Availability'] = $true } else { $DomainSummary['Availability'] = $false }
            try {
                [Array] $Events = Get-Events -LogName "DFS Replication" -Level Error -ComputerName $Hostname -DateFrom $Yesterday -DateTo $Today
                $DomainSummary['DFSErrors'] = $Events.Count
                $DomainSummary['DFSEvents'] = $Events
            } catch { $DomainSummary['DFSErrors'] = $null }
            $DomainSummary['IdenticalCount'] = $DomainSummary['GroupPolicyCount'] -eq $DomainSummary['SYSVOLCount']
            $Registry = Get-PSRegistry -RegistryPath "HKLM\SYSTEM\CurrentControlSet\Services\DFSR\Parameters" -ComputerName $Hostname
            if ($null -ne $Registry.StopReplicationOnAutoRecovery) { $DomainSummary['StopReplicationOnAutoRecovery'] = [bool] $Registry.StopReplicationOnAutoRecovery } else { $DomainSummary['StopReplicationOnAutoRecovery'] = $null }
            $All = @($DomainSummary['SYSVOLSubscription']
                $DomainSummary['ReplicationState'] -eq 'Normal'
                $DomainSummary['DomainSystemVolume']
                $DomainSummary['DFSLocalSetting']
                $DomainSummary['MemberReference']
                $DomainSummary['Availability']
                $DomainSummary['IdenticalCount']
                $DomainSummary['DFSErrors'] -eq 0)
            $DomainSummary['Status'] = $All -notcontains $false
            [PSCustomObject] $DomainSummary
        }
    }
    $Table
}
function Get-WinADForestReplication {
    [CmdletBinding()]
    param([switch] $Extended,
        [Array] $DomainControllers)
    if (-not $DomainControllers) { $DomainControllers = Get-WinADForestControllers }
    $ProcessErrors = [System.Collections.Generic.List[PSCustomObject]]::new()
    $Replication = foreach ($DC in $DomainControllers) {
        try { Get-ADReplicationPartnerMetadata -Target $DC.HostName -Partition * -ErrorAction Stop } catch {
            Write-Warning -Message "Get-WinADForestReplication - Error on server $($_.Exception.ServerName): $($_.Exception.Message)"
            $ProcessErrors.Add([PSCustomObject] @{Server = $_.Exception.ServerName; StatusMessage = $_.Exception.Message })
        }
    }
    foreach ($_ in $Replication) {
        $ServerPartner = (Resolve-DnsName -Name $_.PartnerAddress -Verbose:$false -ErrorAction SilentlyContinue)
        $ServerInitiating = (Resolve-DnsName -Name $_.Server -Verbose:$false -ErrorAction SilentlyContinue)
        $ReplicationObject = [ordered] @{Server = $_.Server
            ServerIPV4 = $ServerInitiating.IP4Address
            ServerPartner = $ServerPartner.NameHost
            ServerPartnerIPV4 = $ServerPartner.IP4Address
            LastReplicationAttempt = $_.LastReplicationAttempt
            LastReplicationResult = $_.LastReplicationResult
            LastReplicationSuccess = $_.LastReplicationSuccess
            ConsecutiveReplicationFailures = $_.ConsecutiveReplicationFailures
            LastChangeUsn = $_.LastChangeUsn
            PartnerType = $_.PartnerType
            Partition = $_.Partition
            TwoWaySync = $_.TwoWaySync
            ScheduledSync = $_.ScheduledSync
            SyncOnStartup = $_.SyncOnStartup
            CompressChanges = $_.CompressChanges
            DisableScheduledSync = $_.DisableScheduledSync
            IgnoreChangeNotifications = $_.IgnoreChangeNotifications
            IntersiteTransport = $_.IntersiteTransport
            IntersiteTransportGuid = $_.IntersiteTransportGuid
            IntersiteTransportType = $_.IntersiteTransportType
            UsnFilter = $_.UsnFilter
            Writable = $_.Writable
            Status = if ($_.LastReplicationResult -ne 0) { $false } else { $true }
            StatusMessage = "Last successful replication time was $($_.LastReplicationSuccess), Consecutive Failures: $($_.ConsecutiveReplicationFailures)"
        }
        if ($Extended) {
            $ReplicationObject.Partner = $_.Partner
            $ReplicationObject.PartnerAddress = $_.PartnerAddress
            $ReplicationObject.PartnerGuid = $_.PartnerGuid
            $ReplicationObject.PartnerInvocationId = $_.PartnerInvocationId
            $ReplicationObject.PartitionGuid = $_.PartitionGuid
        }
        [PSCustomObject] $ReplicationObject
    }
    foreach ($_ in $ProcessErrors) {
        if ($null -ne $_.Server) { $ServerInitiating = (Resolve-DnsName -Name $_.Server -Verbose:$false -ErrorAction SilentlyContinue) } else { $ServerInitiating = [PSCustomObject] @{IP4Address = '127.0.0.1' } }
        $ReplicationObject = [ordered] @{Server = $_.Server
            ServerIPV4 = $ServerInitiating.IP4Address
            ServerPartner = 'Unknown'
            ServerPartnerIPV4 = '127.0.0.1'
            LastReplicationAttempt = $null
            LastReplicationResult = $null
            LastReplicationSuccess = $null
            ConsecutiveReplicationFailures = $null
            LastChangeUsn = $null
            PartnerType = $null
            Partition = $null
            TwoWaySync = $null
            ScheduledSync = $null
            SyncOnStartup = $null
            CompressChanges = $null
            DisableScheduledSync = $null
            IgnoreChangeNotifications = $null
            IntersiteTransport = $null
            IntersiteTransportGuid = $null
            IntersiteTransportType = $null
            UsnFilter = $null
            Writable = $null
            Status = $false
            StatusMessage = $_.StatusMessage
        }
        if ($Extended) {
            $ReplicationObject.Partner = $null
            $ReplicationObject.PartnerAddress = $null
            $ReplicationObject.PartnerGuid = $null
            $ReplicationObject.PartnerInvocationId = $null
            $ReplicationObject.PartitionGuid = $null
        }
        [PSCustomObject] $ReplicationObject
    }
}
Function Get-WinADGPOMissingPermissions {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER Domain
    Parameter description
 
    .EXAMPLE
    An example
 
    .NOTES
    Based on https://secureinfra.blog/2018/12/31/most-common-mistakes-in-active-directory-and-domain-services-part-1/
    #>

    [cmdletBinding()]
    param([string] $Domain = $Env:USERDNSDOMAIN)
    $GPOs = Get-GPO -All -Domain $Domain
    $MissingPermissions = @(foreach ($GPO in $GPOs) {
            If ($GPO.User.Enabled) {
                $GPOPermissionForAuthUsers = Get-GPPermission -Guid $GPO.Id -All | Select-Object -ExpandProperty Trustee | Where-Object { $_.Name -eq "Authenticated Users" }
                $GPOPermissionForDomainComputers = Get-GPPermission -Guid $GPO.Id -All | Select-Object -ExpandProperty Trustee | Where-Object { $_.Name -eq "Domain Computers" }
                If (-not $GPOPermissionForAuthUsers -and -not $GPOPermissionForDomainComputers) { $GPO }
            }
        })
    $MissingPermissions
}
function Get-WinADGPOSysvolFolders {
    [alias('Get-WinADGPOSysvol')]
    [cmdletBinding()]
    param([Array] $GPOs,
        [string] $Domain = $EnV:USERDNSDOMAIN,
        [Array] $ComputerName)
    if (-not $ComputerName) { $ComputerName = Get-ADDomainController -Filter * -Server $Domain -ErrorAction SilentlyContinue } else { $ComputerName = foreach ($_ in $ComputerName) { Get-ADDomainController -Identity $_ -ErrorAction SilentlyContinue -Server $Domain } }
    if (-not $GPOs) { [Array]$GPOs = @(Get-GPO -All -Domain $Domain) }
    foreach ($Server in $ComputerName) {
        $Differences = @{ }
        $SysvolHash = @{ }
        $GPOGUIDS = $GPOs.ID.GUID
        try { $SYSVOL = Get-ChildItem -Path "\\$Server\SYSVOL\$Domain\Policies" -ErrorAction Stop } catch { $Sysvol = $Null }
        foreach ($_ in $SYSVOL) {
            $GUID = $_.Name -replace '{' -replace '}'
            $SysvolHash[$GUID] = $_
        }
        $Files = $SYSVOL.Name -replace '{' -replace '}'
        if ($Files -ne '') {
            $Comparing = Compare-Object -ReferenceObject $GPOGUIDS -DifferenceObject $Files -IncludeEqual
            foreach ($_ in $Comparing) {
                if ($_.SideIndicator -eq '==') { $Found = 'Exists' } elseif ($_.SideIndicator -eq '<=') { $Found = 'Not available on SYSVOL' } else { $Found = 'Orphaned GPO' }
                $Differences[$_.InputObject] = $Found
            }
        } else { }
        $GPOSummary = @(foreach ($GPO in $GPOS) {
                if ($null -ne $SysvolHash[$GPO.Id.GUID].FullName) { $ACL = Get-Acl -Path $SysvolHash[$GPO.Id.GUID].FullName } else { $ACL = $null }
                [PSCustomObject] @{DisplayName = $GPO.DisplayName
                    DomainName = $GPO.DomainName
                    SysvolServer = $Server.HostName
                    SysvolStatus = if ($null -eq $Differences[$GPO.Id.Guid]) { 'Not available on SYSVOL' } else { $Differences[$GPO.Id.Guid] }
                    Owner = $GPO.Owner
                    FileOwner = $ACL.Owner
                    Id = $GPO.Id.Guid
                    GpoStatus = $GPO.GpoStatus
                    Description = $GPO.Description
                    CreationTime = $GPO.CreationTime
                    ModificationTime = $GPO.ModificationTime
                    UserVersion = $GPO.UserVersion
                    ComputerVersion = $GPO.ComputerVersion
                    WmiFilter = $GPO.WmiFilter
                }
            }
            foreach ($_ in $Differences.Keys) {
                if ($Differences[$_] -eq 'Orphaned GPO') {
                    if ($SysvolHash[$_].BaseName -notcontains 'PolicyDefinitions') {
                        if ($null -ne $SysvolHash[$_].FullName) { $ACL = Get-Acl -Path $SysvolHash[$_].FullName } else { $ACL = $null }
                        [PSCustomObject] @{DisplayName = $SysvolHash[$_].BaseName
                            DomainName = $Domain
                            SysvolServer = $Server.HostName
                            SysvolStatus = $Differences[$GPO.Id.Guid]
                            Owner = $ACL.Owner
                            FileOwner = $ACL.Owner
                            Id = $_
                            GpoStatus = 'Orphaned'
                            Description = $null
                            CreationTime = $SysvolHash[$_].CreationTime
                            ModificationTime = $SysvolHash[$_].LastWriteTime
                            UserVersion = $null
                            ComputerVersion = $null
                            WmiFilter = $null
                        }
                    }
                }
            })
        $GPOSummary
    }
}
function Get-WinADLastBackup {
    <#
    .SYNOPSIS
    Gets Active directory forest or domain last backup time
 
    .DESCRIPTION
    Gets Active directory forest or domain last backup time
 
    .PARAMETER Domain
    Optionally you can pass Domains by hand
 
    .EXAMPLE
    $LastBackup = Get-WinADLastBackup
    $LastBackup | Format-Table -AutoSize
 
    .EXAMPLE
    $LastBackup = Get-WinADLastBackup -Domain 'ad.evotec.pl'
    $LastBackup | Format-Table -AutoSize
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param([string[]] $Domains)
    $NameUsed = [System.Collections.Generic.List[string]]::new()
    [DateTime] $CurrentDate = Get-Date
    if (-not $Domains) {
        try {
            $Forest = Get-ADForest -ErrorAction Stop
            $Domains = $Forest.Domains
        } catch { Write-Warning "Get-WinADLastBackup - Failed to gather Forest Domains $($_.Exception.Message)" }
    }
    foreach ($Domain in $Domains) {
        try {
            [string[]]$Partitions = (Get-ADRootDSE -Server $Domain -ErrorAction Stop).namingContexts
            [System.DirectoryServices.ActiveDirectory.DirectoryContextType] $contextType = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain
            [System.DirectoryServices.ActiveDirectory.DirectoryContext] $context = New-Object System.DirectoryServices.ActiveDirectory.DirectoryContext($contextType, $Domain)
            [System.DirectoryServices.ActiveDirectory.DomainController] $domainController = [System.DirectoryServices.ActiveDirectory.DomainController]::FindOne($context)
        } catch { Write-Warning "Get-WinADLastBackup - Failed to gather partitions information for $Domain with error $($_.Exception.Message)" }
        $Output = ForEach ($Name in $Partitions) {
            if ($NameUsed -contains $Name) { continue } else { $NameUsed.Add($Name) }
            $domainControllerMetadata = $domainController.GetReplicationMetadata($Name)
            $dsaSignature = $domainControllerMetadata.Item("dsaSignature")
            $LastBackup = [DateTime] $($dsaSignature.LastOriginatingChangeTime)
            [PSCustomObject] @{Domain = $Domain
                NamingContext = $Name
                LastBackup = $LastBackup
                LastBackupDaysAgo = - (Convert-TimeToDays -StartTime ($CurrentDate) -EndTime ($LastBackup))
            }
        }
        $Output
    }
}
function Get-WinADLMSettings {
    param([string] $DomainController)
    $LSA = Get-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Control\Lsa' -ComputerName $DomainController
    if ($Lsa) {
        if ($LSA.lmcompatibilitylevel) { $LMCompatibilityLevel = $LSA.lmcompatibilitylevel } else { $LMCompatibilityLevel = 3 }
        $LM = @{0 = 'Server sends LM and NTLM response and never uses extended session security. Clients use LM and NTLM authentication, and never use extended session security. DCs accept LM, NTLM, and NTLM v2 authentication.'
            1 = 'Servers use NTLM v2 session security if it is negotiated. Clients use LM and NTLM authentication and use extended session security if the server supports it. DCs accept LM, NTLM, and NTLM v2 authentication.'
            2 = 'Server sends NTLM response only. Clients use only NTLM authentication and use extended session security if the server supports it. DCs accept LM, NTLM, and NTLM v2 authentication.'
            3 = 'Server sends NTLM v2 response only. Clients use NTLM v2 authentication and use extended session security if the server supports it. DCs accept LM, NTLM, and NTLM v2 authentication.'
            4 = 'DCs refuse LM responses. Clients use NTLM authentication and use extended session security if the server supports it. DCs refuse LM authentication but accept NTLM and NTLM v2 authentication.'
            5 = 'DCs refuse LM and NTLM responses, and accept only NTLM v2. Clients use NTLM v2 authentication and use extended session security if the server supports it. DCs refuse NTLM and LM authentication, and accept only NTLM v2 authentication.'
        }
        [PSCustomObject] @{LSAProtectionCredentials = [bool] $LSA.RunAsPPL
            Level = $LMCompatibilityLevel
            LevelDescription = $LM[$LMCompatibilityLevel]
            LimitBlankPasswordUse = [bool] $LSA.LimitBlankPasswordUse
            NoLmHash = [bool] $LSA.NoLmHash
            DisableDomainCreds = [bool] $LSA.disabledomaincreds
            ForceGuest = [bool] $LSA.forceguest
            RestrictAnonymous = [bool] $LSA.restrictanonymous
            RestrictAnonymousSAM = [bool] $LSA.restrictanonymoussam
            SecureBoot = [bool] $LSA.SecureBoot
            LsaCfgFlagsDefault = $LSA.LsaCfgFlagsDefault
            LSAPid = $LSA.LSAPid
            AuditBaseDirectories = [bool] $LSA.auditbasedirectories
            AuditBaseObjects = [bool] $LSA.auditbaseobjects
            CrashOnAuditFail = $LSA.CrashOnAuditFail
        }
    }
}
Function Get-WinADPriviligedObjects {
    [cmdletbinding()]
    param([switch] $LegitimateOnly,
        [switch] $OrphanedOnly,
        [switch] $Unique,
        [switch] $SummaryOnly)
    $Forest = Get-ADForest
    $Domains = $Forest.Domains
    $UsersWithAdminCount = foreach ($domain in $Domains) {
        $Objects = Get-ADObject -filter 'admincount -eq 1 -and iscriticalsystemobject -notlike "*"' -server $domain -properties whenchanged, whencreated, admincount, isCriticalSystemObject, "msDS-ReplAttributeMetaData", samaccountname
        foreach ($_ in $Objects) {
            [PSCustomObject] @{Domain = $Domain
                distinguishedname = $_.distinguishedname
                whenchanged = $_.whenchanged
                whencreated = $_.whencreated
                admincount = $_.admincount
                SamAccountName = $_.SamAccountName
                objectclass = $_.objectclass
                isCriticalSystemObject = $_.isCriticalSystemObject
                adminCountDate = ($_.'msDS-ReplAttributeMetaData' | ForEach-Object { ([XML]$_.Replace("`0", "")).DS_REPL_ATTR_META_DATA | Where-Object { $_.pszAttributeName -eq "admincount" } }).ftimeLastOriginatingChange | Get-Date -Format MM/dd/yyyy
            }
        }
    }
    $CriticalGroups = foreach ($domain in $Domains) { Get-ADGroup -filter 'admincount -eq 1 -and iscriticalsystemobject -eq $true' -server $domain | Select-Object @{name = 'Domain'; expression = { $domain } }, distinguishedname }
    $AdminCountLegitimate = [System.Collections.Generic.List[PSCustomObject]]::new()
    $AdminCountOrphaned = [System.Collections.Generic.List[PSCustomObject]]::new()
    $AdminCountAll = foreach ($object in $UsersWithAdminCount) {
        $DistinguishedName = ($object).distinguishedname
        $Results = foreach ($Group in $CriticalGroups) {
            $IsMember = if (Get-ADGroup -Filter { Member -RecursiveMatch $DistinguishedName } -searchbase $Group.DistinguishedName -server $Group.Domain) { $True } else { $False }
            $User = [PSCustomObject] @{DistinguishedName = $Object.DistinguishedName
                Domain = $Object.domain
                IsMember = $IsMember
                Admincount = $Object.admincount
                AdminCountDate = $Object.adminCountDate
                Whencreated = $Object.whencreated
                ObjectClass = $Object.objectclass
                GroupDomain = if ($IsMember) { $Group.Domain } else { $null }
                GroupDistinguishedname = if ($IsMember) { $Group.DistinguishedName } else { $null }
            }
            if ($User.IsMember) {
                $AdminCountLegitimate.Add($User)
                $User
            }
            if ($User.IsMember -eq $false -and $AdminCountLegitimate.DistinguishedName -notcontains $User.DistinguishedName -and $AdminCountOrphaned.DistinguishedName -notcontains $User.DistinguishedName) {
                $Properties = @('distinguishedname'
                    'domain'
                    'IsMember'
                    'admincount'
                    'adminCountDate'
                    'whencreated'
                    'objectclass')
                $AdminCountOrphaned.Add(($User | Select-Object -Property $Properties))
                $User
            }
        }
        $Results
    }
    $Output = @(if ($OrphanedOnly) { $AdminCountOrphaned } elseif ($LegitimateOnly) { if ($Unique) { $AdminCountLegitimate | Select-Object -Property DistinguishedName, Domain, IsMember, Admincount, AdminCountDate, Whencreated, ObjectClass -Unique } else { $AdminCountLegitimate } } else { if ($Unique) { $AdminCountAll | Select-Object -Property DistinguishedName, Domain, IsMember, Admincount, AdminCountDate, Whencreated, ObjectClass -Unique } else { $AdminCountAll } })
    if ($SummaryOnly) { $Output | Group-Object ObjectClass | Select-Object -Property Name, Count } else { $Output }
}
function Get-WinADSiteConnections {
    [CmdletBinding()]
    param([alias('Joiner')][string] $Splitter,
        [string] $Formatted)
    [Flags()]
    enum ConnectionOption {
        None
        IsGenerated
        TwoWaySync
        OverrideNotifyDefault = 4
        UseNotify = 8
        DisableIntersiteCompression = 16
        UserOwnedSchedule = 32
        RodcTopology = 64
    }
    $NamingContext = (Get-ADRootDSE).configurationNamingContext
    $Connections = Get-ADObject â€“Searchbase $NamingContext -LDAPFilter "(objectCategory=ntDSConnection)" -Properties *
    $FormmatedConnections = foreach ($_ in $Connections) {
        if ($null -eq $_.Options) { $Options = 'None' } else { $Options = ([ConnectionOption] $_.Options) -split ', ' }
        if ($Formatted) {
            $Dictionary = [PSCustomObject] @{'CN' = $_.CN
                'Description' = $_.Description
                'Display Name' = $_.DisplayName
                'Enabled Connection' = $_.enabledConnection
                'Server From' = if ($_.fromServer -match '(?<=CN=NTDS Settings,CN=)(.*)(?=,CN=Servers,)') { $Matches[0] } else { $_.fromServer }
                'Server To' = if ($_.DistinguishedName -match '(?<=CN=NTDS Settings,CN=)(.*)(?=,CN=Servers,)') { $Matches[0] } else { $_.fromServer }
                'Site From' = if ($_.fromServer -match '(?<=,CN=Servers,CN=)(.*)(?=,CN=Sites,CN=Configuration)') { $Matches[0] } else { $_.fromServer }
                'Site To' = if ($_.DistinguishedName -match '(?<=,CN=Servers,CN=)(.*)(?=,CN=Sites,CN=Configuration)') { $Matches[0] } else { $_.fromServer }
                'Options' = if ($Splitter -ne '') { $Options -Join $Splitter } else { $Options }
                'When Created' = $_.WhenCreated
                'When Changed' = $_.WhenChanged
                'Is Deleted' = $_.IsDeleted
            }
        } else {
            $Dictionary = [PSCustomObject] @{CN = $_.CN
                Description = $_.Description
                DisplayName = $_.DisplayName
                EnabledConnection = $_.enabledConnection
                ServerFrom = if ($_.fromServer -match '(?<=CN=NTDS Settings,CN=)(.*)(?=,CN=Servers,)') { $Matches[0] } else { $_.fromServer }
                ServerTo = if ($_.DistinguishedName -match '(?<=CN=NTDS Settings,CN=)(.*)(?=,CN=Servers,)') { $Matches[0] } else { $_.fromServer }
                SiteFrom = if ($_.fromServer -match '(?<=,CN=Servers,CN=)(.*)(?=,CN=Sites,CN=Configuration)') { $Matches[0] } else { $_.fromServer }
                SiteTo = if ($_.DistinguishedName -match '(?<=,CN=Servers,CN=)(.*)(?=,CN=Sites,CN=Configuration)') { $Matches[0] } else { $_.fromServer }
                Options = if ($Splitter -ne '') { $Options -Join $Splitter } else { $Options }
                WhenCreated = $_.WhenCreated
                WhenChanged = $_.WhenChanged
                IsDeleted = $_.IsDeleted
            }
        }
        $Dictionary
    }
    $FormmatedConnections
}
function Get-WinADSiteLinks {
    [CmdletBinding()]
    param([alias('Joiner')][string] $Splitter,
        [string] $Formatted)
    [Flags()]
    enum SiteLinksOptions {
        None = 0
        UseNotify = 1
        TwoWaySync = 2
        DisableCompression = 4
    }
    $NamingContext = (Get-ADRootDSE).configurationNamingContext
    $SiteLinks = Get-ADObject -LDAPFilter "(objectCategory=sitelink)" â€“Searchbase $NamingContext -Properties *
    foreach ($_ in $SiteLinks) {
        if ($null -eq $_.Options) { $Options = 'None' } else { $Options = ([SiteLinksOptions] $_.Options) -split ', ' }
        if ($Formatted) {
            [PSCustomObject] @{Name = $_.CN
                Cost = $_.Cost
                'Replication Frequency In Minutes' = $_.ReplInterval
                Options = if ($Splitter -ne '') { $Options -Join $Splitter } else { $Options }
                Created = $_.WhenCreated
                Modified = $_.WhenChanged
                'Protected From Accidental Deletion' = $_.ProtectedFromAccidentalDeletion
            }
        } else {
            [PSCustomObject] @{Name = $_.CN
                Cost = $_.Cost
                ReplicationFrequencyInMinutes = $_.ReplInterval
                Options = if ($Splitter -ne '') { $Options -Join $Splitter } else { $Options }
                Created = $_.WhenCreated
                Modified = $_.WhenChanged
                ProtectedFromAccidentalDeletion = $_.ProtectedFromAccidentalDeletion
            }
        }
    }
}
function Get-WinADUsersForeignSecurityPrincipalList {
    [alias('Get-WinADUsersFP')]
    param([string] $Domain)
    $ForeignSecurityPrincipalList = Get-ADObject -Filter { ObjectClass -eq 'ForeignSecurityPrincipal' } -Properties * -Server $Domain
    foreach ($FSP in $ForeignSecurityPrincipalList) {
        Try { $Translated = (([System.Security.Principal.SecurityIdentifier]::new($FSP.objectSid)).Translate([System.Security.Principal.NTAccount])).Value } Catch { $Translated = $null }
        Add-Member -InputObject $FSP -Name 'TranslatedName' -Value $Translated -MemberType NoteProperty -Force
    }
    $ForeignSecurityPrincipalList
}
function Set-WinADReplication {
    [CmdletBinding()]
    param([int] $ReplicationInterval = 15,
        [switch] $Instant)
    $NamingContext = (Get-ADRootDSE).configurationNamingContext
    Get-ADObject -LDAPFilter "(objectCategory=sitelink)" â€“Searchbase $NamingContext -Properties options | ForEach-Object { if ($Instant) {
            Set-ADObject $_ -replace @{replInterval = $ReplicationInterval }
            Set-ADObject $_ â€“replace @{options = $($_.options -bor 1) }
        } else { Set-ADObject $_ -replace @{replInterval = $ReplicationInterval } } }
}
function Set-WinADReplicationConnections {
    [CmdletBinding()]
    param([switch] $Force)
    [Flags()]
    enum ConnectionOption {
        None
        IsGenerated
        TwoWaySync
        OverrideNotifyDefault = 4
        UseNotify = 8
        DisableIntersiteCompression = 16
        UserOwnedSchedule = 32
        RodcTopology = 64
    }
    $NamingContext = (Get-ADRootDSE).configurationNamingContext
    $Connections = Get-ADObject â€“Searchbase $NamingContext -LDAPFilter "(objectCategory=ntDSConnection)" -Properties *
    foreach ($_ in $Connections) {
        $OptionsTranslated = [ConnectionOption] $_.Options
        if ($OptionsTranslated -like '*IsGenerated*' -and -not $Force) { Write-Verbose "Set-WinADReplicationConnections - Skipping $($_.CN) automatically generated link" } else {
            Write-Verbose "Set-WinADReplicationConnections - Changing $($_.CN)"
            Set-ADObject $_ â€“replace @{options = $($_.options -bor 8) }
        }
    }
}
function Sync-DomainController {
    [CmdletBinding()]
    param([string] $Domain = $Env:USERDNSDOMAIN)
    $DistinguishedName = (Get-ADDomain -Server $Domain).DistinguishedName
    (Get-ADDomainController -Filter * -Server $Domain).Name | ForEach-Object { Write-Verbose -Message "Sync-DomainController - Forcing synchronization $_"
        repadmin /syncall $_ $DistinguishedName /e /A | Out-Null }
}
function Test-ADDomainController {
    [CmdletBinding()]
    param([string[]] $ComputerName,
        [Parameter(Mandatory = $false)][PSCredential] $Credential = $null)
    $CredentialParameter = @{ }
    if ($null -ne $Credential) { $CredentialParameter['Credential'] = $Credential }
    $Output = foreach ($Computer in $ComputerName) {
        $Result = Invoke-Command -ComputerName $Computer -ScriptBlock { dcdiag.exe /v /c /Skip:OutboundSecureChannels } @CredentialParameter
        for ($Line = 0; $Line -lt $Result.length; $Line++) {
            if ($Result[$Line] -match '^\s{9}.{25} (\S+) (\S+) test$') { $Result[$Line] = $Result[$Line] + ' ' + $Result[$Line + 2].Trim() }
            if ($Result[$Line] -match '^\s{6}Starting test: \S+$') { $LineStart = $Line }
            if ($Result[$Line] -match '^\s{9}.{25} (\S+) (\S+) test (\S+)$') {
                $DiagnosticResult = [PSCustomObject] @{ComputerName = $Computer
                    Target = $Matches[1]
                    Test = $Matches[3]
                    Result = $Matches[2] -eq 'passed'
                    Data = $Result[$LineStart..$Line] -join [System.Environment]::NewLine
                }
                $DiagnosticResult
            }
        }
    }
    $Output
}
function Test-ADRolesAvailability {
    [cmdletBinding()]
    param([string] $Domain)
    $ADModule = Import-Module PSWinDocumentation.AD -PassThru
    $Roles = & $ADModule { param($Domain); Get-WinADForestRoles -Domain $Domain } $Domain
    if ($Domain -ne '') {
        [PSCustomObject] @{PDCEmulator = $Roles['PDCEmulator']
            PDCEmulatorAvailability = if ($Roles['PDCEmulator']) { (Test-NetConnection -ComputerName $Roles['PDCEmulator']).PingSucceeded } else { $false }
            RIDMaster = $Roles['RIDMaster']
            RIDMasterAvailability = if ($Roles['RIDMaster']) { (Test-NetConnection -ComputerName $Roles['RIDMaster']).PingSucceeded } else { $false }
            InfrastructureMaster = $Roles['InfrastructureMaster']
            InfrastructureMasterAvailability = if ($Roles['InfrastructureMaster']) { (Test-NetConnection -ComputerName $Roles['InfrastructureMaster']).PingSucceeded } else { $false }
        }
    } else {
        [PSCustomObject] @{SchemaMaster = $Roles['SchemaMaster']
            SchemaMasterAvailability = if ($Roles['SchemaMaster']) { (Test-NetConnection -ComputerName $Roles['SchemaMaster']).PingSucceeded } else { $false }
            DomainNamingMaster = $Roles['DomainNamingMaster']
            DomainNamingMasterAvailability = if ($Roles['DomainNamingMaster']) { (Test-NetConnection -ComputerName $Roles['DomainNamingMaster']).PingSucceeded } else { $false }
        }
    }
}
function Test-ADSiteLinks {
    [cmdletBinding()]
    param([string] $Splitter)
    [Array] $SiteLinks = Get-WinADSiteConnections
    $Collection = @($SiteLinks).Where( { $_.Options -notcontains 'IsGenerated' -and $_.EnabledConnection -eq $true }, 'Split')
    $LinksManual = foreach ($Link in $Collection[0]) { "$($Link.ServerFrom) to $($Link.ServerTo)" }
    $LinksAutomatic = foreach ($Link in $Collection[1]) { "$($Link.ServerFrom) to $($Link.ServerTo)" }
    $CollectionNotifications = @($SiteLinks).Where( { $_.Options -notcontains 'UseNotify' -and $_.EnabledConnection -eq $true }, 'Split')
    $LinksNotUsingNotifications = foreach ($Link in $CollectionNotifications[0]) { "$($Link.ServerFrom) to $($Link.ServerTo)" }
    $LinksUsingNotifications = foreach ($Link in $CollectionNotifications[1]) { "$($Link.ServerFrom) to $($Link.ServerTo)" }
    [ordered] @{SiteLinksManual = if ($Splitter -eq '') { $LinksManual } else { $LinksManual -join $Splitter }
        SiteLinksAutomatic = if ($Splitter -eq '') { $LinksAutomatic } else { $LinksAutomatic -join $Splitter }
        SiteLinksUseNotify = if ($Splitter -eq '') { $LinksUsingNotifications } else { $LinksUsingNotifications -join $Splitter }
        SiteLinksNotUsingNotify = if ($Splitter -eq '') { $LinksNotUsingNotifications } else { $LinksNotUsingNotifications -join $Splitter }
        SiteLinksUseNotifyCount = $CollectionNotifications[1].Count
        SiteLinksNotUsingNotifyCount = $CollectionNotifications[0].Count
        SiteLinksManualCount = $Collection[0].Count
        SiteLinksAutomaticCount = $Collection[1].Count
        SiteLinksTotalCount = ($SiteLinks | Where-Object { $_.EnabledConnection -eq $true }).Count
    }
}
function Test-DNSNameServers {
    [cmdletBinding()]
    param([string] $DomainController,
        [string] $Domain)
    if ($DomainController) {
        $AllDomainControllers = (Get-ADDomainController -Server $Domain -Filter { IsReadOnly -eq $false }).HostName
        try {
            $Hosts = Get-DnsServerResourceRecord -ZoneName $Domain -ComputerName $DomainController -RRType NS -ErrorAction Stop
            $NameServers = (($Hosts | Where-Object { $_.HostName -eq '@' }).RecordData.NameServer) -replace ".$"
            $Compare = ((Compare-Object -ReferenceObject $AllDomainControllers -DifferenceObject $NameServers -IncludeEqual).SideIndicator -notin @('=>', '<='))
            [PSCustomObject] @{DomainControllers = $AllDomainControllers
                NameServers = $NameServers
                Status = $Compare
                Comment = "Name servers found $($NameServers -join ', ')"
            }
        } catch {
            [PSCustomObject] @{DomainControllers = $AllDomainControllers
                NameServers = $null
                Status = $false
                Comment = $_.Exception.Message
            }
        }
    }
}
function Test-FSMORolesAvailability {
    [cmdletBinding()]
    param([string] $Domain = $Env:USERDNSDOMAIN)
    $DC = Get-ADDomainController -Server $Domain -Filter *
    $Output = foreach ($S in $DC) {
        if ($S.OperationMasterRoles.Count -gt 0) { $Status = Test-Connection -ComputerName $S.HostName -Count 2 -Quiet } else { $Status = $null }
        foreach ($_ in $S.OperationMasterRoles) {
            [PSCustomObject] @{Role = $_
                HostName = $S.HostName
                Status = $Status
            }
        }
    }
    $Output
}
Function Test-LDAP {
    [CmdletBinding()]
    param ([alias('Server', 'IpAddress')][Parameter(Mandatory = $True)][string[]]$ComputerName,
        [int] $GCPortLDAP = 3268,
        [int] $GCPortLDAPSSL = 3269,
        [int] $PortLDAP = 389,
        [int] $PortLDAPS = 636)
    foreach ($Computer in $ComputerName) {
        [Array] $ADServerFQDN = (Resolve-DnsName -Name $Computer -ErrorAction SilentlyContinue)
        if ($ADServerFQDN) {
            if ($ADServerFQDN.NameHost) { $ServerName = $ADServerFQDN[0].NameHost } else {
                [Array] $ADServerFQDN = (Resolve-DnsName -Name $Computer -ErrorAction SilentlyContinue)
                $FilterName = $ADServerFQDN | Where-Object { $_.QueryType -eq 'A' }
                $ServerName = $FilterName[0].Name
            }
        } else { $ServerName = '' }
        $GlobalCatalogSSL = Test-LDAPPorts -ServerName $ServerName -Port $GCPortLDAPSSL
        $GlobalCatalogNonSSL = Test-LDAPPorts -ServerName $ServerName -Port $GCPortLDAP
        $ConnectionLDAPS = Test-LDAPPorts -ServerName $ServerName -Port $PortLDAPS
        $ConnectionLDAP = Test-LDAPPorts -ServerName $ServerName -Port $PortLDAP
        $PortsThatWork = @(if ($GlobalCatalogNonSSL) { $GCPortLDAP }
            if ($GlobalCatalogSSL) { $GCPortLDAPSSL }
            if ($ConnectionLDAP) { $PortLDAP }
            if ($ConnectionLDAPS) { $PortLDAPS }) | Sort-Object
        [pscustomobject]@{Computer = $Computer
            ComputerFQDN = $ServerName
            GlobalCatalogLDAP = $GlobalCatalogNonSSL
            GlobalCatalogLDAPS = $GlobalCatalogSSL
            LDAP = $ConnectionLDAP
            LDAPS = $ConnectionLDAPS
            AvailablePorts = $PortsThatWork -join ','
        }
    }
}
function Test-LDAPPorts {
    [CmdletBinding()]
    param([string] $ServerName,
        [int] $Port)
    if ($ServerName -and $Port -ne 0) {
        try {
            $LDAP = "LDAP://" + $ServerName + ':' + $Port
            $Connection = [ADSI]($LDAP)
            $Connection.Close()
            return $true
        } catch { if ($_.Exception.ToString() -match "The server is not operational") { Write-Warning "Can't open $ServerName`:$Port." } elseif ($_.Exception.ToString() -match "The user name or password is incorrect") { Write-Warning "Current user ($Env:USERNAME) doesn't seem to have access to to LDAP on port $Server`:$Port" } else { Write-Warning -Message $_ } }
        return $False
    }
}
Export-ModuleMember -Function @('Get-WinADDFSHealth', 'Get-WinADForestReplication', 'Get-WinADGPOMissingPermissions', 'Get-WinADGPOSysvolFolders', 'Get-WinADLastBackup', 'Get-WinADLMSettings', 'Get-WinADPriviligedObjects', 'Get-WinADSiteConnections', 'Get-WinADSiteLinks', 'Get-WinADUsersForeignSecurityPrincipalList', 'Set-WinADReplication', 'Set-WinADReplicationConnections', 'Sync-DomainController', 'Test-ADDomainController', 'Test-ADRolesAvailability', 'Test-ADSiteLinks', 'Test-DNSNameServers', 'Test-FSMORolesAvailability', 'Test-LDAP') -Alias @('Get-WinADGPOSysvol', 'Get-WinADUsersFP')