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]
        # Create a hashtable to store the GUID value of each schema class and attribute

        $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()
        #Create a hashtable to store the GUID value of each extended right in the forest
        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-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
    Function that helps converting Exchange email address list into readable, exportable format.
     
    .DESCRIPTION
        Function that helps converting Exchange email address list into readable, exportable format.
     
    .PARAMETER Emails
    List of emails as available in Exchange or Exchange Online, otherwise known as proxy addresses list
     
    .PARAMETER Separator
     
    .PARAMETER RemoveDuplicates
     
    .PARAMETER RemovePrefix
     
    .PARAMETER AddSeparator
     
    .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
     
    .NOTES
    General notes
    #>

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

    if ($RemovePrefix) {
        #$Emails = $Emails.Replace('SMTP:', '').Replace('SIP:', '').Replace('smtp:', '').Replace('sip:', '').Replace('spo:','')
        $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' # AT LEAST IT SEEMS SO
            '-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' # 'RBAC Role Group'
            '1073741830'  = 'ACLableRemoteMailUser'
        }
        if ($All) {
            $ListMsExchRecipientDisplayType
        } else {
            if ($null -ne $ListMsExchRecipientDisplayType[$msExchRecipientDisplayType]) {
                $ListMsExchRecipientDisplayType[$msExchRecipientDisplayType]
            } else {
                $msExchRecipientDisplayType
            }
        }
    } elseif ($PSBoundParameters.ContainsKey('msExchRemoteRecipientType')) {
        $ListMsExchRemoteRecipientType = [ordered] @{
            # RemoteRecipientType
            '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 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) {
                    #$dn = $dn -replace '^.+?,(?=CN|OU|DC)'
                    $Distinguished = $Distinguished -replace '^.+?,(?=..=)'
                    if ($Distinguished -match '^DC=') {
                        break
                    }
                    $Distinguished
                }
            } elseif ($ToDC) {
                #return [Regex]::Match($DistinguishedName, '(?=DC=)(.*\n?)(?<=.)').Value
                # return [Regex]::Match($DistinguishedName, '.*?(DC=.*)').Value
                $Value = $Distinguished -replace '.*?((DC=[^=]+,)+DC=[^=]+)$', '$1'
                if ($Value) {
                    $Value
                }
                #return [Regex]::Match($DistinguishedName, 'CN=.*?(DC=.*)').Groups[1].Value
            } elseif ($ToLastName) {
                # Would be best if it worked, but there is too many edge cases so hand splits seems to be the best solution
                # Feel free to change it back to regex if you know how ;)
                <# https://stackoverflow.com/questions/51761894/regex-extract-ou-from-distinguished-name
                $Regex = "^(?:(?<cn>CN=(?<name>.*?)),)?(?<parent>(?:(?<path>(?:CN|OU).*?),)?(?<domain>(?:DC=.*)+))$"
                $Found = $Distinguished -match $Regex
                if ($Found) {
                    $Matches.name
                }
                #>

                $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.+?))$'
                #$Output = foreach ($_ in $Distinguished) {
                $Found = $Distinguished -match $Regex
                if ($Found) {
                    $Matches.cn
                }
                #}
                #$Output.cn
            }
        }
    }
}
function ConvertFrom-NetbiosName { 
    [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 {
                    # we can't be sure what we got so lets push back what we got
                    [PSCustomObject] @{
                        DomainName = ''
                        Name       = $Ident
                    }
                }
            } else {
                # we can't be sure what we got so lets push back what we got
                [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
    )
    # https://support.microsoft.com/en-au/help/243330/well-known-security-identifiers-in-windows-operating-systems
    $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-113' = [PSCustomObject] @{
        # Name = 'NT AUTHORITY\Local account'
        # SID = 'S-1-5-113'
        # DomainName = ''
        # Type = 'WellKnownGroup'
        # Error = ''
        # }
        # 'S-1-5-114' = [PSCustomObject] @{
        # Name = 'NT AUTHORITY\Local account and member of Administrators group'
        # SID = 'S-1-5-114'
        # DomainName = ''
        # Type = 'WellKnownGroup'
        # Error = ''
        # }
    }
    foreach ($S in $SID) {
        if ($OnlyWellKnownAdministrative) {
            # In this case we only return very few high permissions, otherwise nothing
            if ($WellKnownAdministrative[$S]) {
                $WellKnownAdministrative[$S]
            }
        } elseif ($OnlyWellKnown) {
            # In this case we only return well known cases, otherwise nothing
            if ($wellKnownSIDs[$S]) {
                $wellKnownSIDs[$S]
            }
        } else {
            # In this case we return WellKnown, or try to resolve stuff
            if ($wellKnownSIDs[$S]) {
                $wellKnownSIDs[$S]
            } else {
                if ($DoNotResolve) {
                    if ($S -like "S-1-5-21-*-519" -or $S -like "S-1-5-21-*-512") {
                        # Domain Admins / Enterprise Admins
                        [PSCustomObject] @{
                            Name       = $S
                            SID        = $S
                            DomainName = '' # we don't know from SID which domain it is, without checking LDAP
                            Type       = 'Administrative'
                            Error      = ''
                        }
                    } else {
                        # Return unchanged object
                        [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 {
                        # Return unchanged object
                        [PSCustomObject] @{
                            Name       = $S
                            SID        = $S
                            DomainName = ''
                            Error      = $_.Exception.Message -replace [environment]::NewLine, ' '
                            Type       = 'Unknown'
                        }
                    }
                }
            }
        }
    }
}
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 {
        # [System.Security.Principal.WellKnownSidType]::BuiltinAccountOperatorsSid
        if (-not $Script:GlobalCacheSidConvert -or $Force) {
            $Script:GlobalCacheSidConvert = @{
                # We probably don't need to build it up because4 we will be able to find it, but sometimes some of them are not available
                'NT AUTHORITY\SYSTEM'                         = [PSCustomObject] @{
                    Name       = 'BUILTIN\Administrators'
                    SID        = 'S-1-5-18'
                    DomainName = ''
                    Type       = 'WellKnownAdministrative'
                    Error      = ''
                }
                # 'NT AUTHORITY\NETWORK SERVICE' = [PSCustomObject] @{
                # Name = 'NT AUTHORITY\NETWORK SERVICE'
                # SID = 'S-1-5-20' # or 'S-1-5-19'
                # 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      = ''
                }
            }
        }
    }
    Process {
        if ($Identity) {
            foreach ($Ident in $Identity) {
                $MatchRegex = [Regex]::Matches($Ident, "S-\d-\d+-(\d+-|){1,14}\d+")
                if ($Script:GlobalCacheSidConvert[$Ident]) {
                    # If we cached it, lets return it right away
                    Write-Verbose "Convert-Identity - Processing $Ident (Cache)"
                    $Script:GlobalCacheSidConvert[$Ident]
                } elseif ($MatchRegex.Success) {
                    # regex check if SID .. do something
                    #$MatchRegex = [Regex]::IsMatch($Ident, "S-\d-\d+-(\d+-){1,14}\d+")
                    #$MatchRegex = [Regex]::Matches($Ident, "S-\d-\d+-(\d+-){1,4}\d+")
                    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=*') {
                    # check if DN
                    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 {
                    # Other types
                    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 { 
    [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 { 
    [CmdletBinding()]
    param (
        [string] $Timestring,
        [string] $Ignore = '*1601*'
    )
    Try {
        $DateTime = ([datetime]::FromFileTime($Timestring))
    } catch {
        $DateTime = $null
    }
    if ($null -eq $DateTime -or $DateTime -like $Ignore) {
        return $null
    } else {
        return $DateTime
    }
}
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('/')
                # Assemble the DN by replacing
                $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 = @{
            # This is how it's written in AD
            '10.0 (22000)' = 'Windows 11 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"

            # This is how WMI/CIM stores it
            '10.0.22000'   = 'Windows 11 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"

            # This is how it's written in registry
            '22000'        = 'Windows 11 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 = $OperatingSystem
        }
    } elseif ($OperatingSystem -like 'Windows Server*') {
        # May need updates https://docs.microsoft.com/en-us/windows-server/get-started/windows-server-release-info
        # to detect Core

        $Systems = @{
            # This is how it's written in AD
            '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" # (Datacenter Core, Standard Core)
            '10.0 (17763)' = "Windows Server 2019 1809" # (Datacenter, Essentials, Standard)
            '10.0 (17134)' = "Windows Server 2016 1803" # (Datacenter, Standard)
            '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'

            # This is how WMI/CIM stores it
            '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" # (Datacenter Core, Standard Core)
            '10.0.17763'   = "Windows Server 2019 1809"  # (Datacenter, Essentials, Standard)
            '10.0.17134'   = "Windows Server 2016 1803" ## (Datacenter, Standard)
            '10.0.14393'   = "Windows Server 2016 1607"
            '6.3.9600'     = 'Windows Server 2012 R2'
            '6.1.7601'     = 'Windows Server 2008 R2' # i think
            '5.2.3790'     = 'Windows Server 2003' # i think

            # This is how it's written in registry
            '20348'        = 'Windows Server 2022'
            '19042'        = 'Windows Server 2019 20H2'
            '19041'        = 'Windows Server 2019 2004'
            '18363'        = 'Windows Server 2019 1909'
            '18362'        = "Windows Server 2019 1903" # (Datacenter Core, Standard Core)
            '17763'        = "Windows Server 2019 1809" # (Datacenter, Essentials, Standard)
            '17134'        = "Windows Server 2016 1803" # (Datacenter, Standard)
            '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 = $OperatingSystem
        }
    } else {
        $System = $OperatingSystem
    }
    if ($System) {
        $System
    } else {
        'Unknown'
    }
}
function Convert-UserAccountControl { 
    [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 Copy-Dictionary { 
    [alias('Copy-Hashtable', 'Copy-OrderedHashtable')]
    [cmdletbinding()]
    param(
        [System.Collections.IDictionary] $Dictionary
    )
    # create a deep-clone of an object
    $ms = [System.IO.MemoryStream]::new()
    $bf = [System.Runtime.Serialization.Formatters.Binary.BinaryFormatter]::new()
    $bf.Serialize($ms, $Dictionary)
    $ms.Position = 0
    $clone = $bf.Deserialize($ms)
    $ms.Close()
    $clone
}
function Get-ADADministrativeGroups { 
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER Type
    Parameter description
 
    .PARAMETER Forest
    Parameter description
 
    .PARAMETER ExcludeDomains
    Parameter description
 
    .PARAMETER IncludeDomains
    Parameter description
 
    .PARAMETER ExtendedForestInformation
    Parameter description
 
    .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
    General notes
    #>

    [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] = $_
            }
        }
    }
    # We need to treat EnterpriseAdmins separatly as it should be always available, not only when requested specific domain
    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] = $_ #"$($DomainInformation.NetBIOSName)\$($_.Name)"
            }
        }
    }
    return $ADDictionary
}
function Get-ADEncryptionTypes { 
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER Value
    Parameter description
 
    .EXAMPLE
    Get-ADEncryptionTypes -Value 24
 
    Output:
    AES128-CTS-HMAC-SHA1-96
    AES256-CTS-HMAC-SHA1-96
 
    .NOTES
    General notes
    #>

    [cmdletbinding()]
    Param(
        [parameter(Mandatory = $false, ValueFromPipeline = $True)][int32]$Value
    )
    # https://lists.samba.org/archive/cifs-protocol/attachments/20100111/e8bb3520/attachment-0001.pdf
    # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/6cfc7b50-11ed-4b4d-846d-6f08f0812919
    # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-kile/1163bb03-7035-433e-b5a4-802247262d18#Appendix_A_9
    [String[]]$EncryptionTypes = @(
        Foreach ($V in $Value) {
            if ([int32]$V -band 0x00000001) { "DES-CBC-CRC" } # KERB_ENCTYPE_DES_CBC_CRC -> CRC
            if ([int32]$V -band 0x00000002) { "DES-CBC-MD5" } # KERB_ENCTYPE_DES_CBC_MD5 -> MD5
            if ([int32]$V -band 0x00000004) { "RC4-HMAC" } # KERB_ENCTYPE_RC4_HMAC_MD5 -> RC4
            if ([int32]$V -band 0x00000008) { "AES128-CTS-HMAC-SHA1-96" } # KERB_ENCTYPE_AES128_CTS_HMAC_SHA1_96 -> A128
            if ([int32]$V -band 0x00000010) { "AES256-CTS-HMAC-SHA1-96" } # KERB_ENCTYPE_AES256_CTS_HMAC_SHA1_96 -> A256
            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 { 
    [cmdletbinding()]
    Param(
        [parameter(Mandatory = $false, ValueFromPipeline = $True)][int32]$Value
    )
    # https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-adts/e9a2d23c-c31e-4a6f-88a0-6646fdb51a3c
    [String[]]$TrustAttributes = @(
        Foreach ($V in $Value) {
            if ([int32]$V -band 0x00000001) { "Non Transitive" } # TRUST_ATTRIBUTE_NON_TRANSITIVE
            if ([int32]$V -band 0x00000002) { "UpLevel Only" } # TRUST_ATTRIBUTE_UPLEVEL_ONLY
            if ([int32]$V -band 0x00000004) { "Quarantined Domain" } # TRUST_ATTRIBUTE_QUARANTINED_DOMAIN
            if ([int32]$V -band 0x00000008) { "Forest Transitive" } # TRUST_ATTRIBUTE_FOREST_TRANSITIVE
            if ([int32]$V -band 0x00000010) { "Cross Organization" } #TRUST_ATTRIBUTE_CROSS_ORGANIZATION
            if ([int32]$V -band 0x00000020) { "Within Forest" } # TRUST_ATTRIBUTE_WITHIN_FOREST
            if ([int32]$V -band 0x00000040) { "Treat as External" } # TRUST_ATTRIBUTE_TREAT_AS_EXTERNAL
            if ([int32]$V -band 0x00000080) { "Uses RC4 Encryption" } # TRUST_ATTRIBUTE_USES_RC4_ENCRYPTION
            if ([int32]$V -band 0x00000200) { "No TGT DELEGATION" } # TRUST_ATTRIBUTE_CROSS_ORGANIZATION_NO_TGT_DELEGATION
            if ([int32]$V -band 0x00000800) { "Enable TGT DELEGATION" } #TRUST_ATTRIBUTE_CROSS_ORGANIZATION_ENABLE_TGT_DELEGATION
            if ([int32]$V -band 0x00000400) { "PIM Trust" } #TRUST_ATTRIBUTE_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 parameter 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.
 
    .EXAMPLE
    Get-CimData -Class 'win32_bios' -ComputerName AD1,EVOWIN
 
    Get-CimData -Class 'win32_bios'
 
    # 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',
        [string[]] $Properties = '*'
    )
    $ExcludeProperties = 'CimClass', 'CimInstanceProperties', 'CimSystemProperties', 'SystemCreationClassName', 'CreationClassName'

    # Querying CIM locally usually doesn't work. This means if you're querying same computer you neeed to skip CimSession/ComputerName if it's local query
    [Array] $ComputersSplit = Get-ComputerSplit -ComputerName $ComputerName

    $CimObject = @(
        # requires removal of this property for query
        [string[]] $PropertiesOnly = $Properties | Where-Object { $_ -ne 'PSComputerName' }
        # Process all remote computers
        $Computers = $ComputersSplit[1]
        if ($Computers.Count -gt 0) {
            if ($Protocol = 'Default') {
                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
                $Session = New-CimSession -ComputerName $Computers -SessionOption $Option -ErrorAction SilentlyContinue
                $Info = Get-CimInstance -ClassName $Class -CimSession $Session -ErrorAction SilentlyContinue -Property $PropertiesOnly -Namespace $NameSpace -Verbose:$false -ErrorVariable ErrorsToProcess | Select-Object -Property $Properties -ExcludeProperty $ExcludeProperties
                $null = Remove-CimSession -CimSession $Session -ErrorAction SilentlyContinue
                $Info
            }
        }
        foreach ($E in $ErrorsToProcess) {
            Write-Warning -Message "Get-CimData - No data for computer $($E.OriginInfo.PSComputerName). Failed with errror: $($E.Exception.Message)"
        }
        # Process local computer
        $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
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER Extension
    Parameter description
 
    .PARAMETER Temporary
    Parameter description
 
    .PARAMETER TemporaryFileOnly
    Parameter description
 
    .EXAMPLE
    Get-FileName -Temporary
    Output: 3ymsxvav.tmp
 
    .EXAMPLE
 
    Get-FileName -Temporary
    Output: C:\Users\pklys\AppData\Local\Temp\tmpD74C.tmp
 
    .EXAMPLE
 
    Get-FileName -Temporary -Extension 'xlsx'
    Output: C:\Users\pklys\AppData\Local\Temp\tmp45B6.xlsx
 
 
    .NOTES
    General notes
    #>

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

    if ($Temporary) {
        # C:\Users\przemyslaw.klys\AppData\Local\Temp\p0v4bbif.xlsx
        return [io.path]::Combine([System.IO.Path]::GetTempPath(), "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension")
    }
    if ($TemporaryFileOnly) {
        # Generates 3ymsxvav.tmp
        return "$($([System.IO.Path]::GetRandomFileName()).Split('.')[0]).$Extension"
    }
}
function Get-FileOwner { 
    [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 {
                        # $File = $_
                        $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 { 
    [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) {
                        # if it's not inherited and we require inherited lets continue
                        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 {
                        # this shouldn't happen because Identity should always return value
                        $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.com1/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-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

    # Cleans up registry path and makes sure it's as required to be processed further
    $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) {
                #Write-Verbose -Message "Getting registry value from $Computer : $($R.Registry)"
                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 { 
    [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 { 
    [alias('Get-WinADDomainControllers')]
    <#
    .SYNOPSIS
 
    .DESCRIPTION
    Long description
 
    .PARAMETER TestAvailability
    Parameter description
 
    .EXAMPLE
    Get-WinADForestControllers -TestAvailability | Format-Table
 
    .EXAMPLE
    Get-WinADDomainControllers
 
    .EXAMPLE
    Get-WinADDomainControllers -Credential $Credential
 
    .EXAMPLE
    Get-WinADDomainControllers | Format-Table *
 
    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
    General notes
    #>

    [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 { 
    [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) {
        # standard situation, building data from AD
        $Findings = [ordered] @{ }
        try {
            if ($Forest) {
                $ForestInformation = Get-ADForest -ErrorAction Stop -Identity $Forest
            } else {
                $ForestInformation = Get-ADForest -ErrorAction Stop
            }
            <#
            $ForestInformation = [ordered] @{
                ApplicationPartitions = $ForestInf.ApplicationPartitions | ForEach-Object -Process { $_ } # : {DC=DomainDnsZones,DC=ad,DC=evotec,DC=xyz, DC=DomainDnsZones,DC=ad,DC=evotec,DC=pl, DC=ForestDnsZones,DC=ad,DC=evotec,DC=xyz}
                CrossForestReferences = $ForestInf.CrossForestReferences | ForEach-Object -Process { $_ } # : {}
                DomainNamingMaster = $ForestInf.DomainNamingMaster # : AD1.ad.evotec.xyz
                Domains = $ForestInf.Domains | ForEach-Object -Process { $_ } # : {ad.evotec.xyz, ad.evotec.pl}
                ForestMode = $ForestInf.ForestMode # : Windows2012R2Forest
                GlobalCatalogs = $ForestInf.GlobalCatalogs | ForEach-Object -Process { $_ } # : {AD1.ad.evotec.xyz, AD2.ad.evotec.xyz, ADRODC.ad.evotec.pl, AD3.ad.evotec.xyz...}
                Name = $ForestInf.Name # : ad.evotec.xyz
                PartitionsContainer = $ForestInf.PartitionsContainer # : CN=Partitions,CN=Configuration,DC=ad,DC=evotec,DC=xyz
                RootDomain = $ForestInf.RootDomain # : ad.evotec.xyz
                SchemaMaster = $ForestInf.SchemaMaster # : AD1.ad.evotec.xyz
                Sites = $ForestInf.Sites | ForEach-Object -Process { $_ } # : {KATOWICE-1, KATOWICE-2}
                SPNSuffixes = $ForestInf.SPNSuffixes | ForEach-Object -Process { $_ } # : {}
                UPNSuffixes = $ForestInf.UPNSuffixes | ForEach-Object -Process { $_ } # : {myneva.eu, single.evotec.xyz, newUPN@com, evotec.xyz...}
            }
            #>

        } 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()
                }
                # We skip checking for exclusions
                continue
            }
            if ($Domain -notin $ExcludeDomains) {
                $Domain.ToLower()
            }
        }
        # We want to have QueryServers always available for all domains
        [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
            # lets return domain as something that wroks
            $Domain
        }

        # we need to make sure to remove domains that don't have DCs for some reason
        [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
                            }
                        }
                    }
                    $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
                        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        = ''
                    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 }
                #$Findings[$Domain] = $AllDC | Where-Object { $_.IsReadOnly -eq $false }
            } else {
                [Array] $Findings['DomainDomainControllers'][$Domain] = $AllDC
                #$Findings[$Domain] = $AllDC
            }
            # Building all DCs for whole Forest
            [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]

                    $Findings['DomainsExtended'][$DomainEx] = Get-ADDomain -Server $Findings['QueryServers'][$DomainEx].HostName[0] | ForEach-Object {
                        # We need to use ForEach-Object to convert ADPropertyValueCollection to normal strings. Otherwise Copy-Dictionary fails
                        #True False ADPropertyValueCollection System.Collections.CollectionBase

                        [ordered] @{
                            AllowedDNSSuffixes                 = $_.AllowedDNSSuffixes | ForEach-Object -Process { $_ }                #: { }
                            ChildDomains                       = $_.ChildDomains | ForEach-Object -Process { $_ }                      #: { }
                            ComputersContainer                 = $_.ComputersContainer                 #: CN = Computers, DC = ad, DC = evotec, DC = xyz
                            DeletedObjectsContainer            = $_.DeletedObjectsContainer            #: CN = Deleted Objects, DC = ad, DC = evotec, DC = xyz
                            DistinguishedName                  = $_.DistinguishedName                  #: DC = ad, DC = evotec, DC = xyz
                            DNSRoot                            = $_.DNSRoot                            #: ad.evotec.xyz
                            DomainControllersContainer         = $_.DomainControllersContainer         #: OU = Domain Controllers, DC = ad, DC = evotec, DC = xyz
                            DomainMode                         = $_.DomainMode                         #: Windows2012R2Domain
                            DomainSID                          = $_.DomainSID.Value                        #: S - 1 - 5 - 21 - 853615985 - 2870445339 - 3163598659
                            ForeignSecurityPrincipalsContainer = $_.ForeignSecurityPrincipalsContainer #: CN = ForeignSecurityPrincipals, DC = ad, DC = evotec, DC = xyz
                            Forest                             = $_.Forest                             #: ad.evotec.xyz
                            InfrastructureMaster               = $_.InfrastructureMaster               #: AD1.ad.evotec.xyz
                            LastLogonReplicationInterval       = $_.LastLogonReplicationInterval       #:
                            LinkedGroupPolicyObjects           = $_.LinkedGroupPolicyObjects | ForEach-Object -Process { $_ }           #:
                            LostAndFoundContainer              = $_.LostAndFoundContainer              #: CN = LostAndFound, DC = ad, DC = evotec, DC = xyz
                            ManagedBy                          = $_.ManagedBy                          #:
                            Name                               = $_.Name                               #: ad
                            NetBIOSName                        = $_.NetBIOSName                        #: EVOTEC
                            ObjectClass                        = $_.ObjectClass                        #: domainDNS
                            ObjectGUID                         = $_.ObjectGUID                         #: bc875580 - 4c70-41ad-a487-c57337e26024
                            ParentDomain                       = $_.ParentDomain                       #:
                            PDCEmulator                        = $_.PDCEmulator                        #: AD1.ad.evotec.xyz
                            PublicKeyRequiredPasswordRolling   = $_.PublicKeyRequiredPasswordRolling | ForEach-Object -Process { $_ }   #:
                            QuotasContainer                    = $_.QuotasContainer                    #: CN = NTDS Quotas, DC = ad, DC = evotec, DC = xyz
                            ReadOnlyReplicaDirectoryServers    = $_.ReadOnlyReplicaDirectoryServers | ForEach-Object -Process { $_ }    #: { }
                            ReplicaDirectoryServers            = $_.ReplicaDirectoryServers | ForEach-Object -Process { $_ }           #: { AD1.ad.evotec.xyz, AD2.ad.evotec.xyz, AD3.ad.evotec.xyz }
                            RIDMaster                          = $_.RIDMaster                          #: AD1.ad.evotec.xyz
                            SubordinateReferences              = $_.SubordinateReferences | ForEach-Object -Process { $_ }            #: { DC = ForestDnsZones, DC = ad, DC = evotec, DC = xyz, DC = DomainDnsZones, DC = ad, DC = evotec, DC = xyz, CN = Configuration, DC = ad, DC = evotec, DC = xyz }
                            SystemsContainer                   = $_.SystemsContainer                   #: CN = System, DC = ad, DC = evotec, DC = xyz
                            UsersContainer                     = $_.UsersContainer                     #: CN = Users, DC = ad, DC = evotec, DC = xyz
                        }
                    }

                    $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
                }
            }
        }
        # Bring back setting as per default
        if ($TemporaryProgress) {
            $Global:ProgressPreference = $TemporaryProgress
        }

        $Findings
    } else {
        # this takes care of limiting output to only what we requested, but based on prior input
        # this makes sure we ask once for all AD stuff and then subsequent calls just filter out things
        # this should be much faster then asking again and again for stuff from AD
        $Findings = Copy-DictionaryManual -Dictionary $ExtendedForestInformation
        [Array] $Findings['Domains'] = foreach ($_ in $Findings.Domains) {
            if ($IncludeDomains) {
                if ($_ -in $IncludeDomains) {
                    $_.ToLower()
                }
                # We skip checking for exclusions
                continue
            }
            if ($_ -notin $ExcludeDomains) {
                $_.ToLower()
            }
        }
        # Now that we have Domains we need to remove all DCs that are not from domains we excluded or included
        foreach ($_ in [string[]] $Findings.DomainDomainControllers.Keys) {
            if ($_ -notin $Findings.Domains) {
                $Findings.DomainDomainControllers.Remove($_)
            }
        }
        # Same as above but for query servers - we don't remove queried servers
        #foreach ($_ in [string[]] $Findings.QueryServers.Keys) {
        # if ($_ -notin $Findings.Domains -and $_ -ne 'Forest') {
        # $Findings.QueryServers.Remove($_)
        # }
        #}
        # Now that we have Domains we need to remove all Domains that are excluded or included
        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
            }
            # Building all DCs for whole Forest
            [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()
        #Create a hashtable to store the GUID value of each extended right in the forest
        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 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 { 
    [cmdletBinding(SupportsShouldProcess)]
    param(
        [Array] $Path,
        [switch] $Recursive,
        [string] $Owner,
        [string[]] $Exlude,
        [switch] $JustPath
    )
    <#
    System.Security.Principal.NTAccount new(string domainName, string accountName)
    System.Security.Principal.NTAccount new(string name)
    #>

    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

    # Cleans up registry path and makes sure it's as required to be processed further
    $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]) {
                # Local computer
                Set-PrivateRegistry -RegistryValue $RegistryValue -Computer $Computer -Suppress:$Suppress.IsPresent -ErrorAction $ErrorActionPreference -WhatIf:$WhatIfPreference
            }
            foreach ($Computer in $ComputersSplit[1]) {
                # Remote computer
                Set-PrivateRegistry -RegistryValue $RegistryValue -Computer $Computer -Remote -Suppress:$Suppress.IsPresent -ErrorAction $ErrorActionPreference -WhatIf:$WhatIfPreference
            }
        } else {
            if ($PSBoundParameters.ErrorAction -eq 'Stop') {
                Unregister-MountedRegistry
                throw
            } else {
                # This shouldn't really happen
                Write-Warning "Set-PSRegistry - Setting registry to $Registry have failed. Couldn't translate HIVE."
            }
        }
    }
    Unregister-MountedRegistry
}
function Start-TimeLog { 
    [CmdletBinding()]
    param()
    [System.Diagnostics.Stopwatch]::StartNew()
}
function Stop-TimeLog { 
    [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.
 
        It provides:
        - Easy manipulation of colors,
        - Logging output to file (log)
        - Nice formatting options out of the box.
 
    .DESCRIPTION
        Author: przemyslaw.klys at evotec.pl
        Project website: https://evotec.xyz/hub/scripts/write-color-ps1/
        Project support: https://github.com/EvotecIT/PSWriteColor
 
        Original idea: Josh (https://stackoverflow.com/users/81769/josh)
 
    .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
    # Added in 0.5
    Write-Color -T "My text", " is ", "all colorful" -C Yellow, Red, Green -B Green, Green, Yellow
    wc -t "my text" -c yellow -b green
    wc -text "my text" -c red
 
    .NOTES
        Additional Notes:
        - TimeFormat https://msdn.microsoft.com/en-us/library/8kb3ddd4.aspx
    #>

    [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)
    $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 } }
    if ($StartTab -ne 0) { for ($i = 0; $i -lt $StartTab; $i++) { Write-Host -Object "`t" -NoNewline } }
    if ($StartSpaces -ne 0) { for ($i = 0; $i -lt $StartSpaces; $i++) { Write-Host -Object ' ' -NoNewline } }
    if ($ShowTime) { Write-Host -Object "[$([datetime]::Now.ToString($DateTimeFormat))] " -NoNewline }
    if ($Text.Count -ne 0) {
        if ($Color.Count -ge $Text.Count) { 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 }
    if ($LinesAfter -ne 0) { for ($i = 0; $i -lt $LinesAfter; $i++) { Write-Host -Object "`n" -NoNewline } }
    if ($Text.Count -and $LogFile) {
        $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) { $PSCmdlet.WriteError($_) } 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-GenericRightsToFileSystemRights { 
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER OriginalRights
    Parameter description
 
    .EXAMPLE
    An example
 
    .NOTES
 
    .LINK
    Improved 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 Copy-DictionaryManual { 
    [CmdletBinding()]
    param(
        [System.Collections.IDictionary] $Dictionary
    )

    $clone = @{}
    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 { 
    [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-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 { 
    [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) {
        # Remove additional slashes
        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 { 
    [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 ($R.StartsWith($DictionaryKey, [System.StringComparison]::CurrentCultureIgnoreCase)) {
                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_*") {
                    # this is a special keys to handle for offline users
                    # 'Users\Offline_Przemek\Software\Policies1\Microsoft\Windows\CloudContent' -Key 'DisableTailoredExperiencesWithDiagnosticData'
                    # 'HKU\Offline_Przemek\Software\Policies1\Microsoft\Windows\CloudContent' -Key 'DisableWindowsConsumerFeatures'
                    # 'Users\Offline_test.1\Software\Policies1\Microsoft\Windows\CloudContent' -Key 'DisableTailoredExperiencesWithDiagnosticData'
                    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 { 
    [cmdletBinding()]
    param()
    if ($Script:Dictionary) {
        return
    }
    $Script:Dictionary = @{
        # Those don't really exists, but we want to allow targetting all users or default users
        'HKUAD:'   = 'HKEY_ALL_USERS_DEFAULT' # All users in HKEY_USERS including .DEFAULT_USER (auto mapped from NTUSER.DAT)
        'HKUA:'    = 'HKEY_ALL_USERS' # All users in HKEY_USERS excluding .DEFAULT / .DEFAULT_USER (auto mapped from NTUSER.DAT)
        'HKUD:'    = 'HKEY_DEFAULT_USER' # DEFAULT_USER user in HKEY_USERS (auto mapped from NTUSER.DAT)
        'HKUDUD:'  = 'HKEY_ALL_DOMAIN_USERS_DEFAULT' # All users in HKEY_USERS that are non SPECIAL accounts including .DEFAULT_USER (auto mapped from NTUSER.DAT)

        # All users in HKEY_USERS that are non SPECIAL accounts excluding .DEFAULT / .DEFAULT_USER (auto mapped from NTUSER.DAT)
        'HKUDU:'   = 'HKEY_ALL_DOMAIN_USERS'
        # All users in HKEY_USERS that are non SPECIAL accounts excluding .DEFAULT / .DEFAULT_USER (auto mapped from NTUSER.DAT)
        # But also including non-loaded users (not logged in) (auto mapped from NTUSER.DAT)
        'HKUDUO:'  = 'HKEY_ALL_DOMAIN_USERS_OTHER'
        # All users in HKEY_USERS that are non SPECIAL accounts including .DEFAULT_USER (auto mapped from NTUSER.DAT)
        # But also including non-loaded users (not logged in) (auto mapped from NTUSER.DAT)
        'HKUDUDO:' = 'HKEY_ALL_DOMAIN_USERS_OTHER_DEFAULT'
        # order matters
        '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] @{
        # Those don't really exists, but we want to allow targetting all users or default users
        # The order matters
        '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'
        # Those exists
        '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 { 
    [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 {
        # this should happen if we weren't able to get registry keys in Get-PSConvertSpecialRegistry for HKEY_USERS
        $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 { 
    [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 {
        # this should happen if we weren't able to get registry keys in Get-PSConvertSpecialRegistry for HKEY_USERS
        $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 { 
    [cmdletBinding()]
    param(
        [Array] $RegistryPath,
        [System.Collections.IDictionary] $HiveDictionary,
        [string] $Key
    )
    foreach ($Registry in $RegistryPath) {
        # Remove additional slashes
        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 ($Registry -is [string] -and $Registry.StartsWith($Hive, [System.StringComparison]::CurrentCultureIgnoreCase)) {
                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) {
                #} elseif ($Registry -isnot [string] -and $Registry.RegistryPath.StartsWith($Hive, [System.StringComparison]::CurrentCultureIgnoreCase)) {
                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 Resolve-PrivateRegistry { 
    [CmdletBinding()]
    param(
        [alias('Path')][string[]] $RegistryPath
    )
    foreach ($R in $RegistryPath) {
        # clean up the path
        $R = $R.Replace("\\", "\").Replace("\\", "\")

        # This is to address DEFAULT USER Mapping when after returning value from registry someone takes it and feeds it back to cmdlets
        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
            # This makes sure any short HIVES are converted to expected values
            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
                }
            }
            # Lets try to fix things for user if he uses LONG HIVE names but still with colon
            if (-not $Found) {
                $R.Replace(":", "")
            }
        } else {
            # This makes sure we do any string without ":" as literal and follow what user wants
            $R
        }
    }
}
function Set-PrivateRegistry { 
    [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 {
                #$BaseHive = [Microsoft.Win32.RegistryKey]::OpenBaseKey($RegistryValue.HiveKey, 0 )
                $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 { 
    [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 = [System.Net.Sockets.TcpClient]::new()
                $Connect = $TcpClient.BeginConnect($Computer, $P, $null, $null)
                $Wait = $Connect.AsyncWaitHandle.WaitOne($Timeout, $false)
                if (!$Wait) {
                    $TcpClient.Close()
                    $Output['Status'] = $false
                    $Output['Summary'] = "TCP $P Failed"
                } else {
                    $TcpClient.EndConnect($Connect)
                    $TcpClient.Close()
                    $Output['Status'] = $true
                    $Output['Summary'] = "TCP $P Successful"
                }
                $TcpClient.Close()
                $TcpClient.Dispose()
 
 
                #>


                $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
                # $UdpClient.Connect($Computer, $P)
                $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 {
        # Bring back setting as per default
        if ($TemporaryProgress) {
            $Global:ProgressPreference = $TemporaryProgress
        }
    }
}
function Test-WinRM { 
    [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 { 
    [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 { 
    [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 { 
    [alias('Dismount-RegistryPath')]
    [cmdletbinding()]
    param(
        [Parameter(Mandatory)][string] $MountPoint,
        [switch] $Suppress
    )

    # This is required to force removal of the mount point
    [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 { 
    [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 { 
    [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
    Short description
 
    .DESCRIPTION
    Long description
 
    .EXAMPLE
    Get-OfflineRegistryProfilesPath
 
    .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
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER MountPoint
    Parameter description
 
    .PARAMETER FilePath
    Parameter description
 
    .EXAMPLE
    Mount-PSRegistryPath -MountPoint 'HKEY_USERS\.DEFAULT_USER111' -FilePath 'C:\Users\Default\NTUSER.DAT'
 
    .NOTES
    General notes
    #>

    [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 {
    [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"
            # $SplittedName = $Principal -split '/'
            # [System.Security.Principal.IdentityReference] $Identity = [System.Security.Principal.SecurityIdentifier]::new($SplittedName[1])
            # $ResolvedIdenity = Convert-Identity -Identity $Principal

            #$AccessRuleToAdd = [System.DirectoryServices.ActiveDirectoryAccessRule]::new($Identity, $AccessRule, $AccessControlType)
            #$ntSecurityDescriptor.AddAccessRule($AccessRuleToAdd)
            @{ 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 {
    [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."
    }
}
$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 '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
            }
        }
    }
}
# 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.Laps -eq $false -and $Computer.IsDC -eq $false) {
                    $Script:Reporting['LAPS']['Variables']['ComputersActiveNoLaps']++
                } elseif ($Computer.LastLogonDays -lt 60 -and $Computer.Laps -eq $true -and $Computer.IsDC -eq $false) {
                    $Script:Reporting['LAPS']['Variables']['ComputersActiveWithLaps']++
                }
                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) {
                $Script:Reporting['LAPS']['Variables']['ComputersLapsEnabled']++
                if ($Computer.LapsExpirationDays -lt 0) {
                    $Script:Reporting['LAPS']['Variables']['ComputersLapsExpired']++
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersLapsNotExpired']++
                }
            } else {
                if ($Computer.IsDC -eq $true) {
                    $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']++
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersServerDisabled']++
                }
                if ($Computer.Laps) {
                    $Script:Reporting['LAPS']['Variables']['ComputersServerLapsEnabled']++
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersServerLapsDisabled']++
                }
            } elseif ($Computer.System -notlike "Windows Server*" -and $Computer.System -like "Windows*") {
                $Script:Reporting['LAPS']['Variables']['ComputersWorkstation']++
                if ($Computer.Enabled) {
                    $Script:Reporting['LAPS']['Variables']['ComputersWorkstationEnabled']++
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersWorkstationDisabled']++
                }
                if ($Computer.Laps) {
                    $Script:Reporting['LAPS']['Variables']['ComputersWorkstationLapsEnabled']++
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersWorkstationLapsDisabled']++
                }
            } else {
                $Script:Reporting['LAPS']['Variables']['ComputersOther']++
                if ($Computer.Enabled) {
                    $Script:Reporting['LAPS']['Variables']['ComputersOtherEnabled']++
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersOtherDisabled']++
                }
                if ($Computer.Laps) {
                    $Script:Reporting['LAPS']['Variables']['ComputersOtherLapsEnabled']++
                } else {
                    $Script:Reporting['LAPS']['Variables']['ComputersOtherLapsDisabled']++
                }
            }
        }
    }
    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 computers with LAPS: ", $($Script:Reporting['LAPS']['Variables'].ComputersLapsEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of computers without LAPS: ", $($Script:Reporting['LAPS']['Variables'].ComputersLapsDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of servers with LAPS: ", $($Script:Reporting['LAPS']['Variables'].ComputersServerLapsEnabled) -Color None, BlueMarguerite -FontWeight normal, bold
            New-HTMLListItem -Text "Total number of servers without LAPS: ", $($Script:Reporting['LAPS']['Variables'].ComputersServerLapsDisabled) -Color None, BlueMarguerite -FontWeight normal, bold
        } -FontSize 10pt
    }
    Variables  = @{
        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
        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-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-HTMLSection -HeaderText 'General statistics' -CanCollapse {
                New-HTMLPanel {
                    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-HTMLPanel {
                    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-HTMLPanel {
                    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-HTMLPanel {
                    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 'LAPS statistics' -CanCollapse -Direction column {
                New-HTMLSection -Invisible {
                    New-HTMLPanel {
                        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-HTMLPanel {
                        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-HTMLChart -Gradient {
                            New-ChartPie -Name 'LAPS Expired' -Value $Script:Reporting['LAPS']['Variables'].ComputersLapsExpired
                            New-ChartPie -Name 'LAPS Up-to-date' -Value $Script:Reporting['LAPS']['Variables'].ComputersLapsNotExpired
                        } -Title "LAPS Passwords Expired"
                    }
                }
                New-HTMLSection -Invisible {
                    New-HTMLSection -HeaderText 'Servers (Windows Server)' -CanCollapse {
                        New-HTMLPanel {
                            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-HTMLSection -HeaderText 'Workstations (Windows Client)' -CanCollapse {
                        New-HTMLPanel {
                            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-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 '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
        }
    }
}
$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 '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
                }
            }
            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
                }
                # Define event
                #New-ChartEvent -DataTableID 'NewIDtoSearchInChart' -ColumnID 0
            }

            New-HTMLTable -DataTable $Script:Reporting['LapsAndBitLocker']['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 '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
            }
        }
    }
}
$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: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 domain. "
            "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:ShowWinADUser = [ordered] @{
    Name       = 'All Users'
    Enabled    = $true
    Execute    = {
        Get-WinADUsers -PerDomain -AddOwner
    }
    Processing = {

    }
    Variables  = @{

    }
    Summary    = {

    }
    Solution   = {
        if ($Script:Reporting['Users']['Data'] -is [System.Collections.IDictionary]) {
            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 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-BinaryToIP {
    [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 {
                #$Bytes += # skipping this and collecting "outside" seems to make it like 10 % faster
                [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-IPToBinary {
    [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 Convert-TrustForestTrustInfo {
    [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 {
    [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 {
    [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 {
    [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 Get-ADConfigurationPermission {
    [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 {
    [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 {
    [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
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER Byte
    An array of bytes which describes the forest trust information.
 
    .EXAMPLE
    An example
 
    .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
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER BinaryReader
    Parameter description
 
    .EXAMPLE
    An example
 
    .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 {
    [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-IPAddressRangeInformation {
    <#
    .SYNOPSIS
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER CIDRObject
    Parameter description
 
    .EXAMPLE
    An example
 
    .NOTES
 
    #>

    [cmdletBinding()]
    param(
        [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)'
    $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") {
            # Passing in IP to test, start binary and end binary.
            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') # this gives broadcast... need minus one.
    $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
    # ugh, exceptions for network lengths from 30..32
    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
        # Override the earlier set value for this (bloody exceptions).
        [int64] $DecimalHostMax2 = [System.Convert]::ToInt64($BinaryBroadcastIP, 2) # not minus one here like for the others
        [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)] # remove first element
            $IPRange = $IPRange[0..($IPRange.Count - 2)] # remove last element
        }
        $o.IPEnumerated = $IPRange
    } else {
        $o.IPEnumerated = @()
    }
    [PSCustomObject]$o
}
function Get-IPRange {
    [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-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 {
    [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 {
    [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 {
    [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
    }
}
$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 {
    [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 {
    [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 {
    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [string] $DataTableID,
        [int] $ColumnID,
        [switch] $Online
    )
    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
                    }
                }
            }
        }
    }
}
function New-HTMLGroupDiagramHierachical {
    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [switch] $Online
    )
    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
                    }
                }
            }
        }
    }
}
function New-HTMLGroupDiagramSummary {
    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [string] $DataTableID,
        [int] $ColumnID,
        [switch] $Online
    )
    $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
                }
            }
        }
    }
}
function New-HTMLGroupDiagramSummaryHierarchical {
    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [switch] $Online
    )
    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
                    }
                }
            }
        }
    }
}
function New-HTMLGroupOfDiagramDefault {
    [cmdletBinding()]
    param(
        [Array] $Identity,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [string] $DataTableID,
        [int] $ColumnID,
        [switch] $Online
    )
    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
                    }
                }
            }
        }
    }
}
function New-HTMLGroupOfDiagramHierarchical {
    [cmdletBinding()]
    param(
        [Array] $Identity,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [switch] $Online
    )
    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
                    }
                }
            }
        }
    }
}
function New-HTMLGroupOfDiagramSummary {
    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [string] $DataTableID,
        [int] $ColumnID,
        [switch] $Online
    )
    $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
                }
            }
        }
    }
}
function New-HTMLGroupOfDiagramSummaryHierarchical {
    [cmdletBinding()]
    param(
        [Array] $ADGroup,
        [ValidateSet('Default', 'Hierarchical', 'Both')][string] $HideAppliesTo = 'Both',
        [switch] $HideComputers,
        [switch] $HideUsers,
        [switch] $HideOther,
        [switch] $Online
    )
    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
                    }
                }
            }
        }
    }
}
function New-HTMLReportADEssentials {
    [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 {
    [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 {
    [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)"
        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
            Set-ADObject -Identity $ACL.DistinguishedName -Replace @{ ntSecurityDescriptor = $ntSecurityDescriptor } -ProtectedFromAccidentalDeletion $true -ErrorAction Stop -Server $QueryServer
            $AfterTemporaryObject = Get-ADObject -Identity $ACL.DistinguishedName -Properties ProtectedFromAccidentalDeletion
            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."
                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)"
        }
    } 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 {
    [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
}
function Test-ADSubnet {
    [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 {
    [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-IPIsInNetwork {
    [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-LDAPCertificate {
    [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 {
    [CmdletBinding()]
    param(
        [string] $ServerName,
        [int] $Port
    )
    if ($ServerName -and $Port -ne 0) {
        Write-Verbose "Test-LDAPPorts - Processing $ServerName / $Port"
        try {
            $LDAP = "LDAP://" + $ServerName + ':' + $Port
            $Connection = [ADSI]($LDAP)
            $Connection.Close()
            [PSCustomObject] @{
                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 $Server`:$Port. Error: $ErrorMessage"
            } else {
                Write-Warning -Message "Test-LDAPPorts - Error: $ErrorMessage"
            }
            [PSCustomObject] @{
                Computer     = $ServerName
                Port         = $Port
                Status       = $false
                ErrorMessage = $ErrorMessage
            }
        }
    }
}
function Test-LdapServer {
    [cmdletBinding()]
    param(
        [string] $ServerName,
        [string] $Computer,
        [PSCustomObject] $Advanced
    )
    if ($ServerName -notlike '*.*') {
        # $FQDN = $false
        # 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' }
        $GlobalCatalogNonSSL = Test-LDAPPorts -ServerName $ServerName -Port $GCPortLDAP
        $ConnectionLDAPS = [PSCustomObject] @{ Status = $false; ErrorMessage = 'No FQDN' }
        $ConnectionLDAP = Test-LDAPPorts -ServerName $ServerName -Port $PortLDAP

        $PortsThatWork = @(
            if ($GlobalCatalogNonSSL.Status) { $GCPortLDAP }
            if ($GlobalCatalogSSL.Status) { $GCPortLDAPSSL }
            if ($ConnectionLDAP.Status) { $PortLDAP }
            if ($ConnectionLDAPS.Status) { $PortLDAPS }
        ) | Sort-Object
    } else {
        #$FQDN = $true
        $GlobalCatalogSSL = Test-LDAPPorts -ServerName $ServerName -Port $GCPortLDAPSSL
        $GlobalCatalogNonSSL = Test-LDAPPorts -ServerName $ServerName -Port $GCPortLDAP
        $ConnectionLDAPS = Test-LDAPPorts -ServerName $ServerName -Port $PortLDAPS
        $ConnectionLDAP = Test-LDAPPorts -ServerName $ServerName -Port $PortLDAP

        $PortsThatWork = @(
            if ($GlobalCatalogNonSSL.Status) { $GCPortLDAP }
            if ($GlobalCatalogSSL.Status) { $GCPortLDAPSSL }
            if ($ConnectionLDAP.Status) { $PortLDAP }
            if ($ConnectionLDAPS.Status) { $PortLDAPS }
        ) | Sort-Object
    }
    if ($VerifyCertificate) {
        $Output = [ordered] @{
            Computer                = $ServerName
            Site                    = $Advanced.Site
            IsRO                    = $Advanced.IsReadOnly
            IsGC                    = $Advanced.IsGlobalCatalog
            GlobalCatalogLDAP       = $GlobalCatalogNonSSL.Status
            GlobalCatalogLDAPS      = $GlobalCatalogSSL.Status
            GlobalCatalogLDAPSBind  = $null
            LDAP                    = $ConnectionLDAP.Status
            LDAPS                   = $ConnectionLDAPS.Status
            LDAPSBind               = $null
            AvailablePorts          = $PortsThatWork -join ','
            X509NotBeforeDays       = $null
            X509NotAfterDays        = $null
            X509DnsNameList         = $null
            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
        }
    } else {
        $Output = [ordered] @{
            Computer               = $ServerName
            Site                   = $Advanced.Site
            IsRO                   = $Advanced.IsReadOnly
            IsGC                   = $Advanced.IsGlobalCatalog
            GlobalCatalogLDAP      = $GlobalCatalogNonSSL.Status
            GlobalCatalogLDAPS     = $GlobalCatalogSSL.Status
            GlobalCatalogLDAPSBind = $null
            LDAP                   = $ConnectionLDAP.Status
            LDAPS                  = $ConnectionLDAPS.Status
            LDAPSBind              = $null
            AvailablePorts         = $PortsThatWork -join ','
            OperatingSystem        = $Advanced.OperatingSystem
            IPV4Address            = $Advanced.IPV4Address
            IPV6Address            = $Advanced.IPV6Address
        }
    }
    if ($VerifyCertificate) {
        if ($psboundparameters.ContainsKey("Credential")) {
            $Certificate = Test-LDAPCertificate -Computer $ServerName -Port $PortLDAPS -Credential $Credential
            $CertificateGC = Test-LDAPCertificate -Computer $ServerName -Port $GCPortLDAPSSL -Credential $Credential
        } else {
            $Certificate = Test-LDAPCertificate -Computer $ServerName -Port $PortLDAPS
            $CertificateGC = Test-LDAPCertificate -Computer $ServerName -Port $GCPortLDAPSSL
        }
        $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')
    }
    if (-not $Advanced) {
        $Output.Remove('IPV4Address')
        $Output.Remove('OperatingSystem')
        $Output.Remove('IPV6Address')
        $Output.Remove('Site')
        $Output.Remove('IsRO')
        $Output.Remove('IsGC')
    }
    [PSCustomObject] $Output
}
function Add-ADACL {
    [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 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 {
    [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.IsPresentF
    }
}
function Enable-ADACLInheritance {
    [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 {
    [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 Get-ADACL {
    [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 {
    [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-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
 
    .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
    )
    $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 "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 "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 -SearchBase $Structure.DistinguishedName -Filter { ObjectClass -eq 'container' } -Properties canonicalName -Server $Server -SearchScope Subtree
                $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 "Get-WinADACLForest - [Skip ]$ObjectName[ObjectClass not requested]"
                continue
            }
            if (-not $Containers) {
                $EndTime = Stop-TimeLog -Time $Time -Option OneLiner
                Write-Verbose "Get-WinADACLForest - [End ]$ObjectName[$EndTime]"
                continue
            }
            Write-Verbose "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 ($Separate) {
                $Output[$Domain][$ObjectOutputName] = $MYACL
            } else {
                $MYACL
            }
            $EndTime = Stop-TimeLog -Time $Time -Option OneLiner
            Write-Verbose "Get-WinADACLForest - [End ]$ObjectName[$EndTime]"
        }
        $DomainEndTime = Stop-TimeLog -Time $DomainTime -Option OneLiner
        Write-Verbose "Get-WinADACLForest - [End ][Domain $Domain][$DomainEndTime]"
    }
    $ForestEndTime = Stop-TimeLog -Time $ForestTime -Option OneLiner
    Write-Verbose "Get-WinADACLForest - [End ][Forest][$ForestEndTime]"
    if ($Separate) {
        $Output
    }
}
function Get-WinADBitlockerLapsSummary {
    [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
    if ($ComputerProperties.Name -contains 'ms-Mcs-AdmPwd') {
        $LapsAvailable = $true
        $Properties = @(
            'Name'
            'OperatingSystem'
            'OperatingSystemVersion'
            'DistinguishedName'
            'LastLogonDate'
            'PasswordLastSet'
            'ms-Mcs-AdmPwd'
            'ms-Mcs-AdmPwdExpirationTime'
            'PrimaryGroupID'
        )
    } else {
        $LapsAvailable = $false
        $Properties = @(
            'Name'
            'OperatingSystem'
            'OperatingSystemVersion'
            'DistinguishedName'
            'LastLogonDate'
            'PasswordLastSet'
            'PrimaryGroupID'
        )
    }
    $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-AdmPwd') {
                    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 = 'N/A'
                }
            }
            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
                    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 ($Computer.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 ($Computer.PrimaryGroupID -in 516, 521) { $true } else { $false }
                    Encrypted               = $Encrypted
                    EncryptedTime           = $EncryptedTime
                    Laps                    = $Laps
                    LapsExpirationDays      = $LapsExpirationDays
                    LapsExpirationTime      = $LapsExpirationTime
                    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

            foreach ($ACL in $ACLs) {
                if ($ACL.ObjectTypeName -eq 'ms-Mcs-AdmPwd') {
                    if ($ACL.AccessControlType -eq 'Allow' -and $ACL.ActiveDirectoryRights -like '*WriteProperty*') {
                        $LAPS = $true
                    }
                } elseif ($ACL.ObjectTypeName -eq 'ms-Mcs-AdmPwdExpirationTime') {
                    if ($ACL.AccessControlType -eq 'Allow' -and $ACL.ActiveDirectoryRights -like '*WriteProperty*') {
                        $LAPSExpirationTime = $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
                OperatingSystem      = $Computer.OperatingSystem
                Level0               = $Region
                Level1               = $Country
                DistinguishedName    = $Computer.DistinguishedName
                LastLogonDate        = $Computer.LastLogonDate
                PasswordLastSet      = $Computer.PasswordLastSet
                ServicePrincipalName = $Computer.servicePrincipalName
            }

        }
    }
}
function Get-WinADComputers {
    [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
                    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
                    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 {
        $Output.Values
    }
}
function Get-WinADDelegatedAccounts {
    [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 {
    [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-WinADDHCP {
    [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
    Short description
 
    .DESCRIPTION
    Long description
 
    .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
 
    .EXAMPLE
    An example
 
    .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,
        [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-WinADDomain {
    [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-WinADDuplicateObject {
    [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
    foreach ($Domain in $ForestInformation.Domains) {
        $DC = $ForestInformation['QueryServers']["$Domain"].HostName[0]
        #Get conflict objects
        $getADObjectSplat = @{
            LDAPFilter  = "(|(cn=*\0ACNF:*)(ou=*CNF:*))"
            Properties  = 'DistinguishedName', 'ObjectClass', 'DisplayName', 'SamAccountName', 'Name', 'ObjectCategory', 'WhenCreated', 'WhenChanged', 'ProtectedFromAccidentalDeletion', 'ObjectGUID'
            Server      = $DC
            SearchScope = 'Subtree'
        }
        $Objects = Get-ADObject @getADObjectSplat
        foreach ($_ in $Objects) {
            # Lets allow users to filter on it
            if ($ExcludeObjectClass) {
                if ($ExcludeObjectClass -contains $_.ObjectClass) {
                    continue
                }
            }
            if ($IncludeObjectClass) {
                if ($IncludeObjectClass -notcontains $_.ObjectClass) {
                    continue
                }
            }
            if ($PartialMatchDistinguishedName) {
                if ($_.DistinguishedName -notlike $PartialMatchDistinguishedName) {
                    continue
                }
            }
            if ($NoPostProcessing) {
                $_
                continue
            }
            $DomainName = ConvertFrom-DistinguishedName -DistinguishedName $_.DistinguishedName -ToDomainCN
            # Lets create separate objects for different purpoeses
            $ConflictObject = [ordered] @{
                ConflictDN          = $_.DistinguishedName
                ConflictWhenChanged = $_.WhenChanged
                DomainName          = $DomainName
                ObjectClass         = $_.ObjectClass
            }
            $LiveObjectData = [ordered] @{
                LiveDn          = "N/A"
                LiveWhenChanged = "N/A"
            }
            $RestData = [ordered] @{
                DisplayName                     = $_.DisplayName
                Name                            = $_.Name.Replace("`n", ' ')
                SamAccountName                  = $_.SamAccountName
                ObjectCategory                  = $_.ObjectCategory
                WhenCreated                     = $_.WhenCreated
                WhenChanged                     = $_.WhenChanged
                ProtectedFromAccidentalDeletion = $_.ProtectedFromAccidentalDeletion
                ObjectGUID                      = $_.ObjectGUID.Guid
            }
            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 {
    [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 {
    [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'
        )
        $DCs = Get-ADComputer -Server $QueryServer -SearchBase $ForestInformation['DomainsExtended'][$Domain].DomainControllersContainer -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
            }

            $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
                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
                UseDESKeyOnly              = $DC.UseDESKeyOnly
                WhenCreated                = $DC.WhenCreated
                WhenChanged                = $DC.WhenChanged
                DistinguishedName          = $DC.DistinguishedName
            }
        }
    }
}

function Get-WinADForestOptionalFeatures {
    [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'
    $OptionalFeatures = $(Get-ADOptionalFeature -Filter * -Server $QueryServer)
    $Optional = [ordered]@{
        'Recycle Bin Enabled'                          = $false
        'Privileged Access Management Feature Enabled' = $false
        'Laps Enabled'                                 = ($ComputerProperties.Name -contains $LapsProperties)
    }
    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 {
    [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-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, 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 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 {
    [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 {
    [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 {
    [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 $Subnets
    }
}
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) {
            #if ($ClearCache) {
            # This is to distinguish globally used cache and standard cache
            # As it's entirely possible user used standard approach without cache and then enabled cache so we need to track whether that is the case
            # $Script:WinADGroupMemberCacheGlobal = $false
            #}
            $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"
                        }
                    }
                }

                #if ($CountMembers) {
                # 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 (-not $CircularGroups[$NestedMember.DistinguishedName]) {
                        # $CircularGroups[$NestedMember.DistinguishedName] = $Nesting
                        #} else {
                        # Write-Verbose "Shit... $($CircularGroups[$NestedMember.DistinguishedName])"
                        #}

                        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
                        $OutputFromGroup
                        #if ($CountMembers) {
                        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 ($Output.Count -gt 0) {
        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 {
    [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 = @{}
            #$Forest = [System.DirectoryServices.ActiveDirectory.Forest]::GetCurrentForest()
            #$Script:WinADForestCache = @{
            # Forest = $Forest
            # Domains = $Forest.Domains.Name
            #}
        }
    }
    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 {
    [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-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 {
    [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 {
    [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', '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 {
                            [Array] $Members = foreach ($Member in $GroupMembers) {
                                if ($Member.DistinguishedName) {
                                    $Member.DistinguishedName
                                } elseif ($Member.DisplayName) {
                                    $Member.DisplayName
                                } else {
                                    $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 -eq 'group') { $null } else { $UAC -notcontains 'ACCOUNTDISABLE' }
                    PasswordNeverExpire = if ($ObjectClass -eq 'group') { $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-WinADPrivilegedObjects {
    [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 = @{}
    foreach ($Group in $CriticalGroups) {
        $Members = Get-WinADGroupMember -Identity $Group.distinguishedname -Verbose:$false -All
        foreach ($Member in $Members) {
            if (-not $CacheCritical[$Member.DistinguishedName]) {
                $CacheCritical[$Member.DistinguishedName] = [System.Collections.Generic.List[string]]::new()
            }
            $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, 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
 
    .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
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER ADUser
    ADUser Object
 
    .PARAMETER RemovePrefix
    Removes prefix from proxy address such as SMTP: or smtp:
 
    .PARAMETER ToLower
    Makes sure all returned data is lower case
 
    .PARAMETER Formatted
    Makes sure data is formatted for display, rather than for working with objects
 
    .PARAMETER Splitter
    Splitter or Joiner that connects data together such as an array of 3 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
    General notes
    #>

    [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 {
    [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 {
    [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 {
    [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-WinADSiteLinks {
    [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-WinADTomebstoneLifetime {
    [Alias('Get-WinADForestTomebstoneLifetime')]
    [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) {
        [PSCustomObject] @{
            TombstoneLifeTime = 60
        }
    } else {
        [PSCustomObject] @{
            TombstoneLifeTime = $Output.tombstoneLifetime
        }
    }
}
function Get-WinADTrust {
    [alias('Get-WinADTrusts')]
    [cmdletBinding()]
    param(
        [string] $Forest,
        #[alias('Domain')][string[]] $IncludeDomains,
        #[string[]] $ExcludeDomains,
        [switch] $Recursive,
        # [System.Collections.IDictionary] $ExtendedForestInformation,
        [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 {
    [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 {
    [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 {
    [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', 'Description',
            'PasswordNeverExpires', 'PasswordNotRequired', 'PasswordExpired', 'UserPrincipalName', 'SamAccountName', 'CannotChangePassword',
            'TrustedForDelegation', 'TrustedToAuthForDelegation', 'msExchMailboxGuid', 'msExchRemoteRecipientType', 'msExchRecipientTypeDetails',
            'msExchRecipientDisplayType', 'pwdLastSet', "msDS-UserPasswordExpiryTimeComputed",
            'WhenCreated', 'WhenChanged'
            'nTSecurityDescriptor'
        )
        $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
    }

    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 ($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 ($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 }
                    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
                    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 }
                    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
                    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 {
    [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 {
    [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 Get-WinDNSIPAddresses {
    <#
    .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-WinDNSIPAddresses | Format-Table *
 
    .EXAMPLE
    Get-WinDNSIPAddresses -Prettify | Format-Table *
 
    .EXAMPLE
    Get-WinDNSIPAddresses -Prettify -IncludeDetails -IncludeDNSRecords | Format-Table *
 
    .NOTES
    General notes
    #>

    [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-WinDNSIPAddresses - 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-WinDNSIPAddresses - 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' ) }
        foreach ($Zone in $ZonesToProcess) {
            $ADRecordsPerZone[$Zone.ZoneName] = [ordered]@{}
            Write-Verbose -Message "Get-WinDNSIPAddresses - 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-WinDNSIPAddresses - 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-WinDNSIPAddresses - Error getting AD records for ForestDnsZones zone: $($Zone.ZoneName). Error: $($_.Exception.Message)"
                    }
                } else {
                    Write-Warning -Message "Get-WinDNSIPAddresses - 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-WinDNSRecords {
    <#
    .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
    #>

    [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-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
    }
    if ($IncludeDetails) {
        $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 {
                        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-WinDNSRecords - 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-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
            }
        }
    }
    foreach ($Zone in $DNSRecordsPerZone.PSBase.Keys) {
        foreach ($Record in $DNSRecordsPerZone[$Zone]) {
            if ($Record.HostName -in $Exclusions) {
                continue
            }
            if (-not $DNSRecordsCached["$($Record.HostName).$($Zone)"]) {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"] = [ordered] @{
                    'HostName' = $Record.HostName
                    'Zone'     = $Zone
                    #'RecordType' = $Record.RecordType
                    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["$($Record.HostName).$($Zone)"].WhenCreated = $ADRecordsPerZone[$Zone][$Record.HostName].whenCreated
                    $DNSRecordsCached["$($Record.HostName).$($Zone)"].WhenChanged = $ADRecordsPerZone[$Zone][$Record.HostName].whenChanged
                }
                if ($IncludeDNSRecords) {
                    $DNSRecordsCached["$($Record.HostName).$($Zone)"].List = [System.Collections.Generic.List[Object]]::new()
                }
            }
            if ($IncludeDNSRecords) {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"].List.Add($Record)
            }
            if ($null -ne $Record.TimeStamp) {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"].Timestamps.Add($Record.TimeStamp)
            } else {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"].Timestamps.Add("Not available")
            }
            $DNSRecordsCached["$($Record.HostName).$($Zone)"].RecordIP.Add($Record.RecordData.IPv4Address)
            if ($Null -ne $Record.Timestamp) {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"].Types.Add('Dynamic')
            } else {
                $DNSRecordsCached["$($Record.HostName).$($Zone)"].Types.Add('Static')
            }
            $DNSRecordsCached["$($Record.HostName).$($Zone)"] = [PSCustomObject] $DNSRecordsCached["$($Record.HostName).$($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 Invoke-ADEssentials {
    [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
        }
    }


    #$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 * -Server $QueryServer -Properties $Properties
    # }
    # if (-not $Script:Cache) {
    # $Script:Cache = @{}
    # foreach ($Domain in $AllUsers.Keys) {
    # foreach ($U in $AllUsers[$Domain]) {
    # $Script:Cache[$U.DistinguishedName] = $U
    # }
    # }
    # #foreach ($Domain in $AllComputers.Keys) {
    # # foreach ($C in $AllComputers[$Domain]) {
    # # $Script:Cache[$C.DistinguishedName] = $C
    # # }
    # #}
    # }


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

[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 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 {
    [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
    Short description
 
    .DESCRIPTION
    Long description
 
    .PARAMETER ADObject
    Parameter description
 
    .PARAMETER ACL
    Parameter description
 
    .PARAMETER Principal
    Parameter description
 
    .PARAMETER AccessRule
    Parameter description
 
    .PARAMETER AccessControlType
    Parameter description
 
    .PARAMETER IncludeObjectTypeName
    Parameter description
 
    .PARAMETER IncludeInheritedObjectTypeName
    Parameter description
 
    .PARAMETER InheritanceType
    Parameter description
 
    .PARAMETER Force
    Breaks inheritance on the ACL when the rule has IsInherited set to $true. By default it will skip inherited rules
 
    .EXAMPLE
    An example
 
    .NOTES
    General notes
    #>

    [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-WinADDuplicateObject {
    [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
            } 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.DomainName
        } 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 {
    [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 {
    [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 {
    [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 {
    [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 Set-ADACL {
    [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')

    $FoundDisprepancy = $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)"
                        $FoundDisprepancy = $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)"
                        $FoundDisprepancy = $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)"
                    $FoundDisprepancy = $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)"
                    $FoundDisprepancy = $true
                }
            }
        }
        $Count++
    }
    if ($FoundDisprepancy) {
        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 {
    [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
    }
    if ($ACL) {
        foreach ($A in $ACL) {

        }
    } 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"
            } elseif ($Inheritance -eq 'Disabled') {
                $Action = "Inheritance $Inheritance, RemoveInheritedAccessRules $RemoveInheritedAccessRules"
                $ACL.ACL.SetAccessRuleProtection($true, -not $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)"
                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) failed: $($_.Exception.Message)"
                }
            }
        }
    }
}
function Set-ADACLOwner {
    [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 {
    [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 {
    [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-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
    Parameter description
 
    .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 {
    [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 {
    [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 {
    [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 {
    [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
    )
    # Gather data
    $DNSByName = Get-WinDNSRecords -Prettify -IncludeDetails
    $DNSByIP = Get-WinDNSIPAddresses -Prettify -IncludeDetails

    # Create HTML :-)
    New-HTML {
        New-HTMLTab -Name "DNS by Name" {
            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
            } -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
 
    .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
    )

    $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
    } 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
 
    .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
    )
    $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
            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) {
                        $ADGroup = $GroupMembersCache[$Group]
                    } else {
                        $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
                }
                if (-not $SummaryOnly) {
                    if ($ADGroup) {
                        # Means group returned something
                        $GroupName = $ADGroup[0].GroupName
                        $NetBIOSName = Convert-DomainFqdnToNetBIOS -DomainName $ADGroup[0].DomainName
                        $FullName = "$NetBIOSName\$GroupName"
                    } else {
                        # Means group returned nothing, probably wrong request, but we still need to show something
                        $GroupName = $Group
                        $FullName = $Group
                    }
                    $DataStoreID = -join ('table', (Get-RandomStringName -Size 10 -ToLower))
                    $DataTableID = -join ('table', (Get-RandomStringName -Size 10 -ToLower))
                    New-HTMLTab -TabName $FullName {
                        $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
                                }
                            }
                        }
                        if (-not $SkipDiagram.IsPresent) {
                            New-HTMLTab -TabName 'Information' {
                                $SectionInformation
                            }
                        } else {
                            $SectionInformation
                        }
                        if (-not $SkipDiagram.IsPresent) {
                            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
                                }
                            }
                            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
                                }
                            }
                        }
                    }
                }
            }
            if (-not $SkipDiagram.IsPresent -and ($Summary -or $SummaryOnly)) {
                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
                        }
                    }
                    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:$Online -FilePath $FilePath -ShowHTML:(-not $HideHTML)
    } 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.
 
    .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
    )
    $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
        New-HTMLTabStyle -BorderRadius 0px -TextTransform capitalize -BackgroundColorActive SlateGrey
        foreach ($ADObject in $Identity) {
            if ($null -eq $ADObject) {
                continue
            }
            try {
                Write-Verbose "Show-WinADObjectMember - requesting $Identity member of 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
            }
            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 {
                    $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 CrossForest -Operator eq
                                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
                        }
                        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
                            }
                            #New-HTMLSection -Title "Group membership table $GroupName" {
                            # New-HTMLTable -DataTable $ADGroup -Filtering -DataStoreID $DataStoreID -DataTableID $DataTableID
                            #}
                        }
                        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
                            }
                            #New-HTMLSection -Title "Group membership table $GroupName" {
                            # New-HTMLTable -DataTable $ADGroup -Filtering -DataStoreID $DataStoreID
                            #}
                        }
                    }
                }
            }
        }
        if (-not $SkipDiagram.IsPresent -and ($Summary -or $SummaryOnly)) {
            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
                    }
                }
                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
                    }
                }
            }
        }
    } -Online:$Online -FilePath $FilePath -ShowHTML:(-not $HideHTML)
}
function Show-WinADOrganization {
    [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 {
    [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 {
    [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


        #$Messages = $($ADTrusts = Get-WinADTrust -Recursive:$Recursive) 4>&1 3>&1 2>&1
        #$Messages += Write-Verbose "Show-WinADTrust - Found $($ADTrusts.Count) trusts" 4>&1
        $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 #-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 $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) {
            #$Messages += Write-Verbose "Show-WinADTrust - Processing $($Trust.TrustSource) to $($Trust.TrustTarget)" 4>&1
            Write-Verbose "Show-WinADTrust - Processing $($Trust.TrustSource) to $($Trust.TrustTarget)"
            if (-not $TrustCache[$Trust.TrustSource]) {
                #$Messages += Write-Verbose "Show-WinADTrust - Creating cache for $($Trust.TrustSource)" 4>&1
                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
                                        }
                                    }
                                }
                            }
                        }

                    }
                }
            }
        }
        #New-HTMLTab -TabName "Logs" {
        # New-HTMLTable -DataTable ($Messages.Message)
        #}
    } -Online:$Online -FilePath $FilePath -ShowHTML:(-not $HideHTML)
    if ($PassThru) {
        $Script:ADTrusts
    }
}
function Show-WinADUserSecurity {
    [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-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 {
    [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 {
    [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 }
        }
    }
}

#Test-ADRolesAvailability
function Test-ADSiteLinks {
    [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 {
    [cmdletBinding()]
    param(
        [string] $DomainController,
        [string] $Domain
    )
    if ($DomainController) {
        $AllDomainControllers = (Get-ADDomainController -Server $Domain -Filter { IsReadOnly -eq $false } ).HostName
        try {
            $Hosts = Get-DnsServerResourceRecord -ZoneName $Domain -ComputerName $DomainController -RRType NS -ErrorAction Stop
            $NameServers = (($Hosts | Where-Object { $_.HostName -eq '@' }).RecordData.NameServer) -replace ".$"
            $Compare = ((Compare-Object -ReferenceObject $AllDomainControllers -DifferenceObject $NameServers -IncludeEqual).SideIndicator -notin @('=>', '<='))

            [PSCustomObject] @{
                DomainControllers = $AllDomainControllers
                NameServers       = $NameServers
                Status            = $Compare
                Comment           = "Name servers found $($NameServers -join ', ')"
            }
        } catch {
            [PSCustomObject] @{
                DomainControllers = $AllDomainControllers
                NameServers       = $null
                Status            = $false
                Comment           = $_.Exception.Message
            }
        }

    }
}
function Test-FSMORolesAvailability {
    [cmdletBinding()]
    param(
        [string] $Domain = $Env:USERDNSDOMAIN
    )
    $DC = Get-ADDomainController -Server $Domain -Filter *
    $Output = foreach ($S in $DC) {
        if ($S.OperationMasterRoles.Count -gt 0) {
            $Status = Test-Connection -ComputerName $S.HostName -Count 2 -Quiet
        } else {
            $Status = $null
        }
        #$Summary["$($S.HostName)"] = @{ }
        foreach ($_ in $S.OperationMasterRoles) {
            #$Summary["$_"] = $S.HostName
            [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, 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 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
 
    .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
    )
    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) {
                Write-Verbose "Test-LDAP - Processing $Computer"
                $ServerName = ConvertTo-ComputerFQDN -Computer $Computer
                Test-LdapServer -ServerName $ServerName -Computer $Computer
            }
        } else {
            foreach ($Computer in $ForestInformation.ForestDomainControllers) {
                Write-Verbose "Test-LDAP - Processing $($Computer.HostName)"
                Test-LdapServer -ServerName $($Computer.HostName) -Computer $Computer.HostName -Advanced $Computer
            }
        }
    }
}
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
            }
        }
    }
}


$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-WinADForestTomebstoneLifetime'
        'Get-WinADTrustLegacy'                       = ''
        'Get-WinADUserPrincipalName'                 = ''
        'Get-WinADUsers'                             = ''
        'Get-WinADUsersForeignSecurityPrincipalList' = 'Get-WinADUsersFP'
        'Get-WinADWellKnownFolders'                  = ''
        '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'                                  = ''
    }
    DHCPServer      = @{
        'Get-WinADDHCP' = ''
    }
    DNSServer       = @{
        'Get-WinDNSRecords' = ''
    }
}
[Array] $FunctionsAll = 'Add-ADACL', 'Copy-ADOUSecurity', 'Disable-ADACLInheritance', 'Enable-ADACLInheritance', 'Export-ADACLObject', 'Get-ADACL', 'Get-ADACLOwner', 'Get-DNSServerIP', 'Get-WinADACLConfiguration', 'Get-WinADACLForest', 'Get-WinADBitlockerLapsSummary', 'Get-WinADComputerACLLAPS', 'Get-WinADComputers', 'Get-WinADDelegatedAccounts', 'Get-WinADDFSHealth', 'Get-WinADDHCP', 'Get-WinADDiagnostics', 'Get-WinADDomain', 'Get-WinADDuplicateObject', 'Get-WinADDuplicateSPN', 'Get-WinADForest', 'Get-WinADForestControllerInformation', 'Get-WinADForestOptionalFeatures', 'Get-WinADForestReplication', 'Get-WinADForestRoles', 'Get-WinADForestSchemaProperties', 'Get-WinADForestSites', 'Get-WinADForestSubnet', 'Get-WinADGroupMember', 'Get-WinADGroupMemberOf', 'Get-WinADGroups', 'Get-WinADLastBackup', 'Get-WinADLDAPBindingsSummary', 'Get-WinADLMSettings', 'Get-WinADObject', 'Get-WinADPrivilegedObjects', 'Get-WinADProtocol', 'Get-WinADProxyAddresses', 'Get-WinADServiceAccount', 'Get-WinADSharePermission', 'Get-WinADSiteConnections', 'Get-WinADSiteLinks', 'Get-WinADTomebstoneLifetime', 'Get-WinADTrust', 'Get-WinADTrustLegacy', 'Get-WinADUserPrincipalName', 'Get-WinADUsers', 'Get-WinADUsersForeignSecurityPrincipalList', 'Get-WinADWellKnownFolders', 'Get-WinDNSIPAddresses', 'Get-WinDNSRecords', 'Invoke-ADEssentials', 'New-ADACLObject', 'New-ADSite', 'Remove-ADACL', 'Remove-WinADDuplicateObject', 'Remove-WinADSharePermission', 'Rename-WinADUserPrincipalName', 'Repair-WinADACLConfigurationOwner', 'Repair-WinADEmailAddress', 'Repair-WinADForestControllerInformation', 'Set-ADACL', 'Set-ADACLInheritance', 'Set-ADACLOwner', 'Set-DnsServerIP', 'Set-WinADDiagnostics', 'Set-WinADForestACLOwner', 'Set-WinADReplication', 'Set-WinADReplicationConnections', 'Set-WinADShare', 'Set-WinADTombstoneLifetime', 'Show-WinADDNSRecords', 'Show-WinADGroupCritical', 'Show-WinADGroupMember', 'Show-WinADGroupMemberOf', 'Show-WinADOrganization', 'Show-WinADSites', 'Show-WinADTrust', 'Show-WinADUserSecurity', 'Sync-DomainController', 'Test-ADDomainController', 'Test-ADRolesAvailability', 'Test-ADSiteLinks', 'Test-DNSNameServers', 'Test-FSMORolesAvailability', 'Test-LDAP', 'Test-WinADVulnerableSchemaClass'
[Array] $AliasesAll = 'Get-WinADDomainRoles', 'Get-WinADForestObjectsConflict', 'Get-WinADForestTomebstoneLifetime', 'Get-WinADPriviligedObjects', 'Get-WinADRoles', 'Get-WinADSubnet', 'Get-WinADTrusts', 'Get-WinADUsersFP', 'Get-WinDNSServerIP', 'Set-WinDNSServerIP', 'Show-ADGroupMember', 'Show-ADGroupMemberOf', 'Show-ADTrust', 'Show-ADTrusts', 'Show-WinADCriticalGroups', 'Show-WinADTrusts'
$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-ModuleMember -Function @($FunctionsToLoad) -Alias @($AliasesToLoad)
# SIG # Begin signature block
# MIInPgYJKoZIhvcNAQcCoIInLzCCJysCAQExDzANBglghkgBZQMEAgEFADB5Bgor
# BgEEAYI3AgEEoGswaTA0BgorBgEEAYI3AgEeMCYCAwEAAAQQH8w7YFlLCE63JNLG
# KX7zUQIBAAIBAAIBAAIBAAIBADAxMA0GCWCGSAFlAwQCAQUABCCD/MBVzcaMaK5e
# fhTj/4jWgE5Vs7vmTaJjh/f9ARgOg6CCITcwggO3MIICn6ADAgECAhAM5+DlF9hG
# /o/lYPwb8DA5MA0GCSqGSIb3DQEBBQUAMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQK
# EwxEaWdpQ2VydCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNV
# BAMTG0RpZ2lDZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTAeFw0wNjExMTAwMDAwMDBa
# Fw0zMTExMTAwMDAwMDBaMGUxCzAJBgNVBAYTAlVTMRUwEwYDVQQKEwxEaWdpQ2Vy
# dCBJbmMxGTAXBgNVBAsTEHd3dy5kaWdpY2VydC5jb20xJDAiBgNVBAMTG0RpZ2lD
# ZXJ0IEFzc3VyZWQgSUQgUm9vdCBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
# AQoCggEBAK0OFc7kQ4BcsYfzt2D5cRKlrtwmlIiq9M71IDkoWGAM+IDaqRWVMmE8
# tbEohIqK3J8KDIMXeo+QrIrneVNcMYQq9g+YMjZ2zN7dPKii72r7IfJSYd+fINcf
# 4rHZ/hhk0hJbX/lYGDW8R82hNvlrf9SwOD7BG8OMM9nYLxj+KA+zp4PWw25EwGE1
# lhb+WZyLdm3X8aJLDSv/C3LanmDQjpA1xnhVhyChz+VtCshJfDGYM2wi6YfQMlqi
# uhOCEe05F52ZOnKh5vqk2dUXMXWuhX0irj8BRob2KHnIsdrkVxfEfhwOsLSSplaz
# vbKX7aqn8LfFqD+VFtD/oZbrCF8Yd08CAwEAAaNjMGEwDgYDVR0PAQH/BAQDAgGG
# MA8GA1UdEwEB/wQFMAMBAf8wHQYDVR0OBBYEFEXroq/0ksuCMS1Ri6enIZ3zbcgP
# MB8GA1UdIwQYMBaAFEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBBQUA
# A4IBAQCiDrzf4u3w43JzemSUv/dyZtgy5EJ1Yq6H6/LV2d5Ws5/MzhQouQ2XYFwS
# TFjk0z2DSUVYlzVpGqhH6lbGeasS2GeBhN9/CTyU5rgmLCC9PbMoifdf/yLil4Qf
# 6WXvh+DfwWdJs13rsgkq6ybteL59PyvztyY1bV+JAbZJW58BBZurPSXBzLZ/wvFv
# hsb6ZGjrgS2U60K3+owe3WLxvlBnt2y98/Efaww2BxZ/N3ypW2168RJGYIPXJwS+
# S86XvsNnKmgR34DnDDNmvxMNFG7zfx9jEB76jRslbWyPpbdhAbHSoyahEHGdreLD
# +cOZUbcrBwjOLuZQsqf6CkUvovDyMIIFMDCCBBigAwIBAgIQBAkYG1/Vu2Z1U0O1
# b5VQCDANBgkqhkiG9w0BAQsFADBlMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGln
# aUNlcnQgSW5jMRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMSQwIgYDVQQDExtE
# aWdpQ2VydCBBc3N1cmVkIElEIFJvb3QgQ0EwHhcNMTMxMDIyMTIwMDAwWhcNMjgx
# MDIyMTIwMDAwWjByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5j
# MRkwFwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBT
# SEEyIEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMIIBIjANBgkqhkiG9w0BAQEF
# AAOCAQ8AMIIBCgKCAQEA+NOzHH8OEa9ndwfTCzFJGc/Q+0WZsTrbRPV/5aid2zLX
# cep2nQUut4/6kkPApfmJ1DcZ17aq8JyGpdglrA55KDp+6dFn08b7KSfH03sjlOSR
# I5aQd4L5oYQjZhJUM1B0sSgmuyRpwsJS8hRniolF1C2ho+mILCCVrhxKhwjfDPXi
# TWAYvqrEsq5wMWYzcT6scKKrzn/pfMuSoeU7MRzP6vIK5Fe7SrXpdOYr/mzLfnQ5
# Ng2Q7+S1TqSp6moKq4TzrGdOtcT3jNEgJSPrCGQ+UpbB8g8S9MWOD8Gi6CxR93O8
# vYWxYoNzQYIH5DiLanMg0A9kczyen6Yzqf0Z3yWT0QIDAQABo4IBzTCCAckwEgYD
# VR0TAQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYB
# BQUHAwMweQYIKwYBBQUHAQEEbTBrMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5k
# aWdpY2VydC5jb20wQwYIKwYBBQUHMAKGN2h0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0
# LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJvb3RDQS5jcnQwgYEGA1UdHwR6MHgwOqA4
# oDaGNGh0dHA6Ly9jcmw0LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydEFzc3VyZWRJRFJv
# b3RDQS5jcmwwOqA4oDaGNGh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dEFzc3VyZWRJRFJvb3RDQS5jcmwwTwYDVR0gBEgwRjA4BgpghkgBhv1sAAIEMCow
# KAYIKwYBBQUHAgEWHGh0dHBzOi8vd3d3LmRpZ2ljZXJ0LmNvbS9DUFMwCgYIYIZI
# AYb9bAMwHQYDVR0OBBYEFFrEuXsqCqOl6nEDwGD5LfZldQ5YMB8GA1UdIwQYMBaA
# FEXroq/0ksuCMS1Ri6enIZ3zbcgPMA0GCSqGSIb3DQEBCwUAA4IBAQA+7A1aJLPz
# ItEVyCx8JSl2qB1dHC06GsTvMGHXfgtg/cM9D8Svi/3vKt8gVTew4fbRknUPUbRu
# pY5a4l4kgU4QpO4/cY5jDhNLrddfRHnzNhQGivecRk5c/5CxGwcOkRX7uq+1UcKN
# JK4kxscnKqEpKBo6cSgCPC6Ro8AlEeKcFEehemhor5unXCBc2XGxDI+7qPjFEmif
# z0DLQESlE/DmZAwlCEIysjaKJAL+L3J+HNdJRZboWR3p+nRka7LrZkPas7CM1ekN
# 3fYBIM6ZMWM9CBoYs4GbT8aTEAb8B4H6i9r5gkn3Ym6hU/oSlBiFLpKR6mhsRDKy
# ZqHnGKSaZFHvMIIFPTCCBCWgAwIBAgIQBNXcH0jqydhSALrNmpsqpzANBgkqhkiG
# 9w0BAQsFADByMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMRGlnaUNlcnQgSW5jMRkw
# FwYDVQQLExB3d3cuZGlnaWNlcnQuY29tMTEwLwYDVQQDEyhEaWdpQ2VydCBTSEEy
# IEFzc3VyZWQgSUQgQ29kZSBTaWduaW5nIENBMB4XDTIwMDYyNjAwMDAwMFoXDTIz
# MDcwNzEyMDAwMFowejELMAkGA1UEBhMCUEwxEjAQBgNVBAgMCcWabMSFc2tpZTER
# MA8GA1UEBxMIS2F0b3dpY2UxITAfBgNVBAoMGFByemVteXPFgmF3IEvFgnlzIEVW
# T1RFQzEhMB8GA1UEAwwYUHJ6ZW15c8WCYXcgS8WCeXMgRVZPVEVDMIIBIjANBgkq
# hkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAv7KB3iyBrhkLUbbFe9qxhKKPBYqDBqln
# r3AtpZplkiVjpi9dMZCchSeT5ODsShPuZCIxJp5I86uf8ibo3vi2S9F9AlfFjVye
# 3dTz/9TmCuGH8JQt13ozf9niHecwKrstDVhVprgxi5v0XxY51c7zgMA2g1Ub+3ti
# i0vi/OpmKXdL2keNqJ2neQ5cYly/GsI8CREUEq9SZijbdA8VrRF3SoDdsWGf3tZZ
# zO6nWn3TLYKQ5/bw5U445u/V80QSoykszHRivTj+H4s8ABiforhi0i76beA6Ea41
# zcH4zJuAp48B4UhjgRDNuq8IzLWK4dlvqrqCBHKqsnrF6BmBrv+BXQIDAQABo4IB
# xTCCAcEwHwYDVR0jBBgwFoAUWsS5eyoKo6XqcQPAYPkt9mV1DlgwHQYDVR0OBBYE
# FBixNSfoHFAgJk4JkDQLFLRNlJRmMA4GA1UdDwEB/wQEAwIHgDATBgNVHSUEDDAK
# BggrBgEFBQcDAzB3BgNVHR8EcDBuMDWgM6Axhi9odHRwOi8vY3JsMy5kaWdpY2Vy
# dC5jb20vc2hhMi1hc3N1cmVkLWNzLWcxLmNybDA1oDOgMYYvaHR0cDovL2NybDQu
# ZGlnaWNlcnQuY29tL3NoYTItYXNzdXJlZC1jcy1nMS5jcmwwTAYDVR0gBEUwQzA3
# BglghkgBhv1sAwEwKjAoBggrBgEFBQcCARYcaHR0cHM6Ly93d3cuZGlnaWNlcnQu
# Y29tL0NQUzAIBgZngQwBBAEwgYQGCCsGAQUFBwEBBHgwdjAkBggrBgEFBQcwAYYY
# aHR0cDovL29jc3AuZGlnaWNlcnQuY29tME4GCCsGAQUFBzAChkJodHRwOi8vY2Fj
# ZXJ0cy5kaWdpY2VydC5jb20vRGlnaUNlcnRTSEEyQXNzdXJlZElEQ29kZVNpZ25p
# bmdDQS5jcnQwDAYDVR0TAQH/BAIwADANBgkqhkiG9w0BAQsFAAOCAQEAmr1sz4ls
# LARi4wG1eg0B8fVJFowtect7SnJUrp6XRnUG0/GI1wXiLIeow1UPiI6uDMsRXPHU
# F/+xjJw8SfIbwava2eXu7UoZKNh6dfgshcJmo0QNAJ5PIyy02/3fXjbUREHINrTC
# vPVbPmV6kx4Kpd7KJrCo7ED18H/XTqWJHXa8va3MYLrbJetXpaEPpb6zk+l8Rj9y
# G4jBVRhenUBUUj3CLaWDSBpOA/+sx8/XB9W9opYfYGb+1TmbCkhUg7TB3gD6o6ES
# Jre+fcnZnPVAPESmstwsT17caZ0bn7zETKlNHbc1q+Em9kyBjaQRcEQoQQNpezQu
# g9ufqExx6lHYDjCCBY0wggR1oAMCAQICEA6bGI750C3n79tQ4ghAGFowDQYJKoZI
# hvcNAQEMBQAwZTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZ
# MBcGA1UECxMQd3d3LmRpZ2ljZXJ0LmNvbTEkMCIGA1UEAxMbRGlnaUNlcnQgQXNz
# dXJlZCBJRCBSb290IENBMB4XDTIyMDgwMTAwMDAwMFoXDTMxMTEwOTIzNTk1OVow
# YjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290
# IEc0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAv+aQc2jeu+RdSjww
# IjBpM+zCpyUuySE98orYWcLhKac9WKt2ms2uexuEDcQwH/MbpDgW61bGl20dq7J5
# 8soR0uRf1gU8Ug9SH8aeFaV+vp+pVxZZVXKvaJNwwrK6dZlqczKU0RBEEC7fgvMH
# hOZ0O21x4i0MG+4g1ckgHWMpLc7sXk7Ik/ghYZs06wXGXuxbGrzryc/NrDRAX7F6
# Zu53yEioZldXn1RYjgwrt0+nMNlW7sp7XeOtyU9e5TXnMcvak17cjo+A2raRmECQ
# ecN4x7axxLVqGDgDEI3Y1DekLgV9iPWCPhCRcKtVgkEy19sEcypukQF8IUzUvK4b
# A3VdeGbZOjFEmjNAvwjXWkmkwuapoGfdpCe8oU85tRFYF/ckXEaPZPfBaYh2mHY9
# WV1CdoeJl2l6SPDgohIbZpp0yt5LHucOY67m1O+SkjqePdwA5EUlibaaRBkrfsCU
# tNJhbesz2cXfSwQAzH0clcOP9yGyshG3u3/y1YxwLEFgqrFjGESVGnZifvaAsPvo
# ZKYz0YkH4b235kOkGLimdwHhD5QMIR2yVCkliWzlDlJRR3S+Jqy2QXXeeqxfjT/J
# vNNBERJb5RBQ6zHFynIWIgnffEx1P2PsIV/EIFFrb7GrhotPwtZFX50g/KEexcCP
# orF+CiaZ9eRpL5gdLfXZqbId5RsCAwEAAaOCATowggE2MA8GA1UdEwEB/wQFMAMB
# Af8wHQYDVR0OBBYEFOzX44LScV1kTN8uZz/nupiuHA9PMB8GA1UdIwQYMBaAFEXr
# oq/0ksuCMS1Ri6enIZ3zbcgPMA4GA1UdDwEB/wQEAwIBhjB5BggrBgEFBQcBAQRt
# MGswJAYIKwYBBQUHMAGGGGh0dHA6Ly9vY3NwLmRpZ2ljZXJ0LmNvbTBDBggrBgEF
# BQcwAoY3aHR0cDovL2NhY2VydHMuZGlnaWNlcnQuY29tL0RpZ2lDZXJ0QXNzdXJl
# ZElEUm9vdENBLmNydDBFBgNVHR8EPjA8MDqgOKA2hjRodHRwOi8vY3JsMy5kaWdp
# Y2VydC5jb20vRGlnaUNlcnRBc3N1cmVkSURSb290Q0EuY3JsMBEGA1UdIAQKMAgw
# BgYEVR0gADANBgkqhkiG9w0BAQwFAAOCAQEAcKC/Q1xV5zhfoKN0Gz22Ftf3v1cH
# vZqsoYcs7IVeqRq7IviHGmlUIu2kiHdtvRoU9BNKei8ttzjv9P+Aufih9/Jy3iS8
# UgPITtAq3votVs/59PesMHqai7Je1M/RQ0SbQyHrlnKhSLSZy51PpwYDE3cnRNTn
# f+hZqPC/Lwum6fI0POz3A8eHqNJMQBk1RmppVLC4oVaO7KTVPeix3P0c2PR3WlxU
# jG/voVA9/HYJaISfb8rbII01YBwCA8sgsKxYoA5AY8WYIsGyWfVVa88nq2x2zm8j
# LfR+cWojayL/ErhULSd+2DrZ8LaHlv1b0VysGMNNn3O3AamfV6peKOK5lDCCBq4w
# ggSWoAMCAQICEAc2N7ckVHzYR6z9KGYqXlswDQYJKoZIhvcNAQELBQAwYjELMAkG
# A1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQd3d3LmRp
# Z2ljZXJ0LmNvbTEhMB8GA1UEAxMYRGlnaUNlcnQgVHJ1c3RlZCBSb290IEc0MB4X
# DTIyMDMyMzAwMDAwMFoXDTM3MDMyMjIzNTk1OVowYzELMAkGA1UEBhMCVVMxFzAV
# BgNVBAoTDkRpZ2lDZXJ0LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVk
# IEc0IFJTQTQwOTYgU0hBMjU2IFRpbWVTdGFtcGluZyBDQTCCAiIwDQYJKoZIhvcN
# AQEBBQADggIPADCCAgoCggIBAMaGNQZJs8E9cklRVcclA8TykTepl1Gh1tKD0Z5M
# om2gsMyD+Vr2EaFEFUJfpIjzaPp985yJC3+dH54PMx9QEwsmc5Zt+FeoAn39Q7SE
# 2hHxc7Gz7iuAhIoiGN/r2j3EF3+rGSs+QtxnjupRPfDWVtTnKC3r07G1decfBmWN
# lCnT2exp39mQh0YAe9tEQYncfGpXevA3eZ9drMvohGS0UvJ2R/dhgxndX7RUCyFo
# bjchu0CsX7LeSn3O9TkSZ+8OpWNs5KbFHc02DVzV5huowWR0QKfAcsW6Th+xtVhN
# ef7Xj3OTrCw54qVI1vCwMROpVymWJy71h6aPTnYVVSZwmCZ/oBpHIEPjQ2OAe3Vu
# JyWQmDo4EbP29p7mO1vsgd4iFNmCKseSv6De4z6ic/rnH1pslPJSlRErWHRAKKtz
# Q87fSqEcazjFKfPKqpZzQmiftkaznTqj1QPgv/CiPMpC3BhIfxQ0z9JMq++bPf4O
# uGQq+nUoJEHtQr8FnGZJUlD0UfM2SU2LINIsVzV5K6jzRWC8I41Y99xh3pP+OcD5
# sjClTNfpmEpYPtMDiP6zj9NeS3YSUZPJjAw7W4oiqMEmCPkUEBIDfV8ju2TjY+Cm
# 4T72wnSyPx4JduyrXUZ14mCjWAkBKAAOhFTuzuldyF4wEr1GnrXTdrnSDmuZDNIz
# tM2xAgMBAAGjggFdMIIBWTASBgNVHRMBAf8ECDAGAQH/AgEAMB0GA1UdDgQWBBS6
# FtltTYUvcyl2mi91jGogj57IbzAfBgNVHSMEGDAWgBTs1+OC0nFdZEzfLmc/57qY
# rhwPTzAOBgNVHQ8BAf8EBAMCAYYwEwYDVR0lBAwwCgYIKwYBBQUHAwgwdwYIKwYB
# BQUHAQEEazBpMCQGCCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20w
# QQYIKwYBBQUHMAKGNWh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2Vy
# dFRydXN0ZWRSb290RzQuY3J0MEMGA1UdHwQ8MDowOKA2oDSGMmh0dHA6Ly9jcmwz
# LmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRSb290RzQuY3JsMCAGA1UdIAQZ
# MBcwCAYGZ4EMAQQCMAsGCWCGSAGG/WwHATANBgkqhkiG9w0BAQsFAAOCAgEAfVmO
# wJO2b5ipRCIBfmbW2CFC4bAYLhBNE88wU86/GPvHUF3iSyn7cIoNqilp/GnBzx0H
# 6T5gyNgL5Vxb122H+oQgJTQxZ822EpZvxFBMYh0MCIKoFr2pVs8Vc40BIiXOlWk/
# R3f7cnQU1/+rT4osequFzUNf7WC2qk+RZp4snuCKrOX9jLxkJodskr2dfNBwCnzv
# qLx1T7pa96kQsl3p/yhUifDVinF2ZdrM8HKjI/rAJ4JErpknG6skHibBt94q6/ae
# sXmZgaNWhqsKRcnfxI2g55j7+6adcq/Ex8HBanHZxhOACcS2n82HhyS7T6NJuXdm
# kfFynOlLAlKnN36TU6w7HQhJD5TNOXrd/yVjmScsPT9rp/Fmw0HNT7ZAmyEhQNC3
# EyTN3B14OuSereU0cZLXJmvkOHOrpgFPvT87eK1MrfvElXvtCl8zOYdBeHo46Zzh
# 3SP9HSjTx/no8Zhf+yvYfvJGnXUsHicsJttvFXseGYs2uJPU5vIXmVnKcPA3v5gA
# 3yAWTyf7YGcWoWa63VXAOimGsJigK+2VQbc61RWYMbRiCQ8KvYHZE/6/pNHzV9m8
# BPqC3jLfBInwAM1dwvnQI38AC+R2AibZ8GV2QqYphwlHK+Z/GqSFD/yYlvZVVCsf
# gPrA8g4r5db7qS9EFUrnEw4d2zc4GqEr9u3WfPwwggbAMIIEqKADAgECAhAMTWly
# S5T6PCpKPSkHgD1aMA0GCSqGSIb3DQEBCwUAMGMxCzAJBgNVBAYTAlVTMRcwFQYD
# VQQKEw5EaWdpQ2VydCwgSW5jLjE7MDkGA1UEAxMyRGlnaUNlcnQgVHJ1c3RlZCBH
# NCBSU0E0MDk2IFNIQTI1NiBUaW1lU3RhbXBpbmcgQ0EwHhcNMjIwOTIxMDAwMDAw
# WhcNMzMxMTIxMjM1OTU5WjBGMQswCQYDVQQGEwJVUzERMA8GA1UEChMIRGlnaUNl
# cnQxJDAiBgNVBAMTG0RpZ2lDZXJ0IFRpbWVzdGFtcCAyMDIyIC0gMjCCAiIwDQYJ
# KoZIhvcNAQEBBQADggIPADCCAgoCggIBAM/spSY6xqnya7uNwQ2a26HoFIV0Mxom
# rNAcVR4eNm28klUMYfSdCXc9FZYIL2tkpP0GgxbXkZI4HDEClvtysZc6Va8z7GGK
# 6aYo25BjXL2JU+A6LYyHQq4mpOS7eHi5ehbhVsbAumRTuyoW51BIu4hpDIjG8b7g
# L307scpTjUCDHufLckkoHkyAHoVW54Xt8mG8qjoHffarbuVm3eJc9S/tjdRNlYRo
# 44DLannR0hCRRinrPibytIzNTLlmyLuqUDgN5YyUXRlav/V7QG5vFqianJVHhoV5
# PgxeZowaCiS+nKrSnLb3T254xCg/oxwPUAY3ugjZNaa1Htp4WB056PhMkRCWfk3h
# 3cKtpX74LRsf7CtGGKMZ9jn39cFPcS6JAxGiS7uYv/pP5Hs27wZE5FX/NurlfDHn
# 88JSxOYWe1p+pSVz28BqmSEtY+VZ9U0vkB8nt9KrFOU4ZodRCGv7U0M50GT6Vs/g
# 9ArmFG1keLuY/ZTDcyHzL8IuINeBrNPxB9ThvdldS24xlCmL5kGkZZTAWOXlLimQ
# prdhZPrZIGwYUWC6poEPCSVT8b876asHDmoHOWIZydaFfxPZjXnPYsXs4Xu5zGcT
# B5rBeO3GiMiwbjJ5xwtZg43G7vUsfHuOy2SJ8bHEuOdTXl9V0n0ZKVkDTvpd6kVz
# HIR+187i1Dp3AgMBAAGjggGLMIIBhzAOBgNVHQ8BAf8EBAMCB4AwDAYDVR0TAQH/
# BAIwADAWBgNVHSUBAf8EDDAKBggrBgEFBQcDCDAgBgNVHSAEGTAXMAgGBmeBDAEE
# AjALBglghkgBhv1sBwEwHwYDVR0jBBgwFoAUuhbZbU2FL3MpdpovdYxqII+eyG8w
# HQYDVR0OBBYEFGKK3tBh/I8xFO2XC809KpQU31KcMFoGA1UdHwRTMFEwT6BNoEuG
# SWh0dHA6Ly9jcmwzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJTQTQw
# OTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcmwwgZAGCCsGAQUFBwEBBIGDMIGAMCQG
# CCsGAQUFBzABhhhodHRwOi8vb2NzcC5kaWdpY2VydC5jb20wWAYIKwYBBQUHMAKG
# TGh0dHA6Ly9jYWNlcnRzLmRpZ2ljZXJ0LmNvbS9EaWdpQ2VydFRydXN0ZWRHNFJT
# QTQwOTZTSEEyNTZUaW1lU3RhbXBpbmdDQS5jcnQwDQYJKoZIhvcNAQELBQADggIB
# AFWqKhrzRvN4Vzcw/HXjT9aFI/H8+ZU5myXm93KKmMN31GT8Ffs2wklRLHiIY1UJ
# RjkA/GnUypsp+6M/wMkAmxMdsJiJ3HjyzXyFzVOdr2LiYWajFCpFh0qYQitQ/Bu1
# nggwCfrkLdcJiXn5CeaIzn0buGqim8FTYAnoo7id160fHLjsmEHw9g6A++T/350Q
# p+sAul9Kjxo6UrTqvwlJFTU2WZoPVNKyG39+XgmtdlSKdG3K0gVnK3br/5iyJpU4
# GYhEFOUKWaJr5yI+RCHSPxzAm+18SLLYkgyRTzxmlK9dAlPrnuKe5NMfhgFknADC
# 6Vp0dQ094XmIvxwBl8kZI4DXNlpflhaxYwzGRkA7zl011Fk+Q5oYrsPJy8P7mxNf
# arXH4PMFw1nfJ2Ir3kHJU7n/NBBn9iYymHv+XEKUgZSCnawKi8ZLFUrTmJBFYDOA
# 4CPe+AOk9kVH5c64A0JH6EE2cXet/aLol3ROLtoeHYxayB6a1cLwxiKoT5u92Bya
# UcQvmvZfpyeXupYuhVfAYOd4Vn9q78KVmksRAsiCnMkaBXy6cbVOepls9Oie1FqY
# yJ+/jbsYXEP10Cro4mLueATbvdH7WwqocH7wl4R44wgDXUcsY6glOJcB0j862uXl
# 9uab3H4szP8XTE0AotjWAQ64i+7m4HJViSwnGWH2dwGMMYIFXTCCBVkCAQEwgYYw
# cjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDERpZ2lDZXJ0IEluYzEZMBcGA1UECxMQ
# d3d3LmRpZ2ljZXJ0LmNvbTExMC8GA1UEAxMoRGlnaUNlcnQgU0hBMiBBc3N1cmVk
# IElEIENvZGUgU2lnbmluZyBDQQIQBNXcH0jqydhSALrNmpsqpzANBglghkgBZQME
# AgEFAKCBhDAYBgorBgEEAYI3AgEMMQowCKACgAChAoAAMBkGCSqGSIb3DQEJAzEM
# BgorBgEEAYI3AgEEMBwGCisGAQQBgjcCAQsxDjAMBgorBgEEAYI3AgEVMC8GCSqG
# SIb3DQEJBDEiBCDIXaJxss0+ZlQpc0N+BBWqSy5KSmSfSXPvHFPDYhgTtTANBgkq
# hkiG9w0BAQEFAASCAQAlyYsL5uugi+9t4OIWDmyGwbl6dM8xqBthmqJMrtr9laOO
# 4tbpujecC+0OOaHl3c7n0lt5x6MyK4EdC8/0fc/hn38ve0ze7Csvoi4WThS9rDTp
# +NkK/yzoWVu8Z55GxxKdsCvAaB2vt9Tqz++07K08D+gGgarTZpPkxkx1FW4IGaBN
# WNweb+7VvYbDjCXVhR05KQEQqN6pex5ysv/Y/rteX90bx0oPTJRtrlU0enTpoA5D
# QVoOhX/NZE/2dHp4I4BSMXF59x/3qXv4HJgBTBI0GF6otQCXUAM968PTyo+VH6IP
# w390ksnnfTDGCPey7yFrBtAtwZjQKp4v2/vR+1u+oYIDIDCCAxwGCSqGSIb3DQEJ
# BjGCAw0wggMJAgEBMHcwYzELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDkRpZ2lDZXJ0
# LCBJbmMuMTswOQYDVQQDEzJEaWdpQ2VydCBUcnVzdGVkIEc0IFJTQTQwOTYgU0hB
# MjU2IFRpbWVTdGFtcGluZyBDQQIQDE1pckuU+jwqSj0pB4A9WjANBglghkgBZQME
# AgEFAKBpMBgGCSqGSIb3DQEJAzELBgkqhkiG9w0BBwEwHAYJKoZIhvcNAQkFMQ8X
# DTIzMDQyMDA5MDI0NVowLwYJKoZIhvcNAQkEMSIEIGr1MaJKGICbJSJi7rD7aBGd
# PzoXqwZ2eoQqxHINHFxCMA0GCSqGSIb3DQEBAQUABIICAMnmi2x+njGUmJacTcHj
# 7ds2dhm7VYYay7oP4EpBo/IJ02+A9tHkX/zOWdXCEniNyWpBgBC23m2uzc6F9Ne5
# gHtLA0T55ruvctBsLxOU8KfKPrcGEA80n4Nlkzb7a0YpYjWhosE9SQnt59BJAhtH
# JijyyQuuKzaRnuk9pUvHsy5J3WWHnvs2IOEZG22a7wl2hxFPMUfhOi7AgRicThgS
# O85hWgmw9blgMkPF+iPYkjpCXjaOM2UiAsnnbzYlfAQxDrqZjOdwcVU9It5iluw3
# JUL77N222oHRhSIaaEoz4gzyr2Rr2w4aL8FDljDjGsZDMHDoQyZy1M9rhIUgFNqL
# wTe7ZUGxMlHh4hWZhpwdR15dTgMlpDmIlsJnpGD183vRKRRvtYsNrBdTfuXCos49
# dSweK17l0mhhf2i88d5vmGzetix5a7LNsZUKK9nIxx9l7i6rjKprq0gh4xMxP9XI
# TmAYMZtWurjsHtyK9TBDlwYPoSYfqgNGMQzu6RHIg76RD3YcXIIDJL+npUvIU4No
# 1wDZ3WChNPzM2F2Y9NBsyhCuOUDrk71UMOP8y0GXztqxhHOWZyHRaqlus2Owol+i
# pbIpJxIW+Z3RCLjgl2y4D6Gjaxp05wKvFJQs8HfX56p0gmNSHp1dGljv+adb09Mu
# E5qsoUXC6IRfjX/5ZPqlem4U
# SIG # End signature block