ADEssentials.psm1

function Convert-ADSchemaToGuid { 
    <#
    .SYNOPSIS
    Converts name of schema properties to guids
 
    .DESCRIPTION
    Converts name of schema properties to guids
 
    .PARAMETER SchemaName
    Schema Name to convert to guid
 
    .PARAMETER All
    Get hashtable of all schema properties and their guids
 
    .PARAMETER Domain
    Domain to query. By default the current domain is used
 
    .PARAMETER RootDSE
    RootDSE to query. By default RootDSE is queried from the domain
 
    .PARAMETER AsString
    Return the guid as a string
 
    .EXAMPLE
    Convert-ADSchemaToGuid -SchemaName 'ms-Exch-MSO-Forward-Sync-Cookie'
 
    .EXAMPLE
    Convert-ADSchemaToGuid -SchemaName 'ms-Exch-MSO-Forward-Sync-Cookie' -AsString
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $SchemaName,
        [string] $Domain,
        [Microsoft.ActiveDirectory.Management.ADEntity] $RootDSE,
        [switch] $AsString
    )
    if (-not $Script:ADGuidMap -or -not $Script:ADGuidMapString) {

        if ($RootDSE) {
            $Script:RootDSE = $RootDSE
        } elseif (-not $Script:RootDSE) {
            if ($Domain) {
                $Script:RootDSE = Get-ADRootDSE -Server $Domain
            } else {
                $Script:RootDSE = Get-ADRootDSE
            }
        }
        $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $Script:RootDSE.defaultNamingContext -ToDomainCN
        $QueryServer = (Get-ADDomainController -DomainName $DomainCN -Discover -ErrorAction Stop).Hostname[0]

        $Script:ADGuidMap = [ordered] @{
            'All' = [System.GUID]'00000000-0000-0000-0000-000000000000'
        }
        $Script:ADGuidMapString = [ordered] @{
            'All' = '00000000-0000-0000-0000-000000000000'
        }
        Write-Verbose "Convert-ADSchemaToGuid - Querying Schema from $QueryServer"
        $Time = [System.Diagnostics.Stopwatch]::StartNew()
        if (-not $Script:StandardRights) {
            $Script:StandardRights = Get-ADObject -SearchBase $Script:RootDSE.schemaNamingContext -LDAPFilter "(schemaidguid=*)" -Properties name, lDAPDisplayName, schemaIDGUID -Server $QueryServer -ErrorAction Stop | Select-Object name, lDAPDisplayName, schemaIDGUID
        }
        foreach ($Guid in $Script:StandardRights) {
            $Script:ADGuidMapString[$Guid.lDAPDisplayName] = ([System.GUID]$Guid.schemaIDGUID).Guid
            $Script:ADGuidMapString[$Guid.Name] = ([System.GUID]$Guid.schemaIDGUID).Guid
            $Script:ADGuidMap[$Guid.lDAPDisplayName] = ([System.GUID]$Guid.schemaIDGUID)
            $Script:ADGuidMap[$Guid.Name] = ([System.GUID]$Guid.schemaIDGUID)
        }
        $Time.Stop()
        $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds"
        Write-Verbose "Convert-ADSchemaToGuid - Querying Schema from $QueryServer took $TimeToExecute"
        Write-Verbose "Convert-ADSchemaToGuid - Querying Extended Rights from $QueryServer"
        $Time = [System.Diagnostics.Stopwatch]::StartNew()

        if (-not $Script:ExtendedRightsGuids) {
            $Script:ExtendedRightsGuids = Get-ADObject -SearchBase $Script:RootDSE.ConfigurationNamingContext -LDAPFilter "(&(objectclass=controlAccessRight)(rightsguid=*))" -Properties name, displayName, lDAPDisplayName, rightsGuid -Server $QueryServer -ErrorAction Stop | Select-Object name, displayName, lDAPDisplayName, rightsGuid
        }
        foreach ($Guid in $Script:ExtendedRightsGuids) {
            $Script:ADGuidMapString[$Guid.Name] = ([System.GUID]$Guid.RightsGuid).Guid
            $Script:ADGuidMapString[$Guid.DisplayName] = ([System.GUID]$Guid.RightsGuid).Guid
            $Script:ADGuidMap[$Guid.Name] = ([System.GUID]$Guid.RightsGuid)
            $Script:ADGuidMap[$Guid.DisplayName] = ([System.GUID]$Guid.RightsGuid)
        }
        $Time.Stop()
        $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds"
        Write-Verbose "Convert-ADSchemaToGuid - Querying Extended Rights from $QueryServer took $TimeToExecute"
    }
    if ($SchemaName) {
        if ($AsString) {
            return $Script:ADGuidMapString[$SchemaName]
        } else {
            return $Script:ADGuidMap[$SchemaName]
        }
    } else {
        if ($AsString) {
            $Script:ADGuidMapString
        } else {
            $Script:ADGuidMap
        }
    }
}
function Convert-CountryCodeToCountry { 
    <#
    .SYNOPSIS
    Converts a country code to a country name, or when used with a switch to full culture information
 
    .DESCRIPTION
    Converts a country code to a country name, or when used with a switch to full culture information
 
    .PARAMETER CountryCode
    Country code
 
    .PARAMETER All
    Provide full culture information rather than just the country name
 
    .EXAMPLE
    Convert-CountryCodeToCountry -CountryCode 'PL'
 
    .EXAMPLE
    Convert-CountryCodeToCountry -CountryCode 'PL' -All
 
    .EXAMPLE
    $Test = Convert-CountryCodeToCountry
    $Test['PL']['Culture'] | fl
    $Test['PL']['RegionInformation']
 
    .EXAMPLE
    Convert-CountryCodeToCountry -CountryCode 'PL'
    Convert-CountryCodeToCountry -CountryCode 'POL'
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [string] $CountryCode,
        [switch] $All
    )
    if ($Script:QuickSearch) {
        if ($PSBoundParameters.ContainsKey('CountryCode')) {
            if ($All) {
                $Script:QuickSearch[$CountryCode]
            } else {
                $Script:QuickSearch[$CountryCode].RegionInformation.EnglishName
            }
        } else {
            $Script:QuickSearch
        }
    } else {
        $Script:QuickSearch = [ordered] @{}
        $AllCultures = [cultureinfo]::GetCultures([System.Globalization.CultureTypes]::SpecificCultures)
        foreach ($Culture in $AllCultures) {

            $RegionInformation = [System.Globalization.RegionInfo]::new($Culture)
            $Script:QuickSearch[$RegionInformation.TwoLetterISORegionName] = @{
                'Culture'           = $Culture
                'RegionInformation' = $RegionInformation
            }
            $Script:QuickSearch[$RegionInformation.ThreeLetterISORegionName] = @{
                'Culture'           = $Culture
                'RegionInformation' = $RegionInformation
            }
        }
        if ($PSBoundParameters.ContainsKey('CountryCode')) {
            if ($All) {
                $Script:QuickSearch[$CountryCode]
            } else {
                $Script:QuickSearch[$CountryCode].RegionInformation.EnglishName
            }
        } else {
            $Script:QuickSearch
        }
    }
}
function Convert-DomainFqdnToNetBIOS { 
    <#
    .SYNOPSIS
    Converts FQDN to NetBIOS name for Active Directory Domain
 
    .DESCRIPTION
    Converts FQDN to NetBIOS name for Active Directory Domain
 
    .PARAMETER DomainName
    DomainName for current forest or trusted forest
 
    .EXAMPLE
    Convert-DomainFqdnToNetBIOS -Domain 'ad.evotec.xyz'
 
    .EXAMPLE
    Convert-DomainFqdnToNetBIOS -Domain 'ad.evotec.pl'
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param (
        [string] $DomainName
    )
    if (-not $Script:CacheFQDN) {
        $Script:CacheFQDN = @{}
    }
    if ($Script:CacheFQDN[$DomainName]) {
        $Script:CacheFQDN[$DomainName]
    } else {
        $objRootDSE = [System.DirectoryServices.DirectoryEntry] "LDAP://$DomainName/RootDSE"
        $ConfigurationNC = $objRootDSE.configurationNamingContext
        $Searcher = [System.DirectoryServices.DirectorySearcher] @{
            SearchScope = "subtree"
            SearchRoot  = "LDAP://cn=Partitions,$ConfigurationNC"
            Filter      = "(&(objectcategory=Crossref)(dnsRoot=$DomainName)(netbiosname=*))"
        }
        $null = $Searcher.PropertiesToLoad.Add("netbiosname")
        $Script:CacheFQDN[$DomainName] = ($Searcher.FindOne()).Properties.Item("netbiosname")
        $Script:CacheFQDN[$DomainName]
    }
}
function Convert-ExchangeEmail { 
    <#
    .SYNOPSIS
    Converts a list of Exchange email addresses into a readable and exportable format.
 
    .DESCRIPTION
    This function takes a list of Exchange email addresses and processes them to make them more readable and suitable for export.
 
    .PARAMETER Emails
    List of email addresses in Exchange or Exchange Online format, also known as proxy addresses.
 
    .PARAMETER Separator
    The separator to use between each processed email address. Default is ', '.
 
    .PARAMETER RemoveDuplicates
    Switch to remove duplicate email addresses from the list.
 
    .PARAMETER RemovePrefix
    Switch to remove any prefixes like 'SMTP:', 'SIP:', 'spo:', etc. from the email addresses.
 
    .PARAMETER AddSeparator
    Switch to join the processed email addresses using the specified separator.
 
    .EXAMPLE
    $Emails = @()
    $Emails += 'SIP:test@email.com'
    $Emails += 'SMTP:elo@maiu.com'
    $Emails += 'sip:elo@maiu.com'
    $Emails += 'Spo:dfte@sdsd.com'
    $Emails += 'SPO:myothertest@sco.com'
 
    Convert-ExchangeEmail -Emails $Emails -RemovePrefix -RemoveDuplicates -AddSeparator
    #>

    #>

    [CmdletBinding()]
    param(
        [string[]] $Emails,
        [string] $Separator = ', ',
        [switch] $RemoveDuplicates,
        [switch] $RemovePrefix,
        [switch] $AddSeparator
    )

    if ($RemovePrefix) {

        $Emails = $Emails -replace 'smtp:', '' -replace 'sip:', '' -replace 'spo:', ''
    }
    if ($RemoveDuplicates) {
        $Emails = $Emails | Sort-Object -Unique
    }
    if ($AddSeparator) {
        $Emails = $Emails -join $Separator
    }
    return $Emails
}
function Convert-ExchangeRecipient { 
    <#
    .SYNOPSIS
    Convert msExchRemoteRecipientType, msExchRecipientDisplayType, msExchRecipientTypeDetails to their respective name
 
    .DESCRIPTION
    Convert msExchRemoteRecipientType, msExchRecipientDisplayType, msExchRecipientTypeDetails to their respective name
 
    .PARAMETER RecipientTypeDetails
    RecipientTypeDetails to convert
 
    .PARAMETER RecipientType
    RecipientType to convert
 
    .PARAMETER RemoteRecipientType
    Parameter description
 
    .EXAMPLE
    $Users = Get-ADUser -Filter * -Properties Mail, ProxyAddresses, msExchRemoteRecipientType, msExchRecipientDisplayType, msExchRecipientTypeDetails, MailNickName
    $UsersModified = foreach ($User in $Users) {
        [PSCUstomObject] @{
            Name = $User.Name
            Mail = $User.Mail
            MailNickName = $User.MailNickName
            msExchRemoteRecipientType = Convert-ExchangeRecipient -msExchRemoteRecipientType $User.msExchRemoteRecipientType
            msExchRecipientDisplayType = Convert-ExchangeRecipient -msExchRecipientDisplayType $User.msExchRecipientDisplayType
            msExchRecipientTypeDetails = Convert-ExchangeRecipient -msExchRecipientTypeDetails $User.msExchRecipientTypeDetails
            ProxyAddresses = Convert-ExchangeEmail -AddSeparator -RemovePrefix -RemoveDuplicates -Separator ',' -Emails $User.ProxyAddresses
        }
    }
    $UsersModified | Out-HtmlView -Filtering -ScrollX
 
    .EXAMPLE
    Convert-ExchangeRecipient -msExchRemoteRecipientType 17
    Convert-ExchangeRecipient -msExchRecipientDisplayType 17
    Convert-ExchangeRecipient -msExchRecipientTypeDetails 17
 
    .NOTES
    Based on:
    - https://granikos.eu/exchange-recipient-type-values/
    - https://answers.microsoft.com/en-us/msoffice/forum/all/recipient-type-values/7c2620e5-9870-48ba-b5c2-7772c739c651
    - https://www.undocumented-features.com/2020/05/06/every-last-msexchrecipientdisplaytype-and-msexchrecipienttypedetails-value/
    #>

    [alias('Convert-ExchangeRecipientDetails')]
    [cmdletbinding(DefaultParameterSetName = 'msExchRecipientTypeDetails')]
    param(
        [parameter(ParameterSetName = 'msExchRecipientTypeDetails')][alias('RecipientTypeDetails')][string] $msExchRecipientTypeDetails,
        [parameter(ParameterSetName = 'msExchRecipientDisplayType')][alias('RecipientType')][string] $msExchRecipientDisplayType,
        [parameter(ParameterSetName = 'msExchRemoteRecipientType')][alias('RemoteRecipientType')][string] $msExchRemoteRecipientType,

        [parameter(ParameterSetName = 'msExchRecipientTypeDetails')]
        [parameter(ParameterSetName = 'msExchRecipientDisplayType')]
        [parameter(ParameterSetName = 'msExchRemoteRecipientType')]
        [switch] $All
    )

    if ($PSBoundParameters.ContainsKey('msExchRecipientTypeDetails')) {
        $ListMsExchRecipientTypeDetails = [ordered] @{
            '0'               = 'None'
            '1'               = 'UserMailbox'
            '2'               = 'LinkedMailbox'
            '4'               = 'SharedMailbox'
            '8'               = 'LegacyMailbox'
            '16'              = 'RoomMailbox'
            '32'              = 'EquipmentMailbox'
            '64'              = 'MailContact'
            '128'             = 'MailUser'
            '256'             = 'MailUniversalDistributionGroup'
            '512'             = 'MailNonUniversalGroup'
            '1024'            = 'MailUniversalSecurityGroup'
            '2048'            = 'DynamicDistributionGroup'
            '4096'            = 'PublicFolder'
            '8192'            = 'SystemAttendantMailbox'
            '16384'           = 'SystemMailbox'
            '32768'           = 'MailForestContact'
            '65536'           = 'User'
            '131072'          = 'Contact'
            '262144'          = 'UniversalDistributionGroup'
            '524288'          = 'UniversalSecurityGroup'
            '1048576'         = 'NonUniversalGroup'
            '2097152'         = 'Disable User'
            '4194304'         = 'MicrosoftExchange'
            '8388608'         = 'ArbitrationMailbox'
            '16777216'        = 'MailboxPlan'
            '33554432'        = 'LinkedUser'
            '268435456'       = 'RoomList'
            '536870912'       = 'DiscoveryMailbox'
            '1073741824'      = 'RoleGroup'
            '2147483648'      = 'RemoteUserMailbox'
            '4294967296'      = 'Computer'
            '8589934592'      = 'RemoteRoomMailbox'
            '17179869184'     = 'RemoteEquipmentMailbox'
            '34359738368'     = 'RemoteSharedMailbox'
            '68719476736'     = 'PublicFolderMailbox'
            '137438953472'    = 'Team Mailbox'
            '274877906944'    = 'RemoteTeamMailbox'
            '549755813888'    = 'MonitoringMailbox'
            '1099511627776'   = 'GroupMailbox'
            '2199023255552'   = 'LinkedRoomMailbox'
            '4398046511104'   = 'AuditLogMailbox'
            '8796093022208'   = 'RemoteGroupMailbox'
            '17592186044416'  = 'SchedulingMailbox'
            '35184372088832'  = 'GuestMailUser'
            '70368744177664'  = 'AuxAuditLogMailbox'
            '140737488355328' = 'SupervisoryReviewPolicyMailbox'
        }
        if ($All) {
            $ListMsExchRecipientTypeDetails
        } else {
            if ($null -ne $ListMsExchRecipientTypeDetails[$msExchRecipientTypeDetails]) {
                $ListMsExchRecipientTypeDetails[$msExchRecipientTypeDetails]
            } else {
                $msExchRecipientTypeDetails
            }
        }
    } elseif ($PSBoundParameters.ContainsKey('msExchRecipientDisplayType')) {
        $ListMsExchRecipientDisplayType = [ordered] @{
            '0'           = 'MailboxUser'
            '1'           = 'DistributionGroup'
            '2'           = 'PublicFolder'
            '3'           = 'DynamicDistributionGroup'
            '4'           = 'Organization'
            '5'           = 'PrivateDistributionList'
            '6'           = 'RemoteMailUser'
            '7'           = 'ConferenceRoomMailbox'
            '8'           = 'EquipmentMailbox'
            '10'          = 'ArbitrationMailbox'
            '11'          = 'MailboxPlan'
            '12'          = 'LinkedUser'
            '15'          = 'RoomList'
            '17'          = 'Microsoft365Group' 
            '-2147483642' = 'SyncedMailboxUser'
            '-2147483391' = 'SyncedUDGasUDG'
            '-2147483386' = 'SyncedUDGasContact'
            '-2147483130' = 'SyncedPublicFolder'
            '-2147482874' = 'SyncedDynamicDistributionGroup'
            '-2147482106' = 'SyncedRemoteMailUser'
            '-2147481850' = 'SyncedConferenceRoomMailbox'
            '-2147481594' = 'SyncedEquipmentMailbox'
            '-2147481343' = 'SyncedUSGasUDG'
            '-2147481338' = 'SyncedUSGasContact'
            '-1073741818' = 'ACLableSyncedMailboxUser'
            '-1073740282' = 'ACLableSyncedRemoteMailUser'
            '-1073739514' = 'ACLableSyncedUSGasContact'
            '-1073739511' = 'SyncedUSGasUSG'
            '1043741833'  = 'SecurityDistributionGroup'
            '1073739511'  = 'SyncedUSGasUSG'
            '1073739514'  = 'ACLableSyncedUSGasContact'
            '1073741824'  = 'ACLableMailboxUser' 
            '1073741830'  = 'ACLableRemoteMailUser'
        }
        if ($All) {
            $ListMsExchRecipientDisplayType
        } else {
            if ($null -ne $ListMsExchRecipientDisplayType[$msExchRecipientDisplayType]) {
                $ListMsExchRecipientDisplayType[$msExchRecipientDisplayType]
            } else {
                $msExchRecipientDisplayType
            }
        }
    } elseif ($PSBoundParameters.ContainsKey('msExchRemoteRecipientType')) {
        $ListMsExchRemoteRecipientType = [ordered] @{

            '1'   = 'ProvisionMailbox'
            '2'   = 'ProvisionArchive (On-Prem Mailbox)'
            '3'   = 'ProvisionMailbox, ProvisionArchive'
            '4'   = 'Migrated (UserMailbox)'
            '6'   = 'ProvisionArchive, Migrated'
            '8'   = 'DeprovisionMailbox'
            '10'  = 'ProvisionArchive, DeprovisionMailbox'
            '16'  = 'DeprovisionArchive (On-Prem Mailbox)'
            '17'  = 'ProvisionMailbox, DeprovisionArchive'
            '20'  = 'Migrated, DeprovisionArchive'
            '24'  = 'DeprovisionMailbox, DeprovisionArchive'
            '33'  = 'ProvisionMailbox, RoomMailbox'
            '35'  = 'ProvisionMailbox, ProvisionArchive, RoomMailbox'
            '36'  = 'Migrated, RoomMailbox'
            '38'  = 'ProvisionArchive, Migrated, RoomMailbox'
            '49'  = 'ProvisionMailbox, DeprovisionArchive, RoomMailbox'
            '52'  = 'Migrated, DeprovisionArchive, RoomMailbox'
            '65'  = 'ProvisionMailbox, EquipmentMailbox'
            '67'  = 'ProvisionMailbox, ProvisionArchive, EquipmentMailbox'
            '68'  = 'Migrated, EquipmentMailbox'
            '70'  = 'ProvisionArchive, Migrated, EquipmentMailbox'
            '81'  = 'ProvisionMailbox, DeprovisionArchive, EquipmentMailbox'
            '84'  = 'Migrated, DeprovisionArchive, EquipmentMailbox'
            '100'    = 'Migrated, SharedMailbox'
            '102'    = 'ProvisionArchive, Migrated, SharedMailbox'
            '116'    = 'Migrated, DeprovisionArchive, SharedMailbox'
        }
        if ($All) {
            $ListMsExchRemoteRecipientType
        } else {
            if ($null -ne $ListMsExchRemoteRecipientType[$msExchRemoteRecipientType]) {
                $ListMsExchRemoteRecipientType[$msExchRemoteRecipientType]
            } else {
                $msExchRemoteRecipientType
            }
        }
    }
}
function Convert-Identity { 
    <#
    .SYNOPSIS
    Small command that tries to resolve any given object
 
    .DESCRIPTION
    Small command that tries to resolve any given object - be it SID, DN, FSP or Netbiosname
 
    .PARAMETER Identity
    Type to resolve in form of Identity, DN, SID
 
    .PARAMETER SID
    Allows to pass SID directly, rather then going thru verification process
 
    .PARAMETER Name
    Allows to pass Name directly, rather then going thru verification process
 
    .PARAMETER Force
    Allows to clear cache, useful when you want to force refresh
 
    .EXAMPLE
    $Identity = @(
        'S-1-5-4'
        'S-1-5-4'
        'S-1-5-11'
        'S-1-5-32-549'
        'S-1-5-32-550'
        'S-1-5-32-548'
        'S-1-5-64-10'
        'S-1-5-64-14'
        'S-1-5-64-21'
        'S-1-5-18'
        'S-1-5-19'
        'S-1-5-32-544'
        'S-1-5-20-20-10-51' # Wrong SID
        'S-1-5-21-853615985-2870445339-3163598659-512'
        'S-1-5-21-3661168273-3802070955-2987026695-512'
        'S-1-5-21-1928204107-2710010574-1926425344-512'
        'CN=Test Test 2,OU=Users,OU=Production,DC=ad,DC=evotec,DC=pl'
        'Test Local Group'
        'przemyslaw.klys@evotec.pl'
        'test2'
        'NT AUTHORITY\NETWORK'
        'NT AUTHORITY\SYSTEM'
        'S-1-5-21-853615985-2870445339-3163598659-519'
        'TEST\some'
        'EVOTECPL\Domain Admins'
        'NT AUTHORITY\INTERACTIVE'
        'INTERACTIVE'
        'EVOTEC\Domain Admins'
        'EVOTECPL\Domain Admins'
        'Test\Domain Admins'
        'CN=S-1-5-21-1928204107-2710010574-1926425344-512,CN=ForeignSecurityPrincipals,DC=ad,DC=evotec,DC=xyz' # Valid
        'CN=S-1-5-21-1928204107-2710010574-512,CN=ForeignSecurityPrincipals,DC=ad,DC=evotec,DC=xyz' # not valid
        'CN=S-1-5-21-1928204107-2710010574-1926425344-512,CN=ForeignSecurityPrincipals,DC=ad,DC=evotec,DC=xyz' # cached
    )
 
    $TestOutput = Convert-Identity -Identity $Identity -Verbose
 
    Output:
 
    Name SID DomainName Type Error
    ---- --- ---------- ---- -----
    NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup
    NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup
    NT AUTHORITY\Authenticated Users S-1-5-11 WellKnownGroup
    BUILTIN\Server Operators S-1-5-32-549 WellKnownGroup
    BUILTIN\Print Operators S-1-5-32-550 WellKnownGroup
    BUILTIN\Account Operators S-1-5-32-548 WellKnownGroup
    NT AUTHORITY\NTLM Authentication S-1-5-64-10 WellKnownGroup
    NT AUTHORITY\SChannel Authentication S-1-5-64-14 WellKnownGroup
    NT AUTHORITY\Digest Authentication S-1-5-64-21 WellKnownGroup
    NT AUTHORITY\SYSTEM S-1-5-18 WellKnownAdministrative
    NT AUTHORITY\NETWORK SERVICE S-1-5-19 WellKnownGroup
    BUILTIN\Administrators S-1-5-32-544 WellKnownAdministrative
    S-1-5-20-20-10-51 S-1-5-20-20-10-51 Unknown Exception calling "Translate" with "1" argument(s): "Some or all identity references could not be translated."
    EVOTEC\Domain Admins S-1-5-21-853615985-2870445339-3163598659-512 ad.evotec.xyz Administrative
    EVOTECPL\Domain Admins S-1-5-21-3661168273-3802070955-2987026695-512 ad.evotec.pl Administrative
    TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative
    EVOTECPL\TestingAD S-1-5-21-3661168273-3802070955-2987026695-1111 ad.evotec.pl NotAdministrative
    EVOTEC\Test Local Group S-1-5-21-853615985-2870445339-3163598659-3610 ad.evotec.xyz NotAdministrative
    EVOTEC\przemyslaw.klys S-1-5-21-853615985-2870445339-3163598659-1105 ad.evotec.xyz NotAdministrative
    test2 Unknown Exception calling "Translate" with "1" argument(s): "Some or all identity references could not be translated."
    NT AUTHORITY\NETWORK S-1-5-2 WellKnownGroup
    NT AUTHORITY\SYSTEM S-1-5-18 WellKnownAdministrative
    EVOTEC\Enterprise Admins S-1-5-21-853615985-2870445339-3163598659-519 ad.evotec.xyz Administrative
    TEST\some S-1-5-21-1928204107-2710010574-1926425344-1106 test.evotec.pl NotAdministrative
    EVOTECPL\Domain Admins S-1-5-21-3661168273-3802070955-2987026695-512 ad.evotec.pl Administrative
    NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup
    NT AUTHORITY\INTERACTIVE S-1-5-4 WellKnownGroup
    EVOTEC\Domain Admins S-1-5-21-853615985-2870445339-3163598659-512 ad.evotec.xyz Administrative
    EVOTECPL\Domain Admins S-1-5-21-3661168273-3802070955-2987026695-512 ad.evotec.pl Administrative
    TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative
    TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative
    S-1-5-21-1928204107-2710010574-512 S-1-5-21-1928204107-2710010574-512 Unknown Exception calling "Translate" with "1" argument(s): "Some or all identity references could not be translated."
    TEST\Domain Admins S-1-5-21-1928204107-2710010574-1926425344-512 test.evotec.pl Administrative
 
    .NOTES
    General notes
    #>

    [cmdletBinding(DefaultParameterSetName = 'Identity')]
    param(
        [parameter(ParameterSetName = 'Identity', Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)][string[]] $Identity,
        [parameter(ParameterSetName = 'SID', Mandatory)][System.Security.Principal.SecurityIdentifier[]] $SID,
        [parameter(ParameterSetName = 'Name', Mandatory)][string[]] $Name,
        [switch] $Force
    )
    Begin {

        if (-not $Script:GlobalCacheSidConvert -or $Force) {
            $Script:GlobalCacheSidConvert = @{

                'NT AUTHORITY\SYSTEM'                         = [PSCustomObject] @{
                    Name       = 'BUILTIN\Administrators'
                    SID        = 'S-1-5-18'
                    DomainName = ''
                    Type       = 'WellKnownAdministrative'
                    Error      = ''
                }

                'BUILTIN\Administrators'                      = [PSCustomObject] @{
                    Name       = 'BUILTIN\Administrators'
                    SID        = 'S-1-5-32-544'
                    DomainName = ''
                    Type       = 'WellKnownAdministrative'
                    Error      = ''
                }
                'BUILTIN\Users'                               = [PSCustomObject] @{
                    Name       = 'BUILTIN\Users'
                    SID        = 'S-1-5-32-545'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Guests'                              = [PSCustomObject] @{
                    Name       = 'BUILTIN\Guests'
                    SID        = 'S-1-5-32-546'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Power Users'                         = [PSCustomObject] @{
                    Name       = 'BUILTIN\Power Users'
                    SID        = 'S-1-5-32-547'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Account Operators'                   = [PSCustomObject] @{
                    Name       = 'BUILTIN\Account Operators'
                    SID        = 'S-1-5-32-548'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Server Operators'                    = [PSCustomObject] @{
                    Name       = 'BUILTIN\Server Operators'
                    SID        = 'S-1-5-32-549'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Print Operators'                     = [PSCustomObject] @{
                    Name       = 'BUILTIN\Print Operators'
                    SID        = 'S-1-5-32-550'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Backup Operators'                    = [PSCustomObject] @{
                    Name       = 'BUILTIN\Backup Operators'
                    SID        = 'S-1-5-32-551'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Replicator'                          = [PSCustomObject] @{
                    Name       = 'BUILTIN\Replicators'
                    SID        = 'S-1-5-32-552'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Pre-Windows 2000 Compatible Access'  = [PSCustomObject] @{
                    Name       = 'BUILTIN\Pre-Windows 2000 Compatible Access'
                    SID        = 'S-1-5-32-554'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Remote Desktop Users'                = [PSCustomObject] @{
                    Name       = 'BUILTIN\Remote Desktop Users'
                    SID        = 'S-1-5-32-555'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Network Configuration Operators'     = [PSCustomObject] @{
                    Name       = 'BUILTIN\Network Configuration Operators'
                    SID        = 'S-1-5-32-556'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Incoming Forest Trust Builders'      = [PSCustomObject] @{
                    Name       = 'BUILTIN\Incoming Forest Trust Builders'
                    SID        = 'S-1-5-32-557'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Performance Monitor Users'           = [PSCustomObject] @{
                    Name       = 'BUILTIN\Performance Monitor Users'
                    SID        = 'S-1-5-32-558'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Performance Log Users'               = [PSCustomObject] @{
                    Name       = 'BUILTIN\Performance Log Users'
                    SID        = 'S-1-5-32-559'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Windows Authorization Access Group'  = [PSCustomObject] @{
                    Name       = 'BUILTIN\Windows Authorization Access Group'
                    SID        = 'S-1-5-32-560'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Terminal Server License Servers'     = [PSCustomObject] @{
                    Name       = 'BUILTIN\Terminal Server License Servers'
                    SID        = 'S-1-5-32-561'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Distributed COM Users'               = [PSCustomObject] @{
                    Name       = 'BUILTIN\Distributed COM Users'
                    SID        = 'S-1-5-32-562'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\IIS_IUSRS'                           = [PSCustomObject] @{
                    Name       = 'BUILTIN\IIS_IUSRS'
                    SID        = 'S-1-5-32-568'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Cryptographic Operators'             = [PSCustomObject] @{
                    Name       = 'BUILTIN\Cryptographic Operators'
                    SID        = 'S-1-5-32-569'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Event Log Readers'                   = [PSCustomObject] @{
                    Name       = 'BUILTIN\Event Log Readers'
                    SID        = 'S-1-5-32-573'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Certificate Service DCOM Access'     = [PSCustomObject] @{
                    Name       = 'BUILTIN\Certificate Service DCOM Access'
                    SID        = 'S-1-5-32-574'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\RDS Remote Access Servers'           = [PSCustomObject] @{
                    Name       = 'BUILTIN\RDS Remote Access Servers'
                    SID        = 'S-1-5-32-575'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\RDS Endpoint Servers'                = [PSCustomObject] @{
                    Name       = 'BUILTIN\RDS Endpoint Servers'
                    SID        = 'S-1-5-32-576'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\RDS Management Servers'              = [PSCustomObject] @{
                    Name       = 'BUILTIN\RDS Management Servers'
                    SID        = 'S-1-5-32-577'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Hyper-V Administrators'              = [PSCustomObject] @{
                    Name       = 'BUILTIN\Hyper-V Administrators'
                    SID        = 'S-1-5-32-578'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Access Control Assistance Operators' = [PSCustomObject] @{
                    Name       = 'BUILTIN\Access Control Assistance Operators'
                    SID        = 'S-1-5-32-579'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'BUILTIN\Remote Management Users'             = [PSCustomObject] @{
                    Name       = 'BUILTIN\Remote Management Users'
                    SID        = 'S-1-5-32-580'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'Window Manager\Window Manager Group'         = [PSCustomObject] @{
                    Name       = 'Window Manager\Window Manager Group'
                    SID        = 'S-1-5-90-0'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'NT SERVICE\WdiServiceHost'                   = [PSCustomObject] @{
                    Name       = 'NT SERVICE\WdiServiceHost'
                    SID        = 'S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'NT SERVICE\MSSQLSERVER'                      = [PSCustomObject] @{
                    Name       = 'NT SERVICE\MSSQLSERVER'
                    SID        = 'S-1-5-80-3880718306-3832830129-1677859214-2598158968-1052248003'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'NT SERVICE\SQLSERVERAGENT'                   = [PSCustomObject] @{
                    Name       = 'NT SERVICE\SQLSERVERAGENT'
                    SID        = 'S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'NT SERVICE\SQLTELEMETRY'                     = [PSCustomObject] @{
                    Name       = 'NT SERVICE\SQLTELEMETRY'
                    SID        = 'S-1-5-80-2652535364-2169709536-2857650723-2622804123-1107741775'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
                'NT SERVICE\ADSync'                           = [PSCustomObject] @{
                    Name       = 'NT SERVICE\ADSync'
                    SID        = 'S-1-5-80-3245704983-3664226991-764670653-2504430226-901976451'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }

                'NT Service\himds'                            = [PSCustomObject] @{
                    Name       = 'NT Service\himds'
                    SID        = 'S-1-5-80-4215458991-2034252225-2287069555-1155419622-2701885083'
                    DomainName = ''
                    Type       = 'WellKnownGroup'
                    Error      = ''
                }
            }
        }
    }
    Process {
        if ($Identity) {
            foreach ($Ident in $Identity) {
                $MatchRegex = [Regex]::Matches($Ident, "S-\d-\d+-(\d+-|){1,14}\d+")
                if ($Script:GlobalCacheSidConvert[$Ident]) {

                    Write-Verbose "Convert-Identity - Processing $Ident (Cache)"
                    $Script:GlobalCacheSidConvert[$Ident]
                } elseif ($MatchRegex.Success) {

                    Write-Verbose "Convert-Identity - Processing $Ident (SID)"
                    if ($MatchRegex.Value -ne $Ident) {
                        $Script:GlobalCacheSidConvert[$Ident] = ConvertFrom-SID -SID $MatchRegex.Value
                    } else {
                        $Script:GlobalCacheSidConvert[$Ident] = ConvertFrom-SID -SID $Ident
                    }
                    $Script:GlobalCacheSidConvert[$Ident]
                } elseif ($Ident -like '*DC=*') {

                    Write-Verbose "Convert-Identity - Processing $Ident (DistinguishedName)"
                    try {
                        $Object = [adsi]"LDAP://$($Ident)"
                        $SIDValue = [System.Security.Principal.SecurityIdentifier]::new($Object.objectSid.Value, 0).Value
                        $Script:GlobalCacheSidConvert[$Ident] = ConvertFrom-SID -SID $SIDValue
                    } catch {
                        $Script:GlobalCacheSidConvert[$Ident] = [PSCustomObject] @{
                            Name       = $Ident
                            SID        = $null
                            DomainName = ''
                            Type       = 'Unknown'
                            Error      = $_.Exception.Message -replace [environment]::NewLine, ' '
                        }
                    }
                    $Script:GlobalCacheSidConvert[$Ident]
                } else {

                    Write-Verbose "Convert-Identity - Processing $Ident (Other)"
                    try {
                        $SIDValue = ([System.Security.Principal.NTAccount] $Ident).Translate([System.Security.Principal.SecurityIdentifier]).Value
                        $Script:GlobalCacheSidConvert[$Ident] = ConvertFrom-SID -SID $SIDValue
                    } catch {
                        $Script:GlobalCacheSidConvert[$Ident] = [PSCustomObject] @{
                            Name       = $Ident
                            SID        = $null
                            DomainName = ''
                            Type       = 'Unknown'
                            Error      = $_.Exception.Message -replace [environment]::NewLine, ' '
                        }
                    }
                    $Script:GlobalCacheSidConvert[$Ident]
                }
            }
        } else {
            if ($SID) {
                foreach ($S in $SID) {
                    if ($Script:GlobalCacheSidConvert[$S]) {
                        $Script:GlobalCacheSidConvert[$S]
                    } else {
                        $Script:GlobalCacheSidConvert[$S] = ConvertFrom-SID -SID $S
                        $Script:GlobalCacheSidConvert[$S]
                    }
                }
            } else {
                foreach ($Ident in $Name) {
                    if ($Script:GlobalCacheSidConvert[$Ident]) {
                        $Script:GlobalCacheSidConvert[$Ident]
                    } else {
                        $Script:GlobalCacheSidConvert[$Ident] = ([System.Security.Principal.NTAccount] $Ident).Translate([System.Security.Principal.SecurityIdentifier]).Value
                        $Script:GlobalCacheSidConvert[$Ident]
                    }
                }
            }
        }
    }
    End {
    }
}
function Convert-TimeToDays { 
    <#
    .SYNOPSIS
    Converts the time span between two dates into the number of days.
 
    .DESCRIPTION
    This function calculates the number of days between two given dates. It allows for flexibility in handling different date formats and provides an option to ignore specific dates.
 
    .PARAMETER StartTime
    Specifies the start date and time of the time span.
 
    .PARAMETER EndTime
    Specifies the end date and time of the time span.
 
    .PARAMETER Ignore
    Specifies a pattern to ignore specific dates. Default is '*1601*'.
 
    .EXAMPLE
    Convert-TimeToDays -StartTime (Get-Date).AddDays(-5) -EndTime (Get-Date)
    # Calculates the number of days between 5 days ago and today.
 
    .EXAMPLE
    Convert-TimeToDays -StartTime '2022-01-01' -EndTime '2022-01-10' -Ignore '*2022*'
    # Calculates the number of days between January 1, 2022, and January 10, 2022, ignoring any dates containing '2022'.
 
    #>

    [CmdletBinding()]
    param (
        $StartTime,
        $EndTime,
        #[nullable[DateTime]] $StartTime, # can't use this just yet, some old code uses strings in StartTime/EndTime.
        #[nullable[DateTime]] $EndTime, # After that's fixed will change this.
        [string] $Ignore = '*1601*'
    )
    if ($null -ne $StartTime -and $null -ne $EndTime) {
        try {
            if ($StartTime -notlike $Ignore -and $EndTime -notlike $Ignore) {
                $Days = (New-TimeSpan -Start $StartTime -End $EndTime).Days
            }
        } catch {
        }
    } elseif ($null -ne $EndTime) {
        if ($StartTime -notlike $Ignore -and $EndTime -notlike $Ignore) {
            $Days = (New-TimeSpan -Start (Get-Date) -End ($EndTime)).Days
        }
    } elseif ($null -ne $StartTime) {
        if ($StartTime -notlike $Ignore -and $EndTime -notlike $Ignore) {
            $Days = (New-TimeSpan -Start $StartTime -End (Get-Date)).Days
        }
    }
    return $Days
}
function Convert-ToDateTime { 
    <#
    .SYNOPSIS
    Converts a file time string to a DateTime object.
 
    .DESCRIPTION
    This function converts a file time string to a DateTime object. It handles the conversion and provides flexibility to ignore specific file time strings.
 
    .PARAMETER Timestring
    Specifies the file time string to convert to a DateTime object.
 
    .PARAMETER Ignore
    Specifies a pattern to ignore specific file time strings. Default is '*1601*'.
 
    .EXAMPLE
    Convert-ToDateTime -Timestring '132479040000000000'
    # Converts the file time string '132479040000000000' to a DateTime object.
 
    .EXAMPLE
    Convert-ToDateTime -Timestring '132479040000000000' -Ignore '*1601*'
    # Converts the file time string '132479040000000000' to a DateTime object, ignoring any file time strings containing '1601'.
 
    #>

    [CmdletBinding()]
    param (
        [string] $Timestring,
        [string] $Ignore = '*1601*'
    )
    Try {
        $DateTime = ([datetime]::FromFileTime($Timestring))
    } catch {
        $DateTime = $null
    }
    if ($null -eq $DateTime -or $Timestring -like $Ignore) {
        return $null
    } else {
        return $DateTime
    }
}
function Convert-UserAccountControl { 
    <#
    .SYNOPSIS
    Converts the UserAccountControl flags to their corresponding names.
 
    .DESCRIPTION
    This function takes a UserAccountControl value and converts it into a human-readable format by matching the flags to their corresponding names.
 
    .PARAMETER UserAccountControl
    Specifies the UserAccountControl value to be converted.
 
    .PARAMETER Separator
    Specifies the separator to use when joining the converted flags. If not provided, the flags will be returned as a list.
 
    .EXAMPLE
    Convert-UserAccountControl -UserAccountControl 66048
    Outputs: "DONT_EXPIRE_PASSWORD, PASSWORD_EXPIRED"
 
    .EXAMPLE
    Convert-UserAccountControl -UserAccountControl 512 -Separator ', '
    Outputs: "NORMAL_ACCOUNT"
 
    #>

    [cmdletBinding()]
    param(
        [alias('UAC')][int] $UserAccountControl,
        [string] $Separator
    )
    $UserAccount = [ordered] @{
        "SCRIPT"                         = 1
        "ACCOUNTDISABLE"                 = 2
        "HOMEDIR_REQUIRED"               = 8
        "LOCKOUT"                        = 16
        "PASSWD_NOTREQD"                 = 32
        "ENCRYPTED_TEXT_PWD_ALLOWED"     = 128
        "TEMP_DUPLICATE_ACCOUNT"         = 256
        "NORMAL_ACCOUNT"                 = 512
        "INTERDOMAIN_TRUST_ACCOUNT"      = 2048
        "WORKSTATION_TRUST_ACCOUNT"      = 4096
        "SERVER_TRUST_ACCOUNT"           = 8192
        "DONT_EXPIRE_PASSWORD"           = 65536
        "MNS_LOGON_ACCOUNT"              = 131072
        "SMARTCARD_REQUIRED"             = 262144
        "TRUSTED_FOR_DELEGATION"         = 524288
        "NOT_DELEGATED"                  = 1048576
        "USE_DES_KEY_ONLY"               = 2097152
        "DONT_REQ_PREAUTH"               = 4194304
        "PASSWORD_EXPIRED"               = 8388608
        "TRUSTED_TO_AUTH_FOR_DELEGATION" = 16777216
        "PARTIAL_SECRETS_ACCOUNT"        = 67108864
    }
    $Output = foreach ($_ in $UserAccount.Keys) {
        $binaryAnd = $UserAccount[$_] -band $UserAccountControl
        if ($binaryAnd -ne "0") {
            $_
        }
    }
    if ($Separator) {
        $Output -join $Separator
    } else {
        $Output
    }
}
function ConvertFrom-DistinguishedName { 
    <#
    .SYNOPSIS
    Converts a Distinguished Name to CN, OU, Multiple OUs or DC
 
    .DESCRIPTION
    Converts a Distinguished Name to CN, OU, Multiple OUs or DC
 
    .PARAMETER DistinguishedName
    Distinguished Name to convert
 
    .PARAMETER ToOrganizationalUnit
    Converts DistinguishedName to Organizational Unit
 
    .PARAMETER ToDC
    Converts DistinguishedName to DC
 
    .PARAMETER ToDomainCN
    Converts DistinguishedName to Domain Canonical Name (CN)
 
    .PARAMETER ToCanonicalName
    Converts DistinguishedName to Canonical Name
 
    .EXAMPLE
    $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz'
    ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToOrganizationalUnit
 
    Output:
    OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    $DistinguishedName = 'CN=Przemyslaw Klys,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz'
    ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName
 
    Output:
    Przemyslaw Klys
 
    .EXAMPLE
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit -IncludeParent
 
    Output:
    OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz
    OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToMultipleOrganizationalUnit
 
    Output:
    OU=Production,DC=ad,DC=evotec,DC=xyz
 
    .EXAMPLE
    $Con = @(
        'CN=Windows Authorization Access Group,CN=Builtin,DC=ad,DC=evotec,DC=xyz'
        'CN=Mmm,DC=elo,CN=nee,DC=RootDNSServers,CN=MicrosoftDNS,CN=System,DC=ad,DC=evotec,DC=xyz'
        'CN=e6d5fd00-385d-4e65-b02d-9da3493ed850,CN=Operations,CN=DomainUpdates,CN=System,DC=ad,DC=evotec,DC=xyz'
        'OU=Domain Controllers,DC=ad,DC=evotec,DC=pl'
        'OU=Microsoft Exchange Security Groups,DC=ad,DC=evotec,DC=xyz'
    )
 
    ConvertFrom-DistinguishedName -DistinguishedName $Con -ToLastName
 
    Output:
    Windows Authorization Access Group
    Mmm
    e6d5fd00-385d-4e65-b02d-9da3493ed850
    Domain Controllers
    Microsoft Exchange Security Groups
 
    .EXAMPLEE
    ConvertFrom-DistinguishedName -DistinguishedName 'DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
    ConvertFrom-DistinguishedName -DistinguishedName 'OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
    ConvertFrom-DistinguishedName -DistinguishedName 'CN=test,OU=Users,OU=Production,DC=ad,DC=evotec,DC=xyz' -ToCanonicalName
 
    Output:
    ad.evotec.xyz
    ad.evotec.xyz\Production\Users
    ad.evotec.xyz\Production\Users\test
 
    .NOTES
    General notes
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(ParameterSetName = 'ToOrganizationalUnit')]
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')]
        [Parameter(ParameterSetName = 'ToDC')]
        [Parameter(ParameterSetName = 'ToDomainCN')]
        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'ToLastName')]
        [Parameter(ParameterSetName = 'ToCanonicalName')]
        [alias('Identity', 'DN')][Parameter(ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][string[]] $DistinguishedName,
        [Parameter(ParameterSetName = 'ToOrganizationalUnit')][switch] $ToOrganizationalUnit,
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][alias('ToMultipleOU')][switch] $ToMultipleOrganizationalUnit,
        [Parameter(ParameterSetName = 'ToMultipleOrganizationalUnit')][switch] $IncludeParent,
        [Parameter(ParameterSetName = 'ToDC')][switch] $ToDC,
        [Parameter(ParameterSetName = 'ToDomainCN')][switch] $ToDomainCN,
        [Parameter(ParameterSetName = 'ToLastName')][switch] $ToLastName,
        [Parameter(ParameterSetName = 'ToCanonicalName')][switch] $ToCanonicalName
    )
    Process {
        foreach ($Distinguished in $DistinguishedName) {
            if ($ToDomainCN) {
                $DN = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                $CN = $DN -replace ',DC=', '.' -replace "DC="
                if ($CN) {
                    $CN
                }
            } elseif ($ToOrganizationalUnit) {
                $Value = [Regex]::Match($Distinguished, '(?=OU=)(.*\n?)(?<=.)').Value
                if ($Value) {
                    $Value
                }
            } elseif ($ToMultipleOrganizationalUnit) {
                if ($IncludeParent) {
                    $Distinguished
                }
                while ($true) {

                    $Distinguished = $Distinguished -replace '^.+?,(?=..=)'
                    if ($Distinguished -match '^DC=') {
                        break
                    }
                    $Distinguished
                }
            } elseif ($ToDC) {

                $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                if ($Value) {
                    $Value
                }
            } elseif ($ToLastName) {

                $NewDN = $Distinguished -split ",DC="
                if ($NewDN[0].Contains(",OU=")) {
                    [Array] $ChangedDN = $NewDN[0] -split ",OU="
                } elseif ($NewDN[0].Contains(",CN=")) {
                    [Array] $ChangedDN = $NewDN[0] -split ",CN="
                } else {
                    [Array] $ChangedDN = $NewDN[0]
                }
                if ($ChangedDN[0].StartsWith('CN=')) {
                    $ChangedDN[0] -replace 'CN=', ''
                } else {
                    $ChangedDN[0] -replace 'OU=', ''
                }
            } elseif ($ToCanonicalName) {
                $Domain = $null
                $Rest = $null
                foreach ($O in $Distinguished -split '(?<!\\),') {
                    if ($O -match '^DC=') {
                        $Domain += $O.Substring(3) + '.'
                    } else {
                        $Rest = $O.Substring(3) + '\' + $Rest
                    }
                }
                if ($Domain -and $Rest) {
                    $Domain.Trim('.') + '\' + ($Rest.TrimEnd('\') -replace '\\,', ',')
                } elseif ($Domain) {
                    $Domain.Trim('.')
                } elseif ($Rest) {
                    $Rest.TrimEnd('\') -replace '\\,', ','
                }
            } else {
                $Regex = '^CN=(?<cn>.+?)(?<!\\),(?<ou>(?:(?:OU|CN).+?(?<!\\),)+(?<dc>DC.+?))$'

                $Found = $Distinguished -match $Regex
                if ($Found) {
                    $Matches.cn
                }
            }
        }
    }
}
function ConvertFrom-NetbiosName { 
    <#
    .SYNOPSIS
    Converts a NetBIOS name to its corresponding domain name and object name.
 
    .DESCRIPTION
    This function takes a NetBIOS name in the format 'Domain\Object' and converts it to the corresponding domain name and object name.
 
    .PARAMETER Identity
    Specifies the NetBIOS name(s) to convert.
 
    .EXAMPLE
    'TEST\Domain Admins', 'EVOTEC\Domain Admins', 'EVOTECPL\Domain Admins' | ConvertFrom-NetbiosName
    Converts the NetBIOS names 'TEST\Domain Admins', 'EVOTEC\Domain Admins', and 'EVOTECPL\Domain Admins' to their corresponding domain names and object names.
 
    .EXAMPLE
    ConvertFrom-NetbiosName -Identity 'TEST\Domain Admins', 'EVOTEC\Domain Admins', 'EVOTECPL\Domain Admins'
    Converts the NetBIOS names 'TEST\Domain Admins', 'EVOTEC\Domain Admins', and 'EVOTECPL\Domain Admins' to their corresponding domain names and object names.
 
    #>

    [cmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)]
        [string[]] $Identity
    )
    process {
        foreach ($Ident in $Identity) {
            if ($Ident -like '*\*') {
                $NetbiosWithObject = $Ident -split "\\"
                if ($NetbiosWithObject.Count -eq 2) {
                    $LDAPQuery = ([ADSI]"LDAP://$($NetbiosWithObject[0])")
                    $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $LDAPQuery.distinguishedName -ToDomainCN
                    [PSCustomObject] @{
                        DomainName = $DomainName
                        Name       = $NetbiosWithObject[1]
                    }
                } else {

                    [PSCustomObject] @{
                        DomainName = ''
                        Name       = $Ident
                    }
                }
            } else {

                [PSCustomObject] @{
                    DomainName = ''
                    Name       = $Ident
                }
            }
        }
    }
}
function ConvertFrom-SID { 
    <#
    .SYNOPSIS
    Small command that can resolve SID values
 
    .DESCRIPTION
    Small command that can resolve SID values
 
    .PARAMETER SID
    Value to resolve
 
    .PARAMETER OnlyWellKnown
    Only resolve SID when it's well know SID. Otherwise return $null
 
    .PARAMETER OnlyWellKnownAdministrative
    Only resolve SID when it's administrative well know SID. Otherwise return $null
 
    .PARAMETER DoNotResolve
    Uses only dicrionary values without querying AD
 
    .EXAMPLE
    ConvertFrom-SID -SID 'S-1-5-8', 'S-1-5-9', 'S-1-5-11', 'S-1-5-18', 'S-1-1-0' -DoNotResolve
 
    .NOTES
    General notes
    #>

    [cmdletbinding(DefaultParameterSetName = 'Standard')]
    param(
        [Parameter(ParameterSetName = 'Standard')]
        [Parameter(ParameterSetName = 'OnlyWellKnown')]
        [Parameter(ParameterSetName = 'OnlyWellKnownAdministrative')]
        [string[]] $SID,
        [Parameter(ParameterSetName = 'OnlyWellKnown')][switch] $OnlyWellKnown,
        [Parameter(ParameterSetName = 'OnlyWellKnownAdministrative')][switch] $OnlyWellKnownAdministrative,
        [Parameter(ParameterSetName = 'Standard')][switch] $DoNotResolve
    )

    $WellKnownAdministrative = @{
        'S-1-5-18'     = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\SYSTEM'
            SID        = 'S-1-5-18'
            DomainName = ''
            Type       = 'WellKnownAdministrative'
            Error      = ''
        }
        'S-1-5-32-544' = [PSCustomObject] @{
            Name       = 'BUILTIN\Administrators'
            SID        = 'S-1-5-32-544'
            DomainName = ''
            Type       = 'WellKnownAdministrative'
            Error      = ''
        }
    }
    $wellKnownSIDs = @{
        'S-1-0'                                                           = [PSCustomObject] @{
            Name       = 'Null AUTHORITY'
            SID        = 'S-1-0'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-0-0'                                                         = [PSCustomObject] @{
            Name       = 'NULL SID'
            SID        = 'S-1-0-0'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-1'                                                           = [PSCustomObject] @{
            Name       = 'WORLD AUTHORITY'
            SID        = 'S-1-1'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-1-0'                                                         = [PSCustomObject] @{
            Name       = 'Everyone'
            SID        = 'S-1-1-0'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-2'                                                           = [PSCustomObject] @{
            Name       = 'LOCAL AUTHORITY'
            SID        = 'S-1-2'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-2-0'                                                         = [PSCustomObject] @{
            Name       = 'LOCAL'
            SID        = 'S-1-2-0'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-2-1'                                                         = [PSCustomObject] @{
            Name       = 'CONSOLE LOGON'
            SID        = 'S-1-2-1'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-3'                                                           = [PSCustomObject] @{
            Name       = 'CREATOR AUTHORITY'
            SID        = 'S-1-3'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-3-0'                                                         = [PSCustomObject] @{
            Name       = 'CREATOR OWNER'
            SID        = 'S-1-3-0'
            DomainName = ''
            Type       = 'WellKnownAdministrative'
            Error      = ''
        }
        'S-1-3-1'                                                         = [PSCustomObject] @{
            Name       = 'CREATOR GROUP'
            SID        = 'S-1-3-1'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-3-2'                                                         = [PSCustomObject] @{
            Name       = 'CREATOR OWNER SERVER'
            SID        = 'S-1-3-2'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-3-3'                                                         = [PSCustomObject] @{
            Name       = 'CREATOR GROUP SERVER'
            SID        = 'S-1-3-3'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-3-4'                                                         = [PSCustomObject] @{
            Name       = 'OWNER RIGHTS'
            SID        = 'S-1-3-4'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-80-0'                                                      = [PSCustomObject] @{
            Name       = 'NT SERVICE\ALL SERVICES'
            SID        = 'S-1-5-80-0'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-4'                                                           = [PSCustomObject] @{
            Name       = 'Non-unique Authority'
            SID        = 'S-1-4'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5'                                                           = [PSCustomObject] @{
            Name       = 'NT AUTHORITY'
            SID        = 'S-1-5'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-1'                                                         = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\DIALUP'
            SID        = 'S-1-5-1'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-2'                                                         = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\NETWORK'
            SID        = 'S-1-5-2'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-3'                                                         = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\BATCH'
            SID        = 'S-1-5-3'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-4'                                                         = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\INTERACTIVE'
            SID        = 'S-1-5-4'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-6'                                                         = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\SERVICE'
            SID        = 'S-1-5-6'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-7'                                                         = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\ANONYMOUS LOGON'
            SID        = 'S-1-5-7'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-8'                                                         = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\PROXY'
            SID        = 'S-1-5-8'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-9'                                                         = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\ENTERPRISE DOMAIN CONTROLLERS'
            SID        = 'S-1-5-9'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-10'                                                        = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\SELF'
            SID        = 'S-1-5-10'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-11'                                                        = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\Authenticated Users'
            SID        = 'S-1-5-11'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-12'                                                        = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\RESTRICTED'
            SID        = 'S-1-5-12'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-13'                                                        = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\TERMINAL SERVER USER'
            SID        = 'S-1-5-13'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-14'                                                        = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\REMOTE INTERACTIVE LOGON'
            SID        = 'S-1-5-14'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-15'                                                        = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\This Organization'
            SID        = 'S-1-5-15'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-17'                                                        = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\IUSR'
            SID        = 'S-1-5-17'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-18'                                                        = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\SYSTEM'
            SID        = 'S-1-5-18'
            DomainName = ''
            Type       = 'WellKnownAdministrative'
            Error      = ''
        }
        'S-1-5-19'                                                        = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\LOCAL SERVICE'
            SID        = 'S-1-5-19'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-20'                                                        = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\NETWORK SERVICE'
            SID        = 'S-1-5-20'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-544'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Administrators'
            SID        = 'S-1-5-32-544'
            DomainName = ''
            Type       = 'WellKnownAdministrative'
            Error      = ''
        }
        'S-1-5-32-545'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Users'
            SID        = 'S-1-5-32-545'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-546'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Guests'
            SID        = 'S-1-5-32-546'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-547'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Power Users'
            SID        = 'S-1-5-32-547'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-548'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Account Operators'
            SID        = 'S-1-5-32-548'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-549'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Server Operators'
            SID        = 'S-1-5-32-549'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-550'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Print Operators'
            SID        = 'S-1-5-32-550'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-551'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Backup Operators'
            SID        = 'S-1-5-32-551'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-552'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Replicators'
            SID        = 'S-1-5-32-552'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-64-10'                                                     = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\NTLM Authentication'
            SID        = 'S-1-5-64-10'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-64-14'                                                     = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\SChannel Authentication'
            SID        = 'S-1-5-64-14'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-64-21'                                                     = [PSCustomObject] @{
            Name       = 'NT AUTHORITY\Digest Authentication'
            SID        = 'S-1-5-64-21'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-80'                                                        = [PSCustomObject] @{
            Name       = 'NT SERVICE'
            SID        = 'S-1-5-80'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-83-0'                                                      = [PSCustomObject] @{
            Name       = 'NT VIRTUAL MACHINE\Virtual Machines'
            SID        = 'S-1-5-83-0'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-16-0'                                                        = [PSCustomObject] @{
            Name       = 'Untrusted Mandatory Level'
            SID        = 'S-1-16-0'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-16-4096'                                                     = [PSCustomObject] @{
            Name       = 'Low Mandatory Level'
            SID        = 'S-1-16-4096'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-16-8192'                                                     = [PSCustomObject] @{
            Name       = 'Medium Mandatory Level'
            SID        = 'S-1-16-8192'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-16-8448'                                                     = [PSCustomObject] @{
            Name       = 'Medium Plus Mandatory Level'
            SID        = 'S-1-16-8448'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-16-12288'                                                    = [PSCustomObject] @{
            Name       = 'High Mandatory Level'
            SID        = 'S-1-16-12288'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-16-16384'                                                    = [PSCustomObject] @{
            Name       = 'System Mandatory Level'
            SID        = 'S-1-16-16384'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-16-20480'                                                    = [PSCustomObject] @{
            Name       = 'Protected Process Mandatory Level'
            SID        = 'S-1-16-20480'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-16-28672'                                                    = [PSCustomObject] @{
            Name       = 'Secure Process Mandatory Level'
            SID        = 'S-1-16-28672'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-554'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Pre-Windows 2000 Compatible Access'
            SID        = 'S-1-5-32-554'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-555'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Remote Desktop Users'
            SID        = 'S-1-5-32-555'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-556'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Network Configuration Operators'
            SID        = 'S-1-5-32-556'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-557'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Incoming Forest Trust Builders'
            SID        = 'S-1-5-32-557'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-558'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Performance Monitor Users'
            SID        = 'S-1-5-32-558'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-559'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Performance Log Users'
            SID        = 'S-1-5-32-559'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-560'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Windows Authorization Access Group'
            SID        = 'S-1-5-32-560'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-561'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Terminal Server License Servers'
            SID        = 'S-1-5-32-561'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-562'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Distributed COM Users'
            SID        = 'S-1-5-32-562'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-568'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\IIS_IUSRS'
            SID        = 'S-1-5-32-568'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-569'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Cryptographic Operators'
            SID        = 'S-1-5-32-569'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-573'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Event Log Readers'
            SID        = 'S-1-5-32-573'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-574'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Certificate Service DCOM Access'
            SID        = 'S-1-5-32-574'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-575'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\RDS Remote Access Servers'
            SID        = 'S-1-5-32-575'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-576'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\RDS Endpoint Servers'
            SID        = 'S-1-5-32-576'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-577'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\RDS Management Servers'
            SID        = 'S-1-5-32-577'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-578'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Hyper-V Administrators'
            SID        = 'S-1-5-32-578'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-579'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Access Control Assistance Operators'
            SID        = 'S-1-5-32-579'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-32-580'                                                    = [PSCustomObject] @{
            Name       = 'BUILTIN\Remote Management Users'
            SID        = 'S-1-5-32-580'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-90-0'                                                      = [PSCustomObject] @{
            Name       = 'Window Manager\Window Manager Group'
            SID        = 'S-1-5-90-0'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420'  = [PSCustomObject] @{
            Name       = 'NT SERVICE\WdiServiceHost'
            SID        = 'S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-80-3880718306-3832830129-1677859214-2598158968-1052248003' = [PSCustomObject] @{
            Name       = 'NT SERVICE\MSSQLSERVER'
            SID        = 'S-1-5-80-3139157870-2983391045-3678747466-658725712-1809340420'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430'  = [PSCustomObject] @{
            Name       = 'NT SERVICE\SQLSERVERAGENT'
            SID        = 'S-1-5-80-344959196-2060754871-2302487193-2804545603-1466107430'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-80-2652535364-2169709536-2857650723-2622804123-1107741775' = [PSCustomObject] @{
            Name       = 'NT SERVICE\SQLTELEMETRY'
            SID        = 'S-1-5-80-2652535364-2169709536-2857650723-2622804123-1107741775'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-80-3245704983-3664226991-764670653-2504430226-901976451'   = [PSCustomObject] @{
            Name       = 'NT SERVICE\ADSync'
            SID        = 'S-1-5-80-3245704983-3664226991-764670653-2504430226-901976451'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
        'S-1-5-80-4215458991-2034252225-2287069555-1155419622-2701885083' = [PSCustomObject] @{
            Name       = 'NT Service\himds'
            SID        = 'S-1-5-80-4215458991-2034252225-2287069555-1155419622-2701885083'
            DomainName = ''
            Type       = 'WellKnownGroup'
            Error      = ''
        }
    }
    foreach ($S in $SID) {
        if ($OnlyWellKnownAdministrative) {

            if ($WellKnownAdministrative[$S]) {
                $WellKnownAdministrative[$S]
            }
        } elseif ($OnlyWellKnown) {

            if ($wellKnownSIDs[$S]) {
                $wellKnownSIDs[$S]
            }
        } else {

            if ($wellKnownSIDs[$S]) {
                $wellKnownSIDs[$S]
            } else {
                if ($DoNotResolve) {
                    if ($S -like "S-1-5-21-*-519" -or $S -like "S-1-5-21-*-512") {

                        [PSCustomObject] @{
                            Name       = $S
                            SID        = $S
                            DomainName = '' 
                            Type       = 'Administrative'
                            Error      = ''
                        }
                    } else {

                        [PSCustomObject] @{
                            Name       = $S
                            SID        = $S
                            DomainName = ''
                            Error      = ''
                            Type       = 'NotAdministrative'
                        }
                    }
                } else {
                    if (-not $Script:LocalComputerSID) {
                        $Script:LocalComputerSID = Get-LocalComputerSid
                    }
                    try {
                        if ($S.Length -le 18) {
                            $Type = 'NotAdministrative'
                            $Name = (([System.Security.Principal.SecurityIdentifier]::new($S)).Translate([System.Security.Principal.NTAccount])).Value
                            [PSCustomObject] @{
                                Name       = $Name
                                SID        = $S
                                DomainName = ''
                                Type       = $Type
                                Error      = ''
                            }
                        } else {
                            if ($S -like "S-1-5-21-*-519" -or $S -like "S-1-5-21-*-512") {
                                $Type = 'Administrative'
                            } else {
                                $Type = 'NotAdministrative'
                            }
                            $Name = (([System.Security.Principal.SecurityIdentifier]::new($S)).Translate([System.Security.Principal.NTAccount])).Value
                            [PSCustomObject] @{
                                Name       = $Name
                                SID        = $S
                                DomainName = if ($S -like "$Script:LocalComputerSID*") {
                                    '' 
                                } else {
 (ConvertFrom-NetbiosName -Identity $Name).DomainName 
                                }
                                Type       = $Type
                                Error      = ''
                            }
                        }
                    } catch {

                        [PSCustomObject] @{
                            Name       = $S
                            SID        = $S
                            DomainName = ''
                            Error      = $_.Exception.Message -replace [environment]::NewLine, ' '
                            Type       = 'Unknown'
                        }
                    }
                }
            }
        }
    }
}
function ConvertTo-DistinguishedName { 
    <#
    .SYNOPSIS
    Converts CanonicalName to DistinguishedName
 
    .DESCRIPTION
    Converts CanonicalName to DistinguishedName for 3 different options
 
    .PARAMETER CanonicalName
    One or multiple canonical names
 
    .PARAMETER ToOU
    Converts CanonicalName to OrganizationalUnit DistinguishedName
 
    .PARAMETER ToObject
    Converts CanonicalName to Full Object DistinguishedName
 
    .PARAMETER ToDomain
    Converts CanonicalName to Domain DistinguishedName
 
    .EXAMPLE
 
    $CanonicalObjects = @(
    'ad.evotec.xyz/Production/Groups/Security/ITR03_AD Admins'
    'ad.evotec.xyz/Production/Accounts/Special/SADM Testing 2'
    )
    $CanonicalOU = @(
        'ad.evotec.xyz/Production/Groups/Security/NetworkAdministration'
        'ad.evotec.xyz/Production'
    )
 
    $CanonicalDomain = @(
        'ad.evotec.xyz/Production/Groups/Security/ITR03_AD Admins'
        'ad.evotec.pl'
        'ad.evotec.xyz'
        'test.evotec.pl'
        'ad.evotec.xyz/Production'
    )
    $CanonicalObjects | ConvertTo-DistinguishedName -ToObject
    $CanonicalOU | ConvertTo-DistinguishedName -ToOU
    $CanonicalDomain | ConvertTo-DistinguishedName -ToDomain
 
    Output:
    CN=ITR03_AD Admins,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz
    CN=SADM Testing 2,OU=Special,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz
    Output2:
    OU=NetworkAdministration,OU=Security,OU=Groups,OU=Production,DC=ad,DC=evotec,DC=xyz
    OU=Production,DC=ad,DC=evotec,DC=xyz
    Output3:
    DC=ad,DC=evotec,DC=xyz
    DC=ad,DC=evotec,DC=pl
    DC=ad,DC=evotec,DC=xyz
    DC=test,DC=evotec,DC=pl
    DC=ad,DC=evotec,DC=xyz
 
    .NOTES
    General notes
    #>

    [cmdletBinding(DefaultParameterSetName = 'ToDomain')]
    param(
        [Parameter(ParameterSetName = 'ToOU')]
        [Parameter(ParameterSetName = 'ToObject')]
        [Parameter(ParameterSetName = 'ToDomain')]
        [alias('Identity', 'CN')][Parameter(ValueFromPipeline, Mandatory, ValueFromPipelineByPropertyName, Position = 0)][string[]] $CanonicalName,
        [Parameter(ParameterSetName = 'ToOU')][switch] $ToOU,
        [Parameter(ParameterSetName = 'ToObject')][switch] $ToObject,
        [Parameter(ParameterSetName = 'ToDomain')][switch] $ToDomain
    )
    Process {
        foreach ($CN in $CanonicalName) {
            if ($ToObject) {
                $ADObject = $CN.Replace(',', '\,').Split('/')
                [string]$DN = "CN=" + $ADObject[$ADObject.count - 1]
                for ($i = $ADObject.count - 2; $i -ge 1; $i--) {
                    $DN += ",OU=" + $ADObject[$i]
                }
                $ADObject[0].split(".") | ForEach-Object {
                    $DN += ",DC=" + $_
                }
            } elseif ($ToOU) {
                $ADObject = $CN.Replace(',', '\,').Split('/')
                [string]$DN = "OU=" + $ADObject[$ADObject.count - 1]
                for ($i = $ADObject.count - 2; $i -ge 1; $i--) {
                    $DN += ",OU=" + $ADObject[$i]
                }
                $ADObject[0].split(".") | ForEach-Object {
                    $DN += ",DC=" + $_
                }
            } else {
                $ADObject = $CN.Replace(',', '\,').Split('/')

                $DN = 'DC=' + $ADObject[0].Replace('.', ',DC=')
            }
            $DN
        }
    }
}
function ConvertTo-OperatingSystem { 
    <#
    .SYNOPSIS
    Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD
 
    .DESCRIPTION
    Allows easy conversion of OperatingSystem, Operating System Version to proper Windows 10 naming based on WMI or AD
 
    .PARAMETER OperatingSystem
    Operating System as returned by Active Directory
 
    .PARAMETER OperatingSystemVersion
    Operating System Version as returned by Active Directory
 
    .EXAMPLE
    $Computers = Get-ADComputer -Filter * -Properties OperatingSystem, OperatingSystemVersion | ForEach-Object {
        $OPS = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion
        Add-Member -MemberType NoteProperty -Name 'OperatingSystemTranslated' -Value $OPS -InputObject $_ -Force
        $_
    }
    $Computers | Select-Object DNS*, Name, SamAccountName, Enabled, OperatingSystem*, DistinguishedName | Format-Table
 
    .EXAMPLE
    $Registry = Get-PSRegistry -ComputerName 'AD1' -RegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
    ConvertTo-OperatingSystem -OperatingSystem $Registry.ProductName -OperatingSystemVersion $Registry.CurrentBuildNumber
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $OperatingSystem,
        [string] $OperatingSystemVersion
    )

    if ($OperatingSystem -like 'Windows 10*' -or $OperatingSystem -like 'Windows 11*') {
        $Systems = @{

            '10.0 (22621)' = 'Windows 11 22H2'
            '10.0 (22000)' = 'Windows 11 21H2'
            '10.0 (19045)' = 'Windows 10 22H2'
            '10.0 (19044)' = 'Windows 10 21H2'
            '10.0 (19043)' = 'Windows 10 21H1'
            '10.0 (19042)' = 'Windows 10 20H2'
            '10.0 (19041)' = 'Windows 10 2004'
            '10.0 (18898)' = 'Windows 10 Insider Preview'
            '10.0 (18363)' = "Windows 10 1909"
            '10.0 (18362)' = "Windows 10 1903"
            '10.0 (17763)' = "Windows 10 1809"
            '10.0 (17134)' = "Windows 10 1803"
            '10.0 (16299)' = "Windows 10 1709"
            '10.0 (15063)' = "Windows 10 1703"
            '10.0 (14393)' = "Windows 10 1607"
            '10.0 (10586)' = "Windows 10 1511"
            '10.0 (10240)' = "Windows 10 1507"

            '10.0.22621'   = 'Windows 11 22H2'
            '10.0.22000'   = 'Windows 11 21H2'
            '10.0.19045'   = 'Windows 10 22H2'
            '10.0.19044'   = 'Windows 10 21H2'
            '10.0.19043'   = 'Windows 10 21H1'
            '10.0.19042'   = 'Windows 10 20H2'
            '10.0.19041'   = 'Windows 10 2004'
            '10.0.18898'   = 'Windows 10 Insider Preview'
            '10.0.18363'   = "Windows 10 1909"
            '10.0.18362'   = "Windows 10 1903"
            '10.0.17763'   = "Windows 10 1809"
            '10.0.17134'   = "Windows 10 1803"
            '10.0.16299'   = "Windows 10 1709"
            '10.0.15063'   = "Windows 10 1703"
            '10.0.14393'   = "Windows 10 1607"
            '10.0.10586'   = "Windows 10 1511"
            '10.0.10240'   = "Windows 10 1507"

            '22621'        = 'Windows 11 22H2'
            '22000'        = 'Windows 11 21H2'
            '19045'        = 'Windows 10 22H2'
            '19044'        = 'Windows 10 21H2'
            '19043'        = 'Windows 10 21H1'
            '19042'        = 'Windows 10 20H2'
            '19041'        = 'Windows 10 2004'
            '18898'        = 'Windows 10 Insider Preview'
            '18363'        = "Windows 10 1909"
            '18362'        = "Windows 10 1903"
            '17763'        = "Windows 10 1809"
            '17134'        = "Windows 10 1803"
            '16299'        = "Windows 10 1709"
            '15063'        = "Windows 10 1703"
            '14393'        = "Windows 10 1607"
            '10586'        = "Windows 10 1511"
            '10240'        = "Windows 10 1507"
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) {
            $System = $OperatingSystemVersion
        }
    } elseif ($OperatingSystem -like 'Windows Server*') {

        $Systems = @{

            '10.0 (20348)' = 'Windows Server 2022'
            '10.0 (19042)' = 'Windows Server 2019 20H2'
            '10.0 (19041)' = 'Windows Server 2019 2004'
            '10.0 (18363)' = 'Windows Server 2019 1909'
            '10.0 (18362)' = "Windows Server 2019 1903" 
            '10.0 (17763)' = "Windows Server 2019 1809" 
            '10.0 (17134)' = "Windows Server 2016 1803" 
            '10.0 (14393)' = "Windows Server 2016 1607"
            '6.3 (9600)'   = 'Windows Server 2012 R2'
            '6.1 (7601)'   = 'Windows Server 2008 R2'
            '5.2 (3790)'   = 'Windows Server 2003'

            '10.0.20348'   = 'Windows Server 2022'
            '10.0.19042'   = 'Windows Server 2019 20H2'
            '10.0.19041'   = 'Windows Server 2019 2004'
            '10.0.18363'   = 'Windows Server 2019 1909'
            '10.0.18362'   = "Windows Server 2019 1903" 
            '10.0.17763'   = "Windows Server 2019 1809"  
            '10.0.17134'   = "Windows Server 2016 1803" 
            '10.0.14393'   = "Windows Server 2016 1607"
            '6.3.9600'     = 'Windows Server 2012 R2'
            '6.1.7601'     = 'Windows Server 2008 R2' 
            '5.2.3790'     = 'Windows Server 2003' 

            '20348'        = 'Windows Server 2022'
            '19042'        = 'Windows Server 2019 20H2'
            '19041'        = 'Windows Server 2019 2004'
            '18363'        = 'Windows Server 2019 1909'
            '18362'        = "Windows Server 2019 1903" 
            '17763'        = "Windows Server 2019 1809" 
            '17134'        = "Windows Server 2016 1803" 
            '14393'        = "Windows Server 2016 1607"
            '9600'         = 'Windows Server 2012 R2'
            '7601'         = 'Windows Server 2008 R2'
            '3790'         = 'Windows Server 2003'
        }
        $System = $Systems[$OperatingSystemVersion]
        if (-not $System) {
            $System = $OperatingSystemVersion
        }
    } else {
        $System = $OperatingSystem
    }
    if ($System) {
        $System
    } else {
        'Unknown'
    }
}
function Copy-Dictionary { 
    <#
    .SYNOPSIS
    Copies dictionary/hashtable
 
    .DESCRIPTION
    Copies dictionary uusing PS Serializer. Replaces usage of BinnaryFormatter due to no support in PS 7.4
 
    .PARAMETER Dictionary
    Dictionary to copy
 
    .EXAMPLE
    $Test = [ordered] @{
        Test = 'Test'
        Test1 = @{
            Test2 = 'Test2'
            Test3 = @{
                Test4 = 'Test4'
            }
        }
        Test2 = @(
            "1", "2", "3"
        )
        Test3 = [PSCustomObject] @{
            Test4 = 'Test4'
            Test5 = 'Test5'
        }
    }
 
    $New1 = Copy-Dictionary -Dictionary $Test
    $New1
 
    .NOTES
 
    #>

    [alias('Copy-Hashtable', 'Copy-OrderedHashtable')]
    [cmdletbinding()]
    param(
        [System.Collections.IDictionary] $Dictionary
    )
    $clone = [System.Management.Automation.PSSerializer]::Serialize($Dictionary, [int32]::MaxValue)
    return [System.Management.Automation.PSSerializer]::Deserialize($clone)
}
function Get-ADADministrativeGroups { 
    <#
    .SYNOPSIS
    Retrieves administrative groups information from Active Directory.
 
    .DESCRIPTION
    This function retrieves information about administrative groups in Active Directory based on the specified parameters.
 
    .PARAMETER Type
    Specifies the type of administrative groups to retrieve. Valid values are 'DomainAdmins' and 'EnterpriseAdmins'.
 
    .PARAMETER Forest
    Specifies the name of the forest to query for administrative groups.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domains to exclude from the query.
 
    .PARAMETER IncludeDomains
    Specifies an array of domains to include in the query.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest to include in the query.
 
    .EXAMPLE
    Get-ADADministrativeGroups -Type DomainAdmins, EnterpriseAdmins
 
    Output (Where VALUE is Get-ADGroup output):
    Name Value
    ---- -----
    ByNetBIOS {EVOTEC\Domain Admins, EVOTEC\Enterprise Admins, EVOTECPL\Domain Admins}
    ad.evotec.xyz {DomainAdmins, EnterpriseAdmins}
    ad.evotec.pl {DomainAdmins}
 
    .NOTES
    This function requires Active Directory module to be installed on the system.
    #>

    [cmdletBinding()]
    param(
        [parameter(Mandatory)][validateSet('DomainAdmins', 'EnterpriseAdmins')][string[]] $Type,
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ADDictionary = [ordered] @{ }
    $ADDictionary['ByNetBIOS'] = [ordered] @{ }
    $ADDictionary['BySID'] = [ordered] @{ }

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Domain in $ForestInformation.Domains) {
        $ADDictionary[$Domain] = [ordered] @{ }
        $QueryServer = $ForestInformation['QueryServers'][$Domain]['HostName'][0]
        $DomainInformation = Get-ADDomain -Server $QueryServer

        if ($Type -contains 'DomainAdmins') {
            Get-ADGroup -Filter "SID -eq '$($DomainInformation.DomainSID)-512'" -Server $QueryServer -ErrorAction SilentlyContinue | ForEach-Object {
                $ADDictionary['ByNetBIOS']["$($DomainInformation.NetBIOSName)\$($_.Name)"] = $_
                $ADDictionary[$Domain]['DomainAdmins'] = "$($DomainInformation.NetBIOSName)\$($_.Name)"
                $ADDictionary['BySID'][$_.SID.Value] = $_
            }
        }
    }

    foreach ($Domain in $ForestInformation.Forest.Domains) {
        if (-not $ADDictionary[$Domain]) {
            $ADDictionary[$Domain] = [ordered] @{ }
        }
        if ($Type -contains 'EnterpriseAdmins') {
            $QueryServer = $ForestInformation['QueryServers'][$Domain]['HostName'][0]
            $DomainInformation = Get-ADDomain -Server $QueryServer

            Get-ADGroup -Filter "SID -eq '$($DomainInformation.DomainSID)-519'" -Server $QueryServer -ErrorAction SilentlyContinue | ForEach-Object {
                $ADDictionary['ByNetBIOS']["$($DomainInformation.NetBIOSName)\$($_.Name)"] = $_
                $ADDictionary[$Domain]['EnterpriseAdmins'] = "$($DomainInformation.NetBIOSName)\$($_.Name)"
                $ADDictionary['BySID'][$_.SID.Value] = $_ 
            }
        }
    }
    return $ADDictionary
}
function Get-ADEncryptionTypes { 
    <#
    .SYNOPSIS
    Retrieves the supported encryption types based on the specified value.
 
    .DESCRIPTION
    This function returns the list of encryption types supported by Active Directory based on the provided value. Each encryption type is represented by a string.
 
    .PARAMETER Value
    Specifies the integer value representing the encryption types to retrieve.
 
    .EXAMPLE
    Get-ADEncryptionTypes -Value 24
 
    Retrieves the following encryption types:
    - AES128-CTS-HMAC-SHA1-96
    - AES256-CTS-HMAC-SHA1-96
 
    .NOTES
    This function is designed to provide information about encryption types supported by Active Directory.
    #>

    [cmdletbinding()]
    Param(
        [parameter(Mandatory = $false, ValueFromPipeline = $True)][int32]$Value
    )

    [String[]]$EncryptionTypes = @(
        Foreach ($V in $Value) {
            if ([int32]$V -band 0x00000001) {
                "DES-CBC-CRC" 
            } 
            if ([int32]$V -band 0x00000002) {
                "DES-CBC-MD5" 
            } 
            if ([int32]$V -band 0x00000004) {
                "RC4-HMAC" 
            } 
            if ([int32]$V -band 0x00000008) {
                "AES128-CTS-HMAC-SHA1-96" 
            } 
            if ([int32]$V -band 0x00000010) {
                "AES256-CTS-HMAC-SHA1-96" 
            } 
            if ([int32]$V -band 0x00000020) {
                "FAST-supported" 
            } 
            if ([int32]$V -band 0x00000040) {
                "Compound-identity-supported" 
            } 
            if ([int32]$V -band 0x00000080) {
                "Claims-supported" 
            } 
            if ([int32]$V -band 0x00000200) {
                "Resource-SID-compression-disabled" 
            } 
        }
    )
    $EncryptionTypes
}
function Get-ADTrustAttributes { 
    <#
    .SYNOPSIS
    Retrieves and interprets Active Directory trust attributes based on the provided value.
 
    .DESCRIPTION
    This function retrieves and interprets Active Directory trust attributes based on the provided value. It decodes the binary value into human-readable trust attributes.
 
    .PARAMETER Value
    Specifies the integer value representing the trust attributes.
 
    .EXAMPLE
    Get-ADTrustAttributes -Value 1
    Retrieves and interprets the trust attributes for the value 1.
 
    .EXAMPLE
    1, 2, 4 | Get-ADTrustAttributes
    Retrieves and interprets the trust attributes for the values 1, 2, and 4.
 
    .NOTES
    This function provides a convenient way to decode Active Directory trust attributes.
    #>

    [cmdletbinding()]
    Param(
        [parameter(Mandatory = $false, ValueFromPipeline = $True)][int32]$Value
    )

    [String[]]$TrustAttributes = @(
        Foreach ($V in $Value) {
            if ([int32]$V -band 0x00000001) {
                "Non Transitive" 
            } 
            if ([int32]$V -band 0x00000002) {
                "UpLevel Only" 
            } 
            if ([int32]$V -band 0x00000004) {
                "Quarantined Domain" 
            } 
            if ([int32]$V -band 0x00000008) {
                "Forest Transitive" 
            } 
            if ([int32]$V -band 0x00000010) {
                "Cross Organization" 
            } 
            if ([int32]$V -band 0x00000020) {
                "Within Forest" 
            } 
            if ([int32]$V -band 0x00000040) {
                "Treat as External" 
            } 
            if ([int32]$V -band 0x00000080) {
                "Uses RC4 Encryption" 
            } 
            if ([int32]$V -band 0x00000200) {
                "No TGT DELEGATION" 
            } 
            if ([int32]$V -band 0x00000800) {
                "Enable TGT DELEGATION" 
            } 
            if ([int32]$V -band 0x00000400) {
                "PIM Trust" 
            } 
        }
    )
    return $TrustAttributes
}
function Get-CimData { 
    <#
    .SYNOPSIS
    Helper function for retreiving CIM data from local and remote computers
 
    .DESCRIPTION
    Helper function for retreiving CIM data from local and remote computers
 
    .PARAMETER ComputerName
    Specifies computer on which you want to run the CIM operation. You can specify a fully qualified domain name (FQDN), a NetBIOS name, or an IP address. If you do not specify this parameter, the cmdlet performs the operation on the local computer using Component Object Model (COM).
 
    .PARAMETER Protocol
    Specifies the protocol to use. The acceptable values for this parametDer are: DCOM, Default, or Wsman.
 
    .PARAMETER Class
    Specifies the name of the CIM class for which to retrieve the CIM instances. You can use tab completion to browse the list of classes, because PowerShell gets a list of classes from the local WMI server to provide a list of class names.
 
    .PARAMETER Properties
    Specifies a set of instance properties to retrieve. Use this parameter when you need to reduce the size of the object returned, either in memory or over the network. The object returned also contains the key properties even if you have not listed them using the Property parameter. Other properties of the class are present but they are not populated.
 
    .PARAMETER NameSpace
    Specifies the namespace for the CIM operation. The default namespace is root\cimv2. You can use tab completion to browse the list of namespaces, because PowerShell gets a list of namespaces from the local WMI server to provide a list of namespaces.
 
    .PARAMETER Credential
    Specifies a user account that has permission to perform this action. The default is the current user.
 
    .EXAMPLE
    Get-CimData -Class 'win32_bios' -ComputerName AD1,EVOWIN
 
    .EXAMPLE
    Get-CimData -Class 'win32_bios'
 
    .EXAMPLE
    Get-CimClass to get all classes
 
    .NOTES
    General notes
    #>


    [CmdletBinding()]
    param(
        [parameter(Mandatory)][string] $Class,
        [string] $NameSpace = 'root\cimv2',
        [string[]] $ComputerName = $Env:COMPUTERNAME,
        [ValidateSet('Default', 'Dcom', 'Wsman')][string] $Protocol = 'Default',
        [pscredential] $Credential,
        [string[]] $Properties = '*'
    )
    $ExcludeProperties = 'CimClass', 'CimInstanceProperties', 'CimSystemProperties', 'SystemCreationClassName', 'CreationClassName'

    [Array] $ComputersSplit = Get-ComputerSplit -ComputerName $ComputerName

    $CimObject = @(

        [string[]] $PropertiesOnly = $Properties | Where-Object { $_ -ne 'PSComputerName' }

        $Computers = $ComputersSplit[1]
        if ($Computers.Count -gt 0) {
            if ($Protocol -eq 'Default' -and $null -eq $Credential) {
                Get-CimInstance -ClassName $Class -ComputerName $Computers -ErrorAction SilentlyContinue -Property $PropertiesOnly -Namespace $NameSpace -Verbose:$false -ErrorVariable ErrorsToProcess | Select-Object -Property $Properties -ExcludeProperty $ExcludeProperties
            } else {
                $Option = New-CimSessionOption -Protocol $Protocol
                $newCimSessionSplat = @{
                    ComputerName  = $Computers
                    SessionOption = $Option
                    ErrorAction   = 'SilentlyContinue'
                }
                if ($Credential) {
                    $newCimSessionSplat['Credential'] = $Credential
                }
                $Session = New-CimSession @newCimSessionSplat -Verbose:$false
                if ($Session) {
                    Try {
                        $Info = Get-CimInstance -ClassName $Class -CimSession $Session -ErrorAction Stop -Property $PropertiesOnly -Namespace $NameSpace -Verbose:$false -ErrorVariable ErrorsToProcess | Select-Object -Property $Properties -ExcludeProperty $ExcludeProperties
                    } catch {
                        Write-Warning -Message "Get-CimData - No data for computer $($E.OriginInfo.PSComputerName). Failed with errror: $($E.Exception.Message)"
                    }
                    try {
                        $null = Remove-CimSession -CimSession $Session -ErrorAction SilentlyContinue
                    } catch {
                        Write-Warning -Message "Get-CimData - Failed to remove CimSession $($Session). Failed with errror: $($E.Exception.Message)"
                    }
                    $Info
                } else {
                    Write-Warning -Message "Get-CimData - Failed to create CimSession for $($Computers). Problem with credentials?"
                }
            }
            foreach ($E in $ErrorsToProcess) {
                Write-Warning -Message "Get-CimData - No data for computer $($E.OriginInfo.PSComputerName). Failed with errror: $($E.Exception.Message)"
            }
        } else {

            $Computers = $ComputersSplit[0]
            if ($Computers.Count -gt 0) {
                $Info = Get-CimInstance -ClassName $Class -ErrorAction SilentlyContinue -Property $PropertiesOnly -Namespace $NameSpace -Verbose:$false -ErrorVariable ErrorsLocal | Select-Object -Property $Properties -ExcludeProperty $ExcludeProperties
                $Info | Add-Member -Name 'PSComputerName' -Value $Computers -MemberType NoteProperty -Force
                $Info
            }
            foreach ($E in $ErrorsLocal) {
                Write-Warning -Message "Get-CimData - No data for computer $($Env:COMPUTERNAME). Failed with errror: $($E.Exception.Message)"
            }
        }
    )
    $CimObject
}
function Get-FileName { 
    <#
    .SYNOPSIS
    Generates a temporary file name with the specified extension.
 
    .DESCRIPTION
    This function generates a temporary file name based on the provided extension. It can generate a temporary file name in the system's temporary folder or just the file name itself.
 
    .PARAMETER Extension
    Specifies the extension for the temporary file name. Default is 'tmp'.
 
    .PARAMETER Temporary
    Indicates whether to generate a temporary file name in the system's temporary folder.
 
    .PARAMETER TemporaryFileOnly
    Indicates whether to generate only the temporary file name without the path.
 
    .EXAMPLE
    Get-FileName -Temporary
    Generates a temporary file name in the system's temporary folder. Example output: 3ymsxvav.tmp
 
    .EXAMPLE
    Get-FileName -Temporary
    Generates a temporary file name without the path. Example output: tmpD74C.tmp
 
    .EXAMPLE
    Get-FileName -Temporary -Extension 'xlsx'
    Generates a temporary file name with the specified extension in the system's temporary folder. Example output: tmp45B6.xlsx
 
    .NOTES
    These examples demonstrate how to use the Get-FileName function to generate temporary file names.
    #>

    [CmdletBinding()]
    param(
        [string] $Extension = 'tmp',
        [switch] $Temporary,
        [switch] $TemporaryFileOnly
    )

    if ($Temporary) {

        return [io.path]::Combine([System.IO.Path]::GetTempPath(), "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension")
    }
    if ($TemporaryFileOnly) {

        return "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension"
    }
}
function Get-FileOwner { 
    <#
    .SYNOPSIS
    Retrieves the owner of the specified file or folder.
 
    .DESCRIPTION
    This function retrieves the owner of the specified file or folder. It provides options to resolve the owner's identity and output the results as a hashtable or custom object.
 
    .PARAMETER Path
    Specifies the path to the file or folder.
 
    .PARAMETER Recursive
    Indicates whether to search for files recursively in subdirectories.
 
    .PARAMETER JustPath
    Specifies if only the path information should be returned.
 
    .PARAMETER Resolve
    Indicates whether to resolve the owner's identity.
 
    .PARAMETER AsHashTable
    Specifies if the output should be in hashtable format.
 
    .EXAMPLE
    Get-FileOwner -Path "C:\Example\File.txt"
    Retrieves the owner of the specified file "File.txt".
 
    .EXAMPLE
    Get-FileOwner -Path "C:\Example" -Recursive
    Retrieves the owners of all files in the "Example" directory and its subdirectories.
 
    .EXAMPLE
    Get-FileOwner -Path "C:\Example\File.txt" -Resolve
    Retrieves the owner of the specified file "File.txt" and resolves the owner's identity.
 
    .EXAMPLE
    Get-FileOwner -Path "C:\Example\File.txt" -AsHashTable
    Retrieves the owner of the specified file "File.txt" and outputs the result as a hashtable.
 
    #>

    [cmdletBinding()]
    param(
        [Array] $Path,
        [switch] $Recursive,
        [switch] $JustPath,
        [switch] $Resolve,
        [switch] $AsHashTable
    )
    Begin {
    }
    Process {
        foreach ($P in $Path) {
            if ($P -is [System.IO.FileSystemInfo]) {
                $FullPath = $P.FullName
            } elseif ($P -is [string]) {
                $FullPath = $P
            }
            if ($FullPath -and (Test-Path -Path $FullPath)) {
                if ($JustPath) {
                    $FullPath | ForEach-Object -Process {

                        $ACL = Get-Acl -Path $_
                        $Object = [ordered]@{
                            FullName = $_
                            Owner    = $ACL.Owner
                        }
                        if ($Resolve) {
                            $Identity = Convert-Identity -Identity $ACL.Owner
                            if ($Identity) {
                                $Object['OwnerName'] = $Identity.Name
                                $Object['OwnerSid'] = $Identity.SID
                                $Object['OwnerType'] = $Identity.Type
                            } else {
                                $Object['OwnerName'] = ''
                                $Object['OwnerSid'] = ''
                                $Object['OwnerType'] = ''
                            }
                        }
                        if ($AsHashTable) {
                            $Object
                        } else {
                            [PSCustomObject] $Object
                        }
                    }
                } else {
                    Get-ChildItem -LiteralPath $FullPath -Recurse:$Recursive -Force | ForEach-Object -Process {
                        $File = $_
                        $ACL = Get-Acl -Path $File.FullName
                        $Object = [ordered] @{
                            FullName       = $_.FullName
                            Extension      = $_.Extension
                            CreationTime   = $_.CreationTime
                            LastAccessTime = $_.LastAccessTime
                            LastWriteTime  = $_.LastWriteTime
                            Attributes     = $_.Attributes
                            Owner          = $ACL.Owner
                        }
                        if ($Resolve) {
                            $Identity = Convert-Identity -Identity $ACL.Owner
                            if ($Identity) {
                                $Object['OwnerName'] = $Identity.Name
                                $Object['OwnerSid'] = $Identity.SID
                                $Object['OwnerType'] = $Identity.Type
                            } else {
                                $Object['OwnerName'] = ''
                                $Object['OwnerSid'] = ''
                                $Object['OwnerType'] = ''
                            }
                        }
                        if ($AsHashTable) {
                            $Object
                        } else {
                            [PSCustomObject] $Object
                        }
                    }
                }
            }
        }
    }
    End {
    }
}
function Get-FilePermission { 
    <#
    .SYNOPSIS
    Retrieves and displays file permissions for the specified file or folder.
 
    .DESCRIPTION
    This function retrieves and displays the file permissions for the specified file or folder. It provides options to filter permissions based on inheritance, resolve access control types, and include extended information.
 
    .EXAMPLE
    Get-FilePermission -Path "C:\Example\File.txt"
    Description:
    Retrieves and displays the permissions for the "File.txt" file.
 
    .EXAMPLE
    Get-FilePermission -Path "D:\Folder" -Inherited
    Description:
    Retrieves and displays only the inherited permissions for the "Folder" directory.
 
    .EXAMPLE
    Get-FilePermission -Path "E:\Document.docx" -ResolveTypes -Extended
    Description:
    Retrieves and displays the resolved access control types and extended information for the "Document.docx" file.
 
    .NOTES
    This function supports various options to customize the output and handle different permission scenarios.
    #>

    [alias('Get-PSPermissions', 'Get-FilePermissions')]
    [cmdletBinding()]
    param(
        [Array] $Path,
        [switch] $Inherited,
        [switch] $NotInherited,
        [switch] $ResolveTypes,
        [switch] $Extended,
        [switch] $IncludeACLObject,
        [switch] $AsHashTable,
        [System.Security.AccessControl.FileSystemSecurity] $ACLS
    )
    foreach ($P in $Path) {
        if ($P -is [System.IO.FileSystemInfo]) {
            $FullPath = $P.FullName
        } elseif ($P -is [string]) {
            $FullPath = $P
        }
        $TestPath = Test-Path -Path $FullPath
        if ($TestPath) {
            if (-not $ACLS) {
                try {
                    $ACLS = (Get-Acl -Path $FullPath -ErrorAction Stop)
                } catch {
                    Write-Warning -Message "Get-FilePermission - Can't access $FullPath. Error $($_.Exception.Message)"
                    continue
                }
            }
            $Output = foreach ($ACL in $ACLS.Access) {
                if ($Inherited) {
                    if ($ACL.IsInherited -eq $false) {

                        continue
                    }
                }
                if ($NotInherited) {
                    if ($ACL.IsInherited -eq $true) {
                        continue
                    }
                }
                $TranslateRights = Convert-GenericRightsToFileSystemRights -OriginalRights $ACL.FileSystemRights
                $ReturnObject = [ordered] @{ }
                $ReturnObject['Path' ] = $FullPath
                $ReturnObject['AccessControlType'] = $ACL.AccessControlType
                if ($ResolveTypes) {
                    $Identity = Convert-Identity -Identity $ACL.IdentityReference
                    if ($Identity) {
                        $ReturnObject['Principal'] = $ACL.IdentityReference
                        $ReturnObject['PrincipalName'] = $Identity.Name
                        $ReturnObject['PrincipalSid'] = $Identity.Sid
                        $ReturnObject['PrincipalType'] = $Identity.Type
                    } else {

                        $ReturnObject['Principal'] = $Identity
                        $ReturnObject['PrincipalName'] = ''
                        $ReturnObject['PrincipalSid'] = ''
                        $ReturnObject['PrincipalType'] = ''
                    }
                } else {
                    $ReturnObject['Principal'] = $ACL.IdentityReference.Value
                }
                $ReturnObject['FileSystemRights'] = $TranslateRights
                $ReturnObject['IsInherited'] = $ACL.IsInherited
                if ($Extended) {
                    $ReturnObject['InheritanceFlags'] = $ACL.InheritanceFlags
                    $ReturnObject['PropagationFlags'] = $ACL.PropagationFlags
                }
                if ($IncludeACLObject) {
                    $ReturnObject['ACL'] = $ACL
                    $ReturnObject['AllACL'] = $ACLS
                }
                if ($AsHashTable) {
                    $ReturnObject
                } else {
                    [PSCustomObject] $ReturnObject
                }
            }
            $Output
        } else {
            Write-Warning "Get-PSPermissions - Path $Path doesn't exists. Skipping."
        }
    }
}
function Get-GitHubLatestRelease { 
    <#
    .SYNOPSIS
    Gets one or more releases from GitHub repository
 
    .DESCRIPTION
    Gets one or more releases from GitHub repository
 
    .PARAMETER Url
    Url to github repository
 
    .EXAMPLE
    Get-GitHubLatestRelease -Url "https://api.github.com/repos/evotecit/Testimo/releases" | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdLetBinding()]
    param(
        [parameter(Mandatory)][alias('ReleasesUrl')][uri] $Url
    )
    $ProgressPreference = 'SilentlyContinue'

    $Responds = Test-Connection -ComputerName $URl.Host -Quiet -Count 1
    if ($Responds) {
        Try {
            [Array] $JsonOutput = (Invoke-WebRequest -Uri $Url -ErrorAction Stop | ConvertFrom-Json)
            foreach ($JsonContent in $JsonOutput) {
                [PSCustomObject] @{
                    PublishDate = [DateTime]  $JsonContent.published_at
                    CreatedDate = [DateTime] $JsonContent.created_at
                    PreRelease  = [bool] $JsonContent.prerelease
                    Version     = [version] ($JsonContent.name -replace 'v', '')
                    Tag         = $JsonContent.tag_name
                    Branch      = $JsonContent.target_commitish
                    Errors      = ''
                }
            }
        } catch {
            [PSCustomObject] @{
                PublishDate = $null
                CreatedDate = $null
                PreRelease  = $null
                Version     = $null
                Tag         = $null
                Branch      = $null
                Errors      = $_.Exception.Message
            }
        }
    } else {
        [PSCustomObject] @{
            PublishDate = $null
            CreatedDate = $null
            PreRelease  = $null
            Version     = $null
            Tag         = $null
            Branch      = $null
            Errors      = "No connection (ping) to $($Url.Host)"
        }
    }
    $ProgressPreference = 'Continue'
}
function Get-IPAddressRangeInformation { 
    <#
    .SYNOPSIS
    Provides information about IP Address range
 
    .DESCRIPTION
    Provides information about IP Address range
 
    .PARAMETER Network
    Network in form of IP/NetworkLength (e.g. 10.2.10.0/24')
 
    .PARAMETER IPAddress
    IP Address to use
 
    .PARAMETER NetworkLength
    Network length to use
 
    .PARAMETER CIDRObject
    CIDRObject to use
 
    .EXAMPLE
    $CidrObject = @{
        Ip = '10.2.10.0'
        NetworkLength = 24
    }
    Get-IPAddressRangeInformation -CIDRObject $CidrObject | Format-Table
 
    .EXAMPLE
    Get-IPAddressRangeInformation -Network '10.2.10.0/24' | Format-Table
 
    .EXAMPLE
    Get-IPAddressRangeInformation -IPAddress '10.2.10.0' -NetworkLength 24 | Format-Table
 
    .NOTES
    General notes
    #>

    [cmdletBinding(DefaultParameterSetName = 'Network')]
    param(
        [Parameter(ParameterSetName = 'Network', Mandatory)][string] $Network,
        [Parameter(ParameterSetName = 'IPAddress', Mandatory)][string] $IPAddress,
        [Parameter(ParameterSetName = 'IPAddress', Mandatory)][int] $NetworkLength,
        [Parameter(ParameterSetName = 'CIDR', Mandatory)][psobject] $CIDRObject
    )
    $IPv4Regex = '(?:(?:0?0?\d|0?[1-9]\d|1\d\d|2[0-5][0-5]|2[0-4]\d)\.){3}(?:0?0?\d|0?[1-9]\d|1\d\d|2[0-5][0-5]|2[0-4]\d)'

    if ($Network) {
        $CIDRObject = @{
            Ip            = $Network.Split('/')[0]
            NetworkLength = $Network.Split('/')[1]
        }
    } elseif ($IPAddress -and $NetworkLength) {
        $CIDRObject = @{
            Ip            = $IPAddress
            NetworkLength = $NetworkLength
        }
    } elseif ($CIDRObject) {
    } else {
        Write-Error "Get-IPAddressRangeInformation - Invalid parameters specified"
        return
    }

    $o = [ordered] @{}
    $o.IP = [string] $CIDRObject.IP
    $o.BinaryIP = Convert-IPToBinary $o.IP
    if (-not $o.BinaryIP) {
        return
    }
    $o.NetworkLength = [int32] $CIDRObject.NetworkLength
    $o.SubnetMask = Convert-BinaryToIP ('1' * $o.NetworkLength).PadRight(32, '0')
    $o.BinarySubnetMask = ('1' * $o.NetworkLength).PadRight(32, '0')
    $o.BinaryNetworkAddress = $o.BinaryIP.SubString(0, $o.NetworkLength).PadRight(32, '0')
    if ($Contains) {
        if ($Contains -match "\A${IPv4Regex}\z") {

            return Test-IPIsInNetwork $Contains $o.BinaryNetworkAddress $o.BinaryNetworkAddress.SubString(0, $o.NetworkLength).PadRight(32, '1')
        } else {
            Write-Error "Get-IPAddressRangeInformation - Invalid IPv4 address specified with -Contains"
            return
        }
    }
    $o.NetworkAddress = Convert-BinaryToIP $o.BinaryNetworkAddress
    if ($o.NetworkLength -eq 32 -or $o.NetworkLength -eq 31) {
        $o.HostMin = $o.IP
    } else {
        $o.HostMin = Convert-BinaryToIP ([System.Convert]::ToString(([System.Convert]::ToInt64($o.BinaryNetworkAddress, 2) + 1), 2)).PadLeft(32, '0')
    }
    [string] $BinaryBroadcastIP = $o.BinaryNetworkAddress.SubString(0, $o.NetworkLength).PadRight(32, '1') 
    $o.BinaryBroadcast = $BinaryBroadcastIP
    [int64] $DecimalHostMax = [System.Convert]::ToInt64($BinaryBroadcastIP, 2) - 1
    [string] $BinaryHostMax = [System.Convert]::ToString($DecimalHostMax, 2).PadLeft(32, '0')
    $o.HostMax = Convert-BinaryToIP $BinaryHostMax
    $o.TotalHosts = [int64][System.Convert]::ToString(([System.Convert]::ToInt64($BinaryBroadcastIP, 2) - [System.Convert]::ToInt64($o.BinaryNetworkAddress, 2) + 1))
    $o.UsableHosts = $o.TotalHosts - 2

    if ($o.NetworkLength -eq 32) {
        $o.Broadcast = $Null
        $o.UsableHosts = [int64] 1
        $o.TotalHosts = [int64] 1
        $o.HostMax = $o.IP
    } elseif ($o.NetworkLength -eq 31) {
        $o.Broadcast = $Null
        $o.UsableHosts = [int64] 2
        $o.TotalHosts = [int64] 2

        [int64] $DecimalHostMax2 = [System.Convert]::ToInt64($BinaryBroadcastIP, 2) 
        [string] $BinaryHostMax2 = [System.Convert]::ToString($DecimalHostMax2, 2).PadLeft(32, '0')
        $o.HostMax = Convert-BinaryToIP $BinaryHostMax2
    } elseif ($o.NetworkLength -eq 30) {
        $o.UsableHosts = [int64] 2
        $o.TotalHosts = [int64] 4
        $o.Broadcast = Convert-BinaryToIP $BinaryBroadcastIP
    } else {
        $o.Broadcast = Convert-BinaryToIP $BinaryBroadcastIP
    }
    if ($Enumerate) {
        $IPRange = @(Get-IPRange $o.BinaryNetworkAddress $o.BinaryNetworkAddress.SubString(0, $o.NetworkLength).PadRight(32, '1'))
        if ((31, 32) -notcontains $o.NetworkLength ) {
            $IPRange = $IPRange[1..($IPRange.Count - 1)] 
            $IPRange = $IPRange[0..($IPRange.Count - 2)] 
        }
        $o.IPEnumerated = $IPRange
    } else {
        $o.IPEnumerated = @()
    }
    [PSCustomObject]$o
}
function Get-ProtocolDefaults { 
    <#
    .SYNOPSIS
    Gets a list of default settings for SSL/TLS protocols
 
    .DESCRIPTION
    Gets a list of default settings for SSL/TLS protocols
 
    .PARAMETER WindowsVersion
    Windows Version to search for
 
    .PARAMETER AsList
    If true, returns a list of protocol names for all Windows Versions, otherwise returns a single entry for the specified Windows Version
 
    .EXAMPLE
    Get-ProtocolDefaults -AsList | Format-Table
 
    .EXAMPLE
    Get-ProtocolDefaults -WindowsVersion 'Windows 10 1809' | Format-Table
 
    .NOTES
    Based on: https://docs.microsoft.com/en-us/windows/win32/secauthn/protocols-in-tls-ssl--schannel-ssp-
 
    According to this https://github.com/MicrosoftDocs/windowsserverdocs/issues/2783 SCHANNEL service requires direct enablement so the list is kind of half useful
    #>

    [cmdletbinding(DefaultParameterSetName = 'WindowsVersion')]
    param(
        [Parameter(Mandatory, ParameterSetName = 'WindowsVersion')][string] $WindowsVersion,
        [Parameter(Mandatory, ParameterSetName = 'AsList')][switch] $AsList
    )

    $Defaults = [ordered] @{
        'Windows Server 2022'        = [ordered] @{
            'Version'     = 'Windows Server 2022'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Enabled'
            'TLS13Server' = 'Enabled'
        }
        'Windows Server 2019 20H2'   = [ordered] @{
            'Version'     = 'Windows Server 2019 20H2'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        'Windows Server 2019 2004'   = [ordered] @{
            'Version'     = 'Windows Server 2019 2004'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        'Windows Server 2019 1909'   = [ordered] @{
            'Version'     = 'Windows Server 2019 1909'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows Server 2019 1903"   = [ordered] @{
            'Version'     = 'Windows Server 2019 1903'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows Server 2019 1809"   = [ordered] @{
            'Version'     = 'Windows Server 2019 1809'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows Server 2016 1803"   = [ordered] @{
            'Version'     = 'Windows Server 2016 1803'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows Server 2016 1607"   = [ordered] @{
            'Version'     = 'Windows Server 2019 1607'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        'Windows Server 2012 R2'     = [ordered] @{
            'Version'     = 'Windows Server 2012 R2'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Disabled'
            'SSL2Server'  = 'Disabled'
            'SSL3Client'  = 'Enabled'
            'SSL3Server'  = 'Enabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        'Windows Server 2012'        = [ordered] @{
            'Version'     = 'Windows Server 2012'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Disabled'
            'SSL2Server'  = 'Disabled'
            'SSL3Client'  = 'Enabled'
            'SSL3Server'  = 'Enabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        'Windows Server 2008 R2'     = [ordered] @{
            'Version'     = 'Windows Server 2008 R2'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Disabled'
            'SSL2Server'  = 'Enabled'
            'SSL3Client'  = 'Enabled'
            'SSL3Server'  = 'Enabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Disabled'
            'TLS11Server' = 'Disabled'
            'TLS12Client' = 'Disabled'
            'TLS12Server' = 'Disabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        'Windows Server 2008'        = [ordered] @{
            'Version'     = 'Windows Server 2008'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Disabled'
            'SSL2Server'  = 'Enabled'
            'SSL3Client'  = 'Enabled'
            'SSL3Server'  = 'Enabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Disabled'
            'TLS11Server' = 'Disabled'
            'TLS12Client' = 'Disabled'
            'TLS12Server' = 'Disabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }

        'Windows 11 21H2'            = [ordered] @{
            'Version'     = 'Windows 11 21H2'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Enabled'
            'TLS13Server' = 'Enabled'
        }
        'Windows 10 21H1'            = [ordered] @{
            'Version'     = 'Windows 10 21H1'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        'Windows 10 20H2'            = [ordered] @{
            'Version'     = 'Windows 10 20H2'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        'Windows 10 2004'            = [ordered] @{
            'Version'     = 'Windows 10 2004'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        'Windows 10 Insider Preview' = [ordered] @{
            'Version'     = 'Windows 10 Insider Preview'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows 10 1909"            = [ordered] @{
            'Version'     = 'Windows 10 1909'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows 10 1903"            = [ordered] @{
            'Version'     = 'Windows 10 1903'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows 10 1809"            = [ordered] @{
            'Version'     = 'Windows 10 1809'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows 10 1803"            = [ordered] @{
            'Version'     = 'Windows 10 1803'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows 10 1709"            = [ordered] @{
            'Version'     = 'Windows 10 1709'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows 10 1703"            = [ordered] @{
            'Version'     = 'Windows 10 1703'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows 10 1607"            = [ordered] @{
            'Version'     = 'Windows 10 1607'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Not supported'
            'SSL2Server'  = 'Not supported'
            'SSL3Client'  = 'Disabled'
            'SSL3Server'  = 'Disabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows 10 1511"            = [ordered] @{
            'Version'     = 'Windows 10 1511'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Disabled'
            'SSL2Server'  = 'Disabled'
            'SSL3Client'  = 'Enabled'
            'SSL3Server'  = 'Enabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
        "Windows 10 1507"            = [ordered] @{
            'Version'     = 'Windows 10 1507'
            'PCT10'       = 'Not supported'
            'SSL2Client'  = 'Disabled'
            'SSL2Server'  = 'Disabled'
            'SSL3Client'  = 'Enabled'
            'SSL3Server'  = 'Enabled'
            'TLS10Client' = 'Enabled'
            'TLS10Server' = 'Enabled'
            'TLS11Client' = 'Enabled'
            'TLS11Server' = 'Enabled'
            'TLS12Client' = 'Enabled'
            'TLS12Server' = 'Enabled'
            'TLS13Client' = 'Not supported'
            'TLS13Server' = 'Not supported'
        }
    }
    if ($AsList) {
        foreach ($Key in $Defaults.Keys) {
            [PSCustomObject] $Defaults[$Key]
        }
    } else {
        if ($Defaults[$WindowsVersion]) {
            $Defaults[$WindowsVersion]
        } else {
            [ordered] @{
                'Version'     = 'Unknown'
                'PCT10'       = 'Unknown'
                'SSL2Client'  = 'Unknown'
                'SSL2Server'  = 'Unknown'
                'SSL3Client'  = 'Unknown'
                'SSL3Server'  = 'Unknown'
                'TLS10Client' = 'Unknown'
                'TLS10Server' = 'Unknown'
                'TLS11Client' = 'Unknown'
                'TLS11Server' = 'Unknown'
                'TLS12Client' = 'Unknown'
                'TLS12Server' = 'Unknown'
                'TLS13Client' = 'Unknown'
                'TLS13Server' = 'Unknown'
            }
        }
    }
}
function Get-PSRegistry { 
    <#
    .SYNOPSIS
    Get registry key values.
 
    .DESCRIPTION
    Get registry key values.
 
    .PARAMETER RegistryPath
    The registry path to get the values from.
 
    .PARAMETER ComputerName
    The computer to get the values from. If not specified, the local computer is used.
 
    .PARAMETER ExpandEnvironmentNames
    Expand environment names in the registry value.
    By default it doesn't do that. If you want to expand environment names, use this parameter.
 
    .EXAMPLE
    Get-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' -ComputerName AD1
 
    .EXAMPLE
    Get-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters'
 
    .EXAMPLE
    Get-PSRegistry -RegistryPath "HKLM\SYSTEM\CurrentControlSet\Services\DFSR\Parameters" -ComputerName AD1,AD2,AD3 | ft -AutoSize
 
    .EXAMPLE
    Get-PSRegistry -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\Directory Service'
 
    .EXAMPLE
    Get-PSRegistry -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\Windows PowerShell' | Format-Table -AutoSize
 
    .EXAMPLE
    Get-PSRegistry -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\EventLog\Directory Service' -ComputerName AD1 -Advanced
 
    .EXAMPLE
    Get-PSRegistry -RegistryPath "HKLM:\Software\Microsoft\Powershell\1\Shellids\Microsoft.Powershell\"
 
    .EXAMPLE
    # Get default key and it's value
    Get-PSRegistry -RegistryPath "HKEY_CURRENT_USER\Tests" -Key ""
 
    .EXAMPLE
    # Get default key and it's value (alternative)
    Get-PSRegistry -RegistryPath "HKEY_CURRENT_USER\Tests" -DefaultKey
 
    .NOTES
    General notes
    #>

    [cmdletbinding()]
    param(
        [alias('Path')][string[]] $RegistryPath,
        [string[]] $ComputerName = $Env:COMPUTERNAME,
        [string] $Key,
        [switch] $Advanced,
        [switch] $DefaultKey,
        [switch] $ExpandEnvironmentNames,
        [Parameter(DontShow)][switch] $DoNotUnmount
    )
    $Script:CurrentGetCount++
    Get-PSRegistryDictionaries

    $RegistryPath = Resolve-PrivateRegistry -RegistryPath $RegistryPath

    [Array] $Computers = Get-ComputerSplit -ComputerName $ComputerName

    [Array] $RegistryTranslated = Get-PSConvertSpecialRegistry -RegistryPath $RegistryPath -Computers $ComputerName -HiveDictionary $Script:HiveDictionary -ExpandEnvironmentNames:$ExpandEnvironmentNames.IsPresent

    if ($PSBoundParameters.ContainsKey("Key") -or $DefaultKey) {
        [Array] $RegistryValues = Get-PSSubRegistryTranslated -RegistryPath $RegistryTranslated -HiveDictionary $Script:HiveDictionary -Key $Key
        foreach ($Computer in $Computers[0]) {
            foreach ($R in $RegistryValues) {
                Get-PSSubRegistry -Registry $R -ComputerName $Computer -ExpandEnvironmentNames:$ExpandEnvironmentNames.IsPresent
            }
        }
        foreach ($Computer in $Computers[1]) {
            foreach ($R in $RegistryValues) {
                Get-PSSubRegistry -Registry $R -ComputerName $Computer -Remote -ExpandEnvironmentNames:$ExpandEnvironmentNames.IsPresent
            }
        }
    } else {
        [Array] $RegistryValues = Get-PSSubRegistryTranslated -RegistryPath $RegistryTranslated -HiveDictionary $Script:HiveDictionary
        foreach ($Computer in $Computers[0]) {
            foreach ($R in $RegistryValues) {

                Get-PSSubRegistryComplete -Registry $R -ComputerName $Computer -Advanced:$Advanced -ExpandEnvironmentNames:$ExpandEnvironmentNames.IsPresent
            }
        }
        foreach ($Computer in $Computers[1]) {
            foreach ($R in $RegistryValues) {
                Get-PSSubRegistryComplete -Registry $R -ComputerName $Computer -Remote -Advanced:$Advanced -ExpandEnvironmentNames:$ExpandEnvironmentNames.IsPresent
            }
        }
    }
    $Script:CurrentGetCount--
    if ($Script:CurrentGetCount -eq 0) {
        if (-not $DoNotUnmount) {
            Unregister-MountedRegistry
        }
    }
}
function Get-RandomStringName { 
    <#
    .SYNOPSIS
    Generates a random string of specified length with various options.
 
    .DESCRIPTION
    This function generates a random string of specified length with options to convert the case and include only letters.
 
    .PARAMETER Size
    The length of the random string to generate. Default is 31.
 
    .PARAMETER ToLower
    Convert the generated string to lowercase.
 
    .PARAMETER ToUpper
    Convert the generated string to uppercase.
 
    .PARAMETER LettersOnly
    Generate a random string with only letters.
 
    .EXAMPLE
    Get-RandomStringName -Size 10
    Generates a random string of length 10.
 
    .EXAMPLE
    Get-RandomStringName -Size 8 -ToLower
    Generates a random string of length 8 and converts it to lowercase.
 
    .EXAMPLE
    Get-RandomStringName -Size 12 -ToUpper
    Generates a random string of length 12 and converts it to uppercase.
 
    .EXAMPLE
    Get-RandomStringName -Size 15 -LettersOnly
    Generates a random string of length 15 with only letters.
 
    #>

    [cmdletbinding()]
    param(
        [int] $Size = 31,
        [switch] $ToLower,
        [switch] $ToUpper,
        [switch] $LettersOnly
    )
    [string] $MyValue = @(
        if ($LettersOnly) {
            ( -join ((1..$Size) | ForEach-Object { (65..90) + (97..122) | Get-Random } | ForEach-Object { [char]$_ }))
        } else {
            ( -join ((48..57) + (97..122) | Get-Random -Count $Size | ForEach-Object { [char]$_ }))
        }
    )
    if ($ToLower) {
        return $MyValue.ToLower()
    }
    if ($ToUpper) {
        return $MyValue.ToUpper()
    }
    return $MyValue
}
function Get-WinADForestControllers { 
    <#
    .SYNOPSIS
    Retrieves information about domain controllers in the specified domain(s).
 
    .DESCRIPTION
    This function retrieves detailed information about domain controllers in the specified domain(s), including hostname, IP addresses, roles, and other relevant details.
 
    .PARAMETER TestAvailability
    Specifies whether to test the availability of domain controllers.
 
    .EXAMPLE
    Get-WinADForestControllers -TestAvailability
    Tests the availability of domain controllers in the forest.
 
    .EXAMPLE
    Get-WinADDomainControllers
    Retrieves information about all domain controllers in the forest.
 
    .EXAMPLE
    Get-WinADDomainControllers -Credential $Credential
    Retrieves information about all domain controllers in the forest using specified credentials.
 
    .EXAMPLE
    Get-WinADDomainControllers | Format-Table *
    Displays detailed information about all domain controllers in a tabular format.
 
    Output:
    Domain HostName Forest IPV4Address IsGlobalCatalog IsReadOnly SchemaMaster DomainNamingMasterMaster PDCEmulator RIDMaster InfrastructureMaster Comment
    ------ -------- ------ ----------- --------------- ---------- ------------ ------------------------ ----------- --------- -------------------- -------
    ad.evotec.xyz AD1.ad.evotec.xyz ad.evotec.xyz 192.168.240.189 True False True True True True True
    ad.evotec.xyz AD2.ad.evotec.xyz ad.evotec.xyz 192.168.240.192 True False False False False False False
    ad.evotec.pl ad.evotec.xyz False False False False False Unable to contact the server. This may be becau...
 
    .NOTES
    This function provides essential information about domain controllers in the forest.
    #>

    [alias('Get-WinADDomainControllers')]
    [CmdletBinding()]
    param(
        [string[]] $Domain,
        [switch] $TestAvailability,
        [switch] $SkipEmpty,
        [pscredential] $Credential
    )
    try {
        if ($Credential) {
            $Forest = Get-ADForest -Credential $Credential
        } else {
            $Forest = Get-ADForest
        }
        if (-not $Domain) {
            $Domain = $Forest.Domains
        }
    } catch {
        $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " "
        Write-Warning "Get-WinADForestControllers - Couldn't use Get-ADForest feature. Error: $ErrorMessage"
        return
    }
    $Servers = foreach ($D in $Domain) {
        try {
            $LocalServer = Get-ADDomainController -Discover -DomainName $D -ErrorAction Stop -Writable
            if ($Credential) {
                $DC = Get-ADDomainController -Server $LocalServer.HostName[0] -Credential $Credential -Filter * -ErrorAction Stop
            } else {
                $DC = Get-ADDomainController -Server $LocalServer.HostName[0] -Filter * -ErrorAction Stop 
            }
            foreach ($S in $DC) {
                $Server = [ordered] @{
                    Domain               = $D
                    HostName             = $S.HostName
                    Name                 = $S.Name
                    Forest               = $Forest.RootDomain
                    IPV4Address          = $S.IPV4Address
                    IPV6Address          = $S.IPV6Address
                    IsGlobalCatalog      = $S.IsGlobalCatalog
                    IsReadOnly           = $S.IsReadOnly
                    Site                 = $S.Site
                    SchemaMaster         = ($S.OperationMasterRoles -contains 'SchemaMaster')
                    DomainNamingMaster   = ($S.OperationMasterRoles -contains 'DomainNamingMaster')
                    PDCEmulator          = ($S.OperationMasterRoles -contains 'PDCEmulator')
                    RIDMaster            = ($S.OperationMasterRoles -contains 'RIDMaster')
                    InfrastructureMaster = ($S.OperationMasterRoles -contains 'InfrastructureMaster')
                    LdapPort             = $S.LdapPort
                    SslPort              = $S.SslPort
                    Pingable             = $null
                    Comment              = ''
                }
                if ($TestAvailability) {
                    $Server['Pingable'] = foreach ($_ in $Server.IPV4Address) {
                        Test-Connection -Count 1 -Server $_ -Quiet -ErrorAction SilentlyContinue
                    }
                }
                [PSCustomObject] $Server
            }
        } catch {
            [PSCustomObject]@{
                Domain                   = $D
                HostName                 = ''
                Name                     = ''
                Forest                   = $Forest.RootDomain
                IPV4Address              = ''
                IPV6Address              = ''
                IsGlobalCatalog          = ''
                IsReadOnly               = ''
                Site                     = ''
                SchemaMaster             = $false
                DomainNamingMasterMaster = $false
                PDCEmulator              = $false
                RIDMaster                = $false
                InfrastructureMaster     = $false
                LdapPort                 = ''
                SslPort                  = ''
                Pingable                 = $null
                Comment                  = $_.Exception.Message -replace "`n", " " -replace "`r", " "
            }
        }
    }
    if ($SkipEmpty) {
        return $Servers | Where-Object { $_.HostName -ne '' }
    }
    return $Servers
}
function Get-WinADForestDetails { 
    <#
    .SYNOPSIS
    Get details about Active Directory Forest, Domains and Domain Controllers in a single query
 
    .DESCRIPTION
    Get details about Active Directory Forest, Domains and Domain Controllers in a single query
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExcludeDomainControllers
    Exclude specific domain controllers, by default there are no exclusions, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored.
 
    .PARAMETER IncludeDomainControllers
    Include only specific domain controllers, by default all domain controllers are included, as long as VerifyDomainControllers switch is enabled. Otherwise this parameter is ignored.
 
    .PARAMETER SkipRODC
    Skip Read-Only Domain Controllers. By default all domain controllers are included.
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .PARAMETER Filter
    Filter for Get-ADDomainController
 
    .PARAMETER TestAvailability
    Check if Domain Controllers are available
 
    .PARAMETER Test
    Pick what to check for availability. Options are: All, Ping, WinRM, PortOpen, Ping+WinRM, Ping+PortOpen, WinRM+PortOpen. Default is All
 
    .PARAMETER Ports
    Ports to check for availability. Default is 135
 
    .PARAMETER PortsTimeout
    Ports timeout for availability check. Default is 100
 
    .PARAMETER PingCount
    How many pings to send. Default is 1
 
    .PARAMETER PreferWritable
    Prefer writable domain controllers over read-only ones when returning Query Servers
 
    .PARAMETER Extended
    Return extended information about domains with NETBIOS names
 
    .EXAMPLE
    Get-WinADForestDetails | Format-Table
 
    .EXAMPLE
    Get-WinADForestDetails -Forest 'ad.evotec.xyz' | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [string] $Filter = '*',
        [switch] $TestAvailability,
        [ValidateSet('All', 'Ping', 'WinRM', 'PortOpen', 'Ping+WinRM', 'Ping+PortOpen', 'WinRM+PortOpen')] $Test = 'All',
        [int[]] $Ports = 135,
        [int] $PortsTimeout = 100,
        [int] $PingCount = 1,
        [switch] $PreferWritable,
        [switch] $Extended,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    if ($Global:ProgressPreference -ne 'SilentlyContinue') {
        $TemporaryProgress = $Global:ProgressPreference
        $Global:ProgressPreference = 'SilentlyContinue'
    }

    if (-not $ExtendedForestInformation) {

        $Findings = [ordered] @{ }
        try {
            if ($Forest) {
                $ForestInformation = Get-ADForest -ErrorAction Stop -Identity $Forest
            } else {
                $ForestInformation = Get-ADForest -ErrorAction Stop
            }
        } catch {
            Write-Warning "Get-WinADForestDetails - Error discovering DC for Forest - $($_.Exception.Message)"
            return
        }
        if (-not $ForestInformation) {
            return
        }
        $Findings['Forest'] = $ForestInformation
        $Findings['ForestDomainControllers'] = @()
        $Findings['QueryServers'] = @{ }
        $Findings['DomainDomainControllers'] = @{ }
        [Array] $Findings['Domains'] = foreach ($Domain in $ForestInformation.Domains) {
            if ($IncludeDomains) {
                if ($Domain -in $IncludeDomains) {
                    $Domain.ToLower()
                }

                continue
            }
            if ($Domain -notin $ExcludeDomains) {
                $Domain.ToLower()
            }
        }

        [Array] $DomainsActive = foreach ($Domain in $Findings['Forest'].Domains) {
            try {
                $DC = Get-ADDomainController -DomainName $Domain -Discover -ErrorAction Stop -Writable:$PreferWritable.IsPresent

                $OrderedDC = [ordered] @{
                    Domain      = $DC.Domain
                    Forest      = $DC.Forest
                    HostName    = [Array] $DC.HostName
                    IPv4Address = $DC.IPv4Address
                    IPv6Address = $DC.IPv6Address
                    Name        = $DC.Name
                    Site        = $DC.Site
                }
            } catch {
                Write-Warning "Get-WinADForestDetails - Error discovering DC for domain $Domain - $($_.Exception.Message)"
                continue
            }
            if ($Domain -eq $Findings['Forest']['Name']) {
                $Findings['QueryServers']['Forest'] = $OrderedDC
            }
            $Findings['QueryServers']["$Domain"] = $OrderedDC

            $Domain
        }

        [Array] $Findings['Domains'] = foreach ($Domain in $Findings['Domains']) {
            if ($Domain -notin $DomainsActive) {
                Write-Warning "Get-WinADForestDetails - Domain $Domain doesn't seem to be active (no DCs). Skipping."
                continue
            }
            $Domain
        }

        [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) {
            $QueryServer = $Findings['QueryServers'][$Domain]['HostName'][0]

            [Array] $AllDC = try {
                try {
                    $DomainControllers = Get-ADDomainController -Filter $Filter -Server $QueryServer -ErrorAction Stop
                } catch {
                    Write-Warning "Get-WinADForestDetails - Error listing DCs for domain $Domain - $($_.Exception.Message)"
                    continue
                }
                foreach ($S in $DomainControllers) {
                    if ($IncludeDomainControllers.Count -gt 0) {
                        If (-not $IncludeDomainControllers[0].Contains('.')) {
                            if ($S.Name -notin $IncludeDomainControllers) {
                                continue
                            }
                        } else {
                            if ($S.HostName -notin $IncludeDomainControllers) {
                                continue
                            }
                        }
                    }
                    if ($ExcludeDomainControllers.Count -gt 0) {
                        If (-not $ExcludeDomainControllers[0].Contains('.')) {
                            if ($S.Name -in $ExcludeDomainControllers) {
                                continue
                            }
                        } else {
                            if ($S.HostName -in $ExcludeDomainControllers) {
                                continue
                            }
                        }
                    }

                    $DSAGuid = (Get-ADObject -Identity $S.NTDSSettingsObjectDN -Server $QueryServer).ObjectGUID
                    $Server = [ordered] @{
                        Domain                 = $Domain
                        HostName               = $S.HostName
                        Name                   = $S.Name
                        Forest                 = $ForestInformation.RootDomain
                        Site                   = $S.Site
                        IPV4Address            = $S.IPV4Address
                        IPV6Address            = $S.IPV6Address
                        IsGlobalCatalog        = $S.IsGlobalCatalog
                        IsReadOnly             = $S.IsReadOnly
                        IsSchemaMaster         = ($S.OperationMasterRoles -contains 'SchemaMaster')
                        IsDomainNamingMaster   = ($S.OperationMasterRoles -contains 'DomainNamingMaster')
                        IsPDC                  = ($S.OperationMasterRoles -contains 'PDCEmulator')
                        IsRIDMaster            = ($S.OperationMasterRoles -contains 'RIDMaster')
                        IsInfrastructureMaster = ($S.OperationMasterRoles -contains 'InfrastructureMaster')
                        OperatingSystem        = $S.OperatingSystem
                        OperatingSystemVersion = $S.OperatingSystemVersion
                        OperatingSystemLong    = ConvertTo-OperatingSystem -OperatingSystem $S.OperatingSystem -OperatingSystemVersion $S.OperatingSystemVersion
                        LdapPort               = $S.LdapPort
                        SslPort                = $S.SslPort
                        DistinguishedName      = $S.ComputerObjectDN
                        NTDSSettingsObjectDN   = $S.NTDSSettingsObjectDN
                        DsaGuid                = $DSAGuid
                        DsaGuidName            = "$DSAGuid._msdcs.$($ForestInformation.RootDomain)"
                        Pingable               = $null
                        WinRM                  = $null
                        PortOpen               = $null
                        Comment                = ''
                    }
                    if ($TestAvailability) {
                        if ($Test -eq 'All' -or $Test -like 'Ping*') {
                            $Server.Pingable = Test-Connection -ComputerName $Server.IPV4Address -Quiet -Count $PingCount
                        }
                        if ($Test -eq 'All' -or $Test -like '*WinRM*') {
                            $Server.WinRM = (Test-WinRM -ComputerName $Server.HostName).Status
                        }
                        if ($Test -eq 'All' -or '*PortOpen*') {
                            $Server.PortOpen = (Test-ComputerPort -Server $Server.HostName -PortTCP $Ports -Timeout $PortsTimeout).Status
                        }
                    }
                    [PSCustomObject] $Server
                }
            } catch {
                [PSCustomObject]@{
                    Domain                   = $Domain
                    HostName                 = ''
                    Name                     = ''
                    Forest                   = $ForestInformation.RootDomain
                    IPV4Address              = ''
                    IPV6Address              = ''
                    IsGlobalCatalog          = ''
                    IsReadOnly               = ''
                    Site                     = ''
                    SchemaMaster             = $false
                    DomainNamingMasterMaster = $false
                    PDCEmulator              = $false
                    RIDMaster                = $false
                    InfrastructureMaster     = $false
                    LdapPort                 = ''
                    SslPort                  = ''
                    DistinguishedName        = ''
                    NTDSSettingsObjectDN     = ''
                    DsaGuid                  = ''
                    DsaGuidName              = ''
                    Pingable                 = $null
                    WinRM                    = $null
                    PortOpen                 = $null
                    Comment                  = $_.Exception.Message -replace "`n", " " -replace "`r", " "
                }
            }
            if ($SkipRODC) {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false }
            } else {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC
            }

            if ($null -ne $Findings['DomainDomainControllers'][$Domain]) {
                [Array] $Findings['DomainDomainControllers'][$Domain]
            }
        }
        if ($Extended) {
            $Findings['DomainsExtended'] = @{ }
            $Findings['DomainsExtendedNetBIOS'] = @{ }
            foreach ($DomainEx in $Findings['Domains']) {
                try {

                    $Findings['DomainsExtended'][$DomainEx] = Get-ADDomain -Server $Findings['QueryServers'][$DomainEx].HostName[0] | ForEach-Object {

                        [ordered] @{
                            AllowedDNSSuffixes                 = $_.AllowedDNSSuffixes | ForEach-Object -Process { $_ }                
                            ChildDomains                       = $_.ChildDomains | ForEach-Object -Process { $_ }                      
                            ComputersContainer                 = $_.ComputersContainer                 
                            DeletedObjectsContainer            = $_.DeletedObjectsContainer            
                            DistinguishedName                  = $_.DistinguishedName                  
                            DNSRoot                            = $_.DNSRoot                            
                            DomainControllersContainer         = $_.DomainControllersContainer         
                            DomainMode                         = $_.DomainMode                         
                            DomainSID                          = $_.DomainSID.Value                        
                            ForeignSecurityPrincipalsContainer = $_.ForeignSecurityPrincipalsContainer 
                            Forest                             = $_.Forest                             
                            InfrastructureMaster               = $_.InfrastructureMaster               
                            LastLogonReplicationInterval       = $_.LastLogonReplicationInterval       
                            LinkedGroupPolicyObjects           = $_.LinkedGroupPolicyObjects | ForEach-Object -Process { $_ }           
                            LostAndFoundContainer              = $_.LostAndFoundContainer              
                            ManagedBy                          = $_.ManagedBy                          
                            Name                               = $_.Name                               
                            NetBIOSName                        = $_.NetBIOSName                        
                            ObjectClass                        = $_.ObjectClass                        
                            ObjectGUID                         = $_.ObjectGUID                         
                            ParentDomain                       = $_.ParentDomain                       
                            PDCEmulator                        = $_.PDCEmulator                        
                            PublicKeyRequiredPasswordRolling   = $_.PublicKeyRequiredPasswordRolling | ForEach-Object -Process { $_ }   
                            QuotasContainer                    = $_.QuotasContainer                    
                            ReadOnlyReplicaDirectoryServers    = $_.ReadOnlyReplicaDirectoryServers | ForEach-Object -Process { $_ }    
                            ReplicaDirectoryServers            = $_.ReplicaDirectoryServers | ForEach-Object -Process { $_ }           
                            RIDMaster                          = $_.RIDMaster                          
                            SubordinateReferences              = $_.SubordinateReferences | ForEach-Object -Process { $_ }            
                            SystemsContainer                   = $_.SystemsContainer                   
                            UsersContainer                     = $_.UsersContainer                     
                        }
                    }

                    $NetBios = $Findings['DomainsExtended'][$DomainEx]['NetBIOSName']
                    $Findings['DomainsExtendedNetBIOS'][$NetBios] = $Findings['DomainsExtended'][$DomainEx]
                } catch {
                    Write-Warning "Get-WinADForestDetails - Error gathering Domain Information for domain $DomainEx - $($_.Exception.Message)"
                    continue
                }
            }
        }

        if ($TemporaryProgress) {
            $Global:ProgressPreference = $TemporaryProgress
        }

        $Findings
    } else {

        $Findings = Copy-DictionaryManual -Dictionary $ExtendedForestInformation
        [Array] $Findings['Domains'] = foreach ($_ in $Findings.Domains) {
            if ($IncludeDomains) {
                if ($_ -in $IncludeDomains) {
                    $_.ToLower()
                }

                continue
            }
            if ($_ -notin $ExcludeDomains) {
                $_.ToLower()
            }
        }

        foreach ($_ in [string[]] $Findings.DomainDomainControllers.Keys) {
            if ($_ -notin $Findings.Domains) {
                $Findings.DomainDomainControllers.Remove($_)
            }
        }

        foreach ($_ in [string[]] $Findings.DomainsExtended.Keys) {
            if ($_ -notin $Findings.Domains) {
                $Findings.DomainsExtended.Remove($_)
                $NetBiosName = $Findings.DomainsExtended.$_.'NetBIOSName'
                if ($NetBiosName) {
                    $Findings.DomainsExtendedNetBIOS.Remove($NetBiosName)
                }
            }
        }
        [Array] $Findings['ForestDomainControllers'] = foreach ($Domain in $Findings.Domains) {
            [Array] $AllDC = foreach ($S in $Findings.DomainDomainControllers["$Domain"]) {
                if ($IncludeDomainControllers.Count -gt 0) {
                    If (-not $IncludeDomainControllers[0].Contains('.')) {
                        if ($S.Name -notin $IncludeDomainControllers) {
                            continue
                        }
                    } else {
                        if ($S.HostName -notin $IncludeDomainControllers) {
                            continue
                        }
                    }
                }
                if ($ExcludeDomainControllers.Count -gt 0) {
                    If (-not $ExcludeDomainControllers[0].Contains('.')) {
                        if ($S.Name -in $ExcludeDomainControllers) {
                            continue
                        }
                    } else {
                        if ($S.HostName -in $ExcludeDomainControllers) {
                            continue
                        }
                    }
                }
                $S
            }
            if ($SkipRODC) {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false }
            } else {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC
            }

            [Array] $Findings['DomainDomainControllers'][$Domain]
        }
        $Findings
    }
}
function Convert-ADGuidToSchema { 
    <#
    .SYNOPSIS
    Converts Guid to schema properties
 
    .DESCRIPTION
    Converts Guid to schema properties
 
    .PARAMETER Guid
    Guid to Convert to Schema Name
 
    .PARAMETER Domain
    Domain to query. By default the current domain is used
 
   .PARAMETER RootDSE
    RootDSE to query. By default RootDSE is queried from the domain
 
    .PARAMETER DisplayName
    Return the schema name by display name. By default it returns as Name
 
    .EXAMPLE
    $T2 = '570b9266-bbb3-4fad-a712-d2e3fedc34dd'
    $T = [guid] '570b9266-bbb3-4fad-a712-d2e3fedc34dd'
 
    Convert-ADGuidToSchema -Guid $T
    Convert-ADGuidToSchema -Guid $T2
 
    .NOTES
    General notes
    #>

    [alias('Get-WinADDomainGUIDs', 'Get-WinADForestGUIDs')]
    [cmdletbinding()]
    param(
        [string] $Guid,
        [string] $Domain,
        [Microsoft.ActiveDirectory.Management.ADEntity] $RootDSE,
        [switch] $DisplayName
    )
    if (-not $Script:ADSchemaMap -or -not $Script:ADSchemaMapDisplayName) {
        if ($RootDSE) {
            $Script:RootDSE = $RootDSE
        } elseif (-not $Script:RootDSE) {
            if ($Domain) {
                $Script:RootDSE = Get-ADRootDSE -Server $Domain
            } else {
                $Script:RootDSE = Get-ADRootDSE
            }
        }
        $DomainCN = ConvertFrom-DistinguishedName -DistinguishedName $Script:RootDSE.defaultNamingContext -ToDomainCN
        $QueryServer = (Get-ADDomainController -DomainName $DomainCN -Discover -ErrorAction Stop).Hostname[0]

        $Script:ADSchemaMap = @{ }
        $Script:ADSchemaMapDisplayName = @{ }
        $Script:ADSchemaMapDisplayName['00000000-0000-0000-0000-000000000000'] = 'All'
        $Script:ADSchemaMap.Add('00000000-0000-0000-0000-000000000000', 'All')
        Write-Verbose "Convert-ADGuidToSchema - Querying Schema from $QueryServer"
        $Time = [System.Diagnostics.Stopwatch]::StartNew()
        if (-not $Script:StandardRights) {
            $Script:StandardRights = Get-ADObject -SearchBase $Script:RootDSE.schemaNamingContext -LDAPFilter "(schemaidguid=*)" -Properties name, lDAPDisplayName, schemaIDGUID -Server $QueryServer -ErrorAction Stop | Select-Object name, lDAPDisplayName, schemaIDGUID
        }
        foreach ($S in $Script:StandardRights) {
            $Script:ADSchemaMap["$(([System.GUID]$S.schemaIDGUID).Guid)"] = $S.name
            $Script:ADSchemaMapDisplayName["$(([System.GUID]$S.schemaIDGUID).Guid)"] = $S.lDAPDisplayName
        }
        $Time.Stop()
        $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds"
        Write-Verbose "Convert-ADGuidToSchema - Querying Schema from $QueryServer took $TimeToExecute"
        Write-Verbose "Convert-ADGuidToSchema - Querying Extended Rights from $QueryServer"
        $Time = [System.Diagnostics.Stopwatch]::StartNew()

        if (-not $Script:ExtendedRightsGuids) {
            $Script:ExtendedRightsGuids = Get-ADObject -SearchBase $Script:RootDSE.ConfigurationNamingContext -LDAPFilter "(&(objectclass=controlAccessRight)(rightsguid=*))" -Properties name, displayName, lDAPDisplayName, rightsGuid -Server $QueryServer -ErrorAction Stop | Select-Object name, displayName, lDAPDisplayName, rightsGuid
        }
        foreach ($S in $Script:ExtendedRightsGuids) {
            $Script:ADSchemaMap["$(([System.GUID]$S.rightsGUID).Guid)"] = $S.name
            $Script:ADSchemaMapDisplayName["$(([System.GUID]$S.rightsGUID).Guid)"] = $S.displayName
        }
        $Time.Stop()
        $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds"
        Write-Verbose "Convert-ADGuidToSchema - Querying Extended Rights from $QueryServer took $TimeToExecute"
    }
    if ($Guid) {
        if ($DisplayName) {
            $Script:ADSchemaMapDisplayName[$Guid]
        } else {
            $Script:ADSchemaMap[$Guid]
        }
    } else {
        if ($DisplayName) {
            $Script:ADSchemaMapDisplayName
        } else {
            $Script:ADSchemaMap
        }
    }
}
function Remove-EmptyValue { 
    <#
    .SYNOPSIS
    Removes empty values from a hashtable recursively.
 
    .DESCRIPTION
    This function removes empty values from a given hashtable. It can be used to clean up a hashtable by removing keys with null, empty string, empty array, or empty dictionary values. The function supports recursive removal of empty values.
 
    .PARAMETER Hashtable
    The hashtable from which empty values will be removed.
 
    .PARAMETER ExcludeParameter
    An array of keys to exclude from the removal process.
 
    .PARAMETER Recursive
    Indicates whether to recursively remove empty values from nested hashtables.
 
    .PARAMETER Rerun
    Specifies the number of times to rerun the removal process recursively.
 
    .PARAMETER DoNotRemoveNull
    If specified, null values will not be removed.
 
    .PARAMETER DoNotRemoveEmpty
    If specified, empty string values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyArray
    If specified, empty array values will not be removed.
 
    .PARAMETER DoNotRemoveEmptyDictionary
    If specified, empty dictionary values will not be removed.
 
    .EXAMPLE
    $hashtable = @{
        'Key1' = '';
        'Key2' = $null;
        'Key3' = @();
        'Key4' = @{}
    }
    Remove-EmptyValue -Hashtable $hashtable -Recursive
 
    Description
    -----------
    This example removes empty values from the $hashtable recursively.
 
    #>

    [alias('Remove-EmptyValues')]
    [CmdletBinding()]
    param(
        [alias('Splat', 'IDictionary')][Parameter(Mandatory)][System.Collections.IDictionary] $Hashtable,
        [string[]] $ExcludeParameter,
        [switch] $Recursive,
        [int] $Rerun,
        [switch] $DoNotRemoveNull,
        [switch] $DoNotRemoveEmpty,
        [switch] $DoNotRemoveEmptyArray,
        [switch] $DoNotRemoveEmptyDictionary
    )
    foreach ($Key in [string[]] $Hashtable.Keys) {
        if ($Key -notin $ExcludeParameter) {
            if ($Recursive) {
                if ($Hashtable[$Key] -is [System.Collections.IDictionary]) {
                    if ($Hashtable[$Key].Count -eq 0) {
                        if (-not $DoNotRemoveEmptyDictionary) {
                            $Hashtable.Remove($Key)
                        }
                    } else {
                        Remove-EmptyValue -Hashtable $Hashtable[$Key] -Recursive:$Recursive
                    }
                } else {
                    if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                        $Hashtable.Remove($Key)
                    } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                        $Hashtable.Remove($Key)
                    }
                }
            } else {
                if (-not $DoNotRemoveNull -and $null -eq $Hashtable[$Key]) {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmpty -and $Hashtable[$Key] -is [string] -and $Hashtable[$Key] -eq '') {
                    $Hashtable.Remove($Key)
                } elseif (-not $DoNotRemoveEmptyArray -and $Hashtable[$Key] -is [System.Collections.IList] -and $Hashtable[$Key].Count -eq 0) {
                    $Hashtable.Remove($Key)
                }
            }
        }
    }
    if ($Rerun) {
        for ($i = 0; $i -lt $Rerun; $i++) {
            Remove-EmptyValue -Hashtable $Hashtable -Recursive:$Recursive
        }
    }
}
function Remove-PSRegistry { 
    <#
    .SYNOPSIS
    Remove registry keys and folders
 
    .DESCRIPTION
    Remove registry keys and folders using .NET methods
 
    .PARAMETER ComputerName
    The computer to run the command on. Defaults to local computer.
 
    .PARAMETER RegistryPath
    The registry path to remove.
 
    .PARAMETER Key
    The registry key to remove.
 
    .PARAMETER Recursive
    Forces deletion of registry folder and all keys, including nested folders
 
    .PARAMETER Suppress
    Suppresses the output of the command. By default the command outputs PSObject with the results of the operation.
 
    .EXAMPLE
    Remove-PSRegistry -RegistryPath "HKEY_CURRENT_USER\Tests\Ok\MaybeNot" -Recursive
 
    .EXAMPLE
    Remove-PSRegistry -RegistryPath "HKEY_CURRENT_USER\Tests\Ok\MaybeNot" -Key "LimitBlankPass1wordUse"
 
    .EXAMPLE
    Remove-PSRegistry -RegistryPath "HKCU:\Tests\Ok"
 
    .NOTES
    General notes
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [string[]] $ComputerName = $Env:COMPUTERNAME,
        [Parameter(Mandatory)][string] $RegistryPath,
        [Parameter()][string] $Key,
        [switch] $Recursive,
        [switch] $Suppress
    )
    Get-PSRegistryDictionaries

    [Array] $ComputersSplit = Get-ComputerSplit -ComputerName $ComputerName

    $RegistryPath = Resolve-PrivateRegistry -RegistryPath $RegistryPath

    [Array] $RegistryTranslated = Get-PSConvertSpecialRegistry -RegistryPath $RegistryPath -Computers $ComputerName -HiveDictionary $Script:HiveDictionary

    foreach ($Registry in $RegistryTranslated) {
        $RegistryValue = Get-PrivateRegistryTranslated -RegistryPath $Registry -HiveDictionary $Script:HiveDictionary -Key $Key -ReverseTypesDictionary $Script:ReverseTypesDictionary

        if ($RegistryValue.HiveKey) {
            foreach ($Computer in $ComputersSplit[0]) {

                Remove-PrivateRegistry -Key $Key -RegistryValue $RegistryValue -Computer $Computer -Suppress:$Suppress.IsPresent -ErrorAction $ErrorActionPreference -WhatIf:$WhatIfPreference
            }
            foreach ($Computer in $ComputersSplit[1]) {

                Remove-PrivateRegistry -Key $Key -RegistryValue $RegistryValue -Computer $Computer -Remote -Suppress:$Suppress.IsPresent -ErrorAction $ErrorActionPreference -WhatIf:$WhatIfPreference
            }
        } else {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Unregister-MountedRegistry
                throw
            } else {

                Write-Warning "Remove-PSRegistry - Removing registry $RegistryPath have failed (recursive: $($Recursive.IsPresent)). Couldn't translate HIVE."
            }
        }
    }
    Unregister-MountedRegistry
}
function Rename-LatinCharacters { 
    <#
    .SYNOPSIS
    Renames a name to a name without special chars.
 
    .DESCRIPTION
    Renames a name to a name without special chars.
 
    .PARAMETER String
    Provide a string to rename
 
    .EXAMPLE
    Rename-LatinCharacters -String 'PrzemysÅ‚aw KÅ‚ys'
 
    .EXAMPLE
    Rename-LatinCharacters -String 'PrzemysÅ‚aw'
 
    .NOTES
    General notes
    #>

    [alias('Remove-StringLatinCharacters')]
    [cmdletBinding()]
    param(
        [string] $String
    )
    [Text.Encoding]::ASCII.GetString([Text.Encoding]::GetEncoding("Cyrillic").GetBytes($String))
}
function Set-FileOwner { 
    <#
    .SYNOPSIS
    Sets the owner of a file or folder.
 
    .DESCRIPTION
    This function sets the owner of a specified file or folder to the provided owner.
 
    .PARAMETER Path
    Specifies the path to the file or folder.
 
    .PARAMETER Recursive
    Indicates whether to process the items in the specified path recursively.
 
    .PARAMETER Owner
    Specifies the new owner for the file or folder.
 
    .PARAMETER Exclude
    Specifies an array of owners to exclude from ownership change.
 
    .PARAMETER JustPath
    Indicates whether to only change the owner of the specified path without recursing into subfolders.
 
    .EXAMPLE
    Set-FileOwner -Path "C:\Example\File.txt" -Owner "DOMAIN\User1"
 
    Description:
    Sets the owner of the file "File.txt" to "DOMAIN\User1".
 
    .EXAMPLE
    Set-FileOwner -Path "C:\Example\Folder" -Owner "DOMAIN\User2" -Recursive
 
    Description:
    Sets the owner of the folder "Folder" and all its contents to "DOMAIN\User2" recursively.
 
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [Array] $Path,
        [switch] $Recursive,
        [string] $Owner,
        [string[]] $Exlude,
        [switch] $JustPath
    )

    Begin { 
    }
    Process {
        foreach ($P in $Path) {
            if ($P -is [System.IO.FileSystemInfo]) {
                $FullPath = $P.FullName
            } elseif ($P -is [string]) {
                $FullPath = $P
            }
            $OwnerTranslated = [System.Security.Principal.NTAccount]::new($Owner)
            if ($FullPath -and (Test-Path -Path $FullPath)) {
                if ($JustPath) {
                    $FullPath | ForEach-Object -Process {
                        $File = $_
                        try {
                            $ACL = Get-Acl -Path $File -ErrorAction Stop
                        } catch {
                            Write-Warning "Set-FileOwner - Getting ACL failed with error: $($_.Exception.Message)"
                        }
                        if ($ACL.Owner -notin $Exlude -and $ACL.Owner -ne $OwnerTranslated) {
                            if ($PSCmdlet.ShouldProcess($File, "Replacing owner $($ACL.Owner) to $OwnerTranslated")) {
                                try {
                                    $ACL.SetOwner($OwnerTranslated)
                                    Set-Acl -Path $File -AclObject $ACL -ErrorAction Stop
                                } catch {
                                    Write-Warning "Set-FileOwner - Replacing owner $($ACL.Owner) to $OwnerTranslated failed with error: $($_.Exception.Message)"
                                }
                            }
                        }
                    }
                } else {
                    Get-ChildItem -LiteralPath $FullPath -Recurse:$Recursive -ErrorAction SilentlyContinue -ErrorVariable err | ForEach-Object -Process {
                        $File = $_
                        try {
                            $ACL = Get-Acl -Path $File.FullName -ErrorAction Stop
                        } catch {
                            Write-Warning "Set-FileOwner - Getting ACL failed with error: $($_.Exception.Message)"
                        }
                        if ($ACL.Owner -notin $Exlude -and $ACL.Owner -ne $OwnerTranslated) {
                            if ($PSCmdlet.ShouldProcess($File.FullName, "Replacing owner $($ACL.Owner) to $OwnerTranslated")) {
                                try {
                                    $ACL.SetOwner($OwnerTranslated)
                                    Set-Acl -Path $File.FullName -AclObject $ACL -ErrorAction Stop
                                } catch {
                                    Write-Warning "Set-FileOwner - Replacing owner $($ACL.Owner) to $OwnerTranslated failed with error: $($_.Exception.Message)"
                                }
                            }
                        }
                    }
                    foreach ($e in $err) {
                        Write-Warning "Set-FileOwner - Errors processing $($e.Exception.Message) ($($e.CategoryInfo.Reason))"
                    }
                }
            }
        }
    }
    End {
    }
}
function Set-PSRegistry { 
    <#
    .SYNOPSIS
    Sets/Updates registry entries locally and remotely using .NET methods.
 
    .DESCRIPTION
    Sets/Updates registry entries locally and remotely using .NET methods. If the registry path to key doesn't exists it will be created.
 
    .PARAMETER ComputerName
    The computer to run the command on. Defaults to local computer.
 
    .PARAMETER RegistryPath
    Registry Path to Update
 
    .PARAMETER Type
    Registry type to use. Options are: REG_SZ, REG_EXPAND_SZ, REG_BINARY, REG_DWORD, REG_MULTI_SZ, REG_QWORD, string, expandstring, binary, dword, multistring, qword
 
    .PARAMETER Key
    Registry key to set. If the path to registry key doesn't exists it will be created.
 
    .PARAMETER Value
    Registry value to set.
 
    .PARAMETER Suppress
    Suppresses the output of the command. By default the command outputs PSObject with the results of the operation.
 
    .EXAMPLE
    Set-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics' -Type REG_DWORD -Key "16 LDAP Interface Events" -Value 2 -ComputerName AD1
 
    .EXAMPLE
    Set-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics' -Type REG_SZ -Key "LDAP Interface Events" -Value 'test' -ComputerName AD1
 
    .EXAMPLE
    Set-PSRegistry -RegistryPath "HKCU:\\Tests" -Key "LimitBlankPass1wordUse" -Value "0" -Type REG_DWORD
 
    .EXAMPLE
    Set-PSRegistry -RegistryPath "HKCU:\\Tests\MoreTests\Tests1" -Key "LimitBlankPass1wordUse" -Value "0" -Type REG_DWORD
 
    .EXAMPLE
    # Setting default value
 
    $ValueData = [byte[]] @(
        0, 1, 0, 0, 9, 0, 0, 0, 128, 0, 0, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3,
        0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3,
        0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3,
        0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3, 0, 3,
        0, 3, 0, 0, 0, 5, 0, 10, 0, 14, 0, 3, 0, 5, 0, 6, 0, 6, 0, 4, 0, 4, 0
    )
    Set-PSRegistry -RegistryPath "HKEY_CURRENT_USER\Tests" -Key '' -Value $ValueData -Type 'NONE'
 
    .NOTES
    General notes
    #>

    [cmdletbinding(SupportsShouldProcess)]
    param(
        [string[]] $ComputerName = $Env:COMPUTERNAME,
        [Parameter(Mandatory)][string] $RegistryPath,
        [Parameter(Mandatory)][ValidateSet('REG_SZ', 'REG_NONE', 'None', 'REG_EXPAND_SZ', 'REG_BINARY', 'REG_DWORD', 'REG_MULTI_SZ', 'REG_QWORD', 'string', 'binary', 'dword', 'qword', 'multistring', 'expandstring')][string] $Type,
        [Parameter()][string] $Key,
        [Parameter(Mandatory)][object] $Value,
        [switch] $Suppress
    )
    Unregister-MountedRegistry

    Get-PSRegistryDictionaries

    [Array] $ComputersSplit = Get-ComputerSplit -ComputerName $ComputerName

    $RegistryPath = Resolve-PrivateRegistry -RegistryPath $RegistryPath

    [Array] $RegistryTranslated = Get-PSConvertSpecialRegistry -RegistryPath $RegistryPath -Computers $ComputerName -HiveDictionary $Script:HiveDictionary

    foreach ($Registry in $RegistryTranslated) {
        $RegistryValue = Get-PrivateRegistryTranslated -RegistryPath $Registry -HiveDictionary $Script:HiveDictionary -Key $Key -Value $Value -Type $Type -ReverseTypesDictionary $Script:ReverseTypesDictionary
        if ($RegistryValue.HiveKey) {
            foreach ($Computer in $ComputersSplit[0]) {

                Set-PrivateRegistry -RegistryValue $RegistryValue -Computer $Computer -Suppress:$Suppress.IsPresent -ErrorAction $ErrorActionPreference -WhatIf:$WhatIfPreference
            }
            foreach ($Computer in $ComputersSplit[1]) {

                Set-PrivateRegistry -RegistryValue $RegistryValue -Computer $Computer -Remote -Suppress:$Suppress.IsPresent -ErrorAction $ErrorActionPreference -WhatIf:$WhatIfPreference
            }
        } else {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Unregister-MountedRegistry
                throw
            } else {

                Write-Warning "Set-PSRegistry - Setting registry to $Registry have failed. Couldn't translate HIVE."
            }
        }
    }
    Unregister-MountedRegistry
}
function Start-TimeLog { 
    <#
    .SYNOPSIS
    Starts a new stopwatch for logging time.
 
    .DESCRIPTION
    This function starts a new stopwatch that can be used for logging time durations.
 
    .EXAMPLE
    Start-TimeLog
    Starts a new stopwatch for logging time.
 
    #>

    [CmdletBinding()]
    param()
    [System.Diagnostics.Stopwatch]::StartNew()
}
function Stop-TimeLog { 
    <#
    .SYNOPSIS
    Stops the stopwatch and returns the elapsed time in a specified format.
 
    .DESCRIPTION
    The Stop-TimeLog function stops the provided stopwatch and returns the elapsed time in a specified format. The function can output the elapsed time as a single string or an array of days, hours, minutes, seconds, and milliseconds.
 
    .PARAMETER Time
    Specifies the stopwatch object to stop and retrieve the elapsed time from.
 
    .PARAMETER Option
    Specifies the format in which the elapsed time should be returned. Valid values are 'OneLiner' (default) or 'Array'.
 
    .PARAMETER Continue
    Indicates whether the stopwatch should continue running after retrieving the elapsed time.
 
    .EXAMPLE
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    # Perform some operations
    Stop-TimeLog -Time $stopwatch
    # Output: "0 days, 0 hours, 0 minutes, 5 seconds, 123 milliseconds"
 
    .EXAMPLE
    $stopwatch = [System.Diagnostics.Stopwatch]::StartNew()
    # Perform some operations
    Stop-TimeLog -Time $stopwatch -Option Array
    # Output: ["0 days", "0 hours", "0 minutes", "5 seconds", "123 milliseconds"]
    #>

    [CmdletBinding()]
    param (
        [Parameter(ValueFromPipeline = $true)][System.Diagnostics.Stopwatch] $Time,
        [ValidateSet('OneLiner', 'Array')][string] $Option = 'OneLiner',
        [switch] $Continue
    )
    Begin {
    }
    Process {
        if ($Option -eq 'Array') {
            $TimeToExecute = "$($Time.Elapsed.Days) days", "$($Time.Elapsed.Hours) hours", "$($Time.Elapsed.Minutes) minutes", "$($Time.Elapsed.Seconds) seconds", "$($Time.Elapsed.Milliseconds) milliseconds"
        } else {
            $TimeToExecute = "$($Time.Elapsed.Days) days, $($Time.Elapsed.Hours) hours, $($Time.Elapsed.Minutes) minutes, $($Time.Elapsed.Seconds) seconds, $($Time.Elapsed.Milliseconds) milliseconds"
        }
    }
    End {
        if (-not $Continue) {
            $Time.Stop()
        }
        return $TimeToExecute
    }
}
function Write-Color { 
    <#
    .SYNOPSIS
    Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options.
 
    .DESCRIPTION
    Write-Color is a wrapper around Write-Host delivering a lot of additional features for easier color options.
 
    It provides:
    - Easy manipulation of colors,
    - Logging output to file (log)
    - Nice formatting options out of the box.
    - Ability to use aliases for parameters
 
    .PARAMETER Text
    Text to display on screen and write to log file if specified.
    Accepts an array of strings.
 
    .PARAMETER Color
    Color of the text. Accepts an array of colors. If more than one color is specified it will loop through colors for each string.
    If there are more strings than colors it will start from the beginning.
    Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
 
    .PARAMETER BackGroundColor
    Color of the background. Accepts an array of colors. If more than one color is specified it will loop through colors for each string.
    If there are more strings than colors it will start from the beginning.
    Available colors are: Black, DarkBlue, DarkGreen, DarkCyan, DarkRed, DarkMagenta, DarkYellow, Gray, DarkGray, Blue, Green, Cyan, Red, Magenta, Yellow, White
 
    .PARAMETER StartTab
    Number of tabs to add before text. Default is 0.
 
    .PARAMETER LinesBefore
    Number of empty lines before text. Default is 0.
 
    .PARAMETER LinesAfter
    Number of empty lines after text. Default is 0.
 
    .PARAMETER StartSpaces
    Number of spaces to add before text. Default is 0.
 
    .PARAMETER LogFile
    Path to log file. If not specified no log file will be created.
 
    .PARAMETER DateTimeFormat
    Custom date and time format string. Default is yyyy-MM-dd HH:mm:ss
 
    .PARAMETER LogTime
    If set to $true it will add time to log file. Default is $true.
 
    .PARAMETER LogRetry
    Number of retries to write to log file, in case it can't write to it for some reason, before skipping. Default is 2.
 
    .PARAMETER Encoding
    Encoding of the log file. Default is Unicode.
 
    .PARAMETER ShowTime
    Switch to add time to console output. Default is not set.
 
    .PARAMETER NoNewLine
    Switch to not add new line at the end of the output. Default is not set.
 
    .PARAMETER NoConsoleOutput
    Switch to not output to console. Default all output goes to console.
 
    .EXAMPLE
    Write-Color -Text "Red ", "Green ", "Yellow " -Color Red,Green,Yellow
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                      "followed by red ",
                      "and then we have Magenta... ",
                      "isn't it fun? ",
                      "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan
 
    .EXAMPLE
    Write-Color -Text "This is text in Green ",
                      "followed by red ",
                      "and then we have Magenta... ",
                      "isn't it fun? ",
                      "Here goes DarkCyan" -Color Green,Red,Magenta,White,DarkCyan -StartTab 3 -LinesBefore 1 -LinesAfter 1
 
    .EXAMPLE
    Write-Color "1. ", "Option 1" -Color Yellow, Green
    Write-Color "2. ", "Option 2" -Color Yellow, Green
    Write-Color "3. ", "Option 3" -Color Yellow, Green
    Write-Color "4. ", "Option 4" -Color Yellow, Green
    Write-Color "9. ", "Press 9 to exit" -Color Yellow, Gray -LinesBefore 1
 
    .EXAMPLE
    Write-Color -LinesBefore 2 -Text "This little ","message is ", "written to log ", "file as well." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt" -TimeFormat "yyyy-MM-dd HH:mm:ss"
    Write-Color -Text "This can get ","handy if ", "want to display things, and log actions to file ", "at the same time." `
                -Color Yellow, White, Green, Red, Red -LogFile "C:\testing.txt"
 
    .EXAMPLE
    Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow
    Write-Color -t "my text" -c yellow -b green
    Write-Color -text "my text" -c red
 
    .EXAMPLE
    Write-Color -Text "TestujÄ™ czy siÄ™ Å‚adnie zapisze, czy bÄ™dÄ… problemy" -Encoding unicode -LogFile 'C:\temp\testinggg.txt' -Color Red -NoConsoleOutput
 
    .NOTES
    Understanding Custom date and time format strings: https://learn.microsoft.com/en-us/dotnet/standard/base-types/custom-date-and-time-format-strings
    Project support: https://github.com/EvotecIT/PSWriteColor
    Original idea: Josh (https://stackoverflow.com/users/81769/josh)
 
    #>

    [alias('Write-Colour')]
    [CmdletBinding()]
    param (
        [alias ('T')] [String[]]$Text,
        [alias ('C', 'ForegroundColor', 'FGC')] [ConsoleColor[]]$Color = [ConsoleColor]::White,
        [alias ('B', 'BGC')] [ConsoleColor[]]$BackGroundColor = $null,
        [alias ('Indent')][int] $StartTab = 0,
        [int] $LinesBefore = 0,
        [int] $LinesAfter = 0,
        [int] $StartSpaces = 0,
        [alias ('L')] [string] $LogFile = '',
        [Alias('DateFormat', 'TimeFormat')][string] $DateTimeFormat = 'yyyy-MM-dd HH:mm:ss',
        [alias ('LogTimeStamp')][bool] $LogTime = $true,
        [int] $LogRetry = 2,
        [ValidateSet('unknown', 'string', 'unicode', 'bigendianunicode', 'utf8', 'utf7', 'utf32', 'ascii', 'default', 'oem')][string]$Encoding = 'Unicode',
        [switch] $ShowTime,
        [switch] $NoNewLine,
        [alias('HideConsole')][switch] $NoConsoleOutput
    )
    if (-not $NoConsoleOutput) {
        $DefaultColor = $Color[0]
        if ($null -ne $BackGroundColor -and $BackGroundColor.Count -ne $Color.Count) {
            Write-Error "Colors, BackGroundColors parameters count doesn't match. Terminated."
            return
        }
        if ($LinesBefore -ne 0) {
            for ($i = 0; $i -lt $LinesBefore; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        } # Add empty line before
        if ($StartTab -ne 0) {
            for ($i = 0; $i -lt $StartTab; $i++) {
                Write-Host -Object "`t" -NoNewline 
            } 
        }  # Add TABS before text
        if ($StartSpaces -ne 0) {
            for ($i = 0; $i -lt $StartSpaces; $i++) {
                Write-Host -Object ' ' -NoNewline 
            } 
        }  # Add SPACES before text
        if ($ShowTime) {
            Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline 
        } # Add Time before output
        if ($Text.Count -ne 0) {
            if ($Color.Count -ge $Text.Count) {
                # the real deal coloring
                if ($null -eq $BackGroundColor) {
                    for ($i = 0; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline 
                    }
                } else {
                    for ($i = 0; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline 
                    }
                }
            } else {
                if ($null -eq $BackGroundColor) {
                    for ($i = 0; $i -lt $Color.Length ; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -NoNewline 
                    }
                    for ($i = $Color.Length; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -NoNewline 
                    }
                } else {
                    for ($i = 0; $i -lt $Color.Length ; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $Color[$i] -BackgroundColor $BackGroundColor[$i] -NoNewline 
                    }
                    for ($i = $Color.Length; $i -lt $Text.Length; $i++) {
                        Write-Host -Object $Text[$i] -ForegroundColor $DefaultColor -BackgroundColor $BackGroundColor[0] -NoNewline 
                    }
                }
            }
        }
        if ($NoNewLine -eq $true) {
            Write-Host -NoNewline 
        } else {
            Write-Host 
        } # Support for no new line
        if ($LinesAfter -ne 0) {
            for ($i = 0; $i -lt $LinesAfter; $i++) {
                Write-Host -Object "`n" -NoNewline 
            } 
        }  # Add empty line after
    }
    if ($Text.Count -and $LogFile) {
        # Save to file
        $TextToFile = ""
        for ($i = 0; $i -lt $Text.Length; $i++) {
            $TextToFile += $Text[$i]
        }
        $Saved = $false
        $Retry = 0
        Do {
            $Retry++
            try {
                if ($LogTime) {
                    "[$([datetime]::Now.ToString($DateTimeFormat))] $TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false
                } else {
                    "$TextToFile" | Out-File -FilePath $LogFile -Encoding $Encoding -Append -ErrorAction Stop -WhatIf:$false
                }
                $Saved = $true
            } catch {
                if ($Saved -eq $false -and $Retry -eq $LogRetry) {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Tried ($Retry/$LogRetry))"
                } else {
                    Write-Warning "Write-Color - Couldn't write to log file $($_.Exception.Message). Retrying... ($Retry/$LogRetry)"
                }
            }
        } Until ($Saved -eq $true -or $Retry -ge $LogRetry)
    }
}
function Convert-BinaryToIP { 
    <#
    .SYNOPSIS
    Converts a binary string to an IP address format.
 
    .DESCRIPTION
    This function takes a binary string as input and converts it to an IP address format. The binary string must be evenly divisible by 8.
 
    .PARAMETER Binary
    The binary string to convert to an IP address format.
 
    .EXAMPLE
    Convert-BinaryToIP -Binary "01000001000000100000000100000001"
    Output: 65.0.1.1
    #>

    [cmdletBinding()]
    param(
        [string] $Binary
    )
    $Binary = $Binary -replace '\s+'
    if ($Binary.Length % 8) {
        Write-Warning -Message "Convert-BinaryToIP - Binary string '$Binary' is not evenly divisible by 8."
        return $Null
    }
    [int] $NumberOfBytes = $Binary.Length / 8
    $Bytes = @(foreach ($i in 0..($NumberOfBytes - 1)) {
            try {

                [System.Convert]::ToByte($Binary.Substring(($i * 8), 8), 2)
            } catch {
                Write-Warning -Message "Convert-BinaryToIP - Error converting '$Binary' to bytes. `$i was $i."
                return $Null
            }
        })
    return $Bytes -join '.'
}
function Convert-GenericRightsToFileSystemRights { 
    <#
    .SYNOPSIS
    Converts generic rights to file system rights for a given set of original rights.
 
    .DESCRIPTION
    This function maps generic rights to corresponding file system rights based on the provided original rights.
 
    .PARAMETER OriginalRights
    Specifies the original generic rights to be converted to file system rights.
 
    .EXAMPLE
    Convert-GenericRightsToFileSystemRights -OriginalRights GENERIC_READ
    Converts the generic read rights to file system rights.
 
    .NOTES
    This function is based on the mapping provided in the blog post: https://blog.cjwdev.co.uk/2011/06/28/permissions-not-included-in-net-accessrule-filesystemrights-enum/
 
    .LINK
    https://blog.cjwdev.co.uk/2011/06/28/permissions-not-included-in-net-accessrule-filesystemrights-enum/
    #>

    [cmdletBinding()]
    param(
        [System.Security.AccessControl.FileSystemRights] $OriginalRights
    )
    Begin {
        $FileSystemRights = [System.Security.AccessControl.FileSystemRights]
        $GenericRights = @{
            GENERIC_READ    = 0x80000000;
            GENERIC_WRITE   = 0x40000000;
            GENERIC_EXECUTE = 0x20000000;
            GENERIC_ALL     = 0x10000000;
            FILTER_GENERIC  = 0x0FFFFFFF;
        }
        $MappedGenericRights = @{
            FILE_GENERIC_EXECUTE = $FileSystemRights::ExecuteFile -bor $FileSystemRights::ReadPermissions -bor $FileSystemRights::ReadAttributes -bor $FileSystemRights::Synchronize
            FILE_GENERIC_READ    = $FileSystemRights::ReadAttributes -bor $FileSystemRights::ReadData -bor $FileSystemRights::ReadExtendedAttributes -bor $FileSystemRights::ReadPermissions -bor $FileSystemRights::Synchronize
            FILE_GENERIC_WRITE   = $FileSystemRights::AppendData -bor $FileSystemRights::WriteAttributes -bor $FileSystemRights::WriteData -bor $FileSystemRights::WriteExtendedAttributes -bor $FileSystemRights::ReadPermissions -bor $FileSystemRights::Synchronize
            FILE_GENERIC_ALL     = $FileSystemRights::FullControl
        }
    }
    Process {
        $MappedRights = [System.Security.AccessControl.FileSystemRights]::new()
        if ($OriginalRights -band $GenericRights.GENERIC_EXECUTE) {
            $MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_EXECUTE
        }
        if ($OriginalRights -band $GenericRights.GENERIC_READ) {
            $MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_READ
        }
        if ($OriginalRights -band $GenericRights.GENERIC_WRITE) {
            $MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_WRITE
        }
        if ($OriginalRights -band $GenericRights.GENERIC_ALL) {
            $MappedRights = $MappedRights -bor $MappedGenericRights.FILE_GENERIC_ALL
        }
        (($OriginalRights -bAND $GenericRights.FILTER_GENERIC) -bOR $MappedRights) -as $FileSystemRights
    }
    End {
    }
}
function Convert-IPToBinary { 
    <#
    .SYNOPSIS
    Converts an IPv4 address to binary format.
 
    .DESCRIPTION
    This function takes an IPv4 address as input and converts it to binary format.
 
    .PARAMETER IP
    Specifies the IPv4 address to convert to binary format.
 
    .EXAMPLE
    Convert-IPToBinary -IP "192.168.1.1"
    Converts the IPv4 address "192.168.1.1" to binary format.
 
    .EXAMPLE
    Convert-IPToBinary -IP "10.0.0.1"
    Converts the IPv4 address "10.0.0.1" to binary format.
 
    #>

    [cmdletBinding()]
    param(
        [string] $IP
    )
    $IPv4Regex = '(?:(?:0?0?\d|0?[1-9]\d|1\d\d|2[0-5][0-5]|2[0-4]\d)\.){3}(?:0?0?\d|0?[1-9]\d|1\d\d|2[0-5][0-5]|2[0-4]\d)'
    $IP = $IP.Trim()
    if ($IP -match "\A${IPv4Regex}\z") {
        try {
            return ($IP.Split('.') | ForEach-Object { [System.Convert]::ToString([byte] $_, 2).PadLeft(8, '0') }) -join ''
        } catch {
            Write-Warning -Message "Convert-IPToBinary - Error converting '$IP' to a binary string: $_"
            return $Null
        }
    } else {
        Write-Warning -Message "Convert-IPToBinary - Invalid IP detected: '$IP'. Conversion failed."
        return $Null
    }
}
function Copy-DictionaryManual { 
    <#
    .SYNOPSIS
    Copies a dictionary recursively, handling nested dictionaries and lists.
 
    .DESCRIPTION
    This function copies a dictionary recursively, handling nested dictionaries and lists. It creates a deep copy of the input dictionary, ensuring that modifications to the copied dictionary do not affect the original dictionary.
 
    .PARAMETER Dictionary
    The dictionary to be copied.
 
    .EXAMPLE
    $originalDictionary = @{
        'Key1' = 'Value1'
        'Key2' = @{
            'NestedKey1' = 'NestedValue1'
        }
    }
    $copiedDictionary = Copy-DictionaryManual -Dictionary $originalDictionary
 
    This example demonstrates how to copy a dictionary with nested values.
 
    #>

    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Dictionary
    )

    $clone = [ordered] @{}
    foreach ($Key in $Dictionary.Keys) {
        $value = $Dictionary.$Key

        $clonedValue = switch ($Dictionary.$Key) {
            { $null -eq $_ } {
                $null
                continue
            }
            { $_ -is [System.Collections.IDictionary] } {
                Copy-DictionaryManual -Dictionary $_
                continue
            }
            {
                $type = $_.GetType()
                $type.IsPrimitive -or $type.IsValueType -or $_ -is [string]
            } {
                $_
                continue
            }
            default {
                $_ | Select-Object -Property *
            }
        }

        if ($value -is [System.Collections.IList]) {
            $clone[$Key] = @($clonedValue)
        } else {
            $clone[$Key] = $clonedValue
        }
    }

    $clone
}
function Get-ComputerSplit { 
    <#
    .SYNOPSIS
    This function splits the list of computer names provided into two arrays: one containing remote computers and another containing the local computer.
 
    .DESCRIPTION
    The Get-ComputerSplit function takes an array of computer names as input and splits them into two arrays based on whether they are remote computers or the local computer. It determines the local computer by comparing the provided computer names with the local computer name and DNS name.
 
    .PARAMETER ComputerName
    Specifies an array of computer names to split into remote and local computers.
 
    .EXAMPLE
    Get-ComputerSplit -ComputerName "Computer1", "Computer2", $Env:COMPUTERNAME
    This example splits the computer names "Computer1" and "Computer2" into the remote computers array and the local computer array based on the local computer's name.
 
    #>

    [CmdletBinding()]
    param(
        [string[]] $ComputerName
    )
    if ($null -eq $ComputerName) {
        $ComputerName = $Env:COMPUTERNAME
    }
    try {
        $LocalComputerDNSName = [System.Net.Dns]::GetHostByName($Env:COMPUTERNAME).HostName
    } catch {
        $LocalComputerDNSName = $Env:COMPUTERNAME
    }
    $ComputersLocal = $null
    [Array] $Computers = foreach ($Computer in $ComputerName) {
        if ($Computer -eq '' -or $null -eq $Computer) {
            $Computer = $Env:COMPUTERNAME
        }
        if ($Computer -ne $Env:COMPUTERNAME -and $Computer -ne $LocalComputerDNSName) {
            $Computer
        } else {
            $ComputersLocal = $Computer
        }
    }
    , @($ComputersLocal, $Computers)
}
function Get-IPRange { 
    <#
    .SYNOPSIS
    Generates a list of IP addresses within a specified binary range.
 
    .DESCRIPTION
    This function takes two binary strings representing the start and end IP addresses and generates a list of IP addresses within that range.
 
    .PARAMETER StartBinary
    Specifies the starting IP address in binary format.
 
    .PARAMETER EndBinary
    Specifies the ending IP address in binary format.
 
    .EXAMPLE
    Get-IPRange -StartBinary '11000000' -EndBinary '11000010'
    Description:
    Generates a list of IP addresses between '192.0.0.0' and '192.0.2.0'.
 
    .EXAMPLE
    Get-IPRange -StartBinary '10101010' -EndBinary '10101100'
    Description:
    Generates a list of IP addresses between '170.0.0.0' and '172.0.0.0'.
    #>

    [cmdletBinding()]
    param(
        [string] $StartBinary,
        [string] $EndBinary
    )
    [int64] $StartInt = [System.Convert]::ToInt64($StartBinary, 2)
    [int64] $EndInt = [System.Convert]::ToInt64($EndBinary, 2)
    for ($BinaryIP = $StartInt; $BinaryIP -le $EndInt; $BinaryIP++) {
        Convert-BinaryToIP ([System.Convert]::ToString($BinaryIP, 2).PadLeft(32, '0'))
    }
}
function Get-LocalComputerSid { 
    <#
    .SYNOPSIS
    Get the SID of the local computer.
 
    .DESCRIPTION
    Get the SID of the local computer.
 
    .EXAMPLE
    Get-LocalComputerSid
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param()
    try {
        Add-Type -AssemblyName System.DirectoryServices.AccountManagement
        $PrincipalContext = [System.DirectoryServices.AccountManagement.PrincipalContext]::new([System.DirectoryServices.AccountManagement.ContextType]::Machine)
        $UserPrincipal = [System.DirectoryServices.AccountManagement.UserPrincipal]::new($PrincipalContext)
        $Searcher = [System.DirectoryServices.AccountManagement.PrincipalSearcher]::new()
        $Searcher.QueryFilter = $UserPrincipal
        $User = $Searcher.FindAll()
        foreach ($U in $User) {
            if ($U.Sid.Value -like "*-500") {
                return $U.Sid.Value.TrimEnd("-500")
            }
        }
    } catch {
        Write-Warning -Message "Get-LocalComputerSid - Error: $($_.Exception.Message)"
    }
}
function Get-PrivateRegistryTranslated { 
    <#
    .SYNOPSIS
    Retrieves translated private registry information based on the provided parameters.
 
    .DESCRIPTION
    This function retrieves translated private registry information based on the specified RegistryPath, HiveDictionary, ReverseTypesDictionary, Type, Key, and Value parameters.
 
    .PARAMETER RegistryPath
    Specifies the array of registry paths to be translated.
 
    .PARAMETER HiveDictionary
    Specifies the dictionary containing mappings of registry hives.
 
    .PARAMETER ReverseTypesDictionary
    Specifies the dictionary containing mappings of registry value types.
 
    .PARAMETER Type
    Specifies the type of the registry value. Valid values are 'REG_SZ', 'REG_NONE', 'None', 'REG_EXPAND_SZ', 'REG_BINARY', 'REG_DWORD', 'REG_MULTI_SZ', 'REG_QWORD', 'string', 'binary', 'dword', 'qword', 'multistring', 'expandstring'.
 
    .PARAMETER Key
    Specifies the key associated with the registry value.
 
    .PARAMETER Value
    Specifies the value of the registry key.
 
    .EXAMPLE
    Get-PrivateRegistryTranslated -RegistryPath "HKLM\Software\Microsoft" -HiveDictionary @{"HKLM"="HKEY_LOCAL_MACHINE"} -ReverseTypesDictionary @{"string"="REG_SZ"} -Type "string" -Key "Version" -Value "10.0.19041"
 
    Description
    -----------
    Retrieves translated registry information for the specified registry path.
 
    .EXAMPLE
    Get-PrivateRegistryTranslated -RegistryPath "HKCU\Software\Settings" -HiveDictionary @{"HKCU"="HKEY_CURRENT_USER"} -ReverseTypesDictionary @{"dword"="REG_DWORD"} -Type "dword" -Key "SettingA" -Value 1
 
    Description
    -----------
    Retrieves translated registry information for the specified registry path.
 
    #>

    [cmdletBinding()]
    param(
        [Array] $RegistryPath,
        [System.Collections.IDictionary] $HiveDictionary,
        [System.Collections.IDictionary] $ReverseTypesDictionary,
        [Parameter()][ValidateSet('REG_SZ', 'REG_NONE', 'None', 'REG_EXPAND_SZ', 'REG_BINARY', 'REG_DWORD', 'REG_MULTI_SZ', 'REG_QWORD', 'string', 'binary', 'dword', 'qword', 'multistring', 'expandstring')][string] $Type,
        [Parameter()][string] $Key,
        [Parameter()][object] $Value
    )
    foreach ($Registry in $RegistryPath) {

        if ($Registry -is [string]) {
            $Registry = $Registry.Replace("\\", "\").Replace("\\", "\").TrimStart("\").TrimEnd("\")
        } else {
            $Registry.RegistryPath = $Registry.RegistryPath.Replace("\\", "\").Replace("\\", "\").TrimStart("\").TrimEnd("\")
        }
        foreach ($Hive in $HiveDictionary.Keys) {
            if ($Registry -is [string] -and $Registry.StartsWith($Hive, [System.StringComparison]::CurrentCultureIgnoreCase)) {
                if ($Hive.Length -eq $Registry.Length) {
                    [ordered] @{
                        HiveKey    = $HiveDictionary[$Hive]
                        SubKeyName = $null
                        ValueKind  = if ($Type) {
                            [Microsoft.Win32.RegistryValueKind]::($ReverseTypesDictionary[$Type]) 
                        } else {
                            $null 
                        }
                        Key        = $Key
                        Value      = $Value
                    }
                } else {
                    [ordered] @{
                        HiveKey    = $HiveDictionary[$Hive]
                        SubKeyName = $Registry.substring($Hive.Length + 1)
                        ValueKind  = if ($Type) {
                            [Microsoft.Win32.RegistryValueKind]::($ReverseTypesDictionary[$Type]) 
                        } else {
                            $null 
                        }
                        Key        = $Key
                        Value      = $Value
                    }
                }
                break
            } elseif ($Registry -isnot [string] -and $Registry.RegistryPath.StartsWith($Hive, [System.StringComparison]::CurrentCultureIgnoreCase)) {
                if ($Hive.Length -eq $Registry.RegistryPath.Length) {
                    [ordered] @{
                        ComputerName = $Registry.ComputerName
                        HiveKey      = $HiveDictionary[$Hive]
                        SubKeyName   = $null
                        ValueKind    = if ($Type) {
                            [Microsoft.Win32.RegistryValueKind]::($ReverseTypesDictionary[$Type]) 
                        } else {
                            $null 
                        }
                        Key          = $Key
                        Value        = $Value
                    }
                } else {
                    [ordered] @{
                        ComputerName = $Registry.ComputerName
                        HiveKey      = $HiveDictionary[$Hive]
                        SubKeyName   = $Registry.RegistryPath.substring($Hive.Length + 1)
                        ValueKind    = if ($Type) {
                            [Microsoft.Win32.RegistryValueKind]::($ReverseTypesDictionary[$Type]) 
                        } else {
                            $null 
                        }
                        Key          = $Key
                        Value        = $Value
                    }
                }
                break
            }
        }
    }
}
function Get-PSConvertSpecialRegistry { 
    <#
    .SYNOPSIS
    Converts special registry paths for specified computers.
 
    .DESCRIPTION
    This function converts special registry paths for the specified computers using the provided HiveDictionary.
 
    .PARAMETER RegistryPath
    Specifies the array of registry paths to convert.
 
    .PARAMETER Computers
    Specifies the array of computers to convert registry paths for.
 
    .PARAMETER HiveDictionary
    Specifies the dictionary containing hive keys and their corresponding values.
 
    .PARAMETER ExpandEnvironmentNames
    Indicates whether to expand environment names in the registry paths.
 
    .EXAMPLE
    Get-PSConvertSpecialRegistry -RegistryPath "Users\Offline_Przemek\Software\Policies1\Microsoft\Windows\CloudContent" -Computers "Computer1", "Computer2" -HiveDictionary $HiveDictionary -ExpandEnvironmentNames
 
    Converts the specified registry path for the specified computers using the provided HiveDictionary.
 
    #>

    [cmdletbinding()]
    param(
        [Array] $RegistryPath,
        [Array] $Computers,
        [System.Collections.IDictionary] $HiveDictionary,
        [switch] $ExpandEnvironmentNames
    )
    $FixedPath = foreach ($R in $RegistryPath) {
        foreach ($DictionaryKey in $HiveDictionary.Keys) {
            $SplitParts = $R.Split("\")
            $FirstPart = $SplitParts[0]
            if ($FirstPart -eq $DictionaryKey) {

                if ($HiveDictionary[$DictionaryKey] -in 'All', 'All+Default', 'Default', 'AllDomain+Default', 'AllDomain', 'AllDomain+Other', 'AllDomain+Other+Default') {
                    foreach ($Computer in $Computers) {
                        $SubKeys = Get-PSRegistry -RegistryPath "HKEY_USERS" -ComputerName $Computer -ExpandEnvironmentNames:$ExpandEnvironmentNames.IsPresent -DoNotUnmount
                        if ($SubKeys.PSSubKeys) {
                            $RegistryKeys = ConvertTo-HKeyUser -SubKeys ($SubKeys.PSSubKeys | Sort-Object) -HiveDictionary $HiveDictionary -DictionaryKey $DictionaryKey -RegistryPath $R
                            foreach ($S in $RegistryKeys) {
                                [PSCustomObject] @{
                                    ComputerName = $Computer
                                    RegistryPath = $S
                                    Error        = $null
                                    ErrorMessage = $null
                                }
                            }
                        } else {
                            [PSCustomObject] @{
                                ComputerName = $Computer
                                RegistryPath = $R
                                Error        = $true
                                ErrorMessage = "Couldn't connect to $Computer to list HKEY_USERS"
                            }
                        }
                    }
                } elseif ($FirstPart -in 'Users', 'HKEY_USERS', 'HKU' -and $SplitParts[1] -and $SplitParts[1] -like "Offline_*") {

                    foreach ($Computer in $Computers) {
                        $SubKeys = Get-PSRegistry -RegistryPath "HKEY_USERS" -ComputerName $Computer -ExpandEnvironmentNames:$ExpandEnvironmentNames.IsPresent -DoNotUnmount
                        if ($SubKeys.PSSubKeys) {
                            $RegistryKeys = ConvertTo-HKeyUser -SubKeys ($SubKeys.PSSubKeys + $SplitParts[1] | Sort-Object) -HiveDictionary $HiveDictionary -DictionaryKey $DictionaryKey -RegistryPath $R
                            foreach ($S in $RegistryKeys) {
                                [PSCustomObject] @{
                                    ComputerName = $Computer
                                    RegistryPath = $S
                                    Error        = $null
                                    ErrorMessage = $null
                                }
                            }
                        } else {
                            [PSCustomObject] @{
                                ComputerName = $Computer
                                RegistryPath = $R
                                Error        = $true
                                ErrorMessage = "Couldn't connect to $Computer to list HKEY_USERS"
                            }
                        }
                    }
                } else {
                    $R
                }
                break
            }
        }
    }
    $FixedPath
}
function Get-PSRegistryDictionaries { 
    <#
    .SYNOPSIS
    Retrieves a set of registry dictionaries for common registry hives and keys.
 
    .DESCRIPTION
    This function retrieves a set of registry dictionaries that provide mappings for common registry hives and keys. These dictionaries can be used to easily reference different registry locations in PowerShell scripts.
 
    .EXAMPLE
    Get-PSRegistryDictionaries
 
    Description:
    Retrieves all the registry dictionaries.
 
    #>

    [cmdletBinding()]
    param()
    if ($Script:Dictionary) {
        return
    }
    $Script:Dictionary = @{

        'HKUAD:'   = 'HKEY_ALL_USERS_DEFAULT' 
        'HKUA:'    = 'HKEY_ALL_USERS' 
        'HKUD:'    = 'HKEY_DEFAULT_USER' 
        'HKUDUD:'  = 'HKEY_ALL_DOMAIN_USERS_DEFAULT' 

        'HKUDU:'   = 'HKEY_ALL_DOMAIN_USERS'

        'HKUDUO:'  = 'HKEY_ALL_DOMAIN_USERS_OTHER'

        'HKUDUDO:' = 'HKEY_ALL_DOMAIN_USERS_OTHER_DEFAULT'

        'HKCR:'    = 'HKEY_CLASSES_ROOT'
        'HKCU:'    = 'HKEY_CURRENT_USER'
        'HKLM:'    = 'HKEY_LOCAL_MACHINE'
        'HKU:'     = 'HKEY_USERS'
        'HKCC:'    = 'HKEY_CURRENT_CONFIG'
        'HKDD:'    = 'HKEY_DYN_DATA'
        'HKPD:'    = 'HKEY_PERFORMANCE_DATA'
    }

    $Script:HiveDictionary = [ordered] @{

        'HKEY_ALL_USERS_DEFAULT'              = 'All+Default'
        'HKUAD'                               = 'All+Default'
        'HKEY_ALL_USERS'                      = 'All'
        'HKUA'                                = 'All'
        'HKEY_ALL_DOMAIN_USERS_DEFAULT'       = 'AllDomain+Default'
        'HKUDUD'                              = 'AllDomain+Default'
        'HKEY_ALL_DOMAIN_USERS'               = 'AllDomain'
        'HKUDU'                               = 'AllDomain'
        'HKEY_DEFAULT_USER'                   = 'Default'
        'HKUD'                                = 'Default'
        'HKEY_ALL_DOMAIN_USERS_OTHER'         = 'AllDomain+Other'
        'HKUDUO'                              = 'AllDomain+Other'
        'HKUDUDO'                             = 'AllDomain+Other+Default'
        'HKEY_ALL_DOMAIN_USERS_OTHER_DEFAULT' = 'AllDomain+Other+Default'

        'HKEY_CLASSES_ROOT'                   = 'ClassesRoot'
        'HKCR'                                = 'ClassesRoot'
        'ClassesRoot'                         = 'ClassesRoot'
        'HKCU'                                = 'CurrentUser'
        'HKEY_CURRENT_USER'                   = 'CurrentUser'
        'CurrentUser'                         = 'CurrentUser'
        'HKLM'                                = 'LocalMachine'
        'HKEY_LOCAL_MACHINE'                  = 'LocalMachine'
        'LocalMachine'                        = 'LocalMachine'
        'HKU'                                 = 'Users'
        'HKEY_USERS'                          = 'Users'
        'Users'                               = 'Users'
        'HKCC'                                = 'CurrentConfig'
        'HKEY_CURRENT_CONFIG'                 = 'CurrentConfig'
        'CurrentConfig'                       = 'CurrentConfig'
        'HKDD'                                = 'DynData'
        'HKEY_DYN_DATA'                       = 'DynData'
        'DynData'                             = 'DynData'
        'HKPD'                                = 'PerformanceData'
        'HKEY_PERFORMANCE_DATA '              = 'PerformanceData'
        'PerformanceData'                     = 'PerformanceData'
    }

    $Script:ReverseTypesDictionary = [ordered] @{
        'REG_SZ'        = 'string'
        'REG_NONE'      = 'none'
        'REG_EXPAND_SZ' = 'expandstring'
        'REG_BINARY'    = 'binary'
        'REG_DWORD'     = 'dword'
        'REG_MULTI_SZ'  = 'multistring'
        'REG_QWORD'     = 'qword'
        'string'        = 'string'
        'expandstring'  = 'expandstring'
        'binary'        = 'binary'
        'dword'         = 'dword'
        'multistring'   = 'multistring'
        'qword'         = 'qword'
        'none'          = 'none'
    }
}
function Get-PSSubRegistry { 
    <#
    .SYNOPSIS
    Retrieves a subkey from the Windows Registry on a local or remote computer.
 
    .DESCRIPTION
    The Get-PSSubRegistry function retrieves a subkey from the Windows Registry on a local or remote computer. It can be used to access specific registry keys and their values.
 
    .PARAMETER Registry
    Specifies the registry key to retrieve. This parameter should be an IDictionary object containing information about the registry key.
 
    .PARAMETER ComputerName
    Specifies the name of the computer from which to retrieve the registry key. This parameter is optional and defaults to the local computer.
 
    .PARAMETER Remote
    Indicates that the registry key should be retrieved from a remote computer.
 
    .PARAMETER ExpandEnvironmentNames
    Indicates whether environment variable names in the registry key should be expanded.
 
    .EXAMPLE
    Get-PSSubRegistry -Registry $Registry -ComputerName "RemoteComputer" -Remote
    Retrieves a subkey from the Windows Registry on a remote computer named "RemoteComputer".
 
    .EXAMPLE
    Get-PSSubRegistry -Registry $Registry -ExpandEnvironmentNames
    Retrieves a subkey from the Windows Registry on the local computer with expanded environment variable names.
 
    #>

    [cmdletBinding()]
    param(
        [System.Collections.IDictionary] $Registry,
        [string] $ComputerName,
        [switch] $Remote,
        [switch] $ExpandEnvironmentNames
    )
    if ($Registry.ComputerName) {
        if ($Registry.ComputerName -ne $ComputerName) {
            return
        }
    }
    if (-not $Registry.Error) {
        try {
            if ($Remote) {
                $BaseHive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Registry.HiveKey, $ComputerName, 0 )
            } else {
                $BaseHive = [Microsoft.Win32.RegistryKey]::OpenBaseKey($Registry.HiveKey, 0 )
            }
            $PSConnection = $true
            $PSError = $null
        } catch {
            $PSConnection = $false
            $PSError = $($_.Exception.Message)
        }
    } else {

        $PSConnection = $false
        $PSError = $($Registry.ErrorMessage)
    }
    if ($PSError) {
        [PSCustomObject] @{
            PSComputerName = $ComputerName
            PSConnection   = $PSConnection
            PSError        = $true
            PSErrorMessage = $PSError
            PSPath         = $Registry.Registry
            PSKey          = $Registry.Key
            PSValue        = $null
            PSType         = $null
        }
    } else {
        try {
            $SubKey = $BaseHive.OpenSubKey($Registry.SubKeyName, $false)
            if ($null -ne $SubKey) {
                [PSCustomObject] @{
                    PSComputerName = $ComputerName
                    PSConnection   = $PSConnection
                    PSError        = $false
                    PSErrorMessage = $null
                    PSPath         = $Registry.Registry
                    PSKey          = $Registry.Key
                    PSValue        = if (-not $ExpandEnvironmentNames) {
                        $SubKey.GetValue($Registry.Key, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
                    } else {
                        $SubKey.GetValue($Registry.Key)
                    }
                    PSType         = $SubKey.GetValueKind($Registry.Key)
                }
            } else {
                [PSCustomObject] @{
                    PSComputerName = $ComputerName
                    PSConnection   = $PSConnection
                    PSError        = $true
                    PSErrorMessage = "Registry path $($Registry.Registry) doesn't exists."
                    PSPath         = $Registry.Registry
                    PSKey          = $Registry.Key
                    PSValue        = $null
                    PSType         = $null
                }
            }
        } catch {
            [PSCustomObject] @{
                PSComputerName = $ComputerName
                PSConnection   = $PSConnection
                PSError        = $true
                PSErrorMessage = $_.Exception.Message
                PSPath         = $Registry.Registry
                PSKey          = $Registry.Key
                PSValue        = $null
                PSType         = $null
            }
        }
    }
    if ($null -ne $SubKey) {
        $SubKey.Close()
        $SubKey.Dispose()
    }
    if ($null -ne $BaseHive) {
        $BaseHive.Close()
        $BaseHive.Dispose()
    }
}
function Get-PSSubRegistryComplete { 
    <#
    .SYNOPSIS
    Retrieves sub-registry information from a specified registry key.
 
    .DESCRIPTION
    This function retrieves sub-registry information from a specified registry key on a local or remote computer.
 
    .PARAMETER Registry
    Specifies the registry key information to retrieve.
 
    .PARAMETER ComputerName
    Specifies the name of the computer from which to retrieve the registry information.
 
    .PARAMETER Remote
    Indicates whether the registry key is located on a remote computer.
 
    .PARAMETER Advanced
    Indicates whether to retrieve advanced registry information.
 
    .PARAMETER ExpandEnvironmentNames
    Indicates whether to expand environment variable names in the registry.
 
    .EXAMPLE
    Get-PSSubRegistryComplete -Registry $Registry -ComputerName "Computer01" -Remote -Advanced
    Retrieves advanced sub-registry information from the specified registry key on a remote computer named "Computer01".
 
    .EXAMPLE
    Get-PSSubRegistryComplete -Registry $Registry -ComputerName "Computer02"
    Retrieves sub-registry information from the specified registry key on a local computer named "Computer02".
    #>

    [cmdletBinding()]
    param(
        [System.Collections.IDictionary] $Registry,
        [string] $ComputerName,
        [switch] $Remote,
        [switch] $Advanced,
        [switch] $ExpandEnvironmentNames
    )
    if ($Registry.ComputerName) {
        if ($Registry.ComputerName -ne $ComputerName) {
            return
        }
    }
    if (-not $Registry.Error) {
        try {
            if ($Remote) {
                $BaseHive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($Registry.HiveKey, $ComputerName, 0 )
            } else {
                $BaseHive = [Microsoft.Win32.RegistryKey]::OpenBaseKey($Registry.HiveKey, 0 )
            }
            $PSConnection = $true
            $PSError = $null
        } catch {
            $PSConnection = $false
            $PSError = $($_.Exception.Message)
        }
    } else {

        $PSConnection = $false
        $PSError = $($Registry.ErrorMessage)
    }
    if ($PSError) {
        [PSCustomObject] @{
            PSComputerName = $ComputerName
            PSConnection   = $PSConnection
            PSError        = $true
            PSErrorMessage = $PSError
            PSSubKeys      = $null
            PSPath         = $Registry.Registry
            PSKey          = $Registry.Key
        }
    } else {
        try {
            $SubKey = $BaseHive.OpenSubKey($Registry.SubKeyName, $false)
            if ($null -ne $SubKey) {
                $Object = [ordered] @{
                    PSComputerName = $ComputerName
                    PSConnection   = $PSConnection
                    PSError        = $false
                    PSErrorMessage = $null
                    PSSubKeys      = $SubKey.GetSubKeyNames()
                    PSPath         = $Registry.Registry
                }
                $Keys = $SubKey.GetValueNames()
                foreach ($K in $Keys) {
                    if ($K -eq "") {
                        if ($Advanced) {
                            $Object['DefaultKey'] = [ordered] @{
                                Value = if (-not $ExpandEnvironmentNames) {
                                    $SubKey.GetValue($K, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
                                } else {
                                    $SubKey.GetValue($K)
                                }
                                Type  = $SubKey.GetValueKind($K)
                            }
                        } else {
                            $Object['DefaultKey'] = $SubKey.GetValue($K)
                        }
                    } else {
                        if ($Advanced) {
                            $Object[$K] = [ordered] @{
                                Value = if (-not $ExpandEnvironmentNames) {
                                    $SubKey.GetValue($K, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
                                } else {
                                    $SubKey.GetValue($K)
                                }
                                Type  = $SubKey.GetValueKind($K)
                            }
                        } else {
                            $Object[$K] = if (-not $ExpandEnvironmentNames) {
                                $SubKey.GetValue($K, $null, [Microsoft.Win32.RegistryValueOptions]::DoNotExpandEnvironmentNames)
                            } else {
                                $SubKey.GetValue($K)
                            }
                        }
                    }
                }
                [PSCustomObject] $Object
            } else {
                [PSCustomObject] @{
                    PSComputerName = $ComputerName
                    PSConnection   = $PSConnection
                    PSError        = $true
                    PSErrorMessage = "Registry path $($Registry.Registry) doesn't exists."
                    PSSubKeys      = $null
                    PSPath         = $Registry.Registry
                }
            }
        } catch {
            [PSCustomObject] @{
                PSComputerName = $ComputerName
                PSConnection   = $PSConnection
                PSError        = $true
                PSErrorMessage = $_.Exception.Message
                PSSubKeys      = $null
                PSPath         = $Registry.Registry
            }
        }
    }
    if ($null -ne $SubKey) {
        $SubKey.Close()
        $SubKey.Dispose()
    }
    if ($null -ne $BaseHive) {
        $BaseHive.Close()
        $BaseHive.Dispose()
    }
}
function Get-PSSubRegistryTranslated { 
    <#
    .SYNOPSIS
    Retrieves the translated sub-registry information based on the provided RegistryPath, HiveDictionary, and Key.
 
    .DESCRIPTION
    This function retrieves the translated sub-registry information by matching the RegistryPath with the HiveDictionary. It returns an ordered hashtable with details such as Registry, HiveKey, SubKeyName, Key, Error, and ErrorMessage.
 
    .PARAMETER RegistryPath
    Specifies an array of registry paths to be translated.
 
    .PARAMETER HiveDictionary
    Specifies a dictionary containing mappings of hive names to their corresponding keys.
 
    .PARAMETER Key
    Specifies a string key to be included in the output.
 
    .EXAMPLE
    Get-PSSubRegistryTranslated -RegistryPath "HKLM\Software\Microsoft" -HiveDictionary @{ "HKLM" = "HKEY_LOCAL_MACHINE" } -Key "Version"
    Retrieves the translated sub-registry information for the specified registry path under HKEY_LOCAL_MACHINE hive with the key "Version".
 
    .EXAMPLE
    Get-PSSubRegistryTranslated -RegistryPath "HKCU\Software\Microsoft" -HiveDictionary @{ "HKCU" = "HKEY_CURRENT_USER" }
    Retrieves the translated sub-registry information for the specified registry path under HKEY_CURRENT_USER hive without specifying a key.
 
    #>

    [cmdletBinding()]
    param(
        [Array] $RegistryPath,
        [System.Collections.IDictionary] $HiveDictionary,
        [string] $Key
    )
    foreach ($Registry in $RegistryPath) {

        if ($Registry -is [string]) {
            $Registry = $Registry.Replace("\\", "\").Replace("\\", "\").TrimStart("\").TrimEnd("\")
            $FirstPartSplit = $Registry -split "\\"
            $FirstPart = $FirstPartSplit[0]
        } else {
            $Registry.RegistryPath = $Registry.RegistryPath.Replace("\\", "\").Replace("\\", "\").TrimStart("\").TrimEnd("\")
            $FirstPartSplit = $Registry.RegistryPath -split "\\"
            $FirstPart = $FirstPartSplit[0]
        }

        foreach ($Hive in $HiveDictionary.Keys) {
            if ($Registry -is [string] -and $FirstPart -eq $Hive) {

                if ($Hive.Length -eq $Registry.Length) {
                    [ordered] @{
                        Registry     = $Registry
                        HiveKey      = $HiveDictionary[$Hive]
                        SubKeyName   = $null
                        Key          = if ($Key -eq "") {
                            $null 
                        } else {
                            $Key 
                        }
                        Error        = $null
                        ErrorMessage = $null
                    }
                } else {
                    [ordered] @{
                        Registry     = $Registry
                        HiveKey      = $HiveDictionary[$Hive]
                        SubKeyName   = $Registry.substring($Hive.Length + 1)
                        Key          = if ($Key -eq "") {
                            $null 
                        } else {
                            $Key 
                        }
                        Error        = $null
                        ErrorMessage = $null
                    }
                }
                break
            } elseif ($Registry -isnot [string] -and $FirstPart -eq $Hive) {

                if ($Hive.Length -eq $Registry.RegistryPath.Length) {
                    [ordered] @{
                        ComputerName = $Registry.ComputerName
                        Registry     = $Registry.RegistryPath
                        HiveKey      = $HiveDictionary[$Hive]
                        SubKeyName   = $null
                        Key          = if ($Key -eq "") {
                            $null 
                        } else {
                            $Key 
                        }
                        Error        = $Registry.Error
                        ErrorMessage = $Registry.ErrorMessage
                    }
                } else {
                    [ordered] @{
                        ComputerName = $Registry.ComputerName
                        Registry     = $Registry.RegistryPath
                        HiveKey      = $HiveDictionary[$Hive]
                        SubKeyName   = $Registry.RegistryPath.substring($Hive.Length + 1)
                        Key          = if ($Key -eq "") {
                            $null 
                        } else {
                            $Key 
                        }
                        Error        = $Registry.Error
                        ErrorMessage = $Registry.ErrorMessage
                    }
                }
                break
            }
        }
    }
}
function Remove-PrivateRegistry { 
    <#
    .SYNOPSIS
    Removes a private registry key on a local or remote computer.
 
    .DESCRIPTION
    The Remove-PrivateRegistry function removes a registry key on a specified computer. It can be used to delete registry keys for a specific hive key, subkey, and key value.
 
    .PARAMETER Computer
    Specifies the name of the computer where the registry key will be removed.
 
    .PARAMETER Key
    Specifies the key value to be removed.
 
    .PARAMETER RegistryValue
    Specifies the registry key information to be removed. This should be an IDictionary object containing the hive key, subkey, and key value.
 
    .PARAMETER Remote
    Indicates whether the registry operation should be performed on a remote computer.
 
    .PARAMETER Suppress
    Suppresses the error message if set to true.
 
    .EXAMPLE
    Remove-PrivateRegistry -Computer 'Server01' -Key 'Version' -RegistryValue @{ HiveKey = 'LocalMachine'; SubKeyName = 'Software\MyApp' }
 
    Description:
    Removes the registry key 'Version' under 'LocalMachine\Software\MyApp' on the local computer 'Server01'.
 
    .EXAMPLE
    Remove-PrivateRegistry -Computer 'Workstation01' -Key 'Wallpaper' -RegistryValue @{ HiveKey = 'CurrentUser'; SubKeyName = 'Control Panel\Desktop' } -Remote
 
    Description:
    Removes the registry key 'Wallpaper' under 'CurrentUser\Control Panel\Desktop' on the remote computer 'Workstation01'.
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [string] $Computer,
        [string] $Key,
        [System.Collections.IDictionary] $RegistryValue,
        [switch] $Remote,
        [switch] $Suppress
    )
    $PSConnection = $null
    $PSError = $null
    $PSErrorMessage = $null
    try {
        if ($Remote) {
            $BaseHive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($RegistryValue.HiveKey, $Computer, 0 )
        } else {
            $BaseHive = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryValue.HiveKey, 0 )
        }
        $PSConnection = $true
        $PSError = $null
    } catch {
        $PSConnection = $false
        $PSError = $($_.Exception.Message)
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            if ($null -ne $BaseHive) {
                $BaseHive.Close()
                $BaseHive.Dispose()
            }
            throw
        } else {
            Write-Warning "Remove-PSRegistry - Removing registry $($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName) key $($RegistryValue.Key) on $Computer have failed. Error: $($_.Exception.Message.Replace([System.Environment]::NewLine, " "))"
        }
    }
    if ($PSError) {
        if (-not $Suppress) {
            [PSCustomObject] @{
                PSComputerName = $Computer
                PSConnection   = $PSConnection
                PSError        = $true
                PSErrorMessage = $PSError
                Path           = "$($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName)"
                Key            = $RegistryValue.Key
            }
        }
    } else {
        try {
            if ($Key) {
                $SubKey = $BaseHive.OpenSubKey($RegistryValue.SubKeyName, $true)
                if ($PSCmdlet.ShouldProcess($Computer, "Removing registry $($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName) key $($RegistryValue.Key)")) {
                    if ($SubKey) {
                        $SubKey.DeleteValue($RegistryValue.Key, $true)
                    }
                } else {
                    $PSError = $true
                    $PSErrorMessage = "WhatIf was used. No changes done."
                }
            } else {
                if ($PSCmdlet.ShouldProcess($Computer, "Removing registry $($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName) folder")) {
                    if ($BaseHive) {
                        if ($Recursive) {
                            $BaseHive.DeleteSubKeyTree($RegistryValue.SubKeyName, $true)
                        } else {
                            $BaseHive.DeleteSubKey($RegistryValue.SubKeyName, $true)
                        }
                    }
                } else {
                    $PSError = $true
                    $PSErrorMessage = "WhatIf was used. No changes done."
                }
            }
            if (-not $Suppress) {
                [PSCustomObject] @{
                    PSComputerName = $Computer
                    PSConnection   = $PSConnection
                    PSError        = $PSError
                    PSErrorMessage = $PSErrorMessage
                    Path           = "$($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName)"
                    Key            = $RegistryValue.Key
                }
            }
        } catch {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                if ($null -ne $SubKey) {
                    $SubKey.Close()
                    $SubKey.Dispose()
                }
                if ($null -ne $BaseHive) {
                    $BaseHive.Close()
                    $BaseHive.Dispose()
                }
                throw
            } else {
                Write-Warning "Remove-PSRegistry - Removing registry $($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName) key $($RegistryValue.Key) on $Computer have failed. Error: $($_.Exception.Message.Replace([System.Environment]::NewLine, " "))"
            }
            if (-not $Suppress) {
                [PSCustomObject] @{
                    PSComputerName = $Computer
                    PSConnection   = $PSConnection
                    PSError        = $true
                    PSErrorMessage = $_.Exception.Message
                    Path           = "$($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName)"
                    Key            = $RegistryValue.Key
                }
            }
        }
    }
    if ($null -ne $SubKey) {
        $SubKey.Close()
        $SubKey.Dispose()
    }
    if ($null -ne $BaseHive) {
        $BaseHive.Close()
        $BaseHive.Dispose()
    }
}
function Resolve-PrivateRegistry { 
    <#
    .SYNOPSIS
    Resolves and standardizes registry paths for consistency and compatibility.
 
    .DESCRIPTION
    The Resolve-PrivateRegistry function resolves and standardizes registry paths to ensure uniformity and compatibility across different systems. It cleans up the paths, converts short hive names to full names, and handles special cases like DEFAULT USER mappings.
 
    .PARAMETER RegistryPath
    Specifies an array of registry paths to be resolved and standardized.
 
    .EXAMPLE
    Resolve-PrivateRegistry -RegistryPath 'Users\.DEFAULT_USER\Software\MyApp'
    Resolves the registry path 'Users\.DEFAULT_USER\Software\MyApp' to 'HKUD\Software\MyApp' for consistent usage.
 
    .EXAMPLE
    Resolve-PrivateRegistry -RegistryPath 'HKCU\Software\MyApp'
    Resolves the registry path 'HKCU\Software\MyApp' to 'HKEY_CURRENT_USER\Software\MyApp' for compatibility with standard naming conventions.
 
    #>

    [CmdletBinding()]
    param(
        [alias('Path')][string[]] $RegistryPath
    )
    foreach ($R in $RegistryPath) {

        $R = $R.Replace("\\", "\").Replace("\\", "\")

        If ($R.StartsWith("Users\.DEFAULT_USER") -or $R.StartsWith('HKEY_USERS\.DEFAULT_USER')) {
            $R = $R.Replace("Users\.DEFAULT_USER", "HKUD")
            $R.Replace('HKEY_USERS\.DEFAULT_USER', "HKUD")
        } elseif ($R -like '*:*') {
            $Found = $false

            foreach ($DictionaryKey in $Script:Dictionary.Keys) {
                $SplitParts = $R.Split("\")
                $FirstPart = $SplitParts[0]
                if ($FirstPart -eq $DictionaryKey) {
                    $R -replace $DictionaryKey, $Script:Dictionary[$DictionaryKey]
                    $Found = $true
                    break
                }
            }

            if (-not $Found) {
                $R.Replace(":", "")
            }
        } else {

            $R
        }
    }
}
function Set-PrivateRegistry { 
    <#
    .SYNOPSIS
    Sets a registry value on a local or remote computer.
 
    .DESCRIPTION
    The Set-PrivateRegistry function sets a registry value on a specified computer. It can be used to create new registry keys and values, update existing ones, or delete them.
 
    .PARAMETER RegistryValue
    Specifies the registry value to be set. This parameter should be an IDictionary object containing the following properties:
        - HiveKey: The registry hive key (e.g., 'LocalMachine', 'CurrentUser').
        - SubKeyName: The subkey path where the value will be set.
        - Key: The name of the registry value.
        - Value: The data to be stored in the registry value.
        - ValueKind: The type of data being stored (e.g., String, DWord, MultiString).
 
    .PARAMETER Computer
    Specifies the name of the computer where the registry value will be set.
 
    .PARAMETER Remote
    Indicates that the registry value should be set on a remote computer.
 
    .PARAMETER Suppress
    Suppresses error messages and warnings.
 
    .EXAMPLE
    Set-PrivateRegistry -RegistryValue @{HiveKey='LocalMachine'; SubKeyName='SOFTWARE\MyApp'; Key='Version'; Value='1.0'; ValueKind='String'} -Computer 'Server01'
    Sets the registry value 'Version' under 'HKEY_LOCAL_MACHINE\SOFTWARE\MyApp' to '1.0' on the local computer 'Server01'.
 
    .EXAMPLE
    Set-PrivateRegistry -RegistryValue @{HiveKey='CurrentUser'; SubKeyName='Environment'; Key='Path'; Value='C:\MyApp'; ValueKind='String'} -Computer 'Server02' -Remote
    Sets the registry value 'Path' under 'HKEY_CURRENT_USER\Environment' to 'C:\MyApp' on the remote computer 'Server02'.
 
    .NOTES
    File Name : Set-PrivateRegistry.ps1
    Prerequisite : This function requires administrative privileges to modify the registry.
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [System.Collections.IDictionary] $RegistryValue,
        [string] $Computer,
        [switch] $Remote,
        [switch] $Suppress
    )
    Write-Verbose -Message "Set-PSRegistry - Setting registry $($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName) on $($RegistryValue.Key) to $($RegistryValue.Value) of $($RegistryValue.ValueKind) on $Computer"
    if ($RegistryValue.ComputerName) {
        if ($RegistryValue.ComputerName -ne $Computer) {
            return
        }
    }
    try {
        if ($Remote) {
            $BaseHive = [Microsoft.Win32.RegistryKey]::OpenRemoteBaseKey($RegistryValue.HiveKey, $Computer, 0 )
        } else {
            $BaseHive = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryValue.HiveKey, 0 )
        }
        $PSConnection = $true
        $PSError = $null
    } catch {
        $PSConnection = $false
        $PSError = $($_.Exception.Message)
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            if ($null -ne $BaseHive) {
                $BaseHive.Close()
                $BaseHive.Dispose()
            }
            throw
        } else {
            Write-Warning "Set-PSRegistry - Setting registry $($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName) on $($RegistryValue.Key) to $($RegistryValue.Value) of $($RegistryValue.ValueKind) on $Computer have failed. Error: $($_.Exception.Message.Replace([System.Environment]::NewLine, " "))"
        }
    }
    if ($PSCmdlet.ShouldProcess($Computer, "Setting registry $($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName) on $($RegistryValue.Key) to $($RegistryValue.Value) of $($RegistryValue.ValueKind)")) {
        if ($PSError) {
            if (-not $Suppress) {
                [PSCustomObject] @{
                    PSComputerName = $Computer
                    PSConnection   = $PSConnection
                    PSError        = $true
                    PSErrorMessage = $PSError
                    Path           = "$($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName)"
                    Key            = $RegistryValue.Key
                    Value          = $RegistryValue.Value
                    Type           = $RegistryValue.ValueKind
                }
            }
        } else {
            try {

                $SubKey = $BaseHive.OpenSubKey($RegistryValue.SubKeyName, $true)
                if (-not $SubKey) {
                    $SubKeysSplit = $RegistryValue.SubKeyName.Split('\')
                    $SubKey = $BaseHive.OpenSubKey($SubKeysSplit[0], $true)
                    if (-not $SubKey) {
                        $SubKey = $BaseHive.CreateSubKey($SubKeysSplit[0])
                    }
                    $SubKey = $BaseHive.OpenSubKey($SubKeysSplit[0], $true)
                    foreach ($S in $SubKeysSplit | Select-Object -Skip 1) {
                        $SubKey = $SubKey.CreateSubKey($S)
                    }
                }
                if ($RegistryValue.ValueKind -eq [Microsoft.Win32.RegistryValueKind]::MultiString) {
                    $SubKey.SetValue($RegistryValue.Key, [string[]] $RegistryValue.Value, $RegistryValue.ValueKind)
                } elseif ($RegistryValue.ValueKind -in [Microsoft.Win32.RegistryValueKind]::None, [Microsoft.Win32.RegistryValueKind]::Binary) {
                    $SubKey.SetValue($RegistryValue.Key, [byte[]] $RegistryValue.Value, $RegistryValue.ValueKind)
                } else {
                    $SubKey.SetValue($RegistryValue.Key, $RegistryValue.Value, $RegistryValue.ValueKind)
                }
                if (-not $Suppress) {
                    [PSCustomObject] @{
                        PSComputerName = $Computer
                        PSConnection   = $PSConnection
                        PSError        = $false
                        PSErrorMessage = $null
                        Path           = "$($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName)"
                        Key            = $RegistryValue.Key
                        Value          = $RegistryValue.Value
                        Type           = $RegistryValue.ValueKind
                    }
                }
            } catch {
                if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                    if ($null -ne $SubKey) {
                        $SubKey.Close()
                        $SubKey.Dispose()
                    }
                    if ($null -ne $BaseHive) {
                        $BaseHive.Close()
                        $BaseHive.Dispose()
                    }
                    throw
                } else {
                    Write-Warning "Set-PSRegistry - Setting registry $($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName) on $($RegistryValue.Key) to $($RegistryValue.Value) of $($RegistryValue.ValueKind) on $Computer have failed. Error: $($_.Exception.Message.Replace([System.Environment]::NewLine, " "))"
                }
                if (-not $Suppress) {
                    [PSCustomObject] @{
                        PSComputerName = $Computer
                        PSConnection   = $PSConnection
                        PSError        = $true
                        PSErrorMessage = $_.Exception.Message
                        Path           = "$($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName)"
                        Key            = $RegistryValue.Key
                        Value          = $RegistryValue.Value
                        Type           = $RegistryValue.ValueKind
                    }
                }
            }
        }
    } else {
        if (-not $Suppress) {
            [PSCustomObject] @{
                PSComputerName = $Computer
                PSConnection   = $PSConnection
                PSError        = $true
                PSErrorMessage = if ($PSError) {
                    $PSError 
                } else {
                    "WhatIf used - skipping registry setting" 
                }
                Path           = "$($RegistryValue.HiveKey)\$($RegistryValue.SubKeyName)"
                Key            = $RegistryValue.Key
                Value          = $RegistryValue.Value
                Type           = $RegistryValue.ValueKind
            }
        }
    }
    if ($null -ne $SubKey) {
        $SubKey.Close()
        $SubKey.Dispose()
    }
    if ($null -ne $BaseHive) {
        $BaseHive.Close()
        $BaseHive.Dispose()
    }
}
function Test-ComputerPort { 
    <#
    .SYNOPSIS
    Tests the connectivity of a computer on specified TCP and UDP ports.
 
    .DESCRIPTION
    The Test-ComputerPort function tests the connectivity of a computer on specified TCP and UDP ports. It checks if the specified ports are open and reachable on the target computer.
 
    .PARAMETER ComputerName
    Specifies the name of the computer to test the port connectivity.
 
    .PARAMETER PortTCP
    Specifies an array of TCP ports to test connectivity.
 
    .PARAMETER PortUDP
    Specifies an array of UDP ports to test connectivity.
 
    .PARAMETER Timeout
    Specifies the timeout value in milliseconds for the connection test. Default is 5000 milliseconds.
 
    .EXAMPLE
    Test-ComputerPort -ComputerName "Server01" -PortTCP 80,443 -PortUDP 53 -Timeout 3000
    Tests the connectivity of Server01 on TCP ports 80 and 443, UDP port 53 with a timeout of 3000 milliseconds.
 
    .EXAMPLE
    Test-ComputerPort -ComputerName "Server02" -PortTCP 3389 -PortUDP 123
    Tests the connectivity of Server02 on TCP port 3389, UDP port 123 with the default timeout of 5000 milliseconds.
    #>

    [CmdletBinding()]
    param (
        [alias('Server')][string[]] $ComputerName,
        [int[]] $PortTCP,
        [int[]] $PortUDP,
        [int]$Timeout = 5000
    )
    begin {
        if ($Global:ProgressPreference -ne 'SilentlyContinue') {
            $TemporaryProgress = $Global:ProgressPreference
            $Global:ProgressPreference = 'SilentlyContinue'
        }
    }
    process {
        foreach ($Computer in $ComputerName) {
            foreach ($P in $PortTCP) {
                $Output = [ordered] @{
                    'ComputerName' = $Computer
                    'Port'         = $P
                    'Protocol'     = 'TCP'
                    'Status'       = $null
                    'Summary'      = $null
                    'Response'     = $null
                }

                $TcpClient = Test-NetConnection -ComputerName $Computer -Port $P -InformationLevel Detailed -WarningAction SilentlyContinue
                if ($TcpClient.TcpTestSucceeded) {
                    $Output['Status'] = $TcpClient.TcpTestSucceeded
                    $Output['Summary'] = "TCP $P Successful"
                } else {
                    $Output['Status'] = $false
                    $Output['Summary'] = "TCP $P Failed"
                    $Output['Response'] = $Warnings
                }
                [PSCustomObject]$Output
            }
            foreach ($P in $PortUDP) {
                $Output = [ordered] @{
                    'ComputerName' = $Computer
                    'Port'         = $P
                    'Protocol'     = 'UDP'
                    'Status'       = $null
                    'Summary'      = $null
                }
                $UdpClient = [System.Net.Sockets.UdpClient]::new($Computer, $P)
                $UdpClient.Client.ReceiveTimeout = $Timeout

                $Encoding = [System.Text.ASCIIEncoding]::new()
                $byte = $Encoding.GetBytes("Evotec")
                [void]$UdpClient.Send($byte, $byte.length)
                $RemoteEndpoint = [System.Net.IPEndPoint]::new([System.Net.IPAddress]::Any, 0)
                try {
                    $Bytes = $UdpClient.Receive([ref]$RemoteEndpoint)
                    [string]$Data = $Encoding.GetString($Bytes)
                    If ($Data) {
                        $Output['Status'] = $true
                        $Output['Summary'] = "UDP $P Successful"
                        $Output['Response'] = $Data
                    }
                } catch {
                    $Output['Status'] = $false
                    $Output['Summary'] = "UDP $P Failed"
                    $Output['Response'] = $_.Exception.Message
                }
                $UdpClient.Close()
                $UdpClient.Dispose()
                [PSCustomObject]$Output
            }
        }
    }
    end {

        if ($TemporaryProgress) {
            $Global:ProgressPreference = $TemporaryProgress
        }
    }
}
function Test-IPIsInNetwork { 
    <#
    .SYNOPSIS
    Checks if an IP address falls within a specified range defined by binary start and end values.
 
    .DESCRIPTION
    This function compares the binary representation of an IP address with the binary start and end values to determine if the IP address falls within the specified range.
 
    .EXAMPLE
    Test-IPIsInNetwork -IP "192.168.1.10" -StartBinary "11000000101010000000000100000000" -EndBinary "11000000101010000000000111111111"
 
    Description:
    Checks if the IP address 192.168.1.10 falls within the range defined by the binary start and end values.
 
    #>

    [cmdletBinding()]
    param(
        [string] $IP,
        [string] $StartBinary,
        [string] $EndBinary
    )
    $TestIPBinary = Convert-IPToBinary $IP
    [int64] $TestIPInt64 = [System.Convert]::ToInt64($TestIPBinary, 2)
    [int64] $StartInt64 = [System.Convert]::ToInt64($StartBinary, 2)
    [int64] $EndInt64 = [System.Convert]::ToInt64($EndBinary, 2)
    if ($TestIPInt64 -ge $StartInt64 -and $TestIPInt64 -le $EndInt64) {
        return $True
    } else {
        return $False
    }
}
function Test-WinRM { 
    <#
    .SYNOPSIS
    Tests the WinRM connectivity on the specified computers.
 
    .DESCRIPTION
    The Test-WinRM function tests the WinRM connectivity on the specified computers and returns the status of the connection.
 
    .PARAMETER ComputerName
    Specifies the names of the computers to test WinRM connectivity on.
 
    .EXAMPLE
    Test-WinRM -ComputerName "Server01", "Server02"
    Tests the WinRM connectivity on Server01 and Server02.
 
    .EXAMPLE
    Test-WinRM -ComputerName "Server03"
    Tests the WinRM connectivity on Server03.
 
    #>

    [CmdletBinding()]
    param (
        [alias('Server')][string[]] $ComputerName
    )
    $Output = foreach ($Computer in $ComputerName) {
        $Test = [PSCustomObject] @{
            Output       = $null
            Status       = $null
            ComputerName = $Computer
        }
        try {
            $Test.Output = Test-WSMan -ComputerName $Computer -ErrorAction Stop
            $Test.Status = $true
        } catch {
            $Test.Status = $false
        }
        $Test
    }
    $Output
}
function Unregister-MountedRegistry { 
    <#
    .SYNOPSIS
    Unregisters mounted registry paths.
 
    .DESCRIPTION
    This function unregisters mounted registry paths that were previously mounted using Mount-PSRegistryPath.
 
    .EXAMPLE
    Unregister-MountedRegistry
 
    Description:
    Unregisters all mounted registry paths.
 
    #>

    [CmdletBinding()]
    param(

    )
    if ($null -ne $Script:DefaultRegistryMounted) {
        Write-Verbose -Message "Unregister-MountedRegistry - Dismounting HKEY_USERS\.DEFAULT_USER"
        $null = Dismount-PSRegistryPath -MountPoint "HKEY_USERS\.DEFAULT_USER"
        $Script:DefaultRegistryMounted = $null
    }
    if ($null -ne $Script:OfflineRegistryMounted) {
        foreach ($Key in $Script:OfflineRegistryMounted.Keys) {
            if ($Script:OfflineRegistryMounted[$Key].Status -eq $true) {
                Write-Verbose -Message "Unregister-MountedRegistry - Dismounting HKEY_USERS\$Key"
                $null = Dismount-PSRegistryPath -MountPoint "HKEY_USERS\$Key"
            }
        }
        $Script:OfflineRegistryMounted = $null
    }
}
function ConvertTo-HkeyUser { 
    <#
    .SYNOPSIS
    Converts registry paths based on specified criteria.
 
    .DESCRIPTION
    This function converts registry paths based on the provided HiveDictionary, SubKeys, DictionaryKey, and RegistryPath parameters.
 
    .PARAMETER HiveDictionary
    Specifies the dictionary containing the criteria for converting registry paths.
 
    .PARAMETER SubKeys
    Specifies an array of subkeys to process.
 
    .PARAMETER DictionaryKey
    Specifies the key in the RegistryPath to be replaced.
 
    .PARAMETER RegistryPath
    Specifies the original registry path to be converted.
 
    .EXAMPLE
    ConvertTo-HkeyUser -HiveDictionary @{ 'Key1' = 'AllDomain'; 'Key2' = 'All+Default' } -SubKeys @('S-1-5-21-123456789-123456789-123456789-1001', '.DEFAULT') -DictionaryKey 'Key1' -RegistryPath 'HKLM:\Software\Key1\SubKey'
 
    Description:
    Converts the RegistryPath based on the specified criteria in the HiveDictionary for the provided SubKeys.
 
    .EXAMPLE
    ConvertTo-HkeyUser -HiveDictionary @{ 'Key1' = 'Users'; 'Key2' = 'AllDomain+Other' } -SubKeys @('S-1-5-21-123456789-123456789-123456789-1001', 'Offline_User1') -DictionaryKey 'Key2' -RegistryPath 'HKLM:\Software\Key2\SubKey'
 
    Description:
    Converts the RegistryPath based on the specified criteria in the HiveDictionary for the provided SubKeys.
 
    #>

    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $HiveDictionary,
        [Array] $SubKeys,
        [string] $DictionaryKey,
        [string] $RegistryPath
    )
    $OutputRegistryKeys = foreach ($Sub in $Subkeys) {
        if ($HiveDictionary[$DictionaryKey] -eq 'All') {
            if ($Sub -notlike "*_Classes*" -and $Sub -ne '.DEFAULT') {
                $RegistryPath.Replace($DictionaryKey, "Users\$Sub")
            }
        } elseif ($HiveDictionary[$DictionaryKey] -eq 'All+Default') {
            if ($Sub -notlike "*_Classes*") {
                if (-not $Script:DefaultRegistryMounted) {
                    $Script:DefaultRegistryMounted = Mount-DefaultRegistryPath
                }
                if ($Sub -eq '.DEFAULT') {
                    $RegistryPath.Replace($DictionaryKey, "Users\.DEFAULT_USER")
                } else {
                    $RegistryPath.Replace($DictionaryKey, "Users\$Sub")
                }
            }
        } elseif ($HiveDictionary[$DictionaryKey] -eq 'Default') {
            if ($Sub -eq '.DEFAULT') {
                if (-not $Script:DefaultRegistryMounted) {
                    $Script:DefaultRegistryMounted = Mount-DefaultRegistryPath
                }
                $RegistryPath.Replace($DictionaryKey, "Users\.DEFAULT_USER")
            }
        } elseif ($HiveDictionary[$DictionaryKey] -eq 'AllDomain+Default') {
            if (($Sub.StartsWith("S-1-5-21") -and $Sub -notlike "*_Classes*") -or $Sub -eq '.DEFAULT') {
                if (-not $Script:DefaultRegistryMounted) {
                    $Script:DefaultRegistryMounted = Mount-DefaultRegistryPath
                }
                if ($Sub -eq '.DEFAULT') {
                    $RegistryPath.Replace($DictionaryKey, "Users\.DEFAULT_USER")
                } else {
                    $RegistryPath.Replace($DictionaryKey, "Users\$Sub")
                }
            }
        } elseif ($HiveDictionary[$DictionaryKey] -eq 'AllDomain+Other') {
            if (($Sub.StartsWith("S-1-5-21") -and $Sub -notlike "*_Classes*")) {
                if (-not $Script:OfflineRegistryMounted) {
                    $Script:OfflineRegistryMounted = Mount-AllRegistryPath
                    foreach ($Key in $Script:OfflineRegistryMounted.Keys) {
                        $RegistryPath.Replace($DictionaryKey, "Users\$Key")
                    }
                }
                $RegistryPath.Replace($DictionaryKey, "Users\$Sub")
            }
        } elseif ($HiveDictionary[$DictionaryKey] -eq 'AllDomain+Other+Default') {
            if (($Sub.StartsWith("S-1-5-21") -and $Sub -notlike "*_Classes*") -or $Sub -eq '.DEFAULT') {
                if (-not $Script:DefaultRegistryMounted) {
                    $Script:DefaultRegistryMounted = Mount-DefaultRegistryPath
                }
                if (-not $Script:OfflineRegistryMounted) {
                    $Script:OfflineRegistryMounted = Mount-AllRegistryPath
                    foreach ($Key in $Script:OfflineRegistryMounted.Keys) {
                        $RegistryPath.Replace($DictionaryKey, "Users\$Key")
                    }
                }
                if ($Sub -eq '.DEFAULT') {
                    $RegistryPath.Replace($DictionaryKey, "Users\.DEFAULT_USER")
                } else {
                    $RegistryPath.Replace($DictionaryKey, "Users\$Sub")
                }
            }
        } elseif ($HiveDictionary[$DictionaryKey] -eq 'AllDomain') {
            if ($Sub.StartsWith("S-1-5-21") -and $Sub -notlike "*_Classes*") {
                $RegistryPath.Replace($DictionaryKey, "Users\$Sub")
            }
        } elseif ($HiveDictionary[$DictionaryKey] -eq 'Users') {
            if ($Sub -like "Offline_*") {
                $Script:OfflineRegistryMounted = Mount-AllRegistryPath -MountUsers $Sub
                foreach ($Key in $Script:OfflineRegistryMounted.Keys) {
                    if ($Script:OfflineRegistryMounted[$Key].Status -eq $true) {
                        $RegistryPath
                    }
                }
            }
        }
    }
    $OutputRegistryKeys | Sort-Object -Unique
}
function Dismount-PSRegistryPath { 
    <#
    .SYNOPSIS
    Dismounts a registry path.
 
    .DESCRIPTION
    This function dismounts a registry path specified by the MountPoint parameter. It unloads the registry path using reg.exe command.
 
    .PARAMETER MountPoint
    Specifies the registry path to be dismounted.
 
    .PARAMETER Suppress
    Suppresses the output if set to $true.
 
    .EXAMPLE
    Dismount-PSRegistryPath -MountPoint "HKLM:\Software\MyApp" -Suppress
    Dismounts the registry path "HKLM:\Software\MyApp" without displaying any output.
 
    .EXAMPLE
    Dismount-PSRegistryPath -MountPoint "HKCU:\Software\Settings"
    Dismounts the registry path "HKCU:\Software\Settings" and displays output if successful.
 
    #>

    [alias('Dismount-RegistryPath')]
    [cmdletbinding()]
    param(
        [Parameter(Mandatory)][string] $MountPoint,
        [switch] $Suppress
    )

    [gc]::Collect()

    $pinfo = [System.Diagnostics.ProcessStartInfo]::new()
    $pinfo.FileName = "reg.exe"
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = " unload $MountPoint"
    $pinfo.CreateNoWindow = $true
    $pinfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
    $p = [System.Diagnostics.Process]::new()
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    $p.WaitForExit()
    $Output = $p.StandardOutput.ReadToEnd()
    $Errors = $p.StandardError.ReadToEnd()

    if ($Errors) {
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            throw $Errors
        } else {
            Write-Warning -Message "Dismount-PSRegistryPath - Couldn't unmount $MountPoint. $Errors"
        }
    } else {
        if ($Output -like "*operation completed*") {
            if (-not $Suppress) {
                return $true
            }
        }
    }
    if (-not $Suppress) {
        return $false
    }
}
function Mount-AllRegistryPath { 
    <#
    .SYNOPSIS
    Mounts offline registry paths to specified mount points.
 
    .DESCRIPTION
    This function mounts offline registry paths to specified mount points. It iterates through all offline registry profiles and mounts them to the specified mount point. Optionally, you can specify a specific user profile to mount.
 
    .PARAMETER MountPoint
    Specifies the mount point where the registry paths will be mounted. Default is "HKEY_USERS\".
 
    .PARAMETER MountUsers
    Specifies the user profile to mount. If specified, only the specified user profile will be mounted.
 
    .EXAMPLE
    Mount-AllRegistryPath -MountPoint "HKEY_USERS\" -MountUsers "User1"
    Mounts the offline registry path of user profile "User1" to the default mount point "HKEY_USERS\".
 
    .EXAMPLE
    Mount-AllRegistryPath -MountPoint "HKEY_LOCAL_MACHINE\SOFTWARE" -MountUsers "User2"
    Mounts the offline registry path of user profile "User2" to the specified mount point "HKEY_LOCAL_MACHINE\SOFTWARE".
 
    #>

    [CmdletBinding()]
    param(
        [string] $MountPoint = "HKEY_USERS\",
        [string] $MountUsers
    )
    $AllProfiles = Get-OfflineRegistryProfilesPath
    foreach ($Profile in $AllProfiles.Keys) {
        if ($MountUsers) {
            if ($MountUsers -ne $Profile) {
                continue
            }
        }
        $WhereMount = "$MountPoint\$Profile".Replace("\\", "\")
        Write-Verbose -Message "Mount-OfflineRegistryPath - Mounting $WhereMount to $($AllProfiles[$Profile].FilePath)"
        $AllProfiles[$Profile].Status = Mount-PSRegistryPath -MountPoint $WhereMount -FilePath $AllProfiles[$Profile].FilePath
    }
    $AllProfiles
}
function Mount-DefaultRegistryPath { 
    <#
    .SYNOPSIS
    Mounts the default registry path to a specified mount point.
 
    .DESCRIPTION
    This function mounts the default registry path to a specified mount point. If an error occurs during the process, it provides appropriate feedback.
 
    .PARAMETER MountPoint
    Specifies the mount point where the default registry path will be mounted. Default value is "HKEY_USERS\.DEFAULT_USER".
 
    .EXAMPLE
    Mount-DefaultRegistryPath -MountPoint "HKLM:\Software\CustomMountPoint"
    Mounts the default registry path to the specified custom mount point "HKLM:\Software\CustomMountPoint".
 
    .EXAMPLE
    Mount-DefaultRegistryPath
    Mounts the default registry path to the default mount point "HKEY_USERS\.DEFAULT_USER".
 
    #>

    [CmdletBinding()]
    param(
        [string] $MountPoint = "HKEY_USERS\.DEFAULT_USER"
    )
    $DefaultRegistryPath = Get-PSRegistry -RegistryPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -Key 'Default' -ExpandEnvironmentNames -DoNotUnmount
    if ($PSError -ne $true) {
        $PathToNTUser = [io.path]::Combine($DefaultRegistryPath.PSValue, 'NTUSER.DAT')
        Write-Verbose -Message "Mount-DefaultRegistryPath - Mounting $MountPoint to $PathToNTUser"
        Mount-PSRegistryPath -MountPoint $MountPoint -FilePath $PathToNTUser
    } else {
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            throw $PSErrorMessage
        } else {
            Write-Warning -Message "Mount-DefaultRegistryPath - Couldn't execute. Error: $PSErrorMessage"
        }
    }
}
function Get-OfflineRegistryProfilesPath { 
    <#
    .SYNOPSIS
    Retrieves the paths of offline user profiles in the Windows registry.
 
    .DESCRIPTION
    This function retrieves the paths of offline user profiles in the Windows registry by comparing the profiles listed in 'HKEY_USERS' with those in 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList'. It then checks for the existence of the 'NTUSER.DAT' file for each profile and returns the paths of offline profiles found.
 
    .EXAMPLE
    Get-OfflineRegistryProfilesPath
    Retrieves the paths of offline user profiles in the Windows registry and returns a hashtable containing the profile paths.
 
    .NOTES
    Name Value
    ---- -----
    Przemek {[FilePath, C:\Users\Przemek\NTUSER.DAT], [Status, ]}
    test.1 {[FilePath, C:\Users\test.1\NTUSER.DAT], [Status, ]}
 
    #>

    [CmdletBinding()]
    param(

    )
    $Profiles = [ordered] @{}
    $CurrentMapping = (Get-PSRegistry -RegistryPath 'HKEY_USERS' -ExpandEnvironmentNames -DoNotUnmount).PSSubKeys
    $UsersInSystem = (Get-PSRegistry -RegistryPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList' -ExpandEnvironmentNames -DoNotUnmount).PSSubKeys
    $MissingProfiles = foreach ($Profile in $UsersInSystem) {
        if ($Profile.StartsWith("S-1-5-21") -and $CurrentMapping -notcontains $Profile) {
            Get-PSRegistry -RegistryPath "HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\ProfileList\$Profile" -ExpandEnvironmentNames -DoNotUnmount
        }
    }
    foreach ($Profile in $MissingProfiles) {
        $PathToNTUser = [io.path]::Combine($Profile.ProfileImagePath, 'NTUSER.DAT')
        $ProfileName = [io.path]::GetFileName($Profile.ProfileImagePath)
        $StartPath = "Offline_$ProfileName"
        try {
            $PathExists = Test-Path -LiteralPath $PathToNTUser -ErrorAction Stop
            if ($PathExists) {
                $Profiles[$StartPath] = [ordered] @{
                    FilePath = $PathToNTUser
                    Status   = $null
                }
            }
        } catch {
            Write-Warning -Message "Mount-OfflineRegistryPath - Couldn't execute. Error: $($_.Exception.Message)"
            continue
        }
    }
    $Profiles
}
function Mount-PSRegistryPath { 
    <#
    .SYNOPSIS
    Mounts a registry path to a specified location.
 
    .DESCRIPTION
    This function mounts a registry path to a specified location using the reg.exe utility.
 
    .PARAMETER MountPoint
    Specifies the registry mount point where the registry path will be mounted.
 
    .PARAMETER FilePath
    Specifies the file path of the registry hive to be mounted.
 
    .EXAMPLE
    Mount-PSRegistryPath -MountPoint 'HKEY_USERS\.DEFAULT_USER111' -FilePath 'C:\Users\Default\NTUSER.DAT'
    Mounts the registry hive located at 'C:\Users\Default\NTUSER.DAT' to the registry key 'HKEY_USERS\.DEFAULT_USER111'.
 
    .NOTES
    This function requires administrative privileges to mount registry paths.
    #>

    [alias('Mount-RegistryPath')]
    [cmdletbinding()]
    param(
        [Parameter(Mandatory)][string] $MountPoint,
        [Parameter(Mandatory)][string] $FilePath
    )

    $pinfo = [System.Diagnostics.ProcessStartInfo]::new()
    $pinfo.FileName = "reg.exe"
    $pinfo.RedirectStandardError = $true
    $pinfo.RedirectStandardOutput = $true
    $pinfo.UseShellExecute = $false
    $pinfo.Arguments = " load $MountPoint $FilePath"
    $pinfo.CreateNoWindow = $true
    $pinfo.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
    $p = [System.Diagnostics.Process]::new()
    $p.StartInfo = $pinfo
    $p.Start() | Out-Null
    $p.WaitForExit()
    $Output = $p.StandardOutput.ReadToEnd()
    $Errors = $p.StandardError.ReadToEnd()
    if ($Errors) {
        if ($PSBoundParameters.ErrorAction -eq 'Stop') {
            throw $Errors
        } else {
            Write-Warning -Message "Mount-PSRegistryPath - Couldn't mount $MountPoint. $Errors"
        }
    } else {
        if ($Output -like "*operation completed*") {
            if (-not $Suppress) {
                return $true
            }
        }
    }
    if (-not $Suppress) {
        return $false
    }
}
function Add-ACLRule {
    <#
    .SYNOPSIS
    Adds an access control rule to a security descriptor.
 
    .DESCRIPTION
    The Add-ACLRule function adds an access control rule to a security descriptor. It allows specifying the access rule to add, the security descriptor, and the ACL.
 
    .PARAMETER AccessRuleToAdd
    Specifies the access rule to add.
 
    .PARAMETER ntSecurityDescriptor
    Specifies the security descriptor to which the access rule will be added.
 
    .PARAMETER ACL
    Specifies the ACL to which the access rule will be added.
 
    .EXAMPLE
    Add-ACLRule -AccessRuleToAdd $rule -ntSecurityDescriptor $securityDescriptor -ACL $acl
 
    This example adds the access rule $rule to the security descriptor $securityDescriptor and the ACL $acl.
 
    .NOTES
    This function is designed to handle errors related to identity references that could not be translated.
 
    #>

    [CmdletBinding()]
    param(
        $AccessRuleToAdd,
        $ntSecurityDescriptor,
        $ACL
    )
    try {
        Write-Verbose "Add-ADACL - Adding access for $($AccessRuleToAdd.IdentityReference) / $($AccessRuleToAdd.ActiveDirectoryRights) / $($AccessRuleToAdd.AccessControlType) / $($AccessRuleToAdd.ObjectType) / $($AccessRuleToAdd.InheritanceType) to $($ACL.DistinguishedName)"
        if ($ACL.ACL) {
            $ntSecurityDescriptor = $ACL.ACL
        } elseif ($ntSecurityDescriptor) {
        } else {
            Write-Warning "Add-PrivateACL - No ACL or ntSecurityDescriptor specified"
            return
        }
        $ntSecurityDescriptor.AddAccessRule($AccessRuleToAdd)
        @{ Success = $true; Reason = $null }
    } catch {
        if ($_.Exception.Message -like "*Some or all identity references could not be translated.*") {
            Write-Warning "Add-ADACL - Error adding permissions for $($AccessRuleToAdd.IdentityReference) / $($AccessRuleToAdd.ActiveDirectoryRights) due to error: $($_.Exception.Message). Retrying with SID"
            @{ Success = $false; Reason = "Identity" }
        } else {
            Write-Warning "Add-ADACL - Error adding permissions for $($AccessRuleToAdd.IdentityReference) / $($AccessRuleToAdd.ActiveDirectoryRights) due to error: $($_.Exception.Message)"
            @{ Success = $false; Reason = $($_.Exception.Message) }
        }
    }
}
function Add-PrivateACL {
    <#
    .SYNOPSIS
    Adds a new access control rule to a security descriptor.
 
    .DESCRIPTION
    This function adds a new access control rule to a security descriptor. It allows specifying various parameters such as the ACL, principal, access rule, access control type, object type name, inherited object type name, inheritance type, and NT security descriptor.
 
    .PARAMETER ACL
    Specifies the ACL object to be processed.
 
    .PARAMETER ADObject
    Specifies the Active Directory object to which the ACL belongs.
 
    .PARAMETER Principal
    Specifies the principal for which the access control rule is added.
 
    .PARAMETER AccessRule
    Specifies the access rule to be added.
 
    .PARAMETER AccessControlType
    Specifies the type of access control to be added.
 
    .PARAMETER ObjectType
    Specifies the object type name.
 
    .PARAMETER InheritedObjectType
    Specifies the inherited object type name.
 
    .PARAMETER InheritanceType
    Specifies the inheritance type to consider.
 
    .PARAMETER NTSecurityDescriptor
    Specifies the NT security descriptor to be updated.
 
    .PARAMETER ActiveDirectoryAccessRule
    Specifies the Active Directory access rule to be added.
 
    .EXAMPLE
    Add-PrivateACL -ACL $ACLObject -ADObject "CN=Example,DC=Domain,DC=com" -Principal "User1" -AccessRule "Read" -AccessControlType "Allow" -ObjectType "File" -InheritedObjectType "Folder" -InheritanceType All -NTSecurityDescriptor $SecurityDescriptor -ActiveDirectoryAccessRule $ADAccessRule
 
    Adds a new access control rule for User1 with Read access on files within folders with inheritance for all objects.
 
    .NOTES
    Author: Your Name
    Date: Date
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [PSCustomObject] $ACL,
        [string] $ADObject,
        [string] $Principal,
        [alias('ActiveDirectoryRights')][System.DirectoryServices.ActiveDirectoryRights] $AccessRule,
        [System.Security.AccessControl.AccessControlType] $AccessControlType,
        [alias('ObjectTypeName')][string] $ObjectType,
        [alias('InheritedObjectTypeName')][string] $InheritedObjectType,
        [alias('ActiveDirectorySecurityInheritance')][nullable[System.DirectoryServices.ActiveDirectorySecurityInheritance]] $InheritanceType,
        [alias('ActiveDirectorySecurity')][System.DirectoryServices.ActiveDirectorySecurity] $NTSecurityDescriptor,
        [System.DirectoryServices.ActiveDirectoryAccessRule] $ActiveDirectoryAccessRule
    )
    if ($ACL) {
        $ADObject = $ACL.DistinguishedName
    } else {
        if (-not $ADObject) {
            Write-Warning "Add-PrivateACL - No ACL or ADObject specified"
            return
        }
    }

    $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $ADObject
    if (-not $DomainName) {
        Write-Warning -Message "Add-PrivateACL - Unable to determine domain name for $($ADObject)"
        return
    }
    $QueryServer = $Script:ForestDetails['QueryServers'][$DomainName].HostName[0]

    if (-not $ActiveDirectoryAccessRule) {
        if ($Principal -like '*/*') {
            $SplittedName = $Principal -split '/'
            [System.Security.Principal.IdentityReference] $Identity = [System.Security.Principal.NTAccount]::new($SplittedName[0], $SplittedName[1])
        } else {
            [System.Security.Principal.IdentityReference] $Identity = [System.Security.Principal.NTAccount]::new($Principal)
        }
    }

    $OutputRequiresCommit = @(
        $newActiveDirectoryAccessRuleSplat = @{
            Identity                  = $Identity
            ActiveDirectoryAccessRule = $ActiveDirectoryAccessRule
            ObjectType                = $ObjectType
            InheritanceType           = $InheritanceType
            InheritedObjectType       = $InheritedObjectType
            AccessControlType         = $AccessControlType
            AccessRule                = $AccessRule
        }
        Remove-EmptyValue -Hashtable $newActiveDirectoryAccessRuleSplat
        $AccessRuleToAdd = New-ActiveDirectoryAccessRule @newActiveDirectoryAccessRuleSplat
        if ($AccessRuleToAdd) {
            $RuleAdded = Add-ACLRule -AccessRuleToAdd $AccessRuleToAdd -ntSecurityDescriptor $NTSecurityDescriptor -ACL $ACL
            if (-not $RuleAdded.Success -and $RuleAdded.Reason -eq 'Identity') {
                # rule failed to add, so we need to convert the identity and try with SID
                $AlternativeSID = (Convert-Identity -Identity $Identity).SID
                [System.Security.Principal.IdentityReference] $Identity = [System.Security.Principal.SecurityIdentifier]::new($AlternativeSID)
                $newActiveDirectoryAccessRuleSplat = @{
                    Identity                  = $Identity
                    ActiveDirectoryAccessRule = $ActiveDirectoryAccessRule
                    ObjectType                = $ObjectType
                    InheritanceType           = $InheritanceType
                    InheritedObjectType       = $InheritedObjectType
                    AccessControlType         = $AccessControlType
                    AccessRule                = $AccessRule
                }
                Remove-EmptyValue -Hashtable $newActiveDirectoryAccessRuleSplat
                $AccessRuleToAdd = New-ActiveDirectoryAccessRule @newActiveDirectoryAccessRuleSplat
                $RuleAdded = Add-ACLRule -AccessRuleToAdd $AccessRuleToAdd -ntSecurityDescriptor $NTSecurityDescriptor -ACL $ACL
            }
            # lets now return value
            $RuleAdded.Success
        } else {
            Write-Warning -Message "Add-PrivateACL - Unable to create ActiveDirectoryAccessRule for $($ADObject). Skipped."
            $false
        }
    )
    if ($OutputRequiresCommit -notcontains $false -and $OutputRequiresCommit -contains $true) {
        Write-Verbose "Add-ADACL - Saving permissions for $($ADObject)"
        Set-ADObject -Identity $ADObject -Replace @{ ntSecurityDescriptor = $ntSecurityDescriptor } -ErrorAction Stop -Server $QueryServer
    } elseif ($OutputRequiresCommit -contains $false) {
        Write-Warning "Add-ADACL - Skipping saving permissions for $($ADObject) due to errors."
    }
}
function Compare-InternalMissingObject {
    <#
    .SYNOPSIS
    Compares internal missing objects between domains.
 
    .DESCRIPTION
    This function compares internal missing objects between domains based on the provided ForestInformation, server, source domain, target domains, and limit per domain.
 
    .PARAMETER ForestInformation
    Specifies the forest information containing domain controllers.
 
    .PARAMETER Server
    Specifies the server to retrieve objects from.
 
    .PARAMETER SourceDomain
    Specifies the source domain to compare objects from.
 
    .PARAMETER TargetDomain
    Specifies the target domains to compare against.
 
    .PARAMETER LimitPerDomain
    Specifies the limit of objects to compare per domain.
 
    .EXAMPLE
    Compare-InternalMissingObject -ForestInformation $ForestInfo -Server "Server01" -SourceDomain "DomainA" -TargetDomain @("DomainB", "DomainC") -LimitPerDomain 100
    Compares internal missing objects between DomainA and DomainB, DomainC on Server01 with a limit of 100 objects per domain.
 
    .NOTES
    Ensure that the necessary permissions are in place to retrieve objects from the server.
    #>

    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $ForestInformation,
        [string] $Server,
        [string] $SourceDomain,
        [string[]] $TargetDomain,
        [int] $LimitPerDomain
    )
    $Today = (Get-Date).AddHours(-24)
    $Port = "3268"
    $Summary = [ordered] @{
        'Summary' = [PSCustomObject] @{
            SourceServer    = $Server
            Domain          = $SourceDomain
            MissingObject   = 0
            WrongGuid       = 0
            MissingObjectDC = [System.Collections.Generic.List[string]]::new()
            WrongGuidDC     = [System.Collections.Generic.List[string]]::new()
            UniqueMissing   = [System.Collections.Generic.List[string]]::new()
            UniqueWrongGuid = [System.Collections.Generic.List[string]]::new()
        }
    }
    $Source = [ordered] @{}
    Write-Color -Text "Getting objects from the source domain [$SourceDomain] on server [$Server]." -Color Yellow, White
    try {
        [Array] $ListOU = @(
            Get-ADObject -Filter 'ObjectClass -eq "container"' -SearchScope OneLevel -Server $Server -ErrorAction Stop | Select-Object Name, DistinguishedName
            Get-ADOrganizationalUnit -Filter * -Server $Server -SearchScope OneLevel -ErrorAction Stop | Select-Object Name, DistinguishedName
        )
        [Array] $Objects = foreach ($OU in $ListOU.DistinguishedName) {
            Get-ADObject -Filter * -SearchBase $OU -Server $Server -Properties Name, DistinguishedName, ObjectGuid, WhenChanged -ErrorAction Stop
        }
    } catch {
        Write-Color -Text "Couldn't get the objects from the source domain [$SourceDomain] on server [$Server].", " Error: ", $_.Exception.Message -Color Red, White, Red, White
        return $Source
    }
    foreach ($U in $Objects) {
        $Source[$U.DistinguishedName] = $U
    }
    # Clearing the objects to free up memory
    $Objects = $null
    $DomainControllers = foreach ($Domain in $TargetDomain) {
        if ($LimitPerDomain -gt 0) {
            for ($i = 0; $i -le $ForestInformation['DomainDomainControllers'][$Domain].Count; $i++) {
                if ($i -ge $LimitPerDomain) {
                    break
                }
                $ForestInformation['DomainDomainControllers'][$Domain][$i]
            }
        } else {
            $ForestInformation['DomainDomainControllers'][$Domain]
        }
    }
    $Count = 0
    :nextDC foreach ($DC in $DomainControllers) {
        $Count++
        $Summary[$DC.HostName] = @{
            Missing         = [System.Collections.Generic.List[Object]]::new()
            MissingAtSource = [System.Collections.Generic.List[Object]]::new()
            WrongGuid       = [System.Collections.Generic.List[Object]]::new()
            # Ignored = [System.Collections.Generic.List[Object]]::new()
            Errors          = [System.Collections.Generic.List[Object]]::new()
        }
        if ($DC.HostName -eq $Server) {
            Write-Color -Text "Skipping [$Count/$($DomainControllers.Count)] ", $DC.HostName, " [Same as Source]" -Color Yellow, White, Green
            continue
        }
        if ($DC.IsGlobalCatalog) {
            Write-Color -Text "Processing [$Count/$($DomainControllers.Count)] ", $DC.HostName, " [Is Global Catalog]" -Color Yellow, White, Green
        } else {
            Write-Color -Text "Processing [$Count/$($DomainControllers.Count)] ", $DC.HostName, " [Is not Global Catalog]" -Color Yellow, White, Red
            continue
        }

        $CountOU = 0
        # lets free up memory before we start again
        $UsersTarget = $null
        # $CacheTarget = [ordered] @{}
        [Array] $UsersTarget = foreach ($OU in $ListOU.DistinguishedName) {
            $CountOU++
            Write-Color -Text "Processing [$Count/$($DomainControllers.Count)][$CountOU/$($ListOU.Count)] ", $DC.HostName, " OU: ", $OU -Color Yellow, White, Yellow, White
            if ($Port) {
                $QueryServer = "$($DC.HostName):$Port"
            } else {
                $QueryServer = $DC.HostName
            }
            try {
                Get-ADObject -Filter * -SearchBase $OU -Server $QueryServer -Properties Name, DistinguishedName, ObjectGuid, WhenCreated, WhenChanged -ErrorAction Stop
            } catch {
                Write-Color -Text "Couldn't get the objects from the target domain [$SourceDomain] on server [$QueryServer].", " Error: ", $_.Exception.Message -Color Red, White, Red, White
                $Summary[$DC.Hostname]['Errors'].Add(
                    [PSCustomObject] @{
                        GlobalCatalog = $DC.Hostname
                        Domain        = $SourceDomain
                        Object        = $OU
                        Error         = $_.Exception.Message
                    }
                )
                continue nextDC
            }
        }
        foreach ($U in $UsersTarget) {
            # if ($U.DistinguishedName) {
            # $CacheTarget[$U.DistinguishedName] = $U
            # }
            if (-not $Source[$U.DistinguishedName]) {
                if ($U.WhenChanged -lt $Today) {
                    Write-Color -Text "Missing [$Count/$($DomainControllers.Count)][$CountOU/$($ListOU.Count)] ", $DC.HostName, " OU: ", $OU, " object: ", $U.DistinguishedName, " changed: ", $U.WhenChanged -Color Yellow, White, Yellow, White, Yellow
                    $Summary[$DC.Hostname]['Missing'].Add(
                        [PSCustomObject] @{
                            GlobalCatalog     = $DC.Hostname
                            Type              = 'Missing'
                            Domain            = $SourceDomain
                            DistinguishedName = $U.DistinguishedName
                            Name              = $U.Name
                            ObjectClass       = $U.ObjectClass
                            ObjectGuid        = $U.ObjectGuid.Guid
                            WhenCreated       = $U.WhenCreated
                            WhenChanged       = $U.WhenChanged
                        }
                    )
                    $Summary['Summary'].MissingObject++
                    if (-not $Summary['Summary'].MissingObjectDC.Contains($DC.Hostname)) {
                        $Summary['Summary'].MissingObjectDC.Add($DC.Hostname)
                    }
                    if (-not $Summary['Summary'].UniqueMissing.Contains($U.DistinguishedName)) {
                        $Summary['Summary'].UniqueMissing.Add($U.DistinguishedName)
                    }
                }
            } else {
                if ($Source[$U.DistinguishedName].ObjectGUID.Guid -ne $U.ObjectGuid.Guid) {
                    Write-Color -Text "Wrong GUID [$Count/$($DomainControllers.Count)][$CountOU/$($ListOU.Count)] ", $DC.HostName, " OU: ", $OU, " object: ", $U.DistinguishedName, " expected: ", $Source[$U.DistinguishedName].ObjectGUID.Guid, " got: ", $U.ObjectGuid.Guid -Color Red, White, Yellow, White, Red
                    Write-Color -Text "[*] SourceDN: ", $Source[$U.DistinguishedName].DistinguishedName, " SourceName: ", $Source[$U.DistinguishedName].Name -Color Yellow, White, Yellow, White
                    Write-Color -Text "[*] SourceGuid: ", $Source[$U.DistinguishedName].ObjectGUID.Guid, " SourceWhenCreated: ", $Source[$U.DistinguishedName].WhenCreated, " SourceWhenChanged: ", $Source[$U.DistinguishedName].WhenChanged -Color Yellow, White, Yellow, White, Yellow, White
                    Write-Color -Text "[*] TargetDN: ", $U.DistinguishedName, " TargetName: ", $U.Name -Color Yellow, White, Yellow, White
                    Write-Color -Text "[*] TargetGuid: ", $U.ObjectGuid.Guid, " TargetWhenCreated: ", $U.WhenCreated, " TargetWhenChanged: ", $U.WhenChanged -Color Yellow, White, Yellow, White, Yellow, White

                    try {
                        $TryToFind = Get-ADObject -Filter "ObjectGuid -eq '$($Source[$U.DistinguishedName].ObjectGUID.Guid)'" -Server $QueryServer -Properties Name, DistinguishedName, ObjectGuid, WhenCreated, WhenChanged -ErrorAction Stop
                    } catch {
                        $TryToFind = $null
                    }
                    if ($TryToFind) {
                        Write-Color -Text "[*] Found: ", $TryToFind.DistinguishedName, " Name: ", $TryToFind.Name -Color Yellow, White, Yellow, White
                        Write-Color -Text "[*] FoundGuid: ", $TryToFind.ObjectGuid.Guid, " FoundWhenCreated: ", $TryToFind.WhenCreated, " FoundWhenChanged: ", $TryToFind.WhenChanged -Color Yellow, White, Yellow, White, Yellow, White
                    }

                    if ($U.WhenCreated -gt $Today) {
                        # the object is too new to try and compare, as it could be it was just created/moved
                    } else {
                        $Summary[$DC.Hostname]['WrongGuid'].Add(
                            [PSCustomObject] @{
                                GlobalCatalog           = $DC.Hostname
                                Type                    = 'WrongGuid'
                                Domain                  = $SourceDomain
                                DistinguishedName       = $U.DistinguishedName
                                Name                    = $U.Name
                                ObjectClass             = $U.ObjectClass
                                ObjectGuid              = $U.ObjectGuid.Guid
                                WhenCreated             = $U.WhenCreated
                                WhenChanged             = $U.WhenChanged
                                SourceObjectName        = $Source[$U.DistinguishedName].Name
                                SourceObjectDN          = $Source[$U.DistinguishedName].DistinguishedName
                                SourceObjectGuid        = $Source[$U.DistinguishedName].ObjectGUID.Guid
                                SourceObjectWhenCreated = $Source[$U.DistinguishedName].WhenCreated
                                SourceObjectWhenChanged = $Source[$U.DistinguishedName].WhenChanged
                                NewDistinguishedName    = $TryToFind.DistinguishedName
                            }
                        )
                        $Summary['Summary'].WrongGuid++
                        if (-not $Summary['Summary'].WrongGuidDC.Contains($DC.Hostname)) {
                            $Summary['Summary'].WrongGuidDC.Add($DC.Hostname)
                        }
                        if (-not $Summary['Summary'].UniqueWrongGuid.Contains($U.DistinguishedName)) {
                            $Summary['Summary'].UniqueWrongGuid.Add($U.DistinguishedName)
                        }
                    }
                }
            }
        }
        $UsersTarget = $null
    }
    # Clearing the objects to free up memory
    $Source = $null
    $Summary
}
$Script:ConfigurationACLOwners = [ordered] @{
    Name       = 'Forest ACL Owners'
    Enabled    = $true
    Execute    = {
        Get-WinADACLForest -Owner #-ExcludeOwnerType Administrative, WellKnownAdministrative
    }
    Processing = {
        $Script:Reporting['ForestACLOwners']['Variables']['OwnersAdministrative'] = 0
        $Script:Reporting['ForestACLOwners']['Variables']['OwnersWellKnownAdministrative'] = 0
        $Script:Reporting['ForestACLOwners']['Variables']['OwnersUnknown'] = 0
        $Script:Reporting['ForestACLOwners']['Variables']['OwnersNotAdministrative'] = 0
        $Script:Reporting['ForestACLOwners']['Variables']['RequiringFix'] = 0
        $Script:Reporting['ForestACLOwners']['Variables']['Total'] = 0
        $Script:Reporting['ForestACLOwners']['LimitedData'] = foreach ($Object in $Script:Reporting['ForestACLOwners']['Data']) {
            if ($Object.OwnerType -eq 'Administrative') {
                $Script:Reporting['ForestACLOwners']['Variables']['OwnersAdministrative']++
            } elseif ($Object.OwnerType -eq 'WellKnownAdministrative') {
                $Script:Reporting['ForestACLOwners']['Variables']['OwnersWellKnownAdministrative']++
            } elseif ($Object.OwnerType -eq 'NotAdministrative') {
                $Script:Reporting['ForestACLOwners']['Variables']['OwnersNotAdministrative']++
                $Script:Reporting['ForestACLOwners']['Variables']['RequiringFix']++
                $Object
            } else {
                $Script:Reporting['ForestACLOwners']['Variables']['OwnersUnknown']++
                $Script:Reporting['ForestACLOwners']['Variables']['RequiringFix']++
                $Object
            }
            $Script:Reporting['ForestACLOwners']['Variables']['Total']++
        }
    }
    Summary    = {
        New-HTMLText -TextBlock {
            "This report focuses on finding non-administrative owners owning an object in Active Directory. "
            "It goes thru every single computer, user, group, organizational unit (and other) object and find if the owner is "
            "Administrative (Domain Admins/Enterprise Admins)"
            " or "
            "WellKnownAdministrative (SYSTEM account or similar)"
            ". If it's not any of that it exposes those objects to be fixed."
        } -FontSize 10pt -LineBreak

        New-HTMLList -Type Unordered {
            New-HTMLListItem -Text 'Forest ACL Owners in Total: ', $Script:Reporting['ForestACLOwners']['Variables']['Total'] -FontWeight normal, bold
            New-HTMLListItem -Text 'Forest ACL Owners ', 'Domain Admins / Enterprise Admins' , ' as Owner: ', $Script:Reporting['ForestACLOwners']['Variables']['OwnersAdministrative'] -FontWeight normal, bold, normal, bold
            New-HTMLListItem -Text 'Forest ACL Owners ', 'BUILTIN\Administrators / SYSTEM', ' as Owner: ', $Script:Reporting['ForestACLOwners']['Variables']['OwnersWellKnownAdministrative'] -FontWeight normal, bold, normal, bold
            New-HTMLListItem -Text "Forest ACL Owners requiring change: ", $Script:Reporting['ForestACLOwners']['Variables']['RequiringFix'] -FontWeight normal, bold {
                New-HTMLList -Type Unordered {
                    New-HTMLListItem -Text 'Not Administrative: ', $Script:Reporting['ForestACLOwners']['Variables']['OwnersNotAdministrative'] -FontWeight normal, bold
                    New-HTMLListItem -Text 'Unknown (deleted objects/old trusts): ', $Script:Reporting['ForestACLOwners']['Variables']['OwnersUnknown'] -FontWeight normal, bold
                }
            }
        } -FontSize 10pt
    }
    Variables  = @{
    }
    Solution   = {
        New-HTMLSection -Invisible {
            New-HTMLPanel {
                & $Script:ConfigurationACLOwners['Summary']
            }
            New-HTMLPanel {
                New-HTMLChart {
                    New-ChartPie -Name 'Administrative Owners' -Value $Script:Reporting['ForestACLOwners']['Variables']['OwnersAdministrative'] -Color SpringGreen
                    New-ChartPie -Name 'WellKnown Administrative Owners' -Value $Script:Reporting['ForestACLOwners']['Variables']['OwnersWellKnownAdministrative'] -Color SpringGreen
                    New-ChartPie -Name 'Unknown Owners' -Value $Script:Reporting['ForestACLOwners']['Variables']['OwnersUnknown'] -Color BrilliantRose
                    New-ChartPie -Name 'Not Administrative Owners' -Value $Script:Reporting['ForestACLOwners']['Variables']['OwnersNotAdministrative'] -Color Salmon
                } -Title 'Forest ACL Owners' -TitleAlignment center
            }
        }
        New-HTMLSection -Name 'Forest ACL Owners' {
            #if ($Script:Reporting['ForestACLOwners']['Data']) {
            New-HTMLTable -DataTable $Script:Reporting['ForestACLOwners']['LimitedData'] -Filtering {
                #New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor BlizzardBlue
                #New-HTMLTableCondition -Name 'LapsExpirationDays' -ComparisonType number -Operator lt -Value 0 -BackgroundColor BurntOrange -HighlightHeaders LapsExpirationDays, LapsExpirationTime -FailBackgroundColor LimeGreen
                #New-HTMLTableCondition -Name 'Laps' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor Alizarin

                #New-HTMLTableCondition -Name 'Laps' -ComparisonType string -Operator eq -Value $false -BackgroundColor Alizarin -HighlightHeaders LapsExpirationDays, LapsExpirationTime

                #New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 60 -BackgroundColor Alizarin -HighlightHeaders LastLogonDays, LastLogonDate -FailBackgroundColor LimeGreen
                #New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator ge -Value 0 -BackgroundColor LimeGreen -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
                #New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 300 -BackgroundColor Orange -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
                #New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 360 -BackgroundColor Alizarin -HighlightHeaders PasswordLastSet, PasswordLastChangedDays

                #New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor Alizarin
                #New-HTMLTableCondition -Name 'PasswordExpired' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor Alizarin
            }
            #}
        }
        if ($Script:Reporting['Settings']['HideSteps'] -eq $false) {
            New-HTMLSection -Name 'Steps to fix ownership of non-compliant objects in whole forest/domain' {
                New-HTMLContainer {
                    New-HTMLSpanStyle -FontSize 10pt {
                        New-HTMLWizard {
                            New-HTMLWizardStep -Name 'Prepare environment' {
                                New-HTMLText -Text "To be able to execute actions in automated way please install required modules. Those modules will be installed straight from Microsoft PowerShell Gallery."
                                New-HTMLCodeBlock -Code {
                                    Install-Module ADEssentials -Force
                                    Import-Module ADEssentials -Force
                                } -Style powershell
                                New-HTMLText -Text "Using force makes sure newest version is downloaded from PowerShellGallery regardless of what is currently installed. Once installed you're ready for next step."
                            }
                            New-HTMLWizardStep -Name 'Prepare a report (up to date)' {
                                New-HTMLText -Text "Depending when this report was run you may want to prepare new report before proceeding with removal. To generate new report please use:"
                                New-HTMLCodeBlock -Code {
                                    Invoke-ADEssentials -FilePath $Env:UserProfile\Desktop\ADEssentials-ForestACLOwners.html -Verbose -Type ForestACLOwners
                                }
                                New-HTMLText -TextBlock {
                                    "When executed it will take a while to generate all data and provide you with new report depending on size of environment."
                                    "Once confirmed that data is still showing issues and requires fixing please proceed with next step."
                                }
                                New-HTMLText -Text "Alternatively if you prefer working with console you can run: "
                                New-HTMLCodeBlock -Code {
                                    $ForestACLOwner = Get-WinADACLForest -Owner -Verbose -ExcludeOwnerType Administrative, WellKnownAdministrative
                                    $ForestACLOwner | Format-Table
                                }
                                New-HTMLText -Text "It includes all the data as you see in table above including all the owner types (including administrative and wellknownadministrative)"
                            }
                            New-HTMLWizardStep -Name 'Fix Owners' {
                                New-HTMLText -Text @(
                                    "Following command when executed, finds all object owners within Forest/Domain that doesn't match WellKnownAdministrative (SYSTEM/BUIILTIN\Administrator) or Administrative (Domain Admins/Enterprise Admins) ownership. "
                                    "Once it finds those non-compliant owners it replaces them with Domain Admins for a given domain. It doesn't change/modify compliant owners."
                                )

                                New-HTMLText -Text "Make sure when running it for the first time to run it with ", "WhatIf", " parameter as shown below to prevent accidental removal." -FontWeight normal, bold, normal -Color Black, Red, Black

                                New-HTMLCodeBlock -Code {
                                    Set-WinADForestACLOwner -WhatIf -Verbose -IncludeOwnerType 'NotAdministrative', 'Unknown'
                                }
                                New-HTMLText -TextBlock {
                                    "Alternatively for multi-domain scenario, if you have limited Domain Admin credentials to a single domain please use following command: "
                                }
                                New-HTMLCodeBlock -Code {
                                    Set-WinADForestACLOwner -WhatIf -Verbose -IncludeOwnerType 'NotAdministrative', 'Unknown' -IncludeDomains 'YourDomainYouHavePermissionsFor'
                                }
                                New-HTMLText -TextBlock {
                                    "After execution please make sure there are no errors, make sure to review provided output, and confirm that what is about to be changed matches expected data. "
                                } -LineBreak
                                New-HTMLText -Text "Once happy with results please follow with command (this will start replacement of owners process): " -LineBreak -FontWeight bold
                                New-HTMLText -TextBlock {
                                    "This command when executed sets new owner only on first X non-compliant AD objects (computers/users/organizational units/contacts etc.). "
                                    "Use LimitProcessing parameter to prevent mass change and increase the counter when no errors occur. "
                                    "Repeat step above as much as needed increasing LimitProcessing count till there's nothing left. In case of any issues please review and action accordingly. "
                                }
                                New-HTMLCodeBlock -Code {
                                    Set-WinADForestACLOwner -Verbose -LimitProcessing 2 -IncludeOwnerType 'NotAdministrative', 'Unknown'
                                }
                                New-HTMLText -TextBlock {
                                    "Alternatively for multi-domain scenario, if you have limited Domain Admin credentials to a single domain please use following command: "
                                }
                                New-HTMLCodeBlock -Code {
                                    Set-WinADForestACLOwner -Verbose -LimitProcessing 2 -IncludeOwnerType 'NotAdministrative', 'Unknown'-IncludeDomains 'YourDomainYouHavePermissionsFor'
                                }
                            }
                        } -RemoveDoneStepOnNavigateBack -Theme arrows -ToolbarButtonPosition center -EnableAllAnchors
                    }
                }
            }
        }
        if ($Script:Reporting['ForestACLOwners']['WarningsAndErrors']) {
            New-HTMLSection -Name 'Warnings & Errors to Review' {
                New-HTMLTable -DataTable $Script:Reporting['ForestACLOwners']['WarningsAndErrors'] -Filtering {
                    New-HTMLTableCondition -Name 'Type' -Value 'Warning' -BackgroundColor SandyBrown -ComparisonType string -Row
                    New-HTMLTableCondition -Name 'Type' -Value 'Error' -BackgroundColor Salmon -ComparisonType string -Row
                }
            }
        }
    }
}
$Script:ConfigurationBitLocker = [ordered] @{
    Name       = 'Bitlocker Summary'
    Enabled    = $true
    Execute    = {
        Get-WinADBitlockerLapsSummary -BitlockerOnly
    }
    Processing = {
    }
    Summary    = {
    }
    Variables  = @{
    }
    Solution   = {
        if ($Script:Reporting['BitLocker']['Data']) {
            New-HTMLChart {
                New-ChartLegend -LegendPosition bottom -HorizontalAlign center -Color Red, Blue, Yellow
                New-ChartTheme -Palette palette5
                foreach ($Object in $DataTable) {
                    New-ChartRadial -Name $Object.Name -Value $Object.Money
                }
                # Define event
                #New-ChartEvent -DataTableID 'NewIDtoSearchInChart' -ColumnID 0
            }

            New-HTMLTable -DataTable $Script:Reporting['BitLocker']['Data'] -Filtering -SearchBuilder {
                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor BlizzardBlue

                New-HTMLTableCondition -Name 'Encrypted' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor Salmon

                #New-HTMLTableCondition -Name 'LapsExpirationDays' -ComparisonType number -Operator lt -Value 0 -BackgroundColor BurntOrange -HighlightHeaders LapsExpirationDays, LapsExpirationTime -FailBackgroundColor LimeGreen
                #New-HTMLTableCondition -Name 'Laps' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor Alizarin

                #New-HTMLTableCondition -Name 'Laps' -ComparisonType string -Operator eq -Value $false -BackgroundColor Alizarin -HighlightHeaders LapsExpirationDays, LapsExpirationTime

                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 60 -BackgroundColor Salmon -HighlightHeaders LastLogonDays, LastLogonDate -FailBackgroundColor LimeGreen
                New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator ge -Value 0 -BackgroundColor LimeGreen -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
                New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 300 -BackgroundColor Orange -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
                New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 360 -BackgroundColor Salmon -HighlightHeaders PasswordLastSet, PasswordLastChangedDays

                #New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor Alizarin
                #New-HTMLTableCondition -Name 'PasswordExpired' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor Alizarin
            }
        }
    }
}
# https://www.flaticon.com/free-icons/group - group icons - Group icons created by Freepik
# https://www.flaticon.com/free-icons/people - people icons - People icons created by Freepik
# https://www.flaticon.com/free-icons/person - person icons - Person icons created by photo3idea_studio
# https://www.flaticon.com/free-icons/monitor - monitor icons - Monitor icons created by Nikita Golubev
$Script:ConfigurationIcons = @{
    ImageGroup         = 'https://cdn-icons-png.flaticon.com/512/3791/3791146.png'
    ImageGroupNested   = 'https://cdn-icons-png.flaticon.com/512/476/476863.png'
    ImageGroupCircular = 'https://cdn-icons-png.flaticon.com/512/745/745205.png'
    ImageComputer      = 'https://cdn-icons-png.flaticon.com/512/2289/2289389.png'
    ImageUser          = 'https://cdn-icons-png.flaticon.com/512/3048/3048122.png'
    ImageOther         = 'https://cdn-icons-png.flaticon.com/512/8090/8090771.png'
}
$Script:ConfigurationLAPS = [ordered] @{
    Name       = 'LAPS Summary'
    Enabled    = $true
    Execute    = {
        Get-WinADBitlockerLapsSummary -LapsOnly
    }
    Processing = {
        foreach ($Computer in $Script:Reporting['LAPS']['Data']) {
            $Script:Reporting['LAPS']['Variables']['ComputersTotal']++
            if ($Computer.Enabled) {
                $Script:Reporting['LAPS']['Variables']['ComputersEnabled']++
                if ($Computer.LastLogonDays -lt 60 -and $Computer.System -like "Windows*" -and $Computer.Enabled -eq $true) {
                    if (($Computer.Laps -eq $true -or $Computer.WindowsLaps -eq $true)) {
                        $Script:Reporting['LAPS']['Variables']['ComputersActiveWithLaps']++
                    } else {
                        # we exclude DC from this count, even tho Windows LAPS is supported there
                        if ($Computer.IsDC -eq $false) {
                            $Script:Reporting['LAPS']['Variables']['ComputersActiveNoLaps']++
                        }
                    }
                }
                if ($Computer.LastLogonDays -gt 360) {
                    $Script:Reporting['LAPS']['Variables']['ComputersOver360days']++
                } elseif ($Computer.LastLogonDays -gt 180) {
                    $Script:Reporting['LAPS']['Variables']['ComputersOver180days']++
                } elseif ($Computer.LastLogonDays -gt 90) {
                    $Script:Reporting['LAPS']['Variables']['ComputersOver90days']++
                } elseif ($Computer.LastLogonDays -gt 60) {
                    $Script:Reporting['LAPS']['Variables']['ComputersOver60days']++
                } elseif ($Computer.LastLogonDays -gt 30) {
                    $Script:Reporting['LAPS']['Variables']['ComputersOver30days']++
                } elseif ($Computer.LastLogonDays -gt 15) {
                    $Script:Reporting['LAPS']['Variables']['ComputersOver15days']++
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersRecent']++
                }
            } else {
                $Script:Reporting['LAPS']['Variables']['ComputersDisabled']++
            }
            if (($Computer.Laps -eq $true -or $Computer.WindowsLaps -eq $true) -and $Computer.Enabled -eq $true) {
                $Script:Reporting['LAPS']['Variables']['ComputersLapsEnabled']++
                if ($Computer.LapsExpirationDays -lt 0 -or $Computer.WindowsLapsExpirationDays -lt 0) {
                    $Script:Reporting['LAPS']['Variables']['ComputersLapsExpired']++
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersLapsNotExpired']++
                }
            } elseif ($Computer.Enabled -eq $true) {
                if ($Computer.System -notlike "Windows*") {
                    # since Windows LAPS is supported on DC as well we only check for Windows
                    $Script:Reporting['LAPS']['Variables']['ComputersLapsNotApplicable']++
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersLapsDisabled']++
                }
            }
            if ($Computer.LastLogonDays -gt 60) {
                $Script:Reporting['LAPS']['Variables']['ComputersInactive']++
            } else {
                $Script:Reporting['LAPS']['Variables']['ComputersActive']++
            }
            if ($Computer.System -like "Windows Server*") {
                $Script:Reporting['LAPS']['Variables']['ComputersServer']++
                if ($Computer.Enabled) {
                    $Script:Reporting['LAPS']['Variables']['ComputersServerEnabled']++
                    if ($Computer.Laps -eq $true -or $Computer.WindowsLaps -eq $true) {
                        $Script:Reporting['LAPS']['Variables']['ComputersServerLapsEnabled']++
                    } else {
                        $Script:Reporting['LAPS']['Variables']['ComputersServerLapsDisabled']++
                    }
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersServerDisabled']++
                }
            } elseif ($Computer.System -notlike "Windows Server*" -and $Computer.System -like "Windows*") {
                $Script:Reporting['LAPS']['Variables']['ComputersWorkstation']++
                if ($Computer.Enabled) {
                    $Script:Reporting['LAPS']['Variables']['ComputersWorkstationEnabled']++
                    if ($Computer.Laps -eq $true -or $Computer.WindowsLaps -eq $true) {
                        $Script:Reporting['LAPS']['Variables']['ComputersWorkstationLapsEnabled']++
                    } else {
                        $Script:Reporting['LAPS']['Variables']['ComputersWorkstationLapsDisabled']++
                    }
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersWorkstationDisabled']++
                }
            } else {
                $Script:Reporting['LAPS']['Variables']['ComputersOther']++
                if ($Computer.Enabled) {
                    $Script:Reporting['LAPS']['Variables']['ComputersOtherEnabled']++
                    if ($Computer.Laps -eq $true -or $Computer.WindowsLaps -eq $true) {
                        $Script:Reporting['LAPS']['Variables']['ComputersOtherLapsEnabled']++
                    } else {
                        $Script:Reporting['LAPS']['Variables']['ComputersOtherLapsDisabled']++
                    }
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersOtherDisabled']++
                }
            }
        }
    }
    Summary    = {
        New-HTMLText -Text @(
            "This report focuses on showing LAPS status of all computer objects in the domain. "
            "It shows how many computers are enabled, disabled, have LAPS enabled, disabled, expired, etc."
            "It's perfectly normal that some LAPS passwords are expired, due to working over VPN etc."
        ) -FontSize 10pt -LineBreak
        New-HTMLText -Text "Following computer resources are exempt from LAPS: " -FontSize 10pt
        New-HTMLList {
            New-HTMLListItem -Text "Domain Controllers and Read Only Domain Controllers"
            New-HTMLListItem -Text 'Computer Service accounts such as AZUREADSSOACC$'
        } -FontSize 10pt
        New-HTMLText -Text "Here's an overview of some statistics about computers:" -FontSize 10pt
        New-HTMLList {
            New-HTMLListItem -Text "Total number of computers: ", $($Script:Reporting['LAPS']['Variables'].ComputersTotal) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of enabled computers: ", $($Script:Reporting['LAPS']['Variables'].ComputersEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of disabled computers: ", $($Script:Reporting['LAPS']['Variables'].ComputersDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of active computers (less then 60 days): ", $($Script:Reporting['LAPS']['Variables'].ComputersActive) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of inactive computers (over 60 days): ", $($Script:Reporting['LAPS']['Variables'].ComputersInactive) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of active computers with LAPS (less then 60 days): ", $($Script:Reporting['LAPS']['Variables'].ComputersActiveWithLaps) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of active computers without LAPS (less then 60 days): ", $($Script:Reporting['LAPS']['Variables'].ComputersActiveNoLaps) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of computers (enabled) with LAPS: ", $($Script:Reporting['LAPS']['Variables'].ComputersLapsEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of computers (enabled) without LAPS: ", $($Script:Reporting['LAPS']['Variables'].ComputersLapsDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of servers (enabled): ", $($Script:Reporting['LAPS']['Variables'].ComputersServerEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of servers (enabled) with LAPS: ", $($Script:Reporting['LAPS']['Variables'].ComputersServerLapsEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of servers (enabled) without LAPS: ", $($Script:Reporting['LAPS']['Variables'].ComputersServerLapsDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of servers (disabled): ", $($Script:Reporting['LAPS']['Variables'].ComputersServerDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of workstations (enabled) with LAPS: ", $($Script:Reporting['LAPS']['Variables'].ComputersWorkstationLapsEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of workstations (enabled) without LAPS: ", $($Script:Reporting['LAPS']['Variables'].ComputersWorkstationLapsDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
        } -FontSize 10pt
    }
    Variables  = @{
        ComputersActiveNoLaps            = 0
        ComputersActiveWithLaps          = 0
        ComputersTotal                   = 0
        ComputersEnabled                 = 0
        ComputersDisabled                = 0
        ComputersActive                  = 0
        ComputersInactive                = 0
        ComputersLapsEnabled             = 0
        ComputersLapsDisabled            = 0
        ComputersLapsNotApplicable       = 0
        ComputersLapsExpired             = 0
        ComputersLapsNotExpired          = 0
        ComputersServer                  = 0
        ComputersServerEnabled           = 0
        ComputersServerDisabled          = 0
        ComputersServerLapsEnabled       = 0
        ComputersServerLapsDisabled      = 0
        ComputersServerLapsNotApplicable = 0
        ComputersWorkstation             = 0
        ComputersWorkstationEnabled      = 0
        ComputersWorkstationDisabled     = 0
        ComputersWorkstationLapsEnabled  = 0
        ComputersWorkstationLapsDisabled = 0
        ComputersOther                   = 0
        ComputersOtherEnabled            = 0
        ComputersOtherDisabled           = 0
        ComputersOtherLapsEnabled        = 0
        ComputersOtherLapsDisabled       = 0
        ComputersOver360days             = 0
        ComputersOver180days             = 0
        ComputersOver90days              = 0
        ComputersOver60days              = 0
        ComputersOver30days              = 0
        ComputersOver15days              = 0
        ComputersRecent                  = 0
    }
    Solution   = {
        if ($Script:Reporting['LAPS']['Data']) {
            New-HTMLSection -Invisible {
                New-HTMLPanel {
                    $Script:Reporting['LAPS']['Summary']
                }
                New-HTMLPanel {
                    New-HTMLCarousel -Height auto -Loop {
                        New-CarouselSlide -Height auto {
                            New-HTMLChart {
                                New-ChartBarOptions -Type bar
                                New-ChartLegend -Name 'Active Computers (by last logon age)' -Color SpringGreen, Salmon
                                New-ChartBar -Name 'Computers (over 360 days)' -Value $Script:Reporting['LAPS']['Variables'].ComputersOver360days
                                New-ChartBar -Name 'Computers (over 180 days)' -Value $Script:Reporting['LAPS']['Variables'].ComputersOver180days
                                New-ChartBar -Name 'Computers (over 90 days)' -Value $Script:Reporting['LAPS']['Variables'].ComputersOver90days
                                New-ChartBar -Name 'Computers (over 60 days)' -Value $Script:Reporting['LAPS']['Variables'].ComputersOver60days
                                New-ChartBar -Name 'Computers (over 30 days)' -Value $Script:Reporting['LAPS']['Variables'].ComputersOver30days
                                New-ChartBar -Name 'Computers (over 15 days)' -Value $Script:Reporting['LAPS']['Variables'].ComputersOver15days
                                New-ChartBar -Name 'Computers (Recent)' -Value $Script:Reporting['LAPS']['Variables'].ComputersRecent
                                New-ChartAxisY -LabelMaxWidth 300 -Show
                            } -Title 'Active Computers' -TitleAlignment center
                        }
                        New-CarouselSlide -Height auto {
                            New-HTMLChart -Gradient {
                                New-ChartPie -Name 'Computers Enabled' -Value $Script:Reporting['LAPS']['Variables'].ComputersEnabled
                                New-ChartPie -Name 'Computers Disabled' -Value $Script:Reporting['LAPS']['Variables'].ComputersDisabled
                            } -Title "Enabled vs Disabled All Computer Objects"
                        }
                        New-CarouselSlide -Height auto {
                            New-HTMLChart -Gradient {
                                New-ChartPie -Name 'Clients enabled' -Value $Script:Reporting['LAPS']['Variables'].ComputersWorkstationEnabled
                                New-ChartPie -Name 'Clients disabled' -Value $Script:Reporting['LAPS']['Variables'].ComputersWorkstationDisabled
                            } -Title "Enabled vs Disabled Workstations"
                        }
                        New-CarouselSlide -Height auto {
                            New-HTMLChart -Gradient {
                                New-ChartPie -Name 'Servers enabled' -Value $Script:Reporting['LAPS']['Variables'].ComputersServerEnabled
                                New-ChartPie -Name 'Servers disabled' -Value $Script:Reporting['LAPS']['Variables'].ComputersServerDisabled
                            } -Title "Enabled vs Disabled Servers"
                        }
                        New-CarouselSlide -Height auto {
                            New-HTMLChart -Gradient {
                                New-ChartPie -Name 'Servers' -Value $Script:Reporting['LAPS']['Variables'].ComputersServer
                                New-ChartPie -Name 'Clients' -Value $Script:Reporting['LAPS']['Variables'].ComputersWorkstation
                                New-ChartPie -Name 'Non-Windows' -Value $Script:Reporting['LAPS']['Variables'].ComputersOther
                            } -Title "Computers by Type"
                        }
                    }
                }
            }


            New-HTMLSection -HeaderText 'General statistics' -CanCollapse {
                New-HTMLPanel {
                    New-HTMLCarousel -Height auto -Loop {
                        New-CarouselSlide -Height auto {
                            New-HTMLChart -Gradient {
                                New-ChartPie -Name 'With LAPS' -Value $Script:Reporting['LAPS']['Variables'].ComputersLapsEnabled -Color '#94ffc8'
                                New-ChartPie -Name 'Without LAPS' -Value $Script:Reporting['LAPS']['Variables'].ComputersLapsDisabled -Color 'Salmon'
                                New-ChartPie -Name 'LAPS N/A' -Value $Script:Reporting['LAPS']['Variables'].ComputersLapsNotApplicable -Color 'LightGray'
                            } -Title "All Computers with LAPS"
                        }
                        New-CarouselSlide -Height auto {
                            New-HTMLChart -Gradient {
                                New-ChartPie -Name 'With LAPS' -Value $Script:Reporting['LAPS']['Variables'].ComputersActiveWithLaps -Color '#94ffc8'
                                New-ChartPie -Name 'Without LAPS' -Value $Script:Reporting['LAPS']['Variables'].ComputersActiveNoLaps -Color 'Salmon'
                            } -Title "Active Computers with LAPS" -SubTitle "Logged on within the last 60 days"
                        }
                        New-CarouselSlide -Height auto {
                            New-HTMLChart -Gradient {
                                New-ChartPie -Name 'With LAPS' -Value $Script:Reporting['LAPS']['Variables'].ComputersActiveWithLaps -Color '#94ffc8'
                                New-ChartPie -Name 'Without LAPS' -Value $Script:Reporting['LAPS']['Variables'].ComputersActiveNoLaps -Color 'Salmon'
                            } -Title "Active Computers with LAPS" -SubTitle "Logged on within the last 60 days"
                        }
                    }
                }
                New-HTMLPanel {
                    New-HTMLCarousel -Height auto -Loop -AutoPlay {
                        New-CarouselSlide -Height auto {
                            New-HTMLChart -Gradient {
                                New-ChartPie -Name 'With LAPS' -Value $Script:Reporting['LAPS']['Variables'].ComputersWorkstationLapsEnabled -Color '#94ffc8'
                                New-ChartPie -Name 'Without LAPS' -Value $Script:Reporting['LAPS']['Variables'].ComputersWorkstationLapsDisabled -Color 'Salmon'
                            } -Title "Workstations with LAPS"
                        }
                        New-CarouselSlide -Height auto {
                            New-HTMLChart -Gradient {
                                New-ChartPie -Name 'With LAPS' -Value $Script:Reporting['LAPS']['Variables'].ComputersServerLapsEnabled -Color '#94ffc8'
                                New-ChartPie -Name 'Without LAPS' -Value $Script:Reporting['LAPS']['Variables'].ComputersServerLapsDisabled -Color 'Salmon'
                            } -Title "Servers with LAPS"
                        }
                    }
                }
            }
        }
        New-HTMLTable -DataTable $Script:Reporting['LAPS']['Data'] -Filtering {
            New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor BlizzardBlue
            New-HTMLTableCondition -Name 'LapsExpirationDays' -ComparisonType number -Operator lt -Value 0 -BackgroundColor BurntOrange -HighlightHeaders LapsExpirationDays, LapsExpirationTime -FailBackgroundColor LimeGreen
            New-HTMLTableCondition -Name 'Laps' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor Alizarin
            New-HTMLTableCondition -Name 'Laps' -ComparisonType string -Operator eq -Value $false -BackgroundColor Alizarin -HighlightHeaders LapsExpirationDays, LapsExpirationTime
            New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 60 -BackgroundColor Alizarin -HighlightHeaders LastLogonDays, LastLogonDate -FailBackgroundColor LimeGreen
            New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator ge -Value 0 -BackgroundColor LimeGreen -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
            New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 300 -BackgroundColor Orange -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
            New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 360 -BackgroundColor Alizarin -HighlightHeaders PasswordLastSet, PasswordLastChangedDays

            New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $true -BackgroundColor BlizzardBlue -HighlightHeaders IsDC, Laps, LapsExpirationDays, LapsExpirationTime

            New-HTMLTableCondition -Name 'WindowsLapsExpirationDays' -ComparisonType number -Operator lt -Value 0 -BackgroundColor BurntOrange -HighlightHeaders WindowsLapsExpirationDays, WindowsLapsExpirationTime -FailBackgroundColor LimeGreen
            New-HTMLTableCondition -Name 'WindowsLaps' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor Alizarin
            New-HTMLTableCondition -Name 'WindowsLaps' -ComparisonType string -Operator eq -Value $false -BackgroundColor Alizarin -HighlightHeaders WindowsLaps, WindowsLapsExpirationDays, WindowsLapsExpirationTime
            New-HTMLTableCondition -Name 'WindowsLaps' -ComparisonType string -Operator eq -Value "" -BackgroundColor BlizzardBlue -HighlightHeaders WindowsLaps, WindowsLapsExpirationDays, WindowsLapsExpirationTime
        }
    }
}
$Script:ConfigurationLAPSACL = [ordered] @{
    Name       = 'LAPS ACL'
    Enabled    = $true
    Execute    = {
        Get-WinADComputerACLLAPS -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains
    }
    Processing = {
        foreach ($Object in $Script:Reporting['LAPSACL']['Data']) {
            if ($Object.Enabled) {
                $Script:Reporting['LAPSACL']['Variables']['ComputersEnabled']++
                if ($Object.LapsACL) {
                    $Script:Reporting['LAPSACL']['Variables']['LapsACL']++
                    if ($Object.OperatingSystem -like "Windows Server*") {
                        $Script:Reporting['LAPSACL']['Variables']['LapsACLOKServer']++
                    } elseif ($Object.OperatingSystem -notlike "Windows Server*" -and $Object.OperatingSystem -like "Windows*") {
                        $Script:Reporting['LAPSACL']['Variables']['LapsACLOKClient']++
                    }
                } else {
                    if ($Object.IsDC -eq $false) {
                        $Script:Reporting['LAPSACL']['Variables']['LapsACLNot']++
                        if ($Object.OperatingSystem -like "Windows Server*") {
                            $Script:Reporting['LAPSACL']['Variables']['LapsACLNotServer']++
                        } elseif ($Object.OperatingSystem -notlike "Windows Server*" -and $Object.OperatingSystem -like "Windows*") {
                            $Script:Reporting['LAPSACL']['Variables']['LapsACLNotClient']++
                        }
                    }
                }
            } else {
                $Script:Reporting['LAPSACL']['Variables']['ComputersDisabled']++
            }
        }
    }
    Summary    = {
        New-HTMLText -Text @(
            "This report focuses on detecting whether computer has ability to read/write to LAPS properties in Active Directory. "
            "Often for many reasons such as broken ACL inheritance or not fully implemented SELF write access to LAPS - LAPS is implemented only partially. "
            "This means while IT may be thinking that LAPS should be functioning properly - the computer itself may not have rights to write password back to AD, making LAPS not functional. "

        ) -FontSize 10pt -LineBreak
        New-HTMLText -Text "Following computer resources are exempt from LAPS: " -FontSize 10pt
        New-HTMLList {
            New-HTMLListItem -Text "Domain Controllers and Read Only Domain Controllers"
            New-HTMLListItem -Text 'Computer Service accounts such as AZUREADSSOACC$'
        } -FontSize 10pt
        New-HTMLText -Text 'Everything else should have proper LAPS ACL for the computer to provide data.' -FontSize 10pt
    }
    Variables  = @{
        ComputersEnabled  = 0
        ComputersDisabled = 0
        LapsACL           = 0
        LapsACLNot        = 0
        LapsACLOKServer   = 0
        LapsACLOKClient   = 0
        LapsACLNotServer  = 0
        LapsACLNotClient  = 0
    }
    Solution   = {
        if ($Script:Reporting['LAPSACL']['Data']) {
            New-HTMLSection -Invisible {
                New-HTMLPanel {
                    $Script:Reporting['LAPSACL']['Summary']
                }
                New-HTMLPanel {
                    New-HTMLChart {
                        New-ChartBarOptions -Type barStacked
                        New-ChartLegend -Names 'Enabled', 'Disabled' -Color SpringGreen, Salmon
                        New-ChartBar -Name 'Computers' -Value $Script:Reporting['LAPSACL']['Variables'].ComputersEnabled, $Script:Reporting['LAPSACL']['Variables'].ComputersDisabled
                        # New-ChartAxisY -LabelMaxWidth 300 -Show
                    } -Title 'Active Computers' -TitleAlignment center
                }
            }
            New-HTMLSection -HeaderText 'General statistics' -CanCollapse {
                New-HTMLPanel {
                    New-HTMLChart -Gradient {
                        New-ChartPie -Name 'Computers Enabled' -Value $Script:Reporting['LAPSACL']['Variables'].ComputersEnabled
                        New-ChartPie -Name 'Computers Disabled' -Value $Script:Reporting['LAPSACL']['Variables'].ComputersDisabled
                    } -Title "Enabled vs Disabled All Computer Objects"
                }
                New-HTMLPanel {
                    New-HTMLChart -Gradient {
                        New-ChartPie -Name 'LAPS ACL OK' -Value $Script:Reporting['LAPSACL']['Variables'].LapsACL
                        New-ChartPie -Name 'LAPS ACL Not OK' -Value $Script:Reporting['LAPSACL']['Variables'].LapsACLNot
                    } -Title "LAPS ACL OK vs Not OK"
                }
                New-HTMLPanel {
                    New-HTMLChart -Gradient {
                        New-ChartPie -Name 'LAPS ACL OK - Server' -Value $Script:Reporting['LAPSACL']['Variables'].LapsACLOKServer -Color SpringGreen
                        New-ChartPie -Name 'LAPS ACL OK - Client' -Value $Script:Reporting['LAPSACL']['Variables'].LapsACLOKClient -Color LimeGreen
                        New-ChartPie -Name 'LAPS ACL Not OK - Server' -Value $Script:Reporting['LAPSACL']['Variables'].LapsACLNotServer -Color Salmon
                        New-ChartPie -Name 'LAPS ACL Not OK - Client' -Value $Script:Reporting['LAPSACL']['Variables'].LapsACLNotClient -Color Red
                    } -Title "LAPS ACL OK vs Not OK by Computer Type"
                }
            }
            New-HTMLSection -Name 'LAPS ACL Summary' {
                New-HTMLTable -DataTable $Script:Reporting['LAPSACL']['Data'] -Filtering {
                    New-HTMLTableConditionGroup -Logic AND {
                        New-HTMLTableCondition -Name 'LapsACL' -ComparisonType string -Operator eq -Value $true
                        New-HTMLTableCondition -Name 'LapsExpirationACL' -ComparisonType string -Operator eq -Value $true
                        New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $false
                    } -BackgroundColor LimeGreen -HighlightHeaders LapsACL, LapsExpirationACL
                    New-HTMLTableConditionGroup -Logic AND {
                        New-HTMLTableCondition -Name 'LapsACL' -ComparisonType string -Operator eq -Value $false
                        New-HTMLTableCondition -Name 'LapsExpirationACL' -ComparisonType string -Operator eq -Value $false
                        New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $false
                    } -BackgroundColor Alizarin -HighlightHeaders LapsACL, LapsExpirationACL

                    New-HTMLTableCondition -Name 'WindowsLAPSACL' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen
                    New-HTMLTableCondition -Name 'WindowsLAPSExpirationACL' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen
                    New-HTMLTableCondition -Name 'WindowsLAPSEncryptedPassword' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen

                    New-HTMLTableCondition -Name 'WindowsLAPSACL' -ComparisonType string -Operator eq -Value $false -BackgroundColor Alizarin
                    New-HTMLTableCondition -Name 'WindowsLAPSExpirationACL' -ComparisonType string -Operator eq -Value $false -BackgroundColor Alizarin
                    New-HTMLTableCondition -Name 'WindowsLAPSEncryptedPassword' -ComparisonType string -Operator eq -Value $false -BackgroundColor Alizarin

                    New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor BlizzardBlue
                    New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $false -BackgroundColor LimeGreen -FailBackgroundColor BlizzardBlue
                    New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $true -BackgroundColor BlizzardBlue -HighlightHeaders LapsACL, LapsExpirationACL
                    New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $true -BackgroundColor BlizzardBlue -HighlightHeaders WindowsLAPSACL, WindowsLAPSExpirationACL, WindowsLAPSEncryptedPassword
                }
            }
            if ($Script:Reporting['LAPSACL']['WarningsAndErrors']) {
                New-HTMLSection -Name 'Warnings & Errors to Review' {
                    New-HTMLTable -DataTable $Script:Reporting['LAPSACL']['WarningsAndErrors'] -Filtering {
                        New-HTMLTableCondition -Name 'Type' -Value 'Warning' -BackgroundColor SandyBrown -ComparisonType string -Row
                        New-HTMLTableCondition -Name 'Type' -Value 'Error' -BackgroundColor Salmon -ComparisonType string -Row
                    } -PagingOptions 10, 20, 30, 40, 50
                }
            }
        }
    }
}
$Script:ConfigurationLAPSAndBitlocker = [ordered] @{
    Name       = 'LAPS and BITLOCKER'
    Enabled    = $true
    Execute    = {
        Get-WinADBitlockerLapsSummary
    }
    Processing = {
    }
    Summary    = {
    }
    Variables  = @{
    }
    Solution   = {
        if ($Script:Reporting['LapsAndBitLocker']['Data']) {
            New-HTMLChart {
                New-ChartLegend -LegendPosition bottom -HorizontalAlign center -Color Red, Blue, Yellow
                New-ChartTheme -Palette palette5
                foreach ($Object in $DataTable) {
                    New-ChartRadial -Name $Object.Name -Value $Object.Money
                }
            }

            New-HTMLTable -DataTable $Script:Reporting['LapsAndBitLocker']['Data'] -Filtering {
                New-HTMLTableCondition -Name 'Encrypted' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor Salmon
                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor BlizzardBlue
                New-HTMLTableCondition -Name 'LapsExpirationDays' -ComparisonType number -Operator lt -Value 0 -BackgroundColor BurntOrange -HighlightHeaders LapsExpirationDays, LapsExpirationTime -FailBackgroundColor LimeGreen
                New-HTMLTableCondition -Name 'Laps' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor Alizarin
                New-HTMLTableCondition -Name 'Laps' -ComparisonType string -Operator eq -Value $false -BackgroundColor Alizarin -HighlightHeaders LapsExpirationDays, LapsExpirationTime
                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 60 -BackgroundColor Alizarin -HighlightHeaders LastLogonDays, LastLogonDate -FailBackgroundColor LimeGreen
                New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator ge -Value 0 -BackgroundColor LimeGreen -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
                New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 300 -BackgroundColor Orange -HighlightHeaders PasswordLastSet, PasswordLastChangedDays
                New-HTMLTableCondition -Name 'PasswordLastChangedDays' -ComparisonType number -Operator gt -Value 360 -BackgroundColor Alizarin -HighlightHeaders PasswordLastSet, PasswordLastChangedDays

                New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $true -BackgroundColor BlizzardBlue -HighlightHeaders IsDC, Laps, LapsExpirationDays, LapsExpirationTime

                New-HTMLTableCondition -Name 'WindowsLapsExpirationDays' -ComparisonType number -Operator lt -Value 0 -BackgroundColor BurntOrange -HighlightHeaders WindowsLapsExpirationDays, WindowsLapsExpirationTime -FailBackgroundColor LimeGreen
                New-HTMLTableCondition -Name 'WindowsLaps' -ComparisonType string -Operator eq -Value $true -BackgroundColor LimeGreen -FailBackgroundColor Alizarin
                New-HTMLTableCondition -Name 'WindowsLaps' -ComparisonType string -Operator eq -Value $false -BackgroundColor Alizarin -HighlightHeaders WindowsLaps, WindowsLapsExpirationDays, WindowsLapsExpirationTime
                New-HTMLTableCondition -Name 'WindowsLaps' -ComparisonType string -Operator eq -Value "" -BackgroundColor BlizzardBlue -HighlightHeaders WindowsLaps, WindowsLapsExpirationDays, WindowsLapsExpirationTime
            }
        }
    }
}
$Script:ConfigurationServiceAccounts = [ordered] @{
    Name       = 'Service Accounts'
    Enabled    = $true
    Execute    = {
        Get-WinADServiceAccount -PerDomain
    }
    Processing = {
    }
    Summary    = {
    }
    Variables  = @{
    }
    Solution   = {

        if ($Script:Reporting['ServiceAccounts']['Data'] -is [System.Collections.IDictionary]) {
            New-HTMLTabPanel {
                foreach ($Domain in $Script:Reporting['ServiceAccounts']['Data'].Keys) {

                    New-HTMLTab -Name $Domain {
                        New-HTMLTable -DataTable $Script:Reporting['ServiceAccounts']['Data'][$Domain] -Filtering {
                        }
                    }
                }
            }
        }
    }
}
$Script:ShowWinADAccountDelegation = [ordered] @{
    Name       = 'All Accounts Delegation'
    Enabled    = $true
    Execute    = {
        Get-WinADDelegatedAccounts
    }
    Processing = {
    }
    Summary    = {
    }
    Variables  = @{
    }
    Solution   = {
        New-HTMLTable -DataTable $Script:Reporting['AccountDelegation']['Data'] -Filtering {
            # # highlight whole row as blue if the computer is disabled
            New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $false -Row -BackgroundColor LightYellow
            # # highlight enabled column as red if the computer is disabled
            New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor PaleGreen

            New-HTMLTableConditionGroup {
                New-HTMLTableCondition -Name 'FullDelegation' -ComparisonType string -Operator eq -Value $true
                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true
                New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $false
            } -BackgroundColor Salmon -HighlightHeaders 'FullDelegation' -FailBackgroundColor PaleGreen

            New-HTMLTableCondition -Name 'ConstrainedDelegation' -ComparisonType string -Operator eq -Value $true -BackgroundColor PaleGreen -FailBackgroundColor Yellow
            New-HTMLTableCondition -Name 'ResourceDelegation' -ComparisonType string -Operator eq -Value $true -BackgroundColor PaleGreen -FailBackgroundColor Yellow
            # # highlight whole row as green if the computer is enabled and LastLogon, PasswordDays Over 30
            # New-HTMLTableConditionGroup -Conditions {
            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
            # New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator le -Value 30
            # New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator le -Value 30
            # } -BackgroundColor PaleGreen -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

            # New-HTMLTableConditionGroup -Conditions {
            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
            # New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30
            # New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType string -Operator eq -Value ''
            # } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

            # New-HTMLTableConditionGroup -Conditions {
            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
            # New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value ''
            # New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator gt -Value 30
            # } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

            # New-HTMLTableConditionGroup -Conditions {
            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
            # New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value ''
            # New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType string -Operator eq -Value ''
            # } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

            # # highlight whole row as green if the computer is enabled and LastLogon, PasswordDays Over 30
            # New-HTMLTableConditionGroup -Conditions {
            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
            # New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30
            # New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator gt -Value 30
            # } -BackgroundColor Salmon -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled
            # New-HTMLTableConditionGroup -Conditions {
            # New-HTMLTableCondition -Name 'TrustedForDelegation' -ComparisonType string -Operator eq -Value $True
            # New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $false
            # } -BackgroundColor Red -HighlightHeaders Name, SamAccountName, TrustedForDelegation, IsDC
            # New-HTMLTableConditionGroup -Conditions {
            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
            # New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -Operator eq -Value $True
            # } -BackgroundColor Red -HighlightHeaders Name, SamAccountName, Enabled, PasswordNotRequired
        } -ScrollX
    }
}
$Script:ConfigurationGlobalCatalogObjects = [ordered] @{
    Name       = 'Global Catalogs Object Summary'
    Enabled    = $true
    Execute    = {
        Compare-WinADGlobalCatalogObjects -Advanced -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains
    }
    Processing = {
    }
    Summary    = {
        New-HTMLText -Text @(
            "This report compares all objects on every domain controller in the forest and reports on missing objects and objects with wrong GUIDs between them."
            "By comparing the objects on each domain controller, you can identify replication issues and inconsistencies between domain controllers."
            "This report is useful for identifying issues with the global catalog and replication in your Active Directory forest."
            "The report is split into two sections: Missing Objects and Wrong GUID Objects."
        ) -FontSize 10pt -LineBreak

        foreach ($Domain in $Script:Reporting['GlobalCatalogComparison']['Data'].Keys) {
            New-HTMLText -Text "Summary for ", $Domain, " domain" -FontSize 10pt -FontWeight normal, bold, normal
            New-HTMLList {
                New-HTMLListItem -Text "Missing Objects: ", $($Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Summary.MissingObject) -Color Black, Red -FontWeight normal, bold
                # New-HTMLListItem -Text "Missing Objects in Global Catalog (Reverse Lookup): ", $($Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Summary.MissingAtSource) -Color Black, Red -FontWeight normal, bold
                New-HTMLListItem -Text "Wrong GUID Objects: ", $($Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Summary.WrongGuid) -Color Black, Red -FontWeight normal, bold
                New-HTMLListItem -Text "Domain Controllers with Missing Objects: ", $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Summary.MissingObjectDC.Count -FontSize 10pt -FontWeight normal, bold
                #New-HTMLListItem -Text "Domain Controllers with Missing Objects in Global Catalog (Reverse Lookup): ", $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Summary.MissingAtSourceDC.Count -FontSize 10pt -FontWeight normal, bold
                New-HTMLListItem -Text "Domain Controllers with Wrong GUID Objects: ", $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Summary.WrongGuidD.Count -FontSize 10pt -FontWeight normal, bold
                New-HTMLListItem -Text "Errors during scan: ", $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Summary.Errors.Count -FontSize 10pt -FontWeight normal, bold
            } -FontSize 10pt
        }

        New-HTMLText -Text @(
            "While it's possible to have some missing objects, it should be investigated why that is. ",
            "We also ignore objects that were modified in the last 24 hours to avoid false positives, and that don't exists in the Global Catalog on any given domain controller."
            #"Those objects are shown in the Ignored Objects section, but they are not considered as missing or wrong GUID objects."
            #"However you can investigate them further if needed."
        ) -FontSize 10pt
    }
    Variables  = @{
    }
    Solution   = {
        if ($Script:Reporting['GlobalCatalogComparison']['Data']) {

            New-HTMLSection -Invisible {
                New-HTMLPanel {
                    $Script:Reporting['GlobalCatalogComparison']['Summary']
                }
            }

            New-HTMLTabPanel {
                foreach ($Domain in $Script:Reporting['GlobalCatalogComparison']['Data'].Keys) {

                    New-HTMLTab -Name $Domain {
                        New-HTMLSection -HeaderText "Domain details" {
                            New-HTMLPanel {
                                New-HTMLText -Text "Domain: ", $Domain -FontWeight normal, bold
                                New-HTMLText -Text "Source Domain Controller: ", $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain]['Summary'].'SourceServer' -FontWeight normal, bold
                            } -Invisible
                            New-HTMLPanel {
                                New-HTMLText -Text "Missing Unique Objects: " -Color Black -FontWeight bold
                                New-HTMLList {
                                    foreach ($Unique in $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Summary.UniqueMissing) {
                                        New-HTMLListItem -Text $Unique -Color Black, Red -FontWeight normal, bold
                                    }
                                }
                            } -Invisible
                            New-HTMLPanel {
                                New-HTMLText -Text "Wrong GUID Unique Objects: " -Color Black -FontWeight bold
                                New-HTMLList {
                                    foreach ($Unique in $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Summary.UniqueWrongGuid) {
                                        New-HTMLListItem -Text $Unique -Color Black, Red -FontWeight normal, bold
                                    }
                                }
                            } -Invisible
                        }
                        New-HTMLSection -HeaderText "Missing Objects in $Domain per Domain Controller" {
                            $Data = foreach ($Key in  $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Keys) {
                                if ($Key -eq 'Summary') {
                                    continue
                                }
                                $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain][$Key].Missing
                            }
                            New-HTMLTable -DataTable $Data -Filtering {
                            } -IncludeProperty 'GlobalCatalog', 'Domain', 'Type', 'DistinguishedName', 'Name', 'ObjectClass', 'WhenCreated', 'WhenChanged' -ScrollX
                        }
                        # New-HTMLSection -HeaderText "Missing Objects in $Domain per Global Catalog (Reverse Lookup)" {
                        # $Data = foreach ($Key in $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Keys) {
                        # if ($Key -eq 'Summary') {
                        # continue
                        # }
                        # $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain][$Key].MissingAtSource
                        # }
                        # New-HTMLTable -DataTable $Data -Filtering -ScrollX {

                        # } -IncludeProperty 'GlobalCatalog', 'Domain', 'Type', 'DistinguishedName', 'Name', 'ObjectClass', 'WhenCreated', 'WhenChanged', 'ObjectGuid'
                        # }
                        New-HTMLSection -HeaderText "Wrong GUID Objects in $Domain per Domain Controller" {
                            $Data = foreach ($Key in  $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Keys) {
                                if ($Key -eq 'Summary') {
                                    continue
                                }
                                $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain][$Key].WrongGuid
                            }
                            New-HTMLTable -DataTable $Data -Filtering -ScrollX {
                            } -IncludeProperty 'GlobalCatalog', 'Domain', 'Type', 'DistinguishedName', 'Name', 'ObjectClass', 'WhenCreated', 'WhenChanged', 'ObjectGuid', 'NewDistinguishedName', 'SourceObjectName', 'SourceObjectDN', 'SourceObjectGuid', 'SourceObjectWhenCreated', 'SourceObjectWhenChanged'
                        }
                        New-HTMLSection -HeaderText "Errors during scan in $Domain per Domain Controller" {
                            $Data = foreach ($Key in  $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain].Keys) {
                                if ($Key -eq 'Summary') {
                                    continue
                                }
                                $Script:Reporting['GlobalCatalogComparison']['Data'][$Domain][$Key].Errors
                            }
                            New-HTMLTable -DataTable $Data -Filtering -ScrollX {
                            } -IncludeProperty 'GlobalCatalog', 'Domain', 'Object', 'Error'
                        }
                    }
                }
            }
        }
    }
}
$Script:ShowWinADComputer = [ordered] @{
    Name       = 'All Computers'
    Enabled    = $true
    Execute    = {
        Get-WinADComputers -PerDomain -AddOwner
    }
    Processing = {
        foreach ($Domain in $Script:Reporting['Computers']['Data'].Keys) {
            $Script:Reporting['Computers']['Variables'][$Domain] = [ordered] @{}

            foreach ($Computer in $Script:Reporting['Computers']['Data'][$Domain]) {
                $Script:Reporting['Computers']['Variables']['ComputersTotal']++
                if ($Computer.Enabled) {
                    $Script:Reporting['Computers']['Variables'][$Domain]['ComputersEnabled']++
                    $Script:Reporting['Computers']['Variables']['ComputersEnabled']++
                    if ($Computer.IsDC) {
                        $Script:Reporting['Computers']['Variables'][$Domain]['ComputersDC']++
                    } else {
                        $Script:Reporting['Computers']['Variables'][$Domain]['ComputersNotDC']++
                    }
                    if ($Computer.OperatingSystem -like "Windows Server*") {
                        $Script:Reporting['Computers']['Variables'][$Domain]['ComputersServer']++
                    } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") {
                        $Script:Reporting['Computers']['Variables'][$Domain]['ComputersClient']++
                    }
                } else {
                    $Script:Reporting['Computers']['Variables'][$Domain]['ComputersDisabled']++
                    $Script:Reporting['Computers']['Variables']['ComputersDisabled']++
                }
                if ($Computer.OperatingSystem) {
                    $Script:Reporting['Computers']['Variables']['Systems'][$computer.OperatingSystem]++
                } else {
                    $Script:Reporting['Computers']['Variables']['Systems']['Unknown']++
                }

                if ($Computer.OperatingSystem -like "Windows Server*") {
                    $Script:Reporting['Computers']['Variables']['ComputersServer']++
                    if ($Computer.Enabled) {
                        $Script:Reporting['Computers']['Variables']['ComputersServerEnabled']++
                    } else {
                        $Script:Reporting['Computers']['Variables']['ComputersServerDisabled']++
                    }
                } elseif ($Computer.OperatingSystem -notlike "Windows Server*" -and $Computer.OperatingSystem -like "Windows*") {
                    $Script:Reporting['Computers']['Variables']['ComputersWorkstation']++
                    if ($Computer.Enabled) {
                        $Script:Reporting['Computers']['Variables']['ComputersWorkstationEnabled']++
                    } else {
                        $Script:Reporting['Computers']['Variables']['ComputersWorkstationDisabled']++
                    }
                } else {
                    $Script:Reporting['Computers']['Variables']['ComputersOther']++
                    if ($Computer.Enabled) {
                        $Script:Reporting['Computers']['Variables']['ComputersOtherEnabled']++
                    } else {
                        $Script:Reporting['Computers']['Variables']['ComputersOtherDisabled']++
                    }
                }
            }
        }
    }
    Summary    = {
        New-HTMLText -Text @(
            "This report focuses on showing status of all computer objects in the Active Directory forest. "
            "It shows how many computers are enabled, disabled, expired, etc."
        ) -FontSize 10pt -LineBreak
        New-HTMLText -Text "Here's an overview of some statistics about computers:" -FontSize 10pt
        New-HTMLList {
            New-HTMLListItem -Text "Total number of computers: ", $($Script:Reporting['Computers']['Variables'].ComputersTotal) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of enabled computers: ", $($Script:Reporting['Computers']['Variables'].ComputersEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of disabled computers: ", $($Script:Reporting['Computers']['Variables'].ComputersDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of workstations: ", $($Script:Reporting['Computers']['Variables'].ComputersWorkstation) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of enabled workstations: ", $($Script:Reporting['Computers']['Variables'].ComputersWorkstationEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of disabled workstations: ", $($Script:Reporting['Computers']['Variables'].ComputersWorkstationDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of servers: ", $($Script:Reporting['Computers']['Variables'].ComputersServer) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of enabled servers: ", $($Script:Reporting['Computers']['Variables'].ComputersServerEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of disabled servers: ", $($Script:Reporting['Computers']['Variables'].ComputersServerDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of other computers: ", $($Script:Reporting['Computers']['Variables'].ComputersOther) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of enabled other computers: ", $($Script:Reporting['Computers']['Variables'].ComputersOtherEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of disabled other computers: ", $($Script:Reporting['Computers']['Variables'].ComputersOtherDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
        } -FontSize 10pt
    }
    Variables  = @{
        ComputersTotal               = 0
        ComputersEnabled             = 0
        ComputersDisabled            = 0
        ComputersWorkstation         = 0
        ComputersWorkstationEnabled  = 0
        ComputersWorkstationDisabled = 0
        ComputersServer              = 0
        ComputersServerEnabled       = 0
        ComputersServerDisabled      = 0
        ComputersOther               = 0
        ComputersOtherEnabled        = 0
        ComputersOtherDisabled       = 0
        Systems                      = [ordered] @{
            Unknown = 0
        }
    }
    Solution   = {
        if ($Script:Reporting['Computers']['Data'] -is [System.Collections.IDictionary]) {
            New-HTMLSection -Invisible {
                New-HTMLPanel {
                    $Script:Reporting['Computers']['Summary']
                }
                New-HTMLPanel {
                    New-HTMLChart {
                        New-ChartBarOptions -Type bar
                        New-ChartLegend -Name 'Computers by Operating System' -Color SpringGreen, Salmon
                        foreach ($System in $Script:Reporting['Computers']['Variables'].Systems.Keys) {
                            New-ChartBar -Name $System -Value $Script:Reporting['Computers']['Variables']['Systems'][$System]
                        }
                        New-ChartAxisY -LabelMaxWidth 300 -Show
                    } -Title 'Computers by Operating System' -TitleAlignment center
                }
            }
            New-HTMLSection -HeaderText 'General statistics' -CanCollapse {
                New-HTMLPanel {
                    New-HTMLChart -Gradient {
                        New-ChartPie -Name 'Computers Enabled' -Value $Script:Reporting['Computers']['Variables'].ComputersEnabled
                        New-ChartPie -Name 'Computers Disabled' -Value $Script:Reporting['Computers']['Variables'].ComputersDisabled
                    } -Title "Enabled vs Disabled All Computer Objects"
                }
                New-HTMLPanel {
                    New-HTMLChart -Gradient {
                        New-ChartPie -Name 'Clients enabled' -Value $Script:Reporting['Computers']['Variables'].ComputersWorkstationEnabled
                        New-ChartPie -Name 'Clients disabled' -Value $Script:Reporting['Computers']['Variables'].ComputersWorkstationDisabled
                    } -Title "Enabled vs Disabled Workstations"
                }
                New-HTMLPanel {
                    New-HTMLChart -Gradient {
                        New-ChartPie -Name 'Servers enabled' -Value $Script:Reporting['Computers']['Variables'].ComputersServerEnabled
                        New-ChartPie -Name 'Servers disabled' -Value $Script:Reporting['Computers']['Variables'].ComputersServerDisabled
                    } -Title "Enabled vs Disabled Servers"
                }
                New-HTMLPanel {
                    New-HTMLChart -Gradient {
                        New-ChartPie -Name 'Servers' -Value $Script:Reporting['Computers']['Variables'].ComputersServer
                        New-ChartPie -Name 'Clients' -Value $Script:Reporting['Computers']['Variables'].ComputersWorkstation
                        New-ChartPie -Name 'Non-Windows' -Value $Script:Reporting['Computers']['Variables'].ComputersOther
                    } -Title "Computers by Type"
                }
            }
            New-HTMLTabPanel {
                foreach ($Domain in $Script:Reporting['Computers']['Data'].Keys) {
                    New-HTMLTab -Name $Domain {
                        New-HTMLTable -DataTable $Script:Reporting['Computers']['Data'][$Domain] -Filtering {
                            # highlight whole row as blue if the computer is disabled
                            New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $false -Row -BackgroundColor LightYellow
                            # highlight enabled column as red if the computer is disabled
                            New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $false -BackgroundColor Salmon
                            # highlight whole row as green if the computer is enabled and LastLogon, PasswordDays Over 30
                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator le -Value 30
                                New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator le -Value 30
                            } -BackgroundColor PaleGreen -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30
                                New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType string -Operator eq -Value ''
                            } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value ''
                                New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator gt -Value 30
                            } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value ''
                                New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType string -Operator eq -Value ''
                            } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            # highlight whole row as green if the computer is enabled and LastLogon, PasswordDays Over 30
                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30
                                New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator gt -Value 30
                            } -BackgroundColor Salmon -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled
                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'TrustedForDelegation' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $false
                            } -BackgroundColor Red -HighlightHeaders Name, SamAccountName, TrustedForDelegation, IsDC
                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -Operator eq -Value $True
                            } -BackgroundColor Red -HighlightHeaders Name, SamAccountName, Enabled, PasswordNotRequired
                        } -ScrollX
                    }
                }
            }
        }
    }
}
$Script:ShowWinADGroup = [ordered] @{
    Name       = 'All Groups'
    Enabled    = $true
    Execute    = {
        Get-WinADGroups -PerDomain -AddOwner
    }
    Processing = {
    }
    Summary    = {
    }
    Variables  = @{
    }
    Solution   = {
        if ($Script:Reporting['Groups']['Data'] -is [System.Collections.IDictionary]) {
            New-HTMLTabPanel {
                foreach ($Domain in $Script:Reporting['Groups']['Data'].Keys) {

                    New-HTMLTab -Name $Domain {
                        New-HTMLTable -DataTable $Script:Reporting['Groups']['Data'][$Domain] -Filtering {
                            New-HTMLTableColumnOption -ColumnIndex 17 -Width 3000
                            New-HTMLTableCondition -Name 'ManagerCanUpdateGroupMembership' -ComparisonType string -Operator eq -Value $true -BackgroundColor LightYellow
                            New-HTMLTableCondition -Name 'ManagerCanUpdateGroupMembership' -ComparisonType string -Operator eq -Value $false -BackgroundColor PaleGreen
                            #New-HTMLTableCondition -Name 'GroupWriteBack' -ComparisonType string -Operator eq -Value $true -BackgroundColor Pink
                            #New-HTMLTableCondition -Name 'GroupWriteBack' -ComparisonType string -Operator eq -Value $false -BackgroundColor PaleGreen
                            # highlight whole row as blue if the computer is disabled
                            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $false -Row -BackgroundColor LightYellow
                            # # highlight enabled column as red if the computer is disabled
                            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $false -BackgroundColor Salmon
                            # # highlight whole row as green if the computer is enabled and LastLogon, PasswordDays Over 30
                            # New-HTMLTableConditionGroup -Conditions {
                            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                            # New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator le -Value 30
                            # New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator le -Value 30
                            # } -BackgroundColor PaleGreen -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            # New-HTMLTableConditionGroup -Conditions {
                            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                            # New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30
                            # New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType string -Operator eq -Value ''
                            # } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            # New-HTMLTableConditionGroup -Conditions {
                            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                            # New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value ''
                            # New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator gt -Value 30
                            # } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            # New-HTMLTableConditionGroup -Conditions {
                            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                            # New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value ''
                            # New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType string -Operator eq -Value ''
                            # } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            # # highlight whole row as green if the computer is enabled and LastLogon, PasswordDays Over 30
                            # New-HTMLTableConditionGroup -Conditions {
                            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                            # New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30
                            # New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator gt -Value 30
                            # } -BackgroundColor Salmon -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled
                            # New-HTMLTableConditionGroup -Conditions {
                            # New-HTMLTableCondition -Name 'TrustedForDelegation' -ComparisonType string -Operator eq -Value $True
                            # New-HTMLTableCondition -Name 'IsDC' -ComparisonType string -Operator eq -Value $false
                            # } -BackgroundColor Red -HighlightHeaders Name, SamAccountName, TrustedForDelegation, IsDC
                            # New-HTMLTableConditionGroup -Conditions {
                            # New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                            # New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -Operator eq -Value $True
                            # } -BackgroundColor Red -HighlightHeaders Name, SamAccountName, Enabled, PasswordNotRequired
                        } -ScrollX
                    }
                }
            }
        }
    }
}
$Script:ConfigurationPasswordPolicies = [ordered] @{
    Name       = 'Password Policies Summary'
    Enabled    = $true
    Execute    = {
        Get-WinADPasswordPolicy
    }
    Processing = {
        foreach ($PasswordPolicy in $Script:Reporting['PasswordPolicies']['Data']) {
            if ($PasswordPolicy.Name -eq 'Default Password Policy') {
                $Script:Reporting['PasswordPolicies']['Variables'].DefaultPasswordPolicy += 1
            } else {
                $Script:Reporting['PasswordPolicies']['Variables'].FineGrainedPasswordPolicies += 1
            }
        }
    }
    Summary    = {
        New-HTMLText -Text @(
            "This report focuses on showing all Password Policies in Active Directory forest. "
            "It shows default password policies and fine grained password policies. "
            "Keep in mind that there can only be one valid Default Password Policy per domain. "
            "If you have multiple password policies defined (that are not FGPP), only one will work, the one with the lowest precedence on the Domain Controller OU."
            "Any other Password Policy that you defined will not be shown here."
            "If you are not seeing FGPP password policies and you have them defined, make sure that you have extended rights to read them."
        ) -FontSize 10pt -LineBreak
    }
    Variables  = @{
    }
    Solution   = {
        if ($Script:Reporting['PasswordPolicies']['Data']) {
            New-HTMLSection -Invisible {
                New-HTMLPanel {
                    $Script:Reporting['PasswordPolicies']['Summary']
                }
            }
            New-HTMLTable -DataTable $Script:Reporting['PasswordPolicies']['Data'] -Filtering -SearchBuilder {
                New-HTMLTableCondition -Name 'MinPasswordLength' -ComparisonType number -Operator le -Value 8 -BackgroundColor Salmon
                New-HTMLTableCondition -Name 'MinPasswordLength' -ComparisonType number -Operator le -Value 4 -BackgroundColor Red
                New-HTMLTableCondition -Name 'MinPasswordLength' -ComparisonType number -Operator between -Value 8, 16 -BackgroundColor Yellow
                New-HTMLTableCondition -Name 'MinPasswordLength' -ComparisonType number -Operator between -Value 16, 20 -BackgroundColor LightGreen
                New-HTMLTableCondition -Name 'MinPasswordLength' -ComparisonType number -Operator ge -Value 20 -BackgroundColor Green
                New-HTMLTableCondition -Name 'ComplexityEnabled' -ComparisonType string -Operator eq -Value $false -BackgroundColor Salmon -FailBackgroundColor LightGreen
                New-HTMLTableCondition -Name 'ReversibleEncryptionEnabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor Salmon -FailBackgroundColor LightGreen
            } -ScrollX
        }
    }
}
$Script:ShowWinADUser = [ordered] @{
    Name       = 'All Users'
    Enabled    = $true
    Execute    = {
        Get-WinADUsers -PerDomain -AddOwner
    }
    Processing = {
        foreach ($Domain in $Script:Reporting['Users']['Data'].Keys) {
            foreach ($User in $Script:Reporting['Users']['Data'][$Domain]) {
                $Script:Reporting['Users']['Variables']['UsersTotal']++
                if ($User.Enabled) {
                    $Script:Reporting['Users']['Variables']['UsersEnabled']++

                    if ($User.PasswordNeverExpires) {
                        $Script:Reporting['Users']['Variables']['PasswordNeverExpires']++
                    } else {
                        $Script:Reporting['Users']['Variables']['PasswordExpires']++
                    }
                    if ($User.PasswordNotRequired) {
                        $Script:Reporting['Users']['Variables']['PasswordNotRequired']++
                    }
                    if ($User.PasswordLastDays -gt 360) {
                        $Script:Reporting['Users']['Variables']['PasswordLastDays360']++
                    } elseif ($User.PasswordLastDays -gt 300) {
                        $Script:Reporting['Users']['Variables']['PasswordLastDays300']++
                    } elseif ($User.PasswordLastDays -gt 180) {
                        $Script:Reporting['Users']['Variables']['PasswordLastDays180']++
                    } elseif ($User.PasswordLastDays -gt 90) {
                        $Script:Reporting['Users']['Variables']['PasswordLastDays90']++
                    } elseif ($User.PasswordLastDays -gt 60) {
                        $Script:Reporting['Users']['Variables']['PasswordLastDays60']++
                    } else {
                        $Script:Reporting['Users']['Variables']['PasswordLastDaysRecent']++
                    }
                    if ($User.LastLogonDays -gt 360) {
                        $Script:Reporting['Users']['Variables']['LastLogonDays360']++
                    } elseif ($User.LastLogonDays -gt 300) {
                        $Script:Reporting['Users']['Variables']['LastLogonDays300']++
                    } elseif ($User.LastLogonDays -gt 180) {
                        $Script:Reporting['Users']['Variables']['LastLogonDays180']++
                    } elseif ($User.LastLogonDays -gt 90) {
                        $Script:Reporting['Users']['Variables']['LastLogonDays90']++
                    } elseif ($User.LastLogonDays -gt 60) {
                        $Script:Reporting['Users']['Variables']['LastLogonDays60']++
                    } else {
                        $Script:Reporting['Users']['Variables']['LastLogonDaysRecent']++
                    }
                } else {
                    $Script:Reporting['Users']['Variables']['UsersDisabled']++
                }
                if ($User.OwnerType -notin "WellKnownAdministrative", 'Administrative') {
                    $Script:Reporting['Users']['Variables']['OwnerNotAdministrative']++
                } else {
                    $Script:Reporting['Users']['Variables']['OwnerAdministrative']++
                }
                $Script:Reporting['Users']['Variables'].PasswordPolicies[$User.PasswordPolicyName]++
            }
        }
    }
    Variables  = @{
        PasswordPolicies = [ordered] @{}
    }
    Summary    = {
        New-HTMLText -Text @(
            "This report focuses on showing status of all users objects in the Active Directory forest. "
            "It shows how many users are enabled, disabled, expired, etc."
        ) -FontSize 10pt -LineBreak
        New-HTMLText -Text "Here's an overview of some statistics about users:" -FontSize 10pt
        New-HTMLList {
            New-HTMLListItem -Text "Total number of users: ", $($Script:Reporting['Users']['Variables'].UsersTotal) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of enabled users: ", $($Script:Reporting['Users']['Variables'].UsersEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of disabled users: ", $($Script:Reporting['Users']['Variables'].UsersDisabled) -Color None, BlueMarguerite -FontWeight normal, bold

            New-HTMLListItem -Text "Total number of owners that are Domain Admins/Enterprise Admins: ", $($Script:Reporting['Users']['Variables'].OwnerAdministrative) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of owenrs that are non-administrative: ", $($Script:Reporting['Users']['Variables'].OwnerNotAdministrative) -Color None, BlueMarguerite -FontWeight normal, bold

            foreach ($PasswordPolicy in $Script:Reporting['Users']['Variables'].PasswordPolicies.Keys) {
                $Number = $Script:Reporting['Users']['Variables'].PasswordPolicies[$PasswordPolicy]
                New-HTMLListItem -Text "Total number of users with password policy '$PasswordPolicy': ", $Number -Color None, BlueMarguerite -FontWeight normal, bold
            }
        } -FontSize 10pt
    }
    Solution   = {
        if ($Script:Reporting['Users']['Data'] -is [System.Collections.IDictionary]) {
            New-HTMLSection -Invisible {
                New-HTMLPanel {
                    $Script:Reporting['Users']['Summary']
                }
                New-HTMLPanel {
                    New-HTMLChart {
                        New-ChartBarOptions -Type bar
                        New-ChartLegend -Name 'Users by Password Policies' -Color SpringGreen, Salmon
                        foreach ($PasswordPolicy in $Script:Reporting['Users']['Variables'].PasswordPolicies.Keys) {
                            New-ChartBar -Name $PasswordPolicy -Value $Script:Reporting['Users']['Variables']['PasswordPolicies'][$PasswordPolicy]
                        }
                        New-ChartAxisY -LabelMaxWidth 300 -Show
                    } -Title 'Users by Password Policies' -TitleAlignment center
                }
            }
            New-HTMLSection -HeaderText 'General statistics' -CanCollapse {
                New-HTMLPanel {
                    New-HTMLChart {
                        New-ChartPie -Name 'Users Enabled' -Value $Script:Reporting['Users']['Variables'].UsersEnabled -Color '#58ffc5'
                        New-ChartPie -Name 'Users Disabled' -Value $Script:Reporting['Users']['Variables'].UsersDisabled -Color CoralRed
                    } -Title "Enabled vs Disabled All User Objects"
                }
                New-HTMLPanel {
                    New-HTMLChart {
                        New-ChartPie -Name 'Administrative' -Value $Script:Reporting['Users']['Variables'].OwnerAdministrative -Color '#58ffc5'
                        New-ChartPie -Name 'Other' -Value $Script:Reporting['Users']['Variables'].OwnerNotAdministrative -Color CoralRed
                    } -Title "Owner being Administrative vs Other"
                }
                New-HTMLPanel {
                    New-HTMLChart {
                        New-ChartPie -Name 'Password Never Expires' -Value $Script:Reporting['Users']['Variables'].PasswordNeverExpires -Color CoralRed
                        New-ChartPie -Name 'Password Expires' -Value $Script:Reporting['Users']['Variables'].PasswordExpires -Color '#58ffc5'
                    } -Title "Password Never Expires vs Expires" -SubTitle 'Enabled Only'
                }
            }
            New-HTMLTabPanel -Orientation horizontal {
                foreach ($Domain in $Script:Reporting['Users']['Data'].Keys) {
                    New-HTMLTab -Name $Domain {
                        New-HTMLTable -DataTable $Script:Reporting['Users']['Data'][$Domain] -Filtering {
                            # highlight whole row as blue if the computer is disabled
                            New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $false -Row -BackgroundColor LightYellow
                            # highlight enabled column as red if the computer is disabled
                            New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $false -BackgroundColor Salmon
                            # highlight enabled column as BrightTurquoise if the computer is enabled
                            # we don't know if it's any good, but lets try it
                            New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $true -BackgroundColor BrightTurquoise
                            # highlight whole row as green if the computer is enabled and LastLogon, PasswordDays Over 30
                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator le -Value 30
                                New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator le -Value 30
                            } -BackgroundColor PaleGreen -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30
                                New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType string -Operator eq -Value ''
                            } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value ''
                                New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator gt -Value 30
                            } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType string -Operator eq -Value ''
                                New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType string -Operator eq -Value ''
                            } -BackgroundColor LightPink -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            # highlight whole row as green if the computer is enabled and LastLogon, PasswordDays Over 30
                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'LastLogonDays' -ComparisonType number -Operator gt -Value 30
                                New-HTMLTableCondition -Name 'PasswordLastDays' -ComparisonType number -Operator gt -Value 30
                            } -BackgroundColor Salmon -HighlightHeaders LastLogonDays, PasswordLastDays, Enabled

                            New-HTMLTableConditionGroup -Conditions {
                                New-HTMLTableCondition -Name 'Enabled' -ComparisonType string -Operator eq -Value $True
                                New-HTMLTableCondition -Name 'PasswordNotRequired' -ComparisonType string -Operator eq -Value $True
                            } -BackgroundColor Red -HighlightHeaders Name, SamAccountName, Enabled, PasswordNotRequired
                        } -ScrollX
                    }
                }
            }
        }
    }
}
function Convert-TrustForestTrustInfo {
    <#
    .SYNOPSIS
    Converts the binary data of forest trust information into a readable format.
 
    .DESCRIPTION
    This function takes the binary data of forest trust information and converts it into a readable format, providing details about the trust status, domain names, SIDs, and creation timestamps.
 
    .PARAMETER msDSTrustForestTrustInfo
    The binary data containing forest trust information.
 
    .EXAMPLE
    Convert-TrustForestTrustInfo -msDSTrustForestTrustInfo $binaryData
    Converts the binary forest trust information into a readable format.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [CmdletBinding()]
    param(
        [byte[]] $msDSTrustForestTrustInfo
    )
    # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/66387402-cb2b-490c-bf2a-f4ad687397e4
    $Flags = [ordered] @{
        '0'                      = 'Enabled'
        'LsaTlnDisabledNew'      = 'Not yet enabled'
        'LsaTlnDisabledAdmin'    = 'Disabled by administrator'
        'LsaTlnDisabledConflict' = 'Disabled due to a conflict with another trusted domain'
        'LsaSidDisabledAdmin'    = 'Disabled for SID, NetBIOS, and DNS name-based matches by the administrator'
        'LsaSidDisabledConflict' = 'Disabled for SID, NetBIOS, and DNS name-based matches due to a SID or DNS name-based conflict with another trusted domain'
        'LsaNBDisabledAdmin'     = 'Disabled for NetBIOS name-based matches by the administrator'
        'LsaNBDisabledConflict'  = 'Disabled for NetBIOS name-based matches due to a NetBIOS domain name conflict with another trusted domain'
    }

    if ($msDSTrustForestTrustInfo) {
        $Read = Get-ForestTrustInfo -Byte $msDSTrustForestTrustInfo
        $ForestTrustDomainInfo = [ordered]@{}
        [Array] $Records = foreach ($Record in $Read.Records) {
            if ($Record.RecordType -ne 'ForestTrustDomainInfo') {
                # ForestTrustTopLevelName, ForestTrustTopLevelNameEx
                if ($Record.RecordType -eq 'ForestTrustTopLevelName') {
                    $Type = 'Included'
                } else {
                    $Type = 'Excluded'
                }
                [PSCustomObject] @{
                    DnsName     = $null
                    NetbiosName = $null
                    Sid         = $null
                    Type        = $Type
                    Suffix      = $Record.ForestTrustData
                    Status      = $Flags["$($Record.Flags)"]
                    StatusFlag  = $Record.Flags
                    WhenCreated = $Record.Timestamp
                }
            } else {
                $ForestTrustDomainInfo['DnsName'] = $Record.ForestTrustData.DnsName
                $ForestTrustDomainInfo['NetbiosName'] = $Record.ForestTrustData.NetbiosName
                $ForestTrustDomainInfo['Sid'] = $Record.ForestTrustData.Sid
            }
        }
        foreach ($Record in $Records) {
            $Record.DnsName = $ForestTrustDomainInfo['DnsName']
            $Record.NetbiosName = $ForestTrustDomainInfo['NetbiosName']
            $Record.Sid = $ForestTrustDomainInfo['Sid']
        }
        $Records
    }
}
function ConvertFrom-SimplifiedDelegation {
    <#
    .SYNOPSIS
    Experimental way to define permissions that are prepopulated
 
    .DESCRIPTION
    Experimental way to define permissions that are prepopulated
 
    .PARAMETER Principal
    Principal to apply the permission to
 
    .PARAMETER SimplifiedDelegation
    Simplified delegation to apply
 
    .PARAMETER AccessControlType
    Access control type
 
    .PARAMETER InheritanceType
    Inheritance type, if not specified, it will be set to Descendents
 
    .PARAMETER OneLiner
    If specified, the output will be in one line, rather than a multilevel object
 
    .EXAMPLE
    ConvertFrom-SimplifiedDelegation -Principal $ConvertedPrincipal -SimplifiedDelegation $SimplifiedDelegation -OneLiner:$OneLiner.IsPresent -AccessControlType $AccessControlType -InheritanceType $InheritanceType
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $Principal,
        [string[]] $SimplifiedDelegation,
        [System.Security.AccessControl.AccessControlType] $AccessControlType,
        [alias('ActiveDirectorySecurityInheritance')][nullable[System.DirectoryServices.ActiveDirectorySecurityInheritance]] $InheritanceType,
        [switch] $OneLiner
    )
    # Remember to change SimplifiedDelegationDefinitionList below!!!

    $Script:SimplifiedDelegationDefinition = [ordered] @{
        ComputerDomainJoin   = @(
            # allows only to join computers to domain, but not rejoin or move
            if (-not $InheritanceType) {
                $InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::Descendents
            }
            ConvertTo-Delegation -ConvertedPrincipal $Principal -AccessControlType $AccessControlType -AccessRule 'CreateChild' -InheritanceType $InheritanceType -InheritedObjectType 'Computer' -OneLiner:$OneLiner
        )
        ComputerDomainReJoin = @(
            # allows to join computers to domain, but also rejoin them on demand
            if (-not $InheritanceType) {
                $InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::Descendents
            }
            ConvertTo-Delegation -ConvertedPrincipal $Principal -AccessControlType $AccessControlType -AccessRule 'CreateChild', 'DeleteChild' -InheritanceType $InheritanceType -InheritedObjectType 'Computer' -OneLiner:$OneLiner
            ConvertTo-Delegation -ConvertedPrincipal $Principal -AccessControlType $AccessControlType -AccessRule 'ExtendedRight' -ObjectType 'Reset Password' -InheritanceType $InheritanceType -InheritedObjectType 'Computer' -OneLiner:$OneLiner
            ConvertTo-Delegation -ConvertedPrincipal $Principal -AccessControlType $AccessControlType -AccessRule 'ExtendedRight' -ObjectType 'Account Restrictions' -InheritanceType $InheritanceType -InheritedObjectType 'Computer' -OneLiner:$OneLiner
            ConvertTo-Delegation -ConvertedPrincipal $Principal -AccessControlType $AccessControlType -AccessRule 'ExtendedRight' -ObjectType 'Validated write to DNS host name' -InheritanceType $InheritanceType -InheritedObjectType 'Computer' -OneLiner:$OneLiner
            ConvertTo-Delegation -ConvertedPrincipal $Principal -AccessControlType $AccessControlType -AccessRule 'ExtendedRight' -ObjectType 'Validated write to service principal name' -InheritanceType $InheritanceType -InheritedObjectType 'Computer' -OneLiner:$OneLiner
        )
        FullControl          = @(
            if (-not $InheritanceType) {
                $InheritanceType = [System.DirectoryServices.ActiveDirectorySecurityInheritance]::All
            }
            ConvertTo-Delegation -ConvertedPrincipal $Principal -AccessControlType $AccessControlType -AccessRule 'GenericAll' -InheritanceType $InheritanceType -OneLiner:$OneLiner
        )
    }

    foreach ($Simple in $SimplifiedDelegation) {
        $Script:SimplifiedDelegationDefinition[$Simple]
    }
}

$Script:SimplifiedDelegationDefinitionList = @(
    'ComputerDomainJoin'
    'ComputerDomainReJoin'
    'FullControl'
)

[scriptblock] $ConvertSimplifiedDelegationDefinition = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    $Script:SimplifiedDelegationDefinitionList | Sort-Object | Where-Object { $_ -like "*$wordToComplete*" }
}

Register-ArgumentCompleter -CommandName ConvertFrom-SimplifiedDelegation -ParameterName SimplifiedDelegation -ScriptBlock $ConvertSimplifiedDelegationDefinition

function ConvertTo-ComputerFQDN {
    <#
    .SYNOPSIS
    Converts a computer name to its fully qualified domain name (FQDN).
 
    .DESCRIPTION
    This function checks if the provided computer name is an IP address and converts it to a DNS name to ensure SSL functionality.
 
    .PARAMETER Computer
    The computer name or IP address to convert to FQDN.
 
    .EXAMPLE
    ConvertTo-ComputerFQDN -Computer "192.168.1.1"
    Converts the IP address to its corresponding DNS name.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [cmdletBinding()]
    param(
        [string] $Computer
    )
    # Checks for ServerName - Makes sure to convert IPAddress to DNS, otherwise SSL won't work
    $IPAddressCheck = [System.Net.IPAddress]::TryParse($Computer, [ref][ipaddress]::Any)
    $IPAddressMatch = $Computer -match '^(\d+\.){3}\d+$'
    if ($IPAddressCheck -and $IPAddressMatch) {
        [Array] $ADServerFQDN = (Resolve-DnsName -Name $Computer -ErrorAction SilentlyContinue -Type PTR -Verbose:$false)
        if ($ADServerFQDN.Count -gt 0) {
            $ServerName = $ADServerFQDN[0].NameHost
        } else {
            $ServerName = $Computer
        }
    } else {
        [Array] $ADServerFQDN = (Resolve-DnsName -Name $Computer -ErrorAction SilentlyContinue -Type A -Verbose:$false)
        if ($ADServerFQDN.Count -gt 0) {
            $ServerName = $ADServerFQDN[0].Name
        } else {
            $ServerName = $Computer
        }
    }
    $ServerName
}
function ConvertTo-Date {
    <#
    .SYNOPSIS
    Converts a numerical account expiration value to a readable date.
 
    .DESCRIPTION
    This function takes a numerical account expiration value and converts it to a readable date format. If the value is 0 or exceeds the maximum DateTime value, it returns $null.
 
    .PARAMETER AccountExpires
    The numerical account expiration value to convert.
 
    .EXAMPLE
    ConvertTo-Date -AccountExpires 1324567890
    Converts the numerical account expiration value to a readable date.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [cmdletBinding()]
    Param (
        [Parameter(ValueFromPipeline, Mandatory)]$AccountExpires
    )
    process {
        $lngValue = $AccountExpires
        if (($lngValue -eq 0) -or ($lngValue -gt [DateTime]::MaxValue.Ticks)) {
            $AccountExpirationDate = $null
        } else {
            $Date = [DateTime]$lngValue
            $AccountExpirationDate = $Date.AddYears(1600).ToLocalTime()
        }
        $AccountExpirationDate
    }
}
function ConvertTo-Delegation {
    <#
    .SYNOPSIS
    Converts delegation parameters into a custom object.
 
    .DESCRIPTION
    This function converts delegation parameters into a custom object based on the provided input. It allows for defining permissions in a structured manner for a given principal.
 
    .PARAMETER Principal
    Specifies the principal to which the delegation applies.
 
    .PARAMETER AccessRule
    Specifies the Active Directory rights to assign for the delegation.
 
    .PARAMETER AccessControlType
    Specifies the type of access control to be applied.
 
    .PARAMETER ObjectType
    Specifies the type of object being targeted for the delegation.
 
    .PARAMETER InheritedObjectType
    Specifies the type of inherited object for the delegation.
 
    .PARAMETER InheritanceType
    Specifies the type of inheritance to consider for the delegation.
 
    .PARAMETER OneLiner
    If specified, the output will be in a single line format.
 
    .EXAMPLE
    ConvertTo-Delegation -Principal "User1" -AccessRule "Read" -AccessControlType "Allow" -ObjectType "File" -InheritedObjectType "Folder" -InheritanceType "Descendents" -OneLiner
 
    Converts the delegation parameters into a custom object in a single line format.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [CmdletBinding()]
    param(
        [string] $Principal,
        [System.DirectoryServices.ActiveDirectoryRights] $AccessRule,
        [System.Security.AccessControl.AccessControlType] $AccessControlType,
        [alias('ObjectTypeName')][string] $ObjectType,
        [alias('InheritedObjectTypeName')][string] $InheritedObjectType,
        [alias('ActiveDirectorySecurityInheritance')][nullable[System.DirectoryServices.ActiveDirectorySecurityInheritance]] $InheritanceType,
        [switch] $OneLiner
    )
    if ($OneLiner) {
        [PSCustomObject] @{
            Principal               = $Principal
            ActiveDirectoryRights   = $AccessRule
            AccessControlType       = $AccessControlType
            ObjectTypeName          = $ObjectType
            InheritedObjectTypeName = $InheritedObjectType
            InheritanceType         = $InheritanceType
        }
    } else {
        [PSCustomObject] @{
            Principal   = $Principal
            Permissions = [PSCustomObject] @{
                'ActiveDirectoryRights'   = $AccessRule
                'AccessControlType'       = $AccessControlType
                'ObjectTypeName'          = $ObjectType
                'InheritedObjectTypeName' = $InheritedObjectType
                'InheritanceType'         = $InheritanceType
            }
        }
    }
}

[scriptblock] $ConvertToDelegationAutocompleter = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    if (-not $Script:ADSchemaGuids) {
        Import-Module ActiveDirectory -Verbose:$false
        $Script:ADSchemaGuids = Convert-ADSchemaToGuid
    }
    $Script:ADSchemaGuids.Keys | Where-Object { $_ -like "*$wordToComplete*" } | ForEach-Object { "'$($_)'" } #| Sort-Object
}

Register-ArgumentCompleter -CommandName ConvertTo-Delegation -ParameterName ObjectType -ScriptBlock $ConvertToDelegationAutocompleter
Register-ArgumentCompleter -CommandName ConvertTo-Delegation -ParameterName InheritedObjectType -ScriptBlock $ConvertToDelegationAutocompleter
function ConvertTo-TimeSpanFromRepadmin {
    <#
    .SYNOPSIS
    Converts a string representation of time from Repadmin into a TimeSpan object.
 
    .DESCRIPTION
    This function takes a string representation of time from Repadmin and converts it into a TimeSpan object.
 
    .PARAMETER timeString
    The string representation of time from Repadmin to convert.
 
    .EXAMPLE
    ConvertTo-TimeSpanFromRepadmin -timeString "3d.5h:30m:15s"
    Converts the string representation of time from Repadmin into a TimeSpan object.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [cmdletBinding()]
    param (
        [Parameter(Mandatory)][string]$timeString
    )

    switch -Regex ($timeString) {
        '^\s*(\d+)d\.(\d+)h:(\d+)m:(\d+)s\s*$' {
            $days = $Matches[1]
            $hours = $Matches[2]
            $minutes = $Matches[3]
            $seconds = $Matches[4]
            New-TimeSpan -Days $days -Hours $hours -Minutes $minutes -Seconds $seconds
        }
        '^\s*(\d+)h:(\d+)m:(\d+)s\s*$' {
            $hours = $Matches[1]
            $minutes = $Matches[2]
            $seconds = $Matches[3]
            New-TimeSpan -Hours $hours -Minutes $minutes -Seconds $seconds
        }
        '^\s*(\d+)m:(\d+)s\s*$' {
            $minutes = $Matches[1]
            $seconds = $Matches[2]
            New-TimeSpan -Minutes $minutes -Seconds $seconds
        }
        '^\s*:(\d+)s\s*$' {
            $seconds = $Matches[1]
            New-TimeSpan -Seconds $seconds
        }
        '^\s*(\d+)s\s*$' {
            $seconds = $Matches[1]
            New-TimeSpan -Seconds $seconds
        }
        '^>60 days\s*$' {
            New-TimeSpan -Days 60
        }
        '^\s*\(unknown\)\s*$' {
            $null
        }
        default {
            $null
        }
    }
}
function Get-ADConfigurationPermission {
    <#
    .SYNOPSIS
    Retrieves AD configuration permissions based on specified criteria.
 
    .DESCRIPTION
    This function retrieves AD configuration permissions based on the provided AD object splat, object type, and optional filters.
 
    .PARAMETER ADObjectSplat
    The AD object splat containing LDAP filter and search base information.
 
    .PARAMETER ObjectType
    Specifies the type of object to retrieve permissions for.
 
    .PARAMETER FilterOut
    If specified, filters out specific object types.
 
    .PARAMETER Owner
    If specified, retrieves the owner information for each object.
 
    .EXAMPLE
    Get-ADConfigurationPermission -ADObjectSplat $ADObjectSplat -ObjectType "site" -FilterOut -Owner
    Retrieves AD configuration permissions for site objects, filters out specific object types, and retrieves owner information.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [cmdletBinding()]
    param(
        [System.Collections.IDictionary]$ADObjectSplat,
        [string] $ObjectType,
        [switch] $FilterOut,
        [switch] $Owner
    )
    try {
        $Objects = Get-ADObject @ADObjectSplat -ErrorAction Stop
    } catch {
        Write-Warning "Get-ADConfigurationPermission - LDAP Filter: $($ADObjectSplat.LDAPFilter), SearchBase: $($ADObjectSplat.SearchBase)), Error: $($_.Exception.Message)"
    }
    foreach ($O in $Objects) {
        if ($FilterOut) {
            if ($ObjectType -eq 'site') {
                if ($O.DistinguishedName -like '*CN=Subnets,CN=Sites,CN=Configuration*') {
                    continue
                }
                if ($O.DistinguishedName -like '*CN=Inter-Site Transports,CN=Sites,CN=Configuration*') {
                    continue
                }
            }
        }
        if ($Owner) {
            Write-Verbose "Get-ADConfigurationPermission - Getting Owner from $($O.DistinguishedName)"
            $OwnerACL = Get-ADACLOwner -ADObject $O.DistinguishedName -Resolve
            [PSCustomObject] @{
                Name              = $O.Name
                CanonicalName     = $O.CanonicalName
                ObjectType        = $ObjectType
                ObjectClass       = $O.ObjectClass
                Owner             = $OwnerACL.Owner
                OwnerName         = $OwnerACL.OwnerName
                OwnerType         = $OwnerACL.OwnerType
                WhenCreated       = $O.WhenCreated
                WhenChanged       = $O.WhenChanged
                DistinguishedName = $O.DistinguishedName
            }
        } else {
            Get-ADACL -ADObject $O.DistinguishedName -ResolveTypes
        }
    }
}
function Get-ADSubnet {
    <#
    .SYNOPSIS
    Retrieve Active Directory subnet details.
 
    .DESCRIPTION
    Retrieves subnet information from Active Directory. This function processes the provided subnet objects and provides details such as IP address, network length, site information, and more.
 
    .PARAMETER Subnets
    Specifies an array of subnet objects for which information needs to be retrieved.
 
    .PARAMETER AsHashTable
    If specified, the subnet information is returned as a hashtable.
 
    .EXAMPLE
    Get-ADSubnet -Subnets $SubnetArray -AsHashTable
    Retrieves subnet details for the specified subnet array and returns the information as a hashtable.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [cmdletBinding()]
    param(
        [Array] $Subnets,
        [switch] $AsHashTable
    )
    foreach ($Subnet in $Subnets) {
        if ($Subnet.SiteObject) {
            $SiteObject = ConvertFrom-DistinguishedName -DistinguishedName $Subnet.SiteObject
        } else {
            $SiteObject = ''
        }
        $Addr = $Subnet.Name.Split('/')
        $Address = [PSCustomObject] @{
            IP            = $Addr[0]
            NetworkLength = $Addr[1]
        }
        try {
            $IPAddress = ([IPAddress] $Address.IP)
        } catch {
            Write-Warning "Get-ADSubnet - Conversion to IP failed. Error: $($_.Exception.Message)"
        }
        if ($IPAddress.AddressFamily -eq 'InterNetwork') {
            # IPv4
            $AddressRange = Get-IPAddressRangeInformation -CIDRObject $Address
            $MaskBits = ([int](($Subnet.Name -split "/")[1]))
            if ($AsHashTable) {
                [ordered] @{
                    Name        = $Subnet.Name
                    Type        = 'IPv4'
                    SiteName    = $SiteObject
                    SiteStatus  = if ($SiteObject) {
                        $true 
                    } else {
                        $false 
                    }
                    OverLap     = $null
                    OverLapList = $null
                    Subnet      = ([IPAddress](($Subnet.Name -split "/")[0]))
                    MaskBits    = ([int](($Subnet.Name -split "/")[1]))
                    SubnetMask  = ([IPAddress]"$([system.convert]::ToInt64(("1"*$MaskBits).PadRight(32,"0"),2))")
                    TotalHosts  = $AddressRange.TotalHosts
                    UsableHosts = $AddressRange.UsableHosts
                    HostMin     = $AddressRange.HostMin
                    HostMax     = $AddressRange.HostMax
                    Broadcast   = $AddressRange.Broadcast
                }
            } else {
                [PSCustomObject] @{
                    Name        = $Subnet.Name
                    Type        = 'IPv4'
                    SiteName    = $SiteObject
                    SiteStatus  = if ($SiteObject) {
                        $true 
                    } else {
                        $false 
                    }
                    Subnet      = ([IPAddress](($Subnet.Name -split "/")[0]))
                    MaskBits    = ([int](($Subnet.Name -split "/")[1]))
                    SubnetMask  = ([IPAddress]"$([system.convert]::ToInt64(("1"*$MaskBits).PadRight(32,"0"),2))")
                    TotalHosts  = $AddressRange.TotalHosts
                    UsableHosts = $AddressRange.UsableHosts
                    HostMin     = $AddressRange.HostMin
                    HostMax     = $AddressRange.HostMax
                    Broadcast   = $AddressRange.Broadcast
                }
            }
        } else {
            # IPv6
            $AddressRange = $null
            if ($AsHashTable) {
                [ordered] @{
                    Name        = $Subnet.Name
                    Type        = 'IPv6'
                    SiteName    = $SiteObject
                    SiteStatus  = if ($SiteObject) {
                        $true 
                    } else {
                        $false 
                    }
                    OverLap     = $null
                    OverLapList = $null
                    Subnet      = ([IPAddress](($Subnet.Name -split "/")[0]))
                    MaskBits    = ([int](($Subnet.Name -split "/")[1]))
                    SubnetMask  = $null # Ipv6 doesn't have a subnet mask
                    TotalHosts  = $AddressRange.TotalHosts
                    UsableHosts = $AddressRange.UsableHosts
                    HostMin     = $AddressRange.HostMin
                    HostMax     = $AddressRange.HostMax
                    Broadcast   = $AddressRange.Broadcast
                }
            } else {
                [PSCustomObject] @{
                    Name        = $Subnet.Name
                    Type        = 'IPv6'
                    SiteName    = $SiteObject
                    SiteStatus  = if ($SiteObject) {
                        $true 
                    } else {
                        $false 
                    }
                    Subnet      = ([IPAddress](($Subnet.Name -split "/")[0]))
                    MaskBits    = ([int](($Subnet.Name -split "/")[1]))

                    SubnetMask  = $null # Ipv6 doesn't have a subnet mask
                    TotalHosts  = $AddressRange.TotalHosts
                    UsableHosts = $AddressRange.UsableHosts
                    HostMin     = $AddressRange.HostMin
                    HostMax     = $AddressRange.HostMax
                    Broadcast   = $AddressRange.Broadcast
                }
            }
        }
    }
}
function Get-FilteredACL {
    <#
    .SYNOPSIS
    Retrieves filtered Active Directory Access Control List (ACL) details based on specified criteria.
 
    .DESCRIPTION
    This function retrieves and filters Active Directory Access Control List (ACL) details based on the provided criteria. It allows for filtering by various parameters such as access control type, inheritance status, active directory rights, and more.
 
    .PARAMETER ACL
    Specifies the Active Directory Access Control List (ACL) to filter.
 
    .PARAMETER Resolve
    If specified, resolves the identity reference in the ACL.
 
    .PARAMETER Principal
    Specifies the principal to filter by.
 
    .PARAMETER Inherited
    If specified, includes only inherited ACLs.
 
    .PARAMETER NotInherited
    If specified, includes only non-inherited ACLs.
 
    .PARAMETER AccessControlType
    Specifies the type of access control to filter by.
 
    .PARAMETER IncludeObjectTypeName
    Specifies the object type names to include in the filter.
 
    .PARAMETER IncludeInheritedObjectTypeName
    Specifies the inherited object type names to include in the filter.
 
    .PARAMETER ExcludeObjectTypeName
    Specifies the object type names to exclude from the filter.
 
    .PARAMETER ExcludeInheritedObjectTypeName
    Specifies the inherited object type names to exclude from the filter.
 
    .PARAMETER IncludeActiveDirectoryRights
    Specifies the Active Directory rights to include in the filter.
 
    .PARAMETER ExcludeActiveDirectoryRights
    Specifies the Active Directory rights to exclude from the filter.
 
    .PARAMETER IncludeActiveDirectorySecurityInheritance
    Specifies the Active Directory security inheritance types to include in the filter.
 
    .PARAMETER ExcludeActiveDirectorySecurityInheritance
    Specifies the Active Directory security inheritance types to exclude from the filter.
 
    .PARAMETER PrincipalRequested
    Specifies the requested principal object.
 
    .PARAMETER Bundle
    If specified, bundles the filtered ACL details.
 
    .EXAMPLE
    Get-FilteredACL -ACL $ACL -Resolve -Principal "User1" -Inherited -AccessControlType "Allow" -IncludeObjectTypeName "File" -ExcludeInheritedObjectTypeName "Folder" -IncludeActiveDirectoryRights "Read" -ExcludeActiveDirectoryRights "Write" -IncludeActiveDirectorySecurityInheritance "Descendents" -ExcludeActiveDirectorySecurityInheritance "SelfAndChildren" -PrincipalRequested $PrincipalRequested -Bundle
    Retrieves and filters Active Directory Access Control List (ACL) details based on the specified criteria.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [cmdletBinding()]
    param(
        [System.DirectoryServices.ActiveDirectoryAccessRule] $ACL,
        [alias('ResolveTypes')][switch] $Resolve,
        [string] $Principal,
        [switch] $Inherited,
        [switch] $NotInherited,
        [System.Security.AccessControl.AccessControlType] $AccessControlType,
        [Alias('ObjectTypeName')][string[]] $IncludeObjectTypeName,
        [Alias('InheritedObjectTypeName')][string[]] $IncludeInheritedObjectTypeName,
        [string[]] $ExcludeObjectTypeName,
        [string[]] $ExcludeInheritedObjectTypeName,
        [Alias('ActiveDirectoryRights')][System.DirectoryServices.ActiveDirectoryRights[]] $IncludeActiveDirectoryRights,
        [System.DirectoryServices.ActiveDirectoryRights[]] $ExcludeActiveDirectoryRights,
        [Alias('InheritanceType', 'IncludeInheritanceType')][System.DirectoryServices.ActiveDirectorySecurityInheritance[]] $IncludeActiveDirectorySecurityInheritance,
        [Alias('ExcludeInheritanceType')][System.DirectoryServices.ActiveDirectorySecurityInheritance[]] $ExcludeActiveDirectorySecurityInheritance,
        [PSCustomObject] $PrincipalRequested,
        [switch] $Bundle
    )
    [Array] $ADRights = $ACL.ActiveDirectoryRights -split ', '
    if ($AccessControlType) {
        if ($ACL.AccessControlType -ne $AccessControlType) {
            continue
        }
    }
    if ($Inherited) {
        if ($ACL.IsInherited -eq $false) {
            # if it's not inherited and we require inherited lets continue
            continue
        }
    }
    if ($NotInherited) {
        if ($ACL.IsInherited -eq $true) {
            continue
        }
    }
    if ($IncludeActiveDirectoryRights) {
        $FoundInclude = $false
        foreach ($Right in $ADRights) {
            if ($IncludeActiveDirectoryRights -contains $Right) {
                $FoundInclude = $true
                break
            }
        }
        if (-not $FoundInclude) {
            continue
        }
    }
    if ($ExcludeActiveDirectoryRights) {
        foreach ($Right in $ADRights) {
            $FoundExclusion = $false
            if ($ExcludeActiveDirectoryRights -contains $Right) {
                $FoundExclusion = $true
                break
            }
            if ($FoundExclusion) {
                continue
            }
        }
    }
    if ($IncludeActiveDirectorySecurityInheritance) {
        if ($IncludeActiveDirectorySecurityInheritance -notcontains $ACL.InheritanceType) {
            continue
        }
    }
    if ($ExcludeActiveDirectorySecurityInheritance) {
        if ($ExcludeActiveDirectorySecurityInheritance -contains $ACL.InheritanceType) {
            continue
        }
    }
    $IdentityReference = $ACL.IdentityReference.Value


    $ReturnObject = [ordered] @{ }
    $ReturnObject['DistinguishedName' ] = $DistinguishedName
    if ($CanonicalName) {
        $ReturnObject['CanonicalName'] = $CanonicalName
    }
    if ($ObjectClass) {
        $ReturnObject['ObjectClass'] = $ObjectClass
    }
    $ReturnObject['AccessControlType'] = $ACL.AccessControlType
    $ReturnObject['Principal'] = $IdentityReference
    if ($Resolve) {
        $IdentityResolve = Get-WinADObject -Identity $IdentityReference -AddType -Verbose:$false -Cache
        if (-not $IdentityResolve) {
            #Write-Verbose "Get-ADACL - Reverting to Convert-Identity for $IdentityReference"
            $ConvertIdentity = Convert-Identity -Identity $IdentityReference -Verbose:$false
            $ReturnObject['PrincipalType'] = $ConvertIdentity.Type
            # it's not really foreignSecurityPrincipal but can't tell what it is... # https://superuser.com/questions/1067246/is-nt-authority-system-a-user-or-a-group
            $ReturnObject['PrincipalObjectType'] = 'foreignSecurityPrincipal'
            $ReturnObject['PrincipalObjectDomain'] = $ConvertIdentity.DomainName
            $ReturnObject['PrincipalObjectSid'] = $ConvertIdentity.SID
        } else {
            if ($ReturnObject['Principal']) {
                $ReturnObject['Principal'] = $IdentityResolve.Name
            }
            $ReturnObject['PrincipalType'] = $IdentityResolve.Type
            $ReturnObject['PrincipalObjectType'] = $IdentityResolve.ObjectClass
            $ReturnObject['PrincipalObjectDomain' ] = $IdentityResolve.DomainName
            $ReturnObject['PrincipalObjectSid'] = $IdentityResolve.ObjectSID
        }
        if (-not $ReturnObject['PrincipalObjectDomain']) {
            $ReturnObject['PrincipalObjectDomain'] = ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToDomainCN
        }

        # We compare principal to real principal based on Resolve, we compare both PrincipalName and SID to cover our ground
        if ($PrincipalRequested -and $PrincipalRequested.SID -ne $ReturnObject['PrincipalObjectSid']) {
            continue
        }
    } else {
        # We compare principal to principal as returned without resolve
        if ($Principal -and $Principal -ne $IdentityReference) {
            continue
        }
    }

    $ReturnObject['ObjectTypeName'] = $Script:ForestGUIDs["$($ACL.objectType)"]
    $ReturnObject['InheritedObjectTypeName'] = $Script:ForestGUIDs["$($ACL.inheritedObjectType)"]
    if ($IncludeObjectTypeName) {
        if ($IncludeObjectTypeName -notcontains $ReturnObject['ObjectTypeName']) {
            continue
        }
    }
    if ($IncludeInheritedObjectTypeName) {
        if ($IncludeInheritedObjectTypeName -notcontains $ReturnObject['InheritedObjectTypeName']) {
            continue
        }
    }
    if ($ExcludeObjectTypeName) {
        if ($ExcludeObjectTypeName -contains $ReturnObject['ObjectTypeName']) {
            continue
        }
    }
    if ($ExcludeInheritedObjectTypeName) {
        if ($ExcludeInheritedObjectTypeName -contains $ReturnObject['InheritedObjectTypeName']) {
            continue
        }
    }
    if ($ADRightsAsArray) {
        $ReturnObject['ActiveDirectoryRights'] = $ADRights
    } else {
        $ReturnObject['ActiveDirectoryRights'] = $ACL.ActiveDirectoryRights
    }
    $ReturnObject['InheritanceType'] = $ACL.InheritanceType
    $ReturnObject['IsInherited'] = $ACL.IsInherited

    if ($Extended) {
        $ReturnObject['ObjectType'] = $ACL.ObjectType
        $ReturnObject['InheritedObjectType'] = $ACL.InheritedObjectType
        $ReturnObject['ObjectFlags'] = $ACL.ObjectFlags
        $ReturnObject['InheritanceFlags'] = $ACL.InheritanceFlags
        $ReturnObject['PropagationFlags'] = $ACL.PropagationFlags
    }
    if ($Bundle) {
        $ReturnObject['Bundle'] = $ACL
    }
    [PSCustomObject] $ReturnObject
}
function Get-ForestTrustInfo {
    <#
    .SYNOPSIS
    Retrieves and processes forest trust information from an array of bytes.
 
    .DESCRIPTION
    This function retrieves and processes forest trust information from the provided array of bytes.
 
    .PARAMETER Byte
    An array of bytes containing the forest trust information to be processed.
 
    .EXAMPLE
    Get-ForestTrustInfo -Byte $ByteData
    Retrieves and processes forest trust information from the specified array of bytes.
 
    .NOTES
    Author: Chris Dent
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][byte[]]$Byte
    )

    $reader = [System.IO.BinaryReader][System.IO.MemoryStream]$Byte

    $trustInfo = [PSCustomObject]@{
        Version     = $reader.ReadUInt32()
        RecordCount = $reader.ReadUInt32()
        Records     = $null
    }
    $trustInfo.Records = for ($i = 0; $i -lt $trustInfo.RecordCount; $i++) {
        Get-ForestTrustRecord -BinaryReader $reader
    }
    $trustInfo
}
function Get-ForestTrustRecord {
    <#
    .SYNOPSIS
    Retrieves and processes forest trust record information.
 
    .DESCRIPTION
    This function retrieves and processes forest trust record information from the provided BinaryReader.
 
    .PARAMETER BinaryReader
    Specifies the BinaryReader object containing the forest trust record information.
 
    .EXAMPLE
    Get-ForestTrustRecord -BinaryReader $BinaryReader
    Retrieves and processes forest trust record information from the BinaryReader object.
 
    .NOTES
    Author: Chris Dent
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][System.IO.BinaryReader]$BinaryReader
    )
    [Flags()]
    enum TrustFlags {
        LsaTlnDisabledNew = 0x1
        LsaTlnDisabledAdmin = 0x2
        LsaTlnDisabledConflict = 0x4
    }

    [Flags()]
    enum ForestTrustFlags {
        LsaSidDisabledAdmin = 0x1
        LsaSidDisabledConflict = 0x2
        LsaNBDisabledAdmin = 0x4
        LsaNBDisabledConflict = 0x8
    }

    enum RecordType {
        ForestTrustTopLevelName
        ForestTrustTopLevelNameEx
        ForestTrustDomainInfo
    }


    $record = [PSCustomObject]@{
        RecordLength    = $BinaryReader.ReadUInt32()
        Flags           = $BinaryReader.ReadUInt32()
        Timestamp       = $BinaryReader.ReadUInt32(), $BinaryReader.ReadUInt32()
        RecordType      = $BinaryReader.ReadByte() -as [RecordType]
        ForestTrustData = $null
    }

    $record.Timestamp = [DateTime]::FromFileTimeUtc(
        ($record.Timestamp[0] -as [UInt64] -shl 32) + $record.Timestamp[1]
    )

    $record.Flags = switch ($record.RecordType) {
        ([RecordType]::ForestTrustDomainInfo) {
            $record.Flags -as [ForestTrustFlags] 
        }
        default {
            $record.Flags -as [TrustFlags] 
        }
    }

    if ($record.RecordLength -gt 11) {
        switch ($record.RecordType) {
            ([RecordType]::ForestTrustDomainInfo) {
                $record.ForestTrustData = [PSCustomObject]@{
                    Sid         = $null
                    DnsName     = $null
                    NetbiosName = $null
                }

                $sidLength = $BinaryReader.ReadUInt32()
                if ($sidLength -gt 0) {
                    $record.ForestTrustData.Sid = [System.Security.Principal.SecurityIdentifier]::new(
                        $BinaryReader.ReadBytes($sidLength),
                        0
                    )
                }
                $dnsNameLen = $BinaryReader.ReadUInt32()
                if ($dnsNameLen -gt 0) {
                    $record.ForestTrustData.DnsName = [string]::new($BinaryReader.ReadBytes($dnsNameLen) -as [char[]])
                }
                $NetbiosNameLen = $BinaryReader.ReadUInt32()
                if ($NetbiosNameLen -gt 0) {
                    $record.ForestTrustData.NetbiosName = [string]::new($BinaryReader.ReadBytes($NetbiosNameLen) -as [char[]])
                }
            }
            default {
                $nameLength = $BinaryReader.ReadUInt32()
                if ($nameLength -gt 0) {
                    $record.ForestTrustData = [String]::new($BinaryReader.ReadBytes($nameLength) -as [char[]])
                }
            }
        }
    }

    $record
}
function Get-GitHubVersion {
    <#
    .SYNOPSIS
    Retrieves the latest version information from GitHub for a specified cmdlet.
 
    .DESCRIPTION
    This function retrieves the latest version information from GitHub for a specified cmdlet and compares it with the current version.
 
    .PARAMETER Cmdlet
    Specifies the name of the cmdlet to check for the latest version.
 
    .PARAMETER RepositoryOwner
    Specifies the owner of the GitHub repository.
 
    .PARAMETER RepositoryName
    Specifies the name of the GitHub repository.
 
    .EXAMPLE
    Get-GitHubVersion -Cmdlet "YourCmdlet" -RepositoryOwner "OwnerName" -RepositoryName "RepoName"
    Retrieves and compares the latest version information for the specified cmdlet from the GitHub repository.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [cmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $Cmdlet,
        [Parameter(Mandatory)][string] $RepositoryOwner,
        [Parameter(Mandatory)][string] $RepositoryName
    )
    $App = Get-Command -Name $Cmdlet -ErrorAction SilentlyContinue
    if ($App) {
        [Array] $GitHubReleases = (Get-GitHubLatestRelease -Url "https://api.github.com/repos/$RepositoryOwner/$RepositoryName/releases" -Verbose:$false)
        $LatestVersion = $GitHubReleases[0]
        if (-not $LatestVersion.Errors) {
            if ($App.Version -eq $LatestVersion.Version) {
                "Current/Latest: $($LatestVersion.Version) at $($LatestVersion.PublishDate)"
            } elseif ($App.Version -lt $LatestVersion.Version) {
                "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Update?"
            } elseif ($App.Version -gt $LatestVersion.Version) {
                "Current: $($App.Version), Published: $($LatestVersion.Version) at $($LatestVersion.PublishDate). Lucky you!"
            }
        } else {
            "Current: $($App.Version)"
        }
    }
}
function Get-PrivateACL {
    <#
    .SYNOPSIS
    Get ACL from AD Object
 
    .DESCRIPTION
    Get ACL from AD Object
 
    .PARAMETER ADObject
    AD Object to get ACL from
 
    .PARAMETER FullObject
    Returns full object instead of just ACL
 
    .EXAMPLE
    Get-PrivateACL -ADObject 'DC=ad,DC=evotec,DC=xyz'
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [parameter(Mandatory)][alias('DistinguishedName')][string] $ADObject,
        [switch] $FullObject
    )
    try {
        $ADObjectData = Get-ADObject -Identity $ADObject -Properties ntSecurityDescriptor, CanonicalName -ErrorAction Stop
    } catch {
        Write-Warning -Message "Get-PrivateACL - Unable to get ADObject data for $ADObject. Error: $($_.Exception.Message)"
        return
    }
    if ($FullObject) {
        $ADObjectData
    } else {
        $ADObjectData.ntSecurityDescriptor
    }
}
function Get-ProtocolStatus {
    <#
    .SYNOPSIS
    Translates registry of protocol to status
 
    .DESCRIPTION
    Translates registry of protocol to status
 
    .PARAMETER RegistryEntry
    Accepts registry entry from Get-PSRegistry
 
    .EXAMPLE
    $Client = Get-PSRegistry -ComputerName 'AD1' -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client'
    Get-ProtocolStatus -RegistryEntry $Client
 
    .NOTES
    When DisabledByDefault flag is set to 1, SSL / TLS version X is not used by default. If an SSPI app requests to use this version of SSL / TLS, it will be negotiated. In a nutshell, SSL is not disabled when you use DisabledByDefault flag.
    When Enabled flag is set to 0, SSL / TLS version X is disabled and cannot be nagotiated by any SSPI app (even if DisabledByDefault flag is set to 0).
    #>

    [CmdletBinding()]
    param(
        [PSCustomObject] $RegistryEntry,
        [string] $WindowsVersion,
        [System.Collections.IDictionary] $ProtocolDefaults,
        [string] $Protocol
    )

    if ($Protocol) {
        $Default = $ProtocolDefaults[$Protocol]
        if ($Default -eq 'Not supported') {
            return $Default
        }
    } else {
        Write-Warning -Message "Get-ProtocolStatus - protocol not specified."
    }

    if ($RegistryEntry.PSConnection -eq $true) {
        if ($RegistryEntry.PSError -eq $true) {
            #$Status = 'Not set, enabled'
            $Status = 'Enabled'
        } else {
            if ($RegistryEntry.DisabledByDefault -eq 0 -and $RegistryEntry.Enabled -eq 1) {
                $Status = 'Enabled'
            } elseif ($RegistryEntry.DisabledByDefault -eq 1 -and $RegistryEntry.Enabled -eq 0) {
                $Status = 'Disabled'
            } elseif ($RegistryEntry.DisabledByDefault -eq 1 -and $RegistryEntry.Enabled -eq 1) {
                $Status = 'Enabled'
            } elseif ($RegistryEntry.DisabledByDefault -eq 0 -and $RegistryEntry.Enabled -eq 0) {
                $Status = 'Disabled'
            } elseif ($RegistryEntry.DisabledByDefault -eq 0) {
                $Status = 'Enabled'
            } elseif ($RegistryEntry.DisabledByDefault -eq 1) {
                $Status = 'DisabledDefault'
            } elseif ($RegistryEntry.Enabled -eq 1) {
                $Status = 'Enabled'
            } elseif ($RegistryEntry.Enabled -eq 0) {
                $Status = 'Disabled'
            } else {
                $Status = 'Wont happen'
            }
        }
    } else {
        $Status = 'No connection'
    }
    $Status
}
function Get-WinADCache {
    <#
    .SYNOPSIS
    Retrieves Windows Active Directory cache information.
 
    .DESCRIPTION
    This function retrieves Windows Active Directory cache information based on specified parameters.
 
    .PARAMETER ByDN
    Specifies to retrieve AD cache information by DistinguishedName.
 
    .PARAMETER ByNetBiosName
    Specifies to retrieve AD cache information by NetBIOSName.
 
    .EXAMPLE
    Get-WinADCache -ByDN
    Retrieves AD cache information by DistinguishedName.
 
    .EXAMPLE
    Get-WinADCache -ByNetBiosName
    Retrieves AD cache information by NetBIOSName.
 
    .NOTES
    This cmdlet retrieves and organizes Active Directory cache information for specified parameters.
 
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [alias('Get-ADCache')]
    [cmdletbinding()]
    param(
        [switch] $ByDN,
        [switch] $ByNetBiosName
    )
    $ForestObjectsCache = [ordered] @{ }
    $Forest = Get-ADForest
    foreach ($Domain in $Forest.Domains) {
        $Server = Get-ADDomainController -Discover -DomainName $Domain
        try {
            $DomainInformation = Get-ADDomain -Server $Server.Hostname[0]
            $Users = Get-ADUser -Filter "*" -Server $Server.Hostname[0]
            $Groups = Get-ADGroup -Filter "*" -Server $Server.Hostname[0]
            $Computers = Get-ADComputer -Filter "*" -Server $Server.Hostname[0]
        } catch {
            Write-Warning "Get-ADCache - Can't process domain $Domain - $($_.Exception.Message)"
            continue
        }

        if ($ByDN) {
            foreach ($_ in $Users) {
                $ForestObjectsCache["$($_.DistinguishedName)"] = $_
            }
            foreach ($_ in $Groups) {
                $ForestObjectsCache["$($_.DistinguishedName)"] = $_
            }
            foreach ($_ in $Computers) {
                $ForestObjectsCache["$($_.DistinguishedName)"] = $_
            }
        } elseif ($ByNetBiosName) {
            foreach ($_ in $Users) {
                $Identity = -join ($DomainInformation.NetBIOSName, '\', $($_.SamAccountName))
                $ForestObjectsCache["$Identity"] = $_
            }
            foreach ($_ in $Groups) {
                $Identity = -join ($DomainInformation.NetBIOSName, '\', $($_.SamAccountName))
                $ForestObjectsCache["$Identity"] = $_
            }
            foreach ($_ in $Computers) {
                $Identity = -join ($DomainInformation.NetBIOSName, '\', $($_.SamAccountName))
                $ForestObjectsCache["$Identity"] = $_
            }
        } else {
            Write-Warning "Get-ADCache - No choice made."
        }
    }
    $ForestObjectsCache
}

function Get-WinADDomainOrganizationalUnitsACLExtended {
    <#
    .SYNOPSIS
    Retrieves ACL information for specified Organizational Units in Active Directory.
 
    .DESCRIPTION
    This function retrieves Access Control List (ACL) information for the specified Organizational Units (OUs) in Active Directory. It allows for querying ACLs for multiple OUs within a domain.
 
    .PARAMETER DomainOrganizationalUnitsClean
    Specifies an array of clean Domain Organizational Units to retrieve ACL information for.
 
    .PARAMETER Domain
    Specifies the domain to query for ACL information. Defaults to the current user's DNS domain.
 
    .PARAMETER NetBiosName
    Specifies the NetBIOS name of the domain.
 
    .PARAMETER RootDomainNamingContext
    Specifies the root domain naming context.
 
    .PARAMETER GUID
    Specifies a dictionary of GUIDs for reference.
 
    .PARAMETER ForestObjectsCache
    Specifies a dictionary of cached forest objects for reference.
 
    .PARAMETER Server
    Specifies the server to connect to for querying ACL information.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [cmdletbinding()]
    param(
        [Array] $DomainOrganizationalUnitsClean,
        [string] $Domain = $Env:USERDNSDOMAIN,
        [string] $NetBiosName,
        [string] $RootDomainNamingContext,
        [System.Collections.IDictionary] $GUID,
        [System.Collections.IDictionary] $ForestObjectsCache,
        $Server
    )
    if (-not $GUID) {
        $GUID = @{ }
    }
    if (-not $ForestObjectsCache) {
        $ForestObjectsCache = @{ }
    }
    $OUs = @(
        #@{ Name = 'Root'; Value = $RootDomainNamingContext }
        foreach ($OU in $DomainOrganizationalUnitsClean) {
            @{ Name = 'Organizational Unit'; Value = $OU.DistinguishedName }
        }
    )
    if ($Server) {
        $null = New-PSDrive -Name $NetBiosName -Root '' -PSProvider ActiveDirectory -Server $Server
    } else {
        $null = New-PSDrive -Name $NetBiosName -Root '' -PSProvider ActiveDirectory -Server $Domain
    }
    foreach ($OU in $OUs) {

        $ACLs = Get-Acl -Path "$NetBiosName`:\$($OU.Value)" | Select-Object -ExpandProperty Access
        foreach ($ACL in $ACLs) {
            if ($ACL.IdentityReference -like '*\*') {
                $TemporaryIdentity = $ForestObjectsCache["$($ACL.IdentityReference)"]
                $IdentityReferenceType = $TemporaryIdentity.ObjectClass
                $IdentityReference = $ACL.IdentityReference.Value
            } elseif ($ACL.IdentityReference -like '*-*-*-*') {
                $ConvertedSID = ConvertFrom-SID -SID $ACL.IdentityReference
                $TemporaryIdentity = $ForestObjectsCache["$($ConvertedSID.Name)"]
                $IdentityReferenceType = $TemporaryIdentity.ObjectClass
                $IdentityReference = $ConvertedSID.Name
            } else {
                $IdentityReference = $ACL.IdentityReference
                $IdentityReferenceType = 'Unknown'
            }
            [PSCustomObject] @{
                'Distinguished Name'        = $OU.Value
                'Type'                      = $OU.Name
                'AccessControlType'         = $ACL.AccessControlType
                'Rights'                    = $Global:Rights["$($ACL.ActiveDirectoryRights)"]["$($ACL.ObjectFlags)"]
                'ObjectType Name'           = $GUID["$($ACL.objectType)"]
                'Inherited ObjectType Name' = $GUID["$($ACL.inheritedObjectType)"]
                'ActiveDirectoryRights'     = $ACL.ActiveDirectoryRights
                'InheritanceType'           = $ACL.InheritanceType
                #'ObjectType' = $ACL.ObjectType
                #'InheritedObjectType' = $ACL.InheritedObjectType
                'ObjectFlags'               = $ACL.ObjectFlags
                'IdentityReference'         = $IdentityReference
                'IdentityReferenceType'     = $IdentityReferenceType
                'IsInherited'               = $ACL.IsInherited
                'InheritanceFlags'          = $ACL.InheritanceFlags
                'PropagationFlags'          = $ACL.PropagationFlags
            }
        }
    }
}
function Get-WinADTrustObject {
    <#
    .SYNOPSIS
    Retrieves trust relationship information for a specified domain in Active Directory.
 
    .DESCRIPTION
    This function retrieves trust relationship information for the specified domain in Active Directory. It provides details about the trust type, trust direction, trust attributes, and security identifier.
 
    .PARAMETER Identity
    Specifies the domain identity for which trust relationship information is to be retrieved.
 
    .PARAMETER AsHashTable
    Indicates whether the output should be returned as a hashtable.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [cmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0)][alias('Domain')][string] $Identity,
        [switch] $AsHashTable
    )
    $Summary = [ordered] @{}

    # https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.activedirectory.trusttype?view=dotnet-plat-ext-3.1
    $TrustType = @{
        CrossLink   = 'The trust relationship is a shortcut between two domains that exists to optimize the authentication processing between two domains that are in separate domain trees.' # 2
        External    = 'The trust relationship is with a domain outside of the current forest.' # 3
        Forest      = 'The trust relationship is between two forest root domains in separate Windows Server 2003 forests.' # 4
        Kerberos    = 'The trusted domain is an MIT Kerberos realm.' # 5
        ParentChild    = 'The trust relationship is between a parent and a child domain.' # 1
        TreeRoot    = 'One of the domains in the trust relationship is a tree root.' # 0
        Unknown     = 'The trust is a non-specific type.' #6
    }
    # https://docs.microsoft.com/en-us/dotnet/api/system.directoryservices.activedirectory.trustdirection?view=dotnet-plat-ext-3.1
    $TrustDirection = @{
        Bidirectional    = 'Each domain or forest has access to the resources of the other domain or forest.' # 3
        Inbound       = 'This is a trusting domain or forest. The other domain or forest has access to the resources of this domain or forest. This domain or forest does not have access to resources that belong to the other domain or forest.' # 1
        Outbound      = 'This is a trusted domain or forest. This domain or forest has access to resources of the other domain or forest. The other domain or forest does not have access to the resources of this domain or forest.' # 2
    }

    if ($Identity -contains 'DC=') {
        $DomainName = "LDAP://$Domain"
        $TrustSource = ConvertFrom-DistinguishedName -DistinguishedName $DomainName -ToDomainCN
    } else {
        $DomainDN = ConvertTo-DistinguishedName -CanonicalName $Identity -ToDomain
        $DomainName = "LDAP://$DomainDN"
        $TrustSource = $Identity
    }
    $searcher = [adsisearcher]'(objectClass=trustedDomain)'
    $searcher.SearchRoot = [adsi] $DomainName   #'LDAP://DC=TEST,DC=EVOTEC,DC=PL'
    $Trusts = $searcher.FindAll()

    foreach ($Trust in $Trusts) {
        $TrustD = [System.DirectoryServices.ActiveDirectory.TrustDirection] $Trust.properties.trustdirection[0]
        $TrustT = [System.DirectoryServices.ActiveDirectory.TrustType] $Trust.properties.trusttype[0]


        if ($Trust.properties.'msds-trustforesttrustinfo') {
            $msDSTrustForestTrustInfo = Convert-TrustForestTrustInfo -msDSTrustForestTrustInfo $Trust.properties.'msds-trustforesttrustinfo'[0]
        } else {
            $msDSTrustForestTrustInfo = $null
        }
        if ($Trust.properties.trustattributes) {
            $TrustAttributes = Get-ADTrustAttributes -Value ([int] $Trust.properties.trustattributes[0])
        } else {
            $TrustAttributes = $null
        }
        if ($Trust.properties.securityidentifier) {
            try {
                $ObjectSID = [System.Security.Principal.SecurityIdentifier]::new($Trust.properties.securityidentifier[0], 0).Value
            } catch {
                $ObjectSID = $null
            }
        } else {
            $ObjectSID = $null
        }

        $TrustObject = [PSCustomObject] @{
            #Name = [string] $Trust.properties.name # {ad.evotec.xyz}
            TrustSource                  = $TrustSource
            TrustPartner                 = [string] $Trust.properties.trustpartner           # {ad.evotec.xyz}
            TrustPartnerNetBios          = [string] $Trust.properties.flatname               # {EVOTEC}
            TrustDirection               = $TrustD.ToString()         # {3}
            TrustType                    = $TrustT.ToString()             # {2}
            TrustAttributes              = $TrustAttributes       # {32}
            TrustDirectionText           = $TrustDirection[$TrustD.ToString()]
            TrustTypeText                = $TrustType[$TrustT.ToString()]
            WhenCreated                  = [DateTime] $Trust.properties.whencreated[0]         # {26.07.2018 10:59:52}
            WhenChanged                  = [DateTime] $Trust.properties.whenchanged[0]            # {14.08.2020 22:23:14}
            ObjectSID                    = $ObjectSID
            Distinguishedname            = [string] $Trust.properties.distinguishedname      # {CN=ad.evotec.xyz,CN=System,DC=ad,DC=evotec,DC=pl}
            IsCriticalSystemObject       = [bool]::Parse($Trust.properties.iscriticalsystemobject[0]) # {True}
            ObjectGuid                   = [guid]::new($Trust.properties.objectguid[0])
            ObjectCategory               = [string] $Trust.properties.objectcategory         # {CN=Trusted-Domain,CN=Schema,CN=Configuration,DC=ad,DC=evotec,DC=xyz}
            ObjectClass                  = ([array] $Trust.properties.objectclass)[-1]           # {top, leaf, trustedDomain}
            UsnCreated                   = [string] $Trust.properties.usncreated             # {14149}
            UsnChanged                   = [string] $Trust.properties.usnchanged             # {4926091}
            ShowInAdvancedViewOnly       = [bool]::Parse($Trust.properties.showinadvancedviewonly) # {True}
            TrustPosixOffset             = [string] $Trust.properties.trustposixoffset       # {-2147483648}
            msDSTrustForestTrustInfo     = $msDSTrustForestTrustInfo
            msDSSupportedEncryptionTypes = if ($Trust.properties.'msds-supportedencryptiontypes') {
                Get-ADEncryptionTypes -Value ([int] $Trust.properties.'msds-supportedencryptiontypes'[0]) 
            } else {
                $null 
            }
            #SecurityIdentifier = [string] $Trust.properties.securityidentifier # {1 4 0 0 0 0 0 5 21 0 0 0 113 37 225 50 27 133 23 171 67 175 144 188}
            #InstanceType = $Trust.properties.instancetype # {4}
            #AdsPath = [string] $Trust.properties.adspath # {LDAP://CN=ad.evotec.xyz,CN=System,DC=ad,DC=evotec,DC=pl}
            #CN = [string] $Trust.properties.cn # {ad.evotec.xyz}
            #ObjectGuid = $Trust.properties.objectguid # {193 58 187 220 218 30 146 77 162 218 90 74 159 98 153 219}
            #dscorepropagationdata = $Trust.properties.dscorepropagationdata # {01.01.1601 00:00:00}
        }
        if ($AsHashTable) {
            $Summary[$TrustObject.trustpartner] = $TrustObject
        } else {
            $TrustObject
        }
    }
    if ($AsHashTable) {
        $Summary
    }
}
function Get-WinDnsRootHint {
    <#
    .SYNOPSIS
    Retrieves DNS root hints from specified computers.
 
    .DESCRIPTION
    This function retrieves DNS root hints from the specified computers. If no ComputerName is provided, it uses the default domain controller.
 
    .PARAMETER ComputerName
    Specifies an array of computer names from which to retrieve DNS root hints.
 
    .PARAMETER Domain
    Specifies the domain to use for retrieving DNS root hints. Defaults to the current user's DNS domain.
 
    .EXAMPLE
    Get-WinDnsRootHint -ComputerName "Server01", "Server02" -Domain "contoso.com"
    Retrieves DNS root hints from Server01 and Server02 in the contoso.com domain.
 
    .EXAMPLE
    Get-WinDnsRootHint -Domain "fabrikam.com"
    Retrieves DNS root hints from the default domain controller in the fabrikam.com domain.
    #>

    [CmdLetBinding()]
    param(
        [string[]] $ComputerName,
        [string] $Domain = $ENV:USERDNSDOMAIN
    )
    if ($Domain -and -not $ComputerName) {
        $ComputerName = (Get-ADDomainController -Filter * -Server $Domain).HostName
    }
    foreach ($Computer in $ComputerName) {
        $ServerRootHints = Get-DnsServerRootHint -ComputerName $Computer
        foreach ($_ in $ServerRootHints.IPAddress) {
            [PSCustomObject] @{
                DistinguishedName = $_.DistinguishedName
                HostName          = $_.HostName
                RecordClass       = $_.RecordClass
                IPv4Address       = $_.RecordData.IPv4Address.IPAddressToString
                IPv6Address       = $_.RecordData.IPv6Address.IPAddressToString
                #RecordData = $_.RecordData.IPv4Address -join ', '
                #RecordData1 = $_.RecordData
                RecordType        = $_.RecordType
                Timestamp         = $_.Timestamp
                TimeToLive        = $_.TimeToLive
                Type              = $_.Type
                GatheredFrom      = $Computer
            }
        }
    }
}
function Get-WinDnsServerCache {
    <#
    .SYNOPSIS
    Retrieves DNS server cache information for specified computers.
 
    .DESCRIPTION
    This function retrieves DNS server cache information for the specified computers. If no ComputerName is provided, it retrieves cache information from the default domain controller.
 
    .PARAMETER ComputerName
    Specifies an array of computer names from which to retrieve DNS server cache information.
 
    .PARAMETER Domain
    Specifies the domain to use for retrieving DNS server cache information. Defaults to the current user's DNS domain.
 
    .EXAMPLE
    Get-WinDnsServerCache -ComputerName "Server01", "Server02" -Domain "contoso.com"
    Retrieves DNS server cache information from Server01 and Server02 in the contoso.com domain.
 
    .EXAMPLE
    Get-WinDnsServerCache -Domain "fabrikam.com"
    Retrieves DNS server cache information from the default domain controller in the fabrikam.com domain.
    #>

    [CmdLetBinding()]
    param(
        [string[]] $ComputerName,
        [string] $Domain = $ENV:USERDNSDOMAIN
    )
    if ($Domain -and -not $ComputerName) {
        $ComputerName = (Get-ADDomainController -Filter * -Server $Domain).HostName
    }
    foreach ($Computer in $ComputerName) {
        $DnsServerCache = Get-DnsServerCache -ComputerName $Computer
        foreach ($_ in $DnsServerCache) {
            [PSCustomObject] @{
                DistinguishedName         = $_.DistinguishedName
                IsAutoCreated             = $_.IsAutoCreated
                IsDsIntegrated            = $_.IsDsIntegrated
                IsPaused                  = $_.IsPaused
                IsReadOnly                = $_.IsReadOnly
                IsReverseLookupZone       = $_.IsReverseLookupZone
                IsShutdown                = $_.IsShutdown
                ZoneName                  = $_.ZoneName
                ZoneType                  = $_.ZoneType
                EnablePollutionProtection = $_.EnablePollutionProtection
                IgnorePolicies            = $_.IgnorePolicies
                LockingPercent            = $_.LockingPercent
                MaxKBSize                 = $_.MaxKBSize
                MaxNegativeTtl            = $_.MaxNegativeTtl
                MaxTtl                    = $_.MaxTtl
                GatheredFrom              = $Computer
            }
        }
    }
}
function Get-WinDnsServerDiagnostics {
    <#
    .SYNOPSIS
    Retrieves DNS server diagnostics information for a specified computer.
 
    .DESCRIPTION
    This function retrieves DNS server diagnostics information for the specified computer. It provides details about various settings and configurations related to DNS server operations.
 
    .PARAMETER ComputerName
    Specifies the name of the computer for which DNS server diagnostics information is to be retrieved.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [CmdLetBinding()]
    param(
        [string] $ComputerName
    )

    $DnsServerDiagnostics = Get-DnsServerDiagnostics -ComputerName $ComputerName
    foreach ($_ in $DnsServerDiagnostics) {
        [PSCustomObject] @{
            FilterIPAddressList                  = $_.FilterIPAddressList
            Answers                              = $_.Answers
            EnableLogFileRollover                = $_.EnableLogFileRollover
            EnableLoggingForLocalLookupEvent     = $_.EnableLoggingForLocalLookupEvent
            EnableLoggingForPluginDllEvent       = $_.EnableLoggingForPluginDllEvent
            EnableLoggingForRecursiveLookupEvent = $_.EnableLoggingForRecursiveLookupEvent
            EnableLoggingForRemoteServerEvent    = $_.EnableLoggingForRemoteServerEvent
            EnableLoggingForServerStartStopEvent = $_.EnableLoggingForServerStartStopEvent
            EnableLoggingForTombstoneEvent       = $_.EnableLoggingForTombstoneEvent
            EnableLoggingForZoneDataWriteEvent   = $_.EnableLoggingForZoneDataWriteEvent
            EnableLoggingForZoneLoadingEvent     = $_.EnableLoggingForZoneLoadingEvent
            EnableLoggingToFile                  = $_.EnableLoggingToFile
            EventLogLevel                        = $_.EventLogLevel
            FullPackets                          = $_.FullPackets
            LogFilePath                          = $_.LogFilePath
            MaxMBFileSize                        = $_.MaxMBFileSize
            Notifications                        = $_.Notifications
            Queries                              = $_.Queries
            QuestionTransactions                 = $_.QuestionTransactions
            ReceivePackets                       = $_.ReceivePackets
            SaveLogsToPersistentStorage          = $_.SaveLogsToPersistentStorage
            SendPackets                          = $_.SendPackets
            TcpPackets                           = $_.TcpPackets
            UdpPackets                           = $_.UdpPackets
            UnmatchedResponse                    = $_.UnmatchedResponse
            Update                               = $_.Update
            UseSystemEventLog                    = $_.UseSystemEventLog
            WriteThrough                         = $_.WriteThrough
            GatheredFrom                         = $ComputerName
        }
    }
}
function Get-WinDnsServerDirectoryPartition {
    <#
    .SYNOPSIS
    Retrieves directory partition information for a specified DNS server.
 
    .DESCRIPTION
    This function retrieves directory partition information for the specified DNS server. It provides details about different directory partitions, including their names, distinguished names, flags, replicas, state, and zone count.
 
    .PARAMETER ComputerName
    Specifies the name of the DNS server for which directory partition information is to be retrieved.
 
    .PARAMETER Splitter
    Specifies a character to use for splitting replica information if needed.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    Version: 1.0
    #>

    [CmdLetBinding()]
    param(
        [string] $ComputerName,
        [string] $Splitter
    )
    $DnsServerDirectoryPartition = Get-DnsServerDirectoryPartition -ComputerName $ComputerName
    foreach ($_ in $DnsServerDirectoryPartition) {
        [PSCustomObject] @{
            DirectoryPartitionName              = $_.DirectoryPartitionName
            CrossReferenceDistinguishedName     = $_.CrossReferenceDistinguishedName
            DirectoryPartitionDistinguishedName = $_.DirectoryPartitionDistinguishedName
            Flags                               = $_.Flags
            Replica                             = if ($Splitter -ne '') {
                $_.Replica -join $Splitter 
            } else {
                $_.Replica 
            }
            State                               = $_.State
            ZoneCount                           = $_.ZoneCount
            GatheredFrom                        = $ComputerName
        }
    }
}
function Get-WinDnsServerDsSetting {
    <#
    .SYNOPSIS
    Retrieves DNS server Directory Services settings for a specified computer.
 
    .DESCRIPTION
    This function retrieves DNS server Directory Services settings for the specified computer. It provides details about various settings related to Directory Services, including Directory Partition Auto Enlist Interval, Lazy Update Interval, Minimum Background Load Threads, Remote Replication Delay, Tombstone Interval, and the computer from which the settings were gathered.
 
    .PARAMETER ComputerName
    Specifies the name of the computer for which DNS server Directory Services settings are to be retrieved.
 
    .EXAMPLE
    Get-WinDnsServerDsSettings -ComputerName "Server01"
    Retrieves DNS server Directory Services settings from Server01.
 
    .EXAMPLE
    Get-WinDnsServerDsSettings -ComputerName "Server02"
    Retrieves DNS server Directory Services settings from Server02.
    #>

    [CmdLetBinding()]
    param(
        [string] $ComputerName
    )

    $DnsServerDsSetting = Get-DnsServerDsSetting -ComputerName $ComputerName
    foreach ($_ in $DnsServerDsSetting) {
        [PSCustomObject] @{
            DirectoryPartitionAutoEnlistInterval = $_.DirectoryPartitionAutoEnlistInterval
            LazyUpdateInterval                   = $_.LazyUpdateInterval
            MinimumBackgroundLoadThreads         = $_.MinimumBackgroundLoadThreads
            RemoteReplicationDelay               = $_.RemoteReplicationDelay
            TombstoneInterval                    = $_.TombstoneInterval
            GatheredFrom                         = $ComputerName
        }
    }
}
function Get-WinDnsServerEDns {
    <#
    .SYNOPSIS
    Retrieves DNS server EDNS settings for a specified computer.
 
    .DESCRIPTION
    This function retrieves DNS server EDNS settings for the specified computer. It provides details about various settings related to EDNS, including Cache Timeout, Enable Probes, Enable Reception, and the computer from which the settings were gathered.
 
    .PARAMETER ComputerName
    Specifies the name of the computer for which DNS server EDNS settings are to be retrieved.
 
    .EXAMPLE
    Get-WinDnsServerEDns -ComputerName "Server01"
    Retrieves DNS server EDNS settings from Server01.
 
    .EXAMPLE
    Get-WinDnsServerEDns -ComputerName "Server02"
    Retrieves DNS server EDNS settings from Server02.
    #>

    [CmdLetBinding()]
    param(
        [string] $ComputerName
    )
    $DnsServerDsSetting = Get-DnsServerEDns -ComputerName $ComputerName
    foreach ($_ in $DnsServerDsSetting) {
        [PSCustomObject] @{
            CacheTimeout    = $_.CacheTimeout
            EnableProbes    = $_.EnableProbes
            EnableReception = $_.EnableReception
            GatheredFrom    = $ComputerName
        }
    }
}
function Get-WinDnsServerGlobalNameZone {
    <#
    .SYNOPSIS
    Retrieves global name zone settings for specified DNS servers.
 
    .DESCRIPTION
    This function retrieves global name zone settings for the specified DNS servers. It provides details about various settings related to global name zones, including AlwaysQueryServer, BlockUpdates, Enable, EnableEDnsProbes, GlobalOverLocal, PreferAaaa, SendTimeout, ServerQueryInterval, and the computer from which the settings were gathered.
 
    .PARAMETER ComputerName
    Specifies an array of computer names from which to retrieve global name zone settings.
 
    .PARAMETER Domain
    Specifies the domain to use for retrieving global name zone settings. Defaults to the current user's DNS domain.
 
    .EXAMPLE
    Get-WinDnsServerGlobalNameZone -ComputerName "Server01", "Server02" -Domain "contoso.com"
    Retrieves global name zone settings from Server01 and Server02 in the contoso.com domain.
 
    .EXAMPLE
    Get-WinDnsServerGlobalNameZone -Domain "fabrikam.com"
    Retrieves global name zone settings from the default domain controller in the fabrikam.com domain.
    #>

    [CmdLetBinding()]
    param(
        [string[]] $ComputerName,
        [string] $Domain = $ENV:USERDNSDOMAIN
    )
    if ($Domain -and -not $ComputerName) {
        $ComputerName = (Get-ADDomainController -Filter * -Server $Domain).HostName
    }
    foreach ($Computer in $ComputerName) {
        $DnsServerGlobalNameZone = Get-DnsServerGlobalNameZone -ComputerName $Computer
        foreach ($_ in $DnsServerGlobalNameZone) {
            [PSCustomObject] @{
                AlwaysQueryServer   = $_.AlwaysQueryServer
                BlockUpdates        = $_.BlockUpdates
                Enable              = $_.Enable
                EnableEDnsProbes    = $_.EnableEDnsProbes
                GlobalOverLocal     = $_.GlobalOverLocal
                PreferAaaa          = $_.PreferAaaa
                SendTimeout         = $_.SendTimeout
                ServerQueryInterval = $_.ServerQueryInterval
                GatheredFrom        = $Computer
            }
        }
    }
}

#Get-WinDnsServerGlobalNameZone -ComputerName 'AD1'
function Get-WinDnsServerGlobalQueryBlockList {
    <#
    .SYNOPSIS
    Retrieves the global query block list from DNS servers.
 
    .DESCRIPTION
    This function retrieves the global query block list from DNS servers specified by the ComputerName parameter.
 
    .PARAMETER ComputerName
    Specifies the DNS server(s) from which to retrieve the global query block list.
 
    .PARAMETER Domain
    Specifies the domain to query for DNS servers. Defaults to the current user's DNS domain.
 
    .PARAMETER Formatted
    Indicates whether the output should be formatted.
 
    .PARAMETER Splitter
    Specifies the delimiter to use when formatting the output list.
 
    .EXAMPLE
    Get-WinDnsServerGlobalQueryBlockList -ComputerName "dns-server1", "dns-server2" -Formatted -Splitter ";"
    Retrieves the global query block list from "dns-server1" and "dns-server2" and formats the output with ";" as the delimiter.
 
    #>

    [CmdLetBinding()]
    param(
        [string[]] $ComputerName,
        [string] $Domain = $ENV:USERDNSDOMAIN,
        [switch] $Formatted,
        [string] $Splitter = ', '
    )
    if ($Domain -and -not $ComputerName) {
        $ComputerName = (Get-ADDomainController -Filter * -Server $Domain).HostName
    }
    foreach ($Computer in $ComputerName) {
        $ServerGlobalQueryBlockList = Get-DnsServerGlobalQueryBlockList -ComputerName $Computer
        foreach ($_ in $ServerGlobalQueryBlockList) {
            if ($Formatted) {
                [PSCustomObject] @{
                    Enable       = $_.Enable
                    List         = $_.List -join $Splitter
                    GatheredFrom = $Computer
                }
            } else {
                [PSCustomObject] @{
                    Enable       = $_.Enable
                    List         = $_.List
                    GatheredFrom = $Computer
                }
            }
        }
    }
}
function Get-WinDnsServerRecursion {
    <#
    .SYNOPSIS
    Retrieves DNS server recursion settings from specified computers.
 
    .DESCRIPTION
    This function retrieves DNS server recursion settings from the specified computers. If no ComputerName is provided, it retrieves the settings from the domain controller associated with the specified domain.
 
    .PARAMETER ComputerName
    Specifies the names of the computers from which to retrieve DNS server recursion settings.
 
    .PARAMETER Domain
    Specifies the domain from which to retrieve DNS server recursion settings. Defaults to the current user's DNS domain.
 
    .EXAMPLE
    Get-WinDnsServerRecursion -ComputerName "Server01", "Server02" -Domain "contoso.com"
    Retrieves DNS server recursion settings from Server01 and Server02 in the contoso.com domain.
 
    .EXAMPLE
    Get-WinDnsServerRecursion -Domain "fabrikam.com"
    Retrieves DNS server recursion settings from the domain controller associated with the fabrikam.com domain.
 
    #>

    [CmdLetBinding()]
    param(
        [string[]] $ComputerName,
        [string] $Domain = $ENV:USERDNSDOMAIN
    )
    if ($Domain -and -not $ComputerName) {
        $ComputerName = (Get-ADDomainController -Filter * -Server $Domain).HostName
    }
    foreach ($Computer in $ComputerName) {
        $DnsServerRecursion = Get-DnsServerRecursion -ComputerName $Computer
        foreach ($_ in $DnsServerRecursion) {
            [PSCustomObject] @{
                AdditionalTimeout = $_.AdditionalTimeout
                Enable            = $_.Enable
                RetryInterval     = $_.RetryInterval
                SecureResponse    = $_.SecureResponse
                Timeout           = $_.Timeout
                GatheredFrom      = $Computer
            }
        }
    }
}

function Get-WinDnsServerRecursionScope {
    <#
    .SYNOPSIS
    Retrieves DNS server recursion scope settings from specified computers.
 
    .DESCRIPTION
    This function retrieves DNS server recursion scope settings from the specified computers. If no ComputerName is provided, it retrieves the settings from the domain controller associated with the specified domain.
 
    .PARAMETER ComputerName
    Specifies the names of the computers from which to retrieve DNS server recursion scope settings.
 
    .PARAMETER Domain
    Specifies the domain from which to retrieve DNS server recursion scope settings. Defaults to the current user's DNS domain.
 
    .EXAMPLE
    Get-WinDnsServerRecursionScope -ComputerName "Server01", "Server02" -Domain "contoso.com"
    Retrieves DNS server recursion scope settings from Server01 and Server02 in the contoso.com domain.
 
    .EXAMPLE
    Get-WinDnsServerRecursionScope -Domain "fabrikam.com"
    Retrieves DNS server recursion scope settings from the domain controller associated with the fabrikam.com domain.
 
    #>

    [CmdLetBinding()]
    param(
        [string[]] $ComputerName,
        [string] $Domain = $ENV:USERDNSDOMAIN
    )
    if ($Domain -and -not $ComputerName) {
        $ComputerName = (Get-ADDomainController -Filter * -Server $Domain).HostName
    }
    foreach ($Computer in $ComputerName) {
        $DnsServerRecursionScope = Get-DnsServerRecursionScope -ComputerName $Computer
        foreach ($_ in $DnsServerRecursionScope) {
            [PSCustomObject] @{
                Name            = $_.Name
                Forwarder       = $_.Forwarder
                EnableRecursion = $_.EnableRecursion
                GatheredFrom    = $Computer
            }
        }
    }
}

function Get-WinDnsServerResponseRateLimiting {
    <#
    .SYNOPSIS
    Retrieves DNS server response rate limiting settings from specified computers.
 
    .DESCRIPTION
    This function retrieves DNS server response rate limiting settings from the specified computers. If no ComputerName is provided, it retrieves the settings from the domain controller associated with the specified domain.
 
    .PARAMETER ComputerName
    Specifies the names of the computers from which to retrieve DNS server response rate limiting settings.
 
    .PARAMETER Domain
    Specifies the domain from which to retrieve DNS server response rate limiting settings. Defaults to the current user's DNS domain.
 
    .EXAMPLE
    Get-WinDnsServerResponseRateLimiting -ComputerName "Server01", "Server02" -Domain "contoso.com"
    Retrieves DNS server response rate limiting settings from Server01 and Server02 in the contoso.com domain.
 
    .EXAMPLE
    Get-WinDnsServerResponseRateLimiting -Domain "fabrikam.com"
    Retrieves DNS server response rate limiting settings from the domain controller associated with the fabrikam.com domain.
    #>

    [CmdLetBinding()]
    param(
        [string[]] $ComputerName,
        [string] $Domain = $ENV:USERDNSDOMAIN
    )
    if ($Domain -and -not $ComputerName) {
        $ComputerName = (Get-ADDomainController -Filter * -Server $Domain).HostName
    }
    foreach ($Computer in $ComputerName) {
        $DnsServerResponseRateLimiting = Get-DnsServerResponseRateLimiting -ComputerName $Computer
        foreach ($_ in $DnsServerResponseRateLimiting) {
            [PSCustomObject] @{
                ResponsesPerSec           = $_.ResponsesPerSec
                ErrorsPerSec              = $_.ErrorsPerSec
                WindowInSec               = $_.WindowInSec
                IPv4PrefixLength          = $_.IPv4PrefixLength
                IPv6PrefixLength          = $_.IPv6PrefixLength
                LeakRate                  = $_.LeakRate
                TruncateRate              = $_.TruncateRate
                MaximumResponsesPerWindow = $_.MaximumResponsesPerWindow
                Mode                      = $_.Mode
                GatheredFrom              = $Computer
            }
        }
    }
}
function Get-WinDnsServerSettings {
    <#
    .SYNOPSIS
    Retrieves DNS server settings for a specified computer.
 
    .DESCRIPTION
    This function retrieves various DNS server settings for a specified computer.
 
    .PARAMETER ComputerName
    Specifies the name of the computer for which to retrieve DNS server settings.
 
    .EXAMPLE
    Get-WinDnsServerSettings -ComputerName "AD1.ad.evotec.xyz"
    Retrieves DNS server settings for the computer "AD1.ad.evotec.xyz".
 
    #>

    [CmdLetBinding()]
    param(
        [string] $ComputerName
    )

    <#
    ComputerName : AD1.ad.evotec.xyz
    MajorVersion : 10
    MinorVersion : 0
    BuildNumber : 14393
    IsReadOnlyDC : False
    EnableDnsSec : False
    EnableIPv6 : True
    EnableOnlineSigning : True
    NameCheckFlag : 2
    AddressAnswerLimit : 0
    XfrConnectTimeout(s) : 30
    BootMethod : 3
    AllowUpdate : True
    UpdateOptions : 783
    DsAvailable : True
    DisableAutoReverseZone : False
    AutoCacheUpdate : False
    RoundRobin : True
    LocalNetPriority : True
    StrictFileParsing : False
    LooseWildcarding : False
    BindSecondaries : False
    WriteAuthorityNS : False
    ForwardDelegations : False
    AutoConfigFileZones : 1
    EnableDirectoryPartitions : True
    RpcProtocol : 5
    EnableVersionQuery : 0
    EnableDuplicateQuerySuppression : True
    LameDelegationTTL : 00:00:00
    AutoCreateDelegation : 2
    AllowCnameAtNs : True
    RemoteIPv4RankBoost : 5
    RemoteIPv6RankBoost : 0
    EnableRsoForRodc : True
    MaximumRodcRsoQueueLength : 300
    MaximumRodcRsoAttemptsPerCycle : 100
    OpenAclOnProxyUpdates : True
    NoUpdateDelegations : False
    EnableUpdateForwarding : False
    MaxResourceRecordsInNonSecureUpdate : 30
    EnableWinsR : True
    LocalNetPriorityMask : 255
    DeleteOutsideGlue : False
    AppendMsZoneTransferTag : False
    AllowReadOnlyZoneTransfer : False
    MaximumUdpPacketSize : 4000
    TcpReceivePacketSize : 65536
    EnableSendErrorSuppression : True
    SelfTest : 4294967295
    XfrThrottleMultiplier : 10
    SilentlyIgnoreCnameUpdateConflicts : False
    EnableIQueryResponseGeneration : False
    SocketPoolSize : 2500
    AdminConfigured : True
    SocketPoolExcludedPortRanges : {}
    ForestDirectoryPartitionBaseName : ForestDnsZones
    DomainDirectoryPartitionBaseName : DomainDnsZones
    ServerLevelPluginDll :
    EnableRegistryBoot :
    PublishAutoNet : False
    QuietRecvFaultInterval(s) : 0
    QuietRecvLogInterval(s) : 0
    ReloadException : False
    SyncDsZoneSerial : 2
    EnableDuplicateQuerySuppression : True
    SendPort : Random
    MaximumSignatureScanPeriod : 2.00:00:00
    MaximumTrustAnchorActiveRefreshInterval : 15.00:00:00
    ListeningIPAddress : {192.168.240.189}
    AllIPAddress : {192.168.240.189}
    ZoneWritebackInterval : 00:01:00
    RootTrustAnchorsURL : https://data.iana.org/root-anchors/root-anchors.xml
    ScopeOptionValue : 0
    IgnoreServerLevelPolicies : False
    IgnoreAllPolicies : False
    VirtualizationInstanceOptionValue : 0
 
    #>


    $DnsServerSetting = Get-DnsServerSetting -ComputerName $ComputerName -All
    foreach ($_ in $DnsServerSetting) {
        [PSCustomObject] @{
            AllIPAddress       = $_.AllIPAddress
            ListeningIPAddress = $_.ListeningIPAddress
            BuildNumber        = $_.BuildNumber
            ComputerName       = $_.ComputerName
            EnableDnsSec       = $_.EnableDnsSec
            EnableIPv6         = $_.EnableIPv6
            IsReadOnlyDC       = $_.IsReadOnlyDC
            MajorVersion       = $_.MajorVersion
            MinorVersion       = $_.MinorVersion
            GatheredFrom       = $ComputerName
        }
    }
}

#Get-WinDnsServerSettings -ComputerName 'AD1'
#Get-DnsServerSetting -ComputerName AD1 -All
function Get-WinDnsServerVirtualizationInstance {
    <#
    .SYNOPSIS
    Retrieves information about DNS server virtualization instances on a specified computer.
 
    .DESCRIPTION
    This function retrieves information about DNS server virtualization instances on a specified computer.
 
    .PARAMETER ComputerName
    Specifies the name of the computer from which to retrieve DNS server virtualization instances.
 
    .EXAMPLE
    Get-WinDnsServerVirtualizationInstance -ComputerName "Server01"
    Retrieves DNS server virtualization instances from a computer named Server01.
 
    #>

    [CmdLetBinding()]
    param(
        [string] $ComputerName
    )

    $DnsServerVirtualizationInstance = Get-DnsServerVirtualizationInstance -ComputerName $ComputerName
    foreach ($_ in $DnsServerVirtualizationInstance) {
        [PSCustomObject] @{
            VirtualizationInstance = $_.VirtualizationInstance
            FriendlyName           = $_.FriendlyName
            Description            = $_.Description
            GatheredFrom           = $ComputerName
        }
    }
}
$Script:Rights = @{
    "Self"                            = @{
        "InheritedObjectAceTypePresent"                       = ""
        "ObjectAceTypePresent"                                = ""
        "ObjectAceTypePresent, InheritedObjectAceTypePresent" = ""
        'None'                                                = ""
    }
    "DeleteChild, DeleteTree, Delete" = @{
        "InheritedObjectAceTypePresent"                       = "DeleteChild, DeleteTree, Delete"
        "ObjectAceTypePresent"                                = "DeleteChild, DeleteTree, Delete"
        "ObjectAceTypePresent, InheritedObjectAceTypePresent" = "DeleteChild, DeleteTree, Delete"
        'None'                                                = "DeleteChild, DeleteTree, Delete"
    }
    "GenericRead"                     = @{
        "InheritedObjectAceTypePresent"                       = "Read Permissions,List Contents,Read All Properties,List"
        "ObjectAceTypePresent"                                = "Read Permissions,List Contents,Read All Properties,List"
        "ObjectAceTypePresent, InheritedObjectAceTypePresent" = "Read Permissions,List Contents,Read All Properties,List"
        'None'                                                = "Read Permissions,List Contents,Read All Properties,List"
    }
    "CreateChild"                     = @{
        "InheritedObjectAceTypePresent"                       = "Create"
        "ObjectAceTypePresent"                                = "Create"
        "ObjectAceTypePresent, InheritedObjectAceTypePresent" = "Create"
        'None'                                                = "Create"
    }
    "DeleteChild"                     = @{
        "InheritedObjectAceTypePresent"                       = "Delete"
        "ObjectAceTypePresent"                                = "Delete"
        "ObjectAceTypePresent, InheritedObjectAceTypePresent" = "Delete"
        'None'                                                = "Delete"
    }
    "GenericAll"                      = @{
        "InheritedObjectAceTypePresent"                       = "Full Control"
        "ObjectAceTypePresent"                                = "Full Control"
        "ObjectAceTypePresent, InheritedObjectAceTypePresent" = "Full Control"
        'None'                                                = "Full Control"
    }
    "CreateChild, DeleteChild"        = @{
        "InheritedObjectAceTypePresent"                       = "Create/Delete"
        "ObjectAceTypePresent"                                = "Create/Delete"
        "ObjectAceTypePresent, InheritedObjectAceTypePresent" = "Create/Delete"
        'None'                                                = "Create/Delete"
    }
    "ReadProperty, WriteProperty"     = @{
        "InheritedObjectAceTypePresent"                       = "Read All Properties;Write All Properties"
        "ObjectAceTypePresent"                                = "Read All Properties;Write All Properties"
        "ObjectAceTypePresent, InheritedObjectAceTypePresent" = "Read All Properties;Write All Properties"
        'None'                                                = "Read All Properties;Write All Properties"
    }
    "WriteProperty"                   = @{
        "InheritedObjectAceTypePresent"                       = "Write All Properties"
        "ObjectAceTypePresent"                                = "Write"
        "ObjectAceTypePresent, InheritedObjectAceTypePresent" = "Write"
        'None'                                                = "Write All Properties"
    }
    "ReadProperty"                    = @{
        "InheritedObjectAceTypePresent"                       = "Read All Properties"
        "ObjectAceTypePresent"                                = "Read"
        "ObjectAceTypePresent, InheritedObjectAceTypePresent" = "Read"
        'None'                                                = "Read All Properties"
    }
}
function New-ActiveDirectoryAccessRule {
    <#
    .SYNOPSIS
    Creates a new Active Directory access rule based on the provided parameters.
 
    .DESCRIPTION
    This function creates a new Active Directory access rule based on the provided parameters. It allows for flexibility in defining the access rule by specifying various attributes such as object type, inheritance type, inherited object type, access control type, access rule, and identity.
 
    .PARAMETER ActiveDirectoryAccessRule
    Specifies an existing Active Directory access rule object to use as a template for the new rule.
 
    .PARAMETER ObjectType
    Specifies the type of object for which the access rule applies.
 
    .PARAMETER InheritanceType
    Specifies the inheritance type for the access rule.
 
    .PARAMETER InheritedObjectType
    Specifies the inherited object type for the access rule.
 
    .PARAMETER AccessControlType
    Specifies the type of access control for the access rule.
 
    .PARAMETER AccessRule
    Specifies the access rule to apply.
 
    .PARAMETER Identity
    Specifies the identity to which the access rule applies.
 
    .EXAMPLE
    New-ActiveDirectoryAccessRule -ObjectType "User" -InheritanceType "All" -AccessControlType "Allow" -AccessRule "Read" -Identity "Domain Admins"
    Creates a new Active Directory access rule allowing "Domain Admins" to read objects of type "User" with inheritance for all child objects.
 
    .EXAMPLE
    New-ActiveDirectoryAccessRule -ActiveDirectoryAccessRule $existingRule -InheritanceType "None" -AccessControlType "Deny" -AccessRule "Write" -Identity "Guests"
    Creates a new Active Directory access rule based on an existing rule, denying "Guests" the ability to write to objects with no inheritance.
 
    #>

    [CmdletBinding()]
    param(
        $ActiveDirectoryAccessRule,
        $ObjectType,
        $InheritanceType,
        $InheritedObjectType,
        $AccessControlType,
        $AccessRule,
        $Identity
    )

    try {
        if ($ActiveDirectoryAccessRule) {
            $AccessRuleToAdd = $ActiveDirectoryAccessRule
        } elseif ($ObjectType -and $InheritanceType -and $InheritedObjectType) {
            $ObjectTypeGuid = Convert-ADSchemaToGuid -SchemaName $ObjectType
            $InheritedObjectTypeGuid = Convert-ADSchemaToGuid -SchemaName $InheritedObjectType
            if ($ObjectTypeGuid -and $InheritedObjectTypeGuid) {
                $AccessRuleToAdd = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($Identity, $AccessRule, $AccessControlType, $ObjectTypeGuid, $InheritanceType, $InheritedObjectTypeGuid)
            } else {
                if (-not $ObjectTypeGuid -and -not $InheritedObjectTypeGuid) {
                    Write-Warning "Add-PrivateACL - Object type '$ObjectType' or '$InheritedObjectType' not found in schema"
                } elseif (-not $ObjectTypeGuid) {
                    Write-Warning "Add-PrivateACL - Object type '$ObjectType' not found in schema"
                } else {
                    Write-Warning "Add-PrivateACL - Object type '$InheritedObjectType' not found in schema"
                }
                return
            }
        } elseif ($ObjectType -and $InheritanceType) {
            $ObjectTypeGuid = Convert-ADSchemaToGuid -SchemaName $ObjectType
            if ($ObjectTypeGuid) {
                $AccessRuleToAdd = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($Identity, $AccessRule, $AccessControlType, $ObjectTypeGuid, $InheritanceType)
            } else {
                Write-Warning "Add-PrivateACL - Object type '$ObjectType' not found in schema"
                return
            }
        } elseif ($ObjectType) {
            $ObjectTypeGuid = Convert-ADSchemaToGuid -SchemaName $ObjectType
            if ($ObjectTypeGuid) {
                $AccessRuleToAdd = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($Identity, $AccessRule, $AccessControlType, $ObjectTypeGuid)
            } else {
                Write-Warning "Add-PrivateACL - Object type '$ObjectType' not found in schema"
                return
            }
        } else {
            $AccessRuleToAdd = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($Identity, $AccessRule, $AccessControlType)
        }
    } catch {
        Write-Warning "Add-PrivateACL - Error creating ActiveDirectoryAccessRule: $_"
        return
    }
    $AccessRuleToAdd
}
function New-ADForestDrives {
    <#
    .SYNOPSIS
    Maps network drives for all domains in a specified Active Directory forest.
 
    .DESCRIPTION
    The New-ADForestDrives function maps network drives for all domains in a specified Active Directory forest. It retrieves domain information and maps drives accordingly.
 
    .PARAMETER ForestName
    Specifies the name of the Active Directory forest to map drives for.
 
    .PARAMETER ObjectDN
    Specifies the distinguished name of the object to map drives for.
 
    .EXAMPLE
    New-ADForestDrives -ForestName "example.com" -ObjectDN "CN=Users,DC=example,DC=com"
    This example maps network drives for the specified forest and object distinguished name.
 
    .NOTES
    File Name : New-ADForestDrives.ps1
    Author : Your Name
    Prerequisite : This function requires the Active Directory module.
 
    #>

    [cmdletbinding()]
    param(
        [string] $ForestName,
        [string] $ObjectDN
    )
    if (-not $Global:ADDrivesMapped) {
        if ($ForestName) {
            $Forest = Get-ADForest -Identity $ForestName
        } else {
            $Forest = Get-ADForest
        }
        if ($ObjectDN) {
            $DNConverted = (ConvertFrom-DistinguishedName -DistinguishedName $ObjectDN -ToDC) -replace '=' -replace ','
            if (-not(Get-PSDrive -Name $DNConverted -ErrorAction SilentlyContinue)) {
                try {
                    if ($Server) {
                        $null = New-PSDrive -Name $DNConverted -Root '' -PSProvider ActiveDirectory -Server $Server.Hostname[0] -Scope Global -WhatIf:$false
                        Write-Verbose "New-ADForestDrives - Mapped drive $Domain / $($Server.Hostname[0])"
                    } else {
                        $null = New-PSDrive -Name $DNConverted -Root '' -PSProvider ActiveDirectory -Server $Domain -Scope Global -WhatIf:$false
                    }
                } catch {
                    Write-Warning "New-ADForestDrives - Couldn't map new AD psdrive for $Domain / $($Server.Hostname[0])"
                }
            }
        } else {
            foreach ($Domain in $Forest.Domains) {
                try {
                    $Server = Get-ADDomainController -Discover -DomainName $Domain -Writable
                    $DomainInformation = Get-ADDomain -Server $Server.Hostname[0]
                } catch {
                    Write-Warning "New-ADForestDrives - Can't process domain $Domain - $($_.Exception.Message)"
                    continue
                }
                $ObjectDN = $DomainInformation.DistinguishedName
                $DNConverted = (ConvertFrom-DistinguishedName -DistinguishedName $ObjectDN -ToDC) -replace '=' -replace ','
                if (-not(Get-PSDrive -Name $DNConverted -ErrorAction SilentlyContinue)) {
                    try {
                        if ($Server) {
                            $null = New-PSDrive -Name $DNConverted -Root '' -PSProvider ActiveDirectory -Server $Server.Hostname[0] -Scope Global -WhatIf:$false
                            Write-Verbose "New-ADForestDrives - Mapped drive $Domain / $Server"
                        } else {
                            $null = New-PSDrive -Name $DNConverted -Root '' -PSProvider ActiveDirectory -Server $Domain -Scope Global -WhatIf:$false
                        }
                    } catch {
                        Write-Warning "New-ADForestDrives - Couldn't map new AD psdrive for $Domain / $Server $($_.Exception.Message)"
                    }
                }
            }
        }
        $Global:ADDrivesMapped = $true
    }
}
function New-HTMLGroupDiagramDefault {
    <#
    .SYNOPSIS
    Creates a new HTML group diagram with customizable options.
 
    .DESCRIPTION
    This function creates a new HTML group diagram with customizable options. It allows for displaying Active Directory groups and their members in a visual diagram format.
 
    .PARAMETER ADGroup
    Specifies an array of Active Directory group objects to be displayed in the diagram.
 
    .PARAMETER HideAppliesTo
    Specifies whether to hide groups based on their membership type. Valid values are 'Default', 'Hierarchical', or 'Both'. Default is 'Both'.
 
    .PARAMETER HideComputers
    Indicates whether to hide computer objects in the diagram.
 
    .PARAMETER HideUsers
    Indicates whether to hide user objects in the diagram.
 
    .PARAMETER HideOther
    Indicates whether to hide other types of objects in the diagram.
 
    .PARAMETER DataTableID
    Specifies the ID of the data table associated with the diagram.
 
    .PARAMETER ColumnID
    Specifies the ID of the column associated with the diagram.
 
    .PARAMETER Online
    Indicates whether to display user nodes as online or offline.
 
    .EXAMPLE
    New-HTMLGroupDiagramDefault -ADGroup $ADGroupArray -HideAppliesTo 'Default' -HideComputers -DataTableID 'DataTable1' -ColumnID 1 -Online
    Creates a new HTML group diagram displaying the specified AD groups with default settings, hiding computers, showing online users, and associating with a data table.
 
    .NOTES
    Author: Your Name
    Date: Current Date
    #>

    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [string] $DataTableID,
        [int] $ColumnID,
        [switch] $Online,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3
    )
    New-HTMLDiagram -Height 'calc(100vh - 200px)' {
        #if ($DataTableID) {
        # New-DiagramEvent -ID $DataTableID -ColumnID $ColumnID
        #}
        #New-DiagramOptionsLayout -HierarchicalEnabled $true -HierarchicalDirection FromLeftToRight #-HierarchicalSortMethod directed
        #New-DiagramOptionsPhysics -Enabled $true -HierarchicalRepulsionAvoidOverlap 1 -HierarchicalRepulsionNodeDistance 50
        New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
        if ($ADGroup) {
            # Add it's members to diagram
            foreach ($ADObject in $ADGroup) {
                # Lets build our diagram
                #[int] $Level = $($ADObject.Nesting) + 1
                $ID = "$($ADObject.DomainName)$($ADObject.DistinguishedName)"
                #[int] $LevelParent = $($ADObject.Nesting)
                $IDParent = "$($ADObject.ParentGroupDomain)$($ADObject.ParentGroupDN)"

                if ($ADObject.Type -eq 'User') {
                    if (-not $HideUsers -or $HideAppliesTo -notin 'Both', 'Default') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageUser
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid user -IconColor LightSteelBlue
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Blue -ArrowsToEnabled -Dashes
                    }
                } elseif ($ADObject.Type -eq 'Group') {
                    if ($ADObject.Nesting -eq -1) {
                        $BorderColor = 'Red'
                        $Image = $Script:ConfigurationIcons.ImageGroup
                    } else {
                        $BorderColor = 'Blue'
                        $Image = $Script:ConfigurationIcons.ImageGroupNested
                    }
                    $SummaryMembers = -join ('Total: ', $ADObject.TotalMembers, ' Direct: ', $ADObject.DirectMembers, ' Groups: ', $ADObject.DirectGroups, ' Indirect: ', $ADObject.IndirectMembers)
                    $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName + [System.Environment]::NewLine + $SummaryMembers
                    if ($Online) {
                        New-DiagramNode -Id $ID -Label $Label -Image $Image -ColorBorder $BorderColor
                    } else {
                        New-DiagramNode -Id $ID -Label $Label -IconSolid user-friends -IconColor VeryLightGrey
                    }
                    New-DiagramLink -ColorOpacity 0.5 -From $ID -To $IDParent -Color Orange -ArrowsToEnabled
                } elseif ($ADObject.Type -eq 'Computer') {
                    if (-not $HideComputers -or $HideAppliesTo -notin 'Both', 'Default') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageComputer
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid desktop -IconColor LightGray
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Arsenic -ArrowsToEnabled -Dashes
                    }
                } else {
                    if (-not $HideOther -or $HideAppliesTo -notin 'Both', 'Default') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageOther
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid robot -IconColor LightSalmon
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Boulder -ArrowsToEnabled -Dashes
                    }
                }
            }
        }
    } -EnableFiltering:$EnableDiagramFiltering.IsPresent -MinimumFilteringChars $DiagramFilteringMinimumCharacters -EnableFilteringButton:$EnableDiagramFilteringButton.IsPresent
}
function New-HTMLGroupDiagramHierachical {
    <#
    .SYNOPSIS
    Creates an HTML diagram representing Active Directory groups and their members in a hierarchical layout.
 
    .DESCRIPTION
    The New-HTMLGroupDiagramHierachical function generates an HTML diagram that visually represents Active Directory groups and their members in a hierarchical structure. The diagram includes nodes for users, groups, computers, and other objects, with customizable options for displaying and organizing the information.
 
    .PARAMETER ADGroup
    Specifies an array of Active Directory objects (groups) to be included in the diagram.
 
    .PARAMETER HideAppliesTo
    Specifies whether to hide specific types of objects in the diagram. Valid values are 'Default', 'Hierarchical', and 'Both'. Default value is 'Both'.
 
    .PARAMETER HideComputers
    Indicates whether to hide computer objects in the diagram.
 
    .PARAMETER HideUsers
    Indicates whether to hide user objects in the diagram.
 
    .PARAMETER HideOther
    Indicates whether to hide other types of objects in the diagram.
 
    .PARAMETER Online
    Indicates whether to display online status information in the diagram.
 
    .EXAMPLE
    New-HTMLGroupDiagramHierachical -ADGroup $ADGroupArray -HideAppliesTo 'Both' -Online
    Generates an HTML diagram displaying all objects in the $ADGroupArray with online status information included.
 
    .EXAMPLE
    New-HTMLGroupDiagramHierachical -ADGroup $ADGroupArray -HideComputers -HideUsers
    Generates an HTML diagram excluding computer and user objects from the $ADGroupArray.
 
    .NOTES
    File Name : New-HTMLGroupDiagramHierachical.ps1
    Author : Your Name
    Prerequisite : PowerShell V5
    Copyright 2021 - Your Company
    #>


    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [switch] $Online,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3
    )
    New-HTMLDiagram -Height 'calc(100vh - 200px)' {
        New-DiagramOptionsLayout -HierarchicalEnabled $true #-HierarchicalDirection FromLeftToRight #-HierarchicalSortMethod directed
        New-DiagramOptionsPhysics -Enabled $true -HierarchicalRepulsionAvoidOverlap 1 -HierarchicalRepulsionNodeDistance 200
        #New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
        if ($ADGroup) {
            # Add it's members to diagram
            foreach ($ADObject in $ADGroup) {
                # Lets build our diagram
                [int] $Level = $($ADObject.Nesting) + 1
                $ID = "$($ADObject.DomainName)$($ADObject.DistinguishedName)$Level"
                [int] $LevelParent = $($ADObject.Nesting)
                $IDParent = "$($ADObject.ParentGroupDomain)$($ADObject.ParentGroupDN)$LevelParent"

                [int] $Level = $($ADObject.Nesting) + 1
                if ($ADObject.Type -eq 'User') {
                    if (-not $HideUsers -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageUser -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -Level $Level -IconSolid user -IconColor LightSteelBlue
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Blue -ArrowsToEnabled -Dashes
                    }
                } elseif ($ADObject.Type -eq 'Group') {
                    if ($ADObject.Nesting -eq -1) {
                        $BorderColor = 'LightGreen'
                        $Image = $Script:ConfigurationIcons.ImageGroup
                        $IconSolid = 'user-friends'
                    } elseif ($ADObject.CircularIndirect -eq $true -or $ADObject.CircularDirect -eq $true) {
                        $Image = $Script:ConfigurationIcons.ImageGroupCircular
                        $BorderColor = 'PaleVioletRed'
                        $IconSolid = 'circle-notch'
                    } else {
                        $BorderColor = 'VeryLightGrey'
                        $Image = $Script:ConfigurationIcons.ImageGroupNested
                        $IconSolid = 'users'
                    }
                    $SummaryMembers = -join ('Total: ', $ADObject.TotalMembers, ' Direct: ', $ADObject.DirectMembers, ' Groups: ', $ADObject.DirectGroups, ' Indirect: ', $ADObject.IndirectMembers)
                    if ($ADObject.CircularIndirect -eq $true -or $ADObject.CircularDirect -eq $true) {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName + [System.Environment]::NewLine + $SummaryMembers + [System.Environment]::NewLine + "Circular: $True"
                    } else {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName + [System.Environment]::NewLine + $SummaryMembers
                    }
                    if ($Online) {
                        New-DiagramNode -Id $ID -Label $Label -Image $Image -Level $Level -ColorBorder $BorderColor
                    } else {
                        New-DiagramNode -Id $ID -Label $Label -Level $Level -IconSolid $IconSolid -IconColor $BorderColor
                    }
                    New-DiagramLink -ColorOpacity 0.5 -From $ID -To $IDParent -Color Orange -ArrowsToEnabled
                } elseif ($ADObject.Type -eq 'Computer') {
                    if (-not $HideComputers -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageComputer -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid desktop -IconColor LightGray -Level $Level
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Arsenic -ArrowsToEnabled -Dashes
                    }
                } else {
                    if (-not $HideOther -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageOther -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid robot -IconColor LightSalmon -Level $Level
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Boulder -ArrowsToEnabled -Dashes
                    }
                }
            }
        }
    } -EnableFiltering:$EnableDiagramFiltering.IsPresent -MinimumFilteringChars $DiagramFilteringMinimumCharacters -EnableFilteringButton:$EnableDiagramFilteringButton.IsPresent
}
function New-HTMLGroupDiagramSummary {
    <#
    .SYNOPSIS
    Creates an HTML group diagram summary based on the provided Active Directory group information.
 
    .DESCRIPTION
    The New-HTMLGroupDiagramSummary function generates an HTML diagram summary representing the relationships between Active Directory groups and their members. It allows customization of the diagram layout and appearance based on the input parameters.
 
    .PARAMETER ADGroup
    Specifies an array of Active Directory group objects to be included in the diagram.
 
    .PARAMETER HideAppliesTo
    Specifies whether to hide specific types of objects in the diagram. Valid values are 'Default', 'Hierarchical', and 'Both'.
 
    .PARAMETER HideComputers
    Indicates whether to hide computer objects in the diagram.
 
    .PARAMETER HideUsers
    Indicates whether to hide user objects in the diagram.
 
    .PARAMETER HideOther
    Indicates whether to hide other types of objects in the diagram.
 
    .PARAMETER DataTableID
    Specifies the ID of the data table associated with the diagram.
 
    .PARAMETER ColumnID
    Specifies the ID of the column associated with the diagram.
 
    .PARAMETER Online
    Indicates whether to display online status information in the diagram.
 
    .EXAMPLE
    New-HTMLGroupDiagramSummary -ADGroup $ADGroupArray -HideAppliesTo 'Default' -HideComputers -Online
    Generates an HTML group diagram summary for the specified AD groups, hiding computers and displaying only default objects with online status.
 
    #>

    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [string] $DataTableID,
        [int] $ColumnID,
        [switch] $Online,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3
    )
    $ConnectionsTracker = @{}
    New-HTMLDiagram -Height 'calc(100vh - 200px)' {
        #if ($DataTableID) {
        # New-DiagramEvent -ID $DataTableID -ColumnID $ColumnID
        #}
        #New-DiagramOptionsLayout -HierarchicalEnabled $true -HierarchicalDirection FromLeftToRight #-HierarchicalSortMethod directed
        #New-DiagramOptionsPhysics -Enabled $true -HierarchicalRepulsionAvoidOverlap 1 -HierarchicalRepulsionNodeDistance 50
        New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
        if ($ADGroup) {
            # Add it's members to diagram
            foreach ($ADObject in $ADGroup) {
                # Lets build our diagram
                # This diagram of Summary doesn't use level checking because it's a summary of a groups, and the level will be different per group
                # This means that it will look a bit different than what is there when comparing 1 to 1 with the other diagrams
                #[int] $Level = $($ADObject.Nesting) + 1
                $ID = "$($ADObject.DomainName)$($ADObject.DistinguishedName)"
                #[int] $LevelParent = $($ADObject.Nesting)
                $IDParent = "$($ADObject.ParentGroupDomain)$($ADObject.ParentGroupDN)"
                # We track connection for ID to make sure that only once the conenction is added
                if (-not $ConnectionsTracker[$ID]) {
                    $ConnectionsTracker[$ID] = @{}
                }
                if (-not $ConnectionsTracker[$ID][$IDParent]) {
                    if ($ADObject.Type -eq 'User') {
                        if (-not $HideUsers -or $HideAppliesTo -notin 'Both', 'Default') {
                            $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                            if ($Online) {
                                New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageUser
                            } else {
                                New-DiagramNode -Id $ID -Label $Label -IconSolid user -IconColor LightSteelBlue
                            }
                            New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Blue -ArrowsToEnabled -Dashes
                        }
                    } elseif ($ADObject.Type -eq 'Group') {
                        if ($ADObject.Nesting -eq -1) {
                            $BorderColor = 'Red'
                            $Image = $Script:ConfigurationIcons.ImageGroup
                        } else {
                            $BorderColor = 'Blue'
                            $Image = $Script:ConfigurationIcons.ImageGroupNested
                        }
                        $SummaryMembers = -join ('Total: ', $ADObject.TotalMembers, ' Direct: ', $ADObject.DirectMembers, ' Groups: ', $ADObject.DirectGroups, ' Indirect: ', $ADObject.IndirectMembers)
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName + [System.Environment]::NewLine + $SummaryMembers
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Image -ArrowsToEnabled -ColorBorder $BorderColor
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -ArrowsToEnabled -IconSolid user-friends -IconColor VeryLightGrey
                        }
                        New-DiagramLink -ColorOpacity 0.5 -From $ID -To $IDParent -Color Orange -ArrowsToEnabled
                    } elseif ($ADObject.Type -eq 'Computer') {
                        if (-not $HideComputers -or $HideAppliesTo -notin 'Both', 'Default') {
                            $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                            if ($Online) {
                                New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageComputer
                            } else {
                                New-DiagramNode -Id $ID -Label $Label -IconSolid desktop -IconColor LightGray
                            }
                            New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Arsenic -ArrowsToEnabled -Dashes
                        }
                    } else {
                        if (-not $HideOther -or $HideAppliesTo -notin 'Both', 'Default') {
                            $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                            if ($Online) {
                                New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageOther
                            } else {
                                New-DiagramNode -Id $ID -Label $Label -IconSolid robot -IconColor LightSalmon
                            }
                            New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Boulder -ArrowsToEnabled -Dashes
                        }
                    }
                    $ConnectionsTracker[$ID][$IDParent] = $true
                }
            }
        }
    } -EnableFiltering:$EnableDiagramFiltering.IsPresent -MinimumFilteringChars $DiagramFilteringMinimumCharacters -EnableFilteringButton:$EnableDiagramFilteringButton.IsPresent
}
function New-HTMLGroupDiagramSummaryHierarchical {
    <#
    .SYNOPSIS
    Creates an HTML group diagram summary in a hierarchical layout based on the provided Active Directory group information.
 
    .DESCRIPTION
    The New-HTMLGroupDiagramSummaryHierarchical function generates an HTML diagram summary representing the relationships between Active Directory groups and their members in a hierarchical structure. It allows customization of the diagram layout and appearance based on the input parameters.
 
    .PARAMETER ADGroup
    Specifies an array of Active Directory group objects to be included in the diagram.
 
    .PARAMETER HideAppliesTo
    Specifies whether to hide specific types of objects in the diagram. Valid values are 'Default', 'Hierarchical', and 'Both'.
 
    .PARAMETER HideComputers
    Indicates whether to hide computer objects in the diagram.
 
    .PARAMETER HideUsers
    Indicates whether to hide user objects in the diagram.
 
    .PARAMETER HideOther
    Indicates whether to hide other types of objects in the diagram.
 
    .PARAMETER Online
    Indicates whether to display online status information in the diagram.
 
    .EXAMPLE
    New-HTMLGroupDiagramSummaryHierarchical -ADGroup $ADGroupArray -HideAppliesTo 'Default' -HideComputers -Online
    Generates an HTML group diagram summary for the specified AD groups, hiding computers and displaying only default objects with online status in a hierarchical layout.
 
    #>

    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [switch] $Online,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3
    )
    New-HTMLDiagram -Height 'calc(100vh - 200px)' {
        New-DiagramOptionsLayout -HierarchicalEnabled $true #-HierarchicalDirection FromLeftToRight #-HierarchicalSortMethod directed
        New-DiagramOptionsPhysics -Enabled $true -HierarchicalRepulsionAvoidOverlap 1 -HierarchicalRepulsionNodeDistance 200
        #New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
        if ($ADGroup) {
            # Add it's members to diagram
            foreach ($ADObject in $ADGroup) {
                # Lets build our diagram
                # This diagram of Summary doesn't use level checking because it's a summary of a groups, and the level will be different per group
                # This means that it will look a bit different than what is there when comparing 1 to 1 with the other diagrams
                #[int] $Level = $($ADObject.Nesting) + 1
                $ID = "$($ADObject.DomainName)$($ADObject.DistinguishedName)"
                #[int] $LevelParent = $($ADObject.Nesting)
                $IDParent = "$($ADObject.ParentGroupDomain)$($ADObject.ParentGroupDN)"

                [int] $Level = $($ADObject.Nesting) + 1
                if ($ADObject.Type -eq 'User') {
                    if (-not $HideUsers -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageUser -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -Level $Level -IconSolid user -IconColor LightSteelBlue
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Blue -ArrowsToEnabled -Dashes
                    }
                } elseif ($ADObject.Type -eq 'Group') {
                    if ($ADObject.Nesting -eq -1) {
                        $BorderColor = 'Red'
                        $Image = $Script:ConfigurationIcons.ImageGroup
                    } else {
                        $BorderColor = 'Blue'
                        $Image = $Script:ConfigurationIcons.ImageGroupNested
                    }
                    $SummaryMembers = -join ('Total: ', $ADObject.TotalMembers, ' Direct: ', $ADObject.DirectMembers, ' Groups: ', $ADObject.DirectGroups, ' Indirect: ', $ADObject.IndirectMembers)
                    $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName + [System.Environment]::NewLine + $SummaryMembers
                    if ($Online) {
                        New-DiagramNode -Id $ID -Label $Label -Image $Image -Level $Level -ColorBorder $BorderColor
                    } else {
                        New-DiagramNode -Id $ID -Label $Label -Level $Level -IconSolid user-friends
                    }
                    New-DiagramLink -ColorOpacity 0.5 -From $ID -To $IDParent -Color Orange -ArrowsToEnabled
                } elseif ($ADObject.Type -eq 'Computer') {
                    if (-not $HideComputers -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageComputer -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid desktop -IconColor LightGray -Level $Level
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Arsenic -ArrowsToEnabled -Dashes
                    }
                } else {
                    if (-not $HideOther -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageOther -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid robot -IconColor LightSalmon -Level $Level
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Boulder -ArrowsToEnabled -Dashes
                    }
                }
            }
        }
    } -EnableFiltering:$EnableDiagramFiltering.IsPresent -MinimumFilteringChars $DiagramFilteringMinimumCharacters -EnableFilteringButton:$EnableDiagramFilteringButton.IsPresent
}
function New-HTMLGroupOfDiagramDefault {
    <#
    .SYNOPSIS
    Creates a default HTML group diagram with specified parameters.
 
    .DESCRIPTION
    This function creates a default HTML group diagram with the ability to customize various display options.
 
    .PARAMETER Identity
    Specifies an array of objects to be included in the diagram.
 
    .PARAMETER HideAppliesTo
    Specifies whether to hide elements based on their type. Valid values are 'Default', 'Hierarchical', or 'Both'. Default is 'Both'.
 
    .PARAMETER HideComputers
    Indicates whether to hide computer objects in the diagram.
 
    .PARAMETER HideUsers
    Indicates whether to hide user objects in the diagram.
 
    .PARAMETER HideOther
    Indicates whether to hide other types of objects in the diagram.
 
    .PARAMETER DataTableID
    Specifies the ID of the data table associated with the diagram.
 
    .PARAMETER ColumnID
    Specifies the ID of the column associated with the data table.
 
    .PARAMETER Online
    Indicates whether to display objects as online in the diagram.
 
    .EXAMPLE
    New-HTMLGroupOfDiagramDefault -Identity $ADObjects -HideUsers -DataTableID "DataTable1" -ColumnID 1 -Online
    Creates a default HTML group diagram with Active Directory objects, hides user objects, associates it with DataTable1, and displays objects as online.
 
    .EXAMPLE
    New-HTMLGroupOfDiagramDefault -Identity $ADObjects -HideComputers -HideAppliesTo 'Hierarchical'
    Creates a default HTML group diagram with Active Directory objects, hides computer objects, and only displays elements based on the hierarchical setting.
 
    #>

    [cmdletBinding()]
    param(
        [Array] $Identity,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [string] $DataTableID,
        [int] $ColumnID,
        [switch] $Online,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3
    )
    New-HTMLDiagram -Height 'calc(100vh - 200px)' {
        #if ($DataTableID) {
        # New-DiagramEvent -ID $DataTableID -ColumnID $ColumnID
        #}
        #New-DiagramOptionsLayout -HierarchicalEnabled $true -HierarchicalDirection FromLeftToRight #-HierarchicalSortMethod directed
        #New-DiagramOptionsPhysics -Enabled $true -HierarchicalRepulsionAvoidOverlap 1 -HierarchicalRepulsionNodeDistance 50
        New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
        if ($Identity) {
            # Add it's members to diagram
            foreach ($ADObject in $Identity) {
                # Lets build our diagram
                #[int] $Level = $($ADObject.Nesting) + 1
                $ID = "$($ADObject.DomainName)$($ADObject.DistinguishedName)"
                #[int] $LevelParent = $($ADObject.Nesting)
                $IDParent = "$($ADObject.ParentGroupDomain)$($ADObject.ParentGroupDN)"

                if ($ADObject.Type -eq 'User') {
                    if (-not $HideUsers -or $HideAppliesTo -notin 'Both', 'Default') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageUser
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid user -IconColor LightSteelBlue
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Blue -ArrowsFromEnabled -Dashes
                    }
                } elseif ($ADObject.Type -eq 'Group') {
                    if ($ADObject.Nesting -eq -1) {
                        $BorderColor = 'Red'
                        $Image = $Script:ConfigurationIcons.ImageGroup
                    } else {
                        $BorderColor = 'Blue'
                        $Image = $Script:ConfigurationIcons.ImageGroupNested
                    }
                    #$SummaryMembers = -join ('Total: ', $ADObject.TotalMembers, ' Direct: ', $ADObject.DirectMembers, ' Groups: ', $ADObject.DirectGroups, ' Indirect: ', $ADObject.IndirectMembers)
                    $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName + [System.Environment]::NewLine #+ $SummaryMembers
                    if ($Online) {
                        New-DiagramNode -Id $ID -Label $Label -Image $Image -ColorBorder $BorderColor
                    } else {
                        New-DiagramNode -Id $ID -Label $Label -IconSolid user-friends -IconColor VeryLightGrey
                    }
                    New-DiagramLink -ColorOpacity 0.5 -From $ID -To $IDParent -Color Orange -ArrowsFromEnabled
                } elseif ($ADObject.Type -eq 'Computer') {
                    if (-not $HideComputers -or $HideAppliesTo -notin 'Both', 'Default') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageComputer
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid desktop -IconColor LightGray
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Arsenic -ArrowsFromEnabled -Dashes
                    }
                } else {
                    if (-not $HideOther -or $HideAppliesTo -notin 'Both', 'Default') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageOther
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid robot -IconColor LightSalmon
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Boulder -ArrowsFromEnabled -Dashes
                    }
                }
            }
        }
    } -EnableFiltering:$EnableDiagramFiltering.IsPresent -MinimumFilteringChars $DiagramFilteringMinimumCharacters -EnableFilteringButton:$EnableDiagramFilteringButton.IsPresent
}
function New-HTMLGroupOfDiagramHierarchical {
    <#
    .SYNOPSIS
    Creates a new HTML group diagram with hierarchical layout and customizable options.
 
    .DESCRIPTION
    This function creates a new HTML group diagram with a hierarchical layout and customizable options. It allows for displaying Active Directory groups and their members in a visual diagram format with hierarchical organization.
 
    .PARAMETER Identity
    Specifies an array of objects to be included in the diagram.
 
    .PARAMETER HideAppliesTo
    Specifies whether to hide elements based on their type. Valid values are 'Default', 'Hierarchical', or 'Both'. Default is 'Both'.
 
    .PARAMETER HideComputers
    Indicates whether to hide computer objects in the diagram.
 
    .PARAMETER HideUsers
    Indicates whether to hide user objects in the diagram.
 
    .PARAMETER HideOther
    Indicates whether to hide other types of objects in the diagram.
 
    .PARAMETER Online
    Indicates whether to display objects as online in the diagram.
 
    .EXAMPLE
    New-HTMLGroupOfDiagramHierarchical -Identity $ADObjects -HideUsers -Online
    Creates a new HTML group diagram with hierarchical layout, includes Active Directory objects, hides user objects, and displays objects as online.
 
    .EXAMPLE
    New-HTMLGroupOfDiagramHierarchical -Identity $ADObjects -HideComputers -HideAppliesTo 'Hierarchical'
    Creates a new HTML group diagram with hierarchical layout, includes Active Directory objects, hides computer objects, and only displays elements based on the hierarchical setting.
    #>

    [cmdletBinding()]
    param(
        [Array] $Identity,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [switch] $Online,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3
    )
    New-HTMLDiagram -Height 'calc(100vh - 200px)' {
        New-DiagramOptionsLayout -HierarchicalEnabled $true #-HierarchicalDirection FromLeftToRight #-HierarchicalSortMethod directed
        New-DiagramOptionsPhysics -Enabled $true -HierarchicalRepulsionAvoidOverlap 1 -HierarchicalRepulsionNodeDistance 200
        #New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
        if ($Identity) {
            # Add it's members to diagram
            foreach ($ADObject in $Identity) {
                # Lets build our diagram
                [int] $Level = $($ADObject.Nesting) + 1
                $ID = "$($ADObject.DomainName)$($ADObject.DistinguishedName)$Level"
                [int] $LevelParent = $($ADObject.Nesting)
                $IDParent = "$($ADObject.ParentGroupDomain)$($ADObject.ParentGroupDN)$LevelParent"

                [int] $Level = $($ADObject.Nesting) + 1
                if ($ADObject.Type -eq 'User') {
                    if (-not $HideUsers -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageUser -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -Level $Level -IconSolid user -IconColor LightSteelBlue
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Blue -ArrowsFromEnabled -Dashes
                    }
                } elseif ($ADObject.Type -eq 'Group') {
                    if ($ADObject.Nesting -eq -1) {
                        $BorderColor = 'Red'
                        $Image = $Script:ConfigurationIcons.ImageGroup
                    } else {
                        $BorderColor = 'Blue'
                        $Image = $Script:ConfigurationIcons.ImageGroupNested
                    }
                    # $SummaryMembers = -join ('Total: ', $ADObject.TotalMembers, ' Direct: ', $ADObject.DirectMembers, ' Groups: ', $ADObject.DirectGroups, ' Indirect: ', $ADObject.IndirectMembers)
                    $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName + [System.Environment]::NewLine # + $SummaryMembers
                    if ($Online) {
                        New-DiagramNode -Id $ID -Label $Label -Image $Image -Level $Level -ColorBorder $BorderColor
                    } else {
                        New-DiagramNode -Id $ID -Label $Label -Level $Level -IconSolid user-friends
                    }
                    New-DiagramLink -ColorOpacity 0.5 -From $ID -To $IDParent -Color Orange -ArrowsFromEnabled
                } elseif ($ADObject.Type -eq 'Computer') {
                    if (-not $HideComputers -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageComputer -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid desktop -IconColor LightGray -Level $Level
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Arsenic -ArrowsFromEnabled -Dashes
                    }
                } else {
                    if (-not $HideOther -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageOther -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid robot -IconColor LightSalmon -Level $Level
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Boulder -ArrowsFromEnabled -Dashes
                    }
                }
            }
        }
    } -EnableFiltering:$EnableDiagramFiltering.IsPresent -MinimumFilteringChars $DiagramFilteringMinimumCharacters -EnableFilteringButton:$EnableDiagramFilteringButton.IsPresent
}
function New-HTMLGroupOfDiagramSummary {
    <#
    .SYNOPSIS
    Creates an HTML group diagram summary based on the provided Active Directory group information.
 
    .DESCRIPTION
    The New-HTMLGroupOfDiagramSummary function generates an HTML diagram summary representing the relationships between Active Directory groups and their members. It allows customization of the diagram layout and appearance based on the input parameters.
 
    .PARAMETER ADGroup
    Specifies an array of Active Directory group objects to be included in the diagram.
 
    .PARAMETER HideAppliesTo
    Specifies whether to hide specific types of objects in the diagram. Valid values are 'Default', 'Hierarchical', and 'Both'. Default value is 'Both'.
 
    .PARAMETER HideComputers
    Indicates whether to hide computer objects in the diagram.
 
    .PARAMETER HideUsers
    Indicates whether to hide user objects in the diagram.
 
    .PARAMETER HideOther
    Indicates whether to hide other types of objects in the diagram.
 
    .PARAMETER DataTableID
    Specifies the ID of the data table associated with the diagram.
 
    .PARAMETER ColumnID
    Specifies the ID of the column associated with the data table.
 
    .PARAMETER Online
    Indicates whether to display online status information in the diagram.
 
    .EXAMPLE
    New-HTMLGroupOfDiagramSummary -ADGroup $ADGroupArray -HideAppliesTo 'Default' -HideComputers -Online
    Generates an HTML group diagram summary for the specified AD groups, hiding computers and displaying only default objects with online status.
 
    .EXAMPLE
    New-HTMLGroupOfDiagramSummary -ADGroup $ADGroupArray -HideComputers -HideUsers -HideOther -DataTableID "DataTable1" -ColumnID 1 -Online
    Generates an HTML group diagram summary for the specified AD groups, hiding computers, users, and other objects, associating it with DataTable1, and displaying online status.
 
    #>

    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [string] $DataTableID,
        [int] $ColumnID,
        [switch] $Online,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3
    )
    $ConnectionsTracker = @{}
    New-HTMLDiagram -Height 'calc(100vh - 200px)' {
        #if ($DataTableID) {
        # New-DiagramEvent -ID $DataTableID -ColumnID $ColumnID
        #}
        #New-DiagramOptionsLayout -HierarchicalEnabled $true -HierarchicalDirection FromLeftToRight #-HierarchicalSortMethod directed
        #New-DiagramOptionsPhysics -Enabled $true -HierarchicalRepulsionAvoidOverlap 1 -HierarchicalRepulsionNodeDistance 50
        New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
        if ($ADGroup) {
            # Add it's members to diagram
            foreach ($ADObject in $ADGroup) {
                # Lets build our diagram
                # This diagram of Summary doesn't use level checking because it's a summary of a groups, and the level will be different per group
                # This means that it will look a bit different than what is there when comparing 1 to 1 with the other diagrams
                #[int] $Level = $($ADObject.Nesting) + 1
                $ID = "$($ADObject.DomainName)$($ADObject.DistinguishedName)"
                #[int] $LevelParent = $($ADObject.Nesting)
                $IDParent = "$($ADObject.ParentGroupDomain)$($ADObject.ParentGroupDN)"
                # We track connection for ID to make sure that only once the conenction is added
                if (-not $ConnectionsTracker[$ID]) {
                    $ConnectionsTracker[$ID] = @{}
                }
                if (-not $ConnectionsTracker[$ID][$IDParent]) {
                    if ($ADObject.Type -eq 'User') {
                        if (-not $HideUsers -or $HideAppliesTo -notin 'Both', 'Default') {
                            $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                            if ($Online) {
                                New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageUser
                            } else {
                                New-DiagramNode -Id $ID -Label $Label -IconSolid user -IconColor LightSteelBlue
                            }
                            New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Blue -ArrowsFromEnabled -Dashes
                        }
                    } elseif ($ADObject.Type -eq 'Group') {
                        if ($ADObject.Nesting -eq -1) {
                            $BorderColor = 'Red'
                            $Image = $Script:ConfigurationIcons.ImageGroup
                        } else {
                            $BorderColor = 'Blue'
                            $Image = $Script:ConfigurationIcons.ImageGroupNested
                        }
                        #$SummaryMembers = -join ('Total: ', $ADObject.TotalMembers, ' Direct: ', $ADObject.DirectMembers, ' Groups: ', $ADObject.DirectGroups, ' Indirect: ', $ADObject.IndirectMembers)
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName + [System.Environment]::NewLine #+ $SummaryMembers
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Image -ColorBorder $BorderColor
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid user-friends -IconColor VeryLightGrey
                        }
                        New-DiagramLink -ColorOpacity 0.5 -From $ID -To $IDParent -Color Orange -ArrowsFromEnabled
                    } elseif ($ADObject.Type -eq 'Computer') {
                        if (-not $HideComputers -or $HideAppliesTo -notin 'Both', 'Default') {
                            $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                            if ($Online) {
                                New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageComputer
                            } else {
                                New-DiagramNode -Id $ID -Label $Label -IconSolid desktop -IconColor LightGray
                            }
                            New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Arsenic -ArrowsFromEnabled -Dashes
                        }
                    } else {
                        if (-not $HideOther -or $HideAppliesTo -notin 'Both', 'Default') {
                            $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                            if ($Online) {
                                New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageOther
                            } else {
                                New-DiagramNode -Id $ID -Label $Label -IconSolid robot -IconColor LightSalmon
                            }
                            New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Boulder -ArrowsFromEnabled -Dashes
                        }
                    }
                    $ConnectionsTracker[$ID][$IDParent] = $true
                }
            }
        }
    } -EnableFiltering:$EnableDiagramFiltering.IsPresent -MinimumFilteringChars $DiagramFilteringMinimumCharacters -EnableFilteringButton:$EnableDiagramFilteringButton.IsPresent
}
function New-HTMLGroupOfDiagramSummaryHierarchical {
    <#
    .SYNOPSIS
    Creates an HTML group diagram summary in a hierarchical layout based on the provided Active Directory group information.
 
    .DESCRIPTION
    The New-HTMLGroupDiagramSummaryHierarchical function generates an HTML diagram summary representing the relationships between Active Directory groups and their members in a hierarchical structure. It allows customization of the diagram layout and appearance based on the input parameters.
 
    .PARAMETER ADGroup
    Specifies an array of Active Directory group objects to be included in the diagram.
 
    .PARAMETER HideAppliesTo
    Specifies whether to hide specific types of objects in the diagram. Valid values are 'Default', 'Hierarchical', and 'Both'.
 
    .PARAMETER HideComputers
    Indicates whether to hide computer objects in the diagram.
 
    .PARAMETER HideUsers
    Indicates whether to hide user objects in the diagram.
 
    .PARAMETER HideOther
    Indicates whether to hide other types of objects in the diagram.
 
    .PARAMETER Online
    Indicates whether to display online status information in the diagram.
 
    .EXAMPLE
    New-HTMLGroupDiagramSummaryHierarchical -ADGroup $ADGroupArray -HideAppliesTo 'Default' -HideComputers -Online
    Generates an HTML group diagram summary for the specified AD groups, hiding computers and displaying only default objects with online status in a hierarchical layout.
 
    #>

    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [switch] $Online,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3
    )
    New-HTMLDiagram -Height 'calc(100vh - 200px)' {
        New-DiagramOptionsLayout -HierarchicalEnabled $true #-HierarchicalDirection FromLeftToRight #-HierarchicalSortMethod directed
        New-DiagramOptionsPhysics -Enabled $true -HierarchicalRepulsionAvoidOverlap 1 -HierarchicalRepulsionNodeDistance 200
        #New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
        if ($ADGroup) {
            # Add it's members to diagram
            foreach ($ADObject in $ADGroup) {
                # This diagram of Summary doesn't use level checking because it's a summary of a groups, and the level will be different per group
                # This means that it will look a bit different than what is there when comparing 1 to 1 with the other diagrams
                # Lets build our diagram
                #[int] $Level = $($ADObject.Nesting) + 1
                $ID = "$($ADObject.DomainName)$($ADObject.DistinguishedName)"
                #[int] $LevelParent = $($ADObject.Nesting)
                $IDParent = "$($ADObject.ParentGroupDomain)$($ADObject.ParentGroupDN)"

                [int] $Level = $($ADObject.Nesting) + 1
                if ($ADObject.Type -eq 'User') {
                    if (-not $HideUsers -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageUser -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -Level $Level -IconSolid user -IconColor LightSteelBlue
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Blue -ArrowsFromEnabled -Dashes
                    }
                } elseif ($ADObject.Type -eq 'Group') {
                    if ($ADObject.Nesting -eq -1) {
                        $BorderColor = 'Red'
                        $Image = $Script:ConfigurationIcons.ImageGroup
                    } else {
                        $BorderColor = 'Blue'
                        $Image = $Script:ConfigurationIcons.ImageGroupNested
                    }
                    #$SummaryMembers = -join ('Total: ', $ADObject.TotalMembers, ' Direct: ', $ADObject.DirectMembers, ' Groups: ', $ADObject.DirectGroups, ' Indirect: ', $ADObject.IndirectMembers)
                    $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName + [System.Environment]::NewLine #+ $SummaryMembers
                    if ($Online) {
                        New-DiagramNode -Id $ID -Label $Label -Image $Image -Level $Level -ColorBorder $BorderColor
                    } else {
                        New-DiagramNode -Id $ID -Label $Label -Level $Level -IconSolid user-friends
                    }
                    New-DiagramLink -ColorOpacity 0.5 -From $ID -To $IDParent -Color Orange -ArrowsFromEnabled
                } elseif ($ADObject.Type -eq 'Computer') {
                    if (-not $HideComputers -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageComputer -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid desktop -IconColor LightGray -Level $Level
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Arsenic -ArrowsFromEnabled -Dashes
                    }
                } else {
                    if (-not $HideOther -or $HideAppliesTo -notin 'Both', 'Hierarchical') {
                        $Label = $ADObject.Name + [System.Environment]::NewLine + $ADObject.DomainName
                        if ($Online) {
                            New-DiagramNode -Id $ID -Label $Label -Image $Script:ConfigurationIcons.ImageOther -Level $Level
                        } else {
                            New-DiagramNode -Id $ID -Label $Label -IconSolid robot -IconColor LightSalmon -Level $Level
                        }
                        New-DiagramLink -ColorOpacity 0.2 -From $ID -To $IDParent -Color Boulder -ArrowsFromEnabled -Dashes
                    }
                }
            }
        }
    } -EnableFiltering:$EnableDiagramFiltering.IsPresent -MinimumFilteringChars $DiagramFilteringMinimumCharacters -EnableFilteringButton:$EnableDiagramFilteringButton.IsPresent
}
function New-HTMLReportADEssentials {
    <#
    .SYNOPSIS
    Generates an HTML report for ADEssentials.
 
    .DESCRIPTION
    This function generates an HTML report for ADEssentials based on the specified type. It provides options to generate the report online and hide the HTML output.
 
    .PARAMETER Type
    Specifies the type of report to generate.
 
    .PARAMETER Online
    Switch to indicate if the report should be generated online.
 
    .PARAMETER HideHTML
    Switch to hide the HTML output.
 
    .PARAMETER FilePath
    Specifies the file path where the report will be saved.
 
    .EXAMPLE
    New-HTMLReportADEssentials -Type @('Type1') -Online -HideHTML -FilePath "C:\Reports\"
    Generates an HTML report for 'Type1', hides the HTML output, and saves the report in the specified file path.
 
    .NOTES
    Ensure that the necessary permissions are in place to generate the report.
    #>

    [cmdletBinding()]
    param(
        [Array] $Type,
        [switch] $Online,
        [switch] $HideHTML,
        [string] $FilePath
    )

    New-HTML -Author 'PrzemysÅ‚aw KÅ‚ys' -TitleText 'ADEssentials Report' {
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLPanelStyle -BorderRadius 0px
        New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin

        New-HTMLHeader {
            New-HTMLSection -Invisible {
                New-HTMLSection {
                    New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue
                } -JustifyContent flex-start -Invisible
                New-HTMLSection {
                    New-HTMLText -Text "ADEssentials - $($Script:Reporting['Version'])" -Color Blue
                } -JustifyContent flex-end -Invisible
            }
        }

        if ($Type.Count -eq 1) {
            foreach ($T in $Script:ADEssentialsConfiguration.Keys) {
                if ($Script:ADEssentialsConfiguration[$T].Enabled -eq $true) {
                    if ($Script:ADEssentialsConfiguration[$T]['Summary']) {
                        $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:ADEssentialsConfiguration[$T]['Summary']
                    }
                    & $Script:ADEssentialsConfiguration[$T]['Solution']
                }
            }
        } else {
            foreach ($T in $Script:ADEssentialsConfiguration.Keys) {
                if ($Script:ADEssentialsConfiguration[$T].Enabled -eq $true) {
                    if ($Script:ADEssentialsConfiguration[$T]['Summary']) {
                        $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:ADEssentialsConfiguration[$T]['Summary']
                    }
                    New-HTMLTab -Name $Script:ADEssentialsConfiguration[$T]['Name'] {
                        & $Script:ADEssentialsConfiguration[$T]['Solution']
                    }
                }
            }
        }
    } -Online:$Online.IsPresent -ShowHTML:(-not $HideHTML) -FilePath $FilePath
}
function New-HTMLReportADEssentialsWithSplit {
    <#
    .SYNOPSIS
    Generates HTML reports for ADEssentials with the option to split into multiple files.
 
    .DESCRIPTION
    This function generates HTML reports for ADEssentials. It provides the flexibility to split the reports into multiple files for easier viewing.
 
    .PARAMETER Type
    Specifies the type of report to generate.
 
    .PARAMETER Online
    Switch to indicate if the report should be generated online.
 
    .PARAMETER HideHTML
    Switch to hide the HTML output.
 
    .PARAMETER FilePath
    Specifies the file path where the report will be saved.
 
    .PARAMETER CurrentReport
    Specifies the current report to generate.
 
    .EXAMPLE
    New-HTMLReportADEssentialsWithSplit -Type @('Type1', 'Type2') -Online -HideHTML -FilePath "C:\Reports\" -CurrentReport "Type1"
    Generates HTML reports for 'Type1' and 'Type2', hides the HTML output, and saves the reports in the specified file path.
 
    .NOTES
    Ensure that the necessary permissions are in place to generate the reports.
    #>

    [cmdletBinding()]
    param(
        [Array] $Type,
        [switch] $Online,
        [switch] $HideHTML,
        [string] $FilePath,
        [string] $CurrentReport
    )

    # Split reports into multiple files for easier viewing
    $DateName = $(Get-Date -f yyyy-MM-dd_HHmmss)
    $FileName = [io.path]::GetFileNameWithoutExtension($FilePath)
    $DirectoryName = [io.path]::GetDirectoryName($FilePath)

    foreach ($T in $Script:ADEssentialsConfiguration.Keys) {
        if ($Script:ADEssentialsConfiguration[$T].Enabled -eq $true -and ((-not $CurrentReport) -or ($CurrentReport -and $CurrentReport -eq $T))) {
            $NewFileName = $FileName + '_' + $T + "_" + $DateName + '.html'
            $FilePath = [io.path]::Combine($DirectoryName, $NewFileName)

            New-HTML -Author 'PrzemysÅ‚aw KÅ‚ys' -TitleText "ADEssentials $CurrentReport Report" {
                New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
                New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
                New-HTMLPanelStyle -BorderRadius 0px
                New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin

                New-HTMLHeader {
                    New-HTMLSection -Invisible {
                        New-HTMLSection {
                            New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue
                        } -JustifyContent flex-start -Invisible
                        New-HTMLSection {
                            New-HTMLText -Text "ADEssentials - $($Script:Reporting['Version'])" -Color Blue
                        } -JustifyContent flex-end -Invisible
                    }
                }
                if ($Script:ADEssentialsConfiguration[$T]['Summary']) {
                    $Script:Reporting[$T]['Summary'] = Invoke-Command -ScriptBlock $Script:ADEssentialsConfiguration[$T]['Summary']
                }
                & $Script:ADEssentialsConfiguration[$T]['Solution']
            } -Online:$Online.IsPresent -ShowHTML:(-not $HideHTML) -FilePath $FilePath
        }
    }
}
function Remove-PrivateACL {
    <#
    .SYNOPSIS
    Removes private ACLs based on specified criteria.
 
    .DESCRIPTION
    This function removes private ACLs based on the provided ACL object, principal, access rule, access control type, object type name, inherited object type name, inheritance type, force flag, and NT security descriptor.
 
    .PARAMETER ACL
    Specifies the ACL object to be processed.
 
    .PARAMETER Principal
    Specifies the principal for which the ACL should be removed.
 
    .PARAMETER AccessRule
    Specifies the access rule to be removed.
 
    .PARAMETER AccessControlType
    Specifies the type of access control to be removed.
 
    .PARAMETER IncludeObjectTypeName
    Specifies the object type names to include.
 
    .PARAMETER IncludeInheritedObjectTypeName
    Specifies the inherited object type names to include.
 
    .PARAMETER InheritanceType
    Specifies the inheritance type to consider.
 
    .PARAMETER Force
    Indicates whether to force the removal of inherited ACLs.
 
    .PARAMETER NTSecurityDescriptor
    Specifies the NT security descriptor to be updated.
 
    .EXAMPLE
    Remove-PrivateACL -ACL $ACLObject -Principal "User1" -AccessRule "Read" -AccessControlType "Allow" -IncludeObjectTypeName "File" -Force
 
    Removes the specified ACL for User1 with Read access on files.
 
    .NOTES
    Author: Your Name
    Date: Date
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [PSCustomObject] $ACL,
        [string] $Principal,
        [alias('ActiveDirectoryRights')][System.DirectoryServices.ActiveDirectoryRights] $AccessRule,
        [System.Security.AccessControl.AccessControlType] $AccessControlType,
        [Alias('ObjectTypeName')][string[]] $IncludeObjectTypeName,
        [Alias('InheritedObjectTypeName')][string[]] $IncludeInheritedObjectTypeName,
        [alias('ActiveDirectorySecurityInheritance', 'IncludeActiveDirectorySecurityInheritance')][nullable[System.DirectoryServices.ActiveDirectorySecurityInheritance]] $InheritanceType,
        [switch] $Force,
        [alias('ActiveDirectorySecurity')][System.DirectoryServices.ActiveDirectorySecurity] $NTSecurityDescriptor
    )
    $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $ACL.DistinguishedName
    $QueryServer = $Script:ForestDetails['QueryServers'][$DomainName].HostName[0]

    $OutputRequiresCommit = @(
        # if access rule is defined with just remove access rule we want to remove
        if ($ntSecurityDescriptor -and $ACL.PSObject.Properties.Name -notcontains 'ACLAccessRules') {
            try {
                # We do last minute filtering here to ensure we don't remove the wrong ACL
                if ($Principal) {
                    $PrincipalRequested = Convert-Identity -Identity $Principal -Verbose:$false
                }
                $SplatFilteredACL = @{
                    # I am not sure on this $ACL, needs testing
                    ACL                                       = $ACL.Bundle
                    Resolve                                   = $true
                    Principal                                 = $Principal
                    #Inherited = $Inherited
                    #NotInherited = $NotInherited
                    AccessControlType                         = $AccessControlType
                    IncludeObjectTypeName                     = $IncludeObjectTypeName
                    IncludeInheritedObjectTypeName            = $IncludeInheritedObjectTypeName
                    #ExcludeObjectTypeName = $ExcludeObjectTypeName
                    #ExcludeInheritedObjectTypeName = $ExcludeInheritedObjectTypeName
                    #IncludeActiveDirectoryRights = $IncludeActiveDirectoryRights
                    #ExcludeActiveDirectoryRights = $ExcludeActiveDirectoryRights
                    IncludeActiveDirectorySecurityInheritance = $InheritanceType
                    ExcludeActiveDirectorySecurityInheritance = $ExcludeActiveDirectorySecurityInheritance
                    PrincipalRequested                        = $PrincipalRequested
                    Bundle                                    = $Bundle
                }
                Remove-EmptyValue -Hashtable $SplatFilteredACL
                $CheckAgainstFilters = Get-FilteredACL @SplatFilteredACL
                if (-not $CheckAgainstFilters) {
                    continue
                }
                # Now we do remove the ACL
                Write-Verbose -Message "Remove-ADACL - Removing access from $($ACL.CanonicalName) (type: $($ACL.ObjectClass), IsInherited: $($ACL.IsInherited)) for $($ACL.Principal) / $($ACL.ActiveDirectoryRights) / $($ACL.AccessControlType) / $($ACL.ObjectTypeName) / $($ACL.InheritanceType) / $($ACL.InheritedObjectTypeName)"
                #Write-Verbose -Message "Remove-ADACL - Removing access from $($Rule.CanonicalName) (type: $($Rule.ObjectClass), IsInherited: $($Rule.IsInherited)) for $($Rule.Principal) / $($Rule.ActiveDirectoryRights) / $($Rule.AccessControlType) / $($Rule.ObjectTypeName) / $($Rule.InheritanceType) / $($Rule.InheritedObjectTypeName)"
                if ($ACL.IsInherited) {
                    if ($Force) {
                        # isProtected - true to protect the access rules associated with this ObjectSecurity object from inheritance; false to allow inheritance.
                        # preserveInheritance - true to preserve inherited access rules; false to remove inherited access rules. This parameter is ignored if isProtected is false.
                        $ntSecurityDescriptor.SetAccessRuleProtection($true, $true)
                    } else {
                        Write-Warning "Remove-ADACL - Rule for $($ACL.Principal) / $($ACL.ActiveDirectoryRights) / $($ACL.AccessControlType) / $($ACL.ObjectTypeName) / $($ACL.InheritanceType) / $($ACL.InheritedObjectTypeName) is inherited. Use -Force to remove it."
                        continue
                    }
                }
                $ntSecurityDescriptor.RemoveAccessRuleSpecific($ACL.Bundle)
                $true
            } catch {
                Write-Warning "Remove-ADACL - Removing access from $($ACL.CanonicalName) (type: $($ACL.ObjectClass), IsInherited: $($ACL.IsInherited)) failed: $($_.Exception.Message)"
                $false
            }
        } elseif ($ACL.PSObject.Properties.Name -contains 'ACLAccessRules') {
            foreach ($Rule in $ACL.ACLAccessRules) {
                # We do last minute filtering here to ensure we don't remove the wrong ACL
                if ($Principal) {
                    $PrincipalRequested = Convert-Identity -Identity $Principal -Verbose:$false
                }
                $SplatFilteredACL = @{
                    ACL                                       = $Rule.Bundle
                    Resolve                                   = $true
                    Principal                                 = $Principal
                    #Inherited = $Inherited
                    #NotInherited = $NotInherited
                    AccessControlType                         = $AccessControlType
                    IncludeObjectTypeName                     = $IncludeObjectTypeName
                    IncludeInheritedObjectTypeName            = $IncludeInheritedObjectTypeName
                    #ExcludeObjectTypeName = $ExcludeObjectTypeName
                    #ExcludeInheritedObjectTypeName = $ExcludeInheritedObjectTypeName
                    #IncludeActiveDirectoryRights = $IncludeActiveDirectoryRights
                    #ExcludeActiveDirectoryRights = $ExcludeActiveDirectoryRights
                    IncludeActiveDirectorySecurityInheritance = $InheritanceType
                    ExcludeActiveDirectorySecurityInheritance = $ExcludeActiveDirectorySecurityInheritance
                    PrincipalRequested                        = $PrincipalRequested
                    Bundle                                    = $Bundle
                }
                Remove-EmptyValue -Hashtable $SplatFilteredACL
                $CheckAgainstFilters = Get-FilteredACL @SplatFilteredACL
                if (-not $CheckAgainstFilters) {
                    continue
                }
                # Now we do remove the ACL
                $ntSecurityDescriptor = $ACL.ACL
                try {
                    Write-Verbose -Message "Remove-ADACL - Removing access from $($Rule.CanonicalName) (type: $($Rule.ObjectClass), IsInherited: $($Rule.IsInherited)) for $($Rule.Principal) / $($Rule.ActiveDirectoryRights) / $($Rule.AccessControlType) / $($Rule.ObjectTypeName) / $($Rule.InheritanceType) / $($Rule.InheritedObjectTypeName)"
                    if ($Rule.IsInherited) {
                        if ($Force) {
                            # isProtected - true to protect the access rules associated with this ObjectSecurity object from inheritance; false to allow inheritance.
                            # preserveInheritance - true to preserve inherited access rules; false to remove inherited access rules. This parameter is ignored if isProtected is false.
                            $ntSecurityDescriptor.SetAccessRuleProtection($true, $true)
                        } else {
                            Write-Warning "Remove-ADACL - Rule for $($Rule.Principal) / $($Rule.ActiveDirectoryRights) / $($Rule.AccessControlType) / $($Rule.ObjectTypeName) / $($Rule.InheritanceType) / $($Rule.InheritedObjectTypeName) is inherited. Use -Force to remove it."
                            continue
                        }
                    }
                    $ntSecurityDescriptor.RemoveAccessRuleSpecific($Rule.Bundle)
                    #Write-Verbose -Message "Remove-ADACL - Removing access for $($Identity) / $AccessControlType / $Rule"
                    $true
                } catch {
                    Write-Warning "Remove-ADACL - Removing access from $($Rule.CanonicalName) (type: $($Rule.ObjectClass), IsInherited: $($Rule.IsInherited)) failed: $($_.Exception.Message)"
                    $false
                }
            }
        } else {
            $AllRights = $false
            $ntSecurityDescriptor = $ACL.ACL
            # ACL not provided, we need to get all ourselves
            if ($Principal -like '*-*-*-*') {
                $Identity = [System.Security.Principal.SecurityIdentifier]::new($Principal)
            } else {
                [System.Security.Principal.IdentityReference] $Identity = [System.Security.Principal.NTAccount]::new($Principal)
            }

            if ($ObjectType -and $InheritanceType -and $AccessRule -and $AccessControlType) {
                $ObjectTypeGuid = Convert-ADSchemaToGuid -SchemaName $ObjectType
                if ($ObjectTypeGuid) {
                    $AccessRuleToRemove = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($Identity, $AccessRule, $AccessControlType, $ObjectTypeGuid, $InheritanceType)
                } else {
                    Write-Warning "Remove-PrivateACL - Object type '$ObjectType' not found in schema"
                    return
                }
            } elseif ($ObjectType -and $AccessRule -and $AccessControlType) {
                $ObjectTypeGuid = Convert-ADSchemaToGuid -SchemaName $ObjectType
                if ($ObjectTypeGuid) {
                    $AccessRuleToRemove = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($Identity, $AccessRule, $AccessControlType, $ObjectTypeGuid)
                } else {
                    Write-Warning "Remove-PrivateACL - Object type '$ObjectType' not found in schema"
                    return
                }
            } elseif ($AccessRule -and $AccessControlType) {
                $AccessRuleToRemove = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($Identity, $AccessRule, $AccessControlType)
            } else {
                # this is kind of special we fix it later on, it means user requersted Identity, AccessControlType but nothing else
                # Since there's no direct option with ActiveDirectoryAccessRule we fix it using RemoveAccess
                $AllRights = $true
            }
            try {
                if ($AllRights) {
                    Write-Verbose "Remove-ADACL - Removing access for $($Identity) / $AccessControlType / All Rights"
                    $ntSecurityDescriptor.RemoveAccess($Identity, $AccessControlType)
                } else {
                    Write-Verbose "Remove-ADACL - Removing access for $($AccessRuleToRemove.IdentityReference) / $($AccessRuleToRemove.ActiveDirectoryRights) / $($AccessRuleToRemove.AccessControlType) / $($AccessRuleToRemove.ObjectType) / $($AccessRuleToRemove.InheritanceType) to $($ACL.DistinguishedName)"
                    $ntSecurityDescriptor.RemoveAccessRule($AccessRuleToRemove)
                }
                $true
            } catch {
                Write-Warning "Remove-ADACL - Error removing permissions for $($Identity) / $($AccessControlType) due to error: $($_.Exception.Message)"
                $false
            }
        }

    )
    if ($OutputRequiresCommit -notcontains $false -and $OutputRequiresCommit -contains $true) {
        Write-Verbose "Remove-ADACL - Saving permissions for $($ACL.DistinguishedName) on $($QueryServer)"
        try {
            # TODO: This is a workaround for a ProtectedFromAccidentalDeletion
            # It seems if there's Everyone involved in ntSecurityDescriptor it sets back Protected from Accidental Deletion
            # Need to write some detection mechanism around it
            $TemporaryObject = Get-ADObject -Identity $ACL.DistinguishedName -Properties ProtectedFromAccidentalDeletion -Server $QueryServer
            Set-ADObject -Identity $ACL.DistinguishedName -Replace @{ ntSecurityDescriptor = $ntSecurityDescriptor } -ErrorAction Stop -Server $QueryServer #-ProtectedFromAccidentalDeletion $true
            $AfterTemporaryObject = Get-ADObject -Identity $ACL.DistinguishedName -Properties ProtectedFromAccidentalDeletion -Server $QueryServer
            if ($TemporaryObject.ProtectedFromAccidentalDeletion -ne $AfterTemporaryObject.ProtectedFromAccidentalDeletion) {
                Write-Warning -Message "Remove-ADACL - Restoring ProtectedFromAccidentalDeletion from $($AfterTemporaryObject.ProtectedFromAccidentalDeletion) to $($TemporaryObject.ProtectedFromAccidentalDeletion) for $($ACL.DistinguishedName) as a workaround on $($QueryServer)"
                Set-ADObject -Identity $ACL.DistinguishedName -ProtectedFromAccidentalDeletion $TemporaryObject.ProtectedFromAccidentalDeletion -ErrorAction Stop -Server $QueryServer
            }
            # Old way of doing things
            # Set-Acl -Path $ACL.Path -AclObject $ntSecurityDescriptor -ErrorAction Stop
        } catch {
            Write-Warning "Remove-ADACL - Saving permissions for $($ACL.DistinguishedName) failed: $($_.Exception.Message) oon $($QueryServer)"
        }
    } elseif ($OutputRequiresCommit -contains $false) {
        Write-Warning "Remove-ADACL - Skipping saving permissions for $($ACL.DistinguishedName) due to errors."
    } else {
        Write-Verbose "Remove-ADACL - No changes for $($ACL.DistinguishedName)"
    }
}
function Reset-ADEssentialsStatus {
    <#
    .SYNOPSIS
    Resets the status of ADEssentialsConfiguration based on DefaultTypes.
 
    .DESCRIPTION
    This function resets the status of ADEssentialsConfiguration based on DefaultTypes. It enables the types specified in DefaultTypes and disables the rest.
 
    .PARAMETER DefaultTypes
    Specifies the default types to be enabled.
 
    .EXAMPLE
    Reset-ADEssentialsStatus -DefaultTypes 'Type1', 'Type2'
    Resets the status of ADEssentialsConfiguration enabling 'Type1' and 'Type2' and disabling the rest.
 
    .NOTES
    Author: [Author Name]
    Date: [Date]
    Version: [Version]
    #>

    [cmdletBinding()]
    param(

    )
    if (-not $Script:DefaultTypes) {
        $Script:DefaultTypes = foreach ($T in $Script:ADEssentialsConfiguration.Keys) {
            if ($Script:ADEssentialsConfiguration[$T].Enabled) {
                $T
            }
        }
    } else {
        foreach ($T in $Script:ADEssentialsConfiguration.Keys) {
            if ($Script:ADEssentialsConfiguration[$T]) {
                $Script:ADEssentialsConfiguration[$T]['Enabled'] = $false
            }
        }
        foreach ($T in $Script:DefaultTypes) {
            if ($Script:ADEssentialsConfiguration[$T]) {
                $Script:ADEssentialsConfiguration[$T]['Enabled'] = $true
            }
        }
    }
}
$Script:ADEssentialsConfiguration = [ordered] @{
    AccountDelegation       = $Script:ShowWinADAccountDelegation
    Users                   = $Script:ShowWinADUser
    Computers               = $Script:ShowWinADComputer
    Groups                  = $Script:ShowWinADGroup
    Laps                    = $Script:ConfigurationLAPS
    LapsACL                 = $Script:ConfigurationLAPSACL
    LapsAndBitLocker        = $Script:ConfigurationLAPSAndBitlocker
    BitLocker               = $Script:ConfigurationBitLocker
    ServiceAccounts         = $Script:ConfigurationServiceAccounts
    ForestACLOwners         = $Script:ConfigurationACLOwners
    PasswordPolicies        = $Script:ConfigurationPasswordPolicies
    GlobalCatalogComparison = $Script:ConfigurationGlobalCatalogObjects
}
function Test-ADSubnet {
    <#
    .SYNOPSIS
    Tests for overlapping subnets within the provided array of subnets.
 
    .DESCRIPTION
    This function checks for overlapping subnets within the array of subnets provided. It specifically focuses on IPv4 subnets.
 
    .PARAMETER Subnets
    Specifies an array of subnets to check for overlapping subnets.
 
    .EXAMPLE
    Test-ADSubnet -Subnets @($Subnet1, $Subnet2, $Subnet3)
    Checks for overlapping subnets within the array of subnets provided.
 
    .NOTES
    This function only checks for overlapping IPv4 subnets.
    #>

    [cmdletBinding()]
    param(
        [Array] $Subnets
    )
    foreach ($Subnet in $Subnets) {
        # we only check for IPV4, I have no clue for IPV6
        if ($Subnet.Type -ne 'IPV4') {
            continue
        }
        $SmallSubnets = $Subnets | Where-Object { $_.MaskBits -gt $Subnet.MaskBits -and $Subnet.Type -ne 'IPV4' }
        foreach ($SmallSubnet in $SmallSubnets ) {
            if (($SmallSubnet.Subnet.Address -band $Subnet.SubnetMask.Address) -eq $Subnet.Subnet.Address) {
                [PSCustomObject]@{
                    Name                   = $Subnet.Name
                    SiteName               = $Subnet.SiteName
                    SiteStatus             = $Subnet.SiteStatus
                    SubnetRange            = $Subnet.Subnet
                    OverlappingSubnet      = $SmallSubnet.Name
                    OverlappingSubnetRange = $SmallSubnet.Subnet
                    SiteCollission         = $Subnet.Name -ne $SmallSubnet.Name
                }
            }
        }
    }
}
function Test-DomainTrust {
    <#
    .SYNOPSIS
    Test the trust relationship between two domains.
 
    .DESCRIPTION
    This function tests the trust relationship between the specified domain and a trusted domain.
 
    .PARAMETER Domain
    Specifies the domain to test the trust relationship for.
 
    .PARAMETER TrustedDomain
    Specifies the trusted domain to test the trust relationship against.
 
    .EXAMPLE
    Test-DomainTrust -Domain "contoso.com" -TrustedDomain "fabrikam.com"
    Tests the trust relationship between the "contoso.com" domain and the "fabrikam.com" trusted domain.
 
    .NOTES
    Author: Your Name
    Date: Date
    Version: 1.0
    #>

    [cmdletBinding()]
    param(
        [string] $Domain,
        [string] $TrustedDomain
    )
    #$DomainPDC = $ForestInformation['DomainDomainControllers'][$Domain] | Where-Object { $_.IsPDC -eq $true }
    $DomainInformation = Get-WinADDomain -Domain $Domain
    $DomainPDC = $DomainInformation.PdcRoleOwner.Name

    $PropertiesTrustWMI = @(
        'FlatName',
        'SID',
        'TrustAttributes',
        'TrustDirection',
        'TrustedDCName',
        'TrustedDomain',
        'TrustIsOk',
        'TrustStatus',
        'TrustStatusString', # TrustIsOk/TrustStatus are covered by this
        'TrustType'
    )
    $getCimInstanceSplat = @{
        ClassName    = 'Microsoft_DomainTrustStatus'
        Namespace    = 'root\MicrosoftActiveDirectory'
        ComputerName = $DomainPDC
        ErrorAction  = 'SilentlyContinue'
        Property     = $PropertiesTrustWMI
        Verbose      = $false
    }
    if ($TrustedDomain) {
        $getCimInstanceSplat['Filter'] = "TrustedDomain = `"$TrustedDomain`""
    }
    $TrustStatatuses = Get-CimInstance @getCimInstanceSplat
    if ($TrustStatatuses) {
        foreach ($Status in $TrustStatatuses) {
            [PSCustomObject] @{
                'TrustSource'     = $DomainInformation.Name
                'TrustPartner'    = $Status.TrustedDomain
                'TrustAttributes' = if ($Status.TrustAttributes) {
                    Get-ADTrustAttributes -Value $Status.TrustAttributes 
                } else {
                    'Error - needs fixing' 
                }
                'TrustStatus'     = if ($null -ne $Status) {
                    $Status.TrustStatusString 
                } else {
                    'N/A' 
                }
                'TrustSourceDC'   = if ($null -ne $Status) {
                    $Status.PSComputerName 
                } else {
                    '' 
                }
                'TrustTargetDC'   = if ($null -ne $Status) {
                    $Status.TrustedDCName.Replace('\\', '') 
                } else {
                    '' 
                }
                #'TrustOK' = if ($null -ne $Status) { $Status.TrustIsOK } else { $false }
                #'TrustStatusInt' = if ($null -ne $Status) { $Status.TrustStatus } else { -1 }
            }
        }
    } else {
        [PSCustomObject] @{
            'TrustSource'     = $DomainInformation.Name
            'TrustPartner'    = $TrustedDomain
            'TrustAttributes' = 'Error - needs fixing'
            'TrustStatus'     = 'N/A'
            'TrustSourceDC'   = ''
            'TrustTargetDC'   = ''
            #'TrustOK' = $false
            #'TrustStatusInt' = -1
        }
    }
}
function Test-LDAPCertificate {
    <#
    .SYNOPSIS
    Test-LDAPCertificate function to verify LDAP certificate on a specified computer and port.
 
    .DESCRIPTION
    This function tests the LDAP certificate on a specified computer and port using either Basic or Kerberos authentication.
 
    .PARAMETER Computer
    Specifies the name of the computer to test the LDAP certificate.
 
    .PARAMETER Port
    Specifies the port number to test the LDAP certificate.
 
    .PARAMETER Credential
    Specifies the credentials to use for authentication.
 
    .EXAMPLE
    Test-LDAPCertificate -Computer "ldap.example.com" -Port 636 -Credential $Credential
    Tests the LDAP certificate on the computer "ldap.example.com" using port 636 with specified credentials.
 
    .NOTES
    This function is based on code by ChrisDent.
    #>

    [CmdletBinding()]
    param(
        [string] $Computer,
        [int] $Port,
        [PSCredential] $Credential
    )
    $Date = Get-Date
    if ($Credential) {
        Write-Verbose "Test-LDAPCertificate - Certificate verification $Computer/$Port/Auth Basic"
    } else {
        Write-Verbose "Test-LDAPCertificate - Certificate verification $Computer/$Port/Auth Kerberos"
    }
    # code based on ChrisDent
    $Connection = $null
    $DirectoryIdentifier = [DirectoryServices.Protocols.LdapDirectoryIdentifier]::new($Computer, $Port)
    if ($psboundparameters.ContainsKey("Credential")) {
        $Connection = [DirectoryServices.Protocols.LdapConnection]::new($DirectoryIdentifier, $Credential.GetNetworkCredential())
        $Connection.AuthType = [DirectoryServices.Protocols.AuthType]::Basic
    } else {
        $Connection = [DirectoryServices.Protocols.LdapConnection]::new($DirectoryIdentifier)
        $Connection.AuthType = [DirectoryServices.Protocols.AuthType]::Kerberos
    }
    $Connection.SessionOptions.ProtocolVersion = 3
    $Connection.SessionOptions.SecureSocketLayer = $true

    # Declare a script level variable which can be used to return information from the delegate.
    New-Variable LdapCertificate -Scope Script -Force

    # Create a callback delegate to retrieve the negotiated certificate.
    # Note:
    # * The certificate is unlikely to return the subject.
    # * The delegate is documented as using the X509Certificate type, automatically casting this to X509Certificate2 allows access to more information.
    $Connection.SessionOptions.VerifyServerCertificate = {
        param(
            [DirectoryServices.Protocols.LdapConnection]$Connection,
            [Security.Cryptography.X509Certificates.X509Certificate2]$Certificate
        )
        $Script:LdapCertificate = $Certificate
        return $true
    }

    $State = $true
    try {
        $Connection.Bind()
        $ErrorMessage = ''
    } catch {
        $State = $false
        $ErrorMessage = $_.Exception.Message.Trim()
    }
    $KeyExchangeAlgorithm = @{
        # https://docs.microsoft.com/en-us/dotnet/api/system.security.authentication.exchangealgorithmtype?view=netcore-3.1
        '0'     = 'None' # No key exchange algorithm is used.
        '43522' = 'DiffieHellman' # The Diffie Hellman ephemeral key exchange algorithm.
        '41984' = 'RsaKeyX' # The RSA public-key exchange algorithm.
        '9216'  = 'RsaSign' # The RSA public-key signature algorithm.
        '44550' = 'ECDH_Ephem'
    }

    if ($Script:LdapCertificate.NotBefore -is [DateTime]) {
        $X509NotBeforeDays = (New-TimeSpan -Start $Date -End $Script:LdapCertificate.NotBefore).Days
    } else {
        $X509NotBeforeDays = $null
    }
    if ($Script:LdapCertificate.NotAfter -is [DateTime]) {
        $X509NotAfterDays = (New-TimeSpan -Start $Date -End $Script:LdapCertificate.NotAfter).Days
    } else {
        $X509NotAfterDays = $null
    }

    $Certificate = [ordered]@{
        State                   = $State
        X509NotBeforeDays       = $X509NotBeforeDays
        X509NotAfterDays        = $X509NotAfterDays
        X509DnsNameList         = $Script:LdapCertificate.DnsNameList.Unicode
        X509NotBefore           = $Script:LdapCertificate.NotBefore
        X509NotAfter            = $Script:LdapCertificate.NotAfter
        AlgorithmIdentifier     = $Connection.SessionOptions.SslInformation.AlgorithmIdentifier
        CipherStrength          = $Connection.SessionOptions.SslInformation.CipherStrength
        X509FriendlyName        = $Script:LdapCertificate.FriendlyName
        X509SendAsTrustedIssuer = $Script:LdapCertificate.SendAsTrustedIssuer
        X509SerialNumber        = $Script:LdapCertificate.SerialNumber
        X509Thumbprint          = $Script:LdapCertificate.Thumbprint
        X509SubjectName         = $Script:LdapCertificate.Subject
        X509Issuer              = $Script:LdapCertificate.Issuer
        X509HasPrivateKey       = $Script:LdapCertificate.HasPrivateKey
        X509Version             = $Script:LdapCertificate.Version
        X509Archived            = $Script:LdapCertificate.Archived
        Protocol                = $Connection.SessionOptions.SslInformation.Protocol
        Hash                    = $Connection.SessionOptions.SslInformation.Hash
        HashStrength            = $Connection.SessionOptions.SslInformation.HashStrength
        KeyExchangeAlgorithm    = $KeyExchangeAlgorithm["$($Connection.SessionOptions.SslInformation.KeyExchangeAlgorithm)"]
        ExchangeStrength        = $Connection.SessionOptions.SslInformation.ExchangeStrength
        ErrorMessage            = $ErrorMessage
    }
    $Certificate
}
function Test-LDAPPorts {
    <#
    .SYNOPSIS
    Tests the connectivity to an LDAP server using the specified parameters.
 
    .DESCRIPTION
    This function tests the connectivity to an LDAP server by attempting to establish a connection using the provided server name, port, and optional credentials.
 
    .PARAMETER ServerName
    Specifies the name of the LDAP server to test connectivity.
 
    .PARAMETER Port
    Specifies the port number on the LDAP server to test connectivity.
 
    .PARAMETER Credential
    Specifies the credentials to use for authentication when testing the LDAP server. Optional.
 
    .PARAMETER Identity
    Specifies the user to search for using an LDAP query by objectGUID, objectSID, SamAccountName, UserPrincipalName, Name, or DistinguishedName.
 
    .EXAMPLE
    Test-LDAPPorts -ServerName 'SomeServer' -Port 3269 -Credential (Get-Credential)
 
    Tests the connectivity to the LDAP server 'SomeServer' on port 3269 using provided credentials.
 
    .EXAMPLE
    Test-LDAPPorts -ServerName 'SomeServer' -Port 3269
 
    Tests the connectivity to the LDAP server 'SomeServer' on port 3269 without specifying credentials.
 
    .NOTES
    Ensure that the necessary permissions and network access are in place to perform LDAP server connectivity testing.
    #>

    [CmdletBinding()]
    param(
        [string] $ServerName,
        [int] $Port,
        [pscredential] $Credential,
        [string] $Identity
    )
    if ($ServerName -and $Port -ne 0) {
        Write-Verbose "Test-LDAPPorts - Processing $ServerName / $Port"
        try {
            $LDAP = "LDAP://" + $ServerName + ':' + $Port
            if ($Credential) {
                $Connection = [ADSI]::new($LDAP, $Credential.UserName, $Credential.GetNetworkCredential().Password)
            } else {
                $Connection = [ADSI]($LDAP)
            }
            $Connection.Close()
            $ReturnData = [ordered] @{
                Computer     = $ServerName
                Port         = $Port
                Status       = $true
                ErrorMessage = ''
            }
        } catch {
            $ErrorMessage = $($_.Exception.Message) -replace [System.Environment]::NewLine
            if ($_.Exception.ToString() -match "The server is not operational") {
                Write-Warning "Test-LDAPPorts - Can't open $ServerName`:$Port. Error: $ErrorMessage"
            } elseif ($_.Exception.ToString() -match "The user name or password is incorrect") {
                Write-Warning "Test-LDAPPorts - Current user ($Env:USERNAME) doesn't seem to have access to to LDAP on port $ServerName`:$Port. Error: $ErrorMessage"
            } else {
                Write-Warning -Message "Test-LDAPPorts - Error: $ErrorMessage"
            }
            $ReturnData = [ordered] @{
                Computer     = $ServerName
                Port         = $Port
                Status       = $false
                ErrorMessage = $ErrorMessage
            }
        }

        if ($Identity) {
            if ($ReturnData.Status -eq $true) {
                try {
                    Write-Verbose "Test-LDAPPorts - Processing $ServerName / $Port / $Identity"
                    $LDAP = "LDAP://" + $ServerName + ':' + $Port
                    if ($Credential) {
                        $Connection = [ADSI]::new($LDAP, $Credential.UserName, $Credential.GetNetworkCredential().Password)
                    } else {
                        $Connection = [ADSI]($LDAP)
                    }

                    $Searcher = [System.DirectoryServices.DirectorySearcher]$Connection
                    $Searcher.Filter = "(|(DistinguishedName=$Identity)(Name=$Identity)(SamAccountName=$Identity)(UserPrincipalName=$Identity)(objectGUID=$Identity)(objectSid=$Identity))"
                    $SearchResult = $Searcher.FindOne()
                    #$SearchResult

                    if ($SearchResult) {
                        $UserFound = $true
                    } else {
                        $UserFound = $false
                    }

                    $ReturnData['Identity'] = $Identity
                    $ReturnData['IdentityStatus'] = $UserFound
                    $ReturnData['IdentityData'] = $SearchResult
                    $ReturnData['IdentityErrorMessage'] = if ($UserFound) {
                        '' 
                    } else {
                        "Connection succeeded, but user not found" 
                    }

                    $Connection.Close()
                } catch {
                    $ErrorMessage = $($_.Exception.Message) -replace [System.Environment]::NewLine
                    if ($_.Exception.ToString() -match "The server is not operational") {
                        Write-Warning "Test-LDAPPorts - Can't open $ServerName`:$Port. Error: $ErrorMessage"
                    } elseif ($_.Exception.ToString() -match "The user name or password is incorrect") {
                        Write-Warning "Test-LDAPPorts - Current user ($Env:USERNAME) doesn't seem to have access to to LDAP on port $ServerName`:$Port. Error: $ErrorMessage"
                    } else {
                        Write-Warning -Message "Test-LDAPPorts - Error: $ErrorMessage"
                    }
                    $ReturnData['Identity'] = $Identity
                    $ReturnData['IdentityStatus'] = $false
                    $ReturnData['IdentityData'] = $null
                    $ReturnData['IdentityErrorMessage'] = $ErrorMessage
                }
            } else {
                $ReturnData['Identity'] = $Identity
                $ReturnData['IdentityStatus'] = $false
                $ReturnData['IdentityData'] = $null
                $ReturnData['IdentityErrorMessage'] = $ReturnData.ErrorMessage
            }
        }
        [PSCustomObject] $ReturnData
    }
}
function Test-LdapServer {
    <#
    .SYNOPSIS
    Test the LDAP server ports for connectivity.
 
    .DESCRIPTION
    This function tests the LDAP server ports for connectivity using specified parameters.
 
    .PARAMETER ServerName
    The name of the LDAP server to test.
 
    .PARAMETER Computer
    The computer name.
 
    .PARAMETER Advanced
    An advanced object.
 
    .PARAMETER GCPortLDAP
    The port number for Global Catalog LDAP (default: 3268).
 
    .PARAMETER GCPortLDAPSSL
    The port number for Global Catalog LDAP SSL (default: 3269).
 
    .PARAMETER PortLDAP
    The LDAP port number (default: 389).
 
    .PARAMETER PortLDAPS
    The LDAPS port number (default: 636).
 
    .PARAMETER VerifyCertificate
    Switch to verify the certificate.
 
    .PARAMETER Credential
    The credential to use for testing.
 
    .PARAMETER Identity
    The identity for testing.
 
    .PARAMETER SkipCheckGC
    Switch to skip checking Global Catalog.
 
    .PARAMETER RetryCount
    The number of retries for testing.
 
    .EXAMPLE
    Test-LdapServer -ServerName "ldap.contoso.com" -Computer "MyComputer" -GCPortLDAP 3268 -GCPortLDAPSSL 3269 -PortLDAP 389 -PortLDAPS 636 -VerifyCertificate -Credential $cred -Identity "TestUser" -SkipCheckGC -RetryCount 3
    Tests the LDAP server ports for connectivity with specified parameters.
 
    .NOTES
    Ensure that the necessary permissions are in place to perform the LDAP server port testing.
    #>


    [cmdletBinding()]
    param(
        [string] $ServerName,
        [string] $Computer,
        [PSCustomObject] $Advanced,
        [int] $GCPortLDAP = 3268,
        [int] $GCPortLDAPSSL = 3269,
        [int] $PortLDAP = 389,
        [int] $PortLDAPS = 636,
        [switch] $VerifyCertificate,
        [PSCredential] $Credential,
        [string] $Identity,
        [switch] $SkipCheckGC,
        [int] $RetryCount
    )
    $RetryCountList = [System.Collections.Generic.List[int]]::new()
    $ScriptRetryCount = $RetryCount
    $testLDAPPortsSplat = @{
        ServerName = $ServerName
        Port       = $GCPortLDAP
        Identity   = $Identity
    }
    if ($PSBoundParameters.ContainsKey('Credential')) {
        $testLDAPPortsSplat.Credential = $Credential
    }

    if (-not $SkipCheckGC) {
        if ($Advanced -and $Advanced.IsGlobalCatalog -or -not $Advanced) {
            # Test GC LDAP Port
            $testLDAPPortsSplat['Port'] = $GCPortLDAP
            # Reset RetryCount
            $RetryCount = $ScriptRetryCount
            Do {
                $GlobalCatalogNonSSL = Test-LDAPPorts @testLDAPPortsSplat
                if ($GlobalCatalogNonSSL.Status -eq $false -or $GlobalCatalogNonSSL.IdentityStatus -eq $false) {
                    if ($RetryCount -le 0) {
                        break
                    }
                    $RetryCount--
                }
            } until ($GlobalCatalogNonSSL.Status -eq $true -and ($GlobalCatalogNonSSL.IdentityStatus -eq $true -or $null -eq $GlobalCatalogNonSSL.IdentityStatus))
            $RetryCountList.Add($ScriptRetryCount - $RetryCount)
            # # Test GC LDAPS Port
            if ($ServerName -notlike '*.*') {
                # querying SSL won't work for non-fqdn, we check if after all our checks it's string with dot.
                $GlobalCatalogSSL = [PSCustomObject] @{ Status = $false; ErrorMessage = 'No FQDN' }
            } else {
                $testLDAPPortsSplat['Port'] = $GCPortLDAPSSL
                # Reset RetryCount
                $RetryCount = $ScriptRetryCount
                Do {
                    $GlobalCatalogSSL = Test-LDAPPorts @testLDAPPortsSplat
                    if ($GlobalCatalogSSL.Status -eq $false -or $GlobalCatalogSSL.IdentityStatus -eq $false) {
                        if ($RetryCount -le 0) {
                            break
                        }
                        $RetryCount--
                    }
                } until ($GlobalCatalogSSL.Status -eq $true -and ($GlobalCatalogSSL.IdentityStatus -eq $true -or $null -eq $GlobalCatalogSSL.IdentityStatus))
                $RetryCountList.Add($ScriptRetryCount - $RetryCount)
            }
        } else {
            $GlobalCatalogSSL = [PSCustomObject] @{ Status = $null; ErrorMessage = 'Not Global Catalog' }
            $GlobalCatalogNonSSL = [PSCustomObject] @{ Status = $null; ErrorMessage = 'Not Global Catalog' }
        }
    } else {
        $GlobalCatalogSSL = [PSCustomObject] @{ Status = $null; ErrorMessage = 'Not Global Catalog' }
        $GlobalCatalogNonSSL = [PSCustomObject] @{ Status = $null; ErrorMessage = 'Not Global Catalog' }
    }

    $testLDAPPortsSplat['Port'] = $PortLDAP
    # Reset RetryCount
    $RetryCount = $ScriptRetryCount
    Do {
        $ConnectionLDAP = Test-LDAPPorts @testLDAPPortsSplat
        if ($ConnectionLDAP.Status -eq $false -or $ConnectionLDAP.IdentityStatus -eq $false) {
            $RetryCount--
            if ($RetryCount -le 0) {
                break
            }
        }
    } until ($ConnectionLDAP.Status -eq $true -and ($ConnectionLDAP.IdentityStatus -eq $true -or $null -eq $ConnectionLDAP.IdentityStatus))
    $RetryCountList.Add($ScriptRetryCount - $RetryCount)

    $RetryCount = $ScriptRetryCount
    if ($ServerName -notlike '*.*') {
        # querying SSL won't work for non-fqdn, we check if after all our checks it's string with dot.
        $ConnectionLDAPS = [PSCustomObject] @{ Status = $false; ErrorMessage = 'No FQDN' }
    } else {
        $testLDAPPortsSplat['Port'] = $PortLDAPS
        Do {
            $ConnectionLDAPS = Test-LDAPPorts @testLDAPPortsSplat
            if ($ConnectionLDAPS.Status -eq $false -or $ConnectionLDAPS.IdentityStatus -eq $false) {
                if ($RetryCount -le 0) {
                    break
                }
                $RetryCount--
            }
        } until ($ConnectionLDAPS.Status -eq $true -and ($ConnectionLDAPS.IdentityStatus -eq $true -or $null -eq $ConnectionLDAPS.IdentityStatus))
        $RetryCountList.Add($ScriptRetryCount - $RetryCount)
    }

    $PortsThatWork = @(
        if ($GlobalCatalogNonSSL.Status) {
            $GCPortLDAP 
        }
        if ($GlobalCatalogSSL.Status) {
            $GCPortLDAPSSL 
        }
        if ($ConnectionLDAP.Status) {
            $PortLDAP 
        }
        if ($ConnectionLDAPS.Status) {
            $PortLDAPS 
        }
    ) | Sort-Object

    $PortsIdentityStatus = @(
        if ($GlobalCatalogNonSSL.IdentityStatus) {
            $GCPortLDAP 
        }
        if ($GlobalCatalogSSL.IdentityStatus) {
            $GCPortLDAPSSL 
        }
        if ($ConnectionLDAP.IdentityStatus) {
            $PortLDAP 
        }
        if ($ConnectionLDAPS.IdentityStatus) {
            $PortLDAPS 
        }
    ) | Sort-Object

    $ListIdentityStatus = @(
        $GlobalCatalogSSL.IdentityStatus
        $GlobalCatalogNonSSL.IdentityStatus
        $ConnectionLDAP.IdentityStatus
        $ConnectionLDAPS.IdentityStatus
    )

    if ($ListIdentityStatus -contains $false) {
        $IsIdentical = $false
    } else {
        $IsIdentical = $true
    }

    if ($VerifyCertificate) {
        $testLDAPCertificateSplat = @{
            Computer = $ServerName
            Port     = $PortLDAPS
        }
        if ($PSBoundParameters.ContainsKey("Credential")) {
            $testLDAPCertificateSplat.Credential = $Credential
        }
        # Reset RetryCount
        $RetryCount = $ScriptRetryCount
        Do {
            $Certificate = Test-LDAPCertificate @testLDAPCertificateSplat
            if ($Certificate.State -eq $false) {
                if ($RetryCount -le 0) {
                    break
                }
                $RetryCount--
            }
        } until ($Certificate.State -eq $true)
        $RetryCountList.Add($ScriptRetryCount - $RetryCount)

        if (-not $SkipCheckGC) {
            if (-not $Advanced -or $Advanced.IsGlobalCatalog) {
                $testLDAPCertificateSplat['Port'] = $GCPortLDAPSSL
                # Reset RetryCount
                $RetryCount = $ScriptRetryCount
                Do {
                    $CertificateGC = Test-LDAPCertificate @testLDAPCertificateSplat
                    if ($CertificateGC.State -eq $false) {
                        if ($RetryCount -le 0) {
                            break
                        }
                        $RetryCount--
                    }
                } until ($CertificateGC.State -eq $true)
                $RetryCountList.Add($ScriptRetryCount - $RetryCount)
            } else {
                $CertificateGC = [PSCustomObject] @{ Status = 'N/A'; ErrorMessage = 'Not Global Catalog' }
            }
        }
    }

    if ($VerifyCertificate) {
        $Output = [ordered] @{
            Computer                = $ServerName
            Site                    = $Advanced.Site
            IsRO                    = $Advanced.IsReadOnly
            IsGC                    = $Advanced.IsGlobalCatalog
            StatusDate              = $null
            StatusPorts             = $null
            StatusIdentity          = $null
            AvailablePorts          = $PortsThatWork -join ','
            X509NotBeforeDays       = $null
            X509NotAfterDays        = $null
            X509DnsNameList         = $null
            GlobalCatalogLDAP       = $GlobalCatalogNonSSL.Status
            GlobalCatalogLDAPS      = $GlobalCatalogSSL.Status
            GlobalCatalogLDAPSBind  = $null
            LDAP                    = $ConnectionLDAP.Status
            LDAPS                   = $ConnectionLDAPS.Status
            LDAPSBind               = $null

            Identity                = $Identity
            IdentityStatus          = $IsIdentical
            IdentityAvailablePorts  = $PortsIdentityStatus -join ','
            IdentityData            = $null
            IdentityErrorMessage    = $null

            IdentityGCLDAP          = $GlobalCatalogNonSSL.IdentityStatus
            IdentityGCLDAPS         = $GlobalCatalogSSL.IdentityStatus
            IdentityLDAP            = $ConnectionLDAP.IdentityStatus
            IdentityLDAPS           = $ConnectionLDAPS.IdentityStatus

            OperatingSystem         = $Advanced.OperatingSystem
            IPV4Address             = $Advanced.IPV4Address
            IPV6Address             = $Advanced.IPV6Address
            X509NotBefore           = $null
            X509NotAfter            = $null
            AlgorithmIdentifier     = $null
            CipherStrength          = $null
            X509FriendlyName        = $null
            X509SendAsTrustedIssuer = $null
            X509SerialNumber        = $null
            X509Thumbprint          = $null
            X509SubjectName         = $null
            X509Issuer              = $null
            X509HasPrivateKey       = $null
            X509Version             = $null
            X509Archived            = $null
            Protocol                = $null
            Hash                    = $null
            HashStrength            = $null
            KeyExchangeAlgorithm    = $null
            ExchangeStrength        = $null
            ErrorMessage            = $null
            RetryCount              = $RetryCountList -join ','
        }
    } else {
        $Output = [ordered] @{
            Computer               = $ServerName
            Site                   = $Advanced.Site
            IsRO                   = $Advanced.IsReadOnly
            IsGC                   = $Advanced.IsGlobalCatalog
            StatusDate             = $null
            StatusPorts            = $null
            StatusIdentity         = $null
            AvailablePorts         = $PortsThatWork -join ','
            GlobalCatalogLDAP      = $GlobalCatalogNonSSL.Status
            GlobalCatalogLDAPS     = $GlobalCatalogSSL.Status
            GlobalCatalogLDAPSBind = $null
            LDAP                   = $ConnectionLDAP.Status
            LDAPS                  = $ConnectionLDAPS.Status
            LDAPSBind              = $null
            Identity               = $Identity
            IdentityStatus         = $IsIdentical
            IdentityAvailablePorts = $PortsIdentityStatus -join ','
            IdentityData           = $null
            IdentityErrorMessage   = $null

            OperatingSystem        = $Advanced.OperatingSystem
            IPV4Address            = $Advanced.IPV4Address
            IPV6Address            = $Advanced.IPV6Address

            ErrorMessage           = $null
            RetryCount             = $RetryCountList -join ','
        }
    }
    if ($VerifyCertificate) {
        $Output['LDAPSBind'] = $Certificate.State
        $Output['GlobalCatalogLDAPSBind'] = $CertificateGC.State
        $Output['X509NotBeforeDays'] = $Certificate['X509NotBeforeDays']
        $Output['X509NotAfterDays'] = $Certificate['X509NotAfterDays']
        $Output['X509DnsNameList'] = $Certificate['X509DnsNameList']
        $Output['X509NotBefore'] = $Certificate['X509NotBefore']
        $Output['X509NotAfter'] = $Certificate['X509NotAfter']
        $Output['AlgorithmIdentifier'] = $Certificate['AlgorithmIdentifier']
        $Output['CipherStrength'] = $Certificate['CipherStrength']
        $Output['X509FriendlyName'] = $Certificate['X509FriendlyName']
        $Output['X509SendAsTrustedIssuer'] = $Certificate['X509SendAsTrustedIssuer']
        $Output['X509SerialNumber'] = $Certificate['X509SerialNumber']
        $Output['X509Thumbprint'] = $Certificate['X509Thumbprint']
        $Output['X509SubjectName'] = $Certificate['X509SubjectName']
        $Output['X509Issuer'] = $Certificate['X509Issuer']
        $Output['X509HasPrivateKey'] = $Certificate['X509HasPrivateKey']
        $Output['X509Version'] = $Certificate['X509Version']
        $Output['X509Archived'] = $Certificate['X509Archived']
        $Output['Protocol'] = $Certificate['Protocol']
        $Output['Hash'] = $Certificate['Hash']
        $Output['HashStrength'] = $Certificate['HashStrength']
        $Output['KeyExchangeAlgorithm'] = $Certificate['KeyExchangeAlgorithm']
        $Output['ExchangeStrength'] = $Certificate['ExchangeStrength']
        # $Output['ErrorMessage'] = $Certificate['ErrorMessage']
    } else {
        $Output.Remove('LDAPSBind')
        $Output.Remove('GlobalCatalogLDAPSBind')
    }

    $GlobalErrorMessage = @(
        if ($GlobalCatalogNonSSL.ErrorMessage) {
            $GlobalCatalogNonSSL.ErrorMessage 
        }
        if ($GlobalCatalogSSL.ErrorMessage) {
            $GlobalCatalogSSL.ErrorMessage 
        }
        if ($ConnectionLDAP.ErrorMessage) {
            $ConnectionLDAP.ErrorMessage 
        }
        if ($ConnectionLDAPS.ErrorMessage) {
            $ConnectionLDAPS.ErrorMessage 
        }
        if ($Certificate.ErrorMessage) {
            $Certificate.ErrorMessage 
        }
        if ($CertificateGC.ErrorMessage) {
            $CertificateGC.ErrorMessage 
        }
    )
    if ($GlobalErrorMessage.Count -gt 0) {
        $Output['ErrorMessage'] = ($GlobalErrorMessage | Sort-Object -Unique) -join ', '
    }

    if ($Identity) {
        $Output['IdentityData'] = $ConnectionLDAP.IdentityData
        $IdentityErrorMessages = @(
            if ($GlobalCatalogNonSSL.IdentityErrorMessage) {
                $GlobalCatalogNonSSL.IdentityErrorMessage 
            }
            if ($GlobalCatalogSSL.IdentityErrorMessage) {
                $GlobalCatalogSSL.IdentityErrorMessage 
            }
            if ($ConnectionLDAP.IdentityErrorMessage) {
                $ConnectionLDAP.IdentityErrorMessage 
            }
            if ($ConnectionLDAPS.IdentityErrorMessage) {
                $ConnectionLDAPS.IdentityErrorMessage 
            }
        )
        if ($IdentityErrorMessages.Count -gt 0) {
            $Output['IdentityErrorMessage'] = ($IdentityErrorMessages | Sort-Object -Unique) -join ', '
        }
    } else {
        $Output.Remove('Identity')
        $Output.Remove('IdentityStatus')
        $Output.Remove('IdentityAvailablePorts')
        $Output.Remove('IdentityData')
        $Output.Remove('IdentityErrorMessage')
        $Output.Remove('IdentityGCLDAP')
        $Output.Remove('IdentityGCLDAPS')
        $Output.Remove('IdentityLDAP')
        $Output.Remove('IdentityLDAPS')
    }
    if (-not $Advanced) {
        $Output.Remove('IPV4Address')
        $Output.Remove('OperatingSystem')
        $Output.Remove('IPV6Address')
        $Output.Remove('Site')
        $Output.Remove('IsRO')
        $Output.Remove('IsGC')
    }
    # lets return the objects if required
    if ($Extended) {
        $Output['GlobalCatalogSSL'] = $GlobalCatalogSSL
        $Output['GlobalCatalogNonSSL'] = $GlobalCatalogNonSSL
        $Output['ConnectionLDAP'] = $ConnectionLDAP
        $Output['ConnectionLDAPS'] = $ConnectionLDAPS
        $Output['Certificate'] = $Certificate
        $Output['CertificateGC'] = $CertificateGC
    }

    if (-not $VerifyCertificate) {
        $StatusDate = 'Not available'
    } elseif ($VerifyCertificate -and $Output.X509NotAfterDays -lt 0) {
        $StatusDate = 'Failed'
    } else {
        $StatusDate = 'OK'
    }

    if ($Output.IsGC) {
        if ($Output.GlobalCatalogLDAP -eq $true -and $Output.GlobalCatalogLDAPS -eq $true -and $Output.LDAP -eq $true -and $Output.LDAPS -eq $true) {
            if ($VerifyCertificate) {
                if ($Output.LDAPSBind -eq $true -and $Output.GlobalCatalogLDAPSBind -eq $true) {
                    $StatusPorts = 'OK'
                } else {
                    $StatusPorts = 'Failed'
                }
            } else {
                $StatusPorts = 'OK'
            }
        } else {
            $StatusPorts = 'Failed'
        }
    } else {
        if ($Output.LDAP -eq $true -and $Output.LDAPS -eq $true) {
            if ($VerifyCertificate) {
                if ($Output.LDAPSBind -eq $true) {
                    $StatusPorts = 'OK'
                } else {
                    $StatusPorts = 'Failed'
                }
            } else {
                $StatusPorts = 'OK'
            }
        } else {
            $StatusPorts = 'Failed'
        }
    }
    if ($null -eq $Output.IdentityStatus) {
        $StatusIdentity = 'Not available'
    } elseif ($Output.IdentityStatus -eq $true) {
        $StatusIdentity = 'OK'
    } else {
        $StatusIdentity = 'Failed'
    }
    $Output['StatusDate'] = $StatusDate
    $Output['StatusPorts'] = $StatusPorts
    $Output['StatusIdentity'] = $StatusIdentity

    [PSCustomObject] $Output
}

function Add-ADACL {
    <#
    .SYNOPSIS
    Adds an access control entry (ACE) to one or more Active Directory objects or security principals.
 
    .DESCRIPTION
    The Add-ADACL function allows you to add an ACE to Active Directory objects or security principals. It provides flexibility to specify the object, ACL, principal, access rule, access control type, object type, inherited object type, inheritance type, and NT security descriptor.
 
    .PARAMETER ADObject
    Specifies the Active Directory object to which the ACE will be added.
 
    .PARAMETER ACL
    Specifies the access control list (ACL) to be added.
 
    .PARAMETER Principal
    Specifies the security principal to which the ACE applies.
 
    .PARAMETER AccessRule
    Specifies the access rights granted by the ACE.
 
    .PARAMETER AccessControlType
    Specifies whether the ACE allows or denies access.
 
    .PARAMETER ObjectType
    Specifies the type of object to which the ACE applies.
 
    .PARAMETER InheritedObjectType
    Specifies the type of inherited object to which the ACE applies.
 
    .PARAMETER InheritanceType
    Specifies the inheritance type for the ACE.
 
    .PARAMETER NTSecurityDescriptor
    Specifies the NT security descriptor for the ACE.
 
    .PARAMETER ActiveDirectoryAccessRule
    Specifies the Active Directory access rule to be added.
 
    .EXAMPLE
    Add-ADACL -ADObject 'CN=TestUser,OU=Users,DC=contoso,DC=com' -Principal 'Contoso\HRAdmin' -AccessRule 'Read' -AccessControlType 'Allow' -ObjectType 'User' -InheritedObjectType 'Group' -InheritanceType 'All' -NTSecurityDescriptor $NTSecurityDescriptor
 
    This example adds an ACE to the 'TestUser' object in the 'Users' OU, granting 'Read' access to the 'HRAdmin' security principal.
 
    .EXAMPLE
    Add-ADACL -ACL $ACL -Principal 'Contoso\FinanceAdmin' -AccessRule 'Write' -AccessControlType 'Allow' -ObjectType 'Group' -InheritedObjectType 'User' -InheritanceType 'None' -NTSecurityDescriptor $NTSecurityDescriptor
 
    This example adds an ACE from the specified ACL to the 'FinanceAdmin' security principal, granting 'Write' access.
 
    .NOTES
    Ensure that the necessary permissions are in place to modify the security settings of the specified objects or principals.
    #>

    [cmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ADObject')]
    param(
        [parameter(Mandatory, ParameterSetName = 'ActiveDirectoryAccessRule')]
        [Parameter(Mandatory, ParameterSetName = 'ADObject')][alias('Identity')][string] $ADObject,

        [Parameter(Mandatory, ParameterSetName = 'ACL')][Array] $ACL,

        [Parameter(Mandatory, ParameterSetName = 'ACL')]
        [Parameter(Mandatory, ParameterSetName = 'ADObject')]
        [string] $Principal,

        [Parameter(Mandatory, ParameterSetName = 'ACL')]
        [Parameter(Mandatory, ParameterSetName = 'ADObject')]
        [alias('ActiveDirectoryRights')][System.DirectoryServices.ActiveDirectoryRights] $AccessRule,

        [Parameter(Mandatory, ParameterSetName = 'ACL')]
        [Parameter(Mandatory, ParameterSetName = 'ADObject')]
        [System.Security.AccessControl.AccessControlType] $AccessControlType,

        [Parameter(ParameterSetName = 'ACL')]
        [Parameter(ParameterSetName = 'ADObject')]
        [alias('ObjectTypeName')][string] $ObjectType,

        [Parameter(ParameterSetName = 'ACL')]
        [Parameter(ParameterSetName = 'ADObject')]
        [alias('InheritedObjectTypeName')][string] $InheritedObjectType,

        [Parameter(ParameterSetName = 'ACL')]
        [Parameter(ParameterSetName = 'ADObject')]
        [alias('ActiveDirectorySecurityInheritance')][nullable[System.DirectoryServices.ActiveDirectorySecurityInheritance]] $InheritanceType,

        [parameter(ParameterSetName = 'ADObject', Mandatory = $false)]
        [parameter(ParameterSetName = 'ACL', Mandatory = $false)]
        [parameter(ParameterSetName = 'ActiveDirectoryAccessRule', Mandatory = $false)]
        [alias('ActiveDirectorySecurity')][System.DirectoryServices.ActiveDirectorySecurity] $NTSecurityDescriptor,

        [parameter(ParameterSetName = 'ActiveDirectoryAccessRule', Mandatory = $true)]
        [System.DirectoryServices.ActiveDirectoryAccessRule] $ActiveDirectoryAccessRule
    )
    if (-not $Script:ForestDetails) {
        Write-Verbose "Add-ADACL - Gathering Forest Details"
        $Script:ForestDetails = Get-WinADForestDetails
    }


    if ($PSBoundParameters.ContainsKey('ActiveDirectoryAccessRule')) {
        if (-not $ntSecurityDescriptor) {
            $ntSecurityDescriptor = Get-PrivateACL -ADObject $ADObject
        }
        if (-not $NTSecurityDescriptor) {
            Write-Warning -Message "Add-ADACL - No NTSecurityDescriptor provided and ADObject not found"
            return
        }
        $addPrivateACLSplat = @{
            ActiveDirectoryAccessRule = $ActiveDirectoryAccessRule
            ADObject                  = $ADObject
            ntSecurityDescriptor      = $ntSecurityDescriptor
            WhatIf                    = $WhatIfPreference
        }
        Add-PrivateACL @addPrivateACLSplat
    } elseif ($PSBoundParameters.ContainsKey('NTSecurityDescriptor')) {
        $addPrivateACLSplat = @{
            ntSecurityDescriptor = $ntSecurityDescriptor
            ADObject             = $ADObject
            Principal            = $Principal
            WhatIf               = $WhatIfPreference
            AccessRule           = $AccessRule
            AccessControlType    = $AccessControlType
            ObjectType           = $ObjectType
            InheritedObjectType  = $InheritedObjectType
            InheritanceType      = if ($InheritanceType) {
                $InheritanceType 
            } else {
                $null 
            }
        }
        Add-PrivateACL @addPrivateACLSplat
    } elseif ($PSBoundParameters.ContainsKey('ADObject')) {
        foreach ($Object in $ADObject) {
            $MYACL = Get-ADACL -ADObject $Object -Verbose -NotInherited -Bundle
            $addPrivateACLSplat = @{
                ACL                  = $MYACL
                ADObject             = $Object
                Principal            = $Principal
                WhatIf               = $WhatIfPreference
                AccessRule           = $AccessRule
                AccessControlType    = $AccessControlType
                ObjectType           = $ObjectType
                InheritedObjectType  = $InheritedObjectType
                InheritanceType      = if ($InheritanceType) {
                    $InheritanceType 
                } else {
                    $null 
                }
                NTSecurityDescriptor = $MYACL.ACL
            }
            Add-PrivateACL @addPrivateACLSplat
        }
    } elseif ($PSBoundParameters.ContainsKey('ACL')) {
        foreach ($SubACL in $ACL) {
            $addPrivateACLSplat = @{
                ACL                  = $SubACL
                Principal            = $Principal
                WhatIf               = $WhatIfPreference
                AccessRule           = $AccessRule
                AccessControlType    = $AccessControlType
                ObjectType           = $ObjectType
                InheritedObjectType  = $InheritedObjectType
                InheritanceType      = if ($InheritanceType) {
                    $InheritanceType 
                } else {
                    $null 
                }
                NTSecurityDescriptor = $SubACL.ACL
            }
            Add-PrivateACL @addPrivateACLSplat
        }
    }
}
function Compare-PingCastleReport {
    <#
    .SYNOPSIS
    Compares two PingCastle reports and outputs the differences.
 
    .DESCRIPTION
    This script takes two PingCastle XML reports as input and compares them to identify any changes or differences in the security posture between the two reports.
    It can highlight improvements, regressions, or unchanged aspects of the security assessment.
 
    .PARAMETER FilePathBefore
    The file path to the PingCastle report that represents the earlier state of the security assessment.
 
    .PARAMETER FilePathAfter
    The file path to the PingCastle report that represents the later state of the security assessment.
 
    .PARAMETER Status
    Specifies whether to filter the output based on the status of the findings.
    Possible values are "Points", "Rationale", "Points&Rationale", "Same", "New", "Removed", "All".
    If not specified, all changes will be shown.
 
    .PARAMETER Advanced
    A switch parameter that, when used, provides a more detailed comparison of the reports, including minor changes that might not be significant in a high-level overview.
 
    .EXAMPLE
    Compare-PingCastleReport -FilePathBefore "report_before.xml" -FilePathAfter "report_after.xml"
    This example compares two PingCastle reports and outputs the differences between them.
 
    .EXAMPLE
    Compare-PingCastleReport -FilePathBefore "report_before.xml" -FilePathAfter "report_after.xml" -Status Improved
    This example shows only the improvements in the security posture between two PingCastle reports.
 
    .NOTES
    This script requires that both input files are in the XML format generated by PingCastle. Ensure that the reports are from the same domain for a meaningful comparison.
 
    #>

    [CmdletBinding()]
    param(
        [string] $FilePathBefore,
        [string] $FilePathAfter,
        [ValidateSet("Points", "Rationale", "Points&Rationale", "Same", "New", "Removed", "All")]
        [string[]] $Status,
        [switch] $Advanced
    )
    $PingCastleOutput1 = Get-PingCastleReport -FilePath $FilePathBefore
    $PingCastleOutput2 = Get-PingCastleReport -FilePath $FilePathAfter

    if (-not $PingCastleOutput1 -or -not $PingCastleOutput2) {
        Write-Warning -Message "Compare-PingCastleReport - One of the reports is missing. Cannot compare. "
        return
    }
    if ($PingCastleOutput1.DomainName -ne $PingCastleOutput2.DomainName) {
        Write-Warning -Message "Compare-PingCastleReport - Domains are different. Cannot compare. "
        return
    }
    $Summary = @(
        # find differences
        foreach ($RiskID in $PingCastleOutput1.RisksIds.Keys) {
            $Risk1 = $PingCastleOutput1.RisksIds[$RiskID]
            $Risk2 = $PingCastleOutput2.RisksIds[$RiskID]
            if ($Risk1.Points -ne $Risk2.Points -or $Risk1.Rationale -ne $Risk2.Rationale) {
                [PSCustomObject] @{
                    DomainName      = $PingCastleOutput1.DomainName
                    RiskId          = $RiskID
                    Category        = $Risk2.Category
                    DateDifference  = ($PingCastleOutput2.DateScan - $PingCastleOutput1.DateScan).Days
                    Status          = if ($Risk1.Points -ne $Risk2.Points -and $Risk1.Rationale -ne $Risk2.Rationale) {
                        "Points&Rationale"
                    } elseif ($Risk1.Points -ne $Risk2.Points) {
                        "Points"
                    } else {
                        "Rationale"
                    }
                    PointsBefore    = $Risk1.Points
                    PointsAfter     = $Risk2.Points
                    PointsDiff      = $Risk2.Points - $Risk1.Points
                    RationaleBefore = $Risk1.Rationale
                    RationaleAfter  = $Risk2.Rationale
                }
            } else {
                [PSCustomObject] @{
                    DomainName      = $PingCastleOutput1.DomainName
                    RiskId          = $RiskID
                    Category        = $Risk1.Category
                    DateDifference  = ($PingCastleOutput2.DateScan - $PingCastleOutput1.DateScan).Days
                    Status          = "Same"
                    PointsBefore    = $Risk1.Points
                    PointsAfter     = $Risk2.Points
                    PointsDiff      = 0
                    RationaleBefore = $Risk1.rationale
                    RationaleAfter  = $Risk2.Rationale
                }
            }
        }
        # find if there are any new risks
        foreach ($RiskID in $PingCastleOutput2.RisksIds.Keys) {
            $Risk1 = $PingCastleOutput1.RisksIds[$RiskID]
            $Risk2 = $PingCastleOutput2.RisksIds[$RiskID]
            if (-not $Risk1) {
                [PSCustomObject] @{
                    DomainName      = $PingCastleOutput1.DomainName
                    RiskId          = $RiskID
                    Category        = $Risk2.Category
                    DateDifference  = ($PingCastleOutput2.DateScan - $PingCastleOutput1.DateScan).Days
                    Status          = "New"
                    PointsBefore    = 0
                    PointsAfter     = $Risk2.Points
                    PointsDiff      = $Risk2.Points
                    RationaleBefore = ""
                    RationaleAfter  = $Risk2.Rationale
                }
            }
        }
        # find if there are any removed risks
        foreach ($RiskID in $PingCastleOutput1.RisksIds.Keys) {
            $Risk1 = $PingCastleOutput1.RisksIds[$RiskID]
            $Risk2 = $PingCastleOutput2.RisksIds[$RiskID]
            if (-not $Risk2) {
                [PSCustomObject] @{
                    DomainName      = $PingCastleOutput1.DomainName
                    RiskId          = $RiskID
                    Category        = $Risk1.Category
                    DateDifference  = ($PingCastleOutput2.DateScan - $PingCastleOutput1.DateScan).Days
                    Status          = "Removed"
                    PointsBefore    = $Risk1.Points
                    PointsAfter     = 0
                    PointsDiff      = 0 - $Risk1.Points
                    RationaleBefore = $Risk1.Rationale
                    RationaleAfter  = ""
                }
            }
        }
    )
    if ($null -eq $Status -or $Status -contains "All") {
        $SummaryOutput = $Summary | Sort-Object -Property Category, RiskId
    } else {
        $SummaryOutput = foreach ($Item in $Summary) {
            if ($Status -contains $Item.Status) {
                $Item
            }
        }
    }

    # Summary per category
    $SummaryPerCategory = @(
        foreach ($Category in $PingCastleOutput1.Categories.Keys) {
            $Category1 = $PingCastleOutput1.Categories[$Category]
            $Category2 = $PingCastleOutput2.Categories[$Category]
            $Points1 = $Category1 | Measure-Object -Sum Points
            $Points2 = $Category2 | Measure-Object -Sum Points

            $NewRisks = [System.Collections.Generic.List[object]]::new()
            $RemovedRisks = [System.Collections.Generic.List[object]]::new()
            $SameRisks = [System.Collections.Generic.List[object]]::new()
            $ChangedRisks = [System.Collections.Generic.List[object]]::new()

            # Lets find "New" and "Removed" risks
            foreach ($Risk in $SummaryOutput) {
                if ($Risk.Category -eq $Category) {
                    if ($Risk.Status -eq "New") {
                        $NewRisks.Add($Risk)
                    } elseif ($Risk.Status -eq "Removed") {
                        $RemovedRisks.Add($Risk)
                    } elseif ($Risk.Status -eq "Same") {
                        $SameRisks.Add($Risk)
                    } else {
                        $ChangedRisks.Add($Risk)
                    }
                }
            }

            [PSCustomObject] @{
                DomainName     = $PingCastleOutput1.DomainName
                Category       = $Category
                DateDifference = ($PingCastleOutput2.DateScan - $PingCastleOutput1.DateScan).Days
                PointsBefore   = $Points1.Sum
                PointsAfter    = $Points2.Sum
                PointsDiff     = $Points2.Sum - $Points1.Sum
                NewRisks       = $NewRisks
                RemovedRisks   = $RemovedRisks
                ChangedRisks   = $ChangedRisks
                SameRisks      = $SameRisks
            }
        }
    )

    if ($Advanced) {
        [ordered] @{
            Summary            = $SummaryOutput
            SummaryPerCategory = $SummaryPerCategory
        }
    } else {
        $SummaryOutput
    }
}
function Compare-WinADGlobalCatalogObjects {
    <#
    .SYNOPSIS
    This function compares objects in the Global Catalog of an Active Directory forest.
 
    .DESCRIPTION
    The function iterates over each domain in the forest, and for each domain, it compares the objects in the domain with the objects in the Global Catalog.
    It checks for missing objects and objects with wrong GUIDs. The results are returned in a summary object.
 
    .PARAMETER Advanced
    If this switch is provided, the function will return the full summary object.
    If not, it will only return the missing objects and objects with wrong GUIDs.
 
    .EXAMPLE
    Compare-WinADGlobalCatalogObjects -Advanced
 
    This will return the full summary object for all domains in the forest.
 
    .EXAMPLE
    Compare-WinADGlobalCatalogObjects
 
    This will return only the missing objects and objects with wrong GUIDs for all domains in the forest.
 
    .NOTES
    This function requires the Get-WinADForestDetails and Compare-InternalMissingObject functions.
    #>

    [CmdletBinding()]
    param(
        [switch] $Advanced,
        [string] $Forest,
        [string[]] $IncludeDomains,
        [string[]] $ExcludeDomains,
        [int] $LimitPerDomain
    )

    $SummaryDomains = [ordered] @{}
    $ForestInformation = Get-WinADForestDetails -PreferWritable -Forest $Forest
    foreach ($Domain in $ForestInformation.Domains) {
        if ($IncludeDomains -and $Domain -notin $IncludeDomains) {
            continue 
        }
        if ($ExcludeDomains -and $Domain -in $ExcludeDomains) {
            continue 
        }
        Write-Color -Text "Processing Domain: ", $Domain -Color Yellow, White
        $QueryServer = $ForestInformation['QueryServers'][$Domain].HostName[0]
        $SummaryDomains[$Domain] = Compare-InternalMissingObject -ForestInformation $ForestInformation -Server $QueryServer -SourceDomain $Domain -TargetDomain $ForestInformation.Domains -LimitPerDomain $LimitPerDomain
    }

    if ($Advanced) {
        $SummaryDomains
    } else {
        foreach ($Domain in $SummaryDomains.Keys) {
            foreach ($Server in $SummaryDomains[$Domain].Keys) {
                if ($Server -notin 'Summary') {
                    if ($null -ne $SummaryDomains[$Domain][$Server].Missing.Count -gt 0) {
                        $SummaryDomains[$Domain][$Server].Missing
                    }
                    if ($Null -ne $SummaryDomains[$Domain][$Server].WrongGuid.Count -gt 0) {
                        $SummaryDomains[$Domain][$Server].WrongGuid
                    }
                    if ($Null -ne $SummaryDomains[$Domain][$Server].Ignored.Count -gt 0) {
                        $SummaryDomains[$Domain][$Server].Ignored
                    }
                }
            }
        }
    }
}

function Copy-ADOUSecurity {
    <#
    .SYNOPSIS
        Copy AD security from one OU to another.
 
    .DESCRIPTION
        Copies the security for one OU to another with the ability to use a different target group with source group as reference.
 
    .PARAMETER SourceOU
        The reference OU.
 
    .PARAMETER TargetOU
        Target OU to apply security.
 
    .PARAMETER SourceGroup
        The reference group.
 
    .PARAMETER TargetGroup
        Target group to apply security
 
    .PARAMETER Execute
        Switch to execute - leaving this out will result in a dry run (whatif).
 
    .EXAMPLE
        Copy-ADOUSecurity -SourceOU "OU=Finance,DC=contoso,DC=com" -TargetOU "OU=Sales,DC=contoso,DC=com" -SourceGroup "FinanceAdmins" -TargetGroup "SalesAdmins"
    #>

    [CmdletBinding()]
    param (
        [Parameter(Mandatory)][string]$SourceOU,
        [Parameter(Mandatory)][string]$TargetOU,
        [Parameter(Mandatory)][string]$SourceGroup,
        [Parameter(Mandatory)][string]$TargetGroup,
        [System.Management.Automation.PSCredential]$Credential,
        [switch]$Execute
    )

    process {

        [string]$sDomain = (Get-ADDomain).NetBIOSName
        [string]$sServer = (Get-ADDomainController -Writable -Discover).HostName

        $sSourceOU = $SourceOU.Trim()
        $sDestOU = $TargetOU.Trim()
        $sSourceAccount = $SourceGroup.Trim()
        $sDestAccount = $TargetGroup.Trim()

        [ADSI]$oSourceOU = "LDAP://{0}/{1}" -f $sServer, $sSourceOU
        [ADSI]$oTargetOU = "LDAP://{0}/{1}" -f $sServer, $sDestOU

        if ($Credential) {
            $oSourceOU.PSBase.Username = $Credential.Username
            $oSourceOU.PSBase.Password = $Credential.GetNetworkCredential().Password
            $oTargetOU.PSBase.Username = $Credential.Username
            $oTargetOU.PSBase.Password = $Credential.GetNetworkCredential().Password
        }

        $oDestAccountNT = New-Object -TypeName System.Security.Principal.NTAccount -ArgumentList $sDomain, $sDestAccount

        $oSourceOU.ObjectSecurity.Access | Where-Object { $_.IdentityReference -like "$sDomain\$sSourceAccount" } | ForEach-Object {
            $ActiveDirectoryRights = $_.ActiveDirectoryRights
            $AccessControlType = $_.AccessControlType
            $InheritanceType = $_.InheritanceType
            $InheritedObjectType = $_.InheritedObjectType
            $ObjectType = $_.ObjectType

            $oAce = New-Object System.DirectoryServices.ActiveDirectoryAccessRule ($oDestAccountNT, $ActiveDirectoryRights, $AccessControlType, $ObjectType, $InheritanceType, $InheritedObjectType)
            $oTargetOU.ObjectSecurity.AddAccessRule($oAce)
        }

        $oSourceOU.ObjectSecurity.Access | Where-Object { $_.IdentityReference -like "$sDomain\$sSourceAccount" }
        $oTargetOU.ObjectSecurity.Access | Where-Object { $_.IdentityReference -like "$sDomain\$sDestAccount" }

        if ($Execute) {
            try {
                $oTargetOU.CommitChanges()
                Write-Verbose -Message "Permissions commited"
            } catch {
                $ErrorMessage = $_.Exception.Message
                Write-Warning -Message $ErrorMessage
            }
        } else {
            Write-Warning -Message "Use the switch -Execute to commit changes"
        }
    }
}

function Disable-ADACLInheritance {
    <#
    .SYNOPSIS
    Disables inheritance of access control entries (ACEs) from parent objects for one or more Active Directory objects or security principals.
 
    .DESCRIPTION
    The Disable-ADACLInheritance function disables inheritance of ACEs from parent objects for one or more Active Directory objects or security principals. This function can be used to prevent unwanted ACEs from being inherited by child objects.
 
    .PARAMETER ADObject
    Specifies one or more Active Directory objects or security principals to disable inheritance of ACEs from parent objects. This parameter is mandatory when the 'ADObject' parameter set is used.
 
    .PARAMETER ACL
    Specifies one or more access control lists (ACLs) to disable inheritance of ACEs from parent objects. This parameter is mandatory when the 'ACL' parameter set is used.
 
    .PARAMETER RemoveInheritedAccessRules
    Indicates whether to remove inherited ACEs from the object or principal. If this switch is specified, inherited ACEs are removed from the object or principal. If this switch is not specified, inherited ACEs are retained on the object or principal.
 
    .EXAMPLE
    Disable-ADACLInheritance -ADObject 'CN=TestOU,DC=contoso,DC=com'
 
    This example disables inheritance of ACEs from the parent object for the 'TestOU' organizational unit in the 'contoso.com' domain.
 
    .EXAMPLE
    Disable-ADACLInheritance -ACL $ACL -RemoveInheritedAccessRules
 
    This example disables inheritance of ACEs from parent objects for the ACL specified in the $ACL variable, and removes any inherited ACEs from the object or principal.
 
    .NOTES
 
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ADObject')]
    param(
        [parameter(ParameterSetName = 'ADObject', Mandatory)][alias('Identity')][Array] $ADObject,
        [parameter(ParameterSetName = 'ACL', Mandatory)][Array] $ACL,

        [switch] $RemoveInheritedAccessRules
    )
    if ($ACL) {
        Set-ADACLInheritance -Inheritance 'Disabled' -ACL $ACL -RemoveInheritedAccessRules:$RemoveInheritedAccessRules.IsPresent
    } else {
        Set-ADACLInheritance -Inheritance 'Disabled' -ADObject $ADObject -RemoveInheritedAccessRules:$RemoveInheritedAccessRules.IsPresent
    }
}
function Enable-ADACLInheritance {
    <#
    .SYNOPSIS
    Enables inheritance of access control entries (ACEs) from parent objects for one or more Active Directory objects or security principals.
 
    .DESCRIPTION
    The Enable-ADACLInheritance function enables inheritance of ACEs from parent objects for one or more Active Directory objects or security principals.
    This function can be used to ensure that child objects inherit ACEs from parent objects.
 
    .PARAMETER ADObject
    Specifies one or more Active Directory objects or security principals to enable inheritance of ACEs from parent objects.
 
    .PARAMETER ACL
    Specifies one or more access control lists (ACLs) to enable inheritance of ACEs from parent objects.
 
    .EXAMPLE
    Enable-ADACLInheritance -ADObject 'CN=TestOU,DC=contoso,DC=com'
 
    .NOTES
    General notes
    #>

    [CmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ADObject')]
    param(
        [parameter(ParameterSetName = 'ADObject', Mandatory)][alias('Identity')][Array] $ADObject,
        [parameter(ParameterSetName = 'ACL', Mandatory)][Array] $ACL
    )
    if ($ACL) {
        Set-ADACLInheritance -Inheritance 'Enabled' -ACL $ACL
    } else {
        Set-ADACLInheritance -Inheritance 'Enabled' -ADObject $ADObject
    }
}
function Export-ADACLObject {
    <#
    .SYNOPSIS
    Exports the Access Control List (ACL) information for a specified Active Directory object.
 
    .DESCRIPTION
    This function exports the ACL information for a specified Active Directory object. It provides options to include or exclude specific principals and to bundle the ACL information.
 
    .PARAMETER ADObject
    Specifies the Active Directory object for which to export the ACL information.
 
    .PARAMETER IncludePrincipal
    Specifies the principal(s) to include in the exported ACL information.
 
    .PARAMETER ExcludePrincipal
    Specifies the principal(s) to exclude from the exported ACL information.
 
    .PARAMETER Bundle
    Indicates whether to bundle the ACL information for each object.
 
    .PARAMETER OneLiner
    Indicates whether to output the ACL information in a single line.
 
    .EXAMPLE
    Export-ADACLObject -ADObject 'CN=Users,DC=contoso,DC=com' -Bundle
    Exports the ACL information for the 'Users' container in the 'contoso.com' Active Directory.
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [parameter(Mandatory)][alias('Identity')][string] $ADObject,
        [alias('Principal')][string[]] $IncludePrincipal,
        [string[]] $ExcludePrincipal,
        [switch] $Bundle,
        [switch] $OneLiner
    )
    $ACLOutput = Get-ADACL -ADObject $ADObject -Bundle
    foreach ($ACL in $ACLOutput.ACLAccessRules) {
        $ConvertedIdentity = Convert-Identity -Identity $ACL.Principal -Verbose:$false
        if ($ConvertedIdentity.Error) {
            Write-Warning -Message "Export-ADACLObject - Converting identity $($ACL.Principal) failed with $($ConvertedIdentity.Error). Be warned."
        }

        if ($IncludePrincipal) {
            if ($ConvertedIdentity.Name -notin $IncludePrincipal) {
                continue
            }
        }
        if ($ExcludePrincipal) {
            if ($ConvertedIdentity.Name -in $ExcludePrincipal) {
                continue
            }
        }
        if ($Bundle) {
            [PSCustomObject] @{
                Principal                 = $ACL.Principal
                ActiveDirectoryAccessRule = $ACL.Bundle
                Action                    = 'Copy'
            }
        } else {
            New-ADACLObject -Principal $ACL.Principal -AccessControlType $ACL.AccessControlType -ObjectType $ACL.ObjectTypeName -InheritedObjectType $ACL.InheritedObjectTypeName -AccessRule $ACL.ActiveDirectoryRights -InheritanceType $ACL.InheritanceType -OneLiner:$OneLiner.IsPresent
        }
    }
}
function Find-WinADObjectDifference {
    <#
    .SYNOPSIS
    Finds the differences in Active Directory objects between two sets of objects.
 
    .DESCRIPTION
    This function compares two sets of Active Directory objects and identifies the differences between them.
 
    .PARAMETER Standard
    Specifies the standard parameter set for comparing Active Directory objects.
 
    .PARAMETER Identity
    Specifies the identities of the Active Directory objects to compare.
 
    .PARAMETER Forest
    Specifies the forest to search for the Active Directory objects.
 
    .PARAMETER ExcludeDomains
    Specifies the domains to exclude from the comparison.
 
    .PARAMETER IncludeDomains
    Specifies the domains to include in the comparison.
 
    .PARAMETER GlobalCatalog
    Indicates whether to use the global catalog for the comparison.
 
    .PARAMETER Properties
    Specifies the properties to include in the comparison.
 
    .PARAMETER AddProperties
    Specifies additional properties to include in the comparison.
 
    .EXAMPLE
    Find-WinADObjectDifference -Identity 'CN=User1,OU=Users,DC=domain,DC=com', 'CN=User2,OU=Users,DC=domain,DC=com' -Forest 'domain.com' -IncludeDomains 'domain.com' -Properties 'Name', 'Description'
 
    Compares 'User1' and 'User2' objects in the 'domain.com' forest, including only the 'Name' and 'Description' properties.
 
    .NOTES
    General notes
    #>

    [CmdletBinding(DefaultParameterSetName = 'Standard')]
    param(
        [Parameter(ParameterSetName = 'Standard', Mandatory)]
        [Array] $Identity,

        [Parameter(ParameterSetName = 'Standard')]
        [alias('ForestName')][string] $Forest,

        [Parameter(ParameterSetName = 'Standard')]
        [string[]] $ExcludeDomains,

        [Parameter(ParameterSetName = 'Standard')]
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,

        [Parameter(ParameterSetName = 'Standard')]
        [switch] $GlobalCatalog,

        [string[]] $Properties,
        [string[]] $AddProperties

        # [ValidateSet(
        # 'Summary',
        # 'DetailsPerProperty',
        # 'DetailsPerServer',
        # 'DetailsSummary'
        # )][string[]] $Modes
    )

    $ForestInformation = Get-WinADForestDetails -Extended

    $Output = [ordered] @{
        List                = [System.Collections.Generic.List[Object]]::new()
        ListDetails         = [System.Collections.Generic.List[Object]]::new()
        ListDetailsReversed = [System.Collections.Generic.List[Object]]::new()
        ListSummary         = [System.Collections.Generic.List[Object]]::new()
    }

    $ExcludeProperties = @(
        'MemberOf'
        'servicePrincipalName'
        'WhenChanged'
        'DistinguishedName'
        'uSNChanged'
        'uSNCreated'
    )

    if (-not $Properties) {
        # $PropertiesUser = @(
        # 'AccountExpirationDate'
        # 'accountExpires'
        # 'AccountLockoutTime'
        # 'AccountNotDelegated'
        # 'adminCount'
        # 'AllowReversiblePasswordEncryption'
        # 'CannotChangePassword'
        # 'City'
        # 'codePage'
        # 'Company'
        # 'Country'
        # 'countryCode'
        # 'Department'
        # 'Description'
        # 'DisplayName'
        # 'DistinguishedName'
        # 'Division'
        # 'EmailAddress'
        # 'EmployeeID'
        # 'EmployeeNumber'
        # 'Enabled'
        # 'GivenName'
        # 'HomeDirectory'
        # 'HomedirRequired'
        # 'Initials'
        # 'instanceType'
        # 'KerberosEncryptionType'
        # 'LastLogonDate'
        # 'mail'
        # 'mailNickname'
        # 'Manager'
        # 'MemberOf'
        # 'MobilePhone'
        # 'Name'
        # 'ObjectClass'
        # 'Office'
        # 'OfficePhone'
        # 'Organization'
        # 'OtherName'
        # 'PasswordExpired'
        # 'PasswordLastSet'
        # 'PasswordNeverExpires'
        # 'PasswordNotRequired'
        # 'POBox'
        # 'PostalCode'
        # 'PrimaryGroup'
        # 'primaryGroupID'
        # 'PrincipalsAllowedToDelegateToAccount'
        # 'ProfilePath'
        # 'protocolSettings'
        # 'proxyAddresses'
        # 'pwdLastSet'
        # 'SamAccountName'
        # 'sAMAccountType'
        # 'ScriptPath'
        # 'sDRightsEffective'
        # 'ServicePrincipalNames'
        # 'showInAddressBook'
        # 'SID'
        # 'SIDHistory'
        # 'SmartcardLogonRequired'
        # 'State'
        # 'StreetAddress'
        # 'Surname'
        # 'Title'
        # 'TrustedForDelegation'
        # 'TrustedToAuthForDelegation'
        # 'UseDESKeyOnly'
        # 'userAccountControl'
        # 'UserPrincipalName'
        # 'uSNChanged'
        # 'uSNCreated'
        # 'whenChanged'
        # 'whenCreated'
        # )

        $Properties = @(
            'company'
            'department'
            'Description'
            'info'
            'l'
            for ($i = 1; $i -le 15; $i++) {
                "extensionAttribute$i"
            }
            'manager'
            #'memberOf'
            'facsimileTelephoneNumber'
            'givenName'
            'homePhone'
            'postalCode'
            'pager'
            'lastLogonTimestamp'
            'UserAccountControl', 'DisplayName', 'mailNickname', 'mail', 'ipPhone'
            'whenChanged'
            'whenCreated'
        )
    }
    if ($AddProperties) {
        $Properties += $AddProperties
    }
    $Properties = $Properties | Sort-Object -Unique

    if ($GlobalCatalog) {
        [Array] $GCs = foreach ($DC in $ForestInformation.ForestDomainControllers) {
            if ($DC.IsGlobalCatalog) {
                $DC
            }
        }
    } else {
        $DomainFromIdentity = ConvertFrom-DistinguishedName -DistinguishedName $Identity[0] -ToDomainCN
        [Array] $GCs = foreach ($DC in $ForestInformation.ForestDomainControllers) {
            if ($DC.Domain -eq $DomainFromIdentity) {
                $DC
            }
        }
    }

    $CountObject = 0
    $CachedReversedObjects = [ordered] @{}

    foreach ($I in $Identity) {
        $PrimaryObject = $null

        if (-not $I.DistinguishedName) {
            $DN = $I
        } else {
            $DN = $I.DistinguishedName
        }

        #if ($Modes -contains 'DetailsSummary') {
        $ADObjectDetailedDifferences = [ordered] @{
            DistinguishedName = $DN
        }
        #}
        #if ($Modes -contains 'Details') {
        $ADObjectSummary = [ordered] @{
            DistinguishedName     = $DN
            DifferentServers      = [System.Collections.Generic.List[Object]]::new()
            DifferentServersCount = 0
            DifferentProperties   = [System.Collections.Generic.List[Object]]::new()
            SameServers           = [System.Collections.Generic.List[Object]]::new()
            SameServersCount      = 0
            SameProperties        = [System.Collections.Generic.List[Object]]::new()
        }
        #}
        $CachedReversedObjects[$DN] = [ordered] @{}

        $ADObjectDetailsPerPropertyReversed = [ordered] @{
            DistinguishedName = $DN
            Property          = 'Status'
        }
        $CachedReversedObjects[$DN]['Status'] = $ADObjectDetailsPerPropertyReversed

        foreach ($Property in $Properties) {
            $ADObjectDetailsPerPropertyReversed = [ordered] @{
                DistinguishedName = $DN
                Property          = $Property
            }
            $CachedReversedObjects[$DN][$Property] = $ADObjectDetailsPerPropertyReversed
        }

        $CountObject++

        $Count = 0
        foreach ($GC in $GCs) {
            $Count++
            Write-Verbose -Message "Find-WinADObjectDifference - Processing object [Object: $CountObject / $($Identity.Count)][DC: $Count / $($GCs.Count)] $($GC.HostName) for $I"
            # Query the specific object on each GC
            if ($I -is [Microsoft.ActiveDirectory.Management.ADUser]) {
                Try {
                    if ($GlobalCatalog) {
                        $ObjectInfo = Get-ADUser -Identity $DN -Server "$($GC.HostName):3268" -ErrorAction Stop -Properties $Properties
                    } else {
                        $ObjectInfo = Get-ADUser -Identity $DN -Server $GC.HostName -Properties $Properties -ErrorAction Stop
                    }
                } catch {
                    $ObjectInfo = $null
                    Write-Warning "Find-WinADObjectDifference - Error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
                    $ErrorValue = $_.Exception.Message.Replace([System.Environment]::NewLine, '')
                }
            } elseif ($I -is [Microsoft.ActiveDirectory.Management.ADComputer]) {
                Try {
                    if ($GlobalCatalog) {
                        $ObjectInfo = Get-ADComputer -Identity $DN -Server "$($GC.HostName):3268" -ErrorAction Stop -Properties $Properties
                    } else {
                        $ObjectInfo = Get-ADComputer -Identity $DN -Server $GC.HostName -Properties $Properties -ErrorAction Stop
                    }
                } catch {
                    $ObjectInfo = $null
                    Write-Warning "Find-WinADObjectDifference - Error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
                    $ErrorValue = $_.Exception.Message.Replace([System.Environment]::NewLine, '')
                }
            } else {
                if ($I -is [string] -or $I.DistinguishedName) {
                    Try {
                        if ($GlobalCatalog) {
                            $ObjectInfo = Get-ADObject -Identity $DN -Server "$($GC.HostName):3268" -ErrorAction Stop -Properties $Properties
                        } else {
                            $ObjectInfo = Get-ADObject -Identity $DN -Server $GC.HostName -Properties $Properties -ErrorAction Stop
                        }
                    } catch {
                        $ObjectInfo = $null
                        Write-Warning "Test-ADObject - Error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
                        $ErrorValue = $_.Exception.Message.Replace([System.Environment]::NewLine, '')
                    }
                } else {
                    $ObjectInfo = $null
                    Write-Warning "Test-ADObject - Error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
                    $ErrorValue = $_.Exception.Message.Replace([System.Environment]::NewLine, '')
                }
            }
            if ($ObjectInfo) {
                if (-not $PrimaryObject) {
                    $PrimaryObject = $ObjectInfo
                }
                $ADObjectDetailsPerProperty = [ordered] @{
                    DistinguishedName = $DN
                    Server            = $GC.HostName
                    Status            = 'Exists'
                }
                #$CachedReversedObjects[$DN]['Status']['StatusComparison'] = $true
                $CachedReversedObjects[$DN]['Status'][$GC.HostName] = 'Exists'
                foreach ($Property in $Properties) {
                    #$CachedReversedObjects[$DN]['Status']['StatusComparison'] = $true
                    # Comparing WhenChanged is not needed, because it is special and will always be different
                    if ($Property -notin $ExcludeProperties) {
                        $PropertyNameSame = "$Property-Same"
                        $PropertyNameDiff = "$Property-Diff"
                        if (-not $ADObjectDetailedDifferences[$PropertyNameSame]) {
                            $ADObjectDetailedDifferences[$PropertyNameSame] = [System.Collections.Generic.List[Object]]::new()
                        }
                        if (-not $ADObjectDetailedDifferences[$PropertyNameDiff]) {
                            $ADObjectDetailedDifferences[$PropertyNameDiff] = [System.Collections.Generic.List[Object]]::new()
                        }
                        if ($Property -in 'MemberOf', 'servicePrincipalName') {
                            # this requires complicated logic for comparison
                        } elseif ($null -eq $($PrimaryObject.$Property) -and $null -eq ($ObjectInfo.$Property)) {
                            # Both are null, so it's the same
                            $ADObjectDetailedDifferences[$PropertyNameSame].Add($GC.HostName)
                            if ($Property -notin $ADObjectSummary.SameProperties) {
                                $ADObjectSummary.SameProperties.Add($Property)
                            }
                            if ($GC.HostName -notin $ADObjectSummary.SameServers) {
                                $ADObjectSummary.SameServers.Add($GC.HostName)
                            }
                        } elseif ($null -eq $PrimaryObject.$Property) {
                            # PrimaryObject is null, but ObjectInfo is not, so it's different
                            $ADObjectDetailedDifferences[$PropertyNameDiff].Add($GC.HostName)
                            if ($Property -notin $ADObjectSummary.DifferentProperties) {
                                $ADObjectSummary.DifferentProperties.Add($Property)
                            }
                            if ($GC.HostName -notin $ADObjectSummary.DifferentServers) {
                                $ADObjectSummary.DifferentServers.Add($GC.HostName)
                            }
                            # $CachedReversedObjects[$DN]['Status']['StatusComparison'] = $false
                        } elseif ($null -eq $ObjectInfo.$Property) {
                            # ObjectInfo is null, but PrimaryObject is not, so it's different
                            $ADObjectDetailedDifferences[$PropertyNameDiff].Add($GC.HostName)
                            if ($Property -notin $ADObjectSummary.DifferentProperties) {
                                $ADObjectSummary.DifferentProperties.Add($Property)
                            }
                            if ($GC.HostName -notin $ADObjectSummary.DifferentServers) {
                                $ADObjectSummary.DifferentServers.Add($GC.HostName)
                            }
                            # $CachedReversedObjects[$DN]['Status']['StatusComparison'] = $false
                        } else {
                            if ($ObjectInfo.$Property -ne $PrimaryObject.$Property) {
                                # Both are not null, and they are different
                                $ADObjectDetailedDifferences[$PropertyNameDiff].Add($GC.HostName)
                                if ($Property -notin $ADObjectSummary.DifferentProperties) {
                                    $ADObjectSummary.DifferentProperties.Add($Property)
                                }
                                if ($GC.HostName -notin $ADObjectSummary.DifferentServers) {
                                    $ADObjectSummary.DifferentServers.Add($GC.HostName)
                                }
                                # $CachedReversedObjects[$DN]['Status']['StatusComparison'] = $false
                            } else {
                                # Both are not null, and they are the same
                                $ADObjectDetailedDifferences[$PropertyNameSame].Add($GC.HostName)
                                if ($Property -notin $ADObjectSummary.SameProperties) {
                                    $ADObjectSummary.SameProperties.Add($Property)
                                }
                                if ($GC.HostName -notin $ADObjectSummary.SameServers) {
                                    $ADObjectSummary.SameServers.Add($GC.HostName)
                                }
                            }
                        }
                    }
                    $ADObjectDetailsPerProperty[$Property] = $ObjectInfo.$Property
                    $CachedReversedObjects[$DN][$Property][$GC.HostName] = $ObjectInfo.$Property
                }
                $Output.ListDetails.Add([PSCustomObject] $ADObjectDetailsPerProperty)
            } else {
                if (-not $PrimaryObject) {
                    $PrimaryObject = $ObjectInfo
                }
                $ADObjectDetailsPerProperty = [ordered] @{
                    DistinguishedName = $DN
                    Server            = $GC.HostName
                    Status            = $ErrorValue
                }

                $ADObjectSummary.DifferentServers.Add($GC.HostName)

                $CachedReversedObjects[$DN]['Status'][$GC.HostName] = $ErrorValue
                #$CachedReversedObjects[$DN]['Status']['StatusComparison'] = $false
                foreach ($Property in $Properties) {
                    if ($Property -notin $ExcludeProperties) {
                        $ADObjectDetailsPerProperty[$Property] = $null
                        $CachedReversedObjects[$DN][$Property][$GC.HostName] = $ObjectInfo.$Property
                        if ($Property -notin $ADObjectSummary.DifferentProperties) {
                            $ADObjectSummary.DifferentProperties.Add($Property)
                        }
                        #$CachedReversedObjects[$DN][$Property]['StatusComparison'] = $false
                    }
                }
                $Output.ListDetails.Add([PSCustomObject] $ADObjectDetailsPerProperty)
            }
        }
        $ADObjectSummary.DifferentServersCount = $ADObjectSummary.DifferentServers.Count
        $ADObjectSummary.SameServersCount = $ADObjectSummary.SameServers.Count
        $Output.List.Add([PSCustomObject] $ADObjectDetailedDifferences)
        $Output.ListSummary.Add([PSCustomObject] $ADObjectSummary)
        foreach ($Object in $CachedReversedObjects[$DN].Keys) {
            $Output.ListDetailsReversed.Add([PSCustomObject] $CachedReversedObjects[$DN][$Object])
        }
    }
    $Output
}
function Get-ADACL {
    <#
    .SYNOPSIS
    Retrieves and filters access control list (ACL) information for Active Directory objects.
 
    .DESCRIPTION
    This function retrieves and filters access control list (ACL) information for specified Active Directory objects. It allows for detailed filtering based on various criteria such as principal, access control type, object type, inheritance type, and more.
 
    .PARAMETER ADObject
    Specifies the Active Directory object or objects to retrieve ACL information from.
 
    .PARAMETER Extended
    Indicates whether to retrieve extended ACL information.
 
    .PARAMETER ResolveTypes
    Indicates whether to resolve principal types for ACL filtering.
 
    .PARAMETER Principal
    Specifies the principal to filter ACL information for.
 
    .PARAMETER Inherited
    Indicates to include only inherited ACLs.
 
    .PARAMETER NotInherited
    Indicates to include only non-inherited ACLs.
 
    .PARAMETER Bundle
    Indicates whether to bundle ACL information for each object.
 
    .PARAMETER AccessControlType
    Specifies the access control type to filter ACL information for.
 
    .PARAMETER IncludeObjectTypeName
    Specifies the object types to include in ACL filtering.
 
    .PARAMETER IncludeInheritedObjectTypeName
    Specifies the inherited object types to include in ACL filtering.
 
    .PARAMETER ExcludeObjectTypeName
    Specifies the object types to exclude in ACL filtering.
 
    .PARAMETER ExcludeInheritedObjectTypeName
    Specifies the inherited object types to exclude in ACL filtering.
 
    .PARAMETER IncludeActiveDirectoryRights
    Specifies the Active Directory rights to include in ACL filtering.
 
    .PARAMETER ExcludeActiveDirectoryRights
    Specifies the Active Directory rights to exclude in ACL filtering.
 
    .PARAMETER IncludeActiveDirectorySecurityInheritance
    Specifies the inheritance types to include in ACL filtering.
 
    .PARAMETER ExcludeActiveDirectorySecurityInheritance
    Specifies the inheritance types to exclude in ACL filtering.
 
    .PARAMETER ADRightsAsArray
    Indicates to return Active Directory rights as an array.
 
    .EXAMPLE
    Get-ADACL -ADObject 'CN=Users,DC=contoso,DC=com' -ResolveTypes -Principal 'Domain Admins' -Bundle
 
    Retrieves and bundles ACL information for the 'Domain Admins' principal in the 'Users' container.
 
    .NOTES
    General notes
    #>

    [cmdletbinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [alias('Identity')][Array] $ADObject,
        [switch] $Extended,
        [alias('ResolveTypes')][switch] $Resolve,
        [string] $Principal,
        [switch] $Inherited,
        [switch] $NotInherited,
        [switch] $Bundle,
        [System.Security.AccessControl.AccessControlType] $AccessControlType,
        [Alias('ObjectTypeName')][string[]] $IncludeObjectTypeName,
        [Alias('InheritedObjectTypeName')][string[]] $IncludeInheritedObjectTypeName,
        [string[]] $ExcludeObjectTypeName,
        [string[]] $ExcludeInheritedObjectTypeName,
        [Alias('ActiveDirectoryRights')][System.DirectoryServices.ActiveDirectoryRights[]] $IncludeActiveDirectoryRights,
        [System.DirectoryServices.ActiveDirectoryRights[]] $ExcludeActiveDirectoryRights,
        [Alias('InheritanceType', 'IncludeInheritanceType')][System.DirectoryServices.ActiveDirectorySecurityInheritance[]] $IncludeActiveDirectorySecurityInheritance,
        [Alias('ExcludeInheritanceType')][System.DirectoryServices.ActiveDirectorySecurityInheritance[]] $ExcludeActiveDirectorySecurityInheritance,
        [switch] $ADRightsAsArray
    )
    Begin {
        if (-not $Script:ForestGUIDs) {
            Write-Verbose "Get-ADACL - Gathering Forest GUIDS"
            $Script:ForestGUIDs = Get-WinADForestGUIDs
        }
        if (-not $Script:ForestDetails) {
            Write-Verbose "Get-ADACL - Gathering Forest Details"
            $Script:ForestDetails = Get-WinADForestDetails
        }
        if ($Principal -and $Resolve) {
            $PrincipalRequested = Convert-Identity -Identity $Principal -Verbose:$false
        }
    }
    Process {
        foreach ($Object in $ADObject) {
            $ADObjectData = $null
            if ($Object -is [Microsoft.ActiveDirectory.Management.ADOrganizationalUnit] -or $Object -is [Microsoft.ActiveDirectory.Management.ADEntity]) {
                # if object already has proper security descriptor we don't need to do additional querying
                if ($Object.ntSecurityDescriptor) {
                    $ADObjectData = $Object
                }
                [string] $DistinguishedName = $Object.DistinguishedName
                [string] $CanonicalName = $Object.CanonicalName
                if ($CanonicalName) {
                    $CanonicalName = $CanonicalName.TrimEnd('/')
                }
                [string] $ObjectClass = $Object.ObjectClass
            } elseif ($Object -is [string]) {
                [string] $DistinguishedName = $Object
                [string] $CanonicalName = ''
                [string] $ObjectClass = ''
            } else {
                Write-Warning "Get-ADACL - Object not recognized. Skipping..."
                continue
            }
            if (-not $ADObjectData) {
                $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $DistinguishedName
                $QueryServer = $Script:ForestDetails['QueryServers'][$DomainName].HostName[0]
                try {
                    $ADObjectData = Get-ADObject -Identity $DistinguishedName -Properties ntSecurityDescriptor, CanonicalName -ErrorAction Stop -Server $QueryServer
                    # Since we already request an object we might as well use the data and overwrite it if people use the string
                    $ObjectClass = $ADObjectData.ObjectClass
                    $CanonicalName = $ADObjectData.CanonicalName
                    # Real ACL
                    $ACLs = $ADObjectData.ntSecurityDescriptor
                } catch {
                    Write-Warning "Get-ADACL - Path $PathACL - Error: $($_.Exception.Message)"
                    continue
                }
            } else {
                # Real ACL
                $ACLs = $ADObjectData.ntSecurityDescriptor
            }
            $AccessObjects = foreach ($ACL in $ACLs.Access) {
                $SplatFilteredACL = @{
                    ACL                                       = $ACL
                    Resolve                                   = $Resolve
                    Principal                                 = $Principal
                    Inherited                                 = $Inherited
                    NotInherited                              = $NotInherited
                    AccessControlType                         = $AccessControlType
                    IncludeObjectTypeName                     = $IncludeObjectTypeName
                    IncludeInheritedObjectTypeName            = $IncludeInheritedObjectTypeName
                    ExcludeObjectTypeName                     = $ExcludeObjectTypeName
                    ExcludeInheritedObjectTypeName            = $ExcludeInheritedObjectTypeName
                    IncludeActiveDirectoryRights              = $IncludeActiveDirectoryRights
                    ExcludeActiveDirectoryRights              = $ExcludeActiveDirectoryRights
                    IncludeActiveDirectorySecurityInheritance = $IncludeActiveDirectorySecurityInheritance
                    ExcludeActiveDirectorySecurityInheritance = $ExcludeActiveDirectorySecurityInheritance
                    PrincipalRequested                        = $PrincipalRequested
                    Bundle                                    = $Bundle
                }
                Remove-EmptyValue -Hashtable $SplatFilteredACL
                Get-FilteredACL @SplatFilteredACL
            }
            if ($Bundle) {
                if ($Object.CanonicalName) {
                    $CanonicalName = $Object.CanonicalName
                } else {
                    $CanonicalName = ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToCanonicalName
                }
                [PSCustomObject] @{
                    DistinguishedName = $DistinguishedName
                    CanonicalName     = $CanonicalName
                    ACL               = $ACLs
                    ACLAccessRules    = $AccessObjects
                    Path              = $PathACL
                }
            } else {
                $AccessObjects
            }
        }
    }
    End {
    }
}
function Get-ADACLOwner {
    <#
    .SYNOPSIS
    Gets owner from given Active Directory object
 
    .DESCRIPTION
    Gets owner from given Active Directory object
 
    .PARAMETER ADObject
    Active Directory object to get owner from
 
    .PARAMETER Resolve
    Resolves owner to provide more details about said owner
 
    .PARAMETER IncludeACL
    Include additional ACL information along with owner
 
    .PARAMETER IncludeOwnerType
    Include only specific Owner Type, by default all Owner Types are included
 
    .PARAMETER ExcludeOwnerType
    Exclude specific Owner Type, by default all Owner Types are included
 
    .EXAMPLE
    Get-ADACLOwner -ADObject 'CN=Policies,CN=System,DC=ad,DC=evotec,DC=xyz' -Resolve | Format-Table
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [alias('Identity')][Array] $ADObject,
        [switch] $Resolve,
        [alias('AddACL')][switch] $IncludeACL,
        [validateSet('WellKnownAdministrative', 'Administrative', 'NotAdministrative', 'Unknown')][string[]] $IncludeOwnerType,
        [validateSet('WellKnownAdministrative', 'Administrative', 'NotAdministrative', 'Unknown')][string[]] $ExcludeOwnerType
        #,
        # [System.Collections.IDictionary] $ADAdministrativeGroups,

        # [alias('ForestName')][string] $Forest,
        # [string[]] $ExcludeDomains,
        # [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        # [System.Collections.IDictionary] $ExtendedForestInformation
    )
    Begin {
        #if (-not $Script:ADAdministrativeGroups -and $Resolve) {
        #Write-Verbose "Get-GPOZaurrOwner - Getting ADAdministrativeGroups"
        #$ForestInformation = Get-WinADForestDetails -Extended -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
        #$ADAdministrativeGroups = Get-ADADministrativeGroups -Type DomainAdmins, EnterpriseAdmins -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ForestInformation
        #}
        if (-not $Script:ForestDetails) {
            Write-Verbose "Get-ADACL - Gathering Forest Details"
            $Script:ForestDetails = Get-WinADForestDetails
        }
    }
    Process {
        foreach ($Object in $ADObject) {
            $ADObjectData = $null
            if ($Object -is [Microsoft.ActiveDirectory.Management.ADOrganizationalUnit] -or $Object -is [Microsoft.ActiveDirectory.Management.ADEntity]) {
                # if object already has proper security descriptor we don't need to do additional querying
                if ($Object.ntSecurityDescriptor) {
                    $ADObjectData = $Object
                }
                [string] $DistinguishedName = $Object.DistinguishedName
                [string] $CanonicalName = $Object.CanonicalName
                [string] $ObjectClass = $Object.ObjectClass
            } elseif ($Object -is [string]) {
                [string] $DistinguishedName = $Object
                [string] $CanonicalName = ''
                [string] $ObjectClass = ''
            } else {
                Write-Warning "Get-ADACLOwner - Object not recognized. Skipping..."
                continue
            }
            <#
            $DNConverted = (ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToDC) -replace '=' -replace ','
            if (-not (Get-PSDrive -Name $DNConverted -ErrorAction SilentlyContinue)) {
                Write-Verbose "Get-ADACLOwner - Enabling PSDrives for $DistinguishedName to $DNConverted"
                New-ADForestDrives -ForestName $ForestName # -ObjectDN $DistinguishedName
                if (-not (Get-PSDrive -Name $DNConverted -ErrorAction SilentlyContinue)) {
                    Write-Warning "Set-ADACLOwner - Drive $DNConverted not mapped. Terminating..."
                    return
                }
            }
            $PathACL = "$DNConverted`:\$($DistinguishedName)"
            #>


            try {
                #$ACLs = Get-Acl -Path $PathACL -ErrorAction Stop
                if (-not $ADObjectData) {
                    $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $DistinguishedName
                    $QueryServer = $Script:ForestDetails['QueryServers'][$DomainName].HostName[0]
                    try {
                        $ADObjectData = Get-ADObject -Identity $DistinguishedName -Properties ntSecurityDescriptor, CanonicalName, ObjectClass -ErrorAction Stop -Server $QueryServer
                        # Since we already request an object we might as well use the data and overwrite it if people use the string
                        $ObjectClass = $ADObjectData.ObjectClass
                        $CanonicalName = $ADObjectData.CanonicalName
                        # Real ACL
                        $ACLs = $ADObjectData.ntSecurityDescriptor
                    } catch {
                        Write-Warning "Get-ADACL - Path $PathACL - Error: $($_.Exception.Message)"
                        continue
                    }
                } else {
                    # Real ACL
                    $ACLs = $ADObjectData.ntSecurityDescriptor
                }
                $Hash = [ordered] @{
                    DistinguishedName = $DistinguishedName
                    CanonicalName     = $CanonicalName
                    ObjectClass       = $ObjectClass
                    Owner             = $ACLs.Owner
                }
                $ErrorMessage = ''
            } catch {
                $ACLs = $null
                $Hash = [ordered] @{
                    DistinguishedName = $DistinguishedName
                    CanonicalName     = $CanonicalName
                    ObjectClass       = $ObjectClass
                    Owner             = $null
                }
                $ErrorMessage = $_.Exception.Message
            }
            if ($IncludeACL) {
                $Hash['ACLs'] = $ACLs
            }
            if ($Resolve) {
                #$Identity = ConvertTo-Identity -Identity $Hash.Owner -ExtendedForestInformation $ForestInformation -ADAdministrativeGroups $ADAdministrativeGroups
                if ($null -eq $Hash.Owner) {
                    $Identity = $null
                } else {
                    $Identity = Convert-Identity -Identity $Hash.Owner -Verbose:$false
                }
                if ($Identity) {
                    $Hash['OwnerName'] = $Identity.Name
                    $Hash['OwnerSid'] = $Identity.SID
                    $Hash['OwnerType'] = $Identity.Type
                } else {
                    $Hash['OwnerName'] = ''
                    $Hash['OwnerSid'] = ''
                    $Hash['OwnerType'] = ''
                }

                if ($PSBoundParameters.ContainsKey('IncludeOwnerType')) {
                    if ($Hash['OwnerType'] -in $IncludeOwnerType) {
                    } else {
                        continue
                    }
                }
                if ($PSBoundParameters.ContainsKey('ExcludeOwnerType')) {
                    if ($Hash['OwnerType'] -in $ExcludeOwnerType) {
                        continue
                    }
                }
            }
            $Hash['Error'] = $ErrorMessage
            [PSCustomObject] $Hash
        }
    }
    End { 
    }
}
function Get-DNSServerIP {
    <#
    .SYNOPSIS
    Retrieves DNS server IP information for specified computers.
 
    .DESCRIPTION
    This function retrieves DNS server IP information for the specified computers. It checks if the DNS servers are in the approved list and if at least two DNS servers are configured.
 
    .PARAMETER ComputerName
    Specifies the names of the computers to retrieve DNS server information from.
 
    .PARAMETER ApprovedList
    Specifies the list of approved DNS server IP addresses.
 
    .PARAMETER Credential
    Specifies a credential object to use for accessing the computers.
 
    .EXAMPLE
    Get-DNSServerIP -ComputerName "Computer01" -ApprovedList "192.168.1.1", "192.168.1.2"
 
    .NOTES
    File: Get-DNSServerIP.ps1
    Author: [Your Name]
    Version: 1.0
    Date: [Current Date]
    #>

    [alias('Get-WinDNSServerIP')]
    param(
        [string[]] $ComputerName,
        [string[]] $ApprovedList,
        [pscredential] $Credential
    )
    foreach ($Computer in $ComputerName) {
        $Adapters = Get-CimData -Class Win32_NetworkAdapterConfiguration -ComputerName $Computer -ErrorAction Stop | Where-Object { $_.DHCPEnabled -ne 'True' -and $null -ne $_.DNSServerSearchOrder }
        if ($Adapters) {
            foreach ($Adapter in $Adapters) {
                $AllApproved = $true
                foreach ($DNS in $Adapter.DNSServerSearchOrder) {
                    if ($DNS -notin $ApprovedList) {
                        $AllApproved = $true
                    }
                }
                $AtLeastTwo = $Adapter.DNSServerSearchOrder.Count -ge 2
                $Output = [ordered] @{
                    DNSHostName          = $Adapter.DNSHostName
                    Status               = $AllApproved -and $AtLeastTwo
                    Approved             = $AllApproved
                    AtLeastTwo           = $AtLeastTwo
                    Connected            = $true
                    IPAddress            = $Adapter.IPAddress -join ', '
                    DNSServerSearchOrder = $Adapter.DNSServerSearchOrder -join ', '
                    DefaultIPGateway     = $Adapter.DefaultIPGateway -join ', '
                    IPSubnet             = $Adapter.IPSubnet -join ', '
                    Description          = $Adapter.Description
                }
                if (-not $ApprovedList) {
                    $Output.Remove('Approved')
                    $Output.Remove('Status')
                }
                [PSCustomObject] $Output
            }
        } else {
            $Output = [ordered] @{
                DNSHostName          = $Computer
                Status               = $false
                Approved             = $false
                AtLeastTwo           = $false
                Connected            = $false
                IPAddress            = $null
                DNSServerSearchOrder = $null
                DefaultIPGateway     = $null
                IPSubnet             = $null
                Description          = $ErrorMessage
            }
            if (-not $ApprovedList) {
                $Output.Remove('Approved')
                $Output.Remove('Status')
            }
            [PSCustomObject] $Output
        }
    }
}
function Get-PingCastleReport {
    <#
    .SYNOPSIS
    Retrieves PingCastle report data from the specified file.
 
    .DESCRIPTION
    This function retrieves PingCastle report data from the specified file path.
 
    .PARAMETER FilePath
    Specifies the path to the PingCastle report file.
 
    .EXAMPLE
    Get-PingCastleReport -FilePath "C:\Reports\PingCastleReport.xml"
    Retrieves PingCastle report data from the specified file.
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $FilePath
    )
    if (-not (Test-Path $FilePath)) {
        Write-Warning -Message "Get-PingCastle - File $FilePath does not exist. "
        return
    }

    $XmlRiskRules = (Select-Xml -Path $FilePath -XPath "/HealthcheckData/RiskRules").node
    $XmlDomainName = (Select-Xml -Path $FilePath -XPath "/HealthcheckData/DomainFQDN").node.InnerXML
    $XmlScanDate = [datetime](Select-Xml -Path $FilePath -XPath "/HealthcheckData/GenerationDate").node.InnerXML
    $XmlRisks = $XmlRiskRules.HealthcheckRiskRule | Select-Object Category, Points, Rationale, RiskId
    $XmlRisksPoints = $XmlRisks | Measure-Object -Sum Points

    $DataOutput = [ordered] @{
        DomainName  = $XmlDomainName
        DateScan    = $XmlScanDate
        TotalPoints = $XmlRisksPoints.Sum
        Risks       = $XmlRisks
        Categories  = [ordered]@{}
        RisksIds    = [ordered]@{}
    }
    foreach ($Risk in $XmlRisks) {
        $Category = $Risk.Category
        if (-not $DataOutput.Categories[$Category]) {
            $DataOutput.Categories[$Category] = [System.Collections.Generic.List[object]]::new()
        }
        $DataOutput.Categories[$Category].Add($Risk)
    }
    foreach ($Risk in $XmlRisks) {
        $RiskId = $Risk.RiskId
        $DataOutput.RisksIds[$RiskId] = $Risk
    }
    $DataOutput
}

function Get-WinADACLConfiguration {
    <#
    .SYNOPSIS
    Gets permissions or owners from configuration partition
 
    .DESCRIPTION
    Gets permissions or owners from configuration partition for one or multiple types
 
    .PARAMETER ObjectType
    Gets permissions or owners from one or multiple types (and only that type). Possible choices are sites, subnets, interSiteTransport, siteLink, wellKnownSecurityPrincipals
 
    .PARAMETER ContainerType
    Gets permissions or owners from one or multiple types (including containers and anything below it). Possible choices are sites, subnets, interSiteTransport, siteLink, wellKnownSecurityPrincipals, services
 
    .PARAMETER Owner
    Queries for Owners, instead of permissions
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .EXAMPLE
    Get-WinADACLConfiguration -ObjectType 'interSiteTransport', 'siteLink', 'wellKnownSecurityPrincipals' | Format-Table
 
    .EXAMPLE
    Get-WinADACLConfiguration -ContainerType 'sites' -Owner | Format-Table
 
    .NOTES
    General notes
    #>

    [cmdletBinding(DefaultParameterSetName = 'ObjectType')]
    param(
        [parameter(ParameterSetName = 'ObjectType', Mandatory)][ValidateSet('site', 'subnet', 'interSiteTransport', 'siteLink', 'wellKnownSecurityPrincipal')][string[]] $ObjectType,
        [parameter(ParameterSetName = 'FolderType', Mandatory)][ValidateSet('site', 'subnet', 'interSiteTransport', 'siteLink', 'wellKnownSecurityPrincipal', 'service')][string[]] $ContainerType,
        [switch] $Owner,

        [string] $Forest,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    $QueryServer = $ForestInformation.QueryServers[$($ForestInformation.Forest.Name)]['HostName'][0]
    $ForestDN = ConvertTo-DistinguishedName -ToDomain -CanonicalName $ForestInformation.Forest.Name

    if ($ObjectType) {
        if ($ObjectType -contains 'site') {
            $getADObjectSplat = @{
                Server      = $QueryServer
                LDAPFilter  = '(objectClass=site)'
                SearchBase  = "CN=Sites,CN=Configuration,$($($ForestDN))"
                SearchScope = 'OneLevel'
                Properties  = 'Name', 'CanonicalName', 'DistinguishedName', 'WhenCreated', 'WhenChanged', 'ObjectClass', 'ProtectedFromAccidentalDeletion', 'siteobjectbl', 'gplink', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'Site' -Owner:$Owner
        }
        if ($ObjectType -contains 'subnet') {
            $getADObjectSplat = @{
                Server      = $QueryServer
                LDAPFilter  = '(objectClass=subnet)'
                SearchBase  = "CN=Subnets,CN=Sites,CN=Configuration,$($($ForestDN))"
                SearchScope = 'OneLevel'
                Properties  = 'Name', 'distinguishedName', 'CanonicalName', 'WhenCreated', 'whenchanged', 'ProtectedFromAccidentalDeletion', 'siteObject', 'location', 'objectClass', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'Subnet' -Owner:$Owner
        }
        if ($ObjectType -contains 'interSiteTransport') {
            $getADObjectSplat = @{
                Server      = $QueryServer
                LDAPFilter  = '(objectClass=interSiteTransport)'
                SearchBase  = "CN=Inter-Site Transports,CN=Sites,CN=Configuration,$($($ForestDN))"
                SearchScope = 'OneLevel'
                Properties  = 'Name', 'distinguishedName', 'CanonicalName', 'WhenCreated', 'whenchanged', 'ProtectedFromAccidentalDeletion', 'siteObject', 'location', 'objectClass', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'InterSiteTransport' -Owner:$Owner
        }
        if ($ObjectType -contains 'siteLink') {
            $getADObjectSplat = @{
                Server      = $QueryServer
                LDAPFilter  = '(objectClass=siteLink)'
                SearchBase  = "CN=Inter-Site Transports,CN=Sites,CN=Configuration,$($($ForestDN))"
                SearchScope = 'OneLevel'
                Properties  = 'Name', 'distinguishedName', 'CanonicalName', 'WhenCreated', 'whenchanged', 'ProtectedFromAccidentalDeletion', 'siteObject', 'location', 'objectClass', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'Site' -Owner:$Owner
        }
        if ($ObjectType -contains 'wellKnownSecurityPrincipal') {
            $getADObjectSplat = @{
                Server      = $QueryServer
                LDAPFilter  = '(objectClass=foreignSecurityPrincipal)'
                SearchBase  = "CN=WellKnown Security Principals,CN=Configuration,$($($ForestDN))"
                SearchScope = 'OneLevel'
                Properties  = 'Name', 'distinguishedName', 'CanonicalName', 'WhenCreated', 'whenchanged', 'ProtectedFromAccidentalDeletion', 'siteObject', 'location', 'objectClass', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'WellKnownSecurityPrincipals' -Owner:$Owner
        }
    } else {
        if ($ContainerType -contains 'site') {
            $getADObjectSplat = @{
                Server     = $QueryServer
                #LDAPFilter = '(objectClass=site)'
                Filter     = "*"
                SearchBase = "CN=Sites,CN=Configuration,$($($ForestDN))"
                #SearchScope = 'OneLevel'
                Properties = 'Name', 'CanonicalName', 'DistinguishedName', 'WhenCreated', 'WhenChanged', 'ObjectClass', 'ProtectedFromAccidentalDeletion', 'siteobjectbl', 'gplink', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'Site' -FilterOut -Owner:$Owner
        }
        if ($ContainerType -contains 'subnet') {
            $getADObjectSplat = @{
                Server     = $QueryServer
                #LDAPFilter = '(objectClass=subnet)'
                Filter     = "*"
                SearchBase = "CN=Subnets,CN=Sites,CN=Configuration,$($($ForestDN))"
                #SearchScope = 'OneLevel'
                Properties = 'Name', 'distinguishedName', 'CanonicalName', 'WhenCreated', 'whenchanged', 'ProtectedFromAccidentalDeletion', 'siteObject', 'location', 'objectClass', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'Subnet' -Owner:$Owner
        }
        if ($ContainerType -contains 'interSiteTransport') {
            $getADObjectSplat = @{
                Server     = $QueryServer
                #LDAPFilter = '(objectClass=interSiteTransport)'
                Filter     = '*'
                SearchBase = "CN=Inter-Site Transports,CN=Sites,CN=Configuration,$($($ForestDN))"
                #SearchScope = 'OneLevel'
                Properties = 'Name', 'distinguishedName', 'CanonicalName', 'WhenCreated', 'whenchanged', 'ProtectedFromAccidentalDeletion', 'siteObject', 'location', 'objectClass', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'InterSiteTransport' -Owner:$Owner
        }
        if ($ContainerType -contains 'siteLink') {
            $getADObjectSplat = @{
                Server     = $QueryServer
                Filter     = '*'
                #LDAPFilter = '(objectClass=siteLink)'
                SearchBase = "CN=Inter-Site Transports,CN=Sites,CN=Configuration,$($($ForestDN))"
                #SearchScope = 'OneLevel'
                Properties = 'Name', 'distinguishedName', 'CanonicalName', 'WhenCreated', 'whenchanged', 'ProtectedFromAccidentalDeletion', 'siteObject', 'location', 'objectClass', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'Site' -Owner:$Owner
        }
        if ($ContainerType -contains 'service') {
            $getADObjectSplat = @{
                Server     = $QueryServer
                #LDAPFilter = '(objectClass=foreignSecurityPrincipal)'
                Filter     = '*'
                SearchBase = "CN=Services,CN=Configuration,$($($ForestDN))"
                #SearchScope = 'OneLevel'
                Properties = 'Name', 'distinguishedName', 'CanonicalName', 'WhenCreated', 'whenchanged', 'ProtectedFromAccidentalDeletion', 'siteObject', 'location', 'objectClass', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'service' -Owner:$Owner
        }
        if ($ContainerType -contains 'wellKnownSecurityPrincipal') {
            $getADObjectSplat = @{
                Server     = $QueryServer
                #LDAPFilter = '(objectClass=foreignSecurityPrincipal)'
                Filter     = '*'
                SearchBase = "CN=WellKnown Security Principals,CN=Configuration,$($($ForestDN))"
                #SearchScope = 'OneLevel'
                Properties = 'Name', 'distinguishedName', 'CanonicalName', 'WhenCreated', 'whenchanged', 'ProtectedFromAccidentalDeletion', 'siteObject', 'location', 'objectClass', 'Description'
            }
            Get-ADConfigurationPermission -ADObjectSplat $getADObjectSplat -ObjectType 'WellKnownSecurityPrincipals' -Owner:$Owner
        }
    }
}
function Get-WinADACLForest {
    <#
    .SYNOPSIS
    Gets permissions or owners from forest
 
    .DESCRIPTION
    Gets permissions or owners from forest
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .PARAMETER Owner
    Queries for Owners, instead of permissions
 
    .PARAMETER IncludeOwnerType
    Include only specific Owner Type, by default all Owner Types are included
 
    .PARAMETER ExcludeOwnerType
    Exclude specific Owner Type, by default all Owner Types are included
 
    .PARAMETER Separate
    Returns OrderedDictionary with each top level container being in separate key
 
    .PARAMETER OutputFile
    Saves output to Excel file. Requires PSWriteExcel module.
    This was added to speed up processing and reduce memory usage.
    When using this option, you can use PassThru option, to get objects as well.
 
    .PARAMETER PassThru
    Returns objects as well as saves to Excel file. Requires PSWriteExcel module.
 
    .EXAMPLE
    # With split per sheet
    $FilePath = "$Env:USERPROFILE\Desktop\PermissionsOutputPerSheet.xlsx"
    $Permissions = Get-WinADACLForest -Verbose -SplitWorkSheets
    foreach ($Perm in $Permissions.Keys) {
        $Permissions[$Perm] | ConvertTo-Excel -FilePath $FilePath -ExcelWorkSheetName $Perm -AutoFilter -AutoFit -FreezeTopRowFirstColumn
    }
    $Permissions | Format-Table *
 
    .EXAMPLE
    # With owners in one sheet
    $FilePath = "$Env:USERPROFILE\Desktop\PermissionsOutput.xlsx"
    $Permissions = Get-WinADACLForest -Verbose
    $Permissions | ConvertTo-Excel -FilePath $FilePath -ExcelWorkSheetName 'Permissions' -AutoFilter -AutoFit -FreezeTopRowFirstColumn
    $Permissions | Format-Table *
 
    .EXAMPLE
    # With split per sheet
    $FilePath = "$Env:USERPROFILE\Desktop\OwnersOutput.xlsx"
    $Owners = Get-WinADACLForest -Verbose -SplitWorkSheets -Owner
    foreach ($Owner in $Owners.Keys) {
        $Owners[$Owner] | ConvertTo-Excel -FilePath $FilePath -ExcelWorkSheetName $Owner -AutoFilter -AutoFit -FreezeTopRowFirstColumn
    }
    $Owners | Format-Table *
 
    .EXAMPLE
    # With owners in one sheet
    $FilePath = "$Env:USERPROFILE\Desktop\OwnersOutput.xlsx"
    $Owners = Get-WinADACLForest -Verbose -Owner
    $Owners | ConvertTo-Excel -FilePath $FilePath -ExcelWorkSheetName 'AllOwners' -AutoFilter -AutoFit -FreezeTopRowFirstColumn
    $Owners | Format-Table *
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [string] $Forest,
        [alias('Domain')][string[]] $IncludeDomains,
        [string[]] $ExcludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [string[]] $SearchBase,
        [switch] $Owner,
        [switch] $Separate,
        [switch] $IncludeInherited,
        [validateSet('WellKnownAdministrative', 'Administrative', 'NotAdministrative', 'Unknown')][string[]] $IncludeOwnerType,
        [validateSet('WellKnownAdministrative', 'Administrative', 'NotAdministrative', 'Unknown')][string[]] $ExcludeOwnerType,
        [string] $OutputFile,
        [switch] $PassThru
    )
    if ($OutputFile) {
        $CommandExists = Get-Command -Name 'ConvertTo-Excel' -ErrorAction SilentlyContinue
        if (-not $CommandExists) {
            Write-Warning -Message "ConvertTo-Excel command is missing. Please install PSWriteExcel module when using OutputFile option."
            Write-Warning -Message "Install-Module -Name PSWriteExcel -Force -Verbose"
            return
        }
    }

    $ForestTime = Start-TimeLog
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation -Extended
    $Output = [ordered]@{}
    foreach ($Domain in $ForestInformation.Domains) {
        if ($SearchBase) {
            # Lets do quick removal when domain doesn't match so we don't use search base by accident
            $Found = $false
            foreach ($S in $SearchBase) {
                $DN = $ForestInformation['DomainsExtended'][$Domain].DistinguishedName
                $CurrentObjectDC = ConvertFrom-DistinguishedName -DistinguishedName $S -ToDC
                if ($CurrentObjectDC -eq $DN) {
                    $Found = $true
                    break
                }
            }
            if ($Found -eq $false) {
                continue
            }
        }
        Write-Verbose -Message "Get-WinADACLForest - [Start][Domain $Domain]"
        $DomainTime = Start-TimeLog
        $Output[$Domain] = [ordered] @{}
        $Server = $ForestInformation.QueryServers[$Domain].HostName[0]
        $DomainStructure = @(
            if ($SearchBase) {
                foreach ($S in $SearchBase) {
                    Get-ADObject -Filter "*" -Properties canonicalName, ntSecurityDescriptor -SearchScope Base -SearchBase $S -Server $Server
                }
            } else {
                Get-ADObject -Filter "*" -Properties canonicalName, ntSecurityDescriptor -SearchScope Base -Server $Server
                Get-ADObject -Filter "*" -Properties canonicalName, ntSecurityDescriptor -SearchScope OneLevel -Server $Server
            }
        )
        $LdapFilter = "(|(ObjectClass=user)(ObjectClass=contact)(ObjectClass=computer)(ObjectClass=group)(objectClass=inetOrgPerson)(objectClass=foreignSecurityPrincipal)(objectClass=container)(objectClass=organizationalUnit)(objectclass=msDS-ManagedServiceAccount)(objectclass=msDS-GroupManagedServiceAccount))"
        $DomainStructure = $DomainStructure | Sort-Object -Property canonicalName
        foreach ($Structure in $DomainStructure) {
            $Time = Start-TimeLog
            $ObjectName = "[$Domain][$($Structure.CanonicalName)][$($Structure.ObjectClass)][$($Structure.DistinguishedName)]"
            #$ObjectOutputName = "$($Structure.Name)_$($Structure.ObjectClass)".Replace(' ', '').ToLower()
            $ObjectOutputName = "$($Structure.Name)".Replace(' ', '').ToLower()
            Write-Verbose -Message "Get-WinADACLForest - [Start]$ObjectName"
            if ($Structure.ObjectClass -eq 'organizationalUnit') {
                #$Containers = Get-ADOrganizationalUnit -Filter '*' -Server $Server -SearchBase $Structure.DistinguishedName -Properties canonicalName
                $Ignore = @()
                $Containers = @(
                    Get-ADObject -LDAPFilter $LdapFilter -SearchBase $Structure.DistinguishedName -Properties canonicalName, ntSecurityDescriptor -Server $Server -SearchScope Subtree | ForEach-Object {
                        $Found = $false
                        foreach ($I in $Ignore) {
                            if ($_.DistinguishedName -like $I) {
                                $Found = $true
                            }
                        }
                        if (-not $Found) {
                            $_
                        }
                    }
                ) | Sort-Object canonicalName
            } elseif ($Structure.ObjectClass -eq 'domainDNS') {
                $Containers = $Structure
            } elseif ($Structure.ObjectClass -eq 'container') {
                $Ignore = @(
                    # lets ignore GPO, we deal with it in GPOZaurr
                    -join ('*CN=Policies,CN=System,', $ForestInformation['DomainsExtended'][$DOmain].DistinguishedName)

                    -join ('*,CN=System,', $ForestInformation['DomainsExtended'][$DOmain].DistinguishedName)
                )
                $Containers = Get-ADObject -LDAPFilter $LdapFilter -SearchBase $Structure.DistinguishedName -Properties canonicalName, ntSecurityDescriptor -Server $Server -SearchScope Subtree | ForEach-Object {
                    $Found = $false
                    foreach ($I in $Ignore) {
                        if ($_.DistinguishedName -like $I) {
                            $Found = $true
                        }
                    }
                    if (-not $Found) {
                        $_
                    }
                } | Sort-Object canonicalName
            } else {
                $EndTime = Stop-TimeLog -Time $Time -Option OneLiner
                Write-Verbose -Message "Get-WinADACLForest - [Skip ]$ObjectName[ObjectClass not requested]"
                continue
            }
            if (-not $Containers) {
                $EndTime = Stop-TimeLog -Time $Time -Option OneLiner
                Write-Verbose -Message "Get-WinADACLForest - [End ]$ObjectName[$EndTime]"
                continue
            }
            Write-Verbose -Message "Get-WinADACLForest - [Read ]$ObjectName[Objects to process: $($Containers.Count)]"
            if ($Owner) {
                $getADACLOwnerSplat = @{
                    ADObject         = $Containers
                    Resolve          = $true
                    ExcludeOwnerType = $ExcludeOwnerType
                    IncludeOwnerType = $IncludeOwnerType
                }
                Remove-EmptyValue -IDictionary $getADACLOwnerSplat

                $MYACL = Get-ADACLOwner @getADACLOwnerSplat
            } else {
                if ($IncludeInherited) {
                    $MYACL = Get-ADACL -ADObject $Containers -ResolveTypes
                } else {
                    $MYACL = Get-ADACL -ADObject $Containers -ResolveTypes -NotInherited
                }
            }

            if ($OutputFile) {
                $TimeExport = Start-TimeLog
                $Extension = [io.path]::GetExtension($OutputFile)
                $DirectoryPath = [io.path]::GetDirectoryName($OutputFile)
                $FileName = [io.path]::GetFileNameWithoutExtension($OutputFile)
                if ($ForestInformation.Domains.Count -gt 1) {
                    $FinalPath = [io.path]::Combine($DirectoryPath, "$FileName-$Domain$Extension")
                } else {
                    $FinalPath = [io.path]::Combine($DirectoryPath, "$FileName$Extension")
                }
                Write-Verbose -Message "Get-WinADACLForest - [Save ]$ObjectName[OutputFile: $FinalPath]"
                if ($Structure.ObjectClass -eq 'domainDns') {
                    $WorkSheetName = "$($Structure.CanonicalName)".Replace("/", "")
                } else {
                    $WorkSheetName = "$($Structure.Name)"
                }
                $MYACL | ConvertTo-Excel -FilePath $FinalPath -ExcelWorkSheetName $WorkSheetName -AutoFilter -AutoFit -FreezeTopRowFirstColumn
                $EndTimeExport = Stop-TimeLog -Time $TimeExport -Option OneLiner
                Write-Verbose -Message "Get-WinADACLForest - [End ]$ObjectName[OutputFile: $FinalPath][$EndTimeExport]"
                Write-Verbose -Message "Get-WinADACLForest - [Start]$ObjectName[Garbage Collection]"
                [System.GC]::Collect()
                Start-Sleep -Seconds 5
                [System.GC]::Collect()
                Write-Verbose -Message "Get-WinADACLForest - [End ]$ObjectName[Garbage Collection][Done]"
                if ($PassThru) {
                    $MYACL
                }
            } elseif ($Separate) {
                $Output[$Domain][$ObjectOutputName] = $MYACL
            } else {
                $MYACL
            }
            $EndTime = Stop-TimeLog -Time $Time -Option OneLiner
            Write-Verbose -Message "Get-WinADACLForest - [End ]$ObjectName[$EndTime]"
        }
        $DomainEndTime = Stop-TimeLog -Time $DomainTime -Option OneLiner
        Write-Verbose -Message "Get-WinADACLForest - [End ][Domain $Domain][$DomainEndTime]"
    }
    $ForestEndTime = Stop-TimeLog -Time $ForestTime -Option OneLiner
    Write-Verbose -Message "Get-WinADACLForest - [End ][Forest][$ForestEndTime]"
    if ($Separate) {
        $Output
    }
}
function Get-WinADBitlockerLapsSummary {
    <#
    .SYNOPSIS
    Retrieves BitLocker and LAPS information for computers in Active Directory.
 
    .DESCRIPTION
    This function retrieves BitLocker and LAPS information for computers in Active Directory based on the specified parameters.
 
    .PARAMETER Forest
    Specifies the name of the forest to query for computer information.
 
    .PARAMETER IncludeDomains
    Specifies an array of domains to include in the query.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domains to exclude from the query.
 
    .PARAMETER Filter
    Specifies the filter to apply when querying for computers.
 
    .PARAMETER SearchBase
    Specifies the search base for the query.
 
    .PARAMETER SearchScope
    Specifies the scope of the search (Base, OneLevel, SubTree, None).
 
    .PARAMETER LapsOnly
    Switch to retrieve only LAPS information.
 
    .PARAMETER BitlockerOnly
    Switch to retrieve only BitLocker information.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional forest information to include in the query.
 
    .EXAMPLE
    Get-WinADBitlockerLapsSummary -Forest "contoso.com" -IncludeDomains "child1.contoso.com", "child2.contoso.com" -ExcludeDomains "test.contoso.com" -LapsOnly
    Retrieves LAPS information for computers in the specified domains of the "contoso.com" forest, excluding "test.contoso.com".
 
    .EXAMPLE
    Get-WinADBitlockerLapsSummary -Forest "contoso.com" -IncludeDomains "child1.contoso.com", "child2.contoso.com" -ExcludeDomains "test.contoso.com" -BitlockerOnly
    Retrieves BitLocker information for computers in the specified domains of the "contoso.com" forest, excluding "test.contoso.com".
 
    .NOTES
    General notes
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'LapsOnly')]
        [Parameter(ParameterSetName = 'BitlockerOnly')]
        [alias('ForestName')][string] $Forest,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'LapsOnly')]
        [Parameter(ParameterSetName = 'BitlockerOnly')]
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'LapsOnly')]
        [Parameter(ParameterSetName = 'BitlockerOnly')]
        [string[]] $ExcludeDomains,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'LapsOnly')]
        [Parameter(ParameterSetName = 'BitlockerOnly')]
        [string] $Filter = '*',

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'LapsOnly')]
        [Parameter(ParameterSetName = 'BitlockerOnly')]
        [string] $SearchBase,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'LapsOnly')]
        [Parameter(ParameterSetName = 'BitlockerOnly')]
        [ValidateSet('Base', 'OneLevel', 'SubTree', 'None')] [string] $SearchScope = 'None',


        [Parameter(ParameterSetName = 'LapsOnly')][switch] $LapsOnly,
        [Parameter(ParameterSetName = 'BitlockerOnly')][switch] $BitlockerOnly,

        [Parameter(ParameterSetName = 'Default')]
        [Parameter(ParameterSetName = 'LapsOnly')]
        [Parameter(ParameterSetName = 'BitlockerOnly')]
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $Today = Get-Date
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    $ComputerProperties = Get-WinADForestSchemaProperties -Schema 'Computers' -Forest $Forest -ExtendedForestInformation $ForestInformation
    $Properties = @(
        'Name'
        'OperatingSystem'
        'OperatingSystemVersion'
        'DistinguishedName'
        'LastLogonDate'
        'PasswordLastSet'
        'PrimaryGroupID'
        if ($ComputerProperties.Name -contains 'ms-Mcs-AdmPwd') {
            $LapsAvailable = $true
            'ms-Mcs-AdmPwd'
            'ms-Mcs-AdmPwdExpirationTime'
        } else {
            $LapsAvailable = $false
        }
        if ($ComputerProperties.Name -contains 'msLAPS-Password') {
            $WindowsLapsAvailable = $true
            'msLAPS-PasswordExpirationTime'
            'msLAPS-Password'
            'msLAPS-EncryptedPassword'
            'msLAPS-EncryptedPasswordHistory'
            'msLAPS-EncryptedDSRMPassword'
            'msLAPS-EncryptedDSRMPasswordHistory'
        } else {
            $WindowsLapsAvailable = $false
        }
    )
    $CurrentDate = Get-Date
    $FormattedComputers = foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]

        $Parameters = @{ }
        if ($SearchScope -ne 'None') {
            $Parameters.SearchScope = $SearchScope
        }
        if ($SearchBase) {
            # If SearchBase is defined we need to check it belongs to current domain
            # if it does, great. If not we need to skip it
            $DomainInformation = Get-ADDomain -Server $QueryServer
            $DNExtract = ConvertFrom-DistinguishedName -DistinguishedName $SearchBase -ToDC
            if ($DNExtract -eq $DomainInformation.DistinguishedName) {
                $Parameters.SearchBase = $SearchBase
            } else {
                continue
            }
        }
        try {
            $Computers = Get-ADComputer -Filter $Filter -Properties $Properties -Server $QueryServer @Parameters -ErrorAction Stop
        } catch {
            Write-Warning "Get-WinADBitlockerLapsSummary - Error getting computers $($_.Exception.Message)"
        }

        foreach ($_ in $Computers) {
            if ($LapsOnly -or -not $BitlockerOnly) {
                if ($LapsAvailable) {
                    if ($_.'ms-Mcs-AdmPwdExpirationTime') {
                        $Laps = $true
                        $LapsExpirationDays = Convert-TimeToDays -StartTime ($CurrentDate) -EndTime (Convert-ToDateTime -Timestring ($_.'ms-Mcs-AdmPwdExpirationTime'))
                        $LapsExpirationTime = Convert-ToDateTime -Timestring ($_.'ms-Mcs-AdmPwdExpirationTime')
                    } else {
                        $Laps = $false
                        $LapsExpirationDays = $null
                        $LapsExpirationTime = $null
                    }
                } else {
                    $Laps = $null
                }
            }

            if ($WindowsLapsAvailable) {
                if ($_.'msLAPS-PasswordExpirationTime') {
                    $WindowsLaps = $true
                    $WindowsLapsExpirationDays = Convert-TimeToDays -StartTime ($CurrentDate) -EndTime (Convert-ToDateTime -Timestring ($_.'msLAPS-PasswordExpirationTime'))
                    $WindowsLapsExpirationTime = Convert-ToDateTime -Timestring ($_.'msLAPS-PasswordExpirationTime')
                    $WindowsLapsHistoryCount = $_.'msLAPS-EncryptedPasswordHistory'.Count
                } else {
                    $WindowsLaps = $false
                    $WindowsLapsExpirationDays = $null
                    $WindowsLapsExpirationTime = $null
                    $WindowsLapsHistoryCount = 0
                }
            } else {
                $WindowsLaps = $null
                $WindowsLapsExpirationDays = $null
                $WindowsLapsExpirationTime = $null
                $WindowsLapsHistoryCount = 0
            }

            if (-not $LapsOnly -or $BitlockerOnly) {
                [Array] $Bitlockers = Get-ADObject -Server $QueryServer -Filter 'objectClass -eq "msFVE-RecoveryInformation"' -SearchBase $_.DistinguishedName -Properties 'WhenCreated', 'msFVE-RecoveryPassword' | Sort-Object -Descending
                if ($Bitlockers) {
                    $Encrypted = $true
                    $EncryptedTime = $Bitlockers[0].WhenCreated
                } else {
                    $Encrypted = $false
                    $EncryptedTime = $null
                }
            }
            if ($null -ne $_.LastLogonDate) {
                [int] $LastLogonDays = "$(-$($_.LastLogonDate - $Today).Days)"
            } else {
                $LastLogonDays = $null
            }
            if ($null -ne $_.PasswordLastSet) {
                [int] $PasswordLastChangedDays = "$(-$($_.PasswordLastSet - $Today).Days)"
            } else {
                $PasswordLastChangedDays = $null
            }

            if ($LapsOnly) {
                [PSCustomObject] @{
                    Name                      = $_.Name
                    Enabled                   = $_.Enabled
                    Domain                    = $Domain
                    DNSHostName               = $_.DNSHostName
                    IsDC                      = if ($_.PrimaryGroupID -in 516, 521) {
                        $true 
                    } else {
                        $false 
                    }
                    Laps                      = $Laps
                    LapsExpirationDays        = $LapsExpirationDays
                    LapsExpirationTime        = $LapsExpirationTime
                    WindowsLaps               = $WindowsLaps
                    WindowsLapsExpirationDays = $WindowsLapsExpirationDays
                    WindowsLapsExpirationTime = $WindowsLapsExpirationTime
                    WindowsLapsHistoryCount   = $WindowsLapsHistoryCount
                    System                    = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion
                    LastLogonDate             = $_.LastLogonDate
                    LastLogonDays             = $LastLogonDays
                    PasswordLastSet           = $_.PasswordLastSet
                    PasswordLastChangedDays   = $PasswordLastChangedDays
                    OrganizationalUnit        = ConvertFrom-DistinguishedName -DistinguishedName $_.DistinguishedName -ToOrganizationalUnit
                    DistinguishedName         = $_.DistinguishedName
                }
            } elseif ($BitlockerOnly) {
                [PSCustomObject] @{
                    Name                    = $_.Name
                    Enabled                 = $_.Enabled
                    Domain                  = $Domain
                    DNSHostName             = $_.DNSHostName
                    IsDC                    = if ($_.PrimaryGroupID -in 516, 521) {
                        $true 
                    } else {
                        $false 
                    }
                    Encrypted               = $Encrypted
                    EncryptedTime           = $EncryptedTime
                    System                  = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion
                    LastLogonDate           = $_.LastLogonDate
                    LastLogonDays           = $LastLogonDays
                    PasswordLastSet         = $_.PasswordLastSet
                    PasswordLastChangedDays = $PasswordLastChangedDays
                    OrganizationalUnit      = ConvertFrom-DistinguishedName -DistinguishedName $_.DistinguishedName -ToOrganizationalUnit
                    DistinguishedName       = $_.DistinguishedName
                }
            } else {
                [PSCustomObject] @{
                    Name                      = $_.Name
                    Enabled                   = $_.Enabled
                    Domain                    = $Domain
                    DNSHostName               = $_.DNSHostName
                    IsDC                      = if ($_.PrimaryGroupID -in 516, 521) {
                        $true 
                    } else {
                        $false 
                    }
                    Encrypted                 = $Encrypted
                    EncryptedTime             = $EncryptedTime
                    Laps                      = $Laps
                    LapsExpirationDays        = $LapsExpirationDays
                    LapsExpirationTime        = $LapsExpirationTime
                    WindowsLaps               = $WindowsLaps
                    WindowsLapsExpirationDays = $WindowsLapsExpirationDays
                    WindowsLapsExpirationTime = $WindowsLapsExpirationTime
                    WindowsLapsHistoryCount   = $WindowsLapsHistoryCount
                    System                    = ConvertTo-OperatingSystem -OperatingSystem $_.OperatingSystem -OperatingSystemVersion $_.OperatingSystemVersion
                    LastLogonDate             = $_.LastLogonDate
                    LastLogonDays             = $LastLogonDays
                    PasswordLastSet           = $_.PasswordLastSet
                    PasswordLastChangedDays   = $PasswordLastChangedDays
                    OrganizationalUnit        = ConvertFrom-DistinguishedName -DistinguishedName $_.DistinguishedName -ToOrganizationalUnit
                    DistinguishedName         = $_.DistinguishedName
                }
            }
        }
    }
    $FormattedComputers
}
function Get-WinADComputerACLLAPS {
    <#
    .SYNOPSIS
    Gathers information from all computers whether they have ACL to write to LAPS properties or not
 
    .DESCRIPTION
    Gathers information from all computers whether they have ACL to write to LAPS properties or not
 
    .PARAMETER ACLMissingOnly
    Show only computers which do not have ability to write to LAPS properties
 
    .EXAMPLE
    Get-WinADComputerAclLAPS | Format-Table *
 
    .EXAMPLE
    Get-WinADComputerAclLAPS -ACLMissingOnly | Format-Table *
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [switch] $ACLMissingOnly,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation

    foreach ($Domain in $ForestInformation.Domains) {
        $Computers = Get-ADComputer -Filter "*" -Properties PrimaryGroupID, LastLogonDate, PasswordLastSet, WhenChanged, OperatingSystem, servicePrincipalName -Server $ForestInformation.QueryServers[$Domain].HostName[0]
        foreach ($Computer in $Computers) {
            $ComputerLocation = ($Computer.DistinguishedName -split ',').Replace('OU=', '').Replace('CN=', '').Replace('DC=', '')
            $Region = $ComputerLocation[-4]
            $Country = $ComputerLocation[-5]
            $ACLs = Get-ADACL -ADObject $Computer.DistinguishedName -Principal 'NT AUTHORITY\SELF'

            $LAPS = $false
            $LAPSExpirationTime = $false
            $WindowsLAPS = $false
            $WindowsLAPSExpirationTime = $false
            $WindowsLAPSEncryptedPassword = $false
            #$WindowsLAPSEncryptedPasswordHistory = $false
            #$WindowsLAPSEncryptedDSRMPassword = $false
            # $WindowsLAPSEncryptedDSRMPasswordHistory = $false

            <#
            msLAPS-PasswordExpirationTime
            msLAPS-Password
            msLAPS-EncryptedPassword
            msLAPS-EncryptedPasswordHistory
            msLAPS-EncryptedDSRMPassword
            msLAPS-EncryptedDSRMPasswordHistory
            #>


            foreach ($ACL in $ACLs) {
                if ($ACL.ObjectTypeName -eq 'ms-Mcs-AdmPwd') {
                    # LAPS
                    if ($ACL.AccessControlType -eq 'Allow' -and $ACL.ActiveDirectoryRights -like '*WriteProperty*') {
                        $LAPS = $true
                    }
                } elseif ($ACL.ObjectTypeName -eq 'ms-Mcs-AdmPwdExpirationTime') {
                    # LAPS
                    if ($ACL.AccessControlType -eq 'Allow' -and $ACL.ActiveDirectoryRights -like '*WriteProperty*') {
                        $LAPSExpirationTime = $true
                    }
                } elseif ($ACL.ObjectTypeName -eq 'ms-LAPS-Password') {
                    # Windows LAPS
                    if ($ACL.AccessControlType -eq 'Allow' -and $ACL.ActiveDirectoryRights -like '*WriteProperty*') {
                        $WindowsLAPS = $true
                    }
                } elseif ($ACL.ObjectTypeName -eq 'ms-LAPS-PasswordExpirationTime') {
                    # Windows LAPS
                    if ($ACL.AccessControlType -eq 'Allow' -and $ACL.ActiveDirectoryRights -like '*WriteProperty*') {
                        $WindowsLAPSExpirationTime = $true
                    }
                } elseif ($ACL.ObjectTypeName -eq 'ms-LAPS-Encrypted-Password-Attributes') {
                    if ($ACL.AccessControlType -eq 'Allow' -and $ACL.ActiveDirectoryRights -like '*WriteProperty*') {
                        $WindowsLAPSEncryptedPassword = $true
                    }
                }
                # elseif ($ACL.ObjectTypeName -eq 'ms-LAPS-EncryptedPasswordHistory') {
                # if ($ACL.AccessControlType -eq 'Allow' -and $ACL.ActiveDirectoryRights -like '*WriteProperty*') {
                # $WindowsLAPSEncryptedPasswordHistory = $true
                # }
                # } elseif ($ACL.ObjectTypeName -eq 'ms-LAPS-EncryptedDSRMPassword') {
                # if ($ACL.AccessControlType -eq 'Allow' -and $ACL.ActiveDirectoryRights -like '*WriteProperty*') {
                # $WindowsLAPSEncryptedDSRMPassword = $true
                # }
                # } elseif ($ACL.ObjectTypeName -eq 'ms-LAPS-EncryptedDSRMPasswordHistory') {
                # if ($ACL.AccessControlType -eq 'Allow' -and $ACL.ActiveDirectoryRights -like '*WriteProperty*') {
                # $WindowsLAPSEncryptedDSRMPasswordHistory = $true
                # }
                # }
            }
            if ($ACLMissingOnly -and $LAPS -eq $true) {
                continue
            }

            [PSCustomObject] @{
                Name                         = $Computer.Name
                SamAccountName               = $Computer.SamAccountName
                DomainName                   = $Domain
                Enabled                      = $Computer.Enabled
                IsDC                         = if ($Computer.PrimaryGroupID -in 516, 521) {
                    $true 
                } else {
                    $false 
                }
                WhenChanged                  = $Computer.WhenChanged
                LapsACL                      = $LAPS
                LapsExpirationACL            = $LAPSExpirationTime
                WindowsLAPSACL               = $WindowsLAPS
                WindowsLAPSExpirationACL     = $WindowsLAPSExpirationTime
                WindowsLAPSEncryptedPassword = $WindowsLAPSEncryptedPassword
                #WindowsLAPSEncryptedPasswordHistory = $WindowsLAPSEncryptedPasswordHistory
                #WindowsLAPSEncryptedDSRMPassword = $WindowsLAPSEncryptedDSRMPassword
                #WindowsLAPSEncryptedDSRMPasswordHistory = $WindowsLAPSEncryptedDSRMPasswordHistory
                OperatingSystem              = $Computer.OperatingSystem
                Level0                       = $Region
                Level1                       = $Country
                DistinguishedName            = $Computer.DistinguishedName
                LastLogonDate                = $Computer.LastLogonDate
                PasswordLastSet              = $Computer.PasswordLastSet
                ServicePrincipalName         = $Computer.servicePrincipalName
            }
        }
    }
}
function Get-WinADComputers {
    <#
    .SYNOPSIS
    Retrieves information about computers in Active Directory.
 
    .DESCRIPTION
    This function retrieves information about computers in Active Directory based on the specified parameters.
 
    .PARAMETER Forest
    Specifies the name of the forest to query for computer information.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domains to exclude from the query.
 
    .PARAMETER IncludeDomains
    Specifies an array of domains to include in the query.
 
    .PARAMETER PerDomain
    Indicates whether to retrieve information per domain.
 
    .PARAMETER AddOwner
    Indicates whether to include owner information for the computers.
 
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [switch] $PerDomain,
        [switch] $AddOwner
    )

    $AllUsers = [ordered] @{}
    $AllContacts = [ordered] @{}
    $AllGroups = [ordered] @{}
    $AllComputers = [ordered] @{}
    $CacheUsersReport = [ordered] @{}

    $Today = Get-Date
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]

        $Properties = @(
            'DistinguishedName', 'mail', 'LastLogonDate', 'PasswordLastSet', 'DisplayName', 'Manager', 'Description',
            'PasswordNeverExpires', 'PasswordNotRequired', 'PasswordExpired', 'UserPrincipalName', 'SamAccountName', 'CannotChangePassword',
            'TrustedForDelegation', 'TrustedToAuthForDelegation', 'msExchMailboxGuid', 'msExchRemoteRecipientType', 'msExchRecipientTypeDetails',
            'msExchRecipientDisplayType', 'pwdLastSet', "msDS-UserPasswordExpiryTimeComputed",
            'WhenCreated', 'WhenChanged'
        )
        $AllUsers[$Domain] = Get-ADUser -Filter "*" -Properties $Properties -Server $QueryServer #$ForestInformation['QueryServers'][$Domain].HostName[0]
        $AllContacts[$Domain] = Get-ADObject -Filter 'objectClass -eq "contact"' -Properties SamAccountName, Mail, Name, DistinguishedName, WhenChanged, Whencreated, DisplayName -Server $QueryServer
        $Properties = @(
            'SamAccountName', 'CanonicalName', 'Mail', 'Name', 'DistinguishedName', 'isCriticalSystemObject', 'ObjectSID'
        )
        $AllGroups[$Domain] = Get-ADGroup -Filter "*" -Properties $Properties -Server $QueryServer
        $Properties = @(
            'DistinguishedName', 'LastLogonDate', 'PasswordLastSet', 'Enabled', 'DnsHostName', 'PasswordNeverExpires', 'PasswordNotRequired',
            'PasswordExpired', 'ManagedBy', 'OperatingSystemVersion', 'OperatingSystem' , 'TrustedForDelegation', 'WhenCreated', 'WhenChanged', 'PrimaryGroupID'
            'nTSecurityDescriptor'
        )
        $AllComputers[$Domain] = Get-ADComputer -Filter "*" -Server $QueryServer -Properties $Properties
    }

    foreach ($Domain in $AllUsers.Keys) {
        foreach ($U in $AllUsers[$Domain]) {
            $CacheUsersReport[$U.DistinguishedName] = $U
        }
    }
    foreach ($Domain in $AllContacts.Keys) {
        foreach ($C in $AllContacts[$Domain]) {
            $CacheUsersReport[$C.DistinguishedName] = $C
        }
    }
    foreach ($Domain in $AllGroups.Keys) {
        foreach ($G in $AllGroups[$Domain]) {
            $CacheUsersReport[$G.DistinguishedName] = $G
        }
    }


    $Output = [ordered] @{}
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        $Output[$Domain] = foreach ($Computer in $AllComputers[$Domain]) {
            $ComputerLocation = ($Computer.DistinguishedName -split ',').Replace('OU=', '').Replace('CN=', '').Replace('DC=', '')
            $Region = $ComputerLocation[-4]
            $Country = $ComputerLocation[-5]

            if ($Computer.ManagedBy) {
                $Manager = $CacheUsersReport[$Computer.ManagedBy].Name
                $ManagerSamAccountName = $CacheUsersReport[$Computer.ManagedBy].SamAccountName
                $ManagerEmail = $CacheUsersReport[$Computer.ManagedBy].Mail
                $ManagerEnabled = $CacheUsersReport[$Computer.ManagedBy].Enabled
                $ManagerLastLogon = $CacheUsersReport[$Computer.ManagedBy].LastLogonDate
                if ($ManagerLastLogon) {
                    $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days)
                } else {
                    $ManagerLastLogonDays = $null
                }
                $ManagerStatus = if ($ManagerEnabled -eq $true) {
                    'Enabled' 
                } elseif ($ManagerEnabled -eq $false) {
                    'Disabled' 
                } else {
                    'Not available' 
                }
            } else {
                $ManagerStatus = 'Not available'
                $Manager = $null
                $ManagerSamAccountName = $null
                $ManagerEmail = $null
                $ManagerEnabled = $null
                $ManagerLastLogon = $null
                $ManagerLastLogonDays = $null
            }

            if ($null -ne $Computer.LastLogonDate) {
                $LastLogonDays = "$(-$($Computer.LastLogonDate - $Today).Days)"
            } else {
                $LastLogonDays = $null
            }
            if ($null -ne $Computer.PasswordLastSet) {
                $PasswordLastChangedDays = "$(-$($Computer.PasswordLastSet - $Today).Days)"
            } else {
                $PasswordLastChangedDays = $null
            }

            if ($AddOwner) {
                $Owner = Get-ADACLOwner -ADObject $Computer -Verbose -Resolve
                [PSCustomObject] @{
                    Name                  = $Computer.Name
                    SamAccountName        = $Computer.SamAccountName
                    Domain                = $Domain
                    IsDC                  = if ($Computer.PrimaryGroupID -in 516, 521) {
                        $true 
                    } else {
                        $false 
                    }
                    WhenChanged           = $Computer.WhenChanged
                    Enabled               = $Computer.Enabled
                    LastLogonDays         = $LastLogonDays
                    PasswordLastDays      = $PasswordLastChangedDays
                    Level0                = $Region
                    Level1                = $Country
                    OperatingSystem       = $Computer.OperatingSystem
                    #OperatingSystemVersion = $Computer.OperatingSystemVersion
                    OperatingSystemName   = ConvertTo-OperatingSystem -OperatingSystem $Computer.OperatingSystem -OperatingSystemVersion $Computer.OperatingSystemVersion
                    DistinguishedName     = $Computer.DistinguishedName
                    LastLogonDate         = $Computer.LastLogonDate
                    PasswordLastSet       = $Computer.PasswordLastSet
                    PasswordNeverExpires  = $Computer.PasswordNeverExpires
                    PasswordNotRequired   = $Computer.PasswordNotRequired
                    PasswordExpired       = $Computer.PasswordExpired
                    ManagerStatus         = $ManagerStatus
                    Manager               = $Manager
                    ManagerSamAccountName = $ManagerSamAccountName
                    ManagerEmail          = $ManagerEmail
                    ManagerLastLogonDays  = $ManagerLastLogonDays
                    OwnerName             = $Owner.OwnerName
                    OwnerSID              = $Owner.OwnerSID
                    OwnerType             = $Owner.OwnerType
                    ManagerDN             = $Computer.ManagedBy
                    Description           = $Computer.Description
                    TrustedForDelegation  = $Computer.TrustedForDelegation
                }
            } else {
                $Owner = $null
                [PSCustomObject] @{
                    Name                  = $Computer.Name
                    SamAccountName        = $Computer.SamAccountName
                    Domain                = $Domain
                    IsDC                  = if ($Computer.PrimaryGroupID -in 516, 521) {
                        $true 
                    } else {
                        $false 
                    }
                    WhenChanged           = $Computer.WhenChanged
                    Enabled               = $Computer.Enabled
                    LastLogonDays         = $LastLogonDays
                    PasswordLastDays      = $PasswordLastChangedDays
                    Level0                = $Region
                    Level1                = $Country
                    OperatingSystem       = $Computer.OperatingSystem
                    #OperatingSystemVersion = $Computer.OperatingSystemVersion
                    OperatingSystemName   = ConvertTo-OperatingSystem -OperatingSystem $Computer.OperatingSystem -OperatingSystemVersion $Computer.OperatingSystemVersion
                    DistinguishedName     = $Computer.DistinguishedName
                    LastLogonDate         = $Computer.LastLogonDate
                    PasswordLastSet       = $Computer.PasswordLastSet
                    PasswordNeverExpires  = $Computer.PasswordNeverExpires
                    PasswordNotRequired   = $Computer.PasswordNotRequired
                    PasswordExpired       = $Computer.PasswordExpired
                    ManagerStatus         = $ManagerStatus
                    Manager               = $Manager
                    ManagerSamAccountName = $ManagerSamAccountName
                    ManagerEmail          = $ManagerEmail
                    ManagerLastLogonDays  = $ManagerLastLogonDays
                    ManagerDN             = $Computer.ManagedBy
                    Description           = $Computer.Description
                    TrustedForDelegation  = $Computer.TrustedForDelegation
                }
            }
        }
    }
    if ($PerDomain) {
        $Output
    } else {
        foreach ($O in $Output.Keys) {
            $Output[$O]
        }
    }
}
function Get-WinADDelegatedAccounts {
    <#
    .SYNOPSIS
    Retrieves delegated accounts information from Active Directory.
 
    .DESCRIPTION
    This function retrieves delegated accounts information from Active Directory based on the specified parameters.
 
    .PARAMETER Forest
    Specifies the name of the forest to retrieve delegated accounts information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domains to exclude from the search.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controllers to exclude from the search.
 
    .PARAMETER IncludeDomains
    Specifies an array of domains to include in the search.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controllers to include in the search.
 
    .PARAMETER SkipRODC
    Indicates whether to skip Read-Only Domain Controllers (RODC) during the search.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional forest information to include in the search.
 
    .NOTES
    File Name : Get-WinADDelegatedAccounts.ps1
    Author : Your Name
    Prerequisite : This function requires the Active Directory module.
 
    .EXAMPLE
    Get-WinADDelegatedAccounts -Forest "contoso.com" -IncludeDomains "child1.contoso.com", "child2.contoso.com" -ExcludeDomains "test.contoso.com" -ExtendedForestInformation $ExtendedInfo
    Retrieves delegated accounts information from the "contoso.com" forest, including child domains "child1.contoso.com" and "child2.contoso.com", excluding the "test.contoso.com" domain, and using extended forest information.
 
    #>


    [CmdletBinding()]
    Param (
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation -Extended
    foreach ($Domain in $ForestInformation.Domains) {

        $SERVER_TRUST_ACCOUNT = 0x2000
        $TRUSTED_FOR_DELEGATION = 0x80000
        $TRUSTED_TO_AUTH_FOR_DELEGATION = 0x1000000
        $PARTIAL_SECRETS_ACCOUNT = 0x4000000

        $bitmask = $TRUSTED_FOR_DELEGATION -bor $TRUSTED_TO_AUTH_FOR_DELEGATION -bor $PARTIAL_SECRETS_ACCOUNT

        $filter = @"
(&
  (servicePrincipalname=*)
  (|
    (msDS-AllowedToActOnBehalfOfOtherIdentity=*)
    (msDS-AllowedToDelegateTo=*)
    (UserAccountControl:1.2.840.113556.1.4.804:=$bitmask)
  )
  (|
    (objectcategory=computer)
    (objectcategory=person)
    (objectcategory=msDS-GroupManagedServiceAccount)
    (objectcategory=msDS-ManagedServiceAccount)
  )
)
"@
 -replace "[\s\n]", ''

        $PropertyList = @(
            'Enabled'
            "servicePrincipalname",
            "useraccountcontrol",
            "samaccountname",
            "msDS-AllowedToDelegateTo",
            "msDS-AllowedToActOnBehalfOfOtherIdentity"
            'IsCriticalSystemObject'
            'LastLogon'
            'PwdLastSet'
            'WhenChanged'
            'WhenCreated'
        )

        try {
            $Accounts = Get-ADObject -LDAPFilter $filter -SearchBase $ForestInformation.DomainsExtended[$Domain].DistinguishedName -SearchScope Subtree -Properties $propertylist -Server $ForestInformation.QueryServers[$Domain].HostName[0]
        } catch {
            $Accounts = $null
            Write-Warning -Message "Get-WinADDelegatedAccounts - Failed to get information: $($_.Exception.Message)"
        }

        foreach ($Account in $Accounts) {
            $UAC = Convert-UserAccountControl -UserAccountControl $Account.useraccountcontrol
            $IsDC = ($Account.useraccountcontrol -band $SERVER_TRUST_ACCOUNT) -ne 0
            $FullDelegation = ($Account.useraccountcontrol -band $TRUSTED_FOR_DELEGATION) -ne 0
            $ConstrainedDelegation = ($Account.'msDS-AllowedToDelegateTo').count -gt 0
            $IsRODC = ($Account.useraccountcontrol -band $PARTIAL_SECRETS_ACCOUNT) -ne 0
            $ResourceDelegation = $null -ne $Account.'msDS-AllowedToActOnBehalfOfOtherIdentity'
            $PasswordLastSet = [datetime]::FromFileTimeUtc($Account.pwdLastSet)
            $LastLogonDate = [datetime]::FromFileTimeUtc($Account.LastLogon)

            [PSCustomobject] @{
                DomainName                          = $Domain
                SamAccountName                      = $Account.samaccountname
                Enabled                             = $UAC -notcontains 'ACCOUNTDISABLE'
                ObjectClass                         = $Account.objectclass
                IsDC                                = $IsDC
                IsRODC                              = $IsRODC
                FullDelegation                      = $FullDelegation
                ConstrainedDelegation               = $ConstrainedDelegation
                ResourceDelegation                  = $ResourceDelegation
                LastLogonDate                       = $LastLogonDate
                PasswordLastSet                     = $PasswordLastSet
                UserAccountControl                  = $UAC
                WhenCreated                         = $Account.WhenCreated
                WhenChanged                         = $Account.WhenChanged
                IsCriticalSystemObject              = $Account.IsCriticalSystemObject
                AllowedToDelagateTo                 = $Account.'msDS-AllowedToDelegateTo'
                AllowedToActOnBehalfOfOtherIdentity = $Account.'msDS-AllowedToActOnBehalfOfOtherIdentity'
            }
        }
    }
}
function Get-WinADDFSHealth {
    <#
    .SYNOPSIS
    Retrieves health information for Active Directory Federation Services (AD FS).
 
    .DESCRIPTION
    This function retrieves health information for AD FS based on specified parameters.
 
    .PARAMETER Forest
    Specifies the name of the forest to retrieve information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domains to exclude from the health check.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controllers to exclude from the health check.
 
    .PARAMETER IncludeDomains
    Specifies an array of domains to include in the health check.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controllers to include in the health check.
 
    .PARAMETER SkipRODC
    Indicates whether to skip read-only domain controllers in the health check.
 
    .PARAMETER EventDays
    Specifies the number of days to look back for events. Default is 1 day.
 
    .PARAMETER SkipGPO
    Indicates whether to skip checking Group Policy Objects (GPOs).
 
    .PARAMETER SkipAutodetection
    Indicates whether to skip automatic detection of domain controllers.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional forest information to include in the health check.
 
    .EXAMPLE
    Get-WinADDFSHealth -Forest "contoso.com" -IncludeDomains "contoso.com" -SkipGPO
 
    Retrieves health information for the "contoso.com" forest, including only the "contoso.com" domain and skipping GPO checks.
 
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [int] $EventDays = 1,
        [switch] $SkipGPO,
        [switch] $SkipAutodetection,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $Today = (Get-Date)
    $Yesterday = (Get-Date -Hour 0 -Second 0 -Minute 0 -Millisecond 0).AddDays(-$EventDays)

    if (-not $SkipAutodetection) {
        $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation -Extended
    } else {
        if (-not $IncludeDomains) {
            Write-Warning "Get-WinADDFSHealth - You need to specify domain when using SkipAutodetection."
            return
        }
        # This is for case when Get-ADDomainController -Filter "*" is broken
        $ForestInformation = @{
            Domains                 = $IncludeDomains
            DomainDomainControllers = @{}
        }
        foreach ($Domain in $IncludeDomains) {
            $ForestInformation['DomainDomainControllers'][$Domain] = [System.Collections.Generic.List[Object]]::new()
            foreach ($DC in $IncludeDomainControllers) {
                try {
                    $DCInformation = Get-ADDomainController -Identity $DC -Server $Domain -ErrorAction Stop
                    Add-Member -InputObject $DCInformation -MemberType NoteProperty -Value $DCInformation.ComputerObjectDN -Name 'DistinguishedName' -Force
                    $ForestInformation['DomainDomainControllers'][$Domain].Add($DCInformation)
                } catch {
                    Write-Warning "Get-WinADDFSHealth - Can't get DC details. Skipping with error: $($_.Exception.Message)"
                    continue
                }
            }
        }
    }
    [Array] $Table = foreach ($Domain in $ForestInformation.Domains) {
        Write-Verbose "Get-WinADDFSHealth - Processing $Domain"
        [Array] $DomainControllersFull = $ForestInformation['DomainDomainControllers']["$Domain"]
        if ($DomainControllersFull.Count -eq 0) {
            continue
        }
        if (-not $SkipAutodetection) {
            $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        } else {
            $QueryServer = $DomainControllersFull[0].HostName
        }
        if (-not $SkipGPO) {
            try {
                #[Array]$GPOs = @(Get-GPO -All -Domain $Domain -Server $QueryServer)
                $SystemsContainer = $ForestInformation['DomainsExtended'][$Domain].SystemsContainer
                if ($SystemsContainer) {
                    $PoliciesSearchBase = -join ("CN=Policies,", $SystemsContainer)
                }
                [Array]$GPOs = Get-ADObject -ErrorAction Stop -SearchBase $PoliciesSearchBase -SearchScope OneLevel -Filter "*" -Server $QueryServer -Properties Name, gPCFileSysPath, DisplayName, DistinguishedName, Description, Created, Modified, ObjectClass, ObjectGUID
            } catch {
                $GPOs = $null
            }
        }
        try {
            $CentralRepository = Get-ChildItem -Path "\\$Domain\SYSVOL\$Domain\policies\PolicyDefinitions" -ErrorAction Stop
            $CentralRepositoryDomain = if ($CentralRepository) {
                $true 
            } else {
                $false 
            }
        } catch {
            $CentralRepositoryDomain = $false
        }

        foreach ($DC in $DomainControllersFull) {
            Write-Verbose "Get-WinADDFSHealth - Processing $($DC.HostName) for $Domain"
            $DCName = $DC.Name
            $Hostname = $DC.Hostname
            $DN = $DC.DistinguishedName

            $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.IsPDC
                'GroupPolicyOutput'             = $null -ne $GPOs # This shows whether output was on Get-GPO
                "GroupPolicyCount"              = if ($GPOs) {
                    $GPOs.Count 
                } else {
                    0 
                };
                "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"       = $null
            }
            if ($SkipGPO) {
                $DomainSummary.Remove('GroupPolicyOutput')
                $DomainSummary.Remove('GroupPolicyCount')
                $DomainSummary.Remove('SYSVOLCount')
            }
            <#
            PS C:\Windows\system32> Get-CimData -NameSpace "root\microsoftdfs" -Class 'dfsrreplicatedfolderinfo' -ComputerName ad | Where-Object { $_.ReplicationGroupname -eq 'Domain System Volume' }
 
 
            CurrentConflictSizeInMb : 0
            CurrentStageSizeInMb : 1
            LastConflictCleanupTime : 2020-03-22 23:54:17
            LastErrorCode : 0
            LastErrorMessageId : 0
            LastTombstoneCleanupTime : 2020-03-22 23:54:17
            MemberGuid : 9650D20E-0D00-43AC-AC1F-4D11EDC17E27
            MemberName : AD
            ReplicatedFolderGuid : 5FFB282C-A802-4700-89A5-B59B7A0EF671
            ReplicatedFolderName : SYSVOL Share
            ReplicationGroupGuid : C2E87E8F-18CC-41A4-8072-A1B9A4F2ACF6
            ReplicationGroupName : Domain System Volume
            State : 4
            PSComputerName : AD
 
 
            #>



            <# NameSpace "root\microsoftdfs" Class 'dfsrreplicatedfolderinfo'
            CurrentConflictSizeInMb : 0
            CurrentStageSizeInMb : 0
            LastConflictCleanupTime : 13.09.2019 07:59:38
            LastErrorCode : 0
            LastErrorMessageId : 0
            LastTombstoneCleanupTime : 13.09.2019 07:59:38
            MemberGuid : A8930B63-1405-4E0B-AE43-840DAAC64DCE
            MemberName : AD1
            ReplicatedFolderGuid : 58836C0B-1AB9-49A9-BE64-57689A5A6350
            ReplicatedFolderName : SYSVOL Share
            ReplicationGroupGuid : 7DA3CD45-CF61-4D95-AB46-6DC859DD689B
            ReplicationGroupName : Domain System Volume
            State : 2
            PSComputerName : AD1
            #>

            $WarningVar = $null
            $DFSReplicatedFolderInfoAll = Get-CimData -NameSpace "root\microsoftdfs" -Class 'dfsrreplicatedfolderinfo' -ComputerName $Hostname -WarningAction SilentlyContinue -WarningVariable WarningVar -Verbose:$false
            $DFSReplicatedFolderInfo = $DFSReplicatedFolderInfoAll | Where-Object { $_.ReplicationGroupName -eq 'Domain System Volume' }
            if ($WarningVar) {
                $DomainSummary['ReplicationState'] = 'Unknown'
                #$DomainSummary['ReplicationState'] = $WarningVar -join ', '
            } else {
                $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 -Identity $Subscriber -Properties msDFSR-MemberReference -Server $QueryServer -ErrorAction Stop).'msDFSR-MemberReference' -like "CN=$DCName,*"
                $DomainSummary['MemberReference'] = if ($MemberReference) {
                    $true 
                } else {
                    $false 
                }
            } catch {
                $DomainSummary['MemberReference'] = $false
            }
            try {
                $DFSLocalSetting = Get-ADObject -Identity $LocalSettings -Server $QueryServer -ErrorAction Stop
                $DomainSummary['DFSLocalSetting'] = if ($DFSLocalSetting) {
                    $true 
                } else {
                    $false 
                }
            } catch {
                $DomainSummary['DFSLocalSetting'] = $false
            }

            try {
                $DomainSystemVolume = Get-ADObject -Identity $Subscriber -Server $QueryServer -ErrorAction Stop
                $DomainSummary['DomainSystemVolume'] = if ($DomainSystemVolume) {
                    $true 
                } else {
                    $false 
                }
            } catch {
                $DomainSummary['DomainSystemVolume'] = $false
            }
            try {
                $SysVolSubscription = Get-ADObject -Identity $Subscription -Server $QueryServer -ErrorAction Stop
                $DomainSummary['SYSVOLSubscription'] = if ($SysVolSubscription) {
                    $true 
                } else {
                    $false 
                }
            } catch {
                $DomainSummary['SYSVOLSubscription'] = $false
            }
            if (-not $SkipGPO) {
                try {
                    [Array] $SYSVOL = Get-ChildItem -Path "\\$Hostname\SYSVOL\$Domain\Policies" -Exclude "PolicyDefinitions*" -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']

            try {
                $Registry = Get-PSRegistry -RegistryPath "HKLM\SYSTEM\CurrentControlSet\Services\DFSR\Parameters" -ComputerName $Hostname -ErrorAction Stop
            } catch {
                #$ErrorMessage = $_.Exception.Message
                $Registry = $null
            }
            if ($null -ne $Registry.StopReplicationOnAutoRecovery) {
                $DomainSummary['StopReplicationOnAutoRecovery'] = [bool] $Registry.StopReplicationOnAutoRecovery
            } else {
                $DomainSummary['StopReplicationOnAutoRecovery'] = $null
                # $DomainSummary['StopReplicationOnAutoRecovery'] = $ErrorMessage
            }
            $DomainSummary['DFSReplicatedFolderInfo'] = $DFSReplicatedFolderInfoAll

            $All = @(
                if (-not $SkipGPO) {
                    $DomainSummary['GroupPolicyOutput']
                }
                $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-WinADDFSTopology {
    <#
    .SYNOPSIS
    This command gets the DFS topology for a forest, listing it's current members
 
    .DESCRIPTION
    This command gets the DFS topology for a forest, listing it's current members.
    It can be used to find broken DFS members, which then can be removed using Remove-WinADDFSTopology
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER Type
    Type of objects to return (MissingAtLeastOne, MissingAll, All)
 
    .EXAMPLE
    Get-WinADDFSTopology | ft -AutoSize
 
    .NOTES
    General notes
    #>

    [cmdletbinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [ValidateSet('MissingAtLeastOne', 'MissingAll', 'All')][string] $Type = 'All'
    )
    Write-Verbose -Message "Get-WinADDFSTopology - Getting forest information"
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains

    $Properties = @(
        'Name', 'msDFSR-ComputerReference', 'msDFSR-MemberReferenceBL',
        'ProtectedFromAccidentalDeletion', 'serverReference',
        'WhenChanged', 'WhenCreated',
        'DistinguishedName'
    )

    foreach ($Domain in $ForestInformation.Domains) {
        Write-Verbose -Message "Get-WinADDFSTopology - Getting topology for $Domain"
        $DomainDN = ConvertTo-DistinguishedName -CanonicalName $Domain -ToDomain
        $QueryServer = $ForestInformation['QueryServers'][$Domain].HostName[0]
        $ObjectsInOu = Get-ADObject -LDAPFilter "(ObjectClass=msDFSR-Member)" -Properties $Properties -SearchBase "CN=Topology,CN=Domain System Volume,CN=DFSR-GlobalSettings,CN=System,$DomainDN" -Server $QueryServer
        #$Data = $ObjectsInOu | Select-Object -Property Name, msDFSR-ComputerReference, msDFSR-MemberReferenceBL, ProtectedFromAccidentalDeletion, serverReference, WhenChanged, WhenCreated, DistinguishedName
        foreach ($Object in $ObjectsInOu) {
            if ($null -eq $Object.'msDFSR-ComputerReference' -and ($null -eq $Object.'msDFSR-MemberReferenceBL' -or $Object.'msDFSR-MemberReferenceBL'.Count -eq 0) -and $null -eq $Object.serverReference) {
                $Status = 'MissingAll'
            } elseif ($null -eq $Object.serverReference) {
                $Status = 'MissingAtLeastOne'
            } elseif ($null -eq $Object.'msDFSR-ComputerReference') {
                $Status = 'MissingAtLeastOne'
            } elseif ($null -eq $Object.'msDFSR-MemberReferenceBL' -or $Object.'msDFSR-MemberReferenceBL'.Count -eq 0) {
                $Status = 'MissingAtLeastOne'
            } else {
                $Status = 'OK'
            }

            $DataObject = [PSCustomObject] @{
                'Name'                            = $Object.Name
                'Status'                          = $Status
                'Domain'                          = $Domain
                'msDFSR-ComputerReference'        = $Object.'msDFSR-ComputerReference'
                'msDFSR-MemberReferenceBL'        = $Object.'msDFSR-MemberReferenceBL'
                'ServerReference'                 = $Object.serverReference
                'ProtectedFromAccidentalDeletion' = $Object.ProtectedFromAccidentalDeletion
                'WhenChanged'                     = $Object.WhenChanged
                'WhenCreated'                     = $Object.WhenCreated
                'DistinguishedName'               = $Object.DistinguishedName
                'QueryServer'                     = $QueryServer
            }

            if ($Type -eq 'MissingAll') {
                if ($Status -eq 'MissingAll') {
                    $DataObject
                }
            } elseif ($Type -eq 'MissingAtLeastOne') {
                if ($Status -in 'MissingAll', 'MissingAtLeastOne') {
                    $DataObject
                }
            } else {
                $DataObject
            }
        }
    }
}
function Get-WinADDHCP {
    <#
    .SYNOPSIS
    Retrieves DHCP information from Active Directory forest domain controllers.
 
    .DESCRIPTION
    This function retrieves DHCP information from Active Directory forest domain controllers. It collects DHCP server details such as DNS name, IP address, whether it is a domain controller, read-only domain controller, global catalog, and the associated IPv4 and IPv6 addresses.
 
    .PARAMETER None
    No parameters are required for this function.
 
    .EXAMPLE
    Get-WinADDHCP
    Retrieves DHCP information from all Active Directory forest domain controllers.
 
    .NOTES
    This function requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory DHCP servers.
    #>

    [cmdletBinding()]
    param(

    )
    $ForestDomainControllers = Get-WinADForestControllers
    try {
        $DHCPs = Get-DhcpServerInDC -Verbose
    } catch {
        Write-Warning -Message "Get-WinADDHCP - Couldn't get DHCP data from AD: $($_.Exception.Message)"
        return
    }
    $CacheDHCP = @{}
    $CacheAD = [ordered] @{}
    foreach ($DHCP in $DHCPs) {
        $CacheDHCP[$DHCP.DNSName] = $DHCP
    }
    foreach ($DC in $ForestDomainControllers) {
        $CacheAD[$DC.HostName] = $DC
    }

    foreach ($DHCP in $DHCPs) {
        $DHCPObject = [ordered] @{
            DNSName   = $DHCP.DNSName
            IPAddress = $DHCP.IPAddress
        }
        if ($CacheAD[$DHCP.DNSName]) {
            $DHCPObject['IsDC'] = $true
            $DHCPObject['IsRODC'] = $CacheAD[$DHCP.DNSName].IsReadOnly
            $DHCPObject['IsGlobalCatalog'] = $CacheAD[$DHCP.DNSName].IsGlobalCatalog
            $DHCPObject['DCIPv4'] = $CacheAD[$DHCP.DNSName].IPV4Address
            $DHCPObject['DCIPv6'] = $CacheAD[$DHCP.DNSName].IPV6Address
        } else {
            $DHCPObject['IsDC'] = $false
            $DHCPObject['IsRODC'] = $false
            $DHCPObject['IsGlobalCatalog'] = $false
            $DHCPObject['DCIPv4'] = $null
            $DHCPObject['DCIPv6'] = $null
        }
        $DNS = Resolve-DnsName -Name $DHCP.DNSName -ErrorAction SilentlyContinue
        if ($DNS) {
            $DHCPObject['IsInDNS'] = $true
            $DHCPObject['DNSType'] = $DNS.Type
        } else {
            $DHCPObject['IsInDNS'] = $false
            $DHCPObject['DNSType'] = $null
        }
        [PSCustomObject] $DHCPObject
    }
}
function Get-WinADDiagnostics {
    <#
    .SYNOPSIS
    Retrieves diagnostic information for Active Directory.
 
    .DESCRIPTION
    This function retrieves diagnostic information for Active Directory based on specified parameters.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve diagnostic information from. By default, the current forest is used.
 
    .PARAMETER ExcludeDomains
    Specifies the domains to exclude from the search. By default, the entire forest is scanned.
 
    .PARAMETER IncludeDomains
    Specifies the specific domains to include in the search. By default, the entire forest is scanned.
 
    .PARAMETER ExcludeDomainControllers
    Specifies the domain controllers to exclude. By default, no exclusions are applied unless the VerifyDomainControllers switch is enabled.
 
    .PARAMETER IncludeDomainControllers
    Specifies the domain controllers to include. By default, all domain controllers are included unless the VerifyDomainControllers switch is enabled.
 
    .PARAMETER SkipRODC
    Skips Read-Only Domain Controllers. By default, all domain controllers are included.
 
    .PARAMETER ExtendedForestInformation
    Allows providing Forest Information from another command to speed up processing.
 
    .EXAMPLE
    Get-WinADDiagnostics -Forest "example.com" -IncludeDomains "domain1", "domain2" -ExcludeDomains "domain3" -SkipRODC
 
    .NOTES
    This function is designed to provide diagnostic information for troubleshooting Active Directory issues.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )

    <# Levels
    0 (None): Only critical events and error events are logged at this level. This is the default setting for all entries, and it should be modified only if a problem occurs that you want to investigate.
    1 (Minimal): Very high-level events are recorded in the event log at this setting. Events may include one message for each major task that is performed by the service. Use this setting to start an investigation when you do not know the location of the problem.
    2 (Basic)
    3 (Extensive): This level records more detailed information than the lower levels, such as steps that are performed to complete a task. Use this setting when you have narrowed the problem to a service or a group of categories.
    4 (Verbose)
    5 (Internal): This level logs all events, including debug strings and configuration changes. A complete log of the service is recorded. Use this setting when you have traced the problem to a particular category of a small set of categories.
    #>

    $LevelsDictionary = @{
        '0' = 'None'
        '1' = 'Minimal'
        '2' = 'Basic'
        '3' = 'Extensive'
        '4' = 'Verbose'
        '5' = 'Internal'
        ''  = 'Unknown'
    }

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    [Array] $Computers = $ForestInformation.ForestDomainControllers.HostName

    foreach ($Computer in $Computers) {
        try {
            $Output = Get-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics' -ComputerName $Computer -Verbose:$false -ErrorAction Stop
        } catch {
            $ErrorMessage1 = $_.Exception.Message
            $Output = $null
        }
        try {
            $Output1 = Get-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters'  -ComputerName $Computer -Verbose:$false -ErrorAction Stop
            if ($Output1.DbFlag -eq 545325055) {
                $Netlogon = $true
            } else {
                $Netlogon = $false
            }
        } catch {
            $ErrorMessage2 = $_.Exception.Message
            $Netlogon = 'Unknown'
        }


        if (-not $ErrorMessage1 -and -not $ErrorMessage2) {
            $Comment = 'OK'
            [PSCustomObject] @{
                'ComputerName'                        = $Computer
                'Knowledge Consistency Checker (KCC)' = $LevelsDictionary["$($Output.'1 Knowledge Consistency Checker')"]
                'Security Events'                     = $LevelsDictionary["$($Output.'2 Security Events')"]
                'ExDS Interface Events'               = $LevelsDictionary["$($Output.'3 ExDS Interface Events')"]
                'MAPI Interface Events'               = $LevelsDictionary["$($Output.'4 MAPI Interface Events')"]
                'Replication Events'                  = $LevelsDictionary["$($Output.'5 Replication Events')"]
                'Garbage Collection'                  = $LevelsDictionary["$($Output.'6 Garbage Collection')"]
                'Internal Configuration'              = $LevelsDictionary["$($Output.'7 Internal Configuration')"]
                'Directory Access'                    = $LevelsDictionary["$($Output.'8 Directory Access')"]
                'Internal Processing'                 = $LevelsDictionary["$($Output.'9 Internal Processing')"]
                'Performance Counters'                = $LevelsDictionary["$($Output.'10 Performance Counters')"]
                'Initialization / Termination'        = $LevelsDictionary["$($Output.'11 Initialization/Termination')"]
                'Service Control'                     = $LevelsDictionary["$($Output.'12 Service Control')"]
                'Name Resolution'                     = $LevelsDictionary["$($Output.'13 Name Resolution')"]
                'Backup'                              = $LevelsDictionary["$($Output.'14 Backup')"]
                'Field Engineering'                   = $LevelsDictionary["$($Output.'15 Field Engineering')"]
                'LDAP Interface Events'               = $LevelsDictionary["$($Output.'16 LDAP Interface Events')"]
                'Setup'                               = $LevelsDictionary["$($Output.'17 Setup')"]
                'Global Catalog'                      = $LevelsDictionary["$($Output.'18 Global Catalog')"]
                'Inter-site Messaging'                = $LevelsDictionary["$($Output.'19 Inter-site Messaging')"]
                'Group Caching'                       = $LevelsDictionary["$($Output.'20 Group Caching')"]
                'Linked-Value Replication'            = $LevelsDictionary["$($Output.'21 Linked-Value Replication')"]
                'DS RPC Client'                       = $LevelsDictionary["$($Output.'22 DS RPC Client')"]
                'DS RPC Server'                       = $LevelsDictionary["$($Output.'23 DS RPC Server')"]
                'DS Schema'                           = $LevelsDictionary["$($Output.'24 DS Schema')"]
                'Transformation Engine'               = $LevelsDictionary["$($Output.'25 Transformation Engine')"]
                'Claims-Based Access Control'         = $LevelsDictionary["$($Output.'26 Claims-Based Access Control')"]
                'Netlogon'                            = $Netlogon
                'Comment'                             = $Comment
            }
        } else {
            $Comment = $ErrorMessage1 + ' ' + $ErrorMessage2
            [PSCustomObject] @{
                'ComputerName'                        = $Computer
                'Knowledge Consistency Checker (KCC)' = $LevelsDictionary["$($Output.'1 Knowledge Consistency Checker')"]
                'Security Events'                     = $LevelsDictionary["$($Output.'2 Security Events')"]
                'ExDS Interface Events'               = $LevelsDictionary["$($Output.'3 ExDS Interface Events')"]
                'MAPI Interface Events'               = $LevelsDictionary["$($Output.'4 MAPI Interface Events')"]
                'Replication Events'                  = $LevelsDictionary["$($Output.'5 Replication Events')"]
                'Garbage Collection'                  = $LevelsDictionary["$($Output.'6 Garbage Collection')"]
                'Internal Configuration'              = $LevelsDictionary["$($Output.'7 Internal Configuration')"]
                'Directory Access'                    = $LevelsDictionary["$($Output.'8 Directory Access')"]
                'Internal Processing'                 = $LevelsDictionary["$($Output.'9 Internal Processing')"]
                'Performance Counters'                = $LevelsDictionary["$($Output.'10 Performance Counters')"]
                'Initialization / Termination'        = $LevelsDictionary["$($Output.'11 Initialization/Termination')"]
                'Service Control'                     = $LevelsDictionary["$($Output.'12 Service Control')"]
                'Name Resolution'                     = $LevelsDictionary["$($Output.'13 Name Resolution')"]
                'Backup'                              = $LevelsDictionary["$($Output.'14 Backup')"]
                'Field Engineering'                   = $LevelsDictionary["$($Output.'15 Field Engineering')"]
                'LDAP Interface Events'               = $LevelsDictionary["$($Output.'16 LDAP Interface Events')"]
                'Setup'                               = $LevelsDictionary["$($Output.'17 Setup')"]
                'Global Catalog'                      = $LevelsDictionary["$($Output.'18 Global Catalog')"]
                'Inter-site Messaging'                = $LevelsDictionary["$($Output.'19 Inter-site Messaging')"]
                'Group Caching'                       = $LevelsDictionary["$($Output.'20 Group Caching')"]
                'Linked-Value Replication'            = $LevelsDictionary["$($Output.'21 Linked-Value Replication')"]
                'DS RPC Client'                       = $LevelsDictionary["$($Output.'22 DS RPC Client')"]
                'DS RPC Server'                       = $LevelsDictionary["$($Output.'23 DS RPC Server')"]
                'DS Schema'                           = $LevelsDictionary["$($Output.'24 DS Schema')"]
                'Transformation Engine'               = $LevelsDictionary["$($Output.'25 Transformation Engine')"]
                'Claims-Based Access Control'         = $LevelsDictionary["$($Output.'26 Claims-Based Access Control')"]
                'Netlogon'                            = $Netlogon
                'Comment'                             = $Comment
            }
        }
    }
}
function Get-WinADDnsInformation {
    <#
    .SYNOPSIS
    Retrieves DNS information for specified forest and domains.
 
    .DESCRIPTION
    This function retrieves DNS information for the specified forest and domains. It gathers various DNS server details such as cache, client subnets, diagnostics, directory partitions, DS settings, EDNS, forwarders, global name zones, global query block lists, recursion settings, recursion scopes, response rate limiting, root hints, scavenging details, server settings, virtualization instance, and more.
 
    .PARAMETER Forest
    Specifies the name of the forest to retrieve DNS information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domains to exclude from retrieving DNS information.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controllers to exclude from retrieving DNS information.
 
    .PARAMETER IncludeDomains
    Specifies an array of domains to include for retrieving DNS information.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controllers to include for retrieving DNS information.
 
    .PARAMETER Splitter
    Specifies the delimiter to use for joining IP addresses.
 
    .PARAMETER ExtendedForestInformation
    Provides additional extended forest information to speed up processing.
 
    .EXAMPLE
    Get-WinADDnsInformation -Forest "example.com" -IncludeDomains "domain1.com", "domain2.com" -Splitter ", " -ExtendedForestInformation $ExtendedForestInformation
 
    Retrieves DNS information for the "example.com" forest, including "domain1.com" and "domain2.com" domains, using ", " as the splitter for IP addresses, and with extended forest information.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory DNS servers.
    #>

    [CmdLetBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [string] $Splitter,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    if ($null -eq $TypesRequired) {
        #Write-Verbose 'Get-WinADDomainInformation - TypesRequired is null. Getting all.'
        #$TypesRequired = Get-Types -Types ([PSWinDocumentation.ActiveDirectory])
    } # Gets all types

    # This queries AD ones for Forest/Domain/DomainControllers, passing this value to commands can help speed up discovery
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation

    $DNSServers = @{ }
    foreach ($Computer in $ForestInformation.ForestDomainControllers.HostName) {
        #try {
        # $DNSServer = Get-DNSServer -ComputerName $Computer
        #} catch {
        #
        #}
        $Data = [ordered] @{ }
        $Data.ServerCache = Get-WinDnsServerCache -ComputerName $Computer
        $Data.ServerClientSubnets = Get-DnsServerClientSubnet  -ComputerName $Computer # TODO
        $Data.ServerDiagnostics = Get-WinDnsServerDiagnostics -ComputerName $Computer
        $Data.ServerDirectoryPartition = Get-WinDnsServerDirectoryPartition -ComputerName $Computer -Splitter $Splitter
        $Data.ServerDsSetting = Get-WinDnsServerDsSetting -ComputerName $Computer
        $Data.ServerEdns = Get-WinDnsServerEDns -ComputerName $Computer
        $Data.ServerForwarder = Get-WinADDnsServerForwarder -ComputerName $Computer -ExtendedForestInformation $ForestInformation -Formatted -Splitter $Splitter
        $Data.ServerGlobalNameZone = Get-WinDnsServerGlobalNameZone -ComputerName $Computer
        $Data.ServerGlobalQueryBlockList = Get-WinDnsServerGlobalQueryBlockList -ComputerName $Computer -Splitter $Splitter
        # $Data.ServerPolicies = $DNSServer.ServerPolicies
        $Data.ServerRecursion = Get-WinDnsServerRecursion -ComputerName $Computer

        $Data.ServerRecursionScopes = Get-WinDnsServerRecursionScope -ComputerName $Computer
        $Data.ServerResponseRateLimiting = Get-WinDnsServerResponseRateLimiting -ComputerName $Computer
        $Data.ServerResponseRateLimitingExceptionlists = Get-DnsServerResponseRateLimitingExceptionlist -ComputerName $Computer # TODO
        $Data.ServerRootHint = Get-WinDnsRootHint -ComputerName $Computer
        $Data.ServerScavenging = Get-WinADDnsServerScavenging -ComputerName $Computer
        $Data.ServerSetting = Get-WinDnsServerSettings -ComputerName $Computer
        # $Data.ServerZone = Get-DnsServerZone -ComputerName $Computer # problem
        # $Data.ServerZoneAging = Get-DnsServerZoneAging -ComputerName $Computer # problem
        # $Data.ServerZoneScope = Get-DnsServerZoneScope -ComputerName $Computer # problem
        # $Data.ServerDnsSecZoneSetting = Get-DnsServerDnsSecZoneSetting -ComputerName $Computer # problem
        $Data.VirtualizedServer = $DNSServer.VirtualizedServer
        $Data.VirtualizationInstance = Get-WinDnsServerVirtualizationInstance -ComputerName $Computer
        $DNSServers.$Computer = $Data
    }
    return $DNSServers
}
function Get-WinADDnsIPAddresses {
    <#
    .SYNOPSIS
    Gets all the DNS records from all the zones within a forest sorted by IPAddress
 
    .DESCRIPTION
    Gets all the DNS records from all the zones within a forest sorted by IPAddress
 
    .PARAMETER IncludeZone
    Limit the output of DNS records to specific zones
 
    .PARAMETER ExcludeZone
    Limit the output of dNS records to only zones not in the exclude list
 
    .PARAMETER IncludeDetails
    Adds additional information such as creation time, changed time
 
    .PARAMETER Prettify
    Converts arrays into strings connected with comma
 
    .PARAMETER IncludeDNSRecords
    Include full DNS records just in case one would like to further process them
 
    .PARAMETER AsHashtable
    Outputs the results as a hashtable instead of an array
 
    .EXAMPLE
    Get-WinADDnsIPAddresses | Format-Table *
 
    .EXAMPLE
    Get-WinADDnsIPAddresses -Prettify | Format-Table *
 
    .EXAMPLE
    Get-WinADDnsIPAddresses -Prettify -IncludeDetails -IncludeDNSRecords | Format-Table *
 
    .NOTES
    General notes
    #>

    [alias('Get-WinDnsIPAddresses')]
    [cmdletbinding()]
    param(
        [string[]] $IncludeZone,
        [string[]] $ExcludeZone,
        [switch] $IncludeDetails,
        [switch] $Prettify,
        [switch] $IncludeDNSRecords,
        [switch] $AsHashtable
    )
    $DNSRecordsCached = [ordered] @{}
    $DNSRecordsPerZone = [ordered] @{}
    $ADRecordsPerZone = [ordered] @{}

    try {
        $oRootDSE = Get-ADRootDSE -ErrorAction Stop
    } catch {
        Write-Warning -Message "Get-WinADDnsIPAddresses - Could not get the root DSE. Make sure you're logged in to machine with Active Directory RSAT tools installed, and there's connecitivity to the domain. Error: $($_.Exception.Message)"
        return
    }
    $ADServer = ($oRootDSE.dnsHostName)
    $Exclusions = 'DomainDnsZones', 'ForestDnsZones', '@'
    $DNS = Get-DnsServerZone -ComputerName $ADServer
    [Array] $ZonesToProcess = foreach ($Zone in $DNS) {
        if ($Zone.ZoneType -eq 'Primary' -and $Zone.IsDsIntegrated -eq $true -and $Zone.IsReverseLookupZone -eq $false) {
            if ($Zone.ZoneName -notlike "*_*" -and $Zone.ZoneName -ne 'TrustAnchors') {
                if ($IncludeZone -and $IncludeZone -notcontains $Zone.ZoneName) {
                    continue
                }
                if ($ExcludeZone -and $ExcludeZone -contains $Zone.ZoneName) {
                    continue
                }
                $Zone
            }
        }
    }

    foreach ($Zone in $ZonesToProcess) {
        Write-Verbose -Message "Get-WinADDnsIPAddresses - Processing zone for DNS records: $($Zone.ZoneName)"
        $DNSRecordsPerZone[$Zone.ZoneName] = Get-DnsServerResourceRecord -ComputerName $ADServer -ZoneName $Zone.ZoneName -RRType A
    }
    if ($IncludeDetails) {
        $Filter = "(Name -notlike '@' -and Name -notlike '_*' -and ObjectClass -eq 'dnsNode' -and Name -ne 'ForestDnsZone' -and Name -ne 'DomainDnsZone' )"
        #$Filter = { (Name -notlike "@" -and Name -notlike "_*" -and ObjectClass -eq 'dnsNode' -and Name -ne 'ForestDnsZone' -and Name -ne 'DomainDnsZone' ) }
        foreach ($Zone in $ZonesToProcess) {
            $ADRecordsPerZone[$Zone.ZoneName] = [ordered]@{}
            Write-Verbose -Message "Get-WinADDnsIPAddresses - Processing zone for AD records: $($Zone.ZoneName)"
            $TempObjects = @(
                if ($Zone.ReplicationScope -eq 'Domain') {
                    try {
                        Get-ADObject -Server $ADServer -Filter $Filter -SearchBase ("DC=$($Zone.ZoneName),CN=MicrosoftDNS,DC=DomainDnsZones," + $oRootDSE.defaultNamingContext) -Properties CanonicalName, whenChanged, whenCreated, DistinguishedName, ProtectedFromAccidentalDeletion, dNSTombstoned
                    } catch {
                        Write-Warning -Message "Get-WinADDnsIPAddresses - Error getting AD records for DomainDnsZones zone: $($Zone.ZoneName). Error: $($_.Exception.Message)"
                    }
                } elseif ($Zone.ReplicationScope -eq 'Forest') {
                    try {
                        Get-ADObject -Server $ADServer -Filter $Filter -SearchBase ("DC=$($Zone.ZoneName),CN=MicrosoftDNS,DC=ForestDnsZones," + $oRootDSE.defaultNamingContext) -Properties CanonicalName, whenChanged, whenCreated, DistinguishedName, ProtectedFromAccidentalDeletion, dNSTombstoned
                    } catch {
                        Write-Warning -Message "Get-WinADDnsIPAddresses - Error getting AD records for ForestDnsZones zone: $($Zone.ZoneName). Error: $($_.Exception.Message)"
                    }
                } else {
                    Write-Warning -Message "Get-WinADDnsIPAddresses - Unknown replication scope: $($Zone.ReplicationScope)"
                }
            )
            foreach ($DNSObject in $TempObjects) {
                $ADRecordsPerZone[$Zone.ZoneName][$DNSObject.Name] = $DNSObject
            }
        }
    }
    foreach ($Zone in $DNSRecordsPerZone.PSBase.Keys) {
        foreach ($Record in $DNSRecordsPerZone[$Zone]) {
            if ($Record.HostName -in $Exclusions) {
                continue
            }
            if (-not $DNSRecordsCached[$Record.RecordData.IPv4Address]) {
                $DNSRecordsCached[$Record.RecordData.IPv4Address] = [ordered] @{
                    IPAddress  = $Record.RecordData.IPv4Address
                    DnsNames   = [System.Collections.Generic.List[Object]]::new()
                    Timestamps = [System.Collections.Generic.List[Object]]::new()
                    Types      = [System.Collections.Generic.List[Object]]::new()
                    Count      = 0
                }
                if ($ADRecordsPerZone.Keys.Count -gt 0) {
                    $DNSRecordsCached[$Record.RecordData.IPv4Address].WhenCreated = $ADRecordsPerZone[$Zone][$Record.HostName].whenCreated
                    $DNSRecordsCached[$Record.RecordData.IPv4Address].WhenChanged = $ADRecordsPerZone[$Zone][$Record.HostName].whenChanged
                }
                if ($IncludeDNSRecords) {
                    $DNSRecordsCached[$Record.RecordData.IPv4Address].List = [System.Collections.Generic.List[Object]]::new()
                }
            }
            $DNSRecordsCached[$Record.RecordData.IPv4Address].DnsNames.Add($Record.HostName + "." + $Zone)

            if ($IncludeDNSRecords) {
                $DNSRecordsCached[$Record.RecordData.IPv4Address].List.Add($Record)
            }
            if ($null -ne $Record.TimeStamp) {
                $DNSRecordsCached[$Record.RecordData.IPv4Address].Timestamps.Add($Record.TimeStamp)
            } else {
                $DNSRecordsCached[$Record.RecordData.IPv4Address].Timestamps.Add("Not available")
            }
            if ($Null -ne $Record.Timestamp) {
                $DNSRecordsCached[$Record.RecordData.IPv4Address].Types.Add('Dynamic')
            } else {
                $DNSRecordsCached[$Record.RecordData.IPv4Address].Types.Add('Static')
            }
            $DNSRecordsCached[$Record.RecordData.IPv4Address] = [PSCustomObject] $DNSRecordsCached[$Record.RecordData.IPv4Address]
        }
    }
    foreach ($DNS in $DNSRecordsCached.PSBase.Keys) {
        $DNSRecordsCached[$DNS].Count = $DNSRecordsCached[$DNS].DnsNames.Count
        if ($Prettify) {
            $DNSRecordsCached[$DNS].DnsNames = $DNSRecordsCached[$DNS].DnsNames -join ", "
            $DNSRecordsCached[$DNS].Timestamps = $DNSRecordsCached[$DNS].Timestamps -join ", "
            $DNSRecordsCached[$DNS].Types = $DNSRecordsCached[$DNS].Types -join ", "
        }
    }
    if ($AsHashtable) {
        $DNSRecordsCached
    } else {
        $DNSRecordsCached.Values
    }
}
function Get-WinADDnsRecords {
    <#
    .SYNOPSIS
    Gets all the DNS records from all the zones within a forest
 
    .DESCRIPTION
    Gets all the DNS records from all the zones within a forest
 
    .PARAMETER IncludeZone
    Limit the output of DNS records to specific zones
 
    .PARAMETER ExcludeZone
    Limit the output of dNS records to only zones not in the exclude list
 
    .PARAMETER IncludeDetails
    Adds additional information such as creation time, changed time
 
    .PARAMETER Prettify
    Converts arrays into strings connected with comma
 
    .PARAMETER IncludeDNSRecords
    Include full DNS records just in case one would like to further process them
 
    .PARAMETER AsHashtable
    Outputs the results as a hashtable instead of an array
 
    .EXAMPLE
    Get-WinDNSRecords -Prettify -IncludeDetails | Format-Table
 
    .EXAMPLE
    $Output = Get-WinDNSRecords -Prettify -IncludeDetails -Verbose
    $Output.Count
    $Output | Sort-Object -Property Count -Descending | Select-Object -First 30 | Format-Table
 
    .NOTES
    General notes
    #>

    [alias('Get-WinDNSRecords')]
    [cmdletbinding()]
    param(
        [string[]] $IncludeZone,
        [string[]] $ExcludeZone,
        [switch] $IncludeDetails,
        [switch] $Prettify,
        [switch] $IncludeDNSRecords,
        [switch] $AsHashtable
    )
    $DNSRecordsCached = [ordered] @{}
    $DNSRecordsPerZone = [ordered] @{}
    $ADRecordsPerZone = [ordered] @{}
    $ADRecordsPerZoneByDns = [ordered] @{}

    try {
        $oRootDSE = Get-ADRootDSE -ErrorAction Stop
    } catch {
        Write-Warning -Message "Get-WinDNSRecords - Could not get the root DSE. Make sure you're logged in to machine with Active Directory RSAT tools installed, and there's connecitivity to the domain. Error: $($_.Exception.Message)"
        return
    }
    $ADServer = ($oRootDSE.dnsHostName)
    $Exclusions = 'DomainDnsZones', 'ForestDnsZones', '@'
    $DNS = Get-DnsServerZone -ComputerName $ADServer
    [Array] $ZonesToProcess = foreach ($Zone in $DNS) {
        if ($Zone.ZoneType -eq 'Primary' -and $Zone.IsDsIntegrated -eq $true -and $Zone.IsReverseLookupZone -eq $false) {
            if ($Zone.ZoneName -notlike "*_*" -and $Zone.ZoneName -ne 'TrustAnchors') {
                if ($IncludeZone -and $IncludeZone -notcontains $Zone.ZoneName) {
                    continue
                }
                if ($ExcludeZone -and $ExcludeZone -contains $Zone.ZoneName) {
                    continue
                }
                $Zone
            }
        }
    }

    foreach ($Zone in $ZonesToProcess) {
        Write-Verbose -Message "Get-WinDNSRecords - Processing zone for DNS records: $($Zone.ZoneName)"
        $DNSRecordsPerZone[$Zone.ZoneName] = Get-DnsServerResourceRecord -ComputerName $ADServer -ZoneName $Zone.ZoneName -RRType A
        $ADRecordsPerZoneByDns[$Zone.ZoneName] = [ordered] @{}
        foreach ($Record in  $DNSRecordsPerZone[$Zone.ZoneName]) {
            if (-not $ADRecordsPerZoneByDns[$Zone.ZoneName][$Record.HostName]) {
                $ADRecordsPerZoneByDns[$Zone.ZoneName][$Record.HostName] = [System.Collections.Generic.List[Object]]::new()
            }
            $ADRecordsPerZoneByDns[$Zone.ZoneName][$Record.HostName].Add($Record)
        }
    }
    if ($IncludeDetails) {
        #$Filter = { (Name -notlike "@" -and Name -notlike "_*" -and ObjectClass -eq 'dnsNode' -and Name -ne 'ForestDnsZone' -and Name -ne 'DomainDnsZone' ) }
        $Filter = "(Name -notlike '@' -and Name -notlike '_*' -and ObjectClass -eq 'dnsNode' -and Name -ne 'ForestDnsZone' -and Name -ne 'DomainDnsZone' )"
        foreach ($Zone in $ZonesToProcess) {
            $ADRecordsPerZone[$Zone.ZoneName] = [ordered]@{}
            Write-Verbose -Message "Get-WinDNSRecords - Processing zone for AD records: $($Zone.ZoneName)"
            $TempObjects = @(
                if ($Zone.ReplicationScope -eq 'Domain') {
                    try {
                        $getADObjectSplat = @{
                            Server     = $ADServer
                            Filter     = $Filter
                            SearchBase = ("DC=$($Zone.ZoneName),CN=MicrosoftDNS,DC=DomainDnsZones," + $oRootDSE.defaultNamingContext)
                            Properties = 'CanonicalName', 'whenChanged', 'whenCreated', 'DistinguishedName', 'ProtectedFromAccidentalDeletion', 'dNSTombstoned', 'nTSecurityDescriptor'
                        }
                        Get-ADObject @getADObjectSplat
                    } catch {
                        Write-Warning -Message "Get-WinDNSRecords - Error getting AD records for DomainDnsZones zone: $($Zone.ZoneName). Error: $($_.Exception.Message)"
                    }
                } elseif ($Zone.ReplicationScope -eq 'Forest') {
                    try {
                        $getADObjectSplat = @{
                            Server     = $ADServer
                            Filter     = $Filter
                            SearchBase = ("DC=$($Zone.ZoneName),CN=MicrosoftDNS,DC=ForestDnsZones," + $oRootDSE.defaultNamingContext)
                            Properties = 'CanonicalName', 'whenChanged', 'whenCreated', 'DistinguishedName', 'ProtectedFromAccidentalDeletion', 'dNSTombstoned', 'nTSecurityDescriptor'
                        }
                        Get-ADObject @getADObjectSplat
                    } catch {
                        Write-Warning -Message "Get-WinDNSRecords - Error getting AD records for ForestDnsZones zone: $($Zone.ZoneName). Error: $($_.Exception.Message)"
                    }
                } else {
                    Write-Warning -Message "Get-WinDNSRecords - Unknown replication scope: $($Zone.ReplicationScope)"
                }
            )
            foreach ($DNSObject in $TempObjects) {
                $ADRecordsPerZone[$Zone.ZoneName][$DNSObject.Name] = $DNSObject
            }
        }
    }
    # PSBase is required because of "Keys" DNS name
    foreach ($Zone in $ADRecordsPerZone.PSBase.Keys) {
        foreach ($RecordName in [string[]] $ADRecordsPerZone[$Zone].PSBase.Keys) {
            $ADDNSRecord = $ADRecordsPerZone[$Zone][$RecordName]
            [Array] $ListRecords = $ADRecordsPerZoneByDns[$Zone][$RecordName]

            if ($ADDNSRecord.Name -in $Exclusions) {
                continue
            }
            if (-not $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"]) {
                $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"] = [ordered] @{
                    'HostName' = $ADDNSRecord.Name
                    'Zone'     = $Zone
                    'Status'   = if ($ADDNSRecord.dNSTombstoned -eq $true) {
                        'Tombstoned' 
                    } else {
                        'Active' 
                    }
                    Owner      = $ADDNSRecord.ntsecuritydescriptor.owner
                    RecordIP   = [System.Collections.Generic.List[Object]]::new()
                    Types      = [System.Collections.Generic.List[Object]]::new()
                    Timestamps = [System.Collections.Generic.List[Object]]::new()
                    Count      = 0
                }
                #if ($ADRecordsPerZone.Keys.Count -gt 0) {
                #$DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].dNSTombstoned = $ADRecordsPerZone[$Zone][$ADDNSRecord.Name].dNSTombstoned
                $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].WhenCreated = $ADRecordsPerZone[$Zone][$ADDNSRecord.Name].whenCreated
                $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].WhenChanged = $ADRecordsPerZone[$Zone][$ADDNSRecord.Name].whenChanged
                #}
                if ($IncludeDNSRecords) {
                    $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].List = [System.Collections.Generic.List[Object]]::new()
                }
            }
            if ($ListRecords.Count -gt 0) {
                foreach ($Record in $ListRecords) {
                    if ($IncludeDNSRecords) {
                        $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].List.Add($Record)
                    }
                    if ($null -ne $Record.TimeStamp) {
                        $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].Timestamps.Add($Record.TimeStamp)
                    } else {
                        $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].Timestamps.Add("Not available")
                    }
                    $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].RecordIP.Add($Record.RecordData.IPv4Address)
                    if ($Null -ne $Record.Timestamp) {
                        $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].Types.Add('Dynamic')
                    } else {
                        $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].Types.Add('Static')
                    }
                }
            } else {
                $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].RecordIP.Add('Not available')
                $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].Types.Add('Not available')
                $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"].Timestamps.Add('Not available')
            }
            $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"] = [PSCustomObject] $DNSRecordsCached["$($ADDNSRecord.Name).$($Zone)"]
        }
    }
    foreach ($DNS in $DNSRecordsCached.PSBase.Keys) {
        $DNSRecordsCached[$DNS].Count = $DNSRecordsCached[$DNS].RecordIP.Count
        if ($Prettify) {
            $DNSRecordsCached[$DNS].Types = $DNSRecordsCached[$DNS].Types -join ", "
            $DNSRecordsCached[$DNS].RecordIP = $DNSRecordsCached[$DNS].RecordIP -join ", "
            $DNSRecordsCached[$DNS].Timestamps = $DNSRecordsCached[$DNS].Timestamps -join ", "
        }
    }
    if ($AsHashtable) {
        $DNSRecordsCached
    } else {
        $DNSRecordsCached.Values
    }
}
function Get-WinADDnsServerForwarder {
    <#
    .SYNOPSIS
    Retrieves DNS server forwarder information from Active Directory forest domain controllers.
 
    .DESCRIPTION
    The Get-WinADDnsServerForwarder function retrieves DNS server forwarder information from Active Directory forest domain controllers. It gathers information such as IP addresses, reordering status, timeout, root hint usage, forwarders count, host name, and domain name.
 
    .PARAMETER Forest
    Specifies the name of the forest to retrieve DNS server forwarder information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domains to exclude from retrieving DNS server forwarder information.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controllers to exclude from retrieving DNS server forwarder information.
 
    .PARAMETER IncludeDomains
    Specifies an array of domains to include for retrieving DNS server forwarder information.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controllers to include for retrieving DNS server forwarder information.
 
    .PARAMETER Formatted
    Indicates whether the output should be formatted.
 
    .PARAMETER Splitter
    Specifies the delimiter to use for joining IP addresses.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information to include in the forest details.
 
    .EXAMPLE
    Get-WinADDnsServerForwarder -Forest "example.com" -IncludeDomains "example.com" -Formatted
    Retrieves DNS server forwarder information for the "example.com" forest and includes only the "example.com" domain in a formatted output.
 
    .NOTES
    File: Get-WinADDnsServerForwarder.ps1
    Author: [Author Name]
    Date: [Date]
    Version: [Version]
    #>

    [CmdLetBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $Formatted,
        [string] $Splitter = ', ',
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Computer in $ForestInformation.ForestDomainControllers) {
        try {
            $DnsServerForwarder = Get-DnsServerForwarder -ComputerName $Computer.HostName -ErrorAction Stop
        } catch {
            $ErrorMessage = $_.Exception.Message -replace "`n", " " -replace "`r", " "
            Write-Warning "Get-WinDnsServerForwarder - Error $ErrorMessage"
            continue
        }
        foreach ($_ in $DnsServerForwarder) {
            if ($Formatted) {
                [PSCustomObject] @{
                    IPAddress          = $_.IPAddress.IPAddressToString -join $Splitter
                    ReorderedIPAddress = $_.ReorderedIPAddress.IPAddressToString -join $Splitter
                    EnableReordering   = $_.EnableReordering
                    Timeout            = $_.Timeout
                    UseRootHint        = $_.UseRootHint
                    ForwardersCount    = ($_.IPAddress.IPAddressToString).Count
                    GatheredFrom       = $Computer.HostName
                    GatheredDomain     = $Computer.Domain
                }
            } else {
                [PSCustomObject] @{
                    IPAddress          = $_.IPAddress.IPAddressToString
                    ReorderedIPAddress = $_.ReorderedIPAddress.IPAddressToString
                    EnableReordering   = $_.EnableReordering
                    Timeout            = $_.Timeout
                    UseRootHint        = $_.UseRootHint
                    ForwardersCount    = ($_.IPAddress.IPAddressToString).Count
                    GatheredFrom       = $Computer.HostName
                    GatheredDomain     = $Computer.Domain
                }
            }
        }
    }
}
function Get-WinADDnsServerScavenging {
    <#
    .SYNOPSIS
    Retrieves DNS server scavenging details for specified forest and domains.
 
    .DESCRIPTION
    This function retrieves DNS server scavenging details for the specified forest and domains. It gathers information about DNS server scavenging settings for each domain controller in the forest.
 
    .PARAMETER Forest
    Specifies the name of the forest to retrieve DNS server scavenging details for.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domains to exclude from DNS server scavenging details retrieval.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controllers to exclude from DNS server scavenging details retrieval.
 
    .PARAMETER IncludeDomains
    Specifies an array of domains to include in DNS server scavenging details retrieval.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controllers to include in DNS server scavenging details retrieval.
 
    .PARAMETER SkipRODC
    Indicates whether to skip Read-Only Domain Controllers (RODC) when retrieving DNS server scavenging details.
 
    .PARAMETER GPOs
    Specifies an array of Group Policy Objects (GPOs) related to DNS server scavenging.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional extended forest information to include in the output.
 
    .EXAMPLE
    Get-WinADDnsServerScavenging -Forest "example.com" -IncludeDomains "domain1.com", "domain2.com" -ExcludeDomainControllers "dc1.domain1.com" -SkipRODC
 
    Retrieves DNS server scavenging details for the "example.com" forest, including "domain1.com" and "domain2.com" domains, excluding the "dc1.domain1.com" domain controller, and skipping RODCs.
 
    #>

    [CmdLetBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [Array] $GPOs,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Computer in $ForestInformation.ForestDomainControllers) {
        try {
            $DnsServerScavenging = Get-DnsServerScavenging -ComputerName $Computer.HostName -ErrorAction Stop
        } catch {
            [PSCustomObject] @{
                NoRefreshInterval  = $null
                RefreshInterval    = $null
                ScavengingInterval = $null
                ScavengingState    = $null
                LastScavengeTime   = $null
                GatheredFrom       = $Computer.HostName
                GatheredDomain     = $Computer.Domain
            }
            continue
        }
        foreach ($_ in $DnsServerScavenging) {
            [PSCustomObject] @{
                NoRefreshInterval  = $_.NoRefreshInterval
                RefreshInterval    = $_.RefreshInterval
                ScavengingInterval = $_.ScavengingInterval
                ScavengingState    = $_.ScavengingState
                LastScavengeTime   = $_.LastScavengeTime
                GatheredFrom       = $Computer.HostName
                GatheredDomain     = $Computer.Domain
            }
        }
    }
}
function Get-ADWinDnsServerZones {
    <#
    .SYNOPSIS
    Retrieves detailed information about DNS zones from Active Directory DNS servers.
 
    .DESCRIPTION
    This function retrieves detailed information about DNS zones from Active Directory DNS servers. It queries the DNS servers to gather information such as zone name, type, dynamic update settings, replication scope, and other zone-related details.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve DNS zone information from.
 
    .PARAMETER ExcludeDomains
    Specifies the domains to exclude from the search.
 
    .PARAMETER ExcludeDomainControllers
    Specifies the domain controllers to exclude from the search.
 
    .PARAMETER IncludeDomains
    Specifies the domains to include in the search.
 
    .PARAMETER IncludeDomainControllers
    Specifies the domain controllers to include in the search.
 
    .PARAMETER SkipRODC
    Skips Read-Only Domain Controllers when querying for information.
 
    .PARAMETER ExtendedForestInformation
    Provides forest information from another command to speed up processing.
 
    .PARAMETER ReverseLookupZone
    Indicates whether to retrieve reverse lookup zones.
 
    .PARAMETER PrimaryZone
    Indicates whether to retrieve primary zones.
 
    .PARAMETER Forwarder
    Indicates whether to retrieve forwarder zones.
 
    .PARAMETER ZoneName
    Specifies the name of the zone to retrieve information for.
 
    .EXAMPLE
    Get-ADWinDnsServerZones -Forest "example.com" -IncludeDomains "domain1.com", "domain2.com" -PrimaryZone -ZoneName "example.com" | Format-Table
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory DNS servers.
    #>

    [alias('Get-WinDnsServerZones')]
    [CmdLetBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [System.Collections.IDictionary] $ExtendedForestInformation,

        [switch] $ReverseLookupZone,
        [switch] $PrimaryZone,
        [switch] $Forwarder,
        [string] $ZoneName
    )

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Domain in $ForestInformation.Domains) {
        foreach ($Computer in $ForestInformation['DomainDomainControllers'][$Domain]) {
            $getDnsServerZoneSplat = @{
                ComputerName = $Computer.HostName
                Name         = $ZoneName
            }
            Remove-EmptyValue -Hashtable $getDnsServerZoneSplat
            $Zones = Get-DnsServerZone @getDnsServerZoneSplat -ErrorAction SilentlyContinue
            foreach ($_ in $Zones) {
                if ($ZoneName) {
                    if ($ZoneName -ne $_.ZoneName) {
                        continue
                    }
                }
                if ($_.ZoneType -eq 'Primary') {
                    $ZoneAging = Get-DnsServerZoneAging -Name $_.ZoneName -ComputerName $Computer.HostName
                    $AgingEnabled = $ZoneAging.AgingEnabled
                    $AvailForScavengeTime = $ZoneAging.AvailForScavengeTime
                    $RefreshInterval = $ZoneAging.RefreshInterval
                    $NoRefreshInterval = $ZoneAging.NoRefreshInterval
                    $ScavengeServers = $ZoneAging.ScavengeServers
                } else {
                    $AgingEnabled = $null
                    $AvailForScavengeTime = $null
                    $RefreshInterval = $null
                    $NoRefreshInterval = $null
                    $ScavengeServers = $null
                }
                if ($Forwarder) {
                    if ($_.ZoneType -ne 'Forwarder') {
                        continue
                    }
                } elseif ($ReverseLookupZone -and $PrimaryZone) {
                    if ($_.IsReverseLookupZone -ne $true -or $_.ZoneType -ne 'Primary') {
                        continue
                    }
                } elseif ($ReverseLookupZone) {
                    if ($_.IsReverseLookupZone -ne $true) {
                        continue
                    }
                } elseif ($PrimaryZone) {
                    if ($_.ZoneType -ne 'Primary' -or $_.IsReverseLookupZone -ne $false ) {
                        continue
                    }
                }
                [PSCustomObject] @{
                    'ZoneName'                          = $_.'ZoneName'
                    'ZoneType'                          = $_.'ZoneType'
                    'IsPDC'                             = $Computer.IsPDC
                    'AgingEnabled'                      = $AgingEnabled
                    'AvailForScavengeTime'              = $AvailForScavengeTime
                    'RefreshInterval'                   = $RefreshInterval
                    'NoRefreshInterval'                 = $NoRefreshInterval
                    'ScavengeServers'                   = $ScavengeServers
                    'MasterServers'                     = $_.MasterServers
                    'NotifyServers'                     = $_.'NotifyServers'
                    'SecondaryServers'                  = $_.'SecondaryServers'
                    'AllowedDcForNsRecordsAutoCreation' = $_.'AllowedDcForNsRecordsAutoCreation'
                    'DistinguishedName'                 = $_.'DistinguishedName'
                    'IsAutoCreated'                     = $_.'IsAutoCreated'
                    'IsDsIntegrated'                    = $_.'IsDsIntegrated'
                    'IsPaused'                          = $_.'IsPaused'
                    'IsReadOnly'                        = $_.'IsReadOnly'
                    'IsReverseLookupZone'               = $_.'IsReverseLookupZone'
                    'IsShutdown'                        = $_.'IsShutdown'
                    'DirectoryPartitionName'            = $_.'DirectoryPartitionName'
                    'DynamicUpdate'                     = $_.'DynamicUpdate'
                    'IgnorePolicies'                    = $_.'IgnorePolicies'
                    'IsSigned'                          = $_.'IsSigned'
                    'IsWinsEnabled'                     = $_.'IsWinsEnabled'
                    'Notify'                            = $_.'Notify'
                    'ReplicationScope'                  = $_.'ReplicationScope'
                    'SecureSecondaries'                 = $_.'SecureSecondaries'
                    'ZoneFile'                          = $_.'ZoneFile'
                    'GatheredFrom'                      = $Computer.HostName
                    'GatheredDomain'                    = $Domain
                }
            }
        }
    }
}
function Get-WinADDNSZones {
    <#
    .SYNOPSIS
    Retrieves DNS zone information from the Active Directory DNS server.
 
    .DESCRIPTION
    This function retrieves detailed information about DNS zones from the Active Directory DNS server.
    It queries the DNS server to gather information such as zone name, type, dynamic update settings, replication scope, and other zone-related details.
 
    .PARAMETER None
    This cmdlet does not require any parameters.
 
    .EXAMPLE
    Get-WinDNSZones
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory DNS server.
    #>

    [alias('Get-WinDNSZones')]
    [CmdletBinding()]
    param(

    )
    try {
        $oRootDSE = Get-ADRootDSE -ErrorAction Stop
    } catch {
        Write-Warning -Message "Get-WinDNSZones - Could not get the root DSE. Make sure you're logged in to machine with Active Directory RSAT tools installed, and there's connecitivity to the domain. Error: $($_.Exception.Message)"
        return
    }
    $ADServer = ($oRootDSE.dnsHostName)
    $DNS = Get-DnsServerZone -ComputerName $ADServer
    foreach ($Zone in $DNS) {
        [PSCustomObject] @{
            ZoneName                          = $Zone.ZoneName                            #: _msdcs.ad.evotec.xyz
            ZoneType                          = $Zone.ZoneType                            #: Primary
            DynamicUpdate                     = $Zone.DynamicUpdate                       #: Secure
            ReplicationScope                  = $Zone.ReplicationScope                    #: Forest
            DirectoryPartitionName            = $Zone.DirectoryPartitionName              #: ForestDnsZones.ad.evotec.xyz
            #:
            IsAutoCreated                     = $Zone.IsAutoCreated                       #: False
            IsDsIntegrated                    = $Zone.IsDsIntegrated                      #: True
            IsReadOnly                        = $Zone.IsReadOnly                          #: False
            IsReverseLookupZone               = $Zone.IsReverseLookupZone                 #: False
            IsSigned                          = $Zone.IsSigned                            #: False
            IsPaused                          = $Zone.IsPaused                            #: False
            IsShutdown                        = $Zone.IsShutdown                          #: False
            IsWinsEnabled                     = $Zone.IsWinsEnabled                       #: False
            Notify                            = $Zone.Notify                              #: NotifyServers
            NotifyServers                     = $Zone.NotifyServers                       #:
            SecureSecondaries                 = $Zone.SecureSecondaries                   #: NoTransfer
            SecondaryServers                  = $Zone.SecondaryServers                    #:
            LastZoneTransferAttempt           = $Zone.LastZoneTransferAttempt             #:
            LastSuccessfulZoneTransfer        = $Zone.LastSuccessfulZoneTransfer          #:
            LastZoneTransferResult            = $Zone.LastZoneTransferResult              #:
            LastSuccessfulSOACheck            = $Zone.LastSuccessfulSOACheck              #:
            MasterServers                     = $Zone.MasterServers                       #:
            LocalMasters                      = $Zone.LocalMasters                        #:
            UseRecursion                      = $Zone.UseRecursion                        #:
            ForwarderTimeout                  = $Zone.ForwarderTimeout                    #:
            AllowedDcForNsRecordsAutoCreation = $Zone.AllowedDcForNsRecordsAutoCreation   #:
            DistinguishedName                 = $Zone.DistinguishedName                   #: DC=_msdcs.ad.evotec.xyz,cn=MicrosoftDNS,DC=ForestDnsZones,DC=ad,DC=evotec,DC=xyz
            ZoneFile                          = $Zone.ZoneFile
        }
    }
}
function Get-WinADDomain {
    <#
    .SYNOPSIS
    Retrieves information about a specified Active Directory domain.
 
    .DESCRIPTION
    This function retrieves detailed information about the specified Active Directory domain.
    It queries the domain to gather information such as domain controllers, domain name, and other domain-related details.
 
    .PARAMETER Domain
    Specifies the target domain to retrieve information from.
 
    .EXAMPLE
    Get-WinADDomain -Domain "example.com"
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory domain.
    #>

    [cmdletBinding()]
    param(
        [string] $Domain
    )
    try {
        if ($Domain) {
            $Type = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Domain
            $Context = [System.DirectoryServices.ActiveDirectory.DirectoryContext]::new($Type, $Domain)
            $DomainInformation = [System.DirectoryServices.ActiveDirectory.Domain]::GetDomain($Context)
        } else {
            $DomainInformation = [System.DirectoryServices.ActiveDirectory.Domain]::GetComputerDomain()
        }
    } catch {
        Write-Warning "Get-WinADDomain - Can't get $Domain information, error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
    }
    $DomainInformation
}
function Get-WinADDomainControllerGenerationId {
    <#
    .SYNOPSIS
    Provides information about the msDS-GenerationId of domain controllers
 
    .DESCRIPTION
    Provides information about the msDS-GenerationId of domain controllers
 
    .PARAMETER Forest
    Forest name to use for resolving. If not given it will use current forest.
 
    .PARAMETER ExcludeDomains
    Exclude specific domains from test
 
    .PARAMETER ExcludeDomainControllers
    Exclude specific domain controllers from test
 
    .PARAMETER IncludeDomains
    Include specific domains in test
 
    .PARAMETER IncludeDomainControllers
    Include specific domain controllers in test
 
    .PARAMETER SkipRODC
    Skip Read Only Domain Controllers when querying for information
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .EXAMPLE
    $Output = Get-WinADDomainControllerGenerationId -IncludeDomainControllers 'dc1.ad.evotec.pl'
    $Output | Format-Table
 
    .NOTES
    For virtual machine snapshot resuming detection. This attribute represents the VM Generation ID.
 
    #>

    [CmdletBinding()]
    param(
        [Parameter(ParameterSetName = 'Forest')][alias('ForestName')][string] $Forest,
        [Parameter(ParameterSetName = 'Forest')][string[]] $ExcludeDomains,
        [Parameter(ParameterSetName = 'Forest')][string[]] $ExcludeDomainControllers,
        [Parameter(ParameterSetName = 'Forest')][alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [Parameter(ParameterSetName = 'Forest')][alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [Parameter(ParameterSetName = 'Forest')][switch] $SkipRODC,
        [Parameter(ParameterSetName = 'Forest')][System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestDetails = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation -SkipRODC:$SkipRODC.IsPresent -IncludeDomainControllers $IncludeDomainControllers -ExcludeDomainControllers $ExcludeDomainControllers
    foreach ($Domain in $ForestDetails.Domains) {
        foreach ($D in $ForestDetails.DomainDomainControllers[$Domain]) {
            Write-Verbose -Message "Get-MSDSGenerationID - Executing Get-ADObject $D.ComputerObjectDN -Server $D.HostName -Properties Name, SamAccountName, 'msDS-GenerationId'"
            try {
                $Data = Get-ADObject $D.DistinguishedName -Server $D.HostName -Properties Name, SamAccountName, 'msDS-GenerationId' -ErrorAction Stop
                $ErrorProvided = $null
            } catch {
                $ErrorProvided = $_.Exception.Message
                $Data = $null
            }
            if ($Data) {
                $GenerationID = $Data.'msDS-GenerationId'
            } else {
                $GenerationID = $null
            }
            if ($GenerationID) {
                $TranslatedGenerationID = ($GenerationID | ForEach-Object { $_.ToString("X2") }) -join ''
                #$TranslatedGenerationIDAlternative = [System.Convert]::ToHexString($GenerationID)
            } else {
                #$TranslatedGenerationIDAlternative = $null
                $TranslatedGenerationID = $null
            }
            [PSCustomObject] @{
                HostName            = $D.HostName
                Domain              = $Domain
                Name                = $D.Name
                SamAccountName      = $Data.SamAccountName
                'msDS-GenerationId' = $TranslatedGenerationID
                #'msDS-GenerationId' = $TranslatedGenerationIDAlternative
                Error               = $ErrorProvided
            }
        }
    }
}
function Get-WinADDomainControllerNetLogonSettings {
    <#
    .SYNOPSIS
    Gathers information about NetLogon settings on a Domain Controller
 
    .DESCRIPTION
    Gathers information about NetLogon settings on a Domain Controller
 
    .PARAMETER DomainController
    Specifies the Domain Controller to retrieve information from
 
    .PARAMETER All
    Retrieves all information from registry as is without any processing
 
    .EXAMPLE
    Get-WinADDomainControllerNetLogonSettings -DomainController 'AD1'
 
    .EXAMPLE
    Get-WinADDomainControllerNetLogonSettings -DomainController 'AD1' -All
 
    .NOTES
    Useful links:
    - https://www.oreilly.com/library/view/active-directory-cookbook/0596004648/ch11s20.html
    - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.NetLogon::Netlogon_AutoSiteCoverage
    - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.NetLogon::Netlogon_SiteCoverage
    - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.NetLogon::Netlogon_GcSiteCoverage
 
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][Alias('ComputerName')][string] $DomainController,
        [switch] $All
    )

    $RegistryNetLogon = Get-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -ComputerName $DomainController
    if ($All) {
        $RegistryNetLogon
    } else {
        $GCSiteCoverage = $RegistryNetLogon.'GCSiteCoverage'
        if ($GCSiteCoverage) {
            $GCSiteCoverage = $GCSiteCoverage -split ','
        } else {
            $GCSiteCoverage = @()
        }
        $SiteCoverage = $RegistryNetLogon.'SiteCoverage'
        if ($SiteCoverage) {
            $SiteCoverage = $SiteCoverage -split ','
        } else {
            $SiteCoverage = @()
        }
        [PSCustomObject] @{
            'DomainController'  = $RegistryNetLogon.'PSComputerName'
            'DynamicSiteName'   = $RegistryNetLogon.'DynamicSiteName'
            'SiteCoverage'      = $SiteCoverage
            'GCSiteCoverage'    = $GCSiteCoverage
            'RequireSignOrSeal' = $RegistryNetLogon.'RequireSignOrSeal'
            'RequireSeal'       = $RegistryNetLogon.'RequireSeal'
            'Error'             = $RegistryNetLogon.'PSError'
            'ErrorMessage'      = $RegistryNetLogon.'PSErrorMessage'
        }
    }
}

function Get-WinADDomainControllerNTDSSettings {
    <#
    .SYNOPSIS
    Gathers information about NTDS settings on a Domain Controller
 
    .DESCRIPTION
    Gathers information about NTDS settings on a Domain Controller
 
    .PARAMETER DomainController
    Specifies the Domain Controller to retrieve information from
 
    .PARAMETER All
    Retrieves all information from registry as is without any processing
 
    .EXAMPLE
    Get-WinADDomainControllerNTDSSettings -DomainController 'AD1'
 
    .EXAMPLE
    Get-WinADDomainControllerNTDSSettings -DomainController 'AD1' -All
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][Alias('ComputerName')][string] $DomainController,
        [switch] $All
    )
    $RegistryNTDS = Get-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\NTDS\Parameters" -ComputerName $DomainController
    if ($All) {
        $RegistryNTDS
    } else {
        [PSCustomObject] @{
            'DomainController'                                 = $RegistryNTDS.'PSComputerName'
            'Error'                                            = $RegistryNTDS.'PSError'
            'System Schema Version'                            = $RegistryNTDS.'System Schema Version' #: 87
            'Root Domain'                                      = $RegistryNTDS.'Root Domain' #: DC = ad, DC = evotec, DC = xyz
            'Configuration NC'                                 = $RegistryNTDS.'Configuration NC' #: CN = Configuration, DC = ad, DC = evotec, DC = xyz
            'Machine DN Name'                                  = $RegistryNTDS.'Machine DN Name' #: CN = NTDS Settings, CN = AD1, CN = Servers, CN = Default-First-Site-Name, CN = Sites, CN = Configuration, DC = ad, DC = evotec, DC = xyz
            'DsaOptions'                                       = $RegistryNTDS.'DsaOptions' #: 1
            'IsClone'                                          = $RegistryNTDS.'IsClone' #: 0
            'ServiceDll'                                       = $RegistryNTDS.'ServiceDll' #: % systemroot % \system32\ntdsa.dll
            'DSA Working Directory'                            = $RegistryNTDS.'DSA Working Directory' #: C:\Windows\NTDS
            'DSA Database file'                                = $RegistryNTDS.'DSA Database file' #: C:\Windows\NTDS\ntds.dit
            'Database backup path'                             = $RegistryNTDS.'Database backup path' #: C:\Windows\NTDS\dsadata.bak
            'Database log files path'                          = $RegistryNTDS.'Database log files path' #: C:\Windows\NTDS
            'Hierarchy Table Recalculation interval (minutes)' = $RegistryNTDS.'Hierarchy Table Recalculation interval (minutes)' #: 720
            'Database logging / recovery'                      = $RegistryNTDS.'Database logging / recovery' # : ON
            'DS Drive Mappings'                                = $RegistryNTDS.'DS Drive Mappings' #: c:\=\\?\Volume { 2014dd39-5b27-44a6-be88-1d650346016d }\
            'DSA Database Epoch'                               = $RegistryNTDS.'DSA Database Epoch' #: 24290
            'Strict Replication Consistency'                   = $RegistryNTDS.'Strict Replication Consistency' #: 1
            'Schema Version'                                   = $RegistryNTDS.'Schema Version' #: 88
            'ldapserverintegrity'                              = $RegistryNTDS.'ldapserverintegrity' #: 1
            'Global Catalog Promotion Complete'                = $RegistryNTDS.'Global Catalog Promotion Complete' #: 1
            'DSA Previous Restore Count'                       = $RegistryNTDS.'DSA Previous Restore Count' #: 4
            'ErrorMessage'                                     = $RegistryNTDS.'PSErrorMessage'
        }
    }
}
function Get-WinADDomainControllerOption {
    <#
    .SYNOPSIS
    Command to get the options of a domain controller
 
    .DESCRIPTION
    Command to get the options of a domain controller that uses the repadmin command
 
    Provides information about:
    - DISABLE_OUTBOUND_REPL: Disables outbound replication.
    - DISABLE_INBOUND_REPL: Disables inbound replication.
    - DISABLE_NTDSCONN_XLATE: Disables the translation of NTDSConnection objects.
    - DISABLE_SPN_REGISTRATION: Disables Service Principal Name (SPN) registration.
    - IS_GC: Sets or unsets the Global Catalog (GC) for the domain controller.
 
    .PARAMETER DomainController
    The domain controller to get the options from
 
    .EXAMPLE
    Get-WinADDomainControllerOption -DomainController 'AD1', 'AD2','AD3' | Format-Table *
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [parameter(Mandatory)][string[]] $DomainController
    )
    foreach ($DC in $DomainController) {
        # Execute the repadmin command and capture the output
        Write-Verbose -Message "Get-WinADDomainControllerOption - Executing repadmin /options $DC"
        $AvailableOptions = $null
        $repadminOutput = & repadmin /options $DC
        if ($repadminOutput[0].StartsWith("Repadmin can't connect to a", $true, [System.Globalization.CultureInfo]::InvariantCulture)) {
            Write-Warning -Message "Get-WinADDomainControllerOption - Unable to connect to [$DC]. Error: $($_.Exception.Message)"
        } else {
            $AvailableOptions = $repadminOutput[0].Replace("Current DSA Options: ", "")
        }
        if ($AvailableOptions) {
            $Options = $AvailableOptions -split " "
        } else {
            $Options = @()
        }
        $Output = [ordered] @{
            Name    = $DC
            Status  = if ($AvailableOptions) {
                $true 
            } else {
                $false 
            }
            Options = foreach ($O in $Options) {
                $Value = $O.Trim()
                if ($Value) {
                    $Value
                }
            }
        }
        if ($Output.Options -contains 'IS_GC') {
            $Output['IsGlobalCatalog'] = $true
        } else {
            $Output['IsGlobalCatalog'] = $false
        }
        if ($Output.Options -contains 'IS_RODC') {
            $Output['IsReadOnlyDomainController'] = $true
        } else {
            $Output['IsReadOnlyDomainController'] = $false
        }
        if ($Output.Options -contains 'DISABLE_OUTBOUND_REPL') {
            $Output['DisabledOutboundReplication'] = $true
        } else {
            $Output['DisabledOutboundReplication'] = $false
        }
        if ($Output.Options -contains 'DISABLE_INBOUND_REPL') {
            $Output['DisabledInboundReplication'] = $true
        } else {
            $Output['DisabledInboundReplication'] = $false
        }
        if ($Output.Options -contains 'DISABLE_NTDSCONN_XLATE') {
            $Output['DisabledNTDSConnectionTranslation'] = $true
        } else {
            $Output['DisabledNTDSConnectionTranslation'] = $false
        }
        if ($Output.Options -contains 'DISABLE_SPN_REGISTRATION') {
            $Output['DisabledSPNRegistration'] = $true
        } else {
            $Output['DisabledSPNRegistration'] = $false
        }
        [PSCustomObject] $Output
    }
}
Function Get-WinADDuplicateObject {
    <#
    .SYNOPSIS
    Get duplicate objects in Active Directory (CNF: and CNF:0ACNF:)
 
    .DESCRIPTION
    Get duplicate objects in Active Directory (CNF: and CNF:0ACNF:)
    CNF stands for "Conflict". CNF objects are created when there is a naming conflict in the Active Directory.
    This usually happens during the replication process when two objects are created with the same name in different parts of the replication topology,
    and then a replication attempt is made. Active Directory resolves this by renaming one of the objects with a CNF prefix and a GUID.
    The object with the CNF name is usually the loser in the conflict resolution process.
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .PARAMETER PartialMatchDistinguishedName
    Limit results to specific DistinguishedName
 
    .PARAMETER IncludeObjectClass
    Limit results to specific ObjectClass
 
    .PARAMETER ExcludeObjectClass
    Exclude specific ObjectClass
 
    .PARAMETER Extended
    Provide extended information about the object
 
    .PARAMETER NoPostProcessing
    Do not post process the object, return as is from the AD
 
    .EXAMPLE
    Get-WinADDuplicateObject -Verbose | Format-Table
 
    .NOTES
    General notes
    #>

    [alias('Get-WinADForestObjectsConflict')]
    [CmdletBinding()]
    Param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [string] $PartialMatchDistinguishedName,
        [string[]] $IncludeObjectClass,
        [string[]] $ExcludeObjectClass,
        [switch] $Extended,
        [switch] $NoPostProcessing
    )
    # Based on https://gallery.technet.microsoft.com/scriptcenter/Get-ADForestConflictObjects-4667fa37
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation -Extended
    foreach ($Domain in $ForestInformation.Domains) {
        $DomainInformation = $ForestInformation.DomainsExtended[$Domain]
        Write-Verbose -Message "Get-WinADDuplicateObject - Processing $($Domain)"
        $Partitions = @(
            if ($Domain -eq $ForestInformation.Forest) {
                "CN=Configuration,$($ForestInformation['DomainsExtended'][$Domain].DistinguishedName)"
                if ($DomainInformation.SubordinateReferences -contains "DC=ForestDnsZones,$($ForestInformation['DomainsExtended'][$Domain].DistinguishedName)") {
                    "DC=ForestDnsZones,$($ForestInformation['DomainsExtended'][$Domain].DistinguishedName)"
                } else {
                    Write-Warning -Message "Get-WinADDuplicateObject - ForestDnsZones not found for domain '$Domain'. Skipping"
                }
            }
            # Domain Name
            $ForestInformation['DomainsExtended'][$Domain].DistinguishedName
            # DNS Name
            if ($DomainInformation.SubordinateReferences -contains "DC=DomainDnsZones,$($ForestInformation['DomainsExtended'][$Domain].DistinguishedName)") {
                "DC=DomainDnsZones,$($ForestInformation['DomainsExtended'][$Domain].DistinguishedName)"
            } else {
                Write-Warning -Message "Get-WinADDuplicateObject - DomainDnsZones not found for domain '$Domain'. Skipping"
            }
        )
        $DC = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        #Get conflict objects
        foreach ($Partition in $Partitions) {
            Write-Verbose -Message "Get-WinADDuplicateObject - Processing $($Domain) - $($Partition)"
            $getADObjectSplat = @{
                #Filter = "*"
                LDAPFilter  = "(|(cn=*\0ACNF:*)(ou=*CNF:*))"
                Properties  = 'DistinguishedName', 'ObjectClass', 'DisplayName', 'SamAccountName', 'Name', 'ObjectCategory', 'WhenCreated', 'WhenChanged', 'ProtectedFromAccidentalDeletion', 'ObjectGUID'
                Server      = $DC
                SearchScope = 'Subtree'
            }
            try {
                $Objects = Get-ADObject @getADObjectSplat -SearchBase $Partition -ErrorAction Stop
            } catch {
                Write-Warning -Message "Get-WinADDuplicateObject - Getting objects from domain '$Domain' / partition: '$Partition' failed. Error: $($Object.Exception.Message)"
                continue
            }
            foreach ($Object in $Objects) {
                # Lets allow users to filter on it
                if ($ExcludeObjectClass) {
                    if ($ExcludeObjectClass -contains $Object.ObjectClass) {
                        continue
                    }
                }
                if ($IncludeObjectClass) {
                    if ($IncludeObjectClass -notcontains $Object.ObjectClass) {
                        continue
                    }
                }
                if ($PartialMatchDistinguishedName) {
                    if ($Object.DistinguishedName -notlike $PartialMatchDistinguishedName) {
                        continue
                    }
                }
                if ($NoPostProcessing) {
                    $Object
                    continue
                }
                $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $Object.DistinguishedName -ToDomainCN
                # Lets create separate objects for different purpoeses
                $ConflictObject = [ordered] @{
                    ConflictDN          = $Object.DistinguishedName
                    ConflictWhenChanged = $Object.WhenChanged
                    DomainName          = $DomainName
                    ObjectClass         = $Object.ObjectClass
                }
                $LiveObjectData = [ordered] @{
                    LiveDn          = "N/A"
                    LiveWhenChanged = "N/A"
                }
                $RestData = [ordered] @{
                    DisplayName                     = $Object.DisplayName
                    Name                            = $Object.Name.Replace("`n", ' ')
                    SamAccountName                  = $Object.SamAccountName
                    ObjectCategory                  = $Object.ObjectCategory
                    WhenCreated                     = $Object.WhenCreated
                    WhenChanged                     = $Object.WhenChanged
                    ProtectedFromAccidentalDeletion = $Object.ProtectedFromAccidentalDeletion
                    ObjectGUID                      = $Object.ObjectGUID.Guid
                    # Server used to query the object
                    Server                          = $DC
                    # Partition used to query the object
                    SearchBase                      = $Partition
                }
                if ($Extended) {
                    $LiveObject = $null
                    $ConflictObject = $ConflictObject + $LiveObjectData + $RestData
                    #See if we are dealing with a 'cn' conflict object
                    if (Select-String -SimpleMatch "\0ACNF:" -InputObject $ConflictObject.ConflictDn) {
                        #Split the conflict object DN so we can remove the conflict notation
                        $SplitConfDN = $ConflictObject.ConflictDn -split "0ACNF:"
                        #Remove the conflict notation from the DN and try to get the live AD object
                        try {
                            $LiveObject = Get-ADObject -Identity "$($SplitConfDN[0].TrimEnd("\"))$($SplitConfDN[1].Substring(36))" -Properties WhenChanged -Server $DC -ErrorAction Stop
                        } catch { 
                        }
                        if ($LiveObject) {
                            $ConflictObject.LiveDN = $LiveObject.DistinguishedName
                            $ConflictObject.LiveWhenChanged = $LiveObject.WhenChanged
                        }
                    } else {
                        #Split the conflict object DN so we can remove the conflict notation for OUs
                        $SplitConfDN = $ConflictObject.ConflictDn -split "CNF:"
                        #Remove the conflict notation from the DN and try to get the live AD object
                        try {
                            $LiveObject = Get-ADObject -Identity "$($SplitConfDN[0])$($SplitConfDN[1].Substring(36))" -Properties WhenChanged -Server $DC -ErrorAction Stop
                        } catch { 
                        }
                        if ($LiveObject) {
                            $ConflictObject.LiveDN = $LiveObject.DistinguishedName
                            $ConflictObject.LiveWhenChanged = $LiveObject.WhenChanged
                        }
                    }
                } else {
                    $ConflictObject = $ConflictObject + $RestData
                }
                [PSCustomObject] $ConflictObject
            }
        }
    }
}
function Get-WinADDuplicateSPN {
    <#
    .SYNOPSIS
    Detects and lists duplicate Service Principal Names (SPNs) in the Active Directory Domain.
 
    .DESCRIPTION
    Detects and lists duplicate Service Principal Names (SPNs) in the Active Directory Domain.
 
    .PARAMETER All
    Returns all duplicate and non-duplicate SPNs. Default is to only return duplicate SPNs.
 
    .PARAMETER Exclude
    Provides ability to exclude specific SPNs from the duplicate detection. By default it excludes kadmin/changepw as with multiple forests it will happen for sure.
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .EXAMPLE
    Get-WinADDuplicateSPN | Format-Table
 
    .EXAMPLE
    Get-WinADDuplicateSPN -All | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [switch] $All,
        [string[]] $Exclude,
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [Parameter(ParameterSetName = 'Forest')][System.Collections.IDictionary] $ExtendedForestInformation
    )
    $Excluded = @(
        'kadmin/changepw'
        foreach ($Item in $Exclude) {
            $iTEM
        }
    )

    $SPNCache = [ordered] @{}
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Domain in $ForestInformation.Domains) {
        Write-Verbose -Message "Get-WinADDuplicateSPN - Processing $Domain"
        $Objects = (Get-ADObject -LDAPFilter "ServicePrincipalName=*" -Properties ServicePrincipalName -Server $ForestInformation['QueryServers'][$domain]['HostName'][0])
        Write-Verbose -Message "Get-WinADDuplicateSPN - Found $($Objects.Count) objects. Processing..."
        foreach ($Object in $Objects) {
            foreach ($SPN in $Object.ServicePrincipalName) {
                if (-not $SPNCache[$SPN]) {
                    $SPNCache[$SPN] = [PSCustomObject] @{
                        Name      = $SPN
                        Duplicate = $false
                        Count     = 0
                        Excluded  = $false
                        List      = [System.Collections.Generic.List[Object]]::new()
                    }
                }
                if ($SPN -in $Excluded) {
                    $SPNCache[$SPN].Excluded = $true
                }
                $SPNCache[$SPN].List.Add($Object)
                $SPNCache[$SPN].Count++
            }
        }
    }
    Write-Verbose -Message "Get-WinADDuplicateSPN - Finalizing output. Processing..."
    foreach ($SPN in $SPNCache.Values) {
        if ($SPN.Count -gt 1 -and $SPN.Excluded -ne $true) {
            $SPN.Duplicate = $true
        }
        if ($All) {
            $SPN
        } else {
            if ($SPN.Duplicate) {
                $SPN
            }
        }
    }
}
function Get-WinADForest {
    <#
    .SYNOPSIS
    Retrieves information about a specified Active Directory forest.
 
    .DESCRIPTION
    This function retrieves detailed information about the specified Active Directory forest.
    It queries the forest to gather information such as domain controllers, sites, and other forest-related details.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve information from.
 
    .EXAMPLE
    Get-WinADForest -Forest "example.com"
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [cmdletBinding()]
    param(
        [string] $Forest
    )
    try {
        if ($Forest) {
            $Type = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest
            $Context = [System.DirectoryServices.ActiveDirectory.DirectoryContext]::new($Type, $Forest)
            $ForestInformation = [System.DirectoryServices.ActiveDirectory.Forest]::GetForest($Context)
        } else {
            $ForestInformation = ([System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest())
        }
    } catch {
        Write-Warning "Get-WinADForest - Can't get $Forest information, error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
    }
    $ForestInformation
}
function Get-WinADForestControllerInformation {
    <#
    .SYNOPSIS
    Retrieves information about domain controllers in a specified Active Directory forest.
 
    .DESCRIPTION
    This function retrieves detailed information about domain controllers within the specified Active Directory forest.
    It queries the forest for domain controller properties such as PrimaryGroupID, OperatingSystem, LastLogonDate, etc.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve domain controller information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the search.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the search.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest for retrieving domain controller details.
 
    .EXAMPLE
    Get-WinADForestControllerInformation -Forest "example.com" -IncludeDomains @("example.com") -ExcludeDomains @("test.com")
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $Today = Get-Date
    $ForestInformation = Get-WinADForestDetails -Extended -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation -Verbose:$false
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers'][$Domain]['HostName'][0]
        $Properties = @(
            'PrimaryGroupID'
            'PrimaryGroup'
            'Enabled'
            'ManagedBy'
            'OperatingSystem'
            'OperatingSystemVersion'
            'PasswordLastSet'
            'PasswordExpired'
            'PasswordNeverExpires'
            'PasswordNotRequired'
            'TrustedForDelegation'
            'UseDESKeyOnly'
            'TrustedToAuthForDelegation'
            'WhenCreated'
            'WhenChanged'
            'LastLogonDate'
            'IPv4Address'
            'IPv6Address'
        )
        $Filter = 'Name -ne "AzureADKerberos" -and DNSHostName -like "*"'
        $DCs = Get-ADComputer -Server $QueryServer -SearchBase $ForestInformation['DomainsExtended'][$Domain].DomainControllersContainer -Filter $Filter -Properties $Properties
        $Count = 0
        foreach ($DC in $DCs) {
            $Count++
            Write-Verbose -Message "Get-WinADForestControllerInformation - Processing [$($Domain)]($Count/$($DCs.Count)) $($DC.DNSHostName)"
            $Owner = Get-ADACLOwner -ADObject $DC.DistinguishedName -Resolve

            if ($null -ne $DC.LastLogonDate) {
                [int] $LastLogonDays = "$(-$($DC.LastLogonDate - $Today).Days)"
            } else {
                $LastLogonDays = $null
            }
            if ($null -ne $DC.PasswordLastSet) {
                [int] $PasswordLastChangedDays = "$(-$($DC.PasswordLastSet - $Today).Days)"
            } else {
                $PasswordLastChangedDays = $null
            }

            $Options = Get-WinADDomainControllerOption -DomainController $DC.DNSHostName
            if ($Options.Options -contains 'DISABLE_OUTBOUND_REPL') {
                $DisabledOutboundReplication = $true
            } else {
                $DisabledOutboundReplication = $false
            }
            if ($Options.Options -contains 'DISABLE_INBOUND_REPL') {
                $DisabledInboundReplication = $true
            } else {
                $DisabledInboundReplication = $false
            }
            if ($Options.Options -contains "IS_GC") {
                $IsGlobalCatalog = $true
            } else {
                $IsGlobalCatalog = $false
            }
            if ($Options.Options -contains 'IS_RODC') {
                $IsReadOnlyDomainController = $true
            } else {
                $IsReadOnlyDomainController = $false
            }

            $Roles = [ordered] @{}
            $Roles['SchemaMaster'] = $ForestInformation.Forest.SchemaMaster
            $Roles['DomainNamingMaster'] = $ForestInformation.Forest.DomainNamingMaster
            $Roles['InfrastructureMaster'] = $ForestInformation.DomainsExtended[$Domain].InfrastructureMaster
            $Roles['RIDMaster'] = $ForestInformation.DomainsExtended[$Domain].RIDMaster
            $Roles['PDCEmulator'] = $ForestInformation.DomainsExtended[$Domain].PDCEmulator


            $DNS = Resolve-DnsName -DnsOnly -Name $DC.DNSHostName -ErrorAction SilentlyContinue -QuickTimeout -Verbose:$false
            if ($DNS) {
                $ResolvedIP4 = ($DNS | Where-Object { $_.Section -eq 'Answer' -and $_.Type -eq 'A' }).IPAddress
                $ResolvedIP6 = ($DNS | Where-Object { $_.Section -eq 'Answer' -and $_.Type -eq 'AAAA' }).IPAddress
                $DNSStatus = $true
            } else {
                $ResolvedIP4 = $null
                $ResolvedIP6 = $null
                $DNSStatus = $false
            }
            [PSCustomObject] @{
                DNSHostName                 = $DC.DNSHostName
                DomainName                  = $Domain
                Enabled                     = $DC.Enabled
                DNSStatus                   = $DNSStatus
                IsGC                        = $IsGlobalCatalog
                IsRODC                      = $IsReadOnlyDomainController
                IPAddressStatusV4           = if ($ResolvedIP4 -eq $DC.IPv4Address) {
                    $true 
                } else {
                    $false 
                }
                IPAddressStatusV6           = if ($ResolvedIP6 -eq $DC.IPv6Address) {
                    $true 
                } else {
                    $false 
                }
                IPAddressHasOneIpV4         = $ResolvedIP4 -isnot [Array]
                IPAddressHasOneipV6         = $ResolvedIP6 -isnot [Array]
                ManagerNotSet               = $Null -eq $ManagedBy
                OwnerType                   = $Owner.OwnerType
                PasswordLastChangedDays     = $PasswordLastChangedDays
                LastLogonDays               = $LastLogonDays
                Owner                       = $Owner.OwnerName
                OwnerSid                    = $Owner.OwnerSid
                ManagedBy                   = $DC.ManagedBy
                DNSResolvedIPv4             = $ResolvedIP4
                DNSResolvedIPv6             = $ResolvedIP6
                IPv4Address                 = $DC.IPv4Address
                IPv6Address                 = $DC.IPv6Address
                LastLogonDate               = $DC.LastLogonDate
                OperatingSystem             = $DC.OperatingSystem
                OperatingSystemVersion      = $DC.OperatingSystemVersion
                PasswordExpired             = $DC.PasswordExpired
                PasswordLastSet             = $DC.PasswordLastSet
                PasswordNeverExpires        = $DC.PasswordNeverExpires
                PasswordNotRequired         = $DC.PasswordNotRequired
                TrustedForDelegation        = $DC.TrustedForDelegation
                TrustedToAuthForDelegation  = $DC.TrustedToAuthForDelegation
                DisabledOutboundReplication = $DisabledOutboundReplication
                DisabledInboundReplication  = $DisabledInboundReplication
                Options                     = $Options.Options -join ', '
                UseDESKeyOnly               = $DC.UseDESKeyOnly
                SchemaMaster                = if ($Roles['SchemaMaster'] -eq $DC.DNSHostName) {
                    $true 
                } else {
                    $false 
                }
                DomainNamingMaster          = if ($Roles['DomainNamingMaster'] -eq $DC.DNSHostName) {
                    $true 
                } else {
                    $false 
                }
                InfrastructureMaster        = if ($Roles['InfrastructureMaster'] -eq $DC.DNSHostName) {
                    $true 
                } else {
                    $false 
                }
                RIDMaster                   = if ($Roles['RIDMaster'] -eq $DC.DNSHostName) {
                    $true 
                } else {
                    $false 
                }
                PDCEmulator                 = if ($Roles['PDCEmulator'] -eq $DC.DNSHostName) {
                    $true 
                } else {
                    $false 
                }
                WhenCreated                 = $DC.WhenCreated
                WhenChanged                 = $DC.WhenChanged
                DistinguishedName           = $DC.DistinguishedName
            }
        }
    }
}
function Get-WinADForestOptionalFeatures {
    <#
    .SYNOPSIS
    Retrieves optional features information for a specified Active Directory forest.
 
    .DESCRIPTION
    Retrieves detailed information about optional features within the specified Active Directory forest.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve optional features information from.
 
    .PARAMETER ComputerProperties
    Specifies an array of computer properties to check for specific features.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest for retrieving optional features.
 
    .EXAMPLE
    Get-WinADForestOptionalFeatures -Forest "example.com" -ComputerProperties @("ms-Mcs-AdmPwd", "msLAPS-Password")
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [Array] $ComputerProperties,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    if (-not $ComputerProperties) {
        $ComputerProperties = Get-WinADForestSchemaProperties -Schema 'Computers' -Forest $Forest -ExtendedForestInformation $ForestInformation
    }
    $QueryServer = $ForestInformation['QueryServers']["Forest"].HostName[0]
    $LapsProperties = 'ms-Mcs-AdmPwd'
    $WindowsLapsProperties = 'msLAPS-Password'
    $OptionalFeatures = $(Get-ADOptionalFeature -Filter "*" -Server $QueryServer)
    $Optional = [ordered]@{
        'Recycle Bin Enabled'                          = $false
        'Privileged Access Management Feature Enabled' = $false
        'LAPS Enabled'                                 = ($ComputerProperties.Name -contains $LapsProperties)
        'Windows LAPS Enabled'                         = ($ComputerProperties.Name -contains $WindowsLapsProperties)
    }
    foreach ($Feature in $OptionalFeatures) {
        if ($Feature.Name -eq 'Recycle Bin Feature') {
            $Optional.'Recycle Bin Enabled' = $Feature.EnabledScopes.Count -gt 0
        }
        if ($Feature.Name -eq 'Privileged Access Management Feature') {
            $Optional.'Privileged Access Management Feature Enabled' = $Feature.EnabledScopes.Count -gt 0
        }
    }
    $Optional
}
function Get-WinADForestReplication {
    <#
    .SYNOPSIS
    Retrieves replication information for a specified Active Directory forest.
 
    .DESCRIPTION
    Retrieves detailed information about replication within the specified Active Directory forest.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve replication information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the replication search.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controllers to exclude from the replication search.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the replication search.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controllers to include in the replication search.
 
    .PARAMETER SkipRODC
    Indicates whether to skip read-only domain controllers during replication.
 
    .PARAMETER Extended
    Indicates whether to include extended replication information.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest for replication.
 
    .EXAMPLE
    Get-WinADForestReplication -Forest "example.com" -IncludeDomains @("example.com") -ExcludeDomains @("test.com") -IncludeDomainControllers @("DC1") -ExcludeDomainControllers @("DC2") -SkipRODC -Extended -ExtendedForestInformation $ExtendedForestInfo
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [switch] $Extended,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ProcessErrors = [System.Collections.Generic.List[PSCustomObject]]::new()
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    $Replication = foreach ($DC in $ForestInformation.ForestDomainControllers) {
        try {
            Get-ADReplicationPartnerMetadata -Target $DC.HostName -Partition * -ErrorAction Stop #-ErrorVariable +ProcessErrors
        } 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-WinADForestReplicationSummary {
    <#
    .SYNOPSIS
    Function that retrieves the replication summary of the Active Directory forest.
 
    .DESCRIPTION
    This function retrieves the replication summary of the Active Directory forest.
    It uses the repadmin command to retrieve the replication summary and then parses
    the output to create a custom object with the following properties:
    - Server: The server name.
    - LargestDelta: The largest delta between replication cycles.
    - Fails: The number of failed replication cycles.
    - Total: The total number of replication cycles.
    - PercentageError: The percentage of failed replication cycles.
    - Type: The type of server (Source or Destination).
    - ReplicationError: The replication error message.
 
    .PARAMETER InputContent
    Allow the user to pass the repadmin output as a string.
 
    .PARAMETER FilePath
    Allow the user to pass the path of a file containing the repadmin output.
 
    .PARAMETER IncludeStatisticsVariable
    Allow the user to pass the name of a variable to store the statistics.
 
    .EXAMPLE
    Get-WinADForestReplicationSummary | Format-Table
 
    .EXAMPLE
    Get-WinADForestReplicationSummary -FilePath C:\repadmin.txt | Format-Table
 
    .EXAMPLE
    Get-WinADForestReplicationSummary -InputContent $repadminOutput | Format-Table
 
    .EXAMPLE
    Get-WinADForestReplicationSummary -IncludeStatisticsVariable Statistics | Format-Table
 
    $Statistics | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(ParameterSetName = 'InputContent')][string] $InputContent,
        [Parameter(ParameterSetName = 'FilePath')][string] $FilePath,
        [string] $IncludeStatisticsVariable
    )

    if ($InputContent) {
        $OutputRepadmin = $InputContent
    } elseif ($FilePath) {
        $OutputRepadmin = Get-Content -Path $FilePath -Raw
    } else {
        # Run repadmin and capture the output
        $OutputRepadmin = repadmin /replsummary /bysrc /bydest | Out-String
    }
    # Split the output into sections
    $sections = $OutputRepadmin -split "Source DSA|Destination DSA"

    $lines = $sections[1] -split "`r`n"
    [Array] $sourceData = foreach ($line in $lines) {
        if ($line -match '^Experienced the following operational errors trying to retrieve replication information') {
            break
        }
        if ($line -match '\S' -and $line -notmatch '^\s*largest') {
            if ($line -match "^\s*(?<DSA>\S+)\s+(?<Rest>.*)$") {
                Write-Verbose -Message "Processing line: $line"
                $DSA = $Matches.DSA
                # $rest = $Matches.Rest -split "\s+", 4 # split into 4 parts: LargestDelta, Fails, Total, Percentage and the rest
                $Rest = $Matches.Rest
                if ($rest -match ">60 days") {
                    $RestSplitted = $Rest -split "\s+", 7
                    $LargestDelta = New-TimeSpan -Days 60
                    $Fails = $RestSplitted[2]
                    $Total = $RestSplitted[4]
                    $Percentage = $RestSplitted[5]
                    $ReplicationError = $RestSplitted[6]
                    $Type = "Source"
                } else {
                    $RestSplitted = $Rest -split "\s+", 4 # split into 4 parts: LargestDelta, Fails, Total, Percentage and the rest
                    $LargestDelta = ConvertTo-TimeSpanFromRepadmin -timeString $RestSplitted[0]
                    $Fails = $RestSplitted[1]
                    $Continue = $RestSplitted[3]
                    $Continue = $Continue -split "\s{2,}"
                    $Total = $Continue[0]
                    $Percentage = $Continue[1]

                    $ReplicationError = $Continue[2]
                    if ($null -eq $ReplicationError) {
                        $ReplicationError = "None"
                    }
                    $Type = "Source"
                }


                [PSCustomObject]@{
                    Server           = $DSA
                    LargestDelta     = $LargestDelta
                    Fails            = if ($null -ne $Fails) {
                        [int] $Fails.Replace("/", "").Trim() 
                    } else {
                        $null 
                    }
                    Total            = [int] $Total
                    PercentageError  = $Percentage
                    Type             = $Type
                    ReplicationError = $ReplicationError
                }
            }
        }
    }

    $lines = $sections[2] -split "`r`n"
    [Array] $destinationData = foreach ($line in $lines) {
        if ($line -match '^Experienced the following operational errors trying to retrieve replication information') {
            break
        }
        if ($line -match '\S' -and $line -notmatch '^\s*largest') {
            if ($line -match "^\s*(?<DSA>\S+)\s+(?<Rest>.*)$") {
                Write-Verbose -Message "Processing line: $line"
                $DSA = $Matches.DSA
                $Rest = $Matches.Rest
                if ($rest -match ">60 days") {
                    $RestSplitted = $Rest -split "\s+", 7
                    $LargestDelta = New-TimeSpan -Days 60
                    $Fails = $RestSplitted[2]
                    $Total = $RestSplitted[4]
                    $Percentage = $RestSplitted[5]
                    $ReplicationError = $RestSplitted[6]
                    $Type = "Destination"
                } else {
                    $RestSplitted = $Rest -split "\s+", 4 # split into 4 parts: LargestDelta, Fails, Total, Percentage and the rest
                    $LargestDelta = ConvertTo-TimeSpanFromRepadmin -timeString $RestSplitted[0]
                    $Fails = $RestSplitted[1]
                    $Continue = $RestSplitted[3]
                    $Continue = $Continue -split "\s{2,}"
                    $Total = $Continue[0]
                    $Percentage = $Continue[1]

                    $ReplicationError = $Continue[2]
                    if ($null -eq $ReplicationError) {
                        $ReplicationError = "None"
                    }
                    $Type = "Destination"
                }

                [PSCustomObject]@{
                    Server           = $DSA
                    LargestDelta     = $LargestDelta
                    Fails            = if ($null -ne $Fails) {
                        [int] $Fails.Replace("/", "").Trim() 
                    } else {
                        $null 
                    }
                    Total            = [int] $Total
                    PercentageError  = $Percentage
                    Type             = $Type
                    ReplicationError = $ReplicationError
                }
            }
        }
    }

    [Array] $operationalErrors = foreach ($line in $lines) {
        if ($line -match '^Experienced the following operational errors trying to retrieve replication information') {
            $processingErrors = $true
            continue
        }
        if ($processingErrors) {
            if ($line -match "^\s*(?<ErrorCode>\d+)\s+-\s+(?<ServerName>.*)$") {
                Write-Verbose -Message "Processing error line: $line"
                $ErrorCode = $Matches.ErrorCode
                $ServerName = $Matches.ServerName
                if ($ServerName -match "\.") {
                    $HostName = $ServerName.Split(".")[0]
                } else {
                    $HostName = $ServerName
                }
                [PSCustomObject]@{
                    Server           = $HostName
                    LargestDelta     = $null
                    Fails            = 1
                    Total            = 1
                    PercentageError  = 100
                    Type             = "Unknown"
                    ReplicationError = "($ErrorCode) Error trying to retrieve replication information"
                }
            }
        }
    }


    # Combine the data from both sections
    $ReplicationSummary = $sourceData + $destinationData + $operationalErrors
    $ReplicationSummary

    if ($IncludeStatisticsVariable) {
        $Statistics = [ordered] @{
            "Good"             = 0
            "Failures"         = 0
            "Total"            = 0
            "DeltaOver1Hours"  = 0
            "DeltaOver3Hours"  = 0
            "DeltaOver6Hours"  = 0
            "DeltaOver12Hours" = 0
            "DeltaOver24Hours" = 0
            "UniqueErrors"     = [System.Collections.Generic.List[string]]::new()
            "UniqueWarnings"   = [System.Collections.Generic.List[string]]::new()
        }
        foreach ($Replication in $ReplicationSummary) {
            $Statistics.Total++

            if ($Replication.LargestDelta -gt (New-TimeSpan -Hours 24)) {
                $Statistics.DeltaOver24Hours++
            } elseif ($Replication.LargestDelta -gt (New-TimeSpan -Hours 12)) {
                $Statistics.DeltaOver12Hours++
            } elseif ($Replication.LargestDelta -gt (New-TimeSpan -Hours 6)) {
                $Statistics.DeltaOver6Hours++
            } elseif ($Replication.LargestDelta -gt (New-TimeSpan -Hours 3)) {
                $Statistics.DeltaOver3Hours++
            } elseif ($Replication.LargestDelta -gt (New-TimeSpan -Hours 1)) {
                $Statistics.DeltaOver1Hours++
            }
            if ($Replication.Fails -eq 0) {
                $Statistics.Good++
            } else {
                $Statistics.Failures++
            }
            if ($Replication.ReplicationError -notin "None", "") {
                if ($Replication.ReplicationError -like "*Operational errors trying to retrieve replication information*") {
                    if ($Replication.ReplicationError -notin $Statistics.UniqueWarnings) {
                        $Statistics.UniqueWarnings.Add($Replication.ReplicationError)
                    }
                } elseif ($Replication.ReplicationError -like "*The remote procedure call was cancelled.*") {
                    if ($Replication.ReplicationError -notin $Statistics.UniqueWarnings) {
                        $Statistics.UniqueWarnings.Add($Replication.ReplicationError)
                    }
                } elseif ($Replication.ReplicationError -like "*The RPC server is unavailable*") {
                    if ($Replication.ReplicationError -notin $Statistics.UniqueWarnings) {
                        $Statistics.UniqueWarnings.Add($Replication.ReplicationError)
                    }
                } elseif ($Replication.ReplicationError -notin $Statistics.UniqueErrors) {
                    if ($Statistics.UniqueErrors -notcontains $Replication.ReplicationError) {
                        $Statistics.UniqueErrors.Add($Replication.ReplicationError)
                    }
                }
            }
        }

        Set-Variable -Scope Global -Name $IncludeStatisticsVariable -Value $Statistics
    }
}
function Get-WinADForestRoles {
    <#
    .SYNOPSIS
    Lists all the forest roles for the chosen forest. By default uses current forest.
 
    .DESCRIPTION
    Lists all the forest roles for the chosen forest. By default uses current forest.
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExcludeDomainControllers
    Exclude specific domain controllers, by default there are no exclusions
 
    .PARAMETER IncludeDomainControllers
    Include only specific domain controllers, by default all domain controllers are included
 
    .PARAMETER SkipRODC
    Skip Read-Only Domain Controllers. By default all domain controllers are included.
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .PARAMETER Formatted
    Returns objects in formatted way
 
    .PARAMETER Splitter
    Character to use as splitter/joiner in formatted output
 
    .EXAMPLE
    $Roles = Get-WinADForestRoles
    $Roles | ft *
 
    .NOTES
    General notes
    #>

    [alias('Get-WinADRoles', 'Get-WinADDomainRoles')]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [switch] $Formatted,
        [string] $Splitter = ', ',
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    $Roles = [ordered] @{
        SchemaMaster         = $null
        DomainNamingMaster   = $null
        PDCEmulator          = $null
        RIDMaster            = $null
        InfrastructureMaster = $null
        IsReadOnly           = $null
        IsGlobalCatalog      = $null
    }

    foreach ($_ in $ForestInformation.ForestDomainControllers) {
        if ($_.IsSchemaMaster -eq $true) {
            $Roles['SchemaMaster'] = if ($null -ne $Roles['SchemaMaster']) {
                @($Roles['SchemaMaster']) + $_.HostName 
            } else {
                $_.HostName 
            }
        }
        if ($_.IsDomainNamingMaster -eq $true) {
            $Roles['DomainNamingMaster'] = if ($null -ne $Roles['DomainNamingMaster']) {
                @($Roles['DomainNamingMaster']) + $_.HostName 
            } else {
                $_.HostName 
            }
        }
        if ($_.IsPDC -eq $true) {
            $Roles['PDCEmulator'] = if ($null -ne $Roles['PDCEmulator']) {
                @($Roles['PDCEmulator']) + $_.HostName 
            } else {
                $_.HostName 
            }
        }
        if ($_.IsRIDMaster -eq $true) {
            $Roles['RIDMaster'] = if ($null -ne $Roles['RIDMaster']) {
                @($Roles['RIDMaster']) + $_.HostName 
            } else {
                $_.HostName 
            }
        }
        if ($_.IsInfrastructureMaster -eq $true) {
            $Roles['InfrastructureMaster'] = if ($null -ne $Roles['InfrastructureMaster']) {
                @($Roles['InfrastructureMaster']) + $_.HostName 
            } else {
                $_.HostName 
            }
        }
        if ($_.IsReadOnly -eq $true) {
            $Roles['IsReadOnly'] = if ($null -ne $Roles['IsReadOnly']) {
                @($Roles['IsReadOnly']) + $_.HostName 
            } else {
                $_.HostName 
            }
        }
        if ($_.IsGlobalCatalog -eq $true) {
            $Roles['IsGlobalCatalog'] = if ($null -ne $Roles['IsGlobalCatalog']) {
                @($Roles['IsGlobalCatalog']) + $_.HostName 
            } else {
                $_.HostName 
            }
        }
    }
    if ($Formatted) {
        foreach ($_ in ([string[]] $Roles.Keys)) {
            $Roles[$_] = $Roles[$_] -join $Splitter
        }
    }
    $Roles
}
function Get-WinADForestSchemaProperties {
    <#
    .SYNOPSIS
    Retrieves schema properties for a specified Active Directory forest.
 
    .DESCRIPTION
    Retrieves detailed information about schema properties within the specified Active Directory forest.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve schema properties from.
 
    .PARAMETER Schema
    Specifies the type of schema properties to retrieve. Valid values are 'Computers' and 'Users'.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest.
 
    .EXAMPLE
    Get-WinADForestSchemaProperties -Forest "example.com" -Schema @('Computers', 'Users')
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [validateSet('Computers', 'Users')][string[]] $Schema = @('Computers', 'Users'),
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    <#
    Name : dLMemRejectPermsBL
    CommonName : ms-Exch-DL-Mem-Reject-Perms-BL
    Oid : 1.2.840.113556.1.2.293
    Syntax : DN
    Description :
    IsSingleValued : False
    IsIndexed : False
    IsIndexedOverContainer : False
    IsInAnr : False
    IsOnTombstonedObject : False
    IsTupleIndexed : False
    IsInGlobalCatalog : True
    RangeLower :
    RangeUpper :
    IsDefunct : False
    Link : dLMemRejectPerms
    LinkId : 117
    SchemaGuid : a8df73c3-c5ea-11d1-bbcb-0080c76670c0
    #>

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    if ($Forest) {
        $Type = [System.DirectoryServices.ActiveDirectory.DirectoryContextType]::Forest
        $Context = [System.DirectoryServices.ActiveDirectory.DirectoryContext]::new($Type, $ForestInformation.Forest)
        $CurrentSchema = [directoryservices.activedirectory.activedirectoryschema]::GetSchema($Context)
    } else {
        $CurrentSchema = [directoryservices.activedirectory.activedirectoryschema]::GetCurrentSchema()
    }
    if ($Schema -contains 'Computers') {
        $CurrentSchema.FindClass("computer").mandatoryproperties | Select-Object -Property name, commonname, description, syntax , SchemaGuid
        $CurrentSchema.FindClass("computer").optionalproperties | Select-Object -Property name, commonname, description, syntax, SchemaGuid
    }
    if ($Schema -contains 'Users') {
        $CurrentSchema.FindClass("user").mandatoryproperties | Select-Object -Property name, commonname, description, syntax, SchemaGuid
        $CurrentSchema.FindClass("user").optionalproperties | Select-Object -Property name, commonname, description, syntax, SchemaGuid
    }
}
function Get-WinADForestSites {
    <#
    .SYNOPSIS
    Retrieves site information for a specified Active Directory forest.
 
    .DESCRIPTION
    Retrieves detailed information about sites within the specified Active Directory forest.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve site information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the search.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controllers to exclude from the search.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the search.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controllers to include in the search.
 
    .PARAMETER SkipRODC
    Indicates whether to skip read-only domain controllers.
 
    .PARAMETER Formatted
    Indicates whether to format the output.
 
    .PARAMETER Splitter
    Specifies the delimiter to use for splitting values.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest.
 
    .EXAMPLE
    Get-WinADForestSites -Forest "example.com" -IncludeDomains @("example.com") -ExcludeDomains @("test.com") -IncludeDomainControllers @("DC1") -ExcludeDomainControllers @("DC2") -SkipRODC -Formatted -Splitter ","
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [switch] $Formatted,
        [string] $Splitter,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    <#
                'nTSecurityDescriptor' = $_.'nTSecurityDescriptor'
                LastKnownParent = $_.LastKnownParent
                instanceType = $_.InstanceType
                InterSiteTopologyGenerator = $_.InterSiteTopologyGenerator
                dSCorePropagationData = $_.dSCorePropagationData
                ReplicationSchedule = $_.ReplicationSchedule.RawSchedule -join ','
                msExchServerSiteBL = $_.msExchServerSiteBL -join ','
                siteObjectBL = $_.siteObjectBL -join ','
                systemFlags = $_.systemFlags
                ObjectGUID = $_.ObjectGUID
                ObjectCategory = $_.ObjectCategory
                ObjectClass = $_.ObjectClass
                ScheduleHashingEnabled = $_.ScheduleHashingEnabled
    #>

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    $QueryServer = $ForestInformation.QueryServers[$($ForestInformation.Forest.Name)]['HostName'][0]
    $Sites = Get-ADReplicationSite -Filter "*" -Properties * -Server $QueryServer
    foreach ($Site in $Sites) {
        [Array] $DCs = $ForestInformation.ForestDomainControllers | Where-Object { $_.Site -eq $Site.Name }
        [Array] $Subnets = ConvertFrom-DistinguishedName -DistinguishedName $Site.'Subnets'

        if ($Formatted) {
            [PSCustomObject] @{
                'Name'                                                    = $Site.Name
                #'Display Name' = $Site.'DisplayName'
                'Description'                                             = $Site.'Description'
                'CanonicalName'                                           = $Site.'CanonicalName'
                'Subnets Count'                                           = $Subnets.Count
                'Domain Controllers Count'                                = $DCs.Count
                'Location'                                                = $Site.'Location'
                'ManagedBy'                                               = $Site.'ManagedBy'
                'Subnets'                                                 = if ($Splitter) {
                    $Subnets -join $Splitter 
                } else {
                    $Subnets 
                }
                'Domain Controllers'                                      = if ($Splitter) {
 ($DCs).HostName -join $Splitter 
                } else {
 ($DCs).HostName 
                }
                'DistinguishedName'                                       = $Site.'DistinguishedName'
                'Protected From Accidental Deletion'                      = $Site.'ProtectedFromAccidentalDeletion'
                'Redundant Server Topology Enabled'                       = $Site.'RedundantServerTopologyEnabled'
                'Automatic Inter-Site Topology Generation Enabled'        = $Site.'AutomaticInterSiteTopologyGenerationEnabled'
                'Automatic Topology Generation Enabled'                   = $Site.'AutomaticTopologyGenerationEnabled'
                'sDRightsEffective'                                       = $Site.'sDRightsEffective'
                'Topology Cleanup Enabled'                                = $Site.'TopologyCleanupEnabled'
                'Topology Detect Stale Enabled'                           = $Site.'TopologyDetectStaleEnabled'
                'Topology Minimum Hops Enabled'                           = $Site.'TopologyMinimumHopsEnabled'
                'Universal Group Caching Enabled'                         = $Site.'UniversalGroupCachingEnabled'
                'Universal Group Caching Refresh Site'                    = $Site.'UniversalGroupCachingRefreshSite'
                'Windows Server 2000 Bridgehead Selection Method Enabled' = $Site.'WindowsServer2000BridgeheadSelectionMethodEnabled'
                'Windows Server 2000 KCC ISTG Selection Behavior Enabled' = $Site.'WindowsServer2000KCCISTGSelectionBehaviorEnabled'
                'Windows Server 2003 KCC Behavior Enabled'                = $Site.'WindowsServer2003KCCBehaviorEnabled'
                'Windows Server 2003 KCC Ignore Schedule Enabled'         = $Site.'WindowsServer2003KCCIgnoreScheduleEnabled'
                'Windows Server 2003 KCC SiteLink Bridging Enabled'       = $Site.'WindowsServer2003KCCSiteLinkBridgingEnabled'
                'Created'                                                 = $Site.Created
                'Modified'                                                = $Site.Modified
                'Deleted'                                                 = $Site.Deleted
            }
        } else {
            [PSCustomObject] @{
                'Name'                                              = $Site.Name
                #'DisplayName' = $Site.'DisplayName'
                'Description'                                       = $Site.'Description'
                'CanonicalName'                                     = $Site.'CanonicalName'
                'SubnetsCount'                                      = $Subnets.Count
                'DomainControllersCount'                            = $DCs.Count
                'Subnets'                                           = if ($Splitter) {
                    $Subnets -join $Splitter 
                } else {
                    $Subnets 
                }
                'DomainControllers'                                 = if ($Splitter) {
 ($DCs).HostName -join $Splitter 
                } else {
 ($DCs).HostName 
                }
                'Location'                                          = $Site.'Location'
                'ManagedBy'                                         = $Site.'ManagedBy'
                'DistinguishedName'                                 = $Site.'DistinguishedName'
                'ProtectedFromAccidentalDeletion'                   = $Site.'ProtectedFromAccidentalDeletion'
                'RedundantServerTopologyEnabled'                    = $Site.'RedundantServerTopologyEnabled'
                'AutomaticInterSiteTopologyGenerationEnabled'       = $Site.'AutomaticInterSiteTopologyGenerationEnabled'
                'AutomaticTopologyGenerationEnabled'                = $Site.'AutomaticTopologyGenerationEnabled'
                'sDRightsEffective'                                 = $Site.'sDRightsEffective'
                'TopologyCleanupEnabled'                            = $Site.'TopologyCleanupEnabled'
                'TopologyDetectStaleEnabled'                        = $Site.'TopologyDetectStaleEnabled'
                'TopologyMinimumHopsEnabled'                        = $Site.'TopologyMinimumHopsEnabled'
                'UniversalGroupCachingEnabled'                      = $Site.'UniversalGroupCachingEnabled'
                'UniversalGroupCachingRefreshSite'                  = $Site.'UniversalGroupCachingRefreshSite'
                'WindowsServer2000BridgeheadSelectionMethodEnabled' = $Site.'WindowsServer2000BridgeheadSelectionMethodEnabled'
                'WindowsServer2000KCCISTGSelectionBehaviorEnabled'  = $Site.'WindowsServer2000KCCISTGSelectionBehaviorEnabled'
                'WindowsServer2003KCCBehaviorEnabled'               = $Site.'WindowsServer2003KCCBehaviorEnabled'
                'WindowsServer2003KCCIgnoreScheduleEnabled'         = $Site.'WindowsServer2003KCCIgnoreScheduleEnabled'
                'WindowsServer2003KCCSiteLinkBridgingEnabled'       = $Site.'WindowsServer2003KCCSiteLinkBridgingEnabled'
                'Created'                                           = $Site.Created
                'Modified'                                          = $Site.Modified
                'Deleted'                                           = $Site.Deleted
            }
        }
    }
}
function Get-WinADForestSubnet {
    <#
    .SYNOPSIS
    Retrieves subnet information for a specified Active Directory forest.
 
    .DESCRIPTION
    Retrieves detailed information about subnets within the specified Active Directory forest.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve subnet information from.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest.
 
    .PARAMETER VerifyOverlap
    Indicates whether to verify overlapping subnets.
 
    .EXAMPLE
    Get-WinADForestSubnet -Forest "example.com" -VerifyOverlap
    This example retrieves subnet information for the "example.com" forest and verifies overlapping subnets.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [alias('Get-WinADSubnet')]
    [cmdletBinding()]
    param(
        [string] $Forest,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [switch] $VerifyOverlap
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    $QueryServer = $ForestInformation.QueryServers[$($ForestInformation.Forest.Name)]['HostName'][0]
    $ForestDN = ConvertTo-DistinguishedName -ToDomain -CanonicalName $ForestInformation.Forest.Name

    $ADObjectSplat = @{
        Server      = $QueryServer
        LDAPFilter  = '(objectClass=subnet)'
        SearchBase  = "CN=Subnets,CN=Sites,CN=Configuration,$($($ForestDN))"
        SearchScope = 'OneLevel'
        Properties  = 'Name', 'distinguishedName', 'CanonicalName', 'WhenCreated', 'whenchanged', 'ProtectedFromAccidentalDeletion', 'siteObject', 'location', 'objectClass', 'Description'
    }
    try {
        $SubnetsList = Get-ADObject @ADObjectSplat -ErrorAction Stop
    } catch {
        Write-Warning "Get-WinADSites - LDAP Filter: $($ADObjectSplat.LDAPFilter), SearchBase: $($ADObjectSplat.SearchBase)), Error: $($_.Exception.Message)"
    }

    $Cache = @{}
    if ($VerifyOverlap) {
        $Subnets = Get-ADSubnet -Subnets $SubnetsList -AsHashTable
        $OverlappingSubnets = Test-ADSubnet -Subnets $Subnets
        foreach ($Subnet in $OverlappingSubnets) {
            if (-not $Cache[$Subnet.Name]) {
                $Cache[$Subnet.Name] = [System.Collections.Generic.List[string]]::new()
            }
            $Cache[$Subnet.Name].Add($Subnet.OverlappingSubnet)
        }
        foreach ($Subnet in $Subnets) {
            if ($Subnet.Type -eq 'IPv4') {
                # We only set it to false to IPV4, for IPV6 it will be null as we don't know
                $Subnet['Overlap'] = $false
            }
            if ($Cache[$Subnet.Name]) {
                $Subnet['Overlap'] = $true
                $Subnet['OverLapList'] = $Cache[$Subnet.Name]
            } else {
            }
            [PSCustomObject] $Subnet
        }
    } else {
        Get-ADSubnet -Subnets $SubnetsList
    }
}
function Get-WinADGroupMember {
    <#
    .SYNOPSIS
    The Get-WinADGroupMember cmdlet gets the members of an Active Directory group. Members can be users, groups, and computers.
 
    .DESCRIPTION
    The Get-WinADGroupMember cmdlet gets the members of an Active Directory group. Members can be users, groups, and computers. The Identity parameter specifies the Active Directory group to access. You can identify a group by its distinguished name, GUID, security identifier, or Security Account Manager (SAM) account name. You can also specify the group by passing a group object through the pipeline. For example, you can use the Get-ADGroup cmdlet to get a group object and then pass the object through the pipeline to the Get-WinADGroupMember cmdlet.
 
    .PARAMETER Identity
    Specifies an Active Directory group object
 
    .PARAMETER AddSelf
    Adds details about initial group name to output. Works only with All switch
 
    .PARAMETER SelfOnly
    Returns only one object that's summary for the whole group. Works only with All switch
 
    .PARAMETER AdditionalStatistics
    Adds additional data to Self object (when AddSelf is used). This data is available always if SelfOnly is used. It includes count for NestingMax, NestingGroup, NestingGroupSecurity, NestingGroupDistribution. It allows for easy filtering where we expect security groups only when there are nested distribution groups.
 
    .PARAMETER All
    Adds details about groups, and their nesting. Without this parameter only unique users and computers are returned
 
    .EXAMPLE
    Get-WinADGroupMember -Identity 'EVOTECPL\Domain Admins' -All
 
    .EXAMPLE
    Get-WinADGroupMember -Group 'GDS-TestGroup9' -All -SelfOnly | Format-List *
 
    .EXAMPLE
    Get-WinADGroupMember -Group 'GDS-TestGroup9' | Format-Table *
 
    .EXAMPLE
    Get-WinADGroupMember -Group 'GDS-TestGroup9' -All -AddSelf | Format-Table *
 
    .EXAMPLE
    Get-WinADGroupMember -Group 'GDS-TestGroup9' -All -AddSelf -AdditionalStatistics | Format-Table *
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [alias('GroupName', 'Group')][Parameter(ValuefromPipeline, Mandatory)][Array] $Identity,
        #[switch] $CountMembers,
        [switch] $AddSelf,
        [switch] $All,
        [switch] $ClearCache,
        [switch] $AdditionalStatistics,
        [switch] $SelfOnly,
        [Parameter(DontShow)][int] $Nesting = -1,
        [Parameter(DontShow)][System.Collections.Generic.List[object]] $CollectedGroups,
        [Parameter(DontShow)][System.Object] $Circular,
        [Parameter(DontShow)][System.Collections.IDictionary] $InitialGroup,
        [Parameter(DontShow)][switch] $Nested
    )
    Begin {
        $Properties = 'GroupName', 'Name', 'SamAccountName', 'DisplayName', 'Enabled', 'Type', 'Nesting', 'CrossForest', 'ParentGroup', 'ParentGroupDomain', 'GroupDomainName', 'DistinguishedName', 'Sid'
        if (-not $Script:WinADGroupMemberCache -or $ClearCache) {
            $Script:WinADGroupMemberCache = @{}
            $Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
            $Script:WinADForestCache = @{
                Forest  = $Forest
                Domains = $Forest.Domains.Name
            }
        }
        if ($Nesting -eq -1) {
            $MembersCache = [ordered] @{}
        }
    }
    Process {
        [Array] $Output = foreach ($GroupName in $Identity) {
            # lets initialize our variables
            if (-not $Nested.IsPresent) {
                $InitialGroup = [ordered] @{
                    GroupName         = $GroupName
                    Name              = $null
                    SamAccountName    = $null
                    DomainName        = $null
                    DisplayName       = $null
                    Enabled           = $null
                    GroupType         = $null
                    GroupScope        = $null
                    Type              = 'group'
                    DirectMembers     = 0
                    DirectGroups      = 0
                    IndirectMembers   = 0
                    TotalMembers      = 0
                    Nesting           = $Nesting
                    CircularDirect    = $false
                    CircularIndirect  = $false
                    CrossForest       = $false
                    ParentGroup       = ''
                    ParentGroupDomain = ''
                    ParentGroupDN     = ''
                    GroupDomainName   = $null
                    DistinguishedName = $null
                    Sid               = $null
                }
                $CollectedGroups = [System.Collections.Generic.List[string]]::new()
                $Nesting = -1
            }
            $Nesting++
            # lets get our object
            $ADGroupName = Get-WinADObject -Identity $GroupName -IncludeGroupMembership
            if ($ADGroupName) {
                # we add DomainName to hashtable so we can easily find which group we're dealing with
                if (-not $Nested.IsPresent) {
                    $InitialGroup.GroupName = $ADGroupName.Name
                    $InitialGroup.DomainName = $ADGroupName.DomainName
                    if ($AddSelf -or $SelfOnly) {
                        # Since we want in final run add primary object to array we need to make sure we have it filled
                        $InitialGroup.Name = $ADGroupName.Name
                        $InitialGroup.SamAccountName = $ADGroupName.SamAccountName
                        $InitialGroup.DisplayName = $ADGroupName.DisplayName
                        $InitialGroup.GroupDomainName = $ADGroupName.DomainName
                        $InitialGroup.DistinguishedName = $ADGroupName.DistinguishedName
                        $InitialGroup.Sid = $ADGroupName.ObjectSID
                        $InitialGroup.GroupType = $ADGroupName.GroupType
                        $InitialGroup.GroupScope = $ADGroupName.GroupScope
                    }
                }
                # Lets cache our object
                $Script:WinADGroupMemberCache[$ADGroupName.DistinguishedName] = $ADGroupName
                if ($Circular -or $CollectedGroups -contains $ADGroupName.DistinguishedName) {
                    Write-Verbose -Message "Get-WinADGroupMember - Group '$($ADGroupName.DistinguishedName)' has $($ADGroupName.Members.Count) members"
                    [Array] $NestedMembers = foreach ($MyIdentity in $ADGroupName.Members) {
                        if ($MyIdentity) {
                            if ($Script:WinADGroupMemberCache[$MyIdentity]) {
                                $Script:WinADGroupMemberCache[$MyIdentity]
                            } else {
                                $ADObject = Get-WinADObject -Identity $MyIdentity -IncludeGroupMembership # -Properties SamAccountName, DisplayName, Enabled, userAccountControl, ObjectSID
                                $Script:WinADGroupMemberCache[$MyIdentity] = $ADObject
                                $Script:WinADGroupMemberCache[$MyIdentity]
                            }
                        } else {
                            Write-Verbose "Get-WinADGroupMember - Group '$($ADGroupName.DistinguishedName)' user skipped because it's null"
                        }
                    }
                    [Array] $NestedMembers = foreach ($Member in $NestedMembers) {
                        if ($CollectedGroups -notcontains $Member.DistinguishedName) {
                            $Member
                        }
                    }
                    $Circular = $null
                } else {
                    Write-Verbose -Message "Get-WinADGroupMember - Group '$($ADGroupName.DistinguishedName)' has $($ADGroupName.Members.Count) members"
                    [Array] $NestedMembers = foreach ($MyIdentity in $ADGroupName.Members) {
                        if ($MyIdentity) {
                            if ($Script:WinADGroupMemberCache[$MyIdentity]) {
                                $Script:WinADGroupMemberCache[$MyIdentity]
                            } else {
                                $ADObject = Get-WinADObject -Identity $MyIdentity -IncludeGroupMembership
                                $Script:WinADGroupMemberCache[$MyIdentity] = $ADObject
                                $Script:WinADGroupMemberCache[$MyIdentity]
                            }
                        } else {
                            Write-Verbose "Get-WinADGroupMember - Group '$($ADGroupName.DistinguishedName)' user skipped because it's null"
                        }
                    }
                }

                # This tracks amount of members for our groups
                if (-not $MembersCache[$ADGroupName.DistinguishedName]) {
                    $DirectMembers = $NestedMembers.Where( { $_.ObjectClass -ne 'group' }, 'split')
                    $MembersCache[$ADGroupName.DistinguishedName] = [ordered] @{
                        DirectMembers        = ($DirectMembers[0])
                        DirectMembersCount   = ($DirectMembers[0]).Count
                        DirectGroups         = ($DirectMembers[1])
                        DirectGroupsCount    = ($DirectMembers[1]).Count
                        IndirectMembers      = [System.Collections.Generic.List[PSCustomObject]]::new()
                        IndirectMembersCount = $null
                        IndirectGroups       = [System.Collections.Generic.List[PSCustomObject]]::new()
                        IndirectGroupsCount  = $null
                    }
                }
                $DomainParentGroup = ConvertFrom-DistinguishedName -DistinguishedName $ADGroupName.DistinguishedName -ToDomainCN
                foreach ($NestedMember in $NestedMembers) {
                    # for each member we either create new user or group, if group we will dive into nesting
                    $CreatedObject = [ordered] @{
                        GroupName         = $InitialGroup.GroupName
                        Name              = $NestedMember.name
                        SamAccountName    = $NestedMember.SamAccountName
                        DomainName        = $NestedMember.DomainName #ConvertFrom-DistinguishedName -DistinguishedName $NestedMember.DistinguishedName -ToDomainCN
                        DisplayName       = $NestedMember.DisplayName
                        Enabled           = $NestedMember.Enabled
                        GroupType         = $NestedMember.GroupType
                        GroupScope        = $NestedMember.GroupScope
                        Type              = $NestedMember.ObjectClass
                        DirectMembers     = 0
                        DirectGroups      = 0
                        IndirectMembers   = 0
                        TotalMembers      = 0
                        Nesting           = $Nesting
                        CircularDirect    = $false
                        CircularIndirect  = $false
                        CrossForest       = $false
                        ParentGroup       = $ADGroupName.name
                        ParentGroupDomain = $DomainParentGroup
                        ParentGroupDN     = $ADGroupName.DistinguishedName
                        GroupDomainName   = $InitialGroup.DomainName
                        DistinguishedName = $NestedMember.DistinguishedName
                        Sid               = $NestedMember.ObjectSID
                    }
                    if ($NestedMember.DomainName -notin $Script:WinADForestCache['Domains']) {
                        $CreatedObject['CrossForest'] = $true
                    }
                    if ($NestedMember.ObjectClass -eq "group") {
                        if ($ADGroupName.memberof -contains $NestedMember.DistinguishedName) {
                            $Circular = $ADGroupName.DistinguishedName
                            $CreatedObject['CircularDirect'] = $true
                        }

                        $CollectedGroups.Add($ADGroupName.DistinguishedName)

                        if ($CollectedGroups -contains $NestedMember.DistinguishedName) {
                            $CreatedObject['CircularIndirect'] = $true
                        }
                        if ($All) {
                            [PSCustomObject] $CreatedObject
                        }
                        Write-Verbose "Get-WinADGroupMember - Going into $($NestedMember.DistinguishedName) (Nesting: $Nesting) (Circular:$Circular)"
                        $OutputFromGroup = Get-WinADGroupMember -GroupName $NestedMember -Nesting $Nesting -Circular $Circular -InitialGroup $InitialGroup -CollectedGroups $CollectedGroups -Nested -All:$All.IsPresent #-CountMembers:$CountMembers.IsPresent
                        if ($null -ne $OutputFromGroup) {
                            $OutputFromGroup
                        }
                        foreach ($Member in $OutputFromGroup) {
                            if ($Member.Type -eq 'group') {
                                $MembersCache[$ADGroupName.DistinguishedName]['IndirectGroups'].Add($Member)
                            } else {
                                $MembersCache[$ADGroupName.DistinguishedName]['IndirectMembers'].Add($Member)
                            }
                        }
                    } else {
                        [PSCustomObject] $CreatedObject
                    }
                }
            }
        }
    }
    End {
        if ($Nesting -eq 0) {
            # If nesting is 0 this means we are ending our run
            if (-not $All) {
                # If not ALL it means User wants to receive only users. Basically Get-ADGroupMember -Recursive
                $Output | Sort-Object -Unique -Property DistinguishedName | Select-Object -Property $Properties
            } else {
                # User requested ALL
                if ($AddSelf -or $SelfOnly) {
                    # User also wants summary object added
                    if ($InitialGroup.DistinguishedName) {
                        $InitialGroup.DirectMembers = $MembersCache[$InitialGroup.DistinguishedName].DirectMembersCount
                        $InitialGroup.DirectGroups = $MembersCache[$InitialGroup.DistinguishedName].DirectGroupsCount
                        foreach ($Group in $MembersCache[$InitialGroup.DistinguishedName].DirectGroups) {
                            $InitialGroup.IndirectMembers = $MembersCache[$Group.DistinguishedName].DirectMembersCount + $InitialGroup.IndirectMembers
                        }
                        # To get total memebers for given group we need to add all members from all groups + direct members of a group
                        $AllMembersForGivenGroup = @(
                            # Scan all groups for members
                            foreach ($DirectGroup in $MembersCache[$InitialGroup.DistinguishedName].DirectGroups) {
                                $MembersCache[$DirectGroup.DistinguishedName].DirectMembers
                            }
                            # Scan all direct members of this group
                            $MembersCache[$InitialGroup.DistinguishedName].DirectMembers
                            # Scan all indirect members of this group
                            $MembersCache[$InitialGroup.DistinguishedName].IndirectMembers
                        )
                    }
                    $InitialGroup['TotalMembers'] = @($AllMembersForGivenGroup | Sort-Object -Unique -Property DistinguishedName).Count

                    if ($AdditionalStatistics -or $SelfOnly) {
                        $NestingMax = @($Output.Nesting | Sort-Object -Unique -Descending)[0]
                        $InitialGroup['NestingMax'] = if ($null -eq $NestingMax) {
                            0 
                        } else {
                            $NestingMax 
                        }
                        $NestingObjectTypes = $Output.Where( { $_.Type -eq 'group' }, 'split')
                        $NestingGroupTypes = $NestingObjectTypes[0].Where( { $_.GroupType -eq 'Security' }, 'split')
                        #$InitialGroup['NestingOther'] = ($NestingObjectTypes[1]).Count
                        $InitialGroup['NestingGroup'] = ($NestingObjectTypes[0]).Count
                        $InitialGroup['NestingGroupSecurity'] = ($NestingGroupTypes[0]).Count
                        $InitialGroup['NestingGroupDistribution'] = ($NestingGroupTypes[1]).Count
                    }

                    # Finally returning object we just built
                    [PSCustomObject] $InitialGroup
                }
                if (-not $SelfOnly) {
                    foreach ($Object in $Output) {
                        if ($Object.Type -eq 'group') {
                            # Object is a group, we add direct members, direct groups and other stuff
                            $Object.DirectMembers = $MembersCache[$Object.DistinguishedName].DirectMembersCount
                            $Object.DirectGroups = $MembersCache[$Object.DistinguishedName].DirectGroupsCount
                            foreach ($DirectGroup in $MembersCache[$Object.DistinguishedName].DirectGroups) {
                                $Object.IndirectMembers = $MembersCache[$DirectGroup.DistinguishedName].DirectMembersCount + $Object.IndirectMembers
                            }
                            # To get total memebers for given group we need to add all members from all groups + direct members of a group
                            $AllMembersForGivenGroup = @(
                                # Scan all groups for members
                                foreach ($DirectGroup in $MembersCache[$Object.DistinguishedName].DirectGroups) {
                                    $MembersCache[$DirectGroup.DistinguishedName].DirectMembers
                                }
                                # Scan all direct members of this group
                                $MembersCache[$Object.DistinguishedName].DirectMembers
                                # Scan all indirect members of this group
                                $MembersCache[$Object.DistinguishedName].IndirectMembers
                            )
                            $Object.TotalMembers = @($AllMembersForGivenGroup | Sort-Object -Unique -Property DistinguishedName).Count
                            # Finally returning object we just built
                            $Object
                        } else {
                            # Object is not a group we push it as is
                            $Object
                        }
                    }
                }
            }
        } else {
            # this is nested call so we want to get whatever it gives us
            $Output
        }
    }
}
function Get-WinADGroupMemberOf {
    <#
    .SYNOPSIS
    Retrieves group membership information for Active Directory groups.
 
    .DESCRIPTION
    Retrieves detailed information about group membership for specified Active Directory groups.
 
    .PARAMETER Identity
    Specifies the group identities for which to retrieve membership information.
 
    .PARAMETER AddSelf
    Indicates whether to include the group itself in the membership results.
 
    .PARAMETER ClearCache
    Indicates whether to clear the group object cache before processing.
 
    .EXAMPLE
    Get-WinADGroupMemberOf -Identity "Group1", "Group2" -AddSelf
    This example retrieves membership information for "Group1" and "Group2", including the groups themselves.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [CmdletBinding()]
    param(
        [parameter(Position = 0, Mandatory)][Array] $Identity,
        [switch] $AddSelf,
        [switch] $ClearCache,
        [Parameter(DontShow)][int] $Nesting = -1,
        [Parameter(DontShow)][System.Collections.Generic.List[object]] $CollectedGroups,
        [Parameter(DontShow)][System.Object] $Circular,
        [Parameter(DontShow)][System.Collections.IDictionary] $InitialObject,
        [Parameter(DontShow)][switch] $Nested
    )
    Begin {
        if (-not $Script:WinADGroupObjectCache -or $ClearCache) {
            $Script:WinADGroupObjectCache = @{}
        }
    }
    Process {
        [Array] $Output = foreach ($MyObject in $Identity) {
            $Object = Get-WinADObject -Identity $MyObject
            Write-Verbose "Get-WinADGroupMemberOf - starting $($Object.Name)/$($Object.DomainName)"
            if (-not $Nested.IsPresent) {
                $InitialObject = [ordered] @{
                    ObjectName           = $Object.Name
                    ObjectSamAccountName = $Object.SamAccountName
                    Name                 = $Object.Name
                    SamAccountName       = $Object.SamAccountName
                    DomainName           = $Object.DomainName
                    DisplayName          = $Object.DisplayName
                    Enabled              = $Object.Enabled
                    Type                 = $Object.ObjectClass
                    GroupType            = $Object.GroupType
                    GroupScope           = $Object.GroupScope
                    Nesting              = $Nesting
                    CircularDirect       = $false
                    CircularIndirect     = $false
                    #CrossForest = $false
                    ParentGroup          = ''
                    ParentGroupDomain    = ''
                    ParentGroupDN        = ''
                    ObjectDomainName     = $Object.DomainName
                    DistinguishedName    = $Object.Distinguishedname
                    Sid                  = $Object.ObjectSID
                }
                $CollectedGroups = [System.Collections.Generic.List[string]]::new()
                $Nesting = -1
            }

            $Nesting++

            if ($Object) {
                # Lets cache our object
                $Script:WinADGroupObjectCache[$Object.DistinguishedName] = $Object
                if ($Circular -or $CollectedGroups -contains $Object.DistinguishedName) {
                    [Array] $NestedMembers = foreach ($MyIdentity in $Object.MemberOf) {
                        if ($Script:WinADGroupObjectCache[$MyIdentity]) {
                            $Script:WinADGroupObjectCache[$MyIdentity]
                        } else {
                            Write-Verbose "Get-WinADGroupMemberOf - Requesting more data on $MyIdentity (Circular: $true)"
                            $ADObject = Get-WinADObject -Identity $MyIdentity
                            $Script:WinADGroupObjectCache[$MyIdentity] = $ADObject
                            $Script:WinADGroupObjectCache[$MyIdentity]
                        }
                    }
                    [Array] $NestedMembers = foreach ($Member in $NestedMembers) {
                        if ($CollectedGroups -notcontains $Member.DistinguishedName) {
                            $Member
                        }
                    }
                    $Circular = $null
                } else {
                    [Array] $NestedMembers = foreach ($MyIdentity in $Object.MemberOf) {
                        if ($Script:WinADGroupObjectCache[$MyIdentity]) {
                            $Script:WinADGroupObjectCache[$MyIdentity]
                        } else {
                            Write-Verbose "Get-WinADGroupMemberOf - Requesting more data on $MyIdentity (Circular: $false)"
                            $ADObject = Get-WinADObject -Identity $MyIdentity
                            $Script:WinADGroupObjectCache[$MyIdentity] = $ADObject
                            $Script:WinADGroupObjectCache[$MyIdentity]
                        }
                    }
                }
                foreach ($NestedMember in $NestedMembers) {
                    Write-Verbose "Get-WinADGroupMemberOf - processing $($InitialObject.ObjectName) nested member $($NestedMember.SamAccountName)"
                    #$DomainParentGroup = ConvertFrom-DistinguishedName -DistinguishedName $Object.DistinguishedName -ToDomainCN
                    $CreatedObject = [ordered] @{
                        ObjectName           = $InitialObject.ObjectName
                        ObjectSamAccountName = $InitialObject.SamAccountName
                        Name                 = $NestedMember.name
                        SamAccountName       = $NestedMember.SamAccountName
                        DomainName           = $NestedMember.DomainName
                        DisplayName          = $NestedMember.DisplayName
                        Enabled              = $NestedMember.Enabled
                        Type                 = $NestedMember.ObjectClass
                        GroupType            = $NestedMember.GroupType
                        GroupScope           = $NestedMember.GroupScope
                        Nesting              = $Nesting
                        CircularDirect       = $false
                        CircularIndirect     = $false
                        #CrossForest = $false
                        ParentGroup          = $Object.name
                        ParentGroupDomain    = $Object.DomainName
                        ParentGroupDN        = $Object.DistinguishedName
                        ObjectDomainName     = $InitialObject.DomainName
                        DistinguishedName    = $NestedMember.DistinguishedName
                        Sid                  = $NestedMember.ObjectSID
                    }
                    #if ($NestedMember.DomainName -notin $Script:WinADForestCache['Domains']) {
                    # $CreatedObject['CrossForest'] = $true
                    #}
                    if ($NestedMember.ObjectClass -eq "group") {
                        if ($Object.members -contains $NestedMember.DistinguishedName) {
                            $Circular = $Object.DistinguishedName
                            $CreatedObject['CircularDirect'] = $true
                        }
                        $CollectedGroups.Add($Object.DistinguishedName)
                        if ($CollectedGroups -contains $NestedMember.DistinguishedName) {
                            $CreatedObject['CircularIndirect'] = $true
                        }

                        [PSCustomObject] $CreatedObject
                        Write-Verbose "Get-WinADGroupMemberOf - Going deeper with $($NestedMember.SamAccountName)"
                        try {
                            $OutputFromGroup = Get-WinADGroupMemberOf -Identity $NestedMember -Nesting $Nesting -Circular $Circular -InitialObject $InitialObject -CollectedGroups $CollectedGroups -Nested
                        } catch {
                            Write-Warning "Get-WinADGroupMemberOf - Going deeper with $($NestedMember.SamAccountName) failed $($_.Exception.Message)"
                        }
                        $OutputFromGroup
                    } else {
                        [PSCustomObject] $CreatedObject
                    }
                }
            }
        }
    }
    End {
        if ($Output.Count -gt 0) {
            if ($Nesting -eq 0) {
                if ($AddSelf) {
                    [PSCustomObject] $InitialObject
                }
                foreach ($MyObject in $Output) {
                    $MyObject
                }
            } else {
                # this is nested call so we want to get whatever it gives us
                $Output
            }
        }
    }
}
function Get-WinADGroups {
    <#
    .SYNOPSIS
    Retrieves Active Directory groups information for a specified forest.
 
    .DESCRIPTION
    Retrieves detailed information about Active Directory groups within the specified forest.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve group information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the search.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the search.
 
    .PARAMETER PerDomain
    Indicates whether to retrieve group information per domain.
 
    .PARAMETER AddOwner
    Indicates whether to include group owner information in the retrieval.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [switch] $PerDomain,
        [switch] $AddOwner
    )
    $AllUsers = [ordered] @{}
    $AllContacts = [ordered] @{}
    $AllGroups = [ordered] @{}
    $CacheUsersReport = [ordered] @{}
    $Today = Get-Date
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]

        $Properties = @(
            'DistinguishedName', 'mail', 'LastLogonDate', 'PasswordLastSet', 'DisplayName', 'Manager', 'SamAccountName', 'ObjectSID'
            #'Description',
            #'PasswordNeverExpires', 'PasswordNotRequired', 'PasswordExpired', 'UserPrincipalName', 'SamAccountName', 'CannotChangePassword',
            #'TrustedForDelegation', 'TrustedToAuthForDelegation', 'msExchMailboxGuid', 'msExchRemoteRecipientType', 'msExchRecipientTypeDetails',
            # 'msExchRecipientDisplayType', 'pwdLastSet', "msDS-UserPasswordExpiryTimeComputed",
            # 'WhenCreated', 'WhenChanged'
        )

        $AllUsers[$Domain] = Get-ADUser -Filter "*" -Properties $Properties -Server $QueryServer #$ForestInformation['QueryServers'][$Domain].HostName[0]
        $AllContacts[$Domain] = Get-ADObject -Filter 'objectClass -eq "contact"' -Properties SamAccountName, Mail, Name, DistinguishedName, WhenChanged, Whencreated, DisplayName, ObjectSID -Server $QueryServer

        $Properties = @(
            'SamAccountName', 'msExchRecipientDisplayType', 'msExchRecipientTypeDetails', 'CanonicalName', 'Mail', 'Description', 'Name',
            'GroupScope', 'GroupCategory', 'DistinguishedName', 'isCriticalSystemObject', 'adminCount', 'WhenChanged', 'Whencreated', 'DisplayName',
            'ManagedBy', 'member', 'memberof', 'ProtectedFromAccidentalDeletion', 'nTSecurityDescriptor', 'groupType'
            'SID', 'SIDHistory', 'proxyaddresses', 'ObjectSID'
        )
        $AllGroups[$Domain] = Get-ADGroup -Filter "*" -Properties $Properties -Server $QueryServer
    }

    foreach ($Domain in $AllUsers.Keys) {
        foreach ($U in $AllUsers[$Domain]) {
            $CacheUsersReport[$U.DistinguishedName] = $U
        }
    }
    foreach ($Domain in $AllContacts.Keys) {
        foreach ($C in $AllContacts[$Domain]) {
            $CacheUsersReport[$C.DistinguishedName] = $C
        }
    }
    foreach ($Domain in $AllGroups.Keys) {
        foreach ($G in $AllGroups[$Domain]) {
            $CacheUsersReport[$G.DistinguishedName] = $G
        }
    }

    $Output = [ordered] @{}
    foreach ($Domain in $ForestInformation.Domains) {
        $Output[$Domain] = foreach ($Group in $AllGroups[$Domain]) {
            $UserLocation = ($Group.DistinguishedName -split ',').Replace('OU=', '').Replace('CN=', '').Replace('DC=', '')
            $Region = $UserLocation[-4]
            $Country = $UserLocation[-5]
            if ($Group.ManagedBy) {
                $ManagerAll = $CacheUsersReport[$Group.ManagedBy]
                $Manager = $CacheUsersReport[$Group.ManagedBy].Name
                $ManagerSamAccountName = $CacheUsersReport[$Group.ManagedBy].SamAccountName
                $ManagerEmail = $CacheUsersReport[$Group.ManagedBy].Mail
                $ManagerEnabled = $CacheUsersReport[$Group.ManagedBy].Enabled
                $ManagerLastLogon = $CacheUsersReport[$Group.ManagedBy].LastLogonDate
                if ($ManagerLastLogon) {
                    $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days)
                } else {
                    $ManagerLastLogonDays = $null
                }
                $ManagerStatus = if ($ManagerEnabled -eq $true) {
                    'Enabled' 
                } elseif ($ManagerEnabled -eq $false) {
                    'Disabled' 
                } else {
                    'Not available' 
                }
            } else {
                $ManagerAll = $null
                if ($Group.ObjectClass -eq 'user') {
                    $ManagerStatus = 'Missing'
                } else {
                    $ManagerStatus = 'Not available'
                }
                $Manager = $null
                $ManagerSamAccountName = $null
                $ManagerEmail = $null
                $ManagerEnabled = $null
                $ManagerLastLogon = $null
                $ManagerLastLogonDays = $null
            }
            $msExchRecipientTypeDetails = Convert-ExchangeRecipient -msExchRecipientTypeDetails $Group.msExchRecipientTypeDetails
            $msExchRecipientDisplayType = Convert-ExchangeRecipient -msExchRecipientDisplayType $Group.msExchRecipientDisplayType
            #$msExchRemoteRecipientType = Convert-ExchangeRecipient -msExchRemoteRecipientType $Group.msExchRemoteRecipientType
            if ($ManagerAll.ObjectSID) {
                $ACL = Get-ADACL -ADObject $Group -Resolve -Principal $ManagerAll.ObjectSID -IncludeObjectTypeName 'Self-Membership' -IncludeActiveDirectoryRights WriteProperty
            } else {
                $ACL = $null
            }

            # $GroupWriteback = $false
            # # https://practical365.com/azure-ad-connect-group-writeback-deep-dive/
            # if ($Group.msExchRecipientDisplayType -eq 17) {
            # # M365 Security Group and M365 Mail-Enabled security Group
            # $GroupWriteback = $true
            # } else {
            # # if ($Group.GroupType -eq -2147483640 -and $Group.GroupCategory -eq 'Security' -and $Group.GroupScope -eq 'Universal') {
            # # $GroupWriteback = $true
            # # } else {
            # # $GroupWriteback = $false
            # # }
            # }
            if ($AddOwner) {
                $Owner = Get-ADACLOwner -ADObject $Group -Verbose -Resolve
                [PSCustomObject] @{
                    Name                            = $Group.Name
                    #DisplayName = $Group.DisplayName
                    CanonicalName                   = $Group.CanonicalName
                    Domain                          = $Domain
                    SamAccountName                  = $Group.SamAccountName
                    MemberCount                     = if ($Group.member) {
                        $Group.member.Count 
                    } else {
                        0 
                    }
                    GroupScope                      = $Group.GroupScope
                    GroupCategory                   = $Group.GroupCategory
                    #GroupWriteBack = $GroupWriteBack
                    #ManagedBy = $Group.ManagedBy
                    msExchRecipientTypeDetails      = $msExchRecipientTypeDetails
                    msExchRecipientDisplayType      = $msExchRecipientDisplayType
                    #msExchRemoteRecipientType = $msExchRemoteRecipientType
                    Manager                         = $Manager
                    ManagerCanUpdateGroupMembership = if ($ACL) {
                        $true 
                    } else {
                        $false 
                    }
                    ManagerSamAccountName           = $ManagerSamAccountName
                    ManagerEmail                    = $ManagerEmail
                    ManagerEnabled                  = $ManagerEnabled
                    ManagerLastLogon                = $ManagerLastLogon
                    ManagerLastLogonDays            = $ManagerLastLogonDays
                    ManagerStatus                   = $ManagerStatus
                    OwnerName                       = $Owner.OwnerName
                    OwnerSID                        = $Owner.OwnerSID
                    OwnerType                       = $Owner.OwnerType
                    WhenCreated                     = $Group.WhenCreated
                    WhenChanged                     = $Group.WhenChanged
                    ProtectedFromAccidentalDeletion = $Group.ProtectedFromAccidentalDeletion
                    ProxyAddresses                  = Convert-ExchangeEmail -Emails $Group.ProxyAddresses -RemoveDuplicates -RemovePrefix
                    Description                     = $Group.Description
                    DistinguishedName               = $Group.DistinguishedName
                    Level0                          = $Region
                    Level1                          = $Country
                    ManagerDN                       = $Group.ManagedBy
                }
            } else {
                [PSCustomObject] @{
                    Name                            = $Group.Name
                    #DisplayName = $Group.DisplayName
                    CanonicalName                   = $Group.CanonicalName
                    Domain                          = $Domain
                    SamAccountName                  = $Group.SamAccountName
                    MemberCount                     = if ($Group.member) {
                        $Group.member.Count 
                    } else {
                        0 
                    }
                    GroupScope                      = $Group.GroupScope
                    GroupCategory                   = $Group.GroupCategory
                    #GroupWriteBack = $GroupWriteBack
                    #ManagedBy = $Group.ManagedBy
                    msExchRecipientTypeDetails      = $msExchRecipientTypeDetails
                    msExchRecipientDisplayType      = $msExchRecipientDisplayType
                    #msExchRemoteRecipientType = $msExchRemoteRecipientType
                    Manager                         = $Manager
                    ManagerCanUpdateGroupMembership = if ($ACL) {
                        $true 
                    } else {
                        $false 
                    }
                    ManagerSamAccountName           = $ManagerSamAccountName
                    ManagerEmail                    = $ManagerEmail
                    ManagerEnabled                  = $ManagerEnabled
                    ManagerLastLogon                = $ManagerLastLogon
                    ManagerLastLogonDays            = $ManagerLastLogonDays
                    ManagerStatus                   = $ManagerStatus
                    WhenCreated                     = $Group.WhenCreated
                    WhenChanged                     = $Group.WhenChanged
                    ProtectedFromAccidentalDeletion = $Group.ProtectedFromAccidentalDeletion
                    ProxyAddresses                  = Convert-ExchangeEmail -Emails $Group.ProxyAddresses -RemoveDuplicates -RemovePrefix
                    Description                     = $Group.Description
                    DistinguishedName               = $Group.DistinguishedName
                    Level0                          = $Region
                    Level1                          = $Country
                    ManagerDN                       = $Group.ManagedBy
                }
            }
        }
    }
    if ($PerDomain) {
        $Output
    } else {
        $Output.Values
    }
}
function Get-WinADKerberosAccount {
    <#
    .SYNOPSIS
    Retrieves Kerberos account information for Active Directory.
 
    .DESCRIPTION
    Retrieves Kerberos account information for Active Directory based on specified parameters.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve Kerberos account information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the search.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the search.
 
    .PARAMETER IncludeCriticalAccounts
    Indicates whether to include critical Kerberos accounts in the retrieval.
 
    .EXAMPLE
    Get-WinADKerberosAccount -Forest "example.com" -IncludeDomains "example.com" -IncludeCriticalAccounts
    This example retrieves Kerberos account information for the "example.com" forest, including only the specified domains and critical accounts.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [switch] $IncludeCriticalAccounts
    )
    $Today = Get-Date
    $Accounts = [ordered] @{
        'CriticalAccounts' = [ordered] @{}
        'Data'             = [ordered] @{}
    }
    Write-Verbose -Message "Get-WinADKerberosAccount - Gathering information about forest"
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -PreferWritable
    foreach ($Domain in $ForestInformation.Domains) {
        $Accounts['Data']["$Domain"] = [ordered] @{}
    }
    $DomainCount = 0
    $DomainCountTotal = $ForestInformation.Domains.Count
    foreach ($Domain in $ForestInformation.Domains) {
        $DomainCount++
        $ProcessingText = "[Domain: $DomainCount/$DomainCountTotal]"
        Write-Verbose -Message "Get-WinADKerberosAccount - $ProcessingText Processing domain $Domain"
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]

        $Properties = @(
            'Name', 'SamAccountName', 'msDS-KrbTgtLinkBl',
            'Enabled',
            'PasswordLastSet', 'WhenCreated', 'WhenChanged'
            'AllowReversiblePasswordEncryption', 'BadLogonCount', 'AccountNotDelegated'
            'SID', 'SIDHistory'
        )
        $PropertiesMembers = @(
            'Name', 'SamAccountName'
            'Enabled',
            'PasswordLastSet', 'WhenCreated', 'WhenChanged'
            'AllowReversiblePasswordEncryption', 'BadLogonCount', 'AccountNotDelegated'
            'SID', 'SIDHistory'
        )

        $CountK = 0
        try {
            [Array] $KerberosPasswords = Get-ADUser -Filter "Name -like 'krbtgt*'" -Server $QueryServer -Properties $Properties -ErrorAction Stop
        } catch {
            Write-Warning -Message "Get-WinADKerberosAccount - $ProcessingText Processing domain $Domain - unable to get Kerberos accounts. Error: $($_.Exception.Message)"
            continue
        }

        if ($IncludeCriticalAccounts) {
            $Members = @(
                try {
                    Get-ADGroupMember -Identity 'Domain Admins' -Server $QueryServer -Recursive -ErrorAction Stop
                } catch {
                    Write-Warning -Message "Get-WinADKerberosAccount - $ProcessingText Processing domain $Domain - unable to get Domain Admins. Error: $($_.Exception.Message)"
                }
                try {
                    Get-ADGroupMember -Identity 'Enterprise Admins' -Server $QueryServer -Recursive -ErrorAction Stop
                } catch {
                    Write-Warning -Message "Get-WinADKerberosAccount - $ProcessingText Processing domain $Domain - unable to get Enterprise Admins. Error: $($_.Exception.Message)"
                }
            ) | Sort-Object -Unique -Property DistinguishedName
        } else {
            $Members = @()
        }
        $CriticalAccounts = foreach ($Member in $Members) {
            Try {
                $User = Get-ADUser -Identity $Member.DistinguishedName -Server $QueryServer -Properties $PropertiesMembers -ErrorAction Stop
            } Catch {
                Write-Warning -Message "Get-WinADKerberosAccount - $ProcessingText Processing domain $Domain - unable to get critical account $($Member.DistinguishedName). Error: $($_.Exception.Message)"
            }
            if ($User) {
                if ($null -eq $User.WhenChanged) {
                    $WhenChangedDaysAgo = $null
                } else {
                    $WhenChangedDaysAgo = ($Today) - $User.WhenChanged
                }
                if ($null -eq $User.PasswordLastSet) {
                    $PasswordLastSetAgo = $null
                } else {
                    $PasswordLastSetAgo = ($Today) - $User.PasswordLastSet
                }

                [PSCustomObject] @{
                    'Name'                              = $User.Name
                    'SamAccountName'                    = $User.SamAccountName
                    'Enabled'                           = $User.Enabled
                    'PasswordLastSet'                   = $User.PasswordLastSet
                    'PasswordLastSetDays'               = $PasswordLastSetAgo.Days
                    'WhenChangedDays'                   = $WhenChangedDaysAgo.Days
                    'WhenChanged'                       = $User.WhenChanged
                    'WhenCreated'                       = $User.WhenCreated
                    'AllowReversiblePasswordEncryption' = $User.AllowReversiblePasswordEncryption
                    'BadLogonCount'                     = $User.BadLogonCount
                    'AccountNotDelegated'               = $User.AccountNotDelegated
                    'SID'                               = $User.SID
                    'SIDHistory'                        = $User.SIDHistory
                }
            }
        }

        foreach ($Account in $KerberosPasswords) {
            $CountK++
            $ProcessingText = "[Domain: $DomainCount/$DomainCountTotal / Account: $CountK/$($KerberosPasswords.Count)]"
            Write-Verbose -Message "Get-WinADKerberosAccount - $ProcessingText Processing domain $Domain \ Kerberos account ($CountK/$($KerberosPasswords.Count)) $($Account.SamAccountName) \ DC"

            #if ($Account.SamAccountName -like "*_*" -and -not $Account.'msDS-KrbTgtLinkBl') {
            # Write-Warning -Message "Get-WinADKerberosAccount - Processing domain $Domain \ Kerberos account $($Account.SamAccountName) \ DC - Skipping"
            # continue
            #}

            $CachedServers = [ordered] @{}
            $CountDC = 0
            $CountDCTotal = $ForestInformation.DomainDomainControllers[$Domain].Count
            foreach ($DC in $ForestInformation.DomainDomainControllers[$Domain]) {
                $CountDC++
                $Server = $DC.HostName
                $ProcessingText = "[Domain: $DomainCount/$DomainCountTotal / Account: $CountK/$($KerberosPasswords.Count), DC: $CountDC/$CountDCTotal]"
                Write-Verbose -Message "Get-WinADKerberosAccount - $ProcessingText Processing domain $Domain \ Kerberos account $($Account.SamAccountName) \ DC Server $Server"
                try {
                    $ServerData = Get-ADUser -Identity $Account.DistinguishedName -Server $Server -Properties 'msDS-KrbTgtLinkBl', 'PasswordLastSet', 'WhenCreated', 'WhenChanged' -ErrorAction Stop
                } catch {
                    Write-Warning -Message "Get-WinADKerberosAccount - Processing domain $Domain $ProcessingText \ Kerberos account $($Account.SamAccountName) \ DC Server $Server - Error: $($_.Exception.Message)"
                    $CachedServers[$Server] = [PSCustomObject] @{
                        'Server'              = $Server
                        'Name'                = $Server
                        'PasswordLastSet'     = $null
                        'PasswordLastSetDays' = $null
                        'WhenChangedDays'     = $null
                        'WhenChanged'         = $null
                        'WhenCreated'         = $null
                        'msDS-KrbTgtLinkBl'   = $ServerData.'msDS-KrbTgtLinkBl'
                        'Status'              = $_.Exception.Message
                    }
                }
                if ($ServerData.Name) {
                    if ($null -eq $ServerData.WhenChanged) {
                        $WhenChangedDaysAgo = $null
                    } else {
                        $WhenChangedDaysAgo = ($Today) - $ServerData.WhenChanged
                    }
                    if ($null -eq $ServerData.PasswordLastSet) {
                        $PasswordLastSetAgo = $null
                    } else {
                        $PasswordLastSetAgo = ($Today) - $ServerData.PasswordLastSet
                    }
                    if ($Account.SamAccountName -like "*_*" -and $ServerData.'msDS-KrbTgtLinkBl') {
                        $Status = 'OK'
                    } elseif ($Account.SamAccountName -like "*_*" -and -not $ServerData.'msDS-KrbTgtLinkBl') {
                        $Status = 'Missing link, orphaned?'
                    } else {
                        $Status = 'OK'
                    }

                    $CachedServers[$Server] = [PSCustomObject] @{
                        'Server'              = $Server
                        'Name'                = $ServerData.Name
                        'PasswordLastSet'     = $ServerData.'PasswordLastSet'
                        'PasswordLastSetDays' = $PasswordLastSetAgo.Days
                        'WhenChangedDays'     = $WhenChangedDaysAgo.Days
                        'WhenChanged'         = $ServerData.'WhenChanged'
                        'WhenCreated'         = $ServerData.'WhenCreated'
                        'msDS-KrbTgtLinkBl'   = $ServerData.'msDS-KrbTgtLinkBl'
                        'Status'              = $Status
                    }
                }
            }

            Write-Verbose -Message "Get-WinADKerberosAccount - Gathering information about forest for Global Catalogs"
            $ForestInformationGC = Get-WinADForestDetails -Forest $Forest
            $ProcessingText = "[Domain: $DomainCount/$DomainCountTotal / Account: $CountK/$($KerberosPasswords.Count)]"
            Write-Verbose -Message "Get-WinADKerberosAccount - $ProcessingText Processing domain $Domain \ Kerberos account $($Account.SamAccountName) \ GC"
            $GlobalCatalogs = [ordered] @{}
            $GlobalCatalogCount = 0
            $GlobalCatalogCountTotal = $ForestInformationGC.ForestDomainControllers.Count
            foreach ($DC in $ForestInformationGC.ForestDomainControllers) {
                $GlobalCatalogCount++

                $Server = $DC.HostName
                $ProcessingText = "[Domain: $DomainCount/$DomainCountTotal / Account: $CountK/$($KerberosPasswords.Count), GC: $GlobalCatalogCount/$GlobalCatalogCountTotal]"
                Write-Verbose -Message "Get-WinADKerberosAccount - $ProcessingText Processing domain $Domain \ Kerberos account $($Account.SamAccountName) \ GC Server $Server"

                if ($DC.IsGlobalCatalog ) {
                    try {
                        $ServerData = Get-ADUser -Identity $Account.DistinguishedName -Server "$($Server):3268" -Properties 'msDS-KrbTgtLinkBl', 'PasswordLastSet', 'WhenCreated', 'WhenChanged' -ErrorAction Stop
                    } catch {
                        Write-Warning -Message "Get-WinADKerberosAccount - Processing domain $Domain $ProcessingText \ Kerberos account $($Account.SamAccountName) \ GC Server $Server - Error: $($_.Exception.Message)"
                        $GlobalCatalogs[$Server] = [PSCustomObject] @{
                            'Server'              = $Server
                            'Name'                = $Server
                            'PasswordLastSet'     = $null
                            'PasswordLastSetDays' = $null
                            'WhenChangedDays'     = $null
                            'WhenChanged'         = $null
                            'WhenCreated'         = $null
                            'msDS-KrbTgtLinkBl'   = $null
                            'Status'              = $_.Exception.Message
                        }
                    }

                    if ($ServerData.Name) {
                        if ($null -eq $ServerData.WhenChanged) {
                            $WhenChangedDaysAgo = $null
                        } else {
                            $WhenChangedDaysAgo = ($Today) - $ServerData.WhenChanged
                        }
                        if ($null -eq $ServerData.PasswordLastSet) {
                            $PasswordLastSetAgo = $null
                        } else {
                            $PasswordLastSetAgo = ($Today) - $ServerData.PasswordLastSet
                        }
                        $GlobalCatalogs[$Server] = [PSCustomObject] @{
                            'Server'              = $Server
                            'Name'                = $ServerData.Name
                            'PasswordLastSet'     = $ServerData.'PasswordLastSet'
                            'PasswordLastSetDays' = $PasswordLastSetAgo.Days
                            'WhenChangedDays'     = $WhenChangedDaysAgo.Days
                            'WhenChanged'         = $ServerData.'WhenChanged'
                            'WhenCreated'         = $ServerData.'WhenCreated'
                            'msDS-KrbTgtLinkBl'   = $ServerData.'msDS-KrbTgtLinkBl'
                            'Status'              = 'OK'
                        }
                    }
                }
            }

            if ($null -eq $Account.PasswordLastSet) {
                $PasswordLastSetAgo = $null
            } else {
                $PasswordLastSetAgo = ($Today) - $Account.PasswordLastSet
            }
            if ($null -eq $Account.WhenChanged) {
                $WhenChangedDaysAgo = $null
            } else {
                $WhenChangedDaysAgo = ($Today) - $Account.WhenChanged
            }
            $Accounts['Data']["$Domain"][$Account.SamAccountName] = @{
                FullInformation   = [PSCustomObject] @{
                    'Name'                              = $Account.Name
                    'SamAccountName'                    = $Account.SamAccountName
                    'Enabled'                           = $Account.Enabled
                    'PasswordLastSet'                   = $Account.PasswordLastSet
                    'PasswordLastSetDays'               = $PasswordLastSetAgo.Days
                    'WhenChangedDays'                   = $WhenChangedDaysAgo.Days
                    'WhenChanged'                       = $Account.WhenChanged
                    'WhenCreated'                       = $Account.WhenCreated
                    'AllowReversiblePasswordEncryption' = $Account.AllowReversiblePasswordEncryption
                    'BadLogonCount'                     = $Account.BadLogonCount
                    'AccountNotDelegated'               = $Account.AccountNotDelegated
                    'SID'                               = $Account.SID
                    'SIDHistory'                        = $Account.SIDHistory
                }
                DomainControllers = $CachedServers
                GlobalCatalogs    = $GlobalCatalogs
            }
        }
        $Accounts['CriticalAccounts']["$Domain"] = $CriticalAccounts
    }
    $Accounts
}
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(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $NameUsed = [System.Collections.Generic.List[string]]::new()
    [DateTime] $CurrentDate = Get-Date

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        try {
            [string[]]$Partitions = (Get-ADRootDSE -Server $QueryServer -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")
            try {
                $LastBackup = [DateTime] $($dsaSignature.LastOriginatingChangeTime)
            } catch {
                $LastBackup = [DateTime]::MinValue
            }
            [PSCustomObject] @{
                Domain            = $Domain
                NamingContext     = $Name
                LastBackup        = $LastBackup
                LastBackupDaysAgo = - (Convert-TimeToDays -StartTime ($CurrentDate) -EndTime ($LastBackup))
            }
        }
        $Output
    }
}
function Get-WinADLDAPBindingsSummary {
    <#
    .SYNOPSIS
    Retrieves LDAP binding summary information for Active Directory.
 
    .DESCRIPTION
    Retrieves LDAP binding summary information for Active Directory based on specified parameters.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve LDAP binding information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the search.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controllers to exclude from the search.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the search.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controllers to include in the search.
 
    .PARAMETER SkipRODC
    Skips Read-Only Domain Controllers. By default, all domain controllers are included.
 
    .PARAMETER Days
    Specifies the number of days to consider for retrieving LDAP binding information. Default is 1 day.
 
    .PARAMETER ExtendedForestInformation
    A dictionary object that contains additional information about the forest. This parameter is optional and can be used to provide more context about the forest.
 
    .EXAMPLE
    Get-WinADLdapBindingsSummary -Forest "example.com" -IncludeDomains "example.com" -Days 7
    This example retrieves LDAP binding summary information for the "example.com" forest, including only the specified domains and considering the last 7 days.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [int] $Days = 1,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    $Events = Get-Events -LogName 'Directory Service' -ID 2887 -Machine $ForestInformation.ForestDomainControllers.HostName -DateFrom ((Get-Date).Date.adddays(-$Days))
    foreach ($Event in $Events) {
        [PSCustomobject] @{
            'Domain Controller'                                                        = $Event.Computer
            'Date'                                                                     = $Event.Date
            'Number of simple binds performed without SSL/TLS'                         = $Event.'NoNameA0'
            'Number of Negotiate/Kerberos/NTLM/Digest binds performed without signing' = $Event.'NoNameA1'
            'GatheredFrom'                                                             = $Event.'GatheredFrom'
            'GatheredLogName'                                                          = $Event.'GatheredLogName'
        }
    }
}
function Get-WinADLMSettings {
    <#
    .SYNOPSIS
    Retrieves and displays Active Directory LM settings for Windows Clients and Servers.
 
    .DESCRIPTION
    Retrieves and displays Active Directory LM settings for Windows Clients and Servers. By default, it scans all Domain Controllers in a forest.
 
    .PARAMETER ForestName
    Specifies the target forest to retrieve LM settings from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the search.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controllers to exclude from the search.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the search.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controllers to include in the search.
 
    .PARAMETER SkipRODC
    Skips Read-Only Domain Controllers. By default, all domain controllers are included.
 
    .PARAMETER Days
    Specifies the number of days to consider for retrieving LM settings.
 
    .PARAMETER ExtendedForestInformation
    A dictionary object that contains additional information about the forest. This parameter is optional and can be used to provide more context about the forest.
 
    .EXAMPLE
    Get-WinADLMSettings -ForestName "example.com" -IncludeDomains "example.com" -Days 7
    This example retrieves LM settings for the "example.com" forest, including only the specified domains and considering the last 7 days.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'DomainController')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [int] $Days = 1,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    foreach ($ComputerName in $ForestInformation.ForestDomainControllers.HostName) {
        $LSA = Get-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Control\Lsa' -ComputerName $ComputerName

        <#
        auditbasedirectories : 0
        auditbaseobjects : 0
        Bounds : {0, 48, 0, 0...}
        crashonauditfail : 0
        fullprivilegeauditing : {0}
        LimitBlankPasswordUse : 1
        NoLmHash : 1
        disabledomaincreds : 0
        everyoneincludesanonymous : 0
        forceguest : 0
        LsaCfgFlagsDefault : 0
        LsaPid : 1232
        ProductType : 4
        restrictanonymous : 0
        restrictanonymoussam : 1
        SecureBoot : 1
        ComputerName :
        #>



        if ($Lsa -and $Lsa.PSError -eq $false) {
            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] @{
                ComputerName              = $ComputerName
                LSAProtectionCredentials  = [bool] $LSA.RunAsPPL # https://docs.microsoft.com/en-us/windows-server/security/credentials-protection-and-management/configuring-additional-lsa-protection
                Level                     = $LMCompatibilityLevel
                LevelDescription          = $LM[$LMCompatibilityLevel]
                EveryoneIncludesAnonymous = [bool] $LSA.everyoneincludesanonymous
                LimitBlankPasswordUse     = [bool] $LSA.LimitBlankPasswordUse
                NoLmHash                  = [bool] $LSA.NoLmHash
                DisableDomainCreds        = [bool] $LSA.disabledomaincreds # https://www.stigviewer.com/stig/windows_8/2014-01-07/finding/V-3376
                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 # https://www.stigviewer.com/stig/windows_server_2012_member_server/2014-01-07/finding/V-14228 | Should be false
                CrashOnAuditFail          = $LSA.CrashOnAuditFail # http://systemmanager.ru/win2k_regestry.en/46686.htm | Should be 0
            }
        } else {
            [PSCustomObject] @{
                ComputerName              = $ComputerName
                LSAProtectionCredentials  = $null
                Level                     = $null
                LevelDescription          = $null
                EveryoneIncludesAnonymous = $null
                LimitBlankPasswordUse     = $null
                NoLmHash                  = $null
                DisableDomainCreds        = $null
                ForceGuest                = $null
                RestrictAnonymous         = $null
                RestrictAnonymousSAM      = $null
                SecureBoot                = $null
                LsaCfgFlagsDefault        = $null
                LSAPid                    = $null
                AuditBaseDirectories      = $null
                AuditBaseObjects          = $null
                CrashOnAuditFail          = $null
            }
        }
    }
}
function Get-WinADObject {
    <#
    .SYNOPSIS
    Gets Active Directory Object
 
    .DESCRIPTION
    Returns Active Directory Object (Computers, Groups, Users or ForeignSecurityPrincipal) using ADSI
 
    .PARAMETER Identity
    Identity of an object. It can be SamAccountName, SID, DistinguishedName or multiple other options
 
    .PARAMETER DomainName
    Choose domain name the objects resides in. This is optional for most objects
 
    .PARAMETER Credential
    Parameter description
 
    .PARAMETER IncludeGroupMembership
    Queries for group members when object is a group
 
    .PARAMETER IncludeAllTypes
    Allows functions to return all objects types and not only Computers, Groups, Users or ForeignSecurityPrincipal
 
    .EXAMPLE
    Get-WinADObject -Identity 'TEST\Domain Admins' -Verbose
    Get-WinADObject -Identity 'EVOTEC\Domain Admins' -Verbose
    Get-WinADObject -Identity 'Domain Admins' -DomainName 'DC=AD,DC=EVOTEC,DC=PL' -Verbose
    Get-WinADObject -Identity 'Domain Admins' -DomainName 'ad.evotec.pl' -Verbose
    Get-WinADObject -Identity 'CN=Domain Admins,CN=Users,DC=ad,DC=evotec,DC=pl'
    Get-WinADObject -Identity 'CN=Domain Admins,CN=Users,DC=ad,DC=evotec,DC=xyz'
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [Parameter(Mandatory, ValueFromPipeline, ValueFromPipelineByPropertyName, Position = 0)][Array] $Identity,
        [string] $DomainName,
        [pscredential] $Credential,
        [switch] $IncludeGroupMembership,
        [switch] $IncludeAllTypes,
        [switch] $AddType,
        [switch] $Cache,
        [string[]] $Properties
    )
    Begin {
        if ($Cache -and -not $Script:CacheObjectsWinADObject) {
            $Script:CacheObjectsWinADObject = @{}
        }
        # This is purely for calling group workaround
        Add-Type -AssemblyName System.DirectoryServices.AccountManagement

        $GroupTypes = @{
            '2'           = @{
                Name  = 'Distribution Group - Global' # distribution
                Type  = 'Distribution'
                Scope = 'Global'
            }
            '4'           = @{
                Name  = 'Distribution Group - Domain Local' # distribution
                Type  = 'Distribution'
                Scope = 'Domain local'
            }
            '8'           = @{
                Name  = 'Distribution Group - Universal'
                Type  = 'Distribution'
                Scope = 'Universal'
            }
            '-2147483640' = @{
                Name  = 'Security Group - Universal'
                Type  = 'Security'
                Scope = 'Universal'
            }
            '-2147483643' = @{
                Name  = 'Security Group - Builtin Local' # Builtin local Security Group
                Type  = 'Security'
                Scope = 'Builtin local'
            }
            '-2147483644' = @{
                Name  = 'Security Group - Domain Local'
                Type  = 'Security'
                Scope = 'Domain local'
            }
            '-2147483646' = @{
                Name  = 'Security Group - Global' # security
                Type  = 'Security'
                Scope = 'Global'
            }
        }
    }
    process {
        foreach ($Ident in $Identity) {
            if (-not $Ident) {
                Write-Warning -Message "Get-WinADObject - Identity is empty. Skipping"
                continue
            }
            $ResolvedIdentity = $null
            # If it's an object we need to make sure we pass only DN
            if ($Ident.DistinguishedName) {
                $Ident = $Ident.DistinguishedName
            }
            # we reset domain name to it's given value if at all
            $TemporaryName = $Ident
            $TemporaryDomainName = $DomainName

            # Since we change $Ident below to different names we need to be sure we use original query for cache
            if ($Cache -and $Script:CacheObjectsWinADObject[$TemporaryName]) {
                Write-Verbose "Get-WinADObject - Requesting $TemporaryName from Cache"
                $Script:CacheObjectsWinADObject[$TemporaryName]
                continue
            }
            # if Domain Name is provided we don't check for anything as it's most likely already good Ident value
            if (-not $TemporaryDomainName) {
                $MatchRegex = [Regex]::Matches($Ident, "S-\d-\d+-(\d+-|){1,14}\d+")
                if ($MatchRegex.Success) {
                    $ResolvedIdentity = ConvertFrom-SID -SID $MatchRegex.Value
                    $TemporaryDomainName = $ResolvedIdentity.DomainName
                    $Ident = $MatchRegex.Value
                } elseif ($Ident -like '*\*') {
                    $ResolvedIdentity = Convert-Identity -Identity $Ident -Verbose:$false
                    if ($ResolvedIdentity.SID) {
                        $TemporaryDomainName = $ResolvedIdentity.DomainName
                        $Ident = $ResolvedIdentity.SID
                    } else {
                        $NetbiosConversion = ConvertFrom-NetbiosName -Identity $Ident
                        if ($NetbiosConversion.DomainName) {
                            $TemporaryDomainName = $NetbiosConversion.DomainName
                            $Ident = $NetbiosConversion.Name
                        }
                    }
                } elseif ($Ident -like '*DC=*') {
                    $DNConversion = ConvertFrom-DistinguishedName -DistinguishedName $Ident -ToDomainCN
                    $TemporaryDomainName = $DNConversion
                } elseif ($Ident -like '*@*') {
                    $CNConversion = $Ident -split '@', 2
                    $TemporaryDomainName = $CNConversion[1]
                    $Ident = $CNConversion[0]
                } elseif ($Ident -like '*.*') {
                    $ResolvedIdentity = Convert-Identity -Identity $Ident -Verbose:$false
                    if ($ResolvedIdentity.SID) {
                        $TemporaryDomainName = $ResolvedIdentity.DomainName
                        $Ident = $ResolvedIdentity.SID
                    } else {
                        $CNConversion = $Ident -split '\.', 2
                        $Ident = $CNConversion[0]
                        $TemporaryDomainName = $CNConversion[1]
                    }
                } else {
                    $ResolvedIdentity = Convert-Identity -Identity $Ident -Verbose:$false
                    if ($ResolvedIdentity.SID) {
                        $TemporaryDomainName = $ResolvedIdentity.DomainName
                        $Ident = $ResolvedIdentity.SID
                    } else {
                        $NetbiosConversion = ConvertFrom-NetbiosName -Identity $Ident
                        if ($NetbiosConversion.DomainName) {
                            $TemporaryDomainName = $NetbiosConversion.DomainName
                            $Ident = $NetbiosConversion.Name
                        }
                    }
                }
            }


            # Building up ADSI call
            $Search = [System.DirectoryServices.DirectorySearcher]::new()
            #$Search.SizeLimit = $SizeLimit
            if ($TemporaryDomainName) {
                try {
                    $Context = [System.DirectoryServices.AccountManagement.PrincipalContext]::new('Domain', $TemporaryDomainName)
                } catch {
                    Write-Warning "Get-WinADObject - Building context failed ($TemporaryDomainName), error: $($_.Exception.Message)"
                }
            } else {
                try {
                    $Context = [System.DirectoryServices.AccountManagement.PrincipalContext]::new('Domain')
                } catch {
                    Write-Warning "Get-WinADObject - Building context failed, error: $($_.Exception.Message)"
                }
            }
            #Convert Identity Input String to HEX, if possible
            Try {
                $IdentityGUID = ""
                ([System.Guid]$Ident).ToByteArray() | ForEach-Object { $IdentityGUID += $("\{0:x2}" -f $_) }
            } Catch {
                $IdentityGUID = "null"
            }
            # Building search filter
            $Search.filter = "(|(DistinguishedName=$Ident)(Name=$Ident)(SamAccountName=$Ident)(UserPrincipalName=$Ident)(objectGUID=$IdentityGUID)(objectSid=$Ident))"

            if ($TemporaryDomainName) {
                $Search.SearchRoot = "LDAP://$TemporaryDomainName"
            }
            if ($PSBoundParameters['Credential']) {
                $Cred = [System.DirectoryServices.DirectoryEntry]::new("LDAP://$TemporaryDomainName", $($Credential.UserName), $($Credential.GetNetworkCredential().password))
                $Search.SearchRoot = $Cred
            }
            Write-Verbose "Get-WinADObject - Requesting $Ident ($TemporaryDomainName)"
            try {
                $SearchResults = $($Search.FindAll())
            } catch {
                if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                    throw "Get-WinADObject - Requesting $Ident ($TemporaryDomainName) failed. Error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
                } else {
                    Write-Warning "Get-WinADObject - Requesting $Ident ($TemporaryDomainName) failed. Error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
                    continue
                }
            }

            if ($SearchResults.Count -lt 1) {
                if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                    throw "Requesting $Ident ($TemporaryDomainName) failed with no results."
                }
            }

            foreach ($Object in $SearchResults) {
                $UAC = Convert-UserAccountControl -UserAccountControl ($Object.properties.useraccountcontrol -as [string])
                $ObjectClass = ($Object.properties.objectclass -as [array])[-1]
                if ($ObjectClass -notin 'group', 'contact', 'inetOrgPerson', 'computer', 'user', 'foreignSecurityPrincipal', 'msDS-ManagedServiceAccount', 'msDS-GroupManagedServiceAccount' -and (-not $IncludeAllTypes)) {
                    Write-Warning "Get-WinADObject - Unsupported object ($Ident) of type $ObjectClass. Only user,computer,group, foreignSecurityPrincipal, msDS-ManagedServiceAccount, msDS-GroupManagedServiceAccount are displayed by default. Use IncludeAllTypes switch to display all if nessecary."
                    continue
                }
                $Members = $Object.properties.member -as [array]
                if ($ObjectClass -eq 'group') {
                    # we only do this additional step when requested. It's not nessecary for day to day use but can hurt performance real bad for normal use cases
                    # This was especially visible for group with 50k members and Get-WinADObjectMember which doesn't even require this data
                    if ($IncludeGroupMembership) {
                        # This is weird case but for some reason $Object.properties.member doesn't always return all values
                        # the workaround is to do additional query for group and assing it

                        $GroupMembers = [System.DirectoryServices.AccountManagement.GroupPrincipal]::FindByIdentity($Context, $Ident).Members
                        try {
                            $Members = [System.Collections.Generic.List[string]]::new()
                            foreach ($Member in $Object.properties.member) {
                                if ($Member) {
                                    $Members.Add($Member)
                                }
                            }
                            foreach ($Member in $GroupMembers) {
                                if ($Member.DistinguishedName) {
                                    if ($Member.DistinguishedName -notin $Members) {
                                        $Members.Add($Member.DistinguishedName)
                                    }
                                } elseif ($Member.DisplayName) {
                                    $Members.Add($Member.DisplayName)
                                } else {
                                    $Members.Add($Member.Sid)
                                }
                            }
                        } catch {
                            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                                throw
                                return
                            } else {
                                Write-Warning -Message "Error while parsing group members for $($Ident): $($_.Exception.Message)"
                            }
                        }
                    }
                }
                $ObjectDomainName = ConvertFrom-DistinguishedName -DistinguishedName ($Object.properties.distinguishedname -as [string]) -ToDomainCN
                $DisplayName = $Object.properties.displayname -as [string]
                $SamAccountName = $Object.properties.samaccountname -as [string]
                $Name = $Object.properties.name -as [string]

                if ($ObjectClass -eq 'foreignSecurityPrincipal' -and $DisplayName -eq '') {
                    # If object is foreignSecurityPrincipal (which shouldn't happen at this point) we need to set it to temporary name we
                    # used before. Usually this is to fix 'NT AUTHORITY\INTERACTIVE'
                    # I have no clue if there's better way to do it
                    $DisplayName = $ResolvedIdentity.Name
                    if ($DisplayName -like '*\*') {
                        $NetbiosWithName = $DisplayName -split '\\'
                        if ($NetbiosWithName.Count -eq 2) {
                            #$NetbiosName = $NetbiosWithName[0]
                            $NetbiosUser = $NetbiosWithName[1]
                            $Name = $NetbiosUser
                            $SamAccountName = $NetbiosUser
                        } else {
                            $Name = $DisplayName
                        }
                    } else {
                        $Name = $DisplayName
                    }
                }

                $GroupType = $Object.properties.grouptype -as [string]
                if ($Object.Properties.objectsid) {
                    try {
                        $ObjectSID = [System.Security.Principal.SecurityIdentifier]::new($Object.Properties.objectsid[0], 0).Value
                    } catch {
                        Write-Warning "Get-WinADObject - Getting objectsid failed, error: $($_.Exception.Message)"
                        $ObjectSID = $null
                    }
                } else {
                    $ObjectSID = $null
                }

                $ReturnObject = [ordered] @{
                    DisplayName         = $DisplayName
                    Name                = $Name
                    SamAccountName      = $SamAccountName
                    ObjectClass         = $ObjectClass
                    Enabled             = if ($ObjectClass -in 'group', 'contact') {
                        $null 
                    } else {
                        $UAC -notcontains 'ACCOUNTDISABLE' 
                    }
                    PasswordNeverExpire = if ($ObjectClass -in 'group', 'contact') {
                        $null 
                    } else {
                        $UAC -contains 'DONT_EXPIRE_PASSWORD' 
                    }
                    DomainName          = $ObjectDomainName
                    Distinguishedname   = $Object.properties.distinguishedname -as [string]
                    #Adspath = $Object.properties.adspath -as [string]
                    WhenCreated         = $Object.properties.whencreated -as [string]
                    WhenChanged         = $Object.properties.whenchanged -as [string]
                    #Deleted = $Object.properties.isDeleted -as [string]
                    #Recycled = $Object.properties.isRecycled -as [string]
                    UserPrincipalName   = $Object.properties.userprincipalname -as [string]
                    ObjectSID           = $ObjectSID
                    MemberOf            = $Object.properties.memberof -as [array]
                    Members             = $Members
                    DirectReports       = $Object.Properties.directreports
                    GroupScopedType     = $GroupTypes[$GroupType].Name
                    GroupScope          = $GroupTypes[$GroupType].Scope
                    GroupType           = $GroupTypes[$GroupType].Type
                    #Administrative = if ($Object.properties.admincount -eq '1') { $true } else { $false }
                    #Type = $ResolvedIdentity.Type
                    Description         = $Object.properties.description -as [string]
                }

                if ($Properties -contains 'LastLogonDate') {
                    $LastLogon = [int64] $Object.properties.lastlogontimestamp[0]
                    if ($LastLogon -ne 9223372036854775807) {
                        $ReturnObject['LastLogonDate'] = [datetime]::FromFileTimeUtc($LastLogon)
                    } else {
                        $ReturnObject['LastLogonDate'] = $null
                    }
                }
                if ($Properties -contains 'PasswordLastSet') {
                    $PasswordLastSet = [int64] $Object.properties.pwdlastset[0]
                    if ($PasswordLastSet -ne 9223372036854775807) {
                        $ReturnObject['PasswordLastSet'] = [datetime]::FromFileTimeUtc($PasswordLastSet)
                    } else {
                        $ReturnObject['PasswordLastSet'] = $null
                    }
                }
                if ($Properties -contains 'AccountExpirationDate') {
                    $ExpirationDate = [int64] $Object.properties.accountexpires[0]
                    if ($ExpirationDate -ne 9223372036854775807) {
                        $ReturnObject['AccountExpirationDate'] = [datetime]::FromFileTimeUtc($ExpirationDate)
                    } else {
                        $ReturnObject['AccountExpirationDate'] = $null
                    }
                }

                if ($AddType) {
                    if (-not $ResolvedIdentity) {
                        # This is purely to get special types
                        $ResolvedIdentity = ConvertFrom-SID -SID $ReturnObject['ObjectSID']
                    }
                    $ReturnObject['Type'] = $ResolvedIdentity.Type
                }
                if ($ReturnObject['Type'] -eq 'WellKnownAdministrative') {
                    if (-not $TemporaryDomainName) {
                        # This is so BUILTIN\Administrators would not report domain name that's always related to current one, while it could be someone expects it to be from different forest
                        # this is to mainly address issues with Get-ADACL IdentityReference returning data that's hard to manage otherwise
                        $ReturnObject['DomainName'] = ''
                    }
                }

                <#
                $LastLogon = $Object.properties.lastlogon -as [string]
                if ($LastLogon) {
                    $LastLogonDate = [datetime]::FromFileTime($LastLogon)
                } else {
                    $LastLogonDate = $null
                }
 
                $AccountExpires = $Object.Properties.accountexpires -as [string]
                $AccountExpiresDate = ConvertTo-Date -accountExpires $AccountExpires
 
                $PasswordLastSet = $Object.Properties.pwdlastset -as [string]
                if ($PasswordLastSet) {
                    $PasswordLastSetDate = [datetime]::FromFileTime($PasswordLastSet)
                } else {
                    $PasswordLastSetDate = $null
                }
                $BadPasswordTime = $Object.Properties.badpasswordtime -as [string]
                if ($BadPasswordTime) {
                    $BadPasswordDate = [datetime]::FromFileTime($BadPasswordTime)
                } else {
                    $BadPasswordDate = $null
                }
 
                $ReturnObject['LastLogonDate'] = $LastLogonDate
                $ReturnObject['PasswordLastSet'] = $PasswordLastSetDate
                $ReturnObject['BadPasswordTime'] = $BadPasswordDate
                $ReturnObject['AccountExpiresDate'] = $AccountExpiresDate
                #>

                if ($Cache) {
                    $Script:CacheObjectsWinADObject[$TemporaryName] = [PSCustomObject] $ReturnObject
                    $Script:CacheObjectsWinADObject[$TemporaryName]
                } else {
                    [PSCustomObject] $ReturnObject
                }
            }
        }
    }
}
function Get-WinADPasswordPolicy {
    <#
    .SYNOPSIS
    Get password policies from Active Directory include fine grained password policies
 
    .DESCRIPTION
    Get password policies from Active Directory include fine grained password policies
    Please keep in mind that reading fine grained password policies requires extended rights
    It's not available to standard users
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER NoSorting
    Do not sort output by Precedence
 
    .PARAMETER ReturnHashtable
    Return hashtable instead of array. Useful for internal processing such as Get-WinADUsers
 
    .EXAMPLE
    Get-WinADPasswordPolicy | Format-Table
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,

        [switch] $NoSorting,
        [parameter(DontShow)][switch] $ReturnHashtable
    )
    $FineGrainedPolicy = [ordered] @{}

    $ForestInformation = Get-WinADForestDetails -Extended -Forest $Forest -ExcludeDomains $ExcludeDomains -IncludeDomains $IncludeDomains
    $AllPasswordPolicies = foreach ($Domain in $ForestInformation.Domains) {
        $Policies = @(
            Get-ADDefaultDomainPasswordPolicy -Server $ForestInformation['QueryServers'][$Domain].Hostname[0]
            Get-ADFineGrainedPasswordPolicy -Filter "*" -Server $ForestInformation['QueryServers'][$Domain].Hostname[0]
        )
        foreach ($Policy in $Policies) {
            $FineGrainedPolicy[$Policy.DistinguishedName] = [PSCustomObject] @{
                Name                        = if ($Policy.ObjectClass -contains 'domainDNS') {
                    'Default' 
                } else {
                    $Policy.Name 
                }
                DomainName                  = $Domain
                Type                        = if ($Policy.ObjectClass -contains 'domainDNS') {
                    'Default Password Policy' 
                } else {
                    'Fine Grained Password Policy' 
                }
                Precedence                  = if ($Policy.Precedence) {
                    $Policy.Precedence 
                } else {
                    99999 
                }
                MinPasswordLength           = $Policy.MinPasswordLength
                MaxPasswordAge              = $Policy.MaxPasswordAge
                MinPasswordAge              = $Policy.MinPasswordAge
                PasswordHistoryCount        = $Policy.PasswordHistoryCount
                ComplexityEnabled           = $Policy.ComplexityEnabled
                ReversibleEncryptionEnabled = $Policy.ReversibleEncryptionEnabled
                LockoutDuration             = $Policy.LockoutDuration
                LockoutObservationWindow    = $Policy.LockoutObservationWindow
                LockoutThreshold            = $Policy.LockoutThreshold
                AppliesTo                   = $Policy.AppliesTo
                AppliesToCount              = if ($Policy.AppliesTo) {
                    $Policy.AppliesTo.Count 
                } else {
                    0 
                }
                AppliesToName               = if ($Policy.AppliesTo) {
                    foreach ($DN in $Policy.AppliesTo) {
                        ConvertFrom-DistinguishedName -DistinguishedName $DN -ToLastName
                    }
                } else {
                    $null
                }
                DistinguishedName           = $Policy.DistinguishedName
            }
            if ($Policy.ObjectClass -contains 'domainDNS') {
                $FineGrainedPolicy[$Domain] = $FineGrainedPolicy[$Policy.DistinguishedName]
                $FineGrainedPolicy[$Domain]
            } else {
                $FineGrainedPolicy[$Policy.DistinguishedName] = $FineGrainedPolicy[$Policy.DistinguishedName]
                $FineGrainedPolicy[$Policy.DistinguishedName]
            }
        }
    }
    if ($ReturnHashtable) {
        $FineGrainedPolicy
    } else {
        if (-not $NoSorting) {
            $AllPasswordPolicies | Sort-Object -Property Precedence
        } else {
            $AllPasswordPolicies
        }
    }
}
Function Get-WinADPrivilegedObjects {
    <#
    .SYNOPSIS
    Retrieves privileged objects within an Active Directory forest.
 
    .DESCRIPTION
    This cmdlet retrieves and displays privileged objects within an Active Directory forest. It can be used to identify objects with administrative privileges, including their properties such as when they were changed, created, their admin count, and whether they are critical system objects. The cmdlet also provides information about the associated domain and the date of the last originating change for the admin count.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve privileged objects from. This parameter is required.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the search.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the search.
 
    .PARAMETER LegitimateOnly
    If specified, only objects with legitimate admin counts are returned.
 
    .PARAMETER OrphanedOnly
    If specified, only orphaned objects (not critical system objects and not members of critical groups) are returned.
 
    .PARAMETER SummaryOnly
    A switch parameter that controls the level of detail in the output. If set, the output includes a summary of the privileged objects. If not set, the output includes detailed information.
 
    .PARAMETER DoNotShowCriticalSystemObjects
    If specified, critical system objects are excluded from the results.
 
    .PARAMETER Formatted
    A switch parameter that controls the formatting of the output. If set, the output is formatted for better readability.
 
    .PARAMETER Splitter
    Specifies the character to use as a delimiter when joining multiple data elements together in the output.
 
    .PARAMETER ExtendedForestInformation
    A dictionary object that contains additional information about the forest. This parameter is optional and can be used to provide more context about the forest.
 
    .EXAMPLE
    Get-WinADPrivilegedObjects -Forest "example.com" -IncludeDomains "example.com" -LegitimateOnly -Formatted
    This example retrieves only the privileged objects with legitimate admin counts within the "example.com" forest and formats the output for better readability.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires appropriate permissions to query the Active Directory forest.
    #>

    [alias('Get-WinADPriviligedObjects')]
    [cmdletbinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [switch] $LegitimateOnly,
        [switch] $OrphanedOnly,
        #[switch] $Unique,
        [switch] $SummaryOnly,
        [switch] $DoNotShowCriticalSystemObjects,
        [alias('Display')][switch] $Formatted,
        [string] $Splitter = [System.Environment]::NewLine,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    $Domains = $ForestInformation.Domains
    $UsersWithAdminCount = foreach ($Domain in $Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        if ($DoNotShowCriticalSystemObjects) {
            $Objects = Get-ADObject -Filter 'admincount -eq 1 -and iscriticalsystemobject -notlike "*"' -Server $QueryServer -Properties whenchanged, whencreated, admincount, isCriticalSystemObject, samaccountname, "msDS-ReplAttributeMetaData"
        } else {
            $Objects = Get-ADObject -Filter 'admincount -eq 1' -Server $QueryServer -Properties whenchanged, whencreated, admincount, isCriticalSystemObject, samaccountname, "msDS-ReplAttributeMetaData"
        }
        foreach ($_ in $Objects) {
            [PSCustomObject] @{
                Domain                 = $Domain
                distinguishedname      = $_.distinguishedname
                whenchanged            = $_.whenchanged
                whencreated            = $_.whencreated
                admincount             = $_.admincount
                SamAccountName         = $_.SamAccountName
                objectclass            = $_.objectclass
                isCriticalSystemObject = if ($_.isCriticalSystemObject) {
                    $true 
                } else {
                    $false 
                }
                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) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        Get-ADGroup -Filter 'admincount -eq 1 -and iscriticalsystemobject -eq $true' -Server $QueryServer #| Select-Object @{name = 'Domain'; expression = { $domain } }, distinguishedname
    }

    $CacheCritical = [ordered] @{}
    foreach ($Group in $CriticalGroups) {
        [Array] $Members = Get-WinADGroupMember -Identity $Group.distinguishedname -Verbose:$false -All
        Write-Verbose -Message "Processing $($Group.DistinguishedName) with $($Members.Count) members"
        foreach ($Member in $Members) {
            if ($null -ne $Member -and $Member.DistinguishedName) {
                if (-not $CacheCritical[$Member.DistinguishedName]) {
                    $CacheCritical[$Member.DistinguishedName] = [System.Collections.Generic.List[string]]::new()
                }
                if ($Group.DistinguishedName -notin $CacheCritical[$Member.DistinguishedName]) {
                    $CacheCritical[$Member.DistinguishedName].Add($Group.DistinguishedName)
                }
            }
        }
    }

    $AdminCountAll = foreach ($object in $UsersWithAdminCount) {
        $DistinguishedName = $object.distinguishedname
        [Array] $IsMemberGroups = foreach ($Group in $CriticalGroups) {
            $CacheCritical[$DistinguishedName] -contains $Group.DistinguishedName
        }
        $IsMember = $IsMemberGroups -contains $true
        $GroupDomains = $CacheCritical[$DistinguishedName]
        $IsOrphaned = -not $Object.isCriticalSystemObject -and -not $IsMember

        if ($Formatted) {
            $GroupDomains = $GroupDomains -join $Splitter
            $User = [PSCustomObject] @{
                DistinguishedName      = $Object.DistinguishedName
                Domain                 = $Object.domain
                IsOrphaned             = $IsOrphaned
                IsMember               = $IsMember
                IsCriticalSystemObject = $Object.isCriticalSystemObject
                Admincount             = $Object.admincount
                AdminCountDate         = $Object.adminCountDate
                WhenCreated            = $Object.whencreated
                ObjectClass            = $Object.objectclass
                GroupDomain            = $GroupDomains
            }
        } else {
            $User = [PSCustomObject] @{
                'DistinguishedName'      = $Object.DistinguishedName
                'Domain'                 = $Object.domain
                'IsOrphaned'             = $IsOrphaned
                'IsMember'               = $IsMember
                'IsCriticalSystemObject' = $Object.isCriticalSystemObject
                'AdminCount'             = $Object.admincount
                'AdminCountDate'         = $Object.adminCountDate
                'WhenCreated'            = $Object.whencreated
                'ObjectClass'            = $Object.objectclass
                'GroupDomain'            = $GroupDomains
            }
        }
        $User
    }

    $Output = @(
        if ($OrphanedOnly) {
            $AdminCountAll | Where-Object { $_.IsOrphaned }
        } elseif ($LegitimateOnly) {
            $AdminCountAll | Where-Object { $_.IsOrphaned -eq $false }
        } else {
            $AdminCountAll
        }
    )
    if ($SummaryOnly) {
        $Output | Group-Object ObjectClass | Select-Object -Property Name, Count
    } else {
        $Output
    }
}
function Get-WinADProtocol {
    <#
    .SYNOPSIS
    Gets current SCHANNEL settings for Windows Clients and Servers.
 
    .DESCRIPTION
    Gets current SCHANNEL settings for Windows Clients and Servers. By default scans all Domain Controllers in a forest
 
    .PARAMETER ComputerName
    Provides ability to query specific servers or computers.
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExcludeDomainControllers
    Exclude specific domain controllers, by default there are no exclusions
 
    .PARAMETER IncludeDomainControllers
    Include only specific domain controllers, by default all domain controllers are included
 
    .PARAMETER SkipRODC
    Skip Read-Only Domain Controllers. By default all domain controllers are included.
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .EXAMPLE
    An example
 
    .NOTES
    Based on:
    - https://stackoverflow.com/questions/51405489/what-is-the-difference-between-the-disabledbydefault-and-enabled-ssl-tls-registr
    - https://docs.microsoft.com/en-us/windows-server/identity/ad-fs/operations/manage-ssl-protocols-in-ad-fs
    - https://docs.microsoft.com/en-us/windows-server/security/tls/tls-registry-settings
    - https://docs.microsoft.com/en-us/security/engineering/solving-tls1-problem
    - https://docs.microsoft.com/en-us/windows/win32/secauthn/protocols-in-tls-ssl--schannel-ssp-
    #>

    [CmdletBinding()]
    param(
        [alias('Server')][string[]] $ComputerName,

        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $Computers = @(
        if ($ComputerName) {
            foreach ($Computer in $ComputerName) {
                [PSCustomObject] @{
                    HostName = $Computer
                    Domain   = 'Not provided'
                }
            }
        } else {
            $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
            foreach ($DC in $ForestInformation.ForestDomainControllers) {
                [PSCustomObject] @{
                    HostName = $DC.HostName
                    Domain   = $DC.Domain
                }
            }
        }
    )
    foreach ($DC in $Computers) {
        #$Connectivity = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL'
        $Version = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows NT\CurrentVersion'
        if ($Version.PSConnection -eq $true) {

            $WindowsVersion = ConvertTo-OperatingSystem -OperatingSystem $Version.ProductName -OperatingSystemVersion $Version.CurrentBuildNumber
            # According to this https://github.com/MicrosoftDocs/windowsserverdocs/issues/2783 SCHANNEL service requires direct enablement
            $ProtocolDefaults = Get-ProtocolDefaults -WindowsVersion $WindowsVersion

            $Client = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Client'
            $Server = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 2.0\Server'
            $Client30 = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Client'
            $Server30 = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\SSL 3.0\Server'
            $ClientTLS10 = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Client'
            $ServerTLS10 = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.0\Server'
            $ClientTLS11 = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Client'
            $ServerTLS11 = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.1\Server'
            $ClientTLS12 = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Client'
            $ServerTLS12 = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.2\Server'
            #$ClientTLS13 = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.3\Client'
            #$ServerTLS13 = Get-PSRegistry -ComputerName $DC.HostName -RegistryPath 'HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols\TLS 1.3\Server'

            [PSCustomObject] @{
                ComputerName  = $DC.HostName
                DomainName    = $DC.Domain
                Version       = $WindowsVersion
                SSL_2_Client  = Get-ProtocolStatus -RegistryEntry $Client -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'SSL2Client'
                SSL_2_Server  = Get-ProtocolStatus -RegistryEntry $Server -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'SSL2Server'
                SSL_3_Client  = Get-ProtocolStatus -RegistryEntry $Client30 -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'SSL3Client'
                SSL_3_Server  = Get-ProtocolStatus -RegistryEntry $Server30 -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'SSL3Server'
                TLS_10_Client = Get-ProtocolStatus -RegistryEntry $ClientTLS10 -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'TLS10Client'
                TLS_10_Server = Get-ProtocolStatus -RegistryEntry $ServerTLS10 -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'TLS10Server'
                TLS_11_Client = Get-ProtocolStatus -RegistryEntry $ClientTLS11 -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'TLS11Client'
                TLS_11_Server = Get-ProtocolStatus -RegistryEntry $ServerTLS11 -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'TLS11Server'
                TLS_12_Client = Get-ProtocolStatus -RegistryEntry $ClientTLS12 -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'TLS12Client'
                TLS_12_Server = Get-ProtocolStatus -RegistryEntry $ServerTLS12 -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'TLS12Server'
                TLS_13_Client = Get-ProtocolStatus -RegistryEntry $ClientTLS13 -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'TLS13Client'
                TLS_13_Server = Get-ProtocolStatus -RegistryEntry $ServerTLS13 -WindowsVersion $WindowsVersion -ProtocolDefaults $ProtocolDefaults -Protocol 'TLS13Server'
            }
        } else {
            [PSCustomObject] @{
                ComputerName  = $DC.HostName
                DomainName    = $DC.Domain
                Version       = 'Unknown'
                SSL_2_Client  = 'No connection'
                SSL_2_Server  = 'No connection'
                SSL_3_Client  = 'No connection'
                SSL_3_Server  = 'No connection'
                TLS_10_Client = 'No connection'
                TLS_10_Server = 'No connection'
                TLS_11_Client = 'No connection'
                TLS_11_Server = 'No connection'
                TLS_12_Client = 'No connection'
                TLS_12_Server = 'No connection'
                #TLS_13_Client = Get-ProtocolStatus -RegistryEntry $ClientTLS13
                #TLS_13_Server = Get-ProtocolStatus -RegistryEntry $ServerTLS13
            }
        }
    }
}
function Get-WinADProxyAddresses {
    <#
    .SYNOPSIS
    Retrieves and organizes proxy addresses for an Active Directory user.
 
    .DESCRIPTION
    This function retrieves and organizes the proxy addresses associated with an Active Directory user. It categorizes the addresses into primary, secondary, SIP, X500, and other types based on their prefixes. It also provides options to remove prefixes, convert data to lowercase, and format the output for display purposes.
 
    .PARAMETER ADUser
    Specifies the Active Directory user object for which to retrieve proxy addresses.
 
    .PARAMETER RemovePrefix
    Indicates whether to remove the prefix (e.g., SMTP:, smtp:) from the proxy addresses.
 
    .PARAMETER ToLower
    Specifies whether to convert all returned data to lowercase.
 
    .PARAMETER Formatted
    Indicates whether the data should be formatted for display purposes rather than for working with objects.
 
    .PARAMETER Splitter
    Specifies the character used to join multiple data elements together, such as an array of aliases.
 
    .EXAMPLE
    $ADUsers = Get-ADUser -Filter "*" -Properties ProxyAddresses
    foreach ($User in $ADUsers) {
        Get-WinADProxyAddresses -ADUser $User
    }
 
    .EXAMPLE
    $ADUsers = Get-ADUser -Filter "*" -Properties ProxyAddresses
    foreach ($User in $ADUsers) {
        Get-WinADProxyAddresses -ADUser $User -RemovePrefix
    }
 
    .NOTES
    This function requires the Active Directory module to be available. It provides a structured view of proxy addresses for an AD user.
    #>

    [CmdletBinding()]
    param(
        [Object] $ADUser,
        [switch] $RemovePrefix,
        [switch] $ToLower,
        [switch] $Formatted,
        [alias('Joiner')][string] $Splitter = ','
    )
    $Summary = [PSCustomObject] @{
        EmailAddress = $ADUser.EmailAddress
        Primary      = [System.Collections.Generic.List[string]]::new()
        Secondary    = [System.Collections.Generic.List[string]]::new()
        Sip          = [System.Collections.Generic.List[string]]::new()
        x500         = [System.Collections.Generic.List[string]]::new()
        Other        = [System.Collections.Generic.List[string]]::new()
        Broken       = [System.Collections.Generic.List[string]]::new()
        # MailNickname = $ADUser.mailNickName
    }
    foreach ($Proxy in $ADUser.ProxyAddresses) {
        if ($Proxy -like '*,*') {
            # Most likely someone added proxy address with comma instead of each email address separatly
            $Summary.Broken.Add($Proxy)
        } elseif ($Proxy.StartsWith('SMTP:')) {
            if ($RemovePrefix) {
                $Proxy = $Proxy -replace 'SMTP:', ''
            }
            if ($ToLower) {
                $Proxy = $Proxy.ToLower()
            }
            $Summary.Primary.Add($Proxy)
        } elseif ($Proxy.StartsWith('smtp:') -or $Proxy -notlike "*:*") {
            if ($RemovePrefix) {
                $Proxy = $Proxy -replace 'smtp:', ''
            }
            if ($ToLower) {
                $Proxy = $Proxy.ToLower()
            }
            $Summary.Secondary.Add($Proxy)
        } elseif ($Proxy.StartsWith('x500')) {
            if ($RemovePrefix) {
                $Proxy = $Proxy #-replace 'SMTP:', ''
            }
            if ($ToLower) {
                $Proxy = $Proxy.ToLower()
            }
            $Summary.x500.Add($Proxy)
        } elseif ($Proxy.StartsWith('sip:')) {
            if ($RemovePrefix) {
                $Proxy = $Proxy #-replace 'SMTP:', ''
            }
            if ($ToLower) {
                $Proxy = $Proxy.ToLower()
            }
            $Summary.Sip.Add($Proxy)
        } else {
            if ($RemovePrefix) {
                $Proxy = $Proxy #-replace 'SMTP:', ''
            }
            if ($ToLower) {
                $Proxy = $Proxy.ToLower()
            }
            $Summary.Other.Add($Proxy)
        }
    }
    if ($Formatted) {
        $Summary.Primary = $Summary.Primary -join $Splitter
        $Summary.Secondary = $Summary.Secondary -join $Splitter
        $Summary.Sip = $Summary.Sip -join $Splitter
        $Summary.x500 = $Summary.x500 -join $Splitter
        $Summary.Other = $Summary.Other -join $Splitter
    }
    $Summary
}
function Get-WinADServiceAccount {
    <#
    .SYNOPSIS
    Retrieves detailed information about service accounts across domains in a forest.
 
    .DESCRIPTION
    This cmdlet queries Active Directory for service accounts across specified domains within a forest. It provides detailed information about each account, including its properties, last logon date, password last set date, and other security-related attributes.
 
    .PARAMETER Forest
    Specifies the name of the forest to query. This parameter is required.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the query.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the query. If not specified, all domains in the forest are queried.
 
    .PARAMETER PerDomain
    If specified, the cmdlet returns a hash table with domain names as keys and arrays of service account objects as values. Otherwise, it returns a flat array of service account objects.
 
    .EXAMPLE
    Get-WinADServiceAccount -Forest "example.local" -IncludeDomains "example.local", "child.example.local"
 
    This example queries the "example.local" forest, including only the "example.local" and "child.example.local" domains.
 
    .EXAMPLE
    Get-WinADServiceAccount -Forest "example.local" -ExcludeDomains "child.example.local"
 
    This example queries the "example.local" forest, excluding the "child.example.local" domain.
 
    .EXAMPLE
    Get-WinADServiceAccount -Forest "example.local" -PerDomain
 
    This example queries the "example.local" forest and returns the results per domain.
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [switch] $PerDomain
    )
    $Today = Get-Date
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    $Output = [ordered] @{}
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        $Properties = @(
            'Name', 'ObjectClass', 'PasswordLastSet', 'PasswordNeverExpires', 'PasswordNotRequired', 'UserPrincipalName', 'SamAccountName', 'LastLogonDate' #,'PrimaryGroup', 'PrimaryGroupID',
            'AccountExpirationDate', 'AccountNotDelegated',
            #'AllowReversiblePasswordEncryption', 'CannotChangePassword',
            'CanonicalName', 'WhenCreated', 'WhenChanged', 'DistinguishedName', 'Enabled', 'Description'
            'msDS-HostServiceAccountBL', 'msDS-SupportedEncryptionTypes', 'msDS-User-Account-Control-Computed', 'TrustedForDelegation', 'TrustedToAuthForDelegation'
            'msDS-AuthenticatedAtDC', 'msDS-AllowedToActOnBehalfOfOtherIdentity', 'msDS-AllowedToDelegateTo', 'PrincipalsAllowedToRetrieveManagedPassword', 'PrincipalsAllowedToDelegateToAccount'
            'msDS-ManagedPasswordInterval', 'msDS-GroupMSAMembership', 'ManagedPasswordIntervalInDays', 'msDS-RevealedDSAs', 'servicePrincipalName'
            #'msDS-ManagedPasswordId', 'msDS-ManagedPasswordPreviousId'
        )
        $Accounts = Get-ADServiceAccount -Filter "*" -Server $QueryServer -Properties $Properties
        $Output[$Domain] = foreach ($Account in $Accounts) {
            #$Account

            if ($null -ne $Account.LastLogonDate) {
                [int] $LastLogonDays = "$(-$($Account.LastLogonDate - $Today).Days)"
            } else {
                $LastLogonDays = $null
            }
            if ($null -ne $Account.PasswordLastSet) {
                [int] $PasswordLastChangedDays = "$(-$($Account.PasswordLastSet - $Today).Days)"
            } else {
                $PasswordLastChangedDays = $null
            }

            [PSCUstomObject] @{
                Name                                         = $Account.Name
                Enabled                                      = $Account.Enabled                              # : True # : WO_SVC_Delete$
                ObjectClass                                  = $Account.ObjectClass                          # : msDS-ManagedServiceAccount
                CanonicalName                                = $Account.CanonicalName                        # : ad.evotec.xyz/Managed Service Accounts/WO_SVC_Delete
                DomainName                                   = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $Account.DistinguishedName
                Description                                  = $Account.Description
                PasswordLastChangedDays                      = $PasswordLastChangedDays
                LastLogonDays                                = $LastLogonDays
                'ManagedPasswordIntervalInDays'              = $Account.'ManagedPasswordIntervalInDays'
                'msDS-AllowedToDelegateTo'                   = $Account.'msDS-AllowedToDelegateTo'            # : {CN=EVOWIN,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=xyz}
                'msDS-HostServiceAccountBL'                  = $Account.'msDS-HostServiceAccountBL'            # : {CN=EVOWIN,OU=Computers,OU=Devices,OU=Production,DC=ad,DC=evotec,DC=xyz}
                'msDS-AuthenticatedAtDC'                     = $Account.'msDS-AuthenticatedAtDC'
                'msDS-AllowedToActOnBehalfOfOtherIdentity'   = $Account.'msDS-AllowedToActOnBehalfOfOtherIdentity'
                'PrincipalsAllowedToRetrieveManagedPassword' = $Account.'PrincipalsAllowedToRetrieveManagedPassword'
                'PrincipalsAllowedToDelegateToAccount'       = $Account.'PrincipalsAllowedToDelegateToAccount'

                #'msDS-ManagedPasswordId' = $Account.'msDS-ManagedPasswordId'
                'msDS-GroupMSAMembershipAccess'              = $Account.'msDS-GroupMSAMembership'.Access.IdentityReference.Value
                'msDS-GroupMSAMembershipOwner'               = $Account.'msDS-GroupMSAMembership'.Owner
                #'msDS-ManagedPasswordPreviousId' = $Account.'msDS-ManagedPasswordPreviousId'

                'msDS-RevealedDSAs'                          = $Account.'msDS-RevealedDSAs'
                'servicePrincipalName'                       = $Account.servicePrincipalName
                AccountNotDelegated                          = $Account.AccountNotDelegated                  # : False
                TrustedForDelegation                         = $Account.TrustedForDelegation                 # : False
                TrustedToAuthForDelegation                   = $Account.TrustedToAuthForDelegation           # : False
                AccountExpirationDate                        = $Account.AccountExpirationDate
                #AllowReversiblePasswordEncryption = $Account.AllowReversiblePasswordEncryption # : False
                #CannotChangePassword = $Account.CannotChangePassword # : False
                #'msDS-SupportedEncryptionTypes' = $Account.'msDS-SupportedEncryptionTypes' # : 28
                msDSSupportedEncryptionTypes                 = Get-ADEncryptionTypes -Value $Account.'msds-supportedencryptiontypes'
                # 'msDS-User-Account-Control-Computed' = $Account.'msDS-User-Account-Control-Computed' # : 0
                #ObjectGUID = $Account.ObjectGUID # : 573ff95e-c1f8-45e2-9b64-662fb9cb0615
                PasswordNeverExpires                         = $Account.PasswordNeverExpires                 # : False
                PasswordNotRequired                          = $Account.PasswordNotRequired                  # : False
                #PrimaryGroup = $Account.PrimaryGroup # : CN=Domain Computers,CN=Users,DC=ad,DC=evotec,DC=xyz
                #PrimaryGroupID = $Account.PrimaryGroupID # : 515
                #SID = $Account.SID # : S-1-5-21-853615985-2870445339-3163598659-4607
                #UserPrincipalName = $Account.UserPrincipalName # :
                LastLogonDate                                = $Account.LastLogonDate                        # :
                PasswordLastSet                              = $Account.PasswordLastSet                      # : 15.04.2021 22:47:40
                WhenChanged                                  = $Account.WhenChanged                          # : 15.04.2021 22:47:40
                WhenCreated                                  = $Account.WhenCreated                          # : 15.04.2021 22:47:40
                SamAccountName                               = $Account.SamAccountName
                DistinguishedName                            = $Account.DistinguishedName                    # : CN=WO_SVC_Delete,CN=Managed Service Accounts,DC=ad,DC=evotec,DC=xyz
                'msDS-GroupMSAMembership'                    = $Account.'msDS-GroupMSAMembership'
                # 'msDS-ManagedPasswordInterval' = $Account.'msDS-ManagedPasswordInterval'
            }
        }
    }
    if ($PerDomain) {
        $Output
    } else {
        $Output.Values
    }
}
function Get-WinADSharePermission {
    <#
    .SYNOPSIS
    Retrieves the permissions for a specified Windows Active Directory share or shares based on type.
 
    .DESCRIPTION
    This cmdlet retrieves the permissions for a specified Windows Active Directory share or shares based on type. It can target a specific path or retrieve permissions for shares of a specified type across multiple domains in a forest. The cmdlet can also filter the results to include or exclude specific domains and provide additional forest information.
 
    .PARAMETER Path
    Specifies the path to the share for which to retrieve permissions. This parameter is mandatory when using the 'Path' parameter set.
 
    .PARAMETER ShareType
    Specifies the type of share for which to retrieve permissions. This parameter is mandatory when using the 'ShareType' parameter set. Valid values are 'NetLogon' and 'SYSVOL'.
 
    .PARAMETER Owner
    Specifies that the cmdlet should only return the owner of the share instead of the full permissions.
 
    .PARAMETER Name
    Specifies the name of the share for which to retrieve permissions. This parameter is not currently used.
 
    .PARAMETER Forest
    Specifies the name of the forest to target for share permissions retrieval. This parameter is used in conjunction with the 'ShareType' parameter.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the share permissions retrieval.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the share permissions retrieval.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest to use for share permissions retrieval.
 
    .EXAMPLE
    Get-WinADSharePermission -Path "\\domain\share"
    Retrieves the permissions for the specified share path.
 
    .EXAMPLE
    Get-WinADSharePermission -ShareType NetLogon -Forest MyForest
    Retrieves the permissions for all NetLogon shares across the specified forest.
 
    .EXAMPLE
    Get-WinADSharePermission -ShareType SYSVOL -IncludeDomains MyDomain1, MyDomain2
    Retrieves the permissions for all SYSVOL shares in the specified domains.
 
    .NOTES
    This cmdlet requires the 'Get-WinADForestDetails', 'Get-FileOwner', and 'Get-FilePermission' cmdlets to function.
    #>

    [cmdletBinding(DefaultParameterSetName = 'Path')]
    param(
        [Parameter(ParameterSetName = 'Path', Mandatory)][string] $Path,
        [Parameter(ParameterSetName = 'ShareType', Mandatory)][validateset('NetLogon', 'SYSVOL')][string[]] $ShareType,
        [switch] $Owner,
        [string[]] $Name,
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    if ($ShareType) {
        $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
        foreach ($Domain in $ForestInformation.Domains) {
            $Path = -join ("\\", $Domain, "\$ShareType")
            @(Get-Item -Path $Path -Force) + @(Get-ChildItem -Path $Path -Recurse:$true -Force -ErrorAction SilentlyContinue -ErrorVariable Err) | ForEach-Object -Process {
                if ($Owner) {
                    $Output = Get-FileOwner -JustPath -Path $_ -Resolve -AsHashTable
                    $Output['Attributes'] = $_.Attributes
                    [PSCustomObject] $Output
                } else {
                    $Output = Get-FilePermission -Path $_ -ResolveTypes -Extended -AsHashTable
                    foreach ($O in $Output) {
                        $O['Attributes'] = $_.Attributes
                        [PSCustomObject] $O
                    }
                }
            }
        }
    } else {
        if ($Path -and (Test-Path -Path $Path)) {
            @(Get-Item -Path $Path -Force) + @(Get-ChildItem -Path $Path -Recurse:$true -Force -ErrorAction SilentlyContinue -ErrorVariable Err) | ForEach-Object -Process {
                if ($Owner) {
                    $Output = Get-FileOwner -JustPath -Path $_ -Resolve -AsHashTable -Verbose
                    $Output['Attributes'] = $_.Attributes
                    [PSCustomObject] $Output
                } else {
                    $Output = Get-FilePermission -Path $_ -ResolveTypes -Extended -AsHashTable
                    foreach ($O in $Output) {
                        $O['Attributes'] = $_.Attributes
                        [PSCustomObject] $O
                    }
                }
            }
        }
    }
    foreach ($e in $err) {
        Write-Warning "Get-WinADSharePermission - $($e.Exception.Message) ($($e.CategoryInfo.Reason))"
    }
}
function Get-WinADSiteConnections {
    <#
    .SYNOPSIS
    Retrieves site connections within an Active Directory forest.
 
    .DESCRIPTION
    This cmdlet retrieves and displays site connections within an Active Directory forest. It can be used to identify the connections between sites, including their properties such as options, server names, and site names. The cmdlet can also format the output to include or exclude specific details.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve site connections from. If not specified, the current forest is used.
 
    .PARAMETER Splitter
    Specifies the character to use as a delimiter when joining multiple options into a single string. If not specified, options are returned as an array.
 
    .PARAMETER Formatted
    A switch parameter that controls the level of detail in the output. If set, the output includes all available site connection properties in a formatted manner. If not set, the output is more concise.
 
    .PARAMETER ExtendedForestInformation
    A dictionary object that contains additional information about the forest. This parameter is optional and can be used to provide more context about the forest.
 
    .EXAMPLE
    Get-WinADSiteConnections -Forest "example.com" -Formatted
    This example retrieves all site connections within the "example.com" forest, displaying detailed information in a formatted manner.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires access to the target forest.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [alias('Joiner')][string] $Splitter,
        [switch] $Formatted,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )

    [Flags()]
    enum ConnectionOption {
        None
        IsGenerated
        TwoWaySync
        OverrideNotifyDefault = 4
        UseNotify = 8
        DisableIntersiteCompression = 16
        UserOwnedSchedule = 32
        RodcTopology = 64
    }

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    $QueryServer = $ForestInformation['QueryServers'][$($ForestInformation.Forest.Name)]['HostName'][0]


    $NamingContext = (Get-ADRootDSE -Server $QueryServer).configurationNamingContext
    $Connections = Get-ADObject -SearchBase $NamingContext -LDAPFilter "(objectCategory=ntDSConnection)" -Properties * -Server $QueryServer
    $FormmatedConnections = foreach ($_ in $Connections) {
        if ($null -eq $_.Options) {
            $Options = 'None'
        } else {
            $Options = ([ConnectionOption] $_.Options) -split ', '
        }
        if ($Formatted) {
            $Dictionary = [PSCustomObject] @{

                <# Regex extracts AD1 and AD2
        CN=d1695d10-8d24-41db-bb0f-2963e2c7dfcd,CN=NTDS Settings,CN=AD1,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz
        CN=NTDS Settings,CN=AD2,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz
        #>

                '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
                }
                <# Regex extracts KATOWICE-1
        CN=d1695d10-8d24-41db-bb0f-2963e2c7dfcd,CN=NTDS Settings,CN=AD1,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz
        CN=NTDS Settings,CN=AD2,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz
        #>

                '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 
                }
                #'Options' = $_.Options
                'When Created'       = $_.WhenCreated
                'When Changed'       = $_.WhenChanged
                'Is Deleted'         = $_.IsDeleted
            }
        } else {
            $Dictionary = [PSCustomObject] @{

                <# Regex extracts AD1 and AD2
        CN=d1695d10-8d24-41db-bb0f-2963e2c7dfcd,CN=NTDS Settings,CN=AD1,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz
        CN=NTDS Settings,CN=AD2,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz
        #>

                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
                }
                <# Regex extracts KATOWICE-1
        CN=d1695d10-8d24-41db-bb0f-2963e2c7dfcd,CN=NTDS Settings,CN=AD1,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz
        CN=NTDS Settings,CN=AD2,CN=Servers,CN=KATOWICE-1,CN=Sites,CN=Configuration,DC=ad,DC=evotec,DC=xyz
        #>

                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 
                }
                #Options = $_.Options
                WhenCreated       = $_.WhenCreated
                WhenChanged       = $_.WhenChanged
                IsDeleted         = $_.IsDeleted
            }
        }
        $Dictionary
    }
    $FormmatedConnections
}
function Get-WinADSiteCoverage {
    <#
    .SYNOPSIS
    Provides information about custom configuration for Site Coverage of Domain Controllers
 
    .DESCRIPTION
    Provides information about custom configuration for Site Coverage of Domain Controllers
    It requires Domain Admin rights to execute, as it checks registry settings on Domain Controllers
    It will check what Site Coverage is set on Domain Controllers and for both Site Coverage and GC Site Coverage.
    It will check if the Site exists in AD Sites and Services
 
    .PARAMETER Forest
    Specifies the target forest to retrieve site information from.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the search.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controllers to exclude from the search.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the search.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controllers to include in the search.
 
    .PARAMETER SkipRODC
    Indicates whether to skip read-only domain controllers.
 
    .PARAMETER ExtendedForestInformation
    A dictionary object that contains additional information about the forest. This parameter is optional and can be used to provide more context about the forest.
 
    .EXAMPLE
    Get-WinADSiteCoverage -Forest "example.com"
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [string] $Forest,
        [alias('Domain')][string[]] $IncludeDomains,
        [string[]] $ExcludeDomains,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [string[]] $ExcludeDomainControllers,
        [switch] $SkipRODC,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation

    $AllSitesCache = [ordered] @{}
    try {
        $AllSites = Get-ADReplicationSite -Filter * -ErrorAction Stop -Server $ForestInformation.QueryServers['Forest'].HostName[0]
        foreach ($Site in $AllSites) {
            $AllSitesCache[$Site.Name] = $Site
        }
    } catch {
        Write-Warning -Message "Get-WinADSiteCoverage - We couldn't get all sites. Make sure you have RSAT installed and you have permissions to read AD Sites and Services. Error: $($_.Exception.Message)"
        return
    }

    $Count = 0
    foreach ($Domain in $ForestInformation.Domains) {
        $Count++
        $CountDC = 0
        foreach ($DC in $ForestInformation.DomainDomainControllers[$Domain]) {
            $CountDC++
            $DCSettings = Get-WinADDomainControllerNetLogonSettings -DomainController $DC.HostName
            [Array] $WrongSiteCoverage = foreach ($Site in $DCSettings.SiteCoverage) {
                if (-not $AllSitesCache[$Site]) {
                    $Site
                }
            }
            [Array] $WrongGCSiteCoverage = foreach ($Site in $DCSettings.GCSiteCoverage) {
                if (-not $AllSitesCache[$Site]) {
                    $Site
                }
            }

            Write-Verbose -Message "Get-WinADSiteCoverage - Processing Domain $Domain [$Count/$($ForestInformation.Domains.Count)] - DC $($DC.HostName) [$CountDC/$($ForestInformation.DomainDomainControllers[$Domain].Count)]"
            $Data = [PSCustomObject] @{
                'Domain'                         = $Domain
                'DomainController'               = $DC.HostName
                'Error'                          = $DCSettings.Error
                'HasIssues'                      = $null
                'DynamicSiteName'                = $DCSettings.DynamicSiteName
                'SiteCoverageCount'              = $DCSettings.SiteCoverage.Count
                'GCSiteCoverageCount'            = $DCSettings.GCSiteCoverage.Count
                'SiteCoverage'                   = $DCSettings.SiteCoverage
                'GCSiteCoverage'                 = $DCSettings.GCSiteCoverage
                'NonExistingSiteCoverage'        = $WrongSiteCoverage
                'NonExistingGCSiteCoverage'      = $WrongGCSiteCoverage
                'NonExistingSiteCoverageCount'   = $WrongSiteCoverage.Count
                'NonExistingGCSiteCoverageCount' = $WrongGCSiteCoverage.Count
                'ErrorMessage'                   = $DCSettings.ErrorMessage
            }
            # If there are no non-existing sites, we are good
            if ($Data.NonExistingSiteCoverageCount -eq 0 -and $Data.NonExistingGCSiteCoverageCount -eq 0 -and $false -eq $Data.Error) {
                $Data.HasIssues = $true
            } else {
                $Data.HasIssues = $false
            }
            $Data
        }
    }
}
function Get-WinADSiteLinks {
    <#
    .SYNOPSIS
    Retrieves site links within an Active Directory forest.
 
    .DESCRIPTION
    This cmdlet retrieves and displays site links within an Active Directory forest. It can be used to identify the site links between sites, including their properties such as cost, replication frequency, and options. The cmdlet can also format the output to include or exclude specific details.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve site links from. If not specified, the current forest is used.
 
    .PARAMETER Splitter
    Specifies the character to use as a delimiter when joining multiple options into a single string. If not specified, options are returned as an array.
 
    .PARAMETER Formatted
    A switch parameter that controls the level of detail in the output. If set, the output includes all available site link properties in a formatted manner. If not set, the output is more concise.
 
    .PARAMETER ExtendedForestInformation
    A dictionary object that contains additional information about the forest. This parameter is optional and can be used to provide more context about the forest.
 
    .EXAMPLE
    Get-WinADSiteLinks -Forest "example.com" -Formatted
    This example retrieves all site links within the "example.com" forest, displaying detailed information in a formatted manner.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires access to the target forest.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [alias('Joiner')][string] $Splitter,
        [string] $Formatted,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    [Flags()]
    enum SiteLinksOptions {
        None = 0
        UseNotify = 1
        TwoWaySync = 2
        DisableCompression = 4
    }

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    $QueryServer = $ForestInformation.QueryServers[$($ForestInformation.Forest.Name)]['HostName'][0]
    $NamingContext = (Get-ADRootDSE -Server $QueryServer).configurationNamingContext
    $SiteLinks = Get-ADObject -LDAPFilter "(objectCategory=sitelink)" -SearchBase $NamingContext -Properties * -Server $QueryServer
    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 
                }
                #ReplInterval : 15
                Created                              = $_.WhenCreated
                Modified                             = $_.WhenChanged
                #Deleted :
                #InterSiteTransportProtocol : IP
                'Protected From Accidental Deletion' = $_.ProtectedFromAccidentalDeletion
            }
        } else {
            [PSCustomObject] @{
                Name                            = $_.CN
                Cost                            = $_.Cost
                ReplicationFrequencyInMinutes   = $_.ReplInterval
                Options                         = if ($Splitter -ne '') {
                    $Options -Join $Splitter 
                } else {
                    $Options 
                }
                #ReplInterval : 15
                Created                         = $_.WhenCreated
                Modified                        = $_.WhenChanged
                #Deleted :
                #InterSiteTransportProtocol : IP
                ProtectedFromAccidentalDeletion = $_.ProtectedFromAccidentalDeletion
            }
        }
    }
}
Function Get-WinADSiteOptions {
    <#
    .SYNOPSIS
    This function retrieves the site options for each Active Directory site.
 
    .DESCRIPTION
    The Get-WinADSiteOptions function retrieves the site options for each Active Directory site.
    It uses the nTDSSiteSettingsFlags enumeration to decode the site options.
 
    .PARAMETER None
    This function does not accept any parameters.
 
    .EXAMPLE
    Get-WinADSiteOptions
    #>

    [CmdletBinding()]
    Param(

    )

    [Flags()]
    enum nTDSSiteSettingsFlags {
        NTDSSETTINGS_OPT_IS_AUTO_TOPOLOGY_DISABLED = 0x1
        NTDSSETTINGS_OPT_IS_TOPL_CLEANUP_DISABLED = 0x2
        NTDSSETTINGS_OPT_IS_TOPL_MIN_HOPS_DISABLED = 0x4
        NTDSSETTINGS_OPT_IS_TOPL_DETECT_STALE_DISABLED = 0x8
        NTDSSETTINGS_OPT_IS_INTER_SITE_AUTO_TOPOLOGY_DISABLED = 0x10
        NTDSSETTINGS_OPT_IS_GROUP_CACHING_ENABLED = 0x20
        NTDSSETTINGS_OPT_FORCE_KCC_WHISTLER_BEHAVIOR = 0x40
        NTDSSETTINGS_OPT_FORCE_KCC_W2K_ELECTION = 0x80
        NTDSSETTINGS_OPT_IS_RAND_BH_SELECTION_DISABLED = 0x100
        NTDSSETTINGS_OPT_IS_SCHEDULE_HASHING_ENABLED = 0x200
        NTDSSETTINGS_OPT_IS_REDUNDANT_SERVER_TOPOLOGY_ENABLED = 0x400
        NTDSSETTINGS_OPT_W2K3_IGNORE_SCHEDULES = 0x800
        NTDSSETTINGS_OPT_W2K3_BRIDGES_REQUIRED = 0x1000
    }

    $RootDSE = Get-ADRootDSE
    $Sites = Get-ADObject -Filter 'objectClass -eq "site"' -SearchBase $RootDSE.ConfigurationNamingContext
    foreach ($Site In $Sites) {
        $SiteSettings = Get-ADObject "CN=NTDS Site Settings,$($Site.DistinguishedName)" -Properties Options
        If (-not $SiteSettings.PSObject.Properties.Match('Options').Count -OR $SiteSettings.Options -EQ 0) {

            [PSCustomObject]@{
                SiteName          = $Site.Name
                DistinguishedName = $Site.DistinguishedName
                SiteOptions       = '(none)'
            }
        } Else {
            [PSCustomObject]@{
                SiteName          = $Site.Name;
                DistinguishedName = $Site.DistinguishedName;
                Options           = $SiteSettings.Options
                SiteOptions       = [nTDSSiteSettingsFlags] $SiteSettings.Options
            }
        }
    }
}
function Get-WinADTombstoneLifetime {
    <#
    .SYNOPSIS
    Retrieves the tombstone lifetime for a specified Active Directory forest.
 
    .DESCRIPTION
    This function retrieves the tombstone lifetime for a specified Active Directory forest. If the tombstone lifetime is not explicitly set, it defaults to 60 days. The recommended value is 720 days, and the minimum value is 180 days.
 
    .PARAMETER Forest
    Specifies the name of the Active Directory forest to retrieve the tombstone lifetime for. If not specified, the current forest is used.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest to aid in the retrieval process.
 
    .EXAMPLE
    Get-WinADTombstoneLifetime -Forest "example.com"
    This example retrieves the tombstone lifetime for the "example.com" forest.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires access to the target forest.
    #>

    [Alias('Get-WinADForestTombstoneLifetime')]
    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    # Check tombstone lifetime (if blank value is 60)
    # Recommended value 720
    # Minimum value 180
    $QueryServer = $ForestInformation.QueryServers[$($ForestInformation.Forest.Name)]['HostName'][0]
    $RootDSE = Get-ADRootDSE -Server $QueryServer
    $Output = (Get-ADObject -Server $QueryServer -Identity "CN=Directory Service,CN=Windows NT,CN=Services,$(($RootDSE).configurationNamingContext)" -Properties tombstoneLifetime)
    if ($null -eq $Output -or $null -eq $Output.tombstoneLifetime) {
        [PSCustomObject] @{
            TombstoneLifeTime = 60
        }
    } else {
        [PSCustomObject] @{
            TombstoneLifeTime = $Output.tombstoneLifetime
        }
    }
}

function Get-WinADTrust {
    <#
    .SYNOPSIS
    Retrieves trust relationships within an Active Directory forest.
 
    .DESCRIPTION
    This cmdlet retrieves and displays trust relationships within an Active Directory forest. It can be used to identify the trust relationships between domains and forests, including the type of trust, direction, and other properties. The cmdlet can also recursively explore trust relationships across multiple forests.
 
    .PARAMETER Forest
    Specifies the target forest to retrieve trust relationships from. If not specified, the current forest is used.
 
    .PARAMETER Recursive
    Indicates that the cmdlet should recursively explore trust relationships across multiple forests.
 
    .PARAMETER Nesting
    This parameter is used internally to track the nesting level of recursive calls. It should not be used directly.
 
    .PARAMETER UniqueTrusts
    This parameter is used internally to keep track of unique trust relationships encountered during recursive exploration. It should not be used directly.
 
    .EXAMPLE
    Get-WinADTrust -Recursive
    This example retrieves all trust relationships within the current forest and recursively explores trust relationships across multiple forests.
 
    .NOTES
    This cmdlet is designed to provide detailed information about trust relationships within an Active Directory environment. It can be used for auditing, troubleshooting, and planning purposes.
    #>

    [alias('Get-WinADTrusts')]
    [cmdletBinding()]
    param(
        [string] $Forest,
        [switch] $Recursive,
        [Parameter(DontShow)][int] $Nesting = -1,
        [Parameter(DontShow)][System.Collections.IDictionary] $UniqueTrusts
    )
    Begin {
        if ($Nesting -eq -1) {
            $UniqueTrusts = [ordered]@{}
        }
    }
    Process {
        $Nesting++
        $ForestInformation = Get-WinADForest -Forest $Forest
        [Array] $Trusts = @(
            try {
                $TrustRelationship = $ForestInformation.GetAllTrustRelationships()
                foreach ($Trust in $TrustRelationship) {
                    [ordered] @{
                        Type          = 'Forest'
                        Details       = $Trust
                        ExecuteObject = $ForestInformation
                    }
                }
            } catch {
                Write-Warning "Get-WinADForest - Can't process trusts for $Forest, error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
            }
            foreach ($Domain in $ForestInformation.Domains) {
                $DomainInformation = Get-WinADDomain -Domain $Domain.Name
                try {
                    $TrustRelationship = $DomainInformation.GetAllTrustRelationships()
                    foreach ($Trust in $TrustRelationship) {
                        [ordered] @{
                            Type          = 'Domain'
                            Details       = $Trust
                            ExecuteObject = $DomainInformation
                        }
                    }
                } catch {
                    Write-Warning "Get-WinADForest - Can't process trusts for $Domain, error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
                }
            }
        )
        [Array] $Output = foreach ($Trust in $Trusts) {
            Write-Verbose "Get-WinADTrust - From: $($Trust.Details.SourceName) To: $($Trust.Details.TargetName) Nesting: $Nesting"
            $UniqueID1 = -join ($Trust.Details.SourceName, $Trust.Details.TargetName)
            $UniqueID2 = -join ($Trust.Details.TargetName, $Trust.Details.SourceName)
            if (-not $UniqueTrusts[$UniqueID1]) {
                $UniqueTrusts[$UniqueID1] = $true
            } else {
                Write-Verbose "Get-WinADTrust - Trust already on the list (From: $($Trust.Details.SourceName) To: $($Trust.Details.TargetName) Nesting: $Nesting)"
                continue
            }
            if (-not $UniqueTrusts[$UniqueID2]) {
                $UniqueTrusts[$UniqueID2] = $true
            } else {
                Write-Verbose "Get-WinADTrust - Trust already on the list (Reverse) (From: $($Trust.Details.TargetName) To: $($Trust.Details.SourceName) Nesting: $Nesting"
                continue
            }
            $TrustObject = Get-WinADTrustObject -Domain $Trust.ExecuteObject.Name -AsHashTable
            # https://github.com/vletoux/pingcastle/issues/9
            if ($TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Enable TGT DELEGATION") {
                $TGTDelegation = $true
            } elseif ($TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "No TGT DELEGATION") {
                $TGTDelegation = $false
            } else {
                # Assuming all patches are installed (past July 2019)
                $TGTDelegation = $false
            }

            $TrustStatus = Test-DomainTrust -Domain $Trust.Details.SourceName -TrustedDomain $Trust.Details.TargetName
            $GroupExists = Get-WinADObject -Identity 'S-1-5-32-544' -DomainName $Trust.Details.TargetName
            [PsCustomObject] @{
                'TrustSource'             = $Trust.Details.SourceName #$Domain
                'TrustTarget'             = $Trust.Details.TargetName #$Trust.Target
                'TrustDirection'          = $Trust.Details.TrustDirection.ToString() #$Trust.Direction.ToString()
                'TrustBase'               = $Trust.Type
                'TrustType'               = $Trust.Details.TrustType.ToString()
                'TrustTypeAD'             = $TrustObject[$Trust.Details.TargetName].TrustType
                'Level'                   = $Nesting
                'SuffixesIncluded'        = (($Trust.Details.TopLevelNames | Where-Object { $_.Status -eq 'Enabled' }).Name) -join ', '
                'SuffixesExcluded'        = $Trust.Details.ExcludedTopLevelNames.Name
                'TrustAttributes'         = $TrustObject[$Trust.Details.TargetName].TrustAttributes -join ', '
                'TrustStatus'             = $TrustStatus.TrustStatus
                'QueryStatus'             = if ($GroupExists) {
                    'OK' 
                } else {
                    'NOT OK' 
                }
                'ForestTransitive'        = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Forest Transitive"
                'SelectiveAuthentication' = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Cross Organization"
                #'SIDFilteringForestAware' = $null
                'SIDFilteringQuarantined' = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Quarantined Domain"
                'DisallowTransitivity'    = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Non Transitive"
                'IntraForest'             = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Within Forest"
                #'IsTreeParent' = $null #$Trust.IsTreeParent
                #'IsTreeRoot' = $Trust.Details.TrustType.ToString() -eq 'TreeRoot'
                'IsTGTDelegationEnabled'  = $TGTDelegation
                #'TrustedPolicy' = $null #$Trust.TrustedPolicy
                #'TrustingPolicy' = $null #$Trust.TrustingPolicy
                'UplevelOnly'             = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "UpLevel Only"
                'UsesAESKeys'             = $TrustObject[$Trust.Details.TargetName].msDSSupportedEncryptionTypes -contains "AES128-CTS-HMAC-SHA1-96" -or $TrustObject[$Trust.Details.TargetName].msDSSupportedEncryptionTypes -contains 'AES256-CTS-HMAC-SHA1-96'
                'UsesRC4Encryption'       = $TrustObject[$Trust.Details.TargetName].TrustAttributes -contains "Uses RC4 Encryption"
                'EncryptionTypes'         = $TrustObject[$Trust.Details.TargetName].msDSSupportedEncryptionTypes -join ', '
                'TrustSourceDC'           = $TrustStatus.TrustSourceDC
                'TrustTargetDC'           = $TrustStatus.TrustTargetDC
                'ObjectGUID'              = $TrustObject[$Trust.Details.TargetName].ObjectGuid
                'ObjectSID'               = $TrustObject[$Trust.Details.TargetName].ObjectSID
                'Created'                 = $TrustObject[$Trust.Details.TargetName].WhenCreated
                'Modified'                = $TrustObject[$Trust.Details.TargetName].WhenChanged
                'TrustDirectionText'      = $TrustObject[$Trust.Details.TargetName].TrustDirectionText
                'TrustTypeText'           = $TrustObject[$Trust.Details.TargetName].TrustTypeText
                'AdditionalInformation'   = [ordered] @{
                    'msDSSupportedEncryptionTypes' = $TrustObject[$Trust.Details.TargetName].msDSSupportedEncryptionTypes
                    'msDSTrustForestTrustInfo'     = $TrustObject[$Trust.Details.TargetName].msDSTrustForestTrustInfo
                    'SuffixesInclude'              = $Trust.Details.TopLevelNames
                    'SuffixesExclude'              = $Trust.Details.ExcludedTopLevelNames
                    'TrustObject'                  = $TrustObject
                    'GroupExists'                  = $GroupExists
                }
            }
        }
        if ($Output -and $Output.Count -gt 0) {
            $Output
        }
        if ($Recursive) {
            foreach ($Trust in $Output) {
                if ($Trust.TrustType -notin 'TreeRoot', 'ParentChild') {
                    Get-WinADTrust -Forest $Trust.TrustTarget -Recursive -Nesting $Nesting -UniqueTrusts $UniqueTrusts
                }
            }
        }
    }
}
function Get-WinADTrustLegacy {
    <#
    .SYNOPSIS
    Retrieves trust relationships within a specified Active Directory forest, including legacy trusts.
 
    .DESCRIPTION
    This cmdlet is designed to gather detailed information about trust relationships within an Active Directory forest. It can target a specific forest, include or exclude specific domains, and display the results in a detailed or concise format. Additionally, it can filter out duplicate trusts and provide extended forest information.
 
    .PARAMETER Forest
    Specifies the target forest to query. If not provided, the current forest is used by default.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the search. This parameter is optional and can be used to narrow down the search scope.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the search. This parameter is optional and can be used to limit the search scope to specific domains.
 
    .PARAMETER Display
    A switch parameter that controls the level of detail in the output. If set, the output includes all available trust properties. If not set, the output is more concise.
 
    .PARAMETER ExtendedForestInformation
    A dictionary object that contains additional information about the forest. This parameter is optional and can be used to provide more context about the forest.
 
    .PARAMETER Unique
    A switch parameter that filters out duplicate trusts based on the source and target domains. If set, only unique trusts are returned.
 
    .EXAMPLE
    Get-WinADTrustLegacy -Display -Unique
 
    This example retrieves all trust relationships within the current forest, displaying detailed information and filtering out duplicate trusts.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported. It also requires access to the target forest and domains.
    #>

    [CmdletBinding()]
    param(
        [string] $Forest,
        [alias('Domain')][string[]] $IncludeDomains,
        [string[]] $ExcludeDomains,
        [switch] $Display,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [switch] $Unique
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    $UniqueTrusts = [ordered]@{}
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        $Trusts = Get-ADTrust -Server $QueryServer -Filter "*" -Properties *
        $DomainPDC = $ForestInformation['DomainDomainControllers'][$Domain] | Where-Object { $_.IsPDC -eq $true }

        $PropertiesTrustWMI = @(
            'FlatName',
            'SID',
            'TrustAttributes',
            'TrustDirection',
            'TrustedDCName',
            'TrustedDomain',
            'TrustIsOk',
            'TrustStatus',
            'TrustStatusString', # TrustIsOk/TrustStatus are covered by this
            'TrustType'
        )
        $TrustStatatuses = Get-CimInstance -ClassName Microsoft_DomainTrustStatus -Namespace root\MicrosoftActiveDirectory -ComputerName $DomainPDC.HostName -ErrorAction SilentlyContinue -Verbose:$false -Property $PropertiesTrustWMI

        $ReturnData = foreach ($Trust in $Trusts) {
            if ($Unique) {
                $UniqueID1 = -join ($Domain, $Trust.trustPartner)
                $UniqueID2 = -join ($Trust.trustPartner, $Domain)
                if (-not $UniqueTrusts[$UniqueID1]) {
                    $UniqueTrusts[$UniqueID1] = $true
                } else {
                    continue
                }
                if (-not $UniqueTrusts[$UniqueID2]) {
                    $UniqueTrusts[$UniqueID2] = $true
                } else {
                    continue
                }
            }
            $TrustWMI = $TrustStatatuses | & { process {
                    if ($_.TrustedDomain -eq $Trust.Target ) {
                        $_ 
                    } 
                } }
            if ($Display) {
                [PsCustomObject] @{
                    'Trust Source'               = $Domain
                    'Trust Target'               = $Trust.Target
                    'Trust Direction'            = $Trust.Direction.ToString()
                    'Trust Attributes'           = if ($Trust.TrustAttributes -is [int]) {
 (Get-ADTrustAttributes -Value $Trust.TrustAttributes) -join '; ' 
                    } else {
                        'Error - needs fixing' 
                    }
                    'Trust Status'               = if ($null -ne $TrustWMI) {
                        $TrustWMI.TrustStatusString 
                    } else {
                        'N/A' 
                    }
                    'Forest Transitive'          = $Trust.ForestTransitive
                    'Selective Authentication'   = $Trust.SelectiveAuthentication
                    'SID Filtering Forest Aware' = $Trust.SIDFilteringForestAware
                    'SID Filtering Quarantined'  = $Trust.SIDFilteringQuarantined
                    'Disallow Transivity'        = $Trust.DisallowTransivity
                    'Intra Forest'               = $Trust.IntraForest
                    'Is Tree Parent'             = $Trust.IsTreeParent
                    'Is Tree Root'               = $Trust.IsTreeRoot
                    'TGTDelegation'              = $Trust.TGTDelegation
                    'TrustedPolicy'              = $Trust.TrustedPolicy
                    'TrustingPolicy'             = $Trust.TrustingPolicy
                    'TrustType'                  = $Trust.TrustType.ToString()
                    'UplevelOnly'                = $Trust.UplevelOnly
                    'UsesAESKeys'                = $Trust.UsesAESKeys
                    'UsesRC4Encryption'          = $Trust.UsesRC4Encryption
                    'Trust Source DC'            = if ($null -ne $TrustWMI) {
                        $TrustWMI.PSComputerName 
                    } else {
                        '' 
                    }
                    'Trust Target DC'            = if ($null -ne $TrustWMI) {
                        $TrustWMI.TrustedDCName.Replace('\\', '') 
                    } else {
                        '' 
                    }
                    'Trust Source DN'            = $Trust.Source
                    'ObjectGUID'                 = $Trust.ObjectGUID
                    'Created'                    = $Trust.Created
                    'Modified'                   = $Trust.Modified
                    'Deleted'                    = $Trust.Deleted
                    'SID'                        = $Trust.securityIdentifier
                    'TrustOK'                    = if ($null -ne $TrustWMI) {
                        $TrustWMI.TrustIsOK 
                    } else {
                        $false 
                    }
                    'TrustStatus'                = if ($null -ne $TrustWMI) {
                        $TrustWMI.TrustStatus 
                    } else {
                        -1 
                    }
                }
            } else {
                [PsCustomObject] @{
                    'TrustSource'               = $Domain
                    'TrustTarget'               = $Trust.Target
                    'TrustDirection'            = $Trust.Direction.ToString()
                    'TrustAttributes'           = if ($Trust.TrustAttributes -is [int]) {
                        Get-ADTrustAttributes -Value $Trust.TrustAttributes 
                    } else {
                        'Error - needs fixing' 
                    }
                    'TrustStatus'               = if ($null -ne $TrustWMI) {
                        $TrustWMI.TrustStatusString 
                    } else {
                        'N/A' 
                    }
                    'ForestTransitive'          = $Trust.ForestTransitive
                    'SelectiveAuthentication'   = $Trust.SelectiveAuthentication
                    'SIDFiltering Forest Aware' = $Trust.SIDFilteringForestAware
                    'SIDFiltering Quarantined'  = $Trust.SIDFilteringQuarantined
                    'DisallowTransivity'        = $Trust.DisallowTransivity
                    'IntraForest'               = $Trust.IntraForest
                    'IsTreeParent'              = $Trust.IsTreeParent
                    'IsTreeRoot'                = $Trust.IsTreeRoot
                    'TGTDelegation'             = $Trust.TGTDelegation
                    'TrustedPolicy'             = $Trust.TrustedPolicy
                    'TrustingPolicy'            = $Trust.TrustingPolicy
                    'TrustType'                 = $Trust.TrustType.ToString()
                    'UplevelOnly'               = $Trust.UplevelOnly
                    'UsesAESKeys'               = $Trust.UsesAESKeys
                    'UsesRC4Encryption'         = $Trust.UsesRC4Encryption
                    'TrustSourceDC'             = if ($null -ne $TrustWMI) {
                        $TrustWMI.PSComputerName 
                    } else {
                        '' 
                    }
                    'TrustTargetDC'             = if ($null -ne $TrustWMI) {
                        $TrustWMI.TrustedDCName.Replace('\\', '') 
                    } else {
                        '' 
                    }
                    'TrustSourceDN'             = $Trust.Source
                    'ObjectGUID'                = $Trust.ObjectGUID
                    'Created'                   = $Trust.Created
                    'Modified'                  = $Trust.Modified
                    'Deleted'                   = $Trust.Deleted
                    'SID'                       = $Trust.securityIdentifier
                    'TrustOK'                   = if ($null -ne $TrustWMI) {
                        $TrustWMI.TrustIsOK 
                    } else {
                        $false 
                    }
                    'TrustStatusInt'            = if ($null -ne $TrustWMI) {
                        $TrustWMI.TrustStatus 
                    } else {
                        -1 
                    }
                }
            }
        }
        $ReturnData
    }
}
function Get-WinADUserPrincipalName {
    <#
    .SYNOPSIS
    Modifies the UserPrincipalName of a user object based on specified parameters.
 
    .DESCRIPTION
    This function takes a user object and a domain name as input. It can modify the UserPrincipalName of the user object based on the following options:
    - Replace the domain part of the UserPrincipalName with the specified domain name.
    - Construct a new UserPrincipalName in the format GivenName.Surname@DomainName.
    - Remove Latin characters from the UserPrincipalName.
    - Convert the UserPrincipalName to lowercase.
 
    .PARAMETER User
    The user object whose UserPrincipalName is to be modified.
 
    .PARAMETER DomainName
    The domain name to be used for replacing the domain part of the UserPrincipalName or constructing a new UserPrincipalName.
 
    .PARAMETER ReplaceDomain
    Switch to replace the domain part of the UserPrincipalName with the specified domain name.
 
    .PARAMETER NameSurname
    Switch to construct a new UserPrincipalName in the format GivenName.Surname@DomainName.
 
    .PARAMETER FixLatinChars
    Switch to remove Latin characters from the UserPrincipalName.
 
    .PARAMETER ToLower
    Switch to convert the UserPrincipalName to lowercase.
 
    .EXAMPLE
    Get-WinADUserPrincipalName -User $userObject -DomainName "example.com" -ReplaceDomain
    Replaces the domain part of the UserPrincipalName with "example.com".
 
    .EXAMPLE
    Get-WinADUserPrincipalName -User $userObject -DomainName "example.com" -NameSurname
    Constructs a new UserPrincipalName in the format GivenName.Surname@example.com.
 
    .EXAMPLE
    Get-WinADUserPrincipalName -User $userObject -DomainName "example.com" -FixLatinChars
    Removes Latin characters from the UserPrincipalName.
 
    .EXAMPLE
    Get-WinADUserPrincipalName -User $userObject -DomainName "example.com" -ToLower
    Converts the UserPrincipalName to lowercase.
    #>

    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true)][Object] $User,
        [Parameter(Mandatory = $true)][string] $DomainName,
        [switch] $ReplaceDomain,
        [switch] $NameSurname,
        [switch] $FixLatinChars,
        [switch] $ToLower
    )
    if ($User.UserPrincipalName) {
        $NewUserName = $User.UserPrincipalName

        if ($ReplaceDomain) {
            $NewUserName = ($User.UserPrincipalName -split '@')[0]
            $NewUserName = -join ($NewUserName, '@', $DomainName)
        }
        if ($NameSurname) {
            if ($User.GivenName -and $User.Surname) {
                $NewUsername = -join ($User.GivenName, '.', $User.Surname, '@', $DomainName)
            } else {
                Write-Warning "Get-WinADUserPrincipalName - UserPrincipalName couldn't be changed to GivenName.SurName@$DomainName"
            }
        }

        if ($FixLatinChars) {
            $NewUsername = Remove-StringLatinCharacters -String $NewUsername
        }
        if ($ToLower) {
            $NewUsername = $NewUserName.ToLower()
        }

        if ($NewUserName -eq $User.UserPrincipalName) {
            Write-Warning "Get-WinADUserPrincipalName - UserPrincipalName didn't change. Stays as $NewUserName"
        }
        $NewUsername
    }
}
function Get-WinADUsers {
    <#
    .SYNOPSIS
    Get-WinADUsers is a function that retrieves all users from Active Directory.
    It can be used to retrieve users from a single domain or from all domains in the forest.
 
    .DESCRIPTION
    Get-WinADUsers is a function that retrieves all users from Active Directory.
    It can be used to retrieve users from a single domain or from all domains in the forest.
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER PerDomain
    Return results per domain
 
    .PARAMETER AddOwner
    Add Owner information to the output
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [switch] $PerDomain,
        [switch] $AddOwner
    )
    $AllUsers = [ordered] @{}
    $AllContacts = [ordered] @{}
    $AllGroups = [ordered] @{}
    $CacheUsersReport = [ordered] @{}
    $Today = Get-Date
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    $ErrorCount = 0
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]

        $Properties = @(
            'DistinguishedName', 'mail', 'LastLogonDate', 'PasswordLastSet', 'DisplayName', 'Manager', 'Description',
            'PasswordNeverExpires', 'PasswordNotRequired', 'PasswordExpired', 'UserPrincipalName', 'SamAccountName', 'CannotChangePassword',
            'TrustedForDelegation', 'TrustedToAuthForDelegation', 'msExchMailboxGuid', 'msExchRemoteRecipientType', 'msExchRecipientTypeDetails',
            'msExchRecipientDisplayType', 'pwdLastSet', "msDS-UserPasswordExpiryTimeComputed",
            'WhenCreated', 'WhenChanged'
            'nTSecurityDescriptor',
            'Country', 'Title', 'Department'
            'msds-resultantpso'
        )
        try {
            $AllUsers[$Domain] = Get-ADUser -Filter "*" -Properties $Properties -Server $QueryServer #$ForestInformation['QueryServers'][$Domain].HostName[0]
        } catch {
            $ErrorCount++
            Write-Warning -Message "Get-WinADUsers - Failed to get users from $Domain using $($QueryServer). Error: $($_.Exception.Message)"
        }
        try {
            $AllContacts[$Domain] = Get-ADObject -Filter 'objectClass -eq "contact"' -Properties SamAccountName, Mail, Name, DistinguishedName, WhenChanged, Whencreated, DisplayName -Server $QueryServer
        } catch {
            $ErrorCount++
            Write-Warning -Message "Get-WinADUsers - Failed to get contacts from $Domain using $($QueryServer). Error: $($_.Exception.Message)"
        }
        $Properties = @(
            'SamAccountName', 'CanonicalName', 'Mail', 'Name', 'DistinguishedName', 'isCriticalSystemObject', 'ObjectSID'
        )
        try {
            $AllGroups[$Domain] = Get-ADGroup -Filter "*" -Properties $Properties -Server $QueryServer
        } catch {
            $ErrorCount++
            Write-Warning -Message "Get-WinADUsers - Failed to get groups from $Domain using $($QueryServer). Error: $($_.Exception.Message)"
        }
    }
    if ($ErrorCount -gt 0) {
        Write-Warning -Message "Get-WinADUsers - Failed to get data from domains. Found $ErrorCount errors. Please check the error messages above."
        return
    }

    foreach ($Domain in $AllUsers.Keys) {
        foreach ($U in $AllUsers[$Domain]) {
            $CacheUsersReport[$U.DistinguishedName] = $U
        }
    }
    foreach ($Domain in $AllContacts.Keys) {
        foreach ($C in $AllContacts[$Domain]) {
            $CacheUsersReport[$C.DistinguishedName] = $C
        }
    }
    foreach ($Domain in $AllGroups.Keys) {
        foreach ($G in $AllGroups[$Domain]) {
            $CacheUsersReport[$G.DistinguishedName] = $G
        }
    }

    $PasswordPolicies = Get-WinADPasswordPolicy -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ReturnHashtable

    $Output = [ordered] @{}
    foreach ($Domain in $ForestInformation.Domains) {
        $Output[$Domain] = foreach ($User in $AllUsers[$Domain]) {
            $UserLocation = ($User.DistinguishedName -split ',').Replace('OU=', '').Replace('CN=', '').Replace('DC=', '')
            $Region = $UserLocation[-4]
            $Country = $UserLocation[-5]

            if ($User.LastLogonDate) {
                $LastLogonDays = $( - $($User.LastLogonDate - $Today).Days)
            } else {
                $LastLogonDays = $null
            }
            if ($User.PasswordLastSet) {
                $PasswordLastDays = $( - $($User.PasswordLastSet - $Today).Days)
            } else {
                $PasswordLastDays = $null
            }
            if ($User.Manager) {
                $Manager = $CacheUsersReport[$User.Manager].Name
                $ManagerSamAccountName = $CacheUsersReport[$User.Manager].SamAccountName
                $ManagerEmail = $CacheUsersReport[$User.Manager].Mail
                $ManagerEnabled = $CacheUsersReport[$User.Manager].Enabled
                $ManagerLastLogon = $CacheUsersReport[$User.Manager].LastLogonDate
                if ($ManagerLastLogon) {
                    $ManagerLastLogonDays = $( - $($ManagerLastLogon - $Today).Days)
                } else {
                    $ManagerLastLogonDays = $null
                }
                $ManagerStatus = if ($ManagerEnabled -eq $true) {
                    'Enabled' 
                } elseif ($ManagerEnabled -eq $false) {
                    'Disabled' 
                } else {
                    'Not available' 
                }
            } else {
                if ($User.ObjectClass -eq 'user') {
                    $ManagerStatus = 'Missing'
                } else {
                    $ManagerStatus = 'Not available'
                }
                $Manager = $null
                $ManagerSamAccountName = $null
                $ManagerEmail = $null
                $ManagerEnabled = $null
                $ManagerLastLogon = $null
                $ManagerLastLogonDays = $null
            }

            if ($User."msDS-UserPasswordExpiryTimeComputed" -ne 9223372036854775807) {
                # This is standard situation where users password is expiring as needed
                try {
                    $DateExpiry = ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed"))
                } catch {
                    $DateExpiry = $User."msDS-UserPasswordExpiryTimeComputed"
                }
                try {
                    $DaysToExpire = (New-TimeSpan -Start (Get-Date) -End ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed"))).Days
                } catch {
                    $DaysToExpire = $null
                }
                $PasswordNeverExpires = $User.PasswordNeverExpires
            } else {
                # This is non-standard situation. This basically means most likely Fine Grained Group Policy is in action where it makes PasswordNeverExpires $true
                # Since FGP policies are a bit special they do not tick the PasswordNeverExpires box, but at the same time value for "msDS-UserPasswordExpiryTimeComputed" is set to 9223372036854775807
                $PasswordNeverExpires = $true
            }
            if ($PasswordNeverExpires -or $null -eq $User.PasswordLastSet) {
                $DateExpiry = $null
                $DaysToExpire = $null
            }

            if ($User.'msExchMailboxGuid') {
                $HasMailbox = $true
            } else {
                $HasMailbox = $false
            }
            $msExchRecipientTypeDetails = Convert-ExchangeRecipient -msExchRecipientTypeDetails $User.msExchRecipientTypeDetails
            $msExchRecipientDisplayType = Convert-ExchangeRecipient -msExchRecipientDisplayType $User.msExchRecipientDisplayType
            $msExchRemoteRecipientType = Convert-ExchangeRecipient -msExchRemoteRecipientType $User.msExchRemoteRecipientType

            if ($User.'msds-resultantpso') {
                # $PasswordPolicy = 'FineGrained'
                if ($PasswordPolicies[$User.'msds-resultantpso']) {
                    $PasswordPolicyName = $PasswordPolicies[$User.'msds-resultantpso'].Name
                    $PasswordPolicyLength = $PasswordPolicies[$User.'msds-resultantpso'].MinPasswordLength
                } else {
                    $PasswordPolicyName = ConvertFrom-DistinguishedName -DistinguishedName $User.'msds-resultantpso'
                    $PasswordPolicyLength = 'No permission'
                }
            } else {
                # $PasswordPolicy = 'Default'
                $PasswordPolicyName = 'Default Password Policy'
                $PasswordPolicyLength = $PasswordPolicies[$Domain].MinPasswordLength
            }

            if ($AddOwner) {
                $Owner = Get-ADACLOwner -ADObject $User -Verbose -Resolve
                [PSCustomObject] @{
                    Name                      = $User.Name
                    SamAccountName            = $User.SamAccountName
                    Domain                    = $Domain
                    WhenChanged               = $User.WhenChanged
                    Enabled                   = $User.Enabled
                    ObjectClass               = $User.ObjectClass
                    #IsMissing = if ($Group) { $false } else { $true }
                    HasMailbox                = $HasMailbox
                    MustChangePasswordAtLogon = if ($User.pwdLastSet -eq 0 -and $User.PasswordExpired -eq $true) {
                        $true 
                    } else {
                        $false 
                    }
                    #PasswordPolicy = $PasswordPolicy
                    PasswordPolicyName        = $PasswordPolicyName
                    PasswordPolicyMinLength   = $PasswordPolicyLength
                    PasswordNeverExpires      = $PasswordNeverExpires
                    PasswordNotRequired       = $User.PasswordNotRequired
                    LastLogonDays             = $LastLogonDays
                    PasswordLastDays          = $PasswordLastDays
                    DaysToExpire              = $DaysToExpire
                    ManagerStatus             = $ManagerStatus
                    Manager                   = $Manager
                    ManagerSamAccountName     = $ManagerSamAccountName
                    ManagerEmail              = $ManagerEmail
                    ManagerLastLogonDays      = $ManagerLastLogonDays
                    OwnerName                 = $Owner.OwnerName
                    OwnerSID                  = $Owner.OwnerSID
                    OwnerType                 = $Owner.OwnerType
                    Level0                    = $Region
                    Level1                    = $Country
                    Title                     = $User.'Title'
                    Department                = $User.'Department'
                    Country                   = Convert-CountryCodeToCountry -CountryCode $User.Country
                    DistinguishedName         = $User.DistinguishedName
                    LastLogonDate             = $User.LastLogonDate
                    PasswordLastSet           = $User.PasswordLastSet
                    PasswordExpiresOn         = $DateExpiry
                    PasswordExpired           = $User.PasswordExpired
                    CannotChangePassword      = $User.CannotChangePassword
                    TrustedForDelegation      = $User.TrustedForDelegation
                    ManagerDN                 = $User.Manager
                    ManagerLastLogon          = $ManagerLastLogon
                    Group                     = $Group
                    Description               = $User.Description
                    UserPrincipalName         = $User.UserPrincipalName
                    RecipientTypeDetails      = $msExchRecipientTypeDetails
                    RecipientDisplayType      = $msExchRecipientDisplayType
                    RemoteRecipientType       = $msExchRemoteRecipientType
                    WhenCreated               = $User.WhenCreated
                }
            } else {
                [PSCustomObject] @{
                    Name                      = $User.Name
                    SamAccountName            = $User.SamAccountName
                    Domain                    = $Domain
                    WhenChanged               = $User.WhenChanged
                    Enabled                   = $User.Enabled
                    ObjectClass               = $User.ObjectClass
                    #IsMissing = if ($Group) { $false } else { $true }
                    HasMailbox                = $HasMailbox
                    MustChangePasswordAtLogon = if ($User.pwdLastSet -eq 0 -and $User.PasswordExpired -eq $true) {
                        $true 
                    } else {
                        $false 
                    }
                    #PasswordPolicy = $PasswordPolicy
                    PasswordPolicyName        = $PasswordPolicyName
                    PasswordPolicyMinLength   = $PasswordPolicyLength
                    PasswordNeverExpires      = $PasswordNeverExpires
                    PasswordNotRequired       = $User.PasswordNotRequired
                    LastLogonDays             = $LastLogonDays
                    PasswordLastDays          = $PasswordLastDays
                    DaysToExpire              = $DaysToExpire
                    ManagerStatus             = $ManagerStatus
                    Manager                   = $Manager
                    ManagerSamAccountName     = $ManagerSamAccountName
                    ManagerEmail              = $ManagerEmail
                    ManagerLastLogonDays      = $ManagerLastLogonDays
                    Level0                    = $Region
                    Level1                    = $Country
                    Title                     = $User.'Title'
                    Department                = $User.'Department'
                    Country                   = Convert-CountryCodeToCountry -CountryCode $User.Country
                    DistinguishedName         = $User.DistinguishedName
                    LastLogonDate             = $User.LastLogonDate
                    PasswordLastSet           = $User.PasswordLastSet
                    PasswordExpiresOn         = $DateExpiry
                    PasswordExpired           = $User.PasswordExpired
                    CannotChangePassword      = $User.CannotChangePassword
                    TrustedForDelegation      = $User.TrustedForDelegation
                    ManagerDN                 = $User.Manager
                    ManagerLastLogon          = $ManagerLastLogon
                    Group                     = $Group
                    Description               = $User.Description
                    UserPrincipalName         = $User.UserPrincipalName
                    RecipientTypeDetails      = $msExchRecipientTypeDetails
                    RecipientDisplayType      = $msExchRecipientDisplayType
                    RemoteRecipientType       = $msExchRemoteRecipientType
                    WhenCreated               = $User.WhenCreated
                }
            }
        }
    }
    if ($PerDomain) {
        $Output
    } else {
        $Output.Values
    }
}
function Get-WinADUsersForeignSecurityPrincipalList {
    <#
    .SYNOPSIS
    Retrieves a list of foreign security principals from the specified Active Directory forest or domains.
 
    .DESCRIPTION
    This cmdlet retrieves a list of foreign security principals from the specified Active Directory forest or domains. It supports the option to include or exclude specific domains and translates the security identifiers to NTAccount format for easier identification.
 
    .PARAMETER Forest
    Specifies the name of the Active Directory forest to retrieve foreign security principals from. If not specified, the current forest is used.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the retrieval process. If not specified, all domains in the forest are included.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the retrieval process.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest to aid in the retrieval process.
 
    .EXAMPLE
    Get-WinADUsersForeignSecurityPrincipalList -Forest "example.com" -IncludeDomains "example.com", "subdomain.example.com"
 
    This example retrieves the list of foreign security principals from the "example.com" and "subdomain.example.com" domains in the "example.com" forest.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported.
    #>

    [CmdletBinding()]
    [alias('Get-WinADUsersFP')]
    param(
        [alias('ForestName')][string] $Forest,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [string[]] $ExcludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        $ForeignSecurityPrincipalList = Get-ADObject -Filter "ObjectClass -eq 'ForeignSecurityPrincipal'" -Properties * -Server $QueryServer
        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 Get-WinADWellKnownFolders {
    <#
    .SYNOPSIS
    Retrieves well-known folders for each domain in a forest.
 
    .DESCRIPTION
    This cmdlet retrieves the well-known folders for each domain in a specified forest. It supports the option to include or exclude specific domains and returns the results as a custom object or a hashtable.
 
    .PARAMETER Forest
    Specifies the name of the forest to retrieve well-known folders from.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the retrieval process. If not specified, all domains in the forest will be included.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the retrieval process.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest to aid in the retrieval process.
 
    .PARAMETER AsCustomObject
    If specified, the cmdlet returns the results as a custom object. Otherwise, it returns a hashtable.
 
    .EXAMPLE
    Get-WinADWellKnownFolders -Forest "example.com" -IncludeDomains "example.com", "subdomain.example.com"
 
    This example retrieves the well-known folders for the "example.com" and "subdomain.example.com" domains in the "example.com" forest.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported.
    #>

    [cmdletBinding()]
    param(
        [string] $Forest,
        [alias('Domain')][string[]] $IncludeDomains,
        [string[]] $ExcludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [switch] $AsCustomObject
    )
    $ForestInformation = Get-WinADForestDetails -Extended -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Domain in $ForestInformation.Domains) {
        $DomainInformation = Get-ADDomain -Server $Domain
        $WellKnownFolders = $DomainInformation | Select-Object -Property UsersContainer, ComputersContainer, DomainControllersContainer, DeletedObjectsContainer, SystemsContainer, LostAndFoundContainer, QuotasContainer, ForeignSecurityPrincipalsContainer
        $CurrentWellKnownFolders = [ordered] @{ }
        foreach ($_ in $WellKnownFolders.PSObject.Properties.Name) {
            $CurrentWellKnownFolders[$_] = $DomainInformation.$_
        }
        <#
        $DomainDistinguishedName = $DomainInformation.DistinguishedName
        $DefaultWellKnownFolders = [ordered] @{
            UsersContainer = "CN=Users,$DomainDistinguishedName"
            ComputersContainer = "CN=Computers,$DomainDistinguishedName"
            DomainControllersContainer = "OU=Domain Controllers,$DomainDistinguishedName"
            DeletedObjectsContainer = "CN=Deleted Objects,$DomainDistinguishedName"
            SystemsContainer = "CN=System,$DomainDistinguishedName"
            LostAndFoundContainer = "CN=LostAndFound,$DomainDistinguishedName"
            QuotasContainer = "CN=NTDS Quotas,$DomainDistinguishedName"
            ForeignSecurityPrincipalsContainer = "CN=ForeignSecurityPrincipals,$DomainDistinguishedName"
        }
        #>

        #Compare-MultipleObjects -Object @($DefaultWellKnownFolders, $CurrentWellKnownFolders) -SkipProperties
        if ($AsHashtable) {
            $CurrentWellKnownFolders
        } else {
            [PSCustomObject] $CurrentWellKnownFolders
        }
    }
}

#Get-WinADWellKnownFolders -IncludeDomains 'ad.evotec.xyz'
function Invoke-ADEssentials {
    <#
    .SYNOPSIS
    This command invokes ADEssentials to perform essential Active Directory operations and generates reports.
 
    .DESCRIPTION
    This command runs ADEssentials to perform essential Active Directory operations and generates reports. It supports the option to include specific domains for the operations.
 
    .PARAMETER FilePath
    Specifies the path to the folder where the generated reports will be saved.
 
    .PARAMETER Type
    Specifies the type of operations to perform. If not specified, all supported types will be performed.
 
    .PARAMETER PassThru
    If specified, the command returns the generated reports.
 
    .PARAMETER HideHTML
    If specified, the HTML report will not be displayed.
 
    .PARAMETER HideSteps
    If specified, the steps of the operations will not be displayed.
 
    .PARAMETER ShowError
    If specified, any errors during the operations will be displayed.
 
    .PARAMETER ShowWarning
    If specified, any warnings during the operations will be displayed.
 
    .PARAMETER Forest
    Specifies the name of the forest to perform the operations. If not specified, the current forest will be used.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the operations.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the operations. If not specified, all domains will be included.
 
    .PARAMETER Online
    If specified, the command will perform the operations online.
 
    .PARAMETER SplitReports
    If specified, the command will generate separate reports for each type of operation.
 
    .EXAMPLE
    Invoke-ADEssentials -FilePath "C:\Reports" -Type "UserManagement" -PassThru -HideHTML -HideSteps -ShowError -ShowWarning -Forest "example.com" -ExcludeDomains "subdomain.example.com" -IncludeDomains "example.com" -Online -SplitReports
 
    This example runs ADEssentials to perform user management operations on the "example.com" domain and generates separate reports for each operation type. The reports are saved to "C:\Reports" and the HTML report is not displayed.
 
    .NOTES
    This cmdlet requires the ADEssentials PowerShell module to be installed and imported.
    #>

    [cmdletBinding()]
    param(
        [string] $FilePath,
        [Parameter(Position = 0)][string[]] $Type,
        [switch] $PassThru,
        [switch] $HideHTML,
        [switch] $HideSteps,
        [switch] $ShowError,
        [switch] $ShowWarning,
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [switch] $Online,
        [switch] $SplitReports
    )
    Reset-ADEssentialsStatus

    #$Script:AllUsers = [ordered] @{}
    $Script:Cache = [ordered] @{}
    $Script:Reporting = [ordered] @{}
    $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Invoke-ADEssentials' -RepositoryOwner 'evotecit' -RepositoryName 'ADEssentials'
    $Script:Reporting['Settings'] = @{
        ShowError   = $ShowError.IsPresent
        ShowWarning = $ShowWarning.IsPresent
        HideSteps   = $HideSteps.IsPresent
    }

    Write-Color '[i]', "[ADEssentials] ", 'Version', ' [Informative] ', $Script:Reporting['Version'] -Color Yellow, DarkGray, Yellow, DarkGray, Magenta

    # Verify requested types are supported
    $Supported = [System.Collections.Generic.List[string]]::new()
    [Array] $NotSupported = foreach ($T in $Type) {
        if ($T -notin $Script:ADEssentialsConfiguration.Keys ) {
            $T
        } else {
            $Supported.Add($T)
        }
    }
    if ($Supported) {
        Write-Color '[i]', "[ADEssentials] ", 'Supported types', ' [Informative] ', "Chosen by user: ", ($Supported -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta
    }
    if ($NotSupported) {
        Write-Color '[i]', "[ADEssentials] ", 'Not supported types', ' [Informative] ', "Following types are not supported: ", ($NotSupported -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta
        Write-Color '[i]', "[ADEssentials] ", 'Not supported types', ' [Informative] ', "Please use one/multiple from the list: ", ($Script:ADEssentialsConfiguration.Keys -join ', ') -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta
        return
    }
    $DisplayForest = if ($Forest) {
        $Forest 
    } else {
        'Not defined. Using current one' 
    }
    $DisplayIncludedDomains = if ($IncludeDomains) {
        $IncludeDomains -join "," 
    } else {
        'Not defined. Using all domains of forest' 
    }
    $DisplayExcludedDomains = if ($ExcludeDomains) {
        $ExcludeDomains -join ',' 
    } else {
        'No exclusions provided' 
    }
    Write-Color '[i]', "[ADEssentials] ", 'Domain Information', ' [Informative] ', "Forest: ", $DisplayForest -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta
    Write-Color '[i]', "[ADEssentials] ", 'Domain Information', ' [Informative] ', "Included Domains: ", $DisplayIncludedDomains -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta
    Write-Color '[i]', "[ADEssentials] ", 'Domain Information', ' [Informative] ', "Excluded Domains: ", $DisplayExcludedDomains -Color Yellow, DarkGray, Yellow, DarkGray, Yellow, Magenta

    # Lets make sure we only enable those types which are requestd by user
    if ($Type) {
        foreach ($T in $Script:ADEssentialsConfiguration.Keys) {
            $Script:ADEssentialsConfiguration[$T].Enabled = $false
        }
        # Lets enable all requested ones
        foreach ($T in $Type) {
            $Script:ADEssentialsConfiguration[$T].Enabled = $true
        }
    }

    # Build data
    foreach ($T in $Script:ADEssentialsConfiguration.Keys) {
        if ($Script:ADEssentialsConfiguration[$T].Enabled -eq $true) {
            $Script:Reporting[$T] = [ordered] @{
                Name              = $Script:ADEssentialsConfiguration[$T].Name
                ActionRequired    = $null
                Data              = $null
                Exclusions        = $null
                WarningsAndErrors = $null
                Time              = $null
                Summary           = $null
                Variables         = Copy-Dictionary -Dictionary $Script:ADEssentialsConfiguration[$T]['Variables']
            }
            if ($Exclusions) {
                if ($Exclusions -is [scriptblock]) {
                    $Script:Reporting[$T]['ExclusionsCode'] = $Exclusions
                }
                if ($Exclusions -is [Array]) {
                    $Script:Reporting[$T]['Exclusions'] = $Exclusions
                }
            }

            $TimeLogADEssentials = Start-TimeLog
            Write-Color -Text '[i]', '[Start] ', $($Script:ADEssentialsConfiguration[$T]['Name']) -Color Yellow, DarkGray, Yellow
            $OutputCommand = Invoke-Command -ScriptBlock $Script:ADEssentialsConfiguration[$T]['Execute'] -WarningVariable CommandWarnings -ErrorVariable CommandErrors -ArgumentList $Forest, $ExcludeDomains, $IncludeDomains
            if ($OutputCommand -is [System.Collections.IDictionary]) {
                # in some cases the return will be wrapped in Hashtable/orderedDictionary and we need to handle this without an array
                $Script:Reporting[$T]['Data'] = $OutputCommand
            } else {
                # since sometimes it can be 0 or 1 objects being returned we force it being an array
                $Script:Reporting[$T]['Data'] = [Array] $OutputCommand
            }
            Invoke-Command -ScriptBlock $Script:ADEssentialsConfiguration[$T]['Processing']
            $Script:Reporting[$T]['WarningsAndErrors'] = @(
                if ($ShowWarning) {
                    foreach ($War in $CommandWarnings) {
                        [PSCustomObject] @{
                            Type       = 'Warning'
                            Comment    = $War
                            Reason     = ''
                            TargetName = ''
                        }
                    }
                }
                if ($ShowError) {
                    foreach ($Err in $CommandErrors) {
                        [PSCustomObject] @{
                            Type       = 'Error'
                            Comment    = $Err
                            Reason     = $Err.CategoryInfo.Reason
                            TargetName = $Err.CategoryInfo.TargetName
                        }
                    }
                }
            )
            $TimeEndADEssentials = Stop-TimeLog -Time $TimeLogADEssentials -Option OneLiner
            $Script:Reporting[$T]['Time'] = $TimeEndADEssentials
            Write-Color -Text '[i]', '[End ] ', $($Script:ADEssentialsConfiguration[$T]['Name']), " [Time to execute: $TimeEndADEssentials]" -Color Yellow, DarkGray, Yellow, DarkGray

            if ($SplitReports) {
                Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report for ', $T -Color Yellow, DarkGray, Yellow
                $TimeLogHTML = Start-TimeLog
                New-HTMLReportADEssentialsWithSplit -FilePath $FilePath -Online:$Online -HideHTML:$HideHTML -CurrentReport $T
                $TimeLogEndHTML = Stop-TimeLog -Time $TimeLogHTML -Option OneLiner
                Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report for', $T, " [Time to execute: $TimeLogEndHTML]" -Color Yellow, DarkGray, Yellow, DarkGray
            }
        }
    }
    if ( -not $SplitReports) {
        Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report' -Color Yellow, DarkGray, Yellow
        $TimeLogHTML = Start-TimeLog
        if (-not $FilePath) {
            $FilePath = Get-FileName -Extension 'html' -Temporary
        }
        New-HTMLReportADEssentials -Type $Type -Online:$Online.IsPresent -HideHTML:$HideHTML.IsPresent -FilePath $FilePath
        $TimeLogEndHTML = Stop-TimeLog -Time $TimeLogHTML -Option OneLiner
        Write-Color -Text '[i]', '[HTML ] ', 'Generating HTML report', " [Time to execute: $TimeLogEndHTML]" -Color Yellow, DarkGray, Yellow, DarkGray
    }
    Reset-ADEssentialsStatus
    if ($PassThru) {
        $Script:Reporting
    }
}

[scriptblock] $SourcesAutoCompleter = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    $Script:ADEssentialsConfiguration.Keys | Sort-Object | Where-Object { $_ -like "*$wordToComplete*" }
}

Register-ArgumentCompleter -CommandName Invoke-ADEssentials -ParameterName Type -ScriptBlock $SourcesAutoCompleter
function Invoke-PingCastle {
    <#
    .SYNOPSIS
    Executes PingCastle to perform a health check on specified domains and saves the reports.
 
    .DESCRIPTION
    This cmdlet runs PingCastle to perform a health check on the specified domains and saves the generated reports to a specified location. It supports the option to include specific domains for the health check.
 
    .PARAMETER FolderPath
    Specifies the path to the folder containing the PingCastle executable.
 
    .PARAMETER ReportPath
    Specifies the path where the generated reports will be saved.
 
    .PARAMETER IncludeDomain
    Specifies an array of domain names to include in the health check. If not specified, all domains will be checked.
 
    .EXAMPLE
    Invoke-PingCastle -FolderPath "C:\PingCastle" -ReportPath "C:\Reports" -IncludeDomain "example.local", "subdomain.example.local"
 
    This example runs PingCastle to perform a health check on the "example.local" and "subdomain.example.local" domains and saves the reports to "C:\Reports".
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string] $FolderPath,
        [Parameter(Mandatory)][string] $ReportPath,
        [string[]] $IncludeDomain
    )
    $PingCastleExecutable = [io.path]::Combine($FolderPath, 'PingCastle.exe')
    if ($FolderPath -and (Test-Path -LiteralPath $FolderPath) -and (Test-Path -LiteralPath $PingCastleExecutable)) {
    } else {
        Write-Warning -Message "Invoke-PingCastle - FolderPath [$FolderPath] doesn't exist. Please provide path with PingCastle.exe"
        return
    }

    if ($ReportPath -and (Test-Path -LiteralPath $ReportPath)) {
    } else {
        Write-Warning -Message "Invoke-PingCastle - ReportPath [$ReportPath] doesn't exist. Please provide path with PingCastle report"
        return
    }

    $TemporaryReportFolder = [io.path]::Combine($Env:TEMP, 'PingCastle')
    if (-not (Test-Path -LiteralPath $TemporaryReportFolder)) {
        $null = New-Item -Path $TemporaryReportFolder -ItemType Directory -Force
    }
    if (Test-Path -LiteralPath $TemporaryReportFolder) {
        $Items = Get-ChildItem -LiteralPath $TemporaryReportFolder -Recurse
        foreach ($Item in $Items) {
            Remove-Item -LiteralPath $Item.FullName -Force
        }
    }

    try {
        Set-Location -LiteralPath $TemporaryReportFolder -ErrorAction Stop
    } catch {
        Write-Warning -Message "Invoke-PingCastle - Error while switch to $TemporaryReportFolder. Error: $($_.Exception.Message)"
        return
    }

    if ($IncludeDomain) {
        foreach ($Domain in $IncludeDomain) {
            & $PingCastleExecutable --healthcheck --server $Domain --reachable
        }
    } else {
        & $PingCastleExecutable --healthcheck --server * --reachable
    }
    $AllFiles = Get-ChildItem -LiteralPath $TemporaryReportFolder
    foreach ($File in $AllFiles) {
        $DomainName = $File.BaseName.Replace("ad_hc_", '')
        $Name = "PingCastle-Domain-$($DomainName)_$(Get-Date -f yyyy-MM-dd_HHmmss -Date $File.CreationTime)$($File.Extension)"
        $DestinationPath = [io.path]::Combine($ReportPath, $Name)
        try {
            Move-Item -LiteralPath $File.FullName -Destination $DestinationPath -Force -ErrorAction Stop
            [PSCustomObject] @{
                DomainName = $DomainName
                FilePath   = $DestinationPath
                Error      = $null
            }
        } catch {
            Write-Warning -Message "Invoke-PingCastle - Error while moving file $File to $DestinationPath. Error: $($_.Exception.Message)"
            [PSCustomObject] @{
                DomainName = $DomainName
                FilePath   = $DestinationPath
                Error      = $_.Exception.Message
            }
        }
    }
}


function New-ADACLObject {
    <#
    .SYNOPSIS
    Define ACL permissions to be applied during Set-ADACLObject and in DelegationModel PowerShell Module
 
    .DESCRIPTION
    Define ACL permissions to be applied during Set-ADACLObject and in DelegationModel PowerShell Module
 
    .PARAMETER Principal
    Principal to apply permissions to
 
    .PARAMETER SimplifiedDelegation
    An experimental parameter that allows to choose predefined set of permissions instead of defining multiple rules to cover a single instance.
 
    .PARAMETER AccessRule
    Access rule to apply. Choices are:
    - AccessSystemSecurity - 16777216 - The right to get or set the SACL in the object security descriptor.
    - CreateChild - 1 - The right to create children of the object.
    - Delete - 65536 - The right to delete the object.
    - DeleteChild - 2 - The right to delete children of the object.
    - DeleteTree - 64 - The right to delete all children of this object, regardless of the permissions of the children.
    - ExtendedRight - 256 A customized control access right. For a list of possible extended rights, see the Extended Rights article. For more information about extended rights, see the Control Access Rights article.
    - GenericAll - 983551 The right to create or delete children, delete a subtree, read and write properties, examine children and the object itself, add and remove the object from the directory, and read or write with an extended right.
    - GenericExecute - 131076 The right to read permissions on, and list the contents of, a container object.
    - GenericRead - 131220 The right to read permissions on this object, read all the properties on this object, list this object name when the parent container is listed, and list the contents of this object if it is a container.
    - GenericWrite - 131112 The right to read permissions on this object, write all the properties on this object, and perform all validated writes to this object.
    - ListChildren - 4 The right to list children of this object. For more information about this right, see the Controlling Object Visibility article.
    - ListObject -128 - The right to list a particular object. For more information about this right, see the Controlling Object Visibility article.
    - ReadControl - 131072 - The right to read data from the security descriptor of the object, not including the data in the SACL.
    - ReadProperty - 16 - The right to read properties of the object.
    - Self -8 - The right to perform an operation that is controlled by a validated write access right.
    - Synchronize -1048576 - The right to use the object for synchronization. This right enables a thread to wait until that object is in the signaled state.
    - WriteDacl - 262144 - The right to modify the DACL in the object security descriptor.
    - WriteOwner - 524288 - The right to assume ownership of the object. The user must be an object trustee. The user cannot transfer the ownership to other users.
    - WriteProperty -32 - The right to write properties of the object
 
    .PARAMETER AccessControlType
    Access control type to apply. Choices are:
    - Allow - 0 - The access control entry (ACE) allows the specified access.
    - Deny - 1 - The ACE denies the specified access.
 
    .PARAMETER ObjectType
    A list of schema properties to choose from.
 
    .PARAMETER InheritedObjectType
    A list of schema properties to choose from.
 
    .PARAMETER InheritanceType
    Inheritance type to apply. Choices are:
    - All - 3 - The ACE applies to the object and all its children.
    - Descendents - 2 - The ACE applies to the object and its immediate children.
    - SelfAndChildren - 1 - The ACE applies to the object and its immediate children.
    - None - 0 - The ACE applies only to the object.
 
    .PARAMETER OneLiner
    Return permissions as one liner. If used with Simplified Delegation multiple objects could be retured.
 
    .PARAMETER Force
    Forces refresh of the cache for user/groups. It's useful to run as a first query, especially if one created groups just before running the function
 
    .EXAMPLE
     New-ADACLObject -Principal 'przemyslaw.klys' -AccessControlType Allow -ObjectType All -InheritedObjectTypeName 'All' -AccessRule GenericAll -InheritanceType None
 
    .NOTES
    General notes
    #>

    [cmdletBinding(DefaultParameterSetName = 'Standard')]
    param(
        [parameter(Mandatory, ParameterSetName = 'Simplified')]
        [parameter(Mandatory, ParameterSetName = 'Standard')][string] $Principal,

        [parameter(Mandatory, ParameterSetName = 'Simplified')]
        [string] $SimplifiedDelegation,
        [parameter(Mandatory, ParameterSetName = 'Standard')][alias('ActiveDirectoryRights')][System.DirectoryServices.ActiveDirectoryRights] $AccessRule,
        [parameter(Mandatory, ParameterSetName = 'Simplified')]
        [parameter(Mandatory, ParameterSetName = 'Standard')][System.Security.AccessControl.AccessControlType] $AccessControlType,
        [parameter(Mandatory, ParameterSetName = 'Standard')][alias('ObjectTypeName')][string] $ObjectType,
        [parameter(Mandatory, ParameterSetName = 'Standard')][alias('InheritedObjectTypeName')][string] $InheritedObjectType,
        [parameter(Mandatory, ParameterSetName = 'Simplified')]
        [parameter(Mandatory, ParameterSetName = 'Standard')][alias('ActiveDirectorySecurityInheritance')][nullable[System.DirectoryServices.ActiveDirectorySecurityInheritance]] $InheritanceType,
        [parameter(ParameterSetName = 'Simplified')]
        [parameter(ParameterSetName = 'Standard')][switch] $OneLiner,
        [parameter(ParameterSetName = 'Simplified')]
        [parameter(ParameterSetName = 'Standard')][switch] $Force
    )

    $ConvertedIdentity = Convert-Identity -Identity $Principal -Verbose:$false -Force:$Force.IsPresent
    if ($ConvertedIdentity.Error) {
        Write-Warning -Message "New-ADACLObject - Converting identity $($Principal) failed with $($ConvertedIdentity.Error). Be warned."
    }
    $ConvertedPrincipal = ($ConvertedIdentity).Name

    if ($SimplifiedDelegation) {
        ConvertFrom-SimplifiedDelegation -Principal $ConvertedPrincipal -SimplifiedDelegation $SimplifiedDelegation -OneLiner:$OneLiner.IsPresent -AccessControlType $AccessControlType -InheritanceType $InheritanceType
    } else {
        ConvertTo-Delegation -AccessControlType $AccessControlType -InheritanceType $InheritanceType -Principal $ConvertedPrincipal -AccessRule $AccessRule -ObjectType $ObjectType -InheritedObjectType $InheritedObjectType -OneLiner:$OneLiner.IsPresent
    }
}

[scriptblock] $ADACLObjectAutoCompleter = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    if (-not $Script:ADSchemaGuids) {
        Import-Module ActiveDirectory -Verbose:$false
        $Script:ADSchemaGuids = Convert-ADSchemaToGuid
    }
    $Script:ADSchemaGuids.Keys | Sort-Object | Where-Object { $_ -like "*$wordToComplete*" }
}

Register-ArgumentCompleter -CommandName New-ADACLObject -ParameterName ObjectType -ScriptBlock $ADACLObjectAutoCompleter
Register-ArgumentCompleter -CommandName New-ADACLObject -ParameterName InheritedObjectType -ScriptBlock $ADACLObjectAutoCompleter

[scriptblock] $ADACLSimplifiedDelegationDefinition = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    $Script:SimplifiedDelegationDefinitionList | Sort-Object | Where-Object { $_ -like "*$wordToComplete*" }
}

Register-ArgumentCompleter -CommandName New-ADACLObject -ParameterName SimplifiedDelegation -ScriptBlock $ADACLSimplifiedDelegationDefinition
function New-ADSite {
    <#
    .SYNOPSIS
    Creates a new Active Directory site and configures its properties.
 
    .DESCRIPTION
    This cmdlet creates a new Active Directory site with the specified name and description. It also allows for the configuration of subnets, site links, and default sites. The cmdlet supports the use of credentials for authentication.
 
    .PARAMETER Site
    Specifies the name of the new Active Directory site to create.
 
    .PARAMETER Description
    Specifies the description of the new Active Directory site.
 
    .PARAMETER SitePartner
    Specifies the name of the partner site for the new site link.
 
    .PARAMETER DefaultSite
    Specifies the default site to which the new site will be added.
 
    .PARAMETER Subnets
    Specifies an array of subnet addresses to be associated with the new site.
 
    .PARAMETER Credential
    Specifies the credentials to use for authentication.
 
    .EXAMPLE
    New-ADSite -Site "NewSite" -Description "Description of the new site" -SitePartner "PartnerSite" -DefaultSite "DefaultSite" -Subnets @("10.0.0.0/8", "192.168.0.0/16") -Credential (Get-Credential)
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported.
    #>

    [CmdletBinding(SupportsShouldProcess = $true)]
    param(
        [Parameter(Mandatory = $true)][string]$Site,
        [Parameter(Mandatory = $true)][string]$Description,
        [Parameter(Mandatory = $true)][ValidateScript( { Get-ADReplicationSite -Identity $_ })][string]$SitePartner,
        [Parameter(Mandatory = $true)][array]$DefaultSite,
        [Parameter(Mandatory = $false)][array]$Subnets,
        [Parameter(Mandatory = $false)][System.Management.Automation.PSCredential]$Credential
    )
    begin {
        $InformationPreference = "Continue"
        [string]$sServer = (Get-ADDomainController -Writable -Discover).HostName
        $Site = $Site.ToUpper()
        $SitePartner = $SitePartner.ToUpper()
        $sSiteLink = "$($Site)-$($SitePartner)"
        $sSiteLinkDescr = "$($SitePartner)-$($Site)"
        $aSiteLinkSites = @($Site, $SitePartner)
    }

    process {

        #region Create site
        try {
            $hParams = @{
                Name        = $Site
                Description = $Description
                Server      = $sServer
            }
            if ($Credential) {
                $hParams.Credential = $Credential 
            }

            New-ADReplicationSite @hParams
            Write-Verbose -Message "New-ADSite - Site $($Site) created"
        } catch {
            $ErrorMessage = $PSItem.Exception.Message
            Write-Warning -Message "New-ADSite - Error: $ErrorMessage"
        }
        #endregion

        #region Create/reconnect subnets
        try {
            if ($Subnets) {
                foreach ($subnet in $Subnets) {
                    if (Get-ADReplicationSubnet -Filter "Name -eq '$subnet'") {

                        Write-Warning -Message "$($subnet) exists, will try reconnect to new site"

                        $hParams = @{
                            Identity    = $subnet
                            Site        = $Site
                            Description = $Description
                            Server      = $sServer
                        }
                        if ($Credential) {
                            $hParams.Credential = $Credential 
                        }

                        Set-ADReplicationSubnet @hParams
                        Write-Verbose -Message "New-ADSite - Subnet $($subnet) reconnected"
                    } else {
                        $hParams = @{
                            Name        = $subnet
                            Site        = $Site
                            Description = $Description
                            Server      = $sServer
                        }
                        if ($Credential) {
                            $hParams.Credential = $Credential 
                        }

                        New-ADReplicationSubnet @hParams
                        Write-Verbose -Message "New-ADSite - Subnet $($subnet) created"
                    }
                }
            }
        } catch {
            $ErrorMessage = $PSItem.Exception.Message
            Write-Warning -Message "New-ADSite - Error: $ErrorMessage"
        }
        #endregion

        #region Create sitelink
        try {
            $hParams = @{
                Name                          = $sSiteLink
                Description                   = $sSiteLinkDescr
                ReplicationFrequencyInMinutes = 15
                Cost                          = 10
                SitesIncluded                 = $aSiteLinkSites
                Server                        = $sServer
            }
            if ($Credential) {
                $hParams.Credential = $Credential 
            }

            New-ADReplicationSiteLink @hParams
            Write-Verbose -Message "New-ADSite - $($sSiteLink) site link created"
        } catch {
            $ErrorMessage = $PSItem.Exception.Message
            Write-Warning -Message "New-ADSite - Error: $ErrorMessage"
        }
        #endregion

        #region Attach site to default sitelink
        try {
            $hParams = @{
                Identity      = $DefaultSite
                SitesIncluded = @{ Add = $Site }
                Server        = $sServer
            }
            if ($Credential) {
                $hParams.Credential = $Credential 
            }

            Set-ADReplicationSiteLink @hParams
            Write-Verbose -Message "New-ADSite - $($Site) added to $($DefaultSite)"
        } catch {
            $ErrorMessage = $PSItem.Exception.Message
            Write-Warning -Message "New-ADSite - Error: $ErrorMessage"
        }
        #endregion
    }
    end {
    }
}
function Remove-ADACL {
    <#
    .SYNOPSIS
    Removes an Access Control List (ACL) entry from an Active Directory object or an NTSecurityDescriptor.
 
    .DESCRIPTION
    This cmdlet is designed to remove a specific ACL entry from an Active Directory object or an NTSecurityDescriptor. It allows for granular control over the removal process by specifying the object, ACL, principal, access rule, access control type, and inheritance settings. Additionally, it provides options to include or exclude specific object types and their inherited types.
 
    .PARAMETER ADObject
    Specifies the Active Directory object from which to remove the ACL entry. This can be a single object or an array of objects.
 
    .PARAMETER ACL
    Specifies the ACL from which to remove the entry. This parameter is mandatory when using the ACL or NTSecurityDescriptor parameter sets.
 
    .PARAMETER Principal
    Specifies the principal (user, group, or computer) for whom the ACL entry is being removed.
 
    .PARAMETER AccessRule
    Specifies the access rule to remove. This can be a specific right or a combination of rights.
 
    .PARAMETER AccessControlType
    Specifies the type of access control to apply. The default is Allow.
 
    .PARAMETER IncludeObjectTypeName
    Specifies the object types to include in the removal process.
 
    .PARAMETER IncludeInheritedObjectTypeName
    Specifies the inherited object types to include in the removal process.
 
    .PARAMETER InheritanceType
    Specifies the inheritance type for the ACL entry.
 
    .PARAMETER Force
    Forces the removal of inherited ACL entries. By default, inherited entries are skipped.
 
    .EXAMPLE
    Remove-ADACL -ADObject "CN=User1,DC=example,DC=com" -Principal "CN=User2,DC=example,DC=com" -AccessRule "ReadProperty, WriteProperty" -AccessControlType Allow
 
    This example removes the ACL entry for User2 to read and write properties on User1's object in the example.com domain.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported.
    #>

    [cmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ADObject')]
    param(
        [parameter(ParameterSetName = 'ADObject')][alias('Identity')][Array] $ADObject,

        [parameter(ParameterSetName = 'NTSecurityDescriptor', Mandatory)]
        [parameter(ParameterSetName = 'ACL', Mandatory)]
        [Array] $ACL,

        [parameter(ParameterSetName = 'ACL', Mandatory)]
        [parameter(ParameterSetName = 'ADObject')]
        [string] $Principal,

        [parameter(ParameterSetName = 'ACL')]
        [parameter(ParameterSetName = 'ADObject')]
        [Alias('ActiveDirectoryRights')][System.DirectoryServices.ActiveDirectoryRights] $AccessRule,

        [parameter(ParameterSetName = 'ACL')]
        [parameter(ParameterSetName = 'ADObject')]
        [System.Security.AccessControl.AccessControlType] $AccessControlType = [System.Security.AccessControl.AccessControlType]::Allow,

        [parameter(ParameterSetName = 'ACL')]
        [parameter(ParameterSetName = 'ADObject')]
        [Alias('ObjectTypeName', 'ObjectType')][string[]] $IncludeObjectTypeName,

        [parameter(ParameterSetName = 'ACL')]
        [parameter(ParameterSetName = 'ADObject')]
        [Alias('InheritedObjectTypeName', 'InheritedObjectType')][string[]] $IncludeInheritedObjectTypeName,

        [parameter(ParameterSetName = 'ACL')]
        [parameter(ParameterSetName = 'ADObject')]
        [alias('ActiveDirectorySecurityInheritance')][nullable[System.DirectoryServices.ActiveDirectorySecurityInheritance]] $InheritanceType,

        [parameter(ParameterSetName = 'NTSecurityDescriptor')]
        [parameter(ParameterSetName = 'ACL')]
        [parameter(ParameterSetName = 'ADObject')]
        [switch] $Force,

        [parameter(ParameterSetName = 'NTSecurityDescriptor', Mandatory)]
        [alias('ActiveDirectorySecurity')][System.DirectoryServices.ActiveDirectorySecurity] $NTSecurityDescriptor
    )
    if (-not $Script:ForestDetails) {
        Write-Verbose "Remove-ADACL - Gathering Forest Details"
        $Script:ForestDetails = Get-WinADForestDetails
    }
    if ($PSBoundParameters.ContainsKey('ADObject')) {
        foreach ($Object in $ADObject) {
            $getADACLSplat = @{
                ADObject                                  = $Object
                Bundle                                    = $true
                Resolve                                   = $true
                IncludeActiveDirectoryRights              = $AccessRule
                Principal                                 = $Principal
                AccessControlType                         = $AccessControlType
                IncludeObjectTypeName                     = $IncludeObjectTypeName
                IncludeActiveDirectorySecurityInheritance = $InheritanceType
                IncludeInheritedObjectTypeName            = $IncludeInheritedObjectTypeName
            }
            Remove-EmptyValue -Hashtable $getADACLSplat
            $MYACL = Get-ADACL @getADACLSplat
            $removePrivateACLSplat = @{
                ACL    = $MYACL
                WhatIf = $WhatIfPreference
                Force  = $Force.IsPresent
            }
            Remove-EmptyValue -Hashtable $removePrivateACLSplat
            Remove-PrivateACL @removePrivateACLSplat
        }
    } elseif ($PSBoundParameters.ContainsKey('ACL') -and $PSBoundParameters.ContainsKey('ntSecurityDescriptor')) {
        foreach ($SubACL in $ACL) {
            $removePrivateACLSplat = @{
                ntSecurityDescriptor = $ntSecurityDescriptor
                ACL                  = $SubACL
                WhatIf               = $WhatIfPreference
                Force                = $Force.IsPresent
            }
            Remove-EmptyValue -Hashtable $removePrivateACLSplat
            Remove-PrivateACL @removePrivateACLSplat
        }
    } elseif ($PSBoundParameters.ContainsKey('ACL')) {
        foreach ($SubACL in $ACL) {
            $removePrivateACLSplat = @{
                ACL                            = $SubACL
                Principal                      = $Principal
                AccessRule                     = $AccessRule
                AccessControlType              = $AccessControlType
                IncludeObjectTypeName          = $IncludeObjectTypeName
                IncludeInheritedObjectTypeName = $IncludeInheritedObjectTypeName
                InheritanceType                = $InheritanceType
                WhatIf                         = $WhatIfPreference
                Force                          = $Force.IsPresent
            }
            Remove-EmptyValue -Hashtable $removePrivateACLSplat
            Remove-PrivateACL @removePrivateACLSplat
        }
    }
}
function Remove-WinADDFSTopology {
    <#
    .SYNOPSIS
    This command removes DFS topology objects from Active Directory that are missing one or more properties
 
    .DESCRIPTION
    This command removes DFS topology objects from Active Directory that are missing one or more properties.
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER Type
    Type of objects to remove - to remove those missing at least one property or all properties (MissingAtLeastOne, MissingAll)
 
    .EXAMPLE
    Remove-WinADDFSTopology -Type MissingAll -Verbose -WhatIf
 
    .NOTES
    General notes
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [parameter(Mandatory)][ValidateSet('MissingAtLeastOne', 'MissingAll')][string] $Type
    )
    Write-Verbose -Message "Remove-WinADDFSTopology - Getting topology"
    $Topology = Get-WinADDFSTopology -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -Type $Type
    foreach ($Object in $Topology) {
        Write-Verbose -Message "Remove-WinADDFSTopology - Removing '$($Object.Name)' with status '$($Object.Status)' / DN: '$($Object.DistinguishedName)' using '$($Object.QueryServer)'"
        try {
            Remove-ADObject -Identity $Object.DistinguishedName -Server $Object.QueryServer -Confirm:$false -ErrorAction Stop
        } catch {
            Write-Warning -Message "Remove-WinADDFSTopology - Failed to remove '$($Object.Name)' with status '$($Object.Status)' / DN: '$($Object.DistinguishedName)' using '$($Object.QueryServer)'. Error: $($_.Exception.Message)"
        }
    }
}
function Remove-WinADDuplicateObject {
    <#
    .SYNOPSIS
    Removes duplicate objects from Active Directory based on specified criteria.
 
    .DESCRIPTION
    This cmdlet identifies and removes duplicate objects from Active Directory based on the provided parameters. It first retrieves a list of duplicate objects using Get-WinADDuplicateObject, then iterates through the list to remove each object. If an object is protected from accidental deletion, it attempts to remove the protection before deletion.
 
    .PARAMETER Forest
    Specifies the name of the forest to search for duplicate objects. This parameter is optional.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the search for duplicate objects.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the search for duplicate objects.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest, such as domain controllers or other forest-specific details.
 
    .PARAMETER PartialMatchDistinguishedName
    Specifies a partial distinguished name to match when searching for duplicate objects.
 
    .PARAMETER IncludeObjectClass
    Specifies an array of object classes to include in the search for duplicate objects.
 
    .PARAMETER ExcludeObjectClass
    Specifies an array of object classes to exclude from the search for duplicate objects.
 
    .PARAMETER LimitProcessing
    Specifies the maximum number of duplicate objects to process for removal. The default is to process all found duplicates.
 
    .EXAMPLE
    Remove-WinADDuplicateObject -Forest "example.local" -IncludeDomains "example.local", "subdomain.example.local" -IncludeObjectClass "User", "Group" -LimitProcessing 10
 
    This example removes up to 10 duplicate user and group objects from the "example.local" and "subdomain.example.local" domains in the "example.local" forest.
 
    .EXAMPLE
    Remove-WinADDuplicateObject -ExcludeDomains "example.local" -PartialMatchDistinguishedName "OU=Finance,"
 
    This example removes duplicate objects with a distinguished name containing "OU=Finance," from all domains except "example.local".
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation,

        [string] $PartialMatchDistinguishedName,
        [string[]] $IncludeObjectClass,
        [string[]] $ExcludeObjectClass,

        [int] $LimitProcessing = [int32]::MaxValue
    )

    $getWinADDuplicateObjectSplat = @{
        Forest                        = $Forest
        ExcludeDomains                = $ExcludeDomains
        IncludeDomains                = $IncludeDomains
        IncludeObjectClass            = $IncludeObjectClass
        ExcludeObjectClass            = $ExcludeObjectClass
        PartialMatchDistinguishedName = $PartialMatchDistinguishedName
    }
    $Count = 0
    $DuplicateObjects = Get-WinADDuplicateObject @getWinADDuplicateObjectSplat
    foreach ($Duplicate in $DuplicateObjects | Select-Object -First $LimitProcessing) {
        If ($Duplicate.ProtectedFromAccidentalDeletion -eq $true) {
            Try {
                Set-ADObject -Identity $($Duplicate.ObjectGUID) -ProtectedFromAccidentalDeletion $false -ErrorAction Stop -Server $Duplicate.Server
            } Catch {
                Write-Warning "Skipped object GUID: $($Duplicate.ObjectGUID) from deletion, failed to remove ProtectedFromAccidentalDeletion"
                Write-Verbose "Error message $($_.Exception.Message)"
                Continue
            }
        }
        $Count++
        try {
            Write-Verbose "Remove-WinADDuplicateObject - [$Count/$($DuplicateObjects.Count)] Deleting $($Duplicate.ConflictDN) / $($Duplicate.DomainName) via GUID: $($Duplicate.ObjectGUID)"
            Remove-ADObject -Identity $Duplicate.ObjectGUID -Recursive -ErrorAction Stop -Confirm:$false -Server $Duplicate.Server
        } catch {
            Write-Warning "Remove-WinADDuplicateObject - [$Count/$($DuplicateObjects.Count)] Deleting $($Duplicate.ConflictDN) / $($Duplicate.DomainName) via GUID: $($Duplicate.ObjectGUID) failed with error: $($_.Exception.Message)"
        }
    }
}
function Remove-WinADSharePermission {
    <#
    .SYNOPSIS
    Removes permissions from a specified path or recursively from all items within a specified path.
 
    .DESCRIPTION
    This cmdlet removes permissions from a specified path or recursively from all items within a specified path. It targets permissions of a specific type, defaulting to 'Unknown'. The cmdlet also allows for limiting the number of processing operations.
 
    .PARAMETER Path
    Specifies the path from which to remove permissions. This parameter is mandatory and can be a file or a directory.
 
    .PARAMETER Type
    Specifies the type of permissions to remove. The default value is 'Unknown'. This parameter is validated to only accept 'Unknown'.
 
    .PARAMETER LimitProcessing
    Specifies the maximum number of processing operations to perform. This parameter is optional.
 
    .EXAMPLE
    Remove-WinADSharePermission -Path 'C:\Example\Path' -Type 'Unknown' -LimitProcessing 100
 
    This example removes 'Unknown' type permissions from 'C:\Example\Path' and all items within it, limiting the processing to 100 operations.
 
    .NOTES
    This cmdlet requires the Get-FilePermission and Set-Acl cmdlets to function properly.
    #>

    [cmdletBinding(DefaultParameterSetName = 'Path', SupportsShouldProcess)]
    param(
        [Parameter(ParameterSetName = 'Path', Mandatory)][string] $Path,
        [ValidateSet('Unknown')][string] $Type = 'Unknown',
        [int] $LimitProcessing
    )
    Begin {
        [int] $Count = 0
    }
    Process {
        if ($Path -and (Test-Path -Path $Path)) {
            $Data = @(Get-Item -Path $Path) + @(Get-ChildItem -Path $Path -Recurse:$true)
            foreach ($_ in $Data) {
                $PathToProcess = $_.FullName
                $Permissions = Get-FilePermission -Path $PathToProcess -Extended -IncludeACLObject -ResolveTypes
                $OutputRequiresCommit = foreach ($Permission in $Permissions) {
                    if ($Type -eq 'Unknown' -and $Permission.PrincipalType -eq 'Unknown' -and $Permission.IsInherited -eq $false) {
                        try {
                            Write-Verbose "Remove-WinADSharePermission - Removing permissions from $PathToProcess for $($Permission.Principal) / $($Permission.PrincipalType)"
                            $Permission.AllACL.RemoveAccessRule($Permission.ACL)
                            $true
                        } catch {
                            Write-Warning "Remove-WinADSharePermission - Removing permissions from $PathToProcess for $($Permission.Principal) / $($Permission.PrincipalType) failed: $($_.Exception.Message)"
                            $false
                        }
                    }
                }
                if ($OutputRequiresCommit -notcontains $false -and $OutputRequiresCommit -contains $true) {
                    try {
                        Set-Acl -Path $PathToProcess -AclObject $Permissions[0].ALLACL -ErrorAction Stop
                    } catch {
                        Write-Warning "Remove-WinADSharePermission - Commit for $($PathToProcess) failed: $($_.Exception.Message)"
                    }
                    $Count++
                    if ($Count -eq $LimitProcessing) {
                        break
                    }
                }
            }
        }
    }
    End {
    }
}
function Rename-WinADUserPrincipalName {
    <#
    .SYNOPSIS
    Renames the UserPrincipalName of one or more Active Directory users based on specified parameters.
 
    .DESCRIPTION
    This function iterates through an array of users and generates a new UserPrincipalName based on the provided domain name and optional parameters.
    It then compares the new UserPrincipalName with the existing one and updates it if they differ. The update operation can be simulated with the -WhatIf switch.
 
    .PARAMETER Users
    An array of user objects to process.
 
    .PARAMETER DomainName
    The domain name to use for the new UserPrincipalName.
 
    .PARAMETER ReplaceDomain
    If specified, the existing domain name in the UserPrincipalName will be replaced with the provided DomainName.
 
    .PARAMETER NameSurname
    If specified, the UserPrincipalName will be generated using the user's name and surname.
 
    .PARAMETER FixLatinChars
    If specified, Latin characters with diacritics will be replaced with their closest ASCII equivalent.
 
    .PARAMETER ToLower
    If specified, the UserPrincipalName will be converted to lowercase.
 
    .PARAMETER WhatIf
    Simulates the renaming operation without making actual changes.
 
    .EXAMPLE
    Rename-WinADUserPrincipalName -Users $users -DomainName "example.local" -ReplaceDomain -ToLower
    Renames the UserPrincipalName of the users in the $users array to use the "example.local" domain and converts it to lowercase.
 
    .EXAMPLE
    Rename-WinADUserPrincipalName -Users $users -DomainName "example.local" -NameSurname -FixLatinChars -WhatIf
    Simulates the renaming of the UserPrincipalName of the users in the $users array using their name and surname, replacing Latin characters with diacritics with their closest ASCII equivalent.
    #>

    [cmdletbinding()]
    param(
        [Parameter(Mandatory = $true)][Array] $Users,
        [Parameter(Mandatory = $true)][string] $DomainName,
        [switch] $ReplaceDomain,
        [switch] $NameSurname,
        [switch] $FixLatinChars,
        [switch] $ToLower,
        [switch] $WhatIf
    )
    foreach ($User in $Users) {
        $NewUserPrincipalName = Get-WinADUserPrincipalName -User $User -DomainName $DomainName -ReplaceDomain:$ReplaceDomain -NameSurname:$NameSurname -FixLatinChars:$FixLatinChars -ToLower:$ToLower
        if ($NewUserPrincipalName -ne $User.UserPrincipalName) {
            Set-ADUser -Identity $User.DistinguishedName -UserPrincipalName $NewUserPrincipalName -WhatIf:$WhatIf
        }
    }
}
function Repair-WinADACLConfigurationOwner {
    <#
    .SYNOPSIS
    Fixes all owners of certain object type (site,subnet,sitelink,interSiteTransport,wellKnownSecurityPrincipal) to be Enterprise Admins
 
    .DESCRIPTION
    Fixes all owners of certain object type (site,subnet,sitelink,interSiteTransport,wellKnownSecurityPrincipal) to be Enterprise Admins
 
    .PARAMETER ObjectType
    Gets owners from one or multiple types (and only that type). Possible choices are sites, subnets, interSiteTransport, siteLink, wellKnownSecurityPrincipals
 
    .PARAMETER ContainerType
    Gets owners from one or multiple types (including containers and anything below it). Possible choices are sites, subnets, interSiteTransport, siteLink, wellKnownSecurityPrincipals, services
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .PARAMETER LimitProcessing
    Provide limit of objects that will be fixed in a single run
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [cmdletBinding(DefaultParameterSetName = 'ObjectType', SupportsShouldProcess)]
    param(
        [parameter(ParameterSetName = 'ObjectType', Mandatory)][ValidateSet('site', 'subnet', 'interSiteTransport', 'siteLink', 'wellKnownSecurityPrincipal')][string[]] $ObjectType,
        [parameter(ParameterSetName = 'FolderType', Mandatory)][ValidateSet('site', 'subnet', 'interSiteTransport', 'siteLink', 'wellKnownSecurityPrincipal', 'service')][string[]] $ContainerType,

        [string] $Forest,
        [System.Collections.IDictionary] $ExtendedForestInformation,

        [int] $LimitProcessing = [int32]::MaxValue
    )

    $ADAdministrativeGroups = Get-ADADministrativeGroups -Type DomainAdmins, EnterpriseAdmins -Forest $Forest -ExtendedForestInformation $ForestInformation

    $getWinADACLConfigurationSplat = @{
        ContainerType             = $ContainerType
        ObjectType                = $ObjectType
        Owner                     = $true
        Forest                    = $Forest
        ExtendedForestInformation = $ExtendedForestInformation
    }
    Remove-EmptyValue -Hashtable $getWinADACLConfigurationSplat

    Get-WinADACLConfiguration @getWinADACLConfigurationSplat | Where-Object {
        if ($_.OwnerType -ne 'Administrative' -and $_.OwnerType -ne 'WellKnownAdministrative') {
            $_
        }
    } | Select-Object -First $LimitProcessing | ForEach-Object {
        $ADObject = $_
        $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $_.DistinguishedName
        $EnterpriseAdmin = $ADAdministrativeGroups[$DomainName]['EnterpriseAdmins']
        Set-ADACLOwner -ADObject $ADObject.DistinguishedName -Principal $EnterpriseAdmin
    }
}
function Repair-WinADEmailAddress {
    <#
    .SYNOPSIS
    Repairs and updates the email address and proxy addresses for a given Active Directory user.
 
    .DESCRIPTION
    This cmdlet updates the primary email address and proxy addresses for an Active Directory user. It ensures the primary email address is correctly set and adds or removes secondary email addresses as needed. It also updates the proxy addresses to include the primary email address and any additional secondary email addresses specified.
 
    .PARAMETER ADUser
    The Active Directory user object to update.
 
    .PARAMETER ToEmail
    The new primary email address to set for the user.
 
    .PARAMETER Display
    If specified, displays the summary of the operations performed without making any changes.
 
    .PARAMETER AddSecondary
    An array of secondary email addresses to add to the user's proxy addresses.
 
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Microsoft.ActiveDirectory.Management.ADAccount] $ADUser,
        #[string] $FromEmail,
        [string] $ToEmail,
        [switch] $Display,
        [Array] $AddSecondary #,
        # [switch] $UpdateMailNickName
    )
    $Summary = [ordered] @{
        SamAccountName       = $ADUser.SamAccountName
        UserPrincipalName    = $ADUser.UserPrincipalName
        EmailAddress         = ''
        ProxyAddresses       = ''
        EmailAddressStatus   = 'Not required'
        ProxyAddressesStatus = 'Not required'
        EmailAddressError    = ''
        ProxyAddressesError  = ''
    }
    $RequiredProperties = @(
        'EmailAddress'
        'proxyAddresses'
        #'mailNickName'
    )
    foreach ($Property in $RequiredProperties) {
        if ($ADUser.PSObject.Properties.Name -notcontains $Property) {
            Write-Warning "Repair-WinADEmailAddress - User $($ADUser.SamAccountName) is missing properties ($($RequiredProperties -join ',')) which are required. Try again."
            return
        }
    }
    $ProcessUser = Get-WinADProxyAddresses -ADUser $ADUser -RemovePrefix
    $EmailAddresses = [System.Collections.Generic.List[string]]::new()
    $ProxyAddresses = [System.Collections.Generic.List[string]]::new()

    $ExpectedUser = [ordered] @{
        EmailAddress = $ToEmail
        Primary      = $ToEmail
        Secondary    = ''
        Sip          = $ProcessUser.Sip
        x500         = $ProcessUser.x500
        Other        = $ProcessUser.Other
        #MailNickName = $ProcessUser.mailNickName
    }

    if (-not $ToEmail) {
        # We didn't wanted to change primary email address so we use whatever is set
        $ExpectedUser.EmailAddress = $ProcessUser.EmailAddress
        $ExpectedUser.Primary = $ProcessUser.Primary
        # this is case where Proxy Addresses of current user don't have email address set as primary
        # we want to fix the user right?
        if (-not $ExpectedUser.Primary -and $ExpectedUser.EmailAddress) {
            $ExpectedUser.Primary = $ExpectedUser.EmailAddress
        }
    }
    # if ($UpdateMailNickName) {

    #}

    # Lets add expected primary to proxy addresses we need
    $MakePrimary = "SMTP:$($ExpectedUser.EmailAddress)"
    $ProxyAddresses.Add($MakePrimary)

    # Lets add expected secondary to proxy addresses we need
    $Types = @('Sip', 'x500', 'Other')
    foreach ($Type in $Types) {
        foreach ($Address in $ExpectedUser.$Type) {
            $ProxyAddresses.Add($Address)
        }
    }

    $TypesEmails = @('Primary', 'Secondary')
    foreach ($Type in $TypesEmails) {
        foreach ($Address in $ProcessUser.$Type) {
            if ($Address -ne $ToEmail) {
                $EmailAddresses.Add($Address)
            }
        }
    }
    foreach ($Email in $EmailAddresses) {
        $ProxyAddresses.Add("smtp:$Email".ToLower())
    }
    foreach ($Email in $AddSecondary) {
        if ($Email -like 'smtp:*') {
            $ProxyAddresses.Add($Email.ToLower())
        } else {
            $ProxyAddresses.Add("smtp:$Email".ToLower())
        }
    }


    # Lets fix primary email address
    $Summary['EmailAddress'] = $ExpectedUser.EmailAddress
    if ($ProcessUser.EmailAddress -ne $ExpectedUser.EmailAddress) {
        if ($PSCmdlet.ShouldProcess($ADUser, "Email $ToEmail will be set in EmailAddresss field (1)")) {
            try {
                Set-ADUser -Identity $ADUser -EmailAddress $ExpectedUser.EmailAddress -ErrorAction Stop
                $Summary['EmailAddressStatus'] = 'Success'
                $Summary['EmailAddressError'] = ''
            } catch {
                $Summary['EmailAddressStatus'] = 'Failed'
                $Summary['EmailAddressError'] = $_.Exception.Message
            }
        } else {
            $Summary['EmailAddressStatus'] = 'Whatif'
            $Summary['EmailAddressError'] = ''
        }
    }

    # lets compare Expected Proxy Addresses, against current list
    # lets make sure in new proxy list we have only unique addresses, so if there are duplicates in existing one it will be replaced
    # We need to also convert it to [string[]] as Set-ADUser with -Replace is very picky about it

    # Replacement for Sort-Object -Unique which removes primary SMTP: if it's duplicate of smtp:
    $UniqueProxyList = [System.Collections.Generic.List[string]]::new()
    foreach ($Proxy in $ProxyAddresses) {
        if ($UniqueProxyList -notcontains $Proxy) {
            $UniqueProxyList.Add($Proxy)
        }
    }

    [string[]] $ExpectedProxyAddresses = ($UniqueProxyList | Sort-Object | ForEach-Object { $_ })
    [string[]] $CurrentProxyAddresses = ($ADUser.ProxyAddresses | Sort-Object | ForEach-Object { $_ })
    $Summary['ProxyAddresses'] = $ExpectedProxyAddresses -join ';'
    # we need to compare case sensitive
    if (Compare-Object -ReferenceObject $ExpectedProxyAddresses -DifferenceObject $CurrentProxyAddresses -CaseSensitive) {
        if ($PSCmdlet.ShouldProcess($ADUser, "Email $ExpectedProxyAddresses will replace proxy addresses (2)")) {
            try {
                Set-ADUser -Identity $ADUser -Replace @{ proxyAddresses = $ExpectedProxyAddresses } -ErrorAction Stop
                $Summary['ProxyAddressesStatus'] = 'Success'
                $Summary['ProxyAddressesError'] = ''
            } catch {
                $Summary['ProxyAddressesStatus'] = 'Failed'
                $Summary['ProxyAddressesError'] = $_.Exception.Message
            }
        } else {
            $Summary['ProxyAddressesStatus'] = 'WhatIf'
            $Summary['ProxyAddressesError'] = ''
        }
    }
    if ($Display) {
        [PSCustomObject] $Summary
    }
}


<#
    if ($FromEmail -and $FromEmail -like '*@*') {
        if ($FromEmail -ne $ToEmail) {
            $FindSecondary = "SMTP:$FromEmail"
            if ($ProcessUser.Primary -contains $FromEmail) {
                if ($PSCmdlet.ShouldProcess($ADUser, "Email $FindSecondary will be removed from proxy addresses as primary (1)")) {
                    Set-ADUser -Identity $ADUser -Remove @{ proxyAddresses = $FindSecondary }
                }
            }
            $MakeSecondary = "smtp:$FromEmail"
            if ($ProcessUser.Secondary -notcontains $FromEmail) {
                if ($PSCmdlet.ShouldProcess($ADUser, "Email $MakeSecondary will be added to proxy addresses as secondary (2)")) {
                    Set-ADUser -Identity $ADUser -Add @{ proxyAddresses = $MakeSecondary }
                }
            }
        }
    }
    if ($ToEmail -and $ToEmail -like '*@*') {
        if ($ProcessUser.EmailAddress -ne $ToEmail) {
            if ($PSCmdlet.ShouldProcess($ADUser, "Email $ToEmail will be set in EmailAddresss field (3)")) {
                Set-ADUser -Identity $ADUser -EmailAddress $ToEmail
            }
        }
        if ($ProcessUser.Secondary -contains $ToEmail) {
            $RemovePotential = "smtp:$ToEmail"
            if ($PSCmdlet.ShouldProcess($ADUser, "Email $RemovePotential will be removed from proxy addresses (4)")) {
                Set-ADUser -Identity $ADUser -Remove @{ proxyAddresses = $RemovePotential }
            }
        }
        $MakePrimary = "SMTP:$ToEmail"
        if ($ProcessUser.Primary.Count -in @(0, 1) -and $ProcessUser.Primary -notcontains $ToEmail) {
            if ($PSCmdlet.ShouldProcess($ADUser, "Email $MakePrimary will be added to proxy addresses as primary (5)")) {
                Set-ADUser -Identity $ADUser -Add @{ proxyAddresses = $MakePrimary }
            }
        } elseif ($ProcessUser.Primary.Count -gt 1) {
            [Array] $PrimaryEmail = $ProcessUser.Primary | Sort-Object -Unique
            if ($PrimaryEmail.Count -eq 1) {
                if ($PrimaryEmail -ne $ToEmail) {
                    if ($PSCmdlet.ShouldProcess($ADUser, "Email $MakePrimary will be added to proxy addresses as primary (6)")) {
                        Set-ADUser -Identity $ADUser -Add @{ proxyAddresses = $MakePrimary }
                    }
                } else {
                    if ($ProcessUser.Secondary -notcontains $PrimaryEmail) {
                        $MakeSecondary = "smtp:$PrimaryEmail"
                        if ($PSCmdlet.ShouldProcess($ADUser, "Email $MakeSecondary will be added to proxy addresses as secondary (7)")) {
                            Set-ADUser -Identity $ADUser -Add @{ proxyAddresses = $MakeSecondary }
                        }
                    }
                }
            } else {
                foreach ($Email in $PrimaryEmail) {
 
                }
            }
        }
 
        if ($ProcessUser.Primary -notcontains $ToEmail) {
            #if ($PSCmdlet.ShouldProcess($ADUser, "Email $MakePrimary will be added to proxy addresses as primary (6)")) {
            # Set-ADUser -Identity $ADUser -Add @{ proxyAddresses = $MakePrimary }
            #}
        }
 
    }
    if ($Display) {
        $ProcessUser
    }
    #>



<#
    if ($FromEmail -and $FromEmail -like '*@*') {
        if ($FromEmail -ne $ToEmail) {
            $FindSecondary = "SMTP:$FromEmail"
            if ($ADUser.ProxyAddresses -ccontains $FindSecondary) {
                if ($PSCmdlet.ShouldProcess($ADUser, "Email $FindSecondary will be removed from proxy addresses as primary (1)")) {
                    Set-ADUser -Identity $ADUser -Remove @{ proxyAddresses = $FindSecondary }
                }
            }
            $MakeSecondary = "smtp:$FromEmail"
            if ($ADUser.ProxyAddresses -cnotcontains $MakeSecondary) {
                if ($PSCmdlet.ShouldProcess($ADUser, "Email $MakeSecondary will be added to proxy addresses as secondary (2)")) {
                    Set-ADUser -Identity $ADUser -Add @{ proxyAddresses = $MakeSecondary }
                }
            }
        }
    }
    if ($ToEmail -and $ToEmail -like '*@*') {
        $RemovePotential = "smtp:$ToEmail"
        $MakePrimary = "SMTP:$ToEmail"
        if ($ADUser.EmailAddress -ne $ToEmail) {
            if ($PSCmdlet.ShouldProcess($ADUser, "Email $ToEmail will be set in EmailAddresss field (3)")) {
                Set-ADUser -Identity $ADUser -EmailAddress $ToEmail
            }
        }
        if ($ADUser.ProxyAddresses -ccontains $RemovePotential) {
            if ($PSCmdlet.ShouldProcess($ADUser, "Email $RemovePotential will be removed from proxy addresses (4)")) {
                Set-ADUser -Identity $ADUser -Remove @{ proxyAddresses = $RemovePotential }
            }
        }
        if ($ADUser.ProxyAddresses -cnotcontains $MakePrimary) {
            if ($PSCmdlet.ShouldProcess($ADUser, "Email $MakePrimary will be added to proxy addresses as primary (5)")) {
                Set-ADUser -Identity $ADUser -Add @{ proxyAddresses = $MakePrimary }
            }
        }
    }
    #>

#}
function Repair-WinADForestControllerInformation {
    <#
    .SYNOPSIS
    Repairs the Active Directory forest controller information by fixing ownership and management settings for domain controllers.
 
    .DESCRIPTION
    This cmdlet repairs the Active Directory forest controller information by ensuring that domain controllers are properly owned and managed. It can fix the ownership and management settings for domain controllers based on the specified type of repair. The cmdlet supports processing a limited number of domain controllers at a time.
 
    .PARAMETER Type
    Specifies the type of repair to perform on the domain controllers. The valid types are 'Owner' and 'Manager'. 'Owner' repairs the ownership settings, and 'Manager' repairs the management settings.
 
    .PARAMETER ForestName
    Specifies the name of the forest to repair.
 
    .PARAMETER ExcludeDomains
    Specifies the domains to exclude from the repair process.
 
    .PARAMETER IncludeDomains
    Specifies the domains to include in the repair process.
 
    .PARAMETER ExtendedForestInformation
    Specifies the extended information about the forest to use for the repair process.
 
    .PARAMETER LimitProcessing
    Specifies the maximum number of domain controllers to process in a single run.
 
    .EXAMPLE
    Repair-WinADForestControllerInformation -Type Owner, Manager -ForestName example.com -IncludeDomains example.com, sub.example.com -LimitProcessing 10
 
    This example repairs the ownership and management settings for up to 10 domain controllers in the example.com and sub.example.com domains within the example.com forest.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and imported.
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(Mandatory)][validateSet('Owner', 'Manager')][string[]] $Type,
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [int] $LimitProcessing
    )
    $ForestInformation = Get-WinADForestDetails -Extended -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    if (-not $ADAdministrativeGroups) {
        $ADAdministrativeGroups = Get-ADADministrativeGroups -Type DomainAdmins, EnterpriseAdmins -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ForestInformation
    }
    $Fixed = 0
    $DCs = Get-WinADForestControllerInformation -Forest $Forest -ExtendedForestInformation $ForestInformation -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains | ForEach-Object {
        $DC = $_
        $Done = $false
        if ($Type -contains 'Owner') {
            if ($DC.OwnerType -ne 'Administrative') {
                Write-Verbose -Message "Repair-WinADForestControllerInformation - Fixing (Owner) [$($DC.DomainName)]($Count/$($DCs.Count)) $($DC.DNSHostName)"
                $Principal = $ADAdministrativeGroups[$DC.DomainName]['DomainAdmins']
                Set-ADACLOwner -ADObject $DC.DistinguishedName -Principal $Principal
                $Done = $true
            }
        }
        if ($Type -contains 'Manager') {
            if ($null -ne $DC.ManagedBy) {
                Write-Verbose -Message "Repair-WinADForestControllerInformation - Fixing (Manager) [$($DC.DomainName)]($Count/$($DCs.Count)) $($DC.DNSHostName)"
                Set-ADComputer -Identity $DC.DistinguishedName -Clear ManagedBy -Server $ForestInformation['QueryServers'][$DC.DomainName]['HostName'][0]
                $Done = $true
            }
        }
        if ($Done -eq $true) {
            $Fixed++
        }
        if ($LimitProcessing -ne 0 -and $Fixed -eq $LimitProcessing) {
            break
        }
    }
}
function Request-ChangePasswordAtLogon {
    <#
    .SYNOPSIS
    This command will find all users that have expired password and set them to change password at next logon.
 
    .DESCRIPTION
    This command will find all users that have expired password and set them to change password at next logon.
    This is useful for example for Azure AD Connect where you want to force users to change password on next logon.
    The password expiration doesn't get synced in specific conditions to Azure AD so you need to do it manually.
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER LimitProcessing
    Provide limit of objects that will be processed in a single run
 
    .PARAMETER IgnoreDisplayName
    Allow to ignore certain users based on their DisplayName. -It uses -like operator so you can use wildcards.
    This is useful for example for Exchange accounts that have expired password but are not used for anything else.
 
    .PARAMETER IgnoreDistinguishedName
    Allow to ignore certain users based on their DistinguishedName. It uses -like operator so you can use wildcards.
 
    .PARAMETER IgnoreSamAccountName
    Allow to ignore certain users based on their SamAccountName. It uses -like operator so you can use wildcards.
 
    .PARAMETER OrganizationalUnit
    Provide a list of Organizational Units to search for users that have expired password. If not provided, all users in the forest will be searched.
 
    .PARAMETER PassThru
    Returns objects that were processed.
 
    .EXAMPLE
    $OU = @(
        'OU=Default,OU=Users.NoSync,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz'
        'OU=Administrative,OU=Users.NoSync,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz'
    )
 
    Request-ChangePasswordAtLogon -OrganizationalUnit $OU -LimitProcessing 1 -PassThru -Verbose -WhatIf | Format-Table
 
    .NOTES
    Please note that for Azure AD to pickup the change, you may need:
 
    Get-ADSyncAADCompanyFeature
    Set-ADSyncAADCompanyFeature -ForcePasswordChangeOnLogOn $true
 
    As described in https://learn.microsoft.com/en-us/entra/identity/hybrid/connect/how-to-connect-password-hash-synchronization#synchronizing-temporary-passwords-and-force-password-change-on-next-logon
 
    The above is not required only for new users without a password set. If the password is set the feature is required.
 
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [int] $LimitProcessing,
        [Array] $IgnoreDisplayName,
        [Array] $IgnoreDistinguishedName,
        [Array] $IgnoreSamAccountName,
        [string[]] $OrganizationalUnit,
        [switch] $PassThru
    )
    Begin {
        $ForestDetails = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains
        $IgnoreDisplayNameTotal = @(
            'Microsoft Exchange*'
            foreach ($I in $IgnoreDisplayName) {
                $I
            }
        )
        $IgnoreDistinguishedNameTotal = @(
            "*,CN=Users,*"
            foreach ($I in $IgnoreDistinguishedName) {
                $I
            }
        )
        $IgnoreSamAccountNameTotal = @(
            'Administrator'
            'Guest'
            'krbtgt*'
            'healthmailbox*'
            foreach ($I in $IgnoreSamAccountName) {
                $I
            }
        )
    }
    Process {
        [Array] $UsersFound = foreach ($Domain in $ForestDetails.Domains) {
            $QueryServer = $ForestDetails['QueryServers'][$Domain].HostName[0]
            if ($OrganizationalUnit) {
                $Users = @(
                    foreach ($OU in $OrganizationalUnit) {
                        $OUDomain = ConvertFrom-DistinguishedName -DistinguishedName $OU -ToDomainCN
                        if ($OUDomain -eq $Domain) {
                            Get-ADUser -Filter "Enabled -eq '$true'" -Properties DisplayName, SamAccountName, PasswordExpired, PasswordLastSet, pwdLastSet, PasswordNeverExpires -Server $QueryServer -SearchBase $OU
                        }
                    }
                )
                $Users = $Users | Sort-Object -Property DistinguishedName -Unique
            } else {
                $Users = Get-ADUser -Filter "Enabled -eq '$true'" -Properties DisplayName, SamAccountName, PasswordExpired, PasswordLastSet, pwdLastSet, PasswordNeverExpires -Server $QueryServer
            }
            :SkipUser foreach ($User in $Users) {
                # lets asses if password is set to expire or not
                $DateExpiry = $null
                if ($User."msDS-UserPasswordExpiryTimeComputed" -ne 9223372036854775807) {
                    # This is standard situation where users password is expiring as needed
                    try {
                        $DateExpiry = ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed"))
                    } catch {
                        $DateExpiry = $User."msDS-UserPasswordExpiryTimeComputed"
                    }
                }
                if ($User.pwdLastSet -eq 0 -and $DateExpiry.Year -eq 1601) {
                    $PasswordAtNextLogon = $true
                } else {
                    $PasswordAtNextLogon = $false
                }

                if ($User.PasswordExpired -eq $true -and $PasswordAtNextLogon -eq $false -and $User.PasswordNeverExpires -eq $false) {
                    foreach ($I in $IgnoreSamAccountNameTotal) {
                        if ($User.SamAccountName -like $I) {
                            Write-Verbose -Message "Request-ChangePasswordOnExpiry - Ignoring $($User.SamAccountName) / $($User.DistinguishedName)"
                            continue SkipUser
                        }
                    }
                    foreach ($I in $IgnoreDistinguishedNameTotal) {
                        if ($User.DistinguishedName -like $I) {
                            Write-Verbose -Message "Request-ChangePasswordOnExpiry - Ignoring $($User.SamAccountName) / $($User.DistinguishedName)"
                            continue SkipUser
                        }
                    }
                    foreach ($I in $IgnoreDisplayNameTotal) {
                        if ($User.DisplayName -like $I) {
                            Write-Verbose -Message "Request-ChangePasswordOnExpiry - Ignoring $($User.SamAccountName) / $($User.DistinguishedName)"
                            continue SkipUser
                        }
                    }

                    [PSCustomObject] @{
                        SamAccountName       = $User.SamAccountName
                        Domain               = $Domain
                        DisplayName          = $User.DisplayName
                        DistinguishedName    = $User.DistinguishedName
                        PasswordExpired      = $User.PasswordExpired
                        PasswordLastSet      = $User.PasswordLastSet
                        PasswordNeverExpires = $User.PasswordNeverExpires
                    }
                } else {
                    Write-Verbose -Message "Request-ChangePasswordOnExpiry - Skipping $($User.SamAccountName) / $($User.DistinguishedName) - Password already requested at next logon or never expires."
                }
            }
        }
        $Count = 0
        Write-Verbose -Message "Request-ChangePasswordOnExpiry - Found $($UsersFound.Count) expired users. Processing..."
        foreach ($User in $UsersFound) {
            if ($LimitProcessing -and $Count -ge $LimitProcessing) {
                break
            }
            Write-Verbose -Message "Request-ChangePasswordOnExpiry - Setting $($User.SamAccountName) to change password on next logon / $($User.Domain)"
            Set-ADUser -ChangePasswordAtLogon $true -Identity $User.SamAccountName -Server $ForestDetails['QueryServers'][$User.Domain].HostName[0]
            if ($PassThru) {
                $User
            }
            $Count++
        }
    }
}
function Request-DisableOnAccountExpiration {
    <#
    .SYNOPSIS
    This command will find all users that have expired account and set them to be disabled.
 
    .DESCRIPTION
    This command will find all users that have expired account and set them to be disabled.
    This is useful for example for Azure AD Connect where you want to disable users that have expired account.
    The account expiration doesn't get synced in specific conditions to Azure AD so you need to do it manually.
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER LimitProcessing
    Provide limit of objects that will be processed in a single run
 
    .PARAMETER IgnoreDisplayName
    Allow to ignore certain users based on their DisplayName. -It uses -like operator so you can use wildcards.
    This is useful for example for Exchange accounts that have expired password but are not used for anything else.
 
    .PARAMETER IgnoreDistinguishedName
    Allow to ignore certain users based on their DistinguishedName. It uses -like operator so you can use wildcards.
 
    .PARAMETER IgnoreSamAccountName
    Allow to ignore certain users based on their SamAccountName. It uses -like operator so you can use wildcards.
 
    .PARAMETER OrganizationalUnit
    Provide a list of Organizational Units to search for users that have expired password. If not provided, all users in the forest will be searched.
 
    .PARAMETER PassThru
    Returns objects that were processed.
 
    .EXAMPLE
    Request-DisableOnAccountExpiration -LimitProcessing 1 -PassThru -Verbose -WhatIf | Format-Table
 
    .EXAMPLE
    $OU = @(
        'OU=Default,OU=Users.NoSync,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz'
        'OU=Administrative,OU=Users.NoSync,OU=Accounts,OU=Production,DC=ad,DC=evotec,DC=xyz'
    )
 
    Request-DisableOnAccountExpiration -LimitProcessing 1 -PassThru -Verbose -WhatIf -OrganizationalUnit $OU | Format-Table
 
    .NOTES
    General notes
    #>

    [cmdletbinding(SupportsShouldProcess)]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [int] $LimitProcessing,
        [Array] $IgnoreDisplayName,
        [Array] $IgnoreDistinguishedName,
        [Array] $IgnoreSamAccountName,
        [string[]] $OrganizationalUnit,
        [switch] $PassThru
    )
    Begin {
        $Today = Get-Date
        $ForestDetails = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains
        $IgnoreDisplayNameTotal = @(
            'Microsoft Exchange*'
            foreach ($I in $IgnoreDisplayName) {
                $I
            }
        )
        $IgnoreDistinguishedNameTotal = @(
            "*,CN=Users,*"
            foreach ($I in $IgnoreDistinguishedName) {
                $I
            }
        )
        $IgnoreSamAccountNameTotal = @(
            'Administrator'
            'Guest'
            'krbtgt*'
            'healthmailbox*'
            foreach ($I in $IgnoreSamAccountName) {
                $I
            }
        )
    }
    Process {
        [Array] $UsersFound = foreach ($Domain in $ForestDetails.Domains) {
            $QueryServer = $ForestDetails['QueryServers'][$Domain].HostName[0]
            if ($OrganizationalUnit) {
                $Users = @(
                    foreach ($OU in $OrganizationalUnit) {
                        $OUDomain = ConvertFrom-DistinguishedName -DistinguishedName $OU -ToDomainCN
                        if ($OUDomain -eq $Domain) {
                            Get-ADUser -Filter "Enabled -eq '$true'" -Properties DisplayName, SamAccountName, PasswordExpired, PasswordLastSet, pwdLastSet, PasswordNeverExpires, AccountExpirationDate -Server $QueryServer -SearchBase $OU
                        }
                    }
                )
                $Users = $Users | Sort-Object -Property DistinguishedName -Unique
            } else {
                $Users = Get-ADUser -Filter "Enabled -eq '$true'" -Properties DisplayName, SamAccountName, PasswordExpired, PasswordLastSet, pwdLastSet, PasswordNeverExpires, AccountExpirationDate -Server $QueryServer
            }
            :SkipUser foreach ($User in $Users) {
                # lets asses if password is set to expire or not
                $DateExpiry = $null
                if ($User."msDS-UserPasswordExpiryTimeComputed" -ne 9223372036854775807) {
                    # This is standard situation where users password is expiring as needed
                    try {
                        $DateExpiry = ([datetime]::FromFileTime($User."msDS-UserPasswordExpiryTimeComputed"))
                    } catch {
                        $DateExpiry = $User."msDS-UserPasswordExpiryTimeComputed"
                    }
                }
                if ($User.pwdLastSet -eq 0 -and $DateExpiry.Year -eq 1601) {
                    $PasswordAtNextLogon = $true
                } else {
                    $PasswordAtNextLogon = $false
                }

                if ($User.Enabled -eq $true -and $null -ne $User.AccountExpirationDate) {
                    if ($User.AccountExpirationDate -le $Today) {
                        foreach ($I in $IgnoreSamAccountNameTotal) {
                            if ($User.SamAccountName -like $I) {
                                Write-Verbose -Message "Request-DisableOnAccountExpiration - Ignoring $($User.SamAccountName) / $($User.DistinguishedName)"
                                continue SkipUser
                            }
                        }
                        foreach ($I in $IgnoreDistinguishedNameTotal) {
                            if ($User.DistinguishedName -like $I) {
                                Write-Verbose -Message "Request-DisableOnAccountExpiration - Ignoring $($User.SamAccountName) / $($User.DistinguishedName)"
                                continue SkipUser
                            }
                        }
                        foreach ($I in $IgnoreDisplayNameTotal) {
                            if ($User.DisplayName -like $I) {
                                Write-Verbose -Message "Request-DisableOnAccountExpiration - Ignoring $($User.SamAccountName) / $($User.DistinguishedName)"
                                continue SkipUser
                            }
                        }
                        Write-Verbose -Message "Request-DisableOnAccountExpiration - Found $($User.SamAccountName) / $Domain. Expiration date reached '$($User.AccountExpirationDate)'"
                        [PSCustomObject] @{
                            SamAccountName        = $User.SamAccountName
                            Domain                = $Domain
                            DisplayName           = $User.DisplayName
                            AccountExpirationDate = $User.AccountExpirationDate
                            PasswordAtNextLogon   = $PasswordAtNextLogon
                            PasswordExpired       = $User.PasswordExpired
                            PasswordLastSet       = $User.PasswordLastSet
                            PasswordNeverExpires  = $User.PasswordNeverExpires
                            DistinguishedName     = $User.DistinguishedName
                        }
                    } else {
                        Write-Verbose -Message "Request-DisableOnAccountExpiration - Skipping $($User.SamAccountName) / $Domain. Expiration date not reached '$($User.AccountExpirationDate)'"
                    }
                }
            }
        }
        $Count = 0
        if ($LimitProcessing) {
            Write-Verbose -Message "Request-DisableOnAccountExpiration - Found $($UsersFound.Count) expired users. Processing on disablement with limit of $LimitProcessing..."
        } else {
            Write-Verbose -Message "Request-DisableOnAccountExpiration - Found $($UsersFound.Count) expired users. Processing on disablement..."
        }
        foreach ($User in $UsersFound) {
            if ($LimitProcessing -and $Count -ge $LimitProcessing) {
                break
            }
            Write-Verbose -Message "Request-DisableOnAccountExpiration - Setting $($User.SamAccountName) to be disabled / $($User.Domain)"
            Set-ADUser -Enabled $false -Identity $User.SamAccountName -Server $ForestDetails['QueryServers'][$User.Domain].HostName[0]
            if ($PassThru) {
                $User
            }
            $Count++
        }
    }
}
function Restore-ADACLDefault {
    <#
    .SYNOPSIS
    Restore default permissions for given object in Active Directory
 
    .DESCRIPTION
    Restore default permissions for given object in Active Directory.
    Equivalent of right click on object in Active Directory Users and Computers and selecting 'Restore defaults'
 
    .PARAMETER Object
    Specifies Active Directory objects to restore default permissions. This parameter is mandatory.
 
    .PARAMETER RemoveInheritedAccessRules
    Indicates whether to remove inherited ACEs from the object or principal.
    If this switch is specified, inherited ACEs are removed from the object or principal.
    If this switch is not specified, inherited ACEs are retained on the object or principal.
 
    .EXAMPLE
    $ObjectCheck = Get-ADObject -Id 'OU=_root,DC=ad,DC=evotec,DC=xyz' -Properties 'NtSecurityDescriptor', 'DistinguishedName'
    Restore-ADACLDefault -Object $ObjectCheck -Verbose
 
    .EXAMPLE
    Restore-ADACLDefault -Object 'OU=ITR01,DC=ad,DC=evotec,DC=xyz' -RemoveInheritedAccessRules -Verbose -WhatIf
 
    .NOTES
    Please be aware that when you use Restore-ADACLDefault it clears up existing permissions which may cut you out.
 
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(Mandatory)][alias('Identity')][Object] $Object,
        [switch] $RemoveInheritedAccessRules
    )
    # lets get our forest details
    if (-not $Script:ForestDetails) {
        Write-Verbose "Restore-ADACLDefault - Gathering Forest Details"
        $Script:ForestDetails = Get-WinADForestDetails
    }
    # Lets get our schema
    if (-not $Script:RootDSESchema) {
        $Script:RootDSESchema = (Get-ADRootDSE).SchemaNamingContext
    }

    # lets try to asses what we have for object and if not get it properly
    if ($Object) {
        if ($Object -is [Microsoft.ActiveDirectory.Management.ADEntity]) {
            If ($Object.DistinguishedName -and $Object.NtSecurityDescriptor) {
                # We have what we need
            } else {
                $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $Object.DistinguishedName
                $QueryServer = $Script:ForestDetails['QueryServers'][$DomainName].HostName[0]
                $Object = Get-ADObject -Id $Object.DistinguishedName -Properties 'NtSecurityDescriptor', 'DistinguishedName' -Server $QueryServer
            }
        } elseif ($Object -is [string]) {
            $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $Object
            $QueryServer = $Script:ForestDetails['QueryServers'][$DomainName].HostName[0]
            $Object = Get-ADObject -Id $Object -Properties 'NtSecurityDescriptor', 'DistinguishedName' -Server $QueryServer
        } else {
            Write-Warning -Message "Restore-ADACLDefault - Unknown object type $($Object.GetType().FullName)"
            return
        }
    } else {
        $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $Object
        $QueryServer = $Script:ForestDetails['QueryServers'][$DomainName].HostName[0]
        $Object = Get-ADObject -Id $Object -Properties 'NtSecurityDescriptor', 'DistinguishedName' -Server $QueryServer
    }

    # We have our object, now lets get the default permissions for given type

    if ($Object.ObjectClass -eq 'Unknown') {
        Write-Verbose -Message "Restore-ADACLDefault - Unknown object type $($Object.ObjectClass), using default filter for Organizational-Unit"
        $Filter = 'name -eq "Organizational-Unit"'
    } else {
        $Class = $($Object.ObjectClass)
        $Filter = "lDAPDisplayName -eq '$Class'"
    }

    Write-Verbose "Restore-ADACLDefault - Getting default permissions from $Script:RootDSESchema using filter $Filter"
    #$ADObject = Get-ADObject -Filter $Filter -SearchBase $Script:RootDSESchema -Properties defaultSecurityDescriptor
    $DefaultPermissionsObject = Get-ADObject -Filter $Filter -SearchBase (Get-ADRootDSE).SchemaNamingContext -Properties defaultSecurityDescriptor, canonicalName, lDAPDisplayName
    if (-not $DefaultPermissionsObject.defaultsecuritydescriptor) {
        Write-Warning -Message "Restore-ADACLDefault - Unable to find default permissions for $($Object.ObjectClass)"
        return
    }
    $Descriptor = $DefaultPermissionsObject.defaultsecuritydescriptor

    $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $Object.DistinguishedName
    $QueryServer = $Script:ForestDetails['QueryServers'][$DomainName].HostName[0]

    #Write-Verbose -Message "Restore-ADACLDefault - Disabling inheritance for $($Object.DistinguishedName)"
    #Disable-ADACLInheritance -ADObject $Object.DistinguishedName -RemoveInheritedAccessRules -Verbose

    #Write-Verbose -Message "Restore-ADACLDefault - Removing permissions for $($Object.DistinguishedName)"
    #Remove-ADACL -ADObject $Object.DistinguishedName

    # $Descriptor | ConvertFrom-SddlString -Type ActiveDirectoryRights
    # $SecurityDescriptor = [System.DirectoryServices.ActiveDirectorySecurity]::new()
    # $SecurityDescriptor.SetSecurityDescriptorSddlForm($Descriptor)
    # $SecurityDescriptor

    $Object.NtSecurityDescriptor.SetSecurityDescriptorSddlForm($Descriptor)

    Write-Verbose "Restore-ADACLDefault - Saving permissions for $($Object.DistinguishedName) on $($QueryServer)"
    Set-ADObject -Identity $Object.DistinguishedName -Replace @{ ntSecurityDescriptor = $Object.NtSecurityDescriptor } -ErrorAction Stop -Server $QueryServer

    if ($RemoveInheritedAccessRules) {
        Write-Verbose -Message "Restore-ADACLDefault - Disabling inheritance for $($Object.DistinguishedName)"
        Disable-ADACLInheritance -ADObject $Object.DistinguishedName -RemoveInheritedAccessRules
    }
}

<# Code to use to find default permissions for given object type
$Object = Get-ADObject -Id 'OU=_root,DC=ad,DC=evotec,DC=xyz' -Properties 'NtSecurityDescriptor', 'DistinguishedName'
$Class = $($Object.ObjectClass)
$List = Get-ADObject -Filter "lDAPDisplayName -eq '$Class'" -SearchBase (Get-ADRootDSE).SchemaNamingContext -Properties defaultSecurityDescriptor, canonicalName, lDAPDisplayName
$List
#>

function Set-ADACL {
    <#
    .SYNOPSIS
    Sets the access control list (ACL) for a specified Active Directory object.
 
    .DESCRIPTION
    This cmdlet sets the ACL for a specified Active Directory object. It supports both local and remote operations.
    It can use a credential for remote connections. It filters out adapters that are DHCP enabled or do not have a DNS server search order set.
    It then sets the DNS server IP addresses for the remaining adapters. If the operation is successful, it retrieves the current DNS server IP addresses.
 
    .PARAMETER ADObject
    Specifies the Active Directory object on which to set the ACL.
 
    .PARAMETER ACLSettings
    Specifies the ACL settings to apply to the ADObject.
 
    .PARAMETER Inheritance
    Specifies whether to enable or disable inheritance of ACEs from parent objects.
 
    .PARAMETER Suppress
    Indicates whether to suppress the operation.
 
    .EXAMPLE
    Set-ADACL -ADObject 'CN=TestOU,DC=contoso,DC=com' -ACLSettings @($ACL1, $ACL2) -Inheritance 'Disabled' -Suppress
 
    This example sets the ACL for the specified Active Directory object with the provided ACL settings and inheritance, and suppresses the operation.
 
    .NOTES
    General notes
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [Parameter(Mandatory, Position = 0, ValueFromPipeline, ValueFromPipelineByPropertyName)]
        [alias('Identity')][string] $ADObject,
        [Parameter(Mandatory)][Array] $ACLSettings,
        [Parameter(Mandatory)][ValidateSet('Enabled', 'Disabled')] $Inheritance,
        [switch] $Suppress
    )
    $Results = @{
        Add      = [System.Collections.Generic.List[PSCustomObject]]::new()
        Remove   = [System.Collections.Generic.List[PSCustomObject]]::new()
        Skip     = [System.Collections.Generic.List[PSCustomObject]]::new()
        Warnings = [System.Collections.Generic.List[string]]::new()
        Errors   = [System.Collections.Generic.List[string]]::new()
    }
    $CachedACL = [ordered] @{}

    $ExpectedProperties = @('ActiveDirectoryRights', 'AccessControlType', 'ObjectTypeName', 'InheritedObjectTypeName', 'InheritanceType')

    $FoundDiscrepancy = $false
    $Count = 1
    foreach ($ACL in $ACLSettings) {
        if ($ACL.Action -eq 'Skip') {
            continue
        } elseif ($ACL.Action -eq 'Copy') {
            continue
        }
        # Check if all properties are present
        if ($ACL.Principal -and $ACL.Permissions) {
            foreach ($Permission in $ACL.Permissions) {
                if ($Permission -is [System.Collections.IDictionary]) {
                    Compare-Object -ReferenceObject $ExpectedProperties -DifferenceObject @($Permission.Keys) | Where-Object { $_.SideIndicator -in '<=' } | ForEach-Object {
                        Write-Warning -Message "Set-ADACL - Entry $Count - $($ACL.Principal) is missing property $($_.InputObject) - provided only $($Permission.Keys)"
                        $FoundDiscrepancy = $true
                    }
                } else {
                    Compare-Object -ReferenceObject $ExpectedProperties -DifferenceObject @($Permission.PSObject.Properties.Name) | Where-Object { $_.SideIndicator -in '<=' } | ForEach-Object {
                        Write-Warning -Message "Set-ADACL - Entry $Count - $($ACL.Principal) is missing property $($_.InputObject) - provided only $($Permission.PSObject.Properties.Name)"
                        $FoundDiscrepancy = $true
                    }
                }
            }
        } elseif ($ACL.Principal) {
            if ($ACL -is [System.Collections.IDictionary]) {
                Compare-Object -ReferenceObject $ExpectedProperties -DifferenceObject @($ACL.Keys) | Where-Object { $_.SideIndicator -in '<=' } | ForEach-Object {
                    Write-Warning -Message "Set-ADACL - Entry $Count - $($ACL.Principal) is missing property $($_.InputObject) - provided only $($ACL.Keys)"
                    $FoundDiscrepancy = $true
                }
            } else {
                Compare-Object -ReferenceObject $ExpectedProperties -DifferenceObject @($ACL.PSObject.Properties.Name) | Where-Object { $_.SideIndicator -in '<=' } | ForEach-Object {
                    Write-Warning -Message "Set-ADACL - Entry $Count - $($ACL.Principal) is missing property $($_.InputObject) - provided only $($ACL.PSObject.Properties.Name)"
                    $FoundDiscrepancy = $true
                }
            }
        }
        $Count++
    }
    if ($FoundDiscrepancy) {
        Write-Warning -Message "Set-ADACL - Please check your ACL configuration is correct. Each entry must have the following properties: $($ExpectedProperties -join ', ')"
        $Results.Warnings.Add("Please check your ACL configuration is correct. Each entry must have the following properties: $($ExpectedProperties -join ', ')")
        if (-not $Suppress) {
            return $Results
        } else {
            return
        }
    }
    foreach ($ExpectedACL in $ACLSettings) {
        if ($ExpectedACL.Principal -and $ExpectedACL.Permissions) {
            foreach ($Principal in $ExpectedACL.Principal) {
                $ConvertedIdentity = Convert-Identity -Identity $Principal -Verbose:$false
                if ($ConvertedIdentity.Error) {
                    Write-Warning -Message "Set-ADACL - Converting identity $($Principal) failed with $($ConvertedIdentity.Error). Be warned."
                    $Results.Warnings.Add("Converting identity $($Principal) failed with $($ConvertedIdentity.Error). Be warned.")
                }
                $ConvertedPrincipal = ($ConvertedIdentity).Name
                if (-not $CachedACL[$ConvertedPrincipal]) {
                    $CachedACL[$ConvertedPrincipal] = [ordered] @{}
                }
                # user may not provided any action, so we assume 'Set' as default
                $Action = if ($ExpectedACL.Action) {
                    $ExpectedACL.Action 
                } else {
                    'Add' 
                }
                #$ExpectedACL.Action = $Action

                $CachedACL[$ConvertedPrincipal]['Action'] = $Action

                if (-not $CachedACL[$ConvertedPrincipal]['Permissions']) {
                    $CachedACL[$ConvertedPrincipal]['Permissions'] = [System.Collections.Generic.List[object]]::new()
                }

                if ($ExpectedACL.Permissions) {
                    foreach ($Permission in $ExpectedACL.Permissions) {
                        $CachedACL[$ConvertedPrincipal]['Permissions'].Add([PSCustomObject] $Permission)
                    }
                }
            }
        } elseif ($ExpectedACL.Principal) {
            foreach ($Principal in $ExpectedACL.Principal) {
                $ConvertedIdentity = Convert-Identity -Identity $Principal -Verbose:$false
                if ($ConvertedIdentity.Error) {
                    Write-Warning -Message "Set-ADACL - Converting identity $($Principal) failed with $($ConvertedIdentity.Error). Be warned."
                }
                $ConvertedPrincipal = ($ConvertedIdentity).Name
                if (-not $CachedACL[$ConvertedPrincipal]) {
                    $CachedACL[$ConvertedPrincipal] = [ordered] @{}
                }

                # user may not provided any action, so we assume 'Set' as default
                $Action = if ($ExpectedACL.Action) {
                    $ExpectedACL.Action 
                } else {
                    'Add' 
                }
                #$ExpectedACL.Action = $Action

                $CachedACL[$ConvertedPrincipal]['Action'] = $Action

                if (-not $CachedACL[$ConvertedPrincipal]['Permissions']) {
                    $CachedACL[$ConvertedPrincipal]['Permissions'] = [System.Collections.Generic.List[object]]::new()
                }

                $NewPermission = [ordered] @{}
                if ($ExpectedACL -is [System.Collections.IDictionary]) {
                    foreach ($Key in $ExpectedACL.Keys) {
                        if ($Key -notin @('Principal')) {
                            $NewPermission.$Key = $ExpectedACL.$Key
                        }
                    }
                } else {
                    foreach ($Property in $ExpectedACL.PSObject.Properties) {
                        if ($Property.Name -notin @('Principal')) {
                            $NewPermission.$($Property.Name) = $Property.Value
                        }
                    }
                }
                $CachedACL[$ConvertedPrincipal]['Permissions'].Add([PSCustomObject] $NewPermission)
            }
        }
    }
    $MainAccessRights = Get-ADACL -ADObject $ADObject -Bundle
    foreach ($CurrentACL in $MainAccessRights.ACLAccessRules) {
        $ConvertedIdentity = Convert-Identity -Identity $CurrentACL.Principal -Verbose:$false
        if ($ConvertedIdentity.Error) {
            Write-Warning -Message "Set-ADACL - Converting identity $($Principal) failed with $($ConvertedIdentity.Error). Be warned."
            $Results.Warnings.Add("Converting identity $($Principal) failed with $($ConvertedIdentity.Error). Be warned.")
        }
        $ConvertedPrincipal = ($ConvertedIdentity).Name

        if ($CachedACL[$ConvertedPrincipal]) {
            if ($CachedACL[$ConvertedPrincipal]['Action'] -eq 'Skip') {
                #Write-Verbose "Set-ADACL - Skipping $($CurrentACL.Principal)"
                $Results.Skip.Add(
                    [PSCustomObject] @{
                        Principal         = $ConvertedPrincipal
                        AccessControlType = $CurrentACL.AccessControlType
                        Action            = 'Skip'
                        Permissions       = $CurrentACL
                    }
                )
                continue
            } else {
                Write-Verbose "Set-ADACL - Processing $($ConvertedPrincipal)"
                $DirectMatch = $false
                foreach ($SetPermission in $CachedACL[$ConvertedPrincipal].Permissions) {
                    if ($CurrentACL.AccessControlType -eq $SetPermission.AccessControlType) {
                        # since it's possible people will differently name their object type name, we are going to convert it to GUID
                        $TypeObjectLeft = Convert-ADSchemaToGuid -SchemaName $CurrentACL.ObjectTypeName -AsString
                        $TypeObjectRight = Convert-ADSchemaToGuid -SchemaName $SetPermission.ObjectTypeName -AsString
                        if ($TypeObjectLeft -eq $TypeObjectRight) {

                            if ($CurrentACL.ActiveDirectoryRights -eq $SetPermission.ActiveDirectoryRights) {

                                if ($CurrentACL.InheritedObjectTypeName -eq $SetPermission.InheritedObjectTypeName) {

                                    if ($CurrentACL.InheritanceType -eq $SetPermission.InheritanceType) {
                                        $DirectMatch = $true
                                    }
                                }
                            }
                        }
                    }
                }
                if ($DirectMatch) {
                    $Results.Skip.Add(
                        [PSCustomObject] @{
                            Principal         = $ConvertedPrincipal
                            AccessControlType = $CurrentACL.AccessControlType
                            Action            = 'Skip'
                            Permissions       = $CurrentACL
                        }
                    )
                } else {
                    if ($Inheritance -eq 'Enabled' -and $CurrentACL.IsInherited) {
                        # normally we would try to remove it, but it is inherited, so we will skip it
                        $Results.Skip.Add(
                            [PSCustomObject] @{
                                Principal         = $ConvertedPrincipal
                                AccessControlType = $CurrentACL.AccessControlType
                                Action            = 'Skip'
                                Permissions       = $CurrentACL
                            }
                        )
                    } else {
                        $Results.Remove.Add(
                            [PSCustomObject] @{
                                Principal         = $ConvertedPrincipal
                                AccessControlType = $CurrentACL.AccessControlType
                                Action            = 'Remove'
                                Permissions       = $CurrentACL
                            }
                        )
                    }
                }
            }
        } else {
            # we don't have this principal defined for set, needs to be removed
            Write-Verbose "Set-ADACL - Preparing for removal of $($ConvertedPrincipal)"
            if ($Inheritance -eq 'Enabled' -and $CurrentACL.IsInherited) {
                $Results.Skip.Add(
                    [PSCustomObject] @{
                        Principal         = $ConvertedPrincipal
                        AccessControlType = $CurrentACL.AccessControlType
                        Action            = 'Skip'
                        Permissions       = $CurrentACL
                    }
                )
            } else {
                $Results.Remove.Add(
                    [PSCustomObject] @{
                        Principal         = $ConvertedPrincipal
                        AccessControlType = $CurrentACL.AccessControlType
                        Action            = 'Remove'
                        Permissions       = $CurrentACL
                    }
                )
            }
        }
    }
    $AlreadyCovered = [System.Collections.Generic.List[PSCustomObject]]::new()
    foreach ($Principal in $CachedACL.Keys) {
        if ($CachedACL[$Principal]['Action'] -in 'Add', 'Set') {
            foreach ($SetPermission in $CachedACL[$Principal]['Permissions']) {
                $DirectMatch = $false

                foreach ($CurrentACL in $MainAccessRights.ACLAccessRules) {
                    if ($CurrentACL -in $AlreadyCovered) {
                        continue
                    }
                    $RequestedPrincipal = Convert-Identity -Identity $Principal -Verbose:$false
                    $RequestedPrincipalFromACL = Convert-Identity -Identity $CurrentACL.Principal -Verbose:$false
                    if ($RequestedPrincipalFromACL.Name -ne $RequestedPrincipal.Name) {
                        continue
                    }
                    if ($CurrentACL.AccessControlType -eq $SetPermission.AccessControlType) {

                        # since it's possible people will differently name their object type name, we are going to convert it to GUID
                        $TypeObjectLeft = Convert-ADSchemaToGuid -SchemaName $CurrentACL.ObjectTypeName -AsString
                        $TypeObjectRight = Convert-ADSchemaToGuid -SchemaName $SetPermission.ObjectTypeName -AsString
                        if ($TypeObjectLeft -eq $TypeObjectRight) {

                            if ($CurrentACL.ActiveDirectoryRights -eq $SetPermission.ActiveDirectoryRights) {

                                if ($CurrentACL.InheritedObjectTypeName -eq $SetPermission.InheritedObjectTypeName) {

                                    if ($CurrentACL.InheritanceType -eq $SetPermission.InheritanceType) {
                                        $DirectMatch = $true
                                        $AlreadyCovered.Add($CurrentACL)
                                    }
                                }
                            }
                        }
                    }
                }
                if ($DirectMatch) {
                    Write-Verbose -Message "Set-ADACL - Skipping $($Principal), as it already exists"
                } else {
                    $Results.Add.Add(
                        [PSCustomObject] @{
                            Principal         = $Principal
                            AccessControlType = $SetPermission.AccessControlType
                            Action            = 'Add'
                            Permissions       = $SetPermission
                        }
                    )
                }
            }
        }
    }
    if (-not $WhatIfPreference) {
        Write-Verbose -Message "Set-ADACL - Applying changes to ACL"
        if ($Results.Remove.Permissions) {
            Write-Verbose -Message "Set-ADACL - Removing ACL"
            try {
                Remove-ADACL -ActiveDirectorySecurity $MainAccessRights.ACL -ACL $Results.Remove.Permissions
            } catch {
                Write-Warning -Message "Set-ADACL - Failed to remove ACL for at least one of principals $($Results.Remove.Principal -join ', ')"
                $Results.Errors.Add("Failed to remove ACL for $($Results.Remove.Principal -join ', ')")
            }
        }
        Write-Verbose -Message "Set-ADACL - Adding ACL"
        foreach ($Add in $Results.Add) {
            $addADACLSplat = @{
                NTSecurityDescriptor = $MainAccessRights.ACL
                ADObject             = $ADObject
                Principal            = $Add.Principal
                AccessControlType    = $Add.Permissions.AccessControlType
                AccessRule           = $Add.Permissions.ActiveDirectoryRights
                ObjectType           = $Add.Permissions.ObjectTypeName
                InheritanceType      = $Add.Permissions.InheritanceType
                InheritedObjectType  = $Add.Permissions.InheritedObjectTypeName
            }
            try {
                Add-ADACL @addADACLSplat
            } catch {
                Write-Warning -Message "Set-ADACL - Failed to add ACL for $($Add.Principal)"
                $Results.Errors.Add("Failed to add ACL for $($Add.Principal)")
            }
        }
    }
    if (-not $Suppress) {
        $Results
    }
}
function Set-ADACLInheritance {
    <#
    .SYNOPSIS
    Enables or Disables the inheritance of access control entries (ACEs) from parent objects for one or more Active Directory objects or security principals.
 
    .DESCRIPTION
    Enables or Disables the inheritance of access control entries (ACEs) from parent objects for one or more Active Directory objects or security principals.
 
    .PARAMETER ADObject
    Specifies one or more Active Directory objects or security principals to enable or disable inheritance of ACEs from parent objects.
    This parameter is mandatory when the 'ADObject' parameter set is used.
 
    .PARAMETER ACL
    Specifies one or more access control lists (ACLs) to enable or disable inheritance of ACEs from parent objects.
    This parameter is mandatory when the 'ACL' parameter set is used.
 
    .PARAMETER Inheritance
    Specifies whether to enable or disable inheritance of ACEs from parent objects.
 
    .PARAMETER RemoveInheritedAccessRules
    Indicates whether to remove inherited ACEs from the object or principal.
 
    .EXAMPLE
    Set-ADACLInheritance -ADObject 'CN=TestOU,DC=contoso,DC=com' -Inheritance 'Disabled' -RemoveInheritedAccessRules
 
    .EXAMPLE
    Set-ADACLInheritance -ACL $ACL -Inheritance 'Disabled' -RemoveInheritedAccessRules
 
    .EXAMPLE
    Set-ADACLInheritance -ADObject 'CN=TestOU,DC=contoso,DC=com' -Inheritance 'Enabled'
 
    .NOTES
    General notes
    #>

    [cmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'ADObject')]
    param(
        [parameter(ParameterSetName = 'ADObject', Mandatory)][alias('Identity')][Array] $ADObject,
        [parameter(ParameterSetName = 'ACL', Mandatory)][Array] $ACL,

        [Parameter(Mandatory)][ValidateSet('Enabled', 'Disabled')] $Inheritance,
        [switch] $RemoveInheritedAccessRules
    )
    if (-not $Script:ForestDetails) {
        Write-Verbose "Set-ADACLInheritance - Gathering Forest Details"
        $Script:ForestDetails = Get-WinADForestDetails
    }

    $PreserveInheritance = -not $RemoveInheritedAccessRules.IsPresent

    if ($ACL) {
        foreach ($A in $ACL) {
            # isProtected - true to protect the access rules associated with this ObjectSecurity object from inheritance; false to allow inheritance.
            # preserveInheritance - true to preserve inherited access rules; false to remove inherited access rules. This parameter is ignored if isProtected is false.
            if ($Inheritance -eq 'Enabled') {
                $A.ACL.SetAccessRuleProtection($false, -not $RemoveInheritedAccessRules.IsPresent)
                $Action = "Inheritance $Inheritance"
                Write-Verbose "Set-ADACLInheritance - Enabling inheritance for $($A.DistinguishedName)"
            } elseif ($Inheritance -eq 'Disabled') {
                $Action = "Inheritance $Inheritance, RemoveInheritedAccessRules $RemoveInheritedAccessRules"
                $A.ACL.SetAccessRuleProtection($true, $PreserveInheritance)
                Write-Verbose "Set-ADACLInheritance - Disabling inheritance for $($A.DistinguishedName) / Remove Inherited Rules: $($RemoveInheritedAccessRules.IsPresent)"
            }
            $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $A.DistinguishedName
            $QueryServer = $Script:ForestDetails['QueryServers'][$DomainName].HostName[0]

            if ($PSCmdlet.ShouldProcess($A.DistinguishedName, $Action)) {
                Write-Verbose "Set-ADACLInheritance - Saving permissions for $($A.DistinguishedName) on $QueryServer"
                try {
                    Set-ADObject -Identity $A.DistinguishedName -Replace @{ ntSecurityDescriptor = $A.ACL } -ErrorAction Stop -Server $QueryServer
                } catch {
                    Write-Warning "Set-ADACLInheritance - Saving permissions for $($A.DistinguishedName) on $QueryServer failed: $($_.Exception.Message)"
                }
            }
        }
    } else {
        foreach ($Object in $ADObject) {
            $getADACLSplat = @{
                ADObject = $ADObject
                Bundle   = $true
                Resolve  = $true
            }
            $ACL = Get-ADACL @getADACLSplat
            # isProtected - true to protect the access rules associated with this ObjectSecurity object from inheritance; false to allow inheritance.
            # preserveInheritance - true to preserve inherited access rules; false to remove inherited access rules. This parameter is ignored if isProtected is false.
            if ($Inheritance -eq 'Enabled') {
                $ACL.ACL.SetAccessRuleProtection($false, -not $RemoveInheritedAccessRules.IsPresent)
                $Action = "Inheritance $Inheritance"
                Write-Verbose "Set-ADACLInheritance - Enabling inheritance for $($ACL.DistinguishedName)"
            } elseif ($Inheritance -eq 'Disabled') {
                $Action = "Inheritance $Inheritance, RemoveInheritedAccessRules $RemoveInheritedAccessRules"
                $ACL.ACL.SetAccessRuleProtection($true, $PreserveInheritance)
                Write-Verbose "Set-ADACLInheritance - Disabling inheritance for $($ACL.DistinguishedName) / Remove Inherited Rules: $($RemoveInheritedAccessRules.IsPresent)"
            }
            $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $ACL.DistinguishedName
            $QueryServer = $Script:ForestDetails['QueryServers'][$DomainName].HostName[0]

            if ($PSCmdlet.ShouldProcess($ACL.DistinguishedName, $Action)) {
                Write-Verbose "Set-ADACLInheritance - Saving permissions for $($ACL.DistinguishedName) on $QueryServer"
                try {
                    Set-ADObject -Identity $ACL.DistinguishedName -Replace @{ ntSecurityDescriptor = $ACL.ACL } -ErrorAction Stop -Server $QueryServer
                    # Set-Acl -Path $ACL.Path -AclObject $ACL.ACL -ErrorAction Stop
                } catch {
                    Write-Warning "Set-ADACLInheritance - Saving permissions for $($ACL.DistinguishedName) on $QueryServer failed: $($_.Exception.Message)"
                }
            }
        }
    }
}
function Set-ADACLOwner {
    <#
    .SYNOPSIS
    Sets the owner of the ACLs on specified Active Directory objects to a specified principal.
 
    .DESCRIPTION
    This cmdlet sets the owner of the ACLs on specified Active Directory objects to a specified principal. It supports setting the owner on multiple objects at once and can handle both local and remote operations. It also provides verbose and warning messages to facilitate troubleshooting.
 
    .PARAMETER ADObject
    Specifies the Active Directory objects on which to set the owner. This can be a list of objects or a list of Distinguished Names of objects.
 
    .PARAMETER Principal
    Specifies the principal to set as the owner of the ACLs. This can be a string in the format of 'Domain\Username' or 'Username@Domain'.
 
    .EXAMPLE
    Set-ADACLOwner -ADObject 'OU=Users,DC=example,DC=com', 'CN=Computers,DC=example,DC=com' -Principal 'example\DomainAdmins'
 
    This example sets the owner of the ACLs on the specified OU and CN to 'example\DomainAdmins'.
 
    .EXAMPLE
    Set-ADACLOwner -ADObject 'CN=User1,DC=example,DC=com', 'CN=Computer1,DC=example,DC=com' -Principal 'DomainAdmins@example.com'
 
    This example sets the owner of the ACLs on the specified user and computer to 'DomainAdmins@example.com'.
    #>

    [cmdletBinding(SupportsShouldProcess)]
    param(
        [parameter(Mandatory)][alias('Identity')][Array] $ADObject,
        [Parameter(Mandatory)][string] $Principal
    )
    Begin {
        if ($Principal -is [string]) {
            if ($Principal -like '*/*') {
                $SplittedName = $Principal -split '/'
                [System.Security.Principal.IdentityReference] $PrincipalIdentity = [System.Security.Principal.NTAccount]::new($SplittedName[0], $SplittedName[1])
            } else {
                [System.Security.Principal.IdentityReference] $PrincipalIdentity = [System.Security.Principal.NTAccount]::new($Principal)
            }
        } else {
            # Not yet ready
            return
        }
    }
    Process {
        foreach ($Object in $ADObject) {
            #$ADObjectData = $null
            if ($Object -is [Microsoft.ActiveDirectory.Management.ADOrganizationalUnit] -or $Object -is [Microsoft.ActiveDirectory.Management.ADEntity]) {
                # if object already has proper security descriptor we don't need to do additional querying
                #if ($Object.ntSecurityDescriptor) {
                # $ADObjectData = $Object
                #}
                [string] $DistinguishedName = $Object.DistinguishedName
                [string] $CanonicalName = $Object.CanonicalName
                [string] $ObjectClass = $Object.ObjectClass
            } elseif ($Object -is [string]) {
                [string] $DistinguishedName = $Object
                [string] $CanonicalName = ''
                [string] $ObjectClass = ''
            } else {
                Write-Warning "Set-ADACLOwner - Object not recognized. Skipping..."
                continue
            }

            $DNConverted = (ConvertFrom-DistinguishedName -DistinguishedName $DistinguishedName -ToDC) -replace '=' -replace ','
            if (-not (Get-PSDrive -Name $DNConverted -ErrorAction SilentlyContinue)) {
                Write-Verbose "Set-ADACLOwner - Enabling PSDrives for $DistinguishedName to $DNConverted"
                New-ADForestDrives -ForestName $ForestName # -ObjectDN $DistinguishedName
                if (-not (Get-PSDrive -Name $DNConverted -ErrorAction SilentlyContinue)) {
                    Write-Warning "Set-ADACLOwner - Drive $DNConverted not mapped. Terminating..."
                    continue
                }
            }
            $PathACL = "$DNConverted`:\$($DistinguishedName)"
            try {
                $ACLs = Get-Acl -Path $PathACL -ErrorAction Stop
            } catch {
                Write-Warning "Get-ADACL - Path $DistinguishedName / $PathACL - Error: $($_.Exception.Message)"
                continue
            }
            <#
            if (-not $ADObjectData) {
                try {
                    $ADObjectData = Get-ADObject -Identity $DistinguishedName -Properties ntSecurityDescriptor -ErrorAction Stop
                    $ACLs = $ADObjectData.ntSecurityDescriptor
                } catch {
                    Write-Warning "Get-ADACL - Path $DistinguishedName - Error: $($_.Exception.Message)"
                    continue
                }
            }
            #>

            $CurrentOwner = $ACLs.Owner
            Write-Verbose "Set-ADACLOwner - Changing owner from $($CurrentOwner) to $PrincipalIdentity for $($DistinguishedName)"
            try {
                $ACLs.SetOwner($PrincipalIdentity)
            } catch {
                Write-Warning "Set-ADACLOwner - Unable to change owner from $($CurrentOwner) to $PrincipalIdentity for $($DistinguishedName): $($_.Exception.Message)"
                continue
            }
            try {
                #Set-ADObject -Identity $DistinguishedName -Replace @{ ntSecurityDescriptor = $ACLs } -ErrorAction Stop
                Set-Acl -Path $PathACL -AclObject $ACLs -ErrorAction Stop
            } catch {
                Write-Warning "Set-ADACLOwner - Unable to change owner from $($CurrentOwner) to $PrincipalIdentity for $($DistinguishedName): $($_.Exception.Message)"
            }
            # }
        }
    }
    End {
    }
}
function Set-DnsServerIP {
    <#
    .SYNOPSIS
    Sets the DNS server IP addresses for a specified list of computers.
 
    .DESCRIPTION
    This cmdlet sets the DNS server IP addresses for a specified list of computers. It supports both local and remote operations.
    It can use a credential for remote connections. It filters out adapters that are DHCP enabled or do not have a DNS server search order set.
    It then sets the DNS server IP addresses for the remaining adapters. If the operation is successful, it retrieves the current DNS server IP addresses.
 
    .PARAMETER ComputerName
    Specifies the names of the computers on which to set the DNS server IP addresses.
 
    .PARAMETER DnsIpAddress
    Specifies the IP addresses of the DNS servers to set.
 
    .PARAMETER Credential
    Specifies the credentials to use for remote connections.
 
    .EXAMPLE
    Set-DnsServerIP -ComputerName 'Computer1', 'Computer2' -DnsIpAddress '8.8.8.8', '8.8.4.4'
 
    This example sets the DNS server IP addresses to '8.8.8.8' and '8.8.4.4' for 'Computer1' and 'Computer2'.
 
    .EXAMPLE
    Set-DnsServerIP -ComputerName 'Computer1', 'Computer2' -DnsIpAddress '8.8.8.8', '8.8.4.4' -Credential (Get-Credential)
 
    This example sets the DNS server IP addresses to '8.8.8.8' and '8.8.4.4' for 'Computer1' and 'Computer2' using the credentials provided by the user.
    #>

    [alias('Set-WinDNSServerIP')]
    [cmdletbInding(SupportsShouldProcess)]
    param(
        [string[]] $ComputerName,
        [string[]] $DnsIpAddress,
        [pscredential] $Credential
    )
    foreach ($Computer in $Computers) {
        try {
            if ($Credential) {
                $CimSession = New-CimSession -ComputerName $Computer -Credential $Credential -Authentication Negotiate -ErrorAction Stop
            } else {
                $CimSession = New-CimSession -ComputerName $Computer -ErrorAction Stop -Authentication Negotiate
            }
        } catch {
            Write-Warning "Couldn't authorize session to $Computer. Error $($_.Exception.Message). Skipping."
            continue
        }

        $Adapters = Get-CimData -Class Win32_NetworkAdapterConfiguration -ComputerName $Computer | Where-Object { $_.DHCPEnabled -ne 'True' -and $null -ne $_.DNSServerSearchOrder }
        if ($Adapters) {
            $Text = "Setting DNS to $($DNSIPAddress -join ', ')"
            if ($PSCmdlet.ShouldProcess($Computer, $Text)) {
                if ($Adapters) {
                    try {
                        $Adapters | Set-DnsClientServerAddress -ServerAddresses $DnsIpAddress -CimSession $CimSession
                    } catch {
                        Write-Warning "Couldn't fix adapters with IP Address for $Computer. Error $($_.Exception.Message)"
                        continue
                    }
                }
                Get-DNSServerIP -ComputerName $Computer
            }
        }
    }
}
function Set-WinADDiagnostics {
    <#
    .SYNOPSIS
    Sets the diagnostics level for various Active Directory components on specified domain controllers.
 
    .DESCRIPTION
    This cmdlet sets the diagnostics level for various Active Directory components on specified domain controllers. It allows you to specify the forest name, domains, domain controllers, and diagnostics components to target. Additionally, it provides options to exclude certain domains and domain controllers, as well as skip read-only domain controllers.
 
    .PARAMETER Forest
    Specifies the name of the forest for which to set the diagnostics.
 
    .PARAMETER ExcludeDomains
    Specifies an array of domain names to exclude from the operation.
 
    .PARAMETER ExcludeDomainControllers
    Specifies an array of domain controller names to exclude from the operation.
 
    .PARAMETER IncludeDomains
    Specifies an array of domain names to include in the operation.
 
    .PARAMETER IncludeDomainControllers
    Specifies an array of domain controller names to include in the operation.
 
    .PARAMETER SkipRODC
    Specifies whether to skip read-only domain controllers.
 
    .PARAMETER Diagnostics
    Specifies an array of diagnostics components to set. Valid values include:
    - Knowledge Consistency Checker (KCC)
    - Security Events
    - ExDS Interface Events
    - MAPI Interface Events
    - Replication Events
    - Garbage Collection
    - Internal Configuration
    - Directory Access
    - Internal Processing
    - Performance Counters
    - Initialization / Termination
    - Service Control
    - Name Resolution
    - Backup
    - Field Engineering
    - LDAP Interface Events
    - Setup
    - Global Catalog
    - Inter-site Messaging
    - Group Caching
    - Linked-Value Replication
    - DS RPC Client
    - DS RPC Server
    - DS Schema
    - Transformation Engine
    - Claims-Based Access Control
    - Netlogon
 
    .PARAMETER Level
    Specifies the level of diagnostics to set. Valid values include:
    - None: Only critical events and error events are logged.
    - Minimal: Very high-level events are recorded.
    - Basic: More detailed information is recorded.
    - Extensive: Detailed information, including steps performed to complete tasks, is recorded.
    - Verbose: All events, including debug strings and configuration changes, are logged.
    - Internal: A complete log of the service is recorded.
 
    .PARAMETER ExtendedForestInformation
    Specifies additional information about the forest.
 
    .EXAMPLE
    Set-WinADDiagnostics -Forest 'example.local' -Diagnostics 'Security Events', 'Replication Events' -Level 'Basic'
    Sets the diagnostics level for Security Events and Replication Events to Basic on all domain controllers in the example.local forest.
 
    .EXAMPLE
    Set-WinADDiagnostics -Forest 'example.local' -IncludeDomainControllers 'dc1.example.local', 'dc2.example.local' -Diagnostics 'Netlogon' -Level 'Verbose'
    Sets the diagnostics level for Netlogon to Verbose on the specified domain controllers in the example.local forest.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [ValidateSet(
            'Knowledge Consistency Checker (KCC)',
            'Security Events',
            'ExDS Interface Events',
            'MAPI Interface Events',
            'Replication Events',
            'Garbage Collection',
            'Internal Configuration',
            'Directory Access',
            'Internal Processing',
            'Performance Counters',
            'Initialization / Termination',
            'Service Control',
            'Name Resolution',
            'Backup',
            'Field Engineering',
            'LDAP Interface Events',
            'Setup',
            'Global Catalog',
            'Inter-site Messaging',

            #New to Windows Server 2003:
            'Group Caching',
            'Linked-Value Replication',
            'DS RPC Client',
            'DS RPC Server',
            'DS Schema',

            #New to Windows Server 2012 and Windows 8:
            'Transformation Engine',
            'Claims-Based Access Control',
            # Added, but not setting in same place
            'Netlogon'
        )][string[]] $Diagnostics,
        #[ValidateSet('None', 'Minimal', 'Basic', 'Extensive', 'Verbose', 'Internal')]
        [string] $Level,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )

    <# Levels
    0 (None): Only critical events and error events are logged at this level. This is the default setting for all entries, and it should be modified only if a problem occurs that you want to investigate.
    1 (Minimal): Very high-level events are recorded in the event log at this setting. Events may include one message for each major task that is performed by the service. Use this setting to start an investigation when you do not know the location of the problem.
    2 (Basic)
    3 (Extensive): This level records more detailed information than the lower levels, such as steps that are performed to complete a task. Use this setting when you have narrowed the problem to a service or a group of categories.
    4 (Verbose)
    5 (Internal): This level logs all events, including debug strings and configuration changes. A complete log of the service is recorded. Use this setting when you have traced the problem to a particular category of a small set of categories.
    #>

    $LevelsDictionary = @{
        'None'      = 0
        'Minimal'   = 1
        'Basic'     = 2
        'Extensive' = 3
        'Verbose'   = 4
        'Internal'  = 5
    }
    $Type = @{
        'Knowledge Consistency Checker (KCC)' = '1 Knowledge Consistency Checker'
        'Security Events'                     = '2 Security Events'
        'ExDS Interface Events'               = '3 ExDS Interface Events'
        'MAPI Interface Events'               = '4 MAPI Interface Events'
        'Replication Events'                  = '5 Replication Events'
        'Garbage Collection'                  = '6 Garbage Collection'
        'Internal Configuration'              = '7 Internal Configuration'
        'Directory Access'                    = '8 Directory Access'
        'Internal Processing'                 = '9 Internal Processing'
        'Performance Counters'                = '10 Performance Counters'
        'Initialization / Termination'        = '11 Initialization/Termination'
        'Service Control'                     = '12 Service Control'
        'Name Resolution'                     = '13 Name Resolution'
        'Backup'                              = '14 Backup'
        'Field Engineering'                   = '15 Field Engineering'
        'LDAP Interface Events'               = '16 LDAP Interface Events'
        'Setup'                               = '17 Setup'
        'Global Catalog'                      = '18 Global Catalog'
        'Inter-site Messaging'                = '19 Inter-site Messaging'
        #New to Windows Server 2003: = #New to Windows Server 2003:
        'Group Caching'                       = '20 Group Caching'
        'Linked-Value Replication'            = '21 Linked-Value Replication'
        'DS RPC Client'                       = '22 DS RPC Client'
        'DS RPC Server'                       = '23 DS RPC Server'
        'DS Schema'                           = '24 DS Schema'
        #New to Windows Server 2012 and Windows 8: = #New to Windows Server 2012 and Windows 8:
        'Transformation Engine'               = '25 Transformation Engine'
        'Claims-Based Access Control'         = '26 Claims-Based Access Control'
    }
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    [Array] $Computers = $ForestInformation.ForestDomainControllers.HostName
    foreach ($Computer in $Computers) {
        foreach ($D in $Diagnostics) {
            if ($D) {
                $DiagnosticsType = $Type[$D]
                $DiagnosticsLevel = $LevelsDictionary[$Level]
                if ($null -ne $DiagnosticsType -and $null -ne $DiagnosticsLevel) {
                    Write-Verbose "Set-WinADDiagnostics - Setting $DiagnosticsType to $DiagnosticsLevel on $Computer"
                    Set-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Services\NTDS\Diagnostics' -Type REG_DWORD -Key $DiagnosticsType -Value $DiagnosticsLevel -ComputerName $Computer
                } else {
                    if ($D -eq 'Netlogon') {
                        # https://support.microsoft.com/en-us/help/109626/enabling-debug-logging-for-the-netlogon-service
                        # Weirdly enough nltest sets it as REG_SZ and article above says REG_DWORD
                        if ($Level -eq 'None') {
                            # nltest /dbflag:0x2080ffff # Enable
                            Write-Verbose "Set-WinADDiagnostics - Setting Netlogon Diagnostics to Enabled on $Computer"
                            Set-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' -Type REG_DWORD -Key 'DbFlag' -Value 0 -ComputerName $Computer -Verbose:$false
                        } else {
                            # nltest /dbflag:0x0 # Disable
                            Write-Verbose "Set-WinADDiagnostics - Setting Netlogon Diagnostics to Disabled on $Computer"
                            Set-PSRegistry -RegistryPath 'HKLM\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters' -Type REG_DWORD -Key 'DbFlag' -Value 545325055 -ComputerName $Computer -Verbose:$false
                        }
                        # Retart of NetLogon service is not required.
                    }
                }
            }
        }
    }
}

[scriptblock] $LevelAutoCompleter = {
    param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameters)

    @('None', 'Minimal', 'Basic', 'Extensive', 'Verbose', 'Internal')
}

Register-ArgumentCompleter -CommandName Set-WinADDiagnostics -ParameterName Level -ScriptBlock $LevelAutoCompleter
function Set-WinADDomainControllerNetLogonSettings {
    <#
    .SYNOPSIS
    Helps settings SiteCoverage, GCSiteCoverage and RequireSeal on Domain Controllers
 
    .DESCRIPTION
    Helps settings SiteCoverage, GCSiteCoverage and RequireSeal on Domain Controllers
 
    .PARAMETER DomainController
    Specifies the Domain Controller to set information on
 
    .PARAMETER SiteCoverage
    Specifies the Site Coverage to set on the Domain Controller. If null, it will remove the Site Coverage
 
    .PARAMETER GCSiteCoverage
    Specifies the GC Site Coverage to set on the Domain Controller. If null, it will remove the GC Site Coverage
 
    .PARAMETER RequireSeal
    Specifies the RequireSeal to set on the Domain Controller. Possible values are Disabled, Compatibility, Enforcement
 
    .EXAMPLE
    An example
 
    .NOTES
    SiteCoverage:
    - https://www.oreilly.com/library/view/active-directory-cookbook/0596004648/ch11s20.html
    - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.NetLogon::Netlogon_AutoSiteCoverage
    - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.NetLogon::Netlogon_SiteCoverage
    - https://admx.help/?Category=Windows_10_2016&Policy=Microsoft.Policies.NetLogon::Netlogon_GcSiteCoverage
 
    RequireSeal:
    - https://support.microsoft.com/en-us/topic/kb5021130-how-to-manage-the-netlogon-protocol-changes-related-to-cve-2022-38023-46ea3067-3989-4d40-963c-680fd9e8ee25
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param(
        [Alias('ComputerName')][string] $DomainController,
        [string[]] $SiteCoverage,
        [string[]] $GCSiteCoverage,
        [ValidateSet('Disabled', 'Compatibility', 'Enforcement')] $RequireSeal,
        [switch] $DoNotSuppress
    )
    $RegistryNetLogon = Get-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -ComputerName $DomainController

    if ($RequireSeal) {
        $RequireSealTranslation = @{
            'Disabled'      = 0
            'Compatibility' = 1 # this shouldn't be used after 2023
            'Enforcement'   = 2
        }
        $RequireSealValue = $RequireSealTranslation[$RequireSeal]
        if ($RegistryNetLogon.'RequireSignOrSeal' -ne $RequireSealValue) {
            if ($PSCmdlet.ShouldProcess("Setting RequireSignOrSeal to $RequireSealValue")) {
                $Output = Set-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Key 'RequireSeal' -Value $RequireSealValue -Type REG_DWORD -ComputerName $DomainController
                if ($DoNotSuppress) {
                    $Output
                }
            }
        }
    }

    if ($PSBoundParameters.ContainsKey('SiteCoverage')) {
        if ($null -eq $SiteCoverage) {
            if ($null -ne $RegistryNetLogon.'SiteCoverage') {
                if ($PSCmdlet.ShouldProcess($DomainController, "Removing SiteCoverage from Domain Controller")) {
                    $Output = Remove-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Key 'SiteCoverage' -ComputerName $DomainController
                    if ($DoNotSuppress) {
                        $Output
                    }
                }
            }
        } else {
            if ($SiteCoverage -isnot [string]) {
                $JoinedSiteCoverage = $SiteCoverage -join ','
            } else {
                $JoinedSiteCoverage = $SiteCoverage
            }
            if ($RegistryNetLogon.'SiteCoverage' -ne $JoinedSiteCoverage) {
                if ($PSCmdlet.ShouldProcess($DomainController, "Setting SiteCoverage to $JoinedSiteCoverage")) {
                    $Output = Set-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Type REG_SZ -Key 'SiteCoverage' -Value $JoinedSiteCoverage -ComputerName $DomainController
                    if ($DoNotSuppress) {
                        $Output
                    }
                }
            }
        }
    }
    if ($PSBoundParameters.ContainsKey('GCSiteCoverage')) {
        if ($null -eq $GCSiteCoverage) {
            if ($null -ne $RegistryNetLogon.'GCSiteCoverage') {
                if ($PSCmdlet.ShouldProcess($DomainController, "Removing GCSiteCoverage from Domain Controller")) {
                    $Output = Remove-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Key 'GcSiteCoverage' -ComputerName $DomainController
                    if ($DoNotSuppress) {
                        $Output
                    }
                }
            }
        } else {
            if ($GCSiteCoverage -isnot [string]) {
                $JoinedGCSiteCoverage = $GCSiteCoverage -join ','
            } else {
                $JoinedGCSiteCoverage = $GCSiteCoverage
            }
            if ($RegistryNetLogon.'GCSiteCoverage' -ne $JoinedGCSiteCoverage) {
                if ($PSCmdlet.ShouldProcess($DomainController, "Setting GCSiteCoverage to $JoinedGCSiteCoverage")) {
                    $Output = Set-PSRegistry -RegistryPath "HKEY_LOCAL_MACHINE\SYSTEM\CurrentControlSet\Services\Netlogon\Parameters" -Type REG_SZ -Key 'GcSiteCoverage' -Value $JoinedGCSiteCoverage -ComputerName $DomainController
                    if ($DoNotSuppress) {
                        $Output
                    }
                }
            }
        }
    }
}
function Set-WinADDomainControllerOption {
    <#
    .SYNOPSIS
    Command to set the options of a domain controller
 
    .DESCRIPTION
    Command to set the options of a domain controller that uses the repadmin command
 
    Available options:
    - DISABLE_OUTBOUND_REPL: Disables outbound replication.
    - DISABLE_INBOUND_REPL: Disables inbound replication.
    - DISABLE_NTDSCONN_XLATE: Disables the translation of NTDSConnection objects.
    - DISABLE_SPN_REGISTRATION: Disables Service Principal Name (SPN) registration.
    - IS_GC: Sets or unsets the Global Catalog (GC) for the domain controller.
 
    .PARAMETER DomainController
    The domain controller to set the options on
 
    .PARAMETER Option
    Choose one or more options from the list of available options to enable or disable
 
    Options:
    - DISABLE_OUTBOUND_REPL: Disables outbound replication.
    - DISABLE_INBOUND_REPL: Disables inbound replication.
    - DISABLE_NTDSCONN_XLATE: Disables the translation of NTDSConnection objects.
    - DISABLE_SPN_REGISTRATION: Disables Service Principal Name (SPN) registration.
    - IS_GC: Sets or unsets the Global Catalog (GC) for the domain controller.
 
    .PARAMETER Action
    Choose to enable or disable the option(s)
 
    .EXAMPLE
    Set-WinADDomainControllerOption -DomainController 'ADRODC' -Option 'IS_GC' -Action Enable
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [parameter(Mandatory)][string]$DomainController,
        [ValidateSet(
            "DISABLE_OUTBOUND_REPL", "DISABLE_INBOUND_REPL",
            "DISABLE_NTDSCONN_XLATE", "DISABLE_SPN_REGISTRATION",
            "IS_GC"
        )]
        [parameter(Mandatory)][string[]]$Option,
        [parameter(Mandatory)][ValidateSet("Enable", "Disable")][string]$Action
    )

    # Validate Domain Controller input
    if (-not $DomainController) {
        Write-Host "Domain Controller is required."
        return
    }

    # Determine the action to be taken
    $actionFlag = switch ($Action) {
        "Enable" {
            "+" 
        }
        "Disable" {
            "-" 
        }
    }

    foreach ($O in $Option) {
        # Construct the repadmin command
        # Execute the repadmin command
        try {
            $NewOptions = $null
            $CurrentOptions = $null
            Write-Verbose -Message "Set-WinADDomainControllerOption - Executing repadmin /options $DomainController $actionFlag$O"
            $Output = & repadmin /options $DomainController $actionFlag$O
            if ($Output) {
                foreach ($O in $Output) {
                    if ($O.StartsWith("Current DSA Options:")) {
                        $Options = $O.Split(":")[1].Trim().Split(",")
                        $CurrentOptions = foreach ($O in $Options) {
                            $Value = $O.Trim()
                            if ($Value) {
                                $Value
                            }
                        }
                    } elseif ($O.StartsWith("New DSA Options:")) {
                        $Options = $O.Split(":")[1].Trim().Split(",")
                        $NewOptions = foreach ($O in $Options) {
                            $Value = $O.Trim()
                            if ($Value) {
                                $Value
                            }
                        }
                    }
                }
                If ($CurrentOptions) {
                    $Status = $true
                } else {
                    $Status = $false
                }
                if ($CurrentOptions -eq $NewOptions) {
                    $ActionStatus = $false
                } else {
                    $ActionStatus = $true
                }
                [PSCustomObject] @{
                    DomainController = $DomainController
                    Status           = $Status
                    Action           = $Action
                    ActionStatus     = $ActionStatus
                    ActionStatusText = if ($ActionStatus) {
                        "Changed" 
                    } else {
                        "No changes" 
                    }
                    CurrentOptions   = $CurrentOptions -split " "
                    NewOptions       = $NewOptions -split " "
                    Output           = $Output
                }
            }
        } catch {
            Write-Warning -Message "Set-WinADDomainControllerOption - Failed to execute repadmin /options $DomainController $actionFlag$O. Exception: $($_.Exception.Message)"
        }
    }
}
function Set-WinADForestACLOwner {
    <#
    .SYNOPSIS
    Replaces the owner of the ACLs on all the objects (to Domain Admins) in the forest (or specific domain) that are not Administrative or WellKnownAdministrative.
 
    .DESCRIPTION
    Replaces the owner of the ACLs on all the objects (to Domain Admins) in the forest (or specific domain) that are not Administrative or WellKnownAdministrative.
 
    .PARAMETER IncludeOwnerType
    Defines which object owners are to be included in the replacement. Options are: 'WellKnownAdministrative', 'Administrative', 'NotAdministrative', 'Unknown'
 
    .PARAMETER ExcludeOwnerType
    Defines which object owners are to be included in the replacement. Options are: 'WellKnownAdministrative', 'Administrative', 'NotAdministrative', 'Unknown'
 
    .PARAMETER LimitProcessing
    Provide limit of objects that will be processed in a single run
 
    .PARAMETER Principal
    Defines the principal to be used as the new owner. By default those are Domain Admins for all objects. If you want to use a different principal, you can specify it here. Not really useful as the idea is to always have Domain Admins as object owners.
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .PARAMETER ADAdministrativeGroups
    Ability to provide AD Administrative Groups from another command to speed up processing
 
    .EXAMPLE
    Set-WinADForestACLOwner -WhatIf -Verbose -LimitProcessing 2 -IncludeOwnerType 'NotAdministrative', 'Unknown'
 
    .NOTES
    General notes
    #>

    [cmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Include')]
    param(
        [parameter(Mandatory, ParameterSetName = 'Include')][validateSet('WellKnownAdministrative', 'Administrative', 'NotAdministrative', 'Unknown')][string[]] $IncludeOwnerType,
        [parameter(Mandatory, ParameterSetName = 'Exclude')][validateSet('WellKnownAdministrative', 'Administrative', 'NotAdministrative', 'Unknown')][string[]] $ExcludeOwnerType,
        [int] $LimitProcessing,

        [string] $Principal,

        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [System.Collections.IDictionary] $ExtendedForestInformation,
        [System.Collections.IDictionary] $ADAdministrativeGroups
    )

    $Count = 0
    $getWinADACLForestSplat = @{
        Owner                     = $true
        IncludeOwnerType          = $IncludeOwnerType
        ExcludeOwnerType          = $ExcludeOwnerType
        Forest                    = $Forest
        IncludeDomains            = $IncludeDomains
        ExcludeDomains            = $ExcludeDomains
        ExtendedForestInformation = $ExtendedForestInformation
    }
    Remove-EmptyValue -Hashtable $getWinADACLForestSplat

    if (-not $ADAdministrativeGroups) {
        $ADAdministrativeGroups = Get-ADADministrativeGroups -Type DomainAdmins, EnterpriseAdmins -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
    }

    Get-WinADACLForest @getWinADACLForestSplat | ForEach-Object {
        if (-not $Principal) {
            $DomainName = ConvertFrom-DistinguishedName -ToDomainCN -DistinguishedName $_.DistinguishedName
            $Principal = $ADAdministrativeGroups[$DomainName]['DomainAdmins']
        }
        $Count += 1
        Set-ADACLOwner -ADObject $_.DistinguishedName -Principal $Principal
        if ($LimitProcessing -gt 0 -and $Count -ge $LimitProcessing) {
            break
        }
    }
}
function Set-WinADReplication {
    <#
    .SYNOPSIS
    Sets the replication interval for site links in an Active Directory forest.
 
    .DESCRIPTION
    This cmdlet sets the replication interval for site links within a specified Active Directory forest. It can also enable instant replication for site links if desired. The cmdlet supports setting a custom replication interval and provides an option to force instant replication.
 
    .PARAMETER Forest
    The name of the Active Directory forest for which to set the replication interval.
 
    .PARAMETER ReplicationInterval
    The interval in minutes to set for replication. The default is 15 minutes.
 
    .PARAMETER Instant
    Switch parameter to enable instant replication for site links.
 
    .PARAMETER ExtendedForestInformation
    Additional information about the forest that can be used to facilitate the operation.
 
    .EXAMPLE
    Set-WinADReplication -Forest 'example.com' -ReplicationInterval 30
    This example sets the replication interval for site links in the 'example.com' forest to 30 minutes.
 
    .EXAMPLE
    Set-WinADReplication -Forest 'example.com' -Instant
    This example enables instant replication for site links in the 'example.com' forest.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and configured.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [int] $ReplicationInterval = 15,
        [switch] $Instant,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    $QueryServer = $ForestInformation.QueryServers['Forest']['HostName'][0]
    $NamingContext = (Get-ADRootDSE -Server $QueryServer).configurationNamingContext
    Get-ADObject -LDAPFilter "(objectCategory=sitelink)" -SearchBase $NamingContext -Properties options, replInterval -Server $QueryServer | ForEach-Object {
        if ($Instant) {
            Set-ADObject $_ -Replace @{ replInterval = $ReplicationInterval } -Server $QueryServer
            Set-ADObject $_ -Replace @{ options = $($_.options -bor 1) } -Server $QueryServer
        } else {
            Set-ADObject $_ -Replace @{ replInterval = $ReplicationInterval } -Server $QueryServer
        }
    }
}
function Set-WinADReplicationConnections {
    <#
    .SYNOPSIS
    Modifies the replication connections within an Active Directory forest.
 
    .DESCRIPTION
    This cmdlet updates the replication connections within a specified Active Directory forest. It can be used to enable or disable specific connection options for each connection. The cmdlet supports modifying connections that are automatically generated or manually created.
 
    .PARAMETER Forest
    Specifies the name of the Active Directory forest for which to modify the replication connections.
 
    .PARAMETER Force
    Forces the modification of all replication connections, including those that are automatically generated.
 
    .PARAMETER ExtendedForestInformation
    Provides additional information about the forest that can be used to facilitate the operation.
 
    .EXAMPLE
    Set-WinADReplicationConnections -Forest 'example.com' -Force
    This example modifies all replication connections within the 'example.com' forest, including automatically generated ones.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and configured.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [switch] $Force,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )

    [Flags()]
    enum ConnectionOption {
        None
        IsGenerated
        TwoWaySync
        OverrideNotifyDefault = 4
        UseNotify = 8
        DisableIntersiteCompression = 16
        UserOwnedSchedule = 32
        RodcTopology = 64
    }

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    $QueryServer = $ForestInformation.QueryServers['Forest']['HostName'][0]

    $NamingContext = (Get-ADRootDSE -Server $QueryServer).configurationNamingContext
    $Connections = Get-ADObject -SearchBase $NamingContext -LDAPFilter "(objectCategory=ntDSConnection)" -Properties * -Server $QueryServer
    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) } -Server $QueryServer
        }
    }
}
function Set-WinADShare {
    <#
    .SYNOPSIS
    Sets the owner or displays permissions for a specified Windows Active Directory share.
 
    .DESCRIPTION
    This cmdlet sets the owner or displays permissions for a specified Windows Active Directory share. It can target a specific share type across multiple domains or a single path. It also supports setting the owner to a specific principal or to the default owner.
 
    .PARAMETER Path
    The path to the share to set the owner or display permissions for. This parameter is required if the ShareType parameter is not specified.
 
    .PARAMETER ShareType
    The type of share to target. This parameter is required if the Path parameter is not specified. Valid values are 'NetLogon'.
 
    .PARAMETER Owner
    Switch parameter to indicate that the owner of the share should be set. If this parameter is not specified, the cmdlet will display the permissions of the share instead.
 
    .PARAMETER Principal
    The principal to set as the owner of the share. This parameter is required if the Owner parameter is specified and the ParameterSetName is 'Principal'.
 
    .PARAMETER Type
    The type of share to set the owner for. This parameter is required if the Owner parameter is specified and the ParameterSetName is 'Type'. Valid values are 'Default'.
 
    .EXAMPLE
    Set-WinADShare -Path "\\example.com\NetLogon" -Owner -Principal "Domain Admins"
    This example sets the owner of the NetLogon share on the example.com domain to "Domain Admins".
 
    .EXAMPLE
    Set-WinADShare -ShareType NetLogon -Owner -Type Default
    This example sets the owner of all NetLogon shares across all domains to the default owner.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and configured.
    #>

    [cmdletBinding(SupportsShouldProcess, DefaultParameterSetName = 'Type')]
    param(
        [string] $Path,
        [validateset('NetLogon')][string[]] $ShareType,
        [switch] $Owner,
        [Parameter(ParameterSetName = 'Principal', Mandatory)][string] $Principal,
        [Parameter(ParameterSetName = 'Type', Mandatory)]
        [validateset('Default')][string[]] $Type
    )
    if ($ShareType) {
        $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation
        foreach ($Domain in $ForestInformation.Domains) {
            $Path = -join ("\\", $Domain, "\$ShareType")
            @(Get-Item -Path $Path) + @(Get-ChildItem -Path $Path -Recurse:$true) | ForEach-Object -Process {
                if ($Owner) {
                    Get-FileOwner -JustPath -Path $_ -Resolve
                } else {
                    Get-FilePermission -Path $_ -ResolveTypes -Extended
                }
            }
        }
    } else {
        if ($Path -and (Test-Path -Path $Path)) {
            @(Get-Item -Path $Path) + @(Get-ChildItem -Path $Path -Recurse:$true) | ForEach-Object -Process {
                if ($Owner) {
                    $IdentityOwner = Get-FileOwner -JustPath -Path $_.FullName -Resolve
                    if ($PSCmdlet.ParameterSetName -eq 'Principal') {
                    } else {
                        if ($IdentityOwner.OwnerSid -ne 'S-1-5-32-544') {
                            Set-FileOwner -Path $Path -JustPath -Owner 'S-1-5-32-544'
                        } else {
                            Write-Verbose "Set-WinADShare - Owner of $($_.FullName) already set to $($IdentityOwner.OwnerName). Skipping."
                        }
                    }
                } else {
                    Get-FilePermission -Path $_ -ResolveTypes -Extended
                }
            }
        }
    }
}
function Set-WinADTombstoneLifetime {
    <#
    .SYNOPSIS
    Sets the tombstone lifetime for a specified Active Directory forest.
 
    .DESCRIPTION
    This cmdlet sets the tombstone lifetime for a specified Active Directory forest. The tombstone lifetime determines how long a deleted object is retained in the Active Directory database before it is permanently removed.
 
    .PARAMETER Forest
    The name of the Active Directory forest for which to set the tombstone lifetime.
 
    .PARAMETER Days
    The number of days to set as the tombstone lifetime. The default is 180 days.
 
    .PARAMETER ExtendedForestInformation
    Additional information about the forest that can be used to facilitate the operation.
 
    .EXAMPLE
    Set-WinADTombstoneLifetime -Forest 'example.com' -Days 365
    This example sets the tombstone lifetime for the 'example.com' forest to 365 days.
 
    .NOTES
    This cmdlet requires the Active Directory PowerShell module to be installed and configured.
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [int] $Days = 180,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    $QueryServer = $ForestInformation.QueryServers['Forest']['HostName'][0]

    $Partition = $((Get-ADRootDSE -Server $QueryServer).configurationNamingContext)
    Set-ADObject -Identity "CN=Directory Service,CN=Windows NT,CN=Services,$Partition" -Partition $Partition -Replace @{ tombstonelifetime = $Days } -Server $QueryServer
}
function Show-WinADDNSRecords {
    <#
    .SYNOPSIS
    Small command that gathers quick information about DNS Server records and shows them in HTML output
 
    .DESCRIPTION
    Small command that gathers quick information about DNS Server records and shows them in HTML output
 
    .PARAMETER FilePath
    Path to HTML file where it's saved. If not given temporary path is used
 
    .PARAMETER HideHTML
    Prevents HTML output from being displayed in browser after generation is done
 
    .PARAMETER Online
    Forces use of online CDN for JavaScript/CSS which makes the file smaller. Default - use offline.
 
    .EXAMPLE
    Show-WinADDNSRecords
 
    .EXAMPLE
    Show-WinADDNSRecords -FilePath C:\Temp\test.html
 
    .NOTES
    General notes
    #>

    [cmdletBinding()]
    param(
        [parameter(Mandatory)][string] $FilePath,
        [switch] $HideHTML,
        [switch] $Online,
        [switch] $TabPerZone
    )
    # Gather data
    $DNSByName = Get-WinADDnsRecords -Prettify -IncludeDetails
    $DNSByIP = Get-WinADDnsIPAddresses -Prettify -IncludeDetails
    $DNSZones = Get-WinADDNSZones
    $CachedZones = [ordered] @{}
    if ($TabPerZone) {
        foreach ($DnsEntry in $DNSByName) {
            if (-not $CachedZones[$DnsEntry.Zone]) {
                $CachedZones[$DnsEntry.Zone] = [System.Collections.Generic.List[Object]]::new()
            }
            $CachedZones[$DnsEntry.Zone].Add($DnsEntry)
        }
    }
    New-HTML {
        New-HTMLTab -Name 'DNS Zones' {
            New-HTMLTable -DataTable $DNSZones -DataStore JavaScript -Filtering
        }
        New-HTMLTab -Name "DNS by Name" {
            if ($TabPerZone) {
                foreach ($Zone in $CachedZones.Keys) {
                    New-HTMLTab -Name $Zone {
                        New-HTMLTable -DataTable $CachedZones[$Zone] -DataStore JavaScript -Filtering {
                            New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -BackgroundColor LightGreen
                            New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -Operator gt -BackgroundColor Orange
                            New-HTMLTableConditionGroup -Logic AND {
                                New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -Operator gt
                                New-HTMLTableCondition -Name 'Types' -Operator like -ComparisonType string -Value 'static'
                                New-HTMLTableCondition -Name 'Types' -Operator like -ComparisonType string -Value 'dynamic'
                            } -BackgroundColor Rouge -Row -Color White
                            New-HTMLTableCondition -Name 'Status' -ComparisonType string -Value 'Tombstoned' -BackgroundColor Orange -FailBackgroundColor LightGreen
                        }
                    }
                }
            } else {
                New-HTMLTable -DataTable $DNSByName -Filtering {
                    New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -BackgroundColor LightGreen
                    New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -Operator gt -BackgroundColor Orange
                    New-HTMLTableConditionGroup -Logic AND {
                        New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -Operator gt
                        New-HTMLTableCondition -Name 'Types' -Operator like -ComparisonType string -Value 'static'
                        New-HTMLTableCondition -Name 'Types' -Operator like -ComparisonType string -Value 'dynamic'
                    } -BackgroundColor Rouge -Row -Color White
                    New-HTMLTableCondition -Name 'Status' -ComparisonType string -Value 'Tombstoned' -BackgroundColor Orange -FailBackgroundColor LightGreen
                } -DataStore JavaScript
            }
        }
        New-HTMLTab -Name 'DNS by IP' {
            New-HTMLTable -DataTable $DNSByIP -Filtering {
                New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -BackgroundColor LightGreen
                New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -Operator gt -BackgroundColor Orange
                New-HTMLTableConditionGroup -Logic AND {
                    New-HTMLTableCondition -Name 'Count' -ComparisonType number -Value 1 -Operator gt
                    New-HTMLTableCondition -Name 'Types' -Operator like -ComparisonType string -Value 'static'
                    New-HTMLTableCondition -Name 'Types' -Operator like -ComparisonType string -Value 'dynamic'
                } -BackgroundColor Rouge -Row -Color White
            } -DataStore JavaScript
        }
    } -ShowHTML:(-not $HideHTML.IsPresent) -Online:$Online.IsPresent -TitleText "DNS Configuration" -FilePath $FilePath
}
function Show-WinADGroupCritical {
    <#
    .SYNOPSIS
    Command to gather nested group membership from default critical groups in the Active Directory.
 
    .DESCRIPTION
    Command to gather nested group membership from default critical groups in the Active Directory.
    This command will show data in table and diagrams in HTML format.
 
    .PARAMETER GroupName
    Group Name or Names to search for from provided list. If skipped all groups will be checked.
 
    .PARAMETER FilePath
    Path to HTML file where it's saved. If not given temporary path is used
 
    .PARAMETER HideAppliesTo
    Allows to define to which diagram HideComputers,HideUsers,HideOther applies to
 
    .PARAMETER HideComputers
    Hide computers from diagrams - useful for performance reasons
 
    .PARAMETER HideUsers
    Hide users from diagrams - useful for performance reasons
 
    .PARAMETER HideOther
    Hide other objects from diagrams - useful for performance reasons
 
    .PARAMETER Online
    Forces use of online CDN for JavaScript/CSS which makes the file smaller. Default - use offline.
 
    .PARAMETER HideHTML
    Prevents HTML output from being displayed in browser after generation is done
 
    .PARAMETER DisableBuiltinConditions
    Disables table coloring allowing user to define it's own conditions
 
    .PARAMETER AdditionalStatistics
    Adds additional data to Self object. It includes count for NestingMax, NestingGroup, NestingGroupSecurity, NestingGroupDistribution. It allows for easy filtering where we expect security groups only when there are nested distribution groups.
 
    .PARAMETER SkipDiagram
    Skips diagram generation and only displays table. Useful if the diagram can't handle amount of data or if the diagrams are not nessecary.
 
    .PARAMETER Summary
    Adds additional tab with all groups together on two diagrams
 
    .PARAMETER EnableDiagramFiltering
    Enables search in diagrams. It's useful when there are many groups and it's hard to find the one you are looking for.
 
    .PARAMETER DiagramFilteringMinimumCharacters
    Minimum characters to start search in diagrams. Default is 3.
 
    .PARAMETER EnableDiagramFilteringButton
    Adds button to enable/disable filtering in diagrams. It's extension to EnableDiagramFiltering, when you prefer button over automatic filtering.
 
    .PARAMETER ScrollX
    Adds horizontal scroll to the table. Useful when there are many columns.
 
    .EXAMPLE
    Show-WinADGroupCritical
 
    .NOTES
    General notes
    #>

    [alias('Show-WinADCriticalGroups')]
    [cmdletBinding()]
    param(
        [validateSet(
            "Domain Admins",
            "Cert Publishers",
            "Schema Admins",
            "Enterprise Admins",
            "DnsAdmins",
            "DnsAdmins2",
            "DnsUpdateProxy",
            "Group Policy Creator Owners",
            'Protected Users',
            'Key Admins',
            'Enterprise Key Admins',
            'Server Management',
            'Organization Management',
            'DHCP Users',
            'DHCP Administrators',
            'Administrators',
            'Account Operators',
            'Server Operators',
            'Print Operators',
            'Backup Operators',
            'Replicators',
            'Network Configuration Operations',
            'Incoming Forest Trust Builders',
            'Internet Information Services',
            'Event Log Readers',
            'Hyper-V Administrators',
            'Remote Management Users'
        )]
        [string[]] $GroupName,
        [alias('ReportPath')][string] $FilePath,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [switch] $Online,
        [switch] $HideHTML,
        [switch] $DisableBuiltinConditions,
        [switch] $AdditionalStatistics,
        [switch] $SkipDiagram,
        [switch] $Summary,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3,
        [switch] $ScrollX
    )

    $ForestInformation = Get-WinADForestDetails -Extended
    [Array] $ListGroups = foreach ($Domain in $ForestInformation.Domains) {
        $DomainSidValue = $ForestInformation.DomainsExtended[$Domain].DomainSID
        $PriviligedGroups = [ordered] @{
            "Domain Admins"                    = "$DomainSidValue-512"
            "Cert Publishers"                  = "$DomainSidValue-517"
            "Schema Admins"                    = "$DomainSidValue-518"
            "Enterprise Admins"                = "$DomainSidValue-519"
            "DnsAdmins"                        = "$DomainSidValue-1101"
            "DnsAdmins2"                       = "$DomainSidValue-1105"
            "DnsUpdateProxy"                   = "$DomainSidValue-1106"
            "Group Policy Creator Owners"      = "$DomainSidValue-520"
            'Protected Users'                  = "$DomainSidValue-525"
            'Key Admins'                       = "$DomainSidValue-526"
            'Enterprise Key Admins'            = "$DomainSidValue-527"
            'Server Management'                = "$DomainSidValue-1125"
            'Organization Management'          = "$DomainSidValue-1117"
            'DHCP Users'                       = "$DomainSidValue-2111"
            'DHCP Administrators'              = "$DomainSidValue-2112"
            'Administrators'                   = "S-1-5-32-544"
            'Account Operators'                = "S-1-5-32-548"
            'Server Operators'                 = "S-1-5-32-549"
            'Print Operators'                  = "S-1-5-32-550"
            'Backup Operators'                 = "S-1-5-32-551"
            'Replicators'                      = "S-1-5-32-552"
            'Network Configuration Operations' = "S-1-5-32-556"
            'Incoming Forest Trust Builders'   = "S-1-5-32-557"
            'Internet Information Services'    = "S-1-5-32-568"
            'Event Log Readers'                = "S-1-5-32-573"
            'Hyper-V Administrators'           = "S-1-5-32-578"
            'Remote Management Users'          = "S-1-5-32-580"
        }
        foreach ($Group in $PriviligedGroups.Keys) {
            $SearchName = $PriviligedGroups[$Group]
            if ($GroupName -and $Group -notin $GroupName) {
                continue
            }
            $GroupInformation = (Get-ADGroup -Filter "SID -eq '$SearchName'" -Server $ForestInformation['QueryServers'][$Domain].HostName[0] -ErrorAction SilentlyContinue).DistinguishedName
            if ($GroupInformation) {
                $GroupInformation
            }
        }
    }
    if ($ListGroups.Count -gt 0) {
        Show-WinADGroupMember -Identity $ListGroups -HideHTML:$HideHTML.IsPresent -FilePath $FilePath -DisableBuiltinConditions:$DisableBuiltinConditions.IsPresent -Online:$Online.IsPresent -HideUsers:$HideUsers.IsPresent -HideComputers:$HideComputers.IsPresent -AdditionalStatistics:$AdditionalStatistics.IsPresent -Summary:$Summary.IsPresent -SkipDiagram:$SkipDiagram.IsPresent -EnableDiagramFiltering:$EnableDiagramFiltering.IsPresent -DiagramFilteringMinimumCharacters $DiagramFilteringMinimumCharacters -ScrollX:$ScrollX.IsPresent -EnableDiagramFilteringButton:$EnableDiagramFilteringButton.IsPresent
    } else {
        Write-Warning -Message "Show-WinADGroupCritical - Requested group(s) not found."
    }
}
function Show-WinADGroupMember {
    <#
    .SYNOPSIS
    Command to gather nested group membership from one or more groups and display in table with two diagrams
 
    .DESCRIPTION
    Command to gather nested group membership from one or more groups and display in table with two diagrams
    This command will show data in table and diagrams in HTML format.
 
    .PARAMETER Identity
    Group Name or Names to search for
 
    .PARAMETER Conditions
    Provides ability to control look and feel of tables across HTML
 
    .PARAMETER FilePath
    Path to HTML file where it's saved. If not given temporary path is used
 
    .PARAMETER HideAppliesTo
    Allows to define to which diagram HideComputers,HideUsers,HideOther applies to
 
    .PARAMETER HideComputers
    Hide computers from diagrams - useful for performance reasons
 
    .PARAMETER HideUsers
    Hide users from diagrams - useful for performance reasons
 
    .PARAMETER HideOther
    Hide other objects from diagrams - useful for performance reasons
 
    .PARAMETER Online
    Forces use of online CDN for JavaScript/CSS which makes the file smaller. Default - use offline.
 
    .PARAMETER HideHTML
    Prevents HTML output from being displayed in browser after generation is done
 
    .PARAMETER DisableBuiltinConditions
    Disables table coloring allowing user to define it's own conditions
 
    .PARAMETER AdditionalStatistics
    Adds additional data to Self object. It includes count for NestingMax, NestingGroup, NestingGroupSecurity, NestingGroupDistribution. It allows for easy filtering where we expect security groups only when there are nested distribution groups.
 
    .PARAMETER SkipDiagram
    Skips diagram generation and only displays table. Useful if the diagram can't handle amount of data or if the diagrams are not nessecary.
 
    .PARAMETER Summary
    Adds additional tab with all groups together on two diagrams
 
    .PARAMETER SummaryOnly
    Adds one tab with all groups together on two diagrams
 
    .PARAMETER EnableDiagramFiltering
    Enables search in diagrams. It's useful when there are many groups and it's hard to find the one you are looking for.
 
    .PARAMETER DiagramFilteringMinimumCharacters
    Minimum characters to start search in diagrams. Default is 3.
 
    .PARAMETER EnableDiagramFilteringButton
    Adds button to enable/disable filtering in diagrams. It's extension to EnableDiagramFiltering, when you prefer button over automatic filtering.
 
    .PARAMETER ScrollX
    Adds horizontal scroll to the table. Useful when there are many columns.
 
    .EXAMPLE
   Show-WinADGroupMember -GroupName 'Domain Admins' -FilePath $PSScriptRoot\Reports\GroupMembership1.html -Online -Verbose
 
   .EXAMPLE
   Show-WinADGroupMember -GroupName 'Test-Group', 'Domain Admins' -FilePath $PSScriptRoot\Reports\GroupMembership2.html -Online -Verbose
 
   .EXAMPLE
   Show-WinADGroupMember -GroupName 'GDS-TestGroup4' -FilePath $PSScriptRoot\Reports\GroupMembership3.html -Summary -Online -Verbose
 
   .EXAMPLE
   Show-WinADGroupMember -GroupName 'Group1' -Verbose -Online
 
    .NOTES
    General notes
    #>

    [alias('Show-ADGroupMember')]
    [cmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(Position = 0)][alias('GroupName', 'Group')][Array] $Identity,
        [Parameter(Position = 1)][scriptblock] $Conditions,
        [string] $FilePath,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [switch] $Online,
        [switch] $HideHTML,
        [switch] $DisableBuiltinConditions,
        [switch] $AdditionalStatistics,
        [switch] $SkipDiagram,
        [Parameter(ParameterSetName = 'Default')][switch] $Summary,
        [Parameter(ParameterSetName = 'SummaryOnly')][switch] $SummaryOnly,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3,
        [switch] $ScrollX
    )
    $Script:Reporting = [ordered] @{}
    $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Show-WinADGroupMember' -RepositoryOwner 'evotecit' -RepositoryName 'ADEssentials'

    $VisualizeOnly = $false
    if ($FilePath -eq '') {
        $FilePath = Get-FileName -Extension 'html' -Temporary
    }
    $GroupsList = [System.Collections.Generic.List[object]]::new()
    if ($Identity.Count -gt 0) {
        New-HTML -TitleText "Visual Group Membership" {
            New-HTMLHeader {
                New-HTMLSection -Invisible {
                    New-HTMLSection {
                        New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue
                    } -JustifyContent flex-start -Invisible
                    New-HTMLSection {
                        New-HTMLText -Text "ADEssentials - $($Script:Reporting['Version'])" -Color Blue
                    } -JustifyContent flex-end -Invisible
                }
            }
            New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
            New-HTMLTableOption -DataStore JavaScript -BoolAsString
            New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey

            if ($Identity[0].GroupName) {
                $GroupMembersCache = [ordered] @{}
                $VisualizeOnly = $true
                foreach ($Entry in $Identity) {
                    $IdentityGroupName = "($($Entry.GroupName) / $($Entry.GroupDomainName))"
                    if (-not $GroupMembersCache[$IdentityGroupName]) {
                        $GroupMembersCache[$IdentityGroupName] = [System.Collections.Generic.List[PSCustomObject]]::new()
                    }
                    $GroupMembersCache[$IdentityGroupName].Add($Entry)
                }
                [Array] $IdentityList = $GroupMembersCache.Keys
            } else {
                [Array] $IdentityList = $Identity
            }
            foreach ($Group in $IdentityList) {
                if ($null -eq $Group) {
                    continue
                }
                try {
                    Write-Verbose "Show-WinADGroupMember - requesting $Group group nested membership"
                    if ($VisualizeOnly) {
                        [Array] $ADGroup = $GroupMembersCache[$Group]
                    } else {
                        [Array] $ADGroup = Get-WinADGroupMember -Group $Group -All -AddSelf -AdditionalStatistics:$AdditionalStatistics
                    }
                    if ($Summary -or $SummaryOnly) {
                        foreach ($Object in $ADGroup) {
                            $GroupsList.Add($Object)
                        }
                    }
                } catch {
                    Write-Warning "Show-WinADGroupMember - Error processing group $Group. Skipping. Needs investigation why it failed. Error: $($_.Exception.Message)"
                    continue
                }
                Write-Verbose "Show-WinADGroupMember - processing HTML generation for $Group group"
                if (-not $SummaryOnly) {
                    if ($ADGroup) {
                        # Means group returned something
                        $GroupName = $ADGroup[0].GroupName
                        $NetBIOSName = Convert-DomainFqdnToNetBIOS -DomainName $ADGroup[0].DomainName
                        $ObjectsCount = $ADGroup.Count - 1
                        $FullName = "$NetBIOSName\$GroupName ($ObjectsCount)"
                    } else {
                        # Means group returned nothing, probably wrong request, but we still need to show something
                        $GroupName = $Group
                        $FullName = "$Group (0)"
                    }
                    $DataStoreID = -join ('table', (Get-RandomStringName -Size 10 -ToLower))
                    $DataTableID = -join ('table', (Get-RandomStringName -Size 10 -ToLower))
                    New-HTMLTab -TabName $FullName {
                        Write-Verbose -Message "Show-WinADGroupMember - processing HTML generation for $Group group - Table"
                        $SectionInformation = New-HTMLSection -Title "Information for $GroupName" {
                            New-HTMLTable -DataTable $ADGroup -Filtering -DataStoreID $DataStoreID {
                                if (-not $DisableBuiltinConditions) {
                                    New-TableHeader -Names Name, SamAccountName, DomainName, DisplayName -Title 'Member'
                                    New-TableHeader -Names DirectMembers, DirectGroups, IndirectMembers, TotalMembers -Title 'Statistics'
                                    New-TableHeader -Names GroupType, GroupScope -Title 'Group Details'
                                    New-TableCondition -BackgroundColor CoralRed -Color White -ComparisonType bool -Value $false -Name Enabled -Operator eq
                                    New-TableCondition -BackgroundColor LightBlue -ComparisonType string -Value '' -Name ParentGroup -Operator eq -Row
                                    New-TableCondition -BackgroundColor CoralRed -Color White -ComparisonType bool -Value $true -Name CrossForest -Operator eq
                                    New-TableCondition -BackgroundColor CoralRed -Color White -ComparisonType bool -Value $true -Name CircularIndirect -Operator eq -Row
                                    New-TableCondition -BackgroundColor CoralRed -Color White -ComparisonType bool -Value $true -Name CircularDirect -Operator eq -Row
                                }
                                if ($Conditions) {
                                    & $Conditions
                                }
                            } -ScrollX:$ScrollX.IsPresent
                        }
                        if (-not $SkipDiagram.IsPresent) {
                            New-HTMLTab -TabName 'Information' {
                                $SectionInformation
                            }
                        } else {
                            $SectionInformation
                        }
                        if (-not $SkipDiagram.IsPresent) {
                            Write-Verbose -Message "Show-WinADGroupMember - processing HTML generation for $Group group - Diagram"
                            New-HTMLTab -TabName 'Diagram Basic' {
                                New-HTMLSection -Title "Diagram for $GroupName" {
                                    New-HTMLGroupDiagramDefault -ADGroup $ADGroup -HideAppliesTo $HideAppliesTo -HideUsers:$HideUsers -HideComputers:$HideComputers -HideOther:$HideOther -DataTableID $DataTableID -ColumnID 1 -Online:$Online -EnableDiagramFiltering:$EnableDiagramFiltering.IsPresent -DiagramFilteringMinimumCharacters $DiagramFilteringMinimumCharacters -EnableDiagramFilteringButton:$EnableDiagramFilteringButton.IsPresent
                                }
                            }
                            Write-Verbose -Message "Show-WinADGroupMember - processing HTML generation for $Group group - Diagram Hierarchy"
                            New-HTMLTab -TabName 'Diagram Hierarchy' {
                                New-HTMLSection -Title "Diagram for $GroupName" {
                                    New-HTMLGroupDiagramHierachical -ADGroup $ADGroup -HideAppliesTo $HideAppliesTo -HideUsers:$HideUsers -HideComputers:$HideComputers -HideOther:$HideOther -Online:$Online -EnableDiagramFiltering:$EnableDiagramFiltering.IsPresent -DiagramFilteringMinimumCharacters $DiagramFilteringMinimumCharacters -EnableDiagramFilteringButton:$EnableDiagramFilteringButton.IsPresent
                                }
                            }
                        }
                    }
                }
            }
            if (-not $SkipDiagram.IsPresent -and ($Summary -or $SummaryOnly)) {
                Write-Verbose "Show-WinADGroupMember - processing HTML generation for Summary"
                New-HTMLTab -Name 'Summary' {
                    New-HTMLTab -TabName 'Diagram Basic' {
                        New-HTMLSection -Title "Diagram for Summary" {
                            New-HTMLGroupDiagramSummary -ADGroup $GroupsList -HideAppliesTo $HideAppliesTo -HideUsers:$HideUsers -HideComputers:$HideComputers -HideOther:$HideOther -DataTableID $DataTableID -ColumnID 1 -Online:$Online -EnableDiagramFiltering:$EnableDiagramFiltering.IsPresent -DiagramFilteringMinimumCharacters $DiagramFilteringMinimumCharacters -EnableDiagramFilteringButton:$EnableDiagramFilteringButton.IsPresent
                        }
                    }
                    New-HTMLTab -TabName 'Diagram Hierarchy' {
                        New-HTMLSection -Title "Diagram for Summary" {
                            New-HTMLGroupDiagramSummaryHierarchical -ADGroup $GroupsList -HideAppliesTo $HideAppliesTo -HideUsers:$HideUsers -HideComputers:$HideComputers -HideOther:$HideOther -Online:$Online -EnableDiagramFiltering:$EnableDiagramFiltering.IsPresent -DiagramFilteringMinimumCharacters $DiagramFilteringMinimumCharacters -EnableDiagramFilteringButton:$EnableDiagramFilteringButton.IsPresent
                        }
                    }
                }
            }
            Write-Verbose -Message "Show-WinADGroupMember - saving HTML report"
        } -Online:$Online -FilePath $FilePath -ShowHTML:(-not $HideHTML)
        Write-Verbose -Message "Show-WinADGroupMember - HTML report saved to $FilePath"
    } else {
        Write-Warning -Message "Show-WinADGroupMember - Error processing Identity, as it's empty."
    }
}
function Show-WinADGroupMemberOf {
    <#
    .SYNOPSIS
    Command to gather group membership that the user is member of displaying information in table and diagrams.
 
    .DESCRIPTION
    Command to gather group membership that the user is member of displaying information in table and diagrams.
 
    .PARAMETER Identity
    User or Computer object to get group membership for.
 
    .PARAMETER Conditions
    Provides ability to control look and feel of tables across HTML
 
    .PARAMETER FilePath
    Path to HTML file where it's saved. If not given temporary path is used
 
    .PARAMETER Summary
    Adds additional tab with all groups together on two diagrams
 
    .PARAMETER SummaryOnly
    Adds one tab with all groups together on two diagrams
 
    .PARAMETER Online
    Forces use of online CDN for JavaScript/CSS which makes the file smaller. Default - use offline.
 
    .PARAMETER HideHTML
    Prevents HTML output from being displayed in browser after generation is done
 
    .PARAMETER DisableBuiltinConditions
    Disables table coloring allowing user to define it's own conditions
 
    .PARAMETER SkipDiagram
    Skips diagram generation and only displays table. Useful if the diagram can't handle amount of data or if the diagrams are not nessecary.
 
    .PARAMETER EnableDiagramFiltering
    Enables search in diagrams. It's useful when there are many groups and it's hard to find the one you are looking for.
 
    .PARAMETER DiagramFilteringMinimumCharacters
    Minimum characters to start search in diagrams. Default is 3.
 
    .PARAMETER EnableDiagramFilteringButton
    Adds button to enable/disable filtering in diagrams. It's extension to EnableDiagramFiltering, when you prefer button over automatic filtering.
 
    .EXAMPLE
    Show-WinADGroupMemberOf -Identity 'przemyslaw.klys' -Verbose -Summary
 
    .EXAMPLE
    Show-WinADGroupMemberOf -Identity 'przemyslaw.klys', 'adm.pklys' -Summary
 
    .NOTES
    General notes
    #>

    [alias('Show-ADGroupMemberOf')]
    [cmdletBinding(DefaultParameterSetName = 'Default')]
    param(
        [Parameter(Position = 1)][scriptblock] $Conditions,
        [parameter(Position = 0, Mandatory)][string[]] $Identity,
        [string] $FilePath,
        [Parameter(ParameterSetName = 'Default')][switch] $Summary,
        [Parameter(ParameterSetName = 'SummaryOnly')][switch] $SummaryOnly,
        [switch] $Online,
        [switch] $HideHTML,
        [switch] $DisableBuiltinConditions,
        [switch] $SkipDiagram,
        [switch] $EnableDiagramFiltering,
        [switch] $EnableDiagramFilteringButton,
        [int] $DiagramFilteringMinimumCharacters = 3
    )
    $HideAppliesTo = 'Both'
    $Script:Reporting = [ordered] @{}
    $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Show-WinADGroupMemberOf' -RepositoryOwner 'evotecit' -RepositoryName 'ADEssentials'

    if ($FilePath -eq '') {
        $FilePath = Get-FileName -Extension 'html' -Temporary
    }
    $GroupsList = [System.Collections.Generic.List[object]]::new()
    New-HTML -TitleText "Visual Object MemberOf" {
        New-HTMLHeader {
            New-HTMLSection -Invisible {
                New-HTMLSection {
                    New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue
                } -JustifyContent flex-start -Invisible
                New-HTMLSection {
                    New-HTMLText -Text "ADEssentials - $($Script:Reporting['Version'])" -Color Blue
                } -JustifyContent flex-end -Invisible
            }
        }
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLTableOption -DataStore JavaScript -BoolAsString
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
        foreach ($ADObject in $Identity) {
            if ($null -eq $ADObject) {
                continue
            }
            try {
                Write-Verbose "Show-WinADObjectMember - requesting $ADObject memberof property"
                $MyObject = Get-WinADGroupMemberOf -Identity $ADObject -AddSelf
                if ($Summary -or $SummaryOnly) {
                    foreach ($Object in $MyObject) {
                        $GroupsList.Add($Object)
                    }
                }
            } catch {
                Write-Warning "Show-WinADGroupMemberOf - Error processing group $Group. Skipping. Needs investigation why it failed. Error: $($_.Exception.Message)"
                continue
            }
            Write-Verbose -Message "Show-WinADGroupMemberOf - Processing HTML generation for $ADObject"
            if ($MyObject -and -not $SummaryOnly) {
                $ObjectName = $MyObject[0].ObjectName
                $DataStoreID = -join ('table', (Get-RandomStringName -Size 10 -ToLower))
                $DataTableID = -join ('table', (Get-RandomStringName -Size 10 -ToLower))
                New-HTMLTab -TabName $ObjectName {
                    Write-Verbose -Message "Show-WinADGroupMemberOf - Processing HTML generation for $ObjectName - Table"
                    $DataSection = New-HTMLSection -Title "Information for $ObjectName" {
                        New-HTMLTable -DataTable $MyObject -Filtering -DataStoreID $DataStoreID {
                            if (-not $DisableBuiltinConditions) {
                                New-TableHeader -Names Name, SamAccountName, DomainName, DisplayName -Title 'Member'
                                New-TableHeader -Names GroupType, GroupScope -Title 'Group Details'
                                New-TableCondition -BackgroundColor CoralRed -Color White -ComparisonType bool -Value $false -Name Enabled -Operator eq
                                New-TableCondition -BackgroundColor LightBlue -ComparisonType string -Value '' -Name ParentGroup -Operator eq -Row
                                New-TableCondition -BackgroundColor CoralRed -Color White -ComparisonType bool -Value $true -Name CircularDirect -Operator eq -Row
                                New-TableCondition -BackgroundColor CoralRed -Color White -ComparisonType bool -Value $true -Name CircularIndirect -Operator eq -Row
                            }
                            if ($Conditions) {
                                & $Conditions
                            }
                        }
                    }
                    if ($SkipDiagram.IsPresent) {
                        $DataSection
                    } else {
                        New-HTMLTab -TabName 'Information' {
                            $DataSection
                        }
                        Write-Verbose -Message "Show-WinADGroupMemberOf - Processing HTML generation for $ObjectName - Diagram"
                        New-HTMLTab -TabName 'Diagram Basic' {
                            New-HTMLSection -Title "Diagram for $ObjectName" {
                                New-HTMLGroupOfDiagramDefault -Identity $MyObject -HideAppliesTo $HideAppliesTo -HideUsers:$HideUsers -HideComputers:$HideComputers -HideOther:$HideOther -DataTableID $DataTableID -ColumnID 1 -Online:$Online -EnableDiagramFiltering:$EnableDiagramFiltering.IsPresent -DiagramFilteringMinimumCharacters $DiagramFilteringMinimumCharacters -EnableDiagramFilteringButton:$EnableDiagramFilteringButton.IsPresent
                            }
                        }
                        Write-Verbose -Message "Show-WinADGroupMemberOf - Processing HTML generation for $ObjectName - Diagram Hierarchy"
                        New-HTMLTab -TabName 'Diagram Hierarchy' {
                            New-HTMLSection -Title "Diagram for $ObjectName" {
                                New-HTMLGroupOfDiagramHierarchical -Identity $MyObject -HideAppliesTo $HideAppliesTo -HideUsers:$HideUsers -HideComputers:$HideComputers -HideOther:$HideOther -Online:$Online -EnableDiagramFiltering:$EnableDiagramFiltering.IsPresent -DiagramFilteringMinimumCharacters $DiagramFilteringMinimumCharacters -EnableDiagramFilteringButton:$EnableDiagramFilteringButton.IsPresent
                            }
                        }
                    }
                }
            }
        }
        if (-not $SkipDiagram.IsPresent -and ($Summary -or $SummaryOnly)) {
            Write-Verbose -Message "Show-WinADGroupMemberOf - Processing HTML generation for Summary"
            New-HTMLTab -Name 'Summary' {
                New-HTMLTab -TabName 'Diagram Basic' {
                    New-HTMLSection -Title "Diagram for Summary" {
                        New-HTMLGroupOfDiagramSummary -ADGroup $GroupsList -HideAppliesTo $HideAppliesTo -HideUsers:$HideUsers -HideComputers:$HideComputers -HideOther:$HideOther -DataTableID $DataTableID -ColumnID 1 -Online:$Online -EnableDiagramFiltering:$EnableDiagramFiltering.IsPresent -DiagramFilteringMinimumCharacters $DiagramFilteringMinimumCharacters -EnableDiagramFilteringButton:$EnableDiagramFilteringButton.IsPresent
                    }
                }
                New-HTMLTab -TabName 'Diagram Hierarchy' {
                    New-HTMLSection -Title "Diagram for Summary" {
                        New-HTMLGroupOfDiagramSummaryHierarchical -ADGroup $GroupsList -HideAppliesTo $HideAppliesTo -HideUsers:$HideUsers -HideComputers:$HideComputers -HideOther:$HideOther -Online:$Online -EnableDiagramFiltering:$EnableDiagramFiltering.IsPresent -DiagramFilteringMinimumCharacters $DiagramFilteringMinimumCharacters -EnableDiagramFilteringButton:$EnableDiagramFilteringButton.IsPresent
                    }
                }
            }
        }
        Write-Verbose -Message "Show-WinADGroupMemberOf - saving HTML report"
    } -Online:$Online -FilePath $FilePath -ShowHTML:(-not $HideHTML)
}
function Show-WinADKerberosAccount {
    <#
    .SYNOPSIS
    This function generates an HTML report for Kerberos accounts in Active Directory.
 
    .DESCRIPTION
    The Show-WinADKerberosAccount function generates an HTML report for Kerberos accounts in Active Directory.
    It includes information about the account, domain controllers, global catalogs, and critical accounts.
 
    .PARAMETER Forest
    The name of the forest to generate the report for.
 
    .PARAMETER ExcludeDomains
    An array of domain names to exclude from the report.
 
    .PARAMETER IncludeDomains
    An array of domain names to include in the report.
 
    .PARAMETER Online
    A switch parameter to indicate if the report should be generated online.
 
    .PARAMETER HideHTML
    A switch parameter to indicate if the HTML report should be hidden.
 
    .PARAMETER FilePath
    The path to save the HTML report.
 
    .PARAMETER PassThru
    A switch parameter to indicate if the function should return the account data.
 
    .EXAMPLE
    Show-WinADKerberosAccount -Forest 'example.com' -ExcludeDomains @('test.com') -IncludeDomains @('example.com') -Online -HideHTML -FilePath 'C:\Reports\KerberosReport.html' -PassThru
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [switch] $Online,
        [switch] $HideHTML,
        [string] $FilePath,
        [switch] $PassThru
    )
    $Today = Get-Date
    $Script:Reporting = [ordered] @{}
    $Script:Reporting['Version'] = Get-GitHubVersion -Cmdlet 'Invoke-ADEssentials' -RepositoryOwner 'evotecit' -RepositoryName 'ADEssentials'

    $AccountData = Get-WinADKerberosAccount -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -IncludeCriticalAccounts

    Write-Verbose -Message "Show-WinADKerberosAccount - Building HTML report based on delivered data"
    New-HTML -Author 'PrzemysÅ‚aw KÅ‚ys' -TitleText 'Kerberos Reporting' {
        New-HTMLTabStyle -BorderRadius 0px -TextTransform lowercase -BackgroundColorActive SlateGrey
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLPanelStyle -BorderRadius 0px
        New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ', ' -ArrayJoin

        New-HTMLHeader {
            New-HTMLSection -Invisible {
                New-HTMLSection {
                    New-HTMLText -Text "Report generated on $(Get-Date)" -Color Blue
                } -JustifyContent flex-start -Invisible
                New-HTMLSection {
                    New-HTMLText -Text "ADEssentials - $($Script:Reporting['Version'])" -Color Blue
                } -JustifyContent flex-end -Invisible
            }
        }

        foreach ($Domain in $AccountData.Data.Keys) {
            New-HTMLTab -Name $Domain {
                New-HTMLPanel {
                    New-HTMLTable -DataTable $AccountData['Data'][$Domain].Values.FullInformation -Filtering -DataStore JavaScript -ScrollX {
                        $newHTMLTableConditionSplat = @{
                            Name                = 'PasswordLastSetDays'
                            ComparisonType      = 'number'
                            Operator            = 'le'
                            Value               = 180
                            BackgroundColor     = 'LimeGreen'
                            FailBackgroundColor = 'Salmon'
                            HighlightHeaders    = 'PasswordLastSetDays', 'PasswordLastSet'
                        }

                        New-HTMLTableCondition @newHTMLTableConditionSplat
                    }
                }

                New-HTMLTabPanel {
                    foreach ($Account in $AccountData['Data'][$Domain].Values) {
                        $DomainControllers = $Account.DomainControllers
                        $GlobalCatalogs = $Account.GlobalCatalogs

                        $CountMatched = 0
                        $CountNotMatched = 0
                        $CountTotal = 0
                        $NewestPassword = $DomainControllers.Values.PasswordLastSet | Sort-Object -Descending | Select-Object -First 1
                        foreach ($Password in $DomainControllers.Values.PasswordLastSet) {
                            if ($Password -eq $NewestPassword) {
                                $CountMatched++
                            } else {
                                $CountNotMatched++
                            }
                            $CountTotal++
                        }

                        if ($NewestPassword) {
                            $TimeSinceLastChange = ($Today) - $NewestPassword
                        } else {
                            $TimeSinceLastChange = $null
                        }

                        $CountMatchedGC = 0
                        $CountNotMatchedGC = 0
                        $CountTotalGC = 0
                        $NewestPasswordGC = $GlobalCatalogs.Values.PasswordLastSet | Sort-Object -Descending | Select-Object -First 1
                        foreach ($Password in $GlobalCatalogs.Values.PasswordLastSet) {
                            if ($Password -eq $NewestPasswordGC) {
                                $CountMatchedGC++
                            } else {
                                $CountNotMatchedGC++
                            }
                            $CountTotalGC++
                        }

                        if ($NewestPasswordGC) {
                            $TimeSinceLastChangeGC = ($Today) - $NewestPasswordGC
                        } else {
                            $TimeSinceLastChangeGC = $null
                        }

                        New-HTMLTab -Name $Account.FullInformation.SamAccountName {
                            New-HTMLSection -Invisible {

                                # DC Status
                                New-HTMLSection -Invisible {
                                    New-HTMLPanel -Invisible {
                                        New-HTMLStatus {
                                            $Percentage = "$([math]::Round(($CountMatched / $CountTotal) * 100))%"
                                            if ($Percentage -eq '100%') {
                                                $BackgroundColor = '#0ef49b'
                                                $Icon = 'Good'
                                            } elseif ($Percentage -ge '70%') {
                                                $BackgroundColor = '#d2dc69'
                                                $Icon = 'Bad'
                                            } elseif ($Percentage -ge '30%') {
                                                $BackgroundColor = '#faa04b'
                                                $Icon = 'Bad'
                                            } elseif ($Percentage -ge '10%') {
                                                $BackgroundColor = '#ff9035'
                                                $Icon = 'Bad'
                                            } elseif ($Percentage -ge '0%') {
                                                $BackgroundColor = '#ff5a64'
                                                $Icon = 'Dead'
                                            }

                                            if ($Icon -eq 'Dead') {
                                                $IconType = '&#x2620'
                                            } elseif ($Icon -eq 'Bad') {
                                                $IconType = '&#x2639'
                                            } elseif ($Icon -eq 'Good') {
                                                $IconType = '&#x2714'
                                            }

                                            New-HTMLStatusItem -Name 'Domain Controller' -Status "Synchronized $CountMatched/$CountTotal ($Percentage)" -BackgroundColor $BackgroundColor -IconHex $IconType
                                            $newHTMLToastSplat = @{
                                                TextHeader   = 'Kerberos password date'
                                                Text         = "Password set on: $NewestPassword (Days: $($TimeSinceLastChange.Days), Hours: $($TimeSinceLastChange.Hours), Minutes: $($TimeSinceLastChange.Minutes))"
                                                BarColorLeft = 'AirForceBlue'
                                                IconSolid    = 'info-circle'
                                                IconColor    = 'AirForceBlue'
                                            }
                                            if ($TimeSinceLastChange.Days -ge 180) {
                                                $newHTMLToastSplat['BarColorLeft'] = 'Salmon'
                                                $newHTMLToastSplat['IconSolid'] = 'exclamation-triangle'
                                                $newHTMLToastSplat['IconColor'] = 'Salmon'
                                                $newHTMLToastSplat['TextHeader'] = 'Kerberos password date (outdated)'
                                            }
                                            New-HTMLToast @newHTMLToastSplat
                                        }
                                    }
                                }
                                # GC Status
                                New-HTMLSection -Invisible {
                                    New-HTMLStatus {
                                        $Percentage = "$([math]::Round(($CountMatchedGC / $CountTotalGC) * 100))%"
                                        if ($Percentage -eq '100%') {
                                            $BackgroundColor = '#0ef49b'
                                            $Icon = 'Good'
                                        } elseif ($Percentage -ge '70%') {
                                            $BackgroundColor = '#d2dc69'
                                            $Icon = 'Bad'
                                        } elseif ($Percentage -ge '30%') {
                                            $BackgroundColor = '#faa04b'
                                            $Icon = 'Bad'
                                        } elseif ($Percentage -ge '10%') {
                                            $BackgroundColor = '#ff9035'
                                            $Icon = 'Bad'
                                        } elseif ($Percentage -ge '0%') {
                                            $BackgroundColor = '#ff5a64'
                                            $Icon = 'Dead'
                                        }

                                        if ($Icon -eq 'Dead') {
                                            $IconType = '&#x2620'
                                        } elseif ($Icon -eq 'Bad') {
                                            $IconType = '&#x2639'
                                        } elseif ($Icon -eq 'Good') {
                                            $IconType = '&#x2714'
                                        }

                                        New-HTMLStatusItem -Name 'Global Catalogs' -Status "Synchronized $CountMatchedGC/$CountTotalGC ($Percentage)" -BackgroundColor $BackgroundColor -IconHex $IconType
                                        $newHTMLToastSplat = @{
                                            TextHeader   = 'Kerberos password date'
                                            Text         = "Password set on: $NewestPasswordGC (Days: $($TimeSinceLastChangeGC.Days), Hours: $($TimeSinceLastChangeGC.Hours), Minutes: $($TimeSinceLastChangeGC.Minutes))"
                                            BarColorLeft = 'AirForceBlue'
                                            IconSolid    = 'info-circle'
                                            IconColor    = 'AirForceBlue'
                                        }
                                        if ($TimeSinceLastChange.Days -ge 180) {
                                            $newHTMLToastSplat['BarColorLeft'] = 'Salmon'
                                            $newHTMLToastSplat['IconSolid'] = 'exclamation-triangle'
                                            $newHTMLToastSplat['IconColor'] = 'Salmon'
                                            $newHTMLToastSplat['TextHeader'] = 'Kerberos password date (outdated)'
                                        }
                                        New-HTMLToast @newHTMLToastSplat
                                    }
                                }
                            }


                            #$DataAccount = $Account.FullInformation

                            New-HTMLSection -HeaderText "Domain Controllers for '$($Account.FullInformation.SamAccountName)'" {
                                New-HTMLTable -DataTable $DomainControllers.Values {
                                    New-HTMLTableCondition -Name 'Status' -Operator eq -Value 'OK' -BackgroundColor '#0ef49b' -FailBackgroundColor '#ff5a64'
                                } -Filtering -DataStore JavaScript
                            }
                            New-HTMLSection -HeaderText "Global Catalogs for account '$($Account.FullInformation.SamAccountName)'" {
                                New-HTMLTable -DataTable $GlobalCatalogs.Values {
                                    New-HTMLTableCondition -Name 'Status' -Operator eq -Value 'OK' -BackgroundColor '#0ef49b' -FailBackgroundColor '#ff5a64'
                                } -Filtering -DataStore JavaScript
                            }
                        }
                    }
                }

                $KerberosAccount = $AccountData['Data'][$Domain]['krbtgt'].FullInformation
                $NewestPassword = $KerberosAccount.PasswordLastSetDays

                New-HTMLSection -HeaderText "Critical Accounts for domain '$Domain'" {
                    New-HTMLContainer {
                        New-HTMLPanel {
                            New-HTMLText -Text "Critical accounts that should have their password changed after every kerberos password change."
                            New-HTMLList {
                                New-HTMLListItem -Text 'Domain Admins'
                                New-HTMLListItem -Text 'Enterprise Admins'
                            }
                        }
                        New-HTMLPanel {
                            New-HTMLTable -DataTable $AccountData['CriticalAccounts'][$Domain] {
                                if ($null -ne $NewestPassword) {
                                    New-HTMLTableCondition -Name 'PasswordLastSetDays' -Operator le -Value $NewestPassword -ComparisonType number -BackgroundColor MintGreen -FailBackgroundColor Salmon -HighlightHeaders PasswordLastSetDays, PasswordLastSet
                                }
                            } -Filtering -DataStore JavaScript -ScrollX
                        }
                    }
                }
            }
        }
    } -Online:$Online.IsPresent -ShowHTML:(-not $HideHTML) -FilePath $FilePath

    if ($PassThru) {
        $AccountData
    }
    Write-Verbose -Message "Show-WinADKerberosAccount - HTML Report generated"
}
function Show-WinADObjectDifference {
    <#
    .SYNOPSIS
    This function shows the differences between Active Directory objects.
    .DESCRIPTION
    The function takes an array of Identity, a switch for Global Catalog, an array of Properties, a string for FilePath, and a switch to hide HTML.
    It then generates an HTML report using the Find-WinADObjectDifference function and displays it.
    .PARAMETER Identity
    An array of Identity to compare.
    .PARAMETER GlobalCatalog
    A switch to specify if the comparison should be done in the Global Catalog.
    .PARAMETER Properties
    An array of Properties to compare.
    .PARAMETER FilePath
    A string specifying the file path to save the HTML report.
    .PARAMETER HideHTML
    A switch to hide the HTML report.
    .EXAMPLE
    Show-WinADObjectDifference -Identity "user1", "user2" -GlobalCatalog -Properties "Name", "Email" -FilePath "C:\ADReport.html" -HideHTML
    #>

    [CmdletBinding()]
    param(
        [Array] $Identity,
        [switch] $GlobalCatalog,
        [string[]] $Properties,
        [string] $FilePath,
        [switch] $HideHTML
    )

    $OutputValue = Find-WinADObjectDifference -Identity $Identity -GlobalCatalog:$GlobalCatalog.IsPresent -Properties $Properties

    Write-Verbose -Message "Show-WinADObjectDifference - Generating HTML"
    New-HTML {
        New-HTMLTableOption -DataStore JavaScript -BoolAsString -ArrayJoinString ", " -ArrayJoin

        New-HTMLTab -Name 'Summary' {
            New-HTMLTable -DataTable $OutputValue.ListSummary -Filtering -DataStore JavaScript -ScrollX {
                New-HTMLTableCondition -Name 'DifferentServersCount' -Operator eq -ComparisonType number -Value 0 -BackgroundColor LimeGreen -FailBackgroundColor Salmon -HighlightHeaders 'DifferentServersCount', 'DifferentServers', 'DifferentProperties'
                New-HTMLTableCondition -Name 'SameServersCount' -Operator eq -ComparisonType number -Value 0 -BackgroundColor Salmon -FailBackgroundColor LimeGreen -HighlightHeaders 'SameServersCount', 'SameServers', 'SameProperties'
            }
        }
        New-HTMLTab -Name 'Details per property' {
            New-HTMLTable -DataTable $OutputValue.ListDetails -Filtering -DataStore JavaScript -ScrollX -AllProperties
        }
        New-HTMLTab -Name 'Details per server' {
            New-HTMLTable -DataTable $OutputValue.ListDetailsReversed -Filtering -DataStore JavaScript -ScrollX
        }
        New-HTMLTab -Name 'Detailed Differences' {
            New-HTMLTable -DataTable $OutputValue.List -Filtering -DataStore JavaScript -ScrollX
        }
    } -ShowHTML:(-not $HideHTML.IsPresent) -FilePath $FilePath
    Write-Verbose -Message "Show-WinADObjectDifference - Generating HTML - Done"
}
function Show-WinADOrganization {
    <#
    .SYNOPSIS
    Generates a detailed HTML report on the organizational units and their relationships within a specified Active Directory forest.
 
    .DESCRIPTION
    This cmdlet creates a comprehensive HTML report that includes a diagram of the organizational units and their relationships, as well as a table with detailed information about the organizational units. The report is designed to provide a clear overview of the organizational structure within the Active Directory.
 
    .PARAMETER Conditions
    Specifies the conditions to filter the organizational units and their relationships. This can be a script block that returns a boolean value.
 
    .PARAMETER FilePath
    The path to save the HTML report. If not specified, a temporary file is used.
 
    .EXAMPLE
    Show-WinADOrganization -FilePath "C:\Reports\AD Organization Report.html"
 
    .NOTES
    This cmdlet is useful for administrators to visualize and analyze the organizational structure within Active Directory, helping to identify potential issues and ensure efficient management of resources.
    #>

    [cmdletBinding()]
    param(
        [ScriptBlock] $Conditions,
        [string] $FilePath
    )

    $CachedOU = [ordered] @{}
    $ForestInformation = Get-WinADForestDetails
    $Script:OrganiazationalUnits = @()
    #$Organization = Get-WinADOrganization

    New-HTML -TitleText "Visual Active Directory Organization" {
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLTableOption -DataStore HTML
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
        New-HTMLTabPanel {
            New-HTMLTab -TabName 'Standard' {
                New-HTMLSection -HeaderText 'Organization Diagram' {
                    New-HTMLDiagram -Height 'calc(50vh)' {
                        New-DiagramEvent -ID 'DT-StandardOrg' -ColumnID 3
                        New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
                        #foreach ($OU in $Organization.Keys) {
                        #Add-Node -Name $OU -Organization $Organization
                        #New-DiagramNode -Label $OU
                        #}

                        foreach ($Domain in $ForestInformation.Domains) {
                            New-DiagramNode -Label $Domain -Id $Domain -Image 'https://cdn-icons-png.flaticon.com/512/6329/6329785.png'

                            $Script:OrganiazationalUnits = Get-ADOrganizationalUnit -Filter "*" -Server $ForestInformation['QueryServers'][$Domain].HostName[0] -Properties DistinguishedName, CanonicalName
                            foreach ($OU in $OrganiazationalUnits) {
                                New-DiagramNode -Id $OU.DistinguishedName -Label $OU.Name -Image 'https://cdn-icons-png.flaticon.com/512/3767/3767084.png'

                                [Array] $SubOU = ConvertFrom-DistinguishedName -DistinguishedName $OU.DistinguishedName -ToMultipleOrganizationalUnit
                                if ($SubOU.Count -gt 0) {
                                    foreach ($Sub in $SubOU[0]) {
                                        $Name = ConvertFrom-DistinguishedName -DistinguishedName $Sub -ToLastName
                                        New-DiagramNode -Id $Sub -Label $Name -Image 'https://cdn-icons-png.flaticon.com/512/3767/3767084.png'
                                        New-DiagramEdge -From $OU.DistinguishedName -To $Sub -Color Blue -ArrowsToEnabled -Dashes
                                    }
                                } else {
                                    New-DiagramEdge -From $Domain -To $OU.DistinguishedName -Color Blue -ArrowsToEnabled -Dashes
                                }

                                <#
                            $NameSplit = $OU.canonicalName.Split("/")
                            $CurrentLevel = $CachedOU[$Domain]
                            foreach ($N in $NameSplit) {
                                if ($N -ne $Domain) {
                                    if (-not $CurrentLevel[$N]) {
                                        $CurrentLevel[$N] = [ordered] @{}
                                    } else {
                                        $CurrentLevel = $CurrentLevel[$N]
                                    }
 
                                }
                            }
                            #>

                            }
                            <#
                        foreach ($OU in $OrganiazationalUnits) {
                            [Array] $SubOU = ConvertFrom-DistinguishedName -DistinguishedName $OU.DistinguishedName -ToMultipleOrganizationalUnit -IncludeParent | Select-Object -Last 1
 
                            New-DiagramLink -From $OU.DistinguishedName -To $O
 
                        }
                        #>

                        }
                        #$CachedOU


                        foreach ($Trust in $ADTrusts) {
                            #New-DiagramNode -Label $Trust.'TrustSource' -IconSolid audio-description #-IconColor LightSteelBlue
                            #New-DiagramNode -Label $Trust.'TrustTarget' -IconSolid audio-description #-IconColor LightSteelBlue

                            $newDiagramLinkSplat = @{
                                From         = $Trust.'TrustSource'
                                To           = $Trust.'TrustTarget'
                                ColorOpacity = 0.7
                            }
                            <#
                        if ($Trust.'TrustDirection' -eq 'Disabled') {
 
                        } elseif ($Trust.'TrustDirection' -eq 'Inbound') {
                            $newDiagramLinkSplat.ArrowsFromEnabled = $true
                        } elseif ($Trust.'TrustDirection' -eq 'Outbount') {
                            $newDiagramLinkSplat.ArrowsToEnabled = $true
                            New-DiagramLink @newDiagramLinkSplat
                        } elseif ($Trust.'TrustDirection' -eq 'Bidirectional') {
                            $newDiagramLinkSplat.ArrowsToEnabled = $true
                            $newDiagramLinkSplat.ArrowsFromEnabled = $true
                        }
                        if ($Trust.IntraForest) {
                            $newDiagramLinkSplat.Color = 'DarkSpringGreen'
                        }
                        if ($Trust.QueryStatus -eq 'OK' -or $Trust.TrustStatus -eq 'OK') {
                            $newDiagramLinkSplat.Dashes = $false
                            $newDiagramLinkSplat.FontColor = 'Green'
                        } else {
                            $newDiagramLinkSplat.Dashes = $true
                            $newDiagramLinkSplat.FontColor = 'Red'
                        }
                        if ($Trust.IsTGTDelegationEnabled) {
                            $newDiagramLinkSplat.Color = 'Red'
                            $newDiagramLinkSplat.Label = "Delegation Enabled"
                        } else {
                            $newDiagramLinkSplat.Label = $Trust.QueryStatus
                        }
                        #>

                            #New-DiagramLink @newDiagramLinkSplat
                        }
                    }
                }
            }
            New-HTMLTab -TabName 'Hierarchical' {
                New-HTMLSection -HeaderText 'Organization Diagram' {
                    New-HTMLDiagram -Height 'calc(50vh)' {
                        New-DiagramOptionsLayout -HierarchicalEnabled $true
                        New-DiagramEvent -ID 'DT-StandardOrg' -ColumnID 3
                        New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
                        #foreach ($OU in $Organization.Keys) {
                        #Add-Node -Name $OU -Organization $Organization
                        #New-DiagramNode -Label $OU
                        #}

                        foreach ($Domain in $ForestInformation.Domains) {
                            New-DiagramNode -Label $Domain -Id $Domain -Image 'https://cdn-icons-png.flaticon.com/512/6329/6329785.png'

                            $Script:OrganiazationalUnits = Get-ADOrganizationalUnit -Filter "*" -Server $ForestInformation['QueryServers'][$Domain].HostName[0] -Properties DistinguishedName, CanonicalName
                            foreach ($OU in $OrganiazationalUnits) {
                                New-DiagramNode -Id $OU.DistinguishedName -Label $OU.Name -Image 'https://cdn-icons-png.flaticon.com/512/3767/3767084.png'

                                [Array] $SubOU = ConvertFrom-DistinguishedName -DistinguishedName $OU.DistinguishedName -ToMultipleOrganizationalUnit
                                if ($SubOU.Count -gt 0) {
                                    foreach ($Sub in $SubOU[0]) {
                                        $Name = ConvertFrom-DistinguishedName -DistinguishedName $Sub -ToLastName
                                        New-DiagramNode -Id $Sub -Label $Name -Image 'https://cdn-icons-png.flaticon.com/512/3767/3767084.png'
                                        New-DiagramEdge -From $OU.DistinguishedName -To $Sub
                                    }
                                } else {
                                    New-DiagramEdge -From $Domain -To $OU.DistinguishedName
                                }

                                <#
                            $NameSplit = $OU.canonicalName.Split("/")
                            $CurrentLevel = $CachedOU[$Domain]
                            foreach ($N in $NameSplit) {
                                if ($N -ne $Domain) {
                                    if (-not $CurrentLevel[$N]) {
                                        $CurrentLevel[$N] = [ordered] @{}
                                    } else {
                                        $CurrentLevel = $CurrentLevel[$N]
                                    }
 
                                }
                            }
                            #>

                            }
                            <#
                        foreach ($OU in $OrganiazationalUnits) {
                            [Array] $SubOU = ConvertFrom-DistinguishedName -DistinguishedName $OU.DistinguishedName -ToMultipleOrganizationalUnit -IncludeParent | Select-Object -Last 1
 
                            New-DiagramLink -From $OU.DistinguishedName -To $O
 
                        }
                        #>

                        }
                        #$CachedOU


                        foreach ($Trust in $ADTrusts) {
                            #New-DiagramNode -Label $Trust.'TrustSource' -IconSolid audio-description #-IconColor LightSteelBlue
                            #New-DiagramNode -Label $Trust.'TrustTarget' -IconSolid audio-description #-IconColor LightSteelBlue

                            $newDiagramLinkSplat = @{
                                From         = $Trust.'TrustSource'
                                To           = $Trust.'TrustTarget'
                                ColorOpacity = 0.7
                            }
                            <#
                        if ($Trust.'TrustDirection' -eq 'Disabled') {
 
                        } elseif ($Trust.'TrustDirection' -eq 'Inbound') {
                            $newDiagramLinkSplat.ArrowsFromEnabled = $true
                        } elseif ($Trust.'TrustDirection' -eq 'Outbount') {
                            $newDiagramLinkSplat.ArrowsToEnabled = $true
                            New-DiagramLink @newDiagramLinkSplat
                        } elseif ($Trust.'TrustDirection' -eq 'Bidirectional') {
                            $newDiagramLinkSplat.ArrowsToEnabled = $true
                            $newDiagramLinkSplat.ArrowsFromEnabled = $true
                        }
                        if ($Trust.IntraForest) {
                            $newDiagramLinkSplat.Color = 'DarkSpringGreen'
                        }
                        if ($Trust.QueryStatus -eq 'OK' -or $Trust.TrustStatus -eq 'OK') {
                            $newDiagramLinkSplat.Dashes = $false
                            $newDiagramLinkSplat.FontColor = 'Green'
                        } else {
                            $newDiagramLinkSplat.Dashes = $true
                            $newDiagramLinkSplat.FontColor = 'Red'
                        }
                        if ($Trust.IsTGTDelegationEnabled) {
                            $newDiagramLinkSplat.Color = 'Red'
                            $newDiagramLinkSplat.Label = "Delegation Enabled"
                        } else {
                            $newDiagramLinkSplat.Label = $Trust.QueryStatus
                        }
                        #>

                            #New-DiagramLink @newDiagramLinkSplat
                        }
                    }
                }
            }
        }

        New-HTMLSection -Title "Information about Trusts" {
            New-HTMLTable -DataTable $Script:OrganiazationalUnits -Filtering {
                if (-not $DisableBuiltinConditions) {
                    #New-TableCondition -BackgroundColor MediumSeaGreen -ComparisonType string -Value 'OK' -Name TrustStatus -Operator eq
                    #New-TableCondition -BackgroundColor MediumSeaGreen -ComparisonType string -Value 'OK' -Name QueryStatus -Operator eq
                    #New-TableCondition -BackgroundColor CoralRed -ComparisonType string -Value 'NOT OK' -Name QueryStatus -Operator eq
                    #New-TableCondition -BackgroundColor CoralRed -ComparisonType bool -Value $true -Name IsTGTDelegationEnabled -Operator eq
                }
                if ($Conditions) {
                    & $Conditions
                }
            } -DataTableID 'DT-StandardOrg'
        }
    } -ShowHTML -FilePath $FilePath -Online
}
function Show-WinADSites {
    <#
    .SYNOPSIS
    Generates a detailed HTML report on the sites and replication in a specified Active Directory forest.
 
    .DESCRIPTION
    This cmdlet creates a comprehensive HTML report that includes a diagram of the sites and their relationships, as well as a table with detailed information about the sites and their replication status. The report is designed to provide a clear overview of the site structure and replication health within the Active Directory.
 
    .PARAMETER Conditions
    Specifies the conditions to filter the sites and replication information. This can be a script block that returns a boolean value.
 
    .PARAMETER FilePath
    The path to save the HTML report. If not specified, a temporary file is used.
 
    .EXAMPLE
    Show-WinADSites -FilePath "C:\Reports\AD Sites Report.html"
 
    .NOTES
    This cmdlet is useful for administrators to visualize and analyze the site structure and replication health in Active Directory, helping to identify potential issues and ensure efficient domain controller communication.
    #>

    [cmdletBinding()]
    param(
        [ScriptBlock] $Conditions,
        [string] $FilePath
    )
    $CacheReplication = @{}
    $Sites = Get-WinADForestSites
    $Replication = Get-WinADForestReplication
    foreach ($Rep in $Replication) {
        $CacheReplication["$($Rep.Server)$($Rep.ServerPartner)"] = $Rep
    }

    New-HTML -TitleText "Visual Active Directory Organization" {
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLTableOption -DataStore HTML
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
        New-HTMLTabPanel {
            New-HTMLTab -TabName 'Standard' {
                New-HTMLSection -HeaderText 'Organization Diagram' {
                    New-HTMLDiagram -Height 'calc(50vh)' {
                        New-DiagramEvent -ID 'DT-StandardSites' -ColumnID 0
                        New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
                        foreach ($Site in $Sites) {
                            New-DiagramNode -Id $Site.DistinguishedName -Label $Site.Name -Image 'https://cdn-icons-png.flaticon.com/512/1104/1104991.png'
                            foreach ($Subnet in $Site.Subnets) {
                                New-DiagramNode -Id $Subnet -Label $Subnet -Image 'https://cdn-icons-png.flaticon.com/512/1674/1674968.png'
                                New-DiagramEdge -From $Subnet -To $Site.DistinguishedName
                            }
                            foreach ($DC in $Site.DomainControllers) {
                                New-DiagramNode -Id $DC -Label $DC -Image 'https://cdn-icons-png.flaticon.com/512/1383/1383395.png'
                                New-DiagramEdge -From $DC -To $Site.DistinguishedName
                            }
                        }
                        foreach ($R in $CacheReplication.Values) {
                            if ($R.ConsecutiveReplicationFailures -gt 0) {
                                $Color = 'CoralRed'
                            } else {
                                $Color = 'MediumSeaGreen'
                            }
                            New-DiagramEdge -From $R.Server -To $R.ServerPartner -Color $Color -ArrowsToEnabled -ColorOpacity 0.5
                        }
                    }
                }
            }
        }
        New-HTMLSection -Title "Information about Sites" {
            New-HTMLTable -DataTable $Sites -Filtering {
                if (-not $DisableBuiltinConditions) {
                    New-TableCondition -BackgroundColor MediumSeaGreen -ComparisonType number -Value 0 -Name SubnetsCount -Operator gt
                    New-TableCondition -BackgroundColor CoralRed -ComparisonType number -Value 0 -Name SubnetsCount -Operator eq
                }
                if ($Conditions) {
                    & $Conditions
                }
            } -DataTableID 'DT-StandardSites' -DataStore JavaScript
        }
        New-HTMLTable -DataTable $Replication -Filtering {
            if (-not $DisableBuiltinConditions) {
                New-TableCondition -BackgroundColor MediumSeaGreen -ComparisonType number -Value 0 -Name SubnetsCount -Operator gt
                New-TableCondition -BackgroundColor CoralRed -ComparisonType number -Value 0 -Name SubnetsCount -Operator eq
            }
            if ($Conditions) {
                & $Conditions
            }
        } -DataTableID 'DT-StandardSites1' -DataStore JavaScript
    } -ShowHTML -FilePath $FilePath -Online
}
function Show-WinADTrust {
    <#
    .SYNOPSIS
    Generates a detailed HTML report on the trust relationships in a specified Active Directory forest.
 
    .DESCRIPTION
    This cmdlet creates a comprehensive HTML report that includes the trust relationships, their properties, and a diagram of their relationships. The report is designed to provide a clear overview of the trust relationships within the Active Directory.
 
    .PARAMETER Conditions
    Specifies the conditions to filter the trust relationships. This can be a script block that returns a boolean value.
 
    .PARAMETER Recursive
    A switch to include all trust relationships in the report, including those that are not direct.
 
    .PARAMETER FilePath
    The path to save the HTML report. If not specified, a temporary file is used.
 
    .PARAMETER Online
    A switch to display the HTML report in the default web browser.
 
    .PARAMETER HideHTML
    A switch to hide the HTML report after it is generated.
 
    .PARAMETER DisableBuiltinConditions
    A switch to disable the built-in conditions for filtering the trust relationships.
 
    .PARAMETER PassThru
    A switch to return the trust relationships as objects.
 
    .EXAMPLE
    Show-WinADTrust -Recursive -Online
 
    .NOTES
    This cmdlet is useful for auditing and analyzing the trust relationships in Active Directory, helping administrators to identify potential security risks and ensure compliance with organizational policies.
    #>

    [alias('Show-ADTrust', 'Show-ADTrusts', 'Show-WinADTrusts')]
    [cmdletBinding()]
    param(
        [Parameter(Position = 0)][scriptblock] $Conditions,
        [switch] $Recursive,
        [string] $FilePath,
        [switch] $Online,
        [switch] $HideHTML,
        [switch] $DisableBuiltinConditions,
        [switch] $PassThru
    )
    if ($FilePath -eq '') {
        $FilePath = Get-FileName -Extension 'html' -Temporary
    }
    $Script:ADTrusts = @()
    New-HTML -TitleText "Visual Trusts" {
        New-HTMLSectionStyle -BorderRadius 0px -HeaderBackGroundColor Grey -RemoveShadow
        New-HTMLTableOption -DataStore HTML
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey

        $Script:ADTrusts = Get-WinADTrust -Recursive:$Recursive
        Write-Verbose "Show-WinADTrust - Found $($ADTrusts.Count) trusts"
        New-HTMLTab -TabName 'Summary' {
            New-HTMLSection -HeaderText 'Trusts Diagram' {
                New-HTMLDiagram -Height 'calc(50vh)' {
                    #New-DiagramEvent -ID 'DT-TrustsInformation' -ColumnID 0
                    New-DiagramOptionsPhysics -RepulsionNodeDistance 150 -Solver repulsion
                    foreach ($Node in $AllNodes) {
                        New-DiagramNode -Label $Node.'Trust'
                    }
                    foreach ($Trust in $ADTrusts) {
                        New-DiagramNode -Label $Trust.'TrustSource' -IconSolid audio-description
                        New-DiagramNode -Label $Trust.'TrustTarget' -IconSolid audio-description

                        $newDiagramLinkSplat = @{
                            From         = $Trust.'TrustSource'
                            To           = $Trust.'TrustTarget'
                            ColorOpacity = 0.7
                        }
                        if ($Trust.'TrustDirection' -eq 'Disabled') {
                        } elseif ($Trust.'TrustDirection' -eq 'Inbound') {
                            $newDiagramLinkSplat.ArrowsFromEnabled = $true
                        } elseif ($Trust.'TrustDirection' -eq 'Outbount') {
                            $newDiagramLinkSplat.ArrowsToEnabled = $true
                            New-DiagramLink @newDiagramLinkSplat
                        } elseif ($Trust.'TrustDirection' -eq 'Bidirectional') {
                            $newDiagramLinkSplat.ArrowsToEnabled = $true
                            $newDiagramLinkSplat.ArrowsFromEnabled = $true
                        }
                        if ($Trust.IntraForest) {
                            $newDiagramLinkSplat.Color = 'DarkSpringGreen'
                        }
                        if ($Trust.QueryStatus -eq 'OK' -or $Trust.TrustStatus -eq 'OK') {
                            $newDiagramLinkSplat.Dashes = $false
                            $newDiagramLinkSplat.FontColor = 'Green'
                        } else {
                            $newDiagramLinkSplat.Dashes = $true
                            $newDiagramLinkSplat.FontColor = 'Red'
                        }
                        if ($Trust.IsTGTDelegationEnabled) {
                            $newDiagramLinkSplat.Color = 'Red'
                            $newDiagramLinkSplat.Label = "Delegation Enabled"
                        } else {
                            $newDiagramLinkSplat.Label = $Trust.QueryStatus
                        }
                        New-DiagramLink @newDiagramLinkSplat
                    }
                }
            }
            New-HTMLSection -Title "Information about Trusts" {
                New-HTMLTable -DataTable $ADTrusts -Filtering {
                    if (-not $DisableBuiltinConditions) {
                        New-TableCondition -BackgroundColor MediumSeaGreen -ComparisonType string -Value 'OK' -Name TrustStatus -Operator eq
                        New-TableCondition -BackgroundColor MediumSeaGreen -ComparisonType string -Value 'OK' -Name QueryStatus -Operator eq
                        New-TableCondition -BackgroundColor CoralRed -ComparisonType string -Value 'NOT OK' -Name QueryStatus -Operator eq
                        New-TableCondition -BackgroundColor CoralRed -ComparisonType bool -Value $true -Name IsTGTDelegationEnabled -Operator eq
                    }
                    if ($Conditions) {
                        & $Conditions
                    }
                } -DataTableID 'DT-TrustsInformation'
            }
        }
        # Lets try to sort it into source domain per tab
        $TrustCache = [ordered]@{}
        foreach ($Trust in $ADTrusts) {
            Write-Verbose "Show-WinADTrust - Processing $($Trust.TrustSource) to $($Trust.TrustTarget)"
            if (-not $TrustCache[$Trust.TrustSource]) {
                Write-Verbose "Show-WinADTrust - Creating cache for $($Trust.TrustSource)"
                $TrustCache[$Trust.TrustSource] = [System.Collections.Generic.List[PSCustomObject]]::new()
            }
            $TrustCache[$Trust.TrustSource].Add($Trust)
        }
        foreach ($Source in $TrustCache.Keys) {
            New-HTMLTab -TabName "Source $($Source.ToUpper())" {
                foreach ($Trust in $TrustCache[$Source]) {
                    if ($Trust.QueryStatus -eq 'OK' -or $Trust.TrustStatus -eq 'OK') {
                        $IconColor = 'MediumSeaGreen'
                        $IconSolid = 'smile'
                    } else {
                        $IconColor = 'CoralRed'
                        $IconSolid = 'angry'
                    }

                    New-HTMLTab -TabName "Target $($Trust.TrustTarget.ToUpper())" -IconColor $IconColor -IconSolid $IconSolid -TextColor $IconColor {
                        New-HTMLSection -Invisible {
                            New-HTMLSection -Title "Trust Information" {
                                New-HTMLTable -DataTable $Trust {
                                    New-TableHeader -Names Name, Value -Title 'Trust Information'
                                } -Transpose -HideFooter -DisablePaging -Buttons copyHtml5, excelHtml5, pdfHtml5
                            }
                            New-HTMLSection -Invisible -Wrap wrap {
                                New-HTMLSection -Title "Name suffix status" {
                                    New-HTMLTable -DataTable $Trust.AdditionalInformation.msDSTrustForestTrustInfo -Filtering {
                                        if ($Trust.AdditionalInformation.msDSTrustForestTrustInfo.Count -gt 0) {
                                            New-TableCondition -BackgroundColor MediumSeaGreen -ComparisonType string -Value 'Enabled' -Name Status -Operator eq -Row
                                            New-TableCondition -BackgroundColor CoralRed -ComparisonType string -Value 'Enabled' -Name Status -Operator ne -Row
                                        }
                                    }
                                }
                                New-HTMLSection -Title "Name suffix routing (include)" {
                                    New-HTMLTable -DataTable $Trust.AdditionalInformation.SuffixesInclude -Filtering {
                                        if ($Trust.AdditionalInformation.SuffixesInclude.Count -gt 0) {
                                            New-TableCondition -BackgroundColor MediumSeaGreen -ComparisonType string -Value 'Enabled' -Name Status -Operator eq -Row
                                            New-TableCondition -BackgroundColor CoralRed -ComparisonType string -Value 'Enabled' -Name Status -Operator ne -Row
                                        }
                                    }
                                }
                                New-HTMLSection -Title "Name suffix routing (exclude)" {
                                    New-HTMLTable -DataTable $Trust.AdditionalInformation.SuffixesExclude -Filtering {
                                        if ($Trust.AdditionalInformation.SuffixesExclude.Count -gt 0) {
                                            New-TableCondition -BackgroundColor MediumSeaGreen -ComparisonType string -Value 'Enabled' -Name Status -Operator eq -Row
                                            New-TableCondition -BackgroundColor CoralRed -ComparisonType string -Value 'Enabled' -Name Status -Operator ne -Row
                                        }
                                    }
                                }
                            }
                        }
                    }
                }
            }
        }
    } -Online:$Online -FilePath $FilePath -ShowHTML:(-not $HideHTML)
    if ($PassThru) {
        $Script:ADTrusts
    }
}
function Show-WinADUserSecurity {
    <#
    .SYNOPSIS
    Generates a detailed HTML report on the security settings for a specified Active Directory user.
 
    .DESCRIPTION
    This cmdlet creates a comprehensive HTML report that includes the user's properties, access control list (ACL), group memberships, and a diagram of their group hierarchy. The report is designed to provide a clear overview of the user's security settings and relationships within the Active Directory.
 
    .PARAMETER Identity
    Specifies the identity of the user for whom to generate the report. This can be a distinguished name, GUID, security identifier (SID), or SAM account name.
 
    .EXAMPLE
    Show-WinADUserSecurity -Identity "CN=User1,DC=example,DC=com"
 
    .NOTES
    This cmdlet is useful for auditing and analyzing the security settings of Active Directory users, helping administrators to identify potential security risks and ensure compliance with organizational policies.
    #>

    [cmdletBinding()]
    param(
        [string[]] $Identity
    )

    New-HTML {
        foreach ($I in $Identity) {
            $User = Get-WinADObject -Identity $I
            $ACL = Get-ADACL -ADObject $User.Distinguishedname
            $Objects = [ordered] @{}
            $GroupsList = foreach ($A in $ACL) {
                $Objects[$A.Principal] = Get-WinADObject -Identity $A.Principal
                if ($Objects[$A.Principal].ObjectClass -eq 'group') {
                    $Objects[$A.Principal].Distinguishedname
                }
            }

            $Groups = $Objects.Values | Where-Object { $_.ObjectClass -eq 'group' } | Sort-Object -Property Distinguishedname
            $GroupsList = foreach ($G in $Groups) {
                Get-WinADGroupMember -Identity $G.Distinguishedname -AddSelf
            }

            New-HTMLTab -Name "$($User.DomainName)\$($User.SamAccountName)" {
                New-HTMLSection -Invisible {
                    New-HTMLPanel {
                        New-HTMLTable -DataTable $User
                    }
                    New-HTMLPanel {
                        New-HTMLTable -Filtering -DataTable $ACL -IncludeProperty Principal, AccessControlType, ActiveDirectoryRights, ObjectTypeName, InheritedObjectTypeName, InhertitanceType, IsInherited
                    }
                }
                New-HTMLSection -Invisible {

                    New-HTMLTable -Filtering -DataTable $Objects.Keys
                }
                $HideAppliesTo = 'Default'
                New-HTMLTabPanel {
                    New-HTMLTab -TabName 'Diagram Basic' {
                        New-HTMLSection -Title "Diagram for Summary" {
                            New-HTMLGroupDiagramSummary -ADGroup $GroupsList -HideAppliesTo $HideAppliesTo -HideUsers:$HideUsers -HideComputers:$HideComputers -HideOther:$HideOther -DataTableID $DataTableID -ColumnID 1 -Online:$Online
                        }
                    }
                    New-HTMLTab -TabName 'Diagram Hierarchy' {
                        New-HTMLSection -Title "Diagram for Summary" {
                            New-HTMLGroupDiagramSummaryHierarchical -ADGroup $GroupsList -HideAppliesTo $HideAppliesTo -HideUsers:$HideUsers -HideComputers:$HideComputers -HideOther:$HideOther -Online:$Online
                        }
                    }
                }
            }
        }
    } -Online -ShowHTML
}
function Sync-WinADDomainController {
    <#
    .SYNOPSIS
    Synchronizes domain controllers across a specified forest or domains.
 
    .DESCRIPTION
    This cmdlet synchronizes domain controllers across a specified forest or domains. It uses the repadmin tool to force synchronization of domain controllers for each domain in the forest. The cmdlet can be filtered to include or exclude specific domains or domain controllers, and can also skip Read-Only Domain Controllers (RODCs).
 
    .PARAMETER Forest
    The name of the forest to synchronize domain controllers for. If not specified, the current user's forest is used.
 
    .PARAMETER ExcludeDomains
    An array of domain names to exclude from the synchronization process.
 
    .PARAMETER ExcludeDomainControllers
    An array of domain controller names to exclude from the synchronization process.
 
    .PARAMETER IncludeDomains
    An array of domain names to include in the synchronization process. If specified, only these domains will be synchronized.
 
    .PARAMETER IncludeDomainControllers
    An array of domain controller names to include in the synchronization process. If specified, only these domain controllers will be synchronized.
 
    .PARAMETER SkipRODC
    A switch to skip Read-Only Domain Controllers (RODCs) during the synchronization process.
 
    .PARAMETER ExtendedForestInformation
    A dictionary containing extended information about the forest, which can be used to speed up processing.
 
    .EXAMPLE
    Sync-WinADDomainController -Forest "example.com"
 
    .NOTES
    This cmdlet is useful for ensuring domain controllers are synchronized across a forest or domains, which is essential for maintaining consistency and ensuring all domain controllers have the same information.
    #>

    [alias('Sync-DomainController')]
    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    foreach ($Domain in $ForestInformation.Domains) {
        $QueryServer = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        $DistinguishedName = (Get-ADDomain -Server $QueryServer).DistinguishedName
        ($ForestInformation['DomainDomainControllers']["$Domain"]).Name | ForEach-Object {
            Write-Verbose -Message "Sync-DomainController - Forcing synchronization $_"
            repadmin /syncall $_ $DistinguishedName /e /A | Out-Null
        }
    }
}
function Test-ADDomainController {
    <#
    .SYNOPSIS
    Tests the domain controllers in a specified forest for various aspects of their functionality.
 
    .DESCRIPTION
    This cmdlet tests the domain controllers in a specified forest for various aspects of their functionality, including DNS resolution, LDAP connectivity, and FSMO role availability. It returns a custom object with detailed information about the domain controllers, their status, and any errors encountered during the test.
 
    .PARAMETER Forest
    The name of the forest to test domain controllers for. If not specified, the current user's forest is used.
 
    .PARAMETER ExcludeDomains
    An array of domain names to exclude from the test.
 
    .PARAMETER ExcludeDomainControllers
    An array of domain controller names to exclude from the test.
 
    .PARAMETER IncludeDomains
    An array of domain names to include in the test. If specified, only these domains will be tested.
 
    .PARAMETER IncludeDomainControllers
    An array of domain controller names to include in the test. If specified, only these domain controllers will be tested.
 
    .PARAMETER SkipRODC
    A switch to skip Read-Only Domain Controllers (RODCs) during the test.
 
    .PARAMETER Credential
    A PSCredential object to use for authentication when connecting to domain controllers.
 
    .PARAMETER ExtendedForestInformation
    A dictionary containing extended information about the forest, which can be used to speed up processing.
 
    .EXAMPLE
    Test-ADDomainController -Forest "example.com"
 
    .NOTES
    This cmdlet is useful for monitoring the health and functionality of domain controllers in a forest.
    #>

    [CmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers', 'DomainController', 'ComputerName')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [Parameter(Mandatory = $false)][PSCredential] $Credential = $null,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )

    $CredentialParameter = @{ }
    if ($null -ne $Credential) {
        $CredentialParameter['Credential'] = $Credential
    }

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    $Output = foreach ($Computer in $ForestInformation.ForestDomainControllers.HostName) {
        $Result = Invoke-Command -ComputerName $Computer -ScriptBlock {
            dcdiag.exe /v /c /Skip:OutboundSecureChannels
        } @CredentialParameter

        for ($Line = 0; $Line -lt $Result.length; $Line++) {
            # Correct wrong line breaks
            if ($Result[$Line] -match '^\s{9}.{25} (\S+) (\S+) test$') {
                $Result[$Line] = $Result[$Line] + ' ' + $Result[$Line + 2].Trim()
            }
            # Verify test start line
            if ($Result[$Line] -match '^\s{6}Starting test: \S+$') {
                $LineStart = $Line
            }
            # Verify test end line
            if ($Result[$Line] -match '^\s{9}.{25} (\S+) (\S+) test (\S+)$') {
                $DiagnosticResult = [PSCustomObject] @{
                    ComputerName = $Computer
                    #Domain = $Domain
                    Target       = $Matches[1]
                    Test         = $Matches[3]
                    Result       = $Matches[2] -eq 'passed'
                    Data         = $Result[$LineStart..$Line] -join [System.Environment]::NewLine
                }
                $DiagnosticResult
            }
        }
    }
    $Output
}
function Test-ADRolesAvailability {
    <#
    .SYNOPSIS
    Tests the availability of Active Directory roles across domain controllers in a specified forest.
 
    .DESCRIPTION
    This cmdlet tests the availability of Active Directory roles across domain controllers in a specified forest. It returns a custom object with details about the role, the hostname of the domain controller, and the status of the connection to the domain controller.
 
    .PARAMETER Forest
    The name of the forest to test roles for. If not specified, the current user's forest is used.
 
    .PARAMETER ExcludeDomains
    Exclude specific domains from the test.
 
    .PARAMETER ExcludeDomainControllers
    Exclude specific domain controllers from the test.
 
    .PARAMETER IncludeDomains
    Include only specific domains in the test.
 
    .PARAMETER IncludeDomainControllers
    Include only specific domain controllers in the test.
 
    .PARAMETER SkipRODC
    Skip Read-Only Domain Controllers when testing roles.
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing.
 
    .EXAMPLE
    Test-ADRolesAvailability
 
    .EXAMPLE
    Test-ADRolesAvailability -Forest "example.com"
 
    .NOTES
    This cmdlet is useful for monitoring the availability of Active Directory roles across domain controllers in a forest.
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )
    $Roles = Get-WinADForestRoles -Forest $Forest -IncludeDomains $IncludeDomains -IncludeDomainControllers $IncludeDomainControllers -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation
    if ($IncludeDomains) {
        [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 {
    <#
    .SYNOPSIS
    Tests the Active Directory site links for a specified forest.
 
    .DESCRIPTION
    This cmdlet queries the specified forest to check the status of its site links. It returns a custom object with details about the site links, their status, and any errors encountered during the query.
 
    .PARAMETER Forest
    The name of the forest to query. If not specified, the current user's forest is used.
 
    .PARAMETER Splitter
    The character to use for splitting the site links. If not specified, the default is used.
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing.
 
    .EXAMPLE
    Test-ADSiteLinks -Forest "example.com"
 
    .NOTES
    This cmdlet is useful for monitoring the status of site links in a forest.
    #>

    [cmdletBinding()]
    param(
        [alias('ForestName')][string] $Forest,
        [string] $Splitter,
        [System.Collections.IDictionary] $ExtendedForestInformation
    )

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -ExtendedForestInformation $ExtendedForestInformation
    if (($ForestInformation.ForestDomainControllers).Count -eq 1) {
        [ordered] @{
            SiteLinksManual                     = 'No sitelinks, single DC'
            SiteLinksAutomatic                  = 'No sitelinks, single DC'

            SiteLinksCrossSiteUseNotify         = 'No sitelinks, single DC'
            SiteLinksCrossSiteNotUseNotify      = 'No sitelinks, single DC'
            SiteLinksSameSiteUseNotify          = 'No sitelinks, single DC'
            SiteLinksSameSiteNotUseNotify       = 'No sitelinks, single DC'

            SiteLinksDisabled                   = 'No sitelinks, single DC'
            SiteLinksEnabled                    = 'No sitelinks, single DC'

            SiteLinksCrossSiteUseNotifyCount    = 0
            SiteLinksCrossSiteNotUseNotifyCount = 0
            SiteLinksSameSiteUseNotifyCount     = 0
            SiteLinksSameSiteNotUseNotifyCount  = 0

            SiteLinksManualCount                = 0
            SiteLinksAutomaticCount             = 0
            SiteLinksDisabledCount              = 0
            SiteLinksEnabledCount               = 0
            SiteLinksTotalCount                 = 0
            SiteLinksTotalActiveCount           = 0
            Comment                             = 'No sitelinks, single DC'
        }
    } else {
        [Array] $SiteLinks = Get-WinADSiteConnections -ExtendedForestInformation $ForestInformation
        if ($SiteLinks) {
            $Collection = @($SiteLinks).Where( { $_.Options -notcontains 'IsGenerated' -and $_.EnabledConnection -eq $true }, 'Split')
            [Array] $LinksManual = foreach ($Link in $Collection[0]) {
                "$($Link.ServerFrom) to $($Link.ServerTo)"
            }
            [Array] $LinksAutomatic = foreach ($Link in $Collection[1]) {
                "$($Link.ServerFrom) to $($Link.ServerTo)"
            }
            $LinksUsingNotificationsUnnessecary = [System.Collections.Generic.List[string]]::new()
            $LinksUsingNotifications = [System.Collections.Generic.List[string]]::new()
            $LinksNotUsingNotifications = [System.Collections.Generic.List[string]]::new()
            $LinksUsingNotificationsWhichIsOk = [System.Collections.Generic.List[string]]::new()
            $DisabledLinks = [System.Collections.Generic.List[string]]::new()
            $EnabledLinks = [System.Collections.Generic.List[string]]::new()
            foreach ($Link in $SiteLinks) {
                if ($Link.EnabledConnection -eq $true) {
                    $EnabledLinks.Add("$($Link.ServerFrom) to $($Link.ServerTo)")
                } else {
                    $DisabledLinks.Add("$($Link.ServerFrom) to $($Link.ServerTo)")
                }
                if ($Link.SiteFrom -eq $Link.SiteTo) {
                    if ($Link.Options -contains 'UseNotify') {
                        # Bad
                        $LinksUsingNotificationsUnnessecary.Add("$($Link.ServerFrom) to $($Link.ServerTo)")
                    } else {
                        # Good
                        $LinksUsingNotificationsWhichIsOk.Add("$($Link.ServerFrom) to $($Link.ServerTo)")
                    }
                } else {
                    if ($Link.Options -contains 'UseNotify') {
                        # Good
                        $LinksUsingNotifications.Add("$($Link.ServerFrom) to $($Link.ServerTo)")
                    } else {
                        # Bad
                        $LinksNotUsingNotifications.Add("$($Link.ServerFrom) to $($Link.ServerTo)")
                    }
                }
            }
            [ordered] @{
                SiteLinksManual                     = if ($Splitter -eq '') {
                    $LinksManual 
                } else {
                    $LinksManual -join $Splitter 
                }
                SiteLinksAutomatic                  = if ($Splitter -eq '') {
                    $LinksAutomatic 
                } else {
                    $LinksAutomatic -join $Splitter 
                }

                SiteLinksCrossSiteUseNotify         = if ($Splitter -eq '') {
                    $LinksUsingNotifications 
                } else {
                    $LinksUsingNotifications -join $Splitter 
                }
                SiteLinksCrossSiteNotUseNotify      = if ($Splitter -eq '') {
                    $LinksNotUsingNotifications 
                } else {
                    $LinksNotUsingNotifications -join $Splitter 
                }
                SiteLinksSameSiteUseNotify          = if ($Splitter -eq '') {
                    $LinksUsingNotificationsUnnessecary 
                } else {
                    $LinksUsingNotificationsUnnessecary -join $Splitter 
                }
                SiteLinksSameSiteNotUseNotify       = if ($Splitter -eq '') {
                    $LinksUsingNotificationsWhichIsOk 
                } else {
                    $LinksUsingNotificationsWhichIsOk -join $Splitter 
                }

                SiteLinksDisabled                   = if ($Splitter -eq '') {
                    $DisabledLinks 
                } else {
                    $DisabledLinks -join $Splitter 
                }
                SiteLinksEnabled                    = if ($Splitter -eq '') {
                    $EnabledLinks 
                } else {
                    $EnabledLinks -join $Splitter 
                }

                SiteLinksCrossSiteUseNotifyCount    = $LinksUsingNotifications.Count
                SiteLinksCrossSiteNotUseNotifyCount = $LinksNotUsingNotifications.Count
                SiteLinksSameSiteUseNotifyCount     = $LinksUsingNotificationsUnnessecary.Count
                SiteLinksSameSiteNotUseNotifyCount  = $LinksUsingNotificationsWhichIsOk.Count

                SiteLinksManualCount                = $Collection[0].Count
                SiteLinksAutomaticCount             = $Collection[1].Count
                SiteLinksDisabledCount              = $DisabledLinks.Count
                SiteLinksEnabledCount               = $EnabledLinks.Count
                SiteLinksTotalCount                 = $SiteLinks.Count
                SiteLinksTotalActiveCount           = ($SiteLinks | Where-Object { $_.EnabledConnection -eq $true } ).Count
                Comment                             = 'OK'
            }
        } else {
            [ordered] @{
                SiteLinksManual                     = 'No sitelinks'
                SiteLinksAutomatic                  = 'No sitelinks'

                SiteLinksCrossSiteUseNotify         = 'No sitelinks'
                SiteLinksCrossSiteNotUseNotify      = 'No sitelinks'
                SiteLinksSameSiteUseNotify          = 'No sitelinks'
                SiteLinksSameSiteNotUseNotify       = 'No sitelinks'

                SiteLinksDisabled                   = 'No sitelinks'
                SiteLinksEnabled                    = 'No sitelinks'

                SiteLinksCrossSiteUseNotifyCount    = 0
                SiteLinksCrossSiteNotUseNotifyCount = 0
                SiteLinksSameSiteUseNotifyCount     = 0
                SiteLinksSameSiteNotUseNotifyCount  = 0

                SiteLinksManualCount                = 0
                SiteLinksAutomaticCount             = 0
                SiteLinksDisabledCount              = 0
                SiteLinksEnabledCount               = 0
                SiteLinksTotalCount                 = 0
                SiteLinksTotalActiveCount           = 0
                Comment                             = 'Error'
            }
        }
    }
}
function Test-DNSNameServers {
    <#
    .SYNOPSIS
    Tests the DNS name servers for a specified domain controller and domain.
 
    .DESCRIPTION
    This cmdlet queries the specified domain controller for the DNS name servers of the specified domain. It returns a custom object with details about the domain controllers, name servers, their status, and any errors encountered during the query.
 
    .PARAMETER DomainController
    The name of the domain controller to query.
 
    .PARAMETER Domain
    The name of the domain to query.
 
    .EXAMPLE
    Test-DNSNameServers -DomainController "DC1" -Domain "example.com"
 
    .NOTES
    This cmdlet is useful for monitoring the DNS name servers of a domain.
    #>

    [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 {
    <#
    .SYNOPSIS
    Tests the availability of FSMO roles across domain controllers in a specified domain.
 
    .DESCRIPTION
    This cmdlet tests the availability of Flexible Single Master Operations (FSMO) roles across domain controllers in a specified domain. It returns a custom object with details about the role, the hostname of the domain controller, and the status of the connection to the domain controller.
 
    .PARAMETER Domain
    The name of the domain to test FSMO roles for. If not specified, the current user's DNS domain is used.
 
    .EXAMPLE
    Test-FSMORolesAvailability
 
    .EXAMPLE
    Test-FSMORolesAvailability -Domain "example.com"
 
    .NOTES
    This cmdlet is useful for monitoring the availability of FSMO roles across domain controllers in a domain.
    #>

    [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 {
    <#
    .SYNOPSIS
    Tests LDAP connectivity to one ore more servers.
 
    .DESCRIPTION
    Tests LDAP connectivity to one ore more servers. It's able to gather certificate information which provides useful information.
 
    .PARAMETER Forest
    Target different Forest, by default current forest is used
 
    .PARAMETER ExcludeDomains
    Exclude domain from search, by default whole forest is scanned
 
    .PARAMETER IncludeDomains
    Include only specific domains, by default whole forest is scanned
 
    .PARAMETER ExcludeDomainControllers
    Exclude specific domain controllers, by default there are no exclusions
 
    .PARAMETER IncludeDomainControllers
    Include only specific domain controllers, by default all domain controllers are included
 
    .PARAMETER SkipRODC
    Skip Read-Only Domain Controllers. By default all domain controllers are included.
 
    .PARAMETER ExtendedForestInformation
    Ability to provide Forest Information from another command to speed up processing
 
    .PARAMETER ComputerName
    Provide FQDN, IpAddress or NetBIOS name to test LDAP connectivity. This can be used instead of targetting Forest/Domain specific LDAP Servers
 
    .PARAMETER GCPortLDAP
    Global Catalog Port for LDAP. If not defined uses default 3268 port.
 
    .PARAMETER GCPortLDAPSSL
    Global Catalog Port for LDAPs. If not defined uses default 3269 port.
 
    .PARAMETER PortLDAP
    LDAP port. If not defined uses default 389
 
    .PARAMETER PortLDAPS
    LDAPs port. If not defined uses default 636
 
    .PARAMETER VerifyCertificate
    Binds to LDAP and gathers information about certificate available
 
    .PARAMETER Credential
    Allows to define credentials. This switches authentication for LDAP Binding from Kerberos to Basic
 
    .PARAMETER Identity
    User to search for using LDAP query by objectGUID, objectSID, SamAccountName, UserPrincipalName, Name or DistinguishedName
 
    .PARAMETER Extended
    Returns additional information about LDAP Server including full objects
 
    .PARAMETER SkipCheckGC
    Skips querying GC ports
 
    .PARAMETER RetryCount
    Number of retries to perform in case of failure
 
    .EXAMPLE
    Test-LDAP -ComputerName 'AD1' -VerifyCertificate | Format-Table *
 
    .EXAMPLE
    Test-LDAP -VerifyCertificate -SkipRODC | Format-Table *
 
    .NOTES
    General notes
    #>

    [CmdletBinding(DefaultParameterSetName = 'Forest')]
    param (
        [Parameter(ParameterSetName = 'Forest')][alias('ForestName')][string] $Forest,
        [Parameter(ParameterSetName = 'Forest')][string[]] $ExcludeDomains,
        [Parameter(ParameterSetName = 'Forest')][string[]] $ExcludeDomainControllers,
        [Parameter(ParameterSetName = 'Forest')][alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [Parameter(ParameterSetName = 'Forest')][alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [Parameter(ParameterSetName = 'Forest')][switch] $SkipRODC,
        [Parameter(ParameterSetName = 'Forest')][System.Collections.IDictionary] $ExtendedForestInformation,

        [alias('Server', 'IpAddress')][Parameter(ValueFromPipelineByPropertyName, ValueFromPipeline, Mandatory, ParameterSetName = 'Computer')][string[]]$ComputerName,

        [Parameter(ParameterSetName = 'Forest')]
        [Parameter(ParameterSetName = 'Computer')]
        [int] $GCPortLDAP = 3268,
        [Parameter(ParameterSetName = 'Forest')]
        [Parameter(ParameterSetName = 'Computer')]
        [int] $GCPortLDAPSSL = 3269,
        [Parameter(ParameterSetName = 'Forest')]
        [Parameter(ParameterSetName = 'Computer')]
        [int] $PortLDAP = 389,
        [Parameter(ParameterSetName = 'Forest')]
        [Parameter(ParameterSetName = 'Computer')]
        [int] $PortLDAPS = 636,
        [Parameter(ParameterSetName = 'Forest')]
        [Parameter(ParameterSetName = 'Computer')]
        [switch] $VerifyCertificate,
        [Parameter(ParameterSetName = 'Forest')]
        [Parameter(ParameterSetName = 'Computer')]
        [PSCredential] $Credential,

        [Parameter(ParameterSetName = 'Computer')]
        [Parameter(ParameterSetName = 'Forest')]
        [string] $Identity,

        [Parameter(ParameterSetName = 'Computer')]
        [Parameter(ParameterSetName = 'Forest')]
        [switch] $Extended,

        [Parameter(ParameterSetName = 'Computer')]
        [switch] $SkipCheckGC,

        [Parameter(ParameterSetName = 'Computer')]
        [Parameter(ParameterSetName = 'Forest')]
        [int] $RetryCount
    )
    begin {
        Add-Type -Assembly System.DirectoryServices.Protocols
        if (-not $ComputerName) {
            $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExtendedForestInformation $ExtendedForestInformation -SkipRODC:$SkipRODC.IsPresent -IncludeDomainControllers $IncludeDomainControllers -ExcludeDomainControllers $ExcludeDomainControllers
        }
    }
    Process {
        if ($ComputerName) {
            foreach ($Computer in $ComputerName) {
                if ($Computer -match '^(\d+\.){3}\d+$') {
                    try {
                        $ServerName = [System.Net.Dns]::GetHostByAddress($Computer).HostName
                    } catch {
                        Write-Warning "Test-LDAP - Unable to resolve $Computer. $($_.Exception.Message)"
                        $ServerName = $Computer
                    }
                } else {
                    try {
                        $ServerName = [System.Net.Dns]::GetHostByName($Computer).HostName
                    } catch {
                        Write-Warning "Test-LDAP - Unable to resolve $Computer. $($_.Exception.Message)"
                        $ServerName = $Computer
                    }
                }
                Write-Verbose "Test-LDAP - Processing $Computer / $ServerName"
                $testLdapServerSplat = @{
                    ServerName        = $ServerName
                    Computer          = $Computer
                    GCPortLDAP        = $GCPortLDAP
                    GCPortLDAPSSL     = $GCPortLDAPSSL
                    PortLDAP          = $PortLDAP
                    PortLDAPS         = $PortLDAPS
                    VerifyCertificate = $VerifyCertificate.IsPresent
                    Identity          = $Identity
                    SkipCheckGC       = $SkipCheckGC
                    RetryCount        = $RetryCount
                }
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    $testLdapServerSplat.Credential = $Credential
                }
                Test-LdapServer @testLdapServerSplat
            }
        } else {
            foreach ($Computer in $ForestInformation.ForestDomainControllers) {
                Write-Verbose "Test-LDAP - Processing $($Computer.HostName)"
                $testLdapServerSplat = @{
                    ServerName        = $($Computer.HostName)
                    Computer          = $Computer.HostName
                    Advanced          = $Computer
                    GCPortLDAP        = $GCPortLDAP
                    GCPortLDAPSSL     = $GCPortLDAPSSL
                    PortLDAP          = $PortLDAP
                    PortLDAPS         = $PortLDAPS
                    VerifyCertificate = $VerifyCertificate.IsPresent
                    Identity          = $Identity
                    RetryCount        = $RetryCount
                }
                if ($PSBoundParameters.ContainsKey('Credential')) {
                    $testLdapServerSplat.Credential = $Credential
                }
                Test-LdapServer @testLdapServerSplat
            }
        }
    }
}
function Test-WinADDNSResolving {
    <#
    .SYNOPSIS
    Test DNS resolving for specific DNS record type across all Domain Controllers in the forest.
 
    .DESCRIPTION
    Test DNS resolving for specific DNS record type across all Domain Controllers in the forest.
 
    .PARAMETER Name
    Name of the DNS record to resolve
 
    .PARAMETER Type
    Type of the DNS record to resolve
 
    .PARAMETER Forest
    Forest name to use for resolving. If not given it will use current forest.
 
    .PARAMETER ExcludeDomains
    Exclude specific domains from test
 
    .PARAMETER ExcludeDomainControllers
    Exclude specific domain controllers from test
 
    .PARAMETER IncludeDomains
    Include specific domains in test
 
    .PARAMETER IncludeDomainControllers
    Include specific domain controllers in test
 
    .PARAMETER SkipRODC
    Skip Read Only Domain Controllers when querying for information
 
    .PARAMETER NotDNSOnly
    Do not use DNS only switch for resolving DNS names
 
    .EXAMPLE
    @(
        Test-WinADDNSResolving -Name "PILAFU085.ad.evotec.xyz" -Type "A" -Verbose -IncludeDomains 'ad.evotec.xyz'
        Test-WinADDNSResolving -Name "15.241.168.192.in-addr.arpa" -Type "PTR" -Verbose
        Test-WinADDNSResolving -Name "192.168.241.15" -Type "PTR" -Verbose
        Test-WinADDNSResolving -Name "Evo-win.ad.evotec.xyz" -Type "CNAME" -Verbose
        Test-WinADDNSResolving -Name "test.domain.pl" -Type "MX" -Verbose
    ) | Format-Table
 
    .OUTPUTS
    Name Type DC Resolving Identical ErrorMessage ResolvedName ResolvedData
    ---- ---- -- --------- --------- ------------ ------------ ------------
    PILAFU085.ad.evotec.xyz A AD2.ad.evotec.xyz True True PILAFU085.ad.evotec.xyz 10.104.65.85
    PILAFU085.ad.evotec.xyz A AD1.ad.evotec.xyz True True PILAFU085.ad.evotec.xyz 10.104.65.85
    PILAFU085.ad.evotec.xyz A AD0.ad.evotec.xyz True True PILAFU085.ad.evotec.xyz 10.104.65.85
    15.241.168.192.in-addr.arpa PTR AD2.ad.evotec.xyz True True 15.241.168.192.in-addr.arpa ADConnect.ad.evotec.xyz
    15.241.168.192.in-addr.arpa PTR AD1.ad.evotec.xyz True True 15.241.168.192.in-addr.arpa ADConnect.ad.evotec.xyz
    15.241.168.192.in-addr.arpa PTR AD0.ad.evotec.xyz True True 15.241.168.192.in-addr.arpa ADConnect.ad.evotec.xyz
    15.241.168.192.in-addr.arpa PTR DC1.ad.evotec.pl True True 15.241.168.192.in-addr.arpa ADConnect.ad.evotec.xyz
    15.241.168.192.in-addr.arpa PTR ADRODC.ad.evotec.pl True True 15.241.168.192.in-addr.arpa ADConnect.ad.evotec.xyz
    192.168.241.15 PTR AD2.ad.evotec.xyz True True 15.241.168.192.in-addr.arpa ADConnect.ad.evotec.xyz
    192.168.241.15 PTR AD1.ad.evotec.xyz True True 15.241.168.192.in-addr.arpa ADConnect.ad.evotec.xyz
    192.168.241.15 PTR AD0.ad.evotec.xyz True True 15.241.168.192.in-addr.arpa ADConnect.ad.evotec.xyz
    192.168.241.15 PTR DC1.ad.evotec.pl True True 15.241.168.192.in-addr.arpa ADConnect.ad.evotec.xyz
    192.168.241.15 PTR ADRODC.ad.evotec.pl True True 15.241.168.192.in-addr.arpa ADConnect.ad.evotec.xyz
    Evo-win.ad.evotec.xyz CNAME AD2.ad.evotec.xyz True True Evo-win.ad.evotec.xyz EVOWIN.ad.evotec.xyz
    Evo-win.ad.evotec.xyz CNAME AD1.ad.evotec.xyz True True Evo-win.ad.evotec.xyz EVOWIN.ad.evotec.xyz
    Evo-win.ad.evotec.xyz CNAME AD0.ad.evotec.xyz True True Evo-win.ad.evotec.xyz EVOWIN.ad.evotec.xyz
    Evo-win.ad.evotec.xyz CNAME DC1.ad.evotec.pl True True Evo-win.ad.evotec.xyz EVOWIN.ad.evotec.xyz
    Evo-win.ad.evotec.xyz CNAME ADRODC.ad.evotec.pl True True Evo-win.ad.evotec.xyz EVOWIN.ad.evotec.xyz
    test.domain.pl MX AD2.ad.evotec.xyz True True test.domain.pl 10 office.com
    test.domain.pl MX AD1.ad.evotec.xyz True True test.domain.pl 10 office.com
    test.domain.pl MX AD0.ad.evotec.xyz True True test.domain.pl 10 office.com
    test.domain.pl MX DC1.ad.evotec.pl True True test.domain.pl 10 office.com
    test.domain.pl MX ADRODC.ad.evotec.pl True True test.domain.pl 10 office.com
 
    .NOTES
    General notes
    #>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory)][string[]] $Name,
        [Parameter(Mandatory)][ValidateSet('PTR', 'A', 'AAAA', 'MX', 'CNAME', 'SRV')][string] $Type,

        [alias('ForestName')][string] $Forest,
        [string[]] $ExcludeDomains,
        [string[]] $ExcludeDomainControllers,
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,
        [alias('DomainControllers')][string[]] $IncludeDomainControllers,
        [switch] $SkipRODC,

        [switch] $NotDNSOnly
    )

    $ForestInformation = Get-WinADForestDetails -Forest $Forest -IncludeDomains $IncludeDomains -ExcludeDomains $ExcludeDomains -ExcludeDomainControllers $ExcludeDomainControllers -IncludeDomainControllers $IncludeDomainControllers -SkipRODC:$SkipRODC -ExtendedForestInformation $ExtendedForestInformation -Extended
    $StatusIdentical = [ordered] @{}
    foreach ($N in $Name) {
        foreach ($DC in $ForestInformation.ForestDomainControllers) {
            Write-Verbose -Message "Test-WinADDNSResolving - Processing $N on $($DC.Hostname)"
            try {
                $ResolvedDNS = Resolve-DnsName -Name $N -Server $DC.Hostname -Type $Type -ErrorAction Stop -DnsOnly:(-not $NotDNSOnly) -Verbose:$false
                $ErrorMessage = $null
            } catch {
                $ErrorMessage = $_.Exception.Message
                $ResolvedDNS = $null
                Write-Warning -Message "Test-WinADDNSResolving - Failed to resolve $N on $($DC.HostName). Error: $($_.Exception.Message)"
            }
            $Status = $false
            $ResolvedName = $null
            $ResolvedData = $null

            if ($ResolvedDNS) {
                if ($ResolvedDNS.Type -eq 'SOA') {
                    $Status = $false
                } else {
                    if ($Type -eq "PTR") {
                        $ResolvedName = $ResolvedDNS.Name
                        $ResolvedData = $ResolvedDNS.NameHost
                        $Status = $true
                    } elseif ($Type -eq "A") {
                        $ResolvedName = $ResolvedDNS.Name
                        $ResolvedData = $ResolvedDNS.IPAddress
                        $Status = $true
                    } elseif ($Type -eq 'AAAA') {
                        $ResolvedName = $ResolvedDNS.Name
                        $ResolvedData = $ResolvedDNS.IPAddress
                        $Status = $true
                    } elseif ($Type -eq "SRV") {
                        $ResolvedName = $ResolvedDNS.Name
                        $ResolvedData = $ResolvedDNS.NameTarget
                        $Status = $true
                    } elseif ($Type -eq 'CNAME') {
                        $ResolvedName = $ResolvedDNS.Name
                        $ResolvedData = $ResolvedDNS.NameHost
                        $Status = $true
                    } elseif ($Type -eq 'MX') {
                        $OnlyMX = $ResolvedDNS | Where-Object { $_.QueryType -eq 'MX' }
                        if ($OnlyMX) {
                            $ResolvedName = $OnlyMX.Name
                            $ResolvedData = "$($OnlyMX.Preference) $($OnlyMX.NameExchange)"
                            $Status = $true
                        } else {
                            $ResolvedName = $null
                            $ResolvedData = $null
                            $Status = $false
                        }
                    } else {
                        $ResolvedName = $ResolvedDNS.Name
                        $ResolvedData = $ResolvedDNS.NameHost
                        $Status = $true
                    }
                }
            }

            if (-not $StatusIdentical[$N]) {
                $StatusIdentical[$N] = $ResolvedData
                $Identical = $true
            } else {
                if ($StatusIdentical[$N] -ne $ResolvedData) {
                    $Identical = $false
                } else {
                    $Identical = $true
                }
            }

            [PSCustomObject] @{
                Name         = $N
                Type         = $Type
                DC           = $DC.Hostname
                Resolving    = $Status
                Identical    = $Identical
                ErrorMessage = $ErrorMessage
                ResolvedName = $ResolvedName
                ResolvedData = $ResolvedData
            }
        }
    }
}
function Test-WinADObjectReplicationStatus {
    <#
    .SYNOPSIS
    Tests the replication status of a Windows Active Directory object across domain controllers.
 
    .DESCRIPTION
    This cmdlet queries the specified object across all domain controllers in the forest, including global catalogs if specified, to check its replication status. It returns a custom object with details about the object's properties and any errors encountered during the query.
 
    .PARAMETER Identity
    The identity of the object to test. This can be a distinguished name, GUID, or SAM account name.
 
    .PARAMETER Forest
    The name of the forest to query. If not specified, the current forest is used.
 
    .PARAMETER ExcludeDomains
    An array of domain names to exclude from the query.
 
    .PARAMETER IncludeDomains
    An array of domain names to include in the query. If not specified, all domains in the forest are queried.
 
    .PARAMETER GlobalCatalog
    A switch parameter to include global catalogs in the query.
 
    .EXAMPLE
    Test-WinADObjectReplicationStatus -Identity "CN=User,DC=example,DC=com"
 
    .EXAMPLE
    Test-WinADObjectReplicationStatus -Identity "CN=User,DC=example,DC=com" -GlobalCatalog
 
    .NOTES
    This cmdlet is useful for monitoring the replication status of critical objects across the domain controllers in a forest.
    #>

    [CmdletBinding(DefaultParameterSetName = 'Standard')]
    param(
        [Parameter(ParameterSetName = 'Standard')]
        [string] $Identity,

        [Parameter(ParameterSetName = 'Standard')]
        [alias('ForestName')][string] $Forest,

        [Parameter(ParameterSetName = 'Standard')]
        [string[]] $ExcludeDomains,

        [Parameter(ParameterSetName = 'Standard')]
        [alias('Domain', 'Domains')][string[]] $IncludeDomains,

        [Parameter(ParameterSetName = 'Standard')]
        [switch] $GlobalCatalog
    )

    $ObjectInformation = Get-WinADObject -Identity $Identity
    if ($null -eq $ObjectInformation) {
        Write-Warning "Test-WinADObjectReplicationStatus - Object not found. Try again later or check the object does exists."
        return
    }
    $DomainFromIdentity = $ObjectInformation.Domain
    $DistinguishedName = $ObjectInformation.DistinguishedName

    $ForestInformation = Get-WinADForestDetails -Extended -PreferWritable
    if ($GlobalCatalog) {
        [Array] $GCs = foreach ($DC in $ForestInformation.ForestDomainControllers) {
            if ($DC.IsGlobalCatalog) {
                $DC
            }
        }
    } else {
        [Array] $GCs = foreach ($DC in $ForestInformation.ForestDomainControllers) {
            if ($DC.Domain -eq $DomainFromIdentity) {
                $DC
            }
        }
    }
    $ResultsCached = [ordered] @{}
    $Results = foreach ($GC in $GCs) {
        # Query the specific object on each GC
        Try {
            if ($GlobalCatalog) {
                Write-Verbose -Message "Test-WinADObjectReplicationStatus - Querying $($GC.HostName) on port 3268 for $DistinguishedName"
                $ObjectInfo = Get-ADObject -Identity $DistinguishedName -Server "$($GC.HostName):3268" -Properties * -ErrorAction Stop
            } else {
                Write-Verbose -Message "Test-WinADObjectReplicationStatus - Querying $($GC.HostName) for $DistinguishedName"
                $ObjectInfo = Get-ADObject -Identity $DistinguishedName -Server $GC.HostName -Properties * -ErrorAction Stop
            }
            $ErrorValue = $null
        } catch {
            $ObjectInfo = $null
            Write-Warning "Test-WinADObjectReplicationStatus - Error: $($_.Exception.Message.Replace([System.Environment]::NewLine,''))"
            $ErrorValue = $_.Exception.Message.Replace([System.Environment]::NewLine, '')
        }
        if ($ObjectInfo) {
            $PreparedObject = [PSCustomObject] @{
                DomainController   = $GC.HostName
                Domain             = $GC.Domain
                UserAccountControl = $ObjectInfo.userAccountCOntrol
                Created            = $ObjectInfo.Created
                uSNChanged         = $ObjectInfo.uSNChanged
                uSNCreated         = $ObjectInfo.uSNCreated
                whenCreated        = $ObjectInfo.whenCreated
                WhenChanged        = $ObjectInfo.WhenChanged
                Error              = $ErrorValue
            }
            $ResultsCached[$GC.HostName] = $PreparedObject
            $PreparedObject
        } else {
            $PreparedObject = [PSCustomObject] @{
                DomainController   = $GC.HostName
                Domain             = $GC.Domain
                UserAccountControl = $null
                Created            = $null
                uSNChanged         = $null
                uSNCreated         = $null
                whenCreated        = $null
                WhenChanged        = $null
                Error              = $ErrorValue
            }
            $ResultsCached[$GC.HostName] = $PreparedObject
            $PreparedObject
        }
    }

    $SortedResults = $Results | Sort-Object -Property WhenChanged
    $FistResult = $SortedResults | Where-Object { $null -ne $_.WhenChanged } | Select-Object -First 1
    $Output = foreach ($Result in $SortedResults) {
        [PSCustomObject] @{
            SamAccountName   = $ObjectInformation.SamAccountName
            DomainController = $Result.DomainController
            Domain           = $Result.Domain
            WhenCreated      = $Result.whenCreated
            WhenChanged      = $Result.WhenChanged
            TimeSinceFirst   = if ($Result.WhenChanged) {
                $Result.WhenChanged - $FistResult.WhenChanged 
            } else {
                $null 
            }
            Error            = $Result.Error
        }
    }
    $Output | Sort-Object -Property WhenChanged -Descending
}
function Test-WinADVulnerableSchemaClass {
    <#
    .SYNOPSIS
    Checks for CVE-2021-34470 and returns and object with output
 
    .DESCRIPTION
    Checks for CVE-2021-34470 and returns and object with output
 
    .EXAMPLE
    Test-WinADVulnerableSchemaClass
 
    .NOTES
    Based on https://microsoft.github.io/CSS-Exchange/Security/Test-CVE-2021-34470/
    To repair either upgrade Microsoft Exchange Schema or run the fix from URL above
    #>

    [cmdletBinding()]
    param()
    $schemaMaster = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest().SchemaRoleOwner
    $schemaDN = ([ADSI]"LDAP://$($schemaMaster)/RootDSE").schemaNamingContext
    $storageGroupSchemaEntryDN = "LDAP://$($schemaMaster)/CN=ms-Exch-Storage-Group,$schemaDN"
    if (-not ([System.DirectoryServices.DirectoryEntry]::Exists($storageGroupSchemaEntryDN))) {
        return [PSCustomObject] @{
            "Vulnerable"         = $false
            "Status"             = "Exchange was not installed in this forest. Therefore, CVE-2021-34470 vulnerability is not present."
            "HasUnexpectedValue" = $false
            'Superior'           = $null
        }
    }

    $storageGroupSchemaEntry = [ADSI]($storageGroupSchemaEntryDN)
    if ($storageGroupSchemaEntry.Properties["possSuperiors"].Count -eq 0) {
        return [PSCustomObject] @{
            "Vulnerable"         = $false
            "Status"             = "CVE-2021-34470 vulnerability is not present."
            "HasUnexpectedValue" = $false
            'Superior'           = $null
        }
    }
    foreach ($val in $storageGroupSchemaEntry.Properties["possSuperiors"]) {
        if ($val -eq "computer") {
            return [PSCustomObject] @{
                "Vulnerable"         = $true
                "Status"             = "CVE-2021-34470 vulnerability is present."
                "HasUnexpectedValue" = $false
                'Superior'           = $null
            }
        } else {
            return [PSCustomObject] @{
                "Vulnerable"         = $true
                "Status"             = "CVE-2021-34470 vulnerability may be present due to an unexpected superior: $val"
                "HasUnexpectedValue" = $true
                "Superior"           = $val
            }
        }
    }
}
function Update-LastLogonTimestamp {
    <#
    .SYNOPSIS
    Uses Kerberos to impersonate a user and update the LastLogonTimestamp attribute
 
    .DESCRIPTION
    Uses Kerberos to impersonate a user and update the LastLogonTimestamp attribute
    It's a trick to last logon time updated without actually logging in
 
    .PARAMETER Identity
    The identity of the user to impersonate
 
    .EXAMPLE
    Update-LastLogonTimestamp -UserName 'PUID'
 
    .EXAMPLE
    Update-LastLogonTimestamp -UserName 'PUID@ad.evotec.xyz'
 
    .NOTES
    The lastLogontimeStamp attribute is not updated every time a user or computer logs on to the domain.
    The decision to update the value is based on the current date minus the value of the ( ms-DS-Logon-Time-Sync-Interval attribute minus a random percentage of 5).
    If the result is equal to or greater than lastLogontimeStamp the attribute is updated.
 
    If your Domain Admin is in Protected Users you may need to remove it from there to make it work
    #>

    [CmdletBinding(SupportsShouldProcess)]
    param (
        [parameter(Position = 0, Mandatory)][alias('UserName')][string]$Identity
    )

    begin {
        $impersonatedContext = $null
        $impersonationSuccessful = $false
        $ErrorMessage = $null
    }
    process {
        try {
            $windowsIdentity = [System.Security.Principal.WindowsIdentity]::new($Identity)
        } catch {
            Write-Warning "Update-LastLogonTimestamp - Failed to create WindowsIdentity for $Identity - $($_.Exception.Message)"
            $windowsIdentity = $null
            $ErrorMessage = $_.Exception.Message
        }
        if ($windowsIdentity) {
            try {
                if ($PSCmdlet.ShouldProcess("Impersonating user - $Identity")) {
                    Write-Verbose "Update-LastLogonTimestamp - Impersonating user - $Identity"
                    $impersonatedContext = $windowsIdentity.Impersonate()
                    $impersonationSuccessful = $true
                }
            } catch {
                Write-Warning "Update-LastLogonTimestamp - Failed to impersonate user $Identity - $($_.Exception.Message)"
                $impersonationSuccessful = $false
                $ErrorMessage = $_.Exception.Message
            } finally {
                if ($impersonatedContext) {
                    $impersonatedContext.Undo()
                }
            }
        }
    }
    end {
        [PSCustomObject] @{
            Identity                = $Identity
            WhatIf                  = $WhatIfPreference.ispresent
            UserName                = $windowsIdentity.Name
            ImpersonationSuccessful = $impersonationSuccessful
            ErrorMessage            = $ErrorMessage
            WindowsIdentity         = $windowsIdentity
        }
    }
}


$ModuleFunctions = @{
    ActiveDirectory = @{
        'Add-ADACL'                                  = ''
        'Copy-ADOUSecurity'                          = ''
        'New-ADACLObject'                            = ''
        'Enable-ADACLInheritance'                    = ''
        'Disable-ADACLInheritance'                   = ''
        'Export-ADACLObject'                         = ''
        'Get-ADACL'                                  = ''
        'Get-ADACLOwner'                             = ''
        'Get-WinADACLConfiguration'                  = ''
        'Get-WinADACLForest'                         = ''
        'Get-WinADBitlockerLapsSummary'              = ''
        'Get-WinADComputerACLLAPS'                   = ''
        'Get-WinADComputers'                         = ''
        'Get-WinADDelegatedAccounts'                 = ''
        'Get-WinADDFSHealth'                         = ''
        'Get-WinADDHCP'                              = ''
        'Get-WinADDiagnostics'                       = ''
        'Get-WinADDuplicateObject'                   = 'Get-WinADForestObjectsConflict'
        'Get-WinADDuplicateSPN'                      = ''
        'Get-WinADForestControllerInformation'       = ''
        'Get-WinADForestOptionalFeatures'            = ''
        'Get-WinADForestReplication'                 = ''
        'Get-WinADForestRoles'                       = 'Get-WinADRoles', 'Get-WinADDomainRoles'
        'Get-WinADForestSchemaProperties'            = ''
        'Get-WinADForestSites'                       = ''
        'Get-WinADForestSubnet'                      = 'Get-WinADSubnet'
        'Get-WinADLastBackup'                        = ''
        'Get-WinADLDAPBindingsSummary'               = ''
        'Get-WinADLMSettings'                        = ''
        'Get-WinADPrivilegedObjects'                 = 'Get-WinADPriviligedObjects'
        'Get-WinADProtocol'                          = ''
        'Get-WinADProxyAddresses'                    = ''
        'Get-WinADServiceAccount'                    = ''
        'Get-WinADSharePermission'                   = ''
        'Get-WinADSiteConnections'                   = ''
        'Get-WinADSiteLinks'                         = ''
        'Get-WinADTomebstoneLifetime'                = ''
        'Get-WinADTrustLegacy'                       = ''
        'Get-WinADUserPrincipalName'                 = ''
        'Get-WinADUsers'                             = ''
        'Get-WinADUsersForeignSecurityPrincipalList' = 'Get-WinADUsersFP'
        'Get-WinADWellKnownFolders'                  = ''
        'Get-WinADPasswordPolicy'                    = ''
        'Invoke-ADEssentials'                        = ''
        'Remove-ADACL'                               = ''
        'Remove-WinADDuplicateObject'                = ''
        'Remove-WinADSharePermission'                = ''
        'Rename-WinADUserPrincipalName'              = ''
        'Repair-WinADACLConfigurationOwner'          = ''
        'Repair-WinADEmailAddress'                   = ''
        'Repair-WinADForestControllerInformation'    = ''
        'Set-ADACLOwner'                             = ''
        'Set-DnsServerIP'                            = 'Set-WinDNSServerIP'
        'Set-WinADDiagnostics'                       = ''
        'Set-WinADReplication'                       = ''
        'Set-WinADReplicationConnections'            = ''
        'Set-WinADShare'                             = ''
        'Set-WinADTombstoneLifetime'                 = ''
        'Show-WinADGroupCritical'                    = 'Show-WinADCriticalGroups'
        'Show-WinADOrganization'                     = ''
        'Show-WinADSites'                            = ''
        'Show-WinADUserSecurity'                     = ''
        'Sync-DomainController'                      = ''
        'Test-ADDomainController'                    = ''
        'Test-ADRolesAvailability'                   = ''
        'Test-ADSiteLinks'                           = ''
        'Test-DNSNameServers'                        = ''
        'Test-FSMORolesAvailability'                 = ''
        'Test-LDAP'                                  = ''
        'Get-WinDNSZones'                            = ''
        'Get-WinDNSIPAddresses'                      = ''
        'Find-WinADObjectDifference'                 = ''
        'Show-WinADObjectDifference'                 = ''
        'Test-WinADDNSResolving'                     = ''
        'Get-WinADDomainControllerGenerationId'      = ''
        'Compare-WinADGlobalCatalogObjects'          = ''
        'Test-WinADObjectReplicationStatus'          = ''
        'Get-WinADSiteCoverage'                      = ''
    }
    DHCPServer      = @{
        'Get-WinADDHCP' = ''
    }
    DNSServer       = @{
        'Get-WinADDnsInformation'      = ''
        'Get-WinADDNSIPAddresses'      = 'Get-WinDnsIPAddresses'
        'Get-WinADDNSRecords'          = 'Get-WinDNSRecords'
        'Get-WinADDnsServerForwarder'  = ''
        'Get-WinADDnsServerScavenging' = ''
        'Get-WinADDnsServerZones'      = ''
        'Get-WinADDnsZones'            = 'Get-WinDNSZones'
        'Remove-WinADDnsRecord'        = ''
    }
}
[Array] $FunctionsAll = 'Add-ADACL', 'Compare-PingCastleReport', 'Compare-WinADGlobalCatalogObjects', 'Copy-ADOUSecurity', 'Disable-ADACLInheritance', 'Enable-ADACLInheritance', 'Export-ADACLObject', 'Find-WinADObjectDifference', 'Get-ADACL', 'Get-ADACLOwner', 'Get-ADWinDnsServerZones', 'Get-DNSServerIP', 'Get-PingCastleReport', 'Get-WinADACLConfiguration', 'Get-WinADACLForest', 'Get-WinADBitlockerLapsSummary', 'Get-WinADComputerACLLAPS', 'Get-WinADComputers', 'Get-WinADDelegatedAccounts', 'Get-WinADDFSHealth', 'Get-WinADDFSTopology', 'Get-WinADDHCP', 'Get-WinADDiagnostics', 'Get-WinADDnsInformation', 'Get-WinADDnsIPAddresses', 'Get-WinADDnsRecords', 'Get-WinADDnsServerForwarder', 'Get-WinADDnsServerScavenging', 'Get-WinADDNSZones', 'Get-WinADDomain', 'Get-WinADDomainControllerGenerationId', 'Get-WinADDomainControllerNetLogonSettings', 'Get-WinADDomainControllerNTDSSettings', 'Get-WinADDomainControllerOption', 'Get-WinADDuplicateObject', 'Get-WinADDuplicateSPN', 'Get-WinADForest', 'Get-WinADForestControllerInformation', 'Get-WinADForestOptionalFeatures', 'Get-WinADForestReplication', 'Get-WinADForestReplicationSummary', 'Get-WinADForestRoles', 'Get-WinADForestSchemaProperties', 'Get-WinADForestSites', 'Get-WinADForestSubnet', 'Get-WinADGroupMember', 'Get-WinADGroupMemberOf', 'Get-WinADGroups', 'Get-WinADKerberosAccount', 'Get-WinADLastBackup', 'Get-WinADLDAPBindingsSummary', 'Get-WinADLMSettings', 'Get-WinADObject', 'Get-WinADPasswordPolicy', 'Get-WinADPrivilegedObjects', 'Get-WinADProtocol', 'Get-WinADProxyAddresses', 'Get-WinADServiceAccount', 'Get-WinADSharePermission', 'Get-WinADSiteConnections', 'Get-WinADSiteCoverage', 'Get-WinADSiteLinks', 'Get-WinADSiteOptions', 'Get-WinADTombstoneLifetime', 'Get-WinADTrust', 'Get-WinADTrustLegacy', 'Get-WinADUserPrincipalName', 'Get-WinADUsers', 'Get-WinADUsersForeignSecurityPrincipalList', 'Get-WinADWellKnownFolders', 'Invoke-ADEssentials', 'Invoke-PingCastle', 'New-ADACLObject', 'New-ADSite', 'Remove-ADACL', 'Remove-WinADDFSTopology', 'Remove-WinADDuplicateObject', 'Remove-WinADSharePermission', 'Rename-WinADUserPrincipalName', 'Repair-WinADACLConfigurationOwner', 'Repair-WinADEmailAddress', 'Repair-WinADForestControllerInformation', 'Request-ChangePasswordAtLogon', 'Request-DisableOnAccountExpiration', 'Restore-ADACLDefault', 'Set-ADACL', 'Set-ADACLInheritance', 'Set-ADACLOwner', 'Set-DnsServerIP', 'Set-WinADDiagnostics', 'Set-WinADDomainControllerNetLogonSettings', 'Set-WinADDomainControllerOption', 'Set-WinADForestACLOwner', 'Set-WinADReplication', 'Set-WinADReplicationConnections', 'Set-WinADShare', 'Set-WinADTombstoneLifetime', 'Show-WinADDNSRecords', 'Show-WinADGroupCritical', 'Show-WinADGroupMember', 'Show-WinADGroupMemberOf', 'Show-WinADKerberosAccount', 'Show-WinADObjectDifference', 'Show-WinADOrganization', 'Show-WinADSites', 'Show-WinADTrust', 'Show-WinADUserSecurity', 'Sync-WinADDomainController', 'Test-ADDomainController', 'Test-ADRolesAvailability', 'Test-ADSiteLinks', 'Test-DNSNameServers', 'Test-FSMORolesAvailability', 'Test-LDAP', 'Test-WinADDNSResolving', 'Test-WinADObjectReplicationStatus', 'Test-WinADVulnerableSchemaClass', 'Update-LastLogonTimestamp'
[Array] $AliasesAll = 'Get-WinADDomainRoles', 'Get-WinADForestObjectsConflict', 'Get-WinADForestTombstoneLifetime', 'Get-WinADPriviligedObjects', 'Get-WinADRoles', 'Get-WinADSubnet', 'Get-WinADTrusts', 'Get-WinADUsersFP', 'Get-WinDnsIPAddresses', 'Get-WinDNSRecords', 'Get-WinDNSServerIP', 'Get-WinDnsServerZones', 'Get-WinDNSZones', 'Set-WinDNSServerIP', 'Show-ADGroupMember', 'Show-ADGroupMemberOf', 'Show-ADTrust', 'Show-ADTrusts', 'Show-WinADCriticalGroups', 'Show-WinADTrusts', 'Sync-DomainController'
$AliasesToRemove = [System.Collections.Generic.List[string]]::new()
$FunctionsToRemove = [System.Collections.Generic.List[string]]::new()
foreach ($Module in $ModuleFunctions.Keys) {
    try {
        Import-Module -Name $Module -ErrorAction Stop
    } catch {
        foreach ($Function in $ModuleFunctions[$Module].Keys) {
            $FunctionsToRemove.Add($Function)
            $ModuleFunctions[$Module][$Function] | ForEach-Object {
                if ($_) {
                    $AliasesToRemove.Add($_)
                }
            }
        }
    }
}
$FunctionsToLoad = foreach ($Function in $FunctionsAll) {
    if ($Function -notin $FunctionsToRemove) {
        $Function
    }
}
$AliasesToLoad = foreach ($Alias in $AliasesAll) {
    if ($Alias -notin $AliasesToRemove) {
        $Alias
    }
}
# Export functions and aliases as required
Export-ModuleMember -Function @($FunctionsToLoad) -Alias @($AliasesToLoad)
# SIG # Begin signature block
# MIItsQYJKoZIhvcNAQcCoIItojCCLZ4CAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCClt+H2zpprUPvi
# 0VHnHqJFcyVaP2ylWGOUFIGhCWUjQKCCJrQwggWNMIIEdaADAgECAhAOmxiO+dAt
# 5+/bUOIIQBhaMA0GCSqGSIb3DQEBDAUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0yMjA4MDEwMDAwMDBa
# Fw0zMTExMDkyMzU5NTlaMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lD
# ZXJ0IFRydXN0ZWQgUm9vdCBHNDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoC
# ggIBAL/mkHNo3rvkXUo8MCIwaTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3E
# MB/zG6Q4FutWxpdtHauyefLKEdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKy
# unWZanMylNEQRBAu34LzB4TmdDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsF
# xl7sWxq868nPzaw0QF+xembud8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU1
# 5zHL2pNe3I6PgNq2kZhAkHnDeMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJB
# MtfbBHMqbpEBfCFM1LyuGwN1XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObUR
# WBf3JFxGj2T3wWmIdph2PVldQnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6
# nj3cAORFJYm2mkQZK37AlLTSYW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxB
# YKqxYxhElRp2Yn72gLD76GSmM9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5S
# UUd0viastkF13nqsX40/ybzTQRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+x
# q4aLT8LWRV+dIPyhHsXAj6KxfgommfXkaS+YHS312amyHeUbAgMBAAGjggE6MIIB
# NjAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwP
# TzAfBgNVHSMEGDAWgBRF66Kv9JLLgjEtUYunpyGd823IDzAOBgNVHQ8BAf8EBAMC
# AYYweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdp
# Y2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNv
# bS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwRQYDVR0fBD4wPDA6oDigNoY0
# aHR0cDovL2NybDMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJlZElEUm9vdENB
# LmNybDARBgNVHSAECjAIMAYGBFUdIAAwDQYJKoZIhvcNAQEMBQADggEBAHCgv0Nc
# Vec4X6CjdBs9thbX979XB72arKGHLOyFXqkauyL4hxppVCLtpIh3bb0aFPQTSnov
# Lbc47/T/gLn4offyct4kvFIDyE7QKt76LVbP+fT3rDB6mouyXtTP0UNEm0Mh65Zy
# oUi0mcudT6cGAxN3J0TU53/oWajwvy8LpunyNDzs9wPHh6jSTEAZNUZqaVSwuKFW
# juyk1T3osdz9HNj0d1pcVIxv76FQPfx2CWiEn2/K2yCNNWAcAgPLILCsWKAOQGPF
# mCLBsln1VWvPJ6tsds5vIy30fnFqI2si/xK4VC0nftg62fC2h5b9W9FcrBjDTZ9z
# twGpn1eqXijiuZQwggWQMIIDeKADAgECAhAFmxtXno4hMuI5B72nd3VcMA0GCSqG
# SIb3DQEBDAUAMGIxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMx
# GTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRy
# dXN0ZWQgUm9vdCBHNDAeFw0xMzA4MDExMjAwMDBaFw0zODAxMTUxMjAwMDBaMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAL/mkHNo3rvkXUo8MCIw
# aTPswqclLskhPfKK2FnC4SmnPVirdprNrnsbhA3EMB/zG6Q4FutWxpdtHauyefLK
# EdLkX9YFPFIPUh/GnhWlfr6fqVcWWVVyr2iTcMKyunWZanMylNEQRBAu34LzB4Tm
# dDttceItDBvuINXJIB1jKS3O7F5OyJP4IWGbNOsFxl7sWxq868nPzaw0QF+xembu
# d8hIqGZXV59UWI4MK7dPpzDZVu7Ke13jrclPXuU15zHL2pNe3I6PgNq2kZhAkHnD
# eMe2scS1ahg4AxCN2NQ3pC4FfYj1gj4QkXCrVYJBMtfbBHMqbpEBfCFM1LyuGwN1
# XXhm2ToxRJozQL8I11pJpMLmqaBn3aQnvKFPObURWBf3JFxGj2T3wWmIdph2PVld
# QnaHiZdpekjw4KISG2aadMreSx7nDmOu5tTvkpI6nj3cAORFJYm2mkQZK37AlLTS
# YW3rM9nF30sEAMx9HJXDj/chsrIRt7t/8tWMcCxBYKqxYxhElRp2Yn72gLD76GSm
# M9GJB+G9t+ZDpBi4pncB4Q+UDCEdslQpJYls5Q5SUUd0viastkF13nqsX40/ybzT
# QRESW+UQUOsxxcpyFiIJ33xMdT9j7CFfxCBRa2+xq4aLT8LWRV+dIPyhHsXAj6Kx
# fgommfXkaS+YHS312amyHeUbAgMBAAGjQjBAMA8GA1UdEwEB/wQFMAMBAf8wDgYD
# VR0PAQH/BAQDAgGGMB0GA1UdDgQWBBTs1+OC0nFdZEzfLmc/57qYrhwPTzANBgkq
# hkiG9w0BAQwFAAOCAgEAu2HZfalsvhfEkRvDoaIAjeNkaA9Wz3eucPn9mkqZucl4
# XAwMX+TmFClWCzZJXURj4K2clhhmGyMNPXnpbWvWVPjSPMFDQK4dUPVS/JA7u5iZ
# aWvHwaeoaKQn3J35J64whbn2Z006Po9ZOSJTROvIXQPK7VB6fWIhCoDIc2bRoAVg
# X+iltKevqPdtNZx8WorWojiZ83iL9E3SIAveBO6Mm0eBcg3AFDLvMFkuruBx8lbk
# apdvklBtlo1oepqyNhR6BvIkuQkRUNcIsbiJeoQjYUIp5aPNoiBB19GcZNnqJqGL
# FNdMGbJQQXE9P01wI4YMStyB0swylIQNCAmXHE/A7msgdDDS4Dk0EIUhFQEI6FUy
# 3nFJ2SgXUE3mvk3RdazQyvtBuEOlqtPDBURPLDab4vriRbgjU2wGb2dVf0a1TD9u
# KFp5JtKkqGKX0h7i7UqLvBv9R0oN32dmfrJbQdA75PQ79ARj6e/CVABRoIoqyc54
# zNXqhwQYs86vSYiv85KZtrPmYQ/ShQDnUBrkG5WdGaG5nLGbsQAe79APT0JsyQq8
# 7kP6OnGlyE0mpTX9iV28hWIdMtKgK1TtmlfB2/oQzxm3i0objwG2J5VT6LaJbVu8
# aNQj6ItRolb58KaAoNYes7wPD1N1KarqE3fk3oyBIa0HEEcRrYc9B9F1vM/zZn4w
# ggauMIIElqADAgECAhAHNje3JFR82Ees/ShmKl5bMA0GCSqGSIb3DQEBCwUAMGIx
# CzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3
# dy5kaWdpY2VydC5jb20xITAfBgNVBAMTGERpZ2lDZXJ0IFRydXN0ZWQgUm9vdCBH
# NDAeFw0yMjAzMjMwMDAwMDBaFw0zNzAzMjIyMzU5NTlaMGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwggIiMA0GCSqG
# SIb3DQEBAQUAA4ICDwAwggIKAoICAQDGhjUGSbPBPXJJUVXHJQPE8pE3qZdRodbS
# g9GeTKJtoLDMg/la9hGhRBVCX6SI82j6ffOciQt/nR+eDzMfUBMLJnOWbfhXqAJ9
# /UO0hNoR8XOxs+4rgISKIhjf69o9xBd/qxkrPkLcZ47qUT3w1lbU5ygt69OxtXXn
# HwZljZQp09nsad/ZkIdGAHvbREGJ3HxqV3rwN3mfXazL6IRktFLydkf3YYMZ3V+0
# VAshaG43IbtArF+y3kp9zvU5EmfvDqVjbOSmxR3NNg1c1eYbqMFkdECnwHLFuk4f
# sbVYTXn+149zk6wsOeKlSNbwsDETqVcplicu9Yemj052FVUmcJgmf6AaRyBD40Nj
# gHt1biclkJg6OBGz9vae5jtb7IHeIhTZgirHkr+g3uM+onP65x9abJTyUpURK1h0
# QCirc0PO30qhHGs4xSnzyqqWc0Jon7ZGs506o9UD4L/wojzKQtwYSH8UNM/STKvv
# mz3+DrhkKvp1KCRB7UK/BZxmSVJQ9FHzNklNiyDSLFc1eSuo80VgvCONWPfcYd6T
# /jnA+bIwpUzX6ZhKWD7TA4j+s4/TXkt2ElGTyYwMO1uKIqjBJgj5FBASA31fI7tk
# 42PgpuE+9sJ0sj8eCXbsq11GdeJgo1gJASgADoRU7s7pXcheMBK9Rp6103a50g5r
# mQzSM7TNsQIDAQABo4IBXTCCAVkwEgYDVR0TAQH/BAgwBgEB/wIBADAdBgNVHQ4E
# FgQUuhbZbU2FL3MpdpovdYxqII+eyG8wHwYDVR0jBBgwFoAU7NfjgtJxXWRM3y5n
# P+e6mK4cD08wDgYDVR0PAQH/BAQDAgGGMBMGA1UdJQQMMAoGCCsGAQUFBwMIMHcG
# CCsGAQUFBwEBBGswaTAkBggrBgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQu
# Y29tMEEGCCsGAQUFBzAChjVodHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGln
# aUNlcnRUcnVzdGVkUm9vdEc0LmNydDBDBgNVHR8EPDA6MDigNqA0hjJodHRwOi8v
# Y3JsMy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkUm9vdEc0LmNybDAgBgNV
# HSAEGTAXMAgGBmeBDAEEAjALBglghkgBhv1sBwEwDQYJKoZIhvcNAQELBQADggIB
# AH1ZjsCTtm+YqUQiAX5m1tghQuGwGC4QTRPPMFPOvxj7x1Bd4ksp+3CKDaopafxp
# wc8dB+k+YMjYC+VcW9dth/qEICU0MWfNthKWb8RQTGIdDAiCqBa9qVbPFXONASIl
# zpVpP0d3+3J0FNf/q0+KLHqrhc1DX+1gtqpPkWaeLJ7giqzl/Yy8ZCaHbJK9nXzQ
# cAp876i8dU+6WvepELJd6f8oVInw1YpxdmXazPByoyP6wCeCRK6ZJxurJB4mwbfe
# Kuv2nrF5mYGjVoarCkXJ38SNoOeY+/umnXKvxMfBwWpx2cYTgAnEtp/Nh4cku0+j
# Sbl3ZpHxcpzpSwJSpzd+k1OsOx0ISQ+UzTl63f8lY5knLD0/a6fxZsNBzU+2QJsh
# IUDQtxMkzdwdeDrknq3lNHGS1yZr5Dhzq6YBT70/O3itTK37xJV77QpfMzmHQXh6
# OOmc4d0j/R0o08f56PGYX/sr2H7yRp11LB4nLCbbbxV7HhmLNriT1ObyF5lZynDw
# N7+YAN8gFk8n+2BnFqFmut1VwDophrCYoCvtlUG3OtUVmDG0YgkPCr2B2RP+v6TR
# 81fZvAT6gt4y3wSJ8ADNXcL50CN/AAvkdgIm2fBldkKmKYcJRyvmfxqkhQ/8mJb2
# VVQrH4D6wPIOK+XW+6kvRBVK5xMOHds3OBqhK/bt1nz8MIIGsDCCBJigAwIBAgIQ
# CK1AsmDSnEyfXs2pvZOu2TANBgkqhkiG9w0BAQwFADBiMQswCQYDVQQGEwJVUzEV
# MBMGA1UEChMMRGlnaUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29t
# MSEwHwYDVQQDExhEaWdpQ2VydCBUcnVzdGVkIFJvb3QgRzQwHhcNMjEwNDI5MDAw
# MDAwWhcNMzYwNDI4MjM1OTU5WjBpMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQgQ29kZSBT
# aWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMIICIjANBgkqhkiG9w0BAQEF
# AAOCAg8AMIICCgKCAgEA1bQvQtAorXi3XdU5WRuxiEL1M4zrPYGXcMW7xIUmMJ+k
# jmjYXPXrNCQH4UtP03hD9BfXHtr50tVnGlJPDqFX/IiZwZHMgQM+TXAkZLON4gh9
# NH1MgFcSa0OamfLFOx/y78tHWhOmTLMBICXzENOLsvsI8IrgnQnAZaf6mIBJNYc9
# URnokCF4RS6hnyzhGMIazMXuk0lwQjKP+8bqHPNlaJGiTUyCEUhSaN4QvRRXXegY
# E2XFf7JPhSxIpFaENdb5LpyqABXRN/4aBpTCfMjqGzLmysL0p6MDDnSlrzm2q2AS
# 4+jWufcx4dyt5Big2MEjR0ezoQ9uo6ttmAaDG7dqZy3SvUQakhCBj7A7CdfHmzJa
# wv9qYFSLScGT7eG0XOBv6yb5jNWy+TgQ5urOkfW+0/tvk2E0XLyTRSiDNipmKF+w
# c86LJiUGsoPUXPYVGUztYuBeM/Lo6OwKp7ADK5GyNnm+960IHnWmZcy740hQ83eR
# Gv7bUKJGyGFYmPV8AhY8gyitOYbs1LcNU9D4R+Z1MI3sMJN2FKZbS110YU0/EpF2
# 3r9Yy3IQKUHw1cVtJnZoEUETWJrcJisB9IlNWdt4z4FKPkBHX8mBUHOFECMhWWCK
# ZFTBzCEa6DgZfGYczXg4RTCZT/9jT0y7qg0IU0F8WD1Hs/q27IwyCQLMbDwMVhEC
# AwEAAaOCAVkwggFVMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFGg34Ou2
# O/hfEYb7/mF7CIhl9E5CMB8GA1UdIwQYMBaAFOzX44LScV1kTN8uZz/nupiuHA9P
# MA4GA1UdDwEB/wQEAwIBhjATBgNVHSUEDDAKBggrBgEFBQcDAzB3BggrBgEFBQcB
# AQRrMGkwJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBBBggr
# BgEFBQcwAoY1aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0VHJ1
# c3RlZFJvb3RHNC5jcnQwQwYDVR0fBDwwOjA4oDagNIYyaHR0cDovL2NybDMuZGln
# aWNlcnQuY29tL0RpZ2lDZXJ0VHJ1c3RlZFJvb3RHNC5jcmwwHAYDVR0gBBUwEzAH
# BgVngQwBAzAIBgZngQwBBAEwDQYJKoZIhvcNAQEMBQADggIBADojRD2NCHbuj7w6
# mdNW4AIapfhINPMstuZ0ZveUcrEAyq9sMCcTEp6QRJ9L/Z6jfCbVN7w6XUhtldU/
# SfQnuxaBRVD9nL22heB2fjdxyyL3WqqQz/WTauPrINHVUHmImoqKwba9oUgYftzY
# gBoRGRjNYZmBVvbJ43bnxOQbX0P4PpT/djk9ntSZz0rdKOtfJqGVWEjVGv7XJz/9
# kNF2ht0csGBc8w2o7uCJob054ThO2m67Np375SFTWsPK6Wrxoj7bQ7gzyE84FJKZ
# 9d3OVG3ZXQIUH0AzfAPilbLCIXVzUstG2MQ0HKKlS43Nb3Y3LIU/Gs4m6Ri+kAew
# Q3+ViCCCcPDMyu/9KTVcH4k4Vfc3iosJocsL6TEa/y4ZXDlx4b6cpwoG1iZnt5Lm
# Tl/eeqxJzy6kdJKt2zyknIYf48FWGysj/4+16oh7cGvmoLr9Oj9FpsToFpFSi0HA
# SIRLlk2rREDjjfAVKM7t8RhWByovEMQMCGQ8M4+uKIw8y4+ICw2/O/TOHnuO77Xr
# y7fwdxPm5yg/rBKupS8ibEH5glwVZsxsDsrFhsP2JjMMB0ug0wcCampAMEhLNKhR
# ILutG4UI4lkNbcoFUCvqShyepf2gpx8GdOfy1lKQ/a+FSCH5Vzu0nAPthkX0tGFu
# v2jiJmCG6sivqf6UHedjGzqGVnhOMIIGwjCCBKqgAwIBAgIQBUSv85SdCDmmv9s/
# X+VhFjANBgkqhkiG9w0BAQsFADBjMQswCQYDVQQGEwJVUzEXMBUGA1UEChMORGln
# aUNlcnQsIEluYy4xOzA5BgNVBAMTMkRpZ2lDZXJ0IFRydXN0ZWQgRzQgUlNBNDA5
# NiBTSEEyNTYgVGltZVN0YW1waW5nIENBMB4XDTIzMDcxNDAwMDAwMFoXDTM0MTAx
# MzIzNTk1OVowSDELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJbmMu
# MSAwHgYDVQQDExdEaWdpQ2VydCBUaW1lc3RhbXAgMjAyMzCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAKNTRYcdg45brD5UsyPgz5/X5dLnXaEOCdwvSKOX
# ejsqnGfcYhVYwamTEafNqrJq3RApih5iY2nTWJw1cb86l+uUUI8cIOrHmjsvlmbj
# aedp/lvD1isgHMGXlLSlUIHyz8sHpjBoyoNC2vx/CSSUpIIa2mq62DvKXd4ZGIX7
# ReoNYWyd/nFexAaaPPDFLnkPG2ZS48jWPl/aQ9OE9dDH9kgtXkV1lnX+3RChG4PB
# uOZSlbVH13gpOWvgeFmX40QrStWVzu8IF+qCZE3/I+PKhu60pCFkcOvV5aDaY7Mu
# 6QXuqvYk9R28mxyyt1/f8O52fTGZZUdVnUokL6wrl76f5P17cz4y7lI0+9S769Sg
# LDSb495uZBkHNwGRDxy1Uc2qTGaDiGhiu7xBG3gZbeTZD+BYQfvYsSzhUa+0rRUG
# FOpiCBPTaR58ZE2dD9/O0V6MqqtQFcmzyrzXxDtoRKOlO0L9c33u3Qr/eTQQfqZc
# ClhMAD6FaXXHg2TWdc2PEnZWpST618RrIbroHzSYLzrqawGw9/sqhux7UjipmAmh
# cbJsca8+uG+W1eEQE/5hRwqM/vC2x9XH3mwk8L9CgsqgcT2ckpMEtGlwJw1Pt7U2
# 0clfCKRwo+wK8REuZODLIivK8SgTIUlRfgZm0zu++uuRONhRB8qUt+JQofM604qD
# y0B7AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/BAIwADAW
# BgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEEAjALBglg
# hkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8wHQYDVR0O
# BBYEFKW27xPn783QZKHVVqllMaPe1eNJMFoGA1UdHwRTMFEwT6BNoEuGSWh0dHA6
# Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZTSEEy
# NTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQGCCsGAQUF
# BzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKGTGh0dHA6
# Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQwOTZT
# SEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIBAIEa1t6g
# qbWYF7xwjU+KPGic2CX/yyzkzepdIpLsjCICqbjPgKjZ5+PF7SaCinEvGN1Ott5s
# 1+FgnCvt7T1IjrhrunxdvcJhN2hJd6PrkKoS1yeF844ektrCQDifXcigLiV4JZ0q
# BXqEKZi2V3mP2yZWK7Dzp703DNiYdk9WuVLCtp04qYHnbUFcjGnRuSvExnvPnPp4
# 4pMadqJpddNQ5EQSviANnqlE0PjlSXcIWiHFtM+YlRpUurm8wWkZus8W8oM3NG6w
# QSbd3lqXTzON1I13fXVFoaVYJmoDRd7ZULVQjK9WvUzF4UbFKNOt50MAcN7MmJ4Z
# iQPq1JE3701S88lgIcRWR+3aEUuMMsOI5ljitts++V+wQtaP4xeR0arAVeOGv6wn
# LEHQmjNKqDbUuXKWfpd5OEhfysLcPTLfddY2Z1qJ+Panx+VPNTwAvb6cKmx5Adza
# ROY63jg7B145WPR8czFVoIARyxQMfq68/qTreWWqaNYiyjvrmoI1VygWy2nyMpqy
# 0tg6uLFGhmu6F/3Ed2wVbK6rr3M66ElGt9V/zLY4wNjsHPW2obhDLN9OTH0eaHDA
# dwrUAuBcYLso/zjlUlrWrBciI0707NMX+1Br/wd3H3GXREHJuEbTbDJ8WC9nR2Xl
# G3O2mflrLAZG70Ee8PBf4NvZrZCARK+AEEGKMIIHXzCCBUegAwIBAgIQB8JSdCgU
# otar/iTqF+XdLjANBgkqhkiG9w0BAQsFADBpMQswCQYDVQQGEwJVUzEXMBUGA1UE
# ChMORGlnaUNlcnQsIEluYy4xQTA/BgNVBAMTOERpZ2lDZXJ0IFRydXN0ZWQgRzQg
# Q29kZSBTaWduaW5nIFJTQTQwOTYgU0hBMzg0IDIwMjEgQ0ExMB4XDTIzMDQxNjAw
# MDAwMFoXDTI2MDcwNjIzNTk1OVowZzELMAkGA1UEBhMCUEwxEjAQBgNVBAcMCU1p
# a2/FgsOzdzEhMB8GA1UECgwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMSEwHwYD
# VQQDDBhQcnplbXlzxYJhdyBLxYJ5cyBFVk9URUMwggIiMA0GCSqGSIb3DQEBAQUA
# A4ICDwAwggIKAoICAQCUmgeXMQtIaKaSkKvbAt8GFZJ1ywOH8SwxlTus4McyrWmV
# OrRBVRQA8ApF9FaeobwmkZxvkxQTFLHKm+8knwomEUslca8CqSOI0YwELv5EwTVE
# h0C/Daehvxo6tkmNPF9/SP1KC3c0l1vO+M7vdNVGKQIQrhxq7EG0iezBZOAiukNd
# GVXRYOLn47V3qL5PwG/ou2alJ/vifIDad81qFb+QkUh02Jo24SMjWdKDytdrMXi0
# 235CN4RrW+8gjfRJ+fKKjgMImbuceCsi9Iv1a66bUc9anAemObT4mF5U/yQBgAuA
# o3+jVB8wiUd87kUQO0zJCF8vq2YrVOz8OJmMX8ggIsEEUZ3CZKD0hVc3dm7cWSAw
# 8/FNzGNPlAaIxzXX9qeD0EgaCLRkItA3t3eQW+IAXyS/9ZnnpFUoDvQGbK+Q4/bP
# 0ib98XLfQpxVGRu0cCV0Ng77DIkRF+IyR1PcwVAq+OzVU3vKeo25v/rntiXCmCxi
# W4oHYO28eSQ/eIAcnii+3uKDNZrI15P7VxDrkUIc6FtiSvOhwc3AzY+vEfivUkFK
# RqwvSSr4fCrrkk7z2Qe72Zwlw2EDRVHyy0fUVGO9QMuh6E3RwnJL96ip0alcmhKA
# BGoIqSW05nXdCUbkXmhPCTT5naQDuZ1UkAXbZPShKjbPwzdXP2b8I9nQ89VSgQID
# AQABo4ICAzCCAf8wHwYDVR0jBBgwFoAUaDfg67Y7+F8Rhvv+YXsIiGX0TkIwHQYD
# VR0OBBYEFHrxaiVZuDJxxEk15bLoMuFI5233MA4GA1UdDwEB/wQEAwIHgDATBgNV
# HSUEDDAKBggrBgEFBQcDAzCBtQYDVR0fBIGtMIGqMFOgUaBPhk1odHRwOi8vY3Js
# My5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2RlU2lnbmluZ1JTQTQw
# OTZTSEEzODQyMDIxQ0ExLmNybDBToFGgT4ZNaHR0cDovL2NybDQuZGlnaWNlcnQu
# Y29tL0RpZ2lDZXJ0VHJ1c3RlZEc0Q29kZVNpZ25pbmdSU0E0MDk2U0hBMzg0MjAy
# MUNBMS5jcmwwPgYDVR0gBDcwNTAzBgZngQwBBAEwKTAnBggrBgEFBQcCARYbaHR0
# cDovL3d3dy5kaWdpY2VydC5jb20vQ1BTMIGUBggrBgEFBQcBAQSBhzCBhDAkBggr
# BgEFBQcwAYYYaHR0cDovL29jc3AuZGlnaWNlcnQuY29tMFwGCCsGAQUFBzAChlBo
# dHRwOi8vY2FjZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRUcnVzdGVkRzRDb2Rl
# U2lnbmluZ1JTQTQwOTZTSEEzODQyMDIxQ0ExLmNydDAJBgNVHRMEAjAAMA0GCSqG
# SIb3DQEBCwUAA4ICAQC3EeHXUPhpe31K2DL43Hfh6qkvBHyR1RlD9lVIklcRCR50
# ZHzoWs6EBlTFyohvkpclVCuRdQW33tS6vtKPOucpDDv4wsA+6zkJYI8fHouW6Tqa
# 1W47YSrc5AOShIcJ9+NpNbKNGih3doSlcio2mUKCX5I/ZrzJBkQpJ0kYha/pUST2
# CbE3JroJf2vQWGUiI+J3LdiPNHmhO1l+zaQkSxv0cVDETMfQGZKKRVESZ6Fg61b0
# djvQSx510MdbxtKMjvS3ZtAytqnQHk1ipP+Rg+M5lFHrSkUlnpGa+f3nuQhxDb7N
# 9E8hUVevxALTrFifg8zhslVRH5/Df/CxlMKXC7op30/AyQsOQxHW1uNx3tG1DMgi
# zpwBasrxh6wa7iaA+Lp07q1I92eLhrYbtw3xC2vNIGdMdN7nd76yMIjdYnAn7r38
# wwtaJ3KYD0QTl77EB8u/5cCs3ShZdDdyg4K7NoJl8iEHrbqtooAHOMLiJpiL2i9Y
# n8kQMB6/Q6RMO3IUPLuycB9o6DNiwQHf6Jt5oW7P09k5NxxBEmksxwNbmZvNQ65Z
# n3exUAKqG+x31Egz5IZ4U/jPzRalElEIpS0rgrVg8R8pEOhd95mEzp5WERKFyXhe
# 6nB6bSYHv8clLAV0iMku308rpfjMiQkqS3LLzfUJ5OHqtKKQNMLxz9z185UCszGC
# BlMwggZPAgEBMH0waTELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0LCBJ
# bmMuMUEwPwYDVQQDEzhEaWdpQ2VydCBUcnVzdGVkIEc0IENvZGUgU2lnbmluZyBS
# U0E0MDk2IFNIQTM4NCAyMDIxIENBMQIQB8JSdCgUotar/iTqF+XdLjANBglghkgB
# ZQMEAgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJ
# AzEMBgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8G
# CSqGSIb3DQEJBDEiBCBTnZl2WTOvSzJHkxseQBD2v8jkpDPZ3N+MBvsXf2sQEzAN
# BgkqhkiG9w0BAQEFAASCAgCCrGLVkHZzd1p7H9jhK9h5zLcdezz50+Q3vvPzKndz
# wQ9Ua72+ToPk8bEQcE2XiMa9ND86AFm5wRe0oawzEalWKlDy40RVt/ajgUSKQO10
# b5SAR2q+rS6N7pFRuXNbZiZsXzoVZPSyrOEm9tjZK0Y6BkskZh6Nns+xdqp7sFY0
# p+7cqFlFlTYiPBxGydlITgaVj8a2bVAKit1/ok4ylshHExcAEv6V1xYpYBymj+5L
# gixyc9PVBMqTByqCbv1ObVM5DZcsZVsiLVAEQ5D1x1Br0xr+QePdj9r6qiF/Gbsz
# 7FW4ZQOE7q/wCI08KlPNmIE6UXgG2HOJXMW2QyVwl/Wt2oTOjmde//ji6a0kWVyI
# rFcLBvyLv369LtCdy9SAybORvBWWtGQF20epk1RJkmWNuDK5KrAr1QemP+VsNQIm
# 2FmmAZ/zEAhz5VCDBWK/yyk1CHNvGDaIncrE+c7/J21X1HEKMYXe20RT5GGC/vSG
# 2I0778dcNdaS6+6g6DOEoj92WeeCWEIHZhVRHHmU3nzHScwYNXbDzKHaRS5HZQWb
# EYkrLW9N0PWCtYkHAuD0XJGZQ6Y+nEspF5HHNoy8TaOxKGkn1gwafnzSBa0bjpfl
# HPnnkDLVs8KI7EAK3GYBN84eKGmcQLSZqR4/6loTAcZUOGJ1wmzY3X3hvtLggoMQ
# HaGCAyAwggMcBgkqhkiG9w0BCQYxggMNMIIDCQIBATB3MGMxCzAJBgNVBAYTAlVT
# MRcwFQYDVQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1
# c3RlZCBHNCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0ECEAVEr/OUnQg5
# pr/bP1/lYRYwDQYJYIZIAWUDBAIBBQCgaTAYBgkqhkiG9w0BCQMxCwYJKoZIhvcN
# AQcBMBwGCSqGSIb3DQEJBTEPFw0yNDA4MTMwNjI1NTVaMC8GCSqGSIb3DQEJBDEi
# BCDg13MCDzsjBKRWAYK5w5UWP7QPF8KumN95SuJqITQ+dTANBgkqhkiG9w0BAQEF
# AASCAgAZSO0dOIHLULgHzxifZc0rMhbuPdMa7kbIFK4INjO4BK7HJR6KlMwaDt7+
# NvcNYbCGxTsoHJq6BHVxpn9XoH+JFmGGAQyGP5gWEv8CC3TJn92cRoHmSRGL29GN
# Swh5cUXfREnV0zY5Pc/CK/ERon3rsvktPZT1ZxUajiymtcYdC8qLPJgt6pjB2Uvc
# yYPafQR+2F2rkweiiJuz/Yaa3J7ZEKiO733Vd1o+xHk3Mr1KhTfcMUPGRVdXWQzg
# CJFpQGr/IcmGLnLxuPUigk1sYurcMsHL2LaxLwQIAGLbD8eFBvJlHFoZqvWLbktV
# 9ZL2QBhLsJLrwC016koPd57kNpXtMz6QjihgTXHzpF5X2BNCY/xh6PBh6TIFku50
# iAwTMucWyG7VbHMdDNTE8Q5UfIytaVedWDsS1ljl1JFZaiOZD+gri+G3G2F3llX5
# 9VD9WwgPkk5TDc1aZj28pYNJL8aleKxSMDeZcWuJqa7hDwVkgmYvF2eP4qhA+e5i
# hCfnaPnmoFLCZ4zoEXWKMOYj3WvY4F2RUgraI1Uz+scLXOovKDmERWmnwuitxdee
# K51hfDA/W2SeMA8ProbjndsivFNAJ4Opcc1TlbIVZiAhMZ1U2enJzmfY1zIOsBkf
# inz0fV6jOu4DL+GydDHIJCtqdZQfjagBnIOGD6cdAP/b5clhaQ==
# SIG # End signature block